Compare commits
185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d92923a964 | |||
| 71598c0354 | |||
| 06a8a9b7d2 | |||
| 4d2691e636 | |||
| 6e17874cb9 | |||
| bf22dad8e8 | |||
| 731029cd87 | |||
| 6b5c5dc6e8 | |||
| e7ce6c282a | |||
| 67c58cbd28 | |||
| 7c89f7445f | |||
| 26adcfa997 | |||
| 1e41dddae2 | |||
| a9b219bc9f | |||
| 32dc8750df | |||
| ceeff58a7f | |||
| a55286953a | |||
| 9a0846ef68 | |||
| 06749ac464 | |||
| a6e7126bf1 | |||
| 4a906699ee | |||
| a2a7102859 | |||
| a137f2d459 | |||
| f87dd73661 | |||
| af5644aee6 | |||
| abeb232f17 | |||
| 1a27c8399d | |||
| 5ab59e7c1b | |||
| 9a274f91df | |||
| 08fa042039 | |||
| 09bb8aaaf5 | |||
| 46edd8d749 | |||
| c5d3dabd1b | |||
| 92171bd79b | |||
| 2ce55baba1 | |||
| 6870f9539c | |||
| 2998d412b6 | |||
| 2396b6be05 | |||
| 3c1074cfc6 | |||
| c4a63a1d43 | |||
| 2fdcc5ae49 | |||
| 4678a04827 | |||
| 63ffadfebf | |||
| aee947f0f9 | |||
| ad56dd6a93 | |||
| cafe015ef9 | |||
| 155387bac1 | |||
| de1db6fb8c | |||
| c47e848f4a | |||
| 8b4fe10995 | |||
| 8fb046aaa8 | |||
| 7b31c0567b | |||
| 5efdd52854 | |||
| 61ada52984 | |||
| 6578d591ac | |||
| d9421bf2ca | |||
| 774c45a512 | |||
| 315d8cfeb7 | |||
| 4ab1640dc8 | |||
| 3aee35fe01 | |||
| 5562d9c6c8 | |||
| d16496e057 | |||
| 0690ee5875 | |||
| 1dd921fc52 | |||
| 1d387d3f21 | |||
| 51d237829a | |||
| df4f9d3529 | |||
| ef2f485e15 | |||
| a43cd99e55 | |||
| 8f26457487 | |||
| 07ef94e102 | |||
| 02f276e296 | |||
| 9602587b0d | |||
| f8a98eed33 | |||
| 7629421375 | |||
| d034633265 | |||
| fc51df3b01 | |||
| 8f4d7959df | |||
| 338fa197d3 | |||
| 727bb23c7e | |||
| 8d6c45f640 | |||
| 6d16fae4ed | |||
| ef92f35b16 | |||
| 232a902680 | |||
| cfc4908291 | |||
| 1432131e95 | |||
| 35bc04847a | |||
| 06def2231d | |||
| d39fd6c397 | |||
| 4faf63e48a | |||
| 91ecea8cfd | |||
| c8c0c28fc8 | |||
| f91db06904 | |||
| 9df9d1fbdc | |||
| 8068e8f4b8 | |||
| 863b0777da | |||
| 80889762ce | |||
| a3f293c987 | |||
| e27d17c421 | |||
| 52bc27aaf6 | |||
| c357b1565e | |||
| de9a2bc846 | |||
| 6b6bd26648 | |||
| 8239b914ba | |||
| e95946ac82 | |||
| 46ce12b7f0 | |||
| aba00427fa | |||
| cccf8c1ece | |||
| fb80454368 | |||
| f92e16fa25 | |||
| dcbedbd805 | |||
| 9bb216a357 | |||
| fd9b3fd9f2 | |||
| 6488a540c9 | |||
| 3e70ad9876 | |||
| 30842efb22 | |||
| 549aef17a5 | |||
| 4d738cd210 | |||
| 1cfe77bb85 | |||
| e3f99de8af | |||
| 40b1b70dec | |||
| 0862f1679a | |||
| 2cd54b6af9 | |||
| c9c063620a | |||
| 9060abc83e | |||
| bec762fb2b | |||
| 826fce68eb | |||
| 73f50d7bb4 | |||
| d1927aac25 | |||
| b224ad6dae | |||
| ca10e02573 | |||
| 5500681d6a | |||
| fabeaa329a | |||
| 8f67c7fb95 | |||
| 6bca7d2854 | |||
| eeb686f164 | |||
| 9e3e403b3f | |||
| 0231eec4c6 | |||
| 69f202f1c8 | |||
| 1cbfb889d5 | |||
| ee3cc32525 | |||
| 954232cef1 | |||
| 9092df9b97 | |||
| c1a23f8f97 | |||
| 0fc4cd2747 | |||
| 6d83f95fba | |||
| bf625dff05 | |||
| ed3510a91f | |||
| 8f0130a61b | |||
| 72df682af7 | |||
| 41a450be7c | |||
| b227924be9 | |||
| 28b47059bb | |||
| a19a32ddd7 | |||
| 606e264f6d | |||
| d25f8198fa | |||
| 464f4ac748 | |||
| 03b5248dc1 | |||
| f9dd77e383 | |||
| 4afd24d29f | |||
| 8a74336713 | |||
| 5e4c52a7cb | |||
| 77e0e11775 | |||
| 7bd66b7c06 | |||
| 86db05e538 | |||
| d912f3533b | |||
| 9d7bbb7fa9 | |||
| 6312c2760a | |||
| 3a23d21795 | |||
| e199f9aeff | |||
| e06d6e36d8 | |||
| 9d1ad9f0b9 | |||
| d39a2d28db | |||
| 1e649653ef | |||
| 6988eb0043 | |||
| 23c2244307 | |||
| a52ee850aa | |||
| 65d23a78a5 | |||
| 481687fff3 | |||
| 46a9cf80b8 | |||
| f74c6605c4 | |||
| 215cc17c52 | |||
| c5bc849913 | |||
| 6178d1a45e | |||
| e12afd11cc |
@@ -0,0 +1,20 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
reviews:
|
||||
auto_review:
|
||||
# Dependabot bumps are checked by CI; skip CodeRabbit reviews on them.
|
||||
ignore_usernames:
|
||||
- "dependabot[bot]"
|
||||
path_instructions:
|
||||
- path: "**"
|
||||
instructions: |
|
||||
Project rule: don't change anyone's existing defaults unless the change is purely additive and belongs in the application.
|
||||
Flag any change to something a user already relies on: a default setting or preference value, what a command or tool does when used the usual way, the UI's default layout or behaviour, a keyboard or mouse shortcut, or the result an existing saved design rebuilds to.
|
||||
Don't flag purely additive changes: a new command, a new setting whose default keeps today's behaviour, or a new option that appears only when someone chooses it. Don't flag bug fixes that only turn a crash or an error into the result the feature always promised.
|
||||
When you flag one, start the comment with "Changes a default:", say what a user sees before and after, whether a setting keeps the old behaviour, and whether the PR description's `Changes a default:` line says so.
|
||||
pre_merge_checks:
|
||||
custom_checks:
|
||||
- name: "Existing defaults unchanged"
|
||||
mode: "warning"
|
||||
instructions: |
|
||||
Pass if the pull request changes no default a user already relies on (default setting or preference values, what a command or tool does when used the usual way, the UI's default layout or behaviour, shortcuts, or the result an existing saved design rebuilds to), or if its changes are purely additive.
|
||||
Fail otherwise, naming each changed default and what a user sees before and after. Also fail if the description says `Changes a default: no` while the code changes one.
|
||||
@@ -3,13 +3,13 @@
|
||||
SolidCraft is a clean-room, pure-Rust parametric 3D CAD app (sketch solver, feature timeline, B-rep kernel, an egui UI that agents drive). This file is a router: the hard rules, then the skill to read for your task. Humans: CONTRIBUTING.md.
|
||||
|
||||
## Start here
|
||||
Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelled `help wanted`, `good first issue`, `needs-triage`; architecture in `docs/architecture.md`. Act when the code, docs and rules settle a question; ask only when it changes what users see. Local `plan/` notes are optional.
|
||||
Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelled `help wanted`, `good first issue`, `needs-triage`; architecture in `docs/architecture.md`. Local `plan/` notes are optional.
|
||||
|
||||
## Hard rules (what enforces each)
|
||||
- Independent: never copy another product's code, icons, text or templates; never drive or take data from commercial CAD. `skills/solidcraft-ground-rules/SKILL.md`.
|
||||
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
|
||||
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
|
||||
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
|
||||
- Never crash: no unwrap/expect/panic/unsafe in non-test code, errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
|
||||
- Model decisions (what an operation does, which bodies it touches, which face or edge a point or name means, depths, nearness) use exact geometry; a mesh is for display, export, previews and mesh bodies (`xtask/mesh-decisions.txt` only shrinks): `cargo xtask mesh-decisions`.
|
||||
- New kernel cases go to solidcore: `cargo xtask lists`.
|
||||
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
|
||||
@@ -21,12 +21,14 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
|
||||
- A design-format change is ONE new file in `crates/doc/src/format/pending/`: `scripts/test-format-pending.sh`.
|
||||
- A command has input and output schemas (will become required; `schema/unconverted.txt` only shrinks): `skills/solidcraft-commands/SKILL.md`.
|
||||
- New UI strings go in `assets/locales/pending/<slug>.json`, ATTRIBUTION rows in `attribution/pending/<slug>.md`: `cargo xtask lists`.
|
||||
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line, and no second PR for an issue a lower-numbered one closes: `pr-checks.yml`.
|
||||
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line: `pr-checks.yml`.
|
||||
- A regression test goes in the area's existing test file, never a per-bug file (#3226).
|
||||
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR, never force-push `main`, no pushes just to catch up with `main`: `skills/solidcraft-contributing/SKILL.md`.
|
||||
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR: `skills/solidcraft-contributing/SKILL.md`.
|
||||
- A PR's title and first sentence make sense to someone who never saw your chat or the issue: `skills/solidcraft-contributing/SKILL.md`.
|
||||
- Code, comments, commit messages and docs are in English, always: `skills/solidcraft-contributing/SKILL.md` ("Working as an agent").
|
||||
- Independent work runs in parallel, dependent work stays sequential: `skills/solidcraft-contributing/SKILL.md` ("Working as an agent").
|
||||
- Show observable changes with real screenshots or a recording, captured yourself.
|
||||
- Run `cargo xtask ci` before every commit, without switching branches while it runs. `cargo xtask skills` keeps this file and `skills/` honest.
|
||||
- Run `cargo xtask ci` before every commit. `cargo xtask skills` keeps this file and `skills/` honest.
|
||||
|
||||
## Working on X: read the skill first
|
||||
| Working on | Read |
|
||||
|
||||
@@ -317,7 +317,6 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/ui/design/svg/model/circle_ttt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/coil.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/colored_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/colored_components_off.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/combine.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/component.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
|
||||
@@ -784,6 +783,25 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
|
||||
| `assets/locales/pending/sketch-offset-instances.json` | SolidCraft contributors | Original offset instance-count label and translations | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_profile.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original analytic head and shoulder geometry for settings profiles | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/4561-convert-box-mesh-with-a-zero-area-triangle/box-degenerate.stl | SolidCraft contributors | synthetic ASCII STL written by hand: a box with one zero-area triangle, for the kernel regression case of tangency-labs/solidcraft#4561 | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/theme-blur-ui.json | SolidCraft contributors | Original blur controls, notices and translations | MIT OR Apache-2.0 |
|
||||
| crates/ui-egui/tests/fixtures/theme-blur.json | SolidCraft contributors | Original theme for blur option scenarios and manager review | MIT OR Apache-2.0 |
|
||||
| assets/locales/ka.json | David Machakhelidze (devius) | Original Georgian translation of SolidCraft's English catalog, written with Claude Code from the meaning of each English string; terminology and doubtful strings decided by a native Georgian speaker; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark.svg | SolidCraft contributors | static SolidCraft mark: the five faces and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg (the final frame of assets/styles/web/loading-logo.svg), cropped to the mark's bounds; original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-mono-black.svg | SolidCraft contributors | one-colour (black) version of assets/branding/logo/solidcraft-mark.svg, same paths; original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-mono-white.svg | SolidCraft contributors | one-colour (white) version of assets/branding/logo/solidcraft-mark.svg, same paths; original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-512.png | SolidCraft contributors | transparent 512 px render of assets/branding/logo/solidcraft-mark.svg (cairosvg); original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-1024.png | SolidCraft contributors | transparent 1024 px render of assets/branding/logo/solidcraft-mark.svg (cairosvg); original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-2048.png | SolidCraft contributors | transparent 2048 px render of assets/branding/logo/solidcraft-mark.svg (cairosvg); original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-wordmark.svg | SolidCraft contributors | the name SolidCraft set like the loading logo's wordmark (bold, +0.02 em tracking, #15499e) in IBM Plex Sans Bold from assets/fonts/ibm-plex-sans (SIL OFL 1.1, IBM Corp.), outlined to paths; no font file is embedded | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-lockup-horizontal.svg | SolidCraft contributors | assets/branding/logo/solidcraft-mark.svg beside assets/branding/logo/solidcraft-wordmark.svg, in the proportions of assets/branding/web/source/og.png; original artwork | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/onyx.json | SolidCraft contributors | original Onyx stone-grey palette, option overlays and semantic icon-role mapping; existing SolidCraft artwork is unchanged | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/ui-tour/window.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (`solidcraft --sample`, default dark theme, software renderer, 1280x800) with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/ui-tour/navigate.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (`--sample`, default dark theme, software renderer) orbiting, panning and zooming, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/sketch-tool-hints.json | carlymx | Original sketch tool hints and engine error translations | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/primitive-pick-field-tips.json | SolidCraft contributors | Original primitive pick-field tooltip translations | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/ribbon-panel-hover.json | SolidCraft contributors | Original ribbon panel menu preference labels and translations | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/box-insert-designer.gif | Shawn Marx | original recording made with BoxInsertDesigner's own Motion export (its three.js viewer) of its example game `examples/ccg-example` (https://github.com/ShawnMarx/BoxInsertDesigner): a card game box insert whose trays were built by SolidCraft through `solidcraft-cli`; the published card, board and box art in it is pixelated beyond recognition, and no logos or game names are shown (2.5× playback, 800 px wide) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/box-insert-designer.mp4 | Shawn Marx | original recording made with BoxInsertDesigner's own Motion export (its three.js viewer) of its example game `examples/ccg-example` (https://github.com/ShawnMarx/BoxInsertDesigner): a card game box insert whose trays were built by SolidCraft through `solidcraft-cli`; the published card, board and box art in it is pixelated beyond recognition, and no logos or game names are shown (1280×720) | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
@@ -835,7 +853,7 @@ into THIRD-PARTY-LICENSES.txt when `main` regenerates it.
|
||||
## FreeCAD and Rhino import library
|
||||
|
||||
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
|
||||
0.6.3 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
|
||||
0.6.7 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
|
||||
`cadmpeg-container`, `cadmpeg-codec-freecad`, `cadmpeg-codec-rhino` and `cadmpeg-codec-step`
|
||||
in native and browser builds (`crates/io/src/native_cad.rs`). Only those open-format codecs are built: no
|
||||
default features, so cadmpeg's codecs for commercial CAD formats and its ShapeManager/ACIS
|
||||
|
||||
@@ -88,6 +88,8 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
|
||||
|
||||
- `crates/geom`: vectors, planes, meshes, measures.
|
||||
- `crates/sketch`: our own constraint solver, plus profile finding.
|
||||
- `crates/dynamics`: multibody dynamics: rigid bodies, joint constraints, gravity and springs,
|
||||
integrated in time (RFC #9719). It depends only on `geom`.
|
||||
- `crates/kernel`: the B-rep kernel boundary. It wraps [truck](https://github.com/ricosjp/truck),
|
||||
and it's the only crate allowed to touch truck. Fillets, chamfers, shell/offset, Delete Face,
|
||||
Split Face, STEP/IGES read and write, and face provenance are our own code here. Patched
|
||||
@@ -472,6 +474,16 @@ the same area), first resolve the command registry and source conflicts, then ru
|
||||
The generator tests cover stable ordering and additions that leave existing lines
|
||||
unchanged. The initial switch to sorted rows is a one-time larger diff.
|
||||
|
||||
## Pictures in the user guide
|
||||
|
||||
The user guide's screenshots and short clips (`docs/book/src/media/`) are captured from the real
|
||||
app by `examples/demo/doc_shots.py`, one function per figure. Each run starts a fresh app with its
|
||||
own settings, so your preferences never show. When a change makes a figure out of date, run
|
||||
`python3 examples/demo/doc_shots.py <page or shot>` (it needs Xvfb, ffmpeg, Pillow and a release
|
||||
build) and commit the new file. A new figure is a new function there, an ATTRIBUTION row in
|
||||
`attribution/pending/`, and a `<video>` (clips) or image link on the page. Keep a still under
|
||||
about 250 KB and a clip under about 1 MB.
|
||||
|
||||
## Licence
|
||||
|
||||
By contributing you agree that your contribution is licensed as MIT OR Apache-2.0, like the rest
|
||||
|
||||
@@ -564,9 +564,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-codec-freecad"
|
||||
version = "0.6.3"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c4a8fa61748cbb3ce80838b00a8cebfe553460583941f5aba6e2b97c16b4d37"
|
||||
checksum = "6fe0ed1935ace2c203b87107a064818dec2bb2ec2f250c4ad121767f3da75d1b"
|
||||
dependencies = [
|
||||
"cadmpeg-container",
|
||||
"cadmpeg-core",
|
||||
@@ -580,9 +580,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-codec-rhino"
|
||||
version = "0.6.3"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7af41440d4c1d69e88e612de02cc3c9bad79d03429531e640ac924fef53c6bf"
|
||||
checksum = "7dd2b76bd1d9301c949fbd731fd22cff9d37d10d1f481c754b557c2187bd0171"
|
||||
dependencies = [
|
||||
"cadmpeg-container",
|
||||
"cadmpeg-core",
|
||||
@@ -599,9 +599,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-codec-step"
|
||||
version = "0.6.3"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35e1b6d17cf26b31f59f2edab8ef5c29d2a33216c581fd913f012ad4869bd559"
|
||||
checksum = "f3cbce33ed681a28b62d8b01ae64cca2b01f11f31b9a3e5e2455b9ffde915712"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cadmpeg-container",
|
||||
@@ -616,9 +616,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-container"
|
||||
version = "0.6.3"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ab7bcef48ac7480b4917820272d85cd2c9533e17904e15b0e4d22f19a6045bd"
|
||||
checksum = "03f0d1c22b875294c5fbf4a4738aec8f8c0a4ad035eb28ece404f1dec21f371a"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"crc32fast",
|
||||
@@ -628,9 +628,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-core"
|
||||
version = "0.6.3"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e86bb7a40521832504bb1f5953a1c706b2c52aad73bf6d84e1b35e5cb0beae3f"
|
||||
checksum = "795bdc6e2a10ecc0627138874c17e2948a3925cd22c6c6b0c2e4f6621d92f89c"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"memchr",
|
||||
@@ -643,9 +643,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-ir"
|
||||
version = "0.6.3"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b570a1a8c579e407a9fd7bb10fa1900f8827f1d578b8fcb4ea2a056b5f323c72"
|
||||
checksum = "5592036516aaf2574b159a86a3d1ae872dc245261de86e4c0ce6773fed00a249"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cadmpeg-core",
|
||||
@@ -658,9 +658,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-registry"
|
||||
version = "0.6.3"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2be23469a9bca0b684f1811118fffb581ebd32ded08e6cc8b0716a41f8c1377"
|
||||
checksum = "fc163233d5f664337267192848505854de6aeed4843181adf87f58baddcee9a0"
|
||||
dependencies = [
|
||||
"cadmpeg-codec-freecad",
|
||||
"cadmpeg-codec-rhino",
|
||||
@@ -4102,6 +4102,7 @@ dependencies = [
|
||||
"ureq",
|
||||
"windows-sys 0.61.2",
|
||||
"winit",
|
||||
"winreg",
|
||||
"x11rb",
|
||||
"zbus 5.19.0",
|
||||
]
|
||||
@@ -4152,6 +4153,17 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-dynamics"
|
||||
version = "0.40.0-rc.2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcore-record",
|
||||
"solidcraft-geom",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.40.0-rc.2"
|
||||
|
||||
@@ -22,6 +22,7 @@ solidcraft-kernel = { path = "crates/kernel" }
|
||||
solidcraft-smlib = { path = "crates/smlib" }
|
||||
solidcraft-step = { path = "crates/step" }
|
||||
solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-dynamics = { path = "crates/dynamics" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
solidcraft-io = { path = "crates/io" }
|
||||
@@ -81,9 +82,9 @@ gethostname = "1"
|
||||
# Pinned: its IR and codec APIs change between minor versions. Open formats only: no default
|
||||
# features, so cadmpeg's codecs for commercial formats (f3d, sldprt, inventor, catia, creo, nx)
|
||||
# and its ShapeManager/ACIS `sat` codec are not compiled in. `step` is the writer we restate through.
|
||||
cadmpeg-core = "=0.6.3"
|
||||
cadmpeg-ir = "=0.6.3"
|
||||
cadmpeg-registry = { version = "=0.6.3", default-features = false, features = ["fcstd", "rhino", "step"] }
|
||||
cadmpeg-core = "=0.6.7"
|
||||
cadmpeg-ir = "=0.6.7"
|
||||
cadmpeg-registry = { version = "=0.6.7", default-features = false, features = ["fcstd", "rhino", "step"] }
|
||||
|
||||
# fonts (sketch text outlines; MIT OR Apache-2.0, pure Rust)
|
||||
ttf-parser = { version = "0.25", default-features = false, features = ["std", "variable-fonts"] }
|
||||
|
||||
@@ -57,13 +57,13 @@ bytemuck_derive 1.12.1 (Zlib OR Apache-2.0 OR MIT)
|
||||
byteorder 1.5.0 (Unlicense OR MIT)
|
||||
byteorder-lite 0.1.0 (Unlicense OR MIT)
|
||||
bytes 1.12.1 (MIT)
|
||||
cadmpeg-codec-freecad 0.6.3 (Apache-2.0)
|
||||
cadmpeg-codec-rhino 0.6.3 (Apache-2.0)
|
||||
cadmpeg-codec-step 0.6.3 (Apache-2.0)
|
||||
cadmpeg-container 0.6.3 (Apache-2.0)
|
||||
cadmpeg-core 0.6.3 (Apache-2.0)
|
||||
cadmpeg-ir 0.6.3 (Apache-2.0)
|
||||
cadmpeg-registry 0.6.3 (Apache-2.0)
|
||||
cadmpeg-codec-freecad 0.6.7 (Apache-2.0)
|
||||
cadmpeg-codec-rhino 0.6.7 (Apache-2.0)
|
||||
cadmpeg-codec-step 0.6.7 (Apache-2.0)
|
||||
cadmpeg-container 0.6.7 (Apache-2.0)
|
||||
cadmpeg-core 0.6.7 (Apache-2.0)
|
||||
cadmpeg-ir 0.6.7 (Apache-2.0)
|
||||
cadmpeg-registry 0.6.7 (Apache-2.0)
|
||||
calloop 0.13.0 (MIT)
|
||||
calloop 0.14.5 (MIT)
|
||||
calloop-wayland-source 0.3.0 (MIT)
|
||||
@@ -15978,13 +15978,13 @@ accesskit_windows 0.32.1: MIT OR Apache-2.0 (https://github.com/AccessKit/access
|
||||
accesskit_winit 0.32.2: Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
block2 0.5.1: MIT (https://github.com/madsmtm/objc2)
|
||||
block2 0.6.2: MIT (https://github.com/madsmtm/objc2)
|
||||
cadmpeg-codec-freecad 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-rhino 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-step 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-container 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-core 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-ir 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-registry 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-freecad 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-rhino 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-step 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-container 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-core 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-ir 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-registry 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
clipboard-win 5.4.1: BSL-1.0 (https://github.com/DoumanAsh/clipboard-win)
|
||||
dispatch 0.2.0: MIT (http://github.com/SSheldon/rust-dispatch)
|
||||
dispatch2 0.3.1: Zlib OR Apache-2.0 OR MIT (https://github.com/madsmtm/objc2)
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::process::{Command, ExitCode};
|
||||
const TARGET: &str = "solidcraft-cli";
|
||||
|
||||
fn main() -> ExitCode {
|
||||
eprintln!("solvecraft-cli is deprecated and will be removed in a later release: use solidcraft-cli");
|
||||
solidcraft_engine::note!("solvecraft-cli is deprecated and will be removed in a later release: use solidcraft-cli");
|
||||
let sibling = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join(format!("{TARGET}{}", std::env::consts::EXE_SUFFIX))))
|
||||
@@ -23,7 +23,7 @@ fn main() -> ExitCode {
|
||||
None => ExitCode::FAILURE,
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("solvecraft-cli: could not run {}: {e}", program.to_string_lossy());
|
||||
solidcraft_engine::note!("solvecraft-cli: could not run {}: {e}", program.to_string_lossy());
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ pub fn one(args: &[String]) -> Result<(), String> {
|
||||
std::panic::set_hook(Box::new(|_| {
|
||||
PANICS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
}));
|
||||
eprintln!("design {seed}");
|
||||
let run = fuzz::run_with(seed, steps, &mut |st| eprintln!("step {}", serde_json::to_string(st).unwrap_or_default()));
|
||||
solidcraft_engine::note!("design {seed}");
|
||||
let run = fuzz::run_with(seed, steps, &mut |st| solidcraft_engine::note!("step {}", serde_json::to_string(st).unwrap_or_default()));
|
||||
let mut design = json!({
|
||||
"outcome": run.outcome,
|
||||
"steps": run.steps.iter().map(|s| s.kind).collect::<Vec<_>>(),
|
||||
@@ -45,13 +45,13 @@ pub fn one(args: &[String]) -> Result<(), String> {
|
||||
if run.outcome.is_bug() {
|
||||
design["script"] = run.script();
|
||||
if minimise {
|
||||
eprintln!("minimise");
|
||||
solidcraft_engine::note!("minimise");
|
||||
let m = fuzz::minimise(&run);
|
||||
design["minimal"] = json!({"outcome": m.outcome, "steps": m.steps.iter().map(|s| s.kind).collect::<Vec<_>>(), "script": m.script()});
|
||||
}
|
||||
}
|
||||
let (a, b) = solidcraft_engine::fuzz::boolean_shapes(seed);
|
||||
eprintln!("boolean {seed} {} {}", serde_json::to_string(&a).unwrap_or_default(), serde_json::to_string(&b).unwrap_or_default());
|
||||
solidcraft_engine::note!("boolean {seed} {} {}", serde_json::to_string(&a).unwrap_or_default(), serde_json::to_string(&b).unwrap_or_default());
|
||||
let b = boolean_case(seed);
|
||||
let bp = PANICS.swap(0, std::sync::atomic::Ordering::SeqCst);
|
||||
println!("{}", json!({"seed": seed, "design": design, "boolean": b, "boolean_panics": bp}));
|
||||
@@ -162,11 +162,14 @@ fn record(v: &Value, out_dir: Option<&str>) {
|
||||
let d = &v["design"];
|
||||
let bug = matches!(d["outcome"]["kind"].as_str(), Some("failed" | "panic" | "invalid" | "volume"));
|
||||
if bug || v.get("timeout").is_some() || v.get("crash").is_some() {
|
||||
eprintln!("seed {seed}: {}", if bug { format!("{} {}", d["outcome"]["kind"], d["outcome"]["detail"]) } else { format!("{v}") });
|
||||
solidcraft_engine::note!(
|
||||
"seed {seed}: {}",
|
||||
if bug { format!("{} {}", d["outcome"]["kind"], d["outcome"]["detail"]) } else { format!("{v}") }
|
||||
);
|
||||
}
|
||||
let bk = v["boolean"]["outcome"].as_str().unwrap_or("ok");
|
||||
if bk != "ok" {
|
||||
eprintln!("seed {seed}: boolean {bk} {}", v["boolean"]["detail"]);
|
||||
solidcraft_engine::note!("seed {seed}: boolean {bk} {}", v["boolean"]["detail"]);
|
||||
}
|
||||
if let Some(dir) = out_dir {
|
||||
use std::io::Write;
|
||||
|
||||
@@ -154,7 +154,7 @@ fn main() -> ExitCode {
|
||||
}
|
||||
};
|
||||
if let Err(e) = max_file_size(&mut args) {
|
||||
eprintln!("error: {e}");
|
||||
solidcraft_engine::note!("error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
let r = match args.first().map(String::as_str) {
|
||||
@@ -235,7 +235,7 @@ fn main() -> ExitCode {
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
solidcraft_engine::note!("error: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
@@ -351,7 +351,7 @@ fn run_script_warning(s: &mut Session, path: &str, script: &Value) -> Result<Vec
|
||||
/// Step `step`'s `param_warnings` on stderr, one per line, named after the script at `path`.
|
||||
fn print_param_warnings(path: &str, step: usize, r: &Value) {
|
||||
for w in r.get("param_warnings").and_then(Value::as_array).into_iter().flatten() {
|
||||
eprintln!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
|
||||
solidcraft_engine::note!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,10 +598,10 @@ fn cmd_script(args: &[String]) -> Result<(), String> {
|
||||
let out = flag(args, "--out");
|
||||
let r = s.execute("timeline.export_script", &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
for what in r["omitted"].as_array().into_iter().flatten().filter_map(Value::as_str) {
|
||||
eprintln!("note: the script leaves out {what}");
|
||||
solidcraft_engine::note!("note: the script leaves out {what}");
|
||||
}
|
||||
match out {
|
||||
Some(out) => eprintln!("{out}: {} steps", r["steps"]),
|
||||
Some(out) => solidcraft_engine::note!("{out}: {} steps", r["steps"]),
|
||||
None => println!("{}", pretty(&r["script"])),
|
||||
}
|
||||
Ok(())
|
||||
@@ -632,7 +632,7 @@ fn cmd_gpu(args: &[String]) -> Result<(), String> {
|
||||
);
|
||||
match s.lands_on {
|
||||
Some(solidcraft_gpu::select::Landing::Cpu) => {
|
||||
eprintln!("warning: {}", solidcraft_engine::capture::CPU_WARNING);
|
||||
solidcraft_engine::note!("warning: {}", solidcraft_engine::capture::CPU_WARNING);
|
||||
Ok(())
|
||||
}
|
||||
Some(_) => Ok(()),
|
||||
@@ -709,7 +709,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
std::fs::write(out, &png.bytes).map_err(|e| format!("{out}: {e}"))?;
|
||||
let mut reply = json!({"path": out, "width": w, "height": h, "bytes": png.bytes.len(), "renderer": png.renderer});
|
||||
if let Some(w) = solidcraft_engine::capture::warning(png.renderer) {
|
||||
eprintln!("warning: {w}");
|
||||
solidcraft_engine::note!("warning: {w}");
|
||||
reply["warning"] = json!(w);
|
||||
}
|
||||
println!("{}", pretty(&reply));
|
||||
@@ -796,7 +796,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
.then(|| solidcraft_mcp::agent_scope(flags(args, "--allow-path").into_iter().map(std::path::PathBuf::from)));
|
||||
if let Some(s) = &scope {
|
||||
let roots: Vec<String> = s.roots().iter().map(|r| r.display().to_string()).collect();
|
||||
eprintln!(
|
||||
solidcraft_engine::note!(
|
||||
"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(", ")
|
||||
);
|
||||
@@ -804,7 +804,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
.and_then(|p| std::path::Path::new(p).parent())
|
||||
.map(|d| if d.as_os_str().is_empty() { std::path::Path::new(".") } else { d });
|
||||
if let Some(why) = design_dir.and_then(solidcraft_engine::io::vfs::design_folder_refusal) {
|
||||
eprintln!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
|
||||
solidcraft_engine::note!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
|
||||
}
|
||||
}
|
||||
// How calls that break a command's parameter contract are treated, headless or bridged.
|
||||
@@ -818,7 +818,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
// Serve without the app too: the tools connect once it runs, and say it isn't until then.
|
||||
let mut remote = solidcraft_mcp::Remote::new(addr, flag(args, "--token")).with_param_check(param_check);
|
||||
if let Err(e) = remote.try_connect() {
|
||||
eprintln!(
|
||||
solidcraft_engine::note!(
|
||||
"solidcraft mcp: no SolidCraft app at {addr} yet ({e}); start it with `solidcraft --control {port}` \
|
||||
(or SOLIDCRAFT_CONTROL_PORT={port}), tool calls connect once it runs"
|
||||
);
|
||||
@@ -835,7 +835,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
(None, None) => {
|
||||
let auto = !args.iter().any(|a| a == "--headless");
|
||||
if auto && let Some(port) = discover_app() {
|
||||
eprintln!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
|
||||
solidcraft_engine::note!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
|
||||
let remote = solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token")).with_param_check(param_check);
|
||||
return serve_mcp(Box::new(remote), scope);
|
||||
}
|
||||
|
||||
@@ -188,3 +188,26 @@ fn max_file_size_raises_the_limit_and_refuses_bad_sizes() {
|
||||
assert!(!output.status.success() && !stderr.contains("too large"), "{stderr}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #10111: a line for a stderr whose reader has gone (the pipe closed before it, as an agent may
|
||||
/// leave the MCP server's, and the app that one starts) is dropped, and the command ends as it
|
||||
/// would with stderr open: here a script that isn't there fails with 1, where `eprintln!`
|
||||
/// panicked with "failed printing to stderr" (101).
|
||||
#[test]
|
||||
fn a_closed_stderr_does_not_stop_the_cli() {
|
||||
for _ in 0..5 {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["run", "no-such-script.json"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
drop(child.stderr.take());
|
||||
assert_eq!(child.wait().unwrap().code(), Some(1));
|
||||
}
|
||||
// The old name's alias writes its deprecation notice first: an agent still configured with
|
||||
// `solvecraft-cli mcp` must get the server, not that panic.
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")).arg("--help").stdout(Stdio::null()).stderr(Stdio::piped()).spawn().unwrap();
|
||||
drop(child.stderr.take());
|
||||
assert_eq!(child.wait().unwrap().code(), Some(0));
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ solidcraft-engine = { workspace = true }
|
||||
solidcraft-ui-egui = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
url = "2"
|
||||
percent-encoding = "2"
|
||||
|
||||
@@ -33,7 +34,6 @@ eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
serde_json = { workspace = true }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "Location", "Navigator", "StorageManager",
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemGetDirectoryOptions", "FileSystemWritableFileStream",
|
||||
|
||||
@@ -553,7 +553,7 @@ mod web {
|
||||
/// Open a design remembered from an earlier session (a recent one, after a reload): ask once
|
||||
/// for access to the file (call in the click), read it into the engine's files and queue it
|
||||
/// to open. A file that is gone is forgotten and dropped from the recent designs.
|
||||
pub fn restore(files: &Rc<Files>, path: &str) {
|
||||
pub fn restore(files: &Rc<Files>, path: &str, on_opened: Option<Box<dyn FnOnce()>>) {
|
||||
let Some((id, name)) = parse(path) else { return };
|
||||
let (files, path, id, name) = (files.clone(), path.to_string(), id.to_string(), name.to_string());
|
||||
spawn_local(async move {
|
||||
@@ -568,6 +568,9 @@ mod web {
|
||||
files.opened.borrow_mut().push(path);
|
||||
files.wake();
|
||||
touch(&id).await;
|
||||
if let Some(done) = on_opened {
|
||||
done();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let gone = e.contains("NotFoundError") || e.contains("could not be found") || e.contains("no longer remembers");
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
//! and go out as downloads (exports, and "Download design"). Where the File System Access API
|
||||
//! exists a design also lives on the computer's disk, saved and opened through the browser's file
|
||||
//! dialogs (`disk.rs`). An autosave of unsaved work is kept in OPFS and offered again after a
|
||||
//! reload or a crash.
|
||||
//! reload or a crash. With "keep unsaved work in this browser" on, each open design's unsaved
|
||||
//! changes are kept there instead, one copy per design (`keep.rs`, #10220).
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
@@ -13,7 +14,7 @@ use std::rc::Rc;
|
||||
use js_sys::{Array, Function, Object, Reflect, Uint8Array};
|
||||
use solidcraft_engine::io::vfs;
|
||||
|
||||
use crate::disk;
|
||||
use crate::{disk, keep};
|
||||
use wasm_bindgen::{JsCast as _, JsValue, closure::Closure};
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
use web_sys::{
|
||||
@@ -79,6 +80,16 @@ pub struct Files {
|
||||
pub recovery: RefCell<Option<(String, Option<String>)>>,
|
||||
/// The app's context, to wake it when a browser call finishes.
|
||||
pub ctx: RefCell<Option<egui::Context>>,
|
||||
/// This page's id: the copies it keeps are named after it (`keep::entry_id`).
|
||||
pub keep_page: RefCell<String>,
|
||||
/// Kept copies stored by this page, and the design revision each holds once its write ended.
|
||||
pub kept: RefCell<std::collections::HashMap<String, Option<u64>>>,
|
||||
/// Kept copies being written or removed now (one browser call at a time per copy).
|
||||
pub keeping: RefCell<std::collections::HashSet<String>>,
|
||||
/// When (ms) the last write of a copy failed, to wait before trying again (`keep::may_write`).
|
||||
pub kept_failed: RefCell<std::collections::HashMap<String, f64>>,
|
||||
/// Copies earlier pages left, newer than their design's last save: offered at start.
|
||||
pub keep_offered: RefCell<Vec<keep::Entry>>,
|
||||
}
|
||||
|
||||
impl Files {
|
||||
@@ -288,6 +299,9 @@ pub fn start(files: &Rc<Files>) {
|
||||
let _ = JsFuture::from(p).await;
|
||||
});
|
||||
}
|
||||
let page = keep::page_id(js_sys::Date::now(), js_sys::Math::random());
|
||||
hold_page_lock(&page);
|
||||
*files.keep_page.borrow_mut() = page;
|
||||
let files = files.clone();
|
||||
spawn_local(async move {
|
||||
match disk::known_ids().await {
|
||||
@@ -300,6 +314,7 @@ pub fn start(files: &Rc<Files>) {
|
||||
files.say("This page can't use the browser's file storage (it needs https or localhost): designs are kept in local storage, a few MB at most.", false);
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
let kept = kept_copies(&all);
|
||||
let has_recovery = all.iter().any(|(n, _, _)| n == RECOVERY);
|
||||
let legacy = (!has_recovery).then(|| LEGACY_RECOVERY.into_iter().find(|l| all.iter().any(|(n, _, _)| n == l))).flatten();
|
||||
for (name, bytes, modified) in all {
|
||||
@@ -315,7 +330,7 @@ pub fn start(files: &Rc<Files>) {
|
||||
*files.recovery.borrow_mut() = Some((n, meta["path"].as_str().map(str::to_string)));
|
||||
continue;
|
||||
}
|
||||
if name != RECOVERY {
|
||||
if name != RECOVERY && keep::parse_name(&name).is_none() {
|
||||
entries.push(Entry { name: name.clone(), size: bytes.len() as u64, modified });
|
||||
}
|
||||
vfs::insert(&format!("{OPFS}{name}"), bytes);
|
||||
@@ -326,6 +341,7 @@ pub fn start(files: &Rc<Files>) {
|
||||
if !vfs::exists(&format!("{OPFS}{RECOVERY}")) {
|
||||
*files.recovery.borrow_mut() = None;
|
||||
}
|
||||
sort_kept(&files, kept, &live_pages().await);
|
||||
}
|
||||
Err(e) => files.say(format!("browser storage: {e}"), true),
|
||||
}
|
||||
@@ -343,7 +359,7 @@ pub fn flush(files: &Rc<Files>) {
|
||||
{
|
||||
let mut e = files.entries.borrow_mut();
|
||||
e.retain(|x| x.name != name);
|
||||
if name != RECOVERY && name != RECOVERY_META {
|
||||
if name != RECOVERY && name != RECOVERY_META && keep::parse_name(&name).is_none() {
|
||||
e.push(Entry { name: name.clone(), size: bytes.len() as u64, modified: js_sys::Date::now() });
|
||||
e.sort_by_key(|a| a.name.to_lowercase());
|
||||
}
|
||||
@@ -473,7 +489,15 @@ pub fn recovery_path() -> String {
|
||||
/// A recovery file lives in OPFS, but its relative links belong to the original design's
|
||||
/// folder (which can be on disk). Resolve them before Open sees the temporary file location.
|
||||
pub fn prepare_recovery(original_path: Option<&str>) -> Result<String, String> {
|
||||
let path = recovery_path();
|
||||
prepare_copy(recovery_path(), original_path)
|
||||
}
|
||||
|
||||
/// The same for a kept copy (#10220).
|
||||
pub fn prepare_kept(id: &str, original_path: Option<&str>) -> Result<String, String> {
|
||||
prepare_copy(format!("{OPFS}{}", keep::design_file(id)), original_path)
|
||||
}
|
||||
|
||||
fn prepare_copy(path: String, original_path: Option<&str>) -> Result<String, String> {
|
||||
let bytes = vfs::read(&path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| e.to_string())?;
|
||||
let mut doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?;
|
||||
solidcraft_engine::resolve_design_links(&mut doc, original_path);
|
||||
@@ -646,3 +670,149 @@ pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput, theme: bool) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Hold this page's browser lock while it runs, so other tabs know its kept copies are in use
|
||||
/// (`keep.rs`). Browsers without the Web Locks API hold none; their copies are then offered by
|
||||
/// any later page.
|
||||
fn hold_page_lock(page: &str) {
|
||||
let Some(locks) = web_locks() else { return };
|
||||
let Ok(request) = Reflect::get(&locks, &"request".into()).and_then(|f| f.dyn_into::<Function>().map_err(JsValue::from)) else { return };
|
||||
// A promise that never settles: the lock is let go only when the page goes.
|
||||
let hold = Closure::<dyn FnMut(JsValue) -> js_sys::Promise>::new(|_lock: JsValue| js_sys::Promise::new(&mut |_, _| {}));
|
||||
let name = format!("{}{page}", keep::LOCK_PREFIX);
|
||||
if let Err(e) = request.call2(&locks, &name.into(), hold.as_ref().unchecked_ref()) {
|
||||
log::info!("keep: no page lock: {}", err(e));
|
||||
}
|
||||
// The callback lives as long as the page.
|
||||
hold.forget();
|
||||
}
|
||||
|
||||
fn web_locks() -> Option<JsValue> {
|
||||
let nav = web_sys::window()?.navigator();
|
||||
Reflect::get(&nav, &"locks".into()).ok().filter(|l| l.is_object())
|
||||
}
|
||||
|
||||
/// The pages still open (in this browser profile) that hold their lock.
|
||||
async fn live_pages() -> Vec<String> {
|
||||
let query = async {
|
||||
let locks = web_locks().ok_or("no Web Locks")?;
|
||||
let f: Function = Reflect::get(&locks, &"query".into()).map_err(err)?.dyn_into().map_err(|_| "no query".to_string())?;
|
||||
let p: js_sys::Promise = f.call0(&locks).map_err(err)?.dyn_into().map_err(|_| "no promise".to_string())?;
|
||||
let state = JsFuture::from(p).await.map_err(err)?;
|
||||
let held: Array = Reflect::get(&state, &"held".into()).map_err(err)?.dyn_into().map_err(|_| "no list".to_string())?;
|
||||
Ok::<_, String>(
|
||||
held.iter()
|
||||
.take(10_000)
|
||||
.filter_map(|l| Reflect::get(&l, &"name".into()).ok()?.as_string())
|
||||
.filter_map(|n| n.strip_prefix(keep::LOCK_PREFIX).map(str::to_string))
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
query.await.unwrap_or_else(|e| {
|
||||
log::info!("keep: open pages unknown ({e})");
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
|
||||
/// The kept copies among the stored files: (copy, when its design's file was last written), and
|
||||
/// the ids of designs stored without their metadata.
|
||||
struct Found {
|
||||
copies: Vec<(keep::Entry, Option<f64>)>,
|
||||
orphans: Vec<String>,
|
||||
}
|
||||
|
||||
fn kept_copies(all: &[(String, Vec<u8>, f64)]) -> Found {
|
||||
let modified = |path: &str| path.strip_prefix(OPFS).and_then(|n| all.iter().find(|(name, _, _)| name == n)).map(|(_, _, m)| *m);
|
||||
let mut copies = Vec::new();
|
||||
let mut designs = Vec::new();
|
||||
for (name, bytes, _) in all {
|
||||
match keep::parse_name(name) {
|
||||
Some((id, keep::Part::Meta)) => {
|
||||
if let Some(e) = keep::Entry::parse(id, bytes) {
|
||||
let saved = e.path.as_deref().and_then(modified);
|
||||
copies.push((e, saved));
|
||||
}
|
||||
}
|
||||
Some((id, keep::Part::Design)) => designs.push(id.to_string()),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
// A copy is whole only with its design.
|
||||
copies.retain(|(e, _)| designs.contains(&e.id));
|
||||
let orphans = designs.into_iter().filter(|id| !copies.iter().any(|(e, _)| &e.id == id)).collect();
|
||||
Found { copies, orphans }
|
||||
}
|
||||
|
||||
/// Offer the copies earlier pages left that hold unsaved work, and remove those their design's
|
||||
/// later save made stale (and parts without the rest), leaving open pages' copies alone.
|
||||
fn sort_kept(files: &Rc<Files>, found: Found, live: &[String]) {
|
||||
let mut offered = Vec::new();
|
||||
let mut gone = Vec::new();
|
||||
for (e, saved) in found.copies {
|
||||
match keep::fate(&e, live, saved) {
|
||||
keep::Fate::Offer => offered.push(e),
|
||||
keep::Fate::Drop => gone.push(e.id),
|
||||
keep::Fate::Leave => {}
|
||||
}
|
||||
}
|
||||
gone.extend(found.orphans.into_iter().filter(|id| !keep::page_of(id).is_some_and(|p| live.iter().any(|l| l == p))));
|
||||
keep::order_offered(&mut offered);
|
||||
*files.keep_offered.borrow_mut() = offered;
|
||||
for id in gone {
|
||||
remove_kept(files, &id);
|
||||
}
|
||||
}
|
||||
|
||||
/// The path (in the engine's files) a kept copy opens from.
|
||||
pub fn kept_path(id: &str) -> String {
|
||||
format!("{OPFS}{}", keep::design_file(id))
|
||||
}
|
||||
|
||||
/// Write the copy of a design with unsaved changes (`bytes`, described by `entry`) that holds
|
||||
/// its revision `revision`. Its metadata goes last, so a copy is never offered half written.
|
||||
pub fn write_kept(files: &Rc<Files>, entry: &keep::Entry, bytes: Vec<u8>, revision: u64) {
|
||||
let id = entry.id.clone();
|
||||
if !files.keeping.borrow_mut().insert(id.clone()) {
|
||||
return;
|
||||
}
|
||||
files.kept.borrow_mut().entry(id.clone()).or_insert(None);
|
||||
let meta = entry.to_json();
|
||||
let files = files.clone();
|
||||
spawn_local(async move {
|
||||
let r = async {
|
||||
write_file(&keep::design_file(&id), &bytes).await?;
|
||||
write_file(&keep::meta_file(&id), meta.as_bytes()).await
|
||||
};
|
||||
match r.await {
|
||||
Ok(()) => {
|
||||
files.kept.borrow_mut().insert(id.clone(), Some(revision));
|
||||
files.kept_failed.borrow_mut().remove(&id);
|
||||
}
|
||||
Err(e) => {
|
||||
files.kept_failed.borrow_mut().insert(id.clone(), js_sys::Date::now());
|
||||
files.say(format!("keeping unsaved work in this browser: {e}"), true)
|
||||
}
|
||||
}
|
||||
files.keeping.borrow_mut().remove(&id);
|
||||
files.wake();
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove kept copy `id` (its design was saved, closed, discarded or restored).
|
||||
pub fn remove_kept(files: &Rc<Files>, id: &str) {
|
||||
let id = id.to_string();
|
||||
if !files.keeping.borrow_mut().insert(id.clone()) {
|
||||
return;
|
||||
}
|
||||
let _ = vfs::remove(&kept_path(&id));
|
||||
files.kept_failed.borrow_mut().remove(&id);
|
||||
let files = files.clone();
|
||||
spawn_local(async move {
|
||||
// The metadata first: what's left without it is never offered.
|
||||
let _ = remove_file(&keep::meta_file(&id)).await;
|
||||
let _ = remove_file(&keep::design_file(&id)).await;
|
||||
files.kept.borrow_mut().remove(&id);
|
||||
files.keeping.borrow_mut().remove(&id);
|
||||
files.wake();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
//! Keep unsaved work in this browser (#10220): the decisions, in plain Rust (tested natively).
|
||||
//!
|
||||
//! With Preferences › General › "Keep unsaved work in this browser instead of asking when the tab
|
||||
//! closes" on, each open design with unsaved changes has a copy in the browser's private storage:
|
||||
//! `keep-<id>.sc` (the design) and `keep-<id>.json` (its name, its file and when it last
|
||||
//! changed). The copy is written shortly after each change ([`due`]) and again when the page is
|
||||
//! hidden, so it is already current when the tab closes; the page then doesn't ask through the
|
||||
//! browser's "leave site?" box ([`asks_before_unload`]). A design that is saved, closed or
|
||||
//! discarded has its copy removed. The next visit offers the copies newer than their design's
|
||||
//! last save ([`fate`]) in SolidCraft's own window, answered with [`plan`].
|
||||
//!
|
||||
//! An id is `<page>-<n>`: the page that wrote it (each page load has its own) and the design's
|
||||
//! number in that page. A page holds a browser lock named after it while it runs, so another tab
|
||||
//! never offers, or removes, the copies of a page that is still open.
|
||||
#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// The start of every kept file's name.
|
||||
pub const PREFIX: &str = "keep-";
|
||||
/// The browser lock a page holds while it runs: this, then its page id.
|
||||
pub const LOCK_PREFIX: &str = "solidcraft.keep.";
|
||||
/// A copy is written once the design has been still this long (ms)…
|
||||
pub const QUIET_MS: f64 = 800.0;
|
||||
/// …or at the latest this long after the first change it doesn't hold yet (ms).
|
||||
pub const MAX_WAIT_MS: f64 = 4000.0;
|
||||
/// The most copies offered at once (the rest wait for the next visit).
|
||||
pub const MAX_OFFERED: usize = 32;
|
||||
const MAX_ID: usize = 64;
|
||||
const MAX_NAME: usize = 256;
|
||||
const MAX_PATH: usize = 4096;
|
||||
/// Copies' metadata is small: a damaged or hostile one is not read.
|
||||
pub const MAX_META_BYTES: usize = 64 * 1024;
|
||||
|
||||
/// Ids are short and safe in a file name: ASCII letters, digits and dashes, with a page part.
|
||||
pub fn valid_id(id: &str) -> bool {
|
||||
!id.is_empty() && id.len() <= MAX_ID && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') && page_of(id).is_some()
|
||||
}
|
||||
|
||||
/// The page that wrote copy `id`.
|
||||
pub fn page_of(id: &str) -> Option<&str> {
|
||||
id.rsplit_once('-').map(|(page, _)| page).filter(|p| !p.is_empty())
|
||||
}
|
||||
|
||||
/// This page's id, from the time it started (ms since 1970) and a random number in 0..1:
|
||||
/// letters and digits only.
|
||||
pub fn page_id(now_ms: f64, random: f64) -> String {
|
||||
let now = if now_ms.is_finite() && now_ms > 0.0 { now_ms.min(1e15) as u64 } else { 0 };
|
||||
let random = if random.is_finite() { (random.clamp(0.0, 1.0) * f64::from(0xff_ffff)) as u64 } else { 0 };
|
||||
format!("{now:x}{random:06x}")
|
||||
}
|
||||
|
||||
/// The copy of design `uid` (`Session::uid`) open in page `page`.
|
||||
pub fn entry_id(page: &str, uid: u64) -> String {
|
||||
format!("{page}-{uid}")
|
||||
}
|
||||
|
||||
/// The stored file names of copy `id`: its design and its metadata.
|
||||
pub fn design_file(id: &str) -> String {
|
||||
format!("{PREFIX}{id}.{}", solidcraft_engine::io::DESIGN_EXT)
|
||||
}
|
||||
pub fn meta_file(id: &str) -> String {
|
||||
format!("{PREFIX}{id}.json")
|
||||
}
|
||||
|
||||
/// Which part of a copy a stored file is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Part {
|
||||
Design,
|
||||
Meta,
|
||||
}
|
||||
|
||||
/// A stored file's copy id and part; `None` for every other file.
|
||||
pub fn parse_name(name: &str) -> Option<(&str, Part)> {
|
||||
let rest = name.strip_prefix(PREFIX)?;
|
||||
let (id, part) = if let Some(id) = rest.strip_suffix(".json") {
|
||||
(id, Part::Meta)
|
||||
} else {
|
||||
(rest.strip_suffix(solidcraft_engine::io::DESIGN_EXT)?.strip_suffix('.')?, Part::Design)
|
||||
};
|
||||
valid_id(id).then_some((id, part))
|
||||
}
|
||||
|
||||
/// A kept copy, as its metadata describes it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Entry {
|
||||
pub id: String,
|
||||
/// The design's name.
|
||||
pub name: String,
|
||||
/// Its file (`None`: never saved).
|
||||
pub path: Option<String>,
|
||||
/// When the design last changed (ms since 1970).
|
||||
pub changed_at: f64,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn to_json(&self) -> String {
|
||||
json!({"name": self.name, "path": self.path, "changed_at": self.changed_at}).to_string()
|
||||
}
|
||||
|
||||
/// Read copy `id`'s metadata. Anything malformed, oversized or not finite is no copy.
|
||||
pub fn parse(id: &str, bytes: &[u8]) -> Option<Entry> {
|
||||
if !valid_id(id) || bytes.len() > MAX_META_BYTES {
|
||||
return None;
|
||||
}
|
||||
let v: Value = serde_json::from_slice(bytes).ok()?;
|
||||
let name = v.get("name")?.as_str()?.chars().take(MAX_NAME).collect::<String>();
|
||||
let path = match v.get("path") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(p) => Some(p.as_str().filter(|p| !p.is_empty() && p.len() <= MAX_PATH)?.to_string()),
|
||||
};
|
||||
let changed_at = v.get("changed_at")?.as_f64().filter(|t| t.is_finite() && *t >= 0.0)?;
|
||||
Some(Entry { id: id.to_string(), name, path, changed_at })
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a copy that last changed at `changed_at` holds work its design's file doesn't:
|
||||
/// the file was last written at `saved_at` (ms since 1970; `None` or unknown: never saved, or
|
||||
/// the time can't be read, so the copy is kept rather than lost).
|
||||
pub fn newer_than_save(changed_at: f64, saved_at: Option<f64>) -> bool {
|
||||
match saved_at {
|
||||
Some(saved) if saved.is_finite() && saved > 0.0 => changed_at > saved,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the next visit does with a copy it finds.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Fate {
|
||||
/// Offer it: "Restore unsaved design…?".
|
||||
Offer,
|
||||
/// Its design was saved after it: remove it without asking.
|
||||
Drop,
|
||||
/// Its page is still open (another tab): leave it alone.
|
||||
Leave,
|
||||
}
|
||||
|
||||
/// The copy `entry`, whose design's file was last written at `saved_at`, while the pages
|
||||
/// `live` are open.
|
||||
pub fn fate(entry: &Entry, live: &[String], saved_at: Option<f64>) -> Fate {
|
||||
if page_of(&entry.id).is_some_and(|p| live.iter().any(|l| l == p)) {
|
||||
Fate::Leave
|
||||
} else if newer_than_save(entry.changed_at, saved_at) {
|
||||
Fate::Offer
|
||||
} else {
|
||||
Fate::Drop
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies to offer first: the most recently changed, at most [`MAX_OFFERED`].
|
||||
pub fn order_offered(entries: &mut Vec<Entry>) {
|
||||
entries.sort_by(|a, b| b.changed_at.total_cmp(&a.changed_at).then_with(|| a.id.cmp(&b.id)));
|
||||
entries.truncate(MAX_OFFERED);
|
||||
}
|
||||
|
||||
/// Whether a design's copy is due to be written at `now`: the design last changed at
|
||||
/// `last_change`, and the copy has lacked a change since `first_unwritten` (all ms).
|
||||
pub fn due(now: f64, first_unwritten: f64, last_change: f64) -> bool {
|
||||
now - last_change >= QUIET_MS || now - first_unwritten >= MAX_WAIT_MS
|
||||
}
|
||||
|
||||
/// How long after a failed write of a design's copy it isn't tried again (ms).
|
||||
pub const RETRY_MS: f64 = 30_000.0;
|
||||
|
||||
/// Whether a copy whose write last failed at `failed_at` (ms; `None`: it hasn't) may be written
|
||||
/// at `now`: not before [`RETRY_MS`] has passed, so a full or denied store isn't hit every tick.
|
||||
pub fn may_write(now: f64, failed_at: Option<f64>) -> bool {
|
||||
failed_at.is_none_or(|t| !(now >= t) || now - t >= RETRY_MS)
|
||||
}
|
||||
|
||||
/// Whether `wanted` is the design now open, given the open design's path.
|
||||
pub fn is_open(open: Option<&str>, wanted: &str) -> bool {
|
||||
open == Some(wanted)
|
||||
}
|
||||
|
||||
/// Whether leaving the page asks through the browser's "leave site?" box: there is unsaved work,
|
||||
/// and either the user still wants to be asked or it isn't all kept in the browser yet (with the
|
||||
/// keep preference off it never is, so there the box always stays).
|
||||
pub fn asks_before_unload(persists: bool, keep: bool, also_ask: bool, any_unsaved: bool, all_kept: bool) -> bool {
|
||||
persists && any_unsaved && (also_ask || !(keep && all_kept))
|
||||
}
|
||||
|
||||
/// An answer in the "Restore unsaved design?" window.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Answer {
|
||||
/// Open the copy, with its unsaved changes, as the design it was (saving goes to its file).
|
||||
Restore,
|
||||
/// Open the design's file as last saved; the copy goes.
|
||||
OpenSaved,
|
||||
/// The copy goes.
|
||||
Discard,
|
||||
}
|
||||
|
||||
/// What an answer does.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Plan {
|
||||
/// Open the copy (and, once it is open, give it back its name and file).
|
||||
pub open_copy: bool,
|
||||
/// Open this saved file.
|
||||
pub open_saved: Option<String>,
|
||||
/// Remove the copy.
|
||||
pub drop_copy: bool,
|
||||
}
|
||||
|
||||
/// What `answer` does to copy `entry`; `saved_exists`: its file can still be opened. `None`: the
|
||||
/// answer isn't offered for it (Open saved version of a design never saved, or gone).
|
||||
pub fn plan(answer: Answer, entry: &Entry, saved_exists: bool) -> Option<Plan> {
|
||||
match answer {
|
||||
Answer::Restore => Some(Plan { open_copy: true, open_saved: None, drop_copy: true }),
|
||||
Answer::OpenSaved => {
|
||||
let path = entry.path.clone().filter(|_| saved_exists)?;
|
||||
Some(Plan { open_copy: false, open_saved: Some(path), drop_copy: true })
|
||||
}
|
||||
Answer::Discard => Some(Plan { open_copy: false, open_saved: None, drop_copy: true }),
|
||||
}
|
||||
}
|
||||
|
||||
/// The answers the window offers for `entry`, in order.
|
||||
pub fn answers(entry: &Entry, saved_exists: bool) -> Vec<Answer> {
|
||||
[Answer::Restore, Answer::OpenSaved, Answer::Discard].into_iter().filter(|a| plan(*a, entry, saved_exists).is_some()).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn entry(id: &str, changed_at: f64, path: Option<&str>) -> Entry {
|
||||
Entry { id: id.into(), name: "Bracket".into(), path: path.map(str::to_string), changed_at }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_write_waits_before_the_next_try() {
|
||||
assert!(may_write(1000.0, None));
|
||||
assert!(!may_write(1000.0 + RETRY_MS - 1.0, Some(1000.0)));
|
||||
assert!(may_write(1000.0 + RETRY_MS, Some(1000.0)));
|
||||
// A clock that went back doesn't block for good.
|
||||
assert!(may_write(10.0, Some(1000.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_design_counts_as_opened_only_at_its_own_path() {
|
||||
assert!(is_open(Some("a.sc"), "a.sc"));
|
||||
assert!(!is_open(Some("b.sc"), "a.sc"));
|
||||
assert!(!is_open(None, "a.sc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_are_named_by_page_and_design_and_nothing_else_reads_as_one() {
|
||||
let page = page_id(1_700_000_000_000.0, 0.5);
|
||||
assert!(page.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
let id = entry_id(&page, 7);
|
||||
assert!(valid_id(&id));
|
||||
assert_eq!(page_of(&id), Some(page.as_str()));
|
||||
assert_eq!(parse_name(&design_file(&id)), Some((id.as_str(), Part::Design)));
|
||||
assert_eq!(parse_name(&meta_file(&id)), Some((id.as_str(), Part::Meta)));
|
||||
for other in ["part.sc", "recovery.sc", "recovery.json", "keep-.sc", "keep-x.sc", "keep-a/b-1.sc", "keep-a-1.step", "keep-a-1sc"] {
|
||||
assert_eq!(parse_name(other), None, "{other}");
|
||||
}
|
||||
// Hostile clocks still give a usable id.
|
||||
for (now, random) in [(f64::NAN, f64::NAN), (f64::INFINITY, -1.0), (-5.0, 9.0)] {
|
||||
assert!(valid_id(&entry_id(&page_id(now, random), 1)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_round_trips_and_hostile_metadata_is_no_copy() {
|
||||
let e = entry("a1-3", 1234.0, Some("/opfs/Bracket.sc"));
|
||||
assert_eq!(Entry::parse("a1-3", e.to_json().as_bytes()), Some(e.clone()));
|
||||
let never_saved = entry("a1-4", 99.0, None);
|
||||
assert_eq!(Entry::parse("a1-4", never_saved.to_json().as_bytes()), Some(never_saved));
|
||||
for bad in [
|
||||
"",
|
||||
"[]",
|
||||
r#"{"name":"x","changed_at":"soon"}"#,
|
||||
r#"{"name":"x","changed_at":-1}"#,
|
||||
r#"{"name":"x","changed_at":1e999}"#,
|
||||
r#"{"name":"x","path":7,"changed_at":1}"#,
|
||||
r#"{"name":"x","path":"","changed_at":1}"#,
|
||||
r#"{"changed_at":1}"#,
|
||||
] {
|
||||
assert_eq!(Entry::parse("a1-3", bad.as_bytes()), None, "{bad}");
|
||||
}
|
||||
assert_eq!(Entry::parse("../x", e.to_json().as_bytes()), None);
|
||||
assert_eq!(Entry::parse("a1-3", &vec![b' '; MAX_META_BYTES + 1]), None);
|
||||
let long = json!({"name": "n".repeat(10_000), "changed_at": 1}).to_string();
|
||||
assert_eq!(Entry::parse("a1-3", long.as_bytes()).map(|e| e.name.len()), Some(MAX_NAME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_copy_newer_than_the_last_save_is_offered_an_older_one_dropped_an_open_pages_left() {
|
||||
let e = entry("p1-1", 2000.0, Some("/opfs/Bracket.sc"));
|
||||
// Changed after the file was last written: offered.
|
||||
assert_eq!(fate(&e, &[], Some(1000.0)), Fate::Offer);
|
||||
// Saved since (in this tab or another): nothing to offer.
|
||||
assert_eq!(fate(&e, &[], Some(2000.0)), Fate::Drop);
|
||||
assert_eq!(fate(&e, &[], Some(3000.0)), Fate::Drop);
|
||||
// Never saved, or the save time can't be read: kept rather than lost.
|
||||
for saved in [None, Some(0.0), Some(f64::NAN), Some(f64::INFINITY), Some(-4.0)] {
|
||||
assert_eq!(fate(&e, &[], saved), Fate::Offer, "{saved:?}");
|
||||
}
|
||||
// Its page is still open in another tab: neither offered nor removed.
|
||||
assert_eq!(fate(&e, &["p1".into()], Some(1000.0)), Fate::Leave);
|
||||
assert_eq!(fate(&e, &["p1".into()], Some(3000.0)), Fate::Leave);
|
||||
assert_eq!(fate(&e, &["p2".into()], Some(1000.0)), Fate::Offer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_newest_copies_are_offered_first_and_at_most_a_windowful() {
|
||||
let mut list: Vec<Entry> = (0..40).map(|i| entry(&format!("p-{i}"), f64::from(i), None)).collect();
|
||||
order_offered(&mut list);
|
||||
assert_eq!(list.len(), MAX_OFFERED);
|
||||
assert_eq!(list.first().map(|e| e.id.as_str()), Some("p-39"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_copy_is_written_once_the_design_is_still_or_after_the_longest_wait() {
|
||||
// Still changing, not waited long: not yet.
|
||||
assert!(!due(1000.0, 900.0, 900.0));
|
||||
// Still for QUIET_MS: due.
|
||||
assert!(due(900.0 + QUIET_MS, 900.0, 900.0));
|
||||
// Changing all the time: due after MAX_WAIT_MS anyway.
|
||||
assert!(!due(5000.0, 2000.0, 4900.0));
|
||||
assert!(due(2000.0 + MAX_WAIT_MS, 2000.0, 2000.0 + MAX_WAIT_MS - 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_browser_asks_before_unload_unless_told_not_to_and_every_copy_is_current() {
|
||||
// Keeping off: exactly as before, unsaved work asks, whatever the second option says.
|
||||
assert!(asks_before_unload(true, false, true, true, true));
|
||||
assert!(asks_before_unload(true, false, true, true, false));
|
||||
assert!(asks_before_unload(true, false, false, true, true));
|
||||
// Keeping on, asking on (the default): both, the box and the copy.
|
||||
assert!(asks_before_unload(true, true, true, true, true));
|
||||
// Keeping on, asking off, every unsaved design kept: no browser box.
|
||||
assert!(!asks_before_unload(true, true, false, true, true));
|
||||
// Keeping on, asking off, a copy still being written (or failed): the browser still asks.
|
||||
assert!(asks_before_unload(true, true, false, true, false));
|
||||
// Nothing unsaved, or an embedded page that keeps nothing: never.
|
||||
assert!(!asks_before_unload(true, true, true, false, false));
|
||||
assert!(!asks_before_unload(true, false, true, false, false));
|
||||
assert!(!asks_before_unload(false, false, true, true, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_open_saved_and_discard_each_do_one_thing_and_always_remove_the_copy() {
|
||||
let saved = entry("p-1", 5.0, Some("/opfs/Bracket.sc"));
|
||||
assert_eq!(plan(Answer::Restore, &saved, true), Some(Plan { open_copy: true, open_saved: None, drop_copy: true }));
|
||||
assert_eq!(
|
||||
plan(Answer::OpenSaved, &saved, true),
|
||||
Some(Plan { open_copy: false, open_saved: Some("/opfs/Bracket.sc".into()), drop_copy: true })
|
||||
);
|
||||
assert_eq!(plan(Answer::Discard, &saved, true), Some(Plan { open_copy: false, open_saved: None, drop_copy: true }));
|
||||
assert_eq!(answers(&saved, true), [Answer::Restore, Answer::OpenSaved, Answer::Discard]);
|
||||
// A design never saved, or whose file is gone, has no saved version to open.
|
||||
let never = entry("p-2", 5.0, None);
|
||||
assert_eq!(plan(Answer::OpenSaved, &never, true), None);
|
||||
assert_eq!(plan(Answer::OpenSaved, &saved, false), None);
|
||||
assert_eq!(answers(&never, false), [Answer::Restore, Answer::Discard]);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
//! Runs the same [`solidcraft_ui_egui::SolveApp`] as the desktop app through eframe's web runner
|
||||
//! (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release` from this
|
||||
//! directory (output in `dist/web`). URL flags: `?webgl` forces WebGL2 (remembered until
|
||||
//! `?webgpu`), `?sample` opens the sample part.
|
||||
//! `?webgpu`), `?sample` opens the sample part, `?embed=editor` runs it as an editing embed in a
|
||||
//! host page (no application bar or start page; nothing kept in the browser; #8517).
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -18,6 +19,8 @@ mod easyprint;
|
||||
mod files;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod install;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod keep;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod linked;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
@@ -42,6 +45,8 @@ fn query_flag(query: &str, name: &str) -> bool {
|
||||
struct Launch {
|
||||
open: Option<String>,
|
||||
embed: bool,
|
||||
/// `?embed=editor` (#8517): an editing embed in a host page that keeps the files itself.
|
||||
editor: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
@@ -52,16 +57,19 @@ impl Launch {
|
||||
for (key, value) in url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"open" if launch.open.is_none() => launch.open = Some(value.into_owned()),
|
||||
"embed" if value == "editor" => launch.editor = true,
|
||||
"embed" => embed = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
launch.embed = embed && launch.open.is_some();
|
||||
launch.embed = embed && launch.open.is_some() && !launch.editor;
|
||||
launch
|
||||
}
|
||||
|
||||
/// The browser's own storage (designs, autosave, recovery, preferences) is used: not when
|
||||
/// embedded, where the host keeps what is kept.
|
||||
fn persists(&self) -> bool {
|
||||
!self.embed
|
||||
!self.embed && !self.editor
|
||||
}
|
||||
|
||||
fn full_app_url(&self, mut url: url::Url) -> String {
|
||||
@@ -171,12 +179,14 @@ mod web {
|
||||
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::io::vfs;
|
||||
use solidcraft_ui_egui::ui_kit::BackingTooltipExt as _;
|
||||
use solidcraft_ui_egui::{Services, SolveApp, ui_kit};
|
||||
use wasm_bindgen::JsCast as _;
|
||||
|
||||
use crate::api::Api;
|
||||
use crate::disk::{self, SaveTarget};
|
||||
use crate::files::{self, Files, Intent};
|
||||
use crate::keep;
|
||||
use crate::space_mouse::SpaceMouse;
|
||||
|
||||
const CANVAS_ID: &str = "solidcraft_canvas";
|
||||
@@ -236,7 +246,9 @@ mod web {
|
||||
}
|
||||
|
||||
/// The browser's "leave site? changes you made may not be saved" prompt while a design has
|
||||
/// unsaved changes (the page can't show its own dialog when it is being closed).
|
||||
/// unsaved changes (the page can't show its own dialog when it is being closed). With "keep
|
||||
/// unsaved work in this browser" on and "still ask" off, there is nothing to ask once every
|
||||
/// unsaved design's copy is current (`keep::asks_before_unload`, #10220).
|
||||
fn ask_before_unload(window: &web_sys::Window) {
|
||||
let on_unload = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::BeforeUnloadEvent)>::new(|e: web_sys::BeforeUnloadEvent| {
|
||||
if UNSAVED.with(std::cell::Cell::get) {
|
||||
@@ -249,6 +261,50 @@ mod web {
|
||||
on_unload.forget();
|
||||
}
|
||||
|
||||
/// With "keep unsaved work in this browser" on, the page brings every unsaved design's copy up
|
||||
/// to date when it is hidden or goes (`pagehide`, `visibilitychange`): the browser may not
|
||||
/// wait for that write, so the copies are written shortly after each change as well
|
||||
/// (`Shell::keep_tick`, #10220).
|
||||
fn keep_when_hidden(window: &web_sys::Window, runner: &eframe::WebRunner) {
|
||||
let keep_now = {
|
||||
let runner = runner.clone();
|
||||
move || {
|
||||
if let Some(mut shell) = runner.app_mut::<Shell>() {
|
||||
shell.keep_tick(None, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
let on_hide = {
|
||||
let keep_now = keep_now.clone();
|
||||
wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::Event)>::new(move |_: web_sys::Event| keep_now())
|
||||
};
|
||||
let on_visibility = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::Event)>::new(move |_: web_sys::Event| {
|
||||
let hidden = web_sys::window()
|
||||
.and_then(|w| w.document())
|
||||
.and_then(|d| js_sys::Reflect::get(&d, &"visibilityState".into()).ok())
|
||||
.and_then(|v| v.as_string())
|
||||
.is_some_and(|v| v == "hidden");
|
||||
if hidden {
|
||||
keep_now();
|
||||
}
|
||||
});
|
||||
let _ = window.add_event_listener_with_callback("pagehide", on_hide.as_ref().unchecked_ref());
|
||||
if let Some(document) = window.document() {
|
||||
let _ = document.add_event_listener_with_callback("visibilitychange", on_visibility.as_ref().unchecked_ref());
|
||||
}
|
||||
// The listeners live as long as the page.
|
||||
on_hide.forget();
|
||||
on_visibility.forget();
|
||||
}
|
||||
|
||||
/// One open design followed for its kept copy: its revision, when that last changed, and since
|
||||
/// when its copy has lacked a change (`None`: current, or being written).
|
||||
struct Track {
|
||||
revision: u64,
|
||||
last_change: f64,
|
||||
first_unwritten: Option<f64>,
|
||||
}
|
||||
|
||||
/// eframe cancels the browser's action for every Tab it gets, so the browser couldn't switch
|
||||
/// tabs while the app has the keyboard. Its tab-switching keys stop here, on the way down to
|
||||
/// the canvas, before eframe sees them; the browser then acts on them as usual.
|
||||
@@ -321,6 +377,10 @@ mod web {
|
||||
/// The settings this tab wrote or loaded last, and those another tab wrote since (#8644).
|
||||
prefs_writer: crate::PrefsWriter,
|
||||
other_tab_prefs: PrefsInbox,
|
||||
/// The open designs followed for their kept copy, by `Session::uid` (#10220).
|
||||
keep_track: std::collections::HashMap<u64, Track>,
|
||||
/// Every unsaved design's kept copy is current (and the preference is on).
|
||||
all_kept: bool,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
@@ -435,8 +495,14 @@ mod web {
|
||||
self.sync_recents();
|
||||
}
|
||||
let now = ctx.input(|i| i.time);
|
||||
// With "keep unsaved work in this browser" on, each design's own copy takes over from
|
||||
// the single autosave (#10220).
|
||||
if self.app.preferences.keep_unsaved_in_browser && self.autosaved.is_some() {
|
||||
files::autosave(&self.files, None);
|
||||
self.autosaved = None;
|
||||
}
|
||||
// Unsaved work is kept (at most every AUTOSAVE_S); saved work needs no autosave.
|
||||
if self.files.ready.get() && self.files.recovery.borrow().is_none() {
|
||||
if self.files.ready.get() && self.files.recovery.borrow().is_none() && !self.app.preferences.keep_unsaved_in_browser {
|
||||
let s = &self.app.session;
|
||||
let changed = s.is_dirty() && self.autosaved != Some(s.revision);
|
||||
let wait = AUTOSAVE_S - (now - self.autosaved_at);
|
||||
@@ -455,6 +521,175 @@ mod web {
|
||||
if !self.files.ready.get() || !self.files.incoming.borrow().is_empty() {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
}
|
||||
self.all_kept = self.keep_tick(Some(ctx), false);
|
||||
}
|
||||
|
||||
/// Keep each open design's unsaved changes in the browser, when the preference asks for
|
||||
/// it: a design's copy is written once it has been still a moment (`keep::due`), or at
|
||||
/// once with `now` (the page is being hidden); a copy whose design is saved or closed, or
|
||||
/// with the preference off, goes. Answers whether every unsaved design's copy is current.
|
||||
fn keep_tick(&mut self, ctx: Option<&egui::Context>, now: bool) -> bool {
|
||||
if !self.launch.persists() || !self.files.ready.get() {
|
||||
return false;
|
||||
}
|
||||
let Shell { app, files, keep_track, .. } = self;
|
||||
let on = app.preferences.keep_unsaved_in_browser;
|
||||
let time = js_sys::Date::now();
|
||||
let page = files.keep_page.borrow().clone();
|
||||
let mut wanted = std::collections::HashSet::new();
|
||||
let mut all_kept = true;
|
||||
let mut wake: Option<f64> = None;
|
||||
let mut error = None;
|
||||
let mut soon = |ms: f64| wake = Some(wake.map_or(ms, |w: f64| w.min(ms)));
|
||||
if on {
|
||||
for s in solidcraft_ui_egui::documents::open_sessions(app).into_iter().filter(|s| s.is_dirty()) {
|
||||
let id = keep::entry_id(&page, s.uid);
|
||||
wanted.insert(s.uid);
|
||||
let t = keep_track.entry(s.uid).or_insert(Track { revision: s.revision, last_change: time, first_unwritten: None });
|
||||
if t.revision != s.revision {
|
||||
t.revision = s.revision;
|
||||
t.last_change = time;
|
||||
}
|
||||
if files.kept.borrow().get(&id).copied().flatten() == Some(s.revision) {
|
||||
t.first_unwritten = None;
|
||||
continue;
|
||||
}
|
||||
all_kept = false;
|
||||
if files.keeping.borrow().contains(&id) {
|
||||
soon(100.0);
|
||||
continue;
|
||||
}
|
||||
let first = *t.first_unwritten.get_or_insert(time);
|
||||
let failed = files.kept_failed.borrow().get(&id).copied();
|
||||
if !keep::may_write(time, failed) {
|
||||
// The last write failed: try again after a while.
|
||||
soon(keep::RETRY_MS);
|
||||
continue;
|
||||
}
|
||||
if now || keep::due(time, first, t.last_change) {
|
||||
match solidcraft_engine::io::write_design_container(&s.design()) {
|
||||
Ok(bytes) => {
|
||||
let entry = keep::Entry { id, name: s.doc.name.clone(), path: s.path.clone(), changed_at: t.last_change };
|
||||
files::write_kept(files, &entry, bytes, s.revision);
|
||||
t.first_unwritten = None;
|
||||
}
|
||||
Err(e) => {
|
||||
files.kept_failed.borrow_mut().insert(id, time);
|
||||
error = Some(format!("keeping unsaved work in this browser: {e}"));
|
||||
}
|
||||
}
|
||||
soon(100.0);
|
||||
} else {
|
||||
soon((keep::QUIET_MS - (time - t.last_change)).min(keep::MAX_WAIT_MS - (time - first)).max(16.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
// This page's copies no design needs any more: saved, closed, or the preference off.
|
||||
let stale: Vec<String> = files
|
||||
.kept
|
||||
.borrow()
|
||||
.keys()
|
||||
.filter(|id| keep::page_of(id) == Some(page.as_str()))
|
||||
.filter(|id| !wanted.iter().any(|uid| keep::entry_id(&page, *uid) == **id))
|
||||
.cloned()
|
||||
.collect();
|
||||
for id in stale {
|
||||
files::remove_kept(files, &id);
|
||||
soon(100.0);
|
||||
}
|
||||
keep_track.retain(|uid, _| wanted.contains(uid));
|
||||
if let Some(e) = error {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
if let (Some(ctx), Some(ms)) = (ctx, wake) {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(ms / 1000.0));
|
||||
}
|
||||
on && all_kept
|
||||
}
|
||||
|
||||
/// Offer the copies an earlier visit kept (#10220): "Restore unsaved design: <name>,
|
||||
/// changed <time>?", one window for all of them, each with Restore, Open saved version
|
||||
/// (when it has a file) and Discard.
|
||||
fn keep_prompt(&mut self, ctx: &egui::Context) {
|
||||
let offered = self.files.keep_offered.borrow().clone();
|
||||
if offered.is_empty() || !self.files.ready.get() {
|
||||
return;
|
||||
}
|
||||
let now = (js_sys::Date::now() / 1000.0) as u64;
|
||||
let rows: Vec<_> = offered
|
||||
.iter()
|
||||
.map(|e| solidcraft_ui_egui::recovery_ui::BrowserCopy {
|
||||
name: &e.name,
|
||||
when: solidcraft_ui_egui::recovery_ui::ago_between(now, (e.changed_at / 1000.0) as u64),
|
||||
open_saved: keep::answers(e, self.saved_exists(e)).contains(&keep::Answer::OpenSaved),
|
||||
})
|
||||
.collect();
|
||||
let act = solidcraft_ui_egui::recovery_ui::browser_copies_window(ctx, &self.app.preferences.language, &rows);
|
||||
let Some((index, answer)) = act else { return };
|
||||
let Some(entry) = offered.get(index) else { return };
|
||||
let answer = match answer {
|
||||
solidcraft_ui_egui::recovery_ui::CopyAnswer::Restore => keep::Answer::Restore,
|
||||
solidcraft_ui_egui::recovery_ui::CopyAnswer::OpenSaved => keep::Answer::OpenSaved,
|
||||
solidcraft_ui_egui::recovery_ui::CopyAnswer::Discard => keep::Answer::Discard,
|
||||
};
|
||||
self.keep_answer(entry, answer);
|
||||
}
|
||||
|
||||
/// Whether a kept copy's design file can still be opened: in the browser's storage, or a
|
||||
/// file on the computer this browser remembers.
|
||||
fn saved_exists(&self, e: &keep::Entry) -> bool {
|
||||
e.path.as_deref().is_some_and(|p| vfs::exists(p) || disk::parse(p).is_some_and(|(id, _)| self.files.disk_known.borrow().contains(id)))
|
||||
}
|
||||
|
||||
/// Act on an answer in the "Restore unsaved design?" window (`keep::plan`).
|
||||
fn keep_answer(&mut self, entry: &keep::Entry, answer: keep::Answer) {
|
||||
let Some(plan) = keep::plan(answer, entry, self.saved_exists(entry)) else { return };
|
||||
if plan.open_copy {
|
||||
let copy = match files::prepare_kept(&entry.id, entry.path.as_deref()) {
|
||||
Ok(copy) => copy,
|
||||
Err(error) => {
|
||||
self.app.set_status(error, true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.app.open_path(©);
|
||||
if self.app.session.path.as_deref() != Some(copy.as_str()) {
|
||||
// Not opened (the status says why): it stays offered.
|
||||
return;
|
||||
}
|
||||
// It is its own design again (saving goes to its file), with its changes unsaved.
|
||||
self.app.session.path = entry.path.clone();
|
||||
self.app.session.doc_mut().name = entry.name.clone();
|
||||
self.app.session.mark_unsaved();
|
||||
self.app.home.open = false;
|
||||
}
|
||||
if let Some(path) = &plan.open_saved {
|
||||
if vfs::exists(path) {
|
||||
self.app.open_path(path);
|
||||
if !keep::is_open(self.app.session.path.as_deref(), path) {
|
||||
// Not opened (the status says why): the copy stays offered.
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// A file on the computer: the browser asks for access again, in this click.
|
||||
// The copy goes once the file has opened, not before: if access is refused,
|
||||
// the picker is dismissed or the file is gone, it stays offered.
|
||||
let (files, id) = (self.files.clone(), entry.id.clone());
|
||||
disk::restore(
|
||||
&self.files,
|
||||
path,
|
||||
Some(Box::new(move || {
|
||||
files.keep_offered.borrow_mut().retain(|e| e.id != id);
|
||||
files::remove_kept(&files, &id);
|
||||
})),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if plan.drop_copy {
|
||||
self.files.keep_offered.borrow_mut().retain(|e| e.id != entry.id);
|
||||
files::remove_kept(&self.files, &entry.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// The start page's recent designs are the stored ones: the remembered order first (those
|
||||
@@ -500,7 +735,7 @@ mod web {
|
||||
files::pick_from_disk(&self.files, Intent::Open);
|
||||
}
|
||||
if ui_kit::button(ui, "Insert from computer…", ui_kit::Button::Normal)
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
|
||||
.backing_on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
|
||||
.clicked()
|
||||
{
|
||||
files::pick_from_disk(&self.files, Intent::Insert);
|
||||
@@ -554,7 +789,7 @@ mod web {
|
||||
let design = solidcraft_engine::io::is_design_path(&e.name);
|
||||
if !design
|
||||
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
|
||||
.backing_on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("insert", e.name.clone()));
|
||||
@@ -564,7 +799,7 @@ mod web {
|
||||
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")
|
||||
.backing_on_hover_text("A copy in a file you choose; the stored design stays here")
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("save_copy", e.name.clone()));
|
||||
@@ -691,6 +926,13 @@ mod web {
|
||||
files::autosave(&self.files, None);
|
||||
}
|
||||
|
||||
/// Whether leaving the page asks through the browser's own box (`keep::asks_before_unload`).
|
||||
fn asks_before_unload(&self) -> bool {
|
||||
let any = solidcraft_ui_egui::documents::any_dirty(&self.app);
|
||||
let prefs = &self.app.preferences;
|
||||
keep::asks_before_unload(self.launch.persists(), prefs.keep_unsaved_in_browser, prefs.keep_unsaved_also_asks, any, self.all_kept)
|
||||
}
|
||||
|
||||
/// Whether the shell already has the on-demand face the live language needs.
|
||||
fn script_font_ready(&self) -> bool {
|
||||
match solidcraft_ui_egui::fonts::web_script_font_kind(solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language)) {
|
||||
@@ -802,7 +1044,7 @@ mod web {
|
||||
self.api.poll();
|
||||
self.space_mouse.logic(&mut self.app, ctx);
|
||||
self.files_tick(ctx);
|
||||
UNSAVED.with(|u| u.set(solidcraft_ui_egui::documents::any_dirty(&self.app)));
|
||||
UNSAVED.with(|u| u.set(self.asks_before_unload()));
|
||||
DESIGN_SHOWN.with(|d| d.set(solidcraft_ui_egui::toolbar::design_shown(&self.app)));
|
||||
}
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
@@ -822,7 +1064,7 @@ mod web {
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.linked.tick(&mut self.app, ui.ctx());
|
||||
UNSAVED.with(|u| u.set(self.launch.persists() && solidcraft_ui_egui::documents::any_dirty(&self.app)));
|
||||
UNSAVED.with(|u| u.set(self.asks_before_unload()));
|
||||
if self.launch.embed {
|
||||
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
|
||||
@@ -837,10 +1079,15 @@ mod web {
|
||||
if self.launch.persists() {
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
self.keep_prompt(&ctx);
|
||||
}
|
||||
self.watch_text_for_font(&ctx);
|
||||
self.store_prefs(&ctx);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
||||
Some(&mut *self)
|
||||
}
|
||||
fn persist_egui_memory(&self) -> bool {
|
||||
// eframe keeps this under one fixed key: only stable uses it, so a pre-release never rewrites it.
|
||||
self.launch.persists() && crate::channel::current().is_empty()
|
||||
@@ -915,7 +1162,8 @@ mod web {
|
||||
{
|
||||
create.instance_descriptor.backends = eframe::wgpu::Backends::GL;
|
||||
}
|
||||
let result = eframe::WebRunner::new()
|
||||
let runner = eframe::WebRunner::new();
|
||||
let result = runner
|
||||
.start(
|
||||
canvas,
|
||||
options,
|
||||
@@ -927,7 +1175,7 @@ mod web {
|
||||
}
|
||||
let (panel, opener, saver, shown) = (files.clone(), files.clone(), files.clone(), files.clone());
|
||||
let (restorable, restorer) = (files.clone(), files.clone());
|
||||
let services = if launch.embed { Services::default() } else { Services {
|
||||
let services = if !launch.persists() { Services::default() } else { Services {
|
||||
// Open shows the browser's open dialog for a design where it has one
|
||||
// (the file keeps its handle, so Save writes back to it), else the
|
||||
// browser's files window. A command that reads another kind of file
|
||||
@@ -959,7 +1207,7 @@ mod web {
|
||||
can_restore: Some(Box::new(move |path: &str| {
|
||||
disk::parse(path).is_some_and(|(id, _)| restorable.disk_known.borrow().contains(id))
|
||||
})),
|
||||
restore_file: Some(Box::new(move |path: &str| disk::restore(&restorer, path))),
|
||||
restore_file: Some(Box::new(move |path: &str| disk::restore(&restorer, path, None))),
|
||||
download_design: Some(Box::new(files::download)),
|
||||
easyprint_upload: Some(std::sync::Arc::new(crate::easyprint::upload)),
|
||||
browser_files: Some(Box::new(move || shown.panel.set(true))),
|
||||
@@ -971,6 +1219,7 @@ mod web {
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
|
||||
app.embedded = launch.embed;
|
||||
app.embed_editor = launch.editor;
|
||||
// Listening before reading: settings another tab writes from now on are
|
||||
// newer than those read here.
|
||||
let other_tab_prefs = PrefsInbox::default();
|
||||
@@ -1025,10 +1274,18 @@ mod web {
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
|
||||
prefs_writer: crate::PrefsWriter::default(),
|
||||
other_tab_prefs,
|
||||
keep_track: Default::default(),
|
||||
all_kept: false,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
if result.is_ok()
|
||||
&& crate::Launch::parse(&query()).persists()
|
||||
&& let Some(window) = web_sys::window()
|
||||
{
|
||||
keep_when_hidden(&window, &runner);
|
||||
}
|
||||
if let Some(el) = document.get_element_by_id(LOADING_ID) {
|
||||
match result {
|
||||
Ok(()) => dismiss_loading(el),
|
||||
@@ -1081,6 +1338,17 @@ mod tests {
|
||||
assert!(full.fragment().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embed_editor_needs_no_design_link_and_keeps_nothing_in_the_browser() {
|
||||
let editor = Launch::parse("?embed=editor");
|
||||
assert!(editor.editor && !editor.embed && !editor.persists());
|
||||
let linked = Launch::parse("?embed=editor&open=https://example.org/a.sc");
|
||||
assert!(linked.editor && !linked.embed, "an editing embed may open a linked design, still editable");
|
||||
let viewer = Launch::parse("?embed=viewer&open=https://example.org/a.sc");
|
||||
assert!(viewer.embed && !viewer.editor, "any other value is the viewer embed, as before");
|
||||
assert!(!Launch::parse("?other=editor").editor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_links_refuse_hostile_schemes_credentials_and_sizes() {
|
||||
assert!(design_url("https://example.org/arm.solidcraft?download=1", "http://solidcraft.io").is_ok());
|
||||
|
||||
@@ -81,6 +81,7 @@ windows-sys = { version = "0.61", features = [
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
winreg = "0.56"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
muda = { version = "0.21.2", default-features = false }
|
||||
@@ -92,7 +93,7 @@ hidapi = { version = "2.6.7", default-features = false, features = ["macos-share
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs), and the fullscreen title-bar
|
||||
# inset (src/main.rs); all already come with eframe and rfd.
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSAccessibility", "NSApplication", "NSResponder", "NSWindow", "NSWorkspace"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
|
||||
block2 = "0.6"
|
||||
# Native crash capture (src/native_crash_macos.rs): the signal handler, the faulting module
|
||||
|
||||
@@ -68,7 +68,9 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
|
||||
Ok(l) => {
|
||||
if let Some(port) = listen_on_listener(l, token.clone(), tx, ctx, false) {
|
||||
if matches!(listen_on, control_auth::Listen::Default) {
|
||||
eprintln!("solidcraft: control channel on the default port 127.0.0.1:{port} (Preferences > Assistant turns it off)");
|
||||
solidcraft_engine::note!(
|
||||
"solidcraft: control channel on the default port 127.0.0.1:{port} (Preferences > Assistant turns it off)"
|
||||
);
|
||||
}
|
||||
// The discovery file: where `solidcraft-cli mcp` finds this app with no arguments.
|
||||
match control_auth::write_current_port(port) {
|
||||
@@ -77,14 +79,14 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
|
||||
*p = Some((port, token));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("solidcraft: could not write the control discovery file ({e})"),
|
||||
Err(e) => solidcraft_engine::note!("solidcraft: could not write the control discovery file ({e})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}"),
|
||||
Err(e) => solidcraft_engine::note!("solidcraft: control server failed to bind 127.0.0.1: {e}"),
|
||||
}
|
||||
}
|
||||
None => eprintln!("solidcraft: control server not started: no random token available"),
|
||||
None => solidcraft_engine::note!("solidcraft: control server not started: no random token available"),
|
||||
}
|
||||
rx
|
||||
}
|
||||
@@ -95,13 +97,13 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
|
||||
/// requests go to `tx`, where the chat's gate reads them; one may wait long for the user's Allow.
|
||||
pub fn listen_agent(tx: Sender<ControlRequest>, ctx: egui::Context) -> Option<u16> {
|
||||
let Some(token) = new_token() else {
|
||||
eprintln!("solidcraft: agent chat endpoint not started: no random token available");
|
||||
solidcraft_engine::note!("solidcraft: agent chat endpoint not started: no random token available");
|
||||
return None;
|
||||
};
|
||||
let listener = match TcpListener::bind(("127.0.0.1", 0)) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}");
|
||||
solidcraft_engine::note!("solidcraft: control server failed to bind 127.0.0.1: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
@@ -115,19 +117,21 @@ fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRe
|
||||
match control_auth::token_path(port) {
|
||||
Some(path) => match control_auth::write_token_file(&path, &token) {
|
||||
Ok(()) => {
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
|
||||
solidcraft_engine::note!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
|
||||
if let Ok(mut files) = TOKEN_FILES.lock() {
|
||||
files.push((path, token.clone()));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!(
|
||||
Err(e) => solidcraft_engine::note!(
|
||||
"solidcraft: control server listening on 127.0.0.1:{port}, but its token file {} could not be written ({e}); \
|
||||
clients need SOLIDCRAFT_CONTROL_TOKEN",
|
||||
path.display()
|
||||
),
|
||||
},
|
||||
None => {
|
||||
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}; no data folder for its token, clients need SOLIDCRAFT_CONTROL_TOKEN")
|
||||
solidcraft_engine::note!(
|
||||
"solidcraft: control server listening on 127.0.0.1:{port}; no data folder for its token, clients need SOLIDCRAFT_CONTROL_TOKEN"
|
||||
)
|
||||
}
|
||||
}
|
||||
let token = std::sync::Arc::new(token);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Is the file chooser that opens the desktop's own? (#10159)
|
||||
//!
|
||||
//! `rfd` opens the file chooser through the XDG desktop portal; without the portal it falls
|
||||
//! back to zenity, a window that does not belong to the app and that the app never mentions.
|
||||
//! The desktop's own chooser is the portal one, so the app has to know when it is not the one
|
||||
//! that opens, and say so in the session log.
|
||||
//!
|
||||
//! The session bus is asked once, on a thread of its own: a bus that answers late must not
|
||||
//! hold the window. What it answered is kept, and read when a file is opened.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
|
||||
/// The portal's bus name: the desktop's own file chooser lives behind it.
|
||||
const PORTAL: &str = "org.freedesktop.portal.Desktop";
|
||||
|
||||
/// The bus has not answered yet.
|
||||
const UNKNOWN: u8 = 0;
|
||||
/// There is no session bus to reach the portal on.
|
||||
const NO_SESSION_BUS: u8 = 1;
|
||||
/// The bus is there but the portal does not answer on it.
|
||||
const NO_PORTAL_SERVICE: u8 = 2;
|
||||
/// The bus is there and the portal answers on it.
|
||||
const PORTAL_THERE: u8 = 3;
|
||||
|
||||
/// What the session bus said about the portal.
|
||||
static BUS: AtomicU8 = AtomicU8::new(UNKNOWN);
|
||||
/// A file chooser is opening: whether it is the desktop's own is to be said.
|
||||
static OPENING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Ask the session bus, on a thread of its own, whether the portal is there.
|
||||
pub fn start(ctx: &egui::Context) {
|
||||
let thread_ctx = ctx.clone();
|
||||
let probe = move || {
|
||||
let answer = match session_bus() {
|
||||
None => NO_SESSION_BUS,
|
||||
Some(connection) => match portal_on(&connection) {
|
||||
true => PORTAL_THERE,
|
||||
false => NO_PORTAL_SERVICE,
|
||||
},
|
||||
};
|
||||
BUS.store(answer, Ordering::Relaxed);
|
||||
// The answer is in: a file chooser waiting for it can be told now.
|
||||
thread_ctx.request_repaint();
|
||||
};
|
||||
if let Err(e) = std::thread::Builder::new().name("desktop-chooser".into()).spawn(probe) {
|
||||
log::warn!("file chooser: no thread to ask the session bus: {e}");
|
||||
// Nothing will answer: the desktop's own chooser cannot be told apart from the start.
|
||||
BUS.store(PORTAL_THERE, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// The session bus, when there is one to talk to.
|
||||
fn session_bus() -> Option<zbus::blocking::Connection> {
|
||||
zbus::blocking::Connection::session().ok()
|
||||
}
|
||||
|
||||
/// Whether the portal answers on the bus: it is either running on it or waiting to be started.
|
||||
fn portal_on(connection: &zbus::blocking::Connection) -> bool {
|
||||
let Ok(bus) = zbus::blocking::fdo::DBusProxy::new(connection) else { return false };
|
||||
let named = |names: Vec<zbus::names::OwnedBusName>| names.iter().any(|name| name.as_str() == PORTAL);
|
||||
// A portal that is running is on the bus; one that is not running yet is still there when
|
||||
// the bus can start it, which is what the portal's service file is for.
|
||||
match (bus.list_names().map(named), bus.list_activatable_names().map(named)) {
|
||||
(Ok(running), Ok(startable)) => running || startable,
|
||||
(Ok(running), Err(_)) => running,
|
||||
(Err(_), Ok(startable)) => startable,
|
||||
// The bus does not answer: nothing answers on it.
|
||||
(Err(_), Err(_)) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// A file chooser is opening: ask for a frame, so that the reason it is not the desktop's own
|
||||
/// is said (#10159).
|
||||
pub fn note_opening(ctx: &egui::Context) {
|
||||
OPENING.store(true, Ordering::Relaxed);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// The reason the file chooser that is opening is not the desktop's own, if it is not.
|
||||
///
|
||||
/// The decision is the one the test states: without a session bus there is no bus to reach the
|
||||
/// portal on; with a bus but no portal on it, `rfd` falls back to zenity; and under XWayland the
|
||||
/// window is an X11 one while the session is a Wayland one, which the portal refuses to parent
|
||||
/// its chooser to.
|
||||
pub fn take_reason() -> Option<&'static str> {
|
||||
if !OPENING.load(Ordering::Relaxed) {
|
||||
return None;
|
||||
}
|
||||
let (dbus_session, portal_service) = match BUS.load(Ordering::Relaxed) {
|
||||
NO_SESSION_BUS => (false, false),
|
||||
NO_PORTAL_SERVICE => (true, false),
|
||||
PORTAL_THERE => (true, true),
|
||||
// The bus has not answered yet: wait for it, and say nothing yet.
|
||||
_ => return None,
|
||||
};
|
||||
OPENING.store(false, Ordering::Relaxed);
|
||||
let wayland_display = std::env::var_os("WAYLAND_DISPLAY").is_some();
|
||||
let x11_display = super::window_system_now() == Some("x11");
|
||||
super::desktop_chooser_fallback(dbus_session, portal_service, wayland_display, x11_display)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//! Cached accessibility signal, observed only while a theme offers blur. No UI-thread IO.
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU8, Ordering},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Reading {
|
||||
needed: AtomicBool,
|
||||
started: AtomicBool,
|
||||
value: AtomicU8,
|
||||
}
|
||||
|
||||
pub fn cached(ctx: &egui::Context) -> Box<dyn Fn(bool) -> Option<bool>> {
|
||||
let state = Arc::new(Reading::default());
|
||||
let ctx = ctx.clone();
|
||||
Box::new(move |needed| {
|
||||
state.needed.store(needed, Ordering::Relaxed);
|
||||
if needed && !state.started.swap(true, Ordering::Relaxed) {
|
||||
let weak = Arc::downgrade(&state);
|
||||
let ctx = ctx.clone();
|
||||
if let Err(error) = std::thread::Builder::new().name("desktop-transparency".into()).spawn(move || {
|
||||
while let Some(state) = weak.upgrade() {
|
||||
if state.needed.load(Ordering::Relaxed) {
|
||||
let code = match read() {
|
||||
Some(true) => 1,
|
||||
Some(false) => 2,
|
||||
None => 0,
|
||||
};
|
||||
if state.value.swap(code, Ordering::Relaxed) != code {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
}
|
||||
}) {
|
||||
log::warn!("desktop transparency: could not observe accessibility setting: {error}");
|
||||
}
|
||||
}
|
||||
match state.value.load(Ordering::Relaxed) {
|
||||
1 => Some(true),
|
||||
2 => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn read() -> Option<bool> {
|
||||
let key =
|
||||
winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER).open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize").ok()?;
|
||||
match key.get_value::<u32, _>("EnableTransparency").ok()? {
|
||||
0 => Some(true),
|
||||
1 => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read() -> Option<bool> {
|
||||
objc2::rc::autoreleasepool(|_| Some(objc2_app_kit::NSWorkspace::sharedWorkspace().accessibilityDisplayShouldReduceTransparency()))
|
||||
}
|
||||
@@ -112,7 +112,7 @@ pub fn restart(software: bool, lost_at: u64, reason: &str) {
|
||||
pub fn end() -> ! {
|
||||
let watchdog = std::thread::Builder::new().name("solidcraft-exit".into()).spawn(|| {
|
||||
std::thread::sleep(END_GRACE);
|
||||
eprintln!("SolidCraft graphics: still closing after the device was lost; aborting");
|
||||
solidcraft_engine::note!("SolidCraft graphics: still closing after the device was lost; aborting");
|
||||
std::process::abort();
|
||||
});
|
||||
if let Err(e) = watchdog {
|
||||
|
||||
@@ -23,7 +23,11 @@ mod app_icon;
|
||||
mod come_forward;
|
||||
mod control_server;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod desktop_chooser;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod desktop_theme;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
mod desktop_transparency;
|
||||
mod device_lost;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_crash;
|
||||
@@ -75,7 +79,7 @@ fn mark(step: &str) {
|
||||
static START: std::sync::OnceLock<(std::time::Instant, bool)> = std::sync::OnceLock::new();
|
||||
let (start, on) = START.get_or_init(|| (std::time::Instant::now(), solidcraft_engine::geom::env::var_os("PROFILE_STARTUP").is_some()));
|
||||
if *on {
|
||||
eprintln!("startup {:>6.0} ms {step}", start.elapsed().as_secs_f64() * 1000.0);
|
||||
solidcraft_engine::note!("startup {:>6.0} ms {step}", start.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +244,12 @@ impl eframe::App for App {
|
||||
menu.tick(&mut self.solve, ctx);
|
||||
}
|
||||
self.solve.logic(ctx);
|
||||
// The file chooser that opened is not the desktop's own: the user has to be told (#10159).
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(reason) = desktop_chooser::take_reason() {
|
||||
let text = self.solve.tr(reason).to_string();
|
||||
self.solve.set_status(text, false);
|
||||
}
|
||||
self.dialog_folder.design(self.solve.session.path.as_deref());
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
if !self.solve.file_picker_busy() {
|
||||
@@ -293,6 +303,8 @@ impl eframe::App for App {
|
||||
log::warn!("a picture was still being drawn as the app closed");
|
||||
}
|
||||
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
// A GPU path tracer setup still running did not stop the app (#9236).
|
||||
solidcraft_ui_egui::render_workspace::close(&mut self.solve);
|
||||
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
|
||||
// Before a restart (`Drop for App`) starts the next instance, which would otherwise get
|
||||
// nothing from the 3D mouse driver (`solidcraft_3dx_client::stop`).
|
||||
@@ -356,7 +368,7 @@ impl App {
|
||||
let Some(gpu) = self.gpu.as_mut().filter(|g| g.lost_at.is_none() && g.lost.is_lost()) else { return };
|
||||
let reason = gpu.lost.reason().unwrap_or_default();
|
||||
log::error!("graphics: the device was lost ({reason}); closing to start again");
|
||||
eprintln!("SolidCraft graphics: the device was lost ({reason}); starting again");
|
||||
solidcraft_engine::note!("SolidCraft graphics: the device was lost ({reason}); starting again");
|
||||
solidcraft_engine::crash::phase(&format!("graphics device lost: {reason}"));
|
||||
gpu.lost_at = Some(device_lost::now_secs());
|
||||
gpu.reason = reason;
|
||||
@@ -469,12 +481,19 @@ fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom:
|
||||
|
||||
fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services {
|
||||
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
|
||||
// The file chooser that opens may not be the desktop's own: the user is told when it does
|
||||
// not (#10159).
|
||||
#[cfg(target_os = "linux")]
|
||||
let chooser_ctx = cc.egui_ctx.clone();
|
||||
let open_parent = parent.clone();
|
||||
let program_parent = parent.clone();
|
||||
let file_parent = parent.clone();
|
||||
let (open_folder, file_folder, save_folder) = (folder.clone(), folder.clone(), folder.clone());
|
||||
Services {
|
||||
pick_open: Some(Box::new(move |first: &[&str]| {
|
||||
// The chooser that opens may not be the desktop's own (#10159).
|
||||
#[cfg(target_os = "linux")]
|
||||
desktop_chooser::note_opening(&chooser_ctx);
|
||||
// The file type the command reads comes first, so the picker opens on it (#1801).
|
||||
let mut dialog = open_folder.start(open_parent.clone());
|
||||
if !first.is_empty() {
|
||||
@@ -539,6 +558,10 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
|
||||
desktop_dark: Some(desktop_theme::start(&cc.egui_ctx)),
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
desktop_dark: None,
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
reduce_transparency: Some(desktop_transparency::cached(&cc.egui_ctx)),
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
reduce_transparency: None,
|
||||
http_get: Some(std::sync::Arc::new(http_get)),
|
||||
http_download: Some(std::sync::Arc::new(http_download)),
|
||||
// Already an installed app: only the browser build offers to install.
|
||||
@@ -779,6 +802,28 @@ fn graphics() -> Option<serde_json::Value> {
|
||||
graphics_path().and_then(|p| std::fs::read_to_string(p).ok()).and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||
}
|
||||
|
||||
fn restore_preferences(app: &mut SolveApp, stored_ui: Option<&str>, saved_graphics: Option<&serde_json::Value>, safe_mode: bool) {
|
||||
if let Some(p) = stored_ui.filter(|_| !safe_mode) {
|
||||
app.load_prefs(p);
|
||||
}
|
||||
// Even safe mode edits the real graphics.json. Keep its choices in Preferences so an
|
||||
// anti-aliasing change cannot erase them; safe mode's renderer still uses its own defaults.
|
||||
restore_graphics_preferences(app, saved_graphics);
|
||||
}
|
||||
|
||||
/// Restore startup choices after loading the separately saved UI preferences.
|
||||
fn restore_graphics_preferences(app: &mut SolveApp, saved: Option<&serde_json::Value>) {
|
||||
app.preferences.graphics_adapter = saved.and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str)).unwrap_or_default().to_string();
|
||||
// graphics.json chooses the window before app.ron is loaded. Reflect that saved choice,
|
||||
// not the current session's window system (which an environment override may change).
|
||||
app.preferences.window_system = saved
|
||||
.and_then(|v| v.get("window_system").and_then(serde_json::Value::as_str))
|
||||
.map(str::trim)
|
||||
.filter(|choice| matches!(*choice, "wayland" | "x11"))
|
||||
.unwrap_or("auto")
|
||||
.to_string();
|
||||
}
|
||||
|
||||
/// The graphics backend preference (`graphics.json`'s `backend`: `auto`, `dx12`, `vulkan`, `gl`).
|
||||
fn backend_preference() -> Option<String> {
|
||||
graphics().and_then(|v| v.get("backend").and_then(serde_json::Value::as_str).map(str::to_string))
|
||||
@@ -948,7 +993,7 @@ fn main() -> eframe::Result {
|
||||
if let Some(value) = max_file_size {
|
||||
let applied = startup::max_file_size(value.as_deref()).and_then(|bytes| solidcraft_engine::io::limits::set_override(Some(bytes)));
|
||||
if let Err(error) = applied {
|
||||
eprintln!("SolidCraft: {error}");
|
||||
solidcraft_engine::note!("SolidCraft: {error}");
|
||||
return Err(eframe::Error::AppCreation(std::io::Error::other(error).into()));
|
||||
}
|
||||
}
|
||||
@@ -980,7 +1025,7 @@ fn main() -> eframe::Result {
|
||||
let fallback_note = std::env::var(startup::FALLBACK_NOTE_ENV).ok().map(|s| s.chars().take(4096).collect::<String>());
|
||||
if let Some(note) = &fallback_note {
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
solidcraft_engine::note!("SolidCraft: {note}");
|
||||
notes.push(note.clone());
|
||||
}
|
||||
let saved_samples = if safe_mode { None } else { msaa() };
|
||||
@@ -997,6 +1042,8 @@ fn main() -> eframe::Result {
|
||||
solidcraft_engine::crash::install(dir, build)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// The recovery folder also holds the reopen guard marker (#5064); read once, reused below.
|
||||
let recovery_dir = solidcraft_engine::recovery::default_dir();
|
||||
// A crash in native code (a graphics driver) says where it happened: the exception or signal
|
||||
// and the module on Windows and macOS, and on every system the startup step the app was in.
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -1079,6 +1126,10 @@ fn main() -> eframe::Result {
|
||||
solidcraft_ui_egui::fonts::load_system_fonts_in_background(&cc.egui_ctx);
|
||||
mark("fonts");
|
||||
let dialog_folder = DialogFolder::default();
|
||||
// The file chooser that opens may not be the desktop's own: the session bus is asked
|
||||
// now, so the answer is there when a file is opened (#10159).
|
||||
#[cfg(target_os = "linux")]
|
||||
desktop_chooser::start(&cc.egui_ctx);
|
||||
let mut app = SolveApp::new(Session::default(), services(cc, &dialog_folder));
|
||||
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
|
||||
app.custom_titlebar = custom_titlebar;
|
||||
@@ -1087,21 +1138,20 @@ fn main() -> eframe::Result {
|
||||
// Windows: the taskbar button shows the mark even when slow first frames keep the
|
||||
// taskbar waiting for the window's icon.
|
||||
if !native_frame::set_class_icon(cc) {
|
||||
eprintln!("SolidCraft window: no class icon; the taskbar may show a generic one");
|
||||
solidcraft_engine::note!("SolidCraft window: no class icon; the taskbar may show a generic one");
|
||||
}
|
||||
// The OS hit test offers the resize border, at the window's DPI, from the first
|
||||
// message (#1381).
|
||||
app.native_resize_border = custom_titlebar && native_frame::install(cc);
|
||||
if custom_titlebar && !app.native_resize_border {
|
||||
eprintln!("SolidCraft window: no native resize border; egui edge zones instead");
|
||||
solidcraft_engine::note!("SolidCraft window: no native resize border; egui edge zones instead");
|
||||
}
|
||||
}
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.cpu_meshing = safe_mode;
|
||||
app.graphics_fallback = fallback_note.clone();
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
let stored_ui = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k)));
|
||||
restore_preferences(&mut app, stored_ui.as_deref(), graphics().as_ref(), safe_mode);
|
||||
if let Some(path) = &theme_file
|
||||
&& let Err(e) = solidcraft_ui_egui::prefs::follow_theme_file_for_run(&mut app, path)
|
||||
{
|
||||
@@ -1114,10 +1164,6 @@ fn main() -> eframe::Result {
|
||||
lost_at: None,
|
||||
reason: String::new(),
|
||||
});
|
||||
if !safe_mode {
|
||||
app.preferences.graphics_adapter =
|
||||
graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string)).unwrap_or_default();
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
app.services.graphics_adapters = rs
|
||||
.available_adapters
|
||||
@@ -1129,9 +1175,10 @@ fn main() -> eframe::Result {
|
||||
.collect();
|
||||
let info = rs.adapter.get_info();
|
||||
if !app.preferences.graphics_adapter.is_empty() && !info.name.eq_ignore_ascii_case(app.preferences.graphics_adapter.as_str()) {
|
||||
eprintln!(
|
||||
solidcraft_engine::note!(
|
||||
"SolidCraft graphics: preferred adapter {:?} was not used; using {:?} instead",
|
||||
app.preferences.graphics_adapter, info.name
|
||||
app.preferences.graphics_adapter,
|
||||
info.name
|
||||
);
|
||||
}
|
||||
let samples = scene_msaa_request(saved_samples);
|
||||
@@ -1139,7 +1186,7 @@ fn main() -> eframe::Result {
|
||||
eframe::egui_wgpu::supported_msaa_samples(&rs.adapter, &rs.device, rs.target_format, depth)
|
||||
});
|
||||
let scene_samples = scene_samples(info.backend, u32::from(samples), &supported);
|
||||
eprintln!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
|
||||
solidcraft_engine::note!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
|
||||
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
|
||||
let scene_samples = app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
|
||||
app.msaa_supported = supported;
|
||||
@@ -1190,8 +1237,25 @@ fn main() -> eframe::Result {
|
||||
};
|
||||
// No design given: the designs open at the last quit, when the preference asks (#5542).
|
||||
// Not when driven over the control channel, whose clients expect a fresh design.
|
||||
let files =
|
||||
if files.is_empty() && !sample && control_port.is_none() { solidcraft_ui_egui::documents::designs_to_reopen(&app) } else { files };
|
||||
// A design that stopped the last session while it was being reopened is skipped this
|
||||
// time, so one bad file cannot brick every launch (#5064).
|
||||
app.reopen_guard_dir = recovery_dir.clone();
|
||||
let files = if files.is_empty() && !sample && control_port.is_none() {
|
||||
let (reopen, skipped) = solidcraft_ui_egui::documents::reopen_plan(&app, recovery_dir.as_deref());
|
||||
if !skipped.is_empty() {
|
||||
let names: Vec<String> = skipped
|
||||
.iter()
|
||||
.map(|p| std::path::Path::new(p).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| p.clone()))
|
||||
.collect();
|
||||
notes.push(format!(
|
||||
"The last session stopped while reopening {}. It was not reopened this time; open it yourself if you need it.",
|
||||
names.join(", ")
|
||||
));
|
||||
}
|
||||
reopen
|
||||
} else {
|
||||
files
|
||||
};
|
||||
for f in &files {
|
||||
app.open_path_async(f);
|
||||
}
|
||||
@@ -1207,9 +1271,9 @@ fn main() -> eframe::Result {
|
||||
app.home.show(rev);
|
||||
}
|
||||
// Autosave, and offer what crashed sessions left behind.
|
||||
if let Some(dir) = solidcraft_engine::recovery::default_dir() {
|
||||
if let Some(dir) = recovery_dir.as_ref() {
|
||||
let every = if app.autosave_minutes > 0.0 { app.autosave_minutes * 60.0 } else { f64::MAX / 4.0 };
|
||||
solidcraft_ui_egui::recovery_ui::start(&mut app, &dir, std::time::Duration::from_secs_f64(every.min(1e9)));
|
||||
solidcraft_ui_egui::recovery_ui::start(&mut app, dir, std::time::Duration::from_secs_f64(every.min(1e9)));
|
||||
// Safe mode keeps autosaving but offers nothing: the entries wait for a normal start.
|
||||
if safe_mode {
|
||||
app.recoverable.clear();
|
||||
@@ -1283,7 +1347,7 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
let retry = std::env::var(startup::RETRY_BACKEND_ENV).ok();
|
||||
let preference = if cpu { preference } else { retry.as_deref().or(preference) };
|
||||
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::has_hardware_adapter) {
|
||||
eprintln!("SolidCraft graphics: starting {backends:?}");
|
||||
solidcraft_engine::note!("SolidCraft graphics: starting {backends:?}");
|
||||
setup.instance_descriptor.backends = backends;
|
||||
}
|
||||
let intel_mac = cfg!(all(target_os = "macos", target_arch = "x86_64"));
|
||||
@@ -1309,7 +1373,7 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
/// event loop or a failed device; each retry advances through the APIs, then software once.
|
||||
fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backends, settings: Option<&std::path::Path>) -> eframe::Result {
|
||||
log::error!("the window could not open: {error}");
|
||||
eprintln!("SolidCraft: the window could not open: {error}");
|
||||
solidcraft_engine::note!("SolidCraft: the window could not open: {error}");
|
||||
// This failure was handled, so neither the marker nor the journal should call it a crash.
|
||||
if let Some(dir) = settings {
|
||||
startup::started(dir);
|
||||
@@ -1322,7 +1386,7 @@ fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backen
|
||||
let previous = std::env::var(startup::FALLBACK_NOTE_ENV).unwrap_or_default();
|
||||
let note = startup::fallback_note(&previous, failed, next, &error.to_string());
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
solidcraft_engine::note!("SolidCraft: {note}");
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(std::env::args_os().skip(1)).env(startup::FALLBACK_NOTE_ENV, note);
|
||||
if next == "cpu" {
|
||||
@@ -1397,6 +1461,47 @@ fn prefer_x11(display: bool, wayland_display: bool, desktop: Option<&str>, wayla
|
||||
display && !(niri && wayland_display)
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
const CHOOSER_NO_SESSION_BUS: &str = "file.open.portal_fallback.no_session_bus";
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
const CHOOSER_NO_PORTAL_SERVICE: &str = "file.open.portal_fallback.no_portal_service";
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
const CHOOSER_XWAYLAND: &str = "file.open.portal_fallback.xwayland";
|
||||
|
||||
/// Whether the chooser that opens is NOT the desktop's own, and why.
|
||||
///
|
||||
/// `rfd` opens the file chooser through the XDG desktop portal; when the portal is not there it
|
||||
/// falls back to zenity, which opens a window that does not belong to SolidCraft and that the
|
||||
/// application never mentions. The application has to know that: this decision is what the warning
|
||||
/// is built on.
|
||||
///
|
||||
/// * `dbus_session`: the session bus is there (`DBUS_SESSION_BUS_ADDRESS`).
|
||||
/// * `portal_service`: the file chooser portal answers on it.
|
||||
/// * `wayland_display`: a Wayland session runs (`WAYLAND_DISPLAY`).
|
||||
/// * `x11_display`: the window is an X11 one (`DISPLAY`, or XWayland).
|
||||
///
|
||||
/// `None` means the desktop's own chooser is the one that opens and there is nothing to say;
|
||||
/// `Some` carries the reason to report.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
fn desktop_chooser_fallback(dbus_session: bool, portal_service: bool, wayland_display: bool, x11_display: bool) -> Option<&'static str> {
|
||||
// Without a session bus there is no bus to reach the portal on, so the chooser is not the
|
||||
// desktop's own whatever else is there.
|
||||
if !dbus_session {
|
||||
return Some(CHOOSER_NO_SESSION_BUS);
|
||||
}
|
||||
// The bus is there but the portal does not answer on it: `rfd` falls back to zenity.
|
||||
if !portal_service {
|
||||
return Some(CHOOSER_NO_PORTAL_SERVICE);
|
||||
}
|
||||
// Under XWayland the window is an X11 one while the session is a Wayland one, and the portal
|
||||
// refuses to parent its chooser to an X11 window: `rfd` falls back to zenity.
|
||||
if wayland_display && x11_display {
|
||||
return Some(CHOOSER_XWAYLAND);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix, not(target_os = "macos")))]
|
||||
mod backend_tests {
|
||||
use super::prefer_x11;
|
||||
@@ -1432,6 +1537,30 @@ mod backend_tests {
|
||||
assert!(prefer_x11(true, true, Some("niri"), over(Some("x11"))));
|
||||
}
|
||||
|
||||
/// The file chooser must say when it is NOT the desktop's own: `rfd` falls back to zenity
|
||||
/// when the XDG portal is not there, and the user gets a foreign window with no explanation.
|
||||
#[test]
|
||||
fn the_file_picker_says_when_it_is_not_the_desktops_own_chooser() {
|
||||
use super::desktop_chooser_fallback;
|
||||
// The normal case: the portal is there, the desktop's own chooser opens, nothing to say.
|
||||
assert_eq!(desktop_chooser_fallback(true, true, true, false), None);
|
||||
// No session bus: the portal can not be reached at all, so `rfd` falls back to zenity.
|
||||
assert!(
|
||||
desktop_chooser_fallback(false, true, true, false).is_some(),
|
||||
"without a D-Bus session bus the file chooser is not the desktop's own: the user has to be told"
|
||||
);
|
||||
// The bus is there but the portal service is not: same fallback, same warning.
|
||||
assert!(
|
||||
desktop_chooser_fallback(true, false, true, false).is_some(),
|
||||
"without the desktop portal service the file chooser is not the desktop's own: the user has to be told"
|
||||
);
|
||||
// XWayland: the parent window is an X11 one and the portal refuses it, so `rfd` falls back.
|
||||
assert!(
|
||||
desktop_chooser_fallback(true, true, true, true).is_some(),
|
||||
"under XWayland the portal rejects the parent window: the user has to be told"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_desktops_title_bar_only_when_asked_for() {
|
||||
use super::titlebar_choice;
|
||||
@@ -1709,6 +1838,88 @@ mod viewport_tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod msaa_tests {
|
||||
/// Safe mode uses default UI settings but edits the real graphics.json for the next start.
|
||||
#[test]
|
||||
fn safe_mode_preserves_graphics_choices_when_changing_antialiasing() {
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_ui_egui::scenario::Harness;
|
||||
|
||||
for choice in ["wayland", "x11"] {
|
||||
let mut h = Harness::new();
|
||||
let path = h.dir.join("graphics.json");
|
||||
let saved = json!({"window_system": choice, "adapter": "Selected GPU", "backend": "vulkan", "msaa": 8});
|
||||
std::fs::write(&path, saved.to_string()).unwrap();
|
||||
h.app.services.graphics_path = Some(path.to_string_lossy().into_owned());
|
||||
let defaults = h.app.preferences.clone();
|
||||
let stored_ui = json!({"preferences": {"window_system": "auto", "graphics_adapter": "Stale GPU", "msaa": 2,
|
||||
"display_quality": "fine"}})
|
||||
.to_string();
|
||||
super::restore_preferences(&mut h.app, Some(&stored_ui), Some(&saved), true);
|
||||
|
||||
assert_eq!(h.app.preferences.window_system, choice);
|
||||
assert_eq!(h.app.preferences.graphics_adapter, "Selected GPU");
|
||||
assert_eq!(h.app.preferences.msaa, defaults.msaa, "safe mode ignores saved anti-aliasing");
|
||||
assert_eq!(h.app.preferences.display_quality, defaults.display_quality, "safe mode ignores saved UI settings");
|
||||
assert_eq!(serde_json::from_str::<Value>(&std::fs::read_to_string(&path).unwrap()).unwrap(), saved, "startup does not write settings");
|
||||
|
||||
assert_eq!(h.call("ui.prefs", json!({"open": "Graphics"}))["ok"], true);
|
||||
assert_eq!(h.call("ui.prefs", json!({"msaa": 1}))["ok"], true);
|
||||
assert_eq!(h.call("ui.prefs", json!({"button": "ok"}))["ok"], true);
|
||||
let after: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(after, json!({"window_system": choice, "adapter": "Selected GPU", "backend": "vulkan", "msaa": 1}));
|
||||
|
||||
let mut normal = Harness::new();
|
||||
super::restore_preferences(&mut normal.app, Some(&stored_ui), Some(&after), false);
|
||||
assert_eq!(normal.app.preferences.window_system, choice);
|
||||
assert_eq!(normal.app.preferences.graphics_adapter, "Selected GPU");
|
||||
assert_eq!(normal.app.preferences.display_quality, "fine", "normal startup still restores UI settings");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_invalid_startup_window_system_uses_automatic() {
|
||||
use serde_json::json;
|
||||
|
||||
for saved in [None, Some(json!(null)), Some(json!({})), Some(json!({"window_system": 7})), Some(json!({"window_system": "unknown"}))] {
|
||||
let mut app = super::SolveApp::new(super::Session::default(), super::Services::default());
|
||||
app.preferences.window_system = "wayland".into();
|
||||
super::restore_graphics_preferences(&mut app, saved.as_ref());
|
||||
assert_eq!(app.preferences.window_system, "auto", "{saved:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A manual graphics.json choice wins over stale app.ron preferences and survives an
|
||||
/// unrelated graphics edit (#10116), including on a fresh launch with no saved UI state.
|
||||
#[test]
|
||||
fn startup_window_system_survives_changing_antialiasing() {
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_ui_egui::scenario::Harness;
|
||||
|
||||
for choice in ["wayland", "x11", "auto", " wayland ", " x11 "] {
|
||||
for stale in [None, Some("auto"), Some("wayland"), Some("x11")] {
|
||||
let mut h = Harness::new();
|
||||
if let Some(stale) = stale {
|
||||
h.app.load_prefs(&json!({"preferences": {"window_system": stale}}).to_string());
|
||||
}
|
||||
let path = h.dir.join("graphics.json");
|
||||
std::fs::write(&path, json!({"window_system": choice, "backend": "vulkan", "adapter": "Selected GPU", "msaa": 4}).to_string())
|
||||
.unwrap();
|
||||
h.app.services.graphics_path = Some(path.to_string_lossy().into_owned());
|
||||
let saved: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
super::restore_graphics_preferences(&mut h.app, Some(&saved));
|
||||
|
||||
let result = h.call("ui.prefs", json!({"msaa": 2}));
|
||||
assert_eq!(result["ok"], true, "{result}");
|
||||
let after: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(after["window_system"], choice.trim(), "saved choice {choice:?}, stale UI choice {stale:?}");
|
||||
assert_eq!(result["result"]["window_system"], choice.trim());
|
||||
assert_eq!(after["msaa"], 2);
|
||||
assert_eq!(after["backend"], "vulkan");
|
||||
assert_eq!(after["adapter"], "Selected GPU");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_launch_adopts_the_previous_release_settings() {
|
||||
let d = std::env::temp_dir().join(format!("solidcraft-adopt-settings-{}", std::process::id()));
|
||||
|
||||
@@ -40,10 +40,10 @@ fn connect_to(path: &Path) -> Option<UnixStream> {
|
||||
|
||||
/// The axes of one record, or `None` for a record that is not motion.
|
||||
///
|
||||
/// The daemon's frame is right-handed with x to the right, y away from the user and z up; ours is
|
||||
/// the device's raw one, x right, y towards the user and z down, so y and z (and the rotations
|
||||
/// about them) change sign. With nothing set in `spnavcfg` the two give the same motion; what
|
||||
/// the user swaps or inverts there then carries over.
|
||||
/// The daemon reports x to the right, y up and z away from the user; our raw HID frame is x
|
||||
/// right, y towards the user and z down. Swap and negate y/z and ry/rz to match that frame.
|
||||
/// With nothing set in `spnavcfg` the two readers give the same motion; what the user swaps
|
||||
/// or inverts there then carries over.
|
||||
fn decode(record: &[u8; RECORD_BYTES]) -> Option<Axes> {
|
||||
let mut data = [0_i32; 8];
|
||||
for (slot, bytes) in data.iter_mut().zip(record.as_chunks::<4>().0) {
|
||||
@@ -51,8 +51,8 @@ fn decode(record: &[u8; RECORD_BYTES]) -> Option<Axes> {
|
||||
}
|
||||
let [kind, x, y, z, rx, ry, rz, _period] = data;
|
||||
(kind == 0).then(|| Axes {
|
||||
translation: [unit(x), unit(-i64::from(y)), unit(-i64::from(z))],
|
||||
rotation: [unit(rx), unit(-i64::from(ry)), unit(-i64::from(rz))],
|
||||
translation: [unit(x), unit(-i64::from(z)), unit(-i64::from(y))],
|
||||
rotation: [unit(rx), unit(-i64::from(rz)), unit(-i64::from(ry))],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,22 +149,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn motion_records_become_axes_in_the_devices_raw_frame() {
|
||||
// Full deflection of the daemon's x, y and z is the device's full scale, and y and z flip.
|
||||
assert_eq!(decode(&record([0, 500, 500, 500, 0, 0, 0, 8])), Some(Axes { translation: [350, -350, -350], rotation: [0; 3] }));
|
||||
// Rotations follow translations: right-handed in both frames, so ry and rz flip as y and z do.
|
||||
assert_eq!(decode(&record([0, 0, 0, 0, 250, 250, 250, 8])), Some(Axes { translation: [0; 3], rotation: [175, -175, -175] }));
|
||||
// Each daemon axis reaches only its matching raw HID axis, in both directions.
|
||||
for (daemon_axis, raw_axis, sign) in [(0, 0, 1), (1, 2, -1), (2, 1, -1), (3, 3, 1), (4, 5, -1), (5, 4, -1)] {
|
||||
for value in [-500, 500] {
|
||||
let mut data = [0; 8];
|
||||
data[daemon_axis + 1] = value;
|
||||
let mut expected = [0; 6];
|
||||
expected[raw_axis] = sign * (value * 350 / 500) as i16;
|
||||
assert_eq!(decode(&record(data)), Some(Axes::from(expected)), "daemon axis {daemon_axis}, value {value}");
|
||||
}
|
||||
}
|
||||
// Rest is a motion record of zeros.
|
||||
assert_eq!(decode(&record([0, 0, 0, 0, 0, 0, 0, 1200])), Some(Axes::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_spnavcfg_swaps_reaches_the_view() {
|
||||
// The daemon sends a push away as y. With Y and Z swapped in spnavcfg it sends the same push
|
||||
// as z, and the view gets a lift (raw z) instead of the zoom (raw y).
|
||||
let before = decode(&record([0, 0, 500, 0, 0, 0, 0, 8])).map(|axes| axes.translation);
|
||||
let after = decode(&record([0, 0, 0, 500, 0, 0, 0, 8])).map(|axes| axes.translation);
|
||||
assert_eq!(before, Some([0, -350, 0]));
|
||||
assert_eq!(after, Some([0, 0, -350]));
|
||||
// A push away is daemon z (raw y); swapping Y/Z makes it a lift (raw z).
|
||||
// The same swap changes a sideways tilt (daemon rz, raw ry) into a twist (raw rz).
|
||||
let before = decode(&record([0, 0, 0, 500, 0, 0, 250, 8]));
|
||||
let after = decode(&record([0, 0, 500, 0, 0, 250, 0, 8]));
|
||||
assert_eq!(before, Some(Axes { translation: [0, -350, 0], rotation: [0, -175, 0] }));
|
||||
assert_eq!(after, Some(Axes { translation: [0, 0, -350], rotation: [0, 0, -175] }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -179,7 +185,7 @@ mod tests {
|
||||
// Nothing overflows on the extremes of `i32`.
|
||||
let extreme = decode(&record([0, i32::MIN, i32::MIN, i32::MAX, i32::MAX, i32::MIN, i32::MIN, -1]));
|
||||
assert_eq!(extreme.map(|axes| axes.translation[0]), Some(i16::MIN));
|
||||
assert_eq!(extreme.map(|axes| axes.translation[1]), Some(i16::MAX));
|
||||
assert_eq!(extreme.map(|axes| axes.translation), Some([i16::MIN, i16::MIN, i16::MAX]));
|
||||
assert_eq!(extreme.map(|axes| axes.rotation[0]), Some(i16::MAX));
|
||||
}
|
||||
|
||||
|
||||
@@ -311,13 +311,18 @@ pub const SYSTEM_VERSION_COMPAT_ENV: &str = "SYSTEM_VERSION_COMPAT";
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const OCLP_NVRAM_KEY: &str = "4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102:OCLP-Version";
|
||||
|
||||
/// Where macOS keeps its `nvram` tool.
|
||||
#[cfg(target_os = "macos")]
|
||||
const NVRAM_TOOL: &str = "/usr/sbin/nvram";
|
||||
|
||||
/// `true` if the OCLP NVRAM key is present on this system.
|
||||
///
|
||||
/// Runs `nvram <key>` and treats exit 0 as present. Any error (missing binary,
|
||||
/// permission denied, key absent) returns `false` — this is a best-effort probe.
|
||||
/// Runs `/usr/sbin/nvram <key>` and treats exit 0 as present. The full path keeps a `PATH` that
|
||||
/// lacks `/usr/sbin`, or lists a look-alike first, from changing the answer. Any error (missing
|
||||
/// binary, permission denied, key absent) returns `false`: this is a best-effort probe.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn oclp_nvram_present() -> bool {
|
||||
std::process::Command::new("nvram").arg(OCLP_NVRAM_KEY).output().map(|o| o.status.success()).unwrap_or(false)
|
||||
std::process::Command::new(NVRAM_TOOL).arg(OCLP_NVRAM_KEY).output().map(|o| o.status.success()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether to re-launch with `SYSTEM_VERSION_COMPAT=1`: it is not set yet and OCLP shows through
|
||||
@@ -334,8 +339,8 @@ fn compat_wanted(compat_set: bool, nvram: impl FnOnce() -> bool) -> bool {
|
||||
///
|
||||
/// `SYSTEM_VERSION_COMPAT=1` must be present **before** the process starts: setting it
|
||||
/// with `std::env::set_var` inside `main` is too late because the macOS frameworks have
|
||||
/// already queried the OS version by then. Re-launching is the only safe approach that
|
||||
/// works without `unsafe` (a true `execve` replacement would require it).
|
||||
/// already queried the OS version by then, so the program starts itself again with the variable
|
||||
/// set and waits for that child.
|
||||
///
|
||||
/// Returns `Ok(false)` immediately when:
|
||||
/// - `SYSTEM_VERSION_COMPAT` is already set (re-launched process, no loop), or
|
||||
@@ -352,7 +357,7 @@ pub fn relaunch_with_version_compat() -> Result<bool, String> {
|
||||
return Ok(false);
|
||||
}
|
||||
log::info!("startup: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
eprintln!("SolidCraft: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
solidcraft_engine::note!("SolidCraft: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
let exe = std::env::current_exe().map_err(|e| format!("could not find own executable: {e}"))?;
|
||||
let status = std::process::Command::new(&exe)
|
||||
.args(std::env::args_os().skip(1))
|
||||
|
||||
@@ -17,6 +17,7 @@ assets/
|
||||
ui/design/tools/ # authoring bases, checks and included review app
|
||||
ui/source/ # Rust viewport glyphs, markers and navigation
|
||||
branding/web/source/og.png # shared social preview card
|
||||
branding/logo/ # print/web logo files; guide in branding/README.md
|
||||
styles/ui/palettes.rs # light/dark defaults
|
||||
styles/web/loading.css # web loading/canvas styles
|
||||
appearances/library.rs # built-in visual presets
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# SolidCraft brand kit
|
||||
|
||||
Logo files for stickers, shirts, the web and social posts. Everything in `logo/` is drawn
|
||||
from the app icon (`assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg`),
|
||||
which is also the last frame of the loading animation (`assets/styles/web/loading-logo.svg`).
|
||||
The SVGs are plain paths: no scripts, fonts, animation or external links, so any printer can use them.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Use it for |
|
||||
|---|---|
|
||||
| `logo/solidcraft-mark.svg` | The mark in full colour. First choice for stickers, print and the web. |
|
||||
| `logo/solidcraft-mark-mono-black.svg` | One-colour print on light material: screen-printed shirts, stamps, engraving, vinyl. |
|
||||
| `logo/solidcraft-mark-mono-white.svg` | One-colour print on dark material, such as a white print on a dark shirt. |
|
||||
| `logo/solidcraft-mark-512.png`, `-1024.png`, `-2048.png` | Transparent square PNGs for places that don't take SVG: avatars, social posts, slides, print shops that ask for a bitmap (send the 2048). |
|
||||
| `logo/solidcraft-wordmark.svg` | The name on its own, outlined to paths. |
|
||||
| `logo/solidcraft-lockup-horizontal.svg` | Mark and name side by side: headers, banners, shirt fronts, wide stickers. |
|
||||
| `web/source/og.png` | The existing link preview card for the docs site. |
|
||||
|
||||
The gaps between the faces are part of the logo. In the colour and one-colour files they are
|
||||
transparent, so the material shows through. For a die-cut sticker, cut around the outer shape
|
||||
and let the backing colour (white is best) fill the gaps.
|
||||
|
||||
## Colours
|
||||
|
||||
The mark's five faces each use a two-stop gradient from the icon source:
|
||||
|
||||
| Face | From | To |
|
||||
|---|---|---|
|
||||
| Top | `#51adff` | `#2b83f1` |
|
||||
| Left | `#258df5` | `#1461da` |
|
||||
| Right | `#1b66cf` | `#124596` |
|
||||
| Lower step (top) | `#459fff` | `#2379e7` |
|
||||
| Upper step (inner side) | `#1d63c6` | `#15499e` |
|
||||
|
||||
Other colours, all already used in the project:
|
||||
|
||||
| Colour | Hex | Where it comes from |
|
||||
|---|---|---|
|
||||
| Wordmark blue | `#15499e` | loading logo `--word` |
|
||||
| Interface accent | `#2678da` | light theme `accent` token (`assets/styles/ui/palettes.rs`) |
|
||||
| Light background | `#e8ebf0` | web loading screen (`assets/styles/web/loading.css`) |
|
||||
| Dark background | `#1f232b` | dark theme `file_background` token |
|
||||
| Black / white | `#000000` / `#ffffff` | one-colour versions only |
|
||||
|
||||
If a printer can't do gradients, use the one-colour files, or fill every face with `#2678da`.
|
||||
|
||||
## Clear space and size
|
||||
|
||||
- Keep empty space around the mark of at least one fifth of its height on every side. Treat
|
||||
the lockup the same way, measured from the mark.
|
||||
- Smallest mark: 16 px tall on screen, 12 mm tall in print. Below that the gaps fill in.
|
||||
- Smallest lockup: 120 px wide on screen, 40 mm wide in print.
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Don't recolour it outside the colours above.
|
||||
- Don't stretch, squash, rotate, skew or crop it. Scale it evenly.
|
||||
- Don't add shadows, outlines, glows, bevels or other effects.
|
||||
- Don't fill or close the gaps between the faces, or move the faces apart.
|
||||
- Don't rearrange the lockup, retype the name in another font, or put the mark on a busy
|
||||
background where it loses contrast.
|
||||
|
||||
## Name and logo
|
||||
|
||||
The SolidCraft code and these files are licensed MIT OR Apache-2.0 (see the [README](../../README.md#licence)),
|
||||
so you may copy and change them. The name SolidCraft and this logo identify the SolidCraft
|
||||
project. Use them to refer to the project, for example in an article, a talk, or a sticker for a
|
||||
meetup. If you publish a fork or a modified build, give it its own name and logo so people
|
||||
don't mistake it for SolidCraft.
|
||||
|
||||
The wordmark is set in IBM Plex Sans Bold (SIL Open Font License 1.1, the interface font in
|
||||
`assets/fonts/ibm-plex-sans/`) and outlined, so you don't need the font to print it.
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 2748.71 904.436" width="2749" height="904" role="img"><title>SolidCraft</title><defs><linearGradient id="sc1" x1="619.14" y1="179.734" x2="694.687" y2="396.761" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d63c6"/><stop offset="1" stop-color="#15499e"/></linearGradient><linearGradient id="sc2" x1="619.14" y1="413.244" x2="790.839" y2="605.547" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#459fff"/><stop offset="1" stop-color="#2379e7"/></linearGradient><linearGradient id="sc3" x1="632.876" y1="399.508" x2="783.971" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1b66cf"/><stop offset="1" stop-color="#124596"/></linearGradient><linearGradient id="sc4" x1="207.063" y1="303.357" x2="426.837" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#258df5"/><stop offset="1" stop-color="#1461da"/></linearGradient><linearGradient id="sc5" x1="385.63" y1="53.364" x2="522.989" y2="510.769" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#51adff"/><stop offset="1" stop-color="#2b83f1"/></linearGradient></defs><g><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z" fill="url(#sc1)"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z" fill="url(#sc2)"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z" fill="url(#sc3)"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z" fill="url(#sc4)"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z" fill="url(#sc5)"/></g><path fill="#15499e" transform="translate(1092.78 632.20) scale(0.36281)" d="M303 12Q212 12 147.5 -20Q83 -52 38 -101L138 -202Q173 -162 217.5 -142Q262 -122 313 -122Q369 -122 396 -145Q423 -168 423 -206Q423 -225 416.5 -240.5Q410 -256 392.5 -266.5Q375 -277 342 -281L273 -290Q200 -300 152.5 -326.5Q105 -353 82 -396.5Q59 -440 59 -498Q59 -561 90.5 -608.5Q122 -656 180.5 -683Q239 -710 321 -710Q400 -710 460 -684.5Q520 -659 563 -612L462 -510Q437 -539 401.5 -557.5Q366 -576 312 -576Q261 -576 236 -558Q211 -540 211 -508Q211 -484 219.5 -469Q228 -454 246.5 -446Q265 -438 294 -433L363 -422Q435 -411 482 -385Q529 -359 552 -317Q575 -275 575 -215Q575 -148 543.5 -97Q512 -46 451 -17Q390 12 303 12ZM926 12Q849 12 794 -21.5Q739 -55 709.5 -117Q680 -179 680 -263Q680 -348 709.5 -409Q739 -470 794 -503.5Q849 -537 926 -537Q1003 -537 1058 -503.5Q1113 -470 1142.5 -409Q1172 -348 1172 -263Q1172 -179 1142.5 -117Q1113 -55 1058 -21.5Q1003 12 926 12ZM926 -105Q970 -105 994 -132.5Q1018 -160 1018 -209V-316Q1018 -366 994 -393Q970 -420 926 -420Q883 -420 858.5 -393Q834 -366 834 -316V-209Q834 -160 858.5 -132.5Q883 -105 926 -105ZM1511 0H1444Q1368 0 1332.5 -37Q1297 -74 1297 -144V-740H1445V-115H1511ZM1694 -581Q1649 -581 1628.5 -602Q1608 -623 1608 -654V-676Q1608 -708 1628.5 -728.5Q1649 -749 1694 -749Q1739 -749 1759.5 -728.5Q1780 -708 1780 -676V-654Q1780 -623 1759.5 -602Q1739 -581 1694 -581ZM1620 0V-525H1768V0ZM2248 0V-88H2243Q2232 -45 2192.5 -16.5Q2153 12 2102 12Q2036 12 1991 -19.5Q1946 -51 1923 -112.5Q1900 -174 1900 -263Q1900 -352 1923 -413Q1946 -474 1991 -505.5Q2036 -537 2102 -537Q2153 -537 2192.5 -509Q2232 -481 2243 -437H2248V-740H2396V0ZM2151 -108Q2179 -108 2201 -117Q2223 -126 2235.5 -143.5Q2248 -161 2248 -185V-340Q2248 -364 2235.5 -381.5Q2223 -399 2201 -408Q2179 -417 2151 -417Q2109 -417 2081.5 -387.5Q2054 -358 2054 -308V-217Q2054 -168 2081.5 -138Q2109 -108 2151 -108ZM2844 12Q2750 12 2680.5 -28Q2611 -68 2573 -147Q2535 -226 2535 -344Q2535 -462 2573 -543.5Q2611 -625 2680.5 -667.5Q2750 -710 2844 -710Q2940 -710 3003.5 -670Q3067 -630 3105 -546L2974 -478Q2961 -522 2930.5 -548.5Q2900 -575 2844 -575Q2777 -575 2736.5 -531.5Q2696 -488 2696 -405V-293Q2696 -211 2736.5 -167Q2777 -123 2844 -123Q2900 -123 2933.5 -153.5Q2967 -184 2983 -227L3107 -155Q3068 -76 3004 -32Q2940 12 2844 12ZM3373 0H3225V-525H3373V-411H3378Q3384 -440 3400 -466Q3416 -492 3443.5 -508.5Q3471 -525 3512 -525H3538V-387H3501Q3458 -387 3429.5 -379.5Q3401 -372 3387 -355Q3373 -338 3373 -307ZM4105 0H4023Q3996 0 3972.5 -13.5Q3949 -27 3935 -53.5Q3921 -80 3921 -117V-130L3953 -92H3917Q3905 -41 3864 -14.5Q3823 12 3763 12Q3684 12 3642 -30.5Q3600 -73 3600 -141Q3600 -197 3627.5 -233Q3655 -269 3705.5 -287Q3756 -305 3825 -305H3908V-338Q3908 -376 3888 -398.5Q3868 -421 3821 -421Q3777 -421 3751 -402Q3725 -383 3708 -359L3620 -437Q3652 -484 3701 -510.5Q3750 -537 3832 -537Q3943 -537 3999.5 -487.5Q4056 -438 4056 -345V-115H4105ZM3908 -221H3836Q3793 -221 3771 -206Q3749 -191 3749 -162V-147Q3749 -119 3767 -105Q3785 -91 3818 -91Q3843 -91 3863 -98Q3883 -105 3895.5 -120Q3908 -135 3908 -159ZM4250 0V-410H4176V-525H4250V-587Q4250 -663 4290.5 -701.5Q4331 -740 4407 -740H4503V-625H4398V-525H4503V-410H4398V0ZM4877 0H4789Q4713 0 4673 -39Q4633 -78 4633 -153V-410H4559V-525H4596Q4626 -525 4637 -539.5Q4648 -554 4648 -581V-667H4781V-525H4885V-410H4781V-115H4877Z"/></svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 791.188 904.436" width="791" height="904" role="img"><title>SolidCraft</title><g fill="#000000"><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 612 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 791.188 904.436" width="791" height="904" role="img"><title>SolidCraft</title><g fill="#ffffff"><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 612 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 791.188 904.436" width="791" height="904" role="img"><title>SolidCraft</title><defs><linearGradient id="sc1" x1="619.14" y1="179.734" x2="694.687" y2="396.761" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d63c6"/><stop offset="1" stop-color="#15499e"/></linearGradient><linearGradient id="sc2" x1="619.14" y1="413.244" x2="790.839" y2="605.547" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#459fff"/><stop offset="1" stop-color="#2379e7"/></linearGradient><linearGradient id="sc3" x1="632.876" y1="399.508" x2="783.971" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1b66cf"/><stop offset="1" stop-color="#124596"/></linearGradient><linearGradient id="sc4" x1="207.063" y1="303.357" x2="426.837" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#258df5"/><stop offset="1" stop-color="#1461da"/></linearGradient><linearGradient id="sc5" x1="385.63" y1="53.364" x2="522.989" y2="510.769" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#51adff"/><stop offset="1" stop-color="#2b83f1"/></linearGradient></defs><g><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z" fill="url(#sc1)"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z" fill="url(#sc2)"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z" fill="url(#sc3)"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z" fill="url(#sc4)"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z" fill="url(#sc5)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="38.0 -749.0 4847.0 761.0" width="4847" height="761" role="img"><title>SolidCraft</title><path fill="#15499e" d="M303 12Q212 12 147.5 -20Q83 -52 38 -101L138 -202Q173 -162 217.5 -142Q262 -122 313 -122Q369 -122 396 -145Q423 -168 423 -206Q423 -225 416.5 -240.5Q410 -256 392.5 -266.5Q375 -277 342 -281L273 -290Q200 -300 152.5 -326.5Q105 -353 82 -396.5Q59 -440 59 -498Q59 -561 90.5 -608.5Q122 -656 180.5 -683Q239 -710 321 -710Q400 -710 460 -684.5Q520 -659 563 -612L462 -510Q437 -539 401.5 -557.5Q366 -576 312 -576Q261 -576 236 -558Q211 -540 211 -508Q211 -484 219.5 -469Q228 -454 246.5 -446Q265 -438 294 -433L363 -422Q435 -411 482 -385Q529 -359 552 -317Q575 -275 575 -215Q575 -148 543.5 -97Q512 -46 451 -17Q390 12 303 12ZM926 12Q849 12 794 -21.5Q739 -55 709.5 -117Q680 -179 680 -263Q680 -348 709.5 -409Q739 -470 794 -503.5Q849 -537 926 -537Q1003 -537 1058 -503.5Q1113 -470 1142.5 -409Q1172 -348 1172 -263Q1172 -179 1142.5 -117Q1113 -55 1058 -21.5Q1003 12 926 12ZM926 -105Q970 -105 994 -132.5Q1018 -160 1018 -209V-316Q1018 -366 994 -393Q970 -420 926 -420Q883 -420 858.5 -393Q834 -366 834 -316V-209Q834 -160 858.5 -132.5Q883 -105 926 -105ZM1511 0H1444Q1368 0 1332.5 -37Q1297 -74 1297 -144V-740H1445V-115H1511ZM1694 -581Q1649 -581 1628.5 -602Q1608 -623 1608 -654V-676Q1608 -708 1628.5 -728.5Q1649 -749 1694 -749Q1739 -749 1759.5 -728.5Q1780 -708 1780 -676V-654Q1780 -623 1759.5 -602Q1739 -581 1694 -581ZM1620 0V-525H1768V0ZM2248 0V-88H2243Q2232 -45 2192.5 -16.5Q2153 12 2102 12Q2036 12 1991 -19.5Q1946 -51 1923 -112.5Q1900 -174 1900 -263Q1900 -352 1923 -413Q1946 -474 1991 -505.5Q2036 -537 2102 -537Q2153 -537 2192.5 -509Q2232 -481 2243 -437H2248V-740H2396V0ZM2151 -108Q2179 -108 2201 -117Q2223 -126 2235.5 -143.5Q2248 -161 2248 -185V-340Q2248 -364 2235.5 -381.5Q2223 -399 2201 -408Q2179 -417 2151 -417Q2109 -417 2081.5 -387.5Q2054 -358 2054 -308V-217Q2054 -168 2081.5 -138Q2109 -108 2151 -108ZM2844 12Q2750 12 2680.5 -28Q2611 -68 2573 -147Q2535 -226 2535 -344Q2535 -462 2573 -543.5Q2611 -625 2680.5 -667.5Q2750 -710 2844 -710Q2940 -710 3003.5 -670Q3067 -630 3105 -546L2974 -478Q2961 -522 2930.5 -548.5Q2900 -575 2844 -575Q2777 -575 2736.5 -531.5Q2696 -488 2696 -405V-293Q2696 -211 2736.5 -167Q2777 -123 2844 -123Q2900 -123 2933.5 -153.5Q2967 -184 2983 -227L3107 -155Q3068 -76 3004 -32Q2940 12 2844 12ZM3373 0H3225V-525H3373V-411H3378Q3384 -440 3400 -466Q3416 -492 3443.5 -508.5Q3471 -525 3512 -525H3538V-387H3501Q3458 -387 3429.5 -379.5Q3401 -372 3387 -355Q3373 -338 3373 -307ZM4105 0H4023Q3996 0 3972.5 -13.5Q3949 -27 3935 -53.5Q3921 -80 3921 -117V-130L3953 -92H3917Q3905 -41 3864 -14.5Q3823 12 3763 12Q3684 12 3642 -30.5Q3600 -73 3600 -141Q3600 -197 3627.5 -233Q3655 -269 3705.5 -287Q3756 -305 3825 -305H3908V-338Q3908 -376 3888 -398.5Q3868 -421 3821 -421Q3777 -421 3751 -402Q3725 -383 3708 -359L3620 -437Q3652 -484 3701 -510.5Q3750 -537 3832 -537Q3943 -537 3999.5 -487.5Q4056 -438 4056 -345V-115H4105ZM3908 -221H3836Q3793 -221 3771 -206Q3749 -191 3749 -162V-147Q3749 -119 3767 -105Q3785 -91 3818 -91Q3843 -91 3863 -98Q3883 -105 3895.5 -120Q3908 -135 3908 -159ZM4250 0V-410H4176V-525H4250V-587Q4250 -663 4290.5 -701.5Q4331 -740 4407 -740H4503V-625H4398V-525H4503V-410H4398V0ZM4877 0H4789Q4713 0 4673 -39Q4633 -78 4633 -153V-410H4559V-525H4596Q4626 -525 4637 -539.5Q4648 -554 4648 -581V-667H4781V-525H4885V-410H4781V-115H4877Z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -60,7 +60,6 @@ circle_ttt false
|
||||
close false
|
||||
coil false
|
||||
colored_components false
|
||||
colored_components_off false
|
||||
combine false
|
||||
compare false
|
||||
component false
|
||||
@@ -161,6 +160,7 @@ min_radius false
|
||||
mirror false
|
||||
motion_link false
|
||||
move false
|
||||
nav_settings false
|
||||
new false
|
||||
normal false
|
||||
offset false
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Disable component colors</title>
|
||||
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#328bd3"/>
|
||||
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#236ead"/>
|
||||
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#b9ddf8"/>
|
||||
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
|
||||
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
|
||||
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
|
||||
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#328bd3"/>
|
||||
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#236ead"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#b9ddf8"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -79,17 +79,6 @@ pub(crate) fn snap_hint(painter: &egui::Painter, c: Pos2, kind: SnapKind, col: C
|
||||
painter.circle_filled(c, 1.3, col);
|
||||
}
|
||||
|
||||
/// A placed hole's drag handle: a ring round the hole's centre with a cross through it, filled
|
||||
/// while it is hovered or dragged.
|
||||
pub(crate) fn hole_handle(painter: &egui::Painter, c: Pos2, col: Color32, hot: bool) {
|
||||
let ic = Tokens::get().icons;
|
||||
let r = 6.0;
|
||||
painter.circle_filled(c, r + 3.5, ic.marker_halo);
|
||||
painter.circle(c, r, if hot { col } else { col.gamma_multiply(0.35) }, Stroke::new(1.6, col));
|
||||
let ink = Stroke::new(1.3, if hot { ic.highlight } else { col });
|
||||
painter.line_segment([c - vec2(r + 3.0, 0.0), c + vec2(r + 3.0, 0.0)], ink);
|
||||
painter.line_segment([c - vec2(0.0, r + 3.0), c + vec2(0.0, r + 3.0)], ink);
|
||||
}
|
||||
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
Some(match name {
|
||||
"Coincident" => "c_coincident",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"Bottom Right": "أسفل اليمين",
|
||||
"Boundary": "الحد",
|
||||
"Bounding box": "الصندوق المحيط",
|
||||
"Bridge": "جسر",
|
||||
"CHECK": "تحقق",
|
||||
"COMPONENTS": "المكوّنات",
|
||||
"CONFIGURATIONS": "التكوينات",
|
||||
@@ -628,6 +629,8 @@
|
||||
"Suppress Selected Folders": "تعطيل المجلدات المحددة",
|
||||
"Suppressed": "مُعطّل",
|
||||
"Surface": "السطح",
|
||||
"Surface or face": "سطح أو وجه",
|
||||
"Surface or faces": "سطح أو أوجه",
|
||||
"Surface, or faces to turn over": "السطح، أو الأوجه المراد قلبها",
|
||||
"Surfaces": "الأسطح",
|
||||
"Swap Ends": "تبديل الطرفين",
|
||||
@@ -641,6 +644,7 @@
|
||||
"Target": "الهدف",
|
||||
"Target Body": "الجسم الهدف",
|
||||
"Target Face (optional)": "الوجه الهدف (اختياري)",
|
||||
"Target edges": "الحواف المستهدفة",
|
||||
"Tear": "تمزيق",
|
||||
"Technical details": "التفاصيل التقنية",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "الرسم المتقطع دليل مستوى محفوظ. حدد وجهًا جديدًا لإصلاح الارتباط.",
|
||||
@@ -648,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "يجب أن يكون الاتجاه محورًا أو منحنى رسم مستقيمًا يقع في مستوى الرسم",
|
||||
"The mirror line cannot also be mirrored": "لا يمكن عكس خط المرآة نفسه",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "يجب أن يكون خط المرآة منحنى مستقيمًا من هذا الرسم، وليس أحد محاور نقطة الأصل",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "محور الأصل عمودي على مستوى الرسم، لذا ليس له خط في المستوى",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "نسخة المؤلف المحفوظة مفقودة. حدد موضع مؤلف صالحًا قبل إعادة الربط.",
|
||||
"Then": "ثم",
|
||||
"These features will be deleted:": "ستُحذف هذه الميزات:",
|
||||
@@ -744,22 +749,24 @@
|
||||
"ai.prefs.availability": "التوفر",
|
||||
"ai.prefs.background_commands": "تشغيل الأوامر البطيئة في الخلفية",
|
||||
"ai.prefs.connect_apps": "إضافة SolidCraft إلى تطبيقات الذكاء الاصطناعي",
|
||||
"ai.prefs.connect_apps_note": "إيقافه يزيل الإدخالات مجدداً.\n\nعند بدء تشغيل SolidCraft، يُضاف كخادم MCP إلى تطبيقات الذكاء الاصطناعي الموجودة على هذا الحاسوب (OpenCode وClaude Desktop وClaude Code وCursor وCodex وGemini CLI وCopilot CLI وWindsurf وZed وVS Code)، ليتمكن الوكيل من استخدامه دون أي إعداد. لا يُضاف إلا مُدخل solidcraft أو يُحدَّث مساره؛ ويُترك المُدخل الذي غيّرته كما هو، ويُنسخ الملف احتياطيًا أولًا. عند الإيقاف تُزال المُدخلات التي أضافها. الأمر «المساعدة > ربط مساعدي الذكاء الاصطناعي…» يفعل ذلك يدويًا.",
|
||||
"ai.prefs.connect_apps_note": "إيقافه يزيل الإدخالات مجددًا.\n\nعند بدء تشغيل SolidCraft، يُضاف كخادم MCP إلى تطبيقات الذكاء الاصطناعي الموجودة على هذا الحاسوب (OpenCode وClaude Desktop وClaude Code وCursor وCodex وGemini CLI وCopilot CLI وWindsurf وZed وVS Code)، ليتمكن الوكيل من استخدامه دون أي إعداد. لا يُضاف إلا مُدخل solidcraft أو يُحدَّث مساره؛ ويُترك المُدخل الذي غيّرته كما هو، ويُنسخ الملف احتياطيًا أولًا. عند الإيقاف تُزال المُدخلات التي أضافها. الأمر «المساعدة > ربط مساعدي الذكاء الاصطناعي…» يفعل ذلك يدويًا.",
|
||||
"ai.prefs.connect_at_start": "توصيل آخر وكيل عند البدء",
|
||||
"ai.prefs.connect_at_start_note": "يبدأ في الخلفية الوكيل الذي اتصلت به آخر مرة (أو الوكيل الافتراضي) في كل مرة يبدأ فيها SolidCraft، ليكون جاهزًا للمحادثة. لا يحدث ذلك عند بدء SolidCraft مع --control. إذا تعذّر الاتصال، يعرض المساعد «فشل الاتصال» وزر «إعادة المحاولة».",
|
||||
"ai.prefs.follow_camera": "اتباع الكاميرا",
|
||||
"ai.prefs.follow_camera": "تتبّع الكاميرا",
|
||||
"ai.prefs.follow_camera_off": "تتبّع الكاميرا التلقائي متوقف.",
|
||||
"ai.prefs.follow_camera_on": "تتجه الكاميرا نحو الرسومات التي ينشئها الوكيل.",
|
||||
"ai.prefs.instructions": "تعليمات مخصصة",
|
||||
"ai.prefs.instructions_note": "تُرسَل إلى الوكيل في بداية كل محادثة، بعد تعليمات SolidCraft الخاصة: طريقتك المفضلة في العمل والقواعد التي يجب الالتزام بها. يسري أي تغيير من المحادثة التالية (اتصال أو محادثة جديدة).",
|
||||
"ai.prefs.intro": "وكلاء الذكاء الاصطناعي في SolidCraft.",
|
||||
"ai.prefs.make_default": "تعيين كافتراضي",
|
||||
"ai.prefs.make_default": "تعيين افتراضيًا",
|
||||
"ai.prefs.no_agents": "لا يوجد وكلاء بعد. أضف وكيلًا أو حدّث القائمة للعثور على الوكلاء المثبّتين.",
|
||||
"ai.prefs.no_saved_approvals": "لا توجد موافقات محفوظة.",
|
||||
"ai.prefs.open_at_start": "فتح المحادثة عند البدء",
|
||||
"ai.prefs.open_at_start_note": "يعرض لوحة المساعد (أو نافذتها المستقلة) في كل مرة يبدأ فيها SolidCraft. لا يحدث ذلك عند بدء SolidCraft مع --control.",
|
||||
"ai.prefs.override_all_warning": "قد يحفظ هذا الوكيل التصميم أو يكتب فوقه أو يصدّره أو يستبدله دون أن يسأل.",
|
||||
"ai.prefs.permission_all": "فعل كل شيء",
|
||||
"ai.prefs.permission_all_summary": "لا يسأل أبداً",
|
||||
"ai.prefs.permission_ask": "السؤال أولاً",
|
||||
"ai.prefs.permission_all": "تنفيذ كل شيء",
|
||||
"ai.prefs.permission_all_summary": "لا يسأل أبدًا",
|
||||
"ai.prefs.permission_ask": "السؤال أولًا",
|
||||
"ai.prefs.permission_ask_summary": "يسأل عن كل تغيير",
|
||||
"ai.prefs.permission_edits": "إجراء تعديلات",
|
||||
"ai.prefs.permission_edits_summary": "يعدّل بحرية ويسأل عن الباقي",
|
||||
@@ -963,8 +970,7 @@
|
||||
"cmd.inspect_analysis": "تحليل العرض",
|
||||
"cmd.inspect_center_of_mass": "مركز الكتلة",
|
||||
"cmd.inspect_clearance": "الخلوص",
|
||||
"cmd.inspect_colored_components": "تفعيل ألوان المكوّنات",
|
||||
"cmd.inspect_colored_components_disable": "تعطيل ألوان المكوّنات",
|
||||
"cmd.inspect_colored_components": "ألوان المكوّنات",
|
||||
"cmd.inspect_compare": "مقارنة الأجسام",
|
||||
"cmd.inspect_compare_picture": "مقارنة بصورة مرجعية",
|
||||
"cmd.inspect_continuity": "تحليل الاستمرارية",
|
||||
@@ -1296,6 +1302,7 @@
|
||||
"cmd.timeline_stop": "إيقاف السجل",
|
||||
"cmd.timeline_suppress": "تعطيل الميزة",
|
||||
"cmd.ui.assistant": "إظهار المساعد أو إخفاؤه",
|
||||
"cmd.view.navigator": "متصفح العرض المخصص",
|
||||
"cmd.view.render_browser": "متصفح التصيير",
|
||||
"cmd.view.render_dock": "نقل لوحة التصيير",
|
||||
"cmd.view.render_focus": "عرض تركيز التصيير",
|
||||
@@ -1394,15 +1401,22 @@
|
||||
"dialog.computing": "جارٍ الحساب… {seconds} ث",
|
||||
"dialog.could_not_be_checked": "تعذّر فحصه",
|
||||
"dialog.custom_axis": "↖ محور مخصص",
|
||||
"dialog.cylinder_face_tip": "وجه النموذج أو رأسه الذي تقوم عليه الأسطوانة: انقر هنا ثم انقر عليه في العرض. إذا تُرك فارغًا، تحدد X وY وZ موضعها.",
|
||||
"dialog.dimension_insert_hint": "انقر على بُعد قائد آخر لإدراج اسمه",
|
||||
"dialog.dimension_name": "البُعد: {name}",
|
||||
"dialog.dimension_step_down": "إنقاص بمقدار {step}",
|
||||
"dialog.dimension_step_expression": "يتغير الرقم فقط بالخطوات؛ يبقى التعبير كما كُتب",
|
||||
"dialog.dimension_step_up": "زيادة بمقدار {step}",
|
||||
"dialog.edit_title": "تعديل {title}",
|
||||
"dialog.no_shown_body": "لا يوجد جسم ظاهر لتغييره: أظهر جسمًا، أو اختر «جسم جديد»",
|
||||
"dialog.pattern_sketch_not_object": "اختر أجسامًا أو ميزات: الرسم ليس أيًّا منهما. كرّر منحنياته بأداة النمط أو المرآة داخل الرسم، أو ابثقه أولًا.",
|
||||
"dialog.primitive_point_tip": "نقطة تملأ X وY وZ: انقر هنا ثم على رأس أو حافة أو وجه من النموذج، أو على نقطة من رسم، أو على نقطة الأصل.",
|
||||
"dialog.reference_edge_tip": "حافة مستقيمة من الوجه المختار تُقاس منها المسافة أدناه: انقر هنا ثم على الحافة.",
|
||||
"dialog.select": "↖ تحديد",
|
||||
"dialog.selected_count": "↖ {count} محدد",
|
||||
"dialog.selection_constraint": "قيد {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · منحنى {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "حدد مُدخلًا لتغييره. انقر على العناصر المحددة لإزالتها؛ Ctrl/Cmd مع النقر يبدّل تحديدها أيضًا. راجع المعاينة ثم اضغط موافق.",
|
||||
"dialog.selection_midpoint": "نقطة المنتصف · {id}",
|
||||
"dialog.selection_profile": "{sketch} · مقطع {number}",
|
||||
@@ -1420,6 +1434,7 @@
|
||||
"document.read_only_save": "فُتح هذا التصميم للقراءة فقط: احفظه كملف جديد",
|
||||
"document.save_changes": "حفظ التغييرات على {name}؟",
|
||||
"document.save_copy": "حفظ نسخة…",
|
||||
"document.tab_unsaved_changes": "تغييرات غير محفوظة",
|
||||
"document.unsaved_changes": "في {name} تغييرات غير محفوظة. إن لم تحفظها فستضيع.",
|
||||
"drive.rigid": "{joint} مفصل صلب: يثبّت أجزاءه في مكانها، لذا لا يوجد ما يمكن تحريكه. لتتحرك الأجزاء، غيّر نوعه في «تعديل المفصل»: «دوراني» للدوران، «منزلق» للانزلاق، «أسطواني» لكليهما.",
|
||||
"drive.stopped_by_contact": "توقف عند {value}: سيخترق {a} الجسم {b}",
|
||||
@@ -1440,6 +1455,9 @@
|
||||
"each rail's curves must be in one sketch": "يجب أن تكون منحنيات كل دليل في رسم واحد",
|
||||
"feature computation ended without a result": "انتهى حساب الميزة دون نتيجة",
|
||||
"file.open.drawing_filter": "التصاميم وملفات CAD والشبكات والرسومات",
|
||||
"file.open.portal_fallback.no_portal_service": "منتقي الملفات الذي فُتح ليس تابعًا لسطح المكتب: خدمة منتقي الملفات في سطح المكتب غير متاحة.",
|
||||
"file.open.portal_fallback.no_session_bus": "منتقي الملفات الذي فُتح ليس تابعًا لسطح المكتب: لا يوجد ناقل جلسات D-Bus.",
|
||||
"file.open.portal_fallback.xwayland": "منتقي الملفات الذي فُتح ليس تابعًا لسطح المكتب: في XWayland ترفض البوابة النافذة الأصلية.",
|
||||
"file_load.built": "بُني حتى الآن: {built} من {total} ميزة.",
|
||||
"file_load.converting": "تحويل الجسم الصلب {done} من {total}",
|
||||
"file_load.inserting": "إدراج الهندسة",
|
||||
@@ -1600,7 +1618,7 @@
|
||||
"help.parts.resize": "يغيّر مقاس قطعة قياسية مدرجة أو طولها ويعيد بناءها، وتبقيها مفاصلها مثبتة في مكانها. استخدمه عندما يتبين أن برغيًا أطول من اللازم أو أن ثقبًا يحتاج إلى مقاس آخر.",
|
||||
"help.plastic.assign_rule": "يعيّن قاعدة بلاستيك لأجسام، فتأخذ ميزاتها البلاستيكية ميلها وخلوصها منها كقيم افتراضية. استخدمه لإعداد قطعة مقولبة مرة واحدة لكل ميزاتها.",
|
||||
"help.plastic.boss": "يضيف نتوءًا لمسمار على وجه: عمودًا مع ثقب اختياري وميل وتقويس عند الجذر وأضلاع تقوية. استخدمه لنقاط البراغي في الهياكل المقولبة أو المطبوعة.",
|
||||
"help.plastic.lip": "يضيف شفة أو أخدودًا مطابقًا على طول حافة جسم مفرَّغ. استخدمه ليتمركز نصفا الهيكل أحدهما على الآخر ويُغلقا بإحكام.",
|
||||
"help.plastic.lip": "يضيف شفة أو أخدودًا مطابقًا على طول إطار جسم مفرَّغ (فرّغ الجسم بفتح أحد وجوهه أولًا، ثم اختر الإطار المحيط بتلك الفتحة). استخدمه ليتمركز نصفا الهيكل أحدهما على الآخر ويُغلقا بإحكام.",
|
||||
"help.plastic.manage_rules": "ينشئ قواعد البلاستيك ويعدّلها: المادة وسماكة الجدار وأنصاف الأقطار والخلوص والميل. استخدمها للحفاظ على اتساق ميزات القطعة المقولبة.",
|
||||
"help.plastic.rest": "يضيف مسندًا على وجه: وسادة مستديرة أو مستطيلة، مصمتة أو مجوفة، مع ميل. استخدمه كدعامة أو مُباعِد في هيكل مقولب.",
|
||||
"help.plastic.snap_fit": "يضيف خطاف وصلة كبس على وجه، بطوله وسماكته ونتوء التثبيت. استخدمه لتثبيت القطع معًا بلا براغي.",
|
||||
@@ -1918,7 +1936,7 @@
|
||||
"joint.limit_loop": "{side} {value}: لا يمكن لحلقة مغلقة أن تنغلق بعده",
|
||||
"joint.limit_range": "{side}: لا تلامس ضمن {range}، تُرك كما كان",
|
||||
"joint.limit_turn": "{side} {value}: لا تلامس ضمن دورة واحدة",
|
||||
"joint.limits_overlapping": "متداخلة بالفعل هنا: {pairs}. لا توقف البحث إلا إذا تداخلت أعمق.",
|
||||
"joint.limits_overlapping": "متداخلة بالفعل هنا: {pairs}. لا توقف البحث إلا إذا ازداد تداخلها.",
|
||||
"joint.one_body": "كلا الاختيارين على {body}. اختر الثاني على قطعة أخرى.",
|
||||
"joint.one_component": "هذه القطع أجسام لمكوّن واحد: {bodies}. المفصل يحرّك مكوّنًا مقابل آخر، لذا تحتاج كل قطعة إلى مكوّن خاص بها.",
|
||||
"joint.rigid_group_one_component": "هذه الأجسام وعددها {count} كلها في مكوّن واحد (أو في التصميم نفسه): استخدم {command} أولًا.",
|
||||
@@ -1933,7 +1951,7 @@
|
||||
"joint_move.fine": "الخطوة بعيدًا عن المحور",
|
||||
"joint_move.fixed": "لا يملك هذا الجزء محور مفصل قابلًا للحركة.",
|
||||
"joint_move.help": "تحريك — اسحب سهمًا أو قوسًا. يلغى السحب الحالي بمفتاح Esc.",
|
||||
"joint_move.legacy_hint": "إيقاف: لوحة التحريك السابقة.",
|
||||
"joint_move.legacy_hint": "عند الإيقاف: لوحة التحريك السابقة.",
|
||||
"joint_move.precision": "اسحب خارج الدليل المتقطع للخطوة الدقيقة.",
|
||||
"joint_move.rotation": "الدوران من نقطة البداية",
|
||||
"joint_move.select": "اختر وجهًا أو جزءًا واحدًا من التجميع، ثم اختر تحريك.",
|
||||
@@ -1999,6 +2017,7 @@
|
||||
"material.selection.mixed": "مواد مختلفة",
|
||||
"measure.at": "عند",
|
||||
"measure.item": "العنصر {number}",
|
||||
"measure.line_to_centre": "من الخط إلى المركز",
|
||||
"measure.quick_copied": "تم نسخ القياس",
|
||||
"measure.quick_copy": "انقر لنسخ القياس",
|
||||
"measure.quick_edges": "{count} حواف",
|
||||
@@ -2029,6 +2048,8 @@
|
||||
"nav.blender.help": "السحب بالزر الأوسط يدوّر العرض، وShift مع الأوسط يُزيحه، وCtrl مع الأوسط يكبّره؛ والسحب بالأيمن لا يحرك الكاميرا",
|
||||
"nav.creo.help": "السحب بالزر الأوسط يدوّر العرض، وShift مع الأوسط يُزيحه، وCtrl مع السحب العمودي بالأوسط يكبّره أو يصغّره (السحب لأسفل للتكبير)؛ وتدوير العجلة يكبّره أو يصغّره",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "يستمر السحب بالزر الأوسط أو الأيمن في التدوير أو التحريك أو التكبير كما بدأ حتى تحرير الزر، مهما ضُغطت المفاتيح أو حُرّرت في أثناء ذلك. عند الإيقاف: تقرر المفاتيح المضغوطة في كل لحظة من السحب.",
|
||||
"nav.drag_keeps_mode.label": "يحتفظ السحب بطريقة التنقل التي بدأ بها",
|
||||
"nav.free_orbit.help": "الفأرة والفأرة ثلاثية الأبعاد تدوّران العرض عبر القطبين وتحافظان على اللف. يُعطَّل التقاط المحاور بـ Shift.",
|
||||
"nav.free_orbit.label": "تدوير حر",
|
||||
"nav.free_orbit.snap_help": "الفأرة والفأرة ثلاثية الأبعاد تدوّران العرض عبر القطبين وتحافظان على اللف. اضغط مطولًا على Shift أثناء سحب التدوير لالتقاط اتجاه العرض واللف.",
|
||||
@@ -2240,7 +2261,7 @@
|
||||
"prefs.anti_aliasing_amd_dx12": "يستخدم تنعيم الحواف 1× لأن فحص بدء التشغيل لم يرسم شيئًا مع أخذ العينات المتعددة ونجح عند 1×. يمكنك اختيار إعداد آخر أعلاه؛ أعد التشغيل لتطبيقه إن نجح الفحص.",
|
||||
"prefs.anti_aliasing_note": "ينعّم الحواف المسننة.",
|
||||
"prefs.appearance.colour_vision_help": "يغيّر الألوان ذات الدلالة: حالات الرسم والمحاور والثنيات والتحليلات والأيقونات.",
|
||||
"prefs.appearance.desktop_only": "لسطح المكتب فقط",
|
||||
"prefs.appearance.desktop_only": "لتطبيق سطح المكتب فقط",
|
||||
"prefs.appearance.follow_help": "يعيد التحميل عندما تعيد أداة أخرى كتابة الملف.",
|
||||
"prefs.appearance.interface_scale": "مقياس الواجهة",
|
||||
"prefs.appearance.next_themes": "السمات التالية",
|
||||
@@ -2250,11 +2271,11 @@
|
||||
"prefs.appearance.theme_count": "{count} سمات",
|
||||
"prefs.appearance.theme_own": "ألوان السمة",
|
||||
"prefs.appearance.viewport_colours": "ألوان العرض ثلاثي الأبعاد",
|
||||
"prefs.appearance.viewport_logo": "منفذ العرض والشعار",
|
||||
"prefs.appearance.viewport_logo": "منطقة العرض والشعار",
|
||||
"prefs.applies_on_restart": "يُطبَّق عند إعادة التشغيل",
|
||||
"prefs.assistant": "المساعد",
|
||||
"prefs.assistant_connecting": "الاتصال",
|
||||
"prefs.assistant_follow_help": "تحرّك عروض Look At للرسم المنظور مع المؤشر.",
|
||||
"prefs.assistant_follow_help": "النظر إلى الرسومات يحرّك العرض مع المؤشر.",
|
||||
"prefs.assistant_limit": "الحد",
|
||||
"prefs.assistant_limited": "محدود",
|
||||
"prefs.assistant_pointer_speed": "سرعة المؤشر",
|
||||
@@ -2262,13 +2283,24 @@
|
||||
"prefs.assistant_seconds_aria": "الثواني",
|
||||
"prefs.assistant_use_care": "استخدم بحذر",
|
||||
"prefs.auto_operation": "اختيار العملية تلقائيًا",
|
||||
"prefs.auto_operation_note": "ضم أو جسم جديد أو قطع، حسب ما تلامسه المادة.",
|
||||
"prefs.auto_operation_note": "ضم أو جسم جديد أو قص، حسب ما تلامسه المادة.",
|
||||
"prefs.axes": "المحاور",
|
||||
"prefs.axis": "المحور",
|
||||
"prefs.background_choose": "اختيار صورة…",
|
||||
"prefs.background_image": "الصورة",
|
||||
"prefs.background_unreadable": "تعذرت قراءة صورة الخلفية",
|
||||
"prefs.blur_cost": "تكلفة العرض النسبية. زيادة الأشرطة تعني عادةً مزيدًا من العمل.",
|
||||
"prefs.blur_device": "غير متاح على جهاز الرسوميات أو مسار العرض هذا",
|
||||
"prefs.blur_guard": "خُفّضت جودة التمويه للحفاظ على استجابة الإطارات. لم يتغير نصف القطر.",
|
||||
"prefs.blur_off": "إيقاف",
|
||||
"prefs.blur_reduce_transparency": "معطّل بسبب إعداد تقليل الشفافية في النظام",
|
||||
"prefs.blur_software": "غير متاح مع الرسوميات البرمجية (gpu-software)",
|
||||
"prefs.blur_source": "غير متاح في عرض مساحة العمل هذا",
|
||||
"prefs.blur_web": "غير متاح في مسار عرض الويب هذا",
|
||||
"prefs.bodies_folders": "مجلدات الأجسام",
|
||||
"prefs.bodies_folders_always": "دائمًا",
|
||||
"prefs.bodies_folders_never": "أبدًا",
|
||||
"prefs.bodies_folders_several": "عدة",
|
||||
"prefs.bodies_folders_several": "عند التعدد",
|
||||
"prefs.bottom": "أسفل",
|
||||
"prefs.bottom_centre": "أسفل الوسط",
|
||||
"prefs.bottom_right": "أسفل اليمين",
|
||||
@@ -2278,6 +2310,8 @@
|
||||
"prefs.browser_eye_left": "أيقونة الإظهار بجانب سهم الطي",
|
||||
"prefs.browser_eye_left_note": "تعرض صفوف المستعرض أيقونة الإظهار/الإخفاء بجانب سهم الطي كما كانت من قبل، بدلًا من عمود عند نهاية الصفوف.",
|
||||
"prefs.browser_reveal_on_hover": "إظهار عند مرور المؤشر",
|
||||
"prefs.browser_rows_start_closed": "صفوف المستعرض تبدأ مغلقة",
|
||||
"prefs.browser_rows_start_closed_note": "تبدأ المواضع والمجلدات في المستعرض مغلقة، فيعرض التصميم الذي يضم مكونات كثيرة صفوفه العليا. يُفتح كل صف بسهمه أو بـ«توسيع الكل» أو بـ«البحث في المستعرض» الذي يفتح الصفوف المؤدية إلى الجسم فقط، ويغلق من جديد الصفوف التي فتحها البحث السابق. اختيار جسم في العرض ثلاثي الأبعاد يترك المستعرض كما هو. عند إيقافه تبدأ المجلدات مفتوحة، وتبدأ المواضع كما يحدّد خيار «المكوّنات مفتوحة من البداية»، ويفتح الاختيار في العرض ثلاثي الأبعاد المستعرضَ حتى صف الجسم، كما كان من قبل.",
|
||||
"prefs.browser_show_note": "«ظاهر» يُبقي المستعرض على الشاشة. «إظهار عند مرور المؤشر» يُخفيه حتى يمر المؤشر فوق مكانه أو فوق الشريط الرفيع عند حافته اليسرى، ويبقى مفتوحًا ما دام المؤشر داخله أو كانت قائمة منه مفتوحة.",
|
||||
"prefs.browser_shown": "ظاهر",
|
||||
"prefs.built_in": "مدمج",
|
||||
@@ -2323,6 +2357,13 @@
|
||||
"prefs.dialogs_edge": "عند حافة العرض",
|
||||
"prefs.dialogs_left_column": "في العمود الأيسر",
|
||||
"prefs.dialogs_note": "مكان فتح حوارات الأوامر: مثبتة عند حافة العرض، أو في العمود الأيسر مكان المستعرض والسجل، اللذين يعودان كما كانا عند إغلاق الحوار.",
|
||||
"prefs.dimension_step_angle": "ما تضيفه أسهم النافذة والسهمان لأعلى ولأسفل والعجلة إلى الزاوية",
|
||||
"prefs.dimension_step_length": "ما تضيفه أسهم النافذة والسهمان لأعلى ولأسفل والعجلة إلى الطول",
|
||||
"prefs.dimension_steps": "خطوات النافذة",
|
||||
"prefs.dimension_values": "قيم الأبعاد",
|
||||
"prefs.dimension_values_note": "مكان كتابة قيمة البعد. للنافذة أسهم تغيّرها بخطوات؛ تضع أداة البعد النص أولًا.",
|
||||
"prefs.dimension_values_view": "في العرض",
|
||||
"prefs.dimension_values_window": "في نافذة",
|
||||
"prefs.display": "العرض",
|
||||
"prefs.edges": "الحواف والتظليل",
|
||||
"prefs.extend": "توسيع",
|
||||
@@ -2360,7 +2401,7 @@
|
||||
"prefs.graphics_note": "وازن بين السرعة والنعومة. كل خيار يعرض أثره على عيّنة.",
|
||||
"prefs.graphics_page.current_view": "العرض الحالي",
|
||||
"prefs.graphics_page.fastest": "الأسرع",
|
||||
"prefs.graphics_page.fold_items": "طي العناصر",
|
||||
"prefs.graphics_page.fold_items": "تلخيص العناصر",
|
||||
"prefs.graphics_page.in_view": "داخل العرض",
|
||||
"prefs.graphics_page.live_details": "تفاصيل مباشرة",
|
||||
"prefs.graphics_page.own_mix": "مزيجك الخاص",
|
||||
@@ -2369,7 +2410,7 @@
|
||||
"prefs.graphics_page.restart": "تلزم إعادة التشغيل",
|
||||
"prefs.graphics_page.sharpest": "الأكثر وضوحًا",
|
||||
"prefs.graphics_page.single_surface_analysis": "تحليل سطح واحد",
|
||||
"prefs.graphics_page.status_chips": "شرائح الحالة",
|
||||
"prefs.graphics_page.status_chips": "شارات الحالة",
|
||||
"prefs.graphics_page.subtitle": "وازن بين السرعة والنعومة.",
|
||||
"prefs.graphics_page.supersampling": "التنعيم الفائق",
|
||||
"prefs.graphics_page.supersampling_adaptive": "فقط أثناء سكون العرض",
|
||||
@@ -2402,6 +2443,10 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "سهما التدوير المنحنيان عند زاوية المكعب يدوران العرض داخل مستوى الشاشة. عند التشغيل: سهم التدوير مع عقارب الساعة يدور عكسها، وسهم العكس يدور معها. عند الإيقاف: يدور كل سهم بالعرض حسب انحنائه.",
|
||||
"prefs.invert_navigator_arrows": "عكس أسهم تدوير مكعب العرض",
|
||||
"prefs.invert_navigator_arrows_note": "تدوّر الأسهم المحيطة بالمكعب الكاميرا في الاتجاه الذي تشير إليه، كما في Fusion وInventor: السهم الموجود على اليسار يشير إلى اليمين ويدوّر الكاميرا إلى اليمين. عند الإيقاف: يدوّر السهم المكعب نحو الجهة التي يقع فيها، فيدوّر السهم الأيسر الكاميرا إلى اليسار. لا يتأثر سهما الدوران المنحنيان.",
|
||||
"prefs.keep_unsaved_also_asks": "السؤال أيضًا عند إغلاق علامة التبويب",
|
||||
"prefs.keep_unsaved_also_asks_tip": "عند إيقاف هذا الخيار، تُغلق علامة التبويب دون مربع المتصفح بعد تحديث نسخة كل تصميم غير محفوظ.",
|
||||
"prefs.keep_unsaved_in_browser": "الاحتفاظ بالعمل غير المحفوظ في هذا المتصفح",
|
||||
"prefs.keep_unsaved_in_browser_tip": "تُحدَّث التغييرات غير المحفوظة لكل تصميم مفتوح باستمرار في التخزين الخاص بهذا المتصفح. وتعرض الزيارة التالية استعادتها.",
|
||||
"prefs.keep_work_on_tab_switch": "إبقاء مربعات الحوار والأدوات مفتوحة عند التبديل بين التصاميم",
|
||||
"prefs.keep_work_on_tab_switch_note": "تحتفظ كل علامة تبويب تصميم بمربع الحوار أو الأداة المفتوحة، بما حددته وكتبته فيها، وبعلامة تبويب شريط الأدوات. وتجدها كما هي عند العودة إلى التصميم. عند الإيقاف: يؤدي التبديل بين التصاميم إلى إلغائها وعرض علامة التبويب «النموذج».",
|
||||
"prefs.labels": "التسميات",
|
||||
@@ -2413,10 +2458,12 @@
|
||||
"prefs.layout_page.card": "بطاقة",
|
||||
"prefs.layout_page.column": "عمود",
|
||||
"prefs.layout_page.hover": "عند مرور المؤشر",
|
||||
"prefs.layout_page.hover_panel_menus": "تبديل قوائم اللوحات عند مرور المؤشر",
|
||||
"prefs.layout_page.hover_panel_menus_help": "انقر على اسم لوحة في الشريط لفتح قائمتها. مرّر المؤشر فوق اسم لوحة أخرى للتبديل. حرّك المؤشر خارج الشريط والقائمة المفتوحة لإغلاقها.",
|
||||
"prefs.layout_page.icons_labels": "الأيقونات + التسميات",
|
||||
"prefs.layout_page.ok_cancel": "موافق وإلغاء",
|
||||
"prefs.layout_page.on_hover": "عند مرور المؤشر",
|
||||
"prefs.layout_page.open": "فتح",
|
||||
"prefs.layout_page.open": "تُفتح",
|
||||
"prefs.layout_page.origin_bodies": "الأصل والأجسام في الأعلى",
|
||||
"prefs.layout_page.panels": "اللوحات",
|
||||
"prefs.layout_page.pinned_menus": "استبعاد الأوامر المثبتة من القوائم",
|
||||
@@ -2456,8 +2503,8 @@
|
||||
"prefs.navigation.cube_compact": "المكعب",
|
||||
"prefs.navigation.device": "الجهاز",
|
||||
"prefs.navigation.free": "حر",
|
||||
"prefs.navigation.orbit_style": "نمط الدوران",
|
||||
"prefs.navigation.radial_help": "اضغط مطولاً أو انقر سريعاً بالزر الأيمن لعرض حلقة من الأدوات.",
|
||||
"prefs.navigation.orbit_style": "نمط التدوير",
|
||||
"prefs.navigation.radial_help": "اضغط مطولًا أو انقر سريعًا بالزر الأيمن لعرض حلقة من الأدوات.",
|
||||
"prefs.navigation.turntable": "طاولة دوارة",
|
||||
"prefs.navigation_note": "كيف تحرك الفأرة ولوحة اللمس والفأرة ثلاثية الأبعاد العرض.",
|
||||
"prefs.navigator_fit": "ملاءمة النموذج بعد تغيير العرض من متصفح العرض",
|
||||
@@ -2475,7 +2522,7 @@
|
||||
"prefs.origin_plane_names_note": "الأسماء المعروضة في المستعرض والحوارات والقوائم. {top} هو XY و{front} هو XZ و{right} هو YZ، والمحور Z للأعلى. تحتفظ الأوامر والسكربتات والتصاميم المحفوظة بـ XY وXZ وYZ.",
|
||||
"prefs.overall_speed": "السرعة العامة",
|
||||
"prefs.parameter_list_rows": "صفوف قائمة المعاملات",
|
||||
"prefs.parameter_list_rows_tip": "الحد الأقصى للمعلمات التي يسردها السهم لأسفل في مربع تعبير فارغ.",
|
||||
"prefs.parameter_list_rows_tip": "الحد الأقصى للمعاملات التي يسردها السهم لأسفل في مربع تعبير فارغ.",
|
||||
"prefs.persistent_ribbon_pressed": "تمييز أوامر الشريط النشطة",
|
||||
"prefs.persistent_ribbon_pressed_help": "تمييز أوامر الشريط أثناء تفعيل تأثيراتها. أوقف هذا الخيار للاحتفاظ بالمظهر المحايد السابق ومساعدة الأوامر.",
|
||||
"prefs.perspective_fov": "مجال الرؤية",
|
||||
@@ -2533,6 +2580,8 @@
|
||||
"prefs.section_hatch_by_part": "حسب الجزء",
|
||||
"prefs.section_hatch_one": "نمط واحد",
|
||||
"prefs.select": "تحديد",
|
||||
"prefs.select_hidden_edges": "تحديد الحواف المخفية في نمط «مظلل مع الحواف المخفية»",
|
||||
"prefs.select_hidden_edges_help": "يتيح تحديد الحواف عبر الأوجه عند تفعيل نمط «مظلل مع الحواف المخفية»، بما في ذلك عند استخدام التقويس. عند الإيقاف يبقى سلوك التحديد كما في v0.30.1. تظل الأجسام المخفية غير قابلة للتحديد.",
|
||||
"prefs.select_one_item": "تحديد عنصر واحد",
|
||||
"prefs.select_other_hidden": "تحديد العناصر الهندسية المخفية باستخدام «تحديد عنصر آخر»",
|
||||
"prefs.select_other_hidden_help": "عند التفعيل: تضمين الأجسام والمخططات المخفية والحواف المحجوبة قرب المؤشر. عند التعطيل: استخدام القائمة الأصلية للعناصر الهندسية المرئية أسفل المؤشر.",
|
||||
@@ -2544,19 +2593,21 @@
|
||||
"prefs.shortcuts_import": "استيراد",
|
||||
"prefs.shortcuts_intro": "مفاتيح كل أمر.",
|
||||
"prefs.shortcuts_not_assigned": "غير معيّن",
|
||||
"prefs.shortcuts_press": "اضغط مفتاحاً...",
|
||||
"prefs.shortcuts_press": "اضغط مفتاحًا...",
|
||||
"prefs.single_surface_analysis": "استخدام تحليل سطح واحد",
|
||||
"prefs.single_surface_analysis_help": "عند إيقاف هذا الخيار، يمكن إبقاء تظليل السطح وتحليل الاستمرارية مفعّلين معًا. عند تشغيله، يبقى آخر تحليل تم بدؤه أو تغييره فقط.",
|
||||
"prefs.sketch.align_points": "المحاذاة مع نقاط الرسم الأخرى",
|
||||
"prefs.sketch.align_points_note": "تتحاذى الخطوط أثناء الرسم مع النقاط على المحورين معًا.",
|
||||
"prefs.sketch.auto_dimensions": "إضافة كل الأبعاد للأشكال الجديدة",
|
||||
"prefs.sketch.auto_dimensions_note": "القيم المدخلة وكل بُعد يثبّت الشكل.",
|
||||
"prefs.sketch.auto_locate": "التحديد انطلاقًا من أصل الرسم",
|
||||
"prefs.sketch.auto_locate": "تحديد الموضع من أصل الرسم",
|
||||
"prefs.sketch.auto_locate_note": "مع إضافة كل الأبعاد.",
|
||||
"prefs.sketch.center_arc": "قوس المركز يتبع المؤشر",
|
||||
"prefs.sketch.center_arc": "القوس بنقطة مركز يتبع المؤشر",
|
||||
"prefs.sketch.click_selects": "النقر يحدد",
|
||||
"prefs.sketch.conflict_colour": "لون التعارض",
|
||||
"prefs.sketch.drawing": "الرسم",
|
||||
"prefs.sketch.ellipse_axes": "رسم القطوع الناقصة مع محاورها",
|
||||
"prefs.sketch.ellipse_axes_note": "المحوران الأكبر والأصغر كخطوط مساعدة، لتحديد أبعاد أنصاف المحاور والمحاور. عند الإيقاف: القطع الناقص وحده، ويعطي D عليه نصف قطره الأصغر أينما سُحبت القيمة، مرسومًا ومُعلَّمًا بـ R كما في السابق.",
|
||||
"prefs.sketch.exit_keep": "إبقاء",
|
||||
"prefs.sketch.exit_previous": "العودة إلى العرض السابق",
|
||||
"prefs.sketch.exit_unless_rotated": "إلا إذا تم التدوير",
|
||||
@@ -2568,7 +2619,7 @@
|
||||
"prefs.sketch.prefer_free_points": "تفضيل النقاط القابلة للتحريك عند تداخلها",
|
||||
"prefs.sketch.project_options": "السؤال عن خيارات الإسقاط أولًا",
|
||||
"prefs.sketch.recent_snap": "الالتقاط عند تقاطع دليلين",
|
||||
"prefs.sketch.select_other_hidden": "اختيار آخر يشمل الهندسة المخفية",
|
||||
"prefs.sketch.select_other_hidden": "تحديد آخر يشمل الهندسة المخفية",
|
||||
"prefs.sketch.shortcut_cycles_modes": "الاختصار مجددًا يبدّل الوضع",
|
||||
"prefs.sketch.shortcut_cycles_modes_tip": "عند الإيقاف: تبدأ الأداة مجددًا.",
|
||||
"prefs.sketch.snapping_guides": "الالتقاط والأدلة",
|
||||
@@ -2650,6 +2701,10 @@
|
||||
"prefs.theme_follow_off": "إيقاف",
|
||||
"prefs.theme_follow_stop": "إيقاف الاتباع",
|
||||
"prefs.theme_import_hint": "انقر لاختيار ملف سمة، أو أفلت ملفًا هنا، أو اضغط Ctrl+V (Cmd+V على Mac) بعد نسخ سمة",
|
||||
"prefs.theme_notice_dismiss": "إخفاء",
|
||||
"prefs.theme_option_blur": "تمويه خلف اللوحات",
|
||||
"prefs.theme_option_blur_help": "تمويه خلف اللوحات شبه الشفافة، بالبكسل الفعلي. الصفر يوقف التمويه.",
|
||||
"prefs.theme_option_invalid": "القيمة المحفوظة للخيار {option} غير صالحة؛ تُستخدم القيمة الافتراضية للسمة. تبقى القيمة المحفوظة كما هي.",
|
||||
"prefs.theme_option_opacity": "العتامة",
|
||||
"prefs.theme_option_opacity_help": "اضبط عتامة اللوحات العائمة. يُطبّق حد أدنى للعتامة للحفاظ على وضوح النص فوق ألوان منطقة العرض.",
|
||||
"prefs.theme_option_tab_colours": "ألوان علامات التبويب",
|
||||
@@ -2658,6 +2713,7 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "تتغيّر ألوان اللوحات ومنطقة العرض تدريجيًا عند تبديل علامات التبويب. القيمة صفر تبدّلها فورًا.",
|
||||
"prefs.theme_options_badge": "الخيارات",
|
||||
"prefs.theme_options_more": "المزيد · {count}",
|
||||
"prefs.theme_options_newer": "تتطلب هذه السمة إصدارًا أحدث من SolidCraft. بعد الترقية، أعد استيراد السمة الأصلية لاستعادة تعريفات خياراتها.",
|
||||
"prefs.theme_options_reset": "إعادة تعيين",
|
||||
"prefs.theme_options_unavailable": "غير متاح على هذا الجهاز",
|
||||
"prefs.theme_paste": "لصق السمة",
|
||||
@@ -2686,7 +2742,7 @@
|
||||
"prefs.validation.each_step": "بعد كل خطوة",
|
||||
"prefs.validation.label": "فحص النواة الكامل",
|
||||
"prefs.validation.off": "متوقف (تجريبي)",
|
||||
"prefs.validation_note": "بعد كل خطوة: يفحص كل عملية سحب ودمج وتقويس.\nمرة لكل ميزة: يفحص ناتج كل ميزة مرة واحدة.\nمتوقف (تجريبي): لا فحص.",
|
||||
"prefs.validation_note": "بعد كل خطوة: يفحص كل عملية كنس وعملية منطقية وتقويس.\nمرة لكل ميزة: يفحص ناتج كل ميزة مرة واحدة.\nمتوقف (تجريبي): لا فحص.",
|
||||
"prefs.value_boxes_in_view": "إظهار القيم بجانب مقابضها",
|
||||
"prefs.value_boxes_in_view_tip": "مسافة البثق ونصف قطر التدوير... في مربع داخل العرض.",
|
||||
"prefs.view": "سلوك العرض",
|
||||
@@ -2721,6 +2777,12 @@
|
||||
"recovery.just_now": "الآن",
|
||||
"recovery.min_ago": "قبل {count} دقيقة",
|
||||
"recovery.never_saved": "لم يُحفظ قط",
|
||||
"recovery.open_saved": "فتح النسخة المحفوظة",
|
||||
"recovery.restore": "استعادة",
|
||||
"recovery.restore_intro": "التغييرات التي لم تُحفظ عند آخر إغلاق لهذه الصفحة محفوظة في هذا المتصفح.",
|
||||
"recovery.restore_row": "{name}، تغيّر {when}",
|
||||
"recovery.restore_title": "استعادة التصاميم غير المحفوظة؟",
|
||||
"recovery.restore_title_one": "استعادة التصميم غير المحفوظ: {name}، تغيّر {when}؟",
|
||||
"refused.change": "تعذّر تغيير {body}.",
|
||||
"refused.cut": "تعذّر قطع {body}.",
|
||||
"refused.face_curved": "أحد أوجهه المنحنية لا يمكن استخدامه في النمذجة.",
|
||||
@@ -3242,9 +3304,11 @@
|
||||
"render.frame_wide": "16:9 · عريض",
|
||||
"render.gpu_depth": "أقصى عمق للمسار",
|
||||
"render.gpu_options_help": "تُحفظ العيّنات والعمق والبذرة والتعريض مع هذا الإعداد. تُحفظ طريقة الصورة النشطة فقط.",
|
||||
"render.gpu_path_crashed": "أُغلق SolidCraft أثناء إعداده على محوّل الرسوميات هذا في المرة السابقة، على الأرجح داخل برنامج تشغيل الرسوميات، لذا لن يُعاد إعداده تلقائيًا. استخدم تتبع الأشعة على المعالج المركزي بدلًا منه، أو حاول مرة أخرى (يعيد تحديث برنامج التشغيل المحاولة أيضًا).",
|
||||
"render.gpu_path_failed": "متتبع المسارات على GPU غير متاح",
|
||||
"render.gpu_path_first_setup": "قد يستغرق الإعداد الأول على وحدة GPU هذه عدة دقائق؛ ويبقى SolidCraft قابلًا للاستخدام أثناءها. تعيد المرات اللاحقة استخدام المظللات المترجمة حيث يحتفظ بها برنامج التشغيل.",
|
||||
"render.gpu_path_preparing": "متتبع المسارات على GPU: ترجمة المظللات…",
|
||||
"render.gpu_path_retry": "حاول مرة أخرى",
|
||||
"render.gpu_seed": "البذرة العشوائية",
|
||||
"render.gpu_unavailable": "تتبع المسارات على GPU غير جاهز على هذا الجهاز.",
|
||||
"render.ground_apply": "ملاءمة الأرضية وحفظها",
|
||||
@@ -4052,6 +4116,8 @@
|
||||
"sample.plate.name": "لوح نموذجي",
|
||||
"sample.vents.about": "شقوق في نمط مستطيلي: الصفوف والأعمدة والخطوة معاملات",
|
||||
"sample.vents.name": "لوح تهوية",
|
||||
"scale.per_axis": "لكل محور",
|
||||
"scale.uniform": "موحّد",
|
||||
"search.command_keys": "Enter تشغيل Shift+Enter تشغيل مع الإبقاء مفتوحًا Esc إغلاق",
|
||||
"search.commands": "الأوامر",
|
||||
"search.design": "في هذا التصميم",
|
||||
@@ -4091,7 +4157,7 @@
|
||||
"settings.display_quality_note": "المستويات الأدق ترسم الأوجه والحواف المنحنية بنعومة أكبر وتستغرق وقتًا أطول لإنشاء الشبكة. يتغير العرض فقط، لا التصميم ولا الملفات المصدّرة.",
|
||||
"settings.double_click_components": "النقر المزدوج على مكوّن لتنشيطه",
|
||||
"settings.frame_rate_details_note": "يضيف رسمًا لأزمنة الإطارات الأخيرة مع المثلثات المرسومة فوقه، ومدرّجًا تكراريًا لأزمنة الإطارات، وما رسمته وحدة GPU. انقر معدل الإطارات في العرض لإظهارها أو إخفائها.",
|
||||
"settings.frame_rate_note": "يعرض الإطارات في الثانية في الزاوية اليمنى السفلية من العرض. أثناء تفعيله يُعاد رسم العرض باستمرار.",
|
||||
"settings.frame_rate_note": "يعرض الإطارات في الثانية في الزاوية اليمنى السفلية من العرض.",
|
||||
"settings.insert_lists_open_designs": "إدراج تصميم يسرد التصاميم المفتوحة أولًا",
|
||||
"settings.measure_axis_distances": "مسافات المحاور في القياس",
|
||||
"settings.measure_axis_distances_help": "إظهار مركّبات الأبعاد على محاور العالم بجانب المسافة الكلية أثناء فتح أداة القياس.",
|
||||
@@ -4142,6 +4208,32 @@
|
||||
"sketch.dof_left": "تبقّت {count} درجات حرية",
|
||||
"sketch.dof_left_one": "تبقّت {count} درجة حرية",
|
||||
"sketch.draw_curves": "الرسم كـ {linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "يحتاج المنحنى المغلق إلى نقطتي تقاطع لقصّه",
|
||||
"sketch.error.nothing_to_break": "لا يوجد هنا ما يمكن كسره",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` منحنى إزاحة، وزواياه تتبع منحنياته المصدر: قوّس زوايا المنحنيات المصدر أو اشطفها قبل الإزاحة، أو فك ارتباط الإزاحة أولًا",
|
||||
"sketch.error.offset_trim": "`{id}` منحنى إزاحة، وأطرافه تتبع منحنياته المصدر: عدّل المنحنيات المصدر بدلًا من ذلك، أو فك ارتباط الإزاحة أولًا",
|
||||
"sketch.hint.arc.center_point": "قوس: انقر المركز ثم البداية، ودُر في أي اتجاه وانقر النهاية، أو اكتب زاوية القوس بإشارة موجبة أو سالبة",
|
||||
"sketch.hint.arc.three_point": "قوس: انقر البداية ثم النهاية، ثم نقطة على القوس",
|
||||
"sketch.hint.arc3d": "قوس ثلاثي الأبعاد: انقر البداية ثم النهاية، ثم نقطة على القوس (يمكن أن تقع النقاط خارج مستوى الرسم)",
|
||||
"sketch.hint.break": "كسر: انقر على منحنى في الموضع الذي يُقسم عنده",
|
||||
"sketch.hint.circle.center": "دائرة: انقر المركز، ثم نقطة على الدائرة",
|
||||
"sketch.hint.constraint.generic": "قيد: اختر عناصر الرسم",
|
||||
"sketch.hint.constraint.horizontal_vertical": "أفقي/رأسي: اختر خطًا، أو نقطتين، أو نقطة وخطًا؛ اضغط Shift مع الاستمرار لاختيار منتصف خط",
|
||||
"sketch.hint.constraint.midpoint": "نقطة المنتصف: اختر نقطة وخطًا، أو خطين لتوسيط الثاني على الأول",
|
||||
"sketch.hint.dimension": "البُعد: اختر عنصرًا أو عنصرين واكتب القيمة (أو انقر لوضعه أولًا)؛ انقر على بُعد لتعديله؛ اضغط Enter للتطبيق",
|
||||
"sketch.hint.extend": "تمديد: انقر على منحنى قرب الطرف المراد تمديده (Shift: انقر منحنيين لتمديدهما معًا حتى نقطة التقائهما)",
|
||||
"sketch.hint.fallback": "انقر في الرسم",
|
||||
"sketch.hint.line": "خط: انقر النقاط؛ انقر بالزر الأيمن أو اضغط Esc للإنهاء",
|
||||
"sketch.hint.line3d": "خط ثلاثي الأبعاد: انقر نقاطًا في الفضاء؛ انقر بالزر الأيمن أو اضغط Esc للإنهاء (يمكن أن تقع النقاط خارج مستوى الرسم)",
|
||||
"sketch.hint.open_done": "تم الفتح",
|
||||
"sketch.hint.point3d": "نقطة ثلاثية الأبعاد: انقر حيث توضع النقطة ثلاثية الأبعاد",
|
||||
"sketch.hint.rectangle.center": "مستطيل من المركز: انقر المركز، ثم زاوية",
|
||||
"sketch.hint.rectangle.rounded": "مستطيل مستدير الزوايا: انقر زاويتين متقابلتين (اضغط Tab للانتقال إلى خانة نصف القطر وكتابة نصف قطر الزوايا)",
|
||||
"sketch.hint.rectangle.three_point": "مستطيل بثلاث نقاط: انقر نقطتين على أحد الأضلاع، ثم العرض",
|
||||
"sketch.hint.rectangle.two_point": "مستطيل: انقر زاويتين متقابلتين",
|
||||
"sketch.hint.slot": "شق: انقر الطرفين، ثم العرض",
|
||||
"sketch.hint.spline3d": "منحنى شريحي ثلاثي الأبعاد: انقر نقاط الملاءمة في الفضاء ثلاثي الأبعاد؛ انقر بالزر الأيمن أو اضغط Esc للإنهاء",
|
||||
"sketch.hint.trim": "قص: انقر على الجزء المراد إزالته من المنحنى، أو اسحب عبر الأجزاء المراد إزالتها",
|
||||
"sketch.make_selection": "تحويل التحديد إلى {linetype}",
|
||||
"sketch.open_ends": "مفتوحة {count}",
|
||||
"sketch.status_conflict": "تعارض القيود",
|
||||
@@ -4201,13 +4293,13 @@
|
||||
"thread.automatic_size": "حجم تلقائي",
|
||||
"thread.chamfer_depth_needed": "مطلوب لهذا الشكل",
|
||||
"thread.designation": "التسمية",
|
||||
"thread.drill_range": "مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.drill_range": "مثقاب التسنين الذي يوصي به صانعو ذكور التسنين {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.drill_range_custom": "المثقاب هو القطر الأكبر ناقص الخطوة، {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.drill_range_taper": "مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي عند نهاية السن {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.drill_range_taper": "مثقاب التسنين الذي يوصي به صانعو ذكور التسنين {drill} مم. القطر الأصغر القياسي عند نهاية السن {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.end_square": "قائمة",
|
||||
"thread.fade_short": "1.25 P (ISO 3508)",
|
||||
"thread.fade_standard": "2.5 P (ISO 3508)",
|
||||
"thread.fits_the_face": "يناسب الوجه، مثلاً {example}",
|
||||
"thread.fits_the_face": "يناسب الوجه، مثلًا {example}",
|
||||
"thread.form_bspp": "سن أنابيب G (ISO 228-1)",
|
||||
"thread.form_bspt": "سن أنابيب مخروطي R، Rc (ISO 7-1)",
|
||||
"thread.form_npt": "سن أنابيب مخروطي NPT (ASME B1.20.1)",
|
||||
@@ -4363,6 +4455,7 @@
|
||||
"ui.3d_sketch": "رسم ثلاثي الأبعاد",
|
||||
"ui.activate": "تنشيط",
|
||||
"ui.active_component": "المكوّن النشط",
|
||||
"ui.addToQuickAccess": "إضافة إلى الوصول السريع",
|
||||
"ui.add_configuration": "إضافة تكوين",
|
||||
"ui.add_parameter": "إضافة معامل",
|
||||
"ui.agent_chat": "محادثة الوكيل",
|
||||
@@ -4464,7 +4557,7 @@
|
||||
"ui.agent_chat_no_images": "هذا الوكيل لا يقبل الصور",
|
||||
"ui.agent_chat_no_picture": "لا توجد صورة في الحافظة",
|
||||
"ui.agent_chat_no_tool_call": "لم يستخدم النموذج أدوات SolidCraft، لذا لم يتغيّر التصميم. اختر نموذجًا يدعم استدعاء الأدوات؛ راجع «النماذج المحلية» في دليل محادثة الوكيل.",
|
||||
"ui.agent_chat_node_too_old": "إصدار Node.js {version} قديم جدًا لهذا الوكيل. ثبّت Node.js 20.10 أو أحدث، ثم اضغط على تحديث.",
|
||||
"ui.agent_chat_node_too_old": "إصدار Node.js {version} قديم جدًا لهذا الوكيل. ثبّت Node.js 20.10 أو أحدث، ثم اضغط تحديث.",
|
||||
"ui.agent_chat_none_found": "لم يُعثر على وكيل برمجي. ثبّت أحد هذه ثم اضغط تحديث:",
|
||||
"ui.agent_chat_none_found_short": "لم يُعثر على وكيل",
|
||||
"ui.agent_chat_not_installed": "غير مثبت",
|
||||
@@ -4505,7 +4598,7 @@
|
||||
"ui.agent_chat_send_now_tip": "إيقاف خطوة الوكيل الحالية وإرسال هذه بعدها (Ctrl+Enter)",
|
||||
"ui.agent_chat_setup_body": "يتواصل SolidCraft مع وكيل برمجة مثبّت على هذا الحاسوب. اختر واحدًا من التفضيلات › الوكيل.",
|
||||
"ui.agent_chat_setup_title": "إعداد مساعد",
|
||||
"ui.agent_chat_side": "إرساء المحادثة",
|
||||
"ui.agent_chat_side": "تثبيت المحادثة",
|
||||
"ui.agent_chat_signed_in": "سجّل الدخول",
|
||||
"ui.agent_chat_state_auth": "يلزم تسجيل الدخول",
|
||||
"ui.agent_chat_state_busy": "جارٍ العمل…",
|
||||
@@ -4570,10 +4663,10 @@
|
||||
"ui.ai_key_note": "يُخزّن في سلسلة مفاتيح النظام، وليس في إعدادات SolidCraft أبدًا. اتركه فارغًا لخادم محلي.",
|
||||
"ui.ai_keychain_error": "رفضت سلسلة مفاتيح النظام: {error}",
|
||||
"ui.ai_kind": "النوع",
|
||||
"ui.ai_mode_all": "فعل كل شيء",
|
||||
"ui.ai_mode_all": "تنفيذ كل شيء",
|
||||
"ui.ai_mode_all_note": "لا شيء يسأل أولًا. تبقى تقارير الأعطال محظورة وتظل حدود المجلدات سارية.",
|
||||
"ui.ai_mode_all_warning": "يمكن للوكيل حفظ تصميمك والكتابة فوقه وتصديره واستبداله دون سؤال. استخدم هذا فقط مع وكيل تثق به، وعلى تصاميم حفظتها.",
|
||||
"ui.ai_mode_ask": "السؤال أولاً",
|
||||
"ui.ai_mode_ask": "السؤال أولًا",
|
||||
"ui.ai_mode_ask_note": "كل تغيير في التصميم وكل إجراء على الملفات ينتظر سماحك.",
|
||||
"ui.ai_mode_edits": "إجراء تعديلات",
|
||||
"ui.ai_mode_edits_note": "أوامر النمذجة تعمل دون سؤال؛ أما الحفظ والفتح والتصدير وإجراءات الملفات أو الإجراءات الهدّامة الأخرى فتسأل.",
|
||||
@@ -4744,21 +4837,21 @@
|
||||
"ui.construction.ribbon_label": "خطوط مساعدة",
|
||||
"ui.construction.side_hint": "خط إنشائي: انقر الجهة التي يمتد نحوها الخط؛ انقر بزر الفأرة الأيمن للبدء من جديد",
|
||||
"ui.context_menu": "القائمة السياقية",
|
||||
"ui.continuity_crease": "تجعّد",
|
||||
"ui.continuity_crease": "ثنية",
|
||||
"ui.continuity_curvature": "الانحناء",
|
||||
"ui.continuity_curvature_change": "تغيّر الانحناء",
|
||||
"ui.continuity_curvature_change_tip": "تغيّر الانحناء (G3): يتغير الانحناء أيضًا بسلاسة عبر خط الالتقاء.",
|
||||
"ui.continuity_curvature_tip": "متصل الانحناء (G2): تمر الانعكاسات دون انكسار.",
|
||||
"ui.continuity_g0": "G0 تجعّد",
|
||||
"ui.continuity_g0": "G0 ثنية",
|
||||
"ui.continuity_g1": "G1 مماس",
|
||||
"ui.continuity_g2": "G2 انحناء",
|
||||
"ui.continuity_gap": "فجوة",
|
||||
"ui.continuity_position": "الموضع",
|
||||
"ui.continuity_position_tip": "الموضع (G0): تلتقي الأسطح، مع تجعّد عند التقائها.",
|
||||
"ui.continuity_position_tip": "الموضع (G0): تلتقي الأسطح، مع ثنية عند التقائها.",
|
||||
"ui.continuity_smooth_curvature_change": "تغيّر انحناء سلس",
|
||||
"ui.continuity_smooth_curvature_change_tip": "تغيّر انحناء سلس (G4): حتى طريقة تغيّر الانحناء تستمر بسلاسة عبر خط الالتقاء.",
|
||||
"ui.continuity_tangent": "مماس",
|
||||
"ui.continuity_tangent_tip": "مماس (G1): لا تجعّد، لكن الانحناء يقفز عبر خط الالتقاء.",
|
||||
"ui.continuity_tangent_tip": "مماس (G1): لا ثنية، لكن الانحناء يقفز عبر خط الالتقاء.",
|
||||
"ui.copy": "نسخ",
|
||||
"ui.copy_details": "نسخ التفاصيل",
|
||||
"ui.copy_details_tip": "انسخ الرسالة كاملة للبحث عنها أو للإبلاغ عنها.",
|
||||
@@ -4784,6 +4877,7 @@
|
||||
"ui.default_modelling_orientation": "اتجاه النمذجة الافتراضي",
|
||||
"ui.delete": "حذف",
|
||||
"ui.delete_select_first": "حدّد أولًا ما تريد حذفه: الميزات أو الأجسام أو المكوّنات أو هندسة الرسم",
|
||||
"ui.dependents_select_first": "حدّد أولًا ما تريد سرد معتمداته",
|
||||
"ui.depth": "العمق",
|
||||
"ui.design": "التصميم",
|
||||
"ui.design_properties.author": "المؤلف",
|
||||
@@ -4909,10 +5003,15 @@
|
||||
"ui.extract_done": "أصبح المكوّن تصميمًا مستقلًا، مرتبطًا هنا.",
|
||||
"ui.extrude": "بثق",
|
||||
"ui.extrude_auto_to_object": "البثق إلى هدف مختار",
|
||||
"ui.extrude_auto_to_object_tip": "وجه أو رأس أو مستوى موازٍ يغيّر المدى إلى «إلى كائن».",
|
||||
"ui.extrude_auto_to_object_tip": "وجه أو رأس أو مستوى موازٍ يغيّر الامتداد إلى «إلى كائن».",
|
||||
"ui.factor": "المعامل",
|
||||
"ui.fade": "تلاشٍ",
|
||||
"ui.fade_length": "طول التلاشي",
|
||||
"ui.fair_iterations": "عدد التكرارات",
|
||||
"ui.fair_keep_edges": "الحفاظ على الحواف",
|
||||
"ui.fair_strength": "شدة التنعيم",
|
||||
"ui.fair_surface_title": "تنعيم السطح",
|
||||
"ui.fair_tolerance_default": "0.001 من حجم الجسم",
|
||||
"ui.feature_history": "سجل الميزات",
|
||||
"ui.feature_options": "خيارات الميزة",
|
||||
"ui.features": "الميزات",
|
||||
@@ -4939,9 +5038,13 @@
|
||||
"ui.fillet_setback": "مسافة الإرجاع",
|
||||
"ui.fillet_setback_hint": "{factor} × نصف القطر",
|
||||
"ui.fillet_setback_least": "أكثر من {least}",
|
||||
"ui.fillet_setback_tip": "حيث تبدأ رقعة الزاوية الناعمة: المسافة على طول كل حافة من الزاوية. الافتراضي {factor} ضعف نصف القطر. يجب أن تكون أكبر من أقل مسافة إرجاع، حيث تلتقي الاستدارات بدونها: نصف القطر حيث تكون الأوجه متعامدة.",
|
||||
"ui.fillet_setback_tip": "حيث تبدأ رقعة الزاوية الناعمة: المسافة على طول كل حافة من الزاوية. الافتراضي {factor} ضعف نصف القطر. يجب أن تكون أكبر من أقل مسافة إرجاع، وهي التي تلتقي عندها التقويسات دون إرجاع: نصف القطر حين تكون الأوجه متعامدة.",
|
||||
"ui.fillet_smooth_radius": "تغيّر ناعم لنصف القطر",
|
||||
"ui.fillet_type_choices_tip": "نصف قطر ثابت: نصف قطر واحد على طول الحافة.\nطول الوتر: يبقى عرض التقويس نفسه مهما كانت الزاوية بين الوجوه.\nنصف قطر متغيّر: نصف قطر عند كل طرف، ومزيد على طول الحافة إن أضفته.\nمتصل الانحناء: مقطع بلا انكسار في الانعكاسات حيث يلتقي بالوجوه المستوية (G2).\nمنحنى: يتبع نصف القطر على طول الحافة أنصاف أقطار تحكّم تحدّدها.",
|
||||
"ui.fillet_turn": "الالتفاف",
|
||||
"ui.fillet_turn_mitre": "وصلة مائلة",
|
||||
"ui.fillet_turn_swept": "ملتف",
|
||||
"ui.fillet_turn_tip": "حيث يلتف تقويسان حول حافة ركن حادة بينهما، مثل التقويسات على قاعدة نتوء عند حوافه القائمة.\nوصلة مائلة: يمتد التقويسان حتى يلتقيا في وصلة مائلة.\nملتف: يتوقف التقويسان عند حافة الركن ويلتفان حولها، والركن طارة مماسة لكليهما. فقط لتقويسين بنصف قطر واحد حول حافة ركن مستقيمة ومحدبة.",
|
||||
"ui.fillet_type_choices_tip": "نصف قطر ثابت: نصف قطر واحد على طول الحافة.\nطول الوتر: يبقى عرض التقويس نفسه مهما كانت الزاوية بين الوجوه.\nنصف قطر متغيّر: نصف قطر عند كل طرف، ومزيد على طول الحافة إن أضفته.\nمتصل الانحناء: مقطع بلا انكسار في الانعكاسات حيث يلتقي بالوجوه المستوية (G2).\nمنحنى شريحي: يتبع نصف القطر على طول الحافة أنصاف أقطار تحكّم تحدّدها.",
|
||||
"ui.fillet_type_curvature": "متصل الانحناء",
|
||||
"ui.fillet_type_tip": "المقطع عبر الحافة. متصل الانحناء: مقطع متصل الانحناء (G2) بين وجوه مستوية، فتمر الانعكاسات من الوجوه إلى التقويس دون انكسار.",
|
||||
"ui.filter_commands_or_keys": "تصفية الأوامر أو المفاتيح",
|
||||
@@ -4986,6 +5089,7 @@
|
||||
"ui.heading_construct": "إنشاء مرجعي",
|
||||
"ui.heading_create": "إنشاء",
|
||||
"ui.heading_create_sketch": "إنشاء رسم",
|
||||
"ui.heading_custom_view_navigator": "متصفح العرض المخصص",
|
||||
"ui.heading_cylinder": "أسطوانة",
|
||||
"ui.heading_design": "التصميم",
|
||||
"ui.heading_draft": "ميل السحب",
|
||||
@@ -5053,7 +5157,7 @@
|
||||
"ui.hidePlanes": "إخفاء المستويات",
|
||||
"ui.hideProfile": "إخفاء المقطع",
|
||||
"ui.hide_component_history": "إخفاء سجله",
|
||||
"ui.hide_origin_by_default": "إخفاء مجلد الأصل في التصاميم الجديدة",
|
||||
"ui.hide_origin_by_default": "إخفاء مجلد نقطة الأصل في التصاميم الجديدة",
|
||||
"ui.hide_origin_note": "تبدأ التصاميم الجديدة ومجلد نقطة الأصل مخفي. يحتفظ كل تصميم بإعداد نقطة الأصل الخاص به بمجرد إظهارها أو إخفائها.",
|
||||
"ui.history_back": "الرجوع خطوة",
|
||||
"ui.history_below_browser": "أسفل المستعرض",
|
||||
@@ -5121,6 +5225,7 @@
|
||||
"ui.manipulator_free_alt": "السحب الحر للمُعالج أيضًا مع Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "عند التفعيل: Alt/Option أو Ctrl/Cmd على أسهم البثق / أداة التحويل يعطّل التقاط القيم. عند الإيقاف: Ctrl/Cmd فقط.",
|
||||
"ui.manufacture": "التصنيع",
|
||||
"ui.match_surface_title": "مطابقة السطح",
|
||||
"ui.material_review.axes": "محاور محلية للمادة؛ لم يُحدد اتجاه للجسم.",
|
||||
"ui.material_review.claims": "المصادر والتراخيص ادعاءات غير موثّقة. القيم المفقودة تبقى مجهولة.",
|
||||
"ui.material_review.compressive_strength": "مقاومة الانضغاط",
|
||||
@@ -5184,6 +5289,7 @@
|
||||
"ui.options": "الخيارات",
|
||||
"ui.orbit": "تدوير العرض",
|
||||
"ui.orbit_around": "التدوير حول",
|
||||
"ui.organize": "تنظيم",
|
||||
"ui.orientation": "الاتجاه",
|
||||
"ui.origin": "نقطة الأصل",
|
||||
"ui.palette_construction_mode": "وضع الخطوط المساعدة (X)",
|
||||
@@ -5272,6 +5378,7 @@
|
||||
"ui.quality": "الجودة",
|
||||
"ui.quantity": "الكمية",
|
||||
"ui.quantity_2": "الكمية 2",
|
||||
"ui.quick_access_full": "يتسع شريط الوصول السريع لـ {count} أمرًا؛ أزل أحدها لإضافة آخر",
|
||||
"ui.quit": "خروج",
|
||||
"ui.radial_context_menu": "قائمة سياقية دائرية",
|
||||
"ui.radius": "نصف القطر",
|
||||
@@ -5289,6 +5396,7 @@
|
||||
"ui.relink_not_linked": "هذا المكوّن غير مرتبط بتصميم مصدر.",
|
||||
"ui.relink_select_one": "حدد مكوّنًا مرتبطًا واحدًا أو إحدى نسخه أولًا.",
|
||||
"ui.relink_source_all": "إعادة ربط المصدر (كل النسخ)…",
|
||||
"ui.removeFromQuickAccess": "إزالة من الوصول السريع",
|
||||
"ui.removeFromToolbar": "إزالة من شريط الأدوات",
|
||||
"ui.removeSection": "إزالة المقطع",
|
||||
"ui.render": "التصيير",
|
||||
@@ -5297,6 +5405,8 @@
|
||||
"ui.repeat_nothing": "لا يوجد ما يُكرَّر بعد: ابدأ أداة أو مربع حوار أولًا.",
|
||||
"ui.replace_with_link_done": "أصبحت النسخ الآن مواضع للتصميم المرتبط.",
|
||||
"ui.report_a_bug": "الإبلاغ عن خطأ",
|
||||
"ui.report_characters_left_one": "الأحرف المتبقية: {count}",
|
||||
"ui.report_characters_left_other": "الأحرف المتبقية: {count}",
|
||||
"ui.reset": "إعادة تعيين",
|
||||
"ui.reset_all": "إعادة تعيين الكل",
|
||||
"ui.reset_the_settings_on_this_page": "إعادة تعيين إعدادات هذه الصفحة",
|
||||
@@ -5581,6 +5691,14 @@
|
||||
"values must be numbers or expressions": "يجب أن تكون القيم أعدادًا أو تعابير",
|
||||
"view.auto_ortho_faces": "منظوري أثناء التدوير، ومتعامد على الأوجه",
|
||||
"view.render_inspector": "مفتش التصيير",
|
||||
"view_gizmo.arrows": "أسهم التدوير",
|
||||
"view_gizmo.home": "زر العرض الرئيسي",
|
||||
"view_gizmo.no_such_body": "لا يوجد في هذا التصميم جسم بهذا الاسم.",
|
||||
"view_gizmo.note": "اختر جسمًا في العرض. يُحفظ مع تفضيلاتك وملفات تعريف الإعدادات، ويظهر في كل تصميم. انقر أحد أوجهه للنظر إليه مباشرة.",
|
||||
"view_gizmo.texture": "الخامة",
|
||||
"view_gizmo.themed": "حسب السمة",
|
||||
"view_gizmo.too_detailed": "مفصّل أكثر مما يناسب متصفح العرض (أكثر من 10,000 مثلث)",
|
||||
"view_gizmo.too_thin": "طويل ورفيع أكثر مما يناسب متصفح العرض: يجب ألا يقل أرفع جانب عن خُمس أطول جانب",
|
||||
"web.files.insert_drawings_hint": "ملفات CAD أو شبكات أو DXF أو SVG إلى التصميم المفتوح",
|
||||
"web.files.insert_stored_drawings_hint": "إلى التصميم المفتوح (ملفات CAD، شبكات، DXF، SVG)",
|
||||
"web.open_full": "فتح في SolidCraft",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Analiza dostupnosti",
|
||||
"cmd.inspect_center_of_mass": "Centar mase",
|
||||
"cmd.inspect_clearance": "Provjera zazora",
|
||||
"cmd.inspect_colored_components": "Omogući boje komponenti",
|
||||
"cmd.inspect_colored_components_disable": "Onemogući boje komponenti",
|
||||
"cmd.inspect_compare": "Uporedi tijela",
|
||||
"cmd.inspect_compare_picture": "Uporedi sliku",
|
||||
"cmd.inspect_continuity": "Analiza kontinuiteta",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Analýza přístupnosti",
|
||||
"cmd.inspect_center_of_mass": "Těžiště",
|
||||
"cmd.inspect_clearance": "Vůle",
|
||||
"cmd.inspect_colored_components": "Zapnout barvy komponent",
|
||||
"cmd.inspect_colored_components_disable": "Vypnout barvy komponent",
|
||||
"cmd.inspect_compare": "Porovnat tělesa",
|
||||
"cmd.inspect_compare_picture": "Porovnat obrázek",
|
||||
"cmd.inspect_continuity": "Analýza spojitosti",
|
||||
@@ -3809,6 +3807,8 @@
|
||||
"sample.plate.name": "Ukázková deska",
|
||||
"sample.vents.about": "Drážky v obdélníkovém poli: řádky, sloupce a rozteč jsou parametry",
|
||||
"sample.vents.name": "Větrací deska",
|
||||
"scale.per_axis": "Podle os",
|
||||
"scale.uniform": "Rovnoměrně",
|
||||
"search.command_keys": "Enter spustit Shift+Enter spustit a ponechat otevřené Esc zavřít",
|
||||
"search.commands": "Příkazy",
|
||||
"search.design": "V tomto návrhu",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Tilgængelighedsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massecentrum",
|
||||
"cmd.inspect_clearance": "Frigangskontrol",
|
||||
"cmd.inspect_colored_components": "Aktivér komponentfarver",
|
||||
"cmd.inspect_colored_components_disable": "Deaktivér komponentfarver",
|
||||
"cmd.inspect_compare": "Sammenlign emner",
|
||||
"cmd.inspect_compare_picture": "Sammenlign billede",
|
||||
"cmd.inspect_continuity": "Kontinuitetsanalyse",
|
||||
@@ -3809,6 +3807,8 @@
|
||||
"sample.plate.name": "Eksempelplade",
|
||||
"sample.vents.about": "Slidser i et rektangulært mønster: Rækker, kolonner og deling er parametre",
|
||||
"sample.vents.name": "Ventilationsplade",
|
||||
"scale.per_axis": "Per akse",
|
||||
"scale.uniform": "Ensartet",
|
||||
"search.command_keys": "Enter kør Shift+Enter kør og hold åben Esc luk",
|
||||
"search.commands": "Kommandoer",
|
||||
"search.design": "I dette design",
|
||||
|
||||
@@ -628,6 +628,8 @@
|
||||
"Suppress Selected Folders": "Ausgewählte Ordner unterdrücken",
|
||||
"Suppressed": "Unterdrückt",
|
||||
"Surface": "Flächenkörper",
|
||||
"Surface or face": "Flächenkörper oder Fläche",
|
||||
"Surface or faces": "Flächenkörper oder Flächen",
|
||||
"Surface, or faces to turn over": "Fläche oder umzudrehende Flächen",
|
||||
"Surfaces": "Flächen",
|
||||
"Swap Ends": "Enden vertauschen",
|
||||
@@ -641,6 +643,7 @@
|
||||
"Target": "Ziel",
|
||||
"Target Body": "Zielkörper",
|
||||
"Target Face (optional)": "Zielfläche (optional)",
|
||||
"Target edges": "Zielkanten",
|
||||
"Tear": "Reißen",
|
||||
"Technical details": "Technische Details",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Die gestrichelte Skizze ist eine gespeicherte Ebenenführung. Wählen Sie eine neue Fläche, um die Anbindung zu reparieren.",
|
||||
@@ -648,6 +651,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "Die Richtung muss eine Achse oder eine gerade Skizzenkurve in der Skizzenebene sein",
|
||||
"The mirror line cannot also be mirrored": "Die Spiegellinie kann nicht gleichzeitig gespiegelt werden",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "Die Spiegellinie muss eine gerade Kurve dieser Skizze sein, keine Ursprungsachse",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Die Ursprungsachse steht senkrecht auf der Skizzenebene und hat daher keine Linie in ihr",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Das gespeicherte Autorvorkommen fehlt. Wählen Sie vor dem erneuten Anbinden eine gültige Autorplatzierung.",
|
||||
"Then": "Then",
|
||||
"These features will be deleted:": "Diese Merkmale werden gelöscht:",
|
||||
@@ -960,8 +964,6 @@
|
||||
"cmd.inspect_accessibility": "Zugänglichkeitsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massenschwerpunkt",
|
||||
"cmd.inspect_clearance": "Freiraum",
|
||||
"cmd.inspect_colored_components": "Komponentenfarben einschalten",
|
||||
"cmd.inspect_colored_components_disable": "Komponentenfarben ausschalten",
|
||||
"cmd.inspect_compare": "Körper vergleichen",
|
||||
"cmd.inspect_continuity": "Kontinuitätsprüfung",
|
||||
"cmd.inspect_curvature_comb": "Krümmungskamm-Analyse",
|
||||
@@ -1291,6 +1293,7 @@
|
||||
"cmd.timeline_stop": "Verlauf anhalten",
|
||||
"cmd.timeline_suppress": "Merkmal unterdrücken",
|
||||
"cmd.ui.assistant": "Assistenten ein-/ausblenden",
|
||||
"cmd.view.navigator": "Eigener Ansichtsnavigator",
|
||||
"cmd.view.render_browser": "Render-Browser",
|
||||
"cmd.view.render_dock": "Render-Panel verschieben",
|
||||
"cmd.view.render_focus": "Render-Fokusansicht",
|
||||
@@ -1389,11 +1392,17 @@
|
||||
"dialog.computing": "Wird berechnet… {seconds} s",
|
||||
"dialog.could_not_be_checked": "nicht prüfbar",
|
||||
"dialog.custom_axis": "↖ Eigene Achse",
|
||||
"dialog.cylinder_face_tip": "Die Fläche oder der Eckpunkt des Modells, auf dem der Zylinder steht: hier klicken, dann in der Ansicht darauf klicken. Leer gelassen platzieren ihn X, Y und Z.",
|
||||
"dialog.dimension_insert_hint": "Doppelklicken Sie auf eine andere steuernde Bemaßung, um ihren Namen einzufügen",
|
||||
"dialog.dimension_name": "Bemaßung: {name}",
|
||||
"dialog.dimension_step_down": "Um {step} verringern",
|
||||
"dialog.dimension_step_expression": "Nur eine Zahl lässt sich schrittweise ändern; ein Ausdruck bleibt, wie er eingegeben ist",
|
||||
"dialog.dimension_step_up": "Um {step} erhöhen",
|
||||
"dialog.edit_title": "{title} bearbeiten",
|
||||
"dialog.no_shown_body": "Kein sichtbarer Körper zum Ändern vorhanden. Blenden Sie einen Körper ein oder wählen Sie „Neuer Körper“.",
|
||||
"dialog.pattern_sketch_not_object": "Körper oder Funktionen wählen: Eine Skizze ist keines von beiden. Ihre Kurven mit Muster oder Spiegeln in der Skizze wiederholen oder sie zuerst extrudieren.",
|
||||
"dialog.primitive_point_tip": "Ein Punkt, der X, Y und Z füllt: hier klicken, dann einen Eckpunkt, eine Kante oder Fläche des Modells, einen Punkt einer Skizze oder den Ursprung anklicken.",
|
||||
"dialog.reference_edge_tip": "Eine gerade Kante der gewählten Fläche, von der aus der Abstand darunter gemessen wird: hier klicken, dann die Kante anklicken.",
|
||||
"dialog.select": "↖ Auswählen",
|
||||
"dialog.selected_count": "↖ {count} ausgewählt",
|
||||
"dialog.selection_constraint": "Bedingung {id}",
|
||||
@@ -1415,6 +1424,7 @@
|
||||
"document.read_only_save": "Dieses Design wurde schreibgeschützt geöffnet: Speichern Sie es als neue Datei",
|
||||
"document.save_changes": "Änderungen in {name} speichern?",
|
||||
"document.save_copy": "Kopie speichern…",
|
||||
"document.tab_unsaved_changes": "Ungespeicherte Änderungen",
|
||||
"document.unsaved_changes": "{name} enthält ungespeicherte Änderungen. Wenn Sie nicht speichern, gehen sie verloren.",
|
||||
"drive.rigid": "{joint} ist ein starres Gelenk. Es hält die verbundenen Teile fest zusammen und ermöglicht daher keine Bewegung. Um die Teile beweglich zu machen, ändern Sie den Gelenktyp unter „Gelenk bearbeiten“: Drehgelenk für Drehbewegungen, Schubgelenk für lineare Bewegungen oder Zylindergelenk für beides.",
|
||||
"drive.stopped_by_contact": "Angehalten bei {value}: {a} würde {b} durchdringen",
|
||||
@@ -1435,6 +1445,9 @@
|
||||
"each rail's curves must be in one sketch": "die Kurven jeder Führungskurve müssen in derselben Skizze liegen",
|
||||
"feature computation ended without a result": "die Berechnung des Merkmals wurde ohne Ergebnis beendet",
|
||||
"file.open.drawing_filter": "Designs, CAD-Dateien, Netze und Zeichnungen",
|
||||
"file.open.portal_fallback.no_portal_service": "Die geöffnete Dateiauswahl ist nicht die des Desktops: Der Dateiauswahl-Dienst des Desktops ist nicht verfügbar.",
|
||||
"file.open.portal_fallback.no_session_bus": "Die geöffnete Dateiauswahl ist nicht die des Desktops: Es gibt keinen D-Bus-Sitzungsbus.",
|
||||
"file.open.portal_fallback.xwayland": "Die geöffnete Dateiauswahl ist nicht die des Desktops: Unter XWayland lehnt das Portal das Elternfenster ab.",
|
||||
"file_load.built": "Bisher erstellt: {built} von {total} Merkmalen.",
|
||||
"file_load.converting": "Volumenkörper {done} von {total} wird konvertiert",
|
||||
"file_load.inserting": "Geometrie wird eingefügt",
|
||||
@@ -1982,6 +1995,7 @@
|
||||
"material.selection.mixed": "Unterschiedliche Materialien",
|
||||
"measure.at": "Bei",
|
||||
"measure.item": "Element {number}",
|
||||
"measure.line_to_centre": "Linie zum Mittelpunkt",
|
||||
"measure.quick_copied": "Messwert kopiert",
|
||||
"measure.quick_copy": "Klicken, um den Messwert zu kopieren",
|
||||
"measure.quick_edges": "{count} Kanten",
|
||||
@@ -2012,6 +2026,8 @@
|
||||
"nav.blender.help": "Mittlere Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt; rechtes Ziehen lässt die Kamera unverändert",
|
||||
"nav.creo.help": "Ziehen mit der mittleren Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt beim vertikalen Ziehen (nach unten zum Vergrößern); das Mausrad zoomt",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "Ein Ziehen mit der mittleren oder rechten Taste dreht, verschiebt oder zoomt weiter wie begonnen, bis die Taste losgelassen wird, egal welche Tasten dabei gedrückt oder losgelassen werden. Aus: Die gehaltenen Tasten entscheiden in jedem Moment des Ziehens.",
|
||||
"nav.drag_keeps_mode.label": "Ziehen behält die Navigation bei, mit der es begann",
|
||||
"nav.free_orbit.help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollneigung bei. Das Einrasten mit Umschalt ist deaktiviert.",
|
||||
"nav.free_orbit.label": "Freie Drehung",
|
||||
"nav.free_orbit.snap_help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollausrichtung bei. Halten Sie beim Ziehen zum Drehen die Umschalttaste gedrückt, um Blickrichtung und Rollausrichtung einrasten zu lassen.",
|
||||
@@ -2248,6 +2264,14 @@
|
||||
"prefs.auto_operation_note": "Vereinigung, Neuer Körper oder Schnitt, je nachdem, was das Material berührt.",
|
||||
"prefs.axes": "Achsen",
|
||||
"prefs.axis": "Achse",
|
||||
"prefs.blur_cost": "Relativer Renderaufwand. Mehr Balken bedeuten meist mehr Arbeit.",
|
||||
"prefs.blur_device": "Auf diesem Grafikgerät oder Renderpfad nicht verfügbar",
|
||||
"prefs.blur_guard": "Die Unschärfequalität wurde für flüssigere Bilder verringert. Der Radius bleibt unverändert.",
|
||||
"prefs.blur_off": "Aus",
|
||||
"prefs.blur_reduce_transparency": "Durch die Systemeinstellung zur Transparenzreduzierung deaktiviert",
|
||||
"prefs.blur_software": "Mit Softwaregrafik nicht verfügbar (gpu-software)",
|
||||
"prefs.blur_source": "In dieser Arbeitsbereichsansicht nicht verfügbar",
|
||||
"prefs.blur_web": "Auf diesem Web-Renderpfad nicht verfügbar",
|
||||
"prefs.bodies_folders": "Körperordner",
|
||||
"prefs.bodies_folders_always": "Immer",
|
||||
"prefs.bodies_folders_never": "Nie",
|
||||
@@ -2261,6 +2285,8 @@
|
||||
"prefs.browser_eye_left": "Auge neben dem Aufklapppfeil",
|
||||
"prefs.browser_eye_left_note": "Browserzeilen zeigen ihr Auge zum Ein- und Ausblenden wie bisher neben dem Aufklapppfeil statt in einer Spalte am rechten Zeilenende.",
|
||||
"prefs.browser_reveal_on_hover": "Beim Überfahren einblenden",
|
||||
"prefs.browser_rows_start_closed": "Browser-Zeilen beginnen geschlossen",
|
||||
"prefs.browser_rows_start_closed_note": "Platzierungen und Ordner im Browser beginnen geschlossen, sodass ein Entwurf mit vielen Komponenten seine obersten Zeilen zeigt. Jede öffnet sich mit ihrem Pfeil, „Alle ausklappen“ oder „Im Browser suchen“, das nur die Zeilen bis zu seinem Körper öffnet und die von der letzten Suche geöffneten wieder schließt. Ein in der 3D-Ansicht gewählter Körper lässt den Browser, wie er ist. Ist es aus, beginnen die Ordner geöffnet, Platzierungen so, wie „Komponenten zu Beginn aufgeklappt“ es festlegt, und eine Auswahl in der 3D-Ansicht öffnet den Browser bis zu ihrem Körper, wie bisher.",
|
||||
"prefs.browser_show_note": "Angezeigt lässt den Browser auf dem Bildschirm. Beim Überfahren einblenden blendet ihn aus, bis der Zeiger über seinen Platz oder die schmale Lasche an seinem linken Rand kommt; er bleibt offen, solange der Zeiger darin ist oder ein Menü daraus offen ist.",
|
||||
"prefs.browser_shown": "Angezeigt",
|
||||
"prefs.built_in": "Integriert",
|
||||
@@ -2306,6 +2332,13 @@
|
||||
"prefs.dialogs_edge": "Am Rand der Ansicht",
|
||||
"prefs.dialogs_left_column": "In der linken Spalte",
|
||||
"prefs.dialogs_note": "Wo Befehlsdialoge öffnen: am Rand der Ansicht angedockt oder in der linken Spalte anstelle von Browser und Verlauf, die beim Schließen des Dialogs wie zuvor zurückkehren.",
|
||||
"prefs.dimension_step_angle": "Was die Pfeile des Fensters, Auf/Ab und das Mausrad zu einem Winkel addieren",
|
||||
"prefs.dimension_step_length": "Was die Pfeile des Fensters, Auf/Ab und das Mausrad zu einer Länge addieren",
|
||||
"prefs.dimension_steps": "Schritte im Fenster",
|
||||
"prefs.dimension_values": "Bemaßungswerte",
|
||||
"prefs.dimension_values_note": "Wo der Wert einer Bemaßung eingegeben wird. Ein Fenster hat Pfeile, die ihn schrittweise ändern; das Bemaßungswerkzeug platziert zuerst den Text.",
|
||||
"prefs.dimension_values_view": "In der Ansicht",
|
||||
"prefs.dimension_values_window": "In einem Fenster",
|
||||
"prefs.display": "Anzeige",
|
||||
"prefs.edges": "Kanten und Schattierung",
|
||||
"prefs.extend": "Erweitern",
|
||||
@@ -2394,6 +2427,8 @@
|
||||
"prefs.layout_page.card": "Karte",
|
||||
"prefs.layout_page.column": "Spalte",
|
||||
"prefs.layout_page.hover": "Bei Mauszeiger",
|
||||
"prefs.layout_page.hover_panel_menus": "Bereichsmenüs beim Darüberfahren wechseln",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Klicken Sie auf einen Bereichsnamen, um das Menü zu öffnen. Bewegen Sie den Zeiger auf einen anderen Bereichsnamen, um das Menü zu wechseln. Außerhalb des Menübands und des geöffneten Menüs schließt es sich.",
|
||||
"prefs.layout_page.icons_labels": "Symbole + Beschriftungen",
|
||||
"prefs.layout_page.ok_cancel": "OK und Abbrechen",
|
||||
"prefs.layout_page.on_hover": "Bei Mauszeiger",
|
||||
@@ -2511,6 +2546,8 @@
|
||||
"prefs.search": "Einstellungen suchen",
|
||||
"prefs.search_empty": "Keine passenden Einstellungen",
|
||||
"prefs.select": "Auswählen",
|
||||
"prefs.select_hidden_edges": "Verdeckte Kanten in der schattierten Ansicht mit verdeckten Kanten auswählen",
|
||||
"prefs.select_hidden_edges_help": "Kanten durch Flächen auswählen, wenn die schattierte Ansicht mit verdeckten Kanten aktiv ist, auch zum Verrunden. Aus behält das Auswahlverhalten von v0.30.1 bei. Ausgeblendete Körper bleiben ausgeschlossen.",
|
||||
"prefs.select_one_item": "Ein Element auswählen",
|
||||
"prefs.select_other_hidden": "Verborgene Geometrie mit „Andere auswählen“ auswählen",
|
||||
"prefs.select_other_hidden_help": "Ein: Verborgene Körper, Skizzen und verdeckte Kanten nahe dem Zeiger einbeziehen. Aus: Die ursprüngliche Liste sichtbarer Geometrie direkt unter dem Zeiger verwenden.",
|
||||
@@ -2535,6 +2572,8 @@
|
||||
"prefs.sketch.click_selects": "Klick wählt",
|
||||
"prefs.sketch.conflict_colour": "Konfliktfarbe",
|
||||
"prefs.sketch.drawing": "Zeichnen",
|
||||
"prefs.sketch.ellipse_axes": "Ellipsen mit ihren Achsen zeichnen",
|
||||
"prefs.sketch.ellipse_axes_note": "Haupt- und Nebenachse als Konstruktionslinien, damit Halbachsen und Achsen bemaßt werden können. Aus: nur die Ellipse, und D darauf ergibt ihren Nebenradius, wohin der Wert auch gezogen wird, gezeichnet und mit R beschriftet wie bisher.",
|
||||
"prefs.sketch.exit_keep": "Beibehalten",
|
||||
"prefs.sketch.exit_previous": "Zur Ansicht davor zurückkehren",
|
||||
"prefs.sketch.exit_unless_rotated": "Außer bei Drehung",
|
||||
@@ -2628,6 +2667,10 @@
|
||||
"prefs.theme_follow_off": "Aus",
|
||||
"prefs.theme_follow_stop": "Nicht mehr folgen",
|
||||
"prefs.theme_import_hint": "Klicken, um eine Designdatei zu wählen, eine hierher ziehen oder mit kopiertem Design Strg+V (Cmd+V auf dem Mac) drücken",
|
||||
"prefs.theme_notice_dismiss": "Ausblenden",
|
||||
"prefs.theme_option_blur": "Unschärfe hinter Panels",
|
||||
"prefs.theme_option_blur_help": "Unschärfe hinter durchscheinenden Panels in physischen Pixeln. Null bedeutet aus.",
|
||||
"prefs.theme_option_invalid": "Der gespeicherte Wert für {option} ist ungültig; der Designstandard wird verwendet. Der gespeicherte Wert bleibt erhalten.",
|
||||
"prefs.theme_option_opacity": "Deckkraft",
|
||||
"prefs.theme_option_opacity_help": "Deckkraft schwebender Bedienfelder anpassen. Eine Mindestdeckkraft schützt die Lesbarkeit über den Ansichtsfarben.",
|
||||
"prefs.theme_option_tab_colours": "Tabfarben",
|
||||
@@ -2636,6 +2679,7 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "Panel- und Ansichtsfarben beim Tabwechsel überblenden. Null wechselt sofort.",
|
||||
"prefs.theme_options_badge": "Optionen",
|
||||
"prefs.theme_options_more": "Mehr · {count}",
|
||||
"prefs.theme_options_newer": "Dieses Design benötigt eine neuere Version von SolidCraft. Nach dem Upgrade das Originaldesign erneut importieren, um seine Optionsdefinitionen wiederherzustellen.",
|
||||
"prefs.theme_options_reset": "Zurücksetzen",
|
||||
"prefs.theme_options_unavailable": "Auf diesem Gerät nicht verfügbar",
|
||||
"prefs.theme_paste": "Design einfügen",
|
||||
@@ -3220,9 +3264,11 @@
|
||||
"render.frame_wide": "16:9 · Breitbild",
|
||||
"render.gpu_depth": "Maximale Pfadtiefe",
|
||||
"render.gpu_options_help": "Samples, Tiefe, Startwert und Belichtung werden mit diesem Setup gespeichert. Es wird nur die aktive Bildmethode gespeichert.",
|
||||
"render.gpu_path_crashed": "SolidCraft wurde beim letzten Einrichten auf diesem Grafikadapter beendet, wahrscheinlich im Grafiktreiber, und richtet ihn deshalb nicht von selbst erneut ein. Verwenden Sie stattdessen Raytracing auf der CPU oder versuchen Sie es erneut (nach einem Treiberupdate wird es ebenfalls erneut versucht).",
|
||||
"render.gpu_path_failed": "GPU-Pathtracer nicht verfügbar",
|
||||
"render.gpu_path_first_setup": "Das erste Setup auf dieser GPU kann mehrere Minuten dauern; SolidCraft bleibt währenddessen bedienbar. Spätere Starts verwenden die kompilierten Shader erneut, sofern der Treiber sie behält.",
|
||||
"render.gpu_path_preparing": "GPU-Pathtracer: Shader werden kompiliert…",
|
||||
"render.gpu_path_retry": "Erneut versuchen",
|
||||
"render.gpu_seed": "Zufallsstartwert",
|
||||
"render.gpu_unavailable": "GPU-Pathtracing ist auf diesem Gerät nicht bereit.",
|
||||
"render.ground_apply": "Boden anpassen und speichern",
|
||||
@@ -4030,6 +4076,8 @@
|
||||
"sample.plate.name": "Beispielplatte",
|
||||
"sample.vents.about": "Langlöcher in einem Rechteckmuster: Zeilen, Spalten und Abstand sind Parameter",
|
||||
"sample.vents.name": "Lüftungsplatte",
|
||||
"scale.per_axis": "Je Achse",
|
||||
"scale.uniform": "Gleichmäßig",
|
||||
"search.command_keys": "Eingabe ausführen Umschalt+Eingabe ausführen und offen lassen Esc schließen",
|
||||
"search.commands": "Befehle",
|
||||
"search.design": "In diesem Design",
|
||||
@@ -4759,6 +4807,7 @@
|
||||
"ui.default_modelling_orientation": "Standard-Modellierungsorientierung",
|
||||
"ui.delete": "Löschen",
|
||||
"ui.delete_select_first": "Wählen Sie zuerst aus, was gelöscht werden soll: Features, Körper, Komponenten oder Skizzengeometrie",
|
||||
"ui.dependents_select_first": "Wählen Sie zuerst aus, wessen Abhängige aufgelistet werden sollen",
|
||||
"ui.depth": "Tiefe",
|
||||
"ui.design": "Design",
|
||||
"ui.design_properties.author": "Autor",
|
||||
@@ -4886,6 +4935,11 @@
|
||||
"ui.factor": "Faktor",
|
||||
"ui.fade": "Auslauf",
|
||||
"ui.fade_length": "Auslauflänge",
|
||||
"ui.fair_iterations": "Iterationen",
|
||||
"ui.fair_keep_edges": "Kanten behalten",
|
||||
"ui.fair_strength": "Stärke",
|
||||
"ui.fair_surface_title": "FLÄCHENBEGRADIGUNG",
|
||||
"ui.fair_tolerance_default": "0,001 der Körpergröße",
|
||||
"ui.feature_history": "Feature-Verlauf",
|
||||
"ui.feature_options": "Funktionsoptionen",
|
||||
"ui.features": "Funktionen",
|
||||
@@ -4914,6 +4968,10 @@
|
||||
"ui.fillet_setback_least": "Mehr als {least}",
|
||||
"ui.fillet_setback_tip": "Wo das glatte Eckflächenstück beginnt: wie weit von der Ecke entlang jeder Kante. Standard ist {factor} × der Radius. Er muss größer sein als der kleinste Rücksprung, wo sich die Rundungen ohne ihn treffen: der Radius, wo die Flächen rechtwinklig zueinander stehen.",
|
||||
"ui.fillet_smooth_radius": "Radius glatt ändern",
|
||||
"ui.fillet_turn": "Wendungen",
|
||||
"ui.fillet_turn_mitre": "Gehrung",
|
||||
"ui.fillet_turn_swept": "Geschwenkt",
|
||||
"ui.fillet_turn_tip": "Wo zwei Rundungen um eine scharfe Eckkante zwischen ihnen herumlaufen, etwa die Rundungen am Fuß eines Zapfens an seinen senkrechten Kanten.\nGehrung: Die Rundungen laufen weiter, bis sie sich in einer Gehrung treffen.\nGeschwenkt: Die Rundungen enden an der Eckkante und schwenken um sie herum; die Ecke ist ein Torus, tangential zu beiden. Nur für Rundungen mit einem Radius um eine gerade, konvexe Eckkante.",
|
||||
"ui.fillet_type_choices_tip": "Konstanter Radius: ein Radius entlang der ganzen Kante.\nSehnenlänge: die Breite der Rundung bleibt gleich, in welchem Winkel die Flächen auch aufeinandertreffen.\nVariabler Radius: ein Radius an jedem Ende und weitere entlang der Kante, wenn Sie sie hinzufügen.\nKrümmungsstetig: ein Querschnitt ohne Knick in den Spiegelungen, wo er auf ebene Flächen trifft (G2).\nSpline: der Radius entlang der Kante folgt Steuerradien, die Sie festlegen.",
|
||||
"ui.fillet_type_curvature": "Krümmungsstetig",
|
||||
"ui.fillet_type_tip": "Der Querschnitt über die Kante. Krümmungsstetig: ein krümmungsstetiger Querschnitt (G2) zwischen ebenen Flächen, sodass Spiegelungen ohne Knick von den Flächen in die Rundung laufen.",
|
||||
@@ -4959,6 +5017,7 @@
|
||||
"ui.heading_construct": "KONSTRUKTION",
|
||||
"ui.heading_create": "ERSTELLEN",
|
||||
"ui.heading_create_sketch": "SKIZZE ERSTELLEN",
|
||||
"ui.heading_custom_view_navigator": "EIGENER ANSICHTSNAVIGATOR",
|
||||
"ui.heading_cylinder": "ZYLINDER",
|
||||
"ui.heading_design": "DESIGN",
|
||||
"ui.heading_draft": "ABZIEHEN",
|
||||
@@ -5090,6 +5149,7 @@
|
||||
"ui.manipulator_free_alt": "Freies Ziehen des Manipulators auch mit Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "An: Alt/Option oder Strg/Cmd auf Extrusions-/Gizmo-Pfeilen deaktiviert das Einrasten von Werten. Aus: nur Strg/Cmd.",
|
||||
"ui.manufacture": "Fertigung",
|
||||
"ui.match_surface_title": "FLÄCHENANPASSUNG",
|
||||
"ui.material_review.axes": "Materiallokale Achsen; keine Körperausrichtung zugewiesen.",
|
||||
"ui.material_review.claims": "Quellen und Lizenzen sind ungeprüfte Angaben. Fehlende Werte bleiben unbekannt.",
|
||||
"ui.material_review.compressive_strength": "Druckfestigkeit",
|
||||
@@ -5153,6 +5213,7 @@
|
||||
"ui.options": "Optionen",
|
||||
"ui.orbit": "Drehen",
|
||||
"ui.orbit_around": "Umkreisen um",
|
||||
"ui.organize": "Organisieren",
|
||||
"ui.orientation": "Ausrichtung",
|
||||
"ui.origin": "Ursprung",
|
||||
"ui.palette_construction_mode": "Konstruktionsmodus (X)",
|
||||
@@ -5266,6 +5327,8 @@
|
||||
"ui.repeat_nothing": "Noch nichts zu wiederholen: Starten Sie zuerst ein Werkzeug oder einen Dialog.",
|
||||
"ui.replace_with_link_done": "Die Kopien sind jetzt Platzierungen des verknüpften Designs.",
|
||||
"ui.report_a_bug": "Fehler melden",
|
||||
"ui.report_characters_left_one": "{count} Zeichen übrig",
|
||||
"ui.report_characters_left_other": "{count} Zeichen übrig",
|
||||
"ui.reset": "Zurücksetzen",
|
||||
"ui.reset_all": "Alle zurücksetzen",
|
||||
"ui.reset_the_settings_on_this_page": "Einstellungen auf dieser Seite zurücksetzen",
|
||||
@@ -5547,6 +5610,14 @@
|
||||
"values must be numbers or expressions": "Werte müssen Zahlen oder Ausdrücke sein",
|
||||
"view.auto_ortho_faces": "Perspektivisch beim Drehen, orthografisch auf Flächen",
|
||||
"view.render_inspector": "Render-Inspektor",
|
||||
"view_gizmo.arrows": "Drehpfeile",
|
||||
"view_gizmo.home": "Schaltfläche „Ausgangsansicht“",
|
||||
"view_gizmo.no_such_body": "Diese Konstruktion hat keinen Körper mit diesem Namen.",
|
||||
"view_gizmo.note": "Wählen Sie einen Körper in der Ansicht. Er wird mit Ihren Einstellungen und Einstellungsprofilen gespeichert und in jeder Konstruktion angezeigt. Klicken Sie auf eine seiner Flächen, um direkt darauf zu blicken.",
|
||||
"view_gizmo.texture": "Textur",
|
||||
"view_gizmo.themed": "Design",
|
||||
"view_gizmo.too_detailed": "Zu detailliert für den Ansichtsnavigator (über 10.000 Dreiecke)",
|
||||
"view_gizmo.too_thin": "Zu lang und dünn für den Ansichtsnavigator: Die dünnste Seite muss mindestens ein Fünftel der längsten sein",
|
||||
"web.files.insert_drawings_hint": "CAD-Dateien, Netze, DXF oder SVG in das geöffnete Design",
|
||||
"web.files.insert_stored_drawings_hint": "In das geöffnete Design (CAD-Dateien, Netze, DXF, SVG)",
|
||||
"web.open_full": "In SolidCraft öffnen",
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
"cmd.component_cycle_color": "Cycle Component Colour",
|
||||
"cmd.construct_point_center": "Point At Centre Of Circle/Sphere/Torus",
|
||||
"cmd.inspect_center_of_mass": "Centre of Mass",
|
||||
"cmd.inspect_colored_components": "Enable Component Colours",
|
||||
"cmd.inspect_colored_components_disable": "Disable Component Colours",
|
||||
"cmd.inspect_colored_components": "Component Colours",
|
||||
"cmd.parameters_favorite": "Favourite Parameter",
|
||||
"cmd.render_aging_describe": "Layered Surface Aging",
|
||||
"cmd.render_effects_describe": "Lens and Creative Effects",
|
||||
@@ -69,6 +68,7 @@
|
||||
"help.solid.split_face": "Divides the selected faces along a plane, the face of another body or sketch curves, without cutting the body. Use it to give part of a face its own draft, colour or offset.",
|
||||
"help.third_party": "Third-party licences",
|
||||
"home.new_mm_hint": "Millimetres · opens in Design",
|
||||
"measure.line_to_centre": "Line to centre",
|
||||
"navbar.custom.anchor.bottom_centre": "Bottom centre",
|
||||
"navbar.custom.anchor.top_centre": "Top centre",
|
||||
"navbar.custom.customise": "Customise navigation bar…",
|
||||
@@ -86,6 +86,7 @@
|
||||
"prefs.max_file_size_web": "The web app opens STEP and import files up to 512 MB and designs up to 256 MB: a browser tab can't hold larger files. The desktop app can raise these limits.",
|
||||
"prefs.megabytes": "MB",
|
||||
"prefs.network_locations_note": "For organisations that keep designs on file servers. Off: only servers you allow.",
|
||||
"prefs.select_hidden_edges_help": "Select edges through faces when Shaded with hidden edges is active, including for Fillet. Off keeps v0.30.1 selection behaviour. Hidden bodies stay unselectable.",
|
||||
"prefs.sketch.center_arc": "Centre-point arc follows the cursor",
|
||||
"prefs.sketch.conflict_colour": "Conflict colour",
|
||||
"prefs.sketch.tracking_guides": "Centre and midpoint guides",
|
||||
@@ -234,6 +235,10 @@
|
||||
"render.studio_area_help": "Up to four one-sided rectangles. Orthogonal half-edges U and V define size; U × V faces the lit side. Coordinates are world-space millimetres.",
|
||||
"render.studio_center": "Centre X, Y, Z (mm)",
|
||||
"render.studio_hdr_help": "Radiance .hdr, 2:1 panorama, up to 2048 × 1024. EXR is unavailable. The colour assumption applies only when primaries are unspecified.",
|
||||
"sketch.hint.arc.center_point": "Arc: click centre and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.circle.center": "Circle: click the centre, then a point on the circle",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to centre the second on the first",
|
||||
"sketch.hint.rectangle.center": "Centre rectangle: click the centre, then a corner",
|
||||
"sketch_text.align_center": "Centre",
|
||||
"ui.3d_mouse_kind_cap": "Return to Centre",
|
||||
"ui.3d_mouse_looks_like_cap": "The reading stays put while you hold it, the way a Return to Centre device does.",
|
||||
@@ -261,6 +266,7 @@
|
||||
"ui.layout_compact": "Compact",
|
||||
"ui.layout_floating": "Floating",
|
||||
"ui.material_review.claims": "Sources and licences are unverified claims. Missing values remain unknown.",
|
||||
"ui.organize": "Organise",
|
||||
"ui.point_cloud_color_by": "Colour By",
|
||||
"ui.point_cloud_file_colors": "File Colours",
|
||||
"ui.point_cloud_no_file_colors": "The file has no colours",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"Bottom Right": "Bottom Right",
|
||||
"Boundary": "Boundary",
|
||||
"Bounding box": "Bounding box",
|
||||
"Bridge": "Bridge",
|
||||
"CHECK": "CHECK",
|
||||
"COMPONENTS": "COMPONENTS",
|
||||
"CONFIGURATIONS": "CONFIGURATIONS",
|
||||
@@ -628,6 +629,8 @@
|
||||
"Suppress Selected Folders": "Suppress Selected Folders",
|
||||
"Suppressed": "Suppressed",
|
||||
"Surface": "Surface",
|
||||
"Surface or face": "Surface or face",
|
||||
"Surface or faces": "Surface or faces",
|
||||
"Surface, or faces to turn over": "Surface, or faces to turn over",
|
||||
"Surfaces": "Surfaces",
|
||||
"Swap Ends": "Swap Ends",
|
||||
@@ -641,6 +644,7 @@
|
||||
"Target": "Target",
|
||||
"Target Body": "Target Body",
|
||||
"Target Face (optional)": "Target Face (optional)",
|
||||
"Target edges": "Target edges",
|
||||
"Tear": "Tear",
|
||||
"Technical details": "Technical details",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "The dashed sketch is a saved plane guide. Select a new face to repair the attachment.",
|
||||
@@ -648,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "The direction must be an axis or a straight sketch curve that lies in the sketch plane",
|
||||
"The mirror line cannot also be mirrored": "The mirror line cannot also be mirrored",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "The mirror line must be a straight curve of this sketch, not an origin axis",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "The origin axis is perpendicular to the sketch plane, so it has no line in the plane",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "The saved author occurrence is missing. Select a valid author placement before rebinding.",
|
||||
"Then": "Then",
|
||||
"These features will be deleted:": "These features will be deleted:",
|
||||
@@ -750,6 +755,8 @@
|
||||
"ai.prefs.follow_camera": "Follow the camera",
|
||||
"ai.prefs.follow_camera_off": "Automatic camera following is off.",
|
||||
"ai.prefs.follow_camera_on": "The camera turns to face sketches the agent creates.",
|
||||
"ai.prefs.instructions": "Custom instructions",
|
||||
"ai.prefs.instructions_note": "Sent to the agent at the start of every chat, after SolidCraft's own instructions: how you like to work, rules to keep to. A change applies from the next chat (Connect or New chat).",
|
||||
"ai.prefs.intro": "AI agents in SolidCraft.",
|
||||
"ai.prefs.make_default": "Make default",
|
||||
"ai.prefs.no_agents": "No agents yet. Add an agent or refresh to find installed agents.",
|
||||
@@ -963,8 +970,7 @@
|
||||
"cmd.inspect_analysis": "View Analysis",
|
||||
"cmd.inspect_center_of_mass": "Center of Mass",
|
||||
"cmd.inspect_clearance": "Clearance",
|
||||
"cmd.inspect_colored_components": "Enable Component Colors",
|
||||
"cmd.inspect_colored_components_disable": "Disable Component Colors",
|
||||
"cmd.inspect_colored_components": "Component Colors",
|
||||
"cmd.inspect_compare": "Compare Bodies",
|
||||
"cmd.inspect_compare_picture": "Compare Picture",
|
||||
"cmd.inspect_continuity": "Continuity Analysis",
|
||||
@@ -1172,7 +1178,7 @@
|
||||
"cmd.sketch_arc_tangent": "Tangent Arc",
|
||||
"cmd.sketch_arc_three_point": "3-Point Arc",
|
||||
"cmd.sketch_auto_constrain": "AutoConstrain",
|
||||
"cmd.sketch_auto_constrain_from_datum": "AutoConstrain from datum",
|
||||
"cmd.sketch_auto_constrain_from_datum": "AutoConstrain from Datum",
|
||||
"cmd.sketch_auto_project": "Auto-Project Edges on Reference",
|
||||
"cmd.sketch_blend_curve": "Blend Curve",
|
||||
"cmd.sketch_cancel": "Cancel Sketch",
|
||||
@@ -1186,7 +1192,7 @@
|
||||
"cmd.sketch_circle_two_point": "2-Point Circle",
|
||||
"cmd.sketch_circle_two_tangent": "2-Tangent Circle",
|
||||
"cmd.sketch_constrainer": "Sketch Constrainer",
|
||||
"cmd.sketch_constraint_fix": "Fix/UnFix",
|
||||
"cmd.sketch_constraint_fix": "Fix/Unfix",
|
||||
"cmd.sketch_constraint_midpoint": "MidPoint",
|
||||
"cmd.sketch_construction": "Normal/Construction",
|
||||
"cmd.sketch_copy": "Copy Sketch Entities",
|
||||
@@ -1296,6 +1302,7 @@
|
||||
"cmd.timeline_stop": "Stop History",
|
||||
"cmd.timeline_suppress": "Suppress Feature",
|
||||
"cmd.ui.assistant": "Toggle Assistant",
|
||||
"cmd.view.navigator": "Custom View Navigator",
|
||||
"cmd.view.render_browser": "Render Browser",
|
||||
"cmd.view.render_dock": "Move Render Dock",
|
||||
"cmd.view.render_focus": "Render Focus View",
|
||||
@@ -1311,13 +1318,13 @@
|
||||
"cmd.view_front": "Front View",
|
||||
"cmd.view_graphics_preset": "Graphics Preset",
|
||||
"cmd.view_grid": "Grid",
|
||||
"cmd.view_ground_shadow": "Ground shadow",
|
||||
"cmd.view_home": "Home view",
|
||||
"cmd.view_ground_shadow": "Ground Shadow",
|
||||
"cmd.view_home": "Home View",
|
||||
"cmd.view_isometric": "Isometric View",
|
||||
"cmd.view_left": "Left View",
|
||||
"cmd.view_list": "List Named Views",
|
||||
"cmd.view_navigate": "Navigate View",
|
||||
"cmd.view_navigation_bar": "Navigation bar",
|
||||
"cmd.view_navigation_bar": "Navigation Bar",
|
||||
"cmd.view_next_tab": "Next Tab",
|
||||
"cmd.view_normal_to": "Normal to Selection",
|
||||
"cmd.view_previous_tab": "Previous Tab",
|
||||
@@ -1326,10 +1333,10 @@
|
||||
"cmd.view_right": "Right View",
|
||||
"cmd.view_save": "New Named View",
|
||||
"cmd.view_shots_strip": "Shots Strip",
|
||||
"cmd.view_spin": "Continuous orbit",
|
||||
"cmd.view_tool": "Navigation tool",
|
||||
"cmd.view_spin": "Continuous Orbit",
|
||||
"cmd.view_tool": "Navigation Tool",
|
||||
"cmd.view_top": "Top View",
|
||||
"cmd.view_visual_style": "Visual style",
|
||||
"cmd.view_visual_style": "Visual Style",
|
||||
"color.blue": "Blue",
|
||||
"color.copy": "Click to copy color values",
|
||||
"color.edit": "Click to edit color",
|
||||
@@ -1394,15 +1401,22 @@
|
||||
"dialog.computing": "Computing… {seconds} s",
|
||||
"dialog.could_not_be_checked": "could not be checked",
|
||||
"dialog.custom_axis": "↖ Custom Axis",
|
||||
"dialog.cylinder_face_tip": "The face or vertex of the model the cylinder stands on: click here, then click it in the view. Left empty, X, Y and Z place it.",
|
||||
"dialog.dimension_insert_hint": "Click another driving dimension to insert its name",
|
||||
"dialog.dimension_name": "Dimension: {name}",
|
||||
"dialog.dimension_step_down": "Down by {step}",
|
||||
"dialog.dimension_step_expression": "Only a number steps; an expression stays as typed",
|
||||
"dialog.dimension_step_up": "Up by {step}",
|
||||
"dialog.edit_title": "EDIT {title}",
|
||||
"dialog.no_shown_body": "No shown body to change: show one, or choose New Body",
|
||||
"dialog.pattern_sketch_not_object": "Pick bodies or features: a sketch is neither. Repeat its curves with the sketch's own Pattern or Mirror, or extrude it first.",
|
||||
"dialog.primitive_point_tip": "A point that fills X, Y and Z: click here, then click a vertex, edge or face of the model, a point of a sketch, or the origin.",
|
||||
"dialog.reference_edge_tip": "A straight edge of the picked face to measure the distance below from: click here, then click the edge.",
|
||||
"dialog.select": "↖ Select",
|
||||
"dialog.selected_count": "↖ {count} selected",
|
||||
"dialog.selection_constraint": "Constraint {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · curve {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "Select an input to change it. Click selected objects to remove them; Ctrl/Cmd-click also toggles them. Review the preview, then press OK.",
|
||||
"dialog.selection_midpoint": "Midpoint · {id}",
|
||||
"dialog.selection_profile": "{sketch} · profile {number}",
|
||||
@@ -1420,6 +1434,7 @@
|
||||
"document.read_only_save": "This design was opened read-only: save it as a new file",
|
||||
"document.save_changes": "Save changes to {name}?",
|
||||
"document.save_copy": "Save a Copy…",
|
||||
"document.tab_unsaved_changes": "Unsaved changes",
|
||||
"document.unsaved_changes": "{name} has unsaved changes. If you don't save, they are lost.",
|
||||
"drive.rigid": "{joint} is a rigid joint: it holds its parts still, so it has nothing to drive. To let them move, change its type in Edit Joint: Revolute to rotate, Slider to slide, Cylindrical for both.",
|
||||
"drive.stopped_by_contact": "Stopped at {value}: {a} would pass through {b}",
|
||||
@@ -1440,6 +1455,9 @@
|
||||
"each rail's curves must be in one sketch": "each rail's curves must be in one sketch",
|
||||
"feature computation ended without a result": "feature computation ended without a result",
|
||||
"file.open.drawing_filter": "Designs, CAD files, meshes and drawings",
|
||||
"file.open.portal_fallback.no_portal_service": "The file chooser that opened is not the desktop's own: the desktop portal service is not available.",
|
||||
"file.open.portal_fallback.no_session_bus": "The file chooser that opened is not the desktop's own: there is no D-Bus session bus.",
|
||||
"file.open.portal_fallback.xwayland": "The file chooser that opened is not the desktop's own: under XWayland the portal refuses the parent window.",
|
||||
"file_load.built": "Built so far: {built} of {total} features.",
|
||||
"file_load.converting": "Converting solid {done} of {total}",
|
||||
"file_load.inserting": "Inserting geometry",
|
||||
@@ -1600,7 +1618,7 @@
|
||||
"help.parts.resize": "Changes the size or length of an inserted standard part and rebuilds it; its joints keep it seated. Use it when a screw turns out too long or a hole needs another size.",
|
||||
"help.plastic.assign_rule": "Assigns a plastic rule to bodies, so their plastic features take its draft and clearance as defaults. Use it to set up a moulded part once for all its features.",
|
||||
"help.plastic.boss": "Adds a screw boss on a face: a post with an optional hole, draft, root fillet and supporting ribs. Use it for screw points in moulded or printed housings.",
|
||||
"help.plastic.lip": "Adds a lip or a matching groove along the rim of a shelled body. Use it so that two housing halves locate on each other and close tightly.",
|
||||
"help.plastic.lip": "Adds a lip or a matching groove along the rim of a shelled body (Shell a face open first, then pick the rim around that opening). Use it so that two housing halves locate on each other and close tightly.",
|
||||
"help.plastic.manage_rules": "Creates and edits plastic rules: material, wall thickness, radii, clearance and draft. Use them to keep the features of a moulded part consistent.",
|
||||
"help.plastic.rest": "Adds a rest on a face: a round or rectangular pad, solid or hollow, with draft. Use it as a support or standoff in a moulded housing.",
|
||||
"help.plastic.snap_fit": "Adds a snap-fit hook on a face, with its length, thickness and catch. Use it to clip parts together without screws.",
|
||||
@@ -1999,6 +2017,7 @@
|
||||
"material.selection.mixed": "Different materials",
|
||||
"measure.at": "At",
|
||||
"measure.item": "Item {number}",
|
||||
"measure.line_to_centre": "Line to center",
|
||||
"measure.quick_copied": "Measurement copied",
|
||||
"measure.quick_copy": "Click to copy the measurement",
|
||||
"measure.quick_edges": "{count} edges",
|
||||
@@ -2029,6 +2048,8 @@
|
||||
"nav.blender.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone",
|
||||
"nav.creo.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle drag vertically zooms (down to zoom in); scroll the wheel to zoom",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "A middle or right drag goes on orbiting, panning or zooming as it began until the button is released, whatever keys are pressed or let go meanwhile. Off: the keys held decide at every moment of the drag.",
|
||||
"nav.drag_keeps_mode.label": "A drag keeps the navigation it started with",
|
||||
"nav.free_orbit.help": "Mouse and 3D mouse rotate through both poles and preserve roll. Shift axis snapping is disabled.",
|
||||
"nav.free_orbit.label": "Free orbit",
|
||||
"nav.free_orbit.snap_help": "Mouse and 3D mouse rotate through both poles and preserve roll. Hold Shift during an orbit drag to snap the view direction and roll.",
|
||||
@@ -2265,6 +2286,17 @@
|
||||
"prefs.auto_operation_note": "Join, New Body or Cut, from what the material touches.",
|
||||
"prefs.axes": "Axes",
|
||||
"prefs.axis": "Axis",
|
||||
"prefs.background_choose": "Choose picture…",
|
||||
"prefs.background_image": "Picture",
|
||||
"prefs.background_unreadable": "Could not read the background picture",
|
||||
"prefs.blur_cost": "Relative rendering cost. More bars usually mean more work.",
|
||||
"prefs.blur_device": "Unavailable on this graphics device or rendering path",
|
||||
"prefs.blur_guard": "Blur quality was reduced to keep frames responsive. The radius is unchanged.",
|
||||
"prefs.blur_off": "Off",
|
||||
"prefs.blur_reduce_transparency": "Disabled by the system's Reduce transparency setting",
|
||||
"prefs.blur_software": "Unavailable with software graphics (gpu-software)",
|
||||
"prefs.blur_source": "Unavailable in this workspace view",
|
||||
"prefs.blur_web": "Unavailable on this web rendering path",
|
||||
"prefs.bodies_folders": "Bodies folders",
|
||||
"prefs.bodies_folders_always": "Always",
|
||||
"prefs.bodies_folders_never": "Never",
|
||||
@@ -2278,6 +2310,8 @@
|
||||
"prefs.browser_eye_left": "Eye beside the fold arrow",
|
||||
"prefs.browser_eye_left_note": "Browser rows show their show/hide eye next to the fold arrow, as before, instead of in a column at the right end of the rows.",
|
||||
"prefs.browser_reveal_on_hover": "Reveal on hover",
|
||||
"prefs.browser_rows_start_closed": "Browser rows start closed",
|
||||
"prefs.browser_rows_start_closed_note": "Placements and folders in the Browser start closed, so a design with many components shows its top-level rows. Each opens with its arrow, Expand All or Find in Browser, which opens just the rows down to its body and closes again the ones the last find opened. A body picked in the 3D view leaves the Browser as it is. When off, folders start open, placements start as Components start open says, and a pick in the 3D view opens the Browser down to its body, as before.",
|
||||
"prefs.browser_show_note": "Shown keeps the browser on screen. Reveal on hover keeps it out of sight until the pointer comes over its place or the thin tab at its left edge, and it stays open while the pointer is inside it or a menu from it is open.",
|
||||
"prefs.browser_shown": "Shown",
|
||||
"prefs.built_in": "Built in",
|
||||
@@ -2323,6 +2357,13 @@
|
||||
"prefs.dialogs_edge": "At the view's edge",
|
||||
"prefs.dialogs_left_column": "In the left column",
|
||||
"prefs.dialogs_note": "Where command dialogs open: docked at the view's edge, or in the left column in place of the browser and the History, which come back as they were when the dialog closes.",
|
||||
"prefs.dimension_step_angle": "What the window's arrows, Up/Down and the wheel add to an angle",
|
||||
"prefs.dimension_step_length": "What the window's arrows, Up/Down and the wheel add to a length",
|
||||
"prefs.dimension_steps": "Window steps",
|
||||
"prefs.dimension_values": "Dimension values",
|
||||
"prefs.dimension_values_note": "Where you type a dimension's value. A window has arrows that step it; the Dimension tool places the text first.",
|
||||
"prefs.dimension_values_view": "In the view",
|
||||
"prefs.dimension_values_window": "In a window",
|
||||
"prefs.display": "Display",
|
||||
"prefs.edges": "Edges and shading",
|
||||
"prefs.extend": "Extend",
|
||||
@@ -2402,6 +2443,10 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "The two curved arrows at the cube's corner roll the view within the screen plane. On: the clockwise arrow rolls counterclockwise and the counterclockwise arrow rolls clockwise. Off: each arrow rolls the view the way it curls.",
|
||||
"prefs.invert_navigator_arrows": "Invert the view cube rotation arrows",
|
||||
"prefs.invert_navigator_arrows_note": "The arrows around the cube orbit the camera the way they point, as in Fusion and Inventor: the arrow on the left points right and swings the camera right. Off: an arrow rotates the cube toward the side it sits on, so the left arrow swings the camera left. The curved roll arrows are not affected.",
|
||||
"prefs.keep_unsaved_also_asks": "Still ask when the tab closes",
|
||||
"prefs.keep_unsaved_also_asks_tip": "With this off, the tab closes without the browser's box once every unsaved design's copy is current.",
|
||||
"prefs.keep_unsaved_in_browser": "Keep unsaved work in this browser",
|
||||
"prefs.keep_unsaved_in_browser_tip": "Each open design's unsaved changes are kept up to date in this browser's private storage. The next visit offers to restore them.",
|
||||
"prefs.keep_work_on_tab_switch": "Keep dialogs and tools open when switching designs",
|
||||
"prefs.keep_work_on_tab_switch_note": "Each design tab keeps its open dialog or tool, with what you picked and typed in it, and its toolbar tab. They are there again when you come back to the design. Off: switching designs cancels them and shows the Model tab.",
|
||||
"prefs.labels": "Labels",
|
||||
@@ -2413,6 +2458,8 @@
|
||||
"prefs.layout_page.card": "Card",
|
||||
"prefs.layout_page.column": "Column",
|
||||
"prefs.layout_page.hover": "Hover",
|
||||
"prefs.layout_page.hover_panel_menus": "Switch panel menus on hover",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Click a ribbon panel name to open its menu. Hover another panel name to switch. Move outside the ribbon and open menu to close it.",
|
||||
"prefs.layout_page.icons_labels": "Icons + labels",
|
||||
"prefs.layout_page.ok_cancel": "OK and Cancel",
|
||||
"prefs.layout_page.on_hover": "On hover",
|
||||
@@ -2533,6 +2580,8 @@
|
||||
"prefs.section_hatch_by_part": "By part",
|
||||
"prefs.section_hatch_one": "One style",
|
||||
"prefs.select": "Select",
|
||||
"prefs.select_hidden_edges": "Select hidden edges in Shaded with hidden edges",
|
||||
"prefs.select_hidden_edges_help": "Select edges through faces when Shaded with hidden edges is active, including for Fillet. Off keeps v0.30.1 selection behavior. Hidden bodies stay unselectable.",
|
||||
"prefs.select_one_item": "Select one item",
|
||||
"prefs.select_other_hidden": "Select hidden geometry with Select Other",
|
||||
"prefs.select_other_hidden_help": "On: include hidden bodies, sketches and occluded edges near the pointer. Off: use the original list of visible geometry under the pointer.",
|
||||
@@ -2557,6 +2606,8 @@
|
||||
"prefs.sketch.click_selects": "Click selects",
|
||||
"prefs.sketch.conflict_colour": "Conflict color",
|
||||
"prefs.sketch.drawing": "Drawing",
|
||||
"prefs.sketch.ellipse_axes": "Draw ellipses with their axes",
|
||||
"prefs.sketch.ellipse_axes_note": "The major and minor axes as construction lines, so the half-axes and axes can be dimensioned. Off: the ellipse alone, and D on it gives its minor radius wherever the value is pulled, drawn and labelled R as before.",
|
||||
"prefs.sketch.exit_keep": "Keep",
|
||||
"prefs.sketch.exit_previous": "Return to the view before",
|
||||
"prefs.sketch.exit_unless_rotated": "Unless turned",
|
||||
@@ -2650,6 +2701,10 @@
|
||||
"prefs.theme_follow_off": "Off",
|
||||
"prefs.theme_follow_stop": "Stop following",
|
||||
"prefs.theme_import_hint": "Click to choose a theme file, drop one here, or press Ctrl+V (Cmd+V on a Mac) with a theme copied",
|
||||
"prefs.theme_notice_dismiss": "Dismiss",
|
||||
"prefs.theme_option_blur": "Blur behind panels",
|
||||
"prefs.theme_option_blur_help": "Blur behind translucent panels, in physical pixels. Zero is off.",
|
||||
"prefs.theme_option_invalid": "Saved value for {option} is invalid; using the theme default. The saved value is kept.",
|
||||
"prefs.theme_option_opacity": "Opacity",
|
||||
"prefs.theme_option_opacity_help": "Adjust floating panel opacity. A readability floor protects text over viewport colors.",
|
||||
"prefs.theme_option_tab_colours": "Tab colors",
|
||||
@@ -2658,6 +2713,7 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "Fade panel and viewport colors when switching tabs. Zero switches instantly.",
|
||||
"prefs.theme_options_badge": "Options",
|
||||
"prefs.theme_options_more": "More · {count}",
|
||||
"prefs.theme_options_newer": "This theme requires a newer SolidCraft. Upgrade, then reimport the original theme to recover its option definitions.",
|
||||
"prefs.theme_options_reset": "Reset",
|
||||
"prefs.theme_options_unavailable": "Unavailable on this device",
|
||||
"prefs.theme_paste": "Paste theme",
|
||||
@@ -2721,6 +2777,12 @@
|
||||
"recovery.just_now": "just now",
|
||||
"recovery.min_ago": "{count} min ago",
|
||||
"recovery.never_saved": "never saved",
|
||||
"recovery.open_saved": "Open saved version",
|
||||
"recovery.restore": "Restore",
|
||||
"recovery.restore_intro": "Changes that weren't saved when this page last closed are kept in this browser.",
|
||||
"recovery.restore_row": "{name}, changed {when}",
|
||||
"recovery.restore_title": "Restore unsaved designs?",
|
||||
"recovery.restore_title_one": "Restore unsaved design: {name}, changed {when}?",
|
||||
"refused.change": "Couldn't change {body}.",
|
||||
"refused.cut": "Couldn't cut {body}.",
|
||||
"refused.face_curved": "One of its curved faces can't be used for modelling.",
|
||||
@@ -3242,9 +3304,11 @@
|
||||
"render.frame_wide": "16:9 · Widescreen",
|
||||
"render.gpu_depth": "Maximum path depth",
|
||||
"render.gpu_options_help": "Samples, depth, seed and exposure are saved with this setup. Only the active image method is saved.",
|
||||
"render.gpu_path_crashed": "SolidCraft closed while setting it up on this graphics adapter last time, most likely inside the graphics driver, so it is not set up again by itself. Choose CPU ray tracing instead, or try again (a driver update also tries again).",
|
||||
"render.gpu_path_failed": "GPU path tracer unavailable",
|
||||
"render.gpu_path_first_setup": "The first setup on this GPU can take several minutes; SolidCraft stays usable meanwhile. Later starts reuse the compiled shaders where the driver keeps them.",
|
||||
"render.gpu_path_preparing": "GPU path tracer: compiling shaders…",
|
||||
"render.gpu_path_retry": "Try again",
|
||||
"render.gpu_seed": "Random seed",
|
||||
"render.gpu_unavailable": "GPU path tracing is not ready on this device.",
|
||||
"render.ground_apply": "Fit and save ground",
|
||||
@@ -4052,6 +4116,8 @@
|
||||
"sample.plate.name": "Sample Plate",
|
||||
"sample.vents.about": "Slots in a rectangular pattern: rows, columns and pitch are parameters",
|
||||
"sample.vents.name": "Vent Plate",
|
||||
"scale.per_axis": "Per axis",
|
||||
"scale.uniform": "Uniform",
|
||||
"search.command_keys": "Enter run Shift+Enter run and keep open Esc close",
|
||||
"search.commands": "Commands",
|
||||
"search.design": "In this design",
|
||||
@@ -4142,6 +4208,32 @@
|
||||
"sketch.dof_left": "{count} degrees of freedom left",
|
||||
"sketch.dof_left_one": "{count} degree of freedom left",
|
||||
"sketch.draw_curves": "Draw as {linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "Convert the selection to {linetype}",
|
||||
"sketch.open_ends": "Open {count}",
|
||||
"sketch.status_conflict": "constraints conflict",
|
||||
@@ -4363,6 +4455,7 @@
|
||||
"ui.3d_sketch": "3D Sketch",
|
||||
"ui.activate": "Activate",
|
||||
"ui.active_component": "Active component",
|
||||
"ui.addToQuickAccess": "Add to Quick Access",
|
||||
"ui.add_configuration": "Add configuration",
|
||||
"ui.add_parameter": "Add parameter",
|
||||
"ui.agent_chat": "Agent chat",
|
||||
@@ -4784,6 +4877,7 @@
|
||||
"ui.default_modelling_orientation": "Default modeling orientation",
|
||||
"ui.delete": "Delete",
|
||||
"ui.delete_select_first": "Select what to delete first: features, bodies, components or sketch geometry",
|
||||
"ui.dependents_select_first": "Select what to list the dependents of first",
|
||||
"ui.depth": "Depth",
|
||||
"ui.design": "Design",
|
||||
"ui.design_properties.author": "Author",
|
||||
@@ -4913,6 +5007,11 @@
|
||||
"ui.factor": "Factor",
|
||||
"ui.fade": "Fade",
|
||||
"ui.fade_length": "Fade Length",
|
||||
"ui.fair_iterations": "Iterations",
|
||||
"ui.fair_keep_edges": "Keep edges",
|
||||
"ui.fair_strength": "Strength",
|
||||
"ui.fair_surface_title": "FAIR SURFACE",
|
||||
"ui.fair_tolerance_default": "0.001 of body size",
|
||||
"ui.feature_history": "Feature history",
|
||||
"ui.feature_options": "Feature Options",
|
||||
"ui.features": "Features",
|
||||
@@ -4941,6 +5040,10 @@
|
||||
"ui.fillet_setback_least": "More than {least}",
|
||||
"ui.fillet_setback_tip": "Where the smooth corner patch starts: how far along each edge from the corner. The default is {factor} × the radius. It must be more than the least setback, where the rounds meet without one: the radius where the faces are square to each other.",
|
||||
"ui.fillet_smooth_radius": "Smooth radius change",
|
||||
"ui.fillet_turn": "Turns",
|
||||
"ui.fillet_turn_mitre": "Mitre",
|
||||
"ui.fillet_turn_swept": "Swept",
|
||||
"ui.fillet_turn_tip": "Where two rounds turn round a sharp corner edge between them, such as the rounds along a boss's base at its upright edges.\nMitre: the rounds run on until they meet in a mitre.\nSwept: the rounds stop at the corner edge and turn round it, the corner a torus tangent to both. Only for rounds of one radius round a straight, convex corner edge.",
|
||||
"ui.fillet_type_choices_tip": "Constant radius: one radius all along the edge.\nChord length: the width across the round stays the same, whatever angle the faces meet at.\nVariable radius: a radius at each end, and more along the edge if you add them.\nCurvature continuous: a section without a kink in reflections where it meets flat faces (G2).\nSpline: the radius along the edge follows control radii you set.",
|
||||
"ui.fillet_type_curvature": "Curvature continuous",
|
||||
"ui.fillet_type_tip": "The section across the edge. Curvature continuous: a curvature-continuous (G2) section between planar faces, so reflections run from the faces into the round without a kink.",
|
||||
@@ -4986,6 +5089,7 @@
|
||||
"ui.heading_construct": "CONSTRUCT",
|
||||
"ui.heading_create": "CREATE",
|
||||
"ui.heading_create_sketch": "CREATE SKETCH",
|
||||
"ui.heading_custom_view_navigator": "CUSTOM VIEW NAVIGATOR",
|
||||
"ui.heading_cylinder": "CYLINDER",
|
||||
"ui.heading_design": "DESIGN",
|
||||
"ui.heading_draft": "DRAFT",
|
||||
@@ -5121,6 +5225,7 @@
|
||||
"ui.manipulator_free_alt": "Manipulator free drag also on Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "On: Alt/Option or Ctrl/Cmd on extrude / gizmo arrows disables value snapping. Off: Ctrl/Cmd only.",
|
||||
"ui.manufacture": "Manufacture",
|
||||
"ui.match_surface_title": "MATCH SURFACE",
|
||||
"ui.material_review.axes": "Material-local axes; no body orientation is assigned.",
|
||||
"ui.material_review.claims": "Sources and licenses are unverified claims. Missing values remain unknown.",
|
||||
"ui.material_review.compressive_strength": "Compressive strength",
|
||||
@@ -5184,6 +5289,7 @@
|
||||
"ui.options": "Options",
|
||||
"ui.orbit": "Orbit",
|
||||
"ui.orbit_around": "Orbit around",
|
||||
"ui.organize": "Organize",
|
||||
"ui.orientation": "Orientation",
|
||||
"ui.origin": "Origin",
|
||||
"ui.palette_construction_mode": "Construction mode (X)",
|
||||
@@ -5272,6 +5378,7 @@
|
||||
"ui.quality": "Quality",
|
||||
"ui.quantity": "Quantity",
|
||||
"ui.quantity_2": "Quantity 2",
|
||||
"ui.quick_access_full": "The quick-access bar holds {count} commands; remove one to add another",
|
||||
"ui.quit": "Quit",
|
||||
"ui.radial_context_menu": "Radial context menu",
|
||||
"ui.radius": "Radius",
|
||||
@@ -5289,6 +5396,7 @@
|
||||
"ui.relink_not_linked": "This component is not linked to a source design.",
|
||||
"ui.relink_select_one": "Select one linked component or one of its instances first.",
|
||||
"ui.relink_source_all": "Relink Source (All Instances)…",
|
||||
"ui.removeFromQuickAccess": "Remove from Quick Access",
|
||||
"ui.removeFromToolbar": "Remove from Toolbar",
|
||||
"ui.removeSection": "Remove Section",
|
||||
"ui.render": "Render",
|
||||
@@ -5297,6 +5405,8 @@
|
||||
"ui.repeat_nothing": "Nothing to repeat yet: start a tool or dialog first.",
|
||||
"ui.replace_with_link_done": "The copies are now placements of the linked design.",
|
||||
"ui.report_a_bug": "Report a bug",
|
||||
"ui.report_characters_left_one": "{count} character left",
|
||||
"ui.report_characters_left_other": "{count} characters left",
|
||||
"ui.reset": "Reset",
|
||||
"ui.reset_all": "Reset All",
|
||||
"ui.reset_the_settings_on_this_page": "Reset the settings on this page",
|
||||
@@ -5581,6 +5691,14 @@
|
||||
"values must be numbers or expressions": "values must be numbers or expressions",
|
||||
"view.auto_ortho_faces": "Perspective while orbiting, ortho on faces",
|
||||
"view.render_inspector": "Render Inspector",
|
||||
"view_gizmo.arrows": "Rotation arrows",
|
||||
"view_gizmo.home": "Home button",
|
||||
"view_gizmo.no_such_body": "This design has no body of that name.",
|
||||
"view_gizmo.note": "Pick a body in the view. It is kept with your preferences and settings profiles, and shown in every design. Click one of its faces to look straight at it.",
|
||||
"view_gizmo.texture": "Texture",
|
||||
"view_gizmo.themed": "Themed",
|
||||
"view_gizmo.too_detailed": "Too detailed for the view navigator (over 10,000 triangles)",
|
||||
"view_gizmo.too_thin": "Too long and thin for the view navigator: its thinnest side must be at least a fifth of its longest",
|
||||
"web.files.insert_drawings_hint": "CAD files, meshes, DXF or SVG into the open design",
|
||||
"web.files.insert_stored_drawings_hint": "Into the open design (CAD files, meshes, DXF, SVG)",
|
||||
"web.open_full": "Open in SolidCraft",
|
||||
|
||||
@@ -956,8 +956,6 @@
|
||||
"cmd.inspect_accessibility": "Analyse d'accessibilité",
|
||||
"cmd.inspect_center_of_mass": "Centre de gravité",
|
||||
"cmd.inspect_clearance": "Jeu",
|
||||
"cmd.inspect_colored_components": "Activer les couleurs des composants",
|
||||
"cmd.inspect_colored_components_disable": "Désactiver les couleurs des composants",
|
||||
"cmd.inspect_compare": "Comparer les corps",
|
||||
"cmd.inspect_compare_picture": "Comparer les images",
|
||||
"cmd.inspect_continuity": "Analyse de continuité",
|
||||
@@ -1365,11 +1363,14 @@
|
||||
"dialog.computing": "Calcul… {seconds} s",
|
||||
"dialog.could_not_be_checked": "non vérifiable",
|
||||
"dialog.custom_axis": "↖ Axe personnalisé",
|
||||
"dialog.cylinder_face_tip": "La face ou le sommet du modèle sur lequel le cylindre est posé : cliquer ici, puis dessus dans la vue. Laissé vide, X, Y et Z le placent.",
|
||||
"dialog.dimension_insert_hint": "Double-cliquez sur une autre cote pilotante pour insérer son nom",
|
||||
"dialog.dimension_name": "Cote : {name}",
|
||||
"dialog.edit_title": "MODIFIER {title}",
|
||||
"dialog.no_shown_body": "Aucun corps affiché à modifier : affichez-en un ou choisissez Nouveau corps",
|
||||
"dialog.pattern_sketch_not_object": "Choisissez des corps ou des fonctions : une esquisse n'est ni l'un ni l'autre. Répétez ses courbes avec la Répétition ou la Symétrie de l'esquisse, ou extrudez-la d'abord.",
|
||||
"dialog.primitive_point_tip": "Un point qui remplit X, Y et Z : cliquer ici, puis sur un sommet, une arête ou une face du modèle, sur un point d'une esquisse ou sur l'origine.",
|
||||
"dialog.reference_edge_tip": "Une arête droite de la face choisie, depuis laquelle la distance ci-dessous est mesurée : cliquer ici, puis sur l'arête.",
|
||||
"dialog.select": "↖ Sélectionner",
|
||||
"dialog.selected_count": "↖ {count} sélectionné(s)",
|
||||
"dialog.selection_constraint": "Contrainte {id}",
|
||||
@@ -1391,6 +1392,7 @@
|
||||
"document.read_only_save": "Cette conception a été ouverte en lecture seule : enregistrez-la dans un nouveau fichier",
|
||||
"document.save_changes": "Enregistrer les modifications de {name} ?",
|
||||
"document.save_copy": "Enregistrer une copie…",
|
||||
"document.tab_unsaved_changes": "Modifications non enregistrées",
|
||||
"document.unsaved_changes": "{name} contient des modifications non enregistrées. Si vous n'enregistrez pas, elles seront perdues.",
|
||||
"drive.rigid": "{joint} est une liaison rigide : elle maintient ses pièces immobiles, il n'y a donc rien à piloter. Pour les laisser bouger, changer son type dans Modifier la liaison : Revolute pour tourner, Slider pour coulisser, Cylindrical pour les deux.",
|
||||
"drive.stopped_by_contact": "Arrêté à {value} : {a} traverserait {b}",
|
||||
@@ -3000,9 +3002,11 @@
|
||||
"render.frame_wide": "16:9 · Écran large",
|
||||
"render.gpu_depth": "Profondeur maximale des chemins",
|
||||
"render.gpu_options_help": "Les échantillons, la profondeur, la graine et l'exposition sont enregistrés avec ce préréglage. Seule la méthode d'image active est enregistrée.",
|
||||
"render.gpu_path_crashed": "SolidCraft s’est fermé pendant sa dernière préparation sur cet adaptateur graphique, très probablement dans le pilote graphique ; il n’est donc plus préparé de lui-même. Utilisez le lancer de rayons sur le CPU, ou réessayez (une mise à jour du pilote réessaie aussi).",
|
||||
"render.gpu_path_failed": "Traceur de chemins GPU indisponible",
|
||||
"render.gpu_path_first_setup": "La première initialisation sur ce GPU peut prendre plusieurs minutes ; SolidCraft reste utilisable pendant ce temps. Les démarrages suivants réutilisent les shaders compilés lorsque le pilote les conserve.",
|
||||
"render.gpu_path_preparing": "Traceur de chemins GPU : compilation des shaders…",
|
||||
"render.gpu_path_retry": "Réessayer",
|
||||
"render.gpu_seed": "Graine aléatoire",
|
||||
"render.gpu_unavailable": "Le tracé de chemins GPU n'est pas prêt sur cet appareil.",
|
||||
"render.ground_apply": "Ajuster et enregistrer le sol",
|
||||
@@ -3810,6 +3814,8 @@
|
||||
"sample.plate.name": "Plaque d'exemple",
|
||||
"sample.vents.about": "Rainures en répétition rectangulaire : lignes, colonnes et pas sont des paramètres",
|
||||
"sample.vents.name": "Plaque d'aération",
|
||||
"scale.per_axis": "Par axe",
|
||||
"scale.uniform": "Uniforme",
|
||||
"search.command_keys": "Entrée exécuter Maj+Entrée exécuter et garder ouvert Échap fermer",
|
||||
"search.commands": "Commandes",
|
||||
"search.design": "Dans cette conception",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Análise de accesibilidade",
|
||||
"cmd.inspect_center_of_mass": "Centro de masas",
|
||||
"cmd.inspect_clearance": "Folga",
|
||||
"cmd.inspect_colored_components": "Activar as cores dos compoñentes",
|
||||
"cmd.inspect_colored_components_disable": "Desactivar as cores dos compoñentes",
|
||||
"cmd.inspect_compare": "Comparar corpos",
|
||||
"cmd.inspect_compare_picture": "Comparar a imaxe",
|
||||
"cmd.inspect_continuity": "Análise de continuidade",
|
||||
@@ -3809,6 +3807,8 @@
|
||||
"sample.plate.name": "Placa de exemplo",
|
||||
"sample.vents.about": "Ranuras nun patrón rectangular: filas, columnas e paso son parámetros",
|
||||
"sample.vents.name": "Placa de ventilación",
|
||||
"scale.per_axis": "Por eixo",
|
||||
"scale.uniform": "Uniforme",
|
||||
"search.command_keys": "Intro executar Maiús+Intro executar e manter aberto Esc pechar",
|
||||
"search.commands": "Ordes",
|
||||
"search.design": "Neste deseño",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"Bottom Right": "למטה מימין",
|
||||
"Boundary": "גבול",
|
||||
"Bounding box": "תיבה תוחמת",
|
||||
"Bridge": "גשר",
|
||||
"CHECK": "בדיקה",
|
||||
"COMPONENTS": "רכיבים",
|
||||
"CONFIGURATIONS": "תצורות",
|
||||
@@ -628,6 +629,8 @@
|
||||
"Suppress Selected Folders": "השבתת התיקיות שנבחרו",
|
||||
"Suppressed": "מושבת",
|
||||
"Surface": "משטח",
|
||||
"Surface or face": "משטח או פאה",
|
||||
"Surface or faces": "משטח או פאות",
|
||||
"Surface, or faces to turn over": "משטח, או פאות להיפוך",
|
||||
"Surfaces": "משטחים",
|
||||
"Swap Ends": "החלפת קצוות",
|
||||
@@ -641,6 +644,7 @@
|
||||
"Target": "יעד",
|
||||
"Target Body": "גוף יעד",
|
||||
"Target Face (optional)": "פאת יעד (אופציונלי)",
|
||||
"Target edges": "מקצועות יעד",
|
||||
"Tear": "קריעה",
|
||||
"Technical details": "פרטים טכניים",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "הסקיצה המקווקוות מסמנת מישור שמור. יש לבחור פאה חדשה כדי לתקן את ההצמדה.",
|
||||
@@ -648,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "הכיוון חייב להיות ציר או עקומת סקיצה ישרה הנמצאת במישור הסקיצה",
|
||||
"The mirror line cannot also be mirrored": "אי אפשר לשקף גם את קו השיקוף עצמו",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "קו השיקוף חייב להיות עקומה ישרה של הסקיצה הזו, ולא אחד מצירי הראשית",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "ציר הראשית ניצב למישור הסקיצה, ולכן אין לו קו במישור",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "מופע היצירה השמור חסר. יש לבחור מיקום יצירה תקין לפני הקישור מחדש.",
|
||||
"Then": "אז",
|
||||
"These features will be deleted:": "הפיצ'רים הבאים יימחקו:",
|
||||
@@ -750,6 +755,8 @@
|
||||
"ai.prefs.follow_camera": "לעקוב אחר המצלמה",
|
||||
"ai.prefs.follow_camera_off": "מעקב המצלמה האוטומטי כבוי.",
|
||||
"ai.prefs.follow_camera_on": "המצלמה פונה אל הסקיצות שהסוכן יוצר.",
|
||||
"ai.prefs.instructions": "הוראות מותאמות אישית",
|
||||
"ai.prefs.instructions_note": "נשלחות לסוכן בתחילת כל צ'אט, אחרי ההוראות של SolidCraft עצמה: דרך העבודה המועדפת עליך וכללים שיש להקפיד עליהם. שינוי חל מהצ'אט הבא (התחברות או צ'אט חדש).",
|
||||
"ai.prefs.intro": "סוכני AI ב־SolidCraft.",
|
||||
"ai.prefs.make_default": "לקבוע כברירת מחדל",
|
||||
"ai.prefs.no_agents": "אין עדיין סוכנים. אפשר להוסיף סוכן או לרענן כדי למצוא סוכנים מותקנים.",
|
||||
@@ -963,8 +970,7 @@
|
||||
"cmd.inspect_analysis": "ניתוח תצוגה",
|
||||
"cmd.inspect_center_of_mass": "מרכז מסה",
|
||||
"cmd.inspect_clearance": "מרווח",
|
||||
"cmd.inspect_colored_components": "הפעלת צבעי רכיבים",
|
||||
"cmd.inspect_colored_components_disable": "ביטול צבעי רכיבים",
|
||||
"cmd.inspect_colored_components": "צבעי רכיבים",
|
||||
"cmd.inspect_compare": "השוואת גופים",
|
||||
"cmd.inspect_compare_picture": "השוואה לתמונת ייחוס",
|
||||
"cmd.inspect_continuity": "ניתוח רציפות",
|
||||
@@ -1296,6 +1302,7 @@
|
||||
"cmd.timeline_stop": "עצירת ציר הזמן",
|
||||
"cmd.timeline_suppress": "השבתת פיצ'ר",
|
||||
"cmd.ui.assistant": "הצגה או הסתרה של העוזר",
|
||||
"cmd.view.navigator": "ניווט תצוגה מותאם אישית",
|
||||
"cmd.view.render_browser": "דפדפן רינדור",
|
||||
"cmd.view.render_dock": "הזז את עגינת הרינדור",
|
||||
"cmd.view.render_focus": "תצוגת מיקוד רינדור",
|
||||
@@ -1394,15 +1401,22 @@
|
||||
"dialog.computing": "מחשב… {seconds} שנ'",
|
||||
"dialog.could_not_be_checked": "לא ניתן היה לבדוק",
|
||||
"dialog.custom_axis": "↖ ציר מותאם",
|
||||
"dialog.cylinder_face_tip": "הפאה או הקודקוד של המודל שעליהם הגליל ניצב: יש ללחוץ כאן ואז עליהם בתצוגה. אם נשאר ריק, X, Y ו-Z ממקמים אותו.",
|
||||
"dialog.dimension_insert_hint": "יש ללחוץ על מידה מניעה אחרת כדי להוסיף את שמה",
|
||||
"dialog.dimension_name": "מידה: {name}",
|
||||
"dialog.dimension_step_down": "הפחתה ב־{step}",
|
||||
"dialog.dimension_step_expression": "רק מספר משתנה בצעדים; ביטוי נשאר כפי שנכתב",
|
||||
"dialog.dimension_step_up": "הוספה של {step}",
|
||||
"dialog.edit_title": "עריכת {title}",
|
||||
"dialog.no_shown_body": "אין גוף מוצג לשינוי: הציגו גוף, או בחרו „גוף חדש”",
|
||||
"dialog.pattern_sketch_not_object": "בחרו גופים או פיצ'רים: סקיצה אינה אף אחד מהם. חזרו על העקומות שלה בעזרת המערך או השיקוף של הסקיצה עצמה, או בצעו לה שליפה קודם.",
|
||||
"dialog.primitive_point_tip": "נקודה שממלאת את X, Y ו-Z: יש ללחוץ כאן ואז על קודקוד, מקצוע או פאה של המודל, על נקודה של סקיצה, או על הראשית.",
|
||||
"dialog.reference_edge_tip": "מקצוע ישר של הפאה שנבחרה שממנו נמדד המרחק שלמטה: יש ללחוץ כאן ואז על המקצוע.",
|
||||
"dialog.select": "↖ בחירה",
|
||||
"dialog.selected_count": "↖ {count} נבחרו",
|
||||
"dialog.selection_constraint": "אילוץ {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · עקומה {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "יש לבחור קלט כדי לשנות אותו. לחיצה על אובייקט שנבחר מסירה אותו; גם Ctrl/Cmd+לחיצה מחליפה את מצבו. לאחר בדיקת התצוגה המקדימה יש ללחוץ על אישור.",
|
||||
"dialog.selection_midpoint": "נקודת אמצע · {id}",
|
||||
"dialog.selection_profile": "{sketch} · פרופיל {number}",
|
||||
@@ -1420,6 +1434,7 @@
|
||||
"document.read_only_save": "התכן הזה נפתח לקריאה בלבד: יש לשמור אותו כקובץ חדש",
|
||||
"document.save_changes": "לשמור את השינויים ב־{name}?",
|
||||
"document.save_copy": "שמירת עותק…",
|
||||
"document.tab_unsaved_changes": "שינויים שלא נשמרו",
|
||||
"document.unsaved_changes": "ב־{name} יש שינויים שלא נשמרו. ללא שמירה, הם יאבדו.",
|
||||
"drive.rigid": "{joint} הוא מפרק קשיח: הוא מחזיק את החלקים שלו במקומם, ולכן אין מה להניע. כדי שהחלקים יזוזו, שנו את הסוג שלו ב״עריכת מפרק״: Revolute לסיבוב, Slider להחלקה, Cylindrical לשניהם.",
|
||||
"drive.stopped_by_contact": "נעצר ב־{value}: {a} היה עובר דרך {b}",
|
||||
@@ -1440,6 +1455,9 @@
|
||||
"each rail's curves must be in one sketch": "עקומות כל מסילה חייבות להיות בסקיצה אחת",
|
||||
"feature computation ended without a result": "חישוב הפיצ'ר הסתיים ללא תוצאה",
|
||||
"file.open.drawing_filter": "תכנים, קובצי CAD, רשתות משולשים ושרטוטים",
|
||||
"file.open.portal_fallback.no_portal_service": "בורר הקבצים שנפתח אינו של שולחן העבודה: שירות בחירת הקבצים של שולחן העבודה אינו זמין.",
|
||||
"file.open.portal_fallback.no_session_bus": "בורר הקבצים שנפתח אינו של שולחן העבודה: אין אוטובוס הפעלה של D-Bus.",
|
||||
"file.open.portal_fallback.xwayland": "בורר הקבצים שנפתח אינו של שולחן העבודה: תחת XWayland הפורטל דוחה את חלון האב.",
|
||||
"file_load.built": "נבנו עד כה: {built} מתוך {total} פיצ'רים.",
|
||||
"file_load.converting": "המרת גוף מוצק {done} מתוך {total}",
|
||||
"file_load.inserting": "הוספת גאומטריה",
|
||||
@@ -1999,6 +2017,7 @@
|
||||
"material.selection.mixed": "חומרים שונים",
|
||||
"measure.at": "מיקום",
|
||||
"measure.item": "פריט {number}",
|
||||
"measure.line_to_centre": "מהקו למרכז",
|
||||
"measure.quick_copied": "המדידה הועתקה",
|
||||
"measure.quick_copy": "לחיצה מעתיקה את המדידה",
|
||||
"measure.quick_edges": "{count} מקצועות",
|
||||
@@ -2029,6 +2048,8 @@
|
||||
"nav.blender.help": "גרירה אמצעית מסובבת, Shift וגרירה אמצעית מזיזים, Ctrl וגרירה אמצעית מבצעים זום; גרירה ימנית לא מזיזה את המצלמה",
|
||||
"nav.creo.help": "גרירה בלחצן האמצעי מסובבת את התצוגה, Shift והלחצן האמצעי מזיזים אותה, Ctrl וגרירה אנכית בלחצן האמצעי משנים את הזום (גרירה מטה להגדלה); גלילת הגלגלת משנה גם היא את הזום",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "גרירה בכפתור האמצעי או הימני ממשיכה לסובב, להזיז או לשנות זום כפי שהתחילה עד שמשחררים את הכפתור, בלי קשר למקשים שנלחצים או משתחררים בינתיים. כבוי: המקשים הלחוצים קובעים בכל רגע של הגרירה.",
|
||||
"nav.drag_keeps_mode.label": "גרירה שומרת על הניווט שבו התחילה",
|
||||
"nav.free_orbit.help": "העכבר והעכבר התלת־ממדי מסובבים דרך שני הקטבים ושומרים על הגלגול. הצמדת הצירים עם Shift מושבתת.",
|
||||
"nav.free_orbit.label": "סיבוב חופשי",
|
||||
"nav.free_orbit.snap_help": "העכבר והעכבר התלת־ממדי מסובבים דרך שני הקטבים ושומרים על הגלגול. יש להחזיק את Shift בזמן גרירת סיבוב כדי להצמיד את כיוון התצוגה ואת הגלגול.",
|
||||
@@ -2265,6 +2286,17 @@
|
||||
"prefs.auto_operation_note": "איחוד, גוף חדש או חיתוך, לפי מה שהחומר נוגע בו.",
|
||||
"prefs.axes": "צירים",
|
||||
"prefs.axis": "ציר",
|
||||
"prefs.background_choose": "בחירת תמונה…",
|
||||
"prefs.background_image": "תמונה",
|
||||
"prefs.background_unreadable": "לא ניתן לקרוא את תמונת הרקע",
|
||||
"prefs.blur_cost": "עלות עיבוד יחסית. יותר פסים מציינים בדרך כלל יותר עבודה.",
|
||||
"prefs.blur_device": "לא זמין בהתקן הגרפי או בנתיב העיבוד הזה",
|
||||
"prefs.blur_guard": "איכות הטשטוש הופחתה כדי לשמור על תגובתיות הפריימים. הרדיוס לא השתנה.",
|
||||
"prefs.blur_off": "כבוי",
|
||||
"prefs.blur_reduce_transparency": "מושבת עקב הגדרת הפחתת השקיפות של המערכת",
|
||||
"prefs.blur_software": "לא זמין עם עיבוד גרפי בתוכנה (gpu-software)",
|
||||
"prefs.blur_source": "לא זמין בתצוגת סביבת העבודה הזו",
|
||||
"prefs.blur_web": "לא זמין בנתיב העיבוד הזה בדפדפן",
|
||||
"prefs.bodies_folders": "תיקיות גופים",
|
||||
"prefs.bodies_folders_always": "תמיד",
|
||||
"prefs.bodies_folders_never": "אף פעם",
|
||||
@@ -2278,6 +2310,8 @@
|
||||
"prefs.browser_eye_left": "סמל העין ליד חץ הקיפול",
|
||||
"prefs.browser_eye_left_note": "שורות עץ התכן מציגות את סמל העין ליד חץ הקיפול, כמו קודם, במקום בעמודה בקצה הימני של השורות.",
|
||||
"prefs.browser_reveal_on_hover": "הצגה בריחוף",
|
||||
"prefs.browser_rows_start_closed": "שורות עץ התכן מוצגות סגורות",
|
||||
"prefs.browser_rows_start_closed_note": "מיקומים ותיקיות בעץ התכן מוצגים סגורים, כך שתכן עם רכיבים רבים מציג את השורות העליונות שלו. כל שורה נפתחת בחץ שלה, ב„הרחבת הכול” או ב„איתור בעץ התכן”, שפותח רק את השורות שמובילות לגוף שלו וסוגר שוב את אלה שהאיתור הקודם פתח. גוף שנבחר בתצוגה התלת־ממדית משאיר את עץ התכן כפי שהוא. כשהאפשרות כבויה התיקיות מוצגות פתוחות, המיקומים מוצגים כפי שקובעת האפשרות „רכיבים פתוחים מההתחלה”, ובחירה בתצוגה התלת־ממדית פותחת את עץ התכן עד לגוף שלה, כמו קודם.",
|
||||
"prefs.browser_show_note": "״מוצג״ משאיר את הדפדפן על המסך. ״הצגה בריחוף״ מסתיר אותו עד שהמצביע מגיע למקומו או ללשונית הדקה בקצהו השמאלי, והוא נשאר פתוח כל עוד המצביע בתוכו או שתפריט ממנו פתוח.",
|
||||
"prefs.browser_shown": "מוצג",
|
||||
"prefs.built_in": "מובנה",
|
||||
@@ -2323,6 +2357,13 @@
|
||||
"prefs.dialogs_edge": "בקצה התצוגה",
|
||||
"prefs.dialogs_left_column": "בעמודה השמאלית",
|
||||
"prefs.dialogs_note": "היכן נפתחות תיבות הדו־שיח של הפקודות: מעוגנות בקצה התצוגה, או בעמודה השמאלית במקום הדפדפן וההיסטוריה, שחוזרים כפי שהיו כשהתיבה נסגרת.",
|
||||
"prefs.dimension_step_angle": "מה שהחצים בחלון, למעלה/למטה והגלגלת מוסיפים לזווית",
|
||||
"prefs.dimension_step_length": "מה שהחצים בחלון, למעלה/למטה והגלגלת מוסיפים לאורך",
|
||||
"prefs.dimension_steps": "צעדי החלון",
|
||||
"prefs.dimension_values": "ערכי מידות",
|
||||
"prefs.dimension_values_note": "היכן מקלידים את ערך המידה. לחלון יש חצים שמשנים אותו בצעדים; כלי המידה מציב קודם את הטקסט.",
|
||||
"prefs.dimension_values_view": "בתצוגה",
|
||||
"prefs.dimension_values_window": "בחלון",
|
||||
"prefs.display": "תצוגה",
|
||||
"prefs.edges": "מקצועות והצללה",
|
||||
"prefs.extend": "הרחבה",
|
||||
@@ -2402,6 +2443,10 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "שני החצים המעוגלים בפינת הקובייה מסובבים את התצוגה במישור המסך. פועל: החץ עם כיוון השעון מסובב נגד כיוון השעון, והחץ נגד כיוון השעון מסובב עם כיוון השעון. כבוי: כל חץ מסובב את התצוגה לפי העקום שלו.",
|
||||
"prefs.invert_navigator_arrows": "היפוך חצי סיבוב קוביית התצוגה",
|
||||
"prefs.invert_navigator_arrows_note": "החצים סביב הקובייה מחוויים את המצלמה לכיוון שהחץ מצביע, כמו ב-Fusion ו-Inventor: החץ שמשמאל מצביע ימימה ומניע את המצלמה ימימה. כבוי: החץ מסובב את הקובייה אל הצד שבו הוא יושב, כך שחץ שמאל מניע את המצלמה שמאלה. חצי הגלילה המעוגלים אינם מושפעים.",
|
||||
"prefs.keep_unsaved_also_asks": "לשאול גם כשהכרטיסייה נסגרת",
|
||||
"prefs.keep_unsaved_also_asks_tip": "כשאפשרות זו כבויה, הכרטיסייה נסגרת ללא חלון הדפדפן לאחר שהעותק של כל תכן שלא נשמר עודכן.",
|
||||
"prefs.keep_unsaved_in_browser": "לשמור עבודה שלא נשמרה בדפדפן זה",
|
||||
"prefs.keep_unsaved_in_browser_tip": "השינויים שלא נשמרו בכל תכן פתוח נשמרים מעודכנים באחסון הפרטי של דפדפן זה. בביקור הבא תוצע שחזורם.",
|
||||
"prefs.keep_work_on_tab_switch": "השארת תיבות דו־שיח וכלים פתוחים במעבר בין עיצובים",
|
||||
"prefs.keep_work_on_tab_switch_note": "כל לשונית עיצוב שומרת את תיבת הדו־שיח או הכלי הפתוחים בה, עם מה שבחרת והקלדת בהם, ואת לשונית סרגל הכלים שלה. הם מחכים לך כשתחזור לעיצוב. כבוי: מעבר בין עיצובים מבטל אותם ומציג את הלשונית מודל.",
|
||||
"prefs.labels": "תוויות",
|
||||
@@ -2413,6 +2458,8 @@
|
||||
"prefs.layout_page.card": "כרטיס",
|
||||
"prefs.layout_page.column": "עמודה",
|
||||
"prefs.layout_page.hover": "מעבר עכבר",
|
||||
"prefs.layout_page.hover_panel_menus": "מעבר בין תפריטי לוחות בריחוף",
|
||||
"prefs.layout_page.hover_panel_menus_help": "לחצו על שם לוח ברצועת הכלים כדי לפתוח את התפריט שלו. העבירו את הסמן מעל שם של לוח אחר כדי לעבור לתפריט שלו. הזיזו את הסמן אל מחוץ לרצועת הכלים ולתפריט הפתוח כדי לסגור אותו.",
|
||||
"prefs.layout_page.icons_labels": "סמלים + תוויות",
|
||||
"prefs.layout_page.ok_cancel": "אישור וביטול",
|
||||
"prefs.layout_page.on_hover": "בעת מעבר עכבר",
|
||||
@@ -2533,6 +2580,8 @@
|
||||
"prefs.section_hatch_by_part": "לפי חלק",
|
||||
"prefs.section_hatch_one": "סגנון אחד",
|
||||
"prefs.select": "בחירה",
|
||||
"prefs.select_hidden_edges": "בחירת קצוות מוסתרים בתצוגה מוצללת עם קצוות מוסתרים",
|
||||
"prefs.select_hidden_edges_help": "מאפשר לבחור קצוות דרך פאות בתצוגה מוצללת עם קצוות מוסתרים, גם עבור Fillet. כיבוי שומר על אופן הבחירה של v0.30.1. גופים שהוסתרו עדיין אינם ניתנים לבחירה.",
|
||||
"prefs.select_one_item": "בחירת פריט אחד",
|
||||
"prefs.select_other_hidden": "בחירת גאומטריה מוסתרת באמצעות בחירה אחרת",
|
||||
"prefs.select_other_hidden_help": "מופעל: כולל גופים, סקיצות וקצוות מוסתרים בקרבת הסמן. כבוי: שימוש ברשימה המקורית של גאומטריה גלויה מתחת לסמן.",
|
||||
@@ -2557,6 +2606,8 @@
|
||||
"prefs.sketch.click_selects": "לחיצה בוחרת",
|
||||
"prefs.sketch.conflict_colour": "צבע התנגשות",
|
||||
"prefs.sketch.drawing": "שרטוט",
|
||||
"prefs.sketch.ellipse_axes": "שרטוט אליפסות עם הצירים שלהן",
|
||||
"prefs.sketch.ellipse_axes_note": "הציר הראשי והציר המשני כקווי עזר, כדי שאפשר יהיה לתת מידות לחצאי הצירים ולצירים. כבוי: האליפסה בלבד, ו-D עליה נותן את הרדיוס המשני שלה בכל מקום שאליו נגרר הערך, משורטט ומסומן R כמו קודם.",
|
||||
"prefs.sketch.exit_keep": "שמירה",
|
||||
"prefs.sketch.exit_previous": "חזרה לתצוגה הקודמת",
|
||||
"prefs.sketch.exit_unless_rotated": "אלא אם סובבה",
|
||||
@@ -2650,6 +2701,10 @@
|
||||
"prefs.theme_follow_off": "כבוי",
|
||||
"prefs.theme_follow_stop": "הפסקת המעקב",
|
||||
"prefs.theme_import_hint": "לחצו כדי לבחור קובץ ערכת נושא, שחררו קובץ כאן, או הקישו Ctrl+V (Cmd+V ב-Mac) אחרי שהעתקתם ערכת נושא",
|
||||
"prefs.theme_notice_dismiss": "הסתרה",
|
||||
"prefs.theme_option_blur": "טשטוש מאחורי לוחות",
|
||||
"prefs.theme_option_blur_help": "טשטוש מאחורי לוחות שקופים למחצה, בפיקסלים פיזיים. אפס מכבה את הטשטוש.",
|
||||
"prefs.theme_option_invalid": "הערך השמור עבור {option} אינו תקין; נעשה שימוש בברירת המחדל של ערכת הנושא. הערך השמור נשמר ללא שינוי.",
|
||||
"prefs.theme_option_opacity": "אטימות",
|
||||
"prefs.theme_option_opacity_help": "כוונון אטימות הלוחות הצפים. רמת אטימות מינימלית שומרת על קריאות הטקסט על רקע צבעי חלון התצוגה.",
|
||||
"prefs.theme_option_tab_colours": "צבעי כרטיסיות",
|
||||
@@ -2658,6 +2713,7 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "שנה בהדרגה את צבעי הלוח ואזור התצוגה בעת מעבר בין כרטיסיות. ערך אפס מבצע את המעבר מיד.",
|
||||
"prefs.theme_options_badge": "אפשרויות",
|
||||
"prefs.theme_options_more": "עוד · {count}",
|
||||
"prefs.theme_options_newer": "ערכת נושא זו דורשת גרסה חדשה יותר של SolidCraft. לאחר השדרוג יש לייבא שוב את ערכת הנושא המקורית כדי לשחזר את הגדרות האפשרויות שלה.",
|
||||
"prefs.theme_options_reset": "איפוס",
|
||||
"prefs.theme_options_unavailable": "לא זמין במכשיר זה",
|
||||
"prefs.theme_paste": "הדבקת ערכת נושא",
|
||||
@@ -2721,6 +2777,12 @@
|
||||
"recovery.just_now": "זה עתה",
|
||||
"recovery.min_ago": "לפני {count} דק'",
|
||||
"recovery.never_saved": "לא נשמר מעולם",
|
||||
"recovery.open_saved": "פתיחת הגרסה השמורה",
|
||||
"recovery.restore": "שחזור",
|
||||
"recovery.restore_intro": "שינויים שלא נשמרו כשדף זה נסגר לאחרונה שמורים בדפדפן זה.",
|
||||
"recovery.restore_row": "{name}, שונה {when}",
|
||||
"recovery.restore_title": "לשחזר תכנים שלא נשמרו?",
|
||||
"recovery.restore_title_one": "לשחזר תכן שלא נשמר: {name}, שונה {when}?",
|
||||
"refused.change": "לא ניתן היה לשנות את {body}.",
|
||||
"refused.cut": "לא ניתן היה לחתוך את {body}.",
|
||||
"refused.face_curved": "אחת הפאות המעוקלות שלו אינה ניתנת לשימוש במידול.",
|
||||
@@ -3242,9 +3304,11 @@
|
||||
"render.frame_wide": "16:9 · רחב",
|
||||
"render.gpu_depth": "עומק מסלול מרבי",
|
||||
"render.gpu_options_help": "הדגימות, העומק, הזרע והחשיפה נשמרים עם ההגדרה הזאת. נשמרת רק שיטת התמונה הפעילה.",
|
||||
"render.gpu_path_crashed": "SolidCraft נסגר בזמן ההכנה על מתאם גרפי זה בפעם הקודמת, ככל הנראה בתוך מנהל ההתקן הגרפי, ולכן ההכנה לא תתבצע שוב מעצמה. יש להשתמש במעקב קרניים על המעבד במקום זאת, או לנסות שוב (עדכון מנהל ההתקן גם מנסה שוב).",
|
||||
"render.gpu_path_failed": "מעקב המסלולים ב־GPU אינו זמין",
|
||||
"render.gpu_path_first_setup": "ההגדרה הראשונה ב־GPU הזה עשויה להימשך כמה דקות; SolidCraft נשאר זמין לשימוש בינתיים. הפעלות מאוחרות יותר משתמשות שוב בשיידרים שהודרו, כשמנהל ההתקן שומר אותן.",
|
||||
"render.gpu_path_preparing": "מעקב מסלולים ב־GPU: הידור שיידרים…",
|
||||
"render.gpu_path_retry": "לנסות שוב",
|
||||
"render.gpu_seed": "זרע אקראי",
|
||||
"render.gpu_unavailable": "מעקב מסלולים ב־GPU אינו מוכן במכשיר הזה.",
|
||||
"render.ground_apply": "התאמת הקרקע ושמירתה",
|
||||
@@ -4052,6 +4116,8 @@
|
||||
"sample.plate.name": "לוח לדוגמה",
|
||||
"sample.vents.about": "חריצים במערך מלבני: מספר השורות, מספר העמודות והפסיעה הם פרמטרים",
|
||||
"sample.vents.name": "לוח אוורור",
|
||||
"scale.per_axis": "לפי ציר",
|
||||
"scale.uniform": "אחיד",
|
||||
"search.command_keys": "Enter הרצה Shift+Enter הרצה והשארה פתוח Esc סגירה",
|
||||
"search.commands": "פקודות",
|
||||
"search.design": "בתכן זה",
|
||||
@@ -4142,6 +4208,32 @@
|
||||
"sketch.dof_left": "נותרו {count} דרגות חופש",
|
||||
"sketch.dof_left_one": "נותרה {count} דרגת חופש",
|
||||
"sketch.draw_curves": "שרטוט כ־{linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "המרת הבחירה ל־{linetype}",
|
||||
"sketch.open_ends": "קצוות פתוחים: {count}",
|
||||
"sketch.status_conflict": "אילוצים סותרים",
|
||||
@@ -4363,6 +4455,7 @@
|
||||
"ui.3d_sketch": "סקיצה תלת־ממדית",
|
||||
"ui.activate": "הפעלה",
|
||||
"ui.active_component": "רכיב פעיל",
|
||||
"ui.addToQuickAccess": "הוספה לגישה מהירה",
|
||||
"ui.add_configuration": "הוספת תצורה",
|
||||
"ui.add_parameter": "הוספת פרמטר",
|
||||
"ui.agent_chat": "צ'אט עם סוכן",
|
||||
@@ -4784,6 +4877,7 @@
|
||||
"ui.default_modelling_orientation": "כיוון מידול ברירת מחדל",
|
||||
"ui.delete": "מחיקה",
|
||||
"ui.delete_select_first": "בחרו קודם מה למחוק: תכונות, גופים, רכיבים או גאומטריית סקיצה",
|
||||
"ui.dependents_select_first": "בחרו קודם מה להציג את התלויים שלו",
|
||||
"ui.depth": "עומק",
|
||||
"ui.design": "תכן",
|
||||
"ui.design_properties.author": "מחבר",
|
||||
@@ -4913,6 +5007,11 @@
|
||||
"ui.factor": "מקדם",
|
||||
"ui.fade": "דעיכה",
|
||||
"ui.fade_length": "אורך דעיכה",
|
||||
"ui.fair_iterations": "מספר איטרציות",
|
||||
"ui.fair_keep_edges": "שמירה על המקצועות",
|
||||
"ui.fair_strength": "עוצמה",
|
||||
"ui.fair_surface_title": "החלקת משטח",
|
||||
"ui.fair_tolerance_default": "0.001 מגודל הגוף",
|
||||
"ui.feature_history": "ציר הזמן",
|
||||
"ui.feature_options": "אפשרויות פיצ'ר",
|
||||
"ui.features": "פיצ'רים",
|
||||
@@ -4941,6 +5040,10 @@
|
||||
"ui.fillet_setback_least": "יותר מ-{least}",
|
||||
"ui.fillet_setback_tip": "היכן מתחיל טלאי הפינה החלק: המרחק לאורך כל קצה מהפינה. ברירת המחדל היא פי {factor} מהרדיוס. עליה להיות גדולה מהנסיגה המזערית, שבה העיגולים נפגשים בלעדיה: הרדיוס כשהפאות ניצבות זו לזו.",
|
||||
"ui.fillet_smooth_radius": "שינוי רדיוס חלק",
|
||||
"ui.fillet_turn": "פניות",
|
||||
"ui.fillet_turn_mitre": "מיטר",
|
||||
"ui.fillet_turn_swept": "מסובב",
|
||||
"ui.fillet_turn_tip": "היכן ששני עיגולים מסתובבים סביב קצה פינה חד ביניהם, כמו העיגולים בבסיס בליטה ליד הקצוות הזקופים שלה.\nמיטר: העיגולים נמשכים עד שהם נפגשים במיטר.\nמסובב: העיגולים נעצרים בקצה הפינה ומסתובבים סביבו, והפינה היא טורוס המשיק לשניהם. רק לעיגולים ברדיוס אחד סביב קצה פינה ישר וקמור.",
|
||||
"ui.fillet_type_choices_tip": "רדיוס קבוע: רדיוס אחד לכל אורך הקצה.\nאורך מיתר: רוחב העיגול נשאר זהה, בכל זווית שבה הפאות נפגשות.\nרדיוס משתנה: רדיוס בכל קצה, ועוד לאורך הקצה אם מוסיפים.\nרציף בעקמומיות: חתך בלי שבר בהשתקפויות היכן שהוא פוגש פאות מישוריות (G2).\nספליין: הרדיוס לאורך הקצה עוקב אחרי רדיוסי בקרה שאתם קובעים.",
|
||||
"ui.fillet_type_curvature": "רציף בעקמומיות",
|
||||
"ui.fillet_type_tip": "החתך לרוחב הקצה. רציף בעקמומיות: חתך רציף בעקמומיות (G2) בין פאות מישוריות, כך שההשתקפויות עוברות מהפאות אל העיגול בלי שבר.",
|
||||
@@ -4986,6 +5089,7 @@
|
||||
"ui.heading_construct": "עזר",
|
||||
"ui.heading_create": "יצירה",
|
||||
"ui.heading_create_sketch": "יצירת סקיצה",
|
||||
"ui.heading_custom_view_navigator": "ניווט תצוגה מותאם אישית",
|
||||
"ui.heading_cylinder": "גליל",
|
||||
"ui.heading_design": "תכן",
|
||||
"ui.heading_draft": "שיפוע יציקה",
|
||||
@@ -5121,6 +5225,7 @@
|
||||
"ui.manipulator_free_alt": "גרירה חופשית של הידית גם עם Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "פעיל: Alt/Option או Ctrl/Cmd על חיצי השליפה / הידית מבטלים הצמדת ערכים. כבוי: Ctrl/Cmd בלבד.",
|
||||
"ui.manufacture": "ייצור",
|
||||
"ui.match_surface_title": "התאמת משטח",
|
||||
"ui.material_review.axes": "צירים מקומיים של החומר; לא הוגדרה אוריינטציה לגוף.",
|
||||
"ui.material_review.claims": "המקורות והרישיונות הם טענות שלא אומתו. ערכים חסרים נשארים לא ידועים.",
|
||||
"ui.material_review.compressive_strength": "חוזק לחיצה",
|
||||
@@ -5184,6 +5289,7 @@
|
||||
"ui.options": "אפשרויות",
|
||||
"ui.orbit": "סיבוב מבט",
|
||||
"ui.orbit_around": "סיבוב מבט סביב",
|
||||
"ui.organize": "ארגון",
|
||||
"ui.orientation": "אוריינטציה",
|
||||
"ui.origin": "ראשית",
|
||||
"ui.palette_construction_mode": "מצב עזר (X)",
|
||||
@@ -5272,6 +5378,7 @@
|
||||
"ui.quality": "איכות",
|
||||
"ui.quantity": "כמות",
|
||||
"ui.quantity_2": "כמות 2",
|
||||
"ui.quick_access_full": "סרגל הגישה המהירה מכיל {count} פקודות; יש להסיר אחת כדי להוסיף אחרת",
|
||||
"ui.quit": "יציאה",
|
||||
"ui.radial_context_menu": "תפריט הקשר מעגלי",
|
||||
"ui.radius": "רדיוס",
|
||||
@@ -5289,6 +5396,7 @@
|
||||
"ui.relink_not_linked": "רכיב זה אינו מקושר לתכן מקור.",
|
||||
"ui.relink_select_one": "יש לבחור תחילה רכיב מקושר אחד או אחד המופעים שלו.",
|
||||
"ui.relink_source_all": "קישור מחדש למקור (כל המופעים)…",
|
||||
"ui.removeFromQuickAccess": "הסרה מהגישה המהירה",
|
||||
"ui.removeFromToolbar": "הסרה מסרגל הכלים",
|
||||
"ui.removeSection": "הסרת מקטע",
|
||||
"ui.render": "הדמיה",
|
||||
@@ -5297,6 +5405,8 @@
|
||||
"ui.repeat_nothing": "אין עדיין מה לחזור עליו: התחל קודם כלי או תיבת דו־שיח.",
|
||||
"ui.replace_with_link_done": "העותקים הם עכשיו מיקומים של התכן המקושר.",
|
||||
"ui.report_a_bug": "דיווח על באג",
|
||||
"ui.report_characters_left_one": "תווים שנותרו: {count}",
|
||||
"ui.report_characters_left_other": "תווים שנותרו: {count}",
|
||||
"ui.reset": "איפוס",
|
||||
"ui.reset_all": "איפוס הכול",
|
||||
"ui.reset_the_settings_on_this_page": "איפוס ההגדרות בדף זה",
|
||||
@@ -5581,6 +5691,14 @@
|
||||
"values must be numbers or expressions": "הערכים חייבים להיות מספרים או ביטויים",
|
||||
"view.auto_ortho_faces": "פרספקטיבה בזמן סיבוב, אורתוגרפי על פאות",
|
||||
"view.render_inspector": "מפקח רינדור",
|
||||
"view_gizmo.arrows": "חיצי סיבוב",
|
||||
"view_gizmo.home": "לחצן תצוגת בית",
|
||||
"view_gizmo.no_such_body": "אין בעיצוב הזה גוף בשם הזה.",
|
||||
"view_gizmo.note": "בחרו גוף בתצוגה. הוא נשמר עם ההעדפות ופרופילי ההגדרות שלכם ומוצג בכל עיצוב. לחצו על אחת מפאותיו כדי להביט בה ישירות.",
|
||||
"view_gizmo.texture": "מרקם",
|
||||
"view_gizmo.themed": "לפי ערכת הנושא",
|
||||
"view_gizmo.too_detailed": "מפורט מדי לניווט התצוגה (יותר מ-10,000 משולשים)",
|
||||
"view_gizmo.too_thin": "ארוך ודק מדי לניווט התצוגה: הצד הדק ביותר חייב להיות לפחות חמישית מהצד הארוך ביותר",
|
||||
"web.files.insert_drawings_hint": "קובצי CAD, רשתות משולשים, DXF או SVG אל התכן הפתוח",
|
||||
"web.files.insert_stored_drawings_hint": "אל התכן הפתוח (קובצי CAD, רשתות משולשים, DXF, SVG)",
|
||||
"web.open_full": "פתיחה ב־SolidCraft",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Analiza dostupnosti",
|
||||
"cmd.inspect_center_of_mass": "Središte mase",
|
||||
"cmd.inspect_clearance": "Provjera zazora",
|
||||
"cmd.inspect_colored_components": "Omogući boje komponenti",
|
||||
"cmd.inspect_colored_components_disable": "Onemogući boje komponenti",
|
||||
"cmd.inspect_compare": "Usporedi tijela",
|
||||
"cmd.inspect_compare_picture": "Usporedi sliku",
|
||||
"cmd.inspect_continuity": "Analiza kontinuiteta",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Hozzáférhetőség-elemzés",
|
||||
"cmd.inspect_center_of_mass": "Tömegközéppont",
|
||||
"cmd.inspect_clearance": "Hézag",
|
||||
"cmd.inspect_colored_components": "Komponensszínek bekapcsolása",
|
||||
"cmd.inspect_colored_components_disable": "Komponensszínek kikapcsolása",
|
||||
"cmd.inspect_compare": "Testek összehasonlítása",
|
||||
"cmd.inspect_compare_picture": "Kép összehasonlítása",
|
||||
"cmd.inspect_continuity": "Folytonosságelemzés",
|
||||
@@ -3809,6 +3807,8 @@
|
||||
"sample.plate.name": "Mintalemez",
|
||||
"sample.vents.about": "Hornyok téglalap mintában: a sorok, oszlopok és osztás paraméterek",
|
||||
"sample.vents.name": "Szellőzőlemez",
|
||||
"scale.per_axis": "Tengelyenként",
|
||||
"scale.uniform": "Egyenletes",
|
||||
"search.command_keys": "Enter futtatás Shift+Enter futtatás és nyitva tartás Esc bezárás",
|
||||
"search.commands": "Parancsok",
|
||||
"search.design": "Ebben a tervben",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Analisis Aksesibilitas",
|
||||
"cmd.inspect_center_of_mass": "Pusat Massa",
|
||||
"cmd.inspect_clearance": "Kelonggaran",
|
||||
"cmd.inspect_colored_components": "Aktifkan Warna Komponen",
|
||||
"cmd.inspect_colored_components_disable": "Nonaktifkan Warna Komponen",
|
||||
"cmd.inspect_compare": "Bandingkan Bodi",
|
||||
"cmd.inspect_compare_picture": "Bandingkan dengan Gambar",
|
||||
"cmd.inspect_continuity": "Analisis Kontinuitas",
|
||||
@@ -3809,6 +3807,8 @@
|
||||
"sample.plate.name": "Pelat Contoh",
|
||||
"sample.vents.about": "Slot dalam pola persegi panjang: baris, kolom, dan pitch adalah parameter",
|
||||
"sample.vents.name": "Pelat Ventilasi",
|
||||
"scale.per_axis": "Per sumbu",
|
||||
"scale.uniform": "Seragam",
|
||||
"search.command_keys": "Enter jalankan Shift+Enter jalankan dan tetap buka Esc tutup",
|
||||
"search.commands": "Perintah",
|
||||
"search.design": "Di desain ini",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Analisi di accessibilità",
|
||||
"cmd.inspect_center_of_mass": "Centro di massa",
|
||||
"cmd.inspect_clearance": "Distanza libera",
|
||||
"cmd.inspect_colored_components": "Attiva colori dei componenti",
|
||||
"cmd.inspect_colored_components_disable": "Disattiva colori dei componenti",
|
||||
"cmd.inspect_compare": "Confronta corpi",
|
||||
"cmd.inspect_compare_picture": "Confronta immagine",
|
||||
"cmd.inspect_continuity": "Analisi di continuità",
|
||||
@@ -1366,11 +1364,14 @@
|
||||
"dialog.computing": "Calcolo in corso… {seconds} s",
|
||||
"dialog.could_not_be_checked": "impossibile verificare",
|
||||
"dialog.custom_axis": "↖ Asse personalizzato",
|
||||
"dialog.cylinder_face_tip": "La faccia o il vertice del modello su cui poggia il cilindro: fai clic qui, poi su di esso nella vista. Se resta vuoto, lo posizionano X, Y e Z.",
|
||||
"dialog.dimension_insert_hint": "Fai doppio clic su un’altra quota di controllo per inserirne il nome",
|
||||
"dialog.dimension_name": "Quota: {name}",
|
||||
"dialog.edit_title": "MODIFICA {title}",
|
||||
"dialog.no_shown_body": "Nessun corpo visibile da modificare: mostrane uno o scegli Nuovo corpo",
|
||||
"dialog.pattern_sketch_not_object": "Seleziona corpi o lavorazioni: uno schizzo non è nessuno dei due. Ripeti le sue curve con Ripetizione o Specchia dello schizzo stesso, oppure estrudilo prima.",
|
||||
"dialog.primitive_point_tip": "Un punto che compila X, Y e Z: fai clic qui, poi su un vertice, uno spigolo o una faccia del modello, su un punto di uno schizzo oppure sull'origine.",
|
||||
"dialog.reference_edge_tip": "Uno spigolo rettilineo della faccia scelta da cui si misura la distanza sotto: fai clic qui, poi sullo spigolo.",
|
||||
"dialog.select": "↖ Seleziona",
|
||||
"dialog.selected_count": "↖ Selezionati: {count}",
|
||||
"dialog.selection_constraint": "Vincolo {id}",
|
||||
@@ -1392,6 +1393,7 @@
|
||||
"document.read_only_save": "Questa progettazione è stata aperta in sola lettura: salvala come nuovo file",
|
||||
"document.save_changes": "Salvare le modifiche a {name}?",
|
||||
"document.save_copy": "Salva una copia…",
|
||||
"document.tab_unsaved_changes": "Modifiche non salvate",
|
||||
"document.unsaved_changes": "{name} contiene modifiche non salvate. Se non salvi, andranno perse.",
|
||||
"drive.rigid": "{joint} è un giunto rigido: tiene ferme le sue parti, quindi non c'è nulla da azionare. Per farle muovere, cambia il suo tipo in Modifica giunto: Rotante per ruotare, Scorrevole per scorrere, Cilindrico per entrambi.",
|
||||
"drive.stopped_by_contact": "Arrestato a {value}: {a} attraverserebbe {b}",
|
||||
@@ -3811,6 +3813,8 @@
|
||||
"sample.plate.name": "Piastra di esempio",
|
||||
"sample.vents.about": "Asole in ripetizione rettangolare: righe, colonne e passo sono parametri",
|
||||
"sample.vents.name": "Piastra di ventilazione",
|
||||
"scale.per_axis": "Per asse",
|
||||
"scale.uniform": "Uniforme",
|
||||
"search.command_keys": "Invio esegui Maiusc+Invio esegui e tieni aperto Esc chiudi",
|
||||
"search.commands": "Comandi",
|
||||
"search.design": "In questo progetto",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"Bottom Right": "右下",
|
||||
"Boundary": "境界",
|
||||
"Bounding box": "境界ボックス",
|
||||
"Bridge": "ブリッジ",
|
||||
"CHECK": "チェック",
|
||||
"COMPONENTS": "コンポーネント",
|
||||
"CONFIGURATIONS": "構成",
|
||||
@@ -628,6 +629,8 @@
|
||||
"Suppress Selected Folders": "選択したフォルダーを抑制",
|
||||
"Suppressed": "抑制",
|
||||
"Surface": "サーフェス",
|
||||
"Surface or face": "サーフェスまたは面",
|
||||
"Surface or faces": "サーフェスまたは面",
|
||||
"Surface, or faces to turn over": "サーフェス、または裏返す面",
|
||||
"Surfaces": "サーフェス",
|
||||
"Swap Ends": "端点を入れ替え",
|
||||
@@ -641,6 +644,7 @@
|
||||
"Target": "ターゲット",
|
||||
"Target Body": "ターゲットボディ",
|
||||
"Target Face (optional)": "対象面 (オプション)",
|
||||
"Target edges": "ターゲットのエッジ",
|
||||
"Tear": "引き裂き",
|
||||
"Technical details": "技術情報",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "破線のスケッチは保存された平面ガイドです。新しい面を選択してアタッチメントを修復してください。",
|
||||
@@ -648,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "方向は、軸、またはスケッチ平面上にある直線のスケッチ曲線にしてください",
|
||||
"The mirror line cannot also be mirrored": "ミラー線自体をミラーすることはできません",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "ミラー線は、原点の軸ではなく、このスケッチの直線にしてください",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "原点軸はスケッチ平面に垂直なため、平面内に線がありません",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "保存された作成元のオカレンスが見つかりません。再バインドする前に有効な作成元配置を選択してください。",
|
||||
"Then": "次を実行 (Then)",
|
||||
"These features will be deleted:": "以下のフィーチャーが削除されます:",
|
||||
@@ -750,6 +755,8 @@
|
||||
"ai.prefs.follow_camera": "カメラを追従",
|
||||
"ai.prefs.follow_camera_off": "カメラの自動追従はオフです。",
|
||||
"ai.prefs.follow_camera_on": "エージェントが作成するスケッチにカメラを向けます。",
|
||||
"ai.prefs.instructions": "カスタム指示",
|
||||
"ai.prefs.instructions_note": "各チャットの開始時に、SolidCraft 自身の指示の後に続けてエージェントへ送られます。作業の進め方や守ってほしいルールを書いてください。変更は次のチャット(接続または新しいチャット)から適用されます。",
|
||||
"ai.prefs.intro": "SolidCraftのAIエージェント。",
|
||||
"ai.prefs.make_default": "既定にする",
|
||||
"ai.prefs.no_agents": "エージェントはまだありません。追加するか、更新してインストール済みのエージェントを検索してください。",
|
||||
@@ -963,8 +970,7 @@
|
||||
"cmd.inspect_analysis": "表示解析",
|
||||
"cmd.inspect_center_of_mass": "重心",
|
||||
"cmd.inspect_clearance": "クリアランス",
|
||||
"cmd.inspect_colored_components": "コンポーネントの色分けを有効化",
|
||||
"cmd.inspect_colored_components_disable": "コンポーネントの色分けを無効化",
|
||||
"cmd.inspect_colored_components": "コンポーネントの色分け",
|
||||
"cmd.inspect_compare": "ボディを比較",
|
||||
"cmd.inspect_compare_picture": "参考画像と比較",
|
||||
"cmd.inspect_continuity": "連続性解析",
|
||||
@@ -1296,6 +1302,7 @@
|
||||
"cmd.timeline_stop": "履歴を停止",
|
||||
"cmd.timeline_suppress": "フィーチャーを抑制",
|
||||
"cmd.ui.assistant": "アシスタントの表示切り替え",
|
||||
"cmd.view.navigator": "カスタム ビューナビゲーター",
|
||||
"cmd.view.render_browser": "Renderブラウザー",
|
||||
"cmd.view.render_dock": "レンダードックを移動",
|
||||
"cmd.view.render_focus": "Renderフォーカス表示",
|
||||
@@ -1394,15 +1401,22 @@
|
||||
"dialog.computing": "計算中… {seconds} 秒",
|
||||
"dialog.could_not_be_checked": "チェックできません",
|
||||
"dialog.custom_axis": "↖ カスタム軸",
|
||||
"dialog.cylinder_face_tip": "円柱が立つモデルの面または頂点: ここをクリックしてから、ビューでそれをクリック。空のままなら X、Y、Z で配置します。",
|
||||
"dialog.dimension_insert_hint": "別の駆動寸法をクリックして名前を挿入",
|
||||
"dialog.dimension_name": "寸法: {name}",
|
||||
"dialog.dimension_step_down": "{step} 減らす",
|
||||
"dialog.dimension_step_expression": "増減できるのは数値だけです。式はそのまま残ります",
|
||||
"dialog.dimension_step_up": "{step} 増やす",
|
||||
"dialog.edit_title": "{title}を編集",
|
||||
"dialog.no_shown_body": "変更できる表示中のボディがありません。ボディを表示するか、新規ボディを選択してください",
|
||||
"dialog.pattern_sketch_not_object": "ボディかフィーチャーを選んでください。スケッチはどちらでもありません。曲線はスケッチ内のパターンかミラーで繰り返すか、先に押し出してください。",
|
||||
"dialog.primitive_point_tip": "X、Y、Z を埋める点: ここをクリックしてから、モデルの頂点、エッジ、面、スケッチの点、または原点をクリック。",
|
||||
"dialog.reference_edge_tip": "下の距離を測る基準となる、選んだ面の直線エッジ: ここをクリックしてからエッジをクリック。",
|
||||
"dialog.select": "↖ 選択",
|
||||
"dialog.selected_count": "↖ {count} 個選択中",
|
||||
"dialog.selection_constraint": "拘束 {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · 曲線 {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "入力欄を選んで変更します。選択済みのオブジェクトをクリックすると解除できます。Ctrl/Cmd を押しながらクリックしても切り替えられます。プレビューを確認してから OK を押してください。",
|
||||
"dialog.selection_midpoint": "中点 · {id}",
|
||||
"dialog.selection_profile": "{sketch} · プロファイル {number}",
|
||||
@@ -1420,6 +1434,7 @@
|
||||
"document.read_only_save": "このデザインは読み取り専用で開かれています:新しいファイルとして保存してください",
|
||||
"document.save_changes": "{name} への変更を保存しますか?",
|
||||
"document.save_copy": "コピーを保存…",
|
||||
"document.tab_unsaved_changes": "未保存の変更",
|
||||
"document.unsaved_changes": "{name} には保存されていない変更があります。保存しない場合、変更内容は失われます。",
|
||||
"drive.rigid": "{joint} は剛体ジョイントです。部品をその場に固定するため、駆動するものがありません。動かすには「ジョイントを編集」で種類を変更してください: 回転させるなら Revolute、スライドさせるなら Slider、両方なら Cylindrical。",
|
||||
"drive.stopped_by_contact": "{value} で停止: {a} が {b} を通り抜けてしまいます",
|
||||
@@ -1440,6 +1455,9 @@
|
||||
"each rail's curves must be in one sketch": "各レールの曲線は1つのスケッチ内にある必要があります",
|
||||
"feature computation ended without a result": "フィーチャーの計算が結果なしで終了しました",
|
||||
"file.open.drawing_filter": "デザイン、CADファイル、メッシュ、図面",
|
||||
"file.open.portal_fallback.no_portal_service": "開いたファイル選択はデスクトップのものではありません。デスクトップのファイル選択サービスが利用できません。",
|
||||
"file.open.portal_fallback.no_session_bus": "開いたファイル選択はデスクトップのものではありません。D-Bus セッションがありません。",
|
||||
"file.open.portal_fallback.xwayland": "開いたファイル選択はデスクトップのものではありません。XWayland ではポータルが親ウィンドウを拒否します。",
|
||||
"file_load.built": "構築済み: 全{total}フィーチャー中{built}件。",
|
||||
"file_load.converting": "ソリッドを変換中: {done}/{total}",
|
||||
"file_load.inserting": "ジオメトリを挿入中",
|
||||
@@ -1999,6 +2017,7 @@
|
||||
"material.selection.mixed": "異なる材料",
|
||||
"measure.at": "位置",
|
||||
"measure.item": "項目 {number}",
|
||||
"measure.line_to_centre": "線から中心まで",
|
||||
"measure.quick_copied": "測定値をコピーしました",
|
||||
"measure.quick_copy": "クリックで測定値をコピー",
|
||||
"measure.quick_edges": "エッジ {count} 本",
|
||||
@@ -2029,6 +2048,8 @@
|
||||
"nav.blender.help": "中ドラッグで回転、Shift+中ドラッグでパン、Ctrl+中ドラッグでズーム。右ドラッグではカメラは動きません。",
|
||||
"nav.creo.help": "マウスの中ボタンドラッグで回転、Shift+中ボタンで平行移動、Ctrl+中ボタンの上下ドラッグでズーム(下方向で拡大)。ホイールの回転でもズーム",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "中ボタンまたは右ボタンのドラッグは、途中でキーを押したり離したりしても、ボタンを離すまで開始時の回転・パン・ズームを続けます。オフにすると、ドラッグ中の各時点で押しているキーによって決まります。",
|
||||
"nav.drag_keeps_mode.label": "ドラッグは開始時のナビゲーションを維持",
|
||||
"nav.free_orbit.help": "マウスと3Dマウスで、極点を越えて回転し、ロールを保ちます。\nShiftによる軸スナップは無効になります。",
|
||||
"nav.free_orbit.label": "自由回転",
|
||||
"nav.free_orbit.snap_help": "マウスと3Dマウスで両極を越えて回転でき、ロールも保持されます。オービットのドラッグ中にShiftを押すと、視線方向とロールがスナップします。",
|
||||
@@ -2265,6 +2286,17 @@
|
||||
"prefs.auto_operation_note": "材料が接触する対象に応じて結合、新規ボディ、切り取りを選びます。",
|
||||
"prefs.axes": "軸",
|
||||
"prefs.axis": "軸",
|
||||
"prefs.background_choose": "画像を選択…",
|
||||
"prefs.background_image": "画像",
|
||||
"prefs.background_unreadable": "背景画像を読み込めませんでした",
|
||||
"prefs.blur_cost": "相対的な描画負荷です。バーが多いほど、通常は処理量が増えます。",
|
||||
"prefs.blur_device": "このグラフィックスデバイスまたは描画経路では利用できません",
|
||||
"prefs.blur_guard": "画面の応答性を保つため、ぼかしの品質を下げました。半径は変更されていません。",
|
||||
"prefs.blur_off": "オフ",
|
||||
"prefs.blur_reduce_transparency": "システムの透明度を下げる設定により無効です",
|
||||
"prefs.blur_software": "ソフトウェア描画では利用できません (gpu-software)",
|
||||
"prefs.blur_source": "このワークスペース表示では利用できません",
|
||||
"prefs.blur_web": "このウェブ描画経路では利用できません",
|
||||
"prefs.bodies_folders": "ボディフォルダー",
|
||||
"prefs.bodies_folders_always": "常に表示",
|
||||
"prefs.bodies_folders_never": "表示しない",
|
||||
@@ -2278,6 +2310,8 @@
|
||||
"prefs.browser_eye_left": "折りたたみ矢印の隣の目のアイコン",
|
||||
"prefs.browser_eye_left_note": "ブラウザーの各行の表示切り替えを、行の右端の列ではなく、以前のように折りたたみ矢印の隣に表示します。",
|
||||
"prefs.browser_reveal_on_hover": "ホバーで表示",
|
||||
"prefs.browser_rows_start_closed": "ブラウザーの行を閉じた状態で始める",
|
||||
"prefs.browser_rows_start_closed_note": "ブラウザーの配置とフォルダーを閉じた状態で表示するため、多数のコンポーネントを含むデザインでは最上位の行だけが表示されます。各行は矢印、「すべて展開」または「ブラウザで検索」で開きます。「ブラウザで検索」はボディまでの行だけを開き、前回の検索で開いた行を再び閉じます。3Dビューでボディを選んでもブラウザーはそのままです。オフのときはフォルダーが開いた状態で始まり、配置は「コンポーネントを最初から展開」の設定に従い、3Dビューで選ぶとブラウザーがそのボディまで開きます(以前と同じ)。",
|
||||
"prefs.browser_show_note": "「表示」はブラウザーを常に画面に表示します。「ホバーで表示」はポインターがその位置か左端の細いタブに来るまで隠し、ポインターが中にある間やそのメニューが開いている間は表示したままにします。",
|
||||
"prefs.browser_shown": "表示",
|
||||
"prefs.built_in": "組み込み",
|
||||
@@ -2323,6 +2357,13 @@
|
||||
"prefs.dialogs_edge": "ビューの端",
|
||||
"prefs.dialogs_left_column": "左の列",
|
||||
"prefs.dialogs_note": "コマンドダイアログを開く場所:ビューの端にドッキングするか、左の列でブラウザーと履歴の代わりに表示します。ダイアログを閉じるとブラウザーと履歴は元どおりに戻ります。",
|
||||
"prefs.dimension_step_angle": "ウィンドウの矢印、上下キー、ホイールで角度に加える量",
|
||||
"prefs.dimension_step_length": "ウィンドウの矢印、上下キー、ホイールで長さに加える量",
|
||||
"prefs.dimension_steps": "ウィンドウの増減量",
|
||||
"prefs.dimension_values": "寸法値の入力",
|
||||
"prefs.dimension_values_note": "寸法値を入力する場所です。ウィンドウでは矢印で値を増減でき、寸法ツールは先に文字を配置します。",
|
||||
"prefs.dimension_values_view": "ビュー内",
|
||||
"prefs.dimension_values_window": "ウィンドウ",
|
||||
"prefs.display": "表示",
|
||||
"prefs.edges": "エッジと陰影",
|
||||
"prefs.extend": "拡張",
|
||||
@@ -2402,6 +2443,10 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "キューブの隅にある2つのカーブした矢印は、画面内でビューをロールします。オン: 時計回りの矢印が反時計回りに、反時計回りの矢印が時計回りにロールします。オフ: 各矢印は、そのカーブの向きにビューをロールします。",
|
||||
"prefs.invert_navigator_arrows": "ビューキューブの回転矢印を反転",
|
||||
"prefs.invert_navigator_arrows_note": "キューブの周りの矢印が、矢印の指す方向にカメラを回します(Fusion や Inventor と同じ動き):左の矢印は右を指し、カメラを右に振ります。オフ: 矢印は、そのある面の方へキューブを回すため、左の矢印はカメラを左に振ります。カーブしたロール矢印は影響を受けません。",
|
||||
"prefs.keep_unsaved_also_asks": "タブを閉じるときにも確認する",
|
||||
"prefs.keep_unsaved_also_asks_tip": "オフにすると、未保存の各デザインのコピーが最新であれば、ブラウザーの確認なしでタブが閉じます。",
|
||||
"prefs.keep_unsaved_in_browser": "未保存の作業をこのブラウザーに保持する",
|
||||
"prefs.keep_unsaved_in_browser_tip": "開いている各デザインの未保存の変更を、このブラウザーのプライベートストレージに常に最新の状態で保持します。次回のアクセス時に復元を提案します。",
|
||||
"prefs.keep_work_on_tab_switch": "デザインを切り替えてもダイアログとツールを開いたままにする",
|
||||
"prefs.keep_work_on_tab_switch_note": "各デザインのタブが、開いているダイアログまたはツール(選択や入力した内容を含む)とツールバーのタブを保持します。デザインに戻ると元のまま表示されます。オフ: デザインを切り替えるとそれらはキャンセルされ、モデル タブが表示されます。",
|
||||
"prefs.labels": "ラベル",
|
||||
@@ -2413,6 +2458,8 @@
|
||||
"prefs.layout_page.card": "カード",
|
||||
"prefs.layout_page.column": "列",
|
||||
"prefs.layout_page.hover": "ホバー時",
|
||||
"prefs.layout_page.hover_panel_menus": "ポイントしてパネルメニューを切り替える",
|
||||
"prefs.layout_page.hover_panel_menus_help": "リボンのパネル名をクリックしてメニューを開きます。別のパネル名にポインターを合わせると切り替わります。リボンと開いているメニューの外に移動すると閉じます。",
|
||||
"prefs.layout_page.icons_labels": "アイコン+ラベル",
|
||||
"prefs.layout_page.ok_cancel": "OK とキャンセル",
|
||||
"prefs.layout_page.on_hover": "ホバー時",
|
||||
@@ -2533,6 +2580,8 @@
|
||||
"prefs.section_hatch_by_part": "部品ごと",
|
||||
"prefs.section_hatch_one": "単一スタイル",
|
||||
"prefs.select": "選択",
|
||||
"prefs.select_hidden_edges": "隠線表示で隠れた辺を選択する",
|
||||
"prefs.select_hidden_edges_help": "「Shaded with hidden edges」で面の向こうの辺を選択し、Filletにも使用できます。オフではv0.30.1相当の選択動作になります。非表示のボディは選択対象になりません。",
|
||||
"prefs.select_one_item": "項目を 1 つ選択",
|
||||
"prefs.select_other_hidden": "「他を選択」で非表示のジオメトリを選択",
|
||||
"prefs.select_other_hidden_help": "オン: ポインター付近の非表示のボディやスケッチ、遮られたエッジを含めます。オフ: ポインター直下の表示中のジオメトリだけを元のリストに表示します。",
|
||||
@@ -2557,6 +2606,8 @@
|
||||
"prefs.sketch.click_selects": "クリックで選択",
|
||||
"prefs.sketch.conflict_colour": "競合色",
|
||||
"prefs.sketch.drawing": "作図",
|
||||
"prefs.sketch.ellipse_axes": "楕円を軸と一緒に描く",
|
||||
"prefs.sketch.ellipse_axes_note": "長軸と短軸をコンストラクション線として描き、半軸と軸に寸法を付けられるようにします。オフ: 楕円のみを描き、D で短半径を寸法にします。値をどこへ引いても、これまでどおり R 付きで描かれます。",
|
||||
"prefs.sketch.exit_keep": "維持",
|
||||
"prefs.sketch.exit_previous": "開始前のビューに戻る",
|
||||
"prefs.sketch.exit_unless_rotated": "回転した場合を除く",
|
||||
@@ -2650,6 +2701,10 @@
|
||||
"prefs.theme_follow_off": "オフ",
|
||||
"prefs.theme_follow_stop": "追従を停止",
|
||||
"prefs.theme_import_hint": "クリックしてテーマファイルを選ぶか、ここにドロップするか、テーマをコピーした状態で Ctrl+V(Mac では Cmd+V)を押します",
|
||||
"prefs.theme_notice_dismiss": "非表示",
|
||||
"prefs.theme_option_blur": "パネル背面のぼかし",
|
||||
"prefs.theme_option_blur_help": "半透明パネルの背面のぼかしを物理ピクセル単位で指定します。0でオフになります。",
|
||||
"prefs.theme_option_invalid": "{option} の保存値が無効なため、テーマの既定値を使用します。保存値は保持されます。",
|
||||
"prefs.theme_option_opacity": "不透明度",
|
||||
"prefs.theme_option_opacity_help": "フローティングパネルの不透明度を調整します。ビューポートの色に重なっても文字が読めるように下限が適用されます。",
|
||||
"prefs.theme_option_tab_colours": "タブの色",
|
||||
@@ -2658,6 +2713,7 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "タブの切り替え時にパネルとビューポートの色を徐々に変えます。0で即座に切り替えます。",
|
||||
"prefs.theme_options_badge": "オプション",
|
||||
"prefs.theme_options_more": "その他 · {count}",
|
||||
"prefs.theme_options_newer": "このテーマには新しいバージョンの SolidCraft が必要です。更新後、元のテーマを再インポートしてオプション定義を復元してください。",
|
||||
"prefs.theme_options_reset": "リセット",
|
||||
"prefs.theme_options_unavailable": "このデバイスでは利用できません",
|
||||
"prefs.theme_paste": "テーマを貼り付け",
|
||||
@@ -2721,6 +2777,12 @@
|
||||
"recovery.just_now": "たった今",
|
||||
"recovery.min_ago": "{count}分前",
|
||||
"recovery.never_saved": "未保存",
|
||||
"recovery.open_saved": "保存済みのバージョンを開く",
|
||||
"recovery.restore": "復元",
|
||||
"recovery.restore_intro": "このページを前回閉じたときに保存されていなかった変更は、このブラウザーに保持されています。",
|
||||
"recovery.restore_row": "{name}、{when}に変更",
|
||||
"recovery.restore_title": "未保存のデザインを復元しますか?",
|
||||
"recovery.restore_title_one": "未保存のデザイン {name}({when}に変更)を復元しますか?",
|
||||
"refused.change": "{body} を変更できませんでした。",
|
||||
"refused.cut": "{body} をカットできませんでした。",
|
||||
"refused.face_curved": "曲面の 1 つがモデリングに使えません。",
|
||||
@@ -3242,9 +3304,11 @@
|
||||
"render.frame_wide": "16:9 · ワイドスクリーン",
|
||||
"render.gpu_depth": "最大パス深度",
|
||||
"render.gpu_options_help": "サンプル数、深度、シード、露出をこの設定に保存します。保存するのは選択中の画像方式だけです。",
|
||||
"render.gpu_path_crashed": "前回このグラフィックアダプターでの準備中に SolidCraft が終了しました。グラフィックドライバー内で終了した可能性が高いため、自動では再度準備しません。代わりに CPU レイトレーシングを使うか、もう一度試してください(ドライバーを更新した場合も再試行されます)。",
|
||||
"render.gpu_path_failed": "GPUパストレーサーを利用できません",
|
||||
"render.gpu_path_first_setup": "このGPUでの初回セットアップには数分かかることがあります。その間もSolidCraftは使用できます。以降の起動では、ドライバーが保持していればコンパイル済みのシェーダーを再利用します。",
|
||||
"render.gpu_path_preparing": "GPUパストレーサー:シェーダーをコンパイル中…",
|
||||
"render.gpu_path_retry": "もう一度試す",
|
||||
"render.gpu_seed": "乱数シード",
|
||||
"render.gpu_unavailable": "このデバイスでは GPU パストレーシングの準備ができていません。",
|
||||
"render.ground_apply": "床をフィットして保存",
|
||||
@@ -4052,6 +4116,8 @@
|
||||
"sample.plate.name": "サンプル プレート",
|
||||
"sample.vents.about": "矩形パターンに並んだスロット: 行数、列数、ピッチはパラメータ",
|
||||
"sample.vents.name": "通気プレート",
|
||||
"scale.per_axis": "軸ごと",
|
||||
"scale.uniform": "均等",
|
||||
"search.command_keys": "Enter 実行 Shift+Enter 実行して開いたままにする Esc 閉じる",
|
||||
"search.commands": "コマンド",
|
||||
"search.design": "このデザイン内",
|
||||
@@ -4142,6 +4208,32 @@
|
||||
"sketch.dof_left": "残り自由度 {count}",
|
||||
"sketch.dof_left_one": "残り自由度 {count}",
|
||||
"sketch.draw_curves": "{linetype}として描く",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "選択を{linetype}にする",
|
||||
"sketch.open_ends": "開放 {count}",
|
||||
"sketch.status_conflict": "拘束が競合しています",
|
||||
@@ -4363,6 +4455,7 @@
|
||||
"ui.3d_sketch": "3D スケッチ",
|
||||
"ui.activate": "アクティブ化",
|
||||
"ui.active_component": "アクティブなコンポーネント",
|
||||
"ui.addToQuickAccess": "クイックアクセスに追加",
|
||||
"ui.add_configuration": "構成を追加",
|
||||
"ui.add_parameter": "パラメーターを追加",
|
||||
"ui.agent_chat": "エージェントチャット",
|
||||
@@ -4784,6 +4877,7 @@
|
||||
"ui.default_modelling_orientation": "モデリングのデフォルト方向",
|
||||
"ui.delete": "削除",
|
||||
"ui.delete_select_first": "先に削除するものを選択してください: フィーチャ、ボディ、コンポーネント、スケッチ ジオメトリ",
|
||||
"ui.dependents_select_first": "先に、依存先を表示する対象を選択してください",
|
||||
"ui.depth": "深さ",
|
||||
"ui.design": "デザイン",
|
||||
"ui.design_properties.author": "作成者",
|
||||
@@ -4913,6 +5007,11 @@
|
||||
"ui.factor": "倍率",
|
||||
"ui.fade": "切り上げ",
|
||||
"ui.fade_length": "切り上げ長さ",
|
||||
"ui.fair_iterations": "反復回数",
|
||||
"ui.fair_keep_edges": "エッジを保持",
|
||||
"ui.fair_strength": "強度",
|
||||
"ui.fair_surface_title": "サーフェスフェアリング",
|
||||
"ui.fair_tolerance_default": "ボディサイズの 0.001 倍",
|
||||
"ui.feature_history": "フィーチャー履歴",
|
||||
"ui.feature_options": "フィーチャーオプション",
|
||||
"ui.features": "フィーチャー",
|
||||
@@ -4941,6 +5040,10 @@
|
||||
"ui.fillet_setback_least": "{least} より大きく",
|
||||
"ui.fillet_setback_tip": "なめらかなコーナーパッチが始まる位置: コーナーから各エッジに沿った距離。既定値は半径の {factor} 倍。最小セットバック (セットバックなしでラウンド同士が交わる位置。面どうしが直角なら半径) より大きくする必要があります。",
|
||||
"ui.fillet_smooth_radius": "半径をなめらかに変化",
|
||||
"ui.fillet_turn": "曲がり角",
|
||||
"ui.fillet_turn_mitre": "留め継ぎ",
|
||||
"ui.fillet_turn_swept": "回転",
|
||||
"ui.fillet_turn_tip": "2 つのフィレットが間にある鋭いコーナーエッジを回り込む所(ボスの根元のフィレットが立ち上がりエッジを回る所など)。\n留め継ぎ: フィレットは延びて留め継ぎで出会います。\n回転: フィレットはコーナーエッジで止まり、その周りを回ります。コーナーは両方に接するトーラスです。同じ半径のフィレットが、まっすぐな凸のコーナーエッジを回る場合のみ。",
|
||||
"ui.fillet_type_choices_tip": "一定の半径: エッジ全体で一つの半径。\n弦長: 面がどんな角度で交わっても、フィレットの幅が同じ。\n可変半径: 両端の半径と、追加すればエッジ沿いの半径。\n曲率連続: 平面と接する所で映り込みが折れない断面 (G2)。\nスプライン: エッジ沿いの半径が、設定した制御半径に従います。",
|
||||
"ui.fillet_type_curvature": "曲率連続",
|
||||
"ui.fillet_type_tip": "エッジを横切る断面。曲率連続: 平面の間の曲率連続 (G2) 断面で、映り込みが面からフィレットへ折れずにつながります。",
|
||||
@@ -4986,6 +5089,7 @@
|
||||
"ui.heading_construct": "構築",
|
||||
"ui.heading_create": "作成",
|
||||
"ui.heading_create_sketch": "スケッチを作成",
|
||||
"ui.heading_custom_view_navigator": "カスタム ビューナビゲーター",
|
||||
"ui.heading_cylinder": "円柱",
|
||||
"ui.heading_design": "デザイン",
|
||||
"ui.heading_draft": "勾配",
|
||||
@@ -5121,6 +5225,7 @@
|
||||
"ui.manipulator_free_alt": "マニピュレータのフリードラッグを Alt/Option でも有効",
|
||||
"ui.manipulator_free_alt_tip": "オン: 押し出し/ギズモ矢印で Alt/Option または Ctrl/Cmd が値スナップを無効にします。オフ: Ctrl/Cmd のみ。",
|
||||
"ui.manufacture": "製造",
|
||||
"ui.match_surface_title": "サーフェス一致",
|
||||
"ui.material_review.axes": "材料ローカル軸。ボディの方向は割り当てません。",
|
||||
"ui.material_review.claims": "出典とライセンスの申告は未確認です。欠落値は不明のままです。",
|
||||
"ui.material_review.compressive_strength": "圧縮強度",
|
||||
@@ -5184,6 +5289,7 @@
|
||||
"ui.options": "オプション",
|
||||
"ui.orbit": "回転",
|
||||
"ui.orbit_around": "回転中心",
|
||||
"ui.organize": "整理",
|
||||
"ui.orientation": "向き",
|
||||
"ui.origin": "原点",
|
||||
"ui.palette_construction_mode": "補助線モード (X)",
|
||||
@@ -5272,6 +5378,7 @@
|
||||
"ui.quality": "品質",
|
||||
"ui.quantity": "数量",
|
||||
"ui.quantity_2": "数量 2",
|
||||
"ui.quick_access_full": "クイックアクセスバーに置けるコマンドは {count} 個までです。追加するには 1 つ削除してください",
|
||||
"ui.quit": "終了",
|
||||
"ui.radial_context_menu": "パイ メニュー",
|
||||
"ui.radius": "半径",
|
||||
@@ -5289,6 +5396,7 @@
|
||||
"ui.relink_not_linked": "このコンポーネントは参照元の設計にリンクされていません。",
|
||||
"ui.relink_select_one": "リンクされたコンポーネントまたはそのインスタンスを1つ選択してください。",
|
||||
"ui.relink_source_all": "参照元を再リンク(すべてのインスタンス)…",
|
||||
"ui.removeFromQuickAccess": "クイックアクセスから削除",
|
||||
"ui.removeFromToolbar": "ツールバーから削除",
|
||||
"ui.removeSection": "セクションを解除",
|
||||
"ui.render": "レンダリング",
|
||||
@@ -5297,6 +5405,8 @@
|
||||
"ui.repeat_nothing": "まだ繰り返すものがありません。先にツールかダイアログを開始してください。",
|
||||
"ui.replace_with_link_done": "コピーはリンクされたデザインの配置になりました。",
|
||||
"ui.report_a_bug": "不具合を報告",
|
||||
"ui.report_characters_left_one": "残り {count} 文字",
|
||||
"ui.report_characters_left_other": "残り {count} 文字",
|
||||
"ui.reset": "リセット",
|
||||
"ui.reset_all": "すべてリセット",
|
||||
"ui.reset_the_settings_on_this_page": "このページの設定をリセット",
|
||||
@@ -5581,6 +5691,14 @@
|
||||
"values must be numbers or expressions": "値は数値または式にしてください",
|
||||
"view.auto_ortho_faces": "回転中は透視投影、面を正面に見ると平行投影",
|
||||
"view.render_inspector": "レンダーインスペクター",
|
||||
"view_gizmo.arrows": "回転矢印",
|
||||
"view_gizmo.home": "ホーム ボタン",
|
||||
"view_gizmo.no_such_body": "このデザインにその名前のボディはありません。",
|
||||
"view_gizmo.note": "ビューでボディを選択してください。ボディは環境設定と設定プロファイルに保存され、すべてのデザインで表示されます。面をクリックすると、その面を正面から見ます。",
|
||||
"view_gizmo.texture": "テクスチャ",
|
||||
"view_gizmo.themed": "テーマ",
|
||||
"view_gizmo.too_detailed": "ビューナビゲーターには細かすぎます (10,000 三角形を超えています)",
|
||||
"view_gizmo.too_thin": "ビューナビゲーターには細長すぎます: 最も薄い辺が最も長い辺の 5 分の 1 以上である必要があります",
|
||||
"web.files.insert_drawings_hint": "CADファイル、メッシュ、DXF、SVGを開いているデザインに挿入",
|
||||
"web.files.insert_stored_drawings_hint": "開いているデザインに挿入(CADファイル、メッシュ、DXF、SVG)",
|
||||
"web.open_full": "SolidCraft で開く",
|
||||
|
||||
@@ -544,6 +544,8 @@
|
||||
"Suppress": "비활성화",
|
||||
"Suppressed": "억제됨",
|
||||
"Surface": "곡면",
|
||||
"Surface or face": "곡면 또는 면",
|
||||
"Surface or faces": "곡면 또는 면",
|
||||
"Surface, or faces to turn over": "곡면 또는 뒤집을 면",
|
||||
"Surfaces": "곡면",
|
||||
"Swap Ends": "양 끝 바꾸기",
|
||||
@@ -555,6 +557,7 @@
|
||||
"Target": "대상",
|
||||
"Target Body": "대상 바디",
|
||||
"Target Face (optional)": "대상 면 (선택 사항)",
|
||||
"Target edges": "대상 모서리",
|
||||
"Tear": "찢김",
|
||||
"Technical details": "기술 세부 정보",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "점선 스케치는 저장된 평면 가이드입니다. 새 면을 선택하여 부착을 복구하세요.",
|
||||
@@ -1133,6 +1136,7 @@
|
||||
"cmd.timeline_stop": "기록 재생 중지",
|
||||
"cmd.timeline_suppress": "특성 비활성화",
|
||||
"cmd.ui.assistant": "도우미 표시 전환",
|
||||
"cmd.view.navigator": "사용자 지정 뷰 탐색기",
|
||||
"cmd.view.render_browser": "렌더 브라우저",
|
||||
"cmd.view.render_dock": "렌더 도크 이동",
|
||||
"cmd.view.render_focus": "렌더 집중 보기",
|
||||
@@ -1205,10 +1209,16 @@
|
||||
"dialog.computing": "계산 중… {seconds}초",
|
||||
"dialog.could_not_be_checked": "확인할 수 없음",
|
||||
"dialog.custom_axis": "↖ 사용자 정의 축",
|
||||
"dialog.cylinder_face_tip": "원기둥이 놓일 모델의 면 또는 꼭짓점: 여기를 클릭한 다음 뷰에서 그것을 클릭. 비워 두면 X, Y, Z로 배치합니다.",
|
||||
"dialog.dimension_insert_hint": "다른 구동 치수를 두 번 클릭하여 이름 삽입",
|
||||
"dialog.dimension_name": "치수: {name}",
|
||||
"dialog.dimension_step_down": "{step} 줄이기",
|
||||
"dialog.dimension_step_expression": "숫자만 증감할 수 있으며 수식은 입력한 그대로 유지됩니다",
|
||||
"dialog.dimension_step_up": "{step} 늘리기",
|
||||
"dialog.edit_title": "{title} 편집",
|
||||
"dialog.pattern_sketch_not_object": "바디나 피처를 선택하세요. 스케치는 둘 다 아닙니다. 곡선은 스케치 안의 패턴이나 대칭 복사로 반복하거나, 먼저 돌출하세요.",
|
||||
"dialog.primitive_point_tip": "X, Y, Z를 채우는 점: 여기를 클릭한 다음 모델의 꼭짓점, 모서리, 면, 스케치의 점 또는 원점을 클릭.",
|
||||
"dialog.reference_edge_tip": "아래 거리를 재는 기준이 되는, 선택한 면의 직선 모서리: 여기를 클릭한 다음 모서리를 클릭.",
|
||||
"dialog.select": "↖ 선택",
|
||||
"dialog.selected_count": "↖ {count}개 선택됨",
|
||||
"dialog.selection_constraint": "구속 조건 {id}",
|
||||
@@ -1237,6 +1247,9 @@
|
||||
"dxf.tolerance_help": "표시된 바디와 스케치만 내보내며 곡선은 직선 구간으로 근사합니다. 허용 오차가 작을수록 곡선이 매끄러워지지만 처리 시간과 메모리 사용량이 늘어납니다.",
|
||||
"each rail's curves must be in one sketch": "각 레일의 곡선은 하나의 스케치 안에 있어야 합니다",
|
||||
"feature computation ended without a result": "특성 계산이 결과 없이 끝났습니다",
|
||||
"file.open.portal_fallback.no_portal_service": "열린 파일 선택은 데스크톱의 것이 아닙니다: 데스크톱의 파일 선택 서비스를 사용할 수 없습니다.",
|
||||
"file.open.portal_fallback.no_session_bus": "열린 파일 선택은 데스크톱의 것이 아닙니다: D-Bus 세션 버스가 없습니다.",
|
||||
"file.open.portal_fallback.xwayland": "열린 파일 선택은 데스크톱의 것이 아닙니다: XWayland에서는 포털이 부모 창을 거부합니다.",
|
||||
"file_load.built": "지금까지 빌드됨: 피처 {total}개 중 {built}개.",
|
||||
"file_load.converting": "솔리드 변환 중 {done}/{total}",
|
||||
"file_load.inserting": "기하 삽입 중",
|
||||
@@ -1431,6 +1444,7 @@
|
||||
"material.selection.mixed": "서로 다른 재질",
|
||||
"measure.at": "위치",
|
||||
"measure.item": "항목 {number}",
|
||||
"measure.line_to_centre": "선에서 중심까지",
|
||||
"messages.ago_h": "{n}시간 전",
|
||||
"messages.ago_min": "{n}분 전",
|
||||
"messages.ago_s": "{n}초 전",
|
||||
@@ -1457,6 +1471,8 @@
|
||||
"nav.blender.help": "가운데 드래그는 회전, Shift+가운데 드래그는 이동, Ctrl+가운데 드래그는 확대/축소입니다. 오른쪽 드래그는 카메라에 영향을 주지 않습니다",
|
||||
"nav.creo.help": "가운데 버튼을 끌면 회전, Shift+가운데 버튼은 이동, Ctrl+가운데 버튼을 위아래로 끌면 확대/축소합니다(아래로 끌면 확대). 휠을 굴려도 확대/축소합니다",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "가운데 또는 오른쪽 버튼 드래그는 도중에 키를 누르거나 떼어도 버튼을 놓을 때까지 시작할 때의 회전, 이동 또는 확대/축소를 계속합니다. 끄면 드래그하는 동안 누르고 있는 키가 매 순간 결정합니다.",
|
||||
"nav.drag_keeps_mode.label": "드래그는 시작할 때의 탐색 방식을 유지",
|
||||
"nav.free_orbit.help": "마우스와 3D 마우스로 롤을 유지하며 양 극점을 넘어 회전합니다. Shift 축 스냅은 비활성화됩니다.",
|
||||
"nav.free_orbit.label": "자유 회전",
|
||||
"nav.free_orbit.snap_help": "마우스와 3D 마우스로 양쪽 극점을 지나 회전할 수 있으며 롤 각도가 유지됩니다. 궤도 회전 드래그 중 Shift를 누르면 보기 방향과 롤 각도가 스냅됩니다.",
|
||||
@@ -1657,6 +1673,14 @@
|
||||
"prefs.auto_operation_note": "재료가 닿는 대상에 따라 결합, 새 바디 또는 자르기를 선택합니다.",
|
||||
"prefs.axes": "축",
|
||||
"prefs.axis": "축",
|
||||
"prefs.blur_cost": "상대적인 렌더링 비용입니다. 막대가 많을수록 일반적으로 더 많은 작업이 필요합니다.",
|
||||
"prefs.blur_device": "이 그래픽 장치 또는 렌더링 경로에서는 사용할 수 없습니다",
|
||||
"prefs.blur_guard": "화면의 원활한 응답을 위해 흐림 품질을 낮췄습니다. 반경은 변경되지 않습니다.",
|
||||
"prefs.blur_off": "끄기",
|
||||
"prefs.blur_reduce_transparency": "시스템의 투명도 줄이기 설정에 의해 비활성화됨",
|
||||
"prefs.blur_software": "소프트웨어 그래픽에서는 사용할 수 없습니다 (gpu-software)",
|
||||
"prefs.blur_source": "이 작업 공간 보기에서는 사용할 수 없습니다",
|
||||
"prefs.blur_web": "이 웹 렌더링 경로에서는 사용할 수 없습니다",
|
||||
"prefs.bodies_folders": "바디 폴더",
|
||||
"prefs.bodies_folders_always": "항상 표시",
|
||||
"prefs.bodies_folders_never": "표시 안 함",
|
||||
@@ -1669,6 +1693,8 @@
|
||||
"prefs.browser_components_open_note": "브라우저의 모든 구성요소가 이전처럼 펼쳐진 상태로 시작합니다. 끄면 직접 펼칠 때까지 접혀 있습니다.",
|
||||
"prefs.browser_eye_left": "접기 화살표 옆의 눈 아이콘",
|
||||
"prefs.browser_eye_left_note": "브라우저 행의 표시 아이콘을 행 오른쪽 끝의 열이 아니라 이전처럼 접기 화살표 옆에 표시합니다.",
|
||||
"prefs.browser_rows_start_closed": "브라우저 행을 접힌 상태로 시작",
|
||||
"prefs.browser_rows_start_closed_note": "브라우저의 배치와 폴더가 접힌 상태로 시작하므로, 컴포넌트가 많은 디자인은 최상위 행만 보여 줍니다. 각 행은 화살표, '모두 펼치기' 또는 '브라우저에서 찾기'로 엽니다. '브라우저에서 찾기'는 바디까지의 행만 열고, 지난번 찾기로 연 행은 다시 접습니다. 3D 뷰에서 바디를 골라도 브라우저는 그대로입니다. 끄면 폴더가 펼친 상태로 시작하고, 배치는 '구성요소를 펼친 상태로 시작' 설정을 따르며, 3D 뷰에서 고르면 브라우저가 그 바디까지 열립니다(이전과 같음).",
|
||||
"prefs.built_in": "기본 제공",
|
||||
"prefs.button_left": "왼쪽 드래그",
|
||||
"prefs.button_middle": "가운데 드래그",
|
||||
@@ -1712,6 +1738,13 @@
|
||||
"prefs.dialogs_edge": "뷰 가장자리",
|
||||
"prefs.dialogs_left_column": "왼쪽 열",
|
||||
"prefs.dialogs_note": "명령 대화 상자가 열리는 위치: 뷰 가장자리에 고정하거나, 왼쪽 열에서 브라우저와 이력 대신 표시합니다. 대화 상자를 닫으면 브라우저와 이력이 그대로 돌아옵니다.",
|
||||
"prefs.dimension_step_angle": "창의 화살표, 위/아래 키와 휠이 각도에 더하는 값",
|
||||
"prefs.dimension_step_length": "창의 화살표, 위/아래 키와 휠이 길이에 더하는 값",
|
||||
"prefs.dimension_steps": "창의 증감 단위",
|
||||
"prefs.dimension_values": "치수 값",
|
||||
"prefs.dimension_values_note": "치수 값을 입력하는 위치입니다. 창에는 값을 증감하는 화살표가 있으며, 치수 도구는 텍스트를 먼저 배치합니다.",
|
||||
"prefs.dimension_values_view": "뷰에서",
|
||||
"prefs.dimension_values_window": "창에서",
|
||||
"prefs.display": "표시",
|
||||
"prefs.edges": "모서리 및 음영",
|
||||
"prefs.extend": "확장",
|
||||
@@ -1785,6 +1818,8 @@
|
||||
"prefs.layout_page.card": "카드",
|
||||
"prefs.layout_page.column": "열",
|
||||
"prefs.layout_page.hover": "마우스를 올릴 때",
|
||||
"prefs.layout_page.hover_panel_menus": "포인터를 올려 패널 메뉴 전환",
|
||||
"prefs.layout_page.hover_panel_menus_help": "리본의 패널 이름을 클릭하여 메뉴를 엽니다. 다른 패널 이름에 포인터를 올리면 메뉴가 전환됩니다. 리본과 열린 메뉴 밖으로 포인터를 이동하면 메뉴가 닫힙니다.",
|
||||
"prefs.layout_page.icons_labels": "아이콘 + 레이블",
|
||||
"prefs.layout_page.ok_cancel": "확인과 취소",
|
||||
"prefs.layout_page.on_hover": "마우스를 올릴 때",
|
||||
@@ -1870,6 +1905,8 @@
|
||||
"prefs.search": "설정 검색",
|
||||
"prefs.search_empty": "일치하는 설정이 없습니다",
|
||||
"prefs.select": "선택",
|
||||
"prefs.select_hidden_edges": "숨은 모서리가 표시된 음영 뷰에서 숨은 모서리 선택",
|
||||
"prefs.select_hidden_edges_help": "숨은 모서리가 표시된 음영 뷰에서 면 뒤의 모서리를 선택하여 필렛에 사용할 수 있습니다. 끄면 v0.30.1의 선택 동작을 유지합니다. 숨긴 바디는 선택되지 않습니다.",
|
||||
"prefs.select_one_item": "항목 하나 선택",
|
||||
"prefs.select_other_hidden": "다른 항목 선택으로 숨겨진 형상 선택",
|
||||
"prefs.select_other_hidden_help": "켜짐: 포인터 근처의 숨겨진 바디, 스케치 및 가려진 모서리를 포함합니다. 꺼짐: 포인터 바로 아래에 보이는 형상만 포함하는 기존 목록을 사용합니다.",
|
||||
@@ -1894,6 +1931,8 @@
|
||||
"prefs.sketch.click_selects": "클릭으로 선택",
|
||||
"prefs.sketch.conflict_colour": "충돌 색상",
|
||||
"prefs.sketch.drawing": "그리기",
|
||||
"prefs.sketch.ellipse_axes": "타원을 축과 함께 그리기",
|
||||
"prefs.sketch.ellipse_axes_note": "장축과 단축을 구성선으로 그려 반축과 축에 치수를 지정할 수 있습니다. 끄면: 타원만 그리며, D는 값을 어디로 끌든 단반경을 주며, 이전처럼 R로 표시되어 그려집니다.",
|
||||
"prefs.sketch.exit_keep": "유지",
|
||||
"prefs.sketch.exit_previous": "이전 뷰로 돌아가기",
|
||||
"prefs.sketch.exit_unless_rotated": "회전하지 않았다면",
|
||||
@@ -1975,6 +2014,10 @@
|
||||
"prefs.theme_follow_note": "다른 도구가 파일을 다시 쓸 때마다 다시 불러옵니다. 테마를 선택하면 따르기를 중지합니다.",
|
||||
"prefs.theme_follow_off": "끄기",
|
||||
"prefs.theme_follow_stop": "따르기 중지",
|
||||
"prefs.theme_notice_dismiss": "닫기",
|
||||
"prefs.theme_option_blur": "패널 뒤 흐림",
|
||||
"prefs.theme_option_blur_help": "반투명 패널 뒤의 흐림을 물리적 픽셀 단위로 설정합니다. 0이면 꺼집니다.",
|
||||
"prefs.theme_option_invalid": "{option}의 저장된 값이 유효하지 않아 테마 기본값을 사용합니다. 저장된 값은 유지됩니다.",
|
||||
"prefs.theme_option_opacity": "불투명도",
|
||||
"prefs.theme_option_opacity_help": "플로팅 패널의 불투명도를 조정합니다. 뷰포트 색상 위에서도 텍스트를 읽을 수 있도록 최솟값이 적용됩니다.",
|
||||
"prefs.theme_option_tab_colours": "탭 색상",
|
||||
@@ -1983,6 +2026,7 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "탭을 전환할 때 패널 및 뷰포트 색상을 서서히 변경합니다. 0이면 즉시 전환합니다.",
|
||||
"prefs.theme_options_badge": "옵션",
|
||||
"prefs.theme_options_more": "더 보기 · {count}",
|
||||
"prefs.theme_options_newer": "이 테마에는 더 최신 버전의 SolidCraft가 필요합니다. 업그레이드 후 원본 테마를 다시 가져와 옵션 정의를 복구하세요.",
|
||||
"prefs.theme_options_reset": "초기화",
|
||||
"prefs.theme_options_unavailable": "이 기기에서는 사용할 수 없음",
|
||||
"prefs.timeline_half_hour": "−30분",
|
||||
@@ -2920,6 +2964,8 @@
|
||||
"sample.plate.name": "샘플 플레이트",
|
||||
"sample.vents.about": "직사각형 패턴의 슬롯: 행, 열, 피치가 매개변수",
|
||||
"sample.vents.name": "통풍 플레이트",
|
||||
"scale.per_axis": "축별",
|
||||
"scale.uniform": "균일",
|
||||
"search.command_keys": "Enter 실행 Shift+Enter 실행 후 열어 두기 Esc 닫기",
|
||||
"search.commands": "명령",
|
||||
"search.design": "이 디자인에서",
|
||||
@@ -3562,6 +3608,7 @@
|
||||
"ui.default_modelling_orientation": "기본 모델링 방향",
|
||||
"ui.delete": "삭제",
|
||||
"ui.delete_select_first": "먼저 삭제할 항목을 선택하세요: 피처, 바디, 컴포넌트 또는 스케치 형상",
|
||||
"ui.dependents_select_first": "먼저 종속 항목을 표시할 대상을 선택하세요",
|
||||
"ui.depth": "깊이",
|
||||
"ui.design": "디자인",
|
||||
"ui.design_properties.author": "작성자",
|
||||
@@ -3684,6 +3731,11 @@
|
||||
"ui.factor": "배율",
|
||||
"ui.fade": "페이드",
|
||||
"ui.fade_length": "페이드 길이",
|
||||
"ui.fair_iterations": "반복 횟수",
|
||||
"ui.fair_keep_edges": "모서리 유지",
|
||||
"ui.fair_strength": "강도",
|
||||
"ui.fair_surface_title": "서피스 페어링",
|
||||
"ui.fair_tolerance_default": "바디 크기의 0.001배",
|
||||
"ui.feature_history": "피처 기록",
|
||||
"ui.feature_options": "피처 옵션",
|
||||
"ui.features": "피처",
|
||||
@@ -3712,6 +3764,10 @@
|
||||
"ui.fillet_setback_least": "{least}보다 크게",
|
||||
"ui.fillet_setback_tip": "부드러운 코너 패치가 시작되는 위치: 코너에서 각 모서리를 따른 거리. 기본값은 반지름의 {factor}배. 최소 셋백(셋백 없이 라운드가 만나는 곳, 면이 서로 직각이면 반지름)보다 커야 합니다.",
|
||||
"ui.fillet_smooth_radius": "반지름 부드럽게 변화",
|
||||
"ui.fillet_turn": "꺾임",
|
||||
"ui.fillet_turn_mitre": "마이터",
|
||||
"ui.fillet_turn_swept": "회전",
|
||||
"ui.fillet_turn_tip": "두 라운드가 사이의 날카로운 코너 모서리를 돌아가는 곳(보스 밑동의 라운드가 세로 모서리를 도는 곳 등).\n마이터: 라운드가 계속 이어져 마이터로 만납니다.\n회전: 라운드가 코너 모서리에서 멈추고 그 둘레를 돕니다. 코너는 둘 모두에 접하는 토러스입니다. 반지름이 같은 라운드가 곧고 볼록한 코너 모서리를 돌 때만.",
|
||||
"ui.fillet_type_choices_tip": "일정한 반지름: 모서리 전체에 하나의 반지름.\n현 길이: 면이 어떤 각도로 만나든 라운드의 폭이 같습니다.\n가변 반지름: 양 끝의 반지름과, 추가하면 모서리를 따라 더.\n곡률 연속: 평면과 만나는 곳에서 반사가 꺾이지 않는 단면(G2).\n스플라인: 모서리를 따라 반지름이 설정한 제어 반지름을 따릅니다.",
|
||||
"ui.fillet_type_curvature": "곡률 연속",
|
||||
"ui.fillet_type_tip": "모서리를 가로지르는 단면. 곡률 연속: 평면 사이의 곡률 연속(G2) 단면으로, 반사가 면에서 라운드로 꺾임 없이 이어집니다.",
|
||||
@@ -3755,6 +3811,7 @@
|
||||
"ui.heading_construct": "구성",
|
||||
"ui.heading_create": "만들기",
|
||||
"ui.heading_create_sketch": "스케치 만들기",
|
||||
"ui.heading_custom_view_navigator": "사용자 지정 뷰 탐색기",
|
||||
"ui.heading_cylinder": "원통",
|
||||
"ui.heading_design": "디자인",
|
||||
"ui.heading_draft": "구배",
|
||||
@@ -3878,6 +3935,7 @@
|
||||
"ui.manipulator_free_alt": "Alt/Option으로도 매니퓰레이터 자유 드래그",
|
||||
"ui.manipulator_free_alt_tip": "켬: 돌출 / 기즈모 화살표에서 Alt/Option 또는 Ctrl/Cmd를 누르면 값 스냅이 해제됩니다. 끔: Ctrl/Cmd만 해당.",
|
||||
"ui.manufacture": "제조",
|
||||
"ui.match_surface_title": "서피스 일치",
|
||||
"ui.material_review.axes": "재료 로컬 축입니다. 바디 방향을 지정하지 않습니다.",
|
||||
"ui.material_review.claims": "출처와 라이선스는 검증되지 않은 주장입니다. 누락된 값은 알 수 없는 상태로 유지됩니다.",
|
||||
"ui.material_review.compressive_strength": "압축 강도",
|
||||
@@ -3937,6 +3995,7 @@
|
||||
"ui.options": "옵션",
|
||||
"ui.orbit": "회전",
|
||||
"ui.orbit_around": "궤도 회전 기준",
|
||||
"ui.organize": "정리",
|
||||
"ui.orientation": "배향",
|
||||
"ui.origin": "원점",
|
||||
"ui.palette_construction_mode": "보조선 모드 (X)",
|
||||
@@ -4282,5 +4341,13 @@
|
||||
"values must be finite numbers": "값은 유한한 숫자여야 합니다",
|
||||
"values must be numbers or expressions": "값은 숫자 또는 식이어야 합니다",
|
||||
"view.render_inspector": "렌더 검사기",
|
||||
"view_gizmo.arrows": "회전 화살표",
|
||||
"view_gizmo.home": "홈 버튼",
|
||||
"view_gizmo.no_such_body": "이 디자인에 그런 이름의 바디가 없습니다.",
|
||||
"view_gizmo.note": "뷰에서 바디를 선택하십시오. 바디는 환경설정과 설정 프로필에 저장되어 모든 디자인에 표시됩니다. 면을 클릭하면 그 면을 정면으로 봅니다.",
|
||||
"view_gizmo.texture": "텍스처",
|
||||
"view_gizmo.themed": "테마",
|
||||
"view_gizmo.too_detailed": "뷰 탐색기에 너무 세밀합니다 (삼각형 10,000개 초과)",
|
||||
"view_gizmo.too_thin": "뷰 탐색기에 너무 가늘고 깁니다: 가장 얇은 변이 가장 긴 변의 5분의 1 이상이어야 합니다",
|
||||
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ 선택 · Enter / Tab 완성 · Esc 닫기"
|
||||
}
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Analisis Kebolehcapaian",
|
||||
"cmd.inspect_center_of_mass": "Pusat Jisim",
|
||||
"cmd.inspect_clearance": "Kelegaan",
|
||||
"cmd.inspect_colored_components": "Aktifkan Warna Komponen",
|
||||
"cmd.inspect_colored_components_disable": "Nyahaktifkan Warna Komponen",
|
||||
"cmd.inspect_compare": "Bandingkan Jasad",
|
||||
"cmd.inspect_compare_picture": "Banding dengan Imej",
|
||||
"cmd.inspect_continuity": "Analisis Keselanjaran",
|
||||
@@ -3809,6 +3807,8 @@
|
||||
"sample.plate.name": "Plat Contoh",
|
||||
"sample.vents.about": "Corak slot segi empat tepat: baris, lajur dan pic sebagai parameter",
|
||||
"sample.vents.name": "Plat Pengudaraan",
|
||||
"scale.per_axis": "Mengikut paksi",
|
||||
"scale.uniform": "Seragam",
|
||||
"search.command_keys": "Enter jalankan Shift+Enter jalankan dan kekalkan terbuka Esc tutup",
|
||||
"search.commands": "Arahan",
|
||||
"search.design": "Dalam reka bentuk ini",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Tilgjengelighetsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massesenter",
|
||||
"cmd.inspect_clearance": "Klaring",
|
||||
"cmd.inspect_colored_components": "Aktiver komponentfarger",
|
||||
"cmd.inspect_colored_components_disable": "Deaktiver komponentfarger",
|
||||
"cmd.inspect_compare": "Sammenlign kropper",
|
||||
"cmd.inspect_compare_picture": "Sammenlign bilde",
|
||||
"cmd.inspect_continuity": "Kontinuitetsanalyse",
|
||||
@@ -3809,6 +3807,8 @@
|
||||
"sample.plate.name": "Eksempelplate",
|
||||
"sample.vents.about": "Spor i et rektangulært mønster: rader, kolonner og stigning er parametere",
|
||||
"sample.vents.name": "Ventilasjonsplate",
|
||||
"scale.per_axis": "Per akse",
|
||||
"scale.uniform": "Jevn",
|
||||
"search.command_keys": "Enter kjør Shift+Enter kjør og behold åpen Esc lukk",
|
||||
"search.commands": "Kommandoer",
|
||||
"search.design": "I dette designet",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Toegankelijkheidsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Zwaartepunt",
|
||||
"cmd.inspect_clearance": "Speling",
|
||||
"cmd.inspect_colored_components": "Componentkleuren inschakelen",
|
||||
"cmd.inspect_colored_components_disable": "Componentkleuren uitschakelen",
|
||||
"cmd.inspect_compare": "Lichamen vergelijken",
|
||||
"cmd.inspect_compare_picture": "Afbeelding vergelijken",
|
||||
"cmd.inspect_continuity": "Continuïteitsanalyse",
|
||||
@@ -1368,6 +1366,9 @@
|
||||
"dialog.custom_axis": "↖ Aangepaste as",
|
||||
"dialog.dimension_insert_hint": "Klik op een andere aandrijvende maat om de naam ervan in te voegen",
|
||||
"dialog.dimension_name": "Maat: {name}",
|
||||
"dialog.dimension_step_down": "{step} omlaag",
|
||||
"dialog.dimension_step_expression": "Alleen een getal verspringt; een uitdrukking blijft zoals ingevoerd",
|
||||
"dialog.dimension_step_up": "{step} omhoog",
|
||||
"dialog.edit_title": "{title} BEWERKEN",
|
||||
"dialog.no_shown_body": "Geen zichtbaar lichaam om te wijzigen: toon er een of kies Nieuw lichaam",
|
||||
"dialog.pattern_sketch_not_object": "Kies lichamen of kenmerken: een schets is geen van beide. Herhaal de curven ervan met het eigen Patroon of Spiegelen van de schets, of extrudeer de schets eerst.",
|
||||
@@ -1392,6 +1393,7 @@
|
||||
"document.read_only_save": "Dit ontwerp is alleen-lezen geopend: sla het op als nieuw bestand",
|
||||
"document.save_changes": "Wijzigingen in {name} opslaan?",
|
||||
"document.save_copy": "Kopie opslaan…",
|
||||
"document.tab_unsaved_changes": "Niet-opgeslagen wijzigingen",
|
||||
"document.unsaved_changes": "{name} heeft niet-opgeslagen wijzigingen. Als u niet opslaat, gaan ze verloren.",
|
||||
"drive.rigid": "{joint} is een starre verbinding: die houdt zijn onderdelen vast en heeft dus niets om aan te sturen. Wijzig het type in Verbinding bewerken om ze te laten bewegen: Draaiend om te draaien, Schuivend om te schuiven, Cilindrisch voor beide.",
|
||||
"drive.stopped_by_contact": "Gestopt bij {value}: {a} zou door {b} heen gaan",
|
||||
@@ -2194,6 +2196,13 @@
|
||||
"prefs.dialogs_edge": "Aan de rand van de weergave",
|
||||
"prefs.dialogs_left_column": "In de linkerkolom",
|
||||
"prefs.dialogs_note": "Waar opdrachtdialogen openen: vastgezet aan de rand van de weergave, of in de linkerkolom in plaats van de browser en de geschiedenis, die terugkomen zoals ze waren wanneer de dialoog sluit.",
|
||||
"prefs.dimension_step_angle": "Wat de pijlen van het venster, Omhoog/Omlaag en het wieltje bij een hoek optellen",
|
||||
"prefs.dimension_step_length": "Wat de pijlen van het venster, Omhoog/Omlaag en het wieltje bij een lengte optellen",
|
||||
"prefs.dimension_steps": "Stappen in het venster",
|
||||
"prefs.dimension_values": "Maatwaarden",
|
||||
"prefs.dimension_values_note": "Waar de waarde van een maat wordt ingevoerd. Een venster heeft pijlen die de waarde laten verspringen; het maatgereedschap plaatst eerst de tekst.",
|
||||
"prefs.dimension_values_view": "In de weergave",
|
||||
"prefs.dimension_values_window": "In een venster",
|
||||
"prefs.display": "Weergave",
|
||||
"prefs.edges": "Randen en schaduwing",
|
||||
"prefs.extend": "Verlengen",
|
||||
@@ -3809,6 +3818,8 @@
|
||||
"sample.plate.name": "Voorbeeldplaat",
|
||||
"sample.vents.about": "Sleuven in een rechthoekig patroon: rijen, kolommen en spoed zijn parameters",
|
||||
"sample.vents.name": "Ventilatieplaat",
|
||||
"scale.per_axis": "Per as",
|
||||
"scale.uniform": "Uniform",
|
||||
"search.command_keys": "Enter uitvoeren Shift+Enter uitvoeren en open houden Esc sluiten",
|
||||
"search.commands": "Opdrachten",
|
||||
"search.design": "In dit ontwerp",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"en": {
|
||||
"ai.http.allow": "Allow insecure HTTP",
|
||||
"ai.http.confirm": "Allow for this endpoint",
|
||||
"ai.http.warning": "The API key and conversation will be sent unencrypted. Anyone with access to the network may read or change them. Use this only on a network you trust."
|
||||
},
|
||||
"ar": {
|
||||
"ai.http.allow": "السماح باتصال HTTP غير آمن",
|
||||
"ai.http.confirm": "السماح لهذه النقطة الطرفية",
|
||||
"ai.http.warning": "سيُرسَل مفتاح API والمحادثة دون تشفير. يمكن لأي شخص لديه وصول إلى الشبكة قراءتهما أو تغييرهما. استخدم هذا الخيار فقط على شبكة تثق بها."
|
||||
},
|
||||
"de": {
|
||||
"ai.http.allow": "Unsicheres HTTP erlauben",
|
||||
"ai.http.confirm": "Für diesen Endpunkt erlauben",
|
||||
"ai.http.warning": "Der API-Schlüssel und die Unterhaltung werden unverschlüsselt gesendet. Jeder mit Zugriff auf das Netzwerk kann sie lesen oder verändern. Verwenden Sie dies nur in einem vertrauenswürdigen Netzwerk."
|
||||
},
|
||||
"he": {
|
||||
"ai.http.allow": "אפשר HTTP לא מאובטח",
|
||||
"ai.http.confirm": "אפשר עבור נקודת קצה זו",
|
||||
"ai.http.warning": "מפתח ה־API והשיחה יישלחו ללא הצפנה. כל מי שיש לו גישה לרשת יכול לקרוא או לשנות אותם. יש להשתמש באפשרות זו רק ברשת מהימנה."
|
||||
},
|
||||
"ja": {
|
||||
"ai.http.allow": "安全でない HTTP を許可",
|
||||
"ai.http.confirm": "このエンドポイントで許可",
|
||||
"ai.http.warning": "API キーと会話は暗号化されずに送信されます。ネットワークにアクセスできる人は、内容を読み取ったり改ざんしたりする可能性があります。信頼できるネットワークでのみ使用してください。"
|
||||
},
|
||||
"ko": {
|
||||
"ai.http.allow": "보안되지 않은 HTTP 허용",
|
||||
"ai.http.confirm": "이 엔드포인트에 허용",
|
||||
"ai.http.warning": "API 키와 대화가 암호화되지 않은 상태로 전송됩니다. 네트워크에 접근할 수 있는 누구나 이를 읽거나 변경할 수 있습니다. 신뢰하는 네트워크에서만 사용하세요."
|
||||
},
|
||||
"vi": {
|
||||
"ai.http.allow": "Cho phép HTTP không bảo mật",
|
||||
"ai.http.confirm": "Cho phép với điểm cuối này",
|
||||
"ai.http.warning": "Khóa API và cuộc trò chuyện sẽ được gửi mà không mã hóa. Bất kỳ ai có quyền truy cập mạng đều có thể đọc hoặc thay đổi chúng. Chỉ sử dụng trên mạng mà bạn tin cậy."
|
||||
},
|
||||
"zh": {
|
||||
"ai.http.allow": "允许不安全的 HTTP",
|
||||
"ai.http.confirm": "允许此端点",
|
||||
"ai.http.warning": "API 密钥和对话将以未加密的方式发送。任何能够访问该网络的人都可能读取或篡改它们。请仅在可信网络上使用。"
|
||||
}
|
||||
}
|
||||
@@ -934,8 +934,6 @@
|
||||
"cmd.inspect_accessibility": "Analiza dostępności",
|
||||
"cmd.inspect_center_of_mass": "Środek masy",
|
||||
"cmd.inspect_clearance": "Prześwit",
|
||||
"cmd.inspect_colored_components": "Włącz kolory komponentów",
|
||||
"cmd.inspect_colored_components_disable": "Wyłącz kolory komponentów",
|
||||
"cmd.inspect_compare": "Porównaj bryły",
|
||||
"cmd.inspect_continuity": "Analiza ciągłości",
|
||||
"cmd.inspect_curvature_comb": "Analiza grzebienia krzywizny",
|
||||
@@ -1347,6 +1345,7 @@
|
||||
"document.read_only_save": "Ten projekt otwarto tylko do odczytu: zapisz go jako nowy plik",
|
||||
"document.save_changes": "Zapisać zmiany w {name}?",
|
||||
"document.save_copy": "Zapisz kopię…",
|
||||
"document.tab_unsaved_changes": "Niezapisane zmiany",
|
||||
"document.unsaved_changes": "{name} ma niezapisane zmiany. Jeśli ich nie zapiszesz, zostaną utracone.",
|
||||
"drive.rigid": "{joint} to złącze sztywne: utrzymuje swoje części nieruchomo, więc nie ma czym sterować. Aby mogły się poruszać, zmień jego typ w Edytuj złącze: Obrotowe do obrotu, Przesuwne do przesuwu, Walcowe do obu.",
|
||||
"drive.stopped_by_contact": "Zatrzymano przy {value}: {a} przeszedłby przez {b}",
|
||||
@@ -1985,6 +1984,8 @@
|
||||
"prefs.shortcuts": "Skróty",
|
||||
"prefs.single_surface_analysis": "Używaj jednej analizy powierzchni",
|
||||
"prefs.single_surface_analysis_help": "Gdy wyłączone, cieniowanie powierzchni i Ciągłość mogą być włączone jednocześnie. Włączenie zachowuje ostatnio uruchomioną lub zmienioną analizę.",
|
||||
"prefs.sketch.ellipse_axes": "Rysuj elipsy z osiami",
|
||||
"prefs.sketch.ellipse_axes_note": "Oś wielka i mała jako linie konstrukcyjne, aby można było wymiarować półosie i osie. Wyłączone: sama elipsa, a D na niej daje promień małej półosi, gdziekolwiek przeciągnąć wartość, rysowany i oznaczony R jak dotąd.",
|
||||
"prefs.sketch_align_points": "Wyrównuj do innych punktów szkicu podczas rysowania",
|
||||
"prefs.sketch_align_points_note": "Podczas rysowania punkt wyrównuje się jednocześnie poziomo z jednym punktem szkicu i pionowo z innym, z kropkowaną linią pomocniczą, a kliknięcie zachowuje oba wyrównania. Wyłączone: tylko z poprzednim punktem, po jednej osi.",
|
||||
"prefs.sketch_conflict_colour": "Geometria w konflikcie w kolorze konfliktu",
|
||||
|
||||
@@ -1408,6 +1408,8 @@
|
||||
"sample.plate.name": "Placa de exemplo",
|
||||
"sample.vents.about": "Rasgos em padrão retangular: linhas, colunas e passo são parâmetros",
|
||||
"sample.vents.name": "Placa de ventilação",
|
||||
"scale.per_axis": "Por eixo",
|
||||
"scale.uniform": "Uniforme",
|
||||
"search.command_keys": "Enter executar Shift+Enter executar e manter aberto Esc fechar",
|
||||
"search.commands": "Comandos",
|
||||
"search.design": "Neste projeto",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Analiza accesibilității",
|
||||
"cmd.inspect_center_of_mass": "Centru de masă",
|
||||
"cmd.inspect_clearance": "Joc",
|
||||
"cmd.inspect_colored_components": "Activează culorile componentelor",
|
||||
"cmd.inspect_colored_components_disable": "Dezactivează culorile componentelor",
|
||||
"cmd.inspect_compare": "Compară corpurile",
|
||||
"cmd.inspect_compare_picture": "Compară imaginea",
|
||||
"cmd.inspect_continuity": "Analiza continuității",
|
||||
@@ -3809,6 +3807,8 @@
|
||||
"sample.plate.name": "Placă exemplu",
|
||||
"sample.vents.about": "Canale într-o matrice dreptunghiulară: rândurile, coloanele și pasul sunt parametri",
|
||||
"sample.vents.name": "Placă de ventilație",
|
||||
"scale.per_axis": "Pe axe",
|
||||
"scale.uniform": "Uniform",
|
||||
"search.command_keys": "Enter execută Shift+Enter execută și păstrează deschis Esc închide",
|
||||
"search.commands": "Comenzi",
|
||||
"search.design": "În acest proiect",
|
||||
|
||||
@@ -182,6 +182,8 @@
|
||||
"Designs you open or save appear here.": "Здесь появляются открытые и сохранённые дизайны.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Определяется параметром «До объекта». Выберите «Угол», чтобы задать угловой предел.",
|
||||
"Diameter": "Диаметр",
|
||||
"Did this assembly open the wrong way up?": "Эта сборка открылась не той стороной вверх?",
|
||||
"Did this part open the wrong way up?": "Эта деталь открылась не той стороной вверх?",
|
||||
"Dimension new shapes": "Проставлять размеры новых фигур",
|
||||
"Direction": "Направление",
|
||||
"Discard": "Отменить",
|
||||
@@ -606,6 +608,7 @@
|
||||
"Square": "Квадрат",
|
||||
"Stand it up": "Поставить вертикально",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Поставить вертикально: четверть оборота вокруг X одной операцией «Переместить», которую можно отменить.",
|
||||
"Stand this assembly up?": "Поставить эту сборку вертикально?",
|
||||
"Stand this part up?": "Поставить эту деталь вертикально?",
|
||||
"Start angle": "Начальный угол",
|
||||
"Start bulge": "Выпуклость в начале",
|
||||
@@ -644,6 +647,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "Направление должно быть осью или прямолинейной кривой эскиза, лежащей в плоскости эскиза",
|
||||
"The mirror line cannot also be mirrored": "Линию зеркала нельзя одновременно отражать",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "Линия зеркала должна быть прямолинейной кривой этого эскиза, а не осью начала координат",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Ось начала координат перпендикулярна плоскости эскиза, поэтому линии в этой плоскости нет",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Сохранённое исходное вхождение отсутствует. Выберите корректное исходное положение перед перепривязкой.",
|
||||
"Then": "Затем",
|
||||
"These features will be deleted:": "Эти операции будут удалены:",
|
||||
@@ -953,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Анализ доступности",
|
||||
"cmd.inspect_center_of_mass": "Центр масс",
|
||||
"cmd.inspect_clearance": "Зазор",
|
||||
"cmd.inspect_colored_components": "Включить цвета компонентов",
|
||||
"cmd.inspect_colored_components_disable": "Отключить цвета компонентов",
|
||||
"cmd.inspect_compare": "Сравнить тела",
|
||||
"cmd.inspect_compare_picture": "Сравнить с изображением",
|
||||
"cmd.inspect_continuity": "Анализ непрерывности",
|
||||
@@ -1356,6 +1358,7 @@
|
||||
"dialog.dimension_insert_hint": "Дважды щёлкните другой управляющий размер, чтобы вставить его имя",
|
||||
"dialog.dimension_name": "Размер: {name}",
|
||||
"dialog.edit_title": "ИЗМЕНИТЬ: {title}",
|
||||
"dialog.no_shown_body": "Нет отображаемого тела для изменения: покажите тело или выберите «Новое тело»",
|
||||
"dialog.pattern_sketch_not_object": "Выберите тела или операции: эскиз — ни то, ни другое. Повторите его кривые собственным инструментом «Массив» или «Зеркало» в эскизе либо сначала выдавите его.",
|
||||
"dialog.select": "↖ Выбрать",
|
||||
"dialog.selected_count": "↖ Выбрано: {count}",
|
||||
@@ -1717,6 +1720,8 @@
|
||||
"help.view.spin": "Непрерывно вращает вид вокруг модели до следующего щелчка или нажатия клавиши. Используйте, чтобы показать модель со всех сторон.",
|
||||
"help.view.tool": "Закрепляет инструмент камеры (орбиту, панорамирование или масштаб), так что перетаскивание левой кнопкой мыши двигает вид, пока вы его не отключите. Используйте на трекпаде или без средней кнопки мыши.",
|
||||
"help.view.visual_style": "Переключает способ отрисовки модели: тонированная с рёбрами или без них, каркас или со скрытыми рёбрами. Используйте каркас, чтобы заглянуть внутрь, а тонированный вид — для чистого вида.",
|
||||
"history.sketch_under_constrained": "Недоопределён (степеней свободы: {count})",
|
||||
"history.sketch_under_constrained_one": "Недоопределён ({count} степень свободы)",
|
||||
"hole_presets.confirm_delete": "Подтвердить удаление",
|
||||
"hole_presets.delete": "Удалить",
|
||||
"hole_presets.delete_prompt": "Удалить этот личный пресет? Существующие отверстия не изменятся.",
|
||||
@@ -2128,6 +2133,8 @@
|
||||
"prefs.anti_aliasing_note": "Сглаживает неровные края.",
|
||||
"prefs.applies_on_restart": "Применяется после перезапуска",
|
||||
"prefs.assistant": "Ассистент",
|
||||
"prefs.auto_operation": "Выбирать операцию автоматически",
|
||||
"prefs.auto_operation_note": "Выбор «Объединить», «Новое тело» или «Вырезать» по тому, чего касается материал.",
|
||||
"prefs.axes": "Оси",
|
||||
"prefs.axis": "Ось",
|
||||
"prefs.bodies_folders": "Папки тел",
|
||||
@@ -2349,6 +2356,8 @@
|
||||
"prefs.sketch_selection": "Эскиз и выделение",
|
||||
"prefs.sketch_selection_heading": "Эскиз и выделение",
|
||||
"prefs.sketch_selection_note": "Как работает привязка в эскизах и как щелчки выделяют объекты.",
|
||||
"prefs.sketch_state_icons": "Значки эскизов в цвете их состояния",
|
||||
"prefs.sketch_state_icons_note": "В истории и браузере значок эскиза принимает цвет его состояния (недоопределён, полностью определён или с конфликтом), а подсказка называет это состояние. Выкл.: у всех эскизов одинаковый значок.",
|
||||
"prefs.sketch_tab_always": "Всегда показывать вкладку «Эскиз»",
|
||||
"prefs.sketch_tab_always_note": "Вкладка «Эскиз» остаётся на ленте сразу после «Модель», пока не открыт ни один эскиз. Её инструменты рисования создают эскиз на выбранной плоскости или грани либо на плоскости, которую вы выберете следом, и сразу включают нужный инструмент.",
|
||||
"prefs.sketch_tracking_guides": "Направляющие выравнивания по центру и середине",
|
||||
@@ -2360,6 +2369,11 @@
|
||||
"prefs.source": "Источник",
|
||||
"prefs.speed": "Скорость",
|
||||
"prefs.stand_up_note": "После открытия файла STEP или IGES с осью Z вверх предлагать повернуть его на четверть оборота вокруг X.",
|
||||
"prefs.stand_up_unknown": "Для файлов из неизвестных программ",
|
||||
"prefs.stand_up_unknown.ask": "Спрашивать в диалоговом окне",
|
||||
"prefs.stand_up_unknown.note": "Показывать уведомление",
|
||||
"prefs.stand_up_unknown.off": "Не предлагать",
|
||||
"prefs.stand_up_unknown_note": "Как делается предложение, когда в файле не указана известная программа.",
|
||||
"prefs.status_bar": "Строка состояния",
|
||||
"prefs.status_bar_note": "Тонкая полоса вдоль нижнего края окна показывает, что изменяется, состояние открытого эскиза его цветом и единицы дизайна; щелчок по единицам изменяет их. Состояние эскиза при этом больше не показывается в углу вида.",
|
||||
"prefs.status_done": "Готово",
|
||||
@@ -3737,6 +3751,8 @@
|
||||
"sample.plate.name": "Образец пластины",
|
||||
"sample.vents.about": "Пазы в прямоугольном массиве: число рядов, столбцов и шаг заданы параметрами",
|
||||
"sample.vents.name": "Вентиляционная пластина",
|
||||
"scale.per_axis": "По осям",
|
||||
"scale.uniform": "Равномерно",
|
||||
"search.command_keys": "Enter выполнить Shift+Enter выполнить и оставить открытым Esc закрыть",
|
||||
"search.commands": "Команды",
|
||||
"search.design": "В этом дизайне",
|
||||
@@ -3863,7 +3879,7 @@
|
||||
"status.navigation": "Назначение кнопок мыши: {mapping}",
|
||||
"status.navigation_tip": "Как мышь двигает вид. Щёлкните, чтобы выбрать другое назначение",
|
||||
"status.under_constrained": "Недоопределён (степеней свободы: {count})",
|
||||
"status.under_constrained_one": "Недоопределён (степеней свободы: {count})",
|
||||
"status.under_constrained_one": "Недоопределён ({count} степень свободы)",
|
||||
"status.units_tip": "Единицы дизайна. Щёлкните, чтобы изменить",
|
||||
"the angle must be a number": "угол должен быть числом",
|
||||
"the curves must be in one sketch": "кривые должны быть в одном эскизе",
|
||||
@@ -3902,8 +3918,10 @@
|
||||
"timeline.move_to_group": "Переместить операции в папку",
|
||||
"timeline.rename_group": "Переименовать папку операций",
|
||||
"timeline.reorder_many": "Изменить порядок операций",
|
||||
"timeline.suppress_features": "Подавить операции",
|
||||
"timeline.suppress_folders_tip": "Подавить или снять подавление со всех операций в папках и вложенных папках",
|
||||
"timeline.ungroup": "Убрать папки операций",
|
||||
"timeline.unsuppress_features": "Снять подавление с операций",
|
||||
"to the face": "до грани",
|
||||
"to the next face": "до следующей грани",
|
||||
"toolbar.customize_note": "Вкладка {tab}. Отметьте команду, чтобы добавить её кнопку на панель инструментов. Можно также перетащить строку из выпадающего меню панели на панель инструментов, перетащить кнопку за пределы панели для удаления или использовать значок закрепления в строке выпадающего меню.",
|
||||
@@ -4349,6 +4367,8 @@
|
||||
"ui.cancel": "Отмена",
|
||||
"ui.canvases": "Подложки",
|
||||
"ui.center": "Центр",
|
||||
"ui.center_arc_follows_cursor": "Дуга из центра следует направлению курсора",
|
||||
"ui.center_arc_follows_cursor_tip": "Дуга из центра идёт по часовой стрелке или против неё — в ту сторону, в которую курсор обходит её центр. Выкл.: всегда против часовой стрелки от начальной точки.",
|
||||
"ui.centerline": "Осевая линия",
|
||||
"ui.chain": "Цепочка",
|
||||
"ui.chamfer": "Фаска",
|
||||
@@ -5160,11 +5180,13 @@
|
||||
"update.installed": "SolidCraft {version} установлен. Перезапустите, чтобы использовать его.",
|
||||
"update.installer_started": "Установщик запустится после закрытия SolidCraft.",
|
||||
"update.later": "Напомнить позже",
|
||||
"update.managed": "Доступно обновление: обновите SolidCraft в {manager}",
|
||||
"update.no_notes": "У этого выпуска нет примечаний.",
|
||||
"update.package_hint": "Эта копия установлена из {kind}. Скачайте новый пакет, затем установите его командой:",
|
||||
"update.prefs_dev": "Это сборка для разработки ({version}): она не проверяет обновления.",
|
||||
"update.prefs_flatpak": "Эту копию ({version}) обновляет Flatpak: выполните {command} или воспользуйтесь центром приложений. SolidCraft не проверяет обновления.",
|
||||
"update.prefs_label": "Обновления",
|
||||
"update.prefs_managed": "Это SolidCraft {version}, которым управляет {manager}. Проверка запрашивает у GitHub последний релиз и больше ничего не отправляет; обновляйте SolidCraft в {manager}.",
|
||||
"update.prefs_note": "Это SolidCraft {version} ({kind}). При проверке у GitHub запрашивается последний выпуск, больше ничего не отправляется.",
|
||||
"update.prefs_web": "Перезагрузите страницу, чтобы получить последнюю версию веб-приложения. Если сеть недоступна или обновление не успевает загрузиться, открывается сохранённая версия; незавершённая загрузка продолжится до следующей перезагрузки.",
|
||||
"update.refresh_checking": "Проверка наличия новейшего выпуска…",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Анализа доступности",
|
||||
"cmd.inspect_center_of_mass": "Центар масе",
|
||||
"cmd.inspect_clearance": "Провера зазора",
|
||||
"cmd.inspect_colored_components": "Омогући боје компоненти",
|
||||
"cmd.inspect_colored_components_disable": "Онемогући боје компоненти",
|
||||
"cmd.inspect_compare": "Упореди тела",
|
||||
"cmd.inspect_compare_picture": "Упореди слику",
|
||||
"cmd.inspect_continuity": "Анализа континуитета",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Tillgänglighetsanalys",
|
||||
"cmd.inspect_center_of_mass": "Masscentrum",
|
||||
"cmd.inspect_clearance": "Frigång",
|
||||
"cmd.inspect_colored_components": "Aktivera komponentfärger",
|
||||
"cmd.inspect_colored_components_disable": "Inaktivera komponentfärger",
|
||||
"cmd.inspect_compare": "Jämför kroppar",
|
||||
"cmd.inspect_compare_picture": "Jämför bild",
|
||||
"cmd.inspect_continuity": "Kontinuitetsanalys",
|
||||
@@ -1392,6 +1390,7 @@
|
||||
"document.read_only_save": "Den här konstruktionen öppnades skrivskyddad: spara den som en ny fil",
|
||||
"document.save_changes": "Spara ändringar i {name}?",
|
||||
"document.save_copy": "Spara en kopia…",
|
||||
"document.tab_unsaved_changes": "Osparade ändringar",
|
||||
"document.unsaved_changes": "{name} har osparade ändringar. Om du inte sparar går de förlorade.",
|
||||
"drive.rigid": "{joint} är ett stelt led: det håller sina delar stilla och har därför inget att driva. Om delarna ska kunna röra sig, ändra ledets typ i Redigera led: Roterande för att rotera, Glidande för att glida, Cylindrisk för båda.",
|
||||
"drive.stopped_by_contact": "Stoppade vid {value}: {a} skulle gå igenom {b}",
|
||||
@@ -3809,6 +3808,8 @@
|
||||
"sample.plate.name": "Exempelplatta",
|
||||
"sample.vents.about": "Spår i ett rektangulärt mönster: rader, kolumner och delning är parametrar",
|
||||
"sample.vents.name": "Ventilationsplatta",
|
||||
"scale.per_axis": "Per axel",
|
||||
"scale.uniform": "Likformig",
|
||||
"search.command_keys": "Enter kör Shift+Enter kör och behåll öppen Esc stäng",
|
||||
"search.commands": "Kommandon",
|
||||
"search.design": "I denna konstruktion",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "Erişilebilirlik analizi",
|
||||
"cmd.inspect_center_of_mass": "Kütle merkezi",
|
||||
"cmd.inspect_clearance": "Açıklık",
|
||||
"cmd.inspect_colored_components": "Bileşen Renklerini Etkinleştir",
|
||||
"cmd.inspect_colored_components_disable": "Bileşen Renklerini Devre Dışı Bırak",
|
||||
"cmd.inspect_compare": "Gövdeleri karşılaştır",
|
||||
"cmd.inspect_compare_picture": "Görüntüyle karşılaştır",
|
||||
"cmd.inspect_continuity": "Süreklilik kontrolü",
|
||||
@@ -3809,6 +3807,8 @@
|
||||
"sample.plate.name": "Örnek plaka",
|
||||
"sample.vents.about": "Dikdörtgen desende yuvalar: satırlar, sütunlar ve aralık parametredir",
|
||||
"sample.vents.name": "Havalandırma plakası",
|
||||
"scale.per_axis": "Eksen başına",
|
||||
"scale.uniform": "Tekdüze",
|
||||
"search.command_keys": "Enter çalıştır Shift+Enter çalıştır ve açık tut Esc kapat",
|
||||
"search.commands": "Komutlar",
|
||||
"search.design": "Bu tasarımda",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"Bottom Right": "Dưới cùng bên phải",
|
||||
"Boundary": "Biên dạng bao",
|
||||
"Bounding box": "Hộp bao ngoài",
|
||||
"Bridge": "Cầu nối",
|
||||
"CHECK": "KIỂM TRA",
|
||||
"COMPONENTS": "CHI TIẾT",
|
||||
"CONFIGURATIONS": "CẤU HÌNH",
|
||||
@@ -628,6 +629,8 @@
|
||||
"Suppress Selected Folders": "Bỏ kích hoạt các thư mục đã chọn",
|
||||
"Suppressed": "Bị tạm nén",
|
||||
"Surface": "Bề mặt",
|
||||
"Surface or face": "Bề mặt hoặc mặt",
|
||||
"Surface or faces": "Bề mặt hoặc các mặt",
|
||||
"Surface, or faces to turn over": "Mặt cong, hoặc các mặt cần lật hướng",
|
||||
"Surfaces": "Các bề mặt",
|
||||
"Swap Ends": "Hoán đổi hai đầu",
|
||||
@@ -641,6 +644,7 @@
|
||||
"Target": "Mục tiêu",
|
||||
"Target Body": "Khối mục tiêu",
|
||||
"Target Face (optional)": "Mặt đích (tùy chọn)",
|
||||
"Target edges": "Các cạnh đích",
|
||||
"Tear": "Xé rách mép chấn",
|
||||
"Technical details": "Chi tiết kỹ thuật",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Phác thảo nét đứt là đường dẫn mặt phẳng đã lưu. Hãy chọn một bề mặt mới để sửa lại liên kết.",
|
||||
@@ -648,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "Hướng phải là một trục hoặc một đường cong phác thảo thẳng nằm trong mặt phẳng phác thảo",
|
||||
"The mirror line cannot also be mirrored": "Đường đối xứng không thể đồng thời được lấy đối xứng",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "Đường đối xứng phải là một đường cong thẳng của phác thảo này, không phải trục gốc",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Trục gốc vuông góc với mặt phẳng phác thảo nên không có đường thẳng trong mặt phẳng này",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Bản thể tác giả đã lưu bị thiếu. Hãy chọn một vị trí tác giả hợp lệ trước khi gán lại.",
|
||||
"Then": "Thì",
|
||||
"These features will be deleted:": "Các tính năng sau sẽ bị xóa:",
|
||||
@@ -750,6 +755,8 @@
|
||||
"ai.prefs.follow_camera": "Theo camera",
|
||||
"ai.prefs.follow_camera_off": "Tự động xoay camera theo phác thảo đang tắt.",
|
||||
"ai.prefs.follow_camera_on": "Camera sẽ tự động xoay vuông góc với phác thảo do agent tạo.",
|
||||
"ai.prefs.instructions": "Hướng dẫn tùy chỉnh",
|
||||
"ai.prefs.instructions_note": "Được gửi cho agent khi bắt đầu mỗi cuộc trò chuyện, sau hướng dẫn riêng của SolidCraft: cách bạn muốn làm việc, các quy tắc cần tuân theo. Thay đổi có hiệu lực từ cuộc trò chuyện tiếp theo (Kết nối hoặc Cuộc trò chuyện mới).",
|
||||
"ai.prefs.intro": "Tác nhân AI trong SolidCraft.",
|
||||
"ai.prefs.make_default": "Đặt làm mặc định",
|
||||
"ai.prefs.no_agents": "Chưa có agent nào. Thêm một agent hoặc làm mới để tìm các agent đã cài đặt.",
|
||||
@@ -963,8 +970,7 @@
|
||||
"cmd.inspect_analysis": "Phân tích khung nhìn",
|
||||
"cmd.inspect_center_of_mass": "Trọng tâm",
|
||||
"cmd.inspect_clearance": "Khoảng cách an toàn",
|
||||
"cmd.inspect_colored_components": "Bật màu cụm chi tiết",
|
||||
"cmd.inspect_colored_components_disable": "Tắt màu cụm chi tiết",
|
||||
"cmd.inspect_colored_components": "Màu cụm chi tiết",
|
||||
"cmd.inspect_compare": "So sánh các thân khối",
|
||||
"cmd.inspect_compare_picture": "So sánh với ảnh tham chiếu",
|
||||
"cmd.inspect_continuity": "Phân tích độ liên tục mặt",
|
||||
@@ -1296,6 +1302,7 @@
|
||||
"cmd.timeline_stop": "Dừng lịch sử",
|
||||
"cmd.timeline_suppress": "Tạm nén tính năng",
|
||||
"cmd.ui.assistant": "Bật/tắt trợ lý",
|
||||
"cmd.view.navigator": "Khối điều hướng góc nhìn tùy chỉnh",
|
||||
"cmd.view.render_browser": "Trình duyệt kết xuất",
|
||||
"cmd.view.render_dock": "Di chuyển neo kết xuất",
|
||||
"cmd.view.render_focus": "Chế độ xem tập trung kết xuất",
|
||||
@@ -1394,15 +1401,22 @@
|
||||
"dialog.computing": "Đang tính toán… {seconds} s",
|
||||
"dialog.could_not_be_checked": "không thể kiểm tra",
|
||||
"dialog.custom_axis": "↖ Trục tùy chỉnh",
|
||||
"dialog.cylinder_face_tip": "Bề mặt hoặc đỉnh của mô hình mà khối trụ đứng trên đó: nhấp vào đây, sau đó nhấp vào nó trong khung nhìn. Để trống thì X, Y và Z đặt vị trí.",
|
||||
"dialog.dimension_insert_hint": "Nhấp vào một kích thước điều khiển khác để chèn tên của nó",
|
||||
"dialog.dimension_name": "Kích thước: {name}",
|
||||
"dialog.dimension_step_down": "Giảm {step}",
|
||||
"dialog.dimension_step_expression": "Chỉ số mới thay đổi theo bước; biểu thức giữ nguyên như đã nhập",
|
||||
"dialog.dimension_step_up": "Tăng {step}",
|
||||
"dialog.edit_title": "CHỈNH SỬA {title}",
|
||||
"dialog.no_shown_body": "Không có thân khối đang hiển thị để thay đổi: hãy hiển thị một thân khối, hoặc chọn “Tạo mới Thân khối”",
|
||||
"dialog.pattern_sketch_not_object": "Hãy chọn thân khối hoặc tính năng: bản phác thảo không phải là cả hai. Lặp lại các đường cong của nó bằng Mảng hoặc Lấy đối xứng trong chính bản phác thảo, hoặc đùn khối nó trước.",
|
||||
"dialog.primitive_point_tip": "Một điểm điền vào X, Y và Z: nhấp vào đây, sau đó nhấp vào một đỉnh, cạnh hoặc bề mặt của mô hình, một điểm của bản phác thảo, hoặc gốc tọa độ.",
|
||||
"dialog.reference_edge_tip": "Một cạnh thẳng của bề mặt đã chọn, từ đó đo khoảng cách bên dưới: nhấp vào đây, sau đó nhấp vào cạnh.",
|
||||
"dialog.select": "↖ Chọn",
|
||||
"dialog.selected_count": "↖ {count} đã chọn",
|
||||
"dialog.selection_constraint": "Ràng buộc {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · đường cong {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "Chọn một mục đầu vào để thay đổi. Nhấp vào đối tượng đã chọn để bỏ chọn; Ctrl/Cmd-nhấp để bật/tắt chọn. Xem trước kết quả, sau đó nhấn OK.",
|
||||
"dialog.selection_midpoint": "Trung điểm · {id}",
|
||||
"dialog.selection_profile": "{sketch} · biên dạng {number}",
|
||||
@@ -1420,6 +1434,7 @@
|
||||
"document.read_only_save": "Thiết kế này được mở ở chế độ chỉ đọc: hãy lưu thành tệp mới",
|
||||
"document.save_changes": "Lưu các thay đổi đối với {name}?",
|
||||
"document.save_copy": "Lưu bản sao…",
|
||||
"document.tab_unsaved_changes": "Thay đổi chưa lưu",
|
||||
"document.unsaved_changes": "{name} có các thay đổi chưa lưu. Nếu không lưu, các thay đổi sẽ bị mất.",
|
||||
"drive.rigid": "{joint} là khớp nối cứng: nó giữ cố định các chi tiết, nên không có gì để dẫn động. Để các chi tiết chuyển động, hãy đổi loại khớp nối trong “Chỉnh sửa khớp nối”: Revolute để xoay, Slider để trượt, Cylindrical cho cả hai.",
|
||||
"drive.stopped_by_contact": "Đã dừng tại {value}: {a} sẽ xuyên qua {b}",
|
||||
@@ -1440,6 +1455,9 @@
|
||||
"each rail's curves must be in one sketch": "các đường cong của mỗi đường dẫn hướng phải nằm trong cùng một phác thảo",
|
||||
"feature computation ended without a result": "việc tính toán đặc trưng kết thúc mà không có kết quả",
|
||||
"file.open.drawing_filter": "Thiết kế, tệp CAD, lưới đa giác và bản vẽ",
|
||||
"file.open.portal_fallback.no_portal_service": "Bộ chọn tệp đã mở không phải của riêng desktop: dịch vụ chọn tệp của desktop không khả dụng.",
|
||||
"file.open.portal_fallback.no_session_bus": "Bộ chọn tệp đã mở không phải của riêng desktop: không có bus phiên D-Bus.",
|
||||
"file.open.portal_fallback.xwayland": "Bộ chọn tệp đã mở không phải của riêng desktop: dưới XWayland, portal từ chối cửa sổ cha.",
|
||||
"file_load.built": "Đã dựng được: {built} trên {total} tính năng.",
|
||||
"file_load.converting": "Đang chuyển đổi khối đặc {done} trên {total}",
|
||||
"file_load.inserting": "Đang chèn hình học",
|
||||
@@ -1999,6 +2017,7 @@
|
||||
"material.selection.mixed": "Các vật liệu khác nhau",
|
||||
"measure.at": "Tại",
|
||||
"measure.item": "Mục {number}",
|
||||
"measure.line_to_centre": "Từ đường đến tâm",
|
||||
"measure.quick_copied": "Đã sao chép số đo",
|
||||
"measure.quick_copy": "Nhấp để sao chép số đo",
|
||||
"measure.quick_edges": "{count} cạnh",
|
||||
@@ -2029,6 +2048,8 @@
|
||||
"nav.blender.help": "Kéo chuột giữa để xoay, Shift+chuột giữa để trượt, Ctrl+chuột giữa để thu phóng; kéo chuột phải giữ nguyên camera",
|
||||
"nav.creo.help": "Kéo nút giữa để xoay, Shift+nút giữa để di chuyển, Ctrl+kéo nút giữa theo chiều dọc để thu phóng (kéo xuống để phóng to); lăn con lăn để thu phóng",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "Kéo bằng nút giữa hoặc nút phải sẽ tiếp tục xoay, di chuyển hoặc thu phóng như lúc bắt đầu cho đến khi nhả nút, dù trong lúc đó có nhấn hay nhả phím nào. Tắt: các phím đang giữ quyết định ở mọi thời điểm của thao tác kéo.",
|
||||
"nav.drag_keeps_mode.label": "Thao tác kéo giữ nguyên kiểu điều hướng lúc bắt đầu",
|
||||
"nav.free_orbit.help": "Chuột và chuột 3D xoay tự do qua cả hai cực và giữ nguyên độ lăn nghiêng (Roll). Bắt dính trục bằng phím Shift sẽ bị tắt.",
|
||||
"nav.free_orbit.label": "Xoay góc nhìn tự do",
|
||||
"nav.free_orbit.snap_help": "Chuột và chuột 3D xoay qua cả hai cực và giữ nguyên góc nghiêng cuộn. Giữ Shift trong khi kéo xoay quỹ đạo để bắt dính hướng nhìn và góc nghiêng cuộn.",
|
||||
@@ -2265,6 +2286,17 @@
|
||||
"prefs.auto_operation_note": "Nối, Vật thể mới hoặc Cắt, tùy vào phần vật liệu tiếp xúc.",
|
||||
"prefs.axes": "Các trục tọa độ",
|
||||
"prefs.axis": "Trục tọa độ",
|
||||
"prefs.background_choose": "Chọn ảnh…",
|
||||
"prefs.background_image": "Ảnh",
|
||||
"prefs.background_unreadable": "Không đọc được ảnh nền",
|
||||
"prefs.blur_cost": "Chi phí kết xuất tương đối. Nhiều thanh hơn thường có nghĩa là cần xử lý nhiều hơn.",
|
||||
"prefs.blur_device": "Không khả dụng trên thiết bị đồ họa hoặc đường kết xuất này",
|
||||
"prefs.blur_guard": "Chất lượng làm mờ đã giảm để duy trì độ phản hồi của khung hình. Bán kính không thay đổi.",
|
||||
"prefs.blur_off": "Tắt",
|
||||
"prefs.blur_reduce_transparency": "Bị tắt bởi cài đặt Giảm độ trong suốt của hệ thống",
|
||||
"prefs.blur_software": "Không khả dụng với đồ họa phần mềm (gpu-software)",
|
||||
"prefs.blur_source": "Không khả dụng trong chế độ xem không gian làm việc này",
|
||||
"prefs.blur_web": "Không khả dụng trên đường kết xuất web này",
|
||||
"prefs.bodies_folders": "Thư mục thân khối",
|
||||
"prefs.bodies_folders_always": "Luôn luôn",
|
||||
"prefs.bodies_folders_never": "Không bao giờ",
|
||||
@@ -2278,6 +2310,8 @@
|
||||
"prefs.browser_eye_left": "Biểu tượng mắt cạnh mũi tên thu gọn",
|
||||
"prefs.browser_eye_left_note": "Các hàng trong cây thư mục hiển thị biểu tượng mắt bật/tắt cạnh mũi tên thu gọn như trước đây, thay vì trong một cột ở đầu bên phải của các hàng.",
|
||||
"prefs.browser_reveal_on_hover": "Hiện khi di chuột",
|
||||
"prefs.browser_rows_start_closed": "Các dòng trong cây thư mục đóng sẵn",
|
||||
"prefs.browser_rows_start_closed_note": "Vị trí đặt và thư mục trong cây thư mục bắt đầu ở trạng thái đóng, nên thiết kế có nhiều thành phần chỉ hiện các dòng cấp trên cùng. Mỗi dòng mở bằng mũi tên của nó, “Mở rộng tất cả” hoặc “Tìm trong cây đối tượng”, lệnh này chỉ mở các dòng dẫn tới thân khối và đóng lại các dòng mà lần tìm trước đã mở. Chọn một thân khối trong khung nhìn 3D không làm thay đổi cây thư mục. Khi tắt, các thư mục mở sẵn, vị trí đặt theo tùy chọn “Thành phần mở sẵn”, và việc chọn trong khung nhìn 3D mở cây thư mục tới thân khối đó, như trước đây.",
|
||||
"prefs.browser_show_note": "Hiển thị giữ trình duyệt trên màn hình. Hiện khi di chuột ẩn nó cho đến khi con trỏ đến vị trí của nó hoặc thẻ mảnh ở cạnh trái, và nó vẫn mở khi con trỏ ở bên trong hoặc một menu của nó đang mở.",
|
||||
"prefs.browser_shown": "Hiển thị",
|
||||
"prefs.built_in": "Mặc định tích hợp",
|
||||
@@ -2323,6 +2357,13 @@
|
||||
"prefs.dialogs_edge": "Ở cạnh khung nhìn",
|
||||
"prefs.dialogs_left_column": "Trong cột bên trái",
|
||||
"prefs.dialogs_note": "Nơi hộp thoại lệnh mở ra: gắn ở cạnh khung nhìn, hoặc trong cột bên trái thay cho trình duyệt và Lịch sử, hai phần này trở lại như cũ khi hộp thoại đóng.",
|
||||
"prefs.dimension_step_angle": "Lượng mà mũi tên của cửa sổ, Lên/Xuống và con lăn cộng vào một góc",
|
||||
"prefs.dimension_step_length": "Lượng mà mũi tên của cửa sổ, Lên/Xuống và con lăn cộng vào một độ dài",
|
||||
"prefs.dimension_steps": "Bước của cửa sổ",
|
||||
"prefs.dimension_values": "Giá trị kích thước",
|
||||
"prefs.dimension_values_note": "Nơi bạn nhập giá trị kích thước. Cửa sổ có mũi tên để thay đổi theo bước; công cụ Kích thước đặt văn bản trước.",
|
||||
"prefs.dimension_values_view": "Trong khung nhìn",
|
||||
"prefs.dimension_values_window": "Trong một cửa sổ",
|
||||
"prefs.display": "Hiển thị",
|
||||
"prefs.edges": "Cạnh và đổ bóng",
|
||||
"prefs.extend": "Kéo dài",
|
||||
@@ -2402,6 +2443,10 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "Hai mũi tên cong ở góc khối cuộn khung nhìn trong mặt phẳng màn hình. Khi bật: mũi tên theo chiều kim đồng hồ cuộn ngược chiều kim đồng hồ và ngược lại. Khi tắt: mỗi mũi tên cuộn khung nhìn theo chiều cong của nó.",
|
||||
"prefs.invert_navigator_arrows": "Đảo ngược mũi tên xoay của khối điều hướng",
|
||||
"prefs.invert_navigator_arrows_note": "Các mũi tên quanh khối xoay camera theo hướng mũi tên chỉ, như trong Fusion và Inventor: mũi tên bên trái chỉ sang phải và đưa camera sang phải. Khi tắt: mũi tên xoay khối về phía cạnh chứa nó, nên mũi tên bên trái đưa camera sang trái. Hai mũi tên cuộn cong không bị ảnh hưởng.",
|
||||
"prefs.keep_unsaved_also_asks": "Vẫn hỏi khi đóng thẻ",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Khi tắt, thẻ sẽ đóng mà không có hộp thoại của trình duyệt khi bản sao của mọi thiết kế chưa lưu đã được cập nhật.",
|
||||
"prefs.keep_unsaved_in_browser": "Giữ công việc chưa lưu trong trình duyệt này",
|
||||
"prefs.keep_unsaved_in_browser_tip": "Các thay đổi chưa lưu của mỗi thiết kế đang mở được cập nhật liên tục trong bộ nhớ riêng của trình duyệt này. Lần truy cập sau sẽ đề nghị khôi phục chúng.",
|
||||
"prefs.keep_work_on_tab_switch": "Giữ hộp thoại và công cụ mở khi chuyển giữa các thiết kế",
|
||||
"prefs.keep_work_on_tab_switch_note": "Mỗi thẻ thiết kế giữ hộp thoại hoặc công cụ đang mở, cùng những gì bạn đã chọn và nhập trong đó, và thẻ thanh công cụ của nó. Chúng vẫn còn đó khi bạn quay lại thiết kế. Tắt: chuyển thiết kế sẽ hủy chúng và hiển thị thẻ Mô hình.",
|
||||
"prefs.labels": "Nhãn chữ",
|
||||
@@ -2413,6 +2458,8 @@
|
||||
"prefs.layout_page.card": "Thẻ",
|
||||
"prefs.layout_page.column": "Cột",
|
||||
"prefs.layout_page.hover": "Rê chuột",
|
||||
"prefs.layout_page.hover_panel_menus": "Chuyển menu nhóm khi di chuột",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Nhấp vào tên nhóm trên dải công cụ để mở menu. Di chuột lên tên nhóm khác để chuyển menu. Di chuyển con trỏ ra ngoài dải công cụ và menu đang mở để đóng menu.",
|
||||
"prefs.layout_page.icons_labels": "Biểu tượng + nhãn",
|
||||
"prefs.layout_page.ok_cancel": "Đồng ý và Hủy bỏ",
|
||||
"prefs.layout_page.on_hover": "Khi rê chuột",
|
||||
@@ -2533,6 +2580,8 @@
|
||||
"prefs.section_hatch_by_part": "Theo chi tiết",
|
||||
"prefs.section_hatch_one": "Một kiểu",
|
||||
"prefs.select": "Chọn",
|
||||
"prefs.select_hidden_edges": "Chọn cạnh bị che trong chế độ tô bóng có cạnh ẩn",
|
||||
"prefs.select_hidden_edges_help": "Cho phép chọn cạnh xuyên qua bề mặt khi dùng chế độ tô bóng có cạnh ẩn, kể cả cho lệnh Fillet. Tắt để giữ cách chọn của v0.30.1. Các khối đã ẩn vẫn không thể chọn.",
|
||||
"prefs.select_one_item": "Chọn một mục",
|
||||
"prefs.select_other_hidden": "Chọn hình học ẩn bằng Chọn đối tượng khác",
|
||||
"prefs.select_other_hidden_help": "Bật: bao gồm các khối, bản phác thảo ẩn và cạnh bị che gần con trỏ. Tắt: dùng danh sách ban đầu chỉ gồm hình học đang hiển thị ngay dưới con trỏ.",
|
||||
@@ -2557,6 +2606,8 @@
|
||||
"prefs.sketch.click_selects": "Nhấp để chọn",
|
||||
"prefs.sketch.conflict_colour": "Màu xung đột",
|
||||
"prefs.sketch.drawing": "Vẽ",
|
||||
"prefs.sketch.ellipse_axes": "Vẽ hình elip kèm các trục",
|
||||
"prefs.sketch.ellipse_axes_note": "Trục lớn và trục nhỏ dưới dạng đường gióng dựng hình, để có thể ghi kích thước bán trục và trục. Tắt: chỉ vẽ hình elip, và D trên nó cho bán kính nhỏ dù kéo giá trị đến đâu, được vẽ và ghi nhãn R như trước.",
|
||||
"prefs.sketch.exit_keep": "Giữ nguyên",
|
||||
"prefs.sketch.exit_previous": "Trở về góc nhìn trước đó",
|
||||
"prefs.sketch.exit_unless_rotated": "Trừ khi đã xoay",
|
||||
@@ -2650,6 +2701,10 @@
|
||||
"prefs.theme_follow_off": "Tắt",
|
||||
"prefs.theme_follow_stop": "Dừng theo dõi",
|
||||
"prefs.theme_import_hint": "Nhấp để chọn tệp giao diện, thả tệp vào đây, hoặc nhấn Ctrl+V (Cmd+V trên Mac) khi đã sao chép một giao diện",
|
||||
"prefs.theme_notice_dismiss": "Ẩn",
|
||||
"prefs.theme_option_blur": "Làm mờ phía sau bảng",
|
||||
"prefs.theme_option_blur_help": "Làm mờ phía sau các bảng bán trong suốt, tính bằng pixel vật lý. Giá trị 0 là tắt.",
|
||||
"prefs.theme_option_invalid": "Giá trị đã lưu cho {option} không hợp lệ; đang dùng giá trị mặc định của giao diện. Giá trị đã lưu vẫn được giữ lại.",
|
||||
"prefs.theme_option_opacity": "Độ đục",
|
||||
"prefs.theme_option_opacity_help": "Điều chỉnh độ đục của các bảng nổi. Giới hạn độ đục tối thiểu giúp văn bản luôn dễ đọc trên nền màu của khung nhìn.",
|
||||
"prefs.theme_option_tab_colours": "Màu thẻ",
|
||||
@@ -2658,6 +2713,7 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "Chuyển dần màu của bảng điều khiển và khung nhìn khi đổi thẻ. Đặt thành 0 để chuyển ngay lập tức.",
|
||||
"prefs.theme_options_badge": "Tùy chọn",
|
||||
"prefs.theme_options_more": "Thêm · {count}",
|
||||
"prefs.theme_options_newer": "Giao diện này yêu cầu phiên bản SolidCraft mới hơn. Sau khi nâng cấp, hãy nhập lại giao diện gốc để khôi phục các định nghĩa tùy chọn.",
|
||||
"prefs.theme_options_reset": "Đặt lại",
|
||||
"prefs.theme_options_unavailable": "Không khả dụng trên thiết bị này",
|
||||
"prefs.theme_paste": "Dán giao diện",
|
||||
@@ -2721,6 +2777,12 @@
|
||||
"recovery.just_now": "vừa xong",
|
||||
"recovery.min_ago": "{count} phút trước",
|
||||
"recovery.never_saved": "chưa từng lưu",
|
||||
"recovery.open_saved": "Mở phiên bản đã lưu",
|
||||
"recovery.restore": "Khôi phục",
|
||||
"recovery.restore_intro": "Các thay đổi chưa được lưu khi trang này đóng lần trước được giữ trong trình duyệt này.",
|
||||
"recovery.restore_row": "{name}, thay đổi {when}",
|
||||
"recovery.restore_title": "Khôi phục các thiết kế chưa lưu?",
|
||||
"recovery.restore_title_one": "Khôi phục thiết kế chưa lưu: {name}, thay đổi {when}?",
|
||||
"refused.change": "Không thể thay đổi {body}.",
|
||||
"refused.cut": "Không thể cắt {body}.",
|
||||
"refused.face_curved": "Một trong các mặt cong của nó không dùng được để mô hình hóa.",
|
||||
@@ -3242,9 +3304,11 @@
|
||||
"render.frame_wide": "16:9 · Màn ảnh rộng",
|
||||
"render.gpu_depth": "Độ sâu đường truyền tia tối đa",
|
||||
"render.gpu_options_help": "Số mẫu, độ sâu, hạt giống và độ phơi sáng được lưu cùng thiết lập này. Chỉ lưu phương thức tạo ảnh đang hoạt động.",
|
||||
"render.gpu_path_crashed": "Lần trước SolidCraft đã đóng khi đang thiết lập nó trên bộ điều hợp đồ họa này, nhiều khả năng là trong trình điều khiển đồ họa, nên nó sẽ không tự thiết lập lại. Hãy dùng dò tia trên CPU, hoặc thử lại (cập nhật trình điều khiển cũng sẽ thử lại).",
|
||||
"render.gpu_path_failed": "Bộ dò đường truyền tia GPU không khả dụng",
|
||||
"render.gpu_path_first_setup": "Lần thiết lập đầu tiên trên GPU này có thể mất vài phút; SolidCraft vẫn sử dụng được bình thường. Các lần khởi động sau sẽ tái sử dụng mã đổ bóng đã biên dịch nếu trình điều khiển lưu giữ chúng.",
|
||||
"render.gpu_path_preparing": "Bộ dò đường truyền tia GPU: đang biên dịch mã đổ bóng…",
|
||||
"render.gpu_path_retry": "Thử lại",
|
||||
"render.gpu_seed": "Hạt giống ngẫu nhiên",
|
||||
"render.gpu_unavailable": "Dò đường truyền tia GPU chưa sẵn sàng trên thiết bị này.",
|
||||
"render.ground_apply": "Khớp và lưu mặt sàn",
|
||||
@@ -4052,6 +4116,8 @@
|
||||
"sample.plate.name": "Tấm mẫu",
|
||||
"sample.vents.about": "Các rãnh then trong mảng chữ nhật: số hàng, số cột và bước rãnh là các tham số",
|
||||
"sample.vents.name": "Tấm khe thông gió",
|
||||
"scale.per_axis": "Theo từng trục",
|
||||
"scale.uniform": "Đồng đều",
|
||||
"search.command_keys": "Enter chạy Shift+Enter chạy và giữ mở Esc đóng",
|
||||
"search.commands": "Lệnh",
|
||||
"search.design": "Trong thiết kế này",
|
||||
@@ -4142,6 +4208,32 @@
|
||||
"sketch.dof_left": "Còn {count} bậc tự do",
|
||||
"sketch.dof_left_one": "Còn {count} bậc tự do",
|
||||
"sketch.draw_curves": "Vẽ dưới dạng {linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "Chuyển đổi vùng chọn sang {linetype}",
|
||||
"sketch.open_ends": "Đầu hở: {count}",
|
||||
"sketch.status_conflict": "ràng buộc xung đột",
|
||||
@@ -4363,6 +4455,7 @@
|
||||
"ui.3d_sketch": "3D Phác thảo",
|
||||
"ui.activate": "Kích hoạt",
|
||||
"ui.active_component": "Cụm chi tiết đang kích hoạt",
|
||||
"ui.addToQuickAccess": "Thêm vào Truy cập nhanh",
|
||||
"ui.add_configuration": "Thêm cấu hình",
|
||||
"ui.add_parameter": "Thêm tham số",
|
||||
"ui.agent_chat": "Trò chuyện với AI Agent",
|
||||
@@ -4784,6 +4877,7 @@
|
||||
"ui.default_modelling_orientation": "Hướng mô hình hóa mặc định",
|
||||
"ui.delete": "Xóa",
|
||||
"ui.delete_select_first": "Hãy chọn trước những gì cần xóa: tính năng, khối, thành phần hoặc hình học phác thảo",
|
||||
"ui.dependents_select_first": "Hãy chọn trước đối tượng để liệt kê các phần phụ thuộc của nó",
|
||||
"ui.depth": "Độ sâu",
|
||||
"ui.design": "Thiết kế",
|
||||
"ui.design_properties.author": "Tác giả",
|
||||
@@ -4913,6 +5007,11 @@
|
||||
"ui.factor": "Hệ số",
|
||||
"ui.fade": "Thoát ren",
|
||||
"ui.fade_length": "Chiều dài thoát ren",
|
||||
"ui.fair_iterations": "Số lần lặp",
|
||||
"ui.fair_keep_edges": "Giữ các cạnh",
|
||||
"ui.fair_strength": "Cường độ",
|
||||
"ui.fair_surface_title": "LÀM MƯỢT BỀ MẶT",
|
||||
"ui.fair_tolerance_default": "0,001 lần kích thước thân khối",
|
||||
"ui.feature_history": "Lịch sử tính năng",
|
||||
"ui.feature_options": "Tùy chọn tính năng",
|
||||
"ui.features": "Các tính năng",
|
||||
@@ -4941,6 +5040,10 @@
|
||||
"ui.fillet_setback_least": "Lớn hơn {least}",
|
||||
"ui.fillet_setback_tip": "Nơi mảng góc trơn bắt đầu: khoảng cách dọc theo mỗi cạnh tính từ góc. Mặc định là {factor} × bán kính. Giá trị phải lớn hơn khoảng lùi nhỏ nhất, tại đó các đường bo gặp nhau mà không cần lùi: bằng bán kính khi các mặt vuông góc với nhau.",
|
||||
"ui.fillet_smooth_radius": "Thay đổi bán kính trơn",
|
||||
"ui.fillet_turn": "Chỗ rẽ",
|
||||
"ui.fillet_turn_mitre": "Vát góc",
|
||||
"ui.fillet_turn_swept": "Quét vòng",
|
||||
"ui.fillet_turn_tip": "Nơi hai góc lượn rẽ quanh một cạnh góc sắc nằm giữa chúng, ví dụ các góc lượn ở chân một khối lồi tại các cạnh đứng của nó.\nVát góc: các góc lượn kéo dài cho đến khi gặp nhau theo đường vát.\nQuét vòng: các góc lượn dừng ở cạnh góc và quay quanh nó, góc là một mặt xuyến tiếp xúc với cả hai. Chỉ cho các góc lượn cùng bán kính quanh một cạnh góc thẳng và lồi.",
|
||||
"ui.fillet_type_choices_tip": "Bán kính không đổi: một bán kính dọc suốt cạnh.\nĐộ dài dây cung: độ rộng ngang đường bo giữ nguyên, bất kể góc gặp nhau của các mặt.\nBán kính thay đổi: một bán kính ở mỗi đầu, và thêm dọc cạnh nếu bạn thêm vào.\nLiên tục độ cong: mặt cắt không làm gãy phản chiếu tại nơi gặp các mặt phẳng (G2).\nSpline: bán kính dọc cạnh theo các bán kính điều khiển bạn đặt.",
|
||||
"ui.fillet_type_curvature": "Liên tục độ cong",
|
||||
"ui.fillet_type_tip": "Mặt cắt ngang qua cạnh. Liên tục độ cong: mặt cắt liên tục độ cong (G2) giữa các mặt phẳng, nên phản chiếu đi từ các mặt vào đường bo mà không bị gãy.",
|
||||
@@ -4986,6 +5089,7 @@
|
||||
"ui.heading_construct": "DỰNG HÌNH",
|
||||
"ui.heading_create": "TẠO MỚI",
|
||||
"ui.heading_create_sketch": "TẠO PHÁC THẢO",
|
||||
"ui.heading_custom_view_navigator": "KHỐI ĐIỀU HƯỚNG GÓC NHÌN TÙY CHỈNH",
|
||||
"ui.heading_cylinder": "HÌNH TRỤ",
|
||||
"ui.heading_design": "THIẾT KẾ",
|
||||
"ui.heading_draft": "ĐỘ DỐC THOÁT KHUÔN",
|
||||
@@ -5121,6 +5225,7 @@
|
||||
"ui.manipulator_free_alt": "Kéo tự do bộ thao tác cũng bằng Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "Bật: Alt/Option hoặc Ctrl/Cmd trên các mũi tên đùn / gizmo sẽ tắt bước nhảy giá trị. Tắt: chỉ Ctrl/Cmd.",
|
||||
"ui.manufacture": "Gia công chế tạo",
|
||||
"ui.match_surface_title": "KHỚP BỀ MẶT",
|
||||
"ui.material_review.axes": "Các trục cục bộ của vật liệu; chưa gán hướng cho thân khối.",
|
||||
"ui.material_review.claims": "Nguồn gốc và giấy phép là thông tin chưa được kiểm chứng. Các giá trị thiếu vẫn ở dạng chưa biết.",
|
||||
"ui.material_review.compressive_strength": "Độ bền nén",
|
||||
@@ -5184,6 +5289,7 @@
|
||||
"ui.options": "Tùy chọn",
|
||||
"ui.orbit": "Xoay quanh tâm (Orbit)",
|
||||
"ui.orbit_around": "Xoay quanh",
|
||||
"ui.organize": "Sắp xếp",
|
||||
"ui.orientation": "Định hướng",
|
||||
"ui.origin": "Gốc tọa độ",
|
||||
"ui.palette_construction_mode": "Chế độ đường dựng (X)",
|
||||
@@ -5272,6 +5378,7 @@
|
||||
"ui.quality": "Chất lượng",
|
||||
"ui.quantity": "Số lượng",
|
||||
"ui.quantity_2": "Số lượng 2",
|
||||
"ui.quick_access_full": "Thanh truy cập nhanh chứa tối đa {count} lệnh; hãy gỡ một lệnh để thêm lệnh khác",
|
||||
"ui.quit": "Thoát ứng dụng",
|
||||
"ui.radial_context_menu": "Menu ngữ cảnh dạng vòng (Radial)",
|
||||
"ui.radius": "Bán kính",
|
||||
@@ -5289,6 +5396,7 @@
|
||||
"ui.relink_not_linked": "Chi tiết này chưa được liên kết với thiết kế nguồn nào.",
|
||||
"ui.relink_select_one": "Hãy chọn trước một chi tiết được liên kết hoặc một thể hiện của nó.",
|
||||
"ui.relink_source_all": "Liên kết lại nguồn (tất cả thể hiện)…",
|
||||
"ui.removeFromQuickAccess": "Gỡ khỏi Truy cập nhanh",
|
||||
"ui.removeFromToolbar": "Xóa khỏi thanh công cụ",
|
||||
"ui.removeSection": "Xóa mặt cắt",
|
||||
"ui.render": "Kết xuất hình ảnh",
|
||||
@@ -5297,6 +5405,8 @@
|
||||
"ui.repeat_nothing": "Chưa có gì để lặp lại: hãy bắt đầu một công cụ hoặc hộp thoại trước.",
|
||||
"ui.replace_with_link_done": "Các bản sao hiện đã trở thành các vị trí đặt của thiết kế liên kết.",
|
||||
"ui.report_a_bug": "Báo cáo lỗi",
|
||||
"ui.report_characters_left_one": "Còn {count} ký tự",
|
||||
"ui.report_characters_left_other": "Còn {count} ký tự",
|
||||
"ui.reset": "Đặt lại",
|
||||
"ui.reset_all": "Đặt lại tất cả",
|
||||
"ui.reset_the_settings_on_this_page": "Đặt lại các cài đặt trên trang này",
|
||||
@@ -5581,6 +5691,14 @@
|
||||
"values must be numbers or expressions": "các giá trị phải là số hoặc biểu thức",
|
||||
"view.auto_ortho_faces": "Phối cảnh khi xoay quỹ đạo, trực giao khi nhìn trực diện mặt",
|
||||
"view.render_inspector": "Bảng thuộc tính kết xuất",
|
||||
"view_gizmo.arrows": "Mũi tên xoay",
|
||||
"view_gizmo.home": "Nút góc nhìn mặc định (Home)",
|
||||
"view_gizmo.no_such_body": "Thiết kế này không có thân khối nào mang tên đó.",
|
||||
"view_gizmo.note": "Chọn một thân khối trong khung nhìn. Thân khối được lưu cùng tùy chọn và hồ sơ cài đặt của bạn, và hiển thị trong mọi thiết kế. Nhấp vào một mặt của nó để nhìn thẳng vào mặt đó.",
|
||||
"view_gizmo.texture": "Kết cấu",
|
||||
"view_gizmo.themed": "Theo chủ đề",
|
||||
"view_gizmo.too_detailed": "Quá chi tiết cho khối điều hướng góc nhìn (hơn 10.000 tam giác)",
|
||||
"view_gizmo.too_thin": "Quá dài và mảnh cho khối điều hướng góc nhìn: cạnh mỏng nhất phải ít nhất bằng một phần năm cạnh dài nhất",
|
||||
"web.files.insert_drawings_hint": "Tệp CAD, lưới đa giác, DXF hoặc SVG vào thiết kế đang mở",
|
||||
"web.files.insert_stored_drawings_hint": "Vào thiết kế đang mở (tệp CAD, lưới đa giác, DXF, SVG)",
|
||||
"web.open_full": "Mở trong SolidCraft",
|
||||
|
||||
@@ -957,8 +957,6 @@
|
||||
"cmd.inspect_accessibility": "可達性分析",
|
||||
"cmd.inspect_center_of_mass": "質心",
|
||||
"cmd.inspect_clearance": "間隙",
|
||||
"cmd.inspect_colored_components": "啟用元件顏色",
|
||||
"cmd.inspect_colored_components_disable": "停用元件顏色",
|
||||
"cmd.inspect_compare": "比較實體",
|
||||
"cmd.inspect_compare_picture": "與圖片比較",
|
||||
"cmd.inspect_continuity": "連續性分析",
|
||||
@@ -1366,11 +1364,14 @@
|
||||
"dialog.computing": "正在計算… {seconds} 秒",
|
||||
"dialog.could_not_be_checked": "無法檢查",
|
||||
"dialog.custom_axis": "↖ 自訂軸",
|
||||
"dialog.cylinder_face_tip": "圓柱體所立的模型面或頂點:先點擊此處,再在視圖中點擊它。留空時由 X、Y、Z 放置。",
|
||||
"dialog.dimension_insert_hint": "點擊其他驅動尺寸以插入其名稱",
|
||||
"dialog.dimension_name": "尺寸:{name}",
|
||||
"dialog.edit_title": "編輯 {title}",
|
||||
"dialog.no_shown_body": "沒有可變更的已顯示實體:請顯示一個,或選擇「新建實體」",
|
||||
"dialog.pattern_sketch_not_object": "請選取實體或特徵:草圖兩者皆非。請用草圖自己的「陣列」或「鏡射」重複其曲線,或先將它擠出。",
|
||||
"dialog.primitive_point_tip": "填入 X、Y、Z 的點:先點擊此處,再點擊模型的頂點、邊或面、草圖中的點,或原點。",
|
||||
"dialog.reference_edge_tip": "所選面上的一條直邊,下方的距離從它量起:先點擊此處,再點擊該邊。",
|
||||
"dialog.select": "↖ 選擇",
|
||||
"dialog.selected_count": "↖ 已選擇 {count} 個",
|
||||
"dialog.selection_constraint": "拘束 {id}",
|
||||
@@ -3812,6 +3813,8 @@
|
||||
"sample.plate.name": "範例板",
|
||||
"sample.vents.about": "依矩形陣列排列的槽:列數、欄數和間距均為參數",
|
||||
"sample.vents.name": "通風板",
|
||||
"scale.per_axis": "依軸",
|
||||
"scale.uniform": "等比",
|
||||
"search.command_keys": "Enter 執行 Shift+Enter 執行並保持開啟 Esc 關閉",
|
||||
"search.commands": "命令",
|
||||
"search.design": "此設計中",
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"Bottom Right": "右下",
|
||||
"Boundary": "边界",
|
||||
"Bounding box": "边界框",
|
||||
"Bridge": "桥接",
|
||||
"CHECK": "检查",
|
||||
"COMPONENTS": "组件",
|
||||
"CONFIGURATIONS": "配置",
|
||||
@@ -628,6 +629,8 @@
|
||||
"Suppress Selected Folders": "抑制所选文件夹",
|
||||
"Suppressed": "已抑制",
|
||||
"Surface": "曲面",
|
||||
"Surface or face": "曲面或面",
|
||||
"Surface or faces": "曲面或面",
|
||||
"Surface, or faces to turn over": "曲面,或要翻转的面",
|
||||
"Surfaces": "曲面",
|
||||
"Swap Ends": "交换端点",
|
||||
@@ -641,6 +644,7 @@
|
||||
"Target": "目标",
|
||||
"Target Body": "目标实体",
|
||||
"Target Face (optional)": "目标面(可选)",
|
||||
"Target edges": "目标边",
|
||||
"Tear": "撕裂",
|
||||
"Technical details": "技术详情",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "虚线草图是已保存的平面引导。选择一个新面以修复附着。",
|
||||
@@ -648,6 +652,7 @@
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "方向必须是轴,或位于草图平面内的直线草图曲线",
|
||||
"The mirror line cannot also be mirrored": "镜像线本身不能再被镜像",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "镜像线必须是此草图中的直线曲线,不能是原点轴",
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "原点轴垂直于草图平面,因此在该平面内没有直线",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "保存的作者实例缺失。重新绑定前请选择有效的作者放置。",
|
||||
"Then": "操作",
|
||||
"These features will be deleted:": "以下特征将被删除:",
|
||||
@@ -750,6 +755,8 @@
|
||||
"ai.prefs.follow_camera": "跟随相机",
|
||||
"ai.prefs.follow_camera_off": "相机自动跟随已关闭。",
|
||||
"ai.prefs.follow_camera_on": "相机将转向代理创建的草图。",
|
||||
"ai.prefs.instructions": "自定义指令",
|
||||
"ai.prefs.instructions_note": "在每次对话开始时发送给代理,位于 SolidCraft 自身的指令之后:你喜欢的工作方式、需要遵守的规则。更改从下一次对话(连接或新对话)起生效。",
|
||||
"ai.prefs.intro": "SolidCraft中的AI代理。",
|
||||
"ai.prefs.make_default": "设为默认",
|
||||
"ai.prefs.no_agents": "尚无代理。添加代理或刷新以查找已安装的代理。",
|
||||
@@ -963,8 +970,7 @@
|
||||
"cmd.inspect_analysis": "视图分析",
|
||||
"cmd.inspect_center_of_mass": "质心",
|
||||
"cmd.inspect_clearance": "间隙",
|
||||
"cmd.inspect_colored_components": "启用组件颜色",
|
||||
"cmd.inspect_colored_components_disable": "禁用组件颜色",
|
||||
"cmd.inspect_colored_components": "组件颜色",
|
||||
"cmd.inspect_compare": "比较实体",
|
||||
"cmd.inspect_compare_picture": "比较参考图",
|
||||
"cmd.inspect_continuity": "连续性分析",
|
||||
@@ -1296,6 +1302,7 @@
|
||||
"cmd.timeline_stop": "停止历史播放",
|
||||
"cmd.timeline_suppress": "抑制特征",
|
||||
"cmd.ui.assistant": "切换助手显示",
|
||||
"cmd.view.navigator": "自定义视图导航器",
|
||||
"cmd.view.render_browser": "渲染浏览器",
|
||||
"cmd.view.render_dock": "移动渲染停靠面板",
|
||||
"cmd.view.render_focus": "渲染专注视图",
|
||||
@@ -1394,15 +1401,22 @@
|
||||
"dialog.computing": "正在计算… {seconds} 秒",
|
||||
"dialog.could_not_be_checked": "无法检查",
|
||||
"dialog.custom_axis": "↖ 自定义轴",
|
||||
"dialog.cylinder_face_tip": "圆柱体所立的模型面或顶点:先单击此处,再在视图中单击它。留空时由 X、Y、Z 放置。",
|
||||
"dialog.dimension_insert_hint": "单击其他驱动尺寸以插入其名称",
|
||||
"dialog.dimension_name": "尺寸:{name}",
|
||||
"dialog.dimension_step_down": "减少 {step}",
|
||||
"dialog.dimension_step_expression": "只有数字可以增减;表达式保持不变",
|
||||
"dialog.dimension_step_up": "增加 {step}",
|
||||
"dialog.edit_title": "编辑 {title}",
|
||||
"dialog.no_shown_body": "没有可更改的显示实体:请显示一个实体,或选择新建实体",
|
||||
"dialog.pattern_sketch_not_object": "请选择实体或特征:草图两者都不是。请用草图自身的阵列或镜像重复其曲线,或先将其拉伸。",
|
||||
"dialog.primitive_point_tip": "填入 X、Y、Z 的点:先单击此处,再单击模型的顶点、边或面、草图中的点,或原点。",
|
||||
"dialog.reference_edge_tip": "所选面上的一条直边,下方的距离从它量起:先单击此处,再单击该边。",
|
||||
"dialog.select": "↖ 选择",
|
||||
"dialog.selected_count": "↖ 已选择 {count} 个",
|
||||
"dialog.selection_constraint": "约束 {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · 曲线 {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "选择输入项进行修改。点击已选对象可取消选择;按住 Ctrl/Cmd 点击也可切换选择。检查预览后,再按“确定”。",
|
||||
"dialog.selection_midpoint": "中点 · {id}",
|
||||
"dialog.selection_profile": "{sketch} · 轮廓 {number}",
|
||||
@@ -1420,6 +1434,7 @@
|
||||
"document.read_only_save": "此设计以只读方式打开:请另存为新文件",
|
||||
"document.save_changes": "保存对 {name} 的更改?",
|
||||
"document.save_copy": "保存副本…",
|
||||
"document.tab_unsaved_changes": "未保存的更改",
|
||||
"document.unsaved_changes": "{name} 有未保存的更改。如果不保存,将丢失这些更改。",
|
||||
"drive.rigid": "{joint} 是刚性接头:它把零件固定在原位,因此没有可驱动的运动。要让零件运动,请在“编辑接头”中更改其类型:旋转用 Revolute,滑动用 Slider,两者兼有用 Cylindrical。",
|
||||
"drive.stopped_by_contact": "停在 {value}:{a} 会穿过 {b}",
|
||||
@@ -1440,6 +1455,9 @@
|
||||
"each rail's curves must be in one sketch": "每条导轨的曲线必须位于同一个草图中",
|
||||
"feature computation ended without a result": "特征计算结束时没有返回结果",
|
||||
"file.open.drawing_filter": "设计、CAD 文件、网格和图纸",
|
||||
"file.open.portal_fallback.no_portal_service": "打开的文件选择器并非桌面自带:桌面的文件选择服务不可用。",
|
||||
"file.open.portal_fallback.no_session_bus": "打开的文件选择器并非桌面自带:没有 D-Bus 会话总线。",
|
||||
"file.open.portal_fallback.xwayland": "打开的文件选择器并非桌面自带:在 XWayland 下,门户会拒绝父窗口。",
|
||||
"file_load.built": "已构建:{built}/{total} 个特征。",
|
||||
"file_load.converting": "正在转换实体 {done}/{total}",
|
||||
"file_load.inserting": "正在插入几何体",
|
||||
@@ -1999,6 +2017,7 @@
|
||||
"material.selection.mixed": "不同材料",
|
||||
"measure.at": "位置",
|
||||
"measure.item": "项目 {number}",
|
||||
"measure.line_to_centre": "线到圆心",
|
||||
"measure.quick_copied": "已复制测量值",
|
||||
"measure.quick_copy": "点击复制测量值",
|
||||
"measure.quick_edges": "{count} 条边",
|
||||
@@ -2029,6 +2048,8 @@
|
||||
"nav.blender.help": "中键拖动旋转,Shift+中键拖动平移,Ctrl+中键拖动缩放;右键拖动不会移动相机",
|
||||
"nav.creo.help": "中键拖动旋转,Shift+中键平移,Ctrl+中键上下拖动缩放(向下放大);滚动滚轮也可缩放",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "中键或右键拖动会保持开始时的旋转、平移或缩放,直到松开按键,期间按下或松开任何键都不会改变。关闭后:拖动的每一刻都由当时按住的键决定。",
|
||||
"nav.drag_keeps_mode.label": "拖动保持开始时的导航方式",
|
||||
"nav.free_orbit.help": "鼠标和3D鼠标可保持滚转并越过两个极点旋转。Shift轴吸附将被禁用。",
|
||||
"nav.free_orbit.label": "自由旋转",
|
||||
"nav.free_orbit.snap_help": "鼠标和3D鼠标可越过两极旋转,并保留画面旋转角度。在拖动环绕时按住Shift,可吸附视图方向和画面旋转角度。",
|
||||
@@ -2265,6 +2286,17 @@
|
||||
"prefs.auto_operation_note": "根据材料接触的对象选择合并、新建实体或切割。",
|
||||
"prefs.axes": "坐标轴",
|
||||
"prefs.axis": "轴",
|
||||
"prefs.background_choose": "选择图片…",
|
||||
"prefs.background_image": "图片",
|
||||
"prefs.background_unreadable": "无法读取背景图片",
|
||||
"prefs.blur_cost": "相对渲染开销。条形越多,通常意味着处理工作越多。",
|
||||
"prefs.blur_device": "此图形设备或渲染路径不支持此功能",
|
||||
"prefs.blur_guard": "已降低模糊质量,以保持画面响应。模糊半径未改变。",
|
||||
"prefs.blur_off": "关闭",
|
||||
"prefs.blur_reduce_transparency": "已被系统的降低透明度设置禁用",
|
||||
"prefs.blur_software": "软件图形渲染不支持此功能 (gpu-software)",
|
||||
"prefs.blur_source": "此工作区视图不支持此功能",
|
||||
"prefs.blur_web": "此网页渲染路径不支持此功能",
|
||||
"prefs.bodies_folders": "实体文件夹",
|
||||
"prefs.bodies_folders_always": "始终显示",
|
||||
"prefs.bodies_folders_never": "不显示",
|
||||
@@ -2278,6 +2310,8 @@
|
||||
"prefs.browser_eye_left": "折叠箭头旁的眼睛图标",
|
||||
"prefs.browser_eye_left_note": "浏览器各行的显示切换像以前一样位于折叠箭头旁,而不是在行右端的一列中。",
|
||||
"prefs.browser_reveal_on_hover": "悬停时显示",
|
||||
"prefs.browser_rows_start_closed": "浏览器中的行默认折叠",
|
||||
"prefs.browser_rows_start_closed_note": "浏览器中的放置和文件夹默认折叠,因此包含大量组件的设计只显示顶层行。每一行可通过其箭头、“全部展开”或“在浏览器中查找”打开;“在浏览器中查找”只展开通往该实体的行,并重新折叠上次查找展开的行。在 3D 视图中选取实体不会改变浏览器。关闭时文件夹默认展开,放置按“组件初始展开”的设置显示,在 3D 视图中选取会使浏览器展开到该实体,与以前相同。",
|
||||
"prefs.browser_show_note": "“显示”让浏览器始终留在屏幕上。“悬停时显示”会将其隐藏,直到指针移到它的位置或其左边缘的细标签上;指针在其中或其菜单打开时,它保持打开。",
|
||||
"prefs.browser_shown": "显示",
|
||||
"prefs.built_in": "内置",
|
||||
@@ -2323,6 +2357,13 @@
|
||||
"prefs.dialogs_edge": "视图边缘",
|
||||
"prefs.dialogs_left_column": "左侧栏",
|
||||
"prefs.dialogs_note": "命令对话框的打开位置:停靠在视图边缘,或在左侧栏中替代浏览器和历史;关闭对话框后,浏览器和历史恢复原样。",
|
||||
"prefs.dimension_step_angle": "窗口的箭头、上下方向键和滚轮给角度增减的量",
|
||||
"prefs.dimension_step_length": "窗口的箭头、上下方向键和滚轮给长度增减的量",
|
||||
"prefs.dimension_steps": "窗口步长",
|
||||
"prefs.dimension_values": "尺寸值",
|
||||
"prefs.dimension_values_note": "输入尺寸值的位置。窗口带有可增减数值的箭头;尺寸工具会先放置文字。",
|
||||
"prefs.dimension_values_view": "在视图中",
|
||||
"prefs.dimension_values_window": "在窗口中",
|
||||
"prefs.display": "显示",
|
||||
"prefs.edges": "边缘与着色",
|
||||
"prefs.extend": "扩展",
|
||||
@@ -2402,6 +2443,10 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "立方体角落的两个弯曲箭头在屏幕平面内翻滚视图。开启:顺时针箭头逆时针翻滚,逆时针箭头顺时针翻滚。关闭:每个箭头按其弯曲方向翻滚视图。",
|
||||
"prefs.invert_navigator_arrows": "反转视图立方体的旋转箭头",
|
||||
"prefs.invert_navigator_arrows_note": "立方体周围的箭头按箭头所指的方向转动相机,与 Fusion 和 Inventor 相同:左侧箭头指向右,相机向右转动。关闭:箭头把立方体转向它所在的那一面,因此左侧箭头使相机向左转动。弯曲的翻滚箭头不受影响。",
|
||||
"prefs.keep_unsaved_also_asks": "关闭标签页时仍然询问",
|
||||
"prefs.keep_unsaved_also_asks_tip": "关闭此项后,只要每个未保存设计的副本都是最新的,标签页就会直接关闭,不显示浏览器的提示框。",
|
||||
"prefs.keep_unsaved_in_browser": "在此浏览器中保留未保存的工作",
|
||||
"prefs.keep_unsaved_in_browser_tip": "每个打开的设计的未保存更改都会在此浏览器的私有存储中保持最新。下次访问时会提示恢复。",
|
||||
"prefs.keep_work_on_tab_switch": "切换设计时保持对话框和工具打开",
|
||||
"prefs.keep_work_on_tab_switch_note": "每个设计标签页保留其打开的对话框或工具(包括其中已选择和输入的内容)以及工具栏选项卡。回到该设计时它们仍在原处。关闭时:切换设计会取消它们并显示“模型”选项卡。",
|
||||
"prefs.labels": "标签",
|
||||
@@ -2413,6 +2458,8 @@
|
||||
"prefs.layout_page.card": "卡片",
|
||||
"prefs.layout_page.column": "列",
|
||||
"prefs.layout_page.hover": "悬停",
|
||||
"prefs.layout_page.hover_panel_menus": "悬停切换面板菜单",
|
||||
"prefs.layout_page.hover_panel_menus_help": "点击功能区的面板名称打开菜单。将指针悬停在另一面板名称上即可切换菜单。将指针移到功能区和已打开菜单之外即可关闭菜单。",
|
||||
"prefs.layout_page.icons_labels": "图标+标签",
|
||||
"prefs.layout_page.ok_cancel": "确定和取消",
|
||||
"prefs.layout_page.on_hover": "悬停时",
|
||||
@@ -2533,6 +2580,8 @@
|
||||
"prefs.section_hatch_by_part": "按零件",
|
||||
"prefs.section_hatch_one": "单一样式",
|
||||
"prefs.select": "选择",
|
||||
"prefs.select_hidden_edges": "在带隐藏边线的着色视图中选择隐藏边",
|
||||
"prefs.select_hidden_edges_help": "在带隐藏边线的着色视图中,可穿过面选择边并用于圆角。关闭时保留v0.30.1的选择行为。隐藏的实体仍不可选择。",
|
||||
"prefs.select_one_item": "选择一个项目",
|
||||
"prefs.select_other_hidden": "使用“选择其他”选择隐藏几何体",
|
||||
"prefs.select_other_hidden_help": "开启:包括指针附近的隐藏实体、草图和被遮挡的边。关闭:恢复原来的列表,仅包含指针正下方的可见几何体。",
|
||||
@@ -2557,6 +2606,8 @@
|
||||
"prefs.sketch.click_selects": "单击选择",
|
||||
"prefs.sketch.conflict_colour": "冲突颜色",
|
||||
"prefs.sketch.drawing": "绘制",
|
||||
"prefs.sketch.ellipse_axes": "绘制椭圆时带上轴线",
|
||||
"prefs.sketch.ellipse_axes_note": "将长轴和短轴绘制为构造线,以便标注半轴和轴。关闭:仅绘制椭圆,对其按 D 得到短半径,无论将数值拖到何处,都像以前一样绘制并标注 R。",
|
||||
"prefs.sketch.exit_keep": "保持",
|
||||
"prefs.sketch.exit_previous": "返回之前的视图",
|
||||
"prefs.sketch.exit_unless_rotated": "除非已旋转",
|
||||
@@ -2650,6 +2701,10 @@
|
||||
"prefs.theme_follow_off": "关闭",
|
||||
"prefs.theme_follow_stop": "停止跟随",
|
||||
"prefs.theme_import_hint": "单击选择主题文件,拖放到此处,或复制主题后按 Ctrl+V(Mac 上为 Cmd+V)",
|
||||
"prefs.theme_notice_dismiss": "忽略",
|
||||
"prefs.theme_option_blur": "面板背后模糊",
|
||||
"prefs.theme_option_blur_help": "半透明面板背后的模糊程度,以物理像素为单位。0 表示关闭。",
|
||||
"prefs.theme_option_invalid": "{option} 的已保存值无效;正在使用主题默认值。已保存值将予以保留。",
|
||||
"prefs.theme_option_opacity": "不透明度",
|
||||
"prefs.theme_option_opacity_help": "调整浮动面板的不透明度。最低不透明度限制可确保文字在视口颜色上清晰可读。",
|
||||
"prefs.theme_option_tab_colours": "选项卡颜色",
|
||||
@@ -2658,6 +2713,7 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "切换选项卡时渐变面板和视口颜色。设为零可立即切换。",
|
||||
"prefs.theme_options_badge": "选项",
|
||||
"prefs.theme_options_more": "更多 · {count}",
|
||||
"prefs.theme_options_newer": "此主题需要更新版本的 SolidCraft。升级后,请重新导入原始主题以恢复其选项定义。",
|
||||
"prefs.theme_options_reset": "重置",
|
||||
"prefs.theme_options_unavailable": "此设备不支持",
|
||||
"prefs.theme_paste": "粘贴主题",
|
||||
@@ -2721,6 +2777,12 @@
|
||||
"recovery.just_now": "刚刚",
|
||||
"recovery.min_ago": "{count} 分钟前",
|
||||
"recovery.never_saved": "从未保存",
|
||||
"recovery.open_saved": "打开已保存的版本",
|
||||
"recovery.restore": "恢复",
|
||||
"recovery.restore_intro": "此页面上次关闭时未保存的更改保留在此浏览器中。",
|
||||
"recovery.restore_row": "{name},更改于{when}",
|
||||
"recovery.restore_title": "恢复未保存的设计?",
|
||||
"recovery.restore_title_one": "恢复未保存的设计:{name},更改于{when}?",
|
||||
"refused.change": "无法更改 {body}。",
|
||||
"refused.cut": "无法切割 {body}。",
|
||||
"refused.face_curved": "它的一个曲面无法用于建模。",
|
||||
@@ -3242,9 +3304,11 @@
|
||||
"render.frame_wide": "16:9 · 宽屏",
|
||||
"render.gpu_depth": "最大路径深度",
|
||||
"render.gpu_options_help": "采样数、深度、种子和曝光会随此配置保存。仅保存当前图像方法。",
|
||||
"render.gpu_path_crashed": "上次在此图形适配器上设置时 SolidCraft 关闭了,很可能是在图形驱动程序内部,因此不会再自动设置。请改用 CPU 光线追踪,或重试(更新驱动程序后也会重试)。",
|
||||
"render.gpu_path_failed": "GPU 路径追踪器不可用",
|
||||
"render.gpu_path_first_setup": "首次在此 GPU 上设置可能需要几分钟,期间 SolidCraft 仍可正常使用。之后启动时,如果驱动程序保留了已编译的着色器,将直接复用。",
|
||||
"render.gpu_path_preparing": "GPU 路径追踪器:正在编译着色器…",
|
||||
"render.gpu_path_retry": "重试",
|
||||
"render.gpu_seed": "随机种子",
|
||||
"render.gpu_unavailable": "此设备上的 GPU 路径追踪尚未就绪。",
|
||||
"render.ground_apply": "适配并保存地面",
|
||||
@@ -4052,6 +4116,8 @@
|
||||
"sample.plate.name": "示例板",
|
||||
"sample.vents.about": "按矩形阵列排列的槽:行数、列数和间距均为参数",
|
||||
"sample.vents.name": "通风板",
|
||||
"scale.per_axis": "按轴",
|
||||
"scale.uniform": "等比",
|
||||
"search.command_keys": "Enter 运行 Shift+Enter 运行并保持打开 Esc 关闭",
|
||||
"search.commands": "命令",
|
||||
"search.design": "此设计中",
|
||||
@@ -4142,6 +4208,32 @@
|
||||
"sketch.dof_left": "剩余 {count} 个自由度",
|
||||
"sketch.dof_left_one": "剩余 {count} 个自由度",
|
||||
"sketch.draw_curves": "绘制为 {linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "将选择转换为 {linetype}",
|
||||
"sketch.open_ends": "开放 {count}",
|
||||
"sketch.status_conflict": "约束冲突",
|
||||
@@ -4363,6 +4455,7 @@
|
||||
"ui.3d_sketch": "3D 草图",
|
||||
"ui.activate": "激活",
|
||||
"ui.active_component": "活动组件",
|
||||
"ui.addToQuickAccess": "添加到快速访问",
|
||||
"ui.add_configuration": "添加配置",
|
||||
"ui.add_parameter": "添加参数",
|
||||
"ui.agent_chat": "智能体聊天",
|
||||
@@ -4784,6 +4877,7 @@
|
||||
"ui.default_modelling_orientation": "默认建模方向",
|
||||
"ui.delete": "删除",
|
||||
"ui.delete_select_first": "请先选择要删除的内容:特征、实体、组件或草图几何",
|
||||
"ui.dependents_select_first": "请先选择要列出其依赖项的对象",
|
||||
"ui.depth": "深度",
|
||||
"ui.design": "设计",
|
||||
"ui.design_properties.author": "作者",
|
||||
@@ -4913,6 +5007,11 @@
|
||||
"ui.factor": "系数",
|
||||
"ui.fade": "收尾",
|
||||
"ui.fade_length": "收尾长度",
|
||||
"ui.fair_iterations": "迭代次数",
|
||||
"ui.fair_keep_edges": "保持边",
|
||||
"ui.fair_strength": "强度",
|
||||
"ui.fair_surface_title": "曲面光顺",
|
||||
"ui.fair_tolerance_default": "实体尺寸的 0.001 倍",
|
||||
"ui.feature_history": "特征历史",
|
||||
"ui.feature_options": "特征选项",
|
||||
"ui.features": "特征",
|
||||
@@ -4941,6 +5040,10 @@
|
||||
"ui.fillet_setback_least": "须大于 {least}",
|
||||
"ui.fillet_setback_tip": "平滑角点曲面片开始的位置:从角点沿每条边的距离。默认值为半径的 {factor} 倍。必须大于最小后退距离(不后退时圆角相交之处;面相互垂直时即半径)。",
|
||||
"ui.fillet_smooth_radius": "半径平滑变化",
|
||||
"ui.fillet_turn": "转角",
|
||||
"ui.fillet_turn_mitre": "斜接",
|
||||
"ui.fillet_turn_swept": "旋转",
|
||||
"ui.fillet_turn_tip": "两个圆角绕过它们之间尖锐的角边处,例如凸台根部的圆角绕过其竖直边。\n斜接:圆角继续延伸,直到以斜接相遇。\n旋转:圆角止于角边并绕它旋转,转角是与两者相切的圆环面。仅用于同一半径的圆角绕过笔直的凸角边时。",
|
||||
"ui.fillet_type_choices_tip": "恒定半径:整条边一个半径。\n弦长:无论面以什么角度相交,圆角宽度都相同。\n可变半径:两端各一个半径,添加后沿边还有更多。\n曲率连续:与平面相接处反射没有折痕的截面 (G2)。\n样条:沿边的半径跟随您设置的控制半径。",
|
||||
"ui.fillet_type_curvature": "曲率连续",
|
||||
"ui.fillet_type_tip": "横跨边的截面。曲率连续:平面之间的曲率连续 (G2) 截面,反射从面平滑过渡到圆角,没有折痕。",
|
||||
@@ -4986,6 +5089,7 @@
|
||||
"ui.heading_construct": "构造",
|
||||
"ui.heading_create": "创建",
|
||||
"ui.heading_create_sketch": "创建草图",
|
||||
"ui.heading_custom_view_navigator": "自定义视图导航器",
|
||||
"ui.heading_cylinder": "圆柱体",
|
||||
"ui.heading_design": "设计",
|
||||
"ui.heading_draft": "拔模",
|
||||
@@ -5121,6 +5225,7 @@
|
||||
"ui.manipulator_free_alt": "操纵器自由拖动也适用于 Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "开:在拉伸/小控件箭头上按 Alt/Option 或 Ctrl/Cmd 可关闭数值捕捉。关:仅 Ctrl/Cmd。",
|
||||
"ui.manufacture": "制造",
|
||||
"ui.match_surface_title": "曲面匹配",
|
||||
"ui.material_review.axes": "材料局部坐标轴;不指定实体方向。",
|
||||
"ui.material_review.claims": "来源和许可证声明未经核实。缺失值保持未知。",
|
||||
"ui.material_review.compressive_strength": "抗压强度",
|
||||
@@ -5184,6 +5289,7 @@
|
||||
"ui.options": "选项",
|
||||
"ui.orbit": "环绕",
|
||||
"ui.orbit_around": "环绕",
|
||||
"ui.organize": "整理",
|
||||
"ui.orientation": "定向",
|
||||
"ui.origin": "原点",
|
||||
"ui.palette_construction_mode": "构造模式 (X)",
|
||||
@@ -5272,6 +5378,7 @@
|
||||
"ui.quality": "质量",
|
||||
"ui.quantity": "数量",
|
||||
"ui.quantity_2": "数量 2",
|
||||
"ui.quick_access_full": "快速访问栏最多容纳 {count} 个命令;请先移除一个再添加",
|
||||
"ui.quit": "退出",
|
||||
"ui.radial_context_menu": "环形上下文菜单",
|
||||
"ui.radius": "半径",
|
||||
@@ -5289,6 +5396,7 @@
|
||||
"ui.relink_not_linked": "此组件未链接到源设计。",
|
||||
"ui.relink_select_one": "请先选择一个链接组件或其实例。",
|
||||
"ui.relink_source_all": "重新链接源文件(所有实例)…",
|
||||
"ui.removeFromQuickAccess": "从快速访问中移除",
|
||||
"ui.removeFromToolbar": "从工具栏移除",
|
||||
"ui.removeSection": "移除剖面",
|
||||
"ui.render": "渲染",
|
||||
@@ -5297,6 +5405,8 @@
|
||||
"ui.repeat_nothing": "还没有可重复的操作:请先启动一个工具或对话框。",
|
||||
"ui.replace_with_link_done": "这些副本现在是链接设计的放置。",
|
||||
"ui.report_a_bug": "报告缺陷",
|
||||
"ui.report_characters_left_one": "剩余 {count} 个字符",
|
||||
"ui.report_characters_left_other": "剩余 {count} 个字符",
|
||||
"ui.reset": "重置",
|
||||
"ui.reset_all": "全部重置",
|
||||
"ui.reset_the_settings_on_this_page": "重置此页面上的设置",
|
||||
@@ -5581,6 +5691,14 @@
|
||||
"values must be numbers or expressions": "值必须是数字或表达式",
|
||||
"view.auto_ortho_faces": "旋转时透视,正对表面时正交",
|
||||
"view.render_inspector": "渲染检查器",
|
||||
"view_gizmo.arrows": "旋转箭头",
|
||||
"view_gizmo.home": "主页按钮",
|
||||
"view_gizmo.no_such_body": "此设计中没有该名称的实体。",
|
||||
"view_gizmo.note": "在视图中选择一个实体。它随首选项和设置配置文件保存,并在每个设计中显示。单击它的一个面即可正视该面。",
|
||||
"view_gizmo.texture": "纹理",
|
||||
"view_gizmo.themed": "主题",
|
||||
"view_gizmo.too_detailed": "对视图导航器而言过于精细(超过 10,000 个三角形)",
|
||||
"view_gizmo.too_thin": "对视图导航器而言过于细长:最薄的一边至少要有最长一边的五分之一",
|
||||
"web.files.insert_drawings_hint": "将 CAD 文件、网格、DXF 或 SVG 插入当前设计",
|
||||
"web.files.insert_stored_drawings_hint": "插入当前设计(CAD 文件、网格、DXF、SVG)",
|
||||
"web.open_full": "在 SolidCraft 中打开",
|
||||
|
||||
@@ -366,6 +366,13 @@ fn fs_ao_depth(i: TOut, @builtin(front_facing) front: bool) -> AoDepth {
|
||||
return opaque_depth(i, front);
|
||||
}
|
||||
|
||||
// The virtual ground preserves the viewport background; only contact occlusion is blended.
|
||||
@fragment
|
||||
fn fs_ground_ao(i: TOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 1.0 - occlusion(i));
|
||||
}
|
||||
|
||||
/// The AO prepass's depth of an opaque surface's fragment.
|
||||
fn opaque_depth(i: TOut, front: bool) -> AoDepth {
|
||||
if (clipped(i.wp) || length(i.n) < 0.5) { discard; }
|
||||
@@ -802,7 +809,9 @@ fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(
|
||||
let sb = cb.xy / max(cb.w, 1e-6) * half;
|
||||
var dir = sb - sa;
|
||||
let len = length(dir);
|
||||
if (len < 1e-6) { dir = vec2<f32>(1.0, 0.0); } else { dir = dir / len; }
|
||||
// Intel OpenGL can miscompile vector/scalar division here (#8053). Scalar
|
||||
// quotients keep the same direction and the fallback for a point.
|
||||
if (len < 1e-6) { dir = vec2<f32>(1.0, 0.0); } else { dir = vec2<f32>(dir.x / len, dir.y / len); }
|
||||
let nrm = vec2<f32>(-dir.y, dir.x);
|
||||
// Two triangles: corners (t, side).
|
||||
var ts = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
|
||||
|
||||
@@ -0,0 +1,667 @@
|
||||
{
|
||||
"order": 260,
|
||||
"provenance": "Original SolidCraft stone-grey palette and semantic icon-role mapping",
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Onyx",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#9eafbd",
|
||||
"accent_soft": "#3b4852",
|
||||
"agent_busy": "#9eafbd",
|
||||
"agent_ok": "#a5b9a5",
|
||||
"app_bar": "#1c1e20",
|
||||
"app_bar_hover": "#dededc16",
|
||||
"app_bar_hover_button": "#dededc1c",
|
||||
"app_bar_hover_strong": "#dededc28",
|
||||
"app_bar_hover_subtle": "#dededc0c",
|
||||
"app_bar_separator": "#383c3f",
|
||||
"app_bar_tab_active": "#303235",
|
||||
"app_bar_text": "#dededc",
|
||||
"axis_x": "#d7938f",
|
||||
"axis_y": "#a5b9a5",
|
||||
"axis_z": "#98b2c8",
|
||||
"bend_down": "#b8a7c3",
|
||||
"bend_up": "#a5b9a5",
|
||||
"body": "#93989c",
|
||||
"body_edge": "#60666b",
|
||||
"body_selected": "#3b4852",
|
||||
"border": "#3d4145",
|
||||
"box_window": "#9eafbd",
|
||||
"chip": "#758b9b",
|
||||
"construction_geom": "#b9a78e",
|
||||
"construction_plane": "#b9a78e24",
|
||||
"continuity": [
|
||||
"#a5b9a5",
|
||||
"#98b2c8",
|
||||
"#c7b28b",
|
||||
"#b8a7c3"
|
||||
],
|
||||
"control_border": "#4a4f54",
|
||||
"control_fill": "#303235",
|
||||
"cube_border": "#50565b",
|
||||
"cube_face": "#34373a",
|
||||
"cube_hover": "#53616c",
|
||||
"cube_text": "#dededc",
|
||||
"dialog_bg": "#2b2d2f",
|
||||
"dialog_body": "#2b2d2f",
|
||||
"dialog_border": "#3d4145",
|
||||
"dialog_footer": "#27292b",
|
||||
"dialog_label": "#dededc",
|
||||
"dialog_rule": "#383c3f",
|
||||
"dimension": "#c7b28b",
|
||||
"dimension_driven": "#a59474",
|
||||
"error": "#d7938f",
|
||||
"field": "#1c1e20",
|
||||
"form_accent": "#9d8faa",
|
||||
"form_dim": "#c6c8c9",
|
||||
"form_error": "#d7938f",
|
||||
"form_error_fill": "#493334",
|
||||
"form_fill": "#b9abc5",
|
||||
"form_ink": "#dededc",
|
||||
"form_primary": "#9eafbd",
|
||||
"form_primary_fill": "#9eafbd",
|
||||
"form_soft": "#343039",
|
||||
"form_success": "#a5b9a5",
|
||||
"form_success_fill": "#344139",
|
||||
"form_tab_ink": "#b9abc5",
|
||||
"glyph_bg": "#34373a",
|
||||
"glyph_edge": "#d7938f",
|
||||
"glyph_text": "#d7938f",
|
||||
"grid": "#252729",
|
||||
"grid_major": "#2e3235",
|
||||
"hover": "#3b4852",
|
||||
"hover_edge": "#b1c1ce",
|
||||
"hover_edge_halo": "#181a1c",
|
||||
"hover_profile": "#9eafbd30",
|
||||
"hover_profile_edge": "#b1c1ce",
|
||||
"icon": "#dededc",
|
||||
"icons.agent": "#b8a7c3",
|
||||
"icons.highlight": "#dededc",
|
||||
"icons.marker_halo": "#1c1e20cc",
|
||||
"interference": "#d7938f",
|
||||
"joint_axis": "#98b2c8",
|
||||
"joint_manipulator": "#9eafbd",
|
||||
"logo": [
|
||||
"#c9d4dc",
|
||||
"#9eafbd",
|
||||
"#647986"
|
||||
],
|
||||
"manipulator": "#9eafbd",
|
||||
"mesh_accent": "#a29772",
|
||||
"mesh_fill": "#beb48d",
|
||||
"mesh_ink": "#beb48d",
|
||||
"mesh_soft": "#34332b",
|
||||
"nav_dim": "#b5b7b8",
|
||||
"nav_field": "#1c1e20",
|
||||
"nav_hover": "#43494f",
|
||||
"nav_press": "#59636c",
|
||||
"nav_tip": "#2b2d2f",
|
||||
"nav_tip_ink": "#dededc",
|
||||
"nav_tip_key_border": "#3b3e41",
|
||||
"nav_viewport": "#27292b",
|
||||
"on_accent": "#17191b",
|
||||
"origin_plane": "#b9a78e2c",
|
||||
"origin_plane_edge": "#b9a78e94",
|
||||
"origin_plane_hover": "#9eafbd60",
|
||||
"origin_point": "#b5b7b8",
|
||||
"overlay": "#2b2d2f",
|
||||
"panel": "#2b2d2f",
|
||||
"panel_header": "#303235",
|
||||
"preview_add": "#9eafbd",
|
||||
"preview_cut": "#d7938f68",
|
||||
"preview_new": "#c7b28b",
|
||||
"preview_sketch": "#a5b9a5",
|
||||
"profile_fill": "#9eafbd28",
|
||||
"render_accent": "#a88e88",
|
||||
"render_fill": "#c1a49e",
|
||||
"render_ink": "#c1a49e",
|
||||
"render_soft": "#38302f",
|
||||
"rubber_band": "#9eafbd",
|
||||
"section_cap": "#c7b28b",
|
||||
"sel_edge": "#b1c1ce",
|
||||
"sel_edge_rim": "#d0d9df",
|
||||
"sel_face": "#3b4852",
|
||||
"sel_profile": "#3b4852",
|
||||
"sel_profile_edge": "#b1c1ce",
|
||||
"sel_vertex": "#9eafbd",
|
||||
"selected_bg": "#3b4852",
|
||||
"selected_ink": "#dededc",
|
||||
"sheet_fill": "#9eb9bd",
|
||||
"sheet_ink": "#9eb9bd",
|
||||
"sheet_soft": "#29363a",
|
||||
"sketch": "#9eafbd",
|
||||
"sketch_accent": "#b2b995",
|
||||
"sketch_centerline": "#b5b7b8",
|
||||
"sketch_conflict": "#d7938f",
|
||||
"sketch_construction": "#b5b7b8",
|
||||
"sketch_fixed": "#dededc",
|
||||
"sketch_halo": "#181a1c",
|
||||
"sketch_ink": "#b2b995",
|
||||
"sketch_locked": "#a5b9a5",
|
||||
"sketch_lost": "#d7938f",
|
||||
"sketch_point": "#dededc",
|
||||
"sketch_point_free": "#9eafbd",
|
||||
"sketch_projected": "#b8a7c3",
|
||||
"sketch_soft": "#32352c",
|
||||
"surface_accent": "#a6947d",
|
||||
"surface_fill": "#c0ae96",
|
||||
"surface_ink": "#c0ae96",
|
||||
"surface_soft": "#35322e",
|
||||
"tab_on_fill_dark": "#17191b",
|
||||
"text": "#dededc",
|
||||
"text_dim": "#c6c8c9",
|
||||
"timeline": "#2b2d2f",
|
||||
"timeline_item": "#303235",
|
||||
"timeline_item_error": "#493334",
|
||||
"timeline_marker": "#b5b7b8",
|
||||
"toolbar": "#2b2d2f",
|
||||
"viewport_bottom": "#1b1d1f",
|
||||
"viewport_top": "#222426",
|
||||
"warning": "#c7b28b"
|
||||
},
|
||||
"radii": {
|
||||
"radius_sm": 3,
|
||||
"radius_md": 6,
|
||||
"radius_lg": 8,
|
||||
"radius_xl": 10,
|
||||
"control_radius": 4,
|
||||
"sketch_mode_radius": 6
|
||||
},
|
||||
"icon_palette": {
|
||||
"ink": "#1b1d1f",
|
||||
"line": "#d7d9d8",
|
||||
"passive-1": "#b9bdc0",
|
||||
"passive-2": "#8d9398",
|
||||
"passive-3": "#646c72",
|
||||
"guide": "#a2a8ac",
|
||||
"solid": [
|
||||
"#c9d4dc",
|
||||
"#b2c1cc",
|
||||
"#9eafbd"
|
||||
],
|
||||
"surface": [
|
||||
"#d6cab8",
|
||||
"#c0ae96",
|
||||
"#a6947d"
|
||||
],
|
||||
"mesh": [
|
||||
"#d4ccad",
|
||||
"#beb48d",
|
||||
"#a29772"
|
||||
],
|
||||
"form": [
|
||||
"#d0c5d9",
|
||||
"#b9abc5",
|
||||
"#9d8faa"
|
||||
],
|
||||
"sketch": [
|
||||
"#c9cfb3",
|
||||
"#b2b995",
|
||||
"#979f78"
|
||||
],
|
||||
"constraint": [
|
||||
"#e1b7b2",
|
||||
"#d7938f",
|
||||
"#b77875"
|
||||
],
|
||||
"construction": [
|
||||
"#d2c6b4",
|
||||
"#b9a78e",
|
||||
"#9f8c73"
|
||||
]
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"id": "opacity",
|
||||
"type": "range",
|
||||
"default": 80,
|
||||
"min": 60,
|
||||
"max": 100
|
||||
},
|
||||
{
|
||||
"id": "blur",
|
||||
"type": "range",
|
||||
"default": 60,
|
||||
"max": 60
|
||||
},
|
||||
{
|
||||
"id": "tab_colours",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": "tab_fade_ms",
|
||||
"type": "range",
|
||||
"default": 240
|
||||
},
|
||||
{
|
||||
"id": "accent",
|
||||
"type": "choice",
|
||||
"default": "slate",
|
||||
"label": {
|
||||
"en": "Accent",
|
||||
"de": "Akzent",
|
||||
"ja": "アクセント",
|
||||
"ko": "강조색",
|
||||
"zh": "强调色"
|
||||
},
|
||||
"help": {
|
||||
"en": "Selection, highlights and Model controls; workspace and status colours keep their meaning.",
|
||||
"de": "Auswahl, Hervorhebungen und Modell-Steuerung; Arbeitsbereichs- und Statusfarben behalten ihre Bedeutung.",
|
||||
"ja": "選択・強調・モデルの操作色。ワークスペースや状態の色の意味は変えません。",
|
||||
"ko": "선택, 강조 및 모델 조작의 색상입니다. 작업 공간과 상태 색상의 의미는 유지됩니다.",
|
||||
"zh": "更改选择、高亮与模型控件的色调;工作空间和状态色保持原有含义。"
|
||||
},
|
||||
"values": [
|
||||
{
|
||||
"id": "slate",
|
||||
"label": {
|
||||
"en": "Slate blue",
|
||||
"de": "Schieferblau",
|
||||
"ja": "石板の青",
|
||||
"ko": "슬레이트 블루",
|
||||
"zh": "岩蓝"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "silver",
|
||||
"label": {
|
||||
"en": "Silver",
|
||||
"de": "Silber",
|
||||
"ja": "シルバー",
|
||||
"ko": "은색",
|
||||
"zh": "银灰"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sage",
|
||||
"label": {
|
||||
"en": "Sage",
|
||||
"de": "Salbei",
|
||||
"ja": "セージ",
|
||||
"ko": "세이지",
|
||||
"zh": "鼠尾草绿"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "contrast",
|
||||
"type": "choice",
|
||||
"default": "soft",
|
||||
"label": {
|
||||
"en": "Contrast",
|
||||
"de": "Kontrast",
|
||||
"ja": "コントラスト",
|
||||
"ko": "대비",
|
||||
"zh": "对比度"
|
||||
},
|
||||
"help": {
|
||||
"en": "Soft stone layers or stronger separation between panels and fields.",
|
||||
"de": "Weiche Steinschichten oder stärkere Trennung von Flächen und Feldern.",
|
||||
"ja": "柔らかな石の階調か、パネルと入力欄を明確に分ける階調を選びます。",
|
||||
"ko": "부드러운 석재 톤 또는 패널과 입력란을 더 뚜렷하게 구분합니다.",
|
||||
"zh": "选择柔和的石色层次,或增强面板与输入框的区分。"
|
||||
},
|
||||
"values": [
|
||||
{
|
||||
"id": "soft",
|
||||
"label": {
|
||||
"en": "Soft",
|
||||
"de": "Weich",
|
||||
"ja": "ソフト",
|
||||
"ko": "부드럽게",
|
||||
"zh": "柔和"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "defined",
|
||||
"label": {
|
||||
"en": "Defined",
|
||||
"de": "Deutlich",
|
||||
"ja": "くっきり",
|
||||
"ko": "뚜렷하게",
|
||||
"zh": "分明"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "brightness",
|
||||
"type": "range",
|
||||
"effect": "surface_brightness",
|
||||
"default": 100,
|
||||
"min": 60,
|
||||
"max": 120,
|
||||
"step": 1,
|
||||
"unit": "%",
|
||||
"label": {
|
||||
"en": "Brightness",
|
||||
"de": "Helligkeit",
|
||||
"ja": "明るさ",
|
||||
"ko": "밝기",
|
||||
"zh": "亮度"
|
||||
},
|
||||
"help": {
|
||||
"en": "Adjust stone surfaces, viewport backgrounds and grid brightness. 100% keeps the authored tones; text and semantic colours stay unchanged.",
|
||||
"de": "Helligkeit von Steinflächen, Ansichtsflächen und Raster. 100% erhält die ursprünglichen Töne; Text und Bedeutungsfarben bleiben unverändert.",
|
||||
"ja": "石調の面・ビューポート背景・グリッドの明るさを調整します。100%は元の階調です。文字と意味のある色は変えません。",
|
||||
"ko": "석재 표면·뷰포트 배경·격자의 밝기를 조절합니다. 100%는 원래 톤을 유지하며 글자와 의미 색상은 바꾸지 않습니다.",
|
||||
"zh": "调整石色表面、视口背景和网格的亮度。100%保留原有色调;文字和语义颜色保持不变。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "foreground_colours",
|
||||
"type": "choice",
|
||||
"default": "vibrant",
|
||||
"label": {
|
||||
"en": "Foreground colours",
|
||||
"de": "Vordergrundfarben",
|
||||
"ja": "前景の色",
|
||||
"ko": "전경 색상",
|
||||
"zh": "前景颜色"
|
||||
},
|
||||
"help": {
|
||||
"en": "Vibrant strengthens axes, icons and guides. Muted keeps the original quiet palette. Accent controls selection.",
|
||||
"de": "Leuchtend verstärkt Achsen, Symbole und Hilfen. Gedämpft behält die ruhige ursprüngliche Palette. Akzent steuert die Auswahl.",
|
||||
"ja": "鮮やかは軸・アイコン・作図ガイドを強調します。落ち着いた配色は元の控えめな色を保ちます。選択の色はアクセントで設定します。",
|
||||
"ko": "선명함은 축·아이콘·가이드를 강조합니다. 차분함은 기존의 은은한 팔레트를 유지합니다. 선택 색상은 강조색 설정을 따릅니다.",
|
||||
"zh": "鲜艳增强轴、图标和辅助线的色彩。柔和保留原有的沉静配色。选择颜色仍由强调色控制。"
|
||||
},
|
||||
"values": [
|
||||
{
|
||||
"id": "muted",
|
||||
"label": {
|
||||
"en": "Muted",
|
||||
"de": "Gedämpft",
|
||||
"ja": "落ち着いた",
|
||||
"ko": "차분함",
|
||||
"zh": "柔和"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vibrant",
|
||||
"label": {
|
||||
"en": "Vibrant",
|
||||
"de": "Leuchtend",
|
||||
"ja": "鮮やか",
|
||||
"ko": "선명함",
|
||||
"zh": "鲜艳"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"overlays": [
|
||||
{
|
||||
"when": {
|
||||
"opt": "contrast",
|
||||
"eq": "defined"
|
||||
},
|
||||
"colors": {
|
||||
"app_bar": "#151719",
|
||||
"panel": "#25282a",
|
||||
"dialog_bg": "#25282a",
|
||||
"dialog_body": "#25282a",
|
||||
"dialog_footer": "#202325",
|
||||
"panel_header": "#373b3e",
|
||||
"field": "#171a1c",
|
||||
"control_fill": "#383d41",
|
||||
"border": "#4e555b",
|
||||
"dialog_border": "#4e555b",
|
||||
"dialog_rule": "#444b51",
|
||||
"control_border": "#626c73",
|
||||
"text": "#eeeeec",
|
||||
"text_dim": "#ced1d2",
|
||||
"form_ink": "#eeeeec",
|
||||
"form_dim": "#ced1d2",
|
||||
"dialog_label": "#eeeeec",
|
||||
"toolbar": "#25282a",
|
||||
"timeline": "#25282a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"opt": "accent",
|
||||
"eq": "silver"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "#b7bdc2",
|
||||
"manipulator": "#b7bdc2",
|
||||
"joint_manipulator": "#b7bdc2",
|
||||
"preview_add": "#b7bdc2",
|
||||
"rubber_band": "#b7bdc2",
|
||||
"box_window": "#b7bdc2",
|
||||
"form_primary": "#b7bdc2",
|
||||
"form_primary_fill": "#b7bdc2",
|
||||
"sketch": "#b7bdc2",
|
||||
"sketch_point_free": "#b7bdc2",
|
||||
"sel_vertex": "#b7bdc2",
|
||||
"accent_soft": "#454b50",
|
||||
"hover": "#454b50",
|
||||
"selected_bg": "#454b50",
|
||||
"sel_face": "#454b50",
|
||||
"sel_profile": "#454b50",
|
||||
"body_selected": "#454b50",
|
||||
"sel_edge": "#d5dadf",
|
||||
"sel_profile_edge": "#d5dadf",
|
||||
"hover_edge": "#d5dadf",
|
||||
"hover_profile_edge": "#d5dadf",
|
||||
"sel_edge_rim": "#dde0e3",
|
||||
"hover_profile": "#b7bdc230",
|
||||
"origin_plane_hover": "#b7bdc260",
|
||||
"profile_fill": "#b7bdc228",
|
||||
"cube_hover": "#454b50",
|
||||
"chip": "#b7bdc2",
|
||||
"logo": [
|
||||
"#dde0e3",
|
||||
"#c9cfd4",
|
||||
"#b7bdc2"
|
||||
]
|
||||
},
|
||||
"icon_palette": {
|
||||
"solid": [
|
||||
"#dde0e3",
|
||||
"#c9cfd4",
|
||||
"#b7bdc2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"opt": "accent",
|
||||
"eq": "sage"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "#aabcae",
|
||||
"manipulator": "#aabcae",
|
||||
"joint_manipulator": "#aabcae",
|
||||
"preview_add": "#aabcae",
|
||||
"rubber_band": "#aabcae",
|
||||
"box_window": "#aabcae",
|
||||
"form_primary": "#aabcae",
|
||||
"form_primary_fill": "#aabcae",
|
||||
"sketch": "#aabcae",
|
||||
"sketch_point_free": "#aabcae",
|
||||
"sel_vertex": "#aabcae",
|
||||
"accent_soft": "#414d45",
|
||||
"hover": "#414d45",
|
||||
"selected_bg": "#414d45",
|
||||
"sel_face": "#414d45",
|
||||
"sel_profile": "#414d45",
|
||||
"body_selected": "#414d45",
|
||||
"sel_edge": "#c2d2c5",
|
||||
"sel_profile_edge": "#c2d2c5",
|
||||
"hover_edge": "#c2d2c5",
|
||||
"hover_profile_edge": "#c2d2c5",
|
||||
"sel_edge_rim": "#d1ddd4",
|
||||
"hover_profile": "#aabcae30",
|
||||
"origin_plane_hover": "#aabcae60",
|
||||
"profile_fill": "#aabcae28",
|
||||
"cube_hover": "#414d45",
|
||||
"chip": "#aabcae",
|
||||
"logo": [
|
||||
"#d1ddd4",
|
||||
"#becec2",
|
||||
"#aabcae"
|
||||
]
|
||||
},
|
||||
"icon_palette": {
|
||||
"solid": [
|
||||
"#d1ddd4",
|
||||
"#becec2",
|
||||
"#aabcae"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"opt": "foreground_colours",
|
||||
"eq": "vibrant"
|
||||
},
|
||||
"colors": {
|
||||
"axis_x": "#ff6d79",
|
||||
"axis_y": "#4fd681",
|
||||
"axis_z": "#4da7ff",
|
||||
"agent_busy": "#4da7ff",
|
||||
"agent_ok": "#4fd681",
|
||||
"warning": "#ffd166",
|
||||
"error": "#ff6d79",
|
||||
"form_error": "#ff6d79",
|
||||
"form_success": "#4fd681",
|
||||
"glyph_edge": "#ff6d79",
|
||||
"glyph_text": "#ff6d79",
|
||||
"interference": "#ff6d79",
|
||||
"sketch_conflict": "#ff6d79",
|
||||
"sketch_lost": "#ff6d79",
|
||||
"sketch_locked": "#4fd681",
|
||||
"sketch_projected": "#c78bff",
|
||||
"dimension": "#ffd166",
|
||||
"dimension_driven": "#ffb166",
|
||||
"construction_geom": "#ffb166",
|
||||
"sketch_centerline": "#b5b7b8",
|
||||
"sketch_construction": "#b5b7b8",
|
||||
"joint_axis": "#42d9dd",
|
||||
"bend_down": "#c78bff",
|
||||
"bend_up": "#4fd681",
|
||||
"preview_cut": "#ff6d7968",
|
||||
"preview_new": "#ffd166",
|
||||
"preview_sketch": "#4fd681",
|
||||
"section_cap": "#ffd166",
|
||||
"sketch_accent": "#4fd681",
|
||||
"sketch_ink": "#4fd681",
|
||||
"surface_accent": "#ffb166",
|
||||
"surface_fill": "#ffb166",
|
||||
"surface_ink": "#ffb166",
|
||||
"mesh_accent": "#ffd166",
|
||||
"mesh_fill": "#ffd166",
|
||||
"mesh_ink": "#ffd166",
|
||||
"form_accent": "#c78bff",
|
||||
"form_fill": "#c78bff",
|
||||
"form_tab_ink": "#c78bff",
|
||||
"sheet_fill": "#63cbd5",
|
||||
"sheet_ink": "#63cbd5",
|
||||
"render_accent": "#ff6d79",
|
||||
"render_fill": "#ff6d79",
|
||||
"render_ink": "#ff6d79",
|
||||
"icons.agent": "#c78bff",
|
||||
"continuity": [
|
||||
"#4fd681",
|
||||
"#4da7ff",
|
||||
"#ffd166",
|
||||
"#c78bff"
|
||||
],
|
||||
"origin_plane_edge": "#ffb16694",
|
||||
"sheet_soft": "#283a3e"
|
||||
},
|
||||
"icon_palette": {
|
||||
"ink": "#1b1d1f",
|
||||
"line": "#d7d9d8",
|
||||
"passive-1": "#d3d8dc",
|
||||
"passive-2": "#b2bbc2",
|
||||
"passive-3": "#929fa9",
|
||||
"guide": "#b4c8d6",
|
||||
"solid": [
|
||||
"#82c1ff",
|
||||
"#62b2ff",
|
||||
"#4da7ff"
|
||||
],
|
||||
"surface": [
|
||||
"#ffc894",
|
||||
"#ffba78",
|
||||
"#ffb166"
|
||||
],
|
||||
"mesh": [
|
||||
"#ffdf94",
|
||||
"#ffd778",
|
||||
"#ffd166"
|
||||
],
|
||||
"form": [
|
||||
"#d8aeff",
|
||||
"#ce99ff",
|
||||
"#c78bff"
|
||||
],
|
||||
"sketch": [
|
||||
"#84e2a7",
|
||||
"#64db90",
|
||||
"#4fd681"
|
||||
],
|
||||
"constraint": [
|
||||
"#ff99a1",
|
||||
"#ff7f89",
|
||||
"#ff6d79"
|
||||
],
|
||||
"construction": [
|
||||
"#ffc894",
|
||||
"#ffba78",
|
||||
"#ffb166"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"tabs": {
|
||||
"MODEL": {
|
||||
"viewport_top": "#24292d",
|
||||
"viewport_bottom": "#1b2023",
|
||||
"panel": "#303438"
|
||||
},
|
||||
"SURFACE": {
|
||||
"viewport_top": "#292622",
|
||||
"viewport_bottom": "#211e1b",
|
||||
"panel": "#36332f"
|
||||
},
|
||||
"MESH": {
|
||||
"viewport_top": "#282820",
|
||||
"viewport_bottom": "#202019",
|
||||
"panel": "#35352e"
|
||||
},
|
||||
"FORM": {
|
||||
"viewport_top": "#26232b",
|
||||
"viewport_bottom": "#1e1b22",
|
||||
"panel": "#343137"
|
||||
},
|
||||
"SHEET METAL": {
|
||||
"viewport_top": "#22282a",
|
||||
"viewport_bottom": "#1b2022",
|
||||
"panel": "#303638"
|
||||
},
|
||||
"RENDER": {
|
||||
"viewport_top": "#2b2423",
|
||||
"viewport_bottom": "#221c1b",
|
||||
"panel": "#373130"
|
||||
},
|
||||
"SKETCH": {
|
||||
"viewport_top": "#252921",
|
||||
"viewport_bottom": "#1d211a",
|
||||
"panel": "#33362f"
|
||||
}
|
||||
},
|
||||
"borders": {
|
||||
"primary_inset": {
|
||||
"width": 0.5,
|
||||
"gap": 1,
|
||||
"color": "#dce2e70e"
|
||||
}
|
||||
},
|
||||
"requires": 2
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
| assets/locales/pending/agent-http-consent.json | SolidCraft contributors | Original endpoint-specific HTTP consent messages and translations | MIT OR Apache-2.0 |
|
||||
@@ -2,6 +2,46 @@
|
||||
use crate::{DocError, Document, Mat, ModelBody, ModelState, Result};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// A profile moved into the component receiving its own body is read through the recorded
|
||||
/// transfer frame. Reading its live placement would move the source geometry when that
|
||||
/// component moves, then move the transferred body a second time. All outputs of a constructor
|
||||
/// must reach the sketch's component through the same frame; independent cross-component
|
||||
/// references retain their ordinary live placement.
|
||||
pub(crate) fn profile_frame(doc: &Document, feature: &crate::Feature, sketch: u64) -> Option<Mat> {
|
||||
if !matches!(
|
||||
feature.kind,
|
||||
crate::FeatureKind::Extrude { operation: crate::Operation::NewBody, .. }
|
||||
| crate::FeatureKind::Revolve { operation: crate::Operation::NewBody, .. }
|
||||
) || feature.body_names.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let owner = doc.feature(sketch)?.component;
|
||||
if owner == feature.component {
|
||||
return None;
|
||||
}
|
||||
let mut frames: std::collections::BTreeMap<&str, (u64, Mat)> =
|
||||
feature.body_names.iter().map(|name| (name.as_str(), (feature.component, crate::IDENTITY))).collect();
|
||||
let start = doc.feature_index(feature.id)?.saturating_add(1);
|
||||
for step in doc.features.iter().take(doc.marker.unwrap_or(usize::MAX)).skip(start).filter(|step| !step.suppressed) {
|
||||
let crate::FeatureKind::BodyTransfer { bodies, from, to, transform } = &step.kind else { continue };
|
||||
for name in bodies {
|
||||
if let Some((component, frame)) = frames.get_mut(name.as_ref()) {
|
||||
if *component != *from || !valid_transfer_matrix(transform) {
|
||||
return None;
|
||||
}
|
||||
*component = *to;
|
||||
*frame = crate::mat_mul(transform, frame);
|
||||
}
|
||||
}
|
||||
if frames.values().all(|(component, _)| *component == owner) {
|
||||
let (_, first) = frames.values().next()?;
|
||||
return frames.values().all(|(_, frame)| frame == first).then(|| crate::mat_inverse(first)).flatten();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn valid_transfer_matrix(m: &Mat) -> bool {
|
||||
crate::is_placement(m)
|
||||
}
|
||||
|
||||
@@ -393,6 +393,8 @@ pub(crate) struct ExactSketchCurves<'a> {
|
||||
/// Each analytic curve's id, whether it is projected (linked) reference geometry, and its
|
||||
/// segments.
|
||||
segments: Vec<(&'a str, bool, Vec<Seg2>)>,
|
||||
/// Each free-form curve's id (ellipses, conics, splines) and its exact segments.
|
||||
free_form: Vec<(&'a str, Vec<Seg2>)>,
|
||||
}
|
||||
|
||||
impl<'a> ExactSketchCurves<'a> {
|
||||
@@ -400,10 +402,17 @@ impl<'a> ExactSketchCurves<'a> {
|
||||
/// and segment allocation must not be repeated for every generated face.
|
||||
pub(crate) fn new(sketch: &'a SolvedSketch) -> Self {
|
||||
let mut segments = Vec::new();
|
||||
let mut free_form = Vec::new();
|
||||
if sketch.sketch.curves.len() > 2000 {
|
||||
return Self { segments };
|
||||
return Self { segments, free_form };
|
||||
}
|
||||
for (index, curve) in sketch.sketch.curves.iter().enumerate() {
|
||||
if !curve.construction
|
||||
&& let Some(exact) = sketch.sketch.exact_segs(index)
|
||||
{
|
||||
free_form.push((curve.id.as_str(), exact));
|
||||
continue;
|
||||
}
|
||||
if curve.construction
|
||||
|| !matches!(
|
||||
curve.kind,
|
||||
@@ -425,7 +434,21 @@ impl<'a> ExactSketchCurves<'a> {
|
||||
};
|
||||
segments.push((curve.id.as_str(), curve.link.is_some(), originals));
|
||||
}
|
||||
Self { segments }
|
||||
Self { segments, free_form }
|
||||
}
|
||||
|
||||
/// The one free-form sketch curve (an ellipse, conic or spline) a generated segment is a
|
||||
/// whole piece of, either way round: one of the exact segments the curve is made of. A
|
||||
/// segment two curves share stays ambiguous.
|
||||
pub(crate) fn find_free_form(&self, generated: &Seg2) -> Option<String> {
|
||||
let reversed = generated.reversed();
|
||||
let mut found: Option<Option<&str>> = None;
|
||||
for (id, originals) in &self.free_form {
|
||||
if originals.iter().any(|original| *original == *generated || *original == reversed) {
|
||||
found = Some(if found.is_some() { None } else { Some(*id) });
|
||||
}
|
||||
}
|
||||
found.flatten().map(str::to_owned)
|
||||
}
|
||||
|
||||
/// The one sketch curve a generated segment lies on. A curve drawn over projected
|
||||
|
||||
@@ -89,20 +89,45 @@ pub fn format_value(value: f64, kind: expr::Kind, unit: &str, decimals: u8, f: &
|
||||
if unit_kind != kind {
|
||||
return Err(DocError::Invalid("number display unit has a different kind".into()));
|
||||
}
|
||||
format_display(value / scale, scale, decimals, f)
|
||||
format_display(value / scale, scale, decimals, f, None)
|
||||
}
|
||||
|
||||
/// Format a unit-less number with the same validation and decimal rounding policy.
|
||||
pub fn format_number(x: f64, decimals: u8, f: &NumberFormat) -> Result<Shown> {
|
||||
format_display(x, 1.0, decimals, f)
|
||||
format_display(x, 1.0, decimals, f, None)
|
||||
}
|
||||
|
||||
fn format_display(x: f64, scale: f64, decimals: u8, f: &NumberFormat) -> Result<Shown> {
|
||||
/// Format an internal value with a rounding step in the selected display unit.
|
||||
/// The step is a shortest round-trip decimal, finite, positive and at most 1e6.
|
||||
/// Input decimal preparation precedes step rounding; requested decimal precision follows it.
|
||||
/// Both rounding operations use `f.ties`. The original value is never changed.
|
||||
/// The exact snapped display magnitude must be at most 1e12. An out-of-range snap
|
||||
/// returns [`DocError::Invalid`] before final decimal-place rounding, even if that
|
||||
/// final rounding would bring the displayed value back into range.
|
||||
pub fn format_value_with_step(value: f64, kind: expr::Kind, unit: &str, decimals: u8, step: f64, f: &NumberFormat) -> Result<Shown> {
|
||||
let (unit_kind, scale) = expr::unit_info(unit).ok_or_else(|| DocError::Invalid("unknown number display unit".into()))?;
|
||||
if unit_kind != kind {
|
||||
return Err(DocError::Invalid("number display unit has a different kind".into()));
|
||||
}
|
||||
format_display(value / scale, scale, decimals, f, Some(step))
|
||||
}
|
||||
|
||||
/// Format a unit-less number with the step policy of [`format_value_with_step`].
|
||||
/// Decimal precision can round a step multiple again; `Shown.value` always matches the text.
|
||||
/// Out-of-range snapped magnitudes are rejected before final decimal-place rounding.
|
||||
pub fn format_number_with_step(x: f64, decimals: u8, step: f64, f: &NumberFormat) -> Result<Shown> {
|
||||
format_display(x, 1.0, decimals, f, Some(step))
|
||||
}
|
||||
|
||||
fn format_display(x: f64, scale: f64, decimals: u8, f: &NumberFormat, step: Option<f64>) -> Result<Shown> {
|
||||
// The unit table supplies a positive finite scale. Its decimal step must be at most
|
||||
// 1e-9 internal units; the precision floor can make the effective tie window narrower.
|
||||
let unit_resolution = (scale.log10() + 9.0).ceil().max(0.0) as usize;
|
||||
let resolution = unit_resolution.max(usize::from(decimals) + 3);
|
||||
let (rounded, value) = round_decimal(x, decimals, resolution, f.ties)?;
|
||||
let (rounded, value) = match step {
|
||||
Some(step) => round_decimal_step(x, step, decimals, resolution, f.ties)?,
|
||||
None => round_decimal(x, decimals, resolution, f.ties)?,
|
||||
};
|
||||
let (integer, fraction) = rounded.split_once('.').unwrap_or((&rounded, ""));
|
||||
let fraction = if f.trailing_zeros { fraction } else { fraction.trim_end_matches('0') };
|
||||
let integer = if !f.leading_zero && integer == "0" && !fraction.is_empty() { "" } else { integer };
|
||||
@@ -121,16 +146,25 @@ fn format_display(x: f64, scale: f64, decimals: u8, f: &NumberFormat) -> Result<
|
||||
Ok(Shown { text, value })
|
||||
}
|
||||
|
||||
/// Prepare a bounded magnitude, then round decimal digits, never a binary tie comparison.
|
||||
fn round_decimal(x: f64, decimals: u8, resolution: usize, ties: Ties) -> Result<(String, f64)> {
|
||||
/// Validate and prepare the magnitude once, identically for both rounding paths.
|
||||
fn prepare_decimal(x: f64, decimals: u8, resolution: usize) -> Result<String> {
|
||||
if decimals > 8 {
|
||||
return Err(DocError::Invalid("number precision must be at most 8 decimal places".into()));
|
||||
}
|
||||
if !x.is_finite() || x.abs() > 1e12 {
|
||||
return Err(DocError::Invalid("display value must be finite with magnitude at most 1e12".into()));
|
||||
}
|
||||
let prepared = format!("{:.*}", resolution, x.abs());
|
||||
let (integer, fraction) = prepared.split_once('.').unwrap_or((&prepared, ""));
|
||||
Ok(format!("{:.*}", resolution, x.abs()))
|
||||
}
|
||||
|
||||
/// Prepare a bounded magnitude, then round decimal digits, never a binary tie comparison.
|
||||
fn round_decimal(x: f64, decimals: u8, resolution: usize, ties: Ties) -> Result<(String, f64)> {
|
||||
let prepared = prepare_decimal(x, decimals, resolution)?;
|
||||
round_prepared(&prepared, x.is_sign_negative(), decimals, ties)
|
||||
}
|
||||
|
||||
fn round_prepared(prepared: &str, negative: bool, decimals: u8, ties: Ties) -> Result<(String, f64)> {
|
||||
let (integer, fraction) = prepared.split_once('.').unwrap_or((prepared, ""));
|
||||
let places = usize::from(decimals);
|
||||
let mut digits: Vec<u8> = integer.bytes().chain(fraction.bytes().take(places)).collect();
|
||||
let discarded = fraction.as_bytes().get(places..).unwrap_or_default();
|
||||
@@ -166,10 +200,81 @@ fn round_decimal(x: f64, decimals: u8, resolution: usize, ties: Ties) -> Result<
|
||||
}
|
||||
let rounded = String::from_utf8(digits).map_err(|_| DocError::Invalid("invalid prepared digits".into()))?;
|
||||
let magnitude = rounded.parse::<f64>().map_err(|_| DocError::Invalid("invalid rounded number".into()))?;
|
||||
let negative = x.is_sign_negative() && magnitude != 0.0;
|
||||
let negative = negative && magnitude != 0.0;
|
||||
Ok((rounded, if negative { -magnitude } else { magnitude }))
|
||||
}
|
||||
|
||||
/// Divide decimal digits by a small decimal coefficient, retaining the full quotient.
|
||||
/// A shortest f64 coefficient has at most 17 significant digits. With the unit table's
|
||||
/// resolution <= 15, a scaled divisor is <= 1e21, so remainder * 10 fits u128.
|
||||
/// At most 324 appended zeros are needed, even for the smallest positive subnormal.
|
||||
fn round_decimal_step(x: f64, step: f64, decimals: u8, resolution: usize, ties: Ties) -> Result<(String, f64)> {
|
||||
if !step.is_finite() || step <= 0.0 || step > 1e6 {
|
||||
return Err(DocError::Invalid("number rounding step must be finite, positive and at most 1e6".into()));
|
||||
}
|
||||
let prepared = prepare_decimal(x, decimals, resolution)?;
|
||||
let scientific = format!("{step:e}");
|
||||
let (mantissa, exponent) = scientific.split_once('e').ok_or_else(|| DocError::Invalid("invalid decimal step".into()))?;
|
||||
let fraction = mantissa.split_once('.').map_or(0, |(_, f)| f.len());
|
||||
let coefficient = mantissa.replace('.', "").parse::<u128>().map_err(|_| DocError::Invalid("invalid step coefficient".into()))?;
|
||||
let exponent = exponent.parse::<i32>().map_err(|_| DocError::Invalid("invalid step exponent".into()))? - fraction as i32;
|
||||
let shift = exponent + resolution as i32;
|
||||
let divisor = if shift > 0 { 10_u128.checked_pow(shift as u32).and_then(|n| coefficient.checked_mul(n)) } else { Some(coefficient) }
|
||||
.ok_or_else(|| DocError::Invalid("rounding step scale overflow".into()))?;
|
||||
let mut remainder = 0_u128;
|
||||
let mut quotient = Vec::new();
|
||||
for digit in prepared.bytes().filter(|d| *d != b'.').chain(std::iter::repeat_n(b'0', (-shift).max(0) as usize)) {
|
||||
remainder = remainder * 10 + u128::from(digit - b'0');
|
||||
quotient.push((remainder / divisor) as u8);
|
||||
remainder %= divisor;
|
||||
}
|
||||
let up = remainder * 2 > divisor
|
||||
|| (remainder * 2 == divisor && (ties == Ties::HalfAwayFromZero || quotient.last().is_some_and(|d| !d.is_multiple_of(2))));
|
||||
// Exact quotient * coefficient, with the rounding increment folded into the carry.
|
||||
let mut carry = if up { coefficient } else { 0 };
|
||||
let mut product = Vec::new();
|
||||
for digit in quotient.iter().rev() {
|
||||
let n = u128::from(*digit) * coefficient + carry;
|
||||
product.push(b'0' + (n % 10) as u8);
|
||||
carry = n / 10;
|
||||
}
|
||||
while carry > 0 {
|
||||
product.push(b'0' + (carry % 10) as u8);
|
||||
carry /= 10;
|
||||
}
|
||||
product.reverse();
|
||||
let product = String::from_utf8(product).map_err(|_| DocError::Invalid("invalid step digits".into()))?;
|
||||
let mut magnitude = product.trim_start_matches('0').to_string();
|
||||
if magnitude.is_empty() {
|
||||
magnitude.push('0');
|
||||
}
|
||||
if exponent >= 0 {
|
||||
if magnitude != "0" {
|
||||
magnitude.extend(std::iter::repeat_n('0', exponent as usize));
|
||||
}
|
||||
} else {
|
||||
let places = (-exponent) as usize;
|
||||
if magnitude.len() <= places {
|
||||
magnitude = format!("{}{}", "0".repeat(places + 1 - magnitude.len()), magnitude);
|
||||
}
|
||||
magnitude.insert(magnitude.len() - places, '.');
|
||||
}
|
||||
let (integer, fraction) = magnitude.split_once('.').unwrap_or((&magnitude, ""));
|
||||
let whole = integer.parse::<u64>().map_err(|_| DocError::Invalid("rounded display magnitude overflow".into()))?;
|
||||
if whole > 1_000_000_000_000 || (whole == 1_000_000_000_000 && fraction.bytes().any(|d| d != b'0')) {
|
||||
return Err(DocError::Invalid("rounded display magnitude must be at most 1e12".into()));
|
||||
}
|
||||
// round_prepared needs at least the requested fraction digits, including for integer steps.
|
||||
let missing = usize::from(decimals).saturating_sub(fraction.len());
|
||||
if missing > 0 {
|
||||
if !magnitude.contains('.') {
|
||||
magnitude.push('.');
|
||||
}
|
||||
magnitude.extend(std::iter::repeat_n('0', missing));
|
||||
}
|
||||
round_prepared(&magnitude, x.is_sign_negative(), decimals, ties)
|
||||
}
|
||||
|
||||
fn grouped_digits(text: &mut String, digits: &str, integer: bool, groups: bool) {
|
||||
for (i, digit) in digits.chars().enumerate() {
|
||||
if groups && digits.len() >= 5 && i > 0 && if integer { (digits.len() - i).is_multiple_of(3) } else { i.is_multiple_of(3) } {
|
||||
@@ -471,6 +576,214 @@ mod number_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_ties_and_decimal_fractions() {
|
||||
for (x, step, dp, even, away) in [
|
||||
(1.25, 0.5, 1, "1.0", "1.5"),
|
||||
(1.75, 0.5, 1, "2.0", "2.0"),
|
||||
(12.5, 5.0, 0, "10", "15"),
|
||||
(25.0, 10.0, 0, "20", "30"),
|
||||
(0.125, 0.05, 2, "0.10", "0.15"),
|
||||
(0.375, 0.25, 2, "0.50", "0.50"),
|
||||
(0.15, 0.1, 1, "0.2", "0.2"),
|
||||
(0.25, 0.1, 1, "0.2", "0.3"),
|
||||
(1.25, 0.5, 0, "1", "2"),
|
||||
] {
|
||||
for (ties, expected) in [(Ties::HalfEven, even), (Ties::HalfAwayFromZero, away)] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
for sign in [1.0, -1.0] {
|
||||
let shown = format_number_with_step(sign * x, dp, step, &f).unwrap();
|
||||
assert_eq!(shown.text, if sign < 0.0 { format!("−{expected}") } else { expected.into() });
|
||||
assert_eq!(shown.value, sign * expected.parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_prepares_display_before_division_and_keeps_boundary_neighbors() {
|
||||
for ties in [Ties::HalfEven, Ties::HalfAwayFromZero] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
for x in [1.75_f64.next_down(), 1.75, 1.75_f64.next_up(), 1.7499999996] {
|
||||
assert_eq!(format_number_with_step(x, 1, 0.5, &f).unwrap().text, "2.0");
|
||||
}
|
||||
for x in [1.25_f64.next_down(), 1.25, 1.25_f64.next_up()] {
|
||||
let expected = if ties == Ties::HalfEven { "1.0" } else { "1.5" };
|
||||
assert_eq!(format_number_with_step(x, 1, 0.5, &f).unwrap().text, expected);
|
||||
assert_eq!(format_number_with_step(-x, 1, 0.5, &f).unwrap().text, format!("−{expected}"));
|
||||
}
|
||||
assert_eq!(format_number_with_step(1.7499999994, 1, 0.5, &f).unwrap().text, "1.5");
|
||||
assert_eq!(format_number_with_step(1.7500000006, 1, 0.5, &f).unwrap().text, "2.0");
|
||||
}
|
||||
let f = NumberFormat::default();
|
||||
// The precision floor narrows the preparation window at eight decimals.
|
||||
assert_eq!(format_number_with_step(1.7499999996, 8, 0.5, &f).unwrap().text, "1.50000000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_tiny_values_large_magnitudes_and_exact_final_digits() {
|
||||
let f = NumberFormat::default();
|
||||
for step in [f64::from_bits(1), f64::MIN_POSITIVE, 1e-200, 1e-20, 1e-8] {
|
||||
for x in [0.0, -0.0, f64::from_bits(1), -1e11, 1e11, 34947628153.461784] {
|
||||
let shown = format_number_with_step(x, 5, step, &f).unwrap();
|
||||
assert!(shown.value.is_finite());
|
||||
assert_eq!(shown.value, shown.text.replace('−', "-").parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
assert_eq!(format_number_with_step(1e12, 8, f64::from_bits(1), &f).unwrap().text, "1000000000000.00000000");
|
||||
assert_eq!(format_number_with_step(34947628153.461784, 5, 1e-8, &f).unwrap().text, "34947628153.46178");
|
||||
// A tiny step can move an exact decimal tie by less than any f64 ULP.
|
||||
// Keep those digits through the second rounding rather than parsing the snap to f64.
|
||||
assert_eq!(format_number_with_step(1.25, 1, 3e-323, &f).unwrap().text, "1.3");
|
||||
assert_eq!(format_number_with_step(-1.25, 1, 3e-323, &f).unwrap().text, "−1.3");
|
||||
for step in [0.5, 10.0, 1e6] {
|
||||
for x in [-0.0, -0.1] {
|
||||
let shown = format_number_with_step(x, 2, step, &f).unwrap();
|
||||
assert_eq!(shown.text, "0.00");
|
||||
assert_eq!(shown.value.to_bits(), 0.0_f64.to_bits());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_extreme_coefficients_have_independent_expected_digits() {
|
||||
// Shortest decimals: largest subnormal = 2225073858507201 * 10^-323,
|
||||
// smallest normal = 22250738585072014 * 10^-324. For x=1.25, integer
|
||||
// division leaves remainders 2221948386734081 and 354296156594724.
|
||||
// Twice the first is above its divisor, twice the second below: the
|
||||
// snap is just above/below 1.25, so final rounding gives 1.3/1.2 in BOTH modes.
|
||||
// Other expected digits were calculated with independent rational arithmetic.
|
||||
for (step, quarter) in [(f64::MIN_POSITIVE.next_down(), "1.3"), (f64::MIN_POSITIVE, "1.2")] {
|
||||
for ties in [Ties::HalfEven, Ties::HalfAwayFromZero] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
for (x, dp, expected) in [
|
||||
(1.25, 1, quarter),
|
||||
(99999999999.75, 0, "100000000000"),
|
||||
(99999999999.75, 1, "99999999999.7"),
|
||||
(99999999999.75, 2, "99999999999.75"),
|
||||
(34947628153.461784, 2, "34947628153.46"),
|
||||
(34947628153.461784, 5, "34947628153.46178"),
|
||||
] {
|
||||
for sign in [1.0, -1.0] {
|
||||
let shown = format_number_with_step(sign * x, dp, step, &f).unwrap();
|
||||
let text = if sign < 0.0 { format!("−{expected}") } else { expected.into() };
|
||||
assert_eq!(shown.text, text, "x={x:?}, step={step:e}, dp={dp}, ties={ties:?}");
|
||||
assert_eq!(shown.value, sign * expected.parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_and_final_decimal_rounding_carry_independently() {
|
||||
// 9.975 / .05 = 199.5: both modes increment the step index to 200.
|
||||
// 9.96 / .03 = 332 exactly: only the final rounding carries to 10.0.
|
||||
// 1.26 / .1 = 12.6: the snap is 1.3, then zero decimals give 1 (not 1.26).
|
||||
// 1.49 / .5 = 2.98: the snap is 1.5, then zero decimals give 2 (not 1).
|
||||
for (x, step, dp, even, away) in [
|
||||
(9.975, 0.05, 2, "10.00", "10.00"),
|
||||
(9.96, 0.03, 1, "10.0", "10.0"),
|
||||
(1.26, 0.1, 0, "1", "1"),
|
||||
(1.49, 0.5, 0, "2", "2"),
|
||||
(100000000000.25, 0.1, 0, "100000000000", "100000000000"),
|
||||
(100000000000.25, 0.1, 1, "100000000000.2", "100000000000.3"),
|
||||
(100000000000.25, 0.1, 2, "100000000000.20", "100000000000.30"),
|
||||
(999999999999.75, 0.25, 0, "1000000000000", "1000000000000"),
|
||||
] {
|
||||
for (ties, expected) in [(Ties::HalfEven, even), (Ties::HalfAwayFromZero, away)] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
for sign in [1.0, -1.0] {
|
||||
let shown = format_number_with_step(sign * x, dp, step, &f).unwrap();
|
||||
assert_eq!(shown.text, if sign < 0.0 { format!("−{expected}") } else { expected.into() });
|
||||
assert_eq!(shown.value, sign * expected.parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_rejects_invalid_inputs_and_out_of_range_snap() {
|
||||
let f = NumberFormat::default();
|
||||
for step in [0.0, -0.0, -0.5, f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1e6_f64.next_up()] {
|
||||
assert!(matches!(format_number_with_step(1.0, 2, step, &f), Err(DocError::Invalid(_))));
|
||||
assert!(matches!(format_value_with_step(1.0, Length, "mm", 2, step, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
for x in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1e12_f64.next_up(), -1e12_f64.next_up()] {
|
||||
assert!(matches!(format_number_with_step(x, 2, 0.5, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
for dp in [9, 255] {
|
||||
assert!(matches!(format_number_with_step(1.0, dp, 0.5, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
for x in [1e12, -1e12] {
|
||||
for ties in [Ties::HalfEven, Ties::HalfAwayFromZero] {
|
||||
let f = NumberFormat { ties, ..f };
|
||||
for step in [600_000.0, 0.6] {
|
||||
// The .6 snap has magnitude 1e12 + .2: reject it even though
|
||||
// zero decimal places would otherwise bring it back to 1e12.
|
||||
assert!(matches!(format_number_with_step(x, 0, step, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
}
|
||||
assert_eq!(format_number_with_step(x, 0, 1e6, &f).unwrap().value, x);
|
||||
}
|
||||
for (kind, unit) in [(Length, "deg"), (Angle, "mm"), (Length, "unknown")] {
|
||||
assert!(matches!(format_value_with_step(1.0, kind, unit, 2, 0.5, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_units_options_and_input_preservation() {
|
||||
let f = NumberFormat::default();
|
||||
let input: f64 = 25.4 * 1.25;
|
||||
let bits = input.to_bits();
|
||||
assert_eq!(format_value_with_step(input, Length, "in", 1, 0.5, &f).unwrap().text, "1.0");
|
||||
assert_eq!(input.to_bits(), bits);
|
||||
assert_eq!(format_value_with_step(std::f64::consts::PI / 6.0, Angle, "deg", 1, 5.0, &f).unwrap().text, "30.0");
|
||||
assert_eq!(format_value_with_step(1e15, Length, "m", 1, 10.0, &f).unwrap().value, 1e12);
|
||||
let styled = NumberFormat { marker: DecimalMarker::Comma, groups: true, ..f };
|
||||
assert_eq!(format_number_with_step(-12345.25, 2, 0.5, &styled).unwrap().text, "−12\u{202f}345,00");
|
||||
let trim = NumberFormat { leading_zero: false, trailing_zeros: false, ..styled };
|
||||
assert_eq!(format_number_with_step(-0.6, 3, 0.5, &trim).unwrap().text, "−,5");
|
||||
assert_eq!(format_number_with_step(1.23456789, 8, 1e-8, &styled).unwrap().text, "1,234\u{202f}567\u{202f}89");
|
||||
for (distance, expected) in [(0.4, "1.0"), (0.6, "0.5")] {
|
||||
assert_eq!(format_value_with_step((0.75 - distance * 1e-4) * 1e-6, Length, "nm", 1, 0.5, &f).unwrap().text, expected);
|
||||
}
|
||||
for step in [1e6_f64.next_down(), 0.12345678901234567, 2.2250738585072014e-308] {
|
||||
let shown = format_number_with_step(12345.6789, 8, step, &f).unwrap();
|
||||
assert_eq!(shown.value, shown.text.parse::<f64>().unwrap());
|
||||
}
|
||||
// Explicitly keep both historical policies, including their different decimal ties.
|
||||
assert_eq!(format_number(2.665, 2, &f).unwrap().text, "2.66");
|
||||
assert_eq!(length_number(2.665, "mm", 2), "2.67");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_seeded_integer_reference_and_hostile_domain() {
|
||||
let mut seed = 0x4896_1138_0abc_def1;
|
||||
for _ in 0..20_000 {
|
||||
// Independent bounded integer oracle: x and step are thousandths; dp=3.
|
||||
let n = random(&mut seed) % 1_000_000;
|
||||
let s = random(&mut seed) % 100_000 + 1;
|
||||
for ties in [Ties::HalfEven, Ties::HalfAwayFromZero] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
let q = n / s;
|
||||
let r = n % s;
|
||||
let up = 2 * r > s || (2 * r == s && (ties == Ties::HalfAwayFromZero || q % 2 == 1));
|
||||
let expected = (q + u64::from(up)) * s;
|
||||
let text = format!("{}.{:03}", expected / 1000, expected % 1000);
|
||||
let shown = format_number_with_step(n as f64 / 1000.0, 3, s as f64 / 1000.0, &f).unwrap();
|
||||
assert_eq!(shown.text, text);
|
||||
// Exercise arbitrary positive subnormals and normal steps without quotient overflow.
|
||||
let step = f64::from_bits(random(&mut seed));
|
||||
if step.is_finite() && step > 0.0 && step <= 1e6 {
|
||||
let shown = format_number_with_step(n as f64, 3, step, &f).unwrap();
|
||||
assert!(shown.value.is_finite() && shown.value >= 0.0);
|
||||
assert_eq!(shown.value, shown.text.parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn random(state: &mut u64) -> u64 {
|
||||
*state ^= *state << 13;
|
||||
*state ^= *state >> 7;
|
||||
|
||||
@@ -452,8 +452,14 @@ pub enum AxisRef {
|
||||
SketchAxis { axis: String },
|
||||
/// World X / Y / Z axis through the origin.
|
||||
World { axis: String },
|
||||
/// Any line in the sketch plane, in world coordinates.
|
||||
Line { origin: Position, dir: Dir },
|
||||
/// Any line in the sketch plane, in world coordinates. The origin's coordinates are length
|
||||
/// expressions, so the axis follows parameter changes.
|
||||
Line {
|
||||
#[serde(with = "point_expr")]
|
||||
#[walk(placement)]
|
||||
origin: PointExpr,
|
||||
dir: Dir,
|
||||
},
|
||||
/// An edge or round face of the model (a straight or circular edge, a cylinder, cone or
|
||||
/// torus face), found again on each evaluation: [`crate::construct::model_axis`].
|
||||
Model { geo: GeoRef },
|
||||
@@ -734,6 +740,10 @@ pub enum FeatureKind {
|
||||
/// from the vertex instead of where the rounds meet (a constant-radius fillet only).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
setbacks: Vec<CornerSetback>,
|
||||
/// How two rounds turn where they meet at a sharp side edge: in a mitre (the default) or
|
||||
/// swept round it (a constant-radius, exact, rolling-corner fillet without setbacks only).
|
||||
#[serde(default, skip_serializing_if = "FilletTurn::is_mitre")]
|
||||
turn: FilletTurn,
|
||||
/// Round each picked edge's tangent chain with it (the default: edges continuing it
|
||||
/// smoothly at its ends, as far as they go), or only the picked edges.
|
||||
#[serde(default = "yes", skip_serializing_if = "is_yes")]
|
||||
@@ -1040,6 +1050,8 @@ pub enum FeatureKind {
|
||||
Split {
|
||||
body: BodyName,
|
||||
plane: PlaneRef,
|
||||
// The tool face is picked (and named), so the split follows it (#11674).
|
||||
#[walk(picks_faces)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
tool: Option<FaceAt>,
|
||||
/// A straight line of this sketch as the tool (#1552): the plane through it along the
|
||||
@@ -1971,6 +1983,38 @@ impl FilletCorner {
|
||||
}
|
||||
}
|
||||
|
||||
/// How two constant rounds of one radius turn round a sharp, straight, convex side edge square to
|
||||
/// both on a plane, such as the rounds along a boss's base at its upright edges.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FilletTurn {
|
||||
/// The rounds run on to where they cross and meet in a mitre.
|
||||
#[default]
|
||||
Mitre,
|
||||
/// The rounds end at the side edge and the corner is their section revolved about it, a
|
||||
/// torus tangent to both (`solidcraft_kernel::FilletTurn::Swept`).
|
||||
Swept,
|
||||
}
|
||||
|
||||
/// Why a swept turn is refused with a fillet's other options: the kernel sweeps the turn of two
|
||||
/// constant rounds of one radius, at its own corners only.
|
||||
pub const SWEPT_TAKES: &str = "a swept corner turns two constant rounds of one radius: `turn: swept` takes `type: constant`, \
|
||||
`corner: rolling`, no `setbacks` and `fit: exact` or `reduce`";
|
||||
|
||||
impl FilletTurn {
|
||||
pub fn is_mitre(&self) -> bool {
|
||||
*self == FilletTurn::Mitre
|
||||
}
|
||||
|
||||
/// The kernel's turn.
|
||||
pub fn kernel(self) -> solidcraft_kernel::FilletTurn {
|
||||
match self {
|
||||
FilletTurn::Mitre => solidcraft_kernel::FilletTurn::Mitre,
|
||||
FilletTurn::Swept => solidcraft_kernel::FilletTurn::Swept,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How Extend continues a surface past its edges.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -2374,6 +2418,12 @@ pub enum HoleKind {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[walk(angle = "Tip angle")]
|
||||
tip_angle: Option<Expr>,
|
||||
/// Printing aid: two bridge layers this thick below the counterbore's floor (a slot along
|
||||
/// X, then a square), so a printer bridges the step instead of sagging into it. Absent:
|
||||
/// a plain counterbore.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[walk(length = "Bridge layer")]
|
||||
bridge: Option<Expr>,
|
||||
},
|
||||
Countersink {
|
||||
#[walk(length = "Countersink diameter")]
|
||||
|
||||
@@ -16,14 +16,17 @@ use solidcraft_sketch::Sketch;
|
||||
use crate::eval::{PlacementReads, body_world, hash_placement, placement_digest};
|
||||
use crate::*;
|
||||
|
||||
/// Work counted on this thread: bodies moved, STEP texts digested, joint faces and bodies
|
||||
/// Work counted on this thread: bodies moved, STEP/mesh contents digested, joint faces and bodies
|
||||
/// measured, bodies searched for by name, features searched for by id, bodies a cut (or
|
||||
/// intersect) ran its booleans on, bodies searched for the cylinder a tap marking lies on, and
|
||||
/// bodies whose faces a tapped hole read for its wall.
|
||||
/// intersect) ran its booleans on, bodies searched for the cylinder a tap marking lies on,
|
||||
/// bodies whose faces a tapped hole read for its wall, intersects run to find a pattern's copies
|
||||
/// that cut nothing, and the tools a cut of many tools is made with (one when they are cut in one
|
||||
/// go).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub(super) struct Counts {
|
||||
pub moved: usize,
|
||||
pub digested: usize,
|
||||
pub mesh_digested: usize,
|
||||
pub joint_faces: usize,
|
||||
pub measures: usize,
|
||||
pub body_searches: usize,
|
||||
@@ -31,6 +34,8 @@ pub(super) struct Counts {
|
||||
pub cut: usize,
|
||||
pub cylinder_search: usize,
|
||||
pub tapped_wall: usize,
|
||||
pub miss_intersects: usize,
|
||||
pub cut_tools: usize,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -38,6 +43,7 @@ thread_local! {
|
||||
Cell::new(Counts {
|
||||
moved: 0,
|
||||
digested: 0,
|
||||
mesh_digested: 0,
|
||||
joint_faces: 0,
|
||||
measures: 0,
|
||||
body_searches: 0,
|
||||
@@ -45,6 +51,8 @@ thread_local! {
|
||||
cut: 0,
|
||||
cylinder_search: 0,
|
||||
tapped_wall: 0,
|
||||
miss_intersects: 0,
|
||||
cut_tools: 0,
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -1484,3 +1492,270 @@ fn a_hole_into_a_rim_still_bores_its_sheet_beside_far_ones() {
|
||||
let bore = std::f64::consts::PI * (d / 2.0).powi(2) * 60.0;
|
||||
assert!((removed - bore).abs() < 0.02 * bore, "removed {removed} of {bore}");
|
||||
}
|
||||
|
||||
/// A 1.5 mm disc of 50 mm radius with a 3.5 mm through hole at `radius` on X, patterned `count`
|
||||
/// times around Z (#9007). The pattern's id.
|
||||
fn disc_with_hole_pattern(doc: &mut Document, radius: f64, count: usize) -> u64 {
|
||||
let disc = FeatureKind::Cylinder {
|
||||
base: point_expr(Vec3::ZERO),
|
||||
axis: Dir(Vec3::Z),
|
||||
radius: "50".into(),
|
||||
height: "1.5".into(),
|
||||
operation: Operation::NewBody,
|
||||
references: Vec::new(),
|
||||
};
|
||||
doc.add_feature(disc, Some("Disc")).unwrap();
|
||||
let hole: FeatureKind = serde_json::from_value(serde_json::json!({
|
||||
"type": "hole", "position": [radius, 0, 1.5], "direction": [0, 0, -1], "diameter": "3.5",
|
||||
}))
|
||||
.unwrap();
|
||||
let hole = doc.add_feature(hole, Some("Hole")).unwrap();
|
||||
hole_pattern(doc, hole, count)
|
||||
}
|
||||
|
||||
fn hole_pattern(doc: &mut Document, hole: u64, count: usize) -> u64 {
|
||||
let pattern: FeatureKind = serde_json::from_value(serde_json::json!({
|
||||
"type": "pattern", "features": [hole],
|
||||
"pattern": {"type": "circular", "origin": [0, 0, 0], "axis": [0, 0, 1], "count": count.to_string(), "angle": "360 deg"},
|
||||
}))
|
||||
.unwrap();
|
||||
doc.add_feature(pattern, None).unwrap()
|
||||
}
|
||||
|
||||
fn disc_volume(model: &Model) -> f64 {
|
||||
let st = model.state();
|
||||
assert_eq!(st.bodies.len(), 1);
|
||||
kernel::measure(&st.bodies[0].body).unwrap().volume
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_circular_pattern_of_a_through_hole_cuts_its_copies_at_once() {
|
||||
// The copies' tools run twice the disc's size along Z, so the kernel's box test, which
|
||||
// grows each box by a share of the tool's size, finds them meeting (#9007); they are still
|
||||
// 4.3 mm apart, centre to centre, and are cut in one go. No copy is intersected with the
|
||||
// disc to see whether it takes material: each passes through the disc's material.
|
||||
let mut doc = Document::new("sieve");
|
||||
let pattern = disc_with_hole_pattern(&mut doc, 20.0, 29);
|
||||
let mut model = Model::new();
|
||||
let work = counted(|| model.evaluate(&doc));
|
||||
assert!(model.results.iter().all(|r| r.error.is_none() && r.warning.is_none()), "{:?}", model.results);
|
||||
assert_eq!((work.cut_tools, work.miss_intersects), (1, 0), "{work:?}");
|
||||
let hole = std::f64::consts::PI * 1.75 * 1.75 * 1.5;
|
||||
let disc = std::f64::consts::PI * 2500.0 * 1.5;
|
||||
let v = disc_volume(&model);
|
||||
assert!((v - (disc - 29.0 * hole)).abs() < 1e-3 * hole, "{v}");
|
||||
// The same copies again, landing in the holes cut already: each is intersected, and each
|
||||
// still counts as a copy that removes no material.
|
||||
let hole_id = doc.features.iter().find(|f| f.name == "Hole").unwrap().id;
|
||||
hole_pattern(&mut doc, hole_id, 29);
|
||||
let work = counted(|| model.evaluate(&doc));
|
||||
let again = model.results.last().unwrap();
|
||||
assert!(again.error.is_none(), "{again:?}");
|
||||
// (The kernel may add a note on a retry of a boolean on faces that lie on each other.)
|
||||
assert!(again.warning.as_deref().is_some_and(|w| w.starts_with("28 of 28 cut copies remove no material")), "{again:?}");
|
||||
assert_eq!(work.miss_intersects, 28, "{work:?}");
|
||||
assert!((disc_volume(&model) - v).abs() < 1e-3 * hole);
|
||||
assert!(model.results.iter().any(|r| r.id == pattern && r.warning.is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_of_a_hole_that_come_close_are_cut_one_by_one() {
|
||||
// At 8.5 mm radius, 16 copies of a 3.5 mm hole overlap their neighbours: they are not apart,
|
||||
// and each is cut on its own, as before.
|
||||
let mut doc = Document::new("crowded");
|
||||
disc_with_hole_pattern(&mut doc, 8.5, 16);
|
||||
let mut model = Model::new();
|
||||
let work = counted(|| model.evaluate(&doc));
|
||||
assert!(model.results.iter().all(|r| r.error.is_none() && r.warning.is_none()), "{:?}", model.results);
|
||||
assert_eq!((work.cut_tools, work.miss_intersects), (15, 0), "{work:?}");
|
||||
// At 9 mm they come within 0.012 mm of each other without touching: their exact shapes are
|
||||
// apart, so they are cut in one go, and all 16 holes are cut.
|
||||
let mut doc = Document::new("close");
|
||||
disc_with_hole_pattern(&mut doc, 9.0, 16);
|
||||
let mut model = Model::new();
|
||||
let work = counted(|| model.evaluate(&doc));
|
||||
assert!(model.results.iter().all(|r| r.error.is_none() && r.warning.is_none()), "{:?}", model.results);
|
||||
assert_eq!((work.cut_tools, work.miss_intersects), (1, 0), "{work:?}");
|
||||
let hole = std::f64::consts::PI * 1.75 * 1.75 * 1.5;
|
||||
let disc = std::f64::consts::PI * 2500.0 * 1.5;
|
||||
let v = disc_volume(&model);
|
||||
assert!((v - (disc - 16.0 * hole)).abs() < 1e-3 * hole, "{v}");
|
||||
}
|
||||
|
||||
/// A ring (25 mm outside, 16 mm bore, 55 mm high) with a blind groove, 8 mm wide and 30 mm deep,
|
||||
/// cut into its bore from below, in a circular pattern of ten about Z: their boxes, grown as the
|
||||
/// kernel grows them, meet, their exact shapes are 1.9 mm apart. The ring's faces counted as its
|
||||
/// surfaces are, and the work.
|
||||
fn grooved_ring_faces() -> (usize, Counts) {
|
||||
let mut doc = Document::new("grooves");
|
||||
let cylinder = |base: Vec3, radius: &str, height: &str, operation: Operation| FeatureKind::Cylinder {
|
||||
base: point_expr(base),
|
||||
axis: Dir(Vec3::Z),
|
||||
radius: radius.into(),
|
||||
height: height.into(),
|
||||
operation,
|
||||
references: Vec::new(),
|
||||
};
|
||||
doc.add_feature(cylinder(Vec3::ZERO, "25", "55", Operation::NewBody), Some("Ring")).unwrap();
|
||||
doc.add_feature(cylinder(Vec3::new(0.0, 0.0, -1.0), "16", "57", Operation::Cut), Some("Bore")).unwrap();
|
||||
let groove = doc.add_feature(cylinder(Vec3::new(16.0, 0.0, -1.0), "4", "31", Operation::Cut), Some("Groove")).unwrap();
|
||||
hole_pattern(&mut doc, groove, 10);
|
||||
let mut model = Model::new();
|
||||
let work = counted(|| model.evaluate(&doc));
|
||||
assert!(model.results.iter().all(|r| r.error.is_none() && r.warning.is_none()), "{:?}", model.results);
|
||||
let st = model.state();
|
||||
assert_eq!(st.bodies.len(), 1);
|
||||
(kernel::measure(&st.bodies[0].body).unwrap().merged.faces, work)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_that_notch_the_same_round_face_cut_at_once_leave_the_faces_of_one_by_one() {
|
||||
// The ten grooves notch the same round bore. Cut all in one go, they leave it in as many
|
||||
// faces as cutting them one after another does (#9007).
|
||||
let (patterned, work) = grooved_ring_faces();
|
||||
let cut = |a: kernel::Body, b: &kernel::Body| kernel::boolean(&a, b, kernel::BoolOp::Cut).unwrap().unwrap();
|
||||
let ring =
|
||||
cut(kernel::cylinder(Vec3::ZERO, Vec3::Z, 25.0, 55.0).unwrap(), &kernel::cylinder(Vec3::new(0.0, 0.0, -1.0), Vec3::Z, 16.0, 57.0).unwrap());
|
||||
let groove = kernel::cylinder(Vec3::new(16.0, 0.0, -1.0), Vec3::Z, 4.0, 31.0).unwrap();
|
||||
let one_by_one = (0..10).fold(ring, |ring, k| {
|
||||
let copy = kernel::transform(&groove, Vec3::ZERO, Vec3::ZERO, Vec3::Z, (36.0 * k as f64).to_radians()).unwrap();
|
||||
cut(ring, ©)
|
||||
});
|
||||
assert_eq!(patterned, kernel::measure(&one_by_one).unwrap().merged.faces);
|
||||
assert_eq!(work.cut_tools, 1, "{work:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tools_are_apart_only_when_they_neither_meet_nor_nest() {
|
||||
let rod = |x: f64, y: f64, r: f64| kernel::cylinder(Vec3::new(x, y, -100.0), Vec3::Z, r, 200.0).unwrap();
|
||||
// Long rods whose boxes, grown as the kernel grows them, meet: 0.5 mm apart.
|
||||
assert!(super::tools_clearly_apart(&[rod(0.0, 0.0, 1.0), rod(2.5, 0.0, 1.0)]));
|
||||
// Touching, crossing and nested rods are not apart.
|
||||
assert!(!super::tools_clearly_apart(&[rod(0.0, 0.0, 1.0), rod(2.0, 0.0, 1.0)]));
|
||||
assert!(!super::tools_clearly_apart(&[rod(0.0, 0.0, 1.0), rod(1.5, 0.0, 1.0)]));
|
||||
assert!(!super::tools_clearly_apart(&[rod(0.0, 0.0, 3.0), rod(0.5, 0.0, 1.0)]));
|
||||
let tube = kernel::boolean(&rod(0.0, 0.0, 5.0), &rod(0.0, 0.0, 4.0), kernel::BoolOp::Cut).unwrap().unwrap();
|
||||
assert!(!super::tools_clearly_apart(&[tube.clone(), rod(0.0, 0.0, 4.5)]));
|
||||
// A rod in the tube's bore, clear of its wall, is apart from it.
|
||||
assert!(super::tools_clearly_apart(&[tube, rod(0.0, 0.0, 3.0)]));
|
||||
}
|
||||
|
||||
fn imported_meshes() -> Document {
|
||||
let mut doc = Document::new("reference meshes");
|
||||
let comp = doc.add_component(Some("Part"), 0).unwrap();
|
||||
let meshes = (0..2)
|
||||
.map(|i| MeshData {
|
||||
name: format!("Mesh{i}"),
|
||||
positions: [0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0].into(),
|
||||
triangles: [0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3].into(),
|
||||
color: Some([0.2, 0.4, 0.6]),
|
||||
})
|
||||
.collect();
|
||||
let id = doc.add_feature(FeatureKind::MeshImport { file: "tetrahedra.stl".into(), meshes }, Some("Meshes")).unwrap();
|
||||
doc.feature_mut(id).unwrap().component = comp;
|
||||
doc
|
||||
}
|
||||
|
||||
fn meshes_mut(doc: &mut Document) -> &mut Vec<MeshData> {
|
||||
let FeatureKind::MeshImport { meshes, .. } = &mut doc.features[0].kind else { panic!("mesh fixture") };
|
||||
meshes
|
||||
}
|
||||
|
||||
fn same_as_fresh(doc: &Document, model: &Model) {
|
||||
let mut fresh = Model::new();
|
||||
fresh.no_cache = true;
|
||||
fresh.evaluate(doc);
|
||||
assert_eq!(model.results.iter().map(|r| r.fingerprint).collect::<Vec<_>>(), fresh.results.iter().map(|r| r.fingerprint).collect::<Vec<_>>());
|
||||
let (actual, expected) = (model.state(), fresh.state());
|
||||
assert_eq!(actual.bodies.len(), expected.bodies.len());
|
||||
for (a, b) in actual.bodies.iter().zip(&expected.bodies) {
|
||||
assert_eq!(a.name, b.name);
|
||||
assert_eq!(a.mesh().positions, b.mesh().positions);
|
||||
assert_eq!(a.mesh().triangles, b.mesh().triangles);
|
||||
assert_eq!(a.body.color(), b.body.color());
|
||||
}
|
||||
assert!(model.results.iter().all(|r| r.error.is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_mesh_payloads_are_not_read_again_for_rigid_motion() {
|
||||
let mut doc = imported_meshes();
|
||||
let mut model = Model::new();
|
||||
assert_eq!(counted(|| model.evaluate(&doc)).mesh_digested, 2);
|
||||
for k in 1..5 {
|
||||
doc = doc.clone(); // Undo/preview copies share the arrays, not the document itself.
|
||||
doc.occurrences[0].transform = rigid(Vec3::new(f64::from(k), 2.0, 3.0), Vec3::ZERO, Vec3::Z, 0.1 * f64::from(k));
|
||||
assert_eq!(
|
||||
counted(|| {
|
||||
model.evaluate(&doc);
|
||||
assert!(model.is_current(&doc));
|
||||
})
|
||||
.mesh_digested,
|
||||
0
|
||||
);
|
||||
assert_eq!(model.last_recomputed, 0);
|
||||
same_as_fresh(&doc, &model);
|
||||
}
|
||||
let saved = serde_json::to_string(&doc).unwrap();
|
||||
let reopened: Document = serde_json::from_str(&saved).unwrap();
|
||||
assert_eq!(counted(|| model.evaluate(&reopened)).mesh_digested, 2, "new array identities must be checked");
|
||||
assert_eq!(model.last_recomputed, 0, "equal content has equal fingerprints");
|
||||
same_as_fresh(&reopened, &model);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_payload_edits_invalidate_the_cache_and_undo_restores_the_geometry() {
|
||||
let mut doc = imported_meshes();
|
||||
let original = doc.clone();
|
||||
let mut model = Model::new();
|
||||
model.evaluate(&doc);
|
||||
let fingerprint = model.results[0].fingerprint;
|
||||
Arc::make_mut(&mut meshes_mut(&mut doc)[0].positions)[3] = 5.0;
|
||||
assert!(!model.is_current(&doc));
|
||||
assert_eq!(counted(|| model.evaluate(&doc)).mesh_digested, 1);
|
||||
assert_ne!(model.results[0].fingerprint, fingerprint);
|
||||
same_as_fresh(&doc, &model);
|
||||
let edited = doc.clone();
|
||||
Arc::make_mut(&mut meshes_mut(&mut doc)[0].triangles).swap(0, 1);
|
||||
assert_eq!(counted(|| model.evaluate(&doc)).mesh_digested, 1);
|
||||
same_as_fresh(&doc, &model);
|
||||
for restored in [&original, &edited, &original] {
|
||||
model.evaluate(restored);
|
||||
same_as_fresh(restored, &model);
|
||||
}
|
||||
assert_eq!(model.results[0].fingerprint, fingerprint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_metadata_and_order_remain_part_of_the_fingerprint() {
|
||||
let mut doc = imported_meshes();
|
||||
let mut model = Model::new();
|
||||
model.evaluate(&doc);
|
||||
for edit in 0..7 {
|
||||
let before = model.results[0].fingerprint;
|
||||
match edit {
|
||||
0 => meshes_mut(&mut doc)[0].name = "Renamed".into(),
|
||||
1 => meshes_mut(&mut doc)[0].color = Some([0.8, 0.1, 0.3]),
|
||||
2 => doc.features[0].name = "Renamed feature".into(),
|
||||
3 => meshes_mut(&mut doc).swap(0, 1),
|
||||
4 => {
|
||||
meshes_mut(&mut doc).pop();
|
||||
}
|
||||
5 => {
|
||||
let mesh = meshes_mut(&mut doc)[0].clone();
|
||||
meshes_mut(&mut doc).push(MeshData { name: "Added".into(), ..mesh });
|
||||
}
|
||||
_ => {
|
||||
let FeatureKind::MeshImport { file, .. } = &mut doc.features[0].kind else { panic!("mesh fixture") };
|
||||
*file = "renamed.stl".into();
|
||||
}
|
||||
}
|
||||
assert!(!model.is_current(&doc));
|
||||
let work = counted(|| model.evaluate(&doc));
|
||||
if edit < 3 || edit == 6 {
|
||||
assert_eq!(work.mesh_digested, 0, "metadata alone: {edit}");
|
||||
}
|
||||
assert_ne!(model.results[0].fingerprint, before, "edit {edit}");
|
||||
same_as_fresh(&doc, &model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2318,11 +2318,16 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
_ => false,
|
||||
};
|
||||
let (Some(_), Some(_), false) = (outer, openings.first(), alike) else {
|
||||
// A solid box top has no opening: Shell that face first, then pick the rim
|
||||
// around the cavity (#10437).
|
||||
let what = match (loops.len(), openings.len()) {
|
||||
(0 | 1, _) => "pick the rim face of a shelled body (a face with an inner and an outer edge)".to_string(),
|
||||
(_, 0) => "pick the rim face of a shelled body: this face's inner edges are where other features stand on it, not an opening"
|
||||
(0 | 1, _) => "Lip needs the rim of a shelled body: Shell a face open first, then pick the rim around that opening (a face with an outer edge and an inner hole)"
|
||||
.to_string(),
|
||||
(_, k) => format!("pick the rim face of a shelled body (round one opening): this face has {k} openings alike in size"),
|
||||
(_, 0) => "Lip needs a rim around an opening: this face's inner edges are where other features stand on it, not a shell cavity. Shell a face open, then pick that rim"
|
||||
.to_string(),
|
||||
(_, k) => format!(
|
||||
"Lip needs one clear opening as the rim: this face has {k} openings alike in size (for example a plate of holes). Shell one cavity, or pick a rim that rings a single opening"
|
||||
),
|
||||
};
|
||||
return Err(DocError::Invalid(what));
|
||||
};
|
||||
|
||||
@@ -63,13 +63,16 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
let t = val(vals, thickness, Kind::Length)?;
|
||||
let mut tools = Vec::new();
|
||||
for n in bodies {
|
||||
let b = st.body(n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
|
||||
tools.push(kernel::thicken(&b.body, t, *symmetric)?);
|
||||
let b = st.body(n).cloned().ok_or_else(|| DocError::Unknown(format!("body `{n}`")))?;
|
||||
// Kept face, offset and edge walls. Carry that history so a sketch can attach
|
||||
// to the solid's faces and follow the thickness (#7641).
|
||||
let thickened = kernel::thicken(&b.body, t, *symmetric)?;
|
||||
tools.push(Traced::step(thickened, &[&b.traced()]));
|
||||
}
|
||||
if tools.is_empty() {
|
||||
return Err(DocError::Invalid("pick the surfaces to thicken".into()));
|
||||
}
|
||||
apply_op(doc, st, f, tools, *operation, &crate::plain(targets))
|
||||
apply_traced(doc, st, f, tools, *operation, &crate::plain(targets))
|
||||
}
|
||||
FeatureKind::SurfaceTrim { body, plane, tool, keep, keep_more, curves, with, keep_with, stitch, extend } => {
|
||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
@@ -308,7 +311,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f.component,
|
||||
)?;
|
||||
let (o, d) = revolve_axis(&ss, axis, &*super::axis_state(doc, st, f.component, axis)?, &|name| {
|
||||
let (o, d) = revolve_axis(vals, &ss, axis, &*super::axis_state(doc, st, f.component, axis)?, &|name| {
|
||||
construction_axis_in(Some(doc), st, name, f.component)
|
||||
})?;
|
||||
let a = val(vals, angle, Kind::Angle)?;
|
||||
|
||||
@@ -103,6 +103,11 @@ enum End {
|
||||
/// At (or within half a pitch of) an end of the face where material goes on past it, a
|
||||
/// shoulder, a head, a blind hole's bottom: the groove stops short, clear of it.
|
||||
Closed,
|
||||
/// At an end of an external thread's face where a shoulder starts and stays beyond the face's
|
||||
/// end: the groove runs on through the plane of that end and is trimmed there, so the root
|
||||
/// reaches the shoulder at every angle, a nut bottoms out on it, and nothing past the plane
|
||||
/// is cut.
|
||||
Shoulder,
|
||||
/// Inside the face: the groove stops square there.
|
||||
Inside,
|
||||
}
|
||||
@@ -121,6 +126,9 @@ pub(super) struct ThreadOn {
|
||||
/// A modelled internal thread in a hole wider than it needs: the radius the bore is narrowed
|
||||
/// to (the thread's minor radius) before the groove is cut, with the hole's own radius.
|
||||
narrow: Option<f64>,
|
||||
/// A shoulder keeps the square stop because the kernel couldn't check whether it stays beyond
|
||||
/// the face's end.
|
||||
shoulder_unchecked: bool,
|
||||
}
|
||||
|
||||
/// The free-end test of a face's two ends (`side` 0 at its start, 1 at its end), each made
|
||||
@@ -160,6 +168,22 @@ impl FaceEnds<'_> {
|
||||
}
|
||||
Ok(free)
|
||||
}
|
||||
|
||||
/// Does the shoulder at this closed end stay beyond the plane of the face's end? Not where it
|
||||
/// rises back along the face (a slanted plate, a boss on a round body), which a groove
|
||||
/// running to that plane would cut into: there is material outside the face just inside it.
|
||||
/// The caller keeps the square stop, and says so, when the kernel can't make the check.
|
||||
fn clear_of_end_plane(&self, side: usize) -> Result<bool> {
|
||||
#[cfg(test)]
|
||||
if tests::FAIL_CLEARANCE.get() {
|
||||
return Err(kernel::KernelError::Failed("clearance refused for the test".into()).into());
|
||||
}
|
||||
let (p, e) = (self.pitch, self.pitch * 1e-3);
|
||||
let inside = if side == 0 { (self.cyl.start + e, self.cyl.start + p) } else { (self.cyl.end - p, self.cyl.end - e) };
|
||||
// All the radii the groove cuts beyond the face; only touching their rims is not in its
|
||||
// way, as in `free`.
|
||||
kernel::material_in_ring(self.body, self.cyl, self.beyond.0 + e, self.beyond.1 - e, inside).map(|hit| !hit).map_err(DocError::from)
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread on the cylindrical face at `p`: its size, and the part of the face it runs over.
|
||||
@@ -333,11 +357,23 @@ pub(super) fn thread_on(st: &ModelState, feature: u64, p: Vec3, ask: &ThreadAsk)
|
||||
// How each end of the thread meets the face: at a face end, or within half a pitch (and its
|
||||
// runout) of one, where the groove would reach past it, that end's free-end test decides.
|
||||
let near = |at: f64, side: usize| if side == 0 { at - lo } else { hi - at } < pitch / 2.0 + ease.get(side).copied().unwrap_or(0.0);
|
||||
// A shoulder whose check the kernel couldn't make keeps the square stop, and the feature says so.
|
||||
let mut unchecked = false;
|
||||
let mut kind = |at: f64, side: usize| -> Result<End> {
|
||||
Ok(if !ask.modeled || !near(at, side) {
|
||||
End::Inside
|
||||
} else if !ends.free(side)? {
|
||||
End::Closed
|
||||
let at_end = (if side == 0 { at - lo } else { hi - at }) < pitch * 1e-3;
|
||||
let square = ease.get(side).copied().unwrap_or(0.0) == 0.0;
|
||||
let clear = at_end && square && !cyl.internal && !form.is_taper() && {
|
||||
let check = ends.clear_of_end_plane(side);
|
||||
unchecked |= matches!(check, Err(DocError::Kernel(_)));
|
||||
match check {
|
||||
Err(DocError::Kernel(_)) => false,
|
||||
other => other?,
|
||||
}
|
||||
};
|
||||
if clear { End::Shoulder } else { End::Closed }
|
||||
} else if (if side == 0 { at - lo } else { hi - at }) < pitch * 1e-3 {
|
||||
End::Free
|
||||
} else {
|
||||
@@ -373,7 +409,7 @@ pub(super) fn thread_on(st: &ModelState, feature: u64, p: Vec3, ask: &ThreadAsk)
|
||||
.then(|| form.groove(major, pitch, true))
|
||||
.filter(|g| !(g.root_r - cyl.radius >= g.basic_depth / 4.0))
|
||||
.map(|g| g.crest_r);
|
||||
Ok(ThreadOn { info, cyl, ends, steep: ask.steep, ease, from: side, narrow })
|
||||
Ok(ThreadOn { info, cyl, ends, steep: ask.steep, ease, from: side, narrow, shoulder_unchecked: unchecked })
|
||||
}
|
||||
|
||||
/// The end of the face (0: its start, 1: its end) a thread of limited length starts at: the end
|
||||
@@ -536,7 +572,9 @@ fn thread_grooves(
|
||||
// Past a free end of the face the groove runs out into the air, a pitch beyond it. Before
|
||||
// a closed one it stops with its section clear of the face's end (its centre as far inside
|
||||
// as the section reaches that way), so it never cuts into the shoulder there; inside the
|
||||
// face it stops square at the threaded length's end.
|
||||
// face it stops square at the threaded length's end. At a shoulder it can be trimmed against
|
||||
// ([`End::Shoulder`]) its whole section runs past the plane of the face's end, so every angle
|
||||
// of the last turn is cut down to that plane ([`trim_at_shoulders`] takes off the rest).
|
||||
//
|
||||
// A runout `e` at an end (ISO 3508) fades the groove out over `e`: its section moves out of
|
||||
// the material (an external thread's away from the axis, past the surface; an internal
|
||||
@@ -548,12 +586,14 @@ fn thread_grooves(
|
||||
(End::Free, false) => t.start - p,
|
||||
(End::Free, true) => t.start,
|
||||
(End::Closed, _) => (t.start - ease[0]).max(face.0 + w_lo),
|
||||
(End::Shoulder, _) => face.0 - w_hi - 0.1 * p,
|
||||
(End::Inside, _) => t.start - ease[0],
|
||||
};
|
||||
let s1 = match (ends[1], ease[1] > 0.0) {
|
||||
(End::Free, false) => t.end + p,
|
||||
(End::Free, true) => t.end,
|
||||
(End::Closed, _) => (t.end + ease[1]).min(face.1 - w_hi),
|
||||
(End::Shoulder, _) => face.1 + w_lo + 0.1 * p,
|
||||
(End::Inside, _) => t.end + ease[1],
|
||||
};
|
||||
if !(s1 - s0 > p * 1e-3) {
|
||||
@@ -698,6 +738,24 @@ fn taper_envelope(t: &ThreadInfo, radius: f64, ends: [End; 2], (d0, from): (f64,
|
||||
Ok(ring.pop())
|
||||
}
|
||||
|
||||
/// The groove with what runs past a shoulder's plane taken off ([`End::Shoulder`]), so cutting it
|
||||
/// leaves the shoulder whole.
|
||||
fn trim_at_shoulders(mut tool: Body, axis: Vec3, ends: [End; 2], face: (f64, f64), k: UnitVec3) -> Result<Body> {
|
||||
for (end, at, keep_positive) in [(ends[0], face.0, true), (ends[1], face.1, false)] {
|
||||
if end != End::Shoulder {
|
||||
continue;
|
||||
}
|
||||
let plane = Plane::from_normal(axis + k * at, *k).ok_or_else(|| DocError::Invalid("thread axis".into()))?;
|
||||
// An error of the cut, like the kernel's own: `cut` then falls back to the square stop.
|
||||
let missed = || kernel::KernelError::Failed("the thread's groove misses its face".into());
|
||||
let kept = kernel::half_space(&tool, &plane, keep_positive)?.0;
|
||||
#[cfg(test)]
|
||||
let kept = kept.filter(|_| !tests::EMPTY_TRIM.get());
|
||||
tool = kept.ok_or_else(missed)?;
|
||||
}
|
||||
Ok(tool)
|
||||
}
|
||||
|
||||
/// Evaluate a thread feature: a cosmetic thread is listed in the model's threads, a modelled
|
||||
/// one cut into its body.
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
|
||||
@@ -768,7 +826,13 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
None => note,
|
||||
});
|
||||
}
|
||||
cut(st, on, (*tip, angle, depth))
|
||||
if let Some(note) = cut(st, on, (*tip, angle, depth))? {
|
||||
*warning = Some(match warning.take() {
|
||||
Some(w) => format!("{w}; {note}"),
|
||||
None => note,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A runout as (mm, pitches): ISO 3508's standard 2.5 P and short 1.25 P, or a length.
|
||||
@@ -783,9 +847,10 @@ pub(super) fn runout(vals: &BTreeMap<String, Value>, e: &Option<crate::ThreadRun
|
||||
|
||||
/// Cut a modelled thread resolved on its face into its body: the grooves (SMLib's exact screw
|
||||
/// sweeps, a taper's crests by its envelope) by one B-rep boolean, then the chamfered tip
|
||||
/// (`tip`, with its angle and depth when given).
|
||||
pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (ThreadTip, Option<f64>, Option<f64>)) -> Result<()> {
|
||||
let ThreadOn { info: t, cyl, ends, steep, ease, from: from_side, narrow } = on;
|
||||
/// (`tip`, with its angle and depth when given). What the user should be told about the result, if
|
||||
/// anything, is returned.
|
||||
pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (ThreadTip, Option<f64>, Option<f64>)) -> Result<Option<String>> {
|
||||
let ThreadOn { info: t, cyl, ends, steep, ease, from: from_side, narrow, shoulder_unchecked } = on;
|
||||
let i = st.bodies.iter().position(|b| b.name == t.body).ok_or_else(|| DocError::Invalid("threaded body".into()))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let (mut input, radius) = (mb.traced(), cyl.radius);
|
||||
@@ -822,21 +887,46 @@ pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (Threa
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
let mut grooves = Vec::new();
|
||||
for g in thread_grooves(&t, groove_radius, (cyl.start, cyl.end), ends, steep, ease, taper)? {
|
||||
let section = Region2 { outer: solidcore_record::Loop2::polygon(&g.section), holes: Vec::new() };
|
||||
grooves.push(kernel::sweep_helix_radial(&g.plane, §ion, g.base, g.axis, g.pitch, g.turns, &g.law)?);
|
||||
}
|
||||
if let Some(taper) = taper
|
||||
&& let Some(envelope) = taper_envelope(&t, radius, ends, taper)?
|
||||
{
|
||||
grooves.push(envelope);
|
||||
}
|
||||
let tool = match grooves.as_slice() {
|
||||
[one] => one.clone(),
|
||||
many => kernel::union_all(&many.iter().collect::<Vec<_>>())?,
|
||||
let cut_grooves = |ends: [End; 2]| -> Result<(Option<Body>, usize)> {
|
||||
let mut grooves = Vec::new();
|
||||
for g in thread_grooves(&t, groove_radius, (cyl.start, cyl.end), ends, steep, ease, taper)? {
|
||||
let section = Region2 { outer: solidcore_record::Loop2::polygon(&g.section), holes: Vec::new() };
|
||||
grooves.push(kernel::sweep_helix_radial(&g.plane, §ion, g.base, g.axis, g.pitch, g.turns, &g.law)?);
|
||||
}
|
||||
if let Some(taper) = taper
|
||||
&& let Some(envelope) = taper_envelope(&t, radius, ends, taper)?
|
||||
{
|
||||
grooves.push(envelope);
|
||||
}
|
||||
let tool = match grooves.as_slice() {
|
||||
[one] => one.clone(),
|
||||
many => kernel::union_all(&many.iter().collect::<Vec<_>>())?,
|
||||
};
|
||||
let tool = trim_at_shoulders(tool, t.axis_point, ends, (cyl.start, cyl.end), t.axis)?;
|
||||
let cut = kernel::boolean(&input.body, &tool, BoolOp::Cut)?;
|
||||
// Now rather than at the feature's end, where it could only fail the feature.
|
||||
if ends.contains(&End::Shoulder)
|
||||
&& let Some(body) = &cut
|
||||
{
|
||||
kernel::check_kept(body)?;
|
||||
}
|
||||
Ok((cut, tool.face_count()))
|
||||
};
|
||||
let (nb, groove_faces) = (kernel::boolean(&input.body, &tool, BoolOp::Cut)?, tool.face_count());
|
||||
// A design that built with the square stop keeps building: where the kernel can't make the
|
||||
// trimmed cut, the groove stops clear of the shoulder as it did, and the feature says so.
|
||||
let mut made = cut_grooves(ends);
|
||||
let mut square_stop = shoulder_unchecked;
|
||||
if matches!(made, Err(DocError::Kernel(_))) && ends.contains(&End::Shoulder) {
|
||||
made = cut_grooves(ends.map(|e| if e == End::Shoulder { End::Closed } else { e }));
|
||||
square_stop = true;
|
||||
}
|
||||
let note = (square_stop && made.is_ok()).then(|| {
|
||||
format!(
|
||||
"the {} thread's groove could not be run down to the shoulder here, so it stops clear of it and its last turn is left uncut",
|
||||
t.designation
|
||||
)
|
||||
});
|
||||
let (nb, groove_faces) = made?;
|
||||
let nb = nb.ok_or_else(|| DocError::Invalid("the thread removed everything".into()))?;
|
||||
// The groove's faces have no role to name them by.
|
||||
let groove = crate::lineage::Lineage::unnamed_faces(groove_faces);
|
||||
@@ -854,7 +944,7 @@ pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (Threa
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
Ok(note)
|
||||
}
|
||||
|
||||
/// The chamfered tip of an external thread (ISO 4753, chamfered end CH) at the free ends `tip`
|
||||
@@ -937,3 +1027,99 @@ fn tip_cones(
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
/// Make [`trim_at_shoulders`] find nothing of the groove left.
|
||||
pub(super) static EMPTY_TRIM: Cell<bool> = const { Cell::new(false) };
|
||||
/// Make the rising-shoulder check fail as the kernel would.
|
||||
pub(super) static FAIL_CLEARANCE: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
fn ask() -> ThreadAsk<'static> {
|
||||
ThreadAsk {
|
||||
form: None,
|
||||
designation: Some("M10"),
|
||||
hand: ThreadHand::Right,
|
||||
starts: 1,
|
||||
steep: SteepFlank::default(),
|
||||
extent: Extent::Full,
|
||||
from: None,
|
||||
modeled: true,
|
||||
ease_out: (0.0, 0.0),
|
||||
tapped: false,
|
||||
old_auto_size: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// How a thread's ends meet the face at (5, 0, z) of `body`.
|
||||
fn ends_of(body: Body, z: f64) -> [End; 2] {
|
||||
let mut st = ModelState::default();
|
||||
st.bodies.push(ModelBody::new("Part".into(), body, 1));
|
||||
thread_on(&st, 1, Vec3::new(5.0, 0.0, z), &ask()).unwrap().ends
|
||||
}
|
||||
|
||||
/// A shoulder that stays beyond the face's end takes the trim. One rising back along the face
|
||||
/// (a slanted plate, a boss on a round body) keeps the square stop.
|
||||
#[test]
|
||||
fn only_a_shoulder_that_stays_beyond_the_faces_end_takes_the_trim() {
|
||||
let z = Vec3::new(0.0, 0.0, 1.0);
|
||||
let join = |a: &Body, b: &Body| kernel::boolean(a, b, BoolOp::Union).unwrap().unwrap();
|
||||
let boss = kernel::cylinder(Vec3::new(0.0, 0.0, 2.0), z, 5.0, 26.0).unwrap();
|
||||
let plate = kernel::box_solid(Vec3::new(-20.0, -20.0, -6.0), Vec3::new(20.0, 20.0, 2.0)).unwrap();
|
||||
let flat = ends_of(join(&plate, &boss), 20.0);
|
||||
assert!(flat.contains(&End::Shoulder), "{flat:?}");
|
||||
let tilted = kernel::cylinder(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.2, 0.0, 1.0), 20.0, 6.0).unwrap();
|
||||
let slanted = ends_of(join(&tilted, &boss), 20.0);
|
||||
assert!(slanted.contains(&End::Closed) && !slanted.contains(&End::Shoulder), "{slanted:?}");
|
||||
let round = kernel::cylinder(Vec3::new(0.0, -20.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 10.0, 40.0).unwrap();
|
||||
let on_round = ends_of(join(&round, &boss), 20.0);
|
||||
assert!(on_round.contains(&End::Closed) && !on_round.contains(&End::Shoulder), "{on_round:?}");
|
||||
}
|
||||
|
||||
/// Where the kernel can't check the shoulder, the thread still builds with the square stop, and
|
||||
/// the feature says its last turn is left uncut.
|
||||
#[test]
|
||||
fn a_shoulder_the_kernel_cannot_check_keeps_the_square_stop_and_says_so() {
|
||||
let plate = kernel::box_solid(Vec3::new(-20.0, -20.0, 0.0), Vec3::new(20.0, 20.0, 6.0)).unwrap();
|
||||
let boss = kernel::cylinder(Vec3::new(0.0, 0.0, 6.0), Vec3::new(0.0, 0.0, 1.0), 5.0, 16.0).unwrap();
|
||||
let body = kernel::boolean(&plate, &boss, BoolOp::Union).unwrap().unwrap();
|
||||
let mut st = ModelState::default();
|
||||
st.bodies.push(ModelBody::new("Part".into(), body, 1));
|
||||
FAIL_CLEARANCE.set(true);
|
||||
let on = thread_on(&st, 1, Vec3::new(5.0, 0.0, 14.0), &ask());
|
||||
FAIL_CLEARANCE.set(false);
|
||||
let on = on.unwrap();
|
||||
assert!(on.ends.contains(&End::Closed) && !on.ends.contains(&End::Shoulder), "{:?}", on.ends);
|
||||
let note = cut(&mut st, on, (ThreadTip::None, None, None)).unwrap();
|
||||
assert!(note.is_some_and(|n| n.contains("M10") && n.contains("left uncut")));
|
||||
}
|
||||
|
||||
/// Where the trim leaves nothing of the groove, a thread at a shoulder that built with the
|
||||
/// square stop still builds with it, and the feature says its last turn is left uncut.
|
||||
#[test]
|
||||
fn a_shoulder_cut_the_kernel_refuses_falls_back_to_the_square_stop_and_says_so() {
|
||||
let plate = kernel::box_solid(Vec3::new(-20.0, -20.0, 0.0), Vec3::new(20.0, 20.0, 6.0)).unwrap();
|
||||
let boss = kernel::cylinder(Vec3::new(0.0, 0.0, 6.0), Vec3::new(0.0, 0.0, 1.0), 5.0, 16.0).unwrap();
|
||||
let body = kernel::boolean(&plate, &boss, BoolOp::Union).unwrap().unwrap();
|
||||
let build = |fail: bool| {
|
||||
let mut st = ModelState::default();
|
||||
st.bodies.push(ModelBody::new("Part".into(), body.clone(), 1));
|
||||
let on = thread_on(&st, 1, Vec3::new(5.0, 0.0, 14.0), &ask()).unwrap();
|
||||
EMPTY_TRIM.set(fail);
|
||||
let note = cut(&mut st, on, (ThreadTip::None, None, None));
|
||||
EMPTY_TRIM.set(false);
|
||||
(note.unwrap(), kernel::measure(&st.bodies[0].body).unwrap().volume)
|
||||
};
|
||||
let (none, trimmed) = build(false);
|
||||
assert_eq!(none, None);
|
||||
let (note, square) = build(true);
|
||||
assert!(note.is_some_and(|n| n.contains("M10") && n.contains("left uncut")));
|
||||
// The square stop leaves about half a pitch's worth of groove uncut: well over a mm³.
|
||||
assert!(square > trimmed + 1.0, "{square} vs {trimmed}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ impl FeatureKind {
|
||||
|
||||
/// The body a feature that works on one body's faces names (shell, draft, split, offset,
|
||||
/// delete and replace face, surface offset, the placed plastic features, sheet metal
|
||||
/// conversion, ruled surface, untrim): its face points pick faces of that body.
|
||||
/// conversion, ruled surface, untrim, Split Body's tool face): its face points pick faces of
|
||||
/// that body.
|
||||
pub fn face_body(&self) -> Option<&str> {
|
||||
match self {
|
||||
FeatureKind::Shell { body, .. }
|
||||
@@ -69,7 +70,7 @@ impl FeatureKind {
|
||||
| FeatureKind::SnapFit { body, .. }
|
||||
| FeatureKind::Rest { body, .. } => body.as_deref(),
|
||||
FeatureKind::SheetConvert { body, .. } | FeatureKind::SurfaceRuled { body, .. } => Some(body.as_ref()),
|
||||
FeatureKind::SurfaceUntrim { face, .. } => Some(face.body.as_ref()),
|
||||
FeatureKind::SurfaceUntrim { face, .. } | FeatureKind::Split { tool: Some(face), .. } => Some(face.body.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ revolve
|
||||
moves: axis.dir[0], axis.dir[1], axis.dir[2], axis.origin[0], axis.origin[1], axis.origin[2], to[0], to[1], to[2]
|
||||
face points: [3, 3.5, 3.25]
|
||||
profiles: 1 all
|
||||
placement: 9, 9.5, 9.25
|
||||
fillet_constant
|
||||
moves: edges[0][0], edges[0][1], edges[0][2]
|
||||
fillet_chord
|
||||
@@ -86,6 +87,7 @@ construction_plane
|
||||
moves: plane.axis_dir[0], plane.axis_dir[1], plane.axis_origin[0], plane.axis_origin[1], plane.axis_origin[2]
|
||||
split
|
||||
moves: plane.axis_dir[0], plane.axis_dir[1], plane.axis_origin[0], plane.axis_origin[1], plane.axis_origin[2], tool.point[0], tool.point[1], tool.point[2]
|
||||
face points: [0, 0, 0]
|
||||
split_line
|
||||
moves: plane.axis_dir[0], plane.axis_dir[1], plane.axis_origin[0], plane.axis_origin[1], plane.axis_origin[2]
|
||||
construction_axis
|
||||
@@ -166,6 +168,7 @@ surface_extrude
|
||||
face points: [1, 1.5, 1.25], [2, 2.5, 2.25]
|
||||
surface_revolve
|
||||
moves: axis.dir[0], axis.dir[1], axis.dir[2], axis.origin[0], axis.origin[1], axis.origin[2]
|
||||
placement: 9, 9.5, 9.25
|
||||
surface_sweep
|
||||
surface_sweep_options
|
||||
surface_loft
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
//! A counterbore hole may save a `bridge` length: two printing layers that thick below the
|
||||
//! counterbore's floor (a slot as wide as the hole along X, then a square) so a 3D printer bridges
|
||||
//! the step. An older design has none and keeps a plain counterbore. The last release refuses a
|
||||
//! design that has it (it asks for an update) instead of opening it without the bridge layers.
|
||||
@@ -0,0 +1,5 @@
|
||||
//! A fillet saves `turn: "swept"` when two of its rounds that turn round a sharp side edge (the
|
||||
//! rounds along a boss's base, at its upright edges) are swept round that edge, their corner an
|
||||
//! exact torus; absent, the default `mitre`, they meet in a mitre, as designs before this change were
|
||||
//! built. An older design has none and keeps its mitres. The last release refuses a design with
|
||||
//! this change (the newer format number).
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Nothing new is saved for holes, but a hole placed on a face that names the bodies it drills
|
||||
//! (`participants`: its `targets`, or the body of another component it was picked on) now finds
|
||||
//! the face it starts on among those bodies' faces, where one of them passes through its position
|
||||
//! (#12066). Where another body touched that face at the hole, older designs could save a
|
||||
//! `direction` taken from the other body's face, pointing out of the drilled body; such a hole cut
|
||||
//! only a shallow pocket. It is now mirrored into the drilled body's face when the design is
|
||||
//! evaluated, as format 18 does for a direction taken from the far side of a thin plate, and
|
||||
//! drills through. Holes whose direction enters one of their own bodies' faces there, holes whose
|
||||
//! own bodies have no face at their position (placed on another body's face to drill through it),
|
||||
//! holes that name no bodies, and holes at several sketch points (already limited to their bodies,
|
||||
//! #2882) build as before. No rewrite is needed. The last release refuses a design with this
|
||||
//! change (the newer format number) instead of building the pocket again.
|
||||
@@ -0,0 +1,5 @@
|
||||
//! A revolve's explicit axis `{origin, dir}` can save its origin's coordinates as length
|
||||
//! expressions (`["W / 2", 0, "groove_floor"]`) so the axis follows the parameters; a plain number
|
||||
//! stays a number. An older design has numbers only and reads as before. The last release refuses
|
||||
//! a design with this change and asks for an update, instead of opening it and reading an
|
||||
//! expression as the origin.
|
||||
@@ -0,0 +1,6 @@
|
||||
//! A new-body extrusion or revolve whose outputs transfer into its source sketch's component
|
||||
//! reads the profile through the recorded transfer frame. Capturing that component's position
|
||||
//! therefore moves the body once, as the pending preview does. Existing designs use their
|
||||
//! saved transfer transforms without rewriting fields; independent cross-component references
|
||||
//! keep their live placement. The last release refuses the newer design-format number rather
|
||||
//! than reopening the design with the former double-placement behavior.
|
||||
@@ -92,8 +92,8 @@ pub use eval::{
|
||||
blend_target, curve_pick_error, draft_corner_options, edge_names_for, edge_refs, face_point_names, face_point_state, hole_direction_at,
|
||||
hole_on_surface, import_body_names, join_pieces_added, map_wire, metric_thread_sizes, object_height, parse_metric_thread, pattern_matrices,
|
||||
revolve_extent, revolve_extent_for_handle, revolve_extent_with_convention, select_profiles, sketch_hole_direction, sketch_line_plane,
|
||||
state_in_frame, state_in_frame_at, suggested_thread, suggested_thread_in, tangent_face_chain, through_all_distance, world_state,
|
||||
world_state_reusing,
|
||||
state_in_frame, state_in_frame_at, suggested_thread, suggested_thread_in, tangent_face_chain, through_all_distance, with_placed_copies,
|
||||
world_state, world_state_reusing,
|
||||
};
|
||||
pub use eval::{EvalCache, KeptResults};
|
||||
pub use expr::Expr;
|
||||
|
||||
@@ -239,7 +239,10 @@ impl<'a> ProfileRoles<'a> {
|
||||
// Every curve the segment is drawn on (one, or several coincident or collinear ones).
|
||||
let curve = |segment: usize| -> Vec<String> {
|
||||
let (Some(curves), Some(segment)) = (curves.as_ref(), segments.get(segment)) else { return Vec::new() };
|
||||
let drawn: Vec<String> = curves.find_all_along(segment).into_iter().map(|id| format!("side:{id}")).collect();
|
||||
let mut drawn: Vec<String> = curves.find_all_along(segment).into_iter().map(|id| format!("side:{id}")).collect();
|
||||
if drawn.is_empty() {
|
||||
drawn.extend(curves.find_free_form(segment).map(|id| format!("side:{id}")));
|
||||
}
|
||||
match self.thin {
|
||||
// A thin wall's side beside its curve (the other side may lie on it).
|
||||
Some(thin) if drawn.is_empty() => curves.find_offset(segment, &thin.curves, thin.distances),
|
||||
@@ -754,10 +757,19 @@ pub(crate) fn order_lumps(bodies: &mut [crate::ModelBody], origin: &Arc<crate::n
|
||||
// cut body's own feature's and the rest are the cut's, and `body.rename` stores a name in
|
||||
// the `body_names` of the feature a body says made it (#10438).
|
||||
let names: Vec<(String, u64)> = members.iter().filter_map(|i| bodies.get(*i).map(|b| (b.name.clone(), b.feature))).collect();
|
||||
for ((name, feature), (_, i)) in names.into_iter().zip(keys) {
|
||||
if let Some(b) = bodies.get_mut(i) {
|
||||
b.name = name;
|
||||
b.feature = feature;
|
||||
// The pieces take the family's places in that order too, so the state lists them as
|
||||
// their names run. The session pins a feature's body names in state order, and the next
|
||||
// evaluation hands them out in this order: listed in the kernel's order, the pieces
|
||||
// swapped names once the design was evaluated again from its pinned names (opened).
|
||||
let moved: Vec<crate::ModelBody> = keys.iter().filter_map(|(_, i)| bodies.get(*i).cloned()).collect();
|
||||
if moved.len() != members.len() || names.len() != members.len() {
|
||||
continue;
|
||||
}
|
||||
for ((mut b, (name, feature)), place) in moved.into_iter().zip(names).zip(&members) {
|
||||
b.name = name;
|
||||
b.feature = feature;
|
||||
if let Some(slot) = bodies.get_mut(*place) {
|
||||
*slot = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,7 +760,7 @@ impl Model {
|
||||
return Err(bad("saved model: features"));
|
||||
}
|
||||
let (vals, param_errors) = evaluation_values(doc);
|
||||
let step_digests = StepDigests::of(doc, &StepDigests::default());
|
||||
let import_digests = ImportDigests::of(doc, &ImportDigests::default());
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
// As `evaluate` digests the context under the mode, so the next evaluation reuses these.
|
||||
let ctx = context_digest(doc, &vals) ^ validation_digest(validation);
|
||||
@@ -769,19 +769,39 @@ impl Model {
|
||||
let mut exact = true;
|
||||
let mut prev = 0;
|
||||
let mut results = Vec::with_capacity(features.len());
|
||||
let mut construction = HashMap::new();
|
||||
let mut prepared_states = std::collections::HashSet::new();
|
||||
for (i, (f, r)) in doc.features.iter().zip(features).enumerate() {
|
||||
let saved = unhex(r.get("fingerprint").unwrap_or(&Json::Null))?;
|
||||
// A fingerprint reads the state before its feature (where the bodies it takes lie).
|
||||
// After a state the file left out, the saved one is taken: `evaluate` works that state
|
||||
// out, checks the fingerprints after it, and evaluates again from one that differs.
|
||||
prev = if exact { chain_fingerprint(doc, &vals, f, prev, i >= marker, &step_digests, Some(&before)) } else { saved };
|
||||
prev = if exact { chain_fingerprint(doc, &vals, f, prev, i >= marker, &import_digests, Some(&before)) } else { saved };
|
||||
if r.get("id").and_then(Json::as_u64) != Some(f.id) || saved != prev {
|
||||
return Err(bad("saved model: the document changed"));
|
||||
}
|
||||
let ms = r.get("ms").and_then(Json::as_f64).filter(|ms| ms.is_finite()).ok_or_else(|| bad("saved model: time"))?;
|
||||
// A file without room for every state keeps some features without one (#3220).
|
||||
let state =
|
||||
r.get("state").map(|s| states.get(index(Some(s), states.len())?).cloned().ok_or_else(|| bad("saved model: state"))).transpose()?;
|
||||
let state = r.get("state").map(|s| index(Some(s), states.len())).transpose()?;
|
||||
if let Some(at) = state
|
||||
&& prepared_states.insert(at)
|
||||
{
|
||||
let original = matches!(
|
||||
&f.kind,
|
||||
FeatureKind::Box { operation: Operation::NewBody, .. } | FeatureKind::Extrude { operation: Operation::NewBody, .. }
|
||||
);
|
||||
let current = states.get_mut(at).ok_or_else(|| bad("saved model: state"))?;
|
||||
// Each state is prepared before its first shared clone. Record and NameCell
|
||||
// identity prove this is still the original geometry and its exact names.
|
||||
for body in &mut Arc::make_mut(current).bodies {
|
||||
let key = (std::ptr::from_ref(body.body.record()).addr(), body.names.id());
|
||||
if original && body.feature == f.id && before.body(&body.name).is_none() {
|
||||
construction.insert(key, f.id);
|
||||
}
|
||||
body.names.cached_construction = construction.get(&key).copied();
|
||||
}
|
||||
}
|
||||
let state = state.map(|at| states.get(at).cloned().ok_or_else(|| bad("saved model: state"))).transpose()?;
|
||||
let result = FeatureResult {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
@@ -806,7 +826,7 @@ impl Model {
|
||||
}
|
||||
results.push(result);
|
||||
}
|
||||
let mut model = Model { results, param_errors, step_digests, context: ctx, frame_bodies, validation, ..Model::default() };
|
||||
let mut model = Model { results, param_errors, import_digests, context: ctx, frame_bodies, validation, ..Model::default() };
|
||||
let unkept = model.results.iter().filter(|r| r.unkept).count();
|
||||
if unkept > 0 {
|
||||
// The features whose state the file did not keep, each evaluated again from the state
|
||||
@@ -824,7 +844,7 @@ impl Model {
|
||||
let mut before = Arc::new(ModelState::default());
|
||||
if let Ok(mut cache) = model.cache.lock() {
|
||||
for (f, r) in doc.features.iter().zip(&model.results).filter(|(_, r)| !r.skipped && r.error.is_none()) {
|
||||
let local = fingerprint(doc, 0, f, &vals, false, &model.step_digests) ^ placement_digest(doc, f, Some(&before));
|
||||
let local = fingerprint(doc, 0, f, &vals, false, &model.import_digests) ^ placement_digest(doc, f, Some(&before));
|
||||
cache.put(
|
||||
cache_key(f, r.fingerprint, local, ctx, &before),
|
||||
CachedEval {
|
||||
|
||||
@@ -9,15 +9,19 @@
|
||||
//! that uses it fails, asking for the reference to be picked again
|
||||
//! ([`crate::DocError::Reselect`]). A face whose name was worked out from geometry, found where
|
||||
//! the reference's point no longer lies, is reported: only geometry decided.
|
||||
//! Saved numbered Project links also have a compatibility check for a whole face that
|
||||
//! contains the saved pick and comes directly from its original construction feature.
|
||||
//! This check is local to manual sketch projection.
|
||||
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_geom::Mesh;
|
||||
use solidcraft_geom::brep::eval::SurfaceEval;
|
||||
use solidcraft_geom::prelude::*;
|
||||
|
||||
use crate::history_names::{Match, Names};
|
||||
use crate::naming::{self, closest, edge_names, face_names, strip_piece};
|
||||
use crate::piece_names::strip_edge_number;
|
||||
use crate::{ModelBody, ModelState};
|
||||
use crate::{FeatureKind, ModelBody, ModelState, Operation};
|
||||
|
||||
/// The mark on a name saved before format 14 (`?F2:top#2`). Those names were worked out from
|
||||
/// geometry, and some of them name a face that exact history names differently (a tube's outer
|
||||
@@ -433,6 +437,84 @@ pub(crate) fn locate_on(body: &ModelBody, name: &str, p: Vec3) -> Option<FaceHit
|
||||
locate_in(std::iter::once((0, body)), name, p).ok()
|
||||
}
|
||||
|
||||
/// Find a saved manual Project face in its body. Older designs can name a numbered piece of
|
||||
/// a face that now evaluates whole. Recover it only when the original feature directly made
|
||||
/// the exact whole face and the face contains the
|
||||
/// old pick. Other face references still require the stored name as picked.
|
||||
pub(crate) fn locate_projected_face(body: &ModelBody, name: &str, p: Vec3) -> Option<(usize, Vec3)> {
|
||||
if body.body.is_mesh() {
|
||||
return None;
|
||||
}
|
||||
match locate_in(std::iter::once((0, body)), name, p) {
|
||||
Ok(hit) => Some((hit.face, hit.point)),
|
||||
Err(Miss::Gone) => recover_numbered_projected_face(body, name, p),
|
||||
Err(Miss::Several) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bound the quadratic scan in remove_faces, which takes the list of faces to remove.
|
||||
const MAX_PROJECTION_RECOVERY_FACES: usize = 1024;
|
||||
|
||||
fn recover_numbered_projected_face(body: &ModelBody, name: &str, pick: Vec3) -> Option<(usize, Vec3)> {
|
||||
let (stem, number) = name.rsplit_once('#')?;
|
||||
let count = body.body.face_count();
|
||||
if count > MAX_PROJECTION_RECOVERY_FACES
|
||||
|| !pick.is_finite()
|
||||
|| stem.is_empty()
|
||||
|| strip_piece(stem) != stem
|
||||
|| number.is_empty()
|
||||
|| number.starts_with('0')
|
||||
|| !number.bytes().all(|digit| digit.is_ascii_digit())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// A changed body may keep the same face name while the face absorbs a smaller piece.
|
||||
// Projecting that larger face could silently grow a downstream feature's material.
|
||||
let producer = stem.strip_prefix('F')?.split_once(':')?.0.parse::<u64>().ok()?;
|
||||
let live_construction = body.names.origin.as_ref().is_some_and(|origin| {
|
||||
producer == origin.feature.id
|
||||
&& matches!(
|
||||
&origin.feature.kind,
|
||||
FeatureKind::Box { operation: Operation::NewBody, .. } | FeatureKind::Extrude { operation: Operation::NewBody, .. }
|
||||
)
|
||||
&& origin.before.body(&body.name).is_none()
|
||||
});
|
||||
if producer != body.feature || !(live_construction || body.names.cached_construction == Some(producer)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let candidates = faces_with_stem(body, stem);
|
||||
let &[face] = candidates.as_slice() else { return None };
|
||||
let names = naming::names(body);
|
||||
let info = names.info.get(face)?;
|
||||
if names.faces.get(face)?.as_str() != stem || !info.verified || info.aliases.iter().any(|alias| strip_piece(alias) != stem) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let record = body.body.record().faces.get(face)?;
|
||||
let [[u0, u1], [v0, v1]] = record.domain;
|
||||
let surface = SurfaceEval::new(record.parameters()).ok()?;
|
||||
let tolerance = (body.body.size() * 1e-3).max(1e-3);
|
||||
if !tolerance.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let point = surface.point(surface.project(pick, Some([u0.midpoint(u1), v0.midpoint(v1)]), tolerance)?)?;
|
||||
if !point.is_finite() || !(point.dist(pick) <= tolerance) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Isolate the face so a point on another face of the same surface cannot pass the trim test.
|
||||
let others: Vec<_> = (0..count).filter(|index| *index != face).collect();
|
||||
let isolated;
|
||||
let trimmed = if others.is_empty() {
|
||||
&body.body
|
||||
} else {
|
||||
isolated = solidcraft_kernel::remove_faces(&body.body, &others).ok()?;
|
||||
&isolated
|
||||
};
|
||||
matches!(trimmed.classify_point(point), Ok(solidcraft_kernel::PointClass::OnBoundary)).then_some((face, point))
|
||||
}
|
||||
|
||||
/// The one face a name `model.faces` lists finds, on the body called `body` or, without one, on
|
||||
/// any body. `Err` says why there is no such face, or why the name does not name just one (the
|
||||
/// pieces of a split face, or the same face on several bodies): a command that takes a face by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{Document, ModelBody, ModelState};
|
||||
use solidcore_record::{Plane, Vec3};
|
||||
use solidcore_record::{Plane, Vec2, Vec3};
|
||||
use solidcraft_geom::prelude::*;
|
||||
use solidcraft_sketch::{LinkKind, LinkSource, Sketch};
|
||||
use std::collections::BTreeMap;
|
||||
@@ -49,3 +49,197 @@ fn named_manual_projection_keeps_its_body_and_face_at_shared_boundaries() {
|
||||
assert_eq!(sketch.links[0].id, link);
|
||||
assert!(matches!(&sketch.links[0].source,LinkSource::Face{body,..} if body=="Source"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbered_manual_projection_rebuilds_from_the_whole_planar_face() {
|
||||
let mut doc = Document::default();
|
||||
doc.set_param("size", "5", Some("mm"), None).unwrap();
|
||||
let kind = serde_json::from_value(serde_json::json!({
|
||||
"type": "box", "corner": [0, 0, 0], "length": "size", "width": "size", "height": "10"
|
||||
}))
|
||||
.unwrap();
|
||||
let feature = doc.add_feature(kind, None).unwrap();
|
||||
doc.feature_mut(feature).unwrap().body_names = vec!["Source".into()];
|
||||
let mut model = crate::Model::new();
|
||||
model.evaluate(&doc);
|
||||
let plane = Plane::XY.offset(20.);
|
||||
let source = LinkSource::Face { body: "Source".into(), at: Vec3::new(2., 2., 10.) };
|
||||
let resolved = crate::project::resolve(&doc, &BTreeMap::new(), &model.state(), &plane, LinkKind::Project, &source).unwrap();
|
||||
let mut sketch = Sketch::default();
|
||||
let link = sketch.add_link(LinkKind::Project, source, &resolved.geom).unwrap();
|
||||
let numbered = format!("F{feature}:+z#1");
|
||||
sketch.links[0].face_name = Some(numbered.clone());
|
||||
|
||||
doc.set_param("size", "20", Some("mm"), None).unwrap();
|
||||
model.evaluate(&doc);
|
||||
assert!(crate::project::refresh_links(&doc, &BTreeMap::new(), &model.state(), &plane, &mut sketch).unwrap().is_empty());
|
||||
assert!(!sketch.links[0].lost);
|
||||
assert_eq!(sketch.links[0].face_name.as_deref(), Some(numbered.as_str()));
|
||||
assert_eq!(sketch.link_curves(&link).len(), 4);
|
||||
assert!(sketch.points.iter().any(|point| point.pos.x > 19. && point.pos.y > 19.));
|
||||
|
||||
let saved = model.saved(&doc).unwrap();
|
||||
let reopened = crate::format::read(&doc.to_json()).unwrap();
|
||||
let cached = crate::Model::adopt(&reopened, &saved).unwrap();
|
||||
let state = cached.state();
|
||||
assert!(state.body("Source").unwrap().names.origin.is_none());
|
||||
assert_eq!(state.body("Source").unwrap().names.cached_construction, Some(feature));
|
||||
assert!(crate::project::refresh_links(&reopened, &BTreeMap::new(), &state, &plane, &mut sketch).unwrap().is_empty());
|
||||
assert!(!sketch.links[0].lost);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbered_round_face_projection_recovers_the_whole_face() {
|
||||
let doc = Document {
|
||||
features: serde_json::from_value(serde_json::json!([
|
||||
{
|
||||
"id": 1, "name": "Sketch1", "type": "sketch", "plane": {"name": "XY", "type": "origin"},
|
||||
"sketch": {
|
||||
"points": [{"fixed": true, "id": "origin", "pos": [0., 0.]}, {"fixed": false, "id": "c1.center", "pos": [0., 0.]}],
|
||||
"curves": [{"c": 1, "construction": false, "id": "c1", "r": 20., "type": "circle"}],
|
||||
"constraints": [{"id": "k1", "p": 0, "q": 1, "type": "coincident"}],
|
||||
"counters": {"k": 1, "l": 0, "p": 1}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2, "name": "Extrude1", "type": "extrude", "body_names": ["Body1"],
|
||||
"extent": {"convention": "total_distance", "direction": "positive", "distance": "10"},
|
||||
"operation": "new_body", "profiles": {"loops": [["c1"]], "type": "curves"}, "sketch": 1
|
||||
},
|
||||
{
|
||||
"id": 3, "name": "Sketch2", "type": "sketch", "plane": {"name": "YZ", "type": "origin"},
|
||||
"sketch": {
|
||||
"points": [
|
||||
{"fixed": true, "id": "origin", "pos": [0., 0.]},
|
||||
{"fixed": false, "id": "p1", "pos": [-20., 0.]},
|
||||
{"fixed": false, "id": "p2", "pos": [20., 0.]},
|
||||
{"fixed": false, "id": "p3", "pos": [-20., 10.]},
|
||||
{"fixed": false, "id": "p4", "pos": [20., 10.]}
|
||||
],
|
||||
"curves": [
|
||||
{"a": 1, "b": 2, "construction": false, "id": "l1", "link": "j1", "type": "line"},
|
||||
{"a": 3, "b": 4, "construction": false, "id": "l2", "link": "j1", "type": "line"}
|
||||
],
|
||||
"constraints": [],
|
||||
"counters": {"k": 0, "l": 2, "p": 4},
|
||||
"links": [{
|
||||
"face_name": "F2:side:c1#1", "id": "j1", "kind": "project",
|
||||
"source": {"at": [-19.87, -2.3, 6.], "body": "Body1", "type": "face"}
|
||||
}]
|
||||
}
|
||||
}
|
||||
]))
|
||||
.unwrap(),
|
||||
..Document::default()
|
||||
};
|
||||
let doc = crate::format::read(&doc.to_json()).unwrap();
|
||||
let mut model = crate::Model::new();
|
||||
model.evaluate(&doc);
|
||||
let state = model.state();
|
||||
let body = state.body("Body1").unwrap();
|
||||
assert!(crate::naming::face_names(body).iter().any(|name| name == "F2:side:c1"));
|
||||
let result = model.result(3).unwrap();
|
||||
assert!(result.error.is_none(), "{result:?}");
|
||||
let projected = &state.sketch(3).expect("the saved projection should rebuild").sketch;
|
||||
assert_eq!(projected.links[0].face_name.as_deref(), Some("F2:side:c1#1"));
|
||||
assert_eq!(projected.link_curves("j1").len(), 2);
|
||||
|
||||
// The edge-on projection of the round wall can retain its saved geometry with a warning.
|
||||
// The numbered reference must evaluate the same way as the whole face's existing name.
|
||||
let mut exact_doc = doc.clone();
|
||||
exact_doc.sketch_mut(3).unwrap().links[0].face_name = Some("F2:side:c1".into());
|
||||
let mut exact_model = crate::Model::new();
|
||||
exact_model.evaluate(&exact_doc);
|
||||
let exact_state = exact_model.state();
|
||||
let exact = &exact_state.sketch(3).unwrap().sketch;
|
||||
assert_eq!(result.warning, exact_model.result(3).unwrap().warning);
|
||||
assert_eq!(projected.links[0].lost, exact.links[0].lost);
|
||||
assert_eq!(projected.curves, exact.curves);
|
||||
assert_eq!(projected.points, exact.points);
|
||||
|
||||
let saved = model.saved(&doc).expect("the healthy projection should save its model cache");
|
||||
let mut reopened = crate::format::read(&doc.to_json()).unwrap();
|
||||
let mut reopened_model = crate::Model::adopt(&reopened, &saved).unwrap();
|
||||
assert!(reopened_model.state().body("Body1").unwrap().names.origin.is_none());
|
||||
assert_eq!(reopened_model.state().body("Body1").unwrap().names.cached_construction, Some(2));
|
||||
let crate::FeatureKind::Sketch { plane, .. } = &mut reopened.feature_mut(3).unwrap().kind else { panic!("projection sketch") };
|
||||
*plane = crate::PlaneRef::Custom { plane: Plane::YZ.offset(0.1) };
|
||||
reopened_model.evaluate(&reopened);
|
||||
assert!(reopened_model.result(3).unwrap().error.is_none(), "{:?}", reopened_model.result(3));
|
||||
let pick = Vec3::new(-19.87, -2.3, 6.);
|
||||
assert!(crate::naming::face_on_body(body, "F2:side:c1#1", pick).is_none(), "ordinary lookup stays strict");
|
||||
let mut split = body.clone();
|
||||
let mut names = crate::naming::face_names(body).as_ref().clone();
|
||||
*names.iter_mut().find(|name| name.as_str() != "F2:side:c1").unwrap() = "F2:side:c1#2#[wall]".into();
|
||||
let origin = split.names.origin.clone();
|
||||
split.names = crate::naming::NameCell::known(Arc::new(names));
|
||||
split.names.origin = origin;
|
||||
assert!(crate::naming::face_on_body(&split, "F2:side:c1#1", pick).is_none());
|
||||
assert!(crate::name_lookup::locate_projected_face(&split, "F2:side:c1#1", pick).is_none(), "another piece still exists");
|
||||
|
||||
let mut outside = doc.sketch(3).unwrap().clone();
|
||||
outside.links[0].source = LinkSource::Face { body: "Body1".into(), at: Vec3::new(-19.87, -2.3, 12.) };
|
||||
let error = crate::project::refresh_links(&doc, &BTreeMap::new(), &state, &Plane::YZ, &mut outside).unwrap_err();
|
||||
assert!(matches!(&error, crate::DocError::Reselect { reference, .. } if reference == "F2:side:c1#1"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbered_projection_refuses_modified_or_moved_cached_sources() {
|
||||
for (kind, pick) in [
|
||||
(serde_json::json!({"type": "shell", "body": "Source", "faces": [[10, 10, 10]], "thickness": "1"}), Vec3::new(0., 5., 5.)),
|
||||
(serde_json::json!({"type": "move", "bodies": ["Source"], "translate": ["1", "0", "0"]}), Vec3::new(1., 5., 5.)),
|
||||
] {
|
||||
let mut doc = Document::default();
|
||||
let source = doc
|
||||
.add_feature(
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"type": "box", "corner": [0, 0, 0], "length": "20", "width": "20", "height": "10"
|
||||
}))
|
||||
.unwrap(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
doc.feature_mut(source).unwrap().body_names = vec!["Source".into()];
|
||||
doc.add_feature(serde_json::from_value(kind).unwrap(), None).unwrap();
|
||||
let mut model = crate::Model::new();
|
||||
model.evaluate(&doc);
|
||||
assert!(model.results.iter().all(|result| result.error.is_none()));
|
||||
let saved = model.saved(&doc).unwrap();
|
||||
let reopened = crate::format::read(&doc.to_json()).unwrap();
|
||||
let cached = crate::Model::adopt(&reopened, &saved).unwrap();
|
||||
let state = cached.state();
|
||||
let body = state.body("Source").unwrap();
|
||||
let whole = format!("F{source}:-x");
|
||||
let numbered = format!("{whole}#1");
|
||||
assert!(crate::naming::face_on_body(body, &whole, pick).is_some());
|
||||
assert!(crate::naming::face_on_body(body, &numbered, pick).is_none());
|
||||
assert!(body.names.origin.is_none());
|
||||
assert_eq!(body.names.cached_construction, None);
|
||||
assert!(crate::name_lookup::locate_projected_face(body, &numbered, pick).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbered_projection_refuses_a_pick_inside_a_hole_in_the_named_face() {
|
||||
let mut doc = Document::default();
|
||||
let mut sketch = Sketch::new();
|
||||
sketch.add_circle(Vec2::ZERO, 10., Some(0), Some("c1")).unwrap();
|
||||
sketch.add_circle(Vec2::ZERO, 2., Some(0), Some("c2")).unwrap();
|
||||
let profile = doc.add_feature(crate::FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch }, None).unwrap();
|
||||
let kind = serde_json::from_value(serde_json::json!({
|
||||
"type": "extrude", "sketch": profile, "profiles": {"type": "curves", "loops": [["c1"]]}, "extent": {"distance": "5"}
|
||||
}))
|
||||
.unwrap();
|
||||
let feature = doc.add_feature(kind, None).unwrap();
|
||||
let mut model = crate::Model::new();
|
||||
model.evaluate(&doc);
|
||||
let state = model.state();
|
||||
let body = state.body("Body1").unwrap();
|
||||
let whole = format!("F{feature}:top");
|
||||
let top = crate::naming::face_names(body).iter().position(|name| name == &whole).unwrap();
|
||||
let numbered = format!("{whole}#1");
|
||||
|
||||
let pick = |x| crate::name_lookup::locate_projected_face(body, &numbered, Vec3::new(x, 0., 5.));
|
||||
assert_eq!(pick(5.).map(|(index, _)| index), Some(top));
|
||||
assert!(pick(0.).is_none(), "the pick is on the plane but inside the hole");
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ pub struct NamingOrigin {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NameCell {
|
||||
pub origin: Option<Arc<NamingOrigin>>,
|
||||
/// Direct construction verified from identical cached body/name cells during adoption.
|
||||
/// Used only by manual Project compatibility, independently of general naming history.
|
||||
pub(crate) cached_construction: Option<u64>,
|
||||
/// What the body's faces came from, when the feature that made it reports it.
|
||||
lineage: Option<Arc<Lineage>>,
|
||||
names: Arc<OnceLock<Arc<Names>>>,
|
||||
@@ -57,7 +60,7 @@ impl NameCell {
|
||||
}
|
||||
/// Names of a body made by `origin`'s feature, following `lineage` where it is known.
|
||||
pub(crate) fn traced(origin: Option<Arc<NamingOrigin>>, lineage: Option<Arc<Lineage>>) -> Self {
|
||||
NameCell { origin, lineage, names: Arc::new(OnceLock::new()), copy_of: None, saved_depth: None }
|
||||
NameCell { origin, cached_construction: None, lineage, names: Arc::new(OnceLock::new()), copy_of: None, saved_depth: None }
|
||||
}
|
||||
/// Names already known, taken as they are.
|
||||
pub fn known(names: Arc<Vec<String>>) -> Self {
|
||||
@@ -98,7 +101,7 @@ impl NameCell {
|
||||
fn shared(names: Arc<Names>) -> Self {
|
||||
let cell = OnceLock::new();
|
||||
let _ = cell.set(names);
|
||||
NameCell { origin: None, lineage: None, names: Arc::new(cell), copy_of: None, saved_depth: None }
|
||||
NameCell { origin: None, cached_construction: None, lineage: None, names: Arc::new(cell), copy_of: None, saved_depth: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,7 +835,7 @@ pub(crate) fn face_centres(b: &ModelBody) -> Vec<Vec3> {
|
||||
samples(&b.mesh(), face_count(b)).into_iter().map(|ps| ps.iter().fold(Vec3::ZERO, |a, p| a + *p) * (1.0 / ps.len().max(1) as f64)).collect()
|
||||
}
|
||||
|
||||
/// The name of a face new to `f`.
|
||||
/// The name of a face new to `f` (a feature that went through `eval::naming_feature`).
|
||||
fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usize, fid: u64, curves: &std::cell::OnceCell<Curves>) -> String {
|
||||
let c = ps.iter().fold(Vec3::ZERO, |a, p| a + *p) * (1.0 / ps.len().max(1) as f64);
|
||||
match &f.kind {
|
||||
@@ -855,7 +858,10 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
|
||||
}
|
||||
FeatureKind::Revolve { sketch, axis, .. } => {
|
||||
let Some(ss) = before.sketch(sketch.0) else { return format!("F{fid}:face:{fi}") };
|
||||
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis, before, &|name| crate::eval::construction_axis_in(None, before, name, 0)) else {
|
||||
// The origin was written as its values when the feature was evaluated (`naming_feature`).
|
||||
let Ok((o2, d2)) =
|
||||
crate::eval::revolve_axis(&Default::default(), ss, axis, before, &|name| crate::eval::construction_axis_in(None, before, name, 0))
|
||||
else {
|
||||
return format!("F{fid}:face:{fi}");
|
||||
};
|
||||
let (o, d) = (ss.plane.to_world(o2), (ss.plane.to_world(o2 + d2) - ss.plane.to_world(o2)).normalized().unwrap_or(Vec3::Z));
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::expr::{self, Expr, Kind, ParamDef, Value};
|
||||
use crate::sheet::rule_references;
|
||||
use crate::walk::Walk;
|
||||
use crate::{DocError, Document, FeatureKind, MAX_PARAMS, Result};
|
||||
|
||||
@@ -797,13 +798,14 @@ impl Document {
|
||||
}
|
||||
}
|
||||
let uses = |e: &str| expr::references(e).iter().any(|r| r == name);
|
||||
let rule_uses = |field: &str, e: &str| rule_references(field, e).iter().any(|r| r == name);
|
||||
for r in &self.sheet.rules {
|
||||
for (field, _, _) in r.fields().into_iter().filter(|(_, e, _)| uses(e)) {
|
||||
for (field, _, _) in r.fields().into_iter().filter(|(f, e, _)| rule_uses(f, e)) {
|
||||
out.push(ParamUser::Rule { kind: "sheet metal".into(), name: r.name.clone(), field: field.into() });
|
||||
}
|
||||
}
|
||||
for r in &self.plastic.rules {
|
||||
for (field, _, _) in r.fields().into_iter().filter(|(_, e, _)| uses(e)) {
|
||||
for (field, _, _) in r.fields().into_iter().filter(|(f, e, _)| rule_uses(f, e)) {
|
||||
out.push(ParamUser::Rule { kind: "plastic".into(), name: r.name.clone(), field: field.into() });
|
||||
}
|
||||
}
|
||||
@@ -855,6 +857,21 @@ impl Document {
|
||||
if self.name_taken(new) {
|
||||
return Err(DocError::Invalid(format!("there is already a parameter `{new}`")));
|
||||
}
|
||||
// Outside a rule's own thickness, `Thickness` is the rule's thickness (#9623): renaming to
|
||||
// it would turn those references into the rule's thickness and change their values.
|
||||
if new == "Thickness" {
|
||||
let uses_old = |field: &str, e: &str| field != "thickness" && expr::references(e).iter().any(|r| r == old);
|
||||
let sheet = self.sheet.rules.iter().flat_map(|r| r.fields().into_iter().map(move |(f, e, _)| ("sheet metal", &r.name, f, e)));
|
||||
let plastic = self.plastic.rules.iter().flat_map(|r| r.fields().into_iter().map(move |(f, e, _)| ("plastic", &r.name, f, e)));
|
||||
let list: Vec<String> =
|
||||
sheet.chain(plastic).filter(|(_, _, f, e)| uses_old(f, e)).map(|(kind, name, f, _)| format!("{kind} rule {name} ({f})")).collect();
|
||||
if !list.is_empty() {
|
||||
return Err(DocError::Invalid(format!(
|
||||
"`{old}` can't be renamed to `Thickness`: {} would read the rule's thickness instead",
|
||||
list.join(", ")
|
||||
)));
|
||||
}
|
||||
}
|
||||
let mut n = 0;
|
||||
let text = self.text_param(old).is_some();
|
||||
for p in &mut self.text_params {
|
||||
@@ -964,7 +981,8 @@ impl Document {
|
||||
}
|
||||
// Sheet metal and plastic rules and canvas planes hold expressions too (#4420).
|
||||
let rules = self.sheet.rules.iter_mut().flat_map(|r| r.fields_mut()).chain(self.plastic.rules.iter_mut().flat_map(|r| r.fields_mut()));
|
||||
for (_, e) in rules {
|
||||
// `Thickness` outside a rule's own thickness is the rule's, not the parameter (#9623).
|
||||
for (_, e) in rules.filter(|(field, _)| old != "Thickness" || *field == "thickness") {
|
||||
let r = expr::rename_reference(e, old, new);
|
||||
if r != *e {
|
||||
*e = r;
|
||||
@@ -1025,10 +1043,10 @@ impl Document {
|
||||
}
|
||||
// Sheet metal and plastic features rebuild when their rules' parameters change (#4420).
|
||||
if f.kind.is_sheet() {
|
||||
names.extend(self.sheet.rules.iter().flat_map(|r| r.fields()).flat_map(|(_, e, _)| expr::references(e)));
|
||||
names.extend(self.sheet.rules.iter().flat_map(|r| r.fields()).flat_map(|(f, e, _)| rule_references(f, e)));
|
||||
}
|
||||
if f.kind.is_plastic() {
|
||||
names.extend(self.plastic.rules.iter().flat_map(|r| r.fields()).flat_map(|(_, e, _)| expr::references(e)));
|
||||
names.extend(self.plastic.rules.iter().flat_map(|r| r.fields()).flat_map(|(f, e, _)| rule_references(f, e)));
|
||||
}
|
||||
out.extend(names.into_iter().map(|n| (format!("Feature:{}", f.name), n)));
|
||||
}
|
||||
@@ -1052,9 +1070,9 @@ impl Document {
|
||||
}
|
||||
// A rule that uses it changes every sheet metal (or plastic) feature: the evaluation
|
||||
// hashes the rules into their fingerprint.
|
||||
let uses = |e: &String| expr::references(e).iter().any(|r| affected.contains(r));
|
||||
let sheet = self.sheet.rules.iter().any(|r| r.fields().iter().any(|(_, e, _)| uses(e)));
|
||||
let plastic = self.plastic.rules.iter().any(|r| r.fields().iter().any(|(_, e, _)| uses(e)));
|
||||
let uses = |field: &str, e: &str| rule_references(field, e).iter().any(|r| affected.contains(r));
|
||||
let sheet = self.sheet.rules.iter().any(|r| r.fields().iter().any(|(f, e, _)| uses(f, e)));
|
||||
let plastic = self.plastic.rules.iter().any(|r| r.fields().iter().any(|(f, e, _)| uses(f, e)));
|
||||
let mut out = Vec::new();
|
||||
for f in &self.features {
|
||||
let mut hit = f.param_names.iter().any(|n| affected.contains(n));
|
||||
|
||||
@@ -167,7 +167,7 @@ impl Document {
|
||||
return Err(DocError::Invalid("the plastic thickness must be positive".into()));
|
||||
}
|
||||
let with_t = |e: &str, k: Kind| -> Result<f64> {
|
||||
let f = |n: &str| if n == "Thickness" && !vals.contains_key("Thickness") { Ok(Value::length(t)) } else { look(n) };
|
||||
let f = |n: &str| if n == "Thickness" { Ok(Value::length(t)) } else { look(n) };
|
||||
let v = expr::eval_with(e, &f)?.to_kind(k)?;
|
||||
if !v.is_finite() || v < 0.0 {
|
||||
return Err(DocError::Invalid(format!("plastic rule `{}`: `{e}` must not be negative", r.name)));
|
||||
|
||||
@@ -278,7 +278,7 @@ fn selection_indices(ss: &SolvedSketch, selection: &ProfileSel, mut notes: Optio
|
||||
.profiles
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, profile)| profile.region.contains(*point))
|
||||
.filter(|(_, profile)| profile.contains(*point))
|
||||
.min_by(|(_, a), (_, b)| a.area.total_cmp(&b.area))
|
||||
.ok_or_else(|| DocError::Unknown(format!("profile at {:?} in {}", [point.x, point.y], ss.name)))?;
|
||||
indices.push(index);
|
||||
|
||||
@@ -1034,8 +1034,16 @@ fn refresh_links_in(
|
||||
if !l.auto_face
|
||||
&& let (Some(name), LinkSource::Face { body, at }) = (&l.face_name, &mut source)
|
||||
{
|
||||
let (index, p) =
|
||||
st.body(body).and_then(|target| crate::naming::face_on_body(target, name, *at)).ok_or_else(|| DocError::reselect(name.as_str()))?;
|
||||
let (index, p) = st
|
||||
.body(body)
|
||||
.and_then(|target| {
|
||||
if l.kind == LinkKind::Project {
|
||||
crate::name_lookup::locate_projected_face(target, name, *at)
|
||||
} else {
|
||||
crate::naming::face_on_body(target, name, *at)
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| DocError::reselect(name.as_str()))?;
|
||||
*at = p;
|
||||
named_face = Some((body.clone(), index, p));
|
||||
}
|
||||
@@ -1611,7 +1619,36 @@ pub(crate) fn section(plane: &Plane, m: &Mesh, face: Option<u32>) -> Vec<LinkGeo
|
||||
// where they are) are kept when they take no more curves than the loose fit.
|
||||
let close = refine(fit_segments(&segs, size, true));
|
||||
let plain = |v: &[LinkGeom]| v.iter().all(|g| matches!(g, LinkGeom::Line(..) | LinkGeom::Arc { .. } | LinkGeom::Circle(..)));
|
||||
if plain(&close) && close.len() <= lossy.len() { close } else { lossy }
|
||||
// A loose fit with fewer curves can still be wrong: short faces beside a small round (2 mm
|
||||
// walls each side of an R2 corner) fit within its tolerance as one larger arc. The exact
|
||||
// crossings (on planes and rulings) show it: one of them off every curve of the fit.
|
||||
// Any other curve counts as near, and past a work limit nothing is checked: the loose fit
|
||||
// stands, as before.
|
||||
let tight = size * 1e-4;
|
||||
let checkable = exact.len().saturating_mul(close.len()) <= 4_000_000;
|
||||
let strays = |v: &[LinkGeom]| exact.iter().any(|p| !v.iter().any(|g| plain_distance(g, *p).is_none_or(|d| d <= tight)));
|
||||
if plain(&close) && (close.len() <= lossy.len() || (checkable && strays(&lossy) && !strays(&close))) { close } else { lossy }
|
||||
}
|
||||
|
||||
/// How far `p` lies from a line, arc or circle; None for any other curve.
|
||||
fn plain_distance(g: &LinkGeom, p: Vec2) -> Option<f64> {
|
||||
match *g {
|
||||
LinkGeom::Line(a, b) => {
|
||||
let d = b - a;
|
||||
let t = if d.len2() > 0.0 { ((p - a).dot(d) / d.len2()).clamp(0.0, 1.0) } else { 0.0 };
|
||||
Some(p.dist(a.lerp(b, t)))
|
||||
}
|
||||
LinkGeom::Circle(c, r) => Some((p.dist(c) - r).abs()),
|
||||
LinkGeom::Arc { c, a, b } => {
|
||||
let (s0, sw) = ((a - c).angle(), ((b - c).angle() - (a - c).angle()).rem_euclid(std::f64::consts::TAU));
|
||||
if ((p - c).angle() - s0).rem_euclid(std::f64::consts::TAU) <= sw + 1e-9 {
|
||||
Some((p.dist(c) - c.dist(a)).abs())
|
||||
} else {
|
||||
Some(p.dist(a).min(p.dist(b)))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The segments where the plane crosses the mesh's triangles (in plane coordinates, each with
|
||||
|
||||