Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65d288227e | |||
| fedb3bcdb1 | |||
| a51dfc9e1f | |||
| 9aaa8016e5 | |||
| 632ec4157c | |||
| c3ed8b1c07 | |||
| 8301d1c48e | |||
| 2a02eb71ac | |||
| 097b8b7550 | |||
| f120285c96 | |||
| af7ca3a8a2 | |||
| b31fddb98e | |||
| 27ddb725c7 | |||
| 2bf3d4e11f | |||
| 9852a2e758 | |||
| 17bac141c8 | |||
| 4b42ef50c7 | |||
| fdde3f9da9 | |||
| 74ea81519b | |||
| 769dfff1a5 | |||
| 3ef68c708f | |||
| 3ed1829cc7 | |||
| b982ccd8c4 | |||
| 2586b09f5a | |||
| a5f9ff45dd | |||
| 61360c3b0d | |||
| eec6da060f | |||
| f422bb1263 | |||
| 96ce31153c | |||
| a9c3c8bb8a | |||
| a147a7c083 | |||
| 89a11d75c0 | |||
| 84cad43154 | |||
| 5c280b9a94 | |||
| 1a953736ff | |||
| 4106332738 | |||
| 9559ad398a | |||
| bddb52c96a | |||
| a0fd39a7be | |||
| be87303683 | |||
| bd0cc4bb51 | |||
| 4c8ae5d078 | |||
| e33c371e99 | |||
| e104e241a3 | |||
| 3bca787213 | |||
| 21257c39c2 | |||
| ed6e123d54 | |||
| dad28abecf | |||
| d1f5d6e223 | |||
| 1b215b521d | |||
| 36a354e3a7 | |||
| a05a0f1053 | |||
| 60efbe39e3 | |||
| 9e20a373ca | |||
| 2f55a39c9c | |||
| 80fd2fbbf1 | |||
| c3d1ce8237 | |||
| 567bc50723 | |||
| 1a5fcd05d6 | |||
| 3a6153d18f | |||
| d273c45de6 | |||
| da46c660df | |||
| 92d1331978 | |||
| 01bbc0aa9f | |||
| dbbad6d4ee | |||
| 4f69e6540c | |||
| 55608665ef | |||
| 0fb4a3e868 | |||
| 2dc31a30d4 | |||
| 8b68eb7eac | |||
| 96679f3e2b | |||
| 3ba17bb517 | |||
| ae3b36ac88 | |||
| 5367ef4367 | |||
| caa1e6e196 | |||
| 8bd17cea42 | |||
| 4256775274 | |||
| 5962f56f87 | |||
| b6a45b8278 | |||
| 8ae90f9667 | |||
| 17f0d6bc68 | |||
| 8b1a722f06 | |||
| 63d81cc096 | |||
| 46a8f86e80 | |||
| 94279fe059 | |||
| 867df396ea | |||
| 858e5995d6 | |||
| edf57cec29 | |||
| 0c72ad56d2 | |||
| 92d3a2fde5 | |||
| b0048a6104 | |||
| 0db2abbeca | |||
| 9761d75699 | |||
| a06bdf5cfc | |||
| 030187452a | |||
| 198fbeb814 | |||
| 186096cbe6 | |||
| 3b982ca371 | |||
| aaa110218b | |||
| 46f80b769f | |||
| 26ca82735b | |||
| 408d30ea56 | |||
| 75261cd797 | |||
| 6f5d0038ab | |||
| 1814adb06a | |||
| 971b28c982 | |||
| f79cbc216c | |||
| 4dc1828138 | |||
| f843a3f83c | |||
| f0fd5c10b7 | |||
| 981d23d1d2 | |||
| 01648da72d | |||
| 3d3dd27439 | |||
| 6c60f1a9ed |
@@ -203,6 +203,8 @@ jobs:
|
||||
name: Snap ${{ matrix.arch }}
|
||||
needs: [version, linux]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# The Snap Store hasn't accepted SolidCraft yet, so a snap failure never holds up a release.
|
||||
continue-on-error: true
|
||||
# Repackages the Linux job's tarball, like the Flatpak. On 24.04 because the core24 base is
|
||||
# Ubuntu 24.04, which lets snapcraft build on the runner itself (--destructive-mode).
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
/.claude/worktrees/
|
||||
/target-snapshot
|
||||
__pycache__/
|
||||
.vscode/
|
||||
|
||||
# cargo-mutants output (docs/mutation-testing.md)
|
||||
mutants.out*/
|
||||
|
||||
@@ -33,9 +33,11 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
and calls through its function pointers), two functions in
|
||||
`apps/solidcraft/src/open_documents.rs` (macOS hands Finder files only to an Objective-C
|
||||
method), the Windows-only `apps/solidcraft/src/native_crash.rs` (the exception filter that
|
||||
records crashes inside native code such as graphics drivers) and the Windows-only
|
||||
records crashes inside native code such as graphics drivers), the Windows-only
|
||||
`apps/solidcraft/src/native_frame.rs` (the window procedure that answers the resize-border hit
|
||||
test of the window without OS decorations).
|
||||
test of the window without OS decorations) and the Windows-only
|
||||
`apps/solidcraft/src/space_mouse/windows_hid.rs` (asks Plug and Play which HID interfaces are 3D
|
||||
mice, so no other device is sent anything).
|
||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
||||
returns an error ("not supported yet: …"); it never panics.
|
||||
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
|
||||
|
||||
@@ -14,6 +14,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| examples/chebyshev-motion-trace.json | SolidCraft contributors | Original parametric Chebyshev lambda mechanism and associative-trace demo, constructed from analytic circle intersections; classical link proportions documented at https://doi.org/10.1002/rcs.70095; no external CAD geometry or artwork | MIT OR Apache-2.0 |
|
||||
| examples/render-studio.json | SolidCraft contributors | original procedural box/cylinder enclosure script for the integrated Render workspace | MIT OR Apache-2.0 |
|
||||
| examples/render-opaque-studio.json | SolidCraft contributors | original procedural sphere study varying opaque metal fraction and roughness independently; no external assets or engineering data | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/pbr.rs | SolidCraft contributors | original WGSL translation of SolidCraft's opaque surface and fixed studio reference, embedded as Rust source; no external shader code | MIT OR Apache-2.0 |
|
||||
@@ -107,7 +108,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/app/linux/exports/hicolor/48x48/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/linux/exports/hicolor/64x64/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft drawing code: the borderless mark, drawn from the faces and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/shared/source/windows_ico.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/windows/exports/solidcraft-executable.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
| assets/icons/app/windows/exports/solidcraft.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
|
||||
@@ -196,6 +197,11 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/ui/design/svg/flat/redo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/save.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_keyboard.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_monitor.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_mouse.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_palette.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_plug.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/spin.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/style.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/undo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
@@ -336,6 +342,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/ui/design/svg/flat/send.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/stop.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/shield.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
|
||||
@@ -996,6 +996,15 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.8"
|
||||
@@ -2271,6 +2280,16 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyboard-types"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "3.6.3"
|
||||
@@ -2480,6 +2499,24 @@ dependencies = [
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
"keyboard-types",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"png",
|
||||
"thiserror 2.0.21",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multiversion_no_op"
|
||||
version = "1.0.0"
|
||||
@@ -4162,7 +4199,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4172,6 +4209,7 @@ dependencies = [
|
||||
"hidapi",
|
||||
"keyring",
|
||||
"log",
|
||||
"muda",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
@@ -4193,11 +4231,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4209,7 +4247,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4218,7 +4256,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4235,7 +4273,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"schemars",
|
||||
@@ -4254,7 +4292,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4263,7 +4301,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"naga",
|
||||
@@ -4280,7 +4318,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
@@ -4303,7 +4341,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4323,7 +4361,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -4332,7 +4370,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4346,7 +4384,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4357,14 +4395,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4373,6 +4411,7 @@ dependencies = [
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"pollster 0.4.0",
|
||||
"proc-macro2",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4391,7 +4430,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -5921,7 +5960,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"serde_json",
|
||||
|
||||
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.19.0"
|
||||
version = "0.21.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -70,6 +70,11 @@ pointer movement orbits without a mouse button, and vertical scrolling zooms. Ot
|
||||
retain their saved two-finger scroll preference; camera motion is available through
|
||||
`view.navigate` and modifier-aware `ui.move` automation.
|
||||
|
||||
UI redesign (#2016): the File/start view (#2027) uses shared light/dark navigation,
|
||||
action tiles, recent rows and sample cards, with recovery review and Save As/Export
|
||||
entry points. Document loading, sample caches and recovery safety retain their
|
||||
existing command paths.
|
||||
|
||||
M11 precursor: File → Export Current View as DXF exports shown placed bodies and sketches from the current
|
||||
camera as a 2D millimetre drawing, with hidden edges omitted or dashed. Vector occlusion splits
|
||||
partly hidden edges and includes curved silhouettes. Repeated part meshes and triangle
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Third-party software in SolidCraft
|
||||
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 560 crates below.
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 563 crates below.
|
||||
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
||||
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
||||
|
||||
@@ -95,6 +95,7 @@ core_detect 1.0.0 (MIT/Apache-2.0)
|
||||
cpufeatures 0.2.17 (MIT OR Apache-2.0)
|
||||
cpufeatures 0.3.1 (MIT OR Apache-2.0)
|
||||
crc32fast 1.5.2 (MIT OR Apache-2.0)
|
||||
crossbeam-channel 0.5.17 (MIT OR Apache-2.0)
|
||||
crossbeam-deque 0.8.8 (MIT OR Apache-2.0)
|
||||
crossbeam-epoch 0.9.21 (MIT OR Apache-2.0)
|
||||
crossbeam-utils 0.8.23 (MIT OR Apache-2.0)
|
||||
@@ -210,6 +211,7 @@ jni-sys 0.4.1 (MIT OR Apache-2.0)
|
||||
jni-sys-macros 0.4.1 (MIT OR Apache-2.0)
|
||||
js-sys 0.3.106 (MIT OR Apache-2.0)
|
||||
katexit 0.1.5 (MIT OR Apache-2.0)
|
||||
keyboard-types 0.8.3 (MIT OR Apache-2.0)
|
||||
keyring 3.6.3 (MIT OR Apache-2.0)
|
||||
khronos-egl 6.0.0 (MIT/Apache-2.0)
|
||||
kurbo 0.13.1 (Apache-2.0 OR MIT)
|
||||
@@ -234,6 +236,7 @@ minisign-verify 0.3.0 (MIT)
|
||||
miniz_oxide 0.8.9 (MIT OR Zlib OR Apache-2.0)
|
||||
miniz_oxide 0.9.1 (MIT OR Zlib OR Apache-2.0)
|
||||
moxcms 0.8.1 (BSD-3-Clause OR Apache-2.0)
|
||||
muda 0.21.2 (Apache-2.0 OR MIT)
|
||||
multiversion_no_op 1.0.0 (Apache-2.0 OR MIT)
|
||||
naga 30.0.1 (MIT OR Apache-2.0)
|
||||
naga-types 30.0.1 (MIT OR Apache-2.0)
|
||||
@@ -1108,26 +1111,27 @@ ahash 0.8.12, arrayvec 0.7.8, async-channel 2.5.0, async-executor 1.14.0, async-
|
||||
4.7.1, atomic-waker 1.1.2, base64 0.23.1, bitflags 1.3.2, bitflags 2.13.2, blocking 1.7.0, bumpalo
|
||||
3.20.3, cfg-if 1.0.5, cgl 0.3.2, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation
|
||||
0.9.4, core-foundation-sys 0.8.7, core-graphics 0.23.2, core-graphics-types 0.1.3, core_detect
|
||||
1.0.0, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23, dbus-secret-service
|
||||
4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14, euclid 0.22.14,
|
||||
event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10, form_urlencoded
|
||||
1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1, gltf-derive
|
||||
1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse 1.10.1, idna
|
||||
1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys 0.3.106,
|
||||
khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1, linux-raw-sys 0.4.15, lock_api 0.4.14,
|
||||
log 0.4.34, num 0.4.3, num-bigint 0.4.8, num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46,
|
||||
num-rational 0.4.2, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0,
|
||||
parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5,
|
||||
plain 0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core
|
||||
1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix
|
||||
0.38.44, rustix 1.1.5, rustls 0.23.46, rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard
|
||||
1.2.0, secret-service 4.0.0, security-framework 2.11.1, security-framework 3.7.0,
|
||||
security-framework-sys 2.17.0, signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2,
|
||||
smol_str 0.2.2, stable_deref_trait 1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1,
|
||||
unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, wasi
|
||||
0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, wasm-bindgen-futures
|
||||
0.4.79, wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129,
|
||||
web-sys 0.3.106, weezl 0.1.12, wit-bindgen 0.57.1
|
||||
1.0.0, crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils
|
||||
0.8.23, dbus-secret-service 4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14,
|
||||
euclid 0.22.14, event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10,
|
||||
form_urlencoded 1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1,
|
||||
gltf-derive 1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse
|
||||
1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools 0.15.0, js-sys
|
||||
0.3.106, keyboard-types 0.8.3, khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1,
|
||||
linux-raw-sys 0.4.15, lock_api 0.4.14, log 0.4.34, muda 0.21.2, num 0.4.3, num-bigint 0.4.8,
|
||||
num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46, num-rational 0.4.2, num-traits 0.2.19,
|
||||
once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5,
|
||||
parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, plain 0.2.3, png 0.18.1, polling
|
||||
3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core 1.13.0, renderdoc-sys 1.1.0, robust
|
||||
1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix 0.38.44, rustix 1.1.5, rustls 0.23.46,
|
||||
rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard 1.2.0, secret-service 4.0.0,
|
||||
security-framework 2.11.1, security-framework 3.7.0, security-framework-sys 2.17.0,
|
||||
signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait
|
||||
1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1, unicode-segmentation 1.13.3, unicode-width
|
||||
0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12,
|
||||
wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129,
|
||||
wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12,
|
||||
wit-bindgen 0.57.1
|
||||
================================================================================
|
||||
|
||||
Apache License
|
||||
@@ -4030,7 +4034,605 @@ Apache License
|
||||
limitations under the License.
|
||||
|
||||
================================================================================
|
||||
crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23
|
||||
crossbeam-channel 0.5.17
|
||||
================================================================================
|
||||
|
||||
===============================================================================
|
||||
|
||||
matching.go
|
||||
https://creativecommons.org/licenses/by/3.0/legalcode
|
||||
|
||||
Creative Commons Legal Code
|
||||
|
||||
Attribution 3.0 Unported
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
|
||||
DAMAGES RESULTING FROM ITS USE.
|
||||
|
||||
License
|
||||
|
||||
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
|
||||
COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
|
||||
COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
|
||||
AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
|
||||
|
||||
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
|
||||
TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
|
||||
BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
|
||||
CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
|
||||
CONDITIONS.
|
||||
|
||||
1. Definitions
|
||||
|
||||
a. "Adaptation" means a work based upon the Work, or upon the Work and
|
||||
other pre-existing works, such as a translation, adaptation,
|
||||
derivative work, arrangement of music or other alterations of a
|
||||
literary or artistic work, or phonogram or performance and includes
|
||||
cinematographic adaptations or any other form in which the Work may be
|
||||
recast, transformed, or adapted including in any form recognizably
|
||||
derived from the original, except that a work that constitutes a
|
||||
Collection will not be considered an Adaptation for the purpose of
|
||||
this License. For the avoidance of doubt, where the Work is a musical
|
||||
work, performance or phonogram, the synchronization of the Work in
|
||||
timed-relation with a moving image ("synching") will be considered an
|
||||
Adaptation for the purpose of this License.
|
||||
b. "Collection" means a collection of literary or artistic works, such as
|
||||
encyclopedias and anthologies, or performances, phonograms or
|
||||
broadcasts, or other works or subject matter other than works listed
|
||||
in Section 1(f) below, which, by reason of the selection and
|
||||
arrangement of their contents, constitute intellectual creations, in
|
||||
which the Work is included in its entirety in unmodified form along
|
||||
with one or more other contributions, each constituting separate and
|
||||
independent works in themselves, which together are assembled into a
|
||||
collective whole. A work that constitutes a Collection will not be
|
||||
considered an Adaptation (as defined above) for the purposes of this
|
||||
License.
|
||||
c. "Distribute" means to make available to the public the original and
|
||||
copies of the Work or Adaptation, as appropriate, through sale or
|
||||
other transfer of ownership.
|
||||
d. "Licensor" means the individual, individuals, entity or entities that
|
||||
offer(s) the Work under the terms of this License.
|
||||
e. "Original Author" means, in the case of a literary or artistic work,
|
||||
the individual, individuals, entity or entities who created the Work
|
||||
or if no individual or entity can be identified, the publisher; and in
|
||||
addition (i) in the case of a performance the actors, singers,
|
||||
musicians, dancers, and other persons who act, sing, deliver, declaim,
|
||||
play in, interpret or otherwise perform literary or artistic works or
|
||||
expressions of folklore; (ii) in the case of a phonogram the producer
|
||||
being the person or legal entity who first fixes the sounds of a
|
||||
performance or other sounds; and, (iii) in the case of broadcasts, the
|
||||
organization that transmits the broadcast.
|
||||
f. "Work" means the literary and/or artistic work offered under the terms
|
||||
of this License including without limitation any production in the
|
||||
literary, scientific and artistic domain, whatever may be the mode or
|
||||
form of its expression including digital form, such as a book,
|
||||
pamphlet and other writing; a lecture, address, sermon or other work
|
||||
of the same nature; a dramatic or dramatico-musical work; a
|
||||
choreographic work or entertainment in dumb show; a musical
|
||||
composition with or without words; a cinematographic work to which are
|
||||
assimilated works expressed by a process analogous to cinematography;
|
||||
a work of drawing, painting, architecture, sculpture, engraving or
|
||||
lithography; a photographic work to which are assimilated works
|
||||
expressed by a process analogous to photography; a work of applied
|
||||
art; an illustration, map, plan, sketch or three-dimensional work
|
||||
relative to geography, topography, architecture or science; a
|
||||
performance; a broadcast; a phonogram; a compilation of data to the
|
||||
extent it is protected as a copyrightable work; or a work performed by
|
||||
a variety or circus performer to the extent it is not otherwise
|
||||
considered a literary or artistic work.
|
||||
g. "You" means an individual or entity exercising rights under this
|
||||
License who has not previously violated the terms of this License with
|
||||
respect to the Work, or who has received express permission from the
|
||||
Licensor to exercise rights under this License despite a previous
|
||||
violation.
|
||||
h. "Publicly Perform" means to perform public recitations of the Work and
|
||||
to communicate to the public those public recitations, by any means or
|
||||
process, including by wire or wireless means or public digital
|
||||
performances; to make available to the public Works in such a way that
|
||||
members of the public may access these Works from a place and at a
|
||||
place individually chosen by them; to perform the Work to the public
|
||||
by any means or process and the communication to the public of the
|
||||
performances of the Work, including by public digital performance; to
|
||||
broadcast and rebroadcast the Work by any means including signs,
|
||||
sounds or images.
|
||||
i. "Reproduce" means to make copies of the Work by any means including
|
||||
without limitation by sound or visual recordings and the right of
|
||||
fixation and reproducing fixations of the Work, including storage of a
|
||||
protected performance or phonogram in digital form or other electronic
|
||||
medium.
|
||||
|
||||
2. Fair Dealing Rights. Nothing in this License is intended to reduce,
|
||||
limit, or restrict any uses free from copyright or rights arising from
|
||||
limitations or exceptions that are provided for in connection with the
|
||||
copyright protection under copyright law or other applicable laws.
|
||||
|
||||
3. License Grant. Subject to the terms and conditions of this License,
|
||||
Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
perpetual (for the duration of the applicable copyright) license to
|
||||
exercise the rights in the Work as stated below:
|
||||
|
||||
a. to Reproduce the Work, to incorporate the Work into one or more
|
||||
Collections, and to Reproduce the Work as incorporated in the
|
||||
Collections;
|
||||
b. to create and Reproduce Adaptations provided that any such Adaptation,
|
||||
including any translation in any medium, takes reasonable steps to
|
||||
clearly label, demarcate or otherwise identify that changes were made
|
||||
to the original Work. For example, a translation could be marked "The
|
||||
original work was translated from English to Spanish," or a
|
||||
modification could indicate "The original work has been modified.";
|
||||
c. to Distribute and Publicly Perform the Work including as incorporated
|
||||
in Collections; and,
|
||||
d. to Distribute and Publicly Perform Adaptations.
|
||||
e. For the avoidance of doubt:
|
||||
|
||||
i. Non-waivable Compulsory License Schemes. In those jurisdictions in
|
||||
which the right to collect royalties through any statutory or
|
||||
compulsory licensing scheme cannot be waived, the Licensor
|
||||
reserves the exclusive right to collect such royalties for any
|
||||
exercise by You of the rights granted under this License;
|
||||
ii. Waivable Compulsory License Schemes. In those jurisdictions in
|
||||
which the right to collect royalties through any statutory or
|
||||
compulsory licensing scheme can be waived, the Licensor waives the
|
||||
exclusive right to collect such royalties for any exercise by You
|
||||
of the rights granted under this License; and,
|
||||
iii. Voluntary License Schemes. The Licensor waives the right to
|
||||
collect royalties, whether individually or, in the event that the
|
||||
Licensor is a member of a collecting society that administers
|
||||
voluntary licensing schemes, via that society, from any exercise
|
||||
by You of the rights granted under this License.
|
||||
|
||||
The above rights may be exercised in all media and formats whether now
|
||||
known or hereafter devised. The above rights include the right to make
|
||||
such modifications as are technically necessary to exercise the rights in
|
||||
other media and formats. Subject to Section 8(f), all rights not expressly
|
||||
granted by Licensor are hereby reserved.
|
||||
|
||||
4. Restrictions. The license granted in Section 3 above is expressly made
|
||||
subject to and limited by the following restrictions:
|
||||
|
||||
a. You may Distribute or Publicly Perform the Work only under the terms
|
||||
of this License. You must include a copy of, or the Uniform Resource
|
||||
Identifier (URI) for, this License with every copy of the Work You
|
||||
Distribute or Publicly Perform. You may not offer or impose any terms
|
||||
on the Work that restrict the terms of this License or the ability of
|
||||
the recipient of the Work to exercise the rights granted to that
|
||||
recipient under the terms of the License. You may not sublicense the
|
||||
Work. You must keep intact all notices that refer to this License and
|
||||
to the disclaimer of warranties with every copy of the Work You
|
||||
Distribute or Publicly Perform. When You Distribute or Publicly
|
||||
Perform the Work, You may not impose any effective technological
|
||||
measures on the Work that restrict the ability of a recipient of the
|
||||
Work from You to exercise the rights granted to that recipient under
|
||||
the terms of the License. This Section 4(a) applies to the Work as
|
||||
incorporated in a Collection, but this does not require the Collection
|
||||
apart from the Work itself to be made subject to the terms of this
|
||||
License. If You create a Collection, upon notice from any Licensor You
|
||||
must, to the extent practicable, remove from the Collection any credit
|
||||
as required by Section 4(b), as requested. If You create an
|
||||
Adaptation, upon notice from any Licensor You must, to the extent
|
||||
practicable, remove from the Adaptation any credit as required by
|
||||
Section 4(b), as requested.
|
||||
b. If You Distribute, or Publicly Perform the Work or any Adaptations or
|
||||
Collections, You must, unless a request has been made pursuant to
|
||||
Section 4(a), keep intact all copyright notices for the Work and
|
||||
provide, reasonable to the medium or means You are utilizing: (i) the
|
||||
name of the Original Author (or pseudonym, if applicable) if supplied,
|
||||
and/or if the Original Author and/or Licensor designate another party
|
||||
or parties (e.g., a sponsor institute, publishing entity, journal) for
|
||||
attribution ("Attribution Parties") in Licensor's copyright notice,
|
||||
terms of service or by other reasonable means, the name of such party
|
||||
or parties; (ii) the title of the Work if supplied; (iii) to the
|
||||
extent reasonably practicable, the URI, if any, that Licensor
|
||||
specifies to be associated with the Work, unless such URI does not
|
||||
refer to the copyright notice or licensing information for the Work;
|
||||
and (iv) , consistent with Section 3(b), in the case of an Adaptation,
|
||||
a credit identifying the use of the Work in the Adaptation (e.g.,
|
||||
"French translation of the Work by Original Author," or "Screenplay
|
||||
based on original Work by Original Author"). The credit required by
|
||||
this Section 4 (b) may be implemented in any reasonable manner;
|
||||
provided, however, that in the case of a Adaptation or Collection, at
|
||||
a minimum such credit will appear, if a credit for all contributing
|
||||
authors of the Adaptation or Collection appears, then as part of these
|
||||
credits and in a manner at least as prominent as the credits for the
|
||||
other contributing authors. For the avoidance of doubt, You may only
|
||||
use the credit required by this Section for the purpose of attribution
|
||||
in the manner set out above and, by exercising Your rights under this
|
||||
License, You may not implicitly or explicitly assert or imply any
|
||||
connection with, sponsorship or endorsement by the Original Author,
|
||||
Licensor and/or Attribution Parties, as appropriate, of You or Your
|
||||
use of the Work, without the separate, express prior written
|
||||
permission of the Original Author, Licensor and/or Attribution
|
||||
Parties.
|
||||
c. Except as otherwise agreed in writing by the Licensor or as may be
|
||||
otherwise permitted by applicable law, if You Reproduce, Distribute or
|
||||
Publicly Perform the Work either by itself or as part of any
|
||||
Adaptations or Collections, You must not distort, mutilate, modify or
|
||||
take other derogatory action in relation to the Work which would be
|
||||
prejudicial to the Original Author's honor or reputation. Licensor
|
||||
agrees that in those jurisdictions (e.g. Japan), in which any exercise
|
||||
of the right granted in Section 3(b) of this License (the right to
|
||||
make Adaptations) would be deemed to be a distortion, mutilation,
|
||||
modification or other derogatory action prejudicial to the Original
|
||||
Author's honor and reputation, the Licensor will waive or not assert,
|
||||
as appropriate, this Section, to the fullest extent permitted by the
|
||||
applicable national law, to enable You to reasonably exercise Your
|
||||
right under Section 3(b) of this License (right to make Adaptations)
|
||||
but not otherwise.
|
||||
|
||||
5. Representations, Warranties and Disclaimer
|
||||
|
||||
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
|
||||
OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
|
||||
KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
|
||||
INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
|
||||
LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
|
||||
WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
|
||||
OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
|
||||
|
||||
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
|
||||
LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
|
||||
ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
|
||||
ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
|
||||
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
7. Termination
|
||||
|
||||
a. This License and the rights granted hereunder will terminate
|
||||
automatically upon any breach by You of the terms of this License.
|
||||
Individuals or entities who have received Adaptations or Collections
|
||||
from You under this License, however, will not have their licenses
|
||||
terminated provided such individuals or entities remain in full
|
||||
compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
|
||||
survive any termination of this License.
|
||||
b. Subject to the above terms and conditions, the license granted here is
|
||||
perpetual (for the duration of the applicable copyright in the Work).
|
||||
Notwithstanding the above, Licensor reserves the right to release the
|
||||
Work under different license terms or to stop distributing the Work at
|
||||
any time; provided, however that any such election will not serve to
|
||||
withdraw this License (or any other license that has been, or is
|
||||
required to be, granted under the terms of this License), and this
|
||||
License will continue in full force and effect unless terminated as
|
||||
stated above.
|
||||
|
||||
8. Miscellaneous
|
||||
|
||||
a. Each time You Distribute or Publicly Perform the Work or a Collection,
|
||||
the Licensor offers to the recipient a license to the Work on the same
|
||||
terms and conditions as the license granted to You under this License.
|
||||
b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
|
||||
offers to the recipient a license to the original Work on the same
|
||||
terms and conditions as the license granted to You under this License.
|
||||
c. If any provision of this License is invalid or unenforceable under
|
||||
applicable law, it shall not affect the validity or enforceability of
|
||||
the remainder of the terms of this License, and without further action
|
||||
by the parties to this agreement, such provision shall be reformed to
|
||||
the minimum extent necessary to make such provision valid and
|
||||
enforceable.
|
||||
d. No term or provision of this License shall be deemed waived and no
|
||||
breach consented to unless such waiver or consent shall be in writing
|
||||
and signed by the party to be charged with such waiver or consent.
|
||||
e. This License constitutes the entire agreement between the parties with
|
||||
respect to the Work licensed here. There are no understandings,
|
||||
agreements or representations with respect to the Work not specified
|
||||
here. Licensor shall not be bound by any additional provisions that
|
||||
may appear in any communication from You. This License may not be
|
||||
modified without the mutual written agreement of the Licensor and You.
|
||||
f. The rights granted under, and the subject matter referenced, in this
|
||||
License were drafted utilizing the terminology of the Berne Convention
|
||||
for the Protection of Literary and Artistic Works (as amended on
|
||||
September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
|
||||
Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
|
||||
and the Universal Copyright Convention (as revised on July 24, 1971).
|
||||
These rights and subject matter take effect in the relevant
|
||||
jurisdiction in which the License terms are sought to be enforced
|
||||
according to the corresponding provisions of the implementation of
|
||||
those treaty provisions in the applicable national law. If the
|
||||
standard suite of rights granted under applicable copyright law
|
||||
includes additional rights not granted under this License, such
|
||||
additional rights are deemed to be included in the License; this
|
||||
License is not intended to restrict the license of any rights under
|
||||
applicable law.
|
||||
|
||||
|
||||
Creative Commons Notice
|
||||
|
||||
Creative Commons is not a party to this License, and makes no warranty
|
||||
whatsoever in connection with the Work. Creative Commons will not be
|
||||
liable to You or any party on any legal theory for any damages
|
||||
whatsoever, including without limitation any general, special,
|
||||
incidental or consequential damages arising in connection to this
|
||||
license. Notwithstanding the foregoing two (2) sentences, if Creative
|
||||
Commons has expressly identified itself as the Licensor hereunder, it
|
||||
shall have all rights and obligations of Licensor.
|
||||
|
||||
Except for the limited purpose of indicating to the public that the
|
||||
Work is licensed under the CCPL, Creative Commons does not authorize
|
||||
the use by either party of the trademark "Creative Commons" or any
|
||||
related trademark or logo of Creative Commons without the prior
|
||||
written consent of Creative Commons. Any permitted use will be in
|
||||
compliance with Creative Commons' then-current trademark usage
|
||||
guidelines, as may be published on its website or otherwise made
|
||||
available upon request from time to time. For the avoidance of doubt,
|
||||
this trademark restriction does not form part of this License.
|
||||
|
||||
Creative Commons may be contacted at https://creativecommons.org/.
|
||||
|
||||
===============================================================================
|
||||
|
||||
The Go Programming Language
|
||||
https://golang.org/LICENSE
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
===============================================================================
|
||||
|
||||
The Rust Programming Language
|
||||
https://github.com/rust-lang/rust/blob/master/LICENSE-MIT
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
===============================================================================
|
||||
|
||||
The Rust Programming Language
|
||||
https://github.com/rust-lang/rust/blob/master/LICENSE-APACHE
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================================
|
||||
crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
@@ -7716,6 +8318,30 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
keyboard-types 0.8.3
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2017 Pyfisch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
keyring 3.6.3
|
||||
================================================================================
|
||||
@@ -9452,6 +10078,56 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================================
|
||||
muda 0.21.2
|
||||
================================================================================
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
muda 0.21.2
|
||||
================================================================================
|
||||
|
||||
SPDXVersion: SPDX-2.1
|
||||
DataLicense: CC0-1.0
|
||||
PackageName: muda
|
||||
DataFormat: SPDXRef-1
|
||||
PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
|
||||
PackageHomePage: https://tauri.app
|
||||
PackageLicenseDeclared: Apache-2.0
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: 2020-2022, The Tauri Programme in the Commons Conservancy
|
||||
PackageSummary: <text>Menu Utilities for Desktop Applications.
|
||||
</text>
|
||||
PackageComment: <text>The package includes the following libraries; see
|
||||
Relationship information.
|
||||
</text>
|
||||
Created: 2022-12-05T09:00:00Z
|
||||
PackageDownloadLocation: git://github.com/tauri-apps/muda
|
||||
PackageDownloadLocation: git+https://github.com/tauri-apps/muda.git
|
||||
PackageDownloadLocation: git+ssh://github.com/tauri-apps/muda.git
|
||||
Creator: Person: Daniel Thompson-Yvetot
|
||||
|
||||
================================================================================
|
||||
multiversion_no_op 1.0.0
|
||||
================================================================================
|
||||
|
||||
@@ -0,0 +1,700 @@
|
||||
//! `bench-assistant`: the standard Assistant model test (v1, #4851), run against a desktop app.
|
||||
//!
|
||||
//! The app is already running with `--control PORT`; everything goes through its control channel
|
||||
//! (`docs/control-protocol.md`), so the command needs no window code of its own. For each task it
|
||||
//! starts a new design, runs the task's prescribed setup (a box, for some), adds the endpoint as an
|
||||
//! API agent with All permissions, starts a new chat, sends the prompt once and waits for the turn
|
||||
//! to end. The result is checked on the design (`document.inspect` with measurements), never on
|
||||
//! what the model says, except for the one question whose answer is the reply. A failed task is
|
||||
//! put in one of the issue's failure categories from the conversation's items.
|
||||
//!
|
||||
//! ```text
|
||||
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT --base-url URL --model NAME
|
||||
//! [--token T] [--tasks 1,2,5] [--repeat N] [--timeout-s 300]
|
||||
//! [--json out.json] [--markdown out.md]
|
||||
//! ```
|
||||
//! prints a table of the results; `--markdown` writes the issue's comment template filled in,
|
||||
//! `--json` every task's rules, conversation and design before and after.
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// Standard test v1, as #4851 states it. Changing a prompt or a rule makes it another test.
|
||||
const TASKS: &str = r#"[
|
||||
{"id": "01-cube", "prompt": "Make a 20 mm cube.", "check": {"bodies": 1, "volume": 8000, "bbox_size": [20, 20, 20]}},
|
||||
{"id": "02-cylinder", "prompt": "Make a cylinder 30 mm in diameter and 50 mm tall.", "check": {"bodies": 1, "volume": 35342.92, "bbox_size": [30, 30, 50]}},
|
||||
{"id": "03-block", "prompt": "Create a block 60 mm long, 40 mm wide and 10 mm thick.", "check": {"bodies": 1, "volume": 24000, "bbox_size": [10, 40, 60]}},
|
||||
{"id": "04-sketch-extrude", "prompt": "Sketch a 50 by 30 mm rectangle on the XY plane and extrude it 15 mm.", "check": {"bodies": 1, "volume": 22500, "bbox_size": [15, 30, 50], "feature_kinds": ["Extrude"]}},
|
||||
{"id": "05-plate-hole", "prompt": "Make a plate 80 x 60 x 10 mm with a 20 mm diameter hole straight through the middle.", "check": {"bodies": 1, "volume": 44858.41, "bbox_size": [10, 60, 80], "note": "80*60*10 - pi*10^2*10"}},
|
||||
{"id": "06-fillet-box", "prompt": "Round all the edges of the box with a 3 mm radius.", "setup": [{"command": "solid.box", "params": {"length": 40, "width": 40, "height": 40}}], "check": {"bodies": 1, "volume": 63108.53, "tol": 0.003, "feature_kinds": ["Fillet"], "note": "all 12 edges of a 40 mm cube rounded r=3"}},
|
||||
{"id": "07-move-body", "prompt": "Move the box 50 mm along the X axis.", "setup": [{"command": "solid.box", "params": {"length": 20, "width": 20, "height": 20}}], "check": {"bodies": 1, "volume": 8000, "bbox_min": [50, 0, 0]}},
|
||||
{"id": "08-rename-body", "prompt": "Rename the body to Bracket.", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"bodies": 1, "volume": 6000, "body_name": "Bracket"}},
|
||||
{"id": "09-volume-question", "prompt": "What is the volume of the part?", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"bodies": 1, "volume": 6000, "unchanged": true, "reply_number": "6[ ,. ]?000(?![0-9])|6(\\.0+)? ?cm"}},
|
||||
{"id": "10-two-holes", "prompt": "Make a 100 x 50 x 5 mm plate and drill two 10 mm holes through it, each 15 mm in from the short ends and centred across the width.", "check": {"bodies": 1, "volume": 24214.6, "bbox_size": [5, 50, 100], "note": "100*50*5 - 2*pi*5^2*5"}},
|
||||
{"id": "11-any-change", "prompt": "can you use the software to make a change to the current shape, any change at all", "setup": [{"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}], "check": {"changed": true, "note": "verbatim user prompt from #4846; pass = the design changed"}}
|
||||
]"#;
|
||||
|
||||
/// The agent the test adds. A fixed id replaces it on every run instead of adding another.
|
||||
const AGENT_ID: &str = "api-bench-assistant";
|
||||
const AGENT_NAME: &str = "bench";
|
||||
/// Longest control reply read (the channel's own request limit).
|
||||
const MAX_REPLY: usize = 16 << 20;
|
||||
/// Most result data kept for `--json`.
|
||||
const MAX_RESULTS: usize = 64 << 20;
|
||||
/// Deadline of the requests that only set up or read (each is answered at once by the app).
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
|
||||
/// How often a running turn is polled.
|
||||
const POLL: Duration = Duration::from_millis(250);
|
||||
/// After this long, an idle chat with no reply yet counts as a turn that ended without one.
|
||||
const START_GRACE: Duration = Duration::from_secs(30);
|
||||
/// Absolute tolerance of the lowest corner, in millimetres.
|
||||
const MIN_CORNER_TOL: f64 = 0.05;
|
||||
|
||||
/// A JSON-lines connection to the app. Requests are sent once; a lost reply is never retried,
|
||||
/// so nothing runs twice.
|
||||
struct Control {
|
||||
addr: SocketAddr,
|
||||
token: String,
|
||||
reader: BufReader<TcpStream>,
|
||||
id: u64,
|
||||
}
|
||||
|
||||
impl Control {
|
||||
fn connect(addr: &str, token: Option<&str>) -> Result<Self, String> {
|
||||
let addr: SocketAddr = addr.parse().map_err(|_| format!("--connect: `{addr}` is not an IP address and port"))?;
|
||||
if !addr.ip().is_loopback() || addr.port() == 0 {
|
||||
return Err("--connect requires a loopback address and a nonzero port (the app listens on 127.0.0.1)".into());
|
||||
}
|
||||
let token = solidcraft_engine::control_auth::client_token(addr.port(), token)
|
||||
.ok_or("no control token: pass --token, set SOLIDCRAFT_CONTROL_TOKEN, or run on the app's computer (its token file)")?;
|
||||
if token.len() > 4096 {
|
||||
return Err("the control token is longer than 4096 bytes".into());
|
||||
}
|
||||
Self::open(addr, token)
|
||||
}
|
||||
|
||||
fn open(addr: SocketAddr, token: String) -> Result<Self, String> {
|
||||
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5)).map_err(|e| format!("connecting to {addr}: {e}"))?;
|
||||
Ok(Self { addr, token, reader: BufReader::new(stream), id: 0 })
|
||||
}
|
||||
|
||||
/// A new connection to the same app: a request that outlived its deadline keeps the old one
|
||||
/// busy until it is answered.
|
||||
fn reopen(&self) -> Result<Self, String> {
|
||||
Self::open(self.addr, self.token.clone())
|
||||
}
|
||||
|
||||
fn call(&mut self, method: &str, params: Value, deadline: Instant) -> Result<Value, String> {
|
||||
let left = || deadline.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()).ok_or_else(|| "timeout".to_string());
|
||||
let io = |e: std::io::Error| match e.kind() {
|
||||
std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => "timeout".to_string(),
|
||||
_ => format!("control channel: {e}"),
|
||||
};
|
||||
self.reader.get_ref().set_write_timeout(Some(left()?)).map_err(io)?;
|
||||
self.id += 1;
|
||||
// The first request on a connection carries the token; sending it on every one is allowed.
|
||||
let request = json!({"id": self.id, "method": method, "params": params, "token": self.token});
|
||||
writeln!(self.reader.get_mut(), "{request}").map_err(io)?;
|
||||
let mut line = Vec::new();
|
||||
loop {
|
||||
self.reader.get_ref().set_read_timeout(Some(left()?)).map_err(io)?;
|
||||
let bytes = self.reader.fill_buf().map_err(io)?;
|
||||
if bytes.is_empty() {
|
||||
return Err("the app closed the control connection".into());
|
||||
}
|
||||
let newline = bytes.iter().position(|b| *b == b'\n');
|
||||
let used = newline.map_or(bytes.len(), |n| n + 1);
|
||||
if line.len() + used > MAX_REPLY {
|
||||
return Err("a control reply is larger than 16 MiB".into());
|
||||
}
|
||||
line.extend_from_slice(bytes.get(..used).unwrap_or_default());
|
||||
self.reader.consume(used);
|
||||
if newline.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let reply: Value = serde_json::from_slice(&line).map_err(|e| format!("control reply: {e}"))?;
|
||||
if reply["id"].as_u64() != Some(self.id) {
|
||||
return Err("control reply for another request".into());
|
||||
}
|
||||
if reply["ok"] != true {
|
||||
return Err(format!("{method}: {}", reply["error"].as_str().unwrap_or("failed")));
|
||||
}
|
||||
reply.get("result").cloned().ok_or_else(|| format!("{method}: reply without a result"))
|
||||
}
|
||||
|
||||
fn execute(&mut self, command: &str, params: Value) -> Result<Value, String> {
|
||||
self.call("engine.execute", json!({"command": command, "params": params, "animate": false}), Instant::now() + REQUEST_TIMEOUT)
|
||||
}
|
||||
|
||||
fn chat(&mut self, params: Value) -> Result<Value, String> {
|
||||
self.call("ui.agent_chat", params, Instant::now() + REQUEST_TIMEOUT)
|
||||
}
|
||||
|
||||
fn inspect(&mut self) -> Result<Value, String> {
|
||||
let design = self.call("document.inspect", json!({"measure": true}), Instant::now() + REQUEST_TIMEOUT)?;
|
||||
if design.is_object() { Ok(design) } else { Err("document.inspect returned no design".into()) }
|
||||
}
|
||||
}
|
||||
|
||||
fn tasks() -> Result<Vec<Value>, String> {
|
||||
serde_json::from_str(TASKS).map_err(|e| format!("embedded tasks: {e}"))
|
||||
}
|
||||
|
||||
fn triple(value: &Value) -> Option<[f64; 3]> {
|
||||
match value.as_array()?.as_slice() {
|
||||
[x, y, z] => {
|
||||
let n = |v: &Value| v.as_f64().filter(|n| n.is_finite());
|
||||
Some([n(x)?, n(y)?, n(z)?])
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the model said (thoughts left out).
|
||||
fn reply(items: &[Value]) -> String {
|
||||
items.iter().filter(|i| i["thought"] != true).filter_map(|i| i["agent"].as_str()).collect::<Vec<_>>().join("\n")
|
||||
}
|
||||
|
||||
/// v1's reply pattern `6[ ,. ]?000(?![0-9])|6(\.0+)? ?cm`: "6000", "6 000", "6,000", "6.000" not
|
||||
/// followed by a digit, or "6 cm", "6.0cm", … (a narrow no-break space also separates thousands).
|
||||
fn volume_reply(text: &str) -> bool {
|
||||
text.match_indices('6').any(|(at, _)| {
|
||||
let tail = text.get(at + 1..).unwrap_or_default();
|
||||
let thousands = |t: &str| t.strip_prefix("000").is_some_and(|rest| !rest.starts_with(|c: char| c.is_ascii_digit()));
|
||||
let separated = tail.strip_prefix([' ', ',', '.', '\u{202f}']).is_some_and(thousands);
|
||||
let cm = {
|
||||
let t = tail.strip_prefix(".0").map_or(tail, |t| t.trim_start_matches('0'));
|
||||
t.strip_prefix(' ').unwrap_or(t).starts_with("cm")
|
||||
};
|
||||
thousands(tail) || separated || cm
|
||||
})
|
||||
}
|
||||
|
||||
/// The design as the user sees it, without what reading or saving changes (path, saved state,
|
||||
/// revision, selection, undo depth, evaluation timings).
|
||||
fn content(design: &Value) -> Value {
|
||||
let mut v = design.clone();
|
||||
if let Some(o) = v.as_object_mut() {
|
||||
for key in ["path", "dirty", "revision", "selection", "undo", "redo"] {
|
||||
o.remove(key);
|
||||
}
|
||||
}
|
||||
for feature in v.get_mut("timeline").and_then(Value::as_array_mut).into_iter().flatten() {
|
||||
if let Some(o) = feature.as_object_mut() {
|
||||
o.remove("ms");
|
||||
o.remove("boolean_diagnostics");
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Whether the design changed: its content, or a step on the undo stack (a colour, say, that the
|
||||
/// inspection does not list).
|
||||
fn changed(before: &Value, after: &Value) -> bool {
|
||||
content(before) != content(after) || after["undo"].as_u64() > before["undo"].as_u64()
|
||||
}
|
||||
|
||||
/// The rules a task's result breaks (none: it passed). `before` and `after` are
|
||||
/// `document.inspect {measure: true}` before the prompt and after the turn.
|
||||
pub(super) fn check(rule: &Value, before: &Value, after: &Value, items: &[Value]) -> Vec<String> {
|
||||
let Some(all) = after["bodies"].as_array() else { return vec!["the design could not be read".into()] };
|
||||
// Bodies the user sees; a hidden one (a tool body, a hidden original) is not the result.
|
||||
let bodies: Vec<&Value> = all.iter().filter(|b| b["visible"] != false).collect();
|
||||
let mut errors = Vec::new();
|
||||
if let Some(n) = rule["bodies"].as_u64()
|
||||
&& bodies.len() as u64 != n
|
||||
{
|
||||
errors.push(format!("{} bodies, expected {n}", bodies.len()));
|
||||
}
|
||||
if let Some(expected) = rule["volume"].as_f64() {
|
||||
let tol = rule["tol"].as_f64().unwrap_or(0.01);
|
||||
let volumes: Vec<f64> = bodies.iter().filter_map(|b| b["volume_mm3"].as_f64()).filter(|v| v.is_finite()).collect();
|
||||
let total: f64 = volumes.iter().sum();
|
||||
if volumes.len() != bodies.len() || (total - expected).abs() > expected * tol {
|
||||
errors.push(format!("volume {total:.1} mm³, expected {expected} mm³ ±{}%", tol * 100.0));
|
||||
}
|
||||
}
|
||||
let first = bodies.first().copied().unwrap_or(&Value::Null);
|
||||
let min = triple(&first["bbox"]["min"]);
|
||||
if let Some(mut expected) = triple(&rule["bbox_size"]) {
|
||||
expected.sort_by(f64::total_cmp);
|
||||
let size = min.zip(triple(&first["bbox"]["max"])).map(|(lo, hi)| {
|
||||
let mut size = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
|
||||
size.sort_by(f64::total_cmp);
|
||||
size
|
||||
});
|
||||
if !size.is_some_and(|s| s.iter().zip(expected).all(|(a, e)| (a - e).abs() <= 0.01 * e.max(1.0))) {
|
||||
errors.push(format!("size {size:?}, expected {expected:?} in any orientation"));
|
||||
}
|
||||
}
|
||||
if let Some(expected) = triple(&rule["bbox_min"])
|
||||
&& !min.is_some_and(|m| m.iter().zip(expected).all(|(a, e)| (a - e).abs() <= MIN_CORNER_TOL))
|
||||
{
|
||||
errors.push(format!("lowest corner {min:?}, expected {expected:?}"));
|
||||
}
|
||||
if let Some(name) = rule["body_name"].as_str()
|
||||
&& !bodies.iter().any(|b| b["name"] == name)
|
||||
{
|
||||
errors.push(format!("no body named {name}"));
|
||||
}
|
||||
for kind in rule["feature_kinds"].as_array().into_iter().flatten().filter_map(Value::as_str) {
|
||||
let live = |f: &&Value| f["suppressed"] != true && f["rolled_back"] != true && f["error"].is_null();
|
||||
if !after["timeline"].as_array().into_iter().flatten().filter(live).any(|f| f["type"].as_str().is_some_and(|t| t.contains(kind))) {
|
||||
errors.push(format!("no {kind} in the timeline"));
|
||||
}
|
||||
}
|
||||
if rule["changed"] == true || rule["unchanged"] == true {
|
||||
let changed = before.is_object() && changed(before, after);
|
||||
if rule["changed"] == true && !changed {
|
||||
errors.push("the design did not change".into());
|
||||
}
|
||||
if rule["unchanged"] == true && (changed || !before.is_object()) {
|
||||
errors.push("the design changed".into());
|
||||
}
|
||||
}
|
||||
if rule["reply_number"].is_string() && !volume_reply(&reply(items)) {
|
||||
errors.push("the reply does not give the volume".into());
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
||||
/// A tool call written into the reply as JSON: `{"name": …`, `{"command": …`, `{"function": …`,
|
||||
/// `{"tool": …`.
|
||||
fn call_as_text(text: &str) -> bool {
|
||||
text.match_indices('{').any(|(at, _)| {
|
||||
let rest = text.get(at + 1..).unwrap_or_default().trim_start();
|
||||
["\"name\"", "\"command\"", "\"function\"", "\"tool\""]
|
||||
.iter()
|
||||
.any(|key| rest.strip_prefix(key).is_some_and(|r| r.trim_start().starts_with(':')))
|
||||
})
|
||||
}
|
||||
|
||||
/// How a failed task failed, in the issue's words, from the conversation's items.
|
||||
pub(super) fn classify(items: &[Value], timed_out: bool) -> &'static str {
|
||||
if timed_out {
|
||||
return "timeout";
|
||||
}
|
||||
let tools: Vec<&Value> = items.iter().filter(|i| i.get("tool").is_some()).collect();
|
||||
// A failed call, or a batch whose answer reports a failed step (`"ok": false`).
|
||||
let failed: Vec<String> = tools
|
||||
.iter()
|
||||
.map(|t| t["output"].as_str().unwrap_or_default())
|
||||
.zip(&tools)
|
||||
.filter(|(output, t)| t["status"] == "Failed" || serde_json::from_str::<Value>(output).is_ok_and(|v| v["ok"] == false))
|
||||
.map(|(output, _)| output.to_lowercase())
|
||||
.collect();
|
||||
let notices: Vec<String> = items.iter().filter_map(|i| i["notice"].as_str()).map(str::to_lowercase).collect();
|
||||
let answer = reply(items);
|
||||
if notices.iter().any(|n| n.contains("http 400")) {
|
||||
return "the endpoint refused tools (HTTP 400)";
|
||||
}
|
||||
if notices.iter().any(|n| n.contains("max_turn_requests")) || answer.contains("requests for one message") {
|
||||
return "hit the request limit";
|
||||
}
|
||||
if call_as_text(&answer) {
|
||||
return "wrote the call as text";
|
||||
}
|
||||
if failed.iter().any(|o| o.contains("unknown command") || o.starts_with("unknown tool")) {
|
||||
return "wrong or unknown command";
|
||||
}
|
||||
if failed.iter().any(|o| o.contains("invalid parameters") || o.starts_with("not run:") || o.contains("not valid json")) {
|
||||
return "wrong arguments";
|
||||
}
|
||||
if !failed.is_empty() || notices.iter().any(|n| n.starts_with("http ")) || items.iter().any(|i| i["error"] == true) {
|
||||
return "gave up after errors";
|
||||
}
|
||||
if tools.is_empty() {
|
||||
return if answer.trim().is_empty() { "an empty reply" } else { "only talked" };
|
||||
}
|
||||
"wrong result"
|
||||
}
|
||||
|
||||
/// One run's results in the issue's comment template (`run`: which repetition).
|
||||
pub(super) fn markdown(model: &str, version: &str, results: &[Value], run: u64) -> String {
|
||||
let clean = |s: &str| s.replace(['\n', '\r', '|'], " ");
|
||||
let rows: Vec<&Value> = results.iter().filter(|r| r["repeat"] == run).collect();
|
||||
let passed = rows.iter().filter(|r| r["passed"] == true).count();
|
||||
let mut out = format!(
|
||||
"### {} (<quant>, <download size>)\nRuntime: <Ollama/LM Studio/... version>, context <num_ctx if changed>\nSolidCraft: {}\nResult: {passed}/11\n| # | pass/fail | how it failed |\n|---|---|---|\n",
|
||||
clean(model),
|
||||
clean(version)
|
||||
);
|
||||
for n in 1..=11 {
|
||||
match rows.iter().find(|r| r["task"] == n) {
|
||||
Some(r) if r["passed"] == true => out.push_str(&format!("| {n} | pass | |\n")),
|
||||
Some(r) => out.push_str(&format!("| {n} | fail | {} |\n", clean(r["failure"].as_str().unwrap_or_default()))),
|
||||
None => out.push_str(&format!("| {n} | | not run |\n")),
|
||||
}
|
||||
}
|
||||
out.push_str(if rows.len() == 11 {
|
||||
"Notes: <anything odd, e.g. very slow, needed a retry>\n"
|
||||
} else {
|
||||
"Notes: partial run; the tasks marked \"not run\" were left out.\n"
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
fn write(path: &str, text: &str) -> Result<(), String> {
|
||||
solidcraft_engine::io::vfs::write(path, text.as_bytes()).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
|
||||
struct Options<'a> {
|
||||
connect: &'a str,
|
||||
token: Option<&'a str>,
|
||||
base_url: &'a str,
|
||||
model: &'a str,
|
||||
tasks: Vec<usize>,
|
||||
repeat: u64,
|
||||
timeout: Duration,
|
||||
json: Option<&'a str>,
|
||||
markdown: Option<&'a str>,
|
||||
}
|
||||
|
||||
fn options(args: &[String]) -> Result<Options<'_>, String> {
|
||||
const FLAGS: [&str; 9] = ["--connect", "--token", "--base-url", "--model", "--tasks", "--repeat", "--timeout-s", "--json", "--markdown"];
|
||||
let mut given = std::collections::BTreeMap::new();
|
||||
let mut args = args.iter();
|
||||
while let Some(flag) = args.next() {
|
||||
let Some(flag) = FLAGS.iter().find(|f| **f == flag.as_str()) else { return Err(format!("unknown bench-assistant option: {flag}")) };
|
||||
let value = args
|
||||
.next()
|
||||
.filter(|v| !v.starts_with("--") && !v.is_empty() && v.len() <= 4096)
|
||||
.ok_or_else(|| format!("{flag} needs a value (at most 4096 bytes)"))?;
|
||||
if given.insert(*flag, value.as_str()).is_some() {
|
||||
return Err(format!("{flag} given twice"));
|
||||
}
|
||||
}
|
||||
let required = |key: &str| given.get(key).copied().ok_or_else(|| format!("bench-assistant needs {key}"));
|
||||
let number = |key: &str, default: u64, max: u64| -> Result<u64, String> {
|
||||
given
|
||||
.get(key)
|
||||
.map_or(Ok(default), |s| s.parse::<u64>().ok().filter(|n| (1..=max).contains(n)).ok_or_else(|| format!("{key} must be 1 to {max}")))
|
||||
};
|
||||
let tasks = match given.get("--tasks") {
|
||||
None => (1..=11).collect(),
|
||||
Some(list) => {
|
||||
let mut tasks = Vec::new();
|
||||
for n in list.split(',') {
|
||||
let n = n
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
.filter(|n| (1..=11).contains(n))
|
||||
.ok_or("--tasks takes task numbers 1 to 11, separated by commas")?;
|
||||
if tasks.contains(&n) {
|
||||
return Err(format!("--tasks lists task {n} twice"));
|
||||
}
|
||||
tasks.push(n);
|
||||
}
|
||||
tasks
|
||||
}
|
||||
};
|
||||
Ok(Options {
|
||||
connect: required("--connect")?,
|
||||
token: given.get("--token").copied(),
|
||||
base_url: required("--base-url")?,
|
||||
model: required("--model")?,
|
||||
tasks,
|
||||
repeat: number("--repeat", 1, 100)?,
|
||||
timeout: Duration::from_secs(number("--timeout-s", 300, 3600)?),
|
||||
json: given.get("--json").copied(),
|
||||
markdown: given.get("--markdown").copied(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Waits for the turn the prompt started to end. Returns the chat's last state and whether the
|
||||
/// deadline passed first.
|
||||
fn wait_for_turn(control: &mut Control, mut chat: Value, began: Instant, end: Instant) -> Result<(Value, bool), String> {
|
||||
let mut seen_busy = false;
|
||||
loop {
|
||||
let state = chat["state"].as_str().unwrap_or_default();
|
||||
let turn = !chat["turn"].is_null();
|
||||
let queued = chat["pending"].as_array().is_some_and(|p| !p.is_empty());
|
||||
seen_busy |= state == "busy" || turn;
|
||||
let idle = !matches!(state, "busy" | "starting") && !turn && !queued;
|
||||
// A turn can start and end between two polls: then the conversation has its answer.
|
||||
let answered = chat["items"].as_array().into_iter().flatten().any(|i| i.get("user").is_none());
|
||||
if idle && (seen_busy || answered || matches!(state, "exited" | "auth" | "off") || began.elapsed() >= START_GRACE) {
|
||||
return Ok((chat, false));
|
||||
}
|
||||
let Some(left) = end.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()) else { return Ok((chat, true)) };
|
||||
std::thread::sleep(POLL.min(left));
|
||||
match control.call("ui.agent_chat", json!({}), end) {
|
||||
Ok(next) => chat = next,
|
||||
Err(e) if e == "timeout" => return Ok((chat, true)),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let o = options(args)?;
|
||||
let tasks = tasks()?;
|
||||
let mut control = Control::connect(o.connect, o.token)?;
|
||||
let help = control.call("ui.help", json!({}), Instant::now() + REQUEST_TIMEOUT)?;
|
||||
let version = help["version"].as_str().filter(|s| !s.is_empty() && s.len() <= 256).ok_or("the app reported no version")?.to_string();
|
||||
let config = control.chat(json!({
|
||||
"features": true,
|
||||
"add_agent": {"id": AGENT_ID, "name": AGENT_NAME, "kind": "api", "base_url": o.base_url, "model": o.model},
|
||||
}))?;
|
||||
if !config["agents"].as_array().into_iter().flatten().any(|a| a["id"] == AGENT_ID) {
|
||||
return Err("the app did not add the benchmark's agent".into());
|
||||
}
|
||||
// Put the user's permission setting back afterwards.
|
||||
let permissions = config["permissions"].as_str().map(str::to_string);
|
||||
let result = run_tasks(&mut control, &o, &tasks, &version);
|
||||
let _ = control.chat(json!({"stop": true}));
|
||||
if let Some(p) = permissions {
|
||||
let _ = control.chat(json!({"permissions": p}));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn run_tasks(control: &mut Control, o: &Options, tasks: &[Value], version: &str) -> Result<(), String> {
|
||||
let mut results: Vec<Value> = Vec::new();
|
||||
let mut kept = 0;
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
let mut say = |line: String| writeln!(stdout, "{line}").map_err(|e| format!("stdout: {e}"));
|
||||
say(format!("bench-assistant v1: {} on SolidCraft {version}", o.model))?;
|
||||
say("| run | # | task | pass/fail | how it failed | seconds |\n|---|---|---|---|---|---|".into())?;
|
||||
for run in 1..=o.repeat {
|
||||
for &n in &o.tasks {
|
||||
let task = tasks.get(n - 1).ok_or("missing embedded task")?;
|
||||
// A fresh design and a fresh chat, with nothing of the last task still running.
|
||||
control.chat(json!({"stop": true}))?;
|
||||
control.execute("file.new", json!({"name": "Benchmark"}))?;
|
||||
for step in task["setup"].as_array().into_iter().flatten() {
|
||||
control.execute(step["command"].as_str().ok_or("embedded setup without a command")?, step["params"].clone())?;
|
||||
}
|
||||
control.chat(json!({"agent": AGENT_ID, "permissions": "all", "new_chat": true, "attach_view": false}))?;
|
||||
let before = control.inspect()?;
|
||||
let began = Instant::now();
|
||||
let end = began + o.timeout;
|
||||
let (chat, timed_out) = match control.call("ui.agent_chat", json!({"send": task["prompt"]}), end) {
|
||||
Ok(chat) => wait_for_turn(control, chat, began, end)?,
|
||||
Err(e) if e == "timeout" => (json!({}), true),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
if timed_out {
|
||||
// The late reply would arrive on the old connection; stop the model on a new one
|
||||
// before reading the design.
|
||||
*control = control.reopen()?;
|
||||
}
|
||||
// Stop the agent before reading the result, so nothing changes the design meanwhile.
|
||||
let stopped = control.chat(json!({"stop": true}))?;
|
||||
let items = stopped["items"].as_array().or(chat["items"].as_array()).cloned().unwrap_or_default();
|
||||
let after = control.inspect()?;
|
||||
let failed_rules = check(&task["check"], &before, &after, &items);
|
||||
let passed = failed_rules.is_empty();
|
||||
let failure = if passed { "" } else { classify(&items, timed_out) };
|
||||
let seconds = began.elapsed().as_secs_f64();
|
||||
say(format!(
|
||||
"| {run} | {n} | {} | {} | {failure} | {seconds:.0} |",
|
||||
task["id"].as_str().unwrap_or_default(),
|
||||
if passed { "pass" } else { "fail" }
|
||||
))?;
|
||||
let result = json!({
|
||||
"task": n, "id": task["id"], "prompt": task["prompt"], "repeat": run, "passed": passed, "failure": failure,
|
||||
"failed_rules": failed_rules, "timed_out": timed_out, "seconds": seconds, "check": task["check"],
|
||||
"items": items, "before": before, "after": after,
|
||||
});
|
||||
kept += result.to_string().len();
|
||||
if kept > MAX_RESULTS {
|
||||
return Err("the results passed 64 MiB; run fewer tasks or repetitions".into());
|
||||
}
|
||||
results.push(result);
|
||||
// Written after every task, so an interrupted run keeps what it measured.
|
||||
if let Some(path) = o.json {
|
||||
let all = json!({"test": "v1", "model": o.model, "base_url": o.base_url, "solidcraft": version, "results": results});
|
||||
write(path, &serde_json::to_string_pretty(&all).map_err(|e| e.to_string())?)?;
|
||||
}
|
||||
if let Some(path) = o.markdown {
|
||||
write(path, &(1..=run).map(|r| markdown(o.model, version, &results, r)).collect::<Vec<_>>().join("\n"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for run in 1..=o.repeat {
|
||||
let rows: Vec<&Value> = results.iter().filter(|r| r["repeat"] == run).collect();
|
||||
say(format!("run {run}: {}/{} passed", rows.iter().filter(|r| r["passed"] == true).count(), rows.len()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn design(bodies: Value, timeline: Value) -> Value {
|
||||
json!({"name": "Benchmark", "params": [], "timeline": timeline, "bodies": bodies, "sketches": [], "undo": 1, "revision": 3, "dirty": true})
|
||||
}
|
||||
|
||||
fn body(name: &str, volume: f64, min: [f64; 3], max: [f64; 3]) -> Value {
|
||||
json!({"name": name, "volume_mm3": volume, "bbox": {"min": min, "max": max}, "visible": true})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_embedded_tasks_are_v1() {
|
||||
let tasks = tasks().unwrap();
|
||||
assert_eq!(tasks.len(), 11);
|
||||
assert_eq!(tasks[0]["prompt"], "Make a 20 mm cube.");
|
||||
assert_eq!(tasks[10]["prompt"], "can you use the software to make a change to the current shape, any change at all");
|
||||
assert_eq!(tasks[5]["check"]["tol"], 0.003);
|
||||
assert_eq!(tasks[6]["check"]["bbox_min"], json!([50, 0, 0]));
|
||||
let boxes: Vec<_> = tasks.iter().filter(|t| t["setup"].is_array()).map(|t| t["id"].as_str().unwrap()).collect();
|
||||
assert_eq!(boxes, ["06-fillet-box", "07-move-body", "08-rename-body", "09-volume-question", "11-any-change"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pass_rules() {
|
||||
let tasks = tasks().unwrap();
|
||||
let rule = |n: usize| tasks[n - 1]["check"].clone();
|
||||
let empty = design(json!([]), json!([]));
|
||||
let pass = |n: usize, before: &Value, after: &Value, items: &[Value]| check(&rule(n), before, after, items);
|
||||
// 3: any orientation, within 1%.
|
||||
let block = design(json!([body("Body1", 24_100.0, [0.0; 3], [40.0, 10.0, 60.2])]), json!([{"type": "Box"}]));
|
||||
assert!(pass(3, &empty, &block, &[]).is_empty());
|
||||
let thick = design(json!([body("Body1", 24_000.0, [0.0; 3], [40.0, 12.0, 60.0])]), json!([]));
|
||||
assert_eq!(pass(3, &empty, &thick, &[]).len(), 1);
|
||||
let two = design(json!([body("A", 12_000.0, [0.0; 3], [60.0, 40.0, 5.0]), body("B", 12_000.0, [0.0; 3], [60.0, 40.0, 5.0])]), json!([]));
|
||||
assert!(!pass(3, &empty, &two, &[]).is_empty());
|
||||
// A hidden body is not part of the result.
|
||||
let mut hidden = two.clone();
|
||||
hidden["bodies"][1]["visible"] = json!(false);
|
||||
hidden["bodies"][0]["volume_mm3"] = json!(24_000.0);
|
||||
hidden["bodies"][0]["bbox"]["max"] = json!([60.0, 40.0, 10.0]);
|
||||
assert!(pass(3, &empty, &hidden, &[]).is_empty());
|
||||
// 4: the timeline needs a working Extrude.
|
||||
let plate = |kind: Value| design(json!([body("Body1", 22_500.0, [0.0; 3], [50.0, 30.0, 15.0])]), kind);
|
||||
assert!(pass(4, &empty, &plate(json!([{"type": "Sketch"}, {"type": "Extrude"}])), &[]).is_empty());
|
||||
assert!(!pass(4, &empty, &plate(json!([{"type": "Box"}])), &[]).is_empty());
|
||||
for flag in [json!({"suppressed": true}), json!({"rolled_back": true}), json!({"error": "failed"})] {
|
||||
let mut feature = json!({"type": "Extrude"});
|
||||
feature.as_object_mut().unwrap().extend(flag.as_object().unwrap().clone());
|
||||
assert!(!pass(4, &empty, &plate(json!([feature])), &[]).is_empty(), "{flag}");
|
||||
}
|
||||
// 6: the fillet's tolerance is 0.3%.
|
||||
let start = design(json!([body("Body1", 64_000.0, [0.0; 3], [40.0; 3])]), json!([{"type": "Box"}]));
|
||||
let filleted = |v: f64| design(json!([body("Body1", v, [0.0; 3], [40.0; 3])]), json!([{"type": "Box"}, {"type": "Fillet"}]));
|
||||
assert!(pass(6, &start, &filleted(63_108.53 * 1.002), &[]).is_empty());
|
||||
assert!(!pass(6, &start, &filleted(63_108.53 * 1.004), &[]).is_empty());
|
||||
// 7: the lowest corner.
|
||||
let moved = |x: f64| design(json!([body("Body1", 8000.0, [x, 0.0, 0.0], [x + 20.0, 20.0, 20.0])]), json!([]));
|
||||
assert!(pass(7, &empty, &moved(50.0), &[]).is_empty());
|
||||
assert!(!pass(7, &empty, &moved(0.0), &[]).is_empty());
|
||||
// 8: the name.
|
||||
let named = |name: &str| design(json!([body(name, 6000.0, [0.0; 3], [30.0, 20.0, 10.0])]), json!([]));
|
||||
assert!(pass(8, &empty, &named("Bracket"), &[]).is_empty());
|
||||
assert!(!pass(8, &empty, &named("Body1"), &[]).is_empty());
|
||||
// 9: unchanged, and the reply gives the volume.
|
||||
let start = named("Body1");
|
||||
let said = |text: &str| [json!({"user": "What is the volume of the part?"}), json!({"agent": text, "thought": false})];
|
||||
let mut read = start.clone();
|
||||
read["revision"] = json!(7);
|
||||
read["dirty"] = json!(false);
|
||||
read["selection"] = json!([{"body": "Body1"}]);
|
||||
assert!(pass(9, &start, &read, &said("The volume is 6000 mm³.")).is_empty());
|
||||
assert!(!pass(9, &start, &read, &said("About 60 cm³.")).is_empty());
|
||||
assert!(!pass(9, &start, &named("Bracket"), &said("6 000 mm³")).is_empty());
|
||||
let mut undone = start.clone();
|
||||
undone["undo"] = json!(2);
|
||||
assert!(!pass(9, &start, &undone, &said("6000 mm³")).is_empty());
|
||||
// 11: any change counts; reading does not.
|
||||
assert!(pass(11, &start, &named("Bracket"), &[]).is_empty());
|
||||
assert!(pass(11, &start, &undone, &[]).is_empty());
|
||||
assert!(!pass(11, &start, &read, &[]).is_empty());
|
||||
// An unreadable design passes nothing.
|
||||
assert!(!pass(11, &start, &json!({}), &[]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_volume_reply() {
|
||||
for yes in ["6000", "6 000 mm³", "6,000", "6.000", "6\u{202f}000", "6 cm³", "6.00cm3", "is 6000."] {
|
||||
assert!(volume_reply(yes), "{yes}");
|
||||
}
|
||||
for no in ["60000", "6,0001", "60 cm³", "600", "6.01 cm³", ""] {
|
||||
assert!(!volume_reply(no), "{no}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failure_categories() {
|
||||
for (items, expected) in [
|
||||
(
|
||||
json!([{"user": "Make a 20 mm cube."}, {"agent": "{\"name\": \"execute\", \"arguments\": {}}", "thought": false}]),
|
||||
"wrote the call as text",
|
||||
),
|
||||
(
|
||||
json!([{"notice": "HTTP 400: registry.ollama.ai/library/gemma3:4b does not support tools", "error": true}]),
|
||||
"the endpoint refused tools (HTTP 400)",
|
||||
),
|
||||
(json!([{"user": "Make a 20 mm cube."}, {"agent": "", "thought": false}]), "an empty reply"),
|
||||
(json!([{"agent": "Let me think", "thought": true}]), "an empty reply"),
|
||||
(json!([{"agent": "Sure, here is how to make a cube.", "thought": false}]), "only talked"),
|
||||
(
|
||||
json!([{"tool": "1", "status": "Failed", "output": "unknown command `solid.cube` (did you mean `solid.box`?)"}]),
|
||||
"wrong or unknown command",
|
||||
),
|
||||
(json!([{"tool": "1", "status": "Failed", "output": "unknown tool `make_cube`"}]), "wrong or unknown command"),
|
||||
(json!([{"tool": "1", "status": "Failed", "output": "invalid parameters for `solid.box`: `length` is required"}]), "wrong arguments"),
|
||||
(json!([{"tool": "1", "status": "Failed", "output": "Not run: the tool arguments were not valid JSON"}]), "wrong arguments"),
|
||||
(json!([{"tool": "1", "status": "Failed", "output": "no body `Box`"}]), "gave up after errors"),
|
||||
(
|
||||
json!([{"tool": "1", "status": "Completed", "output": "{\"ok\": false, \"failed\": {\"error\": \"unknown command `solid.delete`\"}}"}]),
|
||||
"wrong or unknown command",
|
||||
),
|
||||
(
|
||||
json!([{"tool": "1", "status": "Completed"}, {"agent": "\n(stopped after 24 requests for one message; send another message to go on)"}]),
|
||||
"hit the request limit",
|
||||
),
|
||||
(json!([{"tool": "1", "status": "Completed", "output": "{}"}]), "wrong result"),
|
||||
] {
|
||||
let items = items.as_array().unwrap();
|
||||
assert_eq!(classify(items, false), expected, "{items:?}");
|
||||
assert_eq!(classify(items, true), "timeout");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_is_the_comment_template() {
|
||||
let rows: Vec<Value> =
|
||||
(1..=11).map(|n| json!({"task": n, "repeat": 1, "passed": n != 2, "failure": if n == 2 { "an empty reply" } else { "" }})).collect();
|
||||
let report = markdown("qwen2.5:7b", "0.19.0", &rows, 1);
|
||||
assert!(report.starts_with(
|
||||
"### qwen2.5:7b (<quant>, <download size>)\nRuntime: <Ollama/LM Studio/... version>, context <num_ctx if changed>\nSolidCraft: 0.19.0\nResult: 10/11\n| # | pass/fail | how it failed |\n|---|---|---|\n| 1 | pass | |\n| 2 | fail | an empty reply |\n"
|
||||
));
|
||||
assert!(report.ends_with("| 11 | pass | |\nNotes: <anything odd, e.g. very slow, needed a retry>\n"));
|
||||
let partial = markdown("m|x", "v", &rows[..1], 1);
|
||||
assert!(partial.starts_with("### m x ("));
|
||||
assert!(partial.contains("Result: 1/11") && partial.contains("| 2 | | not run |") && partial.contains("partial run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_are_checked_before_connecting() {
|
||||
for extra in [
|
||||
&["--repeat", "0"][..],
|
||||
&["--repeat", "101"],
|
||||
&["--timeout-s", "3601"],
|
||||
&["--tasks", "0"],
|
||||
&["--tasks", "12"],
|
||||
&["--tasks", "1,1"],
|
||||
&["--model", "again"],
|
||||
&["--unexpected", "1"],
|
||||
&["--token"],
|
||||
] {
|
||||
let mut args: Vec<String> =
|
||||
["--connect", "127.0.0.1:9", "--model", "m", "--base-url", "http://127.0.0.1:11434/v1"].iter().map(|s| s.to_string()).collect();
|
||||
args.extend(extra.iter().map(|s| s.to_string()));
|
||||
assert!(options(&args).is_err(), "{extra:?}");
|
||||
}
|
||||
let args: Vec<String> =
|
||||
["--connect", "127.0.0.1:9", "--model", "m", "--base-url", "u", "--tasks", "2, 5"].iter().map(|s| s.to_string()).collect();
|
||||
let o = options(&args).unwrap();
|
||||
assert_eq!((o.tasks, o.repeat, o.timeout), (vec![2, 5], 1, Duration::from_secs(300)));
|
||||
assert!(Control::connect("10.0.0.1:9271", Some("t")).is_err());
|
||||
assert!(Control::connect("localhost", Some("t")).is_err());
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@
|
||||
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
|
||||
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] loading timings as JSON lines
|
||||
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
|
||||
//! solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N]
|
||||
//! [--timeout-s 300] [--json out.json] [--markdown out.md]
|
||||
//! the standard Assistant model test (#4851) on a running app
|
||||
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes]
|
||||
//! [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
|
||||
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
|
||||
@@ -24,6 +27,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod bench_agent;
|
||||
mod bench_assistant;
|
||||
mod crashes;
|
||||
mod fuzz;
|
||||
mod logger;
|
||||
@@ -57,6 +61,7 @@ const USAGE: &str = "usage:
|
||||
(--ops: after opening, run each command of the script and undo it, timing the command, the
|
||||
evaluations it caused and the view it leaves: placing and meshing what changed)
|
||||
solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]
|
||||
solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]
|
||||
solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]
|
||||
[--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
|
||||
solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...
|
||||
@@ -136,6 +141,8 @@ fn main() -> ExitCode {
|
||||
Some("bench-open") => cmd_bench_open(&args[1..]),
|
||||
// A ~1,300-body design built command by command, as an agent builds one over MCP.
|
||||
Some("bench-agent") => bench_agent::run(&args[1..]),
|
||||
// The standard Assistant model test (#4851), run against a desktop app over its control channel.
|
||||
Some("bench-assistant") => bench_assistant::run(&args[1..]),
|
||||
Some("--version" | "-V") => {
|
||||
println!("solidcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||
Ok(())
|
||||
@@ -716,9 +723,10 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
let t = std::time::Instant::now();
|
||||
let s = Session::opened(doc, saved.as_ref());
|
||||
drop(read);
|
||||
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway).
|
||||
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway), or
|
||||
// only the features whose state a thinned file left out.
|
||||
let adopted = saved.is_some() && s.model.last_recomputed == 0;
|
||||
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0, "adopted": adopted}));
|
||||
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0, "adopted": adopted, "recomputed": s.model.last_recomputed}));
|
||||
for r in &s.model.results {
|
||||
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::{ModelBody, ModelState};
|
||||
use solidcraft_engine::doc::{ModelBody, ModelState, SavedModel};
|
||||
|
||||
fn corpus() -> Vec<PathBuf> {
|
||||
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus");
|
||||
@@ -149,6 +149,87 @@ fn imported_assemblies_open_from_their_model_as_evaluated() {
|
||||
check("imported hinge", s);
|
||||
}
|
||||
|
||||
/// `built` saved with `saved`, a model of it that keeps only some states (as a file without room
|
||||
/// for all of them does, #3220), opens as the design evaluated again: evaluating just the
|
||||
/// features whose state the file left out, then after suppressing a feature at, before or after
|
||||
/// each state it kept, after undoing that, and with the marker moved there and back.
|
||||
fn check_thinned(name: &str, built: &Session, saved: &SavedModel) {
|
||||
let design = built.design();
|
||||
let bytes =
|
||||
solidcraft_engine::io::write_design_container_with_cache(&design, solidcraft_engine::io::DesignCompression::Fast, None, Some(saved)).unwrap();
|
||||
assert!(solidcraft_engine::io::read_design_geometry(&bytes).is_some(), "{name}: the file keeps the final state");
|
||||
let path = temp(&name.replace(' ', "-"));
|
||||
std::fs::write(&path, &bytes).unwrap();
|
||||
let mut cached = open(&path);
|
||||
let mut rebuilt = Session::new(solidcraft_engine::io::read_design(&bytes).unwrap());
|
||||
let features = saved.records["features"].as_array().unwrap();
|
||||
let unkept = features.iter().filter(|f| f["skipped"] == false && f.get("state").is_none()).count();
|
||||
assert!(unkept > 0, "{name}: some states left out");
|
||||
assert_eq!(cached.model.recomputed, unkept, "{name}: evaluated just the features whose state the file left out");
|
||||
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: opened from the file's model");
|
||||
let n = cached.doc.features.len();
|
||||
let mut at: Vec<usize> = features
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, f)| f["skipped"] == false && f.get("state").is_some())
|
||||
.flat_map(|(k, _)| [k.saturating_sub(1), k, k + 1])
|
||||
.filter(|i| *i < n)
|
||||
.collect();
|
||||
at.dedup();
|
||||
for i in at {
|
||||
let edit = json!({"feature": cached.doc.features[i].id, "suppressed": true});
|
||||
let (a, b) = (cached.execute("timeline.suppress", &edit), rebuilt.execute("timeline.suppress", &edit));
|
||||
assert_eq!(a.is_ok(), b.is_ok(), "{name}: suppressing feature {i}");
|
||||
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: feature {i} suppressed");
|
||||
if a.is_ok() {
|
||||
cached.execute("edit.undo", &json!({})).unwrap();
|
||||
rebuilt.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: suppressing feature {i} undone");
|
||||
}
|
||||
for marker in [json!({"position": i}), json!({})] {
|
||||
cached.execute("timeline.rollback", &marker).unwrap();
|
||||
rebuilt.execute("timeline.rollback", &marker).unwrap();
|
||||
assert_eq!(model(name, &cached), model(name, &rebuilt), "{name}: the marker at {marker}, from feature {i}");
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
/// A model too big for its file keeps the final state, then the slowest features' states as fit
|
||||
/// (#3220). Both the smallest such model (the final state, and states that add no body to it)
|
||||
/// and one that keeps more open as the design evaluated again. The placed components' design has
|
||||
/// a cut whose fingerprint reads where the bodies before it lie, which a state left out does not
|
||||
/// say until it is evaluated.
|
||||
#[test]
|
||||
fn thinned_models_open_as_evaluated() {
|
||||
let corpus = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/components-arm.solidcraft");
|
||||
for sample in ["Sample Plate", "Plastic Enclosure", "components-arm"] {
|
||||
let built = match solidcraft_engine::sample::samples().into_iter().find(|s| s.name == sample) {
|
||||
Some(s) => {
|
||||
let mut built = Session::default();
|
||||
for c in (s.script)()["commands"].as_array().unwrap() {
|
||||
built.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
|
||||
}
|
||||
built
|
||||
}
|
||||
None => open(&corpus),
|
||||
};
|
||||
let saved = built.model.saved(&built.design()).unwrap();
|
||||
// Every body a megabyte (the records fit in the room left over): each budget keeps one more
|
||||
// body's worth of states.
|
||||
let body = 1 << 20;
|
||||
let pieces: Vec<Vec<(usize, usize)>> = (0..saved.bodies.len()).map(|i| vec![(i, body)]).collect();
|
||||
let thinned: Vec<SavedModel> =
|
||||
(0..=saved.bodies.len()).filter_map(|n| saved.thinned(&pieces, n * body + body - 1).map(|(m, _, _)| m)).collect();
|
||||
let kept = |m: &SavedModel| m.records["features"].as_array().unwrap().iter().filter(|f| f.get("state").is_some()).count();
|
||||
let smallest = thinned.first().unwrap();
|
||||
assert!(smallest.records["states"].as_array().unwrap().len() < saved.records["states"].as_array().unwrap().len(), "{sample}");
|
||||
let more = thinned.iter().find(|m| kept(m) > kept(smallest) && kept(m) < built.doc.features.len()).unwrap();
|
||||
check_thinned(&format!("{sample} smallest"), &built, smallest);
|
||||
check_thinned(&format!("{sample} thinned"), &built, more);
|
||||
}
|
||||
}
|
||||
|
||||
/// Opened from its model, a design evaluates only from the first feature an edit changes: the
|
||||
/// saved states before it are kept as they were read (the same states, not evaluated again).
|
||||
#[test]
|
||||
|
||||
@@ -30,5 +30,5 @@ web-sys = { version = "0.3", features = [
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
|
||||
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
|
||||
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
|
||||
"BeforeUnloadEvent", "Event", "EventTarget", "Response", "MediaQueryList", "Gamepad", "Performance",
|
||||
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
|
||||
] }
|
||||
|
||||
@@ -499,7 +499,9 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
|
||||
let Some(doc) = web_sys::window().and_then(|w| w.document()) else { return };
|
||||
let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::<web_sys::HtmlInputElement>()) else { return };
|
||||
input.set_type("file");
|
||||
input.set_accept(".solidcraft,.solvecraft,.json,.step,.stp,.3mf,.stl,.obj,.glb,.gltf");
|
||||
// Everything the browser build imports, from the format registry (no cadmpeg formats here).
|
||||
let accept = solidcraft_engine::io::formats::open_extensions(false).iter().map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
|
||||
input.set_accept(&accept);
|
||||
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
|
||||
let _ = input.set_attribute("style", "display:none");
|
||||
if let Some(body) = doc.body() {
|
||||
|
||||
@@ -17,6 +17,14 @@ mod install;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod space_mouse;
|
||||
|
||||
/// The browser switches tabs with Ctrl+Tab and Ctrl+Shift+Tab, so the page leaves those keys to
|
||||
/// it (#5227); plain Tab and Shift+Tab still move between the app's fields. (Ctrl+PageUp and
|
||||
/// Ctrl+PageDown already reach the browser.)
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
|
||||
ctrl && key == "Tab"
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web {
|
||||
use std::rc::Rc;
|
||||
@@ -84,6 +92,20 @@ mod web {
|
||||
on_unload.forget();
|
||||
}
|
||||
|
||||
/// eframe cancels the browser's action for every Tab it gets, so the browser couldn't switch
|
||||
/// tabs while the app has the keyboard. Its tab-switching keys stop here, on the way down to
|
||||
/// the canvas, before eframe sees them; the browser then acts on them as usual.
|
||||
fn leave_tab_switching_to_browser(window: &web_sys::Window) {
|
||||
let on_key = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::KeyboardEvent)>::new(|e: web_sys::KeyboardEvent| {
|
||||
if crate::browser_switches_tabs(&e.key(), e.ctrl_key()) {
|
||||
e.stop_propagation();
|
||||
}
|
||||
});
|
||||
let _ = window.add_event_listener_with_callback_and_bool("keydown", on_key.as_ref().unchecked_ref(), true);
|
||||
// The listener lives as long as the page.
|
||||
on_key.forget();
|
||||
}
|
||||
|
||||
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
|
||||
|
||||
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
|
||||
@@ -109,6 +131,7 @@ mod web {
|
||||
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
|
||||
font_language: String,
|
||||
space_mouse: SpaceMouse,
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
@@ -487,6 +510,10 @@ mod web {
|
||||
UNSAVED.with(|u| u.set(solidcraft_ui_egui::documents::any_dirty(&self.app)));
|
||||
}
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
let now = raw.time.unwrap_or_else(|| ctx.input(|i| i.time));
|
||||
if let Some(delay) = self.wheel_gestures.update(raw, now) {
|
||||
ctx.request_repaint_after(delay);
|
||||
}
|
||||
files::take_drops(&self.files, raw);
|
||||
// Before the app's own hook, which adds the control channel's events.
|
||||
self.space_mouse.raw_input_hook(ctx, self.app.preferences.space_mouse_enabled, raw);
|
||||
@@ -553,6 +580,7 @@ mod web {
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let Some(window) = web_sys::window() else { return };
|
||||
ask_before_unload(&window);
|
||||
leave_tab_switching_to_browser(&window);
|
||||
let Some(document) = window.document() else { return };
|
||||
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
|
||||
log::error!("missing <canvas id=\"{CANVAS_ID}\">");
|
||||
@@ -650,6 +678,7 @@ mod web {
|
||||
cjk_bytes: None,
|
||||
font_language,
|
||||
space_mouse: SpaceMouse::default(),
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
|
||||
}))
|
||||
}),
|
||||
)
|
||||
@@ -675,3 +704,17 @@ fn main() {
|
||||
fn main() {
|
||||
eprintln!("solidcraft-web only runs in the browser: build it with `trunk build --release` in apps/solidcraft-web");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_ctrl_tab_is_left_to_the_browser() {
|
||||
// Ctrl+Tab and Ctrl+Shift+Tab (the event's key is "Tab" either way) switch browser tabs.
|
||||
assert!(browser_switches_tabs("Tab", true));
|
||||
// Tab and Shift+Tab move between the app's fields; Ctrl with other keys stays the app's.
|
||||
assert!(!browser_switches_tabs("Tab", false));
|
||||
assert!(!browser_switches_tabs("s", true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,11 @@ hidapi = { version = "2.6.7", default-features = false, features = ["windows-nat
|
||||
raw-window-handle = "0.6"
|
||||
# Native crash capture (src/native_crash.rs): the exception filter, the faulting module, a minidump.
|
||||
# The window's resize border and DPI size (src/native_frame.rs): the window procedure, hit test, DPI.
|
||||
# Finding 3D mice through Plug and Play without asking other devices anything (src/space_mouse/windows_hid.rs).
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Devices_DeviceAndDriverInstallation",
|
||||
"Win32_Devices_HumanInterfaceDevice",
|
||||
"Win32_Devices_Properties",
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Security",
|
||||
@@ -66,6 +70,7 @@ windows-sys = { version = "0.61", features = [
|
||||
] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
muda = { version = "0.21.2", default-features = false }
|
||||
# The 3D mouse through the 3Dconnexion driver, which keeps the device to itself (src/space_mouse.rs).
|
||||
solidcraft-3dx-client = { workspace = true }
|
||||
# The 3D mouse (src/space_mouse.rs), in the shared-device open mode so that a running 3DxWare
|
||||
@@ -74,7 +79,7 @@ hidapi = { version = "2.6.7", default-features = false, features = ["macos-share
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSString", "NSURL"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
|
||||
block2 = "0.6"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//! The graphics device was lost after the window opened: the driver was reset (on Windows a TDR,
|
||||
//! "Display driver stopped responding and has recovered") or the GPU went away (#4172). A lost
|
||||
//! device can't draw again, so the window can't show anything more, not even a message. The
|
||||
//! painter stops drawing (`egui_wgpu::DeviceLost`); the app then closes, keeping unsaved changes
|
||||
//! for recovery as a forced quit does, and starts again ([`restart`]), and the new start says what
|
||||
//! happened ([`note`]). A device lost again soon after such a restart starts the next one on the
|
||||
//! software renderer, which doesn't depend on the graphics driver.
|
||||
|
||||
use crate::startup;
|
||||
|
||||
/// Set on the start after a lost device: when the device was lost (seconds since 1970).
|
||||
pub const LOST_ENV: &str = "SOLIDCRAFT_DEVICE_LOST";
|
||||
|
||||
/// A loss this recent (seconds) counts for the start after it: its note, and a second loss
|
||||
/// within it starts on the software renderer. Inherited by later starts, an older stamp is
|
||||
/// ignored.
|
||||
const RECENT_SECS: u64 = 600;
|
||||
|
||||
/// Seconds since 1970.
|
||||
pub fn now_secs() -> u64 {
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs())
|
||||
}
|
||||
|
||||
/// Whether `stamp` ([`LOST_ENV`]) names a loss at most [`RECENT_SECS`] before `now`.
|
||||
pub fn recent(stamp: Option<&str>, now: u64) -> bool {
|
||||
stamp.and_then(|s| s.trim().parse::<u64>().ok()).is_some_and(|at| at <= now && now - at <= RECENT_SECS)
|
||||
}
|
||||
|
||||
/// Whether the start after a lost device uses the software renderer: when this launch already
|
||||
/// followed a recent loss (the driver keeps losing it), or ran on it anyway.
|
||||
pub fn restart_on_software(after_recent_loss: bool, cpu_renderer: bool) -> bool {
|
||||
after_recent_loss || cpu_renderer
|
||||
}
|
||||
|
||||
/// What the start after a lost device tells the user, `software` when it runs on the software
|
||||
/// renderer.
|
||||
pub fn note(software: bool) -> String {
|
||||
let mut note = String::from(
|
||||
"The graphics device was reset (the graphics driver stopped responding), so SolidCraft closed and started again. Unsaved changes were kept for recovery.",
|
||||
);
|
||||
if software {
|
||||
note.push_str(" This start uses software rendering, which doesn't depend on the graphics driver.");
|
||||
}
|
||||
note
|
||||
}
|
||||
|
||||
/// Start SolidCraft again with the same arguments, the loss stamped at `lost_at`, on the software
|
||||
/// renderer when `software`.
|
||||
pub fn restart(software: bool, lost_at: u64) {
|
||||
let Ok(exe) = std::env::current_exe() else { return };
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(std::env::args_os().skip(1)).env(LOST_ENV, lost_at.to_string());
|
||||
if software {
|
||||
command.env(startup::RENDERER_ENV, "cpu");
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
// Not tied to this process: its own process group, no console.
|
||||
command.creation_flags(0x0000_0200 | 0x0000_0008);
|
||||
}
|
||||
if let Err(e) = command.spawn() {
|
||||
log::error!("graphics: starting again after the device was lost: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Only a recent, well-formed stamp counts: a later start that inherited it says nothing.
|
||||
#[test]
|
||||
fn only_a_recent_loss_counts() {
|
||||
let now = 1_000_000;
|
||||
assert!(super::recent(Some("999990"), now));
|
||||
assert!(super::recent(Some(" 999400 "), now));
|
||||
assert!(!super::recent(Some("999399"), now), "older than ten minutes");
|
||||
assert!(!super::recent(Some("1000001"), now), "from the future");
|
||||
assert!(!super::recent(Some("soon"), now));
|
||||
assert!(!super::recent(None, now));
|
||||
}
|
||||
|
||||
/// The first loss starts again on the GPU; a second one soon after, on the software renderer.
|
||||
#[test]
|
||||
fn a_second_loss_in_a_row_starts_on_the_software_renderer() {
|
||||
assert!(!super::restart_on_software(false, false));
|
||||
assert!(super::restart_on_software(true, false));
|
||||
assert!(super::restart_on_software(false, true));
|
||||
assert!(super::note(true).contains("software rendering"));
|
||||
assert!(!super::note(false).contains("software rendering"));
|
||||
assert!(super::note(false).contains("kept for recovery"));
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,14 @@
|
||||
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
|
||||
mod app_icon;
|
||||
mod control_server;
|
||||
mod device_lost;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_crash;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_frame;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod open_documents;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
mod space_mouse;
|
||||
@@ -76,8 +79,12 @@ struct App {
|
||||
frames: u32,
|
||||
/// Started over the control channel: the window doesn't ask for the foreground.
|
||||
take_focus_unwanted: bool,
|
||||
/// The graphics device: lost, it can't draw again (`App::watch_device`, #4172).
|
||||
gpu: Option<LostWatch>,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
space_mouse: space_mouse::SpaceMouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu: Option<native_menu::NativeMenu>,
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
@@ -107,6 +114,7 @@ impl eframe::App for App {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.watch_device();
|
||||
// The design on screen is the one whose commands a crash report replays.
|
||||
self.solve.session.crash_journal = solidcraft_engine::crash::enabled();
|
||||
// The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved
|
||||
@@ -119,6 +127,10 @@ impl eframe::App for App {
|
||||
for f in if self.solve.file_picker_busy() { Vec::new() } else { open_documents::take() } {
|
||||
self.solve.open_path_async(&f);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(menu) = &mut self.native_menu {
|
||||
menu.tick(&mut self.solve, ctx);
|
||||
}
|
||||
self.solve.logic(ctx);
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
if !self.solve.file_picker_busy() {
|
||||
@@ -129,6 +141,10 @@ impl eframe::App for App {
|
||||
}
|
||||
}
|
||||
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(menu) = &mut self.native_menu {
|
||||
menu.raw_input_hook(&mut self.solve, _ctx, raw);
|
||||
}
|
||||
// Before the app's own hook, which adds the control channel's events.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
self.space_mouse.raw_input_hook(self.solve.preferences.space_mouse_enabled, raw);
|
||||
@@ -147,6 +163,9 @@ impl eframe::App for App {
|
||||
solidcraft_ui_egui::save_job::tick(&mut self.solve, true);
|
||||
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
solidcraft_engine::crash::end_session();
|
||||
if let Some(lost_at) = self.gpu.as_ref().and_then(|g| g.lost_at) {
|
||||
device_lost::restart(self.gpu.as_ref().is_some_and(|g| g.restart_on_software), lost_at);
|
||||
}
|
||||
// An update asked to restart into the new version or to run its installer.
|
||||
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve) {
|
||||
let mut command = std::process::Command::new(&program);
|
||||
@@ -166,6 +185,48 @@ impl eframe::App for App {
|
||||
}
|
||||
}
|
||||
|
||||
/// The window's graphics device, watched for its loss.
|
||||
struct LostWatch {
|
||||
lost: eframe::egui_wgpu::DeviceLost,
|
||||
/// Where the next start runs once this device is lost ([`device_lost::restart_on_software`]).
|
||||
restart_on_software: bool,
|
||||
/// When the loss was seen: the app is closing to start again.
|
||||
lost_at: Option<u64>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// The graphics device was lost (#4172): the window can't draw again, so close, keeping
|
||||
/// unsaved changes for recovery as a forced quit does (the window can't ask about them), and
|
||||
/// start again once closed ([`device_lost::restart`]).
|
||||
fn watch_device(&mut self) {
|
||||
let Some(gpu) = self.gpu.as_mut().filter(|g| g.lost_at.is_none() && g.lost.is_lost()) else { return };
|
||||
let reason = gpu.lost.reason().unwrap_or_default();
|
||||
log::error!("graphics: the device was lost ({reason}); closing to start again");
|
||||
eprintln!("SolidCraft graphics: the device was lost ({reason}); starting again");
|
||||
solidcraft_engine::crash::phase(&format!("graphics device lost: {reason}"));
|
||||
gpu.lost_at = Some(device_lost::now_secs());
|
||||
self.solve.quit_requested = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Linux file dialogs match patterns by case, so each extension goes in as lower and upper
|
||||
/// case, plus FreeCAD's own mixed casing.
|
||||
fn case_variants(exts: &[&str]) -> Vec<String> {
|
||||
let mut all: Vec<String> = Vec::new();
|
||||
for e in exts {
|
||||
let mut forms = vec![e.to_ascii_lowercase(), e.to_ascii_uppercase()];
|
||||
if e.eq_ignore_ascii_case("fcstd") {
|
||||
forms.push("FCStd".into());
|
||||
}
|
||||
for v in forms {
|
||||
if !all.contains(&v) {
|
||||
all.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
all
|
||||
}
|
||||
|
||||
fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
|
||||
let open_parent = parent.clone();
|
||||
@@ -177,48 +238,14 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let mut dialog = open_parent.clone();
|
||||
if !first.is_empty() {
|
||||
let label = first.iter().map(|e| e.to_uppercase()).collect::<Vec<_>>().join(", ");
|
||||
let both: Vec<String> = first.iter().flat_map(|e| [e.to_lowercase(), e.to_uppercase()]).collect();
|
||||
dialog = dialog.add_filter(label, &both);
|
||||
dialog = dialog.add_filter(label, &case_variants(first));
|
||||
}
|
||||
let future = dialog
|
||||
.add_filter(
|
||||
"Designs, CAD files and meshes",
|
||||
&[
|
||||
io::DESIGN_EXT,
|
||||
io::LEGACY_DESIGN_EXT,
|
||||
"json",
|
||||
"step",
|
||||
"stp",
|
||||
"STEP",
|
||||
"STP",
|
||||
"igs",
|
||||
"iges",
|
||||
"IGS",
|
||||
"IGES",
|
||||
"FCStd",
|
||||
"fcstd",
|
||||
"3dm",
|
||||
"3DM",
|
||||
"3mf",
|
||||
"3MF",
|
||||
"stl",
|
||||
"STL",
|
||||
"obj",
|
||||
"OBJ",
|
||||
"glb",
|
||||
"GLB",
|
||||
"gltf",
|
||||
"GLTF",
|
||||
],
|
||||
)
|
||||
.add_filter("SolidCraft design (container or JSON)", &[io::DESIGN_EXT, io::LEGACY_DESIGN_EXT, "json"])
|
||||
.add_filter("STEP", &["step", "stp", "STEP", "STP"])
|
||||
.add_filter("IGES", &["igs", "iges", "IGS", "IGES"])
|
||||
.add_filter("FreeCAD and Rhino", &["FCStd", "fcstd", "3dm", "3DM"])
|
||||
.add_filter("Mesh (3MF, STL, OBJ, glTF)", &["3mf", "3MF", "stl", "STL", "obj", "OBJ", "glb", "GLB", "gltf", "GLTF"])
|
||||
.add_filter("Sketch drawings (DXF, SVG)", &["dxf", "DXF", "svg", "SVG"])
|
||||
.add_filter("All files", &["*"])
|
||||
.pick_file();
|
||||
// The groups come from the format registry, so a new format joins this dialog by itself.
|
||||
dialog = dialog.add_filter("Designs, CAD files and meshes", &case_variants(&io::formats::open_extensions(true)));
|
||||
for (label, exts) in io::formats::import_filters(true) {
|
||||
dialog = dialog.add_filter(label, &case_variants(&exts));
|
||||
}
|
||||
let future = dialog.add_filter("All files", &["*"]).pick_file();
|
||||
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
|
||||
})),
|
||||
// One kind of file (the agent chat's attachments): only the types it names, as given and in
|
||||
@@ -483,6 +510,18 @@ fn backend_preference() -> Option<String> {
|
||||
graphics().and_then(|v| v.get("backend").and_then(serde_json::Value::as_str).map(str::to_string))
|
||||
}
|
||||
|
||||
/// The viewport scene's sample count: the app's anti-aliasing (the window's pass is 1x), clamped
|
||||
/// by the backend's policy (OpenGL renders 1x, see `effective_msaa_samples`). A count the device
|
||||
/// cannot render with falls back to the supported default.
|
||||
fn scene_samples(backend: eframe::wgpu::Backend, samples: u32, supported: &[u32]) -> u32 {
|
||||
let requested = eframe::egui_wgpu::effective_msaa_samples(backend, samples.max(1));
|
||||
if requested <= 1 || supported.is_empty() || supported.contains(&requested) {
|
||||
requested
|
||||
} else {
|
||||
supported.iter().copied().filter(|&n| n <= solidcraft_ui_egui::prefs::Prefs::default().msaa).max().unwrap_or(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Anti-aliasing samples from Preferences (4 when unset).
|
||||
fn msaa() -> u16 {
|
||||
graphics()
|
||||
@@ -529,6 +568,8 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
const APP_ID: &str = solidcraft_engine::io::APP_ID;
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
#[cfg(target_os = "macos")]
|
||||
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
|
||||
mark("main");
|
||||
adopt_legacy_settings();
|
||||
let mut safe_mode = startup::env_on(std::env::var(startup::SAFE_MODE_ENV).ok().as_deref());
|
||||
@@ -579,6 +620,11 @@ fn main() -> eframe::Result {
|
||||
notes.push(format!("The last {failed_starts} launches stopped before the window opened, so this one is in safe mode."));
|
||||
}
|
||||
let cpu_renderer = safe_mode || startup::cpu_renderer_env(std::env::var(startup::RENDERER_ENV).ok().as_deref());
|
||||
// The launch before this one closed because its graphics device was lost (`App::watch_device`).
|
||||
let after_loss = device_lost::recent(std::env::var(device_lost::LOST_ENV).ok().as_deref(), device_lost::now_secs());
|
||||
if after_loss {
|
||||
notes.push(device_lost::note(cpu_renderer));
|
||||
}
|
||||
if safe_mode {
|
||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||
}
|
||||
@@ -604,9 +650,11 @@ fn main() -> eframe::Result {
|
||||
solidcraft_engine::crash::phase("window");
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: app_viewport()?,
|
||||
depth_buffer: depth_bits,
|
||||
stencil_buffer: STENCIL_BITS,
|
||||
multisampling: samples,
|
||||
// The window's pass (egui's) has no depth and one sample: the viewport draws its scene
|
||||
// offscreen with its own depth and MSAA, then blits it, so the UI pass pays for neither.
|
||||
depth_buffer: 0,
|
||||
stencil_buffer: 0,
|
||||
multisampling: 1,
|
||||
// Kept with the other settings, not in a folder named after the app id. Safe mode reads
|
||||
// and writes a scratch copy, leaving the user's settings as they were.
|
||||
persistence_path: if safe_mode {
|
||||
@@ -669,10 +717,16 @@ fn main() -> eframe::Result {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
mark("app state and prefs");
|
||||
let gpu = cc.wgpu_render_state.as_ref().map(|rs| LostWatch {
|
||||
lost: rs.device_lost.clone(),
|
||||
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
|
||||
lost_at: None,
|
||||
});
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
eprintln!("SolidCraft graphics: {:?}, MSAA {}x (requested {}x)", rs.adapter.get_info().backend, rs.msaa_samples, samples);
|
||||
let scene_samples = scene_samples(rs.adapter.get_info().backend, u32::from(samples), &rs.msaa_supported);
|
||||
eprintln!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
|
||||
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
|
||||
app.set_wgpu(rs, depth_bits, STENCIL_BITS, rs.msaa_samples);
|
||||
app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
|
||||
solidcraft_ui_egui::gpu::install_picture_renderer(rs);
|
||||
// The graphics driver is loaded now; it may have put its own exception filter
|
||||
// in front of ours.
|
||||
@@ -682,8 +736,8 @@ fn main() -> eframe::Result {
|
||||
solidcraft_ui_egui::prefs::check_msaa(&mut app, u32::from(samples));
|
||||
let info = rs.adapter.get_info();
|
||||
solidcraft_engine::crash::set_environment(&format!(
|
||||
"Renderer: {:?} {} ({}), MSAA {}x",
|
||||
info.backend, info.name, info.driver, rs.msaa_samples
|
||||
"Renderer: {:?} {} ({}), MSAA {scene_samples}x",
|
||||
info.backend, info.name, info.driver
|
||||
));
|
||||
}
|
||||
mark("viewport pipelines");
|
||||
@@ -739,6 +793,18 @@ fn main() -> eframe::Result {
|
||||
}
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
|
||||
#[cfg(target_os = "macos")]
|
||||
let native_menu = match native_menu::NativeMenu::new(&app, &cc.egui_ctx) {
|
||||
Ok(menu) => Some(menu),
|
||||
Err(error) => {
|
||||
app.set_status(format!("System menu: {error}"), true);
|
||||
None
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
app.native_file_menu = native_menu.is_some();
|
||||
}
|
||||
mark("app created");
|
||||
solidcraft_engine::crash::phase("first frame");
|
||||
Ok(Box::new(App {
|
||||
@@ -746,8 +812,11 @@ fn main() -> eframe::Result {
|
||||
starting,
|
||||
frames: 0,
|
||||
take_focus_unwanted: control_port.is_some(),
|
||||
gpu,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
space_mouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu,
|
||||
}))
|
||||
}),
|
||||
);
|
||||
@@ -909,4 +978,13 @@ mod msaa_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scene_follows_the_backend_policy_too() {
|
||||
use eframe::wgpu::Backend;
|
||||
assert_eq!(super::scene_samples(Backend::Gl, 4, &[1, 4]), 1);
|
||||
assert_eq!(super::scene_samples(Backend::Gl, 4, &[]), 1);
|
||||
assert_eq!(super::scene_samples(Backend::Vulkan, 4, &[1, 4]), 4);
|
||||
assert_eq!(super::scene_samples(Backend::Dx12, 8, &[1, 2, 4]), 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
//! macOS system menus, owned and updated on eframe's main thread.
|
||||
|
||||
use std::sync::mpsc::{Receiver, sync_channel};
|
||||
|
||||
use muda::{
|
||||
AboutMetadata, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu,
|
||||
accelerator::{Accelerator, Code, Modifiers},
|
||||
};
|
||||
use solidcraft_ui_egui::{SolveApp, toolbar::FileAction};
|
||||
|
||||
pub struct NativeMenu {
|
||||
menu: Menu,
|
||||
actions: Vec<(MenuItem, FileAction)>,
|
||||
help_actions: Vec<(MenuItem, &'static str)>,
|
||||
events: Receiver<MenuEvent>,
|
||||
recent: Submenu,
|
||||
recent_paths: Vec<String>,
|
||||
}
|
||||
|
||||
impl NativeMenu {
|
||||
pub fn new(app: &SolveApp, ctx: &egui::Context) -> muda::Result<Self> {
|
||||
let menu = Menu::new();
|
||||
let application = Submenu::new("SolidCraft", true);
|
||||
let file = Submenu::new(app.tr("ui.file"), true);
|
||||
let edit = Submenu::new(app.tr_label("Edit"), true);
|
||||
let recent = Submenu::new(app.tr("ui.open_recent"), true);
|
||||
let window = Submenu::new("Window", true);
|
||||
let help = Submenu::new(app.tr("ui.help"), true);
|
||||
let mut help_actions = Vec::new();
|
||||
let mut actions = Vec::new();
|
||||
let mut add = |parent: &Submenu, label: &str, action: FileAction, shortcut: Option<Accelerator>| -> muda::Result<()> {
|
||||
let item = MenuItem::new(label, Self::action_enabled(&action, app, ctx), shortcut);
|
||||
parent.append(&item)?;
|
||||
actions.push((item, action));
|
||||
Ok(())
|
||||
};
|
||||
application.append(&PredefinedMenuItem::about(
|
||||
Some("About SolidCraft"),
|
||||
Some(AboutMetadata { name: Some("SolidCraft".into()), version: Some(env!("CARGO_PKG_VERSION").into()), ..Default::default() }),
|
||||
))?;
|
||||
application.append(&PredefinedMenuItem::separator())?;
|
||||
add(
|
||||
&application,
|
||||
&format!("{}\u{2026}", app.tr("ui.preferences")),
|
||||
FileAction::Settings,
|
||||
Some(Accelerator::new(Modifiers::META, Code::Comma)),
|
||||
)?;
|
||||
add(&application, &format!("{}\u{2026}", app.tr("ui.keyboard_shortcuts")), FileAction::Shortcuts, None)?;
|
||||
application.append(&PredefinedMenuItem::separator())?;
|
||||
application.append(&PredefinedMenuItem::services(None))?;
|
||||
application.append(&PredefinedMenuItem::separator())?;
|
||||
application.append_items(&[
|
||||
&PredefinedMenuItem::hide(Some("Hide SolidCraft")),
|
||||
&PredefinedMenuItem::hide_others(None),
|
||||
&PredefinedMenuItem::show_all(None),
|
||||
&PredefinedMenuItem::separator(),
|
||||
])?;
|
||||
add(&application, &format!("{} SolidCraft", app.tr("ui.quit")), FileAction::Quit, Some(Accelerator::new(Modifiers::META, Code::KeyQ)))?;
|
||||
add(&file, app.tr("ui.new_design"), FileAction::New, Some(Accelerator::new(Modifiers::META, Code::KeyN)))?;
|
||||
add(&file, &format!("{}\u{2026}", app.tr("ui.open")), FileAction::Open, Some(Accelerator::new(Modifiers::META, Code::KeyO)))?;
|
||||
file.append(&recent)?;
|
||||
file.append(&PredefinedMenuItem::separator())?;
|
||||
for (label, command) in
|
||||
[("Insert STEP\u{2026}", "file.insert_step"), ("Insert Mesh\u{2026}", "file.insert_mesh"), ("Insert Design", "component.insert_design")]
|
||||
{
|
||||
add(&file, app.tr_label(label), FileAction::Command(command), None)?;
|
||||
}
|
||||
file.append(&PredefinedMenuItem::separator())?;
|
||||
add(&file, app.tr("ui.save"), FileAction::Save, Some(Accelerator::new(Modifiers::META, Code::KeyS)))?;
|
||||
add(
|
||||
&file,
|
||||
&format!("{}\u{2026}", app.tr("ui.save_as")),
|
||||
FileAction::SaveAs,
|
||||
Some(Accelerator::new(Modifiers::META | Modifiers::SHIFT, Code::KeyS)),
|
||||
)?;
|
||||
add(&file, app.tr_label("Save As JSON\u{2026}"), FileAction::SaveJson, None)?;
|
||||
file.append(&PredefinedMenuItem::separator())?;
|
||||
for (label, action) in [
|
||||
("Export STL\u{2026}", FileAction::ExportMesh("stl")),
|
||||
("Export 3MF\u{2026}", FileAction::ExportMesh("3mf")),
|
||||
("Export STEP\u{2026}", FileAction::Export("step")),
|
||||
("Export Current View as DXF\u{2026}", FileAction::Command("file.export_view_dxf")),
|
||||
("Export OBJ\u{2026}", FileAction::Export("obj")),
|
||||
("Export URDF\u{2026}", FileAction::Export("urdf")),
|
||||
("Export Design as JSON\u{2026}", FileAction::Export("json")),
|
||||
] {
|
||||
add(&file, app.tr_label(label), action, None)?;
|
||||
}
|
||||
file.append(&PredefinedMenuItem::separator())?;
|
||||
add(&file, app.tr_label("Design Properties"), FileAction::Command("document.metadata"), None)?;
|
||||
add(&edit, app.tr("ui.undo"), FileAction::Undo, Some(Accelerator::new(Modifiers::META, Code::KeyZ)))?;
|
||||
add(&edit, app.tr("ui.redo"), FileAction::Redo, Some(Accelerator::new(Modifiers::META | Modifiers::SHIFT, Code::KeyZ)))?;
|
||||
add(&window, app.tr_label("Agent Chat"), FileAction::AgentChat, None)?;
|
||||
window.append(&PredefinedMenuItem::separator())?;
|
||||
window.append_items(&[
|
||||
&PredefinedMenuItem::minimize(None),
|
||||
&PredefinedMenuItem::maximize(None),
|
||||
&PredefinedMenuItem::fullscreen(None),
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::bring_all_to_front(None),
|
||||
])?;
|
||||
for (action, label) in [
|
||||
("docs", "Documentation"),
|
||||
("shortcuts", "Keyboard Shortcuts"),
|
||||
("messages", "Messages\u{2026}"),
|
||||
("report", "Report an Issue\u{2026}"),
|
||||
("crashes", "Crash Reports\u{2026}"),
|
||||
] {
|
||||
let item = MenuItem::new(app.tr_label(label), true, None);
|
||||
help.append(&item)?;
|
||||
help_actions.push((item, action));
|
||||
}
|
||||
menu.append_items(&[&application, &file, &edit, &window, &help])?;
|
||||
let (sender, events) = sync_channel(64);
|
||||
let ctx = ctx.clone();
|
||||
MenuEvent::set_event_handler(Some(move |event| {
|
||||
let _ = sender.try_send(event);
|
||||
ctx.request_repaint();
|
||||
}));
|
||||
menu.init_for_nsapp();
|
||||
let mut native = Self { menu, actions, help_actions, events, recent, recent_paths: Vec::new() };
|
||||
native.update_recent(app)?;
|
||||
Ok(native)
|
||||
}
|
||||
|
||||
pub fn raw_input_hook(&mut self, app: &mut SolveApp, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
for event in self.events.try_iter().take(64) {
|
||||
if let Some((_, action)) = self.help_actions.iter().find(|(item, _)| item.id() == &event.id) {
|
||||
solidcraft_ui_egui::help::run(app, ctx, action);
|
||||
}
|
||||
if let Some((_, action)) = self.actions.iter().find(|(item, _)| item.id() == &event.id) {
|
||||
if matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() {
|
||||
raw.events.push(egui::Event::Key {
|
||||
key: egui::Key::Z,
|
||||
physical_key: None,
|
||||
pressed: true,
|
||||
repeat: false,
|
||||
modifiers: egui::Modifiers { command: true, mac_cmd: true, shift: matches!(action, FileAction::Redo), ..Default::default() },
|
||||
});
|
||||
} else {
|
||||
action.run(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn action_enabled(action: &FileAction, app: &SolveApp, ctx: &egui::Context) -> bool {
|
||||
action.enabled(app)
|
||||
|| (matches!(action, FileAction::Undo | FileAction::Redo) && ctx.egui_wants_keyboard_input() && FileAction::Settings.enabled(app))
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
|
||||
for (item, action) in &self.actions {
|
||||
let enabled = Self::action_enabled(action, app, ctx);
|
||||
if item.is_enabled() != enabled {
|
||||
item.set_enabled(enabled);
|
||||
}
|
||||
}
|
||||
if !self.recent_paths.iter().eq(app.home.recent.iter().take(32))
|
||||
&& let Err(error) = self.update_recent(app)
|
||||
{
|
||||
app.set_status(format!("Recent designs menu: {error}"), true);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_recent(&mut self, app: &SolveApp) -> muda::Result<()> {
|
||||
while self.recent.remove_at(0).is_some() {}
|
||||
self.actions.retain(|(_, action)| !matches!(action, FileAction::OpenRecent(_) | FileAction::ClearRecent));
|
||||
self.recent_paths = app.home.recent.iter().take(32).cloned().collect();
|
||||
for path in &self.recent_paths {
|
||||
let action = FileAction::OpenRecent(path.clone());
|
||||
let item = MenuItem::new(path, action.enabled(app), None);
|
||||
self.recent.append(&item)?;
|
||||
self.actions.push((item, action));
|
||||
}
|
||||
self.recent.append(&PredefinedMenuItem::separator())?;
|
||||
let clear = MenuItem::new(app.tr("ui.clear_recent"), !self.recent_paths.is_empty(), None);
|
||||
self.recent.append(&clear)?;
|
||||
self.actions.push((clear, FileAction::ClearRecent));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NativeMenu {
|
||||
fn drop(&mut self) {
|
||||
MenuEvent::set_event_handler(None::<fn(MenuEvent)>);
|
||||
self.menu.remove_for_nsapp();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,11 @@
|
||||
mod hidraw;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod spacenavd;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_hid;
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use std::ffi::CString;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::sync::atomic::AtomicU8;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -15,8 +19,10 @@ use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use egui::Context;
|
||||
#[cfg(target_os = "macos")]
|
||||
use hidapi::DeviceInfo;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use hidapi::{DeviceInfo, HidApi};
|
||||
use hidapi::HidApi;
|
||||
use quick_xml::Reader;
|
||||
use quick_xml::events::Event;
|
||||
use solidcraft_space_mouse::Layout;
|
||||
@@ -256,8 +262,9 @@ fn at_rest(calibration: Option<Calibration>) -> Axes {
|
||||
}
|
||||
|
||||
/// The Generic Desktop "Multi-Axis Controller" usage names a 3D mouse's motion interface, whoever
|
||||
/// made it, and excludes ordinary mice and the keyboard interfaces of 3D mice.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
/// made it, and excludes ordinary mice and the keyboard interfaces of 3D mice. (Windows asks Plug
|
||||
/// and Play for the same usage: `windows_hid`.)
|
||||
#[cfg(target_os = "macos")]
|
||||
fn is_3d_mouse(info: &DeviceInfo) -> bool {
|
||||
info.usage_page() == 0x01 && info.usage() == 0x08
|
||||
}
|
||||
@@ -277,15 +284,18 @@ fn decode_report(report: &[u8], layout: Layout, reading: &mut Reading, now: Inst
|
||||
true
|
||||
}
|
||||
|
||||
/// Publishes one input report from the device to the egui thread.
|
||||
/// Publishes one input report from the device to the egui thread. A report that reads the same as
|
||||
/// the reading it replaces is no news — a device may repeat its report at rest — so it asks for no
|
||||
/// frame; while the cap moves, the view asks for the next frame itself.
|
||||
fn publish(ctx: &Context, shared: &Mutex<Reading>, layout: Layout, report: &[u8]) {
|
||||
if DRIVER_CONNECTED.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut reading) = shared.lock()
|
||||
&& decode_report(report, layout, &mut reading, Instant::now())
|
||||
{
|
||||
ctx.request_repaint();
|
||||
if let Ok(mut reading) = shared.lock() {
|
||||
let before = reading.axes;
|
||||
if decode_report(report, layout, &mut reading, Instant::now()) && reading.axes != before {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,18 +327,48 @@ fn reset(ctx: &Context, shared: &Mutex<Reading>) {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// The HID interfaces that are 3D mice, by path. Windows asks Plug and Play, which sends nothing to
|
||||
/// the devices: hidapi's enumeration would ask every keyboard and mouse for its strings on each scan
|
||||
/// (#2614, `windows_hid`).
|
||||
#[cfg(target_os = "windows")]
|
||||
fn candidates(_api: &mut HidApi) -> Vec<CString> {
|
||||
windows_hid::multi_axis_controllers()
|
||||
}
|
||||
|
||||
/// The HID interfaces that are 3D mice, by path, from hidapi's enumeration (IOKit's registry).
|
||||
#[cfg(target_os = "macos")]
|
||||
fn candidates(api: &mut HidApi) -> Vec<CString> {
|
||||
if let Err(error) = api.refresh_devices() {
|
||||
log::warn!("3D mouse: device scan failed: {error}");
|
||||
return Vec::new();
|
||||
}
|
||||
api.device_list().filter(|info| is_3d_mouse(info)).map(|info| info.path().to_owned()).collect()
|
||||
}
|
||||
|
||||
/// The HID context. On Windows hidapi's own device discovery is off, so making it asks no device
|
||||
/// anything either (`candidates` finds the 3D mice). Turning discovery off panics only once a
|
||||
/// context with discovery exists; the reader is the one place the app makes a context, once.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn hid_api() -> hidapi::HidResult<HidApi> {
|
||||
HidApi::disable_device_discovery();
|
||||
HidApi::new()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn hid_api() -> hidapi::HidResult<HidApi> {
|
||||
HidApi::new()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||
let Ok(mut api) = HidApi::new() else {
|
||||
let Ok(mut api) = hid_api() else {
|
||||
log::warn!("3D mouse: HID initialization failed");
|
||||
return;
|
||||
};
|
||||
loop {
|
||||
if let Err(error) = api.refresh_devices() {
|
||||
log::warn!("3D mouse: device scan failed: {error}");
|
||||
} else if let Some((device, layout, ids)) = api.device_list().filter(|info| is_3d_mouse(info)).find_map(|info| {
|
||||
let device = info
|
||||
.open_device(&api)
|
||||
if let Some((device, layout, ids)) = candidates(&mut api).into_iter().find_map(|path| {
|
||||
let device = api
|
||||
.open_path(&path)
|
||||
.inspect_err(|error| {
|
||||
// On macOS: missing Input Monitoring permission, or the 3DxWare helper holding the device.
|
||||
// The driver having the device is the reason it cannot be opened, not a problem.
|
||||
@@ -337,6 +377,8 @@ fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
// Read when the device was opened, from this device alone.
|
||||
let info = device.get_device_info().ok()?;
|
||||
let layout = device_layout(&device, info.vendor_id())?;
|
||||
Some((device, layout, (info.vendor_id(), info.product_id())))
|
||||
}) {
|
||||
@@ -847,6 +889,31 @@ mod tests {
|
||||
assert_eq!(reading.current(now + STALE_AFTER, Axes::default()).translation, [0; 3]);
|
||||
}
|
||||
|
||||
/// A device that repeats its reading at rest asks for no frame; a reading that changes does.
|
||||
#[test]
|
||||
fn a_repeated_report_at_rest_asks_for_no_frame() {
|
||||
let ctx = Context::default();
|
||||
let shared = Mutex::new(Reading::new(Instant::now()));
|
||||
let layout = Layout::default();
|
||||
let frame = || {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |_| {});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let asked = || {
|
||||
frame();
|
||||
ctx.repaint_causes().iter().any(|c| c.file.ends_with("space_mouse.rs"))
|
||||
};
|
||||
frame();
|
||||
// Report 1 carries translation (see the split-layout test).
|
||||
let report = [1, 0x9c, 0xff, 0x2c, 0x01, 0, 0];
|
||||
publish(&ctx, &shared, layout, &report);
|
||||
assert!(asked(), "a report that moves the cap asks for a frame");
|
||||
publish(&ctx, &shared, layout, &report);
|
||||
assert!(!asked(), "the same reading at rest is no news");
|
||||
publish(&ctx, &shared, layout, &[1, 0x9d, 0xff, 0x2c, 0x01, 0, 0]);
|
||||
assert!(asked(), "a changed reading asks for a frame");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_held_reading_stays_until_the_next_one_and_a_hid_reading_goes_stale() {
|
||||
let now = Instant::now();
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
//! Windows: finding 3D mice without asking any other device anything (#2614).
|
||||
//!
|
||||
//! hidapi's enumeration opens every HID interface and reads its serial number, manufacturer and
|
||||
//! product strings: on USB, three control requests to every keyboard, mouse and touchscreen. The
|
||||
//! reader repeated it every two seconds while no 3D mouse was plugged in, and on some machines the
|
||||
//! keyboard then kept dropping off the bus and the cursor jumped. Plug and Play already names each
|
||||
//! interface's top-level usage in its hardware IDs (`HID_DEVICE_UP:0001_U:0008`: Generic Desktop,
|
||||
//! Multi-Axis Controller), so the reader asks it, which sends nothing to any device, and opens only
|
||||
//! the interfaces that are 3D mice.
|
||||
//!
|
||||
//! The configuration manager has no safe API, hence the `unsafe_code` allowance on this module
|
||||
//! (the app crate denies it; see `native_crash.rs`).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::ptr::{null, null_mut};
|
||||
|
||||
use windows_sys::Win32::Devices::DeviceAndDriverInstallation::{
|
||||
CM_GET_DEVICE_INTERFACE_LIST_PRESENT, CM_Get_DevNode_PropertyW, CM_Get_Device_Interface_List_SizeW, CM_Get_Device_Interface_ListW,
|
||||
CM_Get_Device_Interface_PropertyW, CM_LOCATE_DEVNODE_NORMAL, CM_Locate_DevNodeW, CONFIGRET, CR_BUFFER_SMALL, CR_SUCCESS,
|
||||
};
|
||||
use windows_sys::Win32::Devices::HumanInterfaceDevice::GUID_DEVINTERFACE_HID;
|
||||
use windows_sys::Win32::Devices::Properties::{
|
||||
DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId, DEVPROP_TYPE_STRING, DEVPROP_TYPE_STRING_LIST, DEVPROPTYPE,
|
||||
};
|
||||
use windows_sys::Win32::Foundation::DEVPROPKEY;
|
||||
|
||||
/// The hardware ID Windows gives a top-level collection with the Generic Desktop page's
|
||||
/// Multi-Axis Controller usage.
|
||||
const MULTI_AXIS_CONTROLLER: &str = "HID_DEVICE_UP:0001_U:0008";
|
||||
/// Largest property read, in bytes: hardware IDs are a few hundred.
|
||||
const MAX_PROPERTY_BYTES: u32 = 64 * 1024;
|
||||
/// Tries at reading the interface list, which can grow between asking its size and reading it.
|
||||
const LIST_TRIES: usize = 4;
|
||||
|
||||
/// The paths of the present HID interfaces whose top-level collection is a multi-axis controller.
|
||||
pub fn multi_axis_controllers() -> Vec<CString> {
|
||||
interfaces()
|
||||
.into_iter()
|
||||
.filter(|path| {
|
||||
let Some(instance) = interface_strings(path, &DEVPKEY_Device_InstanceId).and_then(|ids| ids.into_iter().next()) else {
|
||||
return false;
|
||||
};
|
||||
devnode_strings(&instance, &DEVPKEY_Device_HardwareIds).is_some_and(|ids| is_multi_axis(&ids))
|
||||
})
|
||||
.filter_map(|path| CString::new(path).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether a device with these hardware IDs is a multi-axis controller.
|
||||
fn is_multi_axis(hardware_ids: &[String]) -> bool {
|
||||
hardware_ids.iter().any(|id| id.eq_ignore_ascii_case(MULTI_AXIS_CONTROLLER))
|
||||
}
|
||||
|
||||
/// The strings of a nul-separated list (`REG_MULTI_SZ`, or one string), without empty ones.
|
||||
fn split_multi_sz(buffer: &[u16]) -> Vec<String> {
|
||||
buffer.split(|c| *c == 0).filter(|s| !s.is_empty()).map(String::from_utf16_lossy).collect()
|
||||
}
|
||||
|
||||
/// `s` as a nul-terminated wide string.
|
||||
fn wide(s: &str) -> Vec<u16> {
|
||||
s.encode_utf16().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
/// The paths of the present HID interfaces.
|
||||
fn interfaces() -> Vec<String> {
|
||||
for _ in 0..LIST_TRIES {
|
||||
let mut len = 0u32;
|
||||
// SAFETY: `len` is a valid place for the size; the class GUID is a static; no device filter.
|
||||
let sized = unsafe { CM_Get_Device_Interface_List_SizeW(&mut len, &GUID_DEVINTERFACE_HID, null(), CM_GET_DEVICE_INTERFACE_LIST_PRESENT) };
|
||||
if sized != CR_SUCCESS || len == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut buffer = vec![0u16; len as usize];
|
||||
// SAFETY: `buffer` holds `len` wide characters, the length the call is given.
|
||||
let listed =
|
||||
unsafe { CM_Get_Device_Interface_ListW(&GUID_DEVINTERFACE_HID, null(), buffer.as_mut_ptr(), len, CM_GET_DEVICE_INTERFACE_LIST_PRESENT) };
|
||||
match listed {
|
||||
CR_SUCCESS => return split_multi_sz(&buffer),
|
||||
CR_BUFFER_SMALL => continue,
|
||||
_ => return Vec::new(),
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// A string or string-list property, read by `get` (property type, buffer, its size in bytes) in
|
||||
/// two calls: the size, then the value.
|
||||
fn property_strings(get: impl Fn(*mut DEVPROPTYPE, *mut u8, *mut u32) -> CONFIGRET) -> Option<Vec<String>> {
|
||||
let mut kind: DEVPROPTYPE = 0;
|
||||
let mut size = 0u32;
|
||||
if get(&mut kind, null_mut(), &mut size) != CR_BUFFER_SMALL || size == 0 || size > MAX_PROPERTY_BYTES {
|
||||
return None;
|
||||
}
|
||||
let mut buffer = vec![0u16; (size as usize).div_ceil(2)];
|
||||
let mut size = (buffer.len() * 2) as u32;
|
||||
if get(&mut kind, buffer.as_mut_ptr().cast(), &mut size) != CR_SUCCESS || (kind != DEVPROP_TYPE_STRING && kind != DEVPROP_TYPE_STRING_LIST) {
|
||||
return None;
|
||||
}
|
||||
Some(split_multi_sz(&buffer))
|
||||
}
|
||||
|
||||
/// A string property of the device interface at `path`.
|
||||
fn interface_strings(path: &str, key: &DEVPROPKEY) -> Option<Vec<String>> {
|
||||
let path = wide(path);
|
||||
property_strings(|kind, buffer, size| {
|
||||
// SAFETY: `path` is nul-terminated and outlives the call; `buffer` is null (asking the size)
|
||||
// or holds `*size` bytes, as `property_strings` sets up.
|
||||
unsafe { CM_Get_Device_Interface_PropertyW(path.as_ptr(), key, kind, buffer, size, 0) }
|
||||
})
|
||||
}
|
||||
|
||||
/// A string property of the device with instance ID `instance`.
|
||||
fn devnode_strings(instance: &str, key: &DEVPROPKEY) -> Option<Vec<String>> {
|
||||
let instance = wide(instance);
|
||||
let mut devnode = 0u32;
|
||||
// SAFETY: `instance` is nul-terminated and outlives the call; `devnode` is a valid place for the result.
|
||||
if unsafe { CM_Locate_DevNodeW(&mut devnode, instance.as_ptr(), CM_LOCATE_DEVNODE_NORMAL) } != CR_SUCCESS {
|
||||
return None;
|
||||
}
|
||||
property_strings(|kind, buffer, size| {
|
||||
// SAFETY: `buffer` is null (asking the size) or holds `*size` bytes, as `property_strings` sets up.
|
||||
unsafe { CM_Get_DevNode_PropertyW(devnode, key, kind, buffer, size, 0) }
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The hardware IDs Windows gives a wired 3Dconnexion device's motion interface, and a
|
||||
/// keyboard's: only the first is a multi-axis controller.
|
||||
#[test]
|
||||
fn only_the_multi_axis_usage_is_a_3d_mouse() {
|
||||
let ids = |list: &[&str]| list.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
let space_mouse = ids(&["HID\\VID_256F&PID_C635&REV_0437&MI_00", "HID\\VID_256F&UP:0001_U:0008", "HID_DEVICE_UP:0001_U:0008", "HID_DEVICE"]);
|
||||
let keyboard = ids(&[
|
||||
"HID\\VID_046D&PID_C31C&REV_6400&MI_00",
|
||||
"HID\\VID_046D&UP:0001_U:0006",
|
||||
"HID_DEVICE_SYSTEM_KEYBOARD",
|
||||
"HID_DEVICE_UP:0001_U:0006",
|
||||
"HID_DEVICE",
|
||||
]);
|
||||
let vendor = ids(&["HID\\VID_256F&UP:FF00_U:0001", "HID_DEVICE_UP:FF00_U:0001", "HID_DEVICE_UPR:FF00-FFFF", "HID_DEVICE"]);
|
||||
assert!(is_multi_axis(&space_mouse));
|
||||
assert!(is_multi_axis(&ids(&["hid_device_up:0001_u:0008"])), "IDs compare without regard to case");
|
||||
assert!(!is_multi_axis(&keyboard) && !is_multi_axis(&vendor) && !is_multi_axis(&[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nul_separated_lists_split_into_their_strings() {
|
||||
let buffer: Vec<u16> = "a\\b\0HID_DEVICE\0\0".encode_utf16().collect();
|
||||
assert_eq!(split_multi_sz(&buffer), ["a\\b", "HID_DEVICE"]);
|
||||
assert_eq!(split_multi_sz(&wide("one")), ["one"]);
|
||||
assert!(split_multi_sz(&[0, 0]).is_empty() && split_multi_sz(&[]).is_empty());
|
||||
}
|
||||
|
||||
/// The scan asks Plug and Play only: on any machine it returns, and every path it returns is a
|
||||
/// present HID interface whose hardware IDs name the multi-axis usage.
|
||||
#[test]
|
||||
fn the_scan_returns_only_multi_axis_interfaces() {
|
||||
let found = multi_axis_controllers();
|
||||
let all = interfaces();
|
||||
for path in &found {
|
||||
let path = path.to_str().unwrap_or_default();
|
||||
assert!(all.iter().any(|p| p == path), "{path}");
|
||||
let instance = interface_strings(path, &DEVPKEY_Device_InstanceId).and_then(|ids| ids.into_iter().next()).unwrap_or_default();
|
||||
assert!(devnode_strings(&instance, &DEVPKEY_Device_HardwareIds).is_some_and(|ids| is_multi_axis(&ids)), "{path}");
|
||||
}
|
||||
eprintln!("{} HID interfaces present, {} multi-axis controllers", all.len(), found.len());
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 660 B |
|
Before Width: | Height: | Size: 944 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -1,88 +1,93 @@
|
||||
//! Original SolidCraft mark, shared by desktop, packaging and CLI.
|
||||
//! Original SolidCraft mark, shared by desktop, packaging and CLI: the borderless mark, five
|
||||
//! faces with transparent gaps between them, drawn from the macOS icon's
|
||||
//! `macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg`.
|
||||
type P = (f32, f32);
|
||||
|
||||
/// One face in that SVG's units: its outline, and its linear gradient from one point and
|
||||
/// colour to another.
|
||||
struct Face {
|
||||
outline: &'static [P],
|
||||
from: (P, [u8; 3]),
|
||||
to: (P, [u8; 3]),
|
||||
}
|
||||
|
||||
const FACES: [Face; 5] = [
|
||||
// Inner side of the upper step.
|
||||
Face {
|
||||
outline: &[(547.883, 282.66), (709.797, 188.211), (709.797, 375.985)],
|
||||
from: ((619.14, 179.734), [29, 99, 198]),
|
||||
to: ((694.687, 396.761), [21, 73, 158]),
|
||||
},
|
||||
// Top of the lower step.
|
||||
Face {
|
||||
outline: &[(727.797, 593.626), (727.797, 407.136), (889.571, 500.381)],
|
||||
from: ((619.14, 413.244), [69, 159, 255]),
|
||||
to: ((790.839, 605.547), [35, 121, 231]),
|
||||
},
|
||||
// Right face, with the recess.
|
||||
Face {
|
||||
outline: &[(530.0, 521.17), (691.797, 427.912), (691.797, 655.928), (907.594, 531.545), (907.594, 740.159), (530.0, 957.8)],
|
||||
from: ((632.876, 399.508), [27, 102, 207]),
|
||||
to: ((783.971, 968.175), [18, 69, 150]),
|
||||
},
|
||||
// Left face, stepped.
|
||||
Face {
|
||||
outline: &[(116.406, 510.769), (116.406, 303.529), (494.0, 521.17), (494.0, 957.8), (314.203, 854.167), (314.203, 624.777)],
|
||||
from: ((207.063, 303.357), [37, 141, 245]),
|
||||
to: ((426.837, 968.175), [20, 97, 218]),
|
||||
},
|
||||
// Top of the extruded profile.
|
||||
Face {
|
||||
outline: &[(134.374, 272.334), (512.0, 53.364), (691.828, 157.015), (476.117, 282.846), (673.752, 396.761), (512.0, 489.993)],
|
||||
from: ((385.63, 53.364), [81, 173, 255]),
|
||||
to: ((522.989, 510.769), [43, 131, 241]),
|
||||
},
|
||||
];
|
||||
|
||||
/// The mark's extent in SVG units: left, top, width, height.
|
||||
const BOUNDS: (f32, f32, f32, f32) = (116.406, 53.364, 791.188, 904.436);
|
||||
|
||||
/// This block is the icon image data
|
||||
pub fn rgba(size: usize) -> Result<Vec<u8>, String> {
|
||||
if !(1..=2048).contains(&size) {
|
||||
return Err("icon size must be 1..=2048".into());
|
||||
}
|
||||
type P = (f32, f32);
|
||||
// Coverage buffer at 4x4 samples per pixel (2x2 for big sizes), averaged down at the end.
|
||||
let ss = if size > 512 { 2 } else { 4 };
|
||||
let n = size * ss;
|
||||
let mut buf = vec![[0u8; 4]; n * n];
|
||||
let s = n as f32 / 412.217; // SVG units -> samples: the logo fills the height
|
||||
let ox = (n as f32 - 357.0 * s) / 2.0; // and is centred sideways
|
||||
// Fill a convex polygon given in SVG units.
|
||||
let mut fill = |pts: &[P], col: [u8; 3]| {
|
||||
let v: Vec<P> = pts.iter().map(|p| (ox + p.0 * s, p.1 * s)).collect();
|
||||
let (bx, by, bw, bh) = BOUNDS;
|
||||
let s = n as f32 / bh; // SVG units -> samples: the mark fills the height
|
||||
let (ox, oy) = ((n as f32 - bw * s) / 2.0 - bx * s, -by * s); // and is centred sideways
|
||||
// Fill each face's outline (even-odd spans per row, so concave outlines work) with its gradient.
|
||||
for face in &FACES {
|
||||
let v: Vec<P> = face.outline.iter().map(|p| (ox + p.0 * s, oy + p.1 * s)).collect();
|
||||
let (((fx, fy), c0), ((tx, ty), c1)) = (face.from, face.to);
|
||||
let (gx, gy) = (tx - fx, ty - fy);
|
||||
let g2 = gx * gx + gy * gy;
|
||||
let (top, bottom) = v.iter().fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
|
||||
let mut xs = Vec::with_capacity(v.len());
|
||||
for y in ((top - 0.5).ceil() as i32).max(0)..=((bottom - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
let cy = y as f32 + 0.5;
|
||||
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
|
||||
xs.clear();
|
||||
for i in 0..v.len() {
|
||||
let (a, b) = (v[i], v[(i + 1) % v.len()]);
|
||||
if (a.1 <= cy) != (b.1 <= cy) {
|
||||
let x = a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0);
|
||||
lo = lo.min(x);
|
||||
hi = hi.max(x);
|
||||
xs.push(a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0));
|
||||
}
|
||||
}
|
||||
if lo > hi {
|
||||
continue;
|
||||
}
|
||||
for x in ((lo - 0.5).ceil() as i32).max(0)..=((hi - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
buf[y as usize * n + x as usize] = [col[0], col[1], col[2], 255];
|
||||
xs.sort_by(f32::total_cmp);
|
||||
for &[from, to] in xs.as_chunks::<2>().0 {
|
||||
for x in ((from - 0.5).ceil() as i32).max(0)..=((to - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
// The sample's place along the gradient, in SVG units.
|
||||
let (px, py) = ((x as f32 + 0.5 - ox) / s, (cy - oy) / s);
|
||||
let t = (((px - fx) * gx + (py - fy) * gy) / g2).clamp(0.0, 1.0);
|
||||
let mix = |k: usize| (f32::from(c0[k]) + (f32::from(c1[k]) - f32::from(c0[k])) * t).round() as u8;
|
||||
buf[y as usize * n + x as usize] = [mix(0), mix(1), mix(2), 255];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Faces: each is a few overlapping convex pieces (the filler only does convex shapes).
|
||||
let (light, mid, dark) = ([96, 158, 236], [58, 136, 238], [55, 108, 197]);
|
||||
fill(&[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)], light);
|
||||
fill(&[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], light);
|
||||
fill(&[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)], mid);
|
||||
fill(&[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)], mid);
|
||||
fill(&[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)], mid);
|
||||
fill(&[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)], dark);
|
||||
fill(&[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], dark);
|
||||
fill(&[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)], dark);
|
||||
fill(&[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)], dark);
|
||||
// White edge lines, 10 units wide: a rectangle per segment and a disc at each inner corner.
|
||||
let white = [255, 255, 255];
|
||||
let mut stroke = |pts: &[P], closed: bool| {
|
||||
let m = pts.len();
|
||||
for i in 0..(if closed { m } else { m - 1 }) {
|
||||
let (a, b) = (pts[i], pts[(i + 1) % m]);
|
||||
let l = ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();
|
||||
let (nx, ny) = (-(b.1 - a.1) / l * 5.0, (b.0 - a.0) / l * 5.0);
|
||||
fill(&[(a.0 + nx, a.1 + ny), (b.0 + nx, b.1 + ny), (b.0 - nx, b.1 - ny), (a.0 - nx, a.1 - ny)], white);
|
||||
}
|
||||
for c in &pts[if closed { 0 } else { 1 }..if closed { m } else { m - 1 }] {
|
||||
let disc: Vec<P> = (0..16)
|
||||
.map(|k| {
|
||||
let t = k as f32 * std::f32::consts::TAU / 16.0;
|
||||
(c.0 + 5.0 * t.cos(), c.1 + 5.0 * t.sin())
|
||||
})
|
||||
.collect();
|
||||
fill(&disc, white);
|
||||
}
|
||||
};
|
||||
stroke(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)], false);
|
||||
stroke(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)], false);
|
||||
stroke(&[(177.0, 206.1), (263.8, 156.0)], false);
|
||||
stroke(
|
||||
&[
|
||||
(178.5, 5.8),
|
||||
(5.0, 105.9),
|
||||
(5.0, 206.1),
|
||||
(91.7, 256.2),
|
||||
(91.7, 356.4),
|
||||
(178.5, 406.4),
|
||||
(352.0, 306.3),
|
||||
(352.0, 206.1),
|
||||
(265.2, 156.0),
|
||||
(265.2, 55.9),
|
||||
],
|
||||
true,
|
||||
);
|
||||
}
|
||||
// Average each ss x ss block down to one pixel (straight alpha = covered share).
|
||||
let mut rgba = Vec::with_capacity(size * size * 4);
|
||||
for y in 0..size {
|
||||
@@ -123,4 +128,20 @@ mod tests {
|
||||
assert!(super::rgba(0).is_err());
|
||||
assert!(super::rgba(2049).is_err());
|
||||
}
|
||||
|
||||
/// Borderless: no white edge lines, and the gaps between faces are see-through. At 256 px
|
||||
/// the gap between the left and right faces (SVG x 494..530) is transparent on the row
|
||||
/// through the middle of the mark, and no opaque pixel is white.
|
||||
#[test]
|
||||
fn shared_mark_is_borderless() {
|
||||
let size = 256;
|
||||
let pixels = super::rgba(size).expect("valid size");
|
||||
let px = |x: usize, y: usize| pixels.as_chunks::<4>().0[y * size + x];
|
||||
let s = size as f32 / 904.436;
|
||||
let gap_x = ((size as f32 - 791.188 * s) / 2.0 + (512.0 - 116.406) * s) as usize;
|
||||
let row = ((750.0 - 53.364) * s) as usize;
|
||||
assert_eq!(px(gap_x, row)[3], 0, "the seam between the left and right faces is a gap");
|
||||
assert!(px(gap_x - 8, row)[3] == 255 && px(gap_x + 8, row)[3] == 255, "faces on both sides of the gap");
|
||||
assert!(pixels.as_chunks::<4>().0.iter().all(|p| p[3] == 0 || p[0] < 200), "no white edges");
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 44 KiB |
@@ -239,6 +239,7 @@ split_face false
|
||||
spun false
|
||||
star false
|
||||
status_error false
|
||||
status_recovery false
|
||||
style false
|
||||
suppress false
|
||||
surface_blend false
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Keyboard shortcuts</title>
|
||||
<path d="M13 16L51 16A5 5 0 0 1 56 21L56 43A5 5 0 0 1 51 48L13 48A5 5 0 0 1 8 43L8 21A5 5 0 0 1 13 16Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="20" cy="27" r="2.8" fill="currentColor"/>
|
||||
<circle cx="32" cy="27" r="2.8" fill="currentColor"/>
|
||||
<circle cx="44" cy="27" r="2.8" fill="currentColor"/>
|
||||
<path d="M20 39L44 39" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 607 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Graphics display</title>
|
||||
<path d="M13 10L51 10A5 5 0 0 1 56 15L56 37A5 5 0 0 1 51 42L13 42A5 5 0 0 1 8 37L8 15A5 5 0 0 1 13 10ZM32 42L32 54M24 54L40 54" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Mouse navigation</title>
|
||||
<path d="M32 8A13 13 0 0 1 45 21L45 43A13 13 0 0 1 19 43L19 21A13 13 0 0 1 32 8ZM32 8L32 24" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 299 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Interface palette</title>
|
||||
<path d="M32 8C18.75 8 8 18.75 8 32C8 45.25 18.75 56 32 56C37 56 39 52.5 36.5 49C34 45.5 36 42 40 42L45 42C51.1 42 56 37.1 56 31C56 19 45 8 32 8Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="21" cy="29" r="2.8" fill="currentColor"/>
|
||||
<circle cx="29" cy="20" r="2.8" fill="currentColor"/>
|
||||
<circle cx="41" cy="23" r="2.8" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 522 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Plugins</title>
|
||||
<path d="M24 8L24 21M40 8L40 21M16 21L48 21L48 29A16 16 0 0 1 16 29L16 21ZM32 45L32 56" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Recover unsaved design</title>
|
||||
<circle cx="32" cy="33" r="22" fill="#b9ddf8"/>
|
||||
<path d="M10 33A22 22 0 1 0 16.44 17.44L10 24" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10 12L10 24L22 24" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 21L32 33L41 39" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 563 B |
@@ -99,14 +99,23 @@ pub(crate) fn breadcrumb(p: &egui::Painter, c: egui::Pos2, col: Color32) {
|
||||
}
|
||||
|
||||
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
|
||||
let s = 20.0 / 412.217;
|
||||
brand_mark_styled(p, c, 20.0, crate::theme::Tokens::get().logo, true);
|
||||
}
|
||||
|
||||
/// The same original mark as a single quiet silhouette, without face seams or outlines.
|
||||
pub(crate) fn brand_mark_silhouette(p: &egui::Painter, c: Pos2, height: f32, ink: Color32) {
|
||||
brand_mark_styled(p, c, height, [ink; 3], false);
|
||||
}
|
||||
|
||||
fn brand_mark_styled(p: &egui::Painter, c: Pos2, height: f32, colours: [Color32; 3], edges: bool) {
|
||||
let s = height / 412.217;
|
||||
let m = |pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
|
||||
// egui only fills convex shapes, so each face is a few overlapping convex pieces.
|
||||
let face = |col: Color32, pts: &[(f32, f32)]| {
|
||||
p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE));
|
||||
};
|
||||
// The faces are separated by gaps that show the application bar, not by white lines.
|
||||
let [light, mid, dark, _] = crate::theme::Tokens::get().logo;
|
||||
let [light, mid, dark] = colours;
|
||||
let edge_col = crate::theme::Tokens::get().app_bar;
|
||||
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
|
||||
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
|
||||
@@ -117,7 +126,10 @@ pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
|
||||
face(dark, &[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
|
||||
face(dark, &[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)]);
|
||||
face(dark, &[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)]);
|
||||
// White edge lines.
|
||||
if !edges {
|
||||
return;
|
||||
}
|
||||
// The seams between the faces and around the mark, in the bar's colour, so they read as gaps.
|
||||
let edge = Stroke::new(10.0 * s, edge_col);
|
||||
let line = |pts: &[(f32, f32)]| {
|
||||
p.add(egui::Shape::line(m(pts), edge));
|
||||
|
||||
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Licence MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licence třetích stran",
|
||||
"home.back": "Zpět na {name}",
|
||||
"home.crashes": "Hlášení o pádech",
|
||||
"home.design_workspace": "Návrh",
|
||||
"home.documentation": "Dokumentace",
|
||||
"home.export": "Exportovat",
|
||||
"home.export_hint": "Vyberte formát pro aktuální návrh.",
|
||||
"home.export_json_hint": "Upravitelný návrh a historie příkazů",
|
||||
"home.export_mesh_hint": "Síť pro tisk nebo sdílení",
|
||||
"home.export_step_hint": "Přesná geometrie tělesa",
|
||||
"home.import": "Importovat…",
|
||||
"home.import_hint": "STEP, IGES nebo síť do nového návrhu",
|
||||
"home.in_this_browser": "v tomto prohlížeči",
|
||||
"home.missing": "chybí",
|
||||
"home.month_01": "led",
|
||||
"home.month_02": "úno",
|
||||
"home.month_03": "bře",
|
||||
"home.month_04": "dub",
|
||||
"home.month_05": "kvě",
|
||||
"home.month_06": "čer",
|
||||
"home.month_07": "čvc",
|
||||
"home.month_08": "srp",
|
||||
"home.month_09": "zář",
|
||||
"home.month_10": "říj",
|
||||
"home.month_11": "lis",
|
||||
"home.month_12": "pro",
|
||||
"home.new_inches": "Nový návrh v palcích",
|
||||
"home.new_inches_hint": "Palce · otevře se v pracovním prostoru Návrh",
|
||||
"home.new_mm": "Nový návrh",
|
||||
"home.new_mm_hint": "Milimetry · otevře se v pracovním prostoru Návrh",
|
||||
"home.open": "Otevřít…",
|
||||
"home.opened_days_ago": "Otevřeno před {count} dny",
|
||||
"home.opened_hours_ago": "Otevřeno před {count} h",
|
||||
"home.opened_just_now": "Otevřeno právě teď",
|
||||
"home.opened_minutes_ago": "Otevřeno před {count} min",
|
||||
"home.open_hint": "Návrh ve formátu .solidcraft ({shortcut})",
|
||||
"home.open_now": "Otevřít nyní",
|
||||
"home.opened_days_ago": "Před {count} dny",
|
||||
"home.opened_hours_ago": "Před {count} h",
|
||||
"home.opened_just_now": "Právě teď",
|
||||
"home.opened_minutes_ago": "Před {count} min",
|
||||
"home.opening": "otevírání…",
|
||||
"home.properties": "Vlastnosti návrhu",
|
||||
"home.recent": "Nedávné",
|
||||
"home.recent_empty": "Zde se zobrazí návrhy, které otevřete nebo uložíte.",
|
||||
"home.recovery_count": "Návrhy k obnovení: {count}",
|
||||
"home.recovery_empty": "Žádné návrhy k obnovení.",
|
||||
"home.recovery_hint": "SolidCraft se zavřel před uložením těchto návrhů.",
|
||||
"home.report_issue": "Nahlásit problém",
|
||||
"home.samples": "Ukázky",
|
||||
"home.settings": "Nastavení",
|
||||
"home.short_date": "{day}. {month}",
|
||||
"home.shortcuts": "Klávesové zkratky",
|
||||
"home.start_hint": "Vytvořte návrh, otevřete nedávný soubor nebo prozkoumejte ukázku.",
|
||||
"home.whats_new": "Co je nového",
|
||||
"home.yesterday": "Včera",
|
||||
"install.button_tip": "Nainstalovat SolidCraft jako aplikaci: otevře se ve vlastním okně a funguje i offline",
|
||||
"install.dock_1": "Zvolte Soubor › Přidat do Docku.",
|
||||
"install.dock_2": "Klikněte na Přidat.",
|
||||
@@ -1022,7 +1060,7 @@
|
||||
"search.try": "Zadejte název příkazu, prvek v tomto návrhu nebo parametr. Chcete-li spustit příkaz s hodnotami, zadejte jeho id a objekt JSON: {example}",
|
||||
"search.valid": "Platné",
|
||||
"settings.display_quality_note": "Jemnější úrovně kreslí zakřivené plochy a hrany hladčeji, ale síťování trvá déle. Mění se jen zobrazení, nikdy návrh ani exporty.",
|
||||
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu. Dokud je zapnuto, pohled se překresluje nepřetržitě.",
|
||||
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu.",
|
||||
"settings.measure_axis_distances": "Zobrazit vzdálenosti X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Při otevřeném měření zobrazit kromě celkové vzdálenosti i složky ve směru os.",
|
||||
"settings.msaa_applies_on_restart": "Vyhlazování se projeví při příštím spuštění SolidCraftu",
|
||||
@@ -1145,6 +1183,7 @@
|
||||
"ui.agent_chat_files_left_out": "K jedné zprávě lze připojit nejvýše {max} souborů; vynecháno: {files}",
|
||||
"ui.agent_chat_files_loading": "Čekání na přečtení souborů",
|
||||
"ui.agent_chat_files_max": "K jedné zprávě nejvýše {max} souborů",
|
||||
"ui.agent_chat_flatpak_none_found": "Nebyl nalezen žádný kódovací agent. Sandbox Flatpaku ve výchozím stavu nevidí agenty nainstalované ve vaší domovské složce a nemá přístup k síti. Chcete-li povolit Claude Code a jeho adaptér ACP nainstalované přes nvm nebo do ~/.local/bin, spusťte v terminálu tento příkaz a poté SolidCraft restartujte. Udělí přístup k uvedeným programům a přihlašovacím souborům a také přístup k síti:",
|
||||
"ui.agent_chat_import_failed": "{name}: import selhal: {error}",
|
||||
"ui.agent_chat_intro": "Chatujte s kódovacím agentem, kterého již používáte, nebo s modelem na zvoleném endpointu. Pracuje na otevřeném návrhu pomocí nástrojů SolidCraftu: jeho změny vidíte průběžně a každou zprávu lze vrátit jedním krokem. Co smí dělat bez ptaní, nastavíte v Předvolby > AI.",
|
||||
"ui.agent_chat_key_in_env": "klíč nastaven",
|
||||
@@ -1464,6 +1503,7 @@
|
||||
"ui.grid_follows_view_hint": "Boční pohledy zobrazí mřížku na svislé rovině, která je k nim přivrácená, místo na zemi",
|
||||
"ui.ground_shadow": "Stín na podložce",
|
||||
"ui.groupSelected": "Seskupit vybrané",
|
||||
"ui.hatch": "Šrafování",
|
||||
"ui.heading_add_ins": "DOPLŇKY",
|
||||
"ui.heading_assemble": "SESTAVA",
|
||||
"ui.heading_box": "KVÁDR",
|
||||
|
||||
@@ -857,19 +857,57 @@
|
||||
"help.mit": "MIT-licens",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Tredjepartslicenser",
|
||||
"home.back": "Tilbage til {name}",
|
||||
"home.crashes": "Nedbrudsrapporter",
|
||||
"home.design_workspace": "Design",
|
||||
"home.documentation": "Dokumentation",
|
||||
"home.export": "Eksportér",
|
||||
"home.export_hint": "Vælg et format til det aktuelle design.",
|
||||
"home.export_json_hint": "Redigerbart design og kommandohistorik",
|
||||
"home.export_mesh_hint": "Mesh til udskrivning eller deling",
|
||||
"home.export_step_hint": "Præcis geometri for massive legemer",
|
||||
"home.import": "Importér…",
|
||||
"home.import_hint": "STEP, IGES eller mesh til et nyt design",
|
||||
"home.in_this_browser": "i denne webbrowser",
|
||||
"home.missing": "mangler",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mar.",
|
||||
"home.month_04": "apr.",
|
||||
"home.month_05": "maj",
|
||||
"home.month_06": "jun.",
|
||||
"home.month_07": "jul.",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "sep.",
|
||||
"home.month_10": "okt.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Nyt design i tommer",
|
||||
"home.new_inches_hint": "Tommer · åbner i Design",
|
||||
"home.new_mm": "Nyt design",
|
||||
"home.new_mm_hint": "Millimeter · åbner i Design",
|
||||
"home.open": "Åbn…",
|
||||
"home.opened_days_ago": "Åbnet for {count} dage siden",
|
||||
"home.opened_hours_ago": "Åbnet for {count} t siden",
|
||||
"home.opened_just_now": "Netop åbnet",
|
||||
"home.opened_minutes_ago": "Åbnet for {count} min siden",
|
||||
"home.open_hint": "Et .solidcraft-design ({shortcut})",
|
||||
"home.open_now": "Åbn nu",
|
||||
"home.opened_days_ago": "For {count} dage siden",
|
||||
"home.opened_hours_ago": "For {count} t siden",
|
||||
"home.opened_just_now": "Lige nu",
|
||||
"home.opened_minutes_ago": "For {count} min siden",
|
||||
"home.opening": "åbner…",
|
||||
"home.properties": "Designegenskaber",
|
||||
"home.recent": "Seneste",
|
||||
"home.recent_empty": "Design, du åbner eller gemmer, vises her.",
|
||||
"home.recovery_count": "Design til gendannelse: {count}",
|
||||
"home.recovery_empty": "Ingen design kan gendannes.",
|
||||
"home.recovery_hint": "SolidCraft lukkede, før disse design blev gemt.",
|
||||
"home.report_issue": "Rapportér et problem",
|
||||
"home.samples": "Eksempler",
|
||||
"home.settings": "Indstillinger",
|
||||
"home.short_date": "{day}. {month}",
|
||||
"home.shortcuts": "Tastaturgenveje",
|
||||
"home.start_hint": "Opret et design, åbn en nylig fil, eller udforsk et eksempel.",
|
||||
"home.whats_new": "Nyheder",
|
||||
"home.yesterday": "I går",
|
||||
"install.button_tip": "Installér SolidCraft som en app: Den åbner i sit eget vindue og virker offline",
|
||||
"install.dock_1": "Vælg Arkiv › Føj til Dock.",
|
||||
"install.dock_2": "Klik på Tilføj.",
|
||||
@@ -1022,7 +1060,7 @@
|
||||
"search.try": "Skriv et kommandonavn, en feature i dette design eller en parameter. Du kører en kommando med værdier ved at skrive dens id og et JSON-objekt: {example}",
|
||||
"search.valid": "Gyldig",
|
||||
"settings.display_quality_note": "Finere niveauer viser krumme flader og kanter mere jævnt og tager længere tid at opbygge mesh til. Kun modelvisningen ændres, aldrig designet eller eksporten.",
|
||||
"settings.frame_rate_note": "Viser billeder pr. sekund nederst til højre i modelvisningen. Når dette er slået til, gentegnes visningen løbende.",
|
||||
"settings.frame_rate_note": "Viser billeder pr. sekund nederst til højre i modelvisningen.",
|
||||
"settings.measure_axis_distances": "Vis X/Y/Z-afstande",
|
||||
"settings.measure_axis_distances_help": "Vis afstanden langs X-, Y- og Z-aksen sammen med den samlede afstand i Mål.",
|
||||
"settings.msaa_applies_on_restart": "Kantudjævning træder i kraft, næste gang SolidCraft starter",
|
||||
@@ -1145,6 +1183,7 @@
|
||||
"ui.agent_chat_files_left_out": "Højst {max} filer pr. besked; udeladt: {files}",
|
||||
"ui.agent_chat_files_loading": "Venter på indlæsning af filerne",
|
||||
"ui.agent_chat_files_max": "Højst {max} filer i én besked",
|
||||
"ui.agent_chat_flatpak_none_found": "Ingen kodeagent fundet. Flatpak-sandkassen kan som standard ikke se agenter, der er installeret i din hjemmemappe, og har ingen netværksadgang. For at tillade Claude Code og dets ACP-adapter installeret via nvm eller i ~/.local/bin skal du køre denne kommando i en terminal og derefter genstarte SolidCraft. Den giver adgang til de nævnte programmer og loginfiler samt netværksadgang:",
|
||||
"ui.agent_chat_import_failed": "{name}: Importen mislykkedes: {error}",
|
||||
"ui.agent_chat_intro": "Chat med en kodeagent, du allerede bruger, eller en modeltjeneste, du vælger. Den arbejder på det åbne design gennem SolidCrafts værktøjer: Ændringer vises undervejs og fortrydes som ét trin pr. besked. Hvad den må gøre uden at spørge, angives i Indstillinger > AI.",
|
||||
"ui.agent_chat_key_in_env": "nøgle angivet",
|
||||
@@ -1464,6 +1503,7 @@
|
||||
"ui.grid_follows_view_hint": "Sidevisninger viser gitteret på det lodrette plan, der vender mod dem, i stedet for grundplanet",
|
||||
"ui.ground_shadow": "Skygge på grundplan",
|
||||
"ui.groupSelected": "Gruppér valgte",
|
||||
"ui.hatch": "Skravering",
|
||||
"ui.heading_add_ins": "ADD-INS",
|
||||
"ui.heading_assemble": "SAMLING",
|
||||
"ui.heading_box": "BOKS",
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
"Active": "Aktiv",
|
||||
"Active rule": "Aktive Regel",
|
||||
"Add": "Hinzufügen",
|
||||
"Add Column": "Spalte hinzufügen",
|
||||
"Add Key": "Schlüssel hinzufügen",
|
||||
"Add Mid Radius": "Mittleren Radius hinzufügen",
|
||||
"Add Row": "Zeile hinzufügen",
|
||||
"Add a tool": "Werkzeug hinzufügen",
|
||||
"Add condition": "Bedingung hinzufügen",
|
||||
"Add or edit rule": "Regel hinzufügen oder bearbeiten",
|
||||
"Add to New Folder": "Zu neuem Ordner hinzufügen",
|
||||
"Advanced": "Erweitert",
|
||||
"Against": "Gegen",
|
||||
"Alignment": "Ausrichtung",
|
||||
"All Sketch Points": "Alle Skizzenpunkte",
|
||||
@@ -31,6 +35,7 @@
|
||||
"Appearance name": "Name des Erscheinungsbilds",
|
||||
"Applied Radius": "Angewendeter Radius",
|
||||
"Apply To": "Anwenden auf",
|
||||
"Apply library appearance": "Erscheinungsbild aus der Bibliothek anwenden",
|
||||
"Apply to selection": "Auf Auswahl anwenden",
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Wenden Sie zuerst Ihr Erscheinungsbild an. Exportiert zugewiesene Erscheinungsbilder und in diesem Design erstellte Materialien.",
|
||||
"Arc length": "Bogenlänge",
|
||||
@@ -41,13 +46,18 @@
|
||||
"BLEND SURFACE": "ÜBERGANGSFLÄCHE",
|
||||
"BOUNDARY SURFACE": "BERANDUNGSFLÄCHE",
|
||||
"BROWSER": "BROWSER",
|
||||
"Bend Edges": "Biegekanten",
|
||||
"Bend Line Position": "Position der Biegelinie",
|
||||
"Bend Position": "Biegeposition",
|
||||
"Bend Radius": "Biegeradius",
|
||||
"Bend Table": "Biegetabelle",
|
||||
"Bends": "Biegungen",
|
||||
"Bodies": "Körper",
|
||||
"Bodies (optional)": "Körper (optional)",
|
||||
"Body": "Körper",
|
||||
"Body (last if none)": "Körper (letzter, wenn keiner)",
|
||||
"Body to split": "Zu teilender Körper",
|
||||
"Body, or faces to take out": "Körper oder zu entfernende Flächen",
|
||||
"Boundary": "Begrenzung",
|
||||
"Bounding box": "Hüllquader",
|
||||
"CHECK": "PRÜFEN",
|
||||
@@ -59,7 +69,9 @@
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV or JSON: updates existing parameters, adds new ones",
|
||||
"Capture All Positions": "Alle Positionen erfassen",
|
||||
"Capture Pos.": "Pos. erfassen",
|
||||
"Catch Depth": "Fangtiefe",
|
||||
"Catch Direction": "Fangrichtung",
|
||||
"Catch Length": "Fanglänge",
|
||||
"Cells": "Zellen",
|
||||
"Center X": "Mittelpunkt X",
|
||||
"Center Y": "Mittelpunkt Y",
|
||||
@@ -68,15 +80,21 @@
|
||||
"Chamfer Type": "Abschrägungstyp",
|
||||
"Check collisions": "Kollisionen prüfen",
|
||||
"Checked": "Geprüft",
|
||||
"Choose Variable radius or Spline in Type.": "Unter „Typ“ „Variable radius“ oder „Spline“ wählen.",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Wählen Sie eine neue Ebene. Skizzengeometrie und Bemaßungen bleiben in ihren zweidimensionalen Koordinaten unverändert.",
|
||||
"Choose…": "Auswählen…",
|
||||
"Circular": "Kreisförmig",
|
||||
"Circumference": "Umfang",
|
||||
"Close draft": "Entwurf schließen",
|
||||
"Collapse": "Einklappen",
|
||||
"Collisions": "Kollisionen",
|
||||
"Colour": "Farbe",
|
||||
"Command": "Befehl",
|
||||
"Comment": "Kommentar",
|
||||
"Component 1": "Komponente 1",
|
||||
"Component 2": "Komponente 2",
|
||||
"Component1": "Komponente1",
|
||||
"Components": "Komponenten",
|
||||
"Config Table": "Konfigurationstabelle",
|
||||
"Configuration": "Konfiguration",
|
||||
"Configuration rules": "Configuration rules",
|
||||
@@ -84,6 +102,7 @@
|
||||
"Contact Sets": "Kontaktsätze",
|
||||
"Contact sets": "Kontaktsätze",
|
||||
"Continuity": "Stetigkeit",
|
||||
"Control Radius": "Kontrollradius",
|
||||
"Copy Error Message": "Fehlermeldung kopieren",
|
||||
"Copy Warning Message": "Warnmeldung kopieren",
|
||||
"Copy all licences": "Alle Lizenzen kopieren",
|
||||
@@ -93,8 +112,11 @@
|
||||
"Corner Relief": "Eckenentlastung",
|
||||
"Corner Relief Shape": "Form der Eckenentlastung",
|
||||
"Counterbore depth": "Tiefe der Senkung",
|
||||
"Counterbore Ø": "Tiefenbohrung Ø",
|
||||
"Countersink angle": "Winkel der Kegelsenkung",
|
||||
"Countersink Ø": "Senkung Ø",
|
||||
"Create Issue — review draft": "Issue erstellen — Entwurf prüfen",
|
||||
"Create Issue…": "Issue erstellen…",
|
||||
"Cross curves": "Querkurven",
|
||||
"Cube": "Würfel",
|
||||
"Current orthographic view (1:1 mm)": "Aktuelle orthogonale Ansicht (1:1 mm)",
|
||||
@@ -103,7 +125,11 @@
|
||||
"Curvature (G2)": "Krümmung (G2)",
|
||||
"Curvature change (G3)": "Krümmungsänderung (G3)",
|
||||
"Customize Toolbar": "Symbolleiste anpassen",
|
||||
"Customize…": "Anpassen…",
|
||||
"Cylindrical face": "Zylindrische Fläche",
|
||||
"Delete": "Löschen",
|
||||
"Delete Selected Features…": "Ausgewählte Merkmale löschen…",
|
||||
"Delete Selected Folders": "Ausgewählte Ordner löschen",
|
||||
"Delete all": "Alle löschen",
|
||||
"Delete rule": "Regel löschen",
|
||||
"Delete the row": "Zeile löschen",
|
||||
@@ -112,8 +138,11 @@
|
||||
"Dependents": "Abhängige Elemente",
|
||||
"Designs you open or save appear here.": "Designs, die Sie öffnen oder speichern, erscheinen hier.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Bestimmt durch Bis Objekt. Wählen Sie Winkel, um den Winkelbereich zu bearbeiten.",
|
||||
"Diameter": "Durchmesser",
|
||||
"Direction": "Richtung",
|
||||
"Discard": "Verwerfen",
|
||||
"Distance": "Abstand",
|
||||
"Distance 1": "Abstand 1",
|
||||
"Do not export hidden edges": "Verdeckte Kanten nicht exportieren",
|
||||
"Don't ask again": "Nicht mehr fragen",
|
||||
"Double-click to rename": "Double-click to rename",
|
||||
@@ -125,17 +154,21 @@
|
||||
"Edit Profile Sketch": "Profile-Skizze bearbeiten",
|
||||
"Enable rule": "Regel aktivieren",
|
||||
"Enabled": "Aktiviert",
|
||||
"End Radius": "Endradius",
|
||||
"End bulge": "Endausbauchung",
|
||||
"End continuity": "Endstetigkeit",
|
||||
"Expand": "Ausklappen",
|
||||
"Explode": "Explodieren",
|
||||
"Explode Text": "Text auflösen",
|
||||
"Export Animation…": "Animation exportieren…",
|
||||
"Export Bend Table…": "Biegetabelle exportieren…",
|
||||
"Export CSV…": "CSV exportieren…",
|
||||
"Export Current View as DXF": "Aktuelle Ansicht als DXF exportieren",
|
||||
"Export Current View as DXF…": "Aktuelle Ansicht als DXF exportieren…",
|
||||
"Export DXF": "DXF exportieren",
|
||||
"Export DXF…": "DXF exportieren…",
|
||||
"Export JSON…": "JSON exportieren…",
|
||||
"Export design library...": "Designbibliothek exportieren...",
|
||||
"Export hidden edges as dashed lines": "Verdeckte Kanten gestrichelt exportieren",
|
||||
"Expression": "Ausdruck",
|
||||
"Extend to meet": "Verlängern, bis sie sich treffen",
|
||||
@@ -143,31 +176,40 @@
|
||||
"FILES": "DATEIEN",
|
||||
"FLAT": "ABWICKLUNG",
|
||||
"Face": "Fläche",
|
||||
"Face Appearance": "Flächenerscheinungsbild",
|
||||
"Face attachment lost": "Flächenanbindung verloren",
|
||||
"Faces": "Flächen",
|
||||
"Faces / edges / vertices": "Flächen / Kanten / Ecken",
|
||||
"Faces to split": "Zu teilende Flächen",
|
||||
"Fastener": "Verbindungselement",
|
||||
"Favorites": "Favoriten",
|
||||
"Favourite": "Favourite",
|
||||
"Feature": "Funktion",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Dateien aus vielen CAD-Programmen haben Y nach oben und öffnen sich hier daher auf dem Rücken liegend (Z nach oben).",
|
||||
"Filter": "Filter",
|
||||
"Filter…": "Filtern…",
|
||||
"Find in Browser": "Im Browser suchen",
|
||||
"Finish": "Oberfläche",
|
||||
"First plane": "Erste Ebene",
|
||||
"Fit and G2 corners require Constant radius.": "Passung und G2-Ecken erfordern Constant radius.",
|
||||
"Fixed Face": "Feste Fläche",
|
||||
"Flat Pattern": "Flachmuster",
|
||||
"Flat size": "Abwicklungsgröße",
|
||||
"Follow camera (Look At sketches)": "Kamera folgen (Skizzen ansehen)",
|
||||
"Follow the camera (Look At sketches)": "Der Kamera folgen (Skizzen ansehen)",
|
||||
"Font": "Schriftart",
|
||||
"From": "Von",
|
||||
"From Bodies": "Aus Körpern",
|
||||
"From edges": "Ausgangskanten",
|
||||
"Full Length": "Volle Länge",
|
||||
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2-Ecken erfordern einen konstanten Radius und Exact fit. Die Konstruktion von G2-Ecken wird noch nicht unterstützt.",
|
||||
"Gap": "Spalt",
|
||||
"Grid": "Raster",
|
||||
"HISTORY": "VERLAUF",
|
||||
"Height": "Höhe",
|
||||
"Hidden edges": "Verdeckte Kanten",
|
||||
"History layout": "Verlaufslayout",
|
||||
"Hole (optional)": "Bohrung (optional)",
|
||||
"Holes": "Löcher",
|
||||
"Holes (edges on them)": "Löcher (ihre Kanten)",
|
||||
"Home view": "Startansicht",
|
||||
@@ -178,14 +220,20 @@
|
||||
"In place (surfaces)": "An Ort und Stelle (Flächen)",
|
||||
"In plane": "In der Ebene",
|
||||
"Inside": "Innen",
|
||||
"Instant": "Sofort",
|
||||
"Interface font": "Schriftart der Oberfläche",
|
||||
"Interferes": "Überschneidet sich",
|
||||
"Invert": "Umkehren",
|
||||
"Isocurve": "Isokurve",
|
||||
"Issue title": "Issue-Titel",
|
||||
"Items": "Elemente",
|
||||
"JOINTS": "GELENKE",
|
||||
"Joint Origin1": "Gelenkursprung1",
|
||||
"Keep (points on it)": "Behalten (Punkte darauf)",
|
||||
"Keep Tools": "Werkzeuge behalten",
|
||||
"Keep of the other surface": "Vom anderen Flächenkörper behalten",
|
||||
"Key a joint's value at this step": "Gelenkwert in diesem Schritt als Schlüsselbild setzen",
|
||||
"Keys": "Schlüsselbilder",
|
||||
"LIBRARY": "BIBLIOTHEK",
|
||||
"LINKS": "VERKNÜPFUNGEN",
|
||||
"Later": "Später",
|
||||
@@ -196,9 +244,14 @@
|
||||
"Letter Spacing": "Zeichenabstand",
|
||||
"Library": "Bibliothek",
|
||||
"Library file": "Bibliotheksdatei",
|
||||
"Limits": "Grenzen",
|
||||
"Line Spacing": "Zeilenabstand",
|
||||
"Line Type": "Linientyp",
|
||||
"Listing the installed fonts…": "Installierte Schriftarten werden aufgelistet…",
|
||||
"Load library": "Bibliothek laden",
|
||||
"Local library": "Lokale Bibliothek",
|
||||
"Loop": "Schleife",
|
||||
"M5 or M5x0.5": "M5 oder M5x0.5",
|
||||
"MANAGE": "VERWALTEN",
|
||||
"MATERIALS": "MATERIALIEN",
|
||||
"MODEL": "MODELL",
|
||||
@@ -209,14 +262,20 @@
|
||||
"Matched restrictions check the final values.": "Zutreffende Einschränkungen prüfen die endgültigen Werte.",
|
||||
"Material": "Material",
|
||||
"Max": "Max",
|
||||
"Maximum": "Maximum",
|
||||
"Mesh body": "Netzkörper",
|
||||
"Mid Radius": "Mittlerer Radius",
|
||||
"Middle double-click": "Doppelklick mit mittlerer Taste",
|
||||
"Min": "Min",
|
||||
"Minimum": "Minimum",
|
||||
"Mirror Plane": "Spiegelebene",
|
||||
"Mitre": "Gehrung",
|
||||
"Model Parameters": "Modellparameter",
|
||||
"Modeled": "Modelliert",
|
||||
"More toolbar panels": "Weitere Symbolleistenbereiche",
|
||||
"Motion": "Bewegung",
|
||||
"Mouse gestures": "Mausgesten",
|
||||
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Mausgesten funktionieren nur im SolidWorks-Mausmodus (das Rad dreht, Strg + Rad verschiebt).",
|
||||
"Move / Copy": "Verschieben / Kopieren",
|
||||
"Move Type": "Verschiebungstyp",
|
||||
"Move down": "Nach unten",
|
||||
@@ -238,8 +297,11 @@
|
||||
"Not available yet": "Noch nicht verfügbar",
|
||||
"Notch": "Ausklinkung",
|
||||
"OUTPUT": "AUSGABE",
|
||||
"Of": "Freiheitsgrad",
|
||||
"On your computer": "Auf diesem Computer",
|
||||
"Opacity": "Deckkraft",
|
||||
"Open GitHub draft": "GitHub-Entwurf öffnen",
|
||||
"Open library...": "Bibliothek öffnen...",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "Öffnen Sie lokale Bibliotheken in der Desktop-App. Die Bibliothek dieses Designs können Sie hier exportieren.",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Quelloffenes parametrisches 3D-CAD in Rust. Lizenziert unter MIT OR Apache-2.0.",
|
||||
"Operator": "Operator",
|
||||
@@ -258,8 +320,10 @@
|
||||
"Pick the tool bodies": "Werkzeugkörper wählen",
|
||||
"Picked": "Ausgewählte",
|
||||
"Picked point": "Gewählter Punkt",
|
||||
"Pivot": "Drehpunkt",
|
||||
"Place at the origin instead": "Stattdessen im Ursprung platzieren",
|
||||
"Plastic Rules": "Kunststoffregeln",
|
||||
"Play the joint's motion in the view": "Die Bewegung des Gelenks in der Ansicht abspielen",
|
||||
"Playback": "Wiedergabe",
|
||||
"Plot": "Diagramm",
|
||||
"Position": "Position",
|
||||
@@ -267,14 +331,18 @@
|
||||
"Position (G0)": "Position (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "Position entlang der Kante, 0 am Anfang, 1 am anderen Ende",
|
||||
"Preferences › Design turns the offer back on": "Unter Einstellungen › Design lässt sich das Angebot wieder einschalten",
|
||||
"Preview": "Vorschau",
|
||||
"Preview Row": "Vorschauzeile",
|
||||
"Preview leaves the current design unchanged.": "Die Vorschau lässt das aktuelle Design unverändert.",
|
||||
"Profile / Edges": "Profil / Kanten",
|
||||
"Profiles or curves": "Profile oder Kurven",
|
||||
"Profiles/Curves": "Profile/Kurven",
|
||||
"Quantity": "Größe",
|
||||
"RECENT": "ZULETZT",
|
||||
"REFERENCE": "REFERENZ",
|
||||
"RULED SURFACE": "REGELFLÄCHE",
|
||||
"RULES": "REGELN",
|
||||
"Radius": "Radius",
|
||||
"Radius Fit": "Radiusanpassung",
|
||||
"Ratio": "Verhältnis",
|
||||
"Reattach selected face": "Ausgewählte Fläche neu anbinden",
|
||||
@@ -283,6 +351,8 @@
|
||||
"Rectangular": "Rechteckig",
|
||||
"Reference face": "Referenzfläche",
|
||||
"Remove Tools": "Werkzeuge entfernen",
|
||||
"Remove condition": "Bedingung entfernen",
|
||||
"Remove from Folder": "Aus Ordner entfernen",
|
||||
"Remove from list": "Aus Liste entfernen",
|
||||
"Remove override": "Überschreibung entfernen",
|
||||
"Remove private design details or paths before sharing.": "Entfernen Sie vor dem Teilen private Designdetails oder Pfade.",
|
||||
@@ -290,12 +360,16 @@
|
||||
"Remove the column": "Spalte entfernen",
|
||||
"Remove the track": "Spur entfernen",
|
||||
"Remove this point": "Diesen Punkt entfernen",
|
||||
"Rename Feature": "Merkmal umbenennen",
|
||||
"Rename Folder": "Ordner umbenennen",
|
||||
"Replace existing library file": "Vorhandene Bibliotheksdatei ersetzen",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen",
|
||||
"Reset Toolbar": "Symbolleiste zurücksetzen",
|
||||
"Resize Part": "Teil skalieren",
|
||||
"Restore View": "Ansicht wiederherstellen",
|
||||
"Restore outer boundary": "Äußere Begrenzung wiederherstellen",
|
||||
"Restore the inherited component or physical material appearance": "Das geerbte Erscheinungsbild der Komponente oder des physikalischen Materials wiederherstellen",
|
||||
"Restore this studio's tools": "Werkzeuge dieses Studios wiederherstellen",
|
||||
"Result": "Ergebnis",
|
||||
"Retake screenshot": "Screenshot erneut aufnehmen",
|
||||
"Reverse": "Umkehren",
|
||||
@@ -305,7 +379,13 @@
|
||||
"Revert Pos.": "Pos. zurücksetzen",
|
||||
"Revert Position": "Position zurücksetzen",
|
||||
"Review the report and screenshot before opening GitHub.": "Prüfen Sie Bericht und Screenshot, bevor Sie GitHub öffnen.",
|
||||
"Rib Length": "Rippenlänge",
|
||||
"Rib Offset": "Rippenversatz",
|
||||
"Rib Thickness": "Rippendicke",
|
||||
"Ribs": "Rippen",
|
||||
"Rim Edge": "Randkante",
|
||||
"Ring of tools on the right button": "Werkzeugring auf der rechten Maustaste",
|
||||
"Roll Back Here": "Hierher zurücksetzen",
|
||||
"Roll History Marker Here": "Verlaufsmarkierung hierher verschieben",
|
||||
"Round": "Rund",
|
||||
"Rule": "Rule",
|
||||
@@ -322,13 +402,18 @@
|
||||
"Search": "Suchen",
|
||||
"Search commands": "Befehle suchen",
|
||||
"Search this library": "Diese Bibliothek durchsuchen",
|
||||
"Second plane": "Zweite Ebene",
|
||||
"Section": "Schnitt",
|
||||
"Sections (one sketch each)": "Querschnitte (je eine Skizze)",
|
||||
"Select Other": "Andere auswählen",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Anderes auswählen: Halten Sie die linke Taste ruhig auf dem Modell gedrückt (oder Rechtsklick › Anderes auswählen…), um alles unter dem Cursor aufzulisten; klicken Sie erneut an dieselbe Stelle, um das nächste Element dort auszuwählen. Doppelklick mit der mittleren Taste wechselt zur Startansicht (Einstellungen › Navigation).",
|
||||
"Select bodies (a face picks its body)": "Körper auswählen (eine Fläche wählt ihren Körper)",
|
||||
"Select bodies or faces instead": "Stattdessen Körper oder Flächen auswählen",
|
||||
"Select edges": "Kanten auswählen",
|
||||
"Selection filter": "Auswahlfilter",
|
||||
"Semantic faces share their appearance across instances.": "Semantische Flächen teilen ihr Erscheinungsbild über alle Instanzen hinweg.",
|
||||
"Send your report on GitHub": "Bericht auf GitHub senden",
|
||||
"Shaded": "Schattiert",
|
||||
"Shape": "Form",
|
||||
"Sheet Rules": "Blechregeln",
|
||||
"Show Agent Cursor": "Agenten-Cursor anzeigen",
|
||||
@@ -346,6 +431,7 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft wurde beendet, bevor diese Designs gespeichert wurden. Ihr letzter Autosave kann geöffnet werden.",
|
||||
"Specialise": "Spezialisieren",
|
||||
"Speed": "Geschwindigkeit",
|
||||
"Splitting tool": "Trennwerkzeug",
|
||||
"Stand it up": "Aufrichten",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Aufrichten: eine Vierteldrehung um X, als ein Verschieben-Merkmal, das sich rückgängig machen lässt.",
|
||||
"Stand this part up?": "Dieses Teil aufrichten?",
|
||||
@@ -355,14 +441,20 @@
|
||||
"Steps": "Schritte",
|
||||
"Stitch the kept pieces": "Behaltene Teile vernähen",
|
||||
"Stitch to both bodies": "Mit beiden Körpern vernähen",
|
||||
"Studio": "Studio",
|
||||
"Study": "Untersuchung",
|
||||
"Style": "Stil",
|
||||
"Suppress": "Unterdrücken",
|
||||
"Suppressed": "Unterdrückt",
|
||||
"Surface": "Flächenkörper",
|
||||
"Surface, or faces to turn over": "Fläche oder umzudrehende Flächen",
|
||||
"Swap Ends": "Enden vertauschen",
|
||||
"TRIM": "TRIMMEN",
|
||||
"Tangent (G1)": "Tangente (G1)",
|
||||
"Tangent Chain": "Tangentenkette",
|
||||
"Tangent chain": "Tangentialkette",
|
||||
"Target": "Ziel",
|
||||
"Target Body": "Zielkörper",
|
||||
"Tear": "Reißen",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Die gestrichelte Skizze ist eine gespeicherte Ebenenführung. Wählen Sie eine neue Fläche, um die Anbindung zu reparieren.",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Das gespeicherte Autorvorkommen fehlt. Wählen Sie vor dem erneuten Anbinden eine gültige Autorplatzierung.",
|
||||
@@ -375,24 +467,31 @@
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "Dieses untergeordnete Element ist geteilt. Löschen entfernt es aus jeder Kopie des übergeordneten Elements.",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "Dieses untergeordnete Element ist geteilt. Positionsänderungen gelten für jede Kopie des übergeordneten Elements.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Diese Fläche kann keine dauerhafte Anbindung bieten. Wählen Sie eine andere Fläche oder verwenden Sie ausdrücklich eine feste Ebene.",
|
||||
"To": "Bis",
|
||||
"To Offset": "Bis Versatz",
|
||||
"To edges": "Zielkanten",
|
||||
"Tolerance": "Toleranz",
|
||||
"Too close": "Zu nah",
|
||||
"Tool Bodies": "Werkzeugkörper",
|
||||
"Tools": "Werkzeuge",
|
||||
"Tools, clockwise from the top": "Werkzeuge, im Uhrzeigersinn von oben",
|
||||
"Trimming tool (plane, face or curves)": "Trimmwerkzeug (Ebene, Fläche oder Kurven)",
|
||||
"Turn off section analysis before exporting the view.": "Deaktivieren Sie die Schnittanalyse, bevor Sie die Ansicht exportieren.",
|
||||
"Type": "Typ",
|
||||
"UNTRIM": "TRIMMEN AUFHEBEN",
|
||||
"Unit": "Einheit",
|
||||
"Unsuppress": "Unterdrückung aufheben",
|
||||
"Updating the preview": "Vorschau wird aktualisiert",
|
||||
"Use active author placement": "Aktive Autorplatzierung verwenden",
|
||||
"Use fixed plane": "Feste Ebene verwenden",
|
||||
"Use saved fixed plane": "Gespeicherte feste Ebene verwenden",
|
||||
"Use selected face as fixed plane": "Ausgewählte Fläche als feste Ebene verwenden",
|
||||
"Use the SolidWorks mouse mode": "SolidWorks-Mausmodus verwenden",
|
||||
"User Parameters": "Benutzerparameter",
|
||||
"VIEW": "ANSICHT",
|
||||
"VIEWS": "ANSICHTEN",
|
||||
"Value": "Value",
|
||||
"Variable Radius": "Variabler Radius",
|
||||
"Velocity": "Geschwindigkeit",
|
||||
"Vertices": "Scheitelpunkte",
|
||||
"View navigator": "Ansichtsnavigator",
|
||||
@@ -403,10 +502,18 @@
|
||||
"When": "When",
|
||||
"Width": "Breite",
|
||||
"Will fail": "Schlägt fehl",
|
||||
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "Mit eingeschalteten Mausgesten dreht die rechte Maustaste die Ansicht nicht mehr: Taste gedrückt halten und bewegen öffnet den Werkzeugring, dann die Zone eines Werkzeugs berühren. Ein einfacher Rechtsklick öffnet weiterhin das Menü.",
|
||||
"X": "X",
|
||||
"X Angle": "X-Winkel",
|
||||
"X angle": "X-Winkel",
|
||||
"X distance": "X-Abstand",
|
||||
"Y": "Y",
|
||||
"Y Angle": "Y-Winkel",
|
||||
"Y angle": "Y-Winkel",
|
||||
"Y distance": "Y-Abstand",
|
||||
"Z": "Z",
|
||||
"Z Angle": "Z-Winkel",
|
||||
"Z Distance": "Z-Abstand",
|
||||
"Z angle": "Z-Winkel",
|
||||
"Z distance": "Z-Abstand",
|
||||
"Zebra": "Zebra",
|
||||
@@ -455,8 +562,10 @@
|
||||
"cmd.component_activate": "Komponente aktivieren",
|
||||
"cmd.component_break_link": "Verknüpfung trennen",
|
||||
"cmd.component_capture_position": "Position erfassen",
|
||||
"cmd.component_copies": "Identische Kopien finden",
|
||||
"cmd.component_create": "Neue Komponente",
|
||||
"cmd.component_delete": "Komponente löschen",
|
||||
"cmd.component_extract": "In Datei extrahieren",
|
||||
"cmd.component_from_bodies": "Komponenten aus Körpern erstellen",
|
||||
"cmd.component_insert_design": "Konstruktion einfügen",
|
||||
"cmd.component_links": "Verknüpfungen auflisten",
|
||||
@@ -469,6 +578,7 @@
|
||||
"cmd.component_properties": "Physikalische Eigenschaften der Komponente",
|
||||
"cmd.component_relink": "Quelle neu verknüpfen",
|
||||
"cmd.component_rename": "Komponente umbenennen",
|
||||
"cmd.component_replace_with_link": "Durch Verknüpfung ersetzen",
|
||||
"cmd.component_revert_position": "Position zurücksetzen",
|
||||
"cmd.component_unlock": "Inhalt entsperren",
|
||||
"cmd.component_update_links": "Verknüpfungen aktualisieren",
|
||||
@@ -859,19 +969,57 @@
|
||||
"help.mit": "MIT-Lizenz",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Drittanbieterlizenzen",
|
||||
"home.back": "Zurück zu {name}",
|
||||
"home.crashes": "Absturzberichte",
|
||||
"home.design_workspace": "Entwurf",
|
||||
"home.documentation": "Dokumentation",
|
||||
"home.export": "Exportieren",
|
||||
"home.export_hint": "Wählen Sie ein Format für den aktuellen Entwurf.",
|
||||
"home.export_json_hint": "Bearbeitbarer Entwurf und Befehlsverlauf",
|
||||
"home.export_mesh_hint": "Netz zum Drucken oder Teilen",
|
||||
"home.export_step_hint": "Präzise Volumengeometrie",
|
||||
"home.import": "Importieren…",
|
||||
"home.import_hint": "STEP, IGES oder Netz in einen neuen Entwurf",
|
||||
"home.in_this_browser": "in diesem Browser",
|
||||
"home.missing": "fehlt",
|
||||
"home.month_01": "Jan.",
|
||||
"home.month_02": "Feb.",
|
||||
"home.month_03": "März",
|
||||
"home.month_04": "Apr.",
|
||||
"home.month_05": "Mai",
|
||||
"home.month_06": "Juni",
|
||||
"home.month_07": "Juli",
|
||||
"home.month_08": "Aug.",
|
||||
"home.month_09": "Sept.",
|
||||
"home.month_10": "Okt.",
|
||||
"home.month_11": "Nov.",
|
||||
"home.month_12": "Dez.",
|
||||
"home.new_inches": "Neuer Entwurf in Zoll",
|
||||
"home.new_inches_hint": "Zoll · öffnet sich im Arbeitsbereich Entwurf",
|
||||
"home.new_mm": "Neuer Entwurf",
|
||||
"home.new_mm_hint": "Millimeter · öffnet sich im Arbeitsbereich Entwurf",
|
||||
"home.open": "Öffnen…",
|
||||
"home.opened_days_ago": "Vor {count} Tag(en) geöffnet",
|
||||
"home.opened_hours_ago": "Vor {count} Std. geöffnet",
|
||||
"home.opened_just_now": "Gerade eben geöffnet",
|
||||
"home.opened_minutes_ago": "Vor {count} Min. geöffnet",
|
||||
"home.open_hint": "Ein .solidcraft-Entwurf ({shortcut})",
|
||||
"home.open_now": "Jetzt öffnen",
|
||||
"home.opened_days_ago": "Vor {count} Tagen",
|
||||
"home.opened_hours_ago": "Vor {count} Std.",
|
||||
"home.opened_just_now": "Gerade eben",
|
||||
"home.opened_minutes_ago": "Vor {count} Min.",
|
||||
"home.opening": "wird geöffnet…",
|
||||
"home.properties": "Entwurfseigenschaften",
|
||||
"home.recent": "Zuletzt verwendet",
|
||||
"home.recent_empty": "Entwürfe, die Sie öffnen oder speichern, erscheinen hier.",
|
||||
"home.recovery_count": "Wiederherstellbare Entwürfe: {count}",
|
||||
"home.recovery_empty": "Keine wiederherstellbaren Entwürfe.",
|
||||
"home.recovery_hint": "SolidCraft wurde geschlossen, bevor diese Entwürfe gespeichert wurden.",
|
||||
"home.report_issue": "Problem melden",
|
||||
"home.samples": "Beispiele",
|
||||
"home.settings": "Einstellungen",
|
||||
"home.short_date": "{day}. {month}",
|
||||
"home.shortcuts": "Tastenkombinationen",
|
||||
"home.start_hint": "Erstellen Sie einen Entwurf, öffnen Sie eine zuletzt verwendete Datei oder erkunden Sie ein Beispiel.",
|
||||
"home.whats_new": "Neuigkeiten",
|
||||
"home.yesterday": "Gestern",
|
||||
"install.button_tip": "SolidCraft als App installieren: eigenes Fenster, funktioniert auch offline",
|
||||
"install.dock_1": "Wählen Sie Ablage › Zum Dock hinzufügen.",
|
||||
"install.dock_2": "Klicken Sie auf „Hinzufügen“.",
|
||||
@@ -929,8 +1077,15 @@
|
||||
"motion.to_end": "Zum Ende",
|
||||
"motion.to_start": "Zum Anfang",
|
||||
"motion.track_name": "Spur {number}",
|
||||
"nav.alias.help": "Umschalt+Alt+Linksziehen dreht, Umschalt+Alt+Mitte verschiebt, Umschalt+Alt+Rechtsziehen zoomt; mittleres Ziehen verschiebt ebenfalls und rechtes Ziehen dreht",
|
||||
"nav.blender.help": "Mittlere Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt; rechtes Ziehen lässt die Kamera unverändert",
|
||||
"nav.free_orbit.help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollneigung bei. Das Einrasten mit Umschalt ist deaktiviert.",
|
||||
"nav.free_orbit.label": "Freie Drehung",
|
||||
"nav.fusion.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, rechte Maustaste dreht",
|
||||
"nav.inventor.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, Strg+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.openscad.help": "Linksziehen auf leerem Hintergrund dreht, Umschalt+Linksziehen zieht ein Auswahlrechteck auf, Skizzengeometrie wird durch Ziehen bearbeitet; rechtes oder mittleres Ziehen verschiebt, Umschalt+rechtes oder Umschalt+mittleres Ziehen zoomt",
|
||||
"nav.solidworks.help": "Mittlere Maustaste dreht, Strg+Mitte verschiebt, Umschalt+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.tinkercad.help": "Rechte Maustaste dreht, mittlere oder Umschalt+rechte Maustaste verschiebt",
|
||||
"nav.touchpad.help": "Umschalt gedrückt halten und den Zeiger zum Verschieben bewegen; mit Alt/Option drehen. Keine Maustaste nötig. Vertikales Scrollen zoomt.",
|
||||
"nav.touchpad.label": "Touchpad",
|
||||
"nav.touchpad.orbit": "Drehen (Alt/Option halten und bewegen; ohne Maustaste)",
|
||||
@@ -1026,7 +1181,7 @@
|
||||
"search.try": "Geben Sie einen Befehlsnamen, ein Merkmal in diesem Design oder einen Parameter ein. Um einen Befehl mit Werten auszuführen, geben Sie seine ID und ein JSON-Objekt ein: {example}",
|
||||
"search.valid": "Gültig",
|
||||
"settings.display_quality_note": "Feinere Stufen zeigen gekrümmte Flächen und Kanten glatter und brauchen länger zum Vernetzen. Nur die Ansicht ändert sich, nie das Design oder Exporte.",
|
||||
"settings.frame_rate_note": "Zeigt die Bilder pro Sekunde unten rechts in der Ansicht. Solange sie an ist, wird die Ansicht fortlaufend neu gezeichnet.",
|
||||
"settings.frame_rate_note": "Zeigt die Bilder pro Sekunde unten rechts in der Ansicht.",
|
||||
"settings.measure_axis_distances": "X/Y/Z-Maße anzeigen",
|
||||
"settings.measure_axis_distances_help": "Bei geöffneter Messung die Abstände entlang der Weltachsen zusätzlich zum Gesamtabstand anzeigen.",
|
||||
"settings.msaa_applies_on_restart": "Die Kantenglättung wird beim nächsten Start von SolidCraft übernommen",
|
||||
@@ -1149,6 +1304,7 @@
|
||||
"ui.agent_chat_files_left_out": "Höchstens {max} Dateien pro Nachricht; ausgelassen: {files}",
|
||||
"ui.agent_chat_files_loading": "Warten, bis die Dateien gelesen sind",
|
||||
"ui.agent_chat_files_max": "Höchstens {max} Dateien pro Nachricht",
|
||||
"ui.agent_chat_flatpak_none_found": "Kein Coding-Agent gefunden. Die Flatpak-Sandbox sieht standardmäßig keine Agenten in Ihrem Benutzerordner und hat keinen Netzwerkzugriff. Um Claude Code und seinen ACP-Adapter zu erlauben, die über nvm oder in ~/.local/bin installiert sind, führen Sie diesen Befehl in einem Terminal aus und starten Sie SolidCraft dann neu. Er gewährt Zugriff auf die genannten Programme und Anmeldedateien sowie Netzwerkzugriff:",
|
||||
"ui.agent_chat_import_failed": "{name}: Der Import ist fehlgeschlagen: {error}",
|
||||
"ui.agent_chat_intro": "Chatten Sie mit einem Coding-Agenten, den Sie schon nutzen, oder einem Modell-Endpunkt Ihrer Wahl. Er arbeitet über die Werkzeuge von SolidCraft am geöffneten Design: Seine Änderungen erscheinen sofort und lassen sich pro Nachricht in einem Schritt rückgängig machen. Was er ohne Nachfrage darf, legen Sie unter Einstellungen > KI fest.",
|
||||
"ui.agent_chat_key_in_env": "Schlüssel gesetzt",
|
||||
@@ -1437,6 +1593,9 @@
|
||||
"ui.export_keymap_tip": "Tasten, S-Box-Favoriten und die Symbolleiste als Datei",
|
||||
"ui.extend": "Verlängern",
|
||||
"ui.extent_type": "Ausdehnungstyp",
|
||||
"ui.extract_component": "In Datei extrahieren…",
|
||||
"ui.extract_design_changed": "Das Design hat sich beim Wählen einer Datei geändert. Starten Sie In Datei extrahieren erneut.",
|
||||
"ui.extract_done": "Die Komponente ist jetzt ein eigenes Design und hier verknüpft.",
|
||||
"ui.extrude": "Extrudieren",
|
||||
"ui.factor": "Faktor",
|
||||
"ui.feature_history": "Feature-Verlauf",
|
||||
@@ -1468,6 +1627,7 @@
|
||||
"ui.grid_follows_view_hint": "Seitenansichten zeigen das Raster auf der aufrechten Ebene, die ihnen zugewandt ist, statt auf dem Boden",
|
||||
"ui.ground_shadow": "Bodenschatten",
|
||||
"ui.groupSelected": "Auswahl gruppieren",
|
||||
"ui.hatch": "Schraffur",
|
||||
"ui.heading_add_ins": "ADD-INS",
|
||||
"ui.heading_assemble": "ZUSAMMENBAU",
|
||||
"ui.heading_box": "BLOCK",
|
||||
@@ -1568,6 +1728,10 @@
|
||||
"ui.length_decimals": "Längen-Dezimalstellen",
|
||||
"ui.light": "Hell",
|
||||
"ui.line": "Linie",
|
||||
"ui.link_copies": "Identische Kopien verknüpfen",
|
||||
"ui.link_copies_keep": "Behalten",
|
||||
"ui.link_copies_link": "Verknüpfen",
|
||||
"ui.link_copies_text": "Diese Komponenten erzeugen dieselben Körper wie {name}. Sollen sie zu Platzierungen seiner Verknüpfung werden?",
|
||||
"ui.lock": "Sperren",
|
||||
"ui.loft": "Loft",
|
||||
"ui.look_at": "Ansehen",
|
||||
@@ -1691,6 +1855,7 @@
|
||||
"ui.render": "Rendern",
|
||||
"ui.renderer": "Renderer",
|
||||
"ui.repeat": "Wiederholen",
|
||||
"ui.replace_with_link_done": "Die Kopien sind jetzt Platzierungen des verknüpften Designs.",
|
||||
"ui.report_a_bug": "Fehler melden",
|
||||
"ui.reset": "Zurücksetzen",
|
||||
"ui.reset_all": "Alle zurücksetzen",
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
"Active": "Active",
|
||||
"Active rule": "Active rule",
|
||||
"Add": "Add",
|
||||
"Add Column": "Add Column",
|
||||
"Add Key": "Add Key",
|
||||
"Add Mid Radius": "Add Mid Radius",
|
||||
"Add Row": "Add Row",
|
||||
"Add a tool": "Add a tool",
|
||||
"Add condition": "Add condition",
|
||||
"Add or edit rule": "Add or edit rule",
|
||||
"Add to New Folder": "Add to New Folder",
|
||||
"Advanced": "Advanced",
|
||||
"Against": "Against",
|
||||
"Alignment": "Alignment",
|
||||
"All Sketch Points": "All Sketch Points",
|
||||
@@ -32,6 +36,7 @@
|
||||
"Appearance name": "Appearance name",
|
||||
"Applied Radius": "Applied Radius",
|
||||
"Apply To": "Apply To",
|
||||
"Apply library appearance": "Apply library appearance",
|
||||
"Apply to selection": "Apply to selection",
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Apply your appearance first. Exports assigned appearances and materials created in this design.",
|
||||
"Arc length": "Arc length",
|
||||
@@ -42,13 +47,18 @@
|
||||
"BLEND SURFACE": "BLEND SURFACE",
|
||||
"BOUNDARY SURFACE": "BOUNDARY SURFACE",
|
||||
"BROWSER": "BROWSER",
|
||||
"Bend Edges": "Bend Edges",
|
||||
"Bend Line Position": "Bend Line Position",
|
||||
"Bend Position": "Bend Position",
|
||||
"Bend Radius": "Bend Radius",
|
||||
"Bend Table": "Bend Table",
|
||||
"Bends": "Bends",
|
||||
"Bodies": "Bodies",
|
||||
"Bodies (optional)": "Bodies (optional)",
|
||||
"Body": "Body",
|
||||
"Body (last if none)": "Body (last if none)",
|
||||
"Body to split": "Body to split",
|
||||
"Body, or faces to take out": "Body, or faces to take out",
|
||||
"Boundary": "Boundary",
|
||||
"Bounding box": "Bounding box",
|
||||
"CHECK": "CHECK",
|
||||
@@ -60,7 +70,9 @@
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV or JSON: updates existing parameters, adds new ones",
|
||||
"Capture All Positions": "Capture All Positions",
|
||||
"Capture Pos.": "Capture Pos.",
|
||||
"Catch Depth": "Catch Depth",
|
||||
"Catch Direction": "Catch Direction",
|
||||
"Catch Length": "Catch Length",
|
||||
"Cells": "Cells",
|
||||
"Center X": "Center X",
|
||||
"Center Y": "Center Y",
|
||||
@@ -69,15 +81,21 @@
|
||||
"Chamfer Type": "Chamfer Type",
|
||||
"Check collisions": "Check collisions",
|
||||
"Checked": "Checked",
|
||||
"Choose Variable radius or Spline in Type.": "Choose Variable radius or Spline in Type.",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.",
|
||||
"Choose…": "Choose…",
|
||||
"Circular": "Circular",
|
||||
"Circumference": "Circumference",
|
||||
"Close draft": "Close draft",
|
||||
"Collapse": "Collapse",
|
||||
"Collisions": "Collisions",
|
||||
"Colour": "Colour",
|
||||
"Command": "Command",
|
||||
"Comment": "Comment",
|
||||
"Component 1": "Component 1",
|
||||
"Component 2": "Component 2",
|
||||
"Component1": "Component1",
|
||||
"Components": "Components",
|
||||
"Config Table": "Config Table",
|
||||
"Configuration": "Configuration",
|
||||
"Configuration rules": "Configuration rules",
|
||||
@@ -86,6 +104,7 @@
|
||||
"Contact sets": "Contact sets",
|
||||
"Continue on GitHub": "Continue on GitHub",
|
||||
"Continuity": "Continuity",
|
||||
"Control Radius": "Control Radius",
|
||||
"Copy Error Message": "Copy Error Message",
|
||||
"Copy Warning Message": "Copy Warning Message",
|
||||
"Copy all licences": "Copy all licences",
|
||||
@@ -95,8 +114,11 @@
|
||||
"Corner Relief": "Corner Relief",
|
||||
"Corner Relief Shape": "Corner Relief Shape",
|
||||
"Counterbore depth": "Counterbore depth",
|
||||
"Counterbore Ø": "Counterbore Ø",
|
||||
"Countersink angle": "Countersink angle",
|
||||
"Countersink Ø": "Countersink Ø",
|
||||
"Create Issue — review draft": "Create Issue — review draft",
|
||||
"Create Issue…": "Create Issue…",
|
||||
"Cross curves": "Cross curves",
|
||||
"Cube": "Cube",
|
||||
"Current orthographic view (1:1 mm)": "Current orthographic view (1:1 mm)",
|
||||
@@ -106,7 +128,11 @@
|
||||
"Curvature change (G3)": "Curvature change (G3)",
|
||||
"Custom": "Custom",
|
||||
"Customize Toolbar": "Customize Toolbar",
|
||||
"Customize…": "Customize…",
|
||||
"Cylindrical face": "Cylindrical face",
|
||||
"Delete": "Delete",
|
||||
"Delete Selected Features…": "Delete Selected Features…",
|
||||
"Delete Selected Folders": "Delete Selected Folders",
|
||||
"Delete all": "Delete all",
|
||||
"Delete rule": "Delete rule",
|
||||
"Delete the row": "Delete the row",
|
||||
@@ -117,8 +143,11 @@
|
||||
"Describe what happened before continuing.": "Describe what happened before continuing.",
|
||||
"Designs you open or save appear here.": "Designs you open or save appear here.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Determined by To Object. Choose Angle to edit the angular extent.",
|
||||
"Diameter": "Diameter",
|
||||
"Direction": "Direction",
|
||||
"Discard": "Discard",
|
||||
"Distance": "Distance",
|
||||
"Distance 1": "Distance 1",
|
||||
"Do not export hidden edges": "Do not export hidden edges",
|
||||
"Don't ask again": "Don't ask again",
|
||||
"Double-click to rename": "Double-click to rename",
|
||||
@@ -130,44 +159,57 @@
|
||||
"Edit Profile Sketch": "Edit Profile Sketch",
|
||||
"Enable rule": "Enable rule",
|
||||
"Enabled": "Enabled",
|
||||
"End Radius": "End Radius",
|
||||
"End bulge": "End bulge",
|
||||
"End continuity": "End continuity",
|
||||
"Expand": "Expand",
|
||||
"Expected result (optional)": "Expected result (optional)",
|
||||
"Explode": "Explode",
|
||||
"Explode Text": "Explode Text",
|
||||
"Export Animation…": "Export Animation…",
|
||||
"Export Bend Table…": "Export Bend Table…",
|
||||
"Export CSV…": "Export CSV…",
|
||||
"Export Current View as DXF": "Export Current View as DXF",
|
||||
"Export Current View as DXF…": "Export Current View as DXF…",
|
||||
"Export DXF": "Export DXF",
|
||||
"Export DXF…": "Export DXF…",
|
||||
"Export JSON…": "Export JSON…",
|
||||
"Export design library...": "Export design library...",
|
||||
"Export hidden edges as dashed lines": "Export hidden edges as dashed lines",
|
||||
"Exported by SolidWorks, which usually saves Y up": "Exported by SolidWorks, which usually saves Y up",
|
||||
"Expression": "Expression",
|
||||
"Extend to meet": "Extend to meet",
|
||||
"Extents": "Extents",
|
||||
"FILES": "FILES",
|
||||
"FLAT": "FLAT",
|
||||
"Face": "Face",
|
||||
"Face Appearance": "Face Appearance",
|
||||
"Face attachment lost": "Face attachment lost",
|
||||
"Faces": "Faces",
|
||||
"Faces / edges / vertices": "Faces / edges / vertices",
|
||||
"Faces to split": "Faces to split",
|
||||
"Fastener": "Fastener",
|
||||
"Favorites": "Favorites",
|
||||
"Favourite": "Favourite",
|
||||
"Feature": "Feature",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Files from many CAD programs have Y up, so they open lying on their back here (Z up).",
|
||||
"Filter": "Filter",
|
||||
"Filter…": "Filter…",
|
||||
"Find in Browser": "Find in Browser",
|
||||
"Finish": "Finish",
|
||||
"First plane": "First plane",
|
||||
"Fit and G2 corners require Constant radius.": "Fit and G2 corners require Constant radius.",
|
||||
"Fixed Face": "Fixed Face",
|
||||
"Flat Pattern": "Flat Pattern",
|
||||
"Flat size": "Flat size",
|
||||
"Follow camera (Look At sketches)": "Follow camera (Look At sketches)",
|
||||
"Follow the camera (Look At sketches)": "Follow the camera (Look At sketches)",
|
||||
"Font": "Font",
|
||||
"From": "From",
|
||||
"From Bodies": "From Bodies",
|
||||
"From edges": "From edges",
|
||||
"Full Length": "Full Length",
|
||||
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.",
|
||||
"Gap": "Gap",
|
||||
"Grid": "Grid",
|
||||
"HISTORY": "HISTORY",
|
||||
@@ -175,6 +217,7 @@
|
||||
"Hidden edges": "Hidden edges",
|
||||
"History layout": "History layout",
|
||||
"History names": "History names",
|
||||
"Hole (optional)": "Hole (optional)",
|
||||
"Holes": "Holes",
|
||||
"Holes (edges on them)": "Holes (edges on them)",
|
||||
"Home view": "Home view",
|
||||
@@ -185,14 +228,20 @@
|
||||
"In place (surfaces)": "In place (surfaces)",
|
||||
"In plane": "In plane",
|
||||
"Inside": "Inside",
|
||||
"Instant": "Instant",
|
||||
"Interface font": "Interface font",
|
||||
"Interferes": "Interferes",
|
||||
"Invert": "Invert",
|
||||
"Isocurve": "Isocurve",
|
||||
"Issue title": "Issue title",
|
||||
"Items": "Items",
|
||||
"JOINTS": "JOINTS",
|
||||
"Joint Origin1": "Joint Origin1",
|
||||
"Keep (points on it)": "Keep (points on it)",
|
||||
"Keep Tools": "Keep Tools",
|
||||
"Keep of the other surface": "Keep of the other surface",
|
||||
"Key a joint's value at this step": "Key a joint's value at this step",
|
||||
"Keys": "Keys",
|
||||
"LIBRARY": "LIBRARY",
|
||||
"LINKS": "LINKS",
|
||||
"Later": "Later",
|
||||
@@ -203,9 +252,14 @@
|
||||
"Letter Spacing": "Letter Spacing",
|
||||
"Library": "Library",
|
||||
"Library file": "Library file",
|
||||
"Limits": "Limits",
|
||||
"Line Spacing": "Line Spacing",
|
||||
"Line Type": "Line Type",
|
||||
"Listing the installed fonts…": "Listing the installed fonts…",
|
||||
"Load library": "Load library",
|
||||
"Local library": "Local library",
|
||||
"Loop": "Loop",
|
||||
"M5 or M5x0.5": "M5 or M5x0.5",
|
||||
"MANAGE": "MANAGE",
|
||||
"MATERIALS": "MATERIALS",
|
||||
"MODEL": "MODEL",
|
||||
@@ -216,14 +270,20 @@
|
||||
"Matched restrictions check the final values.": "Matched restrictions check the final values.",
|
||||
"Material": "Material",
|
||||
"Max": "Max",
|
||||
"Maximum": "Maximum",
|
||||
"Mesh body": "Mesh body",
|
||||
"Mid Radius": "Mid Radius",
|
||||
"Middle double-click": "Middle double-click",
|
||||
"Min": "Min",
|
||||
"Minimum": "Minimum",
|
||||
"Mirror Plane": "Mirror Plane",
|
||||
"Mitre": "Mitre",
|
||||
"Model Parameters": "Model Parameters",
|
||||
"Modeled": "Modeled",
|
||||
"More toolbar panels": "More toolbar panels",
|
||||
"Motion": "Motion",
|
||||
"Mouse gestures": "Mouse gestures",
|
||||
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).",
|
||||
"Move / Copy": "Move / Copy",
|
||||
"Move Type": "Move Type",
|
||||
"Move down": "Move down",
|
||||
@@ -246,8 +306,11 @@
|
||||
"Not available yet": "Not available yet",
|
||||
"Notch": "Notch",
|
||||
"OUTPUT": "OUTPUT",
|
||||
"Of": "Of",
|
||||
"On your computer": "On your computer",
|
||||
"Opacity": "Opacity",
|
||||
"Open GitHub draft": "Open GitHub draft",
|
||||
"Open library...": "Open library...",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "Open local libraries in the desktop app. You can export this design's library here.",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.",
|
||||
"Opens your browser and copies the report text for you to paste into GitHub.": "Opens your browser and copies the report text for you to paste into GitHub.",
|
||||
@@ -269,8 +332,10 @@
|
||||
"Picked": "Picked",
|
||||
"Picked point": "Picked point",
|
||||
"Pitch": "Pitch",
|
||||
"Pivot": "Pivot",
|
||||
"Place at the origin instead": "Place at the origin instead",
|
||||
"Plastic Rules": "Plastic Rules",
|
||||
"Play the joint's motion in the view": "Play the joint's motion in the view",
|
||||
"Playback": "Playback",
|
||||
"Plot": "Plot",
|
||||
"Position": "Position",
|
||||
@@ -278,14 +343,18 @@
|
||||
"Position (G0)": "Position (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "Position along the edge, 0 at the start end, 1 at the other",
|
||||
"Preferences › Design turns the offer back on": "Preferences › Design turns the offer back on",
|
||||
"Preview": "Preview",
|
||||
"Preview Row": "Preview Row",
|
||||
"Preview leaves the current design unchanged.": "Preview leaves the current design unchanged.",
|
||||
"Profile / Edges": "Profile / Edges",
|
||||
"Profiles or curves": "Profiles or curves",
|
||||
"Profiles/Curves": "Profiles/Curves",
|
||||
"Quantity": "Quantity",
|
||||
"RECENT": "RECENT",
|
||||
"REFERENCE": "REFERENCE",
|
||||
"RULED SURFACE": "RULED SURFACE",
|
||||
"RULES": "RULES",
|
||||
"Radius": "Radius",
|
||||
"Radius Fit": "Radius Fit",
|
||||
"Ratio": "Ratio",
|
||||
"Reattach selected face": "Reattach selected face",
|
||||
@@ -294,6 +363,8 @@
|
||||
"Rectangular": "Rectangular",
|
||||
"Reference face": "Reference face",
|
||||
"Remove Tools": "Remove Tools",
|
||||
"Remove condition": "Remove condition",
|
||||
"Remove from Folder": "Remove from Folder",
|
||||
"Remove from list": "Remove from list",
|
||||
"Remove override": "Remove override",
|
||||
"Remove private design details or paths before sharing.": "Remove private design details or paths before sharing.",
|
||||
@@ -301,6 +372,9 @@
|
||||
"Remove the column": "Remove the column",
|
||||
"Remove the track": "Remove the track",
|
||||
"Remove this point": "Remove this point",
|
||||
"Rename Feature": "Rename Feature",
|
||||
"Rename Folder": "Rename Folder",
|
||||
"Replace existing library file": "Replace existing library file",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Report a bug — review a screenshot and open a GitHub draft",
|
||||
"Report text copied. Paste it into GitHub and submit your report there.": "Report text copied. Paste it into GitHub and submit your report there.",
|
||||
"Reports are public. A GitHub account is required.": "Reports are public. A GitHub account is required.",
|
||||
@@ -309,6 +383,7 @@
|
||||
"Restore View": "Restore View",
|
||||
"Restore outer boundary": "Restore outer boundary",
|
||||
"Restore the inherited component or physical material appearance": "Restore the inherited component or physical material appearance",
|
||||
"Restore this studio's tools": "Restore this studio's tools",
|
||||
"Result": "Result",
|
||||
"Retake screenshot": "Retake screenshot",
|
||||
"Reverse": "Reverse",
|
||||
@@ -319,7 +394,13 @@
|
||||
"Revert Position": "Revert Position",
|
||||
"Review the report and screenshot before opening GitHub.": "Review the report and screenshot before opening GitHub.",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "Review your report on GitHub and click Submit new issue to publish it.",
|
||||
"Rib Length": "Rib Length",
|
||||
"Rib Offset": "Rib Offset",
|
||||
"Rib Thickness": "Rib Thickness",
|
||||
"Ribs": "Ribs",
|
||||
"Rim Edge": "Rim Edge",
|
||||
"Ring of tools on the right button": "Ring of tools on the right button",
|
||||
"Roll Back Here": "Roll Back Here",
|
||||
"Roll History Marker Here": "Roll History Marker Here",
|
||||
"Round": "Round",
|
||||
"Rule": "Rule",
|
||||
@@ -338,13 +419,18 @@
|
||||
"Search": "Search",
|
||||
"Search commands": "Search commands",
|
||||
"Search this library": "Search this library",
|
||||
"Second plane": "Second plane",
|
||||
"Section": "Section",
|
||||
"Sections (one sketch each)": "Sections (one sketch each)",
|
||||
"Select Other": "Select Other",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).",
|
||||
"Select bodies (a face picks its body)": "Select bodies (a face picks its body)",
|
||||
"Select bodies or faces instead": "Select bodies or faces instead",
|
||||
"Select edges": "Select edges",
|
||||
"Selection filter": "Selection filter",
|
||||
"Semantic faces share their appearance across instances.": "Semantic faces share their appearance across instances.",
|
||||
"Send your report on GitHub": "Send your report on GitHub",
|
||||
"Shaded": "Shaded",
|
||||
"Shape": "Shape",
|
||||
"Sheet Rules": "Sheet Rules",
|
||||
"Show Agent Cursor": "Show Agent Cursor",
|
||||
@@ -362,6 +448,7 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft closed before these designs were saved. Their last autosave can be opened.",
|
||||
"Specialise": "Specialise",
|
||||
"Speed": "Speed",
|
||||
"Splitting tool": "Splitting tool",
|
||||
"Stand it up": "Stand it up",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Stand it up: a quarter turn about X, as one Move feature you can undo.",
|
||||
"Stand this part up?": "Stand this part up?",
|
||||
@@ -372,16 +459,22 @@
|
||||
"Steps to reproduce (optional)": "Steps to reproduce (optional)",
|
||||
"Stitch the kept pieces": "Stitch the kept pieces",
|
||||
"Stitch to both bodies": "Stitch to both bodies",
|
||||
"Studio": "Studio",
|
||||
"Study": "Study",
|
||||
"Style": "Style",
|
||||
"Summary": "Summary",
|
||||
"Suppress": "Suppress",
|
||||
"Suppressed": "Suppressed",
|
||||
"Surface": "Surface",
|
||||
"Surface, or faces to turn over": "Surface, or faces to turn over",
|
||||
"Swap Ends": "Swap Ends",
|
||||
"TRIM": "TRIM",
|
||||
"Tangent (G1)": "Tangent (G1)",
|
||||
"Tangent Chain": "Tangent Chain",
|
||||
"Tangent chain": "Tangent chain",
|
||||
"Tap drill diameter": "Tap drill diameter",
|
||||
"Target": "Target",
|
||||
"Target Body": "Target Body",
|
||||
"Tear": "Tear",
|
||||
"Technical details": "Technical details",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "The dashed sketch is a saved plane guide. Select a new face to repair the attachment.",
|
||||
@@ -396,24 +489,31 @@
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "This child is shared. Position changes apply to every copy of its parent.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.",
|
||||
"To": "To",
|
||||
"To Offset": "To Offset",
|
||||
"To edges": "To edges",
|
||||
"Tolerance": "Tolerance",
|
||||
"Too close": "Too close",
|
||||
"Tool Bodies": "Tool Bodies",
|
||||
"Tools": "Tools",
|
||||
"Tools, clockwise from the top": "Tools, clockwise from the top",
|
||||
"Trimming tool (plane, face or curves)": "Trimming tool (plane, face or curves)",
|
||||
"Turn off section analysis before exporting the view.": "Turn off section analysis before exporting the view.",
|
||||
"Type": "Type",
|
||||
"UNTRIM": "UNTRIM",
|
||||
"Unit": "Unit",
|
||||
"Unsuppress": "Unsuppress",
|
||||
"Updating the preview": "Updating the preview",
|
||||
"Use active author placement": "Use active author placement",
|
||||
"Use fixed plane": "Use fixed plane",
|
||||
"Use saved fixed plane": "Use saved fixed plane",
|
||||
"Use selected face as fixed plane": "Use selected face as fixed plane",
|
||||
"Use the SolidWorks mouse mode": "Use the SolidWorks mouse mode",
|
||||
"User Parameters": "User Parameters",
|
||||
"VIEW": "VIEW",
|
||||
"VIEWS": "VIEWS",
|
||||
"Value": "Value",
|
||||
"Variable Radius": "Variable Radius",
|
||||
"Velocity": "Velocity",
|
||||
"Vertices": "Vertices",
|
||||
"View navigator": "View navigator",
|
||||
@@ -427,13 +527,44 @@
|
||||
"When": "When",
|
||||
"Width": "Width",
|
||||
"Will fail": "Will fail",
|
||||
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.",
|
||||
"X": "X",
|
||||
"X Angle": "X Angle",
|
||||
"X angle": "X angle",
|
||||
"X distance": "X distance",
|
||||
"Y": "Y",
|
||||
"Y Angle": "Y Angle",
|
||||
"Y angle": "Y angle",
|
||||
"Y distance": "Y distance",
|
||||
"Z": "Z",
|
||||
"Z Angle": "Z Angle",
|
||||
"Z Distance": "Z Distance",
|
||||
"Z angle": "Z angle",
|
||||
"Z distance": "Z distance",
|
||||
"Zebra": "Zebra",
|
||||
"ai.prefs.availability": "Assistant controls",
|
||||
"ai.prefs.background_commands": "Run agent commands in the background",
|
||||
"ai.prefs.follow_camera": "Turn the camera to sketches",
|
||||
"ai.prefs.follow_camera_off": "Automatic camera following is off.",
|
||||
"ai.prefs.follow_camera_on": "The camera turns to face sketches the agent creates.",
|
||||
"ai.prefs.intro": "Agents, permissions and how their work appears.",
|
||||
"ai.prefs.make_default": "Use by default",
|
||||
"ai.prefs.no_agents": "No agents yet. Add an agent or refresh to find installed agents.",
|
||||
"ai.prefs.no_saved_approvals": "No saved approvals.",
|
||||
"ai.prefs.override_all_warning": "This agent may save, overwrite, export or replace the design without asking.",
|
||||
"ai.prefs.permission_all": "Allow everything",
|
||||
"ai.prefs.permission_all_summary": "Available SolidCraft tools run without asking.",
|
||||
"ai.prefs.permission_ask": "Ask before changes",
|
||||
"ai.prefs.permission_ask_summary": "Approve every design change and file action.",
|
||||
"ai.prefs.permission_edits": "Allow model edits",
|
||||
"ai.prefs.permission_edits_summary": "Model edits run; file and destructive actions still ask.",
|
||||
"ai.prefs.permission_scope": "For SolidCraft tools only. The agent’s shell and file edits use its own permissions.",
|
||||
"ai.prefs.pointer_preview_hidden": "Pointer hidden",
|
||||
"ai.prefs.pointer_preview_speed": "Agent pointer · {speed}",
|
||||
"ai.prefs.pointer_speed": "Motion speed",
|
||||
"ai.prefs.privacy_note": "Only your chosen agent or endpoint receives the conversation. Endpoint keys are stored in your system keychain.",
|
||||
"ai.prefs.show_pointer": "Show agent pointer",
|
||||
"ai.prefs.turn_limit_hint": "Maximum design-changing tool calls the Assistant may make for one message.",
|
||||
"angle, height": "angle, height",
|
||||
"appearance.dielectric_ior": "Dielectric IOR",
|
||||
"appearance.metallic": "Metallic",
|
||||
@@ -479,8 +610,10 @@
|
||||
"cmd.component_activate": "Activate Component",
|
||||
"cmd.component_break_link": "Break Link",
|
||||
"cmd.component_capture_position": "Capture Position",
|
||||
"cmd.component_copies": "Find Identical Copies",
|
||||
"cmd.component_create": "New Component",
|
||||
"cmd.component_delete": "Delete Component",
|
||||
"cmd.component_extract": "Extract to File",
|
||||
"cmd.component_from_bodies": "Create Components from Bodies",
|
||||
"cmd.component_insert_design": "Insert Design",
|
||||
"cmd.component_links": "List Links",
|
||||
@@ -493,6 +626,7 @@
|
||||
"cmd.component_properties": "Component Physical Properties",
|
||||
"cmd.component_relink": "Relink Source",
|
||||
"cmd.component_rename": "Rename Component",
|
||||
"cmd.component_replace_with_link": "Replace with Link",
|
||||
"cmd.component_revert_position": "Revert Position",
|
||||
"cmd.component_unlock": "Unlock Contents",
|
||||
"cmd.component_update_links": "Update Links",
|
||||
@@ -610,6 +744,7 @@
|
||||
"cmd.motion_plot": "Plot Motion Study",
|
||||
"cmd.motion_study": "Motion Study",
|
||||
"cmd.motion_trace": "Motion Trace",
|
||||
"cmd.motion_trace_edit": "Edit Motion Trace",
|
||||
"cmd.motion_trace_keep": "Keep Motion Trace",
|
||||
"cmd.occurrence_copy": "Paste (Instance)",
|
||||
"cmd.occurrence_ground": "Ground",
|
||||
@@ -889,19 +1024,57 @@
|
||||
"help.solid.revolve": "Spins a sketch profile around an axis to make a round solid. Use it for shafts, knobs, bowls and any part that is round about a centre line.",
|
||||
"help.solid.shell": "Hollows out a solid, leaving walls of the thickness you set, and can open the faces you pick. Use it for boxes, covers and other thin-walled parts.",
|
||||
"help.third_party": "Third-party licences",
|
||||
"home.back": "Back to {name}",
|
||||
"home.crashes": "Crash reports",
|
||||
"home.design_workspace": "Design",
|
||||
"home.documentation": "Documentation",
|
||||
"home.export": "Export",
|
||||
"home.export_hint": "Choose a format for the current design.",
|
||||
"home.export_json_hint": "Editable design and command history",
|
||||
"home.export_mesh_hint": "Mesh for printing or sharing",
|
||||
"home.export_step_hint": "Precise solid geometry",
|
||||
"home.import": "Import…",
|
||||
"home.import_hint": "STEP, IGES or mesh into a new design",
|
||||
"home.in_this_browser": "in this browser",
|
||||
"home.missing": "missing",
|
||||
"home.month_01": "Jan",
|
||||
"home.month_02": "Feb",
|
||||
"home.month_03": "Mar",
|
||||
"home.month_04": "Apr",
|
||||
"home.month_05": "May",
|
||||
"home.month_06": "Jun",
|
||||
"home.month_07": "Jul",
|
||||
"home.month_08": "Aug",
|
||||
"home.month_09": "Sep",
|
||||
"home.month_10": "Oct",
|
||||
"home.month_11": "Nov",
|
||||
"home.month_12": "Dec",
|
||||
"home.new_inches": "New design, inches",
|
||||
"home.new_inches_hint": "Inches · opens in Design",
|
||||
"home.new_mm": "New design",
|
||||
"home.new_mm_hint": "Millimetres · opens in Design",
|
||||
"home.open": "Open…",
|
||||
"home.opened_days_ago": "Opened {count} days ago",
|
||||
"home.opened_hours_ago": "Opened {count} h ago",
|
||||
"home.opened_just_now": "Opened just now",
|
||||
"home.opened_minutes_ago": "Opened {count} min ago",
|
||||
"home.open_hint": "A .solidcraft design ({shortcut})",
|
||||
"home.open_now": "Open now",
|
||||
"home.opened_days_ago": "{count} days ago",
|
||||
"home.opened_hours_ago": "{count} h ago",
|
||||
"home.opened_just_now": "Just now",
|
||||
"home.opened_minutes_ago": "{count} min ago",
|
||||
"home.opening": "opening…",
|
||||
"home.properties": "Design properties",
|
||||
"home.recent": "Recent",
|
||||
"home.recent_empty": "Designs you open or save appear here.",
|
||||
"home.recovery_count": "Recoverable designs: {count}",
|
||||
"home.recovery_empty": "No recoverable designs.",
|
||||
"home.recovery_hint": "SolidCraft closed before these designs were saved.",
|
||||
"home.report_issue": "Report an Issue",
|
||||
"home.samples": "Samples",
|
||||
"home.settings": "Settings",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Keyboard shortcuts",
|
||||
"home.start_hint": "Create a design, open a recent file, or explore a sample.",
|
||||
"home.whats_new": "What's new",
|
||||
"home.yesterday": "Yesterday",
|
||||
"install.button_tip": "Install SolidCraft as an app: it opens in its own window and works offline",
|
||||
"install.dock_1": "Choose File › Add to Dock.",
|
||||
"install.dock_2": "Click Add.",
|
||||
@@ -959,8 +1132,15 @@
|
||||
"motion.to_end": "To the end",
|
||||
"motion.to_start": "To the start",
|
||||
"motion.track_name": "Track {number}",
|
||||
"nav.alias.help": "Shift+Alt+left drag orbits, Shift+Alt+middle pans, Shift+Alt+right zooms; middle drag also pans and right drag orbits",
|
||||
"nav.blender.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone",
|
||||
"nav.free_orbit.help": "Mouse and 3D mouse rotate through both poles and preserve roll. Shift axis snapping is disabled.",
|
||||
"nav.free_orbit.label": "Free orbit",
|
||||
"nav.fusion.help": "Middle drag pans, Shift+middle orbits, right drag orbits",
|
||||
"nav.inventor.help": "Middle drag pans, Shift+middle orbits, Ctrl+middle zooms, right drag orbits",
|
||||
"nav.openscad.help": "Left drag on empty space orbits, Shift+left drag box-selects, sketch geometry drags edit; right or middle drag pans, Shift+right or Shift+middle drag zooms",
|
||||
"nav.solidworks.help": "Middle drag orbits, Ctrl+middle pans, Shift+middle zooms, right drag orbits",
|
||||
"nav.tinkercad.help": "Right drag orbits, middle or Shift+right drag pans",
|
||||
"nav.touchpad.help": "Hold Shift and move the pointer to pan; hold Alt/Option and move it to orbit. No mouse button is needed. Vertical scroll zooms.",
|
||||
"nav.touchpad.label": "Touchpad",
|
||||
"nav.touchpad.orbit": "Orbit (hold Alt/Option and move; no mouse button)",
|
||||
@@ -983,8 +1163,130 @@
|
||||
"pattern.flip_direction": "Flip direction",
|
||||
"pick a hole to seat it, or OK places it at the origin": "pick a hole to seat it, or OK places it at the origin",
|
||||
"pick one or two vertices, edges or faces": "pick one or two vertices, edges or faces",
|
||||
"prefs.add_item": "Add the item",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option also frees manipulator arrows",
|
||||
"prefs.anti_aliasing_note": "Smooths jagged edges.",
|
||||
"prefs.applies_on_restart": "Applies on restart",
|
||||
"prefs.assistant": "Assistant",
|
||||
"prefs.axes": "Axes",
|
||||
"prefs.axis": "Axis",
|
||||
"prefs.bottom": "Bottom",
|
||||
"prefs.browser": "Browser",
|
||||
"prefs.built_in": "Built in",
|
||||
"prefs.button_left": "Left drag",
|
||||
"prefs.button_middle": "Middle drag",
|
||||
"prefs.button_right": "Right drag",
|
||||
"prefs.choose_font": "Choose font file…",
|
||||
"prefs.click": "Click",
|
||||
"prefs.contact_shading": "Soft contact shading",
|
||||
"prefs.contact_shading_note": "Soft shading where parts come close.",
|
||||
"prefs.context_menu": "Context menu",
|
||||
"prefs.context_menu_ring": "Context menu · hold for the radial menu",
|
||||
"prefs.cpu": "CPU (software)",
|
||||
"prefs.crash_reports": "Open crash reports…",
|
||||
"prefs.crash_reports_note": "Reports are kept on this computer, with the design and commands that reproduce the problem.",
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "Curved surfaces",
|
||||
"prefs.curves": "Curved surfaces",
|
||||
"prefs.dead_zone_note": "The hatched band is the dead zone: motion inside it is ignored.",
|
||||
"prefs.default": "default",
|
||||
"prefs.design": "Design defaults",
|
||||
"prefs.design_note": "What new designs start with. Open designs keep their own.",
|
||||
"prefs.devices": "3D mouse",
|
||||
"prefs.display": "Display",
|
||||
"prefs.edges": "Edges and shading",
|
||||
"prefs.extend": "Extend",
|
||||
"prefs.extra_navigation": "Ctrl+left drag on empty space orbits; hold Space to pan.",
|
||||
"prefs.follows_os": "Follows the OS",
|
||||
"prefs.font": "Font",
|
||||
"prefs.font_fallback": "Glyphs the chosen font lacks fall back to the built-in font, so no label shows an empty box.",
|
||||
"prefs.font_sample": "The quick brown fox · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "The selected font could not be read.",
|
||||
"prefs.general": "General",
|
||||
"prefs.general_note": "Language, saving and updates.",
|
||||
"prefs.gesture_orbit": "Orbit",
|
||||
"prefs.gesture_pan": "Pan",
|
||||
"prefs.gesture_zoom": "Zoom",
|
||||
"prefs.graphics": "Graphics",
|
||||
"prefs.graphics_note": "Trade speed for smoothness. Each choice shows its effect on a sample.",
|
||||
"prefs.graphics_presets_note": "Presets set the choices below together.",
|
||||
"prefs.history": "History",
|
||||
"prefs.hold_stop_snapping": "Hold to stop snapping",
|
||||
"prefs.icons_only": "Icons only",
|
||||
"prefs.imported_file": "Imported file",
|
||||
"prefs.input": "Input",
|
||||
"prefs.interface": "Interface",
|
||||
"prefs.interface_language": "Interface language",
|
||||
"prefs.interface_note": "How SolidCraft looks and where its parts sit.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.labels": "Labels",
|
||||
"prefs.layout": "Layout",
|
||||
"prefs.layout_note": "Compact docks panels to the window edges. Browser translucency is available in Floating. Change the Assistant side on the Assistant page.",
|
||||
"prefs.left": "Left",
|
||||
"prefs.left_click": "Left click",
|
||||
"prefs.live_input": "Live input",
|
||||
"prefs.live_preview": "Live preview",
|
||||
"prefs.minutes": "minutes",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "Mouse and trackpad",
|
||||
"prefs.mouse_heading": "Mouse",
|
||||
"prefs.mouse_mapping": "Mouse mapping",
|
||||
"prefs.navigation": "Navigation",
|
||||
"prefs.navigation_note": "How the mouse, trackpad and 3D mouse move the view.",
|
||||
"prefs.not_on_gpu": "not on this GPU",
|
||||
"prefs.on": "On",
|
||||
"prefs.opaque": "Opaque",
|
||||
"prefs.orbit_snap": "Hold Shift during orbit to snap to an axis.",
|
||||
"prefs.orientation": "Orientation",
|
||||
"prefs.overall_speed": "Overall speed",
|
||||
"prefs.preview_choices": "Preview of your choices",
|
||||
"prefs.quality_default": "default",
|
||||
"prefs.quality_fastest": "fastest",
|
||||
"prefs.quality_slowest": "slowest",
|
||||
"prefs.quality_smoother": "smoother",
|
||||
"prefs.reports_saved": "{n} saved",
|
||||
"prefs.right_click": "Right click",
|
||||
"prefs.saving": "Autosave and recovery",
|
||||
"prefs.scale_note": "Also Ctrl + / Ctrl − in the window. Buttons are 28 px at 100%.",
|
||||
"prefs.scale_text": "Scale and text",
|
||||
"prefs.select": "Select",
|
||||
"prefs.select_one_item": "Select one item",
|
||||
"prefs.selecting": "Selecting",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift adds only; Ctrl / Cmd toggles",
|
||||
"prefs.shift_add_note": "On: Shift adds, Ctrl / Cmd toggles. Off (default): both toggle.",
|
||||
"prefs.shortcuts": "Shortcuts",
|
||||
"prefs.sketch_selection": "Sketch & selection",
|
||||
"prefs.sketch_selection_heading": "Sketch and selection",
|
||||
"prefs.sketch_selection_note": "How sketches snap and how clicks select.",
|
||||
"prefs.sketching": "Sketching",
|
||||
"prefs.snap_drawing_note": "While drawing or dragging a sketch point.",
|
||||
"prefs.source": "Source",
|
||||
"prefs.speed": "Speed",
|
||||
"prefs.stand_up_note": "After a STEP or IGES file opens with Z up, offer a quarter turn about X.",
|
||||
"prefs.status_done": "Done",
|
||||
"prefs.status_error": "Error",
|
||||
"prefs.status_selected": "Selected",
|
||||
"prefs.status_warning": "Warning",
|
||||
"prefs.supersampling_brief": "Draws the view larger and filters it down: very crisp, and heavier.",
|
||||
"prefs.tab_accent_note": "The accent follows the active tab",
|
||||
"prefs.theme_file": "theme file",
|
||||
"prefs.timeline_half_hour": "−30 min",
|
||||
"prefs.timeline_now": "now",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
"prefs.toggle_item": "Add the item, or take it out again",
|
||||
"prefs.toolbar": "Toolbar",
|
||||
"prefs.translucent": "Translucent",
|
||||
"prefs.under_browser": "Under browser",
|
||||
"prefs.under_cursor": "Under cursor",
|
||||
"prefs.units_precision": "Units and precision",
|
||||
"prefs.updates": "Updates",
|
||||
"prefs.view": "View behavior",
|
||||
"prefs.viewport": "Viewport",
|
||||
"prefs.wheel": "Wheel",
|
||||
"prefs.wheel_trackpad": "Wheel and trackpad",
|
||||
"prefs.workspace": "Workspace",
|
||||
"prefs.zoom_toward_cursor": "Zoom, toward the cursor",
|
||||
"properties.title": "Properties: {name}",
|
||||
"recovery.days_ago": "{count} days ago",
|
||||
"recovery.detail": "{file} · {features} features · autosaved {when}",
|
||||
@@ -1055,8 +1357,8 @@
|
||||
"search.suggest": "Did you mean {label}?",
|
||||
"search.try": "Type a command name, a feature in this design, or a parameter. To run a command with values, type its id and a JSON object: {example}",
|
||||
"search.valid": "Valid",
|
||||
"settings.display_quality_note": "Finer levels draw curved faces and edges smoother and take longer to mesh. Only the viewport changes, never the design or exports.",
|
||||
"settings.frame_rate_note": "Shows frames per second in the view's bottom-right corner. While it is on, the view redraws continuously.",
|
||||
"settings.display_quality_note": "Only the display. Designs and exports are not affected.",
|
||||
"settings.frame_rate_note": "Shows frames per second in the view's bottom-right corner.",
|
||||
"settings.measure_axis_distances": "Show X/Y/Z distances",
|
||||
"settings.measure_axis_distances_help": "Show world-axis component dimensions alongside the total distance while Measure is open.",
|
||||
"settings.msaa_applies_on_restart": "Anti-aliasing applies the next time SolidCraft starts",
|
||||
@@ -1065,6 +1367,7 @@
|
||||
"settings.reset_intro": "These settings don't work on this computer and would keep SolidCraft from starting, so they were changed:",
|
||||
"settings.reset_title": "Settings were changed",
|
||||
"settings.supersampling_note": "Draws the view at a higher resolution and filters it down: smoother edges, lines and grid on screens below 4K. It costs GPU time: about 2.25× the pixels at 1.5× and 4× at 2×.",
|
||||
"sheet.no_body": "there is no sheet metal body: start one with Flange > Base, or turn a plate into sheet metal with Convert to Sheet Metal (pick its large flat face)",
|
||||
"shortcut.json_error": "parameters must be JSON: {error}",
|
||||
"shortcut.no_command": "no command `{id}`; the text before the JSON must be a command id",
|
||||
"shortcut.pin": "Pin to the top of the S box",
|
||||
@@ -1101,6 +1404,9 @@
|
||||
"thread.designation": "Designation",
|
||||
"thread.iso_metric": "ISO metric",
|
||||
"thread.select_size": "Select size",
|
||||
"timeline.delete_after_marker_one": "Delete 1 Feature After History Marker",
|
||||
"timeline.delete_after_marker_other": "Delete {count} Features After History Marker",
|
||||
"timeline.delete_many": "Delete {count} Selected Features",
|
||||
"timeline.group": "New Feature Folder",
|
||||
"timeline.move_many": "Move Timeline Items",
|
||||
"timeline.move_to_group": "Move Features to Folder",
|
||||
@@ -1179,6 +1485,7 @@
|
||||
"ui.agent_chat_files_left_out": "At most {max} files go with one message; left out: {files}",
|
||||
"ui.agent_chat_files_loading": "Waiting for the files to be read",
|
||||
"ui.agent_chat_files_max": "At most {max} files on one message",
|
||||
"ui.agent_chat_flatpak_none_found": "No coding agent found. The Flatpak sandbox cannot see agents installed in your home folder and has no network access by default. To allow Claude Code and its ACP adapter installed through nvm or ~/.local/bin, run this command in a terminal, then restart SolidCraft. It grants access to the listed program and sign-in files, plus network access:",
|
||||
"ui.agent_chat_import_failed": "{name}: the import failed: {error}",
|
||||
"ui.agent_chat_intro": "Chat with a coding agent you already use, or a model endpoint you choose. It works on the open design through SolidCraft's tools: its changes show as it makes them and undo as one step per message. What it may do without asking is set in Preferences > AI.",
|
||||
"ui.agent_chat_key_in_env": "key set",
|
||||
@@ -1251,8 +1558,10 @@
|
||||
"ui.ai_add": "Add",
|
||||
"ui.ai_add_agent": "Add agent…",
|
||||
"ui.ai_add_agent_title": "Add agent",
|
||||
"ui.ai_add_anthropic": "Anthropic-compatible API",
|
||||
"ui.ai_add_api": "OpenAI-compatible API",
|
||||
"ui.ai_add_custom": "Custom command",
|
||||
"ui.ai_add_endpoint": "API endpoint",
|
||||
"ui.ai_add_installed": "Installed agent",
|
||||
"ui.ai_add_installed_note": "An entry of your own for an installed agent, for example with other arguments. Installed agents are listed by themselves.",
|
||||
"ui.ai_added": "added",
|
||||
@@ -1281,6 +1590,7 @@
|
||||
"ui.ai_key_needs_https": "A key is only sent over HTTPS, or to this computer.",
|
||||
"ui.ai_key_note": "Stored in the system keychain, never in SolidCraft's settings. Leave it empty for a local server.",
|
||||
"ui.ai_keychain_error": "The system keychain refused: {error}",
|
||||
"ui.ai_kind": "Kind",
|
||||
"ui.ai_mode_all": "Allow everything",
|
||||
"ui.ai_mode_all_note": "Nothing asks first. Crash reports stay off limits and the folder limits still apply.",
|
||||
"ui.ai_mode_all_warning": "The agent can save, overwrite, export and replace your design without asking. Use this only with an agent you trust, on designs you have saved.",
|
||||
@@ -1302,6 +1612,7 @@
|
||||
"ui.ai_panel": "Panel",
|
||||
"ui.ai_permissions": "Permissions",
|
||||
"ui.ai_preset": "Preset",
|
||||
"ui.ai_protocol": "Protocol",
|
||||
"ui.ai_remove": "Remove",
|
||||
"ui.ai_request_limit": "stopped after 24 requests for one message; send another message to go on",
|
||||
"ui.ai_reset": "Reset",
|
||||
@@ -1312,6 +1623,8 @@
|
||||
"ui.ai_test": "Test",
|
||||
"ui.ai_test_acp_ok": "Answers: {agent}",
|
||||
"ui.ai_test_acp_tip": "Starts the agent briefly and asks it for a session; ends it again",
|
||||
"ui.ai_test_answered": "Reached: {model} answered",
|
||||
"ui.ai_test_anthropic_tip": "Sends a short message to the endpoint",
|
||||
"ui.ai_test_api_tip": "Asks the endpoint for its models",
|
||||
"ui.ai_test_no_model": "Reached: {count} models, but not {model}",
|
||||
"ui.ai_test_ok": "Reached: {count} models",
|
||||
@@ -1469,6 +1782,9 @@
|
||||
"ui.export_keymap_tip": "Keys, S box favourites and the toolbar, as a file",
|
||||
"ui.extend": "Extend",
|
||||
"ui.extent_type": "Extent Type",
|
||||
"ui.extract_component": "Extract to File…",
|
||||
"ui.extract_design_changed": "The design changed while choosing a file. Start Extract to File again.",
|
||||
"ui.extract_done": "The component is now its own design, linked here.",
|
||||
"ui.extrude": "Extrude",
|
||||
"ui.factor": "Factor",
|
||||
"ui.feature_history": "Feature history",
|
||||
@@ -1500,6 +1816,7 @@
|
||||
"ui.grid_follows_view_hint": "Side views show the grid on the upright plane facing them instead of the ground",
|
||||
"ui.ground_shadow": "Ground shadow",
|
||||
"ui.groupSelected": "Group Selected",
|
||||
"ui.hatch": "Hatch",
|
||||
"ui.heading_add_ins": "ADD-INS",
|
||||
"ui.heading_assemble": "ASSEMBLE",
|
||||
"ui.heading_box": "BOX",
|
||||
@@ -1607,6 +1924,10 @@
|
||||
"ui.length_decimals": "Length decimals",
|
||||
"ui.light": "Light",
|
||||
"ui.line": "Line",
|
||||
"ui.link_copies": "Link Identical Copies",
|
||||
"ui.link_copies_keep": "Keep",
|
||||
"ui.link_copies_link": "Link",
|
||||
"ui.link_copies_text": "These components build the same bodies as {name}. Make them placements of its link?",
|
||||
"ui.lock": "Lock",
|
||||
"ui.loft": "Loft",
|
||||
"ui.look_at": "Look At",
|
||||
@@ -1730,6 +2051,7 @@
|
||||
"ui.render": "Render",
|
||||
"ui.renderer": "Renderer",
|
||||
"ui.repeat": "Repeat",
|
||||
"ui.replace_with_link_done": "The copies are now placements of the linked design.",
|
||||
"ui.report_a_bug": "Report a bug",
|
||||
"ui.reset": "Reset",
|
||||
"ui.reset_all": "Reset All",
|
||||
|
||||
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Licencia MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licencias de terceros",
|
||||
"home.back": "Volver a {name}",
|
||||
"home.crashes": "Informes de fallos",
|
||||
"home.design_workspace": "Diseño",
|
||||
"home.documentation": "Documentación",
|
||||
"home.export": "Exportar",
|
||||
"home.export_hint": "Elige un formato para el diseño actual.",
|
||||
"home.export_json_hint": "Diseño editable e historial de comandos",
|
||||
"home.export_mesh_hint": "Malla para imprimir o compartir",
|
||||
"home.export_step_hint": "Geometría sólida precisa",
|
||||
"home.import": "Importar…",
|
||||
"home.import_hint": "STEP, IGES o malla en un nuevo diseño",
|
||||
"home.in_this_browser": "en este navegador",
|
||||
"home.missing": "no disponible",
|
||||
"home.month_01": "ene",
|
||||
"home.month_02": "feb",
|
||||
"home.month_03": "mar",
|
||||
"home.month_04": "abr",
|
||||
"home.month_05": "may",
|
||||
"home.month_06": "jun",
|
||||
"home.month_07": "jul",
|
||||
"home.month_08": "ago",
|
||||
"home.month_09": "sept",
|
||||
"home.month_10": "oct",
|
||||
"home.month_11": "nov",
|
||||
"home.month_12": "dic",
|
||||
"home.new_inches": "Nuevo diseño en pulgadas",
|
||||
"home.new_inches_hint": "Pulgadas · se abre en Diseño",
|
||||
"home.new_mm": "Nuevo diseño",
|
||||
"home.new_mm_hint": "Milímetros · se abre en Diseño",
|
||||
"home.open": "Abrir…",
|
||||
"home.opened_days_ago": "Abierto hace {count} días",
|
||||
"home.opened_hours_ago": "Abierto hace {count} h",
|
||||
"home.opened_just_now": "Acaba de abrirse",
|
||||
"home.opened_minutes_ago": "Abierto hace {count} min",
|
||||
"home.open_hint": "Un diseño .solidcraft ({shortcut})",
|
||||
"home.open_now": "Abrir ahora",
|
||||
"home.opened_days_ago": "Hace {count} días",
|
||||
"home.opened_hours_ago": "Hace {count} h",
|
||||
"home.opened_just_now": "Ahora mismo",
|
||||
"home.opened_minutes_ago": "Hace {count} min",
|
||||
"home.opening": "abriendo…",
|
||||
"home.properties": "Propiedades del diseño",
|
||||
"home.recent": "Recientes",
|
||||
"home.recent_empty": "Los diseños que abras o guardes aparecerán aquí.",
|
||||
"home.recovery_count": "Diseños recuperables: {count}",
|
||||
"home.recovery_empty": "No hay diseños recuperables.",
|
||||
"home.recovery_hint": "SolidCraft se cerró antes de guardar estos diseños.",
|
||||
"home.report_issue": "Notificar un problema",
|
||||
"home.samples": "Ejemplos",
|
||||
"home.settings": "Configuración",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Atajos de teclado",
|
||||
"home.start_hint": "Crea un diseño, abre un archivo reciente o explora un ejemplo.",
|
||||
"home.whats_new": "Novedades",
|
||||
"home.yesterday": "Ayer",
|
||||
"install.button_tip": "Instalar SolidCraft como aplicación: se abre en una ventana propia y funciona sin conexión",
|
||||
"install.dock_1": "Elige Archivo › Añadir al Dock.",
|
||||
"install.dock_2": "Haz clic en Añadir.",
|
||||
@@ -1022,7 +1060,7 @@
|
||||
"search.try": "Escribe el nombre de un comando, una operación de este diseño o un parámetro. Para ejecutar un comando con valores, escribe su id y un objeto JSON: {example}",
|
||||
"search.valid": "Válido",
|
||||
"settings.display_quality_note": "Los niveles más finos muestran las caras y aristas curvas con mayor suavidad, pero tardan más en generar la malla. Solo cambia el visor, nunca el diseño ni las exportaciones.",
|
||||
"settings.frame_rate_note": "Muestra los fotogramas por segundo en la esquina inferior derecha de la vista. Mientras esté activado, la vista se redibuja continuamente.",
|
||||
"settings.frame_rate_note": "Muestra los fotogramas por segundo en la esquina inferior derecha de la vista.",
|
||||
"settings.measure_axis_distances": "Mostrar distancias X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Mostrar las cotas de cada eje global junto a la distancia total mientras está abierta la herramienta Medir.",
|
||||
"settings.msaa_applies_on_restart": "El suavizado de bordes se aplicará la próxima vez que se inicie SolidCraft",
|
||||
@@ -1145,6 +1183,7 @@
|
||||
"ui.agent_chat_files_left_out": "Se pueden enviar como máximo {max} archivos con un mensaje; omitidos: {files}",
|
||||
"ui.agent_chat_files_loading": "Esperando a que se lean los archivos",
|
||||
"ui.agent_chat_files_max": "Como máximo {max} archivos por mensaje",
|
||||
"ui.agent_chat_flatpak_none_found": "No se ha encontrado ningún agente de programación. De forma predeterminada, el entorno aislado de Flatpak no ve los agentes instalados en tu carpeta personal y no tiene acceso a la red. Para permitir Claude Code y su adaptador ACP instalados con nvm o en ~/.local/bin, ejecuta este comando en un terminal y reinicia SolidCraft. Concede acceso a los programas y archivos de inicio de sesión indicados, además de acceso a la red:",
|
||||
"ui.agent_chat_import_failed": "{name}: la importación falló: {error}",
|
||||
"ui.agent_chat_intro": "Habla con un agente de programación que ya utilices o con un servicio de modelos que elijas. Trabaja sobre el diseño abierto mediante las herramientas de SolidCraft: sus cambios se muestran al realizarlos y pueden deshacerse de una vez por mensaje. Lo que puede hacer sin preguntar se configura en Preferencias > IA.",
|
||||
"ui.agent_chat_key_in_env": "clave configurada",
|
||||
@@ -1464,6 +1503,7 @@
|
||||
"ui.grid_follows_view_hint": "Las vistas laterales muestran la cuadrícula en el plano vertical que tienen enfrente, en lugar del suelo",
|
||||
"ui.ground_shadow": "Sombra en el suelo",
|
||||
"ui.groupSelected": "Agrupar selección",
|
||||
"ui.hatch": "Rayado",
|
||||
"ui.heading_add_ins": "COMPLEMENTOS",
|
||||
"ui.heading_assemble": "ENSAMBLAR",
|
||||
"ui.heading_box": "CAJA",
|
||||
|
||||
@@ -863,19 +863,57 @@
|
||||
"help.solid.revolve": "Fait tourner le profil d'une esquisse autour d'un axe pour obtenir un solide de révolution. À utiliser pour les arbres, boutons, bols et toute pièce ronde autour d'une ligne centrale.",
|
||||
"help.solid.shell": "Évide un solide en laissant des parois de l'épaisseur indiquée, et peut ouvrir les faces choisies. À utiliser pour les boîtes, couvercles et autres pièces à parois minces.",
|
||||
"help.third_party": "Licences tierces",
|
||||
"home.back": "Revenir à {name}",
|
||||
"home.crashes": "Rapports de plantage",
|
||||
"home.design_workspace": "Conception",
|
||||
"home.documentation": "Documentation",
|
||||
"home.export": "Exporter",
|
||||
"home.export_hint": "Choisissez un format pour la conception actuelle.",
|
||||
"home.export_json_hint": "Conception modifiable et historique des commandes",
|
||||
"home.export_mesh_hint": "Maillage pour l’impression ou le partage",
|
||||
"home.export_step_hint": "Géométrie solide précise",
|
||||
"home.import": "Importer…",
|
||||
"home.import_hint": "STEP, IGES ou maillage dans une nouvelle conception",
|
||||
"home.in_this_browser": "dans ce navigateur",
|
||||
"home.missing": "introuvable",
|
||||
"home.month_01": "janv.",
|
||||
"home.month_02": "févr.",
|
||||
"home.month_03": "mars",
|
||||
"home.month_04": "avr.",
|
||||
"home.month_05": "mai",
|
||||
"home.month_06": "juin",
|
||||
"home.month_07": "juil.",
|
||||
"home.month_08": "août",
|
||||
"home.month_09": "sept.",
|
||||
"home.month_10": "oct.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "déc.",
|
||||
"home.new_inches": "Nouvelle conception en pouces",
|
||||
"home.new_inches_hint": "Pouces · ouvre l’espace Conception",
|
||||
"home.new_mm": "Nouvelle conception",
|
||||
"home.new_mm_hint": "Millimètres · ouvre l’espace Conception",
|
||||
"home.open": "Ouvrir…",
|
||||
"home.opened_days_ago": "Ouvert il y a {count} j",
|
||||
"home.opened_hours_ago": "Ouvert il y a {count} h",
|
||||
"home.opened_just_now": "Ouvert à l'instant",
|
||||
"home.opened_minutes_ago": "Ouvert il y a {count} min",
|
||||
"home.open_hint": "Une conception .solidcraft ({shortcut})",
|
||||
"home.open_now": "Ouvrir maintenant",
|
||||
"home.opened_days_ago": "Il y a {count} j",
|
||||
"home.opened_hours_ago": "Il y a {count} h",
|
||||
"home.opened_just_now": "À l’instant",
|
||||
"home.opened_minutes_ago": "Il y a {count} min",
|
||||
"home.opening": "ouverture…",
|
||||
"home.properties": "Propriétés de la conception",
|
||||
"home.recent": "Récents",
|
||||
"home.recent_empty": "Les conceptions que vous ouvrez ou enregistrez apparaissent ici.",
|
||||
"home.recovery_count": "Conceptions récupérables : {count}",
|
||||
"home.recovery_empty": "Aucune conception récupérable.",
|
||||
"home.recovery_hint": "SolidCraft s’est fermé avant l’enregistrement de ces conceptions.",
|
||||
"home.report_issue": "Signaler un problème",
|
||||
"home.samples": "Exemples",
|
||||
"home.settings": "Paramètres",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Raccourcis clavier",
|
||||
"home.start_hint": "Créez une conception, ouvrez un fichier récent ou explorez un exemple.",
|
||||
"home.whats_new": "Nouveautés",
|
||||
"home.yesterday": "Hier",
|
||||
"install.button_tip": "Installer SolidCraft comme une app : il s'ouvre dans sa propre fenêtre et fonctionne hors ligne",
|
||||
"install.dock_1": "Choisissez Fichier › Ajouter au Dock.",
|
||||
"install.dock_2": "Cliquez sur Ajouter.",
|
||||
@@ -1006,13 +1044,13 @@
|
||||
"sample.bracket.name": "Équerre en tôle",
|
||||
"sample.enclosure.about": "Boîte en coque avec lèvre, bossages de vis, appui et clip",
|
||||
"sample.enclosure.name": "Boîtier plastique",
|
||||
"sample.flange.about": "Moyeu et bride avec un réseau circulaire piloté par un paramètre",
|
||||
"sample.flange.about": "Moyeu et bride avec une répétition circulaire pilotée par un paramètre",
|
||||
"sample.flange.name": "Bride boulonnée",
|
||||
"sample.hinge.about": "Deux composants et une liaison pivot, entraînée à 60°",
|
||||
"sample.hinge.name": "Assemblage de charnière",
|
||||
"sample.plate.about": "Plaque paramétrique avec congés, perçages et un bossage alésé",
|
||||
"sample.plate.name": "Plaque d'exemple",
|
||||
"sample.vents.about": "Rainures en réseau rectangulaire : lignes, colonnes et pas sont des paramètres",
|
||||
"sample.vents.about": "Rainures en répétition rectangulaire : lignes, colonnes et pas sont des paramètres",
|
||||
"sample.vents.name": "Plaque d'aération",
|
||||
"search.command_keys": "Entrée exécuter Maj+Entrée exécuter et garder ouvert Échap fermer",
|
||||
"search.commands": "Commandes",
|
||||
@@ -1028,7 +1066,7 @@
|
||||
"search.try": "Saisissez un nom de commande, une entité de cette conception ou un paramètre. Pour exécuter une commande avec des valeurs, saisissez son id et un objet JSON : {example}",
|
||||
"search.valid": "Valide",
|
||||
"settings.display_quality_note": "Les niveaux plus fins affichent plus lisses les faces et arêtes courbes, mais leur maillage prend plus de temps. Seule la vue change, jamais la conception ni les exports.",
|
||||
"settings.frame_rate_note": "Affiche les images par seconde dans le coin inférieur droit de la vue. Tant qu'elle est activée, la vue est redessinée en continu.",
|
||||
"settings.frame_rate_note": "Affiche les images par seconde dans le coin inférieur droit de la vue.",
|
||||
"settings.measure_axis_distances": "Afficher les cotes X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Afficher les cotes suivant les axes du repère global en plus de la distance totale pendant la mesure.",
|
||||
"settings.msaa_applies_on_restart": "L'anticrénelage s'appliquera au prochain démarrage de SolidCraft",
|
||||
@@ -1151,6 +1189,7 @@
|
||||
"ui.agent_chat_files_left_out": "Un message ne peut contenir que {max} fichiers au plus ; non joints : {files}",
|
||||
"ui.agent_chat_files_loading": "Lecture des fichiers en cours",
|
||||
"ui.agent_chat_files_max": "{max} fichiers au plus par message",
|
||||
"ui.agent_chat_flatpak_none_found": "Aucun agent de programmation trouvé. Par défaut, le bac à sable Flatpak ne voit pas les agents installés dans votre dossier personnel et n’a pas accès au réseau. Pour autoriser Claude Code et son adaptateur ACP installés avec nvm ou dans ~/.local/bin, exécuter cette commande dans un terminal, puis redémarrer SolidCraft. Elle donne accès aux programmes et fichiers de connexion indiqués, ainsi qu’au réseau :",
|
||||
"ui.agent_chat_import_failed": "{name} : l'importation a échoué : {error}",
|
||||
"ui.agent_chat_intro": "Discuter avec un agent de programmation que vous utilisez déjà, ou avec un point d'accès de modèle de votre choix. Il travaille sur la conception ouverte avec les outils de SolidCraft : ses modifications apparaissent au fur et à mesure et s'annulent en une seule étape par message. Ce qu'il peut faire sans demander se règle dans Préférences › IA.",
|
||||
"ui.agent_chat_key_in_env": "clé définie",
|
||||
@@ -1335,7 +1374,7 @@
|
||||
"ui.chamfer_type": "Type de chanfrein",
|
||||
"ui.change_workspace": "Changer d'espace de travail",
|
||||
"ui.circle": "Cercle",
|
||||
"ui.circular_pattern": "Réseau circulaire",
|
||||
"ui.circular_pattern": "Répétition circulaire",
|
||||
"ui.clear": "Effacer",
|
||||
"ui.clear_recent": "Effacer les récents",
|
||||
"ui.clear_the_selection": "Effacer la sélection",
|
||||
@@ -1470,11 +1509,12 @@
|
||||
"ui.grid_follows_view_hint": "Dans les vues de côté, la grille s'affiche sur le plan vertical qui leur fait face plutôt que sur le sol",
|
||||
"ui.ground_shadow": "Ombre au sol",
|
||||
"ui.groupSelected": "Grouper la sélection",
|
||||
"ui.hatch": "Hachures",
|
||||
"ui.heading_add_ins": "COMPLÉMENTS",
|
||||
"ui.heading_assemble": "ASSEMBLER",
|
||||
"ui.heading_box": "BOÎTE",
|
||||
"ui.heading_chamfer": "CHANFREIN",
|
||||
"ui.heading_circular_pattern": "RÉSEAU CIRCULAIRE",
|
||||
"ui.heading_circular_pattern": "RÉPÉTITION CIRCULAIRE",
|
||||
"ui.heading_coil": "HÉLICE",
|
||||
"ui.heading_combine": "COMBINER",
|
||||
"ui.heading_configure": "CONFIGURER",
|
||||
@@ -1507,14 +1547,14 @@
|
||||
"ui.heading_offset_plane": "PLAN DÉCALÉ",
|
||||
"ui.heading_ok": "OK",
|
||||
"ui.heading_parameters": "PARAMÈTRES",
|
||||
"ui.heading_pattern": "RÉSEAU",
|
||||
"ui.heading_pattern_on_path": "RÉSEAU SUR TRAJECTOIRE",
|
||||
"ui.heading_pattern": "RÉPÉTITION",
|
||||
"ui.heading_pattern_on_path": "RÉPÉTITION SUR TRAJECTOIRE",
|
||||
"ui.heading_pipe": "TUYAU",
|
||||
"ui.heading_plane_at_angle": "PLAN INCLINÉ",
|
||||
"ui.heading_plastic": "PLASTIQUE",
|
||||
"ui.heading_position": "POSITION",
|
||||
"ui.heading_prepare": "PRÉPARER",
|
||||
"ui.heading_rectangular_pattern": "RÉSEAU RECTANGULAIRE",
|
||||
"ui.heading_rectangular_pattern": "RÉPÉTITION RECTANGULAIRE",
|
||||
"ui.heading_revolve": "RÉVOLUTION",
|
||||
"ui.heading_right": "DROITE",
|
||||
"ui.heading_save_mesh": "ENREGISTRER COMME MAILLAGE",
|
||||
@@ -1644,7 +1684,7 @@
|
||||
"ui.part_size_shared": "Reconstruit la pièce en remplaçant les entités ajoutées. Toutes les occurrences de ce composant sont mises à jour.",
|
||||
"ui.paste": "Coller",
|
||||
"ui.path": "Trajectoire",
|
||||
"ui.pattern_on_path": "Réseau sur trajectoire",
|
||||
"ui.pattern_on_path": "Répétition sur trajectoire",
|
||||
"ui.perpendicular": "Perpendicularité",
|
||||
"ui.perspective_view": "Vue en perspective",
|
||||
"ui.pinToShortcuts": "Épingler aux raccourcis",
|
||||
@@ -1681,7 +1721,7 @@
|
||||
"ui.rail_fit": "Ajustement au rail",
|
||||
"ui.reachable": "Accessible",
|
||||
"ui.rectangle": "Rectangle",
|
||||
"ui.rectangular_pattern": "Réseau rectangulaire",
|
||||
"ui.rectangular_pattern": "Répétition rectangulaire",
|
||||
"ui.redo": "Rétablir",
|
||||
"ui.relink_design_changed": "L’assemblage a changé pendant le choix d’une source. Relancez Relier à nouveau la source.",
|
||||
"ui.relink_done": "Source reliée à nouveau pour toutes les instances.",
|
||||
|
||||
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Licenza MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenzas de terceiros",
|
||||
"home.back": "Volver a {name}",
|
||||
"home.crashes": "Informes de fallos",
|
||||
"home.design_workspace": "Deseño",
|
||||
"home.documentation": "Documentación",
|
||||
"home.export": "Exportar",
|
||||
"home.export_hint": "Escolle un formato para o deseño actual.",
|
||||
"home.export_json_hint": "Deseño editable e historial de comandos",
|
||||
"home.export_mesh_hint": "Malla para imprimir ou compartir",
|
||||
"home.export_step_hint": "Xeometría sólida precisa",
|
||||
"home.import": "Importar…",
|
||||
"home.import_hint": "STEP, IGES ou malla nun novo deseño",
|
||||
"home.in_this_browser": "neste navegador",
|
||||
"home.missing": "non atopado",
|
||||
"home.month_01": "xan.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mar.",
|
||||
"home.month_04": "abr.",
|
||||
"home.month_05": "maio",
|
||||
"home.month_06": "xuño",
|
||||
"home.month_07": "xul.",
|
||||
"home.month_08": "ago.",
|
||||
"home.month_09": "set.",
|
||||
"home.month_10": "out.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Novo deseño en polgadas",
|
||||
"home.new_inches_hint": "Polgadas · ábrese en Deseño",
|
||||
"home.new_mm": "Novo deseño",
|
||||
"home.new_mm_hint": "Milímetros · ábrese en Deseño",
|
||||
"home.open": "Abrir…",
|
||||
"home.opened_days_ago": "Aberto hai {count} días",
|
||||
"home.opened_hours_ago": "Aberto hai {count} h",
|
||||
"home.opened_just_now": "Aberto agora mesmo",
|
||||
"home.opened_minutes_ago": "Aberto hai {count} min",
|
||||
"home.open_hint": "Un deseño .solidcraft ({shortcut})",
|
||||
"home.open_now": "Abrir agora",
|
||||
"home.opened_days_ago": "Hai {count} días",
|
||||
"home.opened_hours_ago": "Hai {count} h",
|
||||
"home.opened_just_now": "Agora mesmo",
|
||||
"home.opened_minutes_ago": "Hai {count} min",
|
||||
"home.opening": "abrindo…",
|
||||
"home.properties": "Propiedades do deseño",
|
||||
"home.recent": "Recentes",
|
||||
"home.recent_empty": "Os deseños que abras ou gardes aparecerán aquí.",
|
||||
"home.recovery_count": "Deseños recuperables: {count}",
|
||||
"home.recovery_empty": "Non hai deseños recuperables.",
|
||||
"home.recovery_hint": "SolidCraft pechouse antes de gardar estes deseños.",
|
||||
"home.report_issue": "Informar dunha incidencia",
|
||||
"home.samples": "Exemplos",
|
||||
"home.settings": "Configuración",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Atallos de teclado",
|
||||
"home.start_hint": "Crea un deseño, abre un ficheiro recente ou explora un exemplo.",
|
||||
"home.whats_new": "Novidades",
|
||||
"home.yesterday": "Onte",
|
||||
"install.button_tip": "Instala SolidCraft como aplicación: ábrese na súa propia xanela e funciona sen conexión",
|
||||
"install.dock_1": "Escolle Ficheiro › Engadir ao Dock.",
|
||||
"install.dock_2": "Fai clic en Engadir.",
|
||||
@@ -1022,7 +1060,7 @@
|
||||
"search.try": "Escribe o nome dunha orde, unha operación deste deseño ou un parámetro. Para executar unha orde con valores, escribe o seu id e un obxecto JSON: {example}",
|
||||
"search.valid": "Válido",
|
||||
"settings.display_quality_note": "Os niveis máis finos debuxan as caras e arestas curvas máis suaves e tardan máis en xerar a malla. Só cambia a vista, nunca o deseño nin as exportacións.",
|
||||
"settings.frame_rate_note": "Mostra as imaxes por segundo na esquina inferior dereita da vista. Mentres está activado, a vista redebúxase continuamente.",
|
||||
"settings.frame_rate_note": "Mostra as imaxes por segundo na esquina inferior dereita da vista.",
|
||||
"settings.measure_axis_distances": "Mostrar as distancias en X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Mostra as compoñentes da distancia segundo os eixes do mundo xunto á distancia total mentres Medir está aberto.",
|
||||
"settings.msaa_applies_on_restart": "O antialiasing aplicarase a próxima vez que se inicie SolidCraft",
|
||||
@@ -1145,6 +1183,7 @@
|
||||
"ui.agent_chat_files_left_out": "Cunha mensaxe van como máximo {max} ficheiros; deixados fóra: {files}",
|
||||
"ui.agent_chat_files_loading": "Agardando a que se lean os ficheiros",
|
||||
"ui.agent_chat_files_max": "Como máximo {max} ficheiros por mensaxe",
|
||||
"ui.agent_chat_flatpak_none_found": "Non se atopou ningún axente de programación. De forma predeterminada, o illamento de Flatpak non ve os axentes instalados no teu cartafol persoal e non ten acceso á rede. Para permitir Claude Code e o seu adaptador ACP instalados con nvm ou en ~/.local/bin, executa esta orde nun terminal e despois reinicia SolidCraft. Concede acceso aos programas e ficheiros de inicio de sesión indicados, ademais de acceso á rede:",
|
||||
"ui.agent_chat_import_failed": "{name}: fallou a importación: {error}",
|
||||
"ui.agent_chat_intro": "Conversa cun axente de programación que xa uses, ou cun punto de acceso de modelo que escollas. Traballa no deseño aberto a través das ferramentas de SolidCraft: os seus cambios vense mentres os fai e desfanse nun paso por mensaxe. O que pode facer sen preguntar defínese en Preferencias > IA.",
|
||||
"ui.agent_chat_key_in_env": "clave configurada",
|
||||
@@ -1464,6 +1503,7 @@
|
||||
"ui.grid_follows_view_hint": "As vistas laterais mostran a grella no plano vertical que ten diante en vez do chan",
|
||||
"ui.ground_shadow": "Sombra no chan",
|
||||
"ui.groupSelected": "Agrupar a selección",
|
||||
"ui.hatch": "Raiado",
|
||||
"ui.heading_add_ins": "COMPLEMENTOS",
|
||||
"ui.heading_assemble": "ENSAMBLAR",
|
||||
"ui.heading_box": "CAIXA",
|
||||
|
||||
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT licenc",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Harmadik felek licencei",
|
||||
"home.back": "Vissza ide: {name}",
|
||||
"home.crashes": "Összeomlási jelentések",
|
||||
"home.design_workspace": "Tervezés",
|
||||
"home.documentation": "Dokumentáció",
|
||||
"home.export": "Exportálás",
|
||||
"home.export_hint": "Válasszon formátumot az aktuális tervhez.",
|
||||
"home.export_json_hint": "Szerkeszthető terv és parancselőzmények",
|
||||
"home.export_mesh_hint": "Háló nyomtatáshoz vagy megosztáshoz",
|
||||
"home.export_step_hint": "Pontos testgeometria",
|
||||
"home.import": "Importálás…",
|
||||
"home.import_hint": "STEP, IGES vagy háló importálása új tervbe",
|
||||
"home.in_this_browser": "ebben a böngészőben",
|
||||
"home.missing": "hiányzik",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "febr.",
|
||||
"home.month_03": "márc.",
|
||||
"home.month_04": "ápr.",
|
||||
"home.month_05": "máj.",
|
||||
"home.month_06": "jún.",
|
||||
"home.month_07": "júl.",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "szept.",
|
||||
"home.month_10": "okt.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Új terv hüvelykben",
|
||||
"home.new_inches_hint": "Hüvelyk · a Tervezés munkaterületen nyílik meg",
|
||||
"home.new_mm": "Új terv",
|
||||
"home.new_mm_hint": "Milliméter · a Tervezés munkaterületen nyílik meg",
|
||||
"home.open": "Megnyitás…",
|
||||
"home.opened_days_ago": "Megnyitva {count} napja",
|
||||
"home.opened_hours_ago": "Megnyitva {count} órája",
|
||||
"home.opened_just_now": "Épp most megnyitva",
|
||||
"home.opened_minutes_ago": "Megnyitva {count} perce",
|
||||
"home.open_hint": "Egy .solidcraft terv ({shortcut})",
|
||||
"home.open_now": "Jelenleg megnyitva",
|
||||
"home.opened_days_ago": "{count} napja",
|
||||
"home.opened_hours_ago": "{count} órája",
|
||||
"home.opened_just_now": "Épp most",
|
||||
"home.opened_minutes_ago": "{count} perce",
|
||||
"home.opening": "megnyitás…",
|
||||
"home.properties": "Terv tulajdonságai",
|
||||
"home.recent": "Legutóbbi",
|
||||
"home.recent_empty": "A megnyitott vagy mentett tervek itt jelennek meg.",
|
||||
"home.recovery_count": "Helyreállítható tervek: {count}",
|
||||
"home.recovery_empty": "Nincs helyreállítható terv.",
|
||||
"home.recovery_hint": "A SolidCraft a tervek mentése előtt bezárult.",
|
||||
"home.report_issue": "Hiba jelentése",
|
||||
"home.samples": "Minták",
|
||||
"home.settings": "Beállítások",
|
||||
"home.short_date": "{month} {day}.",
|
||||
"home.shortcuts": "Billentyűparancsok",
|
||||
"home.start_hint": "Hozzon létre egy tervet, nyisson meg egy legutóbbi fájlt, vagy fedezzen fel egy mintát.",
|
||||
"home.whats_new": "Újdonságok",
|
||||
"home.yesterday": "Tegnap",
|
||||
"install.button_tip": "A SolidCraft telepítése alkalmazásként: saját ablakban nyílik meg, és offline is működik",
|
||||
"install.dock_1": "Válassza a Fájl › Hozzáadás a Dockhoz lehetőséget.",
|
||||
"install.dock_2": "Kattintson a Hozzáadás gombra.",
|
||||
@@ -1021,7 +1059,7 @@
|
||||
"search.try": "Írja be egy parancs nevét, a terv egy elemét vagy egy paramétert. Parancs értékekkel való futtatásához írja be az azonosítóját és egy JSON-objektumot: {example}",
|
||||
"search.valid": "Érvényes",
|
||||
"settings.display_quality_note": "A finomabb szintek simábban rajzolják az ívelt lapokat és éleket, de tovább tart a hálózás. Csak a nézetablak változik, a terv és az exportok soha.",
|
||||
"settings.frame_rate_note": "Megjeleníti a másodpercenkénti képkockaszámot a nézet jobb alsó sarkában. Bekapcsolt állapotban a nézet folyamatosan újrarajzolódik.",
|
||||
"settings.frame_rate_note": "Megjeleníti a másodpercenkénti képkockaszámot a nézet jobb alsó sarkában.",
|
||||
"settings.measure_axis_distances": "X/Y/Z távolságok megjelenítése",
|
||||
"settings.measure_axis_distances_help": "A világtengely-összetevők méreteit is megjeleníti a teljes távolság mellett, amíg a Mérés nyitva van.",
|
||||
"settings.msaa_applies_on_restart": "Az élsimítás a SolidCraft következő indításakor lép érvénybe",
|
||||
@@ -1144,6 +1182,7 @@
|
||||
"ui.agent_chat_files_left_out": "Egy üzenethez legfeljebb {max} fájl tartozhat; kihagyva: {files}",
|
||||
"ui.agent_chat_files_loading": "Várakozás a fájlok beolvasására",
|
||||
"ui.agent_chat_files_max": "Egy üzenetben legfeljebb {max} fájl lehet",
|
||||
"ui.agent_chat_flatpak_none_found": "Nem található kódolóügynök. A Flatpak homokozó alapértelmezés szerint nem látja a saját mappájába telepített ügynököket, és nincs hálózati hozzáférése. Az nvm-mel vagy a ~/.local/bin mappába telepített Claude Code és ACP-adaptere engedélyezéséhez futtassa ezt a parancsot egy terminálban, majd indítsa újra a SolidCraftot. Hozzáférést ad a felsorolt programokhoz és bejelentkezési fájlokhoz, valamint a hálózathoz:",
|
||||
"ui.agent_chat_import_failed": "{name}: az importálás sikertelen: {error}",
|
||||
"ui.agent_chat_intro": "Csevegjen egy már használt kódolóügynökkel vagy egy választott modellvégponttal. A megnyitott terven a SolidCraft eszközein keresztül dolgozik: a módosításai láthatók közben, és üzenetenként egy lépésben vonhatók vissza. Hogy mit tehet kérdés nélkül, azt a Beállítások > AI menüpontban adhatja meg.",
|
||||
"ui.agent_chat_key_in_env": "kulcs beállítva",
|
||||
@@ -1463,6 +1502,7 @@
|
||||
"ui.grid_follows_view_hint": "Az oldalnézetek a talaj helyett a feléjük néző függőleges síkon mutatják a rácsot",
|
||||
"ui.ground_shadow": "Talajárnyék",
|
||||
"ui.groupSelected": "Kijelöltek csoportosítása",
|
||||
"ui.hatch": "Sraffozás",
|
||||
"ui.heading_add_ins": "BŐVÍTMÉNYEK",
|
||||
"ui.heading_assemble": "ÖSSZESZERELÉS",
|
||||
"ui.heading_box": "DOBOZ",
|
||||
|
||||
@@ -856,19 +856,57 @@
|
||||
"help.mit": "Lisensi MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Lisensi pihak ketiga",
|
||||
"home.back": "Kembali ke {name}",
|
||||
"home.crashes": "Laporan kerusakan",
|
||||
"home.design_workspace": "Desain",
|
||||
"home.documentation": "Dokumentasi",
|
||||
"home.export": "Ekspor",
|
||||
"home.export_hint": "Pilih format untuk desain saat ini.",
|
||||
"home.export_json_hint": "Desain yang dapat diedit dan riwayat perintah",
|
||||
"home.export_mesh_hint": "Mesh untuk pencetakan atau berbagi",
|
||||
"home.export_step_hint": "Geometri benda padat yang presisi",
|
||||
"home.import": "Impor…",
|
||||
"home.import_hint": "STEP, IGES, atau mesh ke dalam desain baru",
|
||||
"home.in_this_browser": "di peramban ini",
|
||||
"home.missing": "hilang",
|
||||
"home.month_01": "Jan",
|
||||
"home.month_02": "Feb",
|
||||
"home.month_03": "Mar",
|
||||
"home.month_04": "Apr",
|
||||
"home.month_05": "Mei",
|
||||
"home.month_06": "Jun",
|
||||
"home.month_07": "Jul",
|
||||
"home.month_08": "Agu",
|
||||
"home.month_09": "Sep",
|
||||
"home.month_10": "Okt",
|
||||
"home.month_11": "Nov",
|
||||
"home.month_12": "Des",
|
||||
"home.new_inches": "Desain baru dalam inci",
|
||||
"home.new_inches_hint": "Inci · terbuka di Desain",
|
||||
"home.new_mm": "Desain baru",
|
||||
"home.new_mm_hint": "Milimeter · terbuka di Desain",
|
||||
"home.open": "Buka…",
|
||||
"home.opened_days_ago": "Dibuka {count} hari lalu",
|
||||
"home.opened_hours_ago": "Dibuka {count} jam lalu",
|
||||
"home.opened_just_now": "Baru saja dibuka",
|
||||
"home.opened_minutes_ago": "Dibuka {count} mnt lalu",
|
||||
"home.open_hint": "Desain .solidcraft ({shortcut})",
|
||||
"home.open_now": "Sedang terbuka",
|
||||
"home.opened_days_ago": "{count} hari lalu",
|
||||
"home.opened_hours_ago": "{count} jam lalu",
|
||||
"home.opened_just_now": "Baru saja",
|
||||
"home.opened_minutes_ago": "{count} mnt lalu",
|
||||
"home.opening": "membuka…",
|
||||
"home.properties": "Properti desain",
|
||||
"home.recent": "Terbaru",
|
||||
"home.recent_empty": "Desain yang Anda buka atau simpan akan muncul di sini.",
|
||||
"home.recovery_count": "Desain yang dapat dipulihkan: {count}",
|
||||
"home.recovery_empty": "Tidak ada desain yang dapat dipulihkan.",
|
||||
"home.recovery_hint": "SolidCraft ditutup sebelum desain ini disimpan.",
|
||||
"home.report_issue": "Laporkan Masalah",
|
||||
"home.samples": "Contoh",
|
||||
"home.settings": "Pengaturan",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Pintasan papan ketik",
|
||||
"home.start_hint": "Buat desain, buka berkas terbaru, atau jelajahi contoh.",
|
||||
"home.whats_new": "Yang baru",
|
||||
"home.yesterday": "Kemarin",
|
||||
"install.button_tip": "Pasang SolidCraft sebagai aplikasi: terbuka di jendelanya sendiri dan berfungsi offline",
|
||||
"install.dock_1": "Pilih File › Add to Dock.",
|
||||
"install.dock_2": "Klik Add.",
|
||||
@@ -1021,7 +1059,7 @@
|
||||
"search.try": "Ketik nama perintah, fitur di desain ini, atau parameter. Untuk menjalankan perintah dengan nilai, ketik id-nya dan objek JSON: {example}",
|
||||
"search.valid": "Valid",
|
||||
"settings.display_quality_note": "Level lebih halus menggambar muka dan tepi lengkung lebih mulus dan butuh waktu meshing lebih lama. Hanya viewport yang berubah, tidak pernah desain atau ekspor.",
|
||||
"settings.frame_rate_note": "Menampilkan frame per detik di sudut kanan bawah tampilan. Saat aktif, tampilan digambar ulang terus-menerus.",
|
||||
"settings.frame_rate_note": "Menampilkan frame per detik di sudut kanan bawah tampilan.",
|
||||
"settings.measure_axis_distances": "Tampilkan jarak X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Tampilkan dimensi komponen sumbu dunia di samping jarak total saat Ukur terbuka.",
|
||||
"settings.msaa_applies_on_restart": "Anti-aliasing berlaku saat SolidCraft dimulai berikutnya",
|
||||
@@ -1144,6 +1182,7 @@
|
||||
"ui.agent_chat_files_left_out": "Maksimal {max} file per pesan; tidak disertakan: {files}",
|
||||
"ui.agent_chat_files_loading": "Menunggu file selesai dibaca",
|
||||
"ui.agent_chat_files_max": "Maksimal {max} file per pesan",
|
||||
"ui.agent_chat_flatpak_none_found": "Agen coding tidak ditemukan. Secara bawaan, sandbox Flatpak tidak dapat melihat agen yang terpasang di folder rumah Anda dan tidak memiliki akses jaringan. Untuk mengizinkan Claude Code dan adaptor ACP-nya yang dipasang melalui nvm atau di ~/.local/bin, jalankan perintah ini di terminal, lalu mulai ulang SolidCraft. Perintah ini memberi akses ke program dan berkas masuk yang disebutkan, serta akses jaringan:",
|
||||
"ui.agent_chat_import_failed": "{name}: impor gagal: {error}",
|
||||
"ui.agent_chat_intro": "Mengobrol dengan agen coding yang sudah Anda pakai, atau endpoint model pilihan Anda. Agen bekerja pada desain yang terbuka lewat alat SolidCraft: perubahannya tampil saat dibuat dan dapat diurungkan satu langkah per pesan. Yang boleh dilakukannya tanpa bertanya diatur di Preferensi > AI.",
|
||||
"ui.agent_chat_key_in_env": "key diatur",
|
||||
@@ -1463,6 +1502,7 @@
|
||||
"ui.grid_follows_view_hint": "Tampilan samping menampilkan grid pada bidang tegak yang menghadapnya, bukan pada dasar",
|
||||
"ui.ground_shadow": "Bayangan dasar",
|
||||
"ui.groupSelected": "Kelompokkan yang Dipilih",
|
||||
"ui.hatch": "Arsir",
|
||||
"ui.heading_add_ins": "ADD-IN",
|
||||
"ui.heading_assemble": "RAKIT",
|
||||
"ui.heading_box": "KOTAK",
|
||||
|
||||
@@ -860,19 +860,57 @@
|
||||
"help.mit": "Licenza MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenze di terze parti",
|
||||
"home.back": "Torna a {name}",
|
||||
"home.crashes": "Rapporti di arresto anomalo",
|
||||
"home.design_workspace": "Progettazione",
|
||||
"home.documentation": "Documentazione",
|
||||
"home.export": "Esporta",
|
||||
"home.export_hint": "Scegli un formato per il progetto attuale.",
|
||||
"home.export_json_hint": "Progetto modificabile e cronologia dei comandi",
|
||||
"home.export_mesh_hint": "Mesh per la stampa o la condivisione",
|
||||
"home.export_step_hint": "Geometria solida precisa",
|
||||
"home.import": "Importa…",
|
||||
"home.import_hint": "STEP, IGES o mesh in un nuovo progetto",
|
||||
"home.in_this_browser": "in questo browser",
|
||||
"home.missing": "non presente",
|
||||
"home.month_01": "gen",
|
||||
"home.month_02": "feb",
|
||||
"home.month_03": "mar",
|
||||
"home.month_04": "apr",
|
||||
"home.month_05": "mag",
|
||||
"home.month_06": "giu",
|
||||
"home.month_07": "lug",
|
||||
"home.month_08": "ago",
|
||||
"home.month_09": "set",
|
||||
"home.month_10": "ott",
|
||||
"home.month_11": "nov",
|
||||
"home.month_12": "dic",
|
||||
"home.new_inches": "Nuovo progetto in pollici",
|
||||
"home.new_inches_hint": "Pollici · si apre in Progettazione",
|
||||
"home.new_mm": "Nuovo progetto",
|
||||
"home.new_mm_hint": "Millimetri · si apre in Progettazione",
|
||||
"home.open": "Apri…",
|
||||
"home.opened_days_ago": "Aperto {count} giorni fa",
|
||||
"home.opened_hours_ago": "Aperto {count} h fa",
|
||||
"home.opened_just_now": "Appena aperto",
|
||||
"home.opened_minutes_ago": "Aperto {count} min fa",
|
||||
"home.open_hint": "Un progetto .solidcraft ({shortcut})",
|
||||
"home.open_now": "Apri ora",
|
||||
"home.opened_days_ago": "{count} giorni fa",
|
||||
"home.opened_hours_ago": "{count} h fa",
|
||||
"home.opened_just_now": "Adesso",
|
||||
"home.opened_minutes_ago": "{count} min fa",
|
||||
"home.opening": "apertura…",
|
||||
"home.properties": "Proprietà del progetto",
|
||||
"home.recent": "Recenti",
|
||||
"home.recent_empty": "I progetti che apri o salvi vengono visualizzati qui.",
|
||||
"home.recovery_count": "Progetti recuperabili: {count}",
|
||||
"home.recovery_empty": "Nessun progetto recuperabile.",
|
||||
"home.recovery_hint": "SolidCraft si è chiuso prima del salvataggio di questi progetti.",
|
||||
"home.report_issue": "Segnala un problema",
|
||||
"home.samples": "Esempi",
|
||||
"home.settings": "Impostazioni",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Scorciatoie da tastiera",
|
||||
"home.start_hint": "Crea un progetto, apri un file recente o esplora un esempio.",
|
||||
"home.whats_new": "Novità",
|
||||
"home.yesterday": "Ieri",
|
||||
"install.button_tip": "Installa SolidCraft come applicazione: si apre in una finestra dedicata e funziona offline",
|
||||
"install.dock_1": "Scegli File › Aggiungi al Dock.",
|
||||
"install.dock_2": "Fai clic su Aggiungi.",
|
||||
@@ -1025,7 +1063,7 @@
|
||||
"search.try": "Digita il nome di un comando, una feature di questo progetto o un parametro. Per eseguire un comando con dei valori, digita il suo id e un oggetto JSON: {example}",
|
||||
"search.valid": "Valido",
|
||||
"settings.display_quality_note": "I livelli più fini rendono più uniformi le facce curve e gli spigoli e richiedono più tempo per la mesh. Cambia solo la vista, mai il progetto o le esportazioni.",
|
||||
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista. Quando è attivo, la vista viene ridisegnata continuamente.",
|
||||
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista.",
|
||||
"settings.measure_axis_distances": "Mostra distanze X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Mostra le componenti lungo gli assi globali accanto alla distanza totale quando Misura è aperto.",
|
||||
"settings.msaa_applies_on_restart": "L'antialiasing verrà applicato al prossimo avvio di SolidCraft",
|
||||
@@ -1148,6 +1186,7 @@
|
||||
"ui.agent_chat_files_left_out": "Un messaggio può contenere al massimo {max} file; esclusi: {files}",
|
||||
"ui.agent_chat_files_loading": "In attesa della lettura dei file",
|
||||
"ui.agent_chat_files_max": "Al massimo {max} file per messaggio",
|
||||
"ui.agent_chat_flatpak_none_found": "Nessun agente di programmazione trovato. Per impostazione predefinita, la sandbox Flatpak non vede gli agenti installati nella tua cartella home e non ha accesso alla rete. Per consentire Claude Code e il suo adattatore ACP installati con nvm o in ~/.local/bin, esegui questo comando in un terminale, poi riavvia SolidCraft. Concede l’accesso ai programmi e ai file di accesso elencati, oltre all’accesso alla rete:",
|
||||
"ui.agent_chat_import_failed": "{name}: importazione non riuscita: {error}",
|
||||
"ui.agent_chat_intro": "Conversa con un agente di programmazione che già usi o con un endpoint di modello a tua scelta. Opera sul progetto aperto tramite gli strumenti di SolidCraft: le modifiche sono visibili mentre vengono eseguite e si annullano in un solo passo per messaggio. Le azioni consentite senza conferma si impostano in Preferenze > IA.",
|
||||
"ui.agent_chat_key_in_env": "chiave impostata",
|
||||
@@ -1467,6 +1506,7 @@
|
||||
"ui.grid_follows_view_hint": "Le viste laterali mostrano la griglia sul piano verticale rivolto verso di esse anziché sul piano di appoggio",
|
||||
"ui.ground_shadow": "Ombra sul piano di appoggio",
|
||||
"ui.groupSelected": "Raggruppa selezionati",
|
||||
"ui.hatch": "Tratteggio",
|
||||
"ui.heading_add_ins": "COMPONENTI AGGIUNTIVI",
|
||||
"ui.heading_assemble": "ASSEMBLA",
|
||||
"ui.heading_box": "PARALLELEPIPEDO",
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
"Active": "アクティブ",
|
||||
"Active rule": "有効なルール",
|
||||
"Add": "追加",
|
||||
"Add Column": "列を追加",
|
||||
"Add Key": "キーを追加",
|
||||
"Add Mid Radius": "中間半径を追加",
|
||||
"Add Row": "行を追加",
|
||||
"Add a tool": "ツールを追加",
|
||||
"Add condition": "条件を追加",
|
||||
"Add or edit rule": "ルールを追加または編集",
|
||||
"Add to New Folder": "新しいフォルダーに追加",
|
||||
"Advanced": "詳細",
|
||||
"Against": "相手",
|
||||
"Alignment": "配置",
|
||||
"All Sketch Points": "すべてのスケッチ点",
|
||||
@@ -32,6 +36,7 @@
|
||||
"Appearance name": "外観名",
|
||||
"Applied Radius": "適用半径",
|
||||
"Apply To": "適用先",
|
||||
"Apply library appearance": "ライブラリの外観を適用",
|
||||
"Apply to selection": "選択項目に適用",
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "先に外観を適用してください。割り当てた外観と、このデザインで作成したマテリアルを書き出します。",
|
||||
"Arc length": "円弧の長さ",
|
||||
@@ -42,13 +47,18 @@
|
||||
"BLEND SURFACE": "ブレンドサーフェス",
|
||||
"BOUNDARY SURFACE": "境界サーフェス",
|
||||
"BROWSER": "ブラウザ",
|
||||
"Bend Edges": "曲げエッジ",
|
||||
"Bend Line Position": "曲げ線位置",
|
||||
"Bend Position": "曲げ位置",
|
||||
"Bend Radius": "曲げ半径",
|
||||
"Bend Table": "曲げテーブル",
|
||||
"Bends": "曲げ",
|
||||
"Bodies": "ボディ",
|
||||
"Bodies (optional)": "ボディ (オプション)",
|
||||
"Body": "ボディ",
|
||||
"Body (last if none)": "ボディ (未指定時は最後)",
|
||||
"Body to split": "分割するボディ",
|
||||
"Body, or faces to take out": "ボディ、または取り除く面",
|
||||
"Boundary": "境界",
|
||||
"Bounding box": "境界ボックス",
|
||||
"CHECK": "チェック",
|
||||
@@ -60,7 +70,9 @@
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSVまたはJSON:既存のパラメータを更新し、新しいパラメータを追加します",
|
||||
"Capture All Positions": "すべての位置をキャプチャ",
|
||||
"Capture Pos.": "位置を記録",
|
||||
"Catch Depth": "キャッチ深さ",
|
||||
"Catch Direction": "キャッチ方向",
|
||||
"Catch Length": "キャッチ長さ",
|
||||
"Cells": "セル",
|
||||
"Center X": "中心 X",
|
||||
"Center Y": "中心 Y",
|
||||
@@ -69,15 +81,21 @@
|
||||
"Chamfer Type": "面取りタイプ",
|
||||
"Check collisions": "衝突チェック",
|
||||
"Checked": "チェック済み",
|
||||
"Choose Variable radius or Spline in Type.": "タイプで Variable radius またはスプラインを選択してください。",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "新しい平面を選択してください。スケッチジオメトリと寸法は2次元座標系のまま維持されます。",
|
||||
"Choose…": "選択…",
|
||||
"Circular": "円形",
|
||||
"Circumference": "円周",
|
||||
"Close draft": "下書きを閉じる",
|
||||
"Collapse": "折りたたむ",
|
||||
"Collisions": "衝突",
|
||||
"Colour": "色",
|
||||
"Command": "コマンド",
|
||||
"Comment": "コメント",
|
||||
"Component 1": "コンポーネント 1",
|
||||
"Component 2": "コンポーネント 2",
|
||||
"Component1": "コンポーネント1",
|
||||
"Components": "コンポーネント",
|
||||
"Config Table": "構成テーブル",
|
||||
"Configuration": "構成",
|
||||
"Configuration rules": "構成ルール",
|
||||
@@ -86,6 +104,7 @@
|
||||
"Contact sets": "接触セット",
|
||||
"Continue on GitHub": "GitHubで続ける",
|
||||
"Continuity": "連続性",
|
||||
"Control Radius": "制御半径",
|
||||
"Copy Error Message": "エラーメッセージをコピー",
|
||||
"Copy Warning Message": "警告メッセージをコピー",
|
||||
"Copy all licences": "すべてのライセンスをコピー",
|
||||
@@ -95,8 +114,11 @@
|
||||
"Corner Relief": "コーナーリリーフ",
|
||||
"Corner Relief Shape": "コーナーリリーフ形状",
|
||||
"Counterbore depth": "座ぐり深さ",
|
||||
"Counterbore Ø": "座ぐり Ø",
|
||||
"Countersink angle": "皿穴角度",
|
||||
"Countersink Ø": "皿穴 Ø",
|
||||
"Create Issue — review draft": "Issueを作成 — 下書きの確認",
|
||||
"Create Issue…": "Issueを作成…",
|
||||
"Cross curves": "交差する曲線",
|
||||
"Cube": "キューブ",
|
||||
"Current orthographic view (1:1 mm)": "現在の平行投影(実寸・mm)",
|
||||
@@ -106,7 +128,11 @@
|
||||
"Curvature change (G3)": "曲率変化 (G3)",
|
||||
"Custom": "カスタム",
|
||||
"Customize Toolbar": "ツールバーのカスタマイズ",
|
||||
"Customize…": "カスタマイズ…",
|
||||
"Cylindrical face": "円筒面",
|
||||
"Delete": "削除",
|
||||
"Delete Selected Features…": "選択したフィーチャーを削除…",
|
||||
"Delete Selected Folders": "選択したフォルダーを削除",
|
||||
"Delete all": "すべて削除",
|
||||
"Delete rule": "ルールを削除",
|
||||
"Delete the row": "行を削除",
|
||||
@@ -117,8 +143,11 @@
|
||||
"Describe what happened before continuing.": "続ける前に、何が起きたか記入してください。",
|
||||
"Designs you open or save appear here.": "開いたデザインや保存したデザインがここに表示されます。",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "「対象オブジェクトまで」により決定されます。角度範囲を編集するには「角度」を選択してください。",
|
||||
"Diameter": "直径",
|
||||
"Direction": "方向",
|
||||
"Discard": "破棄",
|
||||
"Distance": "距離",
|
||||
"Distance 1": "距離 1",
|
||||
"Do not export hidden edges": "出力しない",
|
||||
"Don't ask again": "今後確認しない",
|
||||
"Double-click to rename": "ダブルクリックで名前変更",
|
||||
@@ -130,44 +159,57 @@
|
||||
"Edit Profile Sketch": "プロファイルスケッチを編集",
|
||||
"Enable rule": "ルールを有効化",
|
||||
"Enabled": "有効",
|
||||
"End Radius": "終了半径",
|
||||
"End bulge": "終了側の膨らみ",
|
||||
"End continuity": "終了側の連続性",
|
||||
"Expand": "展開",
|
||||
"Expected result (optional)": "期待した結果(任意)",
|
||||
"Explode": "分解",
|
||||
"Explode Text": "テキストを分解",
|
||||
"Export Animation…": "アニメーションをエクスポート…",
|
||||
"Export Bend Table…": "曲げテーブルを書き出す…",
|
||||
"Export CSV…": "CSVを書き出し…",
|
||||
"Export Current View as DXF": "現在の表示をDXFでエクスポート",
|
||||
"Export Current View as DXF…": "現在の表示をDXFでエクスポート…",
|
||||
"Export DXF": "DXF出力",
|
||||
"Export DXF…": "DXFをエクスポート…",
|
||||
"Export JSON…": "JSONを書き出し…",
|
||||
"Export design library...": "デザインライブラリを書き出す...",
|
||||
"Export hidden edges as dashed lines": "点線で出力",
|
||||
"Exported by SolidWorks, which usually saves Y up": "SolidWorks で書き出されたファイルです。通常は Y 軸が上になります",
|
||||
"Expression": "式",
|
||||
"Extend to meet": "交わるまで延長",
|
||||
"Extents": "範囲",
|
||||
"FILES": "ファイル",
|
||||
"FLAT": "展開",
|
||||
"Face": "面",
|
||||
"Face Appearance": "面の外観",
|
||||
"Face attachment lost": "面アタッチメントが失われました",
|
||||
"Faces": "面",
|
||||
"Faces / edges / vertices": "面 / エッジ / 頂点",
|
||||
"Faces to split": "分割する面",
|
||||
"Fastener": "締結部品",
|
||||
"Favorites": "お気に入り",
|
||||
"Favourite": "お気に入り",
|
||||
"Feature": "フィーチャー",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "多くのCADではY軸を上向きとしているため、Z軸を上向きとするSolidCraftでは、読み込んだモデルが横倒しの向きで表示されることがあります。",
|
||||
"Filter": "フィルター",
|
||||
"Filter…": "フィルター…",
|
||||
"Find in Browser": "ブラウザで検索",
|
||||
"Finish": "終了",
|
||||
"First plane": "第1平面",
|
||||
"Fit and G2 corners require Constant radius.": "フィットと G2 コーナーには Constant radius が必要です。",
|
||||
"Fixed Face": "固定面",
|
||||
"Flat Pattern": "フラットパターン",
|
||||
"Flat size": "展開サイズ",
|
||||
"Follow camera (Look At sketches)": "カメラ追従(スケッチを正面視)",
|
||||
"Follow the camera (Look At sketches)": "カメラ追従(スケッチを正面視)",
|
||||
"Font": "フォント",
|
||||
"From": "開始",
|
||||
"From Bodies": "ボディから",
|
||||
"From edges": "開始側エッジ",
|
||||
"Full Length": "全長",
|
||||
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 コーナーには一定半径と Exact fit が必要です。G2 コーナーの構築はまだサポートされていません。",
|
||||
"Gap": "間隔",
|
||||
"Grid": "グリッド",
|
||||
"HISTORY": "履歴",
|
||||
@@ -175,6 +217,7 @@
|
||||
"Hidden edges": "隠線",
|
||||
"History layout": "履歴のレイアウト",
|
||||
"History names": "履歴名",
|
||||
"Hole (optional)": "穴 (オプション)",
|
||||
"Holes": "穴",
|
||||
"Holes (edges on them)": "穴(そのエッジ)",
|
||||
"Home view": "ホームビュー",
|
||||
@@ -185,14 +228,20 @@
|
||||
"In place (surfaces)": "その場で(サーフェス)",
|
||||
"In plane": "平面内",
|
||||
"Inside": "内側",
|
||||
"Instant": "即時",
|
||||
"Interface font": "インターフェースフォント",
|
||||
"Interferes": "干渉あり",
|
||||
"Invert": "反転",
|
||||
"Isocurve": "等パラメータ線",
|
||||
"Issue title": "Issueタイトル",
|
||||
"Items": "項目",
|
||||
"JOINTS": "ジョイント",
|
||||
"Joint Origin1": "ジョイント原点1",
|
||||
"Keep (points on it)": "残す部分(その上の点)",
|
||||
"Keep Tools": "ツールボディを保持",
|
||||
"Keep of the other surface": "相手のサーフェスで残す部分",
|
||||
"Key a joint's value at this step": "このステップでジョイントの値をキー設定",
|
||||
"Keys": "キー",
|
||||
"LIBRARY": "ライブラリ",
|
||||
"LINKS": "リンク",
|
||||
"Later": "後で",
|
||||
@@ -203,9 +252,14 @@
|
||||
"Letter Spacing": "文字間隔",
|
||||
"Library": "ライブラリ",
|
||||
"Library file": "ライブラリファイル",
|
||||
"Limits": "制限",
|
||||
"Line Spacing": "行間",
|
||||
"Line Type": "線種",
|
||||
"Listing the installed fonts…": "インストール済みフォントを一覧表示中…",
|
||||
"Load library": "ライブラリを読み込む",
|
||||
"Local library": "ローカルライブラリ",
|
||||
"Loop": "ループ",
|
||||
"M5 or M5x0.5": "M5 または M5x0.5",
|
||||
"MANAGE": "管理",
|
||||
"MATERIALS": "材料",
|
||||
"MODEL": "モデル",
|
||||
@@ -216,14 +270,20 @@
|
||||
"Matched restrictions check the final values.": "条件に一致する制限ルールが最終値を確認します。",
|
||||
"Material": "材料",
|
||||
"Max": "最大",
|
||||
"Maximum": "最大",
|
||||
"Mesh body": "メッシュボディ",
|
||||
"Mid Radius": "中間半径",
|
||||
"Middle double-click": "中ボタンのダブルクリック",
|
||||
"Min": "最小",
|
||||
"Minimum": "最小",
|
||||
"Mirror Plane": "ミラー平面",
|
||||
"Mitre": "留め継ぎ",
|
||||
"Model Parameters": "モデルパラメータ",
|
||||
"Modeled": "モデル化済み",
|
||||
"More toolbar panels": "その他のツールバーパネル",
|
||||
"Motion": "モーション",
|
||||
"Mouse gestures": "マウス ジェスチャ",
|
||||
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "マウス ジェスチャは SolidWorks マウス モードでのみ機能します(ホイールで回転、Ctrl + ホイールでパン)。",
|
||||
"Move / Copy": "移動 / コピー",
|
||||
"Move Type": "移動タイプ",
|
||||
"Move down": "下へ移動",
|
||||
@@ -246,8 +306,11 @@
|
||||
"Not available yet": "まだ利用できません",
|
||||
"Notch": "ノッチ",
|
||||
"OUTPUT": "出力",
|
||||
"Of": "の値",
|
||||
"On your computer": "このコンピューター上",
|
||||
"Opacity": "不透明度",
|
||||
"Open GitHub draft": "GitHubの下書きを開く",
|
||||
"Open library...": "ライブラリを開く...",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "ローカルライブラリはデスクトップアプリで開いてください。このデザインのライブラリはここで書き出せます。",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Rust製オープンソース・パラメトリック3D CAD。ライセンス: MIT または Apache-2.0。",
|
||||
"Opens your browser and copies the report text for you to paste into GitHub.": "ブラウザーを開き、GitHubに貼り付ける報告本文をコピーします。",
|
||||
@@ -269,8 +332,10 @@
|
||||
"Picked": "選択したもの",
|
||||
"Picked point": "選択した点",
|
||||
"Pitch": "ピッチ",
|
||||
"Pivot": "ピボット",
|
||||
"Place at the origin instead": "代わりに原点に配置",
|
||||
"Plastic Rules": "樹脂ルール",
|
||||
"Play the joint's motion in the view": "ビューでジョイントの動きを再生",
|
||||
"Playback": "再生",
|
||||
"Plot": "グラフ",
|
||||
"Position": "位置",
|
||||
@@ -278,14 +343,18 @@
|
||||
"Position (G0)": "位置 (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "エッジ上の位置(始点が0、終点が1)",
|
||||
"Preferences › Design turns the offer back on": "環境設定 › デザイン で再びオンにできます",
|
||||
"Preview": "プレビュー",
|
||||
"Preview Row": "行をプレビュー",
|
||||
"Preview leaves the current design unchanged.": "プレビューは現在のデザインを変更しません。",
|
||||
"Profile / Edges": "プロファイル / エッジ",
|
||||
"Profiles or curves": "プロファイルまたは曲線",
|
||||
"Profiles/Curves": "プロファイル/曲線",
|
||||
"Quantity": "量",
|
||||
"RECENT": "最近使用した項目",
|
||||
"REFERENCE": "参照",
|
||||
"RULED SURFACE": "ルールドサーフェス",
|
||||
"RULES": "ルール",
|
||||
"Radius": "半径",
|
||||
"Radius Fit": "半径フィット",
|
||||
"Ratio": "比率",
|
||||
"Reattach selected face": "選択した面を再アタッチ",
|
||||
@@ -294,6 +363,8 @@
|
||||
"Rectangular": "長方形",
|
||||
"Reference face": "参照面",
|
||||
"Remove Tools": "ツールボディを削除",
|
||||
"Remove condition": "条件を削除",
|
||||
"Remove from Folder": "フォルダーから削除",
|
||||
"Remove from list": "リストから削除",
|
||||
"Remove override": "オーバーライドを削除",
|
||||
"Remove private design details or paths before sharing.": "共有前に、非公開の設計情報やファイルパスを削除してください。",
|
||||
@@ -301,6 +372,9 @@
|
||||
"Remove the column": "列を削除",
|
||||
"Remove the track": "トラックを削除",
|
||||
"Remove this point": "この点を削除",
|
||||
"Rename Feature": "フィーチャーの名前を変更",
|
||||
"Rename Folder": "フォルダーの名前を変更",
|
||||
"Replace existing library file": "既存のライブラリファイルを置き換える",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "バグを報告 — スクリーンショットを確認してGitHub下書きを作成",
|
||||
"Report text copied. Paste it into GitHub and submit your report there.": "報告本文をコピーしました。GitHubに貼り付けて投稿してください。",
|
||||
"Reports are public. A GitHub account is required.": "報告は公開されます。GitHubアカウントが必要です。",
|
||||
@@ -309,6 +383,7 @@
|
||||
"Restore View": "ビューを復元",
|
||||
"Restore outer boundary": "外側の境界を復元",
|
||||
"Restore the inherited component or physical material appearance": "コンポーネントまたは物理マテリアルから継承した外観に戻す",
|
||||
"Restore this studio's tools": "このスタジオのツールを復元",
|
||||
"Result": "結果",
|
||||
"Retake screenshot": "スクリーンショットを再取得",
|
||||
"Reverse": "反転",
|
||||
@@ -319,7 +394,13 @@
|
||||
"Revert Position": "位置を元に戻す",
|
||||
"Review the report and screenshot before opening GitHub.": "GitHubを開く前にレポートとスクリーンショットを確認してください。",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "GitHubで内容を確認し、Submit new issueをクリックすると公開されます。",
|
||||
"Rib Length": "リブの長さ",
|
||||
"Rib Offset": "リブのオフセット",
|
||||
"Rib Thickness": "リブの厚さ",
|
||||
"Ribs": "リブ",
|
||||
"Rim Edge": "リムエッジ",
|
||||
"Ring of tools on the right button": "右ボタンのツールリング",
|
||||
"Roll Back Here": "ここまでロールバック",
|
||||
"Roll History Marker Here": "履歴マーカーをここに移動",
|
||||
"Round": "丸め",
|
||||
"Rule": "ルール",
|
||||
@@ -338,13 +419,18 @@
|
||||
"Search": "検索",
|
||||
"Search commands": "コマンドを検索",
|
||||
"Search this library": "このライブラリを検索",
|
||||
"Second plane": "第2平面",
|
||||
"Section": "断面",
|
||||
"Sections (one sketch each)": "セクション (各1スケッチ)",
|
||||
"Select Other": "その他を選択",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "その他を選択: モデル上で左ボタンを長押し(または右クリック › その他を選択…)するとカーソル下の要素が一覧表示されます。同じ場所を再度クリックすると次の要素が選択されます。中ボタンをダブルクリックするとホームビューに戻ります(環境設定 › ナビゲーション)。",
|
||||
"Select bodies (a face picks its body)": "ボディを選択(面をクリックするとそのボディが選択されます)",
|
||||
"Select bodies or faces instead": "代わりにボディまたは面を選択",
|
||||
"Select edges": "エッジを選択",
|
||||
"Selection filter": "選択フィルター",
|
||||
"Semantic faces share their appearance across instances.": "セマンティック面はインスタンス間で外観を共有します。",
|
||||
"Send your report on GitHub": "GitHubでレポートを送信",
|
||||
"Shaded": "シェーディング",
|
||||
"Shape": "形状",
|
||||
"Sheet Rules": "板金ルール",
|
||||
"Show Agent Cursor": "エージェントカーソルを表示",
|
||||
@@ -362,6 +448,7 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "これらのデザインが保存される前にSolidCraftが終了しました。最後の自動保存ファイルを開くことができます。",
|
||||
"Specialise": "特殊操作",
|
||||
"Speed": "速度",
|
||||
"Splitting tool": "分割ツール",
|
||||
"Stand it up": "正立させる",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "正立させる: 取り消し可能な1つの移動フィーチャーとして、X軸周りに90度回転します。",
|
||||
"Stand this part up?": "このパーツを正立させますか?",
|
||||
@@ -372,16 +459,22 @@
|
||||
"Steps to reproduce (optional)": "再現手順(任意)",
|
||||
"Stitch the kept pieces": "残した部分をステッチ",
|
||||
"Stitch to both bodies": "両方のボディにステッチ",
|
||||
"Studio": "スタジオ",
|
||||
"Study": "スタディ",
|
||||
"Style": "スタイル",
|
||||
"Summary": "概要",
|
||||
"Suppress": "抑制",
|
||||
"Suppressed": "抑制",
|
||||
"Surface": "サーフェス",
|
||||
"Surface, or faces to turn over": "サーフェス、または裏返す面",
|
||||
"Swap Ends": "端点を入れ替え",
|
||||
"TRIM": "トリム",
|
||||
"Tangent (G1)": "接線 (G1)",
|
||||
"Tangent Chain": "正接チェーン",
|
||||
"Tangent chain": "接線チェーン",
|
||||
"Tap drill diameter": "タップ下穴径",
|
||||
"Target": "ターゲット",
|
||||
"Target Body": "ターゲットボディ",
|
||||
"Tear": "引き裂き",
|
||||
"Technical details": "技術情報",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "破線のスケッチは保存された平面ガイドです。新しい面を選択してアタッチメントを修復してください。",
|
||||
@@ -396,24 +489,31 @@
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "この子要素は共有されています。位置の変更は親のすべてのコピーに適用されます。",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "この面は安定したアタッチメントを提供できません。別の面を選択するか、固定平面を明示的に使用してください。",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "この画像はまだGitHubに添付されていません。コピーまたは保存し、GitHubで添付してください。",
|
||||
"To": "終了",
|
||||
"To Offset": "オフセットまで",
|
||||
"To edges": "終了側エッジ",
|
||||
"Tolerance": "公差",
|
||||
"Too close": "近すぎる",
|
||||
"Tool Bodies": "ツールボディ",
|
||||
"Tools": "ツール",
|
||||
"Tools, clockwise from the top": "上から時計回りにツール",
|
||||
"Trimming tool (plane, face or curves)": "トリムツール(平面、面、または曲線)",
|
||||
"Turn off section analysis before exporting the view.": "断面解析をオフにしてからエクスポートしてください。",
|
||||
"Type": "タイプ",
|
||||
"UNTRIM": "トリム解除",
|
||||
"Unit": "単位",
|
||||
"Unsuppress": "抑制を解除",
|
||||
"Updating the preview": "プレビューを更新中",
|
||||
"Use active author placement": "アクティブな作成元配置を使用",
|
||||
"Use fixed plane": "固定平面を使用",
|
||||
"Use saved fixed plane": "保存された固定平面を使用",
|
||||
"Use selected face as fixed plane": "選択した面を固定平面として使用",
|
||||
"Use the SolidWorks mouse mode": "SolidWorks マウス モードを使用",
|
||||
"User Parameters": "ユーザーパラメータ",
|
||||
"VIEW": "表示",
|
||||
"VIEWS": "ビュー",
|
||||
"Value": "値",
|
||||
"Variable Radius": "可変半径",
|
||||
"Velocity": "速度",
|
||||
"Vertices": "頂点",
|
||||
"View navigator": "ビューナビゲーター",
|
||||
@@ -427,13 +527,44 @@
|
||||
"When": "条件 (When)",
|
||||
"Width": "幅",
|
||||
"Will fail": "失敗します",
|
||||
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "マウス ジェスチャがオンのとき、右ボタンではビューを回転しません。押したまま動かすとツールリングが開くので、ツールのゾーンに触れてください。通常の右クリックでは引き続きメニューが開きます。",
|
||||
"X": "X",
|
||||
"X Angle": "X 角度",
|
||||
"X angle": "X角度",
|
||||
"X distance": "X距離",
|
||||
"Y": "Y",
|
||||
"Y Angle": "Y 角度",
|
||||
"Y angle": "Y角度",
|
||||
"Y distance": "Y距離",
|
||||
"Z": "Z",
|
||||
"Z Angle": "Z 角度",
|
||||
"Z Distance": "Z 距離",
|
||||
"Z angle": "Z角度",
|
||||
"Z distance": "Z距離",
|
||||
"Zebra": "ゼブラ",
|
||||
"ai.prefs.availability": "アシスタントの操作",
|
||||
"ai.prefs.background_commands": "エージェントのコマンドをバックグラウンドで実行",
|
||||
"ai.prefs.follow_camera": "カメラをスケッチに向ける",
|
||||
"ai.prefs.follow_camera_off": "カメラの自動追従はオフです。",
|
||||
"ai.prefs.follow_camera_on": "エージェントが作成するスケッチにカメラを向けます。",
|
||||
"ai.prefs.intro": "エージェント、権限、作業の表示方法を設定します。",
|
||||
"ai.prefs.make_default": "既定にする",
|
||||
"ai.prefs.no_agents": "エージェントはまだありません。追加するか、更新してインストール済みのエージェントを検索してください。",
|
||||
"ai.prefs.no_saved_approvals": "保存された許可はありません。",
|
||||
"ai.prefs.override_all_warning": "このエージェントは確認せずに設計を保存、上書き、書き出し、置換できます。",
|
||||
"ai.prefs.permission_all": "すべて許可",
|
||||
"ai.prefs.permission_all_summary": "利用可能なSolidCraftツールを確認せず実行します。",
|
||||
"ai.prefs.permission_ask": "変更前に確認",
|
||||
"ai.prefs.permission_ask_summary": "設計の変更とファイル操作を毎回確認します。",
|
||||
"ai.prefs.permission_edits": "モデル編集を許可",
|
||||
"ai.prefs.permission_edits_summary": "モデル編集は自動実行。ファイル操作や破壊的な操作は確認します。",
|
||||
"ai.prefs.permission_scope": "SolidCraftツールのみに適用されます。エージェントのシェルやファイル編集には、そのエージェント自身の権限設定が適用されます。",
|
||||
"ai.prefs.pointer_preview_hidden": "ポインターは非表示",
|
||||
"ai.prefs.pointer_preview_speed": "エージェントのポインター · {speed}",
|
||||
"ai.prefs.pointer_speed": "移動速度",
|
||||
"ai.prefs.privacy_note": "会話は選択したエージェントまたはエンドポイントにのみ送信されます。エンドポイントのキーはシステムのキーチェーンに保存されます。",
|
||||
"ai.prefs.show_pointer": "エージェントのポインターを表示",
|
||||
"ai.prefs.turn_limit_hint": "アシスタントが1件のメッセージで行える、設計を変更するツール呼び出しの上限です。",
|
||||
"angle, height": "角度, 高さ",
|
||||
"appearance.dielectric_ior": "誘電体の屈折率",
|
||||
"appearance.metallic": "金属度",
|
||||
@@ -479,8 +610,10 @@
|
||||
"cmd.component_activate": "コンポーネントをアクティブ化",
|
||||
"cmd.component_break_link": "リンクを解除",
|
||||
"cmd.component_capture_position": "位置をキャプチャ",
|
||||
"cmd.component_copies": "同一のコピーを検索",
|
||||
"cmd.component_create": "新規コンポーネント",
|
||||
"cmd.component_delete": "コンポーネントを削除",
|
||||
"cmd.component_extract": "ファイルに抽出",
|
||||
"cmd.component_from_bodies": "ボディからコンポーネントを作成",
|
||||
"cmd.component_insert_design": "デザインを挿入",
|
||||
"cmd.component_links": "リンクを一覧表示",
|
||||
@@ -493,6 +626,7 @@
|
||||
"cmd.component_properties": "コンポーネントの物理的プロパティ",
|
||||
"cmd.component_relink": "参照元を再リンク",
|
||||
"cmd.component_rename": "コンポーネントの名前を変更",
|
||||
"cmd.component_replace_with_link": "リンクに置き換え",
|
||||
"cmd.component_revert_position": "位置を元に戻す",
|
||||
"cmd.component_unlock": "内容のロックを解除",
|
||||
"cmd.component_update_links": "リンクを更新",
|
||||
@@ -610,6 +744,7 @@
|
||||
"cmd.motion_plot": "モーション スタディのグラフ",
|
||||
"cmd.motion_study": "モーション スタディ",
|
||||
"cmd.motion_trace": "モーション トレース",
|
||||
"cmd.motion_trace_edit": "モーション トレースを編集",
|
||||
"cmd.motion_trace_keep": "モーション トレースを保持",
|
||||
"cmd.occurrence_copy": "貼り付け(インスタンス)",
|
||||
"cmd.occurrence_ground": "空間固定",
|
||||
@@ -889,19 +1024,57 @@
|
||||
"help.solid.revolve": "スケッチの輪郭を軸のまわりに回転させ、丸い立体を作ります。軸、つまみ、ボウルなど、中心線まわりに丸い部品に使います。",
|
||||
"help.solid.shell": "立体の中をくり抜き、指定した厚さの壁を残します。選んだ面を開口にすることもできます。箱、カバーなどの薄肉の部品に使います。",
|
||||
"help.third_party": "サードパーティライセンス",
|
||||
"home.back": "{name} に戻る",
|
||||
"home.crashes": "クラッシュレポート",
|
||||
"home.design_workspace": "デザイン",
|
||||
"home.documentation": "ドキュメント",
|
||||
"home.export": "エクスポート",
|
||||
"home.export_hint": "現在のデザインの出力形式を選択してください。",
|
||||
"home.export_json_hint": "編集可能なデザインとコマンド履歴",
|
||||
"home.export_mesh_hint": "印刷や共有用のメッシュ",
|
||||
"home.export_step_hint": "精密なソリッド形状",
|
||||
"home.import": "インポート…",
|
||||
"home.import_hint": "STEP、IGES、またはメッシュを新しいデザインに読み込みます",
|
||||
"home.in_this_browser": "このブラウザ内",
|
||||
"home.missing": "見つかりません",
|
||||
"home.month_01": "1月",
|
||||
"home.month_02": "2月",
|
||||
"home.month_03": "3月",
|
||||
"home.month_04": "4月",
|
||||
"home.month_05": "5月",
|
||||
"home.month_06": "6月",
|
||||
"home.month_07": "7月",
|
||||
"home.month_08": "8月",
|
||||
"home.month_09": "9月",
|
||||
"home.month_10": "10月",
|
||||
"home.month_11": "11月",
|
||||
"home.month_12": "12月",
|
||||
"home.new_inches": "新しいデザイン(インチ)",
|
||||
"home.new_inches_hint": "インチ · デザインで開きます",
|
||||
"home.new_mm": "新しいデザイン",
|
||||
"home.new_mm_hint": "ミリメートル · デザインで開きます",
|
||||
"home.open": "開く…",
|
||||
"home.opened_days_ago": "{count} 日前に開きました",
|
||||
"home.opened_hours_ago": "{count} 時間前に開きました",
|
||||
"home.opened_just_now": "たった今開きました",
|
||||
"home.opened_minutes_ago": "{count} 分前に開きました",
|
||||
"home.open_hint": ".solidcraft 形式のデザイン ({shortcut})",
|
||||
"home.open_now": "今すぐ開く",
|
||||
"home.opened_days_ago": "{count} 日前",
|
||||
"home.opened_hours_ago": "{count} 時間前",
|
||||
"home.opened_just_now": "たった今",
|
||||
"home.opened_minutes_ago": "{count} 分前",
|
||||
"home.opening": "開いています…",
|
||||
"home.properties": "デザインのプロパティ",
|
||||
"home.recent": "最近使用した項目",
|
||||
"home.recent_empty": "開いたり保存したデザインがここに表示されます。",
|
||||
"home.recovery_count": "復元可能なデザイン: {count} 件",
|
||||
"home.recovery_empty": "復元可能なデザインはありません。",
|
||||
"home.recovery_hint": "これらのデザインが保存される前に SolidCraft が終了しました。",
|
||||
"home.report_issue": "問題を報告",
|
||||
"home.samples": "サンプル",
|
||||
"home.settings": "設定",
|
||||
"home.short_date": "{month}{day}日",
|
||||
"home.shortcuts": "キーボードショートカット",
|
||||
"home.start_hint": "デザインを作成するか、最近使ったファイルを開くか、サンプルをご覧ください。",
|
||||
"home.whats_new": "新機能",
|
||||
"home.yesterday": "昨日",
|
||||
"install.button_tip": "SolidCraft をアプリとしてインストールします。専用のウィンドウで開き、オフラインでも使えます",
|
||||
"install.dock_1": "「ファイル」›「Dockに追加」を選びます。",
|
||||
"install.dock_2": "「追加」をクリックします。",
|
||||
@@ -959,8 +1132,15 @@
|
||||
"motion.to_end": "最後へ",
|
||||
"motion.to_start": "最初へ",
|
||||
"motion.track_name": "トラック {number}",
|
||||
"nav.alias.help": "Shift+Alt+左ドラッグで回転、Shift+Alt+中ドラッグでパン、Shift+Alt+右ドラッグでズーム。中ドラッグでもパン、右ドラッグで回転。",
|
||||
"nav.blender.help": "中ドラッグで回転、Shift+中ドラッグでパン、Ctrl+中ドラッグでズーム。右ドラッグではカメラは動きません。",
|
||||
"nav.free_orbit.help": "マウスと3Dマウスで、極点を越えて回転し、ロールを保ちます。\nShiftによる軸スナップは無効になります。",
|
||||
"nav.free_orbit.label": "自由回転",
|
||||
"nav.fusion.help": "中ドラッグでパン、Shift+中ドラッグで回転、右ドラッグで回転。",
|
||||
"nav.inventor.help": "中ドラッグでパン、Shift+中ドラッグで回転、Ctrl+中ドラッグでズーム、右ドラッグで回転。",
|
||||
"nav.openscad.help": "空きスペースで左ドラッグすると回転、Shift+左ドラッグで矩形選択、スケッチ ジオメトリはドラッグで編集。右または中ドラッグでパン、Shift+右または Shift+中ドラッグでズーム。",
|
||||
"nav.solidworks.help": "中ドラッグで回転、Ctrl+中ドラッグでパン、Shift+中ドラッグでズーム、右ドラッグで回転。",
|
||||
"nav.tinkercad.help": "右ドラッグで回転、中ドラッグまたは Shift+右ドラッグでパン。",
|
||||
"nav.touchpad.help": "Shift を押しながらポインターを動かすとパン、Alt/Option を押しながら動かすと回転します。マウスボタンは不要です。縦スクロールでズームします。",
|
||||
"nav.touchpad.label": "タッチパッド",
|
||||
"nav.touchpad.orbit": "回転(Alt/Option を押しながら移動、マウスボタン不要)",
|
||||
@@ -983,8 +1163,130 @@
|
||||
"pattern.flip_direction": "方向を反転",
|
||||
"pick a hole to seat it, or OK places it at the origin": "配置先の穴を選択するか、[OK]を押すと原点に配置します。",
|
||||
"pick one or two vertices, edges or faces": "1つまたは2つの頂点、エッジ、面を選択",
|
||||
"prefs.add_item": "項目を追加",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option でマニピュレーターの矢印も自由に動かす",
|
||||
"prefs.anti_aliasing_note": "輪郭のぎざぎざを滑らかにします。",
|
||||
"prefs.applies_on_restart": "再起動後に適用",
|
||||
"prefs.assistant": "アシスタント",
|
||||
"prefs.axes": "軸",
|
||||
"prefs.axis": "軸",
|
||||
"prefs.bottom": "下",
|
||||
"prefs.browser": "ブラウザー",
|
||||
"prefs.built_in": "組み込み",
|
||||
"prefs.button_left": "左ドラッグ",
|
||||
"prefs.button_middle": "中央ドラッグ",
|
||||
"prefs.button_right": "右ドラッグ",
|
||||
"prefs.choose_font": "フォントファイルを選択…",
|
||||
"prefs.click": "クリック",
|
||||
"prefs.contact_shading": "柔らかな接触陰影",
|
||||
"prefs.contact_shading_note": "部品が近接する部分に柔らかな陰影を付けます。",
|
||||
"prefs.context_menu": "コンテキストメニュー",
|
||||
"prefs.context_menu_ring": "コンテキストメニュー · 長押しで放射状メニュー",
|
||||
"prefs.cpu": "CPU(ソフトウェア)",
|
||||
"prefs.crash_reports": "クラッシュレポートを開く…",
|
||||
"prefs.crash_reports_note": "問題を再現する設計とコマンドを含むレポートをこのコンピューターに保存します。",
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "曲面",
|
||||
"prefs.curves": "曲面",
|
||||
"prefs.dead_zone_note": "斜線の帯はデッドゾーンです。この範囲内の動きは無視されます。",
|
||||
"prefs.default": "既定",
|
||||
"prefs.design": "設計の既定値",
|
||||
"prefs.design_note": "新しいデザインの初期設定です。開いているデザインはそれぞれの設定を保持します。",
|
||||
"prefs.devices": "3D マウス",
|
||||
"prefs.display": "表示",
|
||||
"prefs.edges": "エッジと陰影",
|
||||
"prefs.extend": "拡張",
|
||||
"prefs.extra_navigation": "何もない場所でCtrl+左ドラッグすると回転し、Spaceを押しながらドラッグすると移動します。",
|
||||
"prefs.follows_os": "OS の設定に従う",
|
||||
"prefs.font": "フォント",
|
||||
"prefs.font_fallback": "選択したフォントにない文字は組み込みフォントで表示し、ラベルが空の四角になるのを防ぎます。",
|
||||
"prefs.font_sample": "いろはにほへと · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "選択したフォントを読み込めませんでした。",
|
||||
"prefs.general": "一般",
|
||||
"prefs.general_note": "言語、保存、更新。",
|
||||
"prefs.gesture_orbit": "回転",
|
||||
"prefs.gesture_pan": "移動",
|
||||
"prefs.gesture_zoom": "ズーム",
|
||||
"prefs.graphics": "グラフィックス",
|
||||
"prefs.graphics_note": "速度と滑らかさのバランスを調整します。各選択肢の効果をサンプルで確認できます。",
|
||||
"prefs.graphics_presets_note": "プリセットは以下の設定をまとめて変更します。",
|
||||
"prefs.history": "履歴",
|
||||
"prefs.hold_stop_snapping": "押している間スナップを解除",
|
||||
"prefs.icons_only": "アイコンのみ",
|
||||
"prefs.imported_file": "読み込んだファイル",
|
||||
"prefs.input": "入力",
|
||||
"prefs.interface": "インターフェース",
|
||||
"prefs.interface_language": "表示言語",
|
||||
"prefs.interface_note": "SolidCraft の外観と各パネルの配置。",
|
||||
"prefs.interval_minutes": "{n} 分",
|
||||
"prefs.labels": "ラベル",
|
||||
"prefs.layout": "レイアウト",
|
||||
"prefs.layout_note": "コンパクトではパネルをウィンドウの端に固定します。ブラウザーの半透明表示はフローティングで使用できます。アシスタントの配置はアシスタントページで変更します。",
|
||||
"prefs.left": "左",
|
||||
"prefs.left_click": "左クリック",
|
||||
"prefs.live_input": "現在の入力",
|
||||
"prefs.live_preview": "ライブプレビュー",
|
||||
"prefs.minutes": "分",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "マウスとトラックパッド",
|
||||
"prefs.mouse_heading": "マウス",
|
||||
"prefs.mouse_mapping": "マウスの割り当て",
|
||||
"prefs.navigation": "ナビゲーション",
|
||||
"prefs.navigation_note": "マウス、トラックパッド、3Dマウスによるビューの操作。",
|
||||
"prefs.not_on_gpu": "この GPU は非対応",
|
||||
"prefs.on": "オン",
|
||||
"prefs.opaque": "不透明",
|
||||
"prefs.orbit_snap": "回転中にShiftを押すと軸にスナップします。",
|
||||
"prefs.orientation": "方向",
|
||||
"prefs.overall_speed": "全体の速度",
|
||||
"prefs.preview_choices": "選択した設定のプレビュー",
|
||||
"prefs.quality_default": "既定",
|
||||
"prefs.quality_fastest": "最速",
|
||||
"prefs.quality_slowest": "最も低速",
|
||||
"prefs.quality_smoother": "より滑らか",
|
||||
"prefs.reports_saved": "{n} 件保存済み",
|
||||
"prefs.right_click": "右クリック",
|
||||
"prefs.saving": "自動保存と復元",
|
||||
"prefs.scale_note": "ウィンドウ内では Ctrl + / Ctrl − でも変更できます。100% のボタンは 28 px です。",
|
||||
"prefs.scale_text": "拡大率と文字",
|
||||
"prefs.select": "選択",
|
||||
"prefs.select_one_item": "項目を 1 つ選択",
|
||||
"prefs.selecting": "選択",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift は追加のみ、Ctrl / Cmd は選択を切り替え",
|
||||
"prefs.shift_add_note": "オン:Shift は追加、Ctrl / Cmd は選択を切り替えます。オフ(既定):どちらも選択を切り替えます。",
|
||||
"prefs.shortcuts": "ショートカット",
|
||||
"prefs.sketch_selection": "スケッチと選択",
|
||||
"prefs.sketch_selection_heading": "スケッチと選択",
|
||||
"prefs.sketch_selection_note": "スケッチのスナップとクリックによる選択。",
|
||||
"prefs.sketching": "スケッチ",
|
||||
"prefs.snap_drawing_note": "スケッチの描画中または点のドラッグ中に使用します。",
|
||||
"prefs.source": "入力元",
|
||||
"prefs.speed": "速度",
|
||||
"prefs.stand_up_note": "STEP または IGES ファイルを Z 上向きで開いた後、X 軸周りに 90° 回転するか確認します。",
|
||||
"prefs.status_done": "完了",
|
||||
"prefs.status_error": "エラー",
|
||||
"prefs.status_selected": "選択中",
|
||||
"prefs.status_warning": "警告",
|
||||
"prefs.supersampling_brief": "大きく描画して縮小するため、非常に鮮明になりますが負荷も増えます。",
|
||||
"prefs.tab_accent_note": "アクセント色はアクティブなタブに従います",
|
||||
"prefs.theme_file": "テーマファイル",
|
||||
"prefs.timeline_half_hour": "−30 分",
|
||||
"prefs.timeline_now": "現在",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
"prefs.toggle_item": "項目を追加、または選択から外す",
|
||||
"prefs.toolbar": "ツールバー",
|
||||
"prefs.translucent": "半透明",
|
||||
"prefs.under_browser": "ブラウザー下",
|
||||
"prefs.under_cursor": "カーソル下",
|
||||
"prefs.units_precision": "単位と精度",
|
||||
"prefs.updates": "更新",
|
||||
"prefs.view": "ビューの動作",
|
||||
"prefs.viewport": "ビューポート",
|
||||
"prefs.wheel": "ホイール",
|
||||
"prefs.wheel_trackpad": "ホイールとトラックパッド",
|
||||
"prefs.workspace": "ワークスペース",
|
||||
"prefs.zoom_toward_cursor": "カーソルに向かってズーム",
|
||||
"properties.title": "プロパティ: {name}",
|
||||
"recovery.days_ago": "{count}日前",
|
||||
"recovery.detail": "{file} · {features}フィーチャー · 自動保存: {when}",
|
||||
@@ -1055,8 +1357,8 @@
|
||||
"search.suggest": "{label} ですか?",
|
||||
"search.try": "コマンド名、このデザインのフィーチャー、またはパラメーターを入力します。値を指定して実行するには、IDとJSONオブジェクトを入力します:{example}",
|
||||
"search.valid": "有効",
|
||||
"settings.display_quality_note": "細かいほど曲面とエッジが滑らかに表示されますが、メッシュの生成に時間がかかります。変わるのはビューポートだけで、設計やエクスポートは変わりません。",
|
||||
"settings.frame_rate_note": "ビューの右下に 1 秒あたりのフレーム数を表示します。オンの間、ビューは常に再描画されます。",
|
||||
"settings.display_quality_note": "表示だけに影響します。設計や書き出しは変わりません。",
|
||||
"settings.frame_rate_note": "ビューの右下に 1 秒あたりのフレーム数を表示します。",
|
||||
"settings.measure_axis_distances": "計測のX・Y・Z距離を表示",
|
||||
"settings.measure_axis_distances_help": "計測ダイアログを開いている間、2点間距離に加えてワールドX・Y・Z軸方向の寸法線と距離を表示します。",
|
||||
"settings.msaa_applies_on_restart": "アンチエイリアスは次回 SolidCraft を起動したときに適用されます",
|
||||
@@ -1065,6 +1367,7 @@
|
||||
"settings.reset_intro": "次の設定はこのコンピューターでは使用できず、SolidCraft が起動できなくなるため変更されました:",
|
||||
"settings.reset_title": "設定が変更されました",
|
||||
"settings.supersampling_note": "ビューを高い解像度で描画してから縮小します。4K 未満の画面でエッジ、線、グリッドが滑らかになります。GPU の負荷が増えます(1.5× で約 2.25 倍、2× で 4 倍のピクセル)。",
|
||||
"sheet.no_body": "板金ボディがありません。フランジ > Base で作成するか、板金に変換でプレートを板金に変換してください(広い平面を選択)。",
|
||||
"shortcut.json_error": "パラメータはJSON形式で指定してください: {error}",
|
||||
"shortcut.no_command": "コマンド`{id}`がありません。JSONの前のテキストはコマンドIDである必要があります",
|
||||
"shortcut.pin": "Sボックスの先頭にピン留め",
|
||||
@@ -1101,6 +1404,9 @@
|
||||
"thread.designation": "ねじの呼び",
|
||||
"thread.iso_metric": "ISOメートルねじ",
|
||||
"thread.select_size": "サイズを選択",
|
||||
"timeline.delete_after_marker_one": "履歴マーカー以降の 1 個のフィーチャーを削除",
|
||||
"timeline.delete_after_marker_other": "履歴マーカー以降の {count} 個のフィーチャーを削除",
|
||||
"timeline.delete_many": "選択した {count} 個のフィーチャーを削除",
|
||||
"timeline.group": "フィーチャーフォルダを新規作成",
|
||||
"timeline.move_many": "タイムライン項目を移動",
|
||||
"timeline.move_to_group": "フィーチャーをフォルダへ移動",
|
||||
@@ -1179,6 +1485,7 @@
|
||||
"ui.agent_chat_files_left_out": "1 つのメッセージで送れるファイルは {max} 個までです。送らなかったファイル: {files}",
|
||||
"ui.agent_chat_files_loading": "ファイルの読み込みを待っています",
|
||||
"ui.agent_chat_files_max": "1 つのメッセージに添付できるのは {max} 個までです",
|
||||
"ui.agent_chat_flatpak_none_found": "コーディングエージェントが見つかりません。Flatpak のサンドボックスは、既定ではホームフォルダー内のエージェントにアクセスできず、ネットワークにも接続できません。nvm または ~/.local/bin にインストールした Claude Code と ACP アダプターを許可するには、端末で次のコマンドを実行し、SolidCraft を再起動してください。指定したプログラムとログインファイルへのアクセス、およびネットワーク接続を許可します:",
|
||||
"ui.agent_chat_import_failed": "{name}: インポートに失敗しました: {error}",
|
||||
"ui.agent_chat_intro": "普段使っているコーディングエージェント、または選んだモデルのエンドポイントとチャットできます。SolidCraft のツールで開いているデザインを操作し、変更はその場で表示され、メッセージごとに1回で元に戻せます。確認なしで許可する範囲は「環境設定 > AI」で設定します。",
|
||||
"ui.agent_chat_key_in_env": "キー設定済み",
|
||||
@@ -1251,8 +1558,10 @@
|
||||
"ui.ai_add": "追加",
|
||||
"ui.ai_add_agent": "エージェントを追加…",
|
||||
"ui.ai_add_agent_title": "エージェントを追加",
|
||||
"ui.ai_add_anthropic": "Anthropic 互換 API",
|
||||
"ui.ai_add_api": "OpenAI 互換 API",
|
||||
"ui.ai_add_custom": "カスタムコマンド",
|
||||
"ui.ai_add_endpoint": "API エンドポイント",
|
||||
"ui.ai_add_installed": "インストール済みエージェント",
|
||||
"ui.ai_add_installed_note": "インストール済みエージェントの独自エントリ(例: 別の引数で起動)。インストール済みのエージェントは自動で一覧に表示されます。",
|
||||
"ui.ai_added": "追加済み",
|
||||
@@ -1281,6 +1590,7 @@
|
||||
"ui.ai_key_needs_https": "キーは HTTPS 経由、またはこのコンピューター宛てにのみ送信されます。",
|
||||
"ui.ai_key_note": "システムのキーチェーンに保存され、SolidCraft の設定には保存されません。ローカルサーバーの場合は空欄のままにします。",
|
||||
"ui.ai_keychain_error": "システムのキーチェーンが拒否しました: {error}",
|
||||
"ui.ai_kind": "種類",
|
||||
"ui.ai_mode_all": "すべて許可",
|
||||
"ui.ai_mode_all_note": "確認なしで実行します。クラッシュレポートは引き続き対象外で、フォルダー制限も有効です。",
|
||||
"ui.ai_mode_all_warning": "エージェントは確認なしでデザインの保存・上書き・エクスポート・置き換えができます。信頼できるエージェントで、保存済みのデザインに対してのみ使ってください。",
|
||||
@@ -1302,6 +1612,7 @@
|
||||
"ui.ai_panel": "パネル",
|
||||
"ui.ai_permissions": "権限",
|
||||
"ui.ai_preset": "プリセット",
|
||||
"ui.ai_protocol": "プロトコル",
|
||||
"ui.ai_remove": "削除",
|
||||
"ui.ai_request_limit": "1メッセージあたり24リクエストで停止しました。続けるには、もう一度メッセージを送ってください",
|
||||
"ui.ai_reset": "リセット",
|
||||
@@ -1312,6 +1623,8 @@
|
||||
"ui.ai_test": "テスト",
|
||||
"ui.ai_test_acp_ok": "応答あり: {agent}",
|
||||
"ui.ai_test_acp_tip": "エージェントを短時間起動してセッションを要求し、終了します",
|
||||
"ui.ai_test_answered": "接続成功: {model} が応答しました",
|
||||
"ui.ai_test_anthropic_tip": "エンドポイントに短いメッセージを送信します",
|
||||
"ui.ai_test_api_tip": "エンドポイントにモデル一覧を問い合わせます",
|
||||
"ui.ai_test_no_model": "接続成功: {count} 個のモデル({model} はありません)",
|
||||
"ui.ai_test_ok": "接続成功: {count} 個のモデル",
|
||||
@@ -1469,6 +1782,9 @@
|
||||
"ui.export_keymap_tip": "ショートカット、Sボックスのお気に入り、ツールバー設定をファイルに保存",
|
||||
"ui.extend": "延長",
|
||||
"ui.extent_type": "範囲タイプ",
|
||||
"ui.extract_component": "ファイルに抽出…",
|
||||
"ui.extract_design_changed": "ファイルの選択中にデザインが変更されました。ファイルへの抽出をやり直してください。",
|
||||
"ui.extract_done": "コンポーネントは独立したデザインになり、ここにリンクされました。",
|
||||
"ui.extrude": "押し出し",
|
||||
"ui.factor": "倍率",
|
||||
"ui.feature_history": "フィーチャー履歴",
|
||||
@@ -1500,6 +1816,7 @@
|
||||
"ui.grid_follows_view_hint": "側面ビューでは、グリッドを地面ではなく、そのビューに向いた垂直平面に表示します",
|
||||
"ui.ground_shadow": "地面の影",
|
||||
"ui.groupSelected": "選択をグループ化",
|
||||
"ui.hatch": "ハッチング",
|
||||
"ui.heading_add_ins": "アドイン",
|
||||
"ui.heading_assemble": "アセンブリ",
|
||||
"ui.heading_box": "直方体",
|
||||
@@ -1607,6 +1924,10 @@
|
||||
"ui.length_decimals": "長さの小数点以下の桁数",
|
||||
"ui.light": "ライト",
|
||||
"ui.line": "線分",
|
||||
"ui.link_copies": "同一のコピーをリンク",
|
||||
"ui.link_copies_keep": "そのまま",
|
||||
"ui.link_copies_link": "リンク",
|
||||
"ui.link_copies_text": "これらのコンポーネントは {name} と同じボディを作成します。そのリンクの配置にしますか?",
|
||||
"ui.lock": "ロック",
|
||||
"ui.loft": "ロフト",
|
||||
"ui.look_at": "正面視",
|
||||
@@ -1730,6 +2051,7 @@
|
||||
"ui.render": "レンダリング",
|
||||
"ui.renderer": "レンダラー",
|
||||
"ui.repeat": "繰り返し",
|
||||
"ui.replace_with_link_done": "コピーはリンクされたデザインの配置になりました。",
|
||||
"ui.report_a_bug": "不具合を報告",
|
||||
"ui.reset": "リセット",
|
||||
"ui.reset_all": "すべてリセット",
|
||||
|
||||
@@ -859,19 +859,57 @@
|
||||
"help.mit": "MIT 라이선스",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "타사 라이선스",
|
||||
"home.back": "{name}(으)로 돌아가기",
|
||||
"home.crashes": "충돌 보고서",
|
||||
"home.design_workspace": "설계",
|
||||
"home.documentation": "문서",
|
||||
"home.export": "내보내기",
|
||||
"home.export_hint": "현재 설계의 형식을 선택하세요.",
|
||||
"home.export_json_hint": "편집 가능한 설계 및 명령 기록",
|
||||
"home.export_mesh_hint": "인쇄 또는 공유용 메시",
|
||||
"home.export_step_hint": "정밀한 솔리드 형상",
|
||||
"home.import": "가져오기…",
|
||||
"home.import_hint": "STEP, IGES 또는 메시를 새 설계로 가져오기",
|
||||
"home.in_this_browser": "이 브라우저에 저장됨",
|
||||
"home.missing": "찾을 수 없음",
|
||||
"home.month_01": "1월",
|
||||
"home.month_02": "2월",
|
||||
"home.month_03": "3월",
|
||||
"home.month_04": "4월",
|
||||
"home.month_05": "5월",
|
||||
"home.month_06": "6월",
|
||||
"home.month_07": "7월",
|
||||
"home.month_08": "8월",
|
||||
"home.month_09": "9월",
|
||||
"home.month_10": "10월",
|
||||
"home.month_11": "11월",
|
||||
"home.month_12": "12월",
|
||||
"home.new_inches": "새 설계(인치)",
|
||||
"home.new_inches_hint": "인치 · 설계 작업 공간에서 열기",
|
||||
"home.new_mm": "새 설계",
|
||||
"home.new_mm_hint": "밀리미터 · 설계 작업 공간에서 열기",
|
||||
"home.open": "열기…",
|
||||
"home.opened_days_ago": "{count}일 전에 열림",
|
||||
"home.opened_hours_ago": "{count}시간 전에 열림",
|
||||
"home.opened_just_now": "방금 열림",
|
||||
"home.opened_minutes_ago": "{count}분 전에 열림",
|
||||
"home.open_hint": ".solidcraft 설계 ({shortcut})",
|
||||
"home.open_now": "지금 열기",
|
||||
"home.opened_days_ago": "{count}일 전",
|
||||
"home.opened_hours_ago": "{count}시간 전",
|
||||
"home.opened_just_now": "방금",
|
||||
"home.opened_minutes_ago": "{count}분 전",
|
||||
"home.opening": "여는 중…",
|
||||
"home.properties": "설계 속성",
|
||||
"home.recent": "최근 항목",
|
||||
"home.recent_empty": "열거나 저장한 설계가 여기에 표시됩니다.",
|
||||
"home.recovery_count": "복구 가능한 설계: {count}개",
|
||||
"home.recovery_empty": "복구 가능한 설계가 없습니다.",
|
||||
"home.recovery_hint": "이 설계가 저장되기 전에 SolidCraft가 종료되었습니다.",
|
||||
"home.report_issue": "문제 신고",
|
||||
"home.samples": "샘플",
|
||||
"home.settings": "설정",
|
||||
"home.short_date": "{month} {day}일",
|
||||
"home.shortcuts": "키보드 단축키",
|
||||
"home.start_hint": "설계를 만들거나, 최근 파일을 열거나, 예제를 살펴보세요.",
|
||||
"home.whats_new": "새로운 기능",
|
||||
"home.yesterday": "어제",
|
||||
"install.button_tip": "SolidCraft를 앱으로 설치합니다. 별도 창에서 열리며 오프라인에서도 작동합니다",
|
||||
"install.dock_1": "파일 › Dock에 추가를 선택합니다.",
|
||||
"install.dock_2": "추가를 클릭합니다.",
|
||||
@@ -1026,7 +1064,7 @@
|
||||
"search.try": "명령 이름, 이 디자인의 피처 또는 매개변수를 입력하세요. 값과 함께 명령을 실행하려면 명령 ID와 JSON 객체를 입력하세요: {example}",
|
||||
"search.valid": "올바름",
|
||||
"settings.display_quality_note": "세밀할수록 곡면과 모서리가 더 매끄럽게 표시되지만 메시 생성 시간이 늘어납니다. 뷰포트만 바뀌며 설계나 내보내기는 바뀌지 않습니다.",
|
||||
"settings.frame_rate_note": "뷰의 오른쪽 아래에 초당 프레임 수를 표시합니다. 켜져 있는 동안 뷰를 계속 다시 그립니다.",
|
||||
"settings.frame_rate_note": "뷰의 오른쪽 아래에 초당 프레임 수를 표시합니다.",
|
||||
"settings.measure_axis_distances": "X/Y/Z 거리 표시",
|
||||
"settings.measure_axis_distances_help": "측정 대화상자가 열려 있는 동안 전체 거리와 함께 월드 축 방향 치수와 거리를 표시합니다.",
|
||||
"settings.msaa_applies_on_restart": "안티앨리어싱은 다음에 SolidCraft를 시작할 때 적용됩니다",
|
||||
@@ -1149,6 +1187,7 @@
|
||||
"ui.agent_chat_files_left_out": "메시지 하나에는 파일을 최대 {max}개까지 보낼 수 있습니다. 제외됨: {files}",
|
||||
"ui.agent_chat_files_loading": "파일을 읽는 동안 기다리는 중",
|
||||
"ui.agent_chat_files_max": "메시지 하나에 파일은 최대 {max}개",
|
||||
"ui.agent_chat_flatpak_none_found": "코딩 에이전트를 찾지 못했습니다. Flatpak 샌드박스는 기본적으로 홈 폴더에 설치된 에이전트를 볼 수 없고 네트워크에도 접근할 수 없습니다. nvm 또는 ~/.local/bin에 설치한 Claude Code와 ACP 어댑터를 허용하려면 터미널에서 다음 명령을 실행한 뒤 SolidCraft를 다시 시작하세요. 나열된 프로그램과 로그인 파일에 대한 접근 권한 및 네트워크 접근 권한을 부여합니다:",
|
||||
"ui.agent_chat_import_failed": "{name}: 가져오기 실패: {error}",
|
||||
"ui.agent_chat_intro": "이미 사용 중인 코딩 에이전트나 선택한 모델 엔드포인트와 채팅하세요. SolidCraft 도구로 열린 디자인을 다루며, 변경 사항은 바로 보이고 메시지마다 한 번에 실행 취소할 수 있습니다. 묻지 않고 할 수 있는 일은 환경 설정 > AI에서 정합니다.",
|
||||
"ui.agent_chat_key_in_env": "키 설정됨",
|
||||
@@ -1468,6 +1507,7 @@
|
||||
"ui.grid_follows_view_hint": "측면 뷰에서는 지면 대신 마주 보는 수직 평면에 격자를 표시합니다",
|
||||
"ui.ground_shadow": "바닥 그림자",
|
||||
"ui.groupSelected": "선택 항목 그룹화",
|
||||
"ui.hatch": "해칭",
|
||||
"ui.heading_add_ins": "애드인",
|
||||
"ui.heading_assemble": "조립",
|
||||
"ui.heading_box": "박스",
|
||||
|
||||
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Lesen MIT",
|
||||
"help.notice": "NOTIS",
|
||||
"help.third_party": "Lesen pihak ketiga",
|
||||
"home.back": "Kembali ke {name}",
|
||||
"home.crashes": "Laporan ranap",
|
||||
"home.design_workspace": "Reka Bentuk",
|
||||
"home.documentation": "Dokumentasi",
|
||||
"home.export": "Eksport",
|
||||
"home.export_hint": "Pilih format untuk reka bentuk semasa.",
|
||||
"home.export_json_hint": "Reka bentuk boleh sunting dan sejarah perintah",
|
||||
"home.export_mesh_hint": "Jejaring untuk pencetakan atau perkongsian",
|
||||
"home.export_step_hint": "Geometri pepejal yang tepat",
|
||||
"home.import": "Import…",
|
||||
"home.import_hint": "STEP, IGES atau jejaring ke dalam reka bentuk baharu",
|
||||
"home.in_this_browser": "dalam pelayar ini",
|
||||
"home.missing": "tiada",
|
||||
"home.month_01": "Jan",
|
||||
"home.month_02": "Feb",
|
||||
"home.month_03": "Mac",
|
||||
"home.month_04": "Apr",
|
||||
"home.month_05": "Mei",
|
||||
"home.month_06": "Jun",
|
||||
"home.month_07": "Jul",
|
||||
"home.month_08": "Ogo",
|
||||
"home.month_09": "Sep",
|
||||
"home.month_10": "Okt",
|
||||
"home.month_11": "Nov",
|
||||
"home.month_12": "Dis",
|
||||
"home.new_inches": "Reka bentuk baharu dalam inci",
|
||||
"home.new_inches_hint": "Inci · dibuka dalam Reka Bentuk",
|
||||
"home.new_mm": "Reka bentuk baharu",
|
||||
"home.new_mm_hint": "Milimeter · dibuka dalam Reka Bentuk",
|
||||
"home.open": "Buka…",
|
||||
"home.opened_days_ago": "Dibuka {count} hari lalu",
|
||||
"home.opened_hours_ago": "Dibuka {count} jam lalu",
|
||||
"home.opened_just_now": "Baru dibuka",
|
||||
"home.opened_minutes_ago": "Dibuka {count} minit lalu",
|
||||
"home.open_hint": "Reka bentuk .solidcraft ({shortcut})",
|
||||
"home.open_now": "Buka sekarang",
|
||||
"home.opened_days_ago": "{count} hari lalu",
|
||||
"home.opened_hours_ago": "{count} jam lalu",
|
||||
"home.opened_just_now": "Baru sahaja",
|
||||
"home.opened_minutes_ago": "{count} minit lalu",
|
||||
"home.opening": "membuka…",
|
||||
"home.properties": "Sifat reka bentuk",
|
||||
"home.recent": "Terkini",
|
||||
"home.recent_empty": "Reka bentuk yang anda buka atau simpan dipaparkan di sini.",
|
||||
"home.recovery_count": "Reka bentuk boleh dipulihkan: {count}",
|
||||
"home.recovery_empty": "Tiada reka bentuk yang boleh dipulihkan.",
|
||||
"home.recovery_hint": "SolidCraft ditutup sebelum reka bentuk ini disimpan.",
|
||||
"home.report_issue": "Laporkan Isu",
|
||||
"home.samples": "Contoh",
|
||||
"home.settings": "Tetapan",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Pintasan papan kekunci",
|
||||
"home.start_hint": "Cipta reka bentuk, buka fail terkini atau terokai contoh.",
|
||||
"home.whats_new": "Apa yang baharu",
|
||||
"home.yesterday": "Semalam",
|
||||
"install.button_tip": "Pasang SolidCraft: tetingkap sendiri dan boleh guna di luar talian",
|
||||
"install.dock_1": "Pilih File › Add to Dock.",
|
||||
"install.dock_2": "Klik Add.",
|
||||
@@ -1022,7 +1060,7 @@
|
||||
"search.try": "Taip nama arahan, ciri dalam reka bentuk ini, atau parameter. Untuk menjalankan arahan dengan nilai, taip ID-nya dan objek JSON: {example}",
|
||||
"search.valid": "Sah",
|
||||
"settings.display_quality_note": "Tahap halus melicinkan muka dan tepi melengkung, tetapi mesh lebih lambat disiapkan. Hanya paparan berubah, bukan reka bentuk atau eksport.",
|
||||
"settings.frame_rate_note": "Papar bingkai sesaat di kanan bawah. Jika diaktifkan, pandangan dilukis semula tanpa henti.",
|
||||
"settings.frame_rate_note": "Papar bingkai sesaat di kanan bawah.",
|
||||
"settings.measure_axis_distances": "Papar jarak X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Papar jarak paksi dunia X/Y/Z bersama jarak keseluruhan semasa Ukur dibuka.",
|
||||
"settings.msaa_applies_on_restart": "Antialias digunakan apabila SolidCraft dimulakan semula",
|
||||
@@ -1145,6 +1183,7 @@
|
||||
"ui.agent_chat_files_left_out": "Paling banyak {max} fail bersama satu mesej; ditinggalkan: {files}",
|
||||
"ui.agent_chat_files_loading": "Menunggu fail dibaca",
|
||||
"ui.agent_chat_files_max": "Paling banyak {max} fail pada satu mesej",
|
||||
"ui.agent_chat_flatpak_none_found": "Tiada ejen pengekodan. Secara lalai, kotak pasir Flatpak tidak dapat melihat ejen yang dipasang dalam folder rumah anda dan tiada akses rangkaian. Untuk membenarkan Claude Code dan penyesuai ACP-nya yang dipasang melalui nvm atau dalam ~/.local/bin, jalankan arahan ini dalam terminal, kemudian mulakan semula SolidCraft. Ia memberi akses kepada program dan fail log masuk yang disenaraikan, serta akses rangkaian:",
|
||||
"ui.agent_chat_import_failed": "{name}: import gagal: {error}",
|
||||
"ui.agent_chat_intro": "Sembang dengan ejen pengekodan biasa anda atau titik akhir model pilihan. Ejen mengubah reka bentuk melalui alat SolidCraft; perubahan dipaparkan terus dan boleh dibatalkan sekali bagi setiap mesej. Tetapkan kebenaran di Tetapan > AI.",
|
||||
"ui.agent_chat_key_in_env": "kunci tersedia",
|
||||
@@ -1464,6 +1503,7 @@
|
||||
"ui.grid_follows_view_hint": "Pandangan sisi guna grid satah tegak yang menghadapnya, bukan satah tanah",
|
||||
"ui.ground_shadow": "Bayang tanah",
|
||||
"ui.groupSelected": "Kumpulkan Pilihan",
|
||||
"ui.hatch": "Lorek",
|
||||
"ui.heading_add_ins": "TAMBAHAN",
|
||||
"ui.heading_assemble": "PASANG",
|
||||
"ui.heading_box": "KOTAK",
|
||||
|
||||
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT-licentie",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenties van derden",
|
||||
"home.back": "Terug naar {name}",
|
||||
"home.crashes": "Crashrapporten",
|
||||
"home.design_workspace": "Ontwerp",
|
||||
"home.documentation": "Documentatie",
|
||||
"home.export": "Exporteren",
|
||||
"home.export_hint": "Kies een indeling voor het huidige ontwerp.",
|
||||
"home.export_json_hint": "Bewerkbaar ontwerp en opdrachtgeschiedenis",
|
||||
"home.export_mesh_hint": "Mesh om af te drukken of te delen",
|
||||
"home.export_step_hint": "Nauwkeurige volumegeometrie",
|
||||
"home.import": "Importeren…",
|
||||
"home.import_hint": "STEP, IGES of een mesh in een nieuw ontwerp",
|
||||
"home.in_this_browser": "in deze browser",
|
||||
"home.missing": "ontbreekt",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mrt.",
|
||||
"home.month_04": "apr.",
|
||||
"home.month_05": "mei",
|
||||
"home.month_06": "jun.",
|
||||
"home.month_07": "jul.",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "sep.",
|
||||
"home.month_10": "okt.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Nieuw ontwerp in inches",
|
||||
"home.new_inches_hint": "Inches · opent in Ontwerp",
|
||||
"home.new_mm": "Nieuw ontwerp",
|
||||
"home.new_mm_hint": "Millimeters · opent in Ontwerp",
|
||||
"home.open": "Openen…",
|
||||
"home.opened_days_ago": "{count} dagen geleden geopend",
|
||||
"home.opened_hours_ago": "{count} u geleden geopend",
|
||||
"home.opened_just_now": "Zojuist geopend",
|
||||
"home.opened_minutes_ago": "{count} min geleden geopend",
|
||||
"home.open_hint": "Een .solidcraft-ontwerp ({shortcut})",
|
||||
"home.open_now": "Momenteel geopend",
|
||||
"home.opened_days_ago": "{count} dagen geleden",
|
||||
"home.opened_hours_ago": "{count} u geleden",
|
||||
"home.opened_just_now": "Zojuist",
|
||||
"home.opened_minutes_ago": "{count} min geleden",
|
||||
"home.opening": "openen…",
|
||||
"home.properties": "Ontwerpeigenschappen",
|
||||
"home.recent": "Recent",
|
||||
"home.recent_empty": "Ontwerpen die u opent of opslaat, verschijnen hier.",
|
||||
"home.recovery_count": "Herstelbare ontwerpen: {count}",
|
||||
"home.recovery_empty": "Geen herstelbare ontwerpen.",
|
||||
"home.recovery_hint": "SolidCraft is gesloten voordat deze ontwerpen werden opgeslagen.",
|
||||
"home.report_issue": "Probleem melden",
|
||||
"home.samples": "Voorbeelden",
|
||||
"home.settings": "Instellingen",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Sneltoetsen",
|
||||
"home.start_hint": "Maak een ontwerp, open een recent bestand of bekijk een voorbeeld.",
|
||||
"home.whats_new": "Wat is er nieuw",
|
||||
"home.yesterday": "Gisteren",
|
||||
"install.button_tip": "Installeer SolidCraft als app: deze opent in een eigen venster en werkt offline",
|
||||
"install.dock_1": "Kies Bestand › Toevoegen aan Dock.",
|
||||
"install.dock_2": "Klik op Voeg toe.",
|
||||
@@ -1021,7 +1059,7 @@
|
||||
"search.try": "Typ een opdrachtnaam, een kenmerk in dit ontwerp of een parameter. Om een opdracht met waarden uit te voeren, typt u de id en een JSON-object: {example}",
|
||||
"search.valid": "Geldig",
|
||||
"settings.display_quality_note": "Fijnere niveaus tekenen gebogen vlakken en randen vloeiender en kosten meer tijd om te meshen. Alleen de viewport verandert, nooit het ontwerp of de exports.",
|
||||
"settings.frame_rate_note": "Toont frames per seconde in de rechteronderhoek van de weergave. Zolang dit aan staat, wordt de weergave continu opnieuw getekend.",
|
||||
"settings.frame_rate_note": "Toont frames per seconde in de rechteronderhoek van de weergave.",
|
||||
"settings.measure_axis_distances": "X/Y/Z-afstanden tonen",
|
||||
"settings.measure_axis_distances_help": "Toon de componentmaten langs de wereldassen naast de totale afstand terwijl Meten open is.",
|
||||
"settings.msaa_applies_on_restart": "Anti-aliasing wordt toegepast bij de volgende start van SolidCraft",
|
||||
@@ -1144,6 +1182,7 @@
|
||||
"ui.agent_chat_files_left_out": "Maximaal {max} bestanden per bericht; weggelaten: {files}",
|
||||
"ui.agent_chat_files_loading": "Wachten tot de bestanden zijn gelezen",
|
||||
"ui.agent_chat_files_max": "Maximaal {max} bestanden per bericht",
|
||||
"ui.agent_chat_flatpak_none_found": "Geen codeeragent gevonden. De Flatpak-sandbox ziet standaard geen agents die in je persoonlijke map zijn geïnstalleerd en heeft geen netwerktoegang. Voer dit commando uit in een terminal en start SolidCraft daarna opnieuw om Claude Code en de ACP-adapter toe te staan die via nvm of in ~/.local/bin zijn geïnstalleerd. Het geeft toegang tot de genoemde programma’s en aanmeldbestanden, plus netwerktoegang:",
|
||||
"ui.agent_chat_import_failed": "{name}: importeren mislukt: {error}",
|
||||
"ui.agent_chat_intro": "Chat met een codeeragent die u al gebruikt, of met een modeleindpunt naar keuze. De agent werkt aan het geopende ontwerp via de tools van SolidCraft: de wijzigingen verschijnen terwijl ze worden gemaakt en worden per bericht als één stap ongedaan gemaakt. Wat hij zonder te vragen mag doen, stelt u in via Voorkeuren > AI.",
|
||||
"ui.agent_chat_key_in_env": "sleutel ingesteld",
|
||||
@@ -1463,6 +1502,7 @@
|
||||
"ui.grid_follows_view_hint": "Zijaanzichten tonen het raster op het rechtopstaande vlak dat naar hen toe gericht is in plaats van op de grond",
|
||||
"ui.ground_shadow": "Grondschaduw",
|
||||
"ui.groupSelected": "Selectie groeperen",
|
||||
"ui.hatch": "Arcering",
|
||||
"ui.heading_add_ins": "INVOEGMODULES",
|
||||
"ui.heading_assemble": "SAMENSTELLEN",
|
||||
"ui.heading_box": "KUB",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"Also deletes": "Usuwa również",
|
||||
"Angle 2": "Kąt 2",
|
||||
"Animate": "Animuj",
|
||||
"App/system information and command settings are included. Review private details before sharing.": "Dołączone są informacje o aplikacji i systemie oraz ustawienia poleceń. Przed udostępnieniem sprawdź, czy nie ma tam prywatnych danych.",
|
||||
"Appearance name": "Nazwa wyglądu",
|
||||
"Applied Radius": "Zastosowany promień",
|
||||
"Apply To": "Zastosuj do",
|
||||
@@ -83,6 +84,7 @@
|
||||
"Contact": "Kontakt",
|
||||
"Contact Sets": "Zestawy kontaktów",
|
||||
"Contact sets": "Zestawy kontaktów",
|
||||
"Continue on GitHub": "Kontynuuj na GitHubie",
|
||||
"Continuity": "Ciągłość",
|
||||
"Copy Error Message": "Kopiuj komunikat błędu",
|
||||
"Copy Warning Message": "Kopiuj komunikat ostrzeżenia",
|
||||
@@ -102,6 +104,7 @@
|
||||
"Curv. Comb": "Grzebień krzyw.",
|
||||
"Curvature (G2)": "Krzywizna (G2)",
|
||||
"Curvature change (G3)": "Zmiana krzywizny (G3)",
|
||||
"Custom": "Niestandardowy",
|
||||
"Customize Toolbar": "Dostosuj pasek narzędzi",
|
||||
"Delete": "Usuń",
|
||||
"Delete all": "Usuń wszystko",
|
||||
@@ -110,6 +113,8 @@
|
||||
"Delete the set": "Usuń zestaw",
|
||||
"Density": "Gęstość",
|
||||
"Dependents": "Elementy zależne",
|
||||
"Describe the problem you saw": "Opisz zauważony problem",
|
||||
"Describe what happened before continuing.": "Zanim przejdziesz dalej, opisz, co się stało.",
|
||||
"Designs you open or save appear here.": "Tutaj pojawiają się otwierane lub zapisywane projekty.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Wynika z opcji Do obiektu. Wybierz Kąt, aby edytować zakres kątowy.",
|
||||
"Direction": "Kierunek",
|
||||
@@ -127,6 +132,7 @@
|
||||
"Enabled": "Włączone",
|
||||
"End bulge": "Wybrzuszenie końca",
|
||||
"End continuity": "Ciągłość końca",
|
||||
"Expected result (optional)": "Oczekiwany wynik (opcjonalnie)",
|
||||
"Explode": "Rozstrzel",
|
||||
"Explode Text": "Rozbij tekst",
|
||||
"Export Animation…": "Eksportuj animację…",
|
||||
@@ -168,6 +174,7 @@
|
||||
"Height": "Wysokość",
|
||||
"Hidden edges": "Krawędzie niewidoczne",
|
||||
"History layout": "Układ historii",
|
||||
"History names": "Nazwy w historii",
|
||||
"Holes": "Otwory",
|
||||
"Holes (edges on them)": "Otwory (krawędzie na nich)",
|
||||
"Home view": "Widok główny",
|
||||
@@ -234,6 +241,7 @@
|
||||
"No matching appearance presets.": "Brak pasujących ustawień wyglądu.",
|
||||
"No matching appearances": "Brak pasujących wyglądów",
|
||||
"No visible bodies or sketches to export.": "Brak widocznych brył lub szkiców do wyeksportowania.",
|
||||
"Nominal diameter": "Średnica nominalna",
|
||||
"None": "Brak",
|
||||
"Not available yet": "Jeszcze niedostępne",
|
||||
"Notch": "Wycięcie",
|
||||
@@ -242,7 +250,10 @@
|
||||
"Open GitHub draft": "Otwórz szkic zgłoszenia na GitHubie",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "Otwórz biblioteki lokalne w aplikacji komputerowej. Tutaj możesz wyeksportować bibliotekę tego projektu.",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Otwartoźródłowy parametryczny CAD 3D w języku Rust. Licencja MIT OR Apache-2.0.",
|
||||
"Opens your browser and copies the report text for you to paste into GitHub.": "Otwiera przeglądarkę internetową i kopiuje treść raportu, którą wkleisz na GitHubie.",
|
||||
"Opens your browser. Review and submit your report on GitHub.": "Otwiera przeglądarkę internetową. Sprawdź raport i wyślij go na GitHubie.",
|
||||
"Operator": "Operator",
|
||||
"Orbit (right drag)": "Obracanie widoku (przeciąganie prawym przyciskiem)",
|
||||
"Other side": "Druga strona",
|
||||
"Other surface (mutual trim)": "Druga powierzchnia (wzajemne przycięcie)",
|
||||
"Outside": "Na zewnątrz",
|
||||
@@ -257,6 +268,7 @@
|
||||
"Pick the tool bodies": "Wybierz bryły narzędziowe",
|
||||
"Picked": "Wybrane",
|
||||
"Picked point": "Wybrany punkt",
|
||||
"Pitch": "Skok",
|
||||
"Place at the origin instead": "Zamiast tego umieść w początku układu",
|
||||
"Plastic Rules": "Reguły tworzyw",
|
||||
"Playback": "Odtwarzanie",
|
||||
@@ -290,6 +302,8 @@
|
||||
"Remove the track": "Usuń ścieżkę",
|
||||
"Remove this point": "Usuń ten punkt",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Zgłoś błąd — sprawdź zrzut ekranu i otwórz szkic zgłoszenia na GitHubie",
|
||||
"Report text copied. Paste it into GitHub and submit your report there.": "Skopiowano treść raportu. Wklej ją na GitHubie i tam wyślij raport.",
|
||||
"Reports are public. A GitHub account is required.": "Raporty są publiczne. Wymagane jest konto GitHub.",
|
||||
"Reset Toolbar": "Przywróć pasek narzędzi",
|
||||
"Resize Part": "Zmień rozmiar części",
|
||||
"Restore View": "Przywróć widok",
|
||||
@@ -304,6 +318,7 @@
|
||||
"Revert Pos.": "Przywróć poł.",
|
||||
"Revert Position": "Przywróć położenie",
|
||||
"Review the report and screenshot before opening GitHub.": "Przed otwarciem GitHuba sprawdź raport i zrzut ekranu.",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "Sprawdź raport na GitHubie i kliknij Submit new issue, aby go opublikować.",
|
||||
"Rim Edge": "Krawędź obrzeża",
|
||||
"Roll History Marker Here": "Przesuń tutaj znacznik historii",
|
||||
"Round": "Okrągłe",
|
||||
@@ -314,10 +329,12 @@
|
||||
"SURFACE QUALITY": "JAKOŚĆ POWIERZCHNI",
|
||||
"Save Rule": "Zapisz regułę",
|
||||
"Save View": "Zapisz widok",
|
||||
"Save screenshot…": "Zapisz zrzut ekranu…",
|
||||
"Save the study as an animated GIF, seen as the viewport shows it": "Zapisz badanie jako animowany GIF, tak jak widać je w widoku",
|
||||
"Saved preview. Geometry is being checked before editing.": "Zapisany podgląd. Geometria jest sprawdzana przed edycją.",
|
||||
"Scale factor": "Współczynnik skali",
|
||||
"Scene": "Scena",
|
||||
"Screenshot (optional)": "Zrzut ekranu (opcjonalnie)",
|
||||
"Search": "Szukaj",
|
||||
"Search commands": "Szukaj poleceń",
|
||||
"Search this library": "Przeszukaj tę bibliotekę",
|
||||
@@ -352,17 +369,21 @@
|
||||
"Start continuity": "Ciągłość początku",
|
||||
"Step": "Krok",
|
||||
"Steps": "Kroki",
|
||||
"Steps to reproduce (optional)": "Kroki do odtworzenia (opcjonalnie)",
|
||||
"Stitch the kept pieces": "Zszyj zachowane części",
|
||||
"Stitch to both bodies": "Zszyj z obiema bryłami",
|
||||
"Study": "Badanie",
|
||||
"Style": "Styl",
|
||||
"Summary": "Podsumowanie",
|
||||
"Suppressed": "Wygaszone",
|
||||
"Surface": "Powierzchnia",
|
||||
"Swap Ends": "Zamień końce",
|
||||
"TRIM": "PRZYCINANIE",
|
||||
"Tangent (G1)": "Styczność (G1)",
|
||||
"Tangent Chain": "Łańcuch styczny",
|
||||
"Tap drill diameter": "Średnica wiertła pod gwint",
|
||||
"Tear": "Rozdarcie",
|
||||
"Technical details": "Szczegóły techniczne",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Przerywany szkic to zapisana płaszczyzna pomocnicza. Wybierz nową ścianę, aby naprawić zaczepienie.",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Brak zapisanego wystąpienia autora. Przed ponownym powiązaniem wybierz prawidłowe położenie autora.",
|
||||
"Then": "Wtedy",
|
||||
@@ -374,6 +395,7 @@
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "Ten element podrzędny jest współdzielony. Usunięcie go usuwa go z każdej kopii elementu nadrzędnego.",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "Ten element podrzędny jest współdzielony. Zmiany położenia dotyczą każdej kopii elementu nadrzędnego.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Ta ściana nie zapewnia trwałego zaczepienia. Wybierz inną ścianę lub jawnie użyj stałej płaszczyzny.",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Ten obraz nie jest jeszcze dołączony na GitHubie. Skopiuj go lub zapisz, a potem dołącz na GitHubie.",
|
||||
"To Offset": "Do odsunięcia",
|
||||
"To edges": "Do krawędzi",
|
||||
"Tolerance": "Tolerancja",
|
||||
@@ -399,6 +421,9 @@
|
||||
"Wall": "Ścianka",
|
||||
"Wall Location": "Położenie ścianki",
|
||||
"Wall Thickness": "Grubość ścianki",
|
||||
"What did you do just before the problem? A short list is enough": "Co robiłeś tuż przed wystąpieniem problemu? Wystarczy krótka lista",
|
||||
"What did you expect to see?": "Czego się spodziewałeś?",
|
||||
"What happened?": "Co się stało?",
|
||||
"When": "Gdy",
|
||||
"Width": "Szerokość",
|
||||
"Will fail": "Nie powiedzie się",
|
||||
@@ -409,6 +434,29 @@
|
||||
"Z angle": "Kąt Z",
|
||||
"Z distance": "Odległość Z",
|
||||
"Zebra": "Zebra",
|
||||
"ai.prefs.availability": "Sterowanie asystentem",
|
||||
"ai.prefs.background_commands": "Uruchamiaj polecenia agenta w tle",
|
||||
"ai.prefs.follow_camera": "Obracaj kamerę do szkiców",
|
||||
"ai.prefs.follow_camera_off": "Automatyczne podążanie kamery jest wyłączone.",
|
||||
"ai.prefs.follow_camera_on": "Kamera obraca się przodem do szkiców tworzonych przez agenta.",
|
||||
"ai.prefs.intro": "Agenty, uprawnienia i sposób pokazywania ich pracy.",
|
||||
"ai.prefs.make_default": "Używaj domyślnie",
|
||||
"ai.prefs.no_agents": "Brak agentów. Dodaj agenta lub odśwież, aby wyszukać zainstalowane agenty.",
|
||||
"ai.prefs.no_saved_approvals": "Brak zapisanych zgód.",
|
||||
"ai.prefs.override_all_warning": "Ten agent może zapisywać, nadpisywać, eksportować i zastępować projekt bez pytania.",
|
||||
"ai.prefs.permission_all": "Zezwalaj na wszystko",
|
||||
"ai.prefs.permission_all_summary": "Dostępne narzędzia SolidCraft działają bez pytania.",
|
||||
"ai.prefs.permission_ask": "Pytaj przed zmianami",
|
||||
"ai.prefs.permission_ask_summary": "Zatwierdzaj każdą zmianę projektu i każdą operację na plikach.",
|
||||
"ai.prefs.permission_edits": "Zezwalaj na edycję modelu",
|
||||
"ai.prefs.permission_edits_summary": "Edycja modelu działa bez pytania; operacje na plikach i nieodwracalne nadal wymagają pytania.",
|
||||
"ai.prefs.permission_scope": "Dotyczy tylko narzędzi SolidCraft. Powłoka agenta i edycja plików podlegają jego własnym uprawnieniom.",
|
||||
"ai.prefs.pointer_preview_hidden": "Wskaźnik ukryty",
|
||||
"ai.prefs.pointer_preview_speed": "Wskaźnik agenta · {speed}",
|
||||
"ai.prefs.pointer_speed": "Szybkość ruchu",
|
||||
"ai.prefs.privacy_note": "Rozmowę otrzymuje tylko wybrany przez Ciebie agent lub punkt końcowy. Klucze punktów końcowych są przechowywane w systemowym pęku kluczy.",
|
||||
"ai.prefs.show_pointer": "Pokaż wskaźnik agenta",
|
||||
"ai.prefs.turn_limit_hint": "Maksymalna liczba wywołań narzędzi zmieniających projekt, które asystent może wykonać dla jednej wiadomości.",
|
||||
"angle, height": "kąt, wysokość",
|
||||
"appearance.dielectric_ior": "Współczynnik załamania dielektryka (IOR)",
|
||||
"appearance.metallic": "Metaliczność",
|
||||
@@ -759,6 +807,7 @@
|
||||
"cmd.view_graphics_preset": "Ustawienie graficzne",
|
||||
"cmd.view_list": "Lista widoków nazwanych",
|
||||
"cmd.view_navigate": "Nawiguj widokiem",
|
||||
"cmd.view_normal_to": "Prostopadle do zaznaczenia",
|
||||
"cmd.view_rename": "Zmień nazwę widoku nazwanego",
|
||||
"cmd.view_save": "Nowy widok nazwany",
|
||||
"color.copy": "Kliknij, aby skopiować wartości koloru",
|
||||
@@ -856,20 +905,64 @@
|
||||
"help.licences": "Licencje",
|
||||
"help.mit": "Licencja MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.solid.chamfer": "Ścina zaznaczone krawędzie płaską fazą o zadanej odległości. Służy do łamania ostrych krawędzi lub ułatwienia wprowadzenia kołka czy śruby.",
|
||||
"help.solid.extrude": "Wyciąga profil szkicu w 3D na zadaną odległość, dodając lub usuwając materiał. Służy do zamiany płaskiego obrysu w blok, występ lub kieszeń.",
|
||||
"help.solid.fillet": "Zaokrągla zaznaczone krawędzie gładkim łukiem o zadanym promieniu. Służy do złagodzenia ostrych krawędzi dla wytrzymałości, wygody lub wyglądu.",
|
||||
"help.solid.hole": "Wierci okrągły otwór o zadanej średnicy w wybranym punkcie ściany. Służy do otworów przelotowych pod śruby, kołki i innych prostych okrągłych otworów.",
|
||||
"help.solid.revolve": "Obraca profil szkicu wokół osi, tworząc okrągłą bryłę. Służy do wałków, gałek, misek i wszystkich części okrągłych względem osi.",
|
||||
"help.solid.shell": "Wydrąża bryłę, pozostawiając ścianki o zadanej grubości, i może otworzyć wybrane ściany. Służy do pudełek, pokryw i innych części cienkościennych.",
|
||||
"help.third_party": "Licencje stron trzecich",
|
||||
"home.back": "Wróć do {name}",
|
||||
"home.crashes": "Raporty awarii",
|
||||
"home.design_workspace": "Projektowanie",
|
||||
"home.documentation": "Dokumentacja",
|
||||
"home.export": "Eksportuj",
|
||||
"home.export_hint": "Wybierz format dla bieżącego projektu.",
|
||||
"home.export_json_hint": "Edytowalny projekt i historia poleceń",
|
||||
"home.export_mesh_hint": "Siatka do druku lub udostępniania",
|
||||
"home.export_step_hint": "Precyzyjna geometria bryłowa",
|
||||
"home.import": "Importuj…",
|
||||
"home.import_hint": "STEP, IGES lub siatka do nowego projektu",
|
||||
"home.in_this_browser": "w tej przeglądarce",
|
||||
"home.missing": "brak",
|
||||
"home.month_01": "sty",
|
||||
"home.month_02": "lut",
|
||||
"home.month_03": "mar",
|
||||
"home.month_04": "kwi",
|
||||
"home.month_05": "maj",
|
||||
"home.month_06": "cze",
|
||||
"home.month_07": "lip",
|
||||
"home.month_08": "sie",
|
||||
"home.month_09": "wrz",
|
||||
"home.month_10": "paź",
|
||||
"home.month_11": "lis",
|
||||
"home.month_12": "gru",
|
||||
"home.new_inches": "Nowy projekt w calach",
|
||||
"home.new_inches_hint": "Cale · otwiera w obszarze Projektowanie",
|
||||
"home.new_mm": "Nowy projekt",
|
||||
"home.new_mm_hint": "Milimetry · otwiera w obszarze Projektowanie",
|
||||
"home.open": "Otwórz…",
|
||||
"home.opened_days_ago": "Otwarto {count} dni temu",
|
||||
"home.opened_hours_ago": "Otwarto {count} godz. temu",
|
||||
"home.opened_just_now": "Otwarto przed chwilą",
|
||||
"home.opened_minutes_ago": "Otwarto {count} min temu",
|
||||
"home.open_hint": "Projekt .solidcraft ({shortcut})",
|
||||
"home.open_now": "Otwórz teraz",
|
||||
"home.opened_days_ago": "{count} dni temu",
|
||||
"home.opened_hours_ago": "{count} godz. temu",
|
||||
"home.opened_just_now": "Przed chwilą",
|
||||
"home.opened_minutes_ago": "{count} min temu",
|
||||
"home.opening": "otwieranie…",
|
||||
"home.properties": "Właściwości projektu",
|
||||
"home.recent": "Ostatnie",
|
||||
"home.recent_empty": "Tutaj pojawią się projekty, które otworzysz lub zapiszesz.",
|
||||
"home.recovery_count": "Projekty do odzyskania: {count}",
|
||||
"home.recovery_empty": "Brak projektów do odzyskania.",
|
||||
"home.recovery_hint": "SolidCraft zamknął się przed zapisaniem tych projektów.",
|
||||
"home.report_issue": "Zgłoś problem",
|
||||
"home.samples": "Przykłady",
|
||||
"home.settings": "Ustawienia",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Skróty klawiszowe",
|
||||
"home.start_hint": "Utwórz projekt, otwórz ostatni plik lub obejrzyj przykład.",
|
||||
"home.whats_new": "Co nowego",
|
||||
"home.yesterday": "Wczoraj",
|
||||
"install.button_tip": "Zainstaluj SolidCraft jako aplikację: otwiera się we własnym oknie i działa offline",
|
||||
"install.dock_1": "Wybierz Plik › Dodaj do Docka.",
|
||||
"install.dock_2": "Kliknij Dodaj.",
|
||||
@@ -927,6 +1020,8 @@
|
||||
"motion.to_end": "Na koniec",
|
||||
"motion.to_start": "Na początek",
|
||||
"motion.track_name": "Ścieżka {number}",
|
||||
"nav.free_orbit.help": "Mysz i mysz 3D obracają widok przez oba bieguny i zachowują przechylenie. Przyciąganie do osi z Shift jest wyłączone.",
|
||||
"nav.free_orbit.label": "Swobodne obracanie",
|
||||
"nav.touchpad.help": "Przytrzymaj Shift i poruszaj wskaźnikiem, aby przesuwać; przytrzymaj Alt/Option i poruszaj nim, aby obracać widok. Przycisk myszy nie jest potrzebny. Przewijanie w pionie powiększa.",
|
||||
"nav.touchpad.label": "Touchpad",
|
||||
"nav.touchpad.orbit": "Obracanie widoku (przytrzymaj Alt/Option i poruszaj; bez przycisku myszy)",
|
||||
@@ -949,8 +1044,130 @@
|
||||
"pattern.flip_direction": "Odwróć kierunek",
|
||||
"pick a hole to seat it, or OK places it at the origin": "wybierz otwór, aby go osadzić, lub OK umieszcza go w początku układu",
|
||||
"pick one or two vertices, edges or faces": "wybierz jeden lub dwa wierzchołki, krawędzie lub ściany",
|
||||
"prefs.add_item": "Dodaj element",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option zwalnia również strzałki manipulatora",
|
||||
"prefs.anti_aliasing_note": "Wygładza poszarpane krawędzie.",
|
||||
"prefs.applies_on_restart": "Działa po ponownym uruchomieniu",
|
||||
"prefs.assistant": "Asystent",
|
||||
"prefs.axes": "Osie",
|
||||
"prefs.axis": "Oś",
|
||||
"prefs.bottom": "Dół",
|
||||
"prefs.browser": "Przeglądarka",
|
||||
"prefs.built_in": "Wbudowana",
|
||||
"prefs.button_left": "Przeciąganie lewym",
|
||||
"prefs.button_middle": "Przeciąganie środkowym",
|
||||
"prefs.button_right": "Przeciąganie prawym",
|
||||
"prefs.choose_font": "Wybierz plik czcionki…",
|
||||
"prefs.click": "Kliknięcie",
|
||||
"prefs.contact_shading": "Miękkie cieniowanie kontaktowe",
|
||||
"prefs.contact_shading_note": "Miękkie cieniowanie tam, gdzie części są blisko siebie.",
|
||||
"prefs.context_menu": "Menu kontekstowe",
|
||||
"prefs.context_menu_ring": "Menu kontekstowe · przytrzymaj, aby otworzyć menu promieniste",
|
||||
"prefs.cpu": "CPU (programowo)",
|
||||
"prefs.crash_reports": "Otwórz raporty o awariach…",
|
||||
"prefs.crash_reports_note": "Raporty są przechowywane na tym komputerze, razem z projektem i poleceniami, które odtwarzają problem.",
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "Powierzchnie zakrzywione",
|
||||
"prefs.curves": "Powierzchnie zakrzywione",
|
||||
"prefs.dead_zone_note": "Zakreskowany pas to strefa martwa: ruch wewnątrz niej jest pomijany.",
|
||||
"prefs.default": "domyślnie",
|
||||
"prefs.design": "Ustawienia domyślne projektu",
|
||||
"prefs.design_note": "Ustawienia, z którymi startują nowe projekty. Otwarte projekty zachowują własne.",
|
||||
"prefs.devices": "Mysz 3D",
|
||||
"prefs.display": "Wyświetlanie",
|
||||
"prefs.edges": "Krawędzie i cieniowanie",
|
||||
"prefs.extend": "Rozszerz",
|
||||
"prefs.extra_navigation": "Ctrl+przeciąganie lewym na pustym miejscu obraca widok; przytrzymaj Spację, aby przesuwać.",
|
||||
"prefs.follows_os": "Zgodnie z systemem",
|
||||
"prefs.font": "Czcionka",
|
||||
"prefs.font_fallback": "Znaki, których brakuje w wybranej czcionce, są brane z czcionki wbudowanej, więc żadna etykieta nie pokaże pustego prostokąta.",
|
||||
"prefs.font_sample": "Zażółć gęślą jaźń · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "Nie można odczytać wybranej czcionki.",
|
||||
"prefs.general": "Ogólne",
|
||||
"prefs.general_note": "Język, zapisywanie i aktualizacje.",
|
||||
"prefs.gesture_orbit": "Obracanie widoku",
|
||||
"prefs.gesture_pan": "Przesuwanie",
|
||||
"prefs.gesture_zoom": "Powiększanie",
|
||||
"prefs.graphics": "Grafika",
|
||||
"prefs.graphics_note": "Wybierz między szybkością a gładkością. Każda opcja pokazuje swój efekt na przykładzie.",
|
||||
"prefs.graphics_presets_note": "Ustawienia wstępne zmieniają razem poniższe opcje.",
|
||||
"prefs.history": "Historia",
|
||||
"prefs.hold_stop_snapping": "Przytrzymaj, aby wyłączyć przyciąganie",
|
||||
"prefs.icons_only": "Tylko ikony",
|
||||
"prefs.imported_file": "Zaimportowany plik",
|
||||
"prefs.input": "Wprowadzanie",
|
||||
"prefs.interface": "Interfejs",
|
||||
"prefs.interface_language": "Język interfejsu",
|
||||
"prefs.interface_note": "Wygląd SolidCraft i rozmieszczenie jego elementów.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.labels": "Etykiety",
|
||||
"prefs.layout": "Układ",
|
||||
"prefs.layout_note": "Kompaktowy dokuje panele do krawędzi okna. Przezroczystość przeglądarki jest dostępna w układzie Pływający. Stronę asystenta zmienisz na stronie Asystent.",
|
||||
"prefs.left": "Lewo",
|
||||
"prefs.left_click": "Kliknięcie lewym",
|
||||
"prefs.live_input": "Bieżące wejście",
|
||||
"prefs.live_preview": "Podgląd na żywo",
|
||||
"prefs.minutes": "minut",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "Mysz i gładzik",
|
||||
"prefs.mouse_heading": "Mysz",
|
||||
"prefs.mouse_mapping": "Przypisanie przycisków myszy",
|
||||
"prefs.navigation": "Nawigacja",
|
||||
"prefs.navigation_note": "Jak mysz, gładzik i mysz 3D poruszają widokiem.",
|
||||
"prefs.not_on_gpu": "niedostępne na tym GPU",
|
||||
"prefs.on": "Włączone",
|
||||
"prefs.opaque": "Nieprzezroczysta",
|
||||
"prefs.orbit_snap": "Przytrzymaj Shift podczas obracania widoku, aby przyciągnąć do osi.",
|
||||
"prefs.orientation": "Orientacja",
|
||||
"prefs.overall_speed": "Szybkość ogólna",
|
||||
"prefs.preview_choices": "Podgląd wybranych opcji",
|
||||
"prefs.quality_default": "domyślnie",
|
||||
"prefs.quality_fastest": "najszybciej",
|
||||
"prefs.quality_slowest": "najwolniej",
|
||||
"prefs.quality_smoother": "gładziej",
|
||||
"prefs.reports_saved": "Zapisane: {n}",
|
||||
"prefs.right_click": "Kliknięcie prawym",
|
||||
"prefs.saving": "Autozapis i odzyskiwanie",
|
||||
"prefs.scale_note": "Także Ctrl + / Ctrl − w oknie. Przyciski mają 28 px przy 100%.",
|
||||
"prefs.scale_text": "Skala i tekst",
|
||||
"prefs.select": "Zaznacz",
|
||||
"prefs.select_one_item": "Zaznacz jeden element",
|
||||
"prefs.selecting": "Zaznaczanie",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift tylko dodaje; Ctrl / Cmd przełącza",
|
||||
"prefs.shift_add_note": "Włączone: Shift dodaje, Ctrl / Cmd przełącza. Wyłączone (domyślnie): oba przełączają.",
|
||||
"prefs.shortcuts": "Skróty",
|
||||
"prefs.sketch_selection": "Szkic i zaznaczanie",
|
||||
"prefs.sketch_selection_heading": "Szkic i zaznaczanie",
|
||||
"prefs.sketch_selection_note": "Jak działa przyciąganie w szkicach i jak kliknięcia zaznaczają.",
|
||||
"prefs.sketching": "Szkicowanie",
|
||||
"prefs.snap_drawing_note": "Podczas rysowania lub przeciągania punktu szkicu.",
|
||||
"prefs.source": "Źródło",
|
||||
"prefs.speed": "Szybkość",
|
||||
"prefs.stand_up_note": "Gdy plik STEP lub IGES otworzy się z osią Z w górę, proponuj obrót o ćwierć obrotu wokół osi X.",
|
||||
"prefs.status_done": "Gotowe",
|
||||
"prefs.status_error": "Błąd",
|
||||
"prefs.status_selected": "Zaznaczone",
|
||||
"prefs.status_warning": "Ostrzeżenie",
|
||||
"prefs.supersampling_brief": "Rysuje widok w większym rozmiarze i go zmniejsza: bardzo ostro, ale bardziej obciąża.",
|
||||
"prefs.tab_accent_note": "Kolor akcentu podąża za aktywną kartą",
|
||||
"prefs.theme_file": "plik motywu",
|
||||
"prefs.timeline_half_hour": "−30 min",
|
||||
"prefs.timeline_now": "teraz",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
"prefs.toggle_item": "Dodaj element lub usuń go ponownie",
|
||||
"prefs.toolbar": "Pasek narzędzi",
|
||||
"prefs.translucent": "Półprzezroczysta",
|
||||
"prefs.under_browser": "Pod przeglądarką",
|
||||
"prefs.under_cursor": "Pod kursorem",
|
||||
"prefs.units_precision": "Jednostki i dokładność",
|
||||
"prefs.updates": "Aktualizacje",
|
||||
"prefs.view": "Zachowanie widoku",
|
||||
"prefs.viewport": "Widok",
|
||||
"prefs.wheel": "Kółko",
|
||||
"prefs.wheel_trackpad": "Kółko i gładzik",
|
||||
"prefs.workspace": "Środowisko pracy",
|
||||
"prefs.zoom_toward_cursor": "Powiększanie w stronę kursora",
|
||||
"properties.title": "Właściwości: {name}",
|
||||
"recovery.days_ago": "{count} dni temu",
|
||||
"recovery.detail": "{file} · elementy: {features} · autozapis {when}",
|
||||
@@ -1022,7 +1239,7 @@
|
||||
"search.try": "Wpisz nazwę polecenia, element tego projektu lub parametr. Aby uruchomić polecenie z wartościami, wpisz jego identyfikator i obiekt JSON: {example}",
|
||||
"search.valid": "Prawidłowe",
|
||||
"settings.display_quality_note": "Wyższe poziomy rysują zakrzywione ściany i krawędzie gładziej, ale siatka powstaje dłużej. Zmienia się tylko widok, nigdy projekt ani eksport.",
|
||||
"settings.frame_rate_note": "Pokazuje liczbę klatek na sekundę w prawym dolnym rogu widoku. Gdy jest włączone, widok odświeża się bez przerwy.",
|
||||
"settings.frame_rate_note": "Pokazuje liczbę klatek na sekundę w prawym dolnym rogu widoku.",
|
||||
"settings.measure_axis_distances": "Pokaż odległości X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Gdy pomiar jest otwarty, pokazuj składowe wzdłuż osi globalnych obok odległości całkowitej.",
|
||||
"settings.msaa_applies_on_restart": "Wygładzanie krawędzi zadziała po ponownym uruchomieniu SolidCraft",
|
||||
@@ -1067,6 +1284,9 @@
|
||||
"thread.designation": "Oznaczenie",
|
||||
"thread.iso_metric": "Metryczny ISO",
|
||||
"thread.select_size": "Wybierz rozmiar",
|
||||
"timeline.delete_after_marker_one": "Usuń 1 funkcję za znacznikiem historii",
|
||||
"timeline.delete_after_marker_other": "Usuń funkcje za znacznikiem historii: {count}",
|
||||
"timeline.delete_many": "Usuń zaznaczone funkcje: {count}",
|
||||
"timeline.group": "Nowy folder funkcji",
|
||||
"timeline.move_many": "Przenieś elementy osi czasu",
|
||||
"timeline.move_to_group": "Przenieś funkcje do folderu",
|
||||
@@ -1145,6 +1365,7 @@
|
||||
"ui.agent_chat_files_left_out": "Do jednej wiadomości można dołączyć najwyżej {max} plików; pominięto: {files}",
|
||||
"ui.agent_chat_files_loading": "Oczekiwanie na odczytanie plików",
|
||||
"ui.agent_chat_files_max": "Najwyżej {max} plików w jednej wiadomości",
|
||||
"ui.agent_chat_flatpak_none_found": "Nie znaleziono agenta programistycznego. Piaskownica Flatpak domyślnie nie widzi agentów zainstalowanych w folderze domowym i nie ma dostępu do sieci. Aby zezwolić na Claude Code i jego adapter ACP zainstalowane przez nvm lub w ~/.local/bin, uruchom to polecenie w terminalu, a potem uruchom ponownie SolidCraft. Daje ono dostęp do wymienionych programów i plików logowania oraz dostęp do sieci:",
|
||||
"ui.agent_chat_import_failed": "{name}: import się nie powiódł: {error}",
|
||||
"ui.agent_chat_intro": "Rozmawiaj z agentem programistycznym, którego już używasz, lub z wybranym punktem końcowym modelu. Pracuje on na otwartym projekcie za pomocą narzędzi SolidCraft: jego zmiany są widoczne na bieżąco, a cofa się je jednym krokiem na wiadomość. To, co może robić bez pytania, ustawisz w Preferencje > AI.",
|
||||
"ui.agent_chat_key_in_env": "klucz ustawiony",
|
||||
@@ -1212,6 +1433,8 @@
|
||||
"ui.agent_chat_waiting": "Czeka na Ciebie",
|
||||
"ui.agent_cursor": "Kursor agenta",
|
||||
"ui.agent_cursor_speed": "Szybkość kursora agenta",
|
||||
"ui.agent_off_thread": "Zachowaj responsywność okna, gdy agenty wykonują polecenia",
|
||||
"ui.agent_off_thread_note": "Wolne polecenia agenta (z panelu AI lub klienta MCP) są liczone obok okna: możesz dalej obracać i oglądać widok oraz zatrzymać polecenie przyciskiem w widoku. Wyłączone: każde działa w wątku okna, a okno na nie czeka.",
|
||||
"ui.ai_add": "Dodaj",
|
||||
"ui.ai_add_agent": "Dodaj agenta…",
|
||||
"ui.ai_add_agent_title": "Dodaj agenta",
|
||||
@@ -1464,6 +1687,7 @@
|
||||
"ui.grid_follows_view_hint": "Widoki boczne pokazują siatkę na pionowej płaszczyźnie zwróconej do nich zamiast na podłożu",
|
||||
"ui.ground_shadow": "Cień na podłożu",
|
||||
"ui.groupSelected": "Grupuj zaznaczone",
|
||||
"ui.hatch": "Kreskowanie",
|
||||
"ui.heading_add_ins": "DODATKI",
|
||||
"ui.heading_assemble": "ZŁOŻENIE",
|
||||
"ui.heading_box": "PROSTOPADŁOŚCIAN",
|
||||
@@ -1539,10 +1763,17 @@
|
||||
"ui.hideProfile": "Ukryj profil",
|
||||
"ui.history_back": "Cofnij o jeden krok",
|
||||
"ui.history_below_browser": "Pod przeglądarką",
|
||||
"ui.history_edge_hint": "kliknij dwukrotnie krawędź",
|
||||
"ui.history_first": "Przejdź do początku",
|
||||
"ui.history_forward": "Przejdź o jeden krok do przodu",
|
||||
"ui.history_hide_names": "Ukryj nazwy",
|
||||
"ui.history_last": "Przejdź do końca",
|
||||
"ui.history_marker_tip": "Znacznik historii: przeciągnij, aby cofnąć model; nowe funkcje trafiają tutaj",
|
||||
"ui.history_names": "Nazwy",
|
||||
"ui.history_play": "Odtwórz do końca",
|
||||
"ui.history_show_names": "Pokaż nazwy",
|
||||
"ui.history_title": "Historia",
|
||||
"ui.history_toggle_names": "Pokaż lub ukryj nazwy funkcji",
|
||||
"ui.hole": "Otwór",
|
||||
"ui.hole_tap_type": "Typ gwintu otworu",
|
||||
"ui.hole_type": "Typ otworu",
|
||||
@@ -1754,6 +1985,7 @@
|
||||
"ui.smooth_wheel_zoom": "Płynne powiększanie kółkiem",
|
||||
"ui.smooth_wheel_zoom_tip": "Wyłącz, aby każdy ruch kółka działał od razu, bez wygładzania",
|
||||
"ui.snap": "Przyciąganie",
|
||||
"ui.space_mouse_axes_reassigned": "Osie są przypisane inaczej w Preferencjach i to przypisanie nadal działa niezależnie od tej kalibracji.",
|
||||
"ui.space_mouse_calibrate": "Kalibruj…",
|
||||
"ui.space_mouse_calibrate_tip": "Przesuwaj nakładkę zgodnie z poleceniami okna, aby SolidCraft nauczył się kierunku i zakresu każdej osi tej myszy 3D. Zapisywane dla tej myszy 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Kalibracja myszy 3D",
|
||||
@@ -1763,12 +1995,26 @@
|
||||
"ui.space_mouse_calibration_no_response": "Jedna oś prawie się nie poruszyła, więc nie można jej skalibrować. Zacznij od nowa i za każdym razem wychylaj nakładkę mocniej.",
|
||||
"ui.space_mouse_dead_zone": "Strefa martwa",
|
||||
"ui.space_mouse_dead_zone_tip": "O ile trzeba wychylić nakładkę, zanim widok się poruszy, jako część pełnego wychylenia. Zwiększ, jeśli widok dryfuje przy nieruszanej nakładce.",
|
||||
"ui.space_mouse_driven_by": "Który skalibrowany ruch urządzenia steruje tą osią. Wybranie ruchu używanego już przez inną oś zamienia je miejscami.",
|
||||
"ui.space_mouse_driven_by_label": "sterowana przez",
|
||||
"ui.space_mouse_driver": "Sterownik myszy 3D",
|
||||
"ui.space_mouse_driver_auto": "Automatycznie",
|
||||
"ui.space_mouse_driver_direct": "Bezpośrednio (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Bezpośredni odczyt urządzenia; spacenavd nie działa",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Odczyt przez spacenavd, jedyny dostępny sposób",
|
||||
"ui.space_mouse_forget": "Zapomnij kalibrację",
|
||||
"ui.space_mouse_hint": "Przesuwaj i obracaj widok jednocześnie; w szkicach działa tylko przesuwanie i powiększanie.",
|
||||
"ui.space_mouse_motion_rotation_x": "Obrót X",
|
||||
"ui.space_mouse_motion_rotation_y": "Obrót Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Obrót Z",
|
||||
"ui.space_mouse_motion_translation_x": "Przesunięcie X",
|
||||
"ui.space_mouse_motion_translation_y": "Przesunięcie Y",
|
||||
"ui.space_mouse_motion_translation_z": "Przesunięcie Z",
|
||||
"ui.space_mouse_navigate": "Nawiguj za pomocą SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Nie znaleziono myszy 3D",
|
||||
"ui.space_mouse_not_calibrated": "Nieskalibrowano",
|
||||
"ui.space_mouse_reading": "Odczyt",
|
||||
"ui.space_mouse_reset_axes": "Resetuj przypisanie osi",
|
||||
"ui.space_mouse_step_lift": "Unieś nakładkę, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_press": "Wciśnij nakładkę w dół, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_pull": "Pociągnij nakładkę do siebie, przytrzymaj, a następnie puść.",
|
||||
@@ -1782,6 +2028,16 @@
|
||||
"ui.space_mouse_step_tilt_right": "Przechyl nakładkę w prawo (prawa krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Obróć nakładkę zgodnie z ruchem wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Obróć nakładkę przeciwnie do ruchu wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_view_axis": "Oś widoku",
|
||||
"ui.space_mouse_view_off": "Wyłączona",
|
||||
"ui.space_mouse_view_off_tip": "Profil sterownika myszy 3D wyłącza tę oś, więc jej ruch nie porusza widokiem. Wybranie tutaj ruchu innej osi zamienia je miejscami, a ten ruch przestaje poruszać widokiem.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Obracanie w lewo / w prawo",
|
||||
"ui.space_mouse_view_orbit_up_down": "Obracanie w górę / w dół",
|
||||
"ui.space_mouse_view_pan_sideways": "Przesuwanie w lewo / w prawo",
|
||||
"ui.space_mouse_view_pan_up_down": "Przesuwanie w górę / w dół",
|
||||
"ui.space_mouse_view_roll": "Przechylenie",
|
||||
"ui.space_mouse_view_unmapped": "Oś {n}",
|
||||
"ui.space_mouse_view_zoom": "Powiększanie",
|
||||
"ui.spacing": "Odstęp",
|
||||
"ui.spacing_2": "Odstęp 2",
|
||||
"ui.sphere": "Sfera",
|
||||
|
||||
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Licença MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenças de terceiros",
|
||||
"home.back": "Voltar para {name}",
|
||||
"home.crashes": "Relatórios de falhas",
|
||||
"home.design_workspace": "Projeto",
|
||||
"home.documentation": "Documentação",
|
||||
"home.export": "Exportar",
|
||||
"home.export_hint": "Escolha um formato para o projeto atual.",
|
||||
"home.export_json_hint": "Projeto editável e histórico de comandos",
|
||||
"home.export_mesh_hint": "Malha para impressão ou compartilhamento",
|
||||
"home.export_step_hint": "Geometria sólida precisa",
|
||||
"home.import": "Importar…",
|
||||
"home.import_hint": "STEP, IGES ou malha em um novo projeto",
|
||||
"home.in_this_browser": "neste navegador",
|
||||
"home.missing": "ausente",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "fev.",
|
||||
"home.month_03": "mar.",
|
||||
"home.month_04": "abr.",
|
||||
"home.month_05": "mai.",
|
||||
"home.month_06": "jun.",
|
||||
"home.month_07": "jul.",
|
||||
"home.month_08": "ago.",
|
||||
"home.month_09": "set.",
|
||||
"home.month_10": "out.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dez.",
|
||||
"home.new_inches": "Novo projeto em polegadas",
|
||||
"home.new_inches_hint": "Polegadas · abre em Projeto",
|
||||
"home.new_mm": "Novo projeto",
|
||||
"home.new_mm_hint": "Milímetros · abre em Projeto",
|
||||
"home.open": "Abrir…",
|
||||
"home.opened_days_ago": "Aberto há {count} dias",
|
||||
"home.opened_hours_ago": "Aberto há {count} h",
|
||||
"home.opened_just_now": "Aberto agora mesmo",
|
||||
"home.opened_minutes_ago": "Aberto há {count} min",
|
||||
"home.open_hint": "Um projeto .solidcraft ({shortcut})",
|
||||
"home.open_now": "Abrir agora",
|
||||
"home.opened_days_ago": "Há {count} dias",
|
||||
"home.opened_hours_ago": "Há {count} h",
|
||||
"home.opened_just_now": "Agora mesmo",
|
||||
"home.opened_minutes_ago": "Há {count} min",
|
||||
"home.opening": "abrindo…",
|
||||
"home.properties": "Propriedades do projeto",
|
||||
"home.recent": "Recentes",
|
||||
"home.recent_empty": "Os projetos que você abrir ou salvar aparecerão aqui.",
|
||||
"home.recovery_count": "Projetos recuperáveis: {count}",
|
||||
"home.recovery_empty": "Nenhum projeto recuperável.",
|
||||
"home.recovery_hint": "O SolidCraft foi fechado antes que estes projetos fossem salvos.",
|
||||
"home.report_issue": "Relatar um problema",
|
||||
"home.samples": "Exemplos",
|
||||
"home.settings": "Configurações",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Atalhos de teclado",
|
||||
"home.start_hint": "Crie um projeto, abra um arquivo recente ou explore um exemplo.",
|
||||
"home.whats_new": "Novidades",
|
||||
"home.yesterday": "Ontem",
|
||||
"install.button_tip": "Instale o SolidCraft como aplicativo: ele abre em sua própria janela e funciona offline",
|
||||
"install.dock_1": "Escolha Arquivo › Adicionar ao Dock.",
|
||||
"install.dock_2": "Clique em Adicionar.",
|
||||
@@ -1022,7 +1060,7 @@
|
||||
"search.try": "Digite o nome de um comando, um recurso deste projeto ou um parâmetro. Para executar um comando com valores, digite seu id e um objeto JSON: {example}",
|
||||
"search.valid": "Válido",
|
||||
"settings.display_quality_note": "Níveis mais finos desenham faces e arestas curvas mais suaves e demoram mais para gerar a malha. Só a janela de visualização muda, nunca o projeto ou as exportações.",
|
||||
"settings.frame_rate_note": "Mostra quadros por segundo no canto inferior direito da vista. Enquanto estiver ativo, a vista é redesenhada continuamente.",
|
||||
"settings.frame_rate_note": "Mostra quadros por segundo no canto inferior direito da vista.",
|
||||
"settings.measure_axis_distances": "Mostrar distâncias X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Mostrar as componentes nos eixos do mundo junto com a distância total enquanto Medir está aberto.",
|
||||
"settings.msaa_applies_on_restart": "O antisserrilhamento vale na próxima vez que o SolidCraft iniciar",
|
||||
@@ -1145,6 +1183,7 @@
|
||||
"ui.agent_chat_files_left_out": "No máximo {max} arquivos acompanham uma mensagem; deixados de fora: {files}",
|
||||
"ui.agent_chat_files_loading": "Aguardando a leitura dos arquivos",
|
||||
"ui.agent_chat_files_max": "No máximo {max} arquivos por mensagem",
|
||||
"ui.agent_chat_flatpak_none_found": "Nenhum agente de programação encontrado. Por padrão, a sandbox do Flatpak não vê agentes instalados na sua pasta pessoal e não tem acesso à rede. Para permitir o Claude Code e o adaptador ACP dele instalados via nvm ou em ~/.local/bin, execute este comando em um terminal e reinicie o SolidCraft. Ele concede acesso aos programas e arquivos de login listados, além de acesso à rede:",
|
||||
"ui.agent_chat_import_failed": "{name}: falha na importação: {error}",
|
||||
"ui.agent_chat_intro": "Converse com um agente de programação que você já usa, ou com um endpoint de modelo que você escolher. Ele trabalha no projeto aberto pelas ferramentas do SolidCraft: as alterações aparecem conforme são feitas e se desfazem em um passo por mensagem. O que ele pode fazer sem perguntar é definido em Preferências > IA.",
|
||||
"ui.agent_chat_key_in_env": "chave definida",
|
||||
@@ -1464,6 +1503,7 @@
|
||||
"ui.grid_follows_view_hint": "As vistas laterais mostram a grade no plano vertical voltado para elas, em vez do solo",
|
||||
"ui.ground_shadow": "Sombra no chão",
|
||||
"ui.groupSelected": "Agrupar seleção",
|
||||
"ui.hatch": "Hachura",
|
||||
"ui.heading_add_ins": "COMPLEMENTOS",
|
||||
"ui.heading_assemble": "MONTAR",
|
||||
"ui.heading_box": "CAIXA",
|
||||
|
||||
@@ -856,19 +856,57 @@
|
||||
"help.mit": "Licența MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licențe terțe",
|
||||
"home.back": "Înapoi la {name}",
|
||||
"home.crashes": "Rapoarte de blocare",
|
||||
"home.design_workspace": "Proiectare",
|
||||
"home.documentation": "Documentație",
|
||||
"home.export": "Exportă",
|
||||
"home.export_hint": "Alege un format pentru proiectul curent.",
|
||||
"home.export_json_hint": "Proiect editabil și istoricul comenzilor",
|
||||
"home.export_mesh_hint": "Plasă pentru imprimare sau partajare",
|
||||
"home.export_step_hint": "Geometrie solidă precisă",
|
||||
"home.import": "Importă…",
|
||||
"home.import_hint": "STEP, IGES sau plasă într-un proiect nou",
|
||||
"home.in_this_browser": "în acest browser",
|
||||
"home.missing": "lipsă",
|
||||
"home.month_01": "ian.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mar.",
|
||||
"home.month_04": "apr.",
|
||||
"home.month_05": "mai",
|
||||
"home.month_06": "iun.",
|
||||
"home.month_07": "iul.",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "sept.",
|
||||
"home.month_10": "oct.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Proiect nou în inci",
|
||||
"home.new_inches_hint": "Inci · se deschide în Proiectare",
|
||||
"home.new_mm": "Proiect nou",
|
||||
"home.new_mm_hint": "Milimetri · se deschide în Proiectare",
|
||||
"home.open": "Deschide…",
|
||||
"home.opened_days_ago": "Deschis acum {count} zile",
|
||||
"home.opened_hours_ago": "Deschis acum {count} h",
|
||||
"home.opened_just_now": "Deschis chiar acum",
|
||||
"home.opened_minutes_ago": "Deschis acum {count} min",
|
||||
"home.open_hint": "Un proiect .solidcraft ({shortcut})",
|
||||
"home.open_now": "Deschis acum",
|
||||
"home.opened_days_ago": "Acum {count} zile",
|
||||
"home.opened_hours_ago": "Acum {count} h",
|
||||
"home.opened_just_now": "Chiar acum",
|
||||
"home.opened_minutes_ago": "Acum {count} min",
|
||||
"home.opening": "se deschide…",
|
||||
"home.properties": "Proprietățile proiectului",
|
||||
"home.recent": "Recente",
|
||||
"home.recent_empty": "Proiectele pe care le deschizi sau le salvezi apar aici.",
|
||||
"home.recovery_count": "Proiecte recuperabile: {count}",
|
||||
"home.recovery_empty": "Nu există proiecte recuperabile.",
|
||||
"home.recovery_hint": "SolidCraft s-a închis înainte ca aceste proiecte să fie salvate.",
|
||||
"home.report_issue": "Raportează o problemă",
|
||||
"home.samples": "Exemple",
|
||||
"home.settings": "Setări",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Scurtături de la tastatură",
|
||||
"home.start_hint": "Creează un proiect, deschide un fișier recent sau explorează un exemplu.",
|
||||
"home.whats_new": "Noutăți",
|
||||
"home.yesterday": "Ieri",
|
||||
"install.button_tip": "Instalați SolidCraft ca aplicație: se deschide în propria fereastră și funcționează offline",
|
||||
"install.dock_1": "Alegeți Fișier › Adaugă în Dock.",
|
||||
"install.dock_2": "Faceți clic pe Adaugă.",
|
||||
@@ -1021,7 +1059,7 @@
|
||||
"search.try": "Tastați numele unei comenzi, o operație din acest proiect sau un parametru. Pentru a executa o comandă cu valori, tastați id-ul ei și un obiect JSON: {example}",
|
||||
"search.valid": "Valid",
|
||||
"settings.display_quality_note": "Nivelurile mai fine desenează fețele și muchiile curbe mai neted și durează mai mult la generarea plasei. Se schimbă doar fereastra de vizualizare, niciodată proiectul sau exporturile.",
|
||||
"settings.frame_rate_note": "Afișează cadrele pe secundă în colțul din dreapta jos al vizualizării. Cât timp este activat, vizualizarea se redesenează continuu.",
|
||||
"settings.frame_rate_note": "Afișează cadrele pe secundă în colțul din dreapta jos al vizualizării.",
|
||||
"settings.measure_axis_distances": "Afișează distanțele X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Afișează cotele pe axele globale alături de distanța totală cât timp Măsurare este deschis.",
|
||||
"settings.msaa_applies_on_restart": "Anti-aliasing se aplică la următoarea pornire a SolidCraft",
|
||||
@@ -1144,6 +1182,7 @@
|
||||
"ui.agent_chat_files_left_out": "Cel mult {max} fișiere merg cu un mesaj; omise: {files}",
|
||||
"ui.agent_chat_files_loading": "Se așteaptă citirea fișierelor",
|
||||
"ui.agent_chat_files_max": "Cel mult {max} fișiere pe un mesaj",
|
||||
"ui.agent_chat_flatpak_none_found": "Nu s-a găsit niciun agent de programare. Implicit, sandboxul Flatpak nu vede agenții instalați în dosarul personal și nu are acces la rețea. Pentru a permite Claude Code și adaptorul său ACP instalate prin nvm sau în ~/.local/bin, rulați această comandă într-un terminal, apoi reporniți SolidCraft. Ea acordă acces la programele și fișierele de autentificare enumerate, precum și acces la rețea:",
|
||||
"ui.agent_chat_import_failed": "{name}: importul a eșuat: {error}",
|
||||
"ui.agent_chat_intro": "Conversați cu un agent de programare pe care îl folosiți deja sau cu un endpoint de model ales de dumneavoastră. Lucrează la proiectul deschis prin instrumentele SolidCraft: modificările lui apar pe măsură ce le face și se anulează ca un singur pas per mesaj. Ce poate face fără să întrebe se stabilește în Preferințe > AI.",
|
||||
"ui.agent_chat_key_in_env": "cheie setată",
|
||||
@@ -1463,6 +1502,7 @@
|
||||
"ui.grid_follows_view_hint": "Vederile laterale afișează grila pe planul vertical orientat spre ele, în loc de sol",
|
||||
"ui.ground_shadow": "Umbra la sol",
|
||||
"ui.groupSelected": "Grupează selecția",
|
||||
"ui.hatch": "Hașură",
|
||||
"ui.heading_add_ins": "EXTENSII",
|
||||
"ui.heading_assemble": "ASAMBLARE",
|
||||
"ui.heading_box": "CUTIE",
|
||||
|
||||
@@ -856,19 +856,57 @@
|
||||
"help.mit": "Лицензия MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Лицензии сторонних компонентов",
|
||||
"home.back": "Назад к {name}",
|
||||
"home.crashes": "Отчёты о сбоях",
|
||||
"home.design_workspace": "Проектирование",
|
||||
"home.documentation": "Документация",
|
||||
"home.export": "Экспорт",
|
||||
"home.export_hint": "Выберите формат для текущего проекта.",
|
||||
"home.export_json_hint": "Редактируемый проект и история команд",
|
||||
"home.export_mesh_hint": "Сетка для печати или обмена",
|
||||
"home.export_step_hint": "Точная твердотельная геометрия",
|
||||
"home.import": "Импорт…",
|
||||
"home.import_hint": "STEP, IGES или сетка в новый проект",
|
||||
"home.in_this_browser": "в этом браузере",
|
||||
"home.missing": "отсутствует",
|
||||
"home.month_01": "янв.",
|
||||
"home.month_02": "февр.",
|
||||
"home.month_03": "мар.",
|
||||
"home.month_04": "апр.",
|
||||
"home.month_05": "мая",
|
||||
"home.month_06": "июн.",
|
||||
"home.month_07": "июл.",
|
||||
"home.month_08": "авг.",
|
||||
"home.month_09": "сент.",
|
||||
"home.month_10": "окт.",
|
||||
"home.month_11": "нояб.",
|
||||
"home.month_12": "дек.",
|
||||
"home.new_inches": "Новый проект в дюймах",
|
||||
"home.new_inches_hint": "Дюймы · откроется в рабочей области «Проектирование»",
|
||||
"home.new_mm": "Новый проект",
|
||||
"home.new_mm_hint": "Миллиметры · откроется в рабочей области «Проектирование»",
|
||||
"home.open": "Открыть…",
|
||||
"home.opened_days_ago": "Открыт {count} дн. назад",
|
||||
"home.opened_hours_ago": "Открыт {count} ч назад",
|
||||
"home.opened_just_now": "Открыт только что",
|
||||
"home.opened_minutes_ago": "Открыт {count} мин назад",
|
||||
"home.open_hint": "Проект .solidcraft ({shortcut})",
|
||||
"home.open_now": "Открыть сейчас",
|
||||
"home.opened_days_ago": "{count} дн. назад",
|
||||
"home.opened_hours_ago": "{count} ч назад",
|
||||
"home.opened_just_now": "Только что",
|
||||
"home.opened_minutes_ago": "{count} мин назад",
|
||||
"home.opening": "открывается…",
|
||||
"home.properties": "Свойства проекта",
|
||||
"home.recent": "Недавние",
|
||||
"home.recent_empty": "Здесь появятся проекты, которые вы откроете или сохраните.",
|
||||
"home.recovery_count": "Проектов для восстановления: {count}",
|
||||
"home.recovery_empty": "Нет проектов для восстановления.",
|
||||
"home.recovery_hint": "SolidCraft закрылся до сохранения этих проектов.",
|
||||
"home.report_issue": "Сообщить о проблеме",
|
||||
"home.samples": "Примеры",
|
||||
"home.settings": "Настройки",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Сочетания клавиш",
|
||||
"home.start_hint": "Создайте проект, откройте недавний файл или изучите пример.",
|
||||
"home.whats_new": "Что нового",
|
||||
"home.yesterday": "Вчера",
|
||||
"install.button_tip": "Установить SolidCraft как приложение: отдельное окно и работа без сети",
|
||||
"install.dock_1": "Выберите «Файл» › «Добавить в Dock».",
|
||||
"install.dock_2": "Нажмите «Добавить».",
|
||||
@@ -1021,7 +1059,7 @@
|
||||
"search.try": "Введите название команды, операцию этого дизайна или параметр. Чтобы выполнить команду со значениями, введите её id и объект JSON: {example}",
|
||||
"search.valid": "Допустимо",
|
||||
"settings.display_quality_note": "Более высокие уровни рисуют криволинейные грани и рёбра глаже, но сетка строится дольше. Меняется только видовой экран, но не дизайн и не экспорт.",
|
||||
"settings.frame_rate_note": "Показывает кадры в секунду в правом нижнем углу вида. Пока включено, вид перерисовывается непрерывно.",
|
||||
"settings.frame_rate_note": "Показывает кадры в секунду в правом нижнем углу вида.",
|
||||
"settings.measure_axis_distances": "Расстояния X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Показывать размеры вдоль мировых осей вместе с общим расстоянием, пока открыто окно измерения.",
|
||||
"settings.msaa_applies_on_restart": "Сглаживание вступит в силу при следующем запуске SolidCraft",
|
||||
@@ -1144,6 +1182,7 @@
|
||||
"ui.agent_chat_files_left_out": "К одному сообщению можно приложить не более {max} файлов; не добавлены: {files}",
|
||||
"ui.agent_chat_files_loading": "Ожидание чтения файлов",
|
||||
"ui.agent_chat_files_max": "Не более {max} файлов в одном сообщении",
|
||||
"ui.agent_chat_flatpak_none_found": "Кодирующий агент не найден. По умолчанию песочница Flatpak не видит агентов, установленных в домашней папке, и не имеет доступа к сети. Чтобы разрешить Claude Code и его адаптер ACP, установленные через nvm или в ~/.local/bin, выполните эту команду в терминале и перезапустите SolidCraft. Она даёт доступ к перечисленным программам и файлам входа, а также доступ к сети:",
|
||||
"ui.agent_chat_import_failed": "{name}: не удалось импортировать: {error}",
|
||||
"ui.agent_chat_intro": "Общайтесь с уже используемым кодирующим агентом или выбранной конечной точкой модели. Он работает с открытым дизайном через инструменты SolidCraft: изменения видны по мере выполнения и отменяются одним шагом на сообщение. Что ему разрешено без запроса, задаётся в Настройки > ИИ.",
|
||||
"ui.agent_chat_key_in_env": "ключ задан",
|
||||
@@ -1463,6 +1502,7 @@
|
||||
"ui.grid_follows_view_hint": "На боковых видах сетка показывается на вертикальной плоскости, обращённой к вам, а не на земле",
|
||||
"ui.ground_shadow": "Тень на земле",
|
||||
"ui.groupSelected": "Группировать выбранное",
|
||||
"ui.hatch": "Штриховка",
|
||||
"ui.heading_add_ins": "НАДСТРОЙКИ",
|
||||
"ui.heading_assemble": "СБОРКА",
|
||||
"ui.heading_box": "БОКС",
|
||||
|
||||
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT-licens",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenser från tredje part",
|
||||
"home.back": "Tillbaka till {name}",
|
||||
"home.crashes": "Kraschrapporter",
|
||||
"home.design_workspace": "Konstruktion",
|
||||
"home.documentation": "Dokumentation",
|
||||
"home.export": "Exportera",
|
||||
"home.export_hint": "Välj ett format för den aktuella konstruktionen.",
|
||||
"home.export_json_hint": "Redigerbar konstruktion och kommandohistorik",
|
||||
"home.export_mesh_hint": "Mesh för utskrift eller delning",
|
||||
"home.export_step_hint": "Exakt solidgeometri",
|
||||
"home.import": "Importera…",
|
||||
"home.import_hint": "STEP, IGES eller mesh till en ny konstruktion",
|
||||
"home.in_this_browser": "i den här webbläsaren",
|
||||
"home.missing": "saknas",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mars",
|
||||
"home.month_04": "apr.",
|
||||
"home.month_05": "maj",
|
||||
"home.month_06": "juni",
|
||||
"home.month_07": "juli",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "sep.",
|
||||
"home.month_10": "okt.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Ny konstruktion i tum",
|
||||
"home.new_inches_hint": "Tum · öppnas i Konstruktion",
|
||||
"home.new_mm": "Ny konstruktion",
|
||||
"home.new_mm_hint": "Millimeter · öppnas i Konstruktion",
|
||||
"home.open": "Öppna…",
|
||||
"home.opened_days_ago": "Öppnades för {count} dagar sedan",
|
||||
"home.opened_hours_ago": "Öppnades för {count} h sedan",
|
||||
"home.opened_just_now": "Öppnades nyss",
|
||||
"home.opened_minutes_ago": "Öppnades för {count} min sedan",
|
||||
"home.open_hint": "En .solidcraft-konstruktion ({shortcut})",
|
||||
"home.open_now": "Öppen nu",
|
||||
"home.opened_days_ago": "För {count} dagar sedan",
|
||||
"home.opened_hours_ago": "För {count} h sedan",
|
||||
"home.opened_just_now": "Nyss",
|
||||
"home.opened_minutes_ago": "För {count} min sedan",
|
||||
"home.opening": "öppnar…",
|
||||
"home.properties": "Konstruktionsegenskaper",
|
||||
"home.recent": "Senaste",
|
||||
"home.recent_empty": "Konstruktioner som du öppnar eller sparar visas här.",
|
||||
"home.recovery_count": "Återställningsbara konstruktioner: {count}",
|
||||
"home.recovery_empty": "Inga återställningsbara konstruktioner.",
|
||||
"home.recovery_hint": "SolidCraft stängdes innan dessa konstruktioner sparades.",
|
||||
"home.report_issue": "Rapportera ett problem",
|
||||
"home.samples": "Exempel",
|
||||
"home.settings": "Inställningar",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Kortkommandon",
|
||||
"home.start_hint": "Skapa en konstruktion, öppna en nyligen använd fil eller utforska ett exempel.",
|
||||
"home.whats_new": "Nyheter",
|
||||
"home.yesterday": "I går",
|
||||
"install.button_tip": "Installera SolidCraft som en app: den öppnas i ett eget fönster och fungerar offline",
|
||||
"install.dock_1": "Välj Arkiv › Lägg till i Dock.",
|
||||
"install.dock_2": "Klicka på Lägg till.",
|
||||
@@ -1021,7 +1059,7 @@
|
||||
"search.try": "Skriv ett kommandonamn, en funktion i denna konstruktion eller en parameter. För att köra ett kommando med värden, skriv dess id och ett JSON-objekt: {example}",
|
||||
"search.valid": "Giltig",
|
||||
"settings.display_quality_note": "Finare nivåer ritar krökta ytor och kanter jämnare och tar längre tid att mesha. Endast vyporten ändras, aldrig konstruktionen eller exporter.",
|
||||
"settings.frame_rate_note": "Visar bildrutor per sekund i vyns nedre högra hörn. Medan det är på ritas vyn om kontinuerligt.",
|
||||
"settings.frame_rate_note": "Visar bildrutor per sekund i vyns nedre högra hörn.",
|
||||
"settings.measure_axis_distances": "Visa X/Y/Z-avstånd",
|
||||
"settings.measure_axis_distances_help": "Visa världsaxelns komponentmått bredvid det totala avståndet medan Mät är öppet.",
|
||||
"settings.msaa_applies_on_restart": "Kantutjämning tillämpas nästa gång SolidCraft startar",
|
||||
@@ -1144,6 +1182,7 @@
|
||||
"ui.agent_chat_files_left_out": "Högst {max} filer följer med ett meddelande; utelämnade: {files}",
|
||||
"ui.agent_chat_files_loading": "Väntar på att filerna ska läsas",
|
||||
"ui.agent_chat_files_max": "Högst {max} filer i ett meddelande",
|
||||
"ui.agent_chat_flatpak_none_found": "Ingen kodningsagent hittades. Flatpak-sandlådan ser som standard inte agenter som är installerade i din hemmapp och har ingen nätverksåtkomst. För att tillåta Claude Code och dess ACP-adapter installerade via nvm eller i ~/.local/bin kör du det här kommandot i en terminal och startar sedan om SolidCraft. Det ger åtkomst till de angivna programmen och inloggningsfilerna samt nätverksåtkomst:",
|
||||
"ui.agent_chat_import_failed": "{name}: importen misslyckades: {error}",
|
||||
"ui.agent_chat_intro": "Chatta med en kodningsagent du redan använder, eller en modellslutpunkt du väljer. Den arbetar med den öppna konstruktionen via SolidCrafts verktyg: dess ändringar syns medan den gör dem och ångras som ett steg per meddelande. Vad den får göra utan att fråga ställs in under Inställningar > AI.",
|
||||
"ui.agent_chat_key_in_env": "nyckel angiven",
|
||||
@@ -1463,6 +1502,7 @@
|
||||
"ui.grid_follows_view_hint": "Sidovyer visar rutnätet på det upprätta planet som vetter mot dem istället för marken",
|
||||
"ui.ground_shadow": "Markskugga",
|
||||
"ui.groupSelected": "Gruppera markerade",
|
||||
"ui.hatch": "Skraffering",
|
||||
"ui.heading_add_ins": "TILLÄGG",
|
||||
"ui.heading_assemble": "MONTERA",
|
||||
"ui.heading_box": "LÅDA",
|
||||
|
||||
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT lisansı",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Üçüncü taraf lisansları",
|
||||
"home.back": "{name} tasarımına dön",
|
||||
"home.crashes": "Çökme raporları",
|
||||
"home.design_workspace": "Tasarım",
|
||||
"home.documentation": "Belgeler",
|
||||
"home.export": "Dışa aktar",
|
||||
"home.export_hint": "Geçerli tasarım için bir biçim seçin.",
|
||||
"home.export_json_hint": "Düzenlenebilir tasarım ve komut geçmişi",
|
||||
"home.export_mesh_hint": "Yazdırma veya paylaşma için ağ",
|
||||
"home.export_step_hint": "Hassas katı geometrisi",
|
||||
"home.import": "İçe aktar…",
|
||||
"home.import_hint": "STEP, IGES veya ağı yeni bir tasarıma aktar",
|
||||
"home.in_this_browser": "bu tarayıcıda",
|
||||
"home.missing": "eksik",
|
||||
"home.month_01": "Oca",
|
||||
"home.month_02": "Şub",
|
||||
"home.month_03": "Mar",
|
||||
"home.month_04": "Nis",
|
||||
"home.month_05": "May",
|
||||
"home.month_06": "Haz",
|
||||
"home.month_07": "Tem",
|
||||
"home.month_08": "Ağu",
|
||||
"home.month_09": "Eyl",
|
||||
"home.month_10": "Eki",
|
||||
"home.month_11": "Kas",
|
||||
"home.month_12": "Ara",
|
||||
"home.new_inches": "Yeni tasarım, inç",
|
||||
"home.new_inches_hint": "İnç · Tasarım alanında açılır",
|
||||
"home.new_mm": "Yeni tasarım",
|
||||
"home.new_mm_hint": "Milimetre · Tasarım alanında açılır",
|
||||
"home.open": "Aç…",
|
||||
"home.opened_days_ago": "{count} gün önce açıldı",
|
||||
"home.opened_hours_ago": "{count} sa. önce açıldı",
|
||||
"home.opened_just_now": "Az önce açıldı",
|
||||
"home.opened_minutes_ago": "{count} dk. önce açıldı",
|
||||
"home.open_hint": "Bir .solidcraft tasarımı ({shortcut})",
|
||||
"home.open_now": "Şimdi aç",
|
||||
"home.opened_days_ago": "{count} gün önce",
|
||||
"home.opened_hours_ago": "{count} sa. önce",
|
||||
"home.opened_just_now": "Az önce",
|
||||
"home.opened_minutes_ago": "{count} dk. önce",
|
||||
"home.opening": "açılıyor…",
|
||||
"home.properties": "Tasarım özellikleri",
|
||||
"home.recent": "Son kullanılanlar",
|
||||
"home.recent_empty": "Açtığınız veya kaydettiğiniz tasarımlar burada görünür.",
|
||||
"home.recovery_count": "Kurtarılabilir tasarımlar: {count}",
|
||||
"home.recovery_empty": "Kurtarılabilir tasarım yok.",
|
||||
"home.recovery_hint": "Bu tasarımlar kaydedilmeden SolidCraft kapandı.",
|
||||
"home.report_issue": "Sorun bildir",
|
||||
"home.samples": "Örnekler",
|
||||
"home.settings": "Ayarlar",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Klavye kısayolları",
|
||||
"home.start_hint": "Bir tasarım oluşturun, son kullanılan bir dosyayı açın veya bir örneği inceleyin.",
|
||||
"home.whats_new": "Yenilikler",
|
||||
"home.yesterday": "Dün",
|
||||
"install.button_tip": "SolidCraft'ı uygulama olarak yükleyin: kendi penceresinde açılır ve çevrimdışı da çalışır",
|
||||
"install.dock_1": "Dosya › Dock'a Ekle'yi seçin.",
|
||||
"install.dock_2": "“Ekle” düğmesine tıklayın.",
|
||||
@@ -1021,7 +1059,7 @@
|
||||
"search.try": "Bir komut adı, bu tasarımdaki bir özellik veya bir parametre yazın. Bir komutu değerlerle çalıştırmak için kimliğini ve bir JSON nesnesi yazın: {example}",
|
||||
"search.valid": "Geçerli",
|
||||
"settings.display_quality_note": "Daha ince seviyeler eğri yüzeyleri ve kenarları daha pürüzsüz gösterir, ancak ağ oluşturma daha uzun sürer. Yalnızca görünüm değişir; tasarım ve dışa aktarımlar asla değişmez.",
|
||||
"settings.frame_rate_note": "Görünümün sağ alt köşesinde saniyedeki kare sayısını gösterir. Açıkken görünüm sürekli yeniden çizilir.",
|
||||
"settings.frame_rate_note": "Görünümün sağ alt köşesinde saniyedeki kare sayısını gösterir.",
|
||||
"settings.measure_axis_distances": "X/Y/Z mesafelerini göster",
|
||||
"settings.measure_axis_distances_help": "Ölç açıkken toplam mesafenin yanında dünya ekseni bileşen ölçülerini gösterir.",
|
||||
"settings.msaa_applies_on_restart": "Kenar yumuşatma, SolidCraft bir sonraki başlatıldığında uygulanır",
|
||||
@@ -1144,6 +1182,7 @@
|
||||
"ui.agent_chat_files_left_out": "Bir iletiyle en fazla {max} dosya gider; dışarıda kalanlar: {files}",
|
||||
"ui.agent_chat_files_loading": "Dosyaların okunması bekleniyor",
|
||||
"ui.agent_chat_files_max": "Bir iletide en fazla {max} dosya olabilir",
|
||||
"ui.agent_chat_flatpak_none_found": "Kodlama ajanı bulunamadı. Flatpak korumalı alanı varsayılan olarak ana klasörünüze yüklenen ajanları göremez ve ağ erişimi yoktur. nvm ile veya ~/.local/bin içine yüklenen Claude Code'a ve ACP bağdaştırıcısına izin vermek için bu komutu bir terminalde çalıştırın, ardından SolidCraft'ı yeniden başlatın. Komut, listelenen programlara ve oturum açma dosyalarına erişimin yanı sıra ağ erişimi de verir:",
|
||||
"ui.agent_chat_import_failed": "{name}: içe aktarma başarısız oldu: {error}",
|
||||
"ui.agent_chat_intro": "Zaten kullandığınız bir kodlama ajanıyla veya seçtiğiniz bir model uç noktasıyla sohbet edin. Ajan, SolidCraft'ın araçlarıyla açık tasarım üzerinde çalışır: Değişiklikleri hemen görünür ve mesaj başına tek adımda geri alınabilir. Sormadan neleri yapabileceğini Ayarlar > AI altında belirlersiniz.",
|
||||
"ui.agent_chat_key_in_env": "anahtar ayarlı",
|
||||
@@ -1463,6 +1502,7 @@
|
||||
"ui.grid_follows_view_hint": "Yan görünümlerde ızgara, zemin yerine onlara bakan dik düzlemde gösterilir",
|
||||
"ui.ground_shadow": "Zemin gölgesi",
|
||||
"ui.groupSelected": "Seçimi grupla",
|
||||
"ui.hatch": "Tarama",
|
||||
"ui.heading_add_ins": "EKLENTİLER",
|
||||
"ui.heading_assemble": "MONTAJ",
|
||||
"ui.heading_box": "KUTU",
|
||||
|
||||
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Ліцензія MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Ліцензії сторонніх компонентів",
|
||||
"home.back": "Назад до {name}",
|
||||
"home.crashes": "Звіти про збої",
|
||||
"home.design_workspace": "Проєктування",
|
||||
"home.documentation": "Документація",
|
||||
"home.export": "Експортувати",
|
||||
"home.export_hint": "Виберіть формат для поточного проєкту.",
|
||||
"home.export_json_hint": "Редагований проєкт та історія команд",
|
||||
"home.export_mesh_hint": "Сітка для друку або поширення",
|
||||
"home.export_step_hint": "Точна твердотільна геометрія",
|
||||
"home.import": "Імпортувати…",
|
||||
"home.import_hint": "STEP, IGES або сітку в новий проєкт",
|
||||
"home.in_this_browser": "у цьому браузері",
|
||||
"home.missing": "відсутній",
|
||||
"home.month_01": "січ.",
|
||||
"home.month_02": "лют.",
|
||||
"home.month_03": "бер.",
|
||||
"home.month_04": "квіт.",
|
||||
"home.month_05": "трав.",
|
||||
"home.month_06": "черв.",
|
||||
"home.month_07": "лип.",
|
||||
"home.month_08": "серп.",
|
||||
"home.month_09": "вер.",
|
||||
"home.month_10": "жовт.",
|
||||
"home.month_11": "лист.",
|
||||
"home.month_12": "груд.",
|
||||
"home.new_inches": "Новий проєкт у дюймах",
|
||||
"home.new_inches_hint": "Дюйми · відкриється в робочому просторі «Проєктування»",
|
||||
"home.new_mm": "Новий проєкт",
|
||||
"home.new_mm_hint": "Міліметри · відкриється в робочому просторі «Проєктування»",
|
||||
"home.open": "Відкрити…",
|
||||
"home.opened_days_ago": "Відкрито {count} дн. тому",
|
||||
"home.opened_hours_ago": "Відкрито {count} год тому",
|
||||
"home.opened_just_now": "Щойно відкрито",
|
||||
"home.opened_minutes_ago": "Відкрито {count} хв тому",
|
||||
"home.open_hint": "Проєкт .solidcraft ({shortcut})",
|
||||
"home.open_now": "Відкрити зараз",
|
||||
"home.opened_days_ago": "{count} дн. тому",
|
||||
"home.opened_hours_ago": "{count} год тому",
|
||||
"home.opened_just_now": "Щойно",
|
||||
"home.opened_minutes_ago": "{count} хв тому",
|
||||
"home.opening": "відкриття…",
|
||||
"home.properties": "Властивості проєкту",
|
||||
"home.recent": "Недавні",
|
||||
"home.recent_empty": "Тут з’являться проєкти, які ви відкриєте або збережете.",
|
||||
"home.recovery_count": "Проєктів для відновлення: {count}",
|
||||
"home.recovery_empty": "Немає проєктів для відновлення.",
|
||||
"home.recovery_hint": "SolidCraft закрився до збереження цих проєктів.",
|
||||
"home.report_issue": "Повідомити про проблему",
|
||||
"home.samples": "Зразки",
|
||||
"home.settings": "Налаштування",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Сполучення клавіш",
|
||||
"home.start_hint": "Створіть проєкт, відкрийте нещодавній файл або перегляньте приклад.",
|
||||
"home.whats_new": "Що нового",
|
||||
"home.yesterday": "Учора",
|
||||
"install.button_tip": "Встановити SolidCraft як застосунок: він відкривається в окремому вікні й працює без мережі",
|
||||
"install.dock_1": "Виберіть «Файл» › «Додати в Dock».",
|
||||
"install.dock_2": "Натисніть «Додати».",
|
||||
@@ -1022,7 +1060,7 @@
|
||||
"search.try": "Введіть назву команди, елемент цього проєкту або параметр. Щоб виконати команду зі значеннями, введіть її id та об'єкт JSON: {example}",
|
||||
"search.valid": "Дійсне",
|
||||
"settings.display_quality_note": "Дрібніші рівні малюють криві грані й ребра плавнішими, але тріангуляція триває довше. Змінюється лише вікно перегляду, ніколи не проєкт чи експорт.",
|
||||
"settings.frame_rate_note": "Показує кадри за секунду в правому нижньому куті виду. Поки це ввімкнено, вид перемальовується безперервно.",
|
||||
"settings.frame_rate_note": "Показує кадри за секунду в правому нижньому куті виду.",
|
||||
"settings.measure_axis_distances": "Відстані X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Показувати розміри вздовж світових осей разом із загальною відстанню, поки відкрите вікно вимірювання.",
|
||||
"settings.msaa_applies_on_restart": "Згладжування застосується під час наступного запуску SolidCraft",
|
||||
@@ -1145,6 +1183,7 @@
|
||||
"ui.agent_chat_files_left_out": "З одним повідомленням надсилається щонайбільше {max} файлів; пропущено: {files}",
|
||||
"ui.agent_chat_files_loading": "Очікування читання файлів",
|
||||
"ui.agent_chat_files_max": "Щонайбільше {max} файлів на одне повідомлення",
|
||||
"ui.agent_chat_flatpak_none_found": "Агента для програмування не знайдено. Пісочниця Flatpak типово не бачить агентів, установлених у домашній теці, і не має доступу до мережі. Щоб дозволити Claude Code і його адаптер ACP, установлені через nvm або в ~/.local/bin, виконайте цю команду в терміналі, а потім перезапустіть SolidCraft. Вона надає доступ до перелічених програм і файлів входу, а також доступ до мережі:",
|
||||
"ui.agent_chat_import_failed": "{name}: не вдалося імпортувати: {error}",
|
||||
"ui.agent_chat_intro": "Спілкуйтеся з агентом для програмування, яким уже користуєтеся, або з обраною вами модельною кінцевою точкою. Він працює з відкритим проєктом через інструменти SolidCraft: його зміни з'являються в міру виконання й скасовуються одним кроком на повідомлення. Що він може робити без запиту, задано в Налаштування > ШІ.",
|
||||
"ui.agent_chat_key_in_env": "ключ задано",
|
||||
@@ -1464,6 +1503,7 @@
|
||||
"ui.grid_follows_view_hint": "Бічні види показують сітку на вертикальній площині, повернутій до них, а не на землі",
|
||||
"ui.ground_shadow": "Тінь на землі",
|
||||
"ui.groupSelected": "Згрупувати виділене",
|
||||
"ui.hatch": "Штрихування",
|
||||
"ui.heading_add_ins": "НАДБУДОВИ",
|
||||
"ui.heading_assemble": "СКЛАДАННЯ",
|
||||
"ui.heading_box": "КУБ",
|
||||
|
||||
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Giấy phép MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Giấy phép của bên thứ ba",
|
||||
"home.back": "Quay lại {name}",
|
||||
"home.crashes": "Báo cáo sự cố",
|
||||
"home.design_workspace": "Thiết kế",
|
||||
"home.documentation": "Tài liệu hướng dẫn",
|
||||
"home.export": "Xuất",
|
||||
"home.export_hint": "Chọn định dạng cho thiết kế hiện tại.",
|
||||
"home.export_json_hint": "Thiết kế có thể chỉnh sửa và lịch sử lệnh",
|
||||
"home.export_mesh_hint": "Lưới để in hoặc chia sẻ",
|
||||
"home.export_step_hint": "Hình học khối chính xác",
|
||||
"home.import": "Nhập…",
|
||||
"home.import_hint": "Nhập STEP, IGES hoặc lưới vào thiết kế mới",
|
||||
"home.in_this_browser": "trong trình duyệt này",
|
||||
"home.missing": "thiếu",
|
||||
"home.month_01": "thg 1",
|
||||
"home.month_02": "thg 2",
|
||||
"home.month_03": "thg 3",
|
||||
"home.month_04": "thg 4",
|
||||
"home.month_05": "thg 5",
|
||||
"home.month_06": "thg 6",
|
||||
"home.month_07": "thg 7",
|
||||
"home.month_08": "thg 8",
|
||||
"home.month_09": "thg 9",
|
||||
"home.month_10": "thg 10",
|
||||
"home.month_11": "thg 11",
|
||||
"home.month_12": "thg 12",
|
||||
"home.new_inches": "Thiết kế mới theo inch",
|
||||
"home.new_inches_hint": "Inch · mở trong Thiết kế",
|
||||
"home.new_mm": "Thiết kế mới",
|
||||
"home.new_mm_hint": "Milimét · mở trong Thiết kế",
|
||||
"home.open": "Mở…",
|
||||
"home.opened_days_ago": "Đã mở {count} ngày trước",
|
||||
"home.opened_hours_ago": "Đã mở {count} giờ trước",
|
||||
"home.opened_just_now": "Vừa mới mở",
|
||||
"home.opened_minutes_ago": "Đã mở {count} phút trước",
|
||||
"home.open_hint": "Một thiết kế .solidcraft ({shortcut})",
|
||||
"home.open_now": "Mở ngay",
|
||||
"home.opened_days_ago": "{count} ngày trước",
|
||||
"home.opened_hours_ago": "{count} giờ trước",
|
||||
"home.opened_just_now": "Vừa xong",
|
||||
"home.opened_minutes_ago": "{count} phút trước",
|
||||
"home.opening": "đang mở…",
|
||||
"home.properties": "Thuộc tính thiết kế",
|
||||
"home.recent": "Gần đây",
|
||||
"home.recent_empty": "Các thiết kế bạn mở hoặc lưu sẽ xuất hiện ở đây.",
|
||||
"home.recovery_count": "Thiết kế có thể khôi phục: {count}",
|
||||
"home.recovery_empty": "Không có thiết kế nào có thể khôi phục.",
|
||||
"home.recovery_hint": "SolidCraft đã đóng trước khi các thiết kế này được lưu.",
|
||||
"home.report_issue": "Báo cáo sự cố",
|
||||
"home.samples": "Mẫu thiết kế",
|
||||
"home.settings": "Cài đặt",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Phím tắt",
|
||||
"home.start_hint": "Tạo thiết kế, mở tệp gần đây hoặc khám phá mẫu.",
|
||||
"home.whats_new": "Có gì mới",
|
||||
"home.yesterday": "Hôm qua",
|
||||
"install.button_tip": "Cài đặt SolidCraft dưới dạng ứng dụng: mở trong cửa sổ riêng và hoạt động ngoại tuyến",
|
||||
"install.dock_1": "Chọn Tệp › Thêm vào Dock.",
|
||||
"install.dock_2": "Nhấp vào Thêm.",
|
||||
@@ -1022,7 +1060,7 @@
|
||||
"search.try": "Nhập tên lệnh, một tính năng trong thiết kế này hoặc một tham số. Để chạy lệnh kèm giá trị, hãy nhập mã lệnh và một đối tượng JSON: {example}",
|
||||
"search.valid": "Hợp lệ",
|
||||
"settings.display_quality_note": "Mức độ chi tiết cao hơn giúp hiển thị các mặt cong và cạnh mượt mà hơn nhưng tốn nhiều thời gian tạo lưới hơn. Thiết lập này chỉ thay đổi vùng nhìn, không ảnh hưởng đến thiết kế hay dữ liệu xuất tệp.",
|
||||
"settings.frame_rate_note": "Hiển thị số khung hình trên giây (FPS) ở góc dưới bên phải khung nhìn. Khi bật, khung nhìn sẽ vẽ lại liên tục.",
|
||||
"settings.frame_rate_note": "Hiển thị số khung hình trên giây (FPS) ở góc dưới bên phải khung nhìn.",
|
||||
"settings.measure_axis_distances": "Hiển thị khoảng cách theo trục X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Hiển thị kích thước thành phần theo các trục tọa độ cùng với khoảng cách tổng thể khi mở công cụ Đo lường.",
|
||||
"settings.msaa_applies_on_restart": "Khử răng cưa sẽ áp dụng vào lần khởi động tiếp theo của SolidCraft",
|
||||
@@ -1145,6 +1183,7 @@
|
||||
"ui.agent_chat_files_left_out": "Tối đa {max} tệp đính kèm trong một tin nhắn; đã bỏ qua: {files}",
|
||||
"ui.agent_chat_files_loading": "Đang chờ đọc các tệp đính kèm",
|
||||
"ui.agent_chat_files_max": "Tối đa {max} tệp trong một tin nhắn",
|
||||
"ui.agent_chat_flatpak_none_found": "Không tìm thấy tác tử lập trình nào. Theo mặc định, hộp cát Flatpak không thấy các tác tử được cài trong thư mục nhà của bạn và không có quyền truy cập mạng. Để cho phép Claude Code và bộ điều hợp ACP của nó được cài qua nvm hoặc trong ~/.local/bin, hãy chạy lệnh này trong terminal, rồi khởi động lại SolidCraft. Lệnh cấp quyền truy cập các chương trình và tệp đăng nhập được liệt kê, cùng quyền truy cập mạng:",
|
||||
"ui.agent_chat_import_failed": "{name}: nhập tệp thất bại: {error}",
|
||||
"ui.agent_chat_intro": "Trò chuyện với tác tử lập trình (AI agent) bạn đang dùng hoặc một điểm cuối mô hình bạn chọn. AI thao tác trên thiết kế đang mở thông qua các công cụ của SolidCraft: các thay đổi hiển thị ngay khi thực hiện và có thể hoàn tác theo từng bước cho mỗi tin nhắn. Các thao tác AI được phép thực hiện mà không cần hỏi được đặt trong Tùy chọn cài đặt > AI.",
|
||||
"ui.agent_chat_key_in_env": "đã đặt khóa",
|
||||
@@ -1464,6 +1503,7 @@
|
||||
"ui.grid_follows_view_hint": "Các góc nhìn cạnh hiển thị lưới trên mặt phẳng thẳng đứng hướng về phía camera thay vì trên mặt sàn",
|
||||
"ui.ground_shadow": "Bóng đổ mặt sàn",
|
||||
"ui.groupSelected": "Gộp nhóm vùng chọn",
|
||||
"ui.hatch": "Gạch mặt cắt",
|
||||
"ui.heading_add_ins": "TIỆN ÍCH MỞ RỘNG",
|
||||
"ui.heading_assemble": "LẮP RÁP",
|
||||
"ui.heading_box": "KHỐI HỘP",
|
||||
|
||||
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT 授權",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "第三方授權",
|
||||
"home.back": "返回 {name}",
|
||||
"home.crashes": "當機報告",
|
||||
"home.design_workspace": "設計",
|
||||
"home.documentation": "文件",
|
||||
"home.export": "匯出",
|
||||
"home.export_hint": "選擇目前設計的格式。",
|
||||
"home.export_json_hint": "可編輯的設計與命令歷程",
|
||||
"home.export_mesh_hint": "用於列印或分享的網格",
|
||||
"home.export_step_hint": "精確的實體幾何",
|
||||
"home.import": "匯入…",
|
||||
"home.import_hint": "將 STEP、IGES 或網格匯入新設計",
|
||||
"home.in_this_browser": "在此瀏覽器中",
|
||||
"home.missing": "找不到檔案",
|
||||
"home.month_01": "1月",
|
||||
"home.month_02": "2月",
|
||||
"home.month_03": "3月",
|
||||
"home.month_04": "4月",
|
||||
"home.month_05": "5月",
|
||||
"home.month_06": "6月",
|
||||
"home.month_07": "7月",
|
||||
"home.month_08": "8月",
|
||||
"home.month_09": "9月",
|
||||
"home.month_10": "10月",
|
||||
"home.month_11": "11月",
|
||||
"home.month_12": "12月",
|
||||
"home.new_inches": "新增設計(英吋)",
|
||||
"home.new_inches_hint": "英吋 · 在設計工作區中開啟",
|
||||
"home.new_mm": "新增設計",
|
||||
"home.new_mm_hint": "公釐 · 在設計工作區中開啟",
|
||||
"home.open": "開啟…",
|
||||
"home.opened_days_ago": "{count} 天前開啟",
|
||||
"home.opened_hours_ago": "{count} 小時前開啟",
|
||||
"home.opened_just_now": "剛剛開啟",
|
||||
"home.opened_minutes_ago": "{count} 分鐘前開啟",
|
||||
"home.open_hint": ".solidcraft 設計檔案 ({shortcut})",
|
||||
"home.open_now": "目前開啟中",
|
||||
"home.opened_days_ago": "{count} 天前",
|
||||
"home.opened_hours_ago": "{count} 小時前",
|
||||
"home.opened_just_now": "剛剛",
|
||||
"home.opened_minutes_ago": "{count} 分鐘前",
|
||||
"home.opening": "正在開啟…",
|
||||
"home.properties": "設計屬性",
|
||||
"home.recent": "最近使用",
|
||||
"home.recent_empty": "您開啟或儲存的設計會顯示於此。",
|
||||
"home.recovery_count": "可復原的設計:{count} 個",
|
||||
"home.recovery_empty": "沒有可復原的設計。",
|
||||
"home.recovery_hint": "SolidCraft 在這些設計儲存前已關閉。",
|
||||
"home.report_issue": "回報問題",
|
||||
"home.samples": "範例",
|
||||
"home.settings": "設定",
|
||||
"home.short_date": "{month}{day}日",
|
||||
"home.shortcuts": "鍵盤快速鍵",
|
||||
"home.start_hint": "建立設計、開啟最近使用的檔案,或瀏覽範例。",
|
||||
"home.whats_new": "新功能",
|
||||
"home.yesterday": "昨天",
|
||||
"install.button_tip": "將 SolidCraft 安裝為應用程式:在獨立視窗中開啟,離線也能使用",
|
||||
"install.dock_1": "選擇「檔案」›「加入 Dock」。",
|
||||
"install.dock_2": "按一下「加入」。",
|
||||
@@ -1021,7 +1059,7 @@
|
||||
"search.try": "輸入命令名稱、此設計中的特徵或參數。要帶值執行命令,請輸入其ID和JSON物件:{example}",
|
||||
"search.valid": "有效",
|
||||
"settings.display_quality_note": "等級越精細,曲面和邊顯示越平滑,但網格化耗時更長。只影響檢視區,不影響設計或匯出。",
|
||||
"settings.frame_rate_note": "在檢視右下角顯示每秒影格數。開啟時檢視會持續重繪。",
|
||||
"settings.frame_rate_note": "在檢視右下角顯示每秒影格數。",
|
||||
"settings.measure_axis_distances": "顯示 X/Y/Z 距離",
|
||||
"settings.measure_axis_distances_help": "開啟測量時,在總距離之外顯示沿世界座標軸方向的分量尺寸。",
|
||||
"settings.msaa_applies_on_restart": "抗鋸齒將在下次啟動 SolidCraft 時生效",
|
||||
@@ -1144,6 +1182,7 @@
|
||||
"ui.agent_chat_files_left_out": "一條訊息最多傳送 {max} 個檔案;未傳送:{files}",
|
||||
"ui.agent_chat_files_loading": "正在等待檔案讀取完成",
|
||||
"ui.agent_chat_files_max": "一條訊息最多附加 {max} 個檔案",
|
||||
"ui.agent_chat_flatpak_none_found": "未找到程式設計代理。預設情況下,Flatpak 沙箱無法存取主資料夾中安裝的代理,也無法存取網路。若要允許透過 nvm 或在 ~/.local/bin 中安裝的 Claude Code 及其 ACP 轉接器,請在終端機執行以下命令,然後重新啟動 SolidCraft。此命令會授予對所列程式和登入檔案的存取權限,以及網路存取權限:",
|
||||
"ui.agent_chat_import_failed": "{name}:匯入失敗:{error}",
|
||||
"ui.agent_chat_intro": "與您已在使用的程式設計代理或您選擇的模型端點聊天。它透過 SolidCraft 的工具處理開啟的設計:變更即時顯示,每條訊息的變更可一步復原。無需詢問即可執行的範圍在「偏好設定 > AI」中設定。",
|
||||
"ui.agent_chat_key_in_env": "已設定金鑰",
|
||||
@@ -1463,6 +1502,7 @@
|
||||
"ui.grid_follows_view_hint": "在側檢視中,將網格顯示在朝向該檢視的豎直平面上,而不是地面上",
|
||||
"ui.ground_shadow": "地面陰影",
|
||||
"ui.groupSelected": "群組所選",
|
||||
"ui.hatch": "剖面線",
|
||||
"ui.heading_add_ins": "增益集",
|
||||
"ui.heading_assemble": "組裝",
|
||||
"ui.heading_box": "長方體",
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
"Active": "活动",
|
||||
"Active rule": "活动规则",
|
||||
"Add": "添加",
|
||||
"Add Column": "添加列",
|
||||
"Add Key": "添加按键",
|
||||
"Add Mid Radius": "添加中间半径",
|
||||
"Add Row": "添加行",
|
||||
"Add a tool": "添加工具",
|
||||
"Add condition": "添加条件",
|
||||
"Add or edit rule": "添加或编辑规则",
|
||||
"Add to New Folder": "添加到新文件夹",
|
||||
"Advanced": "高级",
|
||||
"Against": "对照",
|
||||
"Alignment": "对齐",
|
||||
"All Sketch Points": "所有草图点",
|
||||
@@ -32,6 +36,7 @@
|
||||
"Appearance name": "外观名称",
|
||||
"Applied Radius": "应用半径",
|
||||
"Apply To": "应用于",
|
||||
"Apply library appearance": "应用库外观",
|
||||
"Apply to selection": "应用到选择",
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "请先应用外观。导出已指定的外观和在此设计中创建的材质。",
|
||||
"Arc length": "弧长",
|
||||
@@ -42,13 +47,18 @@
|
||||
"BLEND SURFACE": "混合曲面",
|
||||
"BOUNDARY SURFACE": "边界曲面",
|
||||
"BROWSER": "浏览器",
|
||||
"Bend Edges": "折弯边",
|
||||
"Bend Line Position": "弯折线位置",
|
||||
"Bend Position": "折弯位置",
|
||||
"Bend Radius": "折弯半径",
|
||||
"Bend Table": "折弯表",
|
||||
"Bends": "折弯",
|
||||
"Bodies": "实体",
|
||||
"Bodies (optional)": "实体(可选)",
|
||||
"Body": "实体",
|
||||
"Body (last if none)": "实体(未指定时为最后一个)",
|
||||
"Body to split": "要分割的实体",
|
||||
"Body, or faces to take out": "实体,或要移除的面",
|
||||
"Boundary": "边界",
|
||||
"Bounding box": "边界框",
|
||||
"CHECK": "检查",
|
||||
@@ -60,7 +70,9 @@
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV 或 JSON:更新现有参数、添加新参数",
|
||||
"Capture All Positions": "捕获所有位置",
|
||||
"Capture Pos.": "捕获位置",
|
||||
"Catch Depth": "卡扣深度",
|
||||
"Catch Direction": "卡扣方向",
|
||||
"Catch Length": "卡扣长度",
|
||||
"Cells": "区域",
|
||||
"Center X": "中心 X",
|
||||
"Center Y": "中心 Y",
|
||||
@@ -69,15 +81,21 @@
|
||||
"Chamfer Type": "倒角类型",
|
||||
"Check collisions": "检查碰撞",
|
||||
"Checked": "已检查",
|
||||
"Choose Variable radius or Spline in Type.": "请在“类型”中选择 Variable radius 或样条曲线。",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "选择一个新平面。草图几何和尺寸在其二维坐标中保持不变。",
|
||||
"Choose…": "选择…",
|
||||
"Circular": "圆周",
|
||||
"Circumference": "周长",
|
||||
"Close draft": "关闭草稿",
|
||||
"Collapse": "折叠",
|
||||
"Collisions": "碰撞",
|
||||
"Colour": "颜色",
|
||||
"Command": "命令",
|
||||
"Comment": "注释",
|
||||
"Component 1": "组件 1",
|
||||
"Component 2": "组件 2",
|
||||
"Component1": "组件1",
|
||||
"Components": "组件",
|
||||
"Config Table": "配置表",
|
||||
"Configuration": "配置",
|
||||
"Configuration rules": "配置规则",
|
||||
@@ -86,6 +104,7 @@
|
||||
"Contact sets": "接触集",
|
||||
"Continue on GitHub": "在GitHub中继续",
|
||||
"Continuity": "连续性",
|
||||
"Control Radius": "控制半径",
|
||||
"Copy Error Message": "复制错误消息",
|
||||
"Copy Warning Message": "复制警告消息",
|
||||
"Copy all licences": "复制所有许可证",
|
||||
@@ -95,8 +114,11 @@
|
||||
"Corner Relief": "拐角释放",
|
||||
"Corner Relief Shape": "拐角释放形状",
|
||||
"Counterbore depth": "沉孔深度",
|
||||
"Counterbore Ø": "柱形沉孔 Ø",
|
||||
"Countersink angle": "沉头角度",
|
||||
"Countersink Ø": "锥形沉孔 Ø",
|
||||
"Create Issue — review draft": "创建 Issue——审阅草稿",
|
||||
"Create Issue…": "创建 Issue…",
|
||||
"Cross curves": "交叉曲线",
|
||||
"Cube": "立方体",
|
||||
"Current orthographic view (1:1 mm)": "当前正交视图(1:1毫米)",
|
||||
@@ -106,7 +128,11 @@
|
||||
"Curvature change (G3)": "曲率变化 (G3)",
|
||||
"Custom": "自定义",
|
||||
"Customize Toolbar": "自定义工具栏",
|
||||
"Customize…": "自定义…",
|
||||
"Cylindrical face": "圆柱面",
|
||||
"Delete": "删除",
|
||||
"Delete Selected Features…": "删除所选特征…",
|
||||
"Delete Selected Folders": "删除所选文件夹",
|
||||
"Delete all": "全部删除",
|
||||
"Delete rule": "删除规则",
|
||||
"Delete the row": "删除此行",
|
||||
@@ -117,8 +143,11 @@
|
||||
"Describe what happened before continuing.": "继续之前,请描述发生了什么。",
|
||||
"Designs you open or save appear here.": "你打开或保存的设计会显示在此处。",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "由“到对象”确定。选择“角度”可编辑角度范围。",
|
||||
"Diameter": "直径",
|
||||
"Direction": "方向",
|
||||
"Discard": "放弃",
|
||||
"Distance": "距离",
|
||||
"Distance 1": "距离 1",
|
||||
"Do not export hidden edges": "不导出隐藏线",
|
||||
"Don't ask again": "不再询问",
|
||||
"Double-click to rename": "双击重命名",
|
||||
@@ -130,44 +159,57 @@
|
||||
"Edit Profile Sketch": "编辑轮廓草图",
|
||||
"Enable rule": "启用规则",
|
||||
"Enabled": "已启用",
|
||||
"End Radius": "结束半径",
|
||||
"End bulge": "终止鼓度",
|
||||
"End continuity": "终止连续性",
|
||||
"Expand": "展开",
|
||||
"Expected result (optional)": "预期结果(可选)",
|
||||
"Explode": "爆炸",
|
||||
"Explode Text": "分解文字",
|
||||
"Export Animation…": "导出动画…",
|
||||
"Export Bend Table…": "导出折弯表…",
|
||||
"Export CSV…": "导出 CSV…",
|
||||
"Export Current View as DXF": "将当前视图导出为DXF",
|
||||
"Export Current View as DXF…": "将当前视图导出为DXF…",
|
||||
"Export DXF": "导出DXF",
|
||||
"Export DXF…": "导出DXF…",
|
||||
"Export JSON…": "导出 JSON…",
|
||||
"Export design library...": "导出设计库...",
|
||||
"Export hidden edges as dashed lines": "以虚线导出隐藏线",
|
||||
"Exported by SolidWorks, which usually saves Y up": "由 SolidWorks 导出,通常以 Y 轴向上保存",
|
||||
"Expression": "表达式",
|
||||
"Extend to meet": "延伸至相交",
|
||||
"Extents": "范围",
|
||||
"FILES": "文件",
|
||||
"FLAT": "展开",
|
||||
"Face": "面",
|
||||
"Face Appearance": "面外观",
|
||||
"Face attachment lost": "面附着丢失",
|
||||
"Faces": "面",
|
||||
"Faces / edges / vertices": "面 / 边 / 顶点",
|
||||
"Faces to split": "要分割的面",
|
||||
"Fastener": "紧固件",
|
||||
"Favorites": "收藏",
|
||||
"Favourite": "收藏",
|
||||
"Feature": "特征",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "许多 CAD 程序的文件以 Y 为上轴,因此在此处打开时会平躺(Z 朝上)。",
|
||||
"Filter": "筛选",
|
||||
"Filter…": "筛选…",
|
||||
"Find in Browser": "在浏览器中查找",
|
||||
"Finish": "完成",
|
||||
"First plane": "第一个平面",
|
||||
"Fit and G2 corners require Constant radius.": "拟合和 G2 拐角需要 Constant radius。",
|
||||
"Fixed Face": "固定面",
|
||||
"Flat Pattern": "展开图",
|
||||
"Flat size": "平板尺寸",
|
||||
"Follow camera (Look At sketches)": "跟随相机(注视草图)",
|
||||
"Follow the camera (Look At sketches)": "跟随相机(注视草图)",
|
||||
"Font": "字体",
|
||||
"From": "起点",
|
||||
"From Bodies": "从实体创建",
|
||||
"From edges": "起始边",
|
||||
"Full Length": "全长",
|
||||
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 拐角需要恒定半径和 Exact fit。尚不支持构建 G2 拐角。",
|
||||
"Gap": "间隙",
|
||||
"Grid": "网格",
|
||||
"HISTORY": "历史",
|
||||
@@ -175,6 +217,7 @@
|
||||
"Hidden edges": "隐藏线",
|
||||
"History layout": "历史布局",
|
||||
"History names": "历史名称",
|
||||
"Hole (optional)": "孔(可选)",
|
||||
"Holes": "孔",
|
||||
"Holes (edges on them)": "孔(孔上的边)",
|
||||
"Home view": "主视图",
|
||||
@@ -185,14 +228,20 @@
|
||||
"In place (surfaces)": "原位(曲面)",
|
||||
"In plane": "在平面内",
|
||||
"Inside": "内侧",
|
||||
"Instant": "即时",
|
||||
"Interface font": "界面字体",
|
||||
"Interferes": "发生干涉",
|
||||
"Invert": "反转",
|
||||
"Isocurve": "等参线",
|
||||
"Issue title": "Issue 标题",
|
||||
"Items": "项目",
|
||||
"JOINTS": "关节",
|
||||
"Joint Origin1": "接头原点1",
|
||||
"Keep (points on it)": "保留(其上的点)",
|
||||
"Keep Tools": "保留工具",
|
||||
"Keep of the other surface": "另一曲面保留的部分",
|
||||
"Key a joint's value at this step": "在此步骤为接头值设置关键帧",
|
||||
"Keys": "关键帧",
|
||||
"LIBRARY": "库",
|
||||
"LINKS": "链接",
|
||||
"Later": "稍后",
|
||||
@@ -203,9 +252,14 @@
|
||||
"Letter Spacing": "字间距",
|
||||
"Library": "零件库",
|
||||
"Library file": "库文件",
|
||||
"Limits": "限制",
|
||||
"Line Spacing": "行间距",
|
||||
"Line Type": "线型",
|
||||
"Listing the installed fonts…": "正在列出已安装字体…",
|
||||
"Load library": "加载库",
|
||||
"Local library": "本地库",
|
||||
"Loop": "循环",
|
||||
"M5 or M5x0.5": "M5 或 M5x0.5",
|
||||
"MANAGE": "管理",
|
||||
"MATERIALS": "材料",
|
||||
"MODEL": "模型",
|
||||
@@ -216,14 +270,20 @@
|
||||
"Matched restrictions check the final values.": "满足条件的限制规则检查最终值。",
|
||||
"Material": "材质",
|
||||
"Max": "最大",
|
||||
"Maximum": "最大",
|
||||
"Mesh body": "网格实体",
|
||||
"Mid Radius": "中间半径",
|
||||
"Middle double-click": "中键双击",
|
||||
"Min": "最小",
|
||||
"Minimum": "最小",
|
||||
"Mirror Plane": "镜像平面",
|
||||
"Mitre": "斜接",
|
||||
"Model Parameters": "模型参数",
|
||||
"Modeled": "已建模",
|
||||
"More toolbar panels": "更多工具栏面板",
|
||||
"Motion": "运动",
|
||||
"Mouse gestures": "鼠标手势",
|
||||
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "鼠标手势仅在 SolidWorks 鼠标模式下有效(滚轮旋转,Ctrl + 滚轮平移)。",
|
||||
"Move / Copy": "移动 / 复制",
|
||||
"Move Type": "移动类型",
|
||||
"Move down": "下移",
|
||||
@@ -246,8 +306,11 @@
|
||||
"Not available yet": "暂不可用",
|
||||
"Notch": "缺口",
|
||||
"OUTPUT": "输出",
|
||||
"Of": "的值",
|
||||
"On your computer": "在此计算机上",
|
||||
"Opacity": "不透明度",
|
||||
"Open GitHub draft": "打开 GitHub 草稿",
|
||||
"Open library...": "打开库...",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "请在桌面应用中打开本地库。您可以在此导出当前设计的库。",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "使用 Rust 编写的开源参数化 3D CAD。基于 MIT OR Apache-2.0 许可。",
|
||||
"Opens your browser and copies the report text for you to paste into GitHub.": "打开浏览器,并复制报告文本以便粘贴到GitHub。",
|
||||
@@ -269,8 +332,10 @@
|
||||
"Picked": "所选",
|
||||
"Picked point": "选取的点",
|
||||
"Pitch": "螺距",
|
||||
"Pivot": "枢轴",
|
||||
"Place at the origin instead": "改为放置在原点",
|
||||
"Plastic Rules": "塑料规则",
|
||||
"Play the joint's motion in the view": "在视图中播放接头运动",
|
||||
"Playback": "播放",
|
||||
"Plot": "图表",
|
||||
"Position": "位置",
|
||||
@@ -278,14 +343,18 @@
|
||||
"Position (G0)": "位置 (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "沿边的位置,起点端为 0,另一端为 1",
|
||||
"Preferences › Design turns the offer back on": "可在 首选项 › 设计 中重新开启此提示",
|
||||
"Preview": "预览",
|
||||
"Preview Row": "预览行",
|
||||
"Preview leaves the current design unchanged.": "预览不会更改当前设计。",
|
||||
"Profile / Edges": "轮廓 / 边",
|
||||
"Profiles or curves": "轮廓或曲线",
|
||||
"Profiles/Curves": "轮廓/曲线",
|
||||
"Quantity": "物理量",
|
||||
"RECENT": "最近使用",
|
||||
"REFERENCE": "参考",
|
||||
"RULED SURFACE": "直纹曲面",
|
||||
"RULES": "规则",
|
||||
"Radius": "半径",
|
||||
"Radius Fit": "半径拟合",
|
||||
"Ratio": "比率",
|
||||
"Reattach selected face": "重新附着所选面",
|
||||
@@ -294,6 +363,8 @@
|
||||
"Rectangular": "矩形",
|
||||
"Reference face": "参考面",
|
||||
"Remove Tools": "移除工具",
|
||||
"Remove condition": "删除条件",
|
||||
"Remove from Folder": "从文件夹中移除",
|
||||
"Remove from list": "从列表移除",
|
||||
"Remove override": "移除覆盖",
|
||||
"Remove private design details or paths before sharing.": "共享前请移除私密设计细节或路径。",
|
||||
@@ -301,6 +372,9 @@
|
||||
"Remove the column": "移除此列",
|
||||
"Remove the track": "移除轨迹",
|
||||
"Remove this point": "移除此点",
|
||||
"Rename Feature": "重命名特征",
|
||||
"Rename Folder": "重命名文件夹",
|
||||
"Replace existing library file": "替换现有库文件",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "报告缺陷 — 复核截图并开启 GitHub 草稿",
|
||||
"Report text copied. Paste it into GitHub and submit your report there.": "报告文本已复制。请在GitHub中粘贴并提交。",
|
||||
"Reports are public. A GitHub account is required.": "报告将公开发布。您需要GitHub账户。",
|
||||
@@ -309,6 +383,7 @@
|
||||
"Restore View": "恢复视图",
|
||||
"Restore outer boundary": "恢复外边界",
|
||||
"Restore the inherited component or physical material appearance": "还原继承的组件或物理材质外观",
|
||||
"Restore this studio's tools": "恢复此工作室的工具",
|
||||
"Result": "结果",
|
||||
"Retake screenshot": "重新截图",
|
||||
"Reverse": "反向",
|
||||
@@ -319,7 +394,13 @@
|
||||
"Revert Position": "恢复位置",
|
||||
"Review the report and screenshot before opening GitHub.": "打开 GitHub 前请审阅报告和截图。",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "请在GitHub中检查报告,然后点击Submit new issue发布。",
|
||||
"Rib Length": "筋板长度",
|
||||
"Rib Offset": "筋板偏移",
|
||||
"Rib Thickness": "筋板厚度",
|
||||
"Ribs": "筋板",
|
||||
"Rim Edge": "轮缘边",
|
||||
"Ring of tools on the right button": "右键工具环",
|
||||
"Roll Back Here": "回滚到此处",
|
||||
"Roll History Marker Here": "将历史标记滚动到此处",
|
||||
"Round": "圆角",
|
||||
"Rule": "规则",
|
||||
@@ -338,13 +419,18 @@
|
||||
"Search": "搜索",
|
||||
"Search commands": "搜索命令",
|
||||
"Search this library": "搜索此库",
|
||||
"Second plane": "第二个平面",
|
||||
"Section": "剖面",
|
||||
"Sections (one sketch each)": "截面(每个一个草图)",
|
||||
"Select Other": "选择其他",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。双击中键转到主视图(首选项 › 导航)。",
|
||||
"Select bodies (a face picks its body)": "选择实体(单击面即选择其所属实体)",
|
||||
"Select bodies or faces instead": "改为选择实体或面",
|
||||
"Select edges": "选择边",
|
||||
"Selection filter": "选择筛选",
|
||||
"Semantic faces share their appearance across instances.": "语义面在各实例间共享外观。",
|
||||
"Send your report on GitHub": "在 GitHub 上发送报告",
|
||||
"Shaded": "着色",
|
||||
"Shape": "形状",
|
||||
"Sheet Rules": "钣金规则",
|
||||
"Show Agent Cursor": "显示代理光标",
|
||||
@@ -362,6 +448,7 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft 在这些设计保存前已关闭。可以打开它们的上次自动保存。",
|
||||
"Specialise": "专用操作",
|
||||
"Speed": "速度",
|
||||
"Splitting tool": "分割工具",
|
||||
"Stand it up": "将其立起",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "将其立起:绕 X 轴旋转四分之一周,作为一个可撤销的移动特征。",
|
||||
"Stand this part up?": "立起此零件?",
|
||||
@@ -372,16 +459,22 @@
|
||||
"Steps to reproduce (optional)": "重现步骤(可选)",
|
||||
"Stitch the kept pieces": "缝合保留的部分",
|
||||
"Stitch to both bodies": "缝合到两个实体",
|
||||
"Studio": "工作室",
|
||||
"Study": "研究",
|
||||
"Style": "样式",
|
||||
"Summary": "摘要",
|
||||
"Suppress": "抑制",
|
||||
"Suppressed": "已抑制",
|
||||
"Surface": "曲面",
|
||||
"Surface, or faces to turn over": "曲面,或要翻转的面",
|
||||
"Swap Ends": "交换端点",
|
||||
"TRIM": "修剪",
|
||||
"Tangent (G1)": "相切 (G1)",
|
||||
"Tangent Chain": "相切链",
|
||||
"Tangent chain": "相切链",
|
||||
"Tap drill diameter": "攻丝底孔直径",
|
||||
"Target": "目标",
|
||||
"Target Body": "目标实体",
|
||||
"Tear": "撕裂",
|
||||
"Technical details": "技术详情",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "虚线草图是已保存的平面引导。选择一个新面以修复附着。",
|
||||
@@ -396,24 +489,31 @@
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "此子项是共享的。位置更改会应用到其父项的每个副本。",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "此面无法提供持久附着。请选择其他面或显式使用固定平面。",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "此图片尚未附加到GitHub。请复制或保存图片,然后在GitHub中附加。",
|
||||
"To": "终点",
|
||||
"To Offset": "偏移",
|
||||
"To edges": "终止边",
|
||||
"Tolerance": "公差",
|
||||
"Too close": "过近",
|
||||
"Tool Bodies": "工具实体",
|
||||
"Tools": "工具",
|
||||
"Tools, clockwise from the top": "工具,从顶部顺时针排列",
|
||||
"Trimming tool (plane, face or curves)": "修剪工具(平面、面或曲线)",
|
||||
"Turn off section analysis before exporting the view.": "请关闭截面分析后再导出视图。",
|
||||
"Type": "类型",
|
||||
"UNTRIM": "取消修剪",
|
||||
"Unit": "单位",
|
||||
"Unsuppress": "取消抑制",
|
||||
"Updating the preview": "正在更新预览",
|
||||
"Use active author placement": "使用活动作者放置",
|
||||
"Use fixed plane": "使用固定平面",
|
||||
"Use saved fixed plane": "使用已保存的固定平面",
|
||||
"Use selected face as fixed plane": "使用所选面作为固定平面",
|
||||
"Use the SolidWorks mouse mode": "使用 SolidWorks 鼠标模式",
|
||||
"User Parameters": "用户参数",
|
||||
"VIEW": "视图",
|
||||
"VIEWS": "视图",
|
||||
"Value": "值",
|
||||
"Variable Radius": "可变半径",
|
||||
"Velocity": "速度",
|
||||
"Vertices": "顶点",
|
||||
"View navigator": "视图导航器",
|
||||
@@ -427,13 +527,44 @@
|
||||
"When": "条件",
|
||||
"Width": "宽度",
|
||||
"Will fail": "将失败",
|
||||
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "开启鼠标手势后,右键不再旋转视图:按住右键并移动以打开工具环,然后触碰某个工具的区域。普通右键单击仍会打开菜单。",
|
||||
"X": "X",
|
||||
"X Angle": "X 角度",
|
||||
"X angle": "X 角度",
|
||||
"X distance": "X 距离",
|
||||
"Y": "Y",
|
||||
"Y Angle": "Y 角度",
|
||||
"Y angle": "Y 角度",
|
||||
"Y distance": "Y 距离",
|
||||
"Z": "Z",
|
||||
"Z Angle": "Z 角度",
|
||||
"Z Distance": "Z 距离",
|
||||
"Z angle": "Z 角度",
|
||||
"Z distance": "Z 距离",
|
||||
"Zebra": "斑马纹",
|
||||
"ai.prefs.availability": "助手控制",
|
||||
"ai.prefs.background_commands": "在后台执行代理命令",
|
||||
"ai.prefs.follow_camera": "将相机转向草图",
|
||||
"ai.prefs.follow_camera_off": "相机自动跟随已关闭。",
|
||||
"ai.prefs.follow_camera_on": "相机将转向代理创建的草图。",
|
||||
"ai.prefs.intro": "设置代理、权限及其工作方式。",
|
||||
"ai.prefs.make_default": "设为默认",
|
||||
"ai.prefs.no_agents": "尚无代理。添加代理或刷新以查找已安装的代理。",
|
||||
"ai.prefs.no_saved_approvals": "没有已保存的批准。",
|
||||
"ai.prefs.override_all_warning": "此代理可在不询问的情况下保存、覆盖、导出或替换设计。",
|
||||
"ai.prefs.permission_all": "全部允许",
|
||||
"ai.prefs.permission_all_summary": "可用的 SolidCraft 工具无需询问即可运行。",
|
||||
"ai.prefs.permission_ask": "更改前询问",
|
||||
"ai.prefs.permission_ask_summary": "每次设计更改和文件操作均需批准。",
|
||||
"ai.prefs.permission_edits": "允许模型编辑",
|
||||
"ai.prefs.permission_edits_summary": "模型编辑自动执行;文件及破坏性操作仍需询问。",
|
||||
"ai.prefs.permission_scope": "仅适用于 SolidCraft 工具。代理的终端和文件编辑使用其自身权限。",
|
||||
"ai.prefs.pointer_preview_hidden": "指针已隐藏",
|
||||
"ai.prefs.pointer_preview_speed": "代理指针 · {speed}",
|
||||
"ai.prefs.pointer_speed": "移动速度",
|
||||
"ai.prefs.privacy_note": "对话仅发送给所选代理或端点。端点密钥保存在系统钥匙串中。",
|
||||
"ai.prefs.show_pointer": "显示代理指针",
|
||||
"ai.prefs.turn_limit_hint": "助手针对一条消息可调用的设计更改工具次数上限。",
|
||||
"angle, height": "角度、高度",
|
||||
"appearance.dielectric_ior": "电介质折射率",
|
||||
"appearance.metallic": "金属度",
|
||||
@@ -479,8 +610,10 @@
|
||||
"cmd.component_activate": "激活组件",
|
||||
"cmd.component_break_link": "断开链接",
|
||||
"cmd.component_capture_position": "捕获位置",
|
||||
"cmd.component_copies": "查找相同副本",
|
||||
"cmd.component_create": "新建组件",
|
||||
"cmd.component_delete": "删除组件",
|
||||
"cmd.component_extract": "提取到文件",
|
||||
"cmd.component_from_bodies": "由实体创建组件",
|
||||
"cmd.component_insert_design": "插入设计",
|
||||
"cmd.component_links": "列出链接",
|
||||
@@ -493,6 +626,7 @@
|
||||
"cmd.component_properties": "组件物理属性",
|
||||
"cmd.component_relink": "重新链接源文件",
|
||||
"cmd.component_rename": "重命名组件",
|
||||
"cmd.component_replace_with_link": "替换为链接",
|
||||
"cmd.component_revert_position": "恢复位置",
|
||||
"cmd.component_unlock": "解锁内容",
|
||||
"cmd.component_update_links": "更新链接",
|
||||
@@ -610,6 +744,7 @@
|
||||
"cmd.motion_plot": "运动研究图表",
|
||||
"cmd.motion_study": "运动研究",
|
||||
"cmd.motion_trace": "运动轨迹",
|
||||
"cmd.motion_trace_edit": "编辑运动轨迹",
|
||||
"cmd.motion_trace_keep": "保留运动轨迹",
|
||||
"cmd.occurrence_copy": "粘贴(实例)",
|
||||
"cmd.occurrence_ground": "固定",
|
||||
@@ -889,19 +1024,57 @@
|
||||
"help.solid.revolve": "将草图轮廓绕一条轴旋转,生成圆形的实体。用于轴、旋钮、碗等围绕中心线呈圆形的零件。",
|
||||
"help.solid.shell": "将实体掏空,留下指定厚度的壁,还可以打开所选的面。用于盒子、盖子等薄壁零件。",
|
||||
"help.third_party": "第三方许可证",
|
||||
"home.back": "返回 {name}",
|
||||
"home.crashes": "崩溃报告",
|
||||
"home.design_workspace": "设计",
|
||||
"home.documentation": "文档",
|
||||
"home.export": "导出",
|
||||
"home.export_hint": "为当前设计选择一种格式。",
|
||||
"home.export_json_hint": "可编辑的设计和命令历史记录",
|
||||
"home.export_mesh_hint": "用于打印或共享的网格",
|
||||
"home.export_step_hint": "精确的实体几何",
|
||||
"home.import": "导入…",
|
||||
"home.import_hint": "将 STEP、IGES 或网格导入新设计",
|
||||
"home.in_this_browser": "在此浏览器中",
|
||||
"home.missing": "文件不存在",
|
||||
"home.month_01": "1月",
|
||||
"home.month_02": "2月",
|
||||
"home.month_03": "3月",
|
||||
"home.month_04": "4月",
|
||||
"home.month_05": "5月",
|
||||
"home.month_06": "6月",
|
||||
"home.month_07": "7月",
|
||||
"home.month_08": "8月",
|
||||
"home.month_09": "9月",
|
||||
"home.month_10": "10月",
|
||||
"home.month_11": "11月",
|
||||
"home.month_12": "12月",
|
||||
"home.new_inches": "新建设计(英寸)",
|
||||
"home.new_inches_hint": "英寸 · 在设计工作区中打开",
|
||||
"home.new_mm": "新建设计",
|
||||
"home.new_mm_hint": "毫米 · 在设计工作区中打开",
|
||||
"home.open": "打开…",
|
||||
"home.opened_days_ago": "{count} 天前打开",
|
||||
"home.opened_hours_ago": "{count} 小时前打开",
|
||||
"home.opened_just_now": "刚刚打开",
|
||||
"home.opened_minutes_ago": "{count} 分钟前打开",
|
||||
"home.open_hint": ".solidcraft 设计文件 ({shortcut})",
|
||||
"home.open_now": "立即打开",
|
||||
"home.opened_days_ago": "{count} 天前",
|
||||
"home.opened_hours_ago": "{count} 小时前",
|
||||
"home.opened_just_now": "刚刚",
|
||||
"home.opened_minutes_ago": "{count} 分钟前",
|
||||
"home.opening": "正在打开…",
|
||||
"home.properties": "设计属性",
|
||||
"home.recent": "最近使用",
|
||||
"home.recent_empty": "您打开或保存的设计将显示在这里。",
|
||||
"home.recovery_count": "可恢复的设计: {count} 个",
|
||||
"home.recovery_empty": "没有可恢复的设计。",
|
||||
"home.recovery_hint": "SolidCraft 在这些设计保存前已关闭。",
|
||||
"home.report_issue": "报告问题",
|
||||
"home.samples": "示例",
|
||||
"home.settings": "设置",
|
||||
"home.short_date": "{month}{day}日",
|
||||
"home.shortcuts": "键盘快捷键",
|
||||
"home.start_hint": "创建设计、打开最近的文件或浏览示例。",
|
||||
"home.whats_new": "新功能",
|
||||
"home.yesterday": "昨天",
|
||||
"install.button_tip": "将 SolidCraft 安装为应用:在独立窗口中打开,离线也能使用",
|
||||
"install.dock_1": "选择“文件”›“添加到程序坞”。",
|
||||
"install.dock_2": "点按“添加”。",
|
||||
@@ -959,8 +1132,15 @@
|
||||
"motion.to_end": "到结尾",
|
||||
"motion.to_start": "到开头",
|
||||
"motion.track_name": "轨道 {number}",
|
||||
"nav.alias.help": "Shift+Alt+左键拖动旋转,Shift+Alt+中键拖动平移,Shift+Alt+右键拖动缩放;中键拖动也可平移,右键拖动旋转",
|
||||
"nav.blender.help": "中键拖动旋转,Shift+中键拖动平移,Ctrl+中键拖动缩放;右键拖动不会移动相机",
|
||||
"nav.free_orbit.help": "鼠标和3D鼠标可保持滚转并越过两个极点旋转。Shift轴吸附将被禁用。",
|
||||
"nav.free_orbit.label": "自由旋转",
|
||||
"nav.fusion.help": "中键拖动平移,Shift+中键拖动旋转,右键拖动旋转",
|
||||
"nav.inventor.help": "中键拖动平移,Shift+中键拖动旋转,Ctrl+中键拖动缩放,右键拖动旋转",
|
||||
"nav.openscad.help": "在空白处左键拖动旋转,Shift+左键拖动框选,拖动草图几何可编辑;右键或中键拖动平移,Shift+右键或 Shift+中键拖动缩放",
|
||||
"nav.solidworks.help": "中键拖动旋转,Ctrl+中键拖动平移,Shift+中键拖动缩放,右键拖动旋转",
|
||||
"nav.tinkercad.help": "右键拖动旋转,中键或 Shift+右键拖动平移",
|
||||
"nav.touchpad.help": "按住 Shift 并移动指针可平移;按住 Alt/Option 并移动可旋转。无需按鼠标按钮。垂直滚动可缩放。",
|
||||
"nav.touchpad.label": "触控板",
|
||||
"nav.touchpad.orbit": "旋转(按住 Alt/Option 并移动,无需鼠标按钮)",
|
||||
@@ -983,8 +1163,130 @@
|
||||
"pattern.flip_direction": "反转方向",
|
||||
"pick a hole to seat it, or OK places it at the origin": "选择一个孔放置,或单击确定放置在原点",
|
||||
"pick one or two vertices, edges or faces": "选择一个或两个顶点、边或面",
|
||||
"prefs.add_item": "添加项目",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option 也可自由拖动操纵器箭头",
|
||||
"prefs.anti_aliasing_note": "平滑锯齿边缘。",
|
||||
"prefs.applies_on_restart": "重启后生效",
|
||||
"prefs.assistant": "助手",
|
||||
"prefs.axes": "坐标轴",
|
||||
"prefs.axis": "轴",
|
||||
"prefs.bottom": "底部",
|
||||
"prefs.browser": "浏览器",
|
||||
"prefs.built_in": "内置",
|
||||
"prefs.button_left": "左键拖动",
|
||||
"prefs.button_middle": "中键拖动",
|
||||
"prefs.button_right": "右键拖动",
|
||||
"prefs.choose_font": "选择字体文件…",
|
||||
"prefs.click": "单击",
|
||||
"prefs.contact_shading": "柔和接触阴影",
|
||||
"prefs.contact_shading_note": "在零件相近处添加柔和阴影。",
|
||||
"prefs.context_menu": "上下文菜单",
|
||||
"prefs.context_menu_ring": "上下文菜单 · 按住显示环形菜单",
|
||||
"prefs.cpu": "CPU(软件)",
|
||||
"prefs.crash_reports": "打开崩溃报告…",
|
||||
"prefs.crash_reports_note": "报告与重现问题的设计和命令一起保存在此计算机上。",
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "曲面",
|
||||
"prefs.curves": "曲面",
|
||||
"prefs.dead_zone_note": "斜线区域为死区:此范围内的移动将被忽略。",
|
||||
"prefs.default": "默认",
|
||||
"prefs.design": "设计默认值",
|
||||
"prefs.design_note": "新建设计的初始设置。已打开的设计保留各自的设置。",
|
||||
"prefs.devices": "3D 鼠标",
|
||||
"prefs.display": "显示",
|
||||
"prefs.edges": "边缘与着色",
|
||||
"prefs.extend": "扩展",
|
||||
"prefs.extra_navigation": "在空白处按住Ctrl并用鼠标左键拖动可旋转;按住空格键可平移。",
|
||||
"prefs.follows_os": "跟随操作系统",
|
||||
"prefs.font": "字体",
|
||||
"prefs.font_fallback": "所选字体缺少的字符会使用内置字体,避免标签显示为空方框。",
|
||||
"prefs.font_sample": "字体示例 · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "无法读取所选字体。",
|
||||
"prefs.general": "常规",
|
||||
"prefs.general_note": "语言、保存和更新。",
|
||||
"prefs.gesture_orbit": "环绕",
|
||||
"prefs.gesture_pan": "平移",
|
||||
"prefs.gesture_zoom": "缩放",
|
||||
"prefs.graphics": "图形",
|
||||
"prefs.graphics_note": "在速度与平滑度之间取舍。每个选项均以示例展示效果。",
|
||||
"prefs.graphics_presets_note": "预设会同时设置以下选项。",
|
||||
"prefs.history": "历史",
|
||||
"prefs.hold_stop_snapping": "按住以停用吸附",
|
||||
"prefs.icons_only": "仅图标",
|
||||
"prefs.imported_file": "已导入文件",
|
||||
"prefs.input": "输入",
|
||||
"prefs.interface": "界面",
|
||||
"prefs.interface_language": "界面语言",
|
||||
"prefs.interface_note": "SolidCraft 的外观和面板布局。",
|
||||
"prefs.interval_minutes": "{n} 分钟",
|
||||
"prefs.labels": "标签",
|
||||
"prefs.layout": "布局",
|
||||
"prefs.layout_note": "紧凑模式将面板停靠在窗口边缘。浏览器半透明仅适用于浮动模式。助手面板的位置在助手页面中设置。",
|
||||
"prefs.left": "左侧",
|
||||
"prefs.left_click": "左键单击",
|
||||
"prefs.live_input": "实时输入",
|
||||
"prefs.live_preview": "实时预览",
|
||||
"prefs.minutes": "分钟",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "鼠标与触控板",
|
||||
"prefs.mouse_heading": "鼠标",
|
||||
"prefs.mouse_mapping": "鼠标映射",
|
||||
"prefs.navigation": "导航",
|
||||
"prefs.navigation_note": "鼠标、触控板和3D鼠标如何移动视图。",
|
||||
"prefs.not_on_gpu": "此 GPU 不支持",
|
||||
"prefs.on": "开启",
|
||||
"prefs.opaque": "不透明",
|
||||
"prefs.orbit_snap": "旋转时按住Shift可捕捉到坐标轴。",
|
||||
"prefs.orientation": "方向",
|
||||
"prefs.overall_speed": "整体速度",
|
||||
"prefs.preview_choices": "所选设置预览",
|
||||
"prefs.quality_default": "默认",
|
||||
"prefs.quality_fastest": "最快",
|
||||
"prefs.quality_slowest": "最慢",
|
||||
"prefs.quality_smoother": "更平滑",
|
||||
"prefs.reports_saved": "已保存 {n} 份",
|
||||
"prefs.right_click": "右键单击",
|
||||
"prefs.saving": "自动保存和恢复",
|
||||
"prefs.scale_note": "也可在窗口中使用 Ctrl + / Ctrl −。100% 时按钮高度为 28 像素。",
|
||||
"prefs.scale_text": "缩放与文字",
|
||||
"prefs.select": "选择",
|
||||
"prefs.select_one_item": "选择一个项目",
|
||||
"prefs.selecting": "选择",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift 仅添加;Ctrl / Cmd 切换选择",
|
||||
"prefs.shift_add_note": "开启:Shift 添加,Ctrl / Cmd 切换选择。关闭(默认):两者均切换选择。",
|
||||
"prefs.shortcuts": "快捷键",
|
||||
"prefs.sketch_selection": "草图与选择",
|
||||
"prefs.sketch_selection_heading": "草图和选择",
|
||||
"prefs.sketch_selection_note": "草图如何捕捉,以及单击如何选择。",
|
||||
"prefs.sketching": "草图",
|
||||
"prefs.snap_drawing_note": "在绘制或拖动草图点时使用。",
|
||||
"prefs.source": "来源",
|
||||
"prefs.speed": "速度",
|
||||
"prefs.stand_up_note": "以 Z 轴向上打开 STEP 或 IGES 文件后,询问是否绕 X 轴旋转 90°。",
|
||||
"prefs.status_done": "完成",
|
||||
"prefs.status_error": "错误",
|
||||
"prefs.status_selected": "已选择",
|
||||
"prefs.status_warning": "警告",
|
||||
"prefs.supersampling_brief": "以更高分辨率绘制后滤波缩小:更清晰,但负载更高。",
|
||||
"prefs.tab_accent_note": "强调色跟随当前选项卡",
|
||||
"prefs.theme_file": "主题文件",
|
||||
"prefs.timeline_half_hour": "−30 分钟",
|
||||
"prefs.timeline_now": "现在",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
"prefs.toggle_item": "添加项目,或取消选择",
|
||||
"prefs.toolbar": "工具栏",
|
||||
"prefs.translucent": "半透明",
|
||||
"prefs.under_browser": "浏览器下方",
|
||||
"prefs.under_cursor": "光标下方",
|
||||
"prefs.units_precision": "单位与精度",
|
||||
"prefs.updates": "更新",
|
||||
"prefs.view": "视图行为",
|
||||
"prefs.viewport": "视口",
|
||||
"prefs.wheel": "滚轮",
|
||||
"prefs.wheel_trackpad": "滚轮与触控板",
|
||||
"prefs.workspace": "工作区",
|
||||
"prefs.zoom_toward_cursor": "朝光标缩放",
|
||||
"properties.title": "属性:{name}",
|
||||
"recovery.days_ago": "{count} 天前",
|
||||
"recovery.detail": "{file} · {features} 个特征 · 自动保存于 {when}",
|
||||
@@ -1055,8 +1357,8 @@
|
||||
"search.suggest": "您是否想找 {label}?",
|
||||
"search.try": "输入命令名称、此设计中的特征或参数。要带值运行命令,请输入其ID和JSON对象:{example}",
|
||||
"search.valid": "有效",
|
||||
"settings.display_quality_note": "级别越精细,曲面和边显示越平滑,但网格生成耗时更长。只影响视口,不影响设计或导出。",
|
||||
"settings.frame_rate_note": "在视图右下角显示每秒帧数。开启时视图会持续重绘。",
|
||||
"settings.display_quality_note": "仅影响显示,不影响设计和导出。",
|
||||
"settings.frame_rate_note": "在视图右下角显示每秒帧数。",
|
||||
"settings.measure_axis_distances": "显示 X/Y/Z 距离",
|
||||
"settings.measure_axis_distances_help": "测量对话框打开时,在总距离之外显示沿世界坐标轴方向的尺寸线和距离。",
|
||||
"settings.msaa_applies_on_restart": "抗锯齿将在下次启动 SolidCraft 时生效",
|
||||
@@ -1065,6 +1367,7 @@
|
||||
"settings.reset_intro": "这些设置在此计算机上无法正常工作,会导致 SolidCraft 无法启动,因此已更改:",
|
||||
"settings.reset_title": "设置已更改",
|
||||
"settings.supersampling_note": "以更高分辨率绘制视图再缩小:在低于 4K 的屏幕上边、线和网格更平滑。会消耗 GPU 时间:1.5× 时约 2.25 倍像素,2× 时 4 倍。",
|
||||
"sheet.no_body": "没有钣金实体:请使用法兰 > Base 创建,或使用转换为钣金将板材转换为钣金(选择其较大的平面)。",
|
||||
"shortcut.json_error": "参数必须采用 JSON 格式:{error}",
|
||||
"shortcut.no_command": "没有命令 `{id}`;JSON 前的文本必须是命令 ID",
|
||||
"shortcut.pin": "固定到 S 框顶部",
|
||||
@@ -1101,6 +1404,9 @@
|
||||
"thread.designation": "螺纹标记",
|
||||
"thread.iso_metric": "ISO 公制",
|
||||
"thread.select_size": "选择尺寸",
|
||||
"timeline.delete_after_marker_one": "删除历史标记之后的 1 个特征",
|
||||
"timeline.delete_after_marker_other": "删除历史标记之后的 {count} 个特征",
|
||||
"timeline.delete_many": "删除所选的 {count} 个特征",
|
||||
"timeline.group": "新建特征文件夹",
|
||||
"timeline.move_many": "移动时间线项目",
|
||||
"timeline.move_to_group": "将特征移至文件夹",
|
||||
@@ -1179,6 +1485,7 @@
|
||||
"ui.agent_chat_files_left_out": "一条消息最多发送 {max} 个文件;未发送:{files}",
|
||||
"ui.agent_chat_files_loading": "正在等待文件读取完成",
|
||||
"ui.agent_chat_files_max": "一条消息最多附加 {max} 个文件",
|
||||
"ui.agent_chat_flatpak_none_found": "未找到编程代理。默认情况下,Flatpak 沙箱无法访问主目录中安装的代理,也无法访问网络。要允许使用通过 nvm 或 ~/.local/bin 安装的 Claude Code 及其 ACP 适配器,请在终端运行以下命令,然后重新启动 SolidCraft。此命令授予对所列程序和登录文件的访问权限以及网络访问权限:",
|
||||
"ui.agent_chat_import_failed": "{name}:导入失败:{error}",
|
||||
"ui.agent_chat_intro": "与你已在使用的编程智能体或你选择的模型端点聊天。它通过 SolidCraft 的工具处理打开的设计:更改即时显示,每条消息的更改可一步撤销。无需询问即可执行的范围在“首选项 > AI”中设置。",
|
||||
"ui.agent_chat_key_in_env": "已设置密钥",
|
||||
@@ -1251,8 +1558,10 @@
|
||||
"ui.ai_add": "添加",
|
||||
"ui.ai_add_agent": "添加智能体…",
|
||||
"ui.ai_add_agent_title": "添加智能体",
|
||||
"ui.ai_add_anthropic": "Anthropic 兼容 API",
|
||||
"ui.ai_add_api": "OpenAI 兼容 API",
|
||||
"ui.ai_add_custom": "自定义命令",
|
||||
"ui.ai_add_endpoint": "API 端点",
|
||||
"ui.ai_add_installed": "已安装的智能体",
|
||||
"ui.ai_add_installed_note": "为已安装的智能体添加一个自己的条目,例如使用其他参数。已安装的智能体会自动列出。",
|
||||
"ui.ai_added": "已添加",
|
||||
@@ -1281,6 +1590,7 @@
|
||||
"ui.ai_key_needs_https": "密钥只会通过 HTTPS 发送,或发送到本机。",
|
||||
"ui.ai_key_note": "保存在系统钥匙串中,绝不保存在 SolidCraft 的设置里。本地服务器可留空。",
|
||||
"ui.ai_keychain_error": "系统钥匙串拒绝:{error}",
|
||||
"ui.ai_kind": "类型",
|
||||
"ui.ai_mode_all": "全部允许",
|
||||
"ui.ai_mode_all_note": "不再询问。崩溃报告仍不可访问,文件夹限制仍然有效。",
|
||||
"ui.ai_mode_all_warning": "智能体可以不经询问保存、覆盖、导出和替换你的设计。只在你信任的智能体和已保存的设计上使用。",
|
||||
@@ -1302,6 +1612,7 @@
|
||||
"ui.ai_panel": "面板",
|
||||
"ui.ai_permissions": "权限",
|
||||
"ui.ai_preset": "预设",
|
||||
"ui.ai_protocol": "协议",
|
||||
"ui.ai_remove": "移除",
|
||||
"ui.ai_request_limit": "每条消息 24 次请求后已停止;再发送一条消息即可继续",
|
||||
"ui.ai_reset": "重置",
|
||||
@@ -1312,6 +1623,8 @@
|
||||
"ui.ai_test": "测试",
|
||||
"ui.ai_test_acp_ok": "已响应:{agent}",
|
||||
"ui.ai_test_acp_tip": "短暂启动智能体并请求一个会话,然后结束它",
|
||||
"ui.ai_test_answered": "已连接:{model} 已响应",
|
||||
"ui.ai_test_anthropic_tip": "向端点发送一条简短消息",
|
||||
"ui.ai_test_api_tip": "向端点查询其模型列表",
|
||||
"ui.ai_test_no_model": "已连接:{count} 个模型,但没有 {model}",
|
||||
"ui.ai_test_ok": "已连接:{count} 个模型",
|
||||
@@ -1469,6 +1782,9 @@
|
||||
"ui.export_keymap_tip": "将按键、S 框收藏和工具栏导出为文件",
|
||||
"ui.extend": "延伸",
|
||||
"ui.extent_type": "延伸类型",
|
||||
"ui.extract_component": "提取到文件…",
|
||||
"ui.extract_design_changed": "选择文件时设计已更改。请重新开始提取到文件。",
|
||||
"ui.extract_done": "该组件现在是独立的设计,并已在此处链接。",
|
||||
"ui.extrude": "拉伸",
|
||||
"ui.factor": "系数",
|
||||
"ui.feature_history": "特征历史",
|
||||
@@ -1500,6 +1816,7 @@
|
||||
"ui.grid_follows_view_hint": "在侧视图中,将网格显示在朝向该视图的竖直平面上,而不是地面上",
|
||||
"ui.ground_shadow": "地面阴影",
|
||||
"ui.groupSelected": "将所选编组",
|
||||
"ui.hatch": "剖面线",
|
||||
"ui.heading_add_ins": "加载项",
|
||||
"ui.heading_assemble": "装配",
|
||||
"ui.heading_box": "长方体",
|
||||
@@ -1607,6 +1924,10 @@
|
||||
"ui.length_decimals": "长度小数位",
|
||||
"ui.light": "浅色",
|
||||
"ui.line": "直线",
|
||||
"ui.link_copies": "链接相同副本",
|
||||
"ui.link_copies_keep": "保留",
|
||||
"ui.link_copies_link": "链接",
|
||||
"ui.link_copies_text": "这些组件生成的实体与 {name} 相同。要将它们改为其链接的放置吗?",
|
||||
"ui.lock": "锁定",
|
||||
"ui.loft": "放样",
|
||||
"ui.look_at": "朝向",
|
||||
@@ -1730,6 +2051,7 @@
|
||||
"ui.render": "渲染",
|
||||
"ui.renderer": "渲染器",
|
||||
"ui.repeat": "重复",
|
||||
"ui.replace_with_link_done": "这些副本现在是链接设计的放置。",
|
||||
"ui.report_a_bug": "报告缺陷",
|
||||
"ui.reset": "重置",
|
||||
"ui.reset_all": "全部重置",
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
//! Built-in physical materials: densities in g/cm³ and sRGB colours.
|
||||
|
||||
/// The default material's name: what a body without a usable material of its own is made of.
|
||||
/// Assigning it clears a body's assignment.
|
||||
pub const DEFAULT_MATERIAL: &str = "Default";
|
||||
/// Density of the default material (g/cm³).
|
||||
pub const DEFAULT_DENSITY: f64 = 1.29;
|
||||
|
||||
/// Physical materials: name and density (g/cm³).
|
||||
pub const MATERIALS: [(&str, f64); 14] = [
|
||||
("Default", DEFAULT_DENSITY),
|
||||
(DEFAULT_MATERIAL, DEFAULT_DENSITY),
|
||||
("Steel", 7.85),
|
||||
("Stainless Steel", 8.0),
|
||||
("Aluminum", 2.70),
|
||||
|
||||
@@ -16,6 +16,8 @@ struct U {
|
||||
ana_c: array<vec4<f32>, 8>,
|
||||
ana_f: vec4<f32>, // x = 1: stripe the negative draft and blocked faces
|
||||
depth: vec4<f32>, // x = 1: reversed depth (1 near, 0 far); y, z: the faces' push (`FACE_BIAS`)
|
||||
tint: vec4<f32>, // xyz added to a whole hovered body's colour before lighting
|
||||
sel: vec4<f32>, // xyz a whole selected body's colour before lighting
|
||||
};
|
||||
|
||||
fn reversed() -> bool {
|
||||
@@ -64,7 +66,138 @@ fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c:
|
||||
return o;
|
||||
}
|
||||
|
||||
/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
|
||||
/// Shared triangles (in their part's own frame) drawn at one placement: the rows of its rigid
|
||||
/// move (`r0`..`r2`, a point's coordinates are their dot products with (p, 1)) and its colour.
|
||||
/// The vertex colour (location 2) is not read.
|
||||
@vertex
|
||||
fn vs_tri_inst(
|
||||
@location(0) p: vec3<f32>,
|
||||
@location(1) n: vec3<f32>,
|
||||
@location(3) r0: vec4<f32>,
|
||||
@location(4) r1: vec4<f32>,
|
||||
@location(5) r2: vec4<f32>,
|
||||
@location(6) c: vec4<f32>,
|
||||
) -> TOut {
|
||||
let q = vec4<f32>(p, 1.0);
|
||||
let wp = vec3<f32>(dot(r0, q), dot(r1, q), dot(r2, q));
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(wp, 1.0);
|
||||
o.n = vec3<f32>(dot(r0.xyz, n), dot(r1.xyz, n), dot(r2.xyz, n));
|
||||
o.c = c;
|
||||
o.wp = wp;
|
||||
return o;
|
||||
}
|
||||
|
||||
/// A flat-shaded placement (`vs_tri_inst_flat`): its vertices are shared between triangles, so
|
||||
/// the face's normal is the triangle's own plane, worked out per fragment. The fragment gets its
|
||||
/// position from the eye (`rp`): small near what is looked at, so the plane comes out exact at
|
||||
/// any zoom however far from the origin the part stands, and few values per vertex (a tiled GPU
|
||||
/// stores every vertex's outputs: more of them cost more than the triangles' shared vertices
|
||||
/// save).
|
||||
struct FOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) rp: vec3<f32>,
|
||||
@location(1) c: vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_tri_inst_flat(
|
||||
@location(0) p: vec3<f32>,
|
||||
@location(3) r0: vec4<f32>,
|
||||
@location(4) r1: vec4<f32>,
|
||||
@location(5) r2: vec4<f32>,
|
||||
@location(6) c: vec4<f32>,
|
||||
) -> FOut {
|
||||
let q = vec4<f32>(p, 1.0);
|
||||
let wp = vec3<f32>(dot(r0, q), dot(r1, q), dot(r2, q));
|
||||
var o: FOut;
|
||||
o.pos = u.vp * vec4<f32>(wp, 1.0);
|
||||
o.rp = wp - u.eye.xyz;
|
||||
o.c = c;
|
||||
return o;
|
||||
}
|
||||
|
||||
/// The fragment of a flat-shaded placement as the other triangles' fragments are: its normal
|
||||
/// the outward normal of its triangle (toward the eye on a front face, away on a back face, as
|
||||
/// the triangles are wound outward).
|
||||
fn flat_fragment(i: FOut, front: bool) -> TOut {
|
||||
var n = cross(dpdx(i.rp), dpdy(i.rp));
|
||||
let toward = select(u.back.xyz, -i.rp, u.eye.w > 0.5);
|
||||
n = n * inverseSqrt(max(dot(n, n), 1e-30));
|
||||
if ((dot(n, toward) < 0.0) == front) { n = -n; }
|
||||
var o: TOut;
|
||||
o.pos = i.pos;
|
||||
o.n = n;
|
||||
o.c = i.c;
|
||||
o.wp = i.rp + u.eye.xyz;
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_solid_flat(i: FOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let t = flat_fragment(i, front);
|
||||
let c = solid(t, front);
|
||||
return c;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_solid_cut_flat(i: FOut, @builtin(front_facing) front: bool) -> CutOut {
|
||||
let t = flat_fragment(i, front);
|
||||
var o: CutOut;
|
||||
o.depth = pushed(cap_depth(t.wp, t.pos.z));
|
||||
o.c = solid(t, front);
|
||||
return o;
|
||||
}
|
||||
|
||||
/// Weighted blended order-independent transparency (`view/oit.rs`): a see-through fragment's
|
||||
/// colour, premultiplied and weighted by its alpha and nearness, into the accumulation target,
|
||||
/// and its alpha into the revealage target (which keeps what is behind it by 1 - alpha).
|
||||
struct Oit {
|
||||
@location(0) accum: vec4<f32>,
|
||||
@location(1) reveal: vec4<f32>,
|
||||
};
|
||||
|
||||
fn oit(c: vec4<f32>, z: f32) -> Oit {
|
||||
let a = clamp(c.a, 0.0, 1.0);
|
||||
// Depth 0 near and 1 far, whichever way the buffer runs.
|
||||
let d = select(z, 1.0 - z, reversed());
|
||||
let w = clamp(a * max(1e-2, 3e3 * pow(1.0 - d, 3.0)), 1e-2, 3e3);
|
||||
var o: Oit;
|
||||
o.accum = vec4<f32>(c.rgb * a, a) * w;
|
||||
o.reveal = vec4<f32>(a);
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_oit(i: TOut) -> Oit {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return oit(shade(i), i.pos.z);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_oit_flat(i: FOut, @builtin(front_facing) front: bool) -> Oit {
|
||||
let t = flat_fragment(i, front);
|
||||
let o = oit(shade(t), t.pos.z);
|
||||
if (clipped(t.wp)) { discard; }
|
||||
return o;
|
||||
}
|
||||
|
||||
/// A see-through flat-shaded placement (faded or glass), shaded like `fs_tri`.
|
||||
@fragment
|
||||
fn fs_tri_flat(i: FOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let t = flat_fragment(i, front);
|
||||
let c = shade(t);
|
||||
if (clipped(t.wp)) { discard; }
|
||||
return c;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_ao_depth_flat(i: FOut, @builtin(front_facing) front: bool) -> AoDepth {
|
||||
let t = flat_fragment(i, front);
|
||||
return opaque_depth(t, front);
|
||||
}
|
||||
|
||||
/// Opaque model surfaces: cut by the section plane/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
|
||||
@fragment
|
||||
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let c = solid(i, front);
|
||||
@@ -122,7 +255,9 @@ fn solid(i: TOut, front: bool) -> vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
if (!front && length(u.clip.xyz) > 0.5) {
|
||||
// The cut face in its body's colour ("from component" colouring), tinted toward the
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen.
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen; with the hatch off
|
||||
// (`u.cap.a` 0), plain in the body's own colour.
|
||||
if (u.cap.a < 0.5) { return out_color(vec4<f32>(i.c.rgb, 1.0)); }
|
||||
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
|
||||
let h = fract((i.pos.x + i.pos.y) / 9.0);
|
||||
return out_color(vec4<f32>(select(base, base * 0.6, h < 0.22), 1.0));
|
||||
@@ -150,6 +285,11 @@ struct AoDepth {
|
||||
|
||||
@fragment
|
||||
fn fs_ao_depth(i: TOut, @builtin(front_facing) front: bool) -> AoDepth {
|
||||
return opaque_depth(i, front);
|
||||
}
|
||||
|
||||
/// The AO prepass's depth of an opaque surface's fragment.
|
||||
fn opaque_depth(i: TOut, front: bool) -> AoDepth {
|
||||
if (clipped(i.wp) || length(i.n) < 0.5) { discard; }
|
||||
var depth = i.pos.z;
|
||||
if (!front && length(u.clip.xyz) > 0.5) { depth = cap_depth(i.wp, i.pos.z); }
|
||||
@@ -286,6 +426,20 @@ fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
|
||||
return shade(i);
|
||||
}
|
||||
|
||||
/// A whole hovered body, lit over the model from its own plan triangles in a lifted colour.
|
||||
@fragment
|
||||
fn fs_hl_hover(i: TOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return lit_color(i, 1.0, vec4<f32>(i.c.rgb + u.tint.rgb, i.c.a));
|
||||
}
|
||||
|
||||
/// A whole selected body, lit over the model from its own plan triangles in the selection colour.
|
||||
@fragment
|
||||
fn fs_hl_selected(i: TOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return lit_color(i, 1.0, vec4<f32>(u.sel.rgb, i.c.a));
|
||||
}
|
||||
|
||||
/// Reference planes: shaded like `fs_tri`, but never cut by the section plane.
|
||||
@fragment
|
||||
fn fs_tri_all(i: TOut) -> @location(0) vec4<f32> {
|
||||
@@ -297,9 +451,15 @@ fn shade(i: TOut) -> vec4<f32> {
|
||||
}
|
||||
|
||||
fn lit(i: TOut, visibility: f32) -> vec4<f32> {
|
||||
return lit_color(i, visibility, i.c);
|
||||
}
|
||||
|
||||
/// [`lit`] in a colour of its own: a whole highlighted body is lit in its colour lifted by the
|
||||
/// hover amount, or in the selection colour, from the model's own plan triangles.
|
||||
fn lit_color(i: TOut, visibility: f32, base: vec4<f32>) -> vec4<f32> {
|
||||
// A zero normal marks flat colour (selection fills, translucent planes).
|
||||
if (length(i.n) < 0.5) {
|
||||
return out_color(i.c);
|
||||
return out_color(base);
|
||||
}
|
||||
let v = normalize(u.back.xyz);
|
||||
var n = normalize(i.n);
|
||||
@@ -310,7 +470,7 @@ fn lit(i: TOut, visibility: f32) -> vec4<f32> {
|
||||
let h = normalize(key + v);
|
||||
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
|
||||
let k = 0.42 + 0.38 * diff + 0.25 * fill - (0.42 + 0.25 * fill) * (1.0 - visibility);
|
||||
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
|
||||
return out_color(vec4<f32>(min(base.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), base.a));
|
||||
}
|
||||
|
||||
// Lines, decals and ghosts are drawn at their own depth: opaque faces are pushed back by their
|
||||
|
||||
@@ -8,6 +8,35 @@ const fn icon_primary(rgb: [u8; 3]) -> Color32 {
|
||||
impl Tokens {
|
||||
pub const fn light() -> Tokens {
|
||||
Tokens {
|
||||
file_dim: Color32::from_rgb(58, 70, 86),
|
||||
file_recovery_well: Color32::from_rgb(247, 226, 184),
|
||||
file_action_height: 32.0,
|
||||
file_back_height: 40.0,
|
||||
file_recovery_icon: 32.0,
|
||||
file_grid_gap: 14.0,
|
||||
file_background: Color32::from_rgb(247, 249, 251),
|
||||
file_surface: Color32::WHITE,
|
||||
file_border: Color32::from_rgb(224, 229, 236),
|
||||
file_image_tint: Color32::WHITE,
|
||||
file_rail: Color32::from_rgb(35, 110, 173),
|
||||
file_rail_ink: Color32::WHITE,
|
||||
file_rail_hover: Color32::from_rgb(43, 118, 180),
|
||||
file_primary: Color32::from_rgb(227, 240, 251),
|
||||
file_primary_ink: Color32::from_rgb(26, 80, 127),
|
||||
file_primary_border: Color32::from_rgb(50, 139, 211),
|
||||
file_recovery: Color32::from_rgb(255, 246, 229),
|
||||
file_recovery_ink: Color32::from_rgb(122, 74, 5),
|
||||
file_recovery_border: Color32::from_rgb(240, 210, 154),
|
||||
file_nav_width: 248.0,
|
||||
file_nav_height: 38.0,
|
||||
file_tile_height: 84.0,
|
||||
file_tile_icon: 44.0,
|
||||
file_recent_height: 52.0,
|
||||
file_sample_image_height: 84.0,
|
||||
file_content_inset_x: 40.0,
|
||||
file_content_inset_y: 32.0,
|
||||
file_section_gap: 28.0,
|
||||
font_start_title: 28.0,
|
||||
space_2: 2.0,
|
||||
space_3: 3.0,
|
||||
space_4: 4.0,
|
||||
@@ -201,7 +230,7 @@ impl Tokens {
|
||||
],
|
||||
analysis: AnalysisColors::DEFAULT,
|
||||
icons: IconColors { highlight: Color32::WHITE, agent: Color32::from_rgb(168, 85, 247), marker_halo: Color32::from_black_alpha(150) },
|
||||
logo: [Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197), Color32::WHITE],
|
||||
logo: [Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197)],
|
||||
}
|
||||
}
|
||||
pub const fn dark() -> Tokens {
|
||||
@@ -236,6 +265,19 @@ impl Tokens {
|
||||
t.sketch_mode_tint = 0.12;
|
||||
t.sketch_footer_icon_size = 15.0;
|
||||
t.font_section_tracking = 0.04;
|
||||
t.file_dim = Color32::from_rgb(180, 187, 200);
|
||||
t.file_recovery_well = Color32::from_rgb(92, 68, 32);
|
||||
t.file_background = Color32::from_rgb(31, 35, 43);
|
||||
t.file_surface = Color32::from_rgb(39, 43, 52);
|
||||
t.file_border = Color32::from_rgb(63, 69, 82);
|
||||
t.file_rail = Color32::from_rgb(30, 80, 128);
|
||||
t.file_rail_hover = Color32::from_rgb(46, 99, 149);
|
||||
t.file_primary = Color32::from_rgb(27, 48, 72);
|
||||
t.file_primary_ink = Color32::from_rgb(156, 199, 245);
|
||||
t.file_primary_border = Color32::from_rgb(58, 136, 238);
|
||||
t.file_recovery = Color32::from_rgb(67, 51, 28);
|
||||
t.file_recovery_ink = Color32::from_rgb(245, 204, 137);
|
||||
t.file_recovery_border = Color32::from_rgb(115, 88, 43);
|
||||
t.app_bar = Color32::from_rgb(28, 31, 38);
|
||||
t.toolbar = Color32::from_rgb(39, 43, 52);
|
||||
t.panel = Color32::from_rgb(39, 43, 52);
|
||||
@@ -268,12 +310,12 @@ impl Tokens {
|
||||
t.render_ink = Color32::from_rgb(224, 116, 174);
|
||||
t.render_soft = Color32::from_rgb(64, 35, 54);
|
||||
t.tab_on_fill_dark = Color32::BLACK;
|
||||
t.dialog_body = Color32::from_rgb(59, 68, 83);
|
||||
t.dialog_body = Color32::from_rgb(39, 43, 52);
|
||||
t.dialog_footer = Color32::from_rgb(47, 52, 62);
|
||||
t.dialog_rule = Color32::from_rgb(53, 59, 71);
|
||||
t.control_fill = Color32::from_rgb(39, 43, 52);
|
||||
t.control_fill = Color32::from_rgb(28, 31, 38);
|
||||
t.control_border = Color32::from_rgb(74, 82, 98);
|
||||
t.dialog_label = Color32::from_rgb(180, 187, 200);
|
||||
t.dialog_label = t.text;
|
||||
t.selected_ink = Color32::from_rgb(156, 199, 245);
|
||||
t.selected_bg = Color32::from_rgb(42, 70, 110);
|
||||
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[1]);
|
||||
@@ -298,8 +340,8 @@ impl Tokens {
|
||||
t.dimension_driven = Color32::from_rgb(142, 150, 164);
|
||||
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
|
||||
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
|
||||
t.dialog_bg = Color32::from_rgb(59, 68, 83);
|
||||
t.dialog_border = Color32::from_rgb(72, 83, 99);
|
||||
t.dialog_bg = Color32::from_rgb(39, 43, 52);
|
||||
t.dialog_border = Color32::from_rgb(63, 69, 82);
|
||||
t.chip = Color32::from_rgb(59, 101, 168);
|
||||
t.glyph_bg = Color32::from_rgb(44, 50, 60);
|
||||
t.glyph_edge = Color32::from_rgb(200, 130, 120);
|
||||
@@ -315,7 +357,7 @@ impl Tokens {
|
||||
t.bend_up = Color32::from_rgb(110, 200, 120);
|
||||
t.bend_down = Color32::from_rgb(240, 150, 80);
|
||||
t.form_ink = t.text;
|
||||
t.form_dim = t.text_dim;
|
||||
t.form_dim = Color32::from_rgb(180, 187, 200);
|
||||
t.form_primary = t.accent;
|
||||
t.form_error = t.error;
|
||||
t.form_error_fill = t.timeline_item_error;
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#ffad38",
|
||||
"#eda134",
|
||||
"#855a1d",
|
||||
"#ffad38"
|
||||
"#855a1d"
|
||||
],
|
||||
"manipulator": "#ffad38",
|
||||
"mesh_accent": "#d6922f",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#d8f4ff",
|
||||
"#8edcf5",
|
||||
"#4386ac",
|
||||
"#eef8ff"
|
||||
"#4386ac"
|
||||
],
|
||||
"manipulator": "#66ddff",
|
||||
"mesh_accent": "#78c6e8",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#cba6f7",
|
||||
"#477dc8",
|
||||
"#586382",
|
||||
"#cdd6f4"
|
||||
"#586382"
|
||||
],
|
||||
"manipulator": "#cba6f7",
|
||||
"mesh_accent": "#cba6f7",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#85b9ef",
|
||||
"#4096e9",
|
||||
"#2b71be",
|
||||
"#ffffff"
|
||||
"#2b71be"
|
||||
],
|
||||
"manipulator": "#007aff",
|
||||
"mesh_accent": "#927300",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#4aa3d8",
|
||||
"#2878a9",
|
||||
"#68717d",
|
||||
"#e0e2e5"
|
||||
"#68717d"
|
||||
],
|
||||
"manipulator": "#4aa3d8",
|
||||
"mesh_accent": "#4aa3d8",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#b3ffc4",
|
||||
"#79ed95",
|
||||
"#34854b",
|
||||
"#d8ffdf"
|
||||
"#34854b"
|
||||
],
|
||||
"manipulator": "#4dff79",
|
||||
"mesh_accent": "#59d67b",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#00bfff",
|
||||
"#0070d0",
|
||||
"#555555",
|
||||
"#ffffff"
|
||||
"#555555"
|
||||
],
|
||||
"manipulator": "#00bfff",
|
||||
"mesh_accent": "#00bfff",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#88c0d0",
|
||||
"#5e81ac",
|
||||
"#657991",
|
||||
"#eceff4"
|
||||
"#657991"
|
||||
],
|
||||
"manipulator": "#88c0d0",
|
||||
"mesh_accent": "#88c0d0",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#bd93f9",
|
||||
"#8456b8",
|
||||
"#656581",
|
||||
"#f8f8f2"
|
||||
"#656581"
|
||||
],
|
||||
"manipulator": "#bd93f9",
|
||||
"mesh_accent": "#bd93f9",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#61afef",
|
||||
"#56b6c2",
|
||||
"#c678dd",
|
||||
"#abb2bf"
|
||||
"#c678dd"
|
||||
],
|
||||
"manipulator": "#61afef",
|
||||
"mesh_accent": "#857100",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#ccccff",
|
||||
"#9999cc",
|
||||
"#666699",
|
||||
"#ffffff"
|
||||
"#666699"
|
||||
],
|
||||
"manipulator": "#555599",
|
||||
"mesh_accent": "#666699",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#268bd2",
|
||||
"#346e88",
|
||||
"#b5b6a0",
|
||||
"#586e75"
|
||||
"#b5b6a0"
|
||||
],
|
||||
"manipulator": "#268bd2",
|
||||
"mesh_accent": "#268bd2",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#ffffff",
|
||||
"#c0c0c0",
|
||||
"#808080",
|
||||
"#ffffff"
|
||||
"#808080"
|
||||
],
|
||||
"manipulator": "#000080",
|
||||
"mesh_accent": "#000080",
|
||||
|
||||
@@ -126,8 +126,7 @@
|
||||
"logo": [
|
||||
"#7aa2f7",
|
||||
"#4775ce",
|
||||
"#4f6089",
|
||||
"#c0caf5"
|
||||
"#4f6089"
|
||||
],
|
||||
"manipulator": "#7aa2f7",
|
||||
"mesh_accent": "#7aa2f7",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{DocError, Document, Result};
|
||||
use crate::{DEFAULT_MATERIAL, DocError, Document, Result};
|
||||
|
||||
#[path = "config_rules.rs"]
|
||||
mod rules;
|
||||
@@ -96,7 +96,7 @@ impl Document {
|
||||
/// returning None for the default material.
|
||||
pub fn material_cell(&self, value: &str) -> Result<Option<String>> {
|
||||
let name = self.material_name(value).ok_or_else(|| DocError::Invalid(format!("unknown configuration material `{value}`")))?;
|
||||
Ok((name != "Default").then_some(name))
|
||||
Ok((name != DEFAULT_MATERIAL).then_some(name))
|
||||
}
|
||||
|
||||
/// Freeze design-unit-sensitive configuration literals when changing display units.
|
||||
@@ -140,7 +140,7 @@ impl Document {
|
||||
}
|
||||
}),
|
||||
Column::Suppress(id) => self.feature(*id).map(|f| f.suppressed.to_string()),
|
||||
Column::Material(body) => Some(self.materials.get(body).cloned().unwrap_or_else(|| "Default".into())),
|
||||
Column::Material(body) => Some(self.materials.get(body).cloned().unwrap_or_else(|| DEFAULT_MATERIAL.into())),
|
||||
Column::Appearance(body) => Some(self.appearances.bodies.get(body).and_then(|look| serde_json::to_string(look).ok()).unwrap_or_default()),
|
||||
Column::ComponentAppearance(id) => {
|
||||
Some(self.appearances.components.get(id).and_then(|look| serde_json::to_string(look).ok()).unwrap_or_default())
|
||||
|
||||
@@ -368,6 +368,8 @@ pub enum AxisRef {
|
||||
World { axis: String },
|
||||
/// Any line in the sketch plane, in world coordinates.
|
||||
Line { origin: Position, dir: Dir },
|
||||
/// A construction axis feature, by name, where it is when the revolve is evaluated.
|
||||
Construction { name: FeatureName },
|
||||
}
|
||||
|
||||
/// Persistent cylindrical Emboss selection, authored in the feature component frame.
|
||||
@@ -1854,14 +1856,16 @@ fn fair_keep_edges() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Triangles of one imported mesh body (millimetres).
|
||||
/// Triangles of one imported mesh body (millimetres). The arrays are shared by clones: every
|
||||
/// edit keeps the design as it was for undo, and a scanned part's triangles (hundreds of
|
||||
/// megabytes) are not copied for that.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MeshData {
|
||||
pub name: String,
|
||||
/// x, y, z per vertex.
|
||||
pub positions: Vec<f64>,
|
||||
pub positions: std::sync::Arc<[f64]>,
|
||||
/// Three vertex indices per triangle.
|
||||
pub triangles: Vec<u32>,
|
||||
pub triangles: std::sync::Arc<[u32]>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<[f32; 3]>,
|
||||
}
|
||||
@@ -2026,6 +2030,10 @@ pub enum PatternKind {
|
||||
alignment: solidcraft_sketch::PatternAlignment,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
reversed: bool,
|
||||
/// A construction axis the copies turn about, by name, followed when it moves;
|
||||
/// `origin` and `axis` are where it was when the pattern was made.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
axis_feature: Option<FeatureName>,
|
||||
},
|
||||
/// Copies along a chain of sketch curves: instance k moves by path(k·spacing) − path(0),
|
||||
/// measured along the path from its start.
|
||||
@@ -2411,6 +2419,12 @@ pub struct Document {
|
||||
pub marker: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub next_id: u64,
|
||||
/// Body names the design used and must not use again: a name a `body.rename` freed (#4957).
|
||||
/// The design's own counter starts over at every evaluation, so without these a design that
|
||||
/// is evaluated again after a rename hands the freed name to the next body, and a script
|
||||
/// that names a body finds another one.
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
|
||||
pub reserved_body_names: std::collections::BTreeSet<String>,
|
||||
/// Components below the root, as a tree (by parent).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub components: Vec<Component>,
|
||||
@@ -2618,6 +2632,33 @@ impl Default for Document {
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// The next shared identity, including auxiliary lists written by older versions or
|
||||
/// populated programmatically. Reading it does not reserve an id (used by joint previews).
|
||||
pub fn next_item_id(&self) -> Result<u64> {
|
||||
let a = &self.assembly;
|
||||
let largest = self
|
||||
.browser_groups
|
||||
.iter()
|
||||
.map(|g| g.id)
|
||||
.chain(self.timeline_groups.iter().map(|g| g.id))
|
||||
.chain(self.canvases.iter().map(|c| c.id))
|
||||
.chain(a.joints.iter().map(|j| j.id))
|
||||
.chain(a.contact_sets.iter().map(|c| c.id))
|
||||
.chain(a.studies.iter().map(|s| s.id))
|
||||
.chain(a.exploded.iter().map(|v| v.id))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let floor = largest.checked_add(1).ok_or_else(|| DocError::Invalid("too many items in the design".into()))?;
|
||||
Ok(self.next_id.max(floor).max(1))
|
||||
}
|
||||
|
||||
/// Reserve an identity in the saved counter. Removing an item never rewinds this counter.
|
||||
pub fn allocate_id(&mut self) -> Result<u64> {
|
||||
let id = self.next_item_id()?;
|
||||
self.next_id = id.checked_add(1).ok_or_else(|| DocError::Invalid("too many items in the design".into()))?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn new(name: &str) -> Self {
|
||||
Document {
|
||||
render: Default::default(),
|
||||
@@ -2630,6 +2671,7 @@ impl Document {
|
||||
features: Vec::new(),
|
||||
marker: None,
|
||||
next_id: 1,
|
||||
reserved_body_names: Default::default(),
|
||||
components: Vec::new(),
|
||||
occurrences: Vec::new(),
|
||||
assembly: Default::default(),
|
||||
@@ -2657,7 +2699,14 @@ impl Document {
|
||||
/// Rename body `old` to `new` where the design keeps things by body name outside its
|
||||
/// features: materials, appearances, configuration columns, offsets, components and the
|
||||
/// browser's groups and order. (The features are the rename command's to change.)
|
||||
///
|
||||
/// A name the design numbered (`Body1`) is not used again after the rename: the design's
|
||||
/// counter starts over at every evaluation, so without this a design that is evaluated
|
||||
/// again hands the freed name to the next body (#4957).
|
||||
pub fn rename_body_keys(&mut self, old: &str, new: &str) {
|
||||
if old.strip_prefix("Body").and_then(|n| n.parse::<usize>().ok()).is_some() {
|
||||
self.reserved_body_names.insert(old.to_string());
|
||||
}
|
||||
if let Some(m) = self.materials.remove(old) {
|
||||
self.materials.insert(new.to_string(), m);
|
||||
}
|
||||
@@ -2807,9 +2856,21 @@ impl Document {
|
||||
st
|
||||
}
|
||||
|
||||
/// Density of a body's material (g/cm³).
|
||||
/// The material a body's mass is computed with, as the library or this design spells it:
|
||||
/// its assigned material when that is known and has a usable density, else
|
||||
/// [`DEFAULT_MATERIAL`]. The assignment as stored (`materials`) is left as it is.
|
||||
pub fn body_material(&self, body: &str) -> String {
|
||||
self.materials
|
||||
.get(body)
|
||||
.and_then(|m| self.material_name(m))
|
||||
.filter(|m| self.material_density(m).is_some())
|
||||
.unwrap_or_else(|| DEFAULT_MATERIAL.into())
|
||||
}
|
||||
|
||||
/// Density of a body's material (g/cm³): its [`Document::body_material`]'s, so the name a
|
||||
/// body reads and the mass it has never disagree.
|
||||
pub fn density(&self, body: &str) -> f64 {
|
||||
self.materials.get(body).and_then(|m| self.material_density(m)).unwrap_or(DEFAULT_DENSITY)
|
||||
self.material_density(&self.body_material(body)).unwrap_or(DEFAULT_DENSITY)
|
||||
}
|
||||
|
||||
/// A material defined in this design, by name (case-insensitive).
|
||||
@@ -3417,6 +3478,24 @@ impl Document {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve circular Quantity expressions before solving. Rebuilding the copies here also
|
||||
/// covers cold document evaluation and configuration changes, without an engine dependency.
|
||||
pub fn apply_pattern_quantities(&self, vals: &std::collections::BTreeMap<String, expr::Value>, sk: &mut Sketch) -> Result<()> {
|
||||
let counts: Vec<_> = sk
|
||||
.patterns
|
||||
.iter()
|
||||
.filter_map(|p| p.count_expression.as_ref().map(|e| (p.id.clone(), e)))
|
||||
.map(|(id, e)| {
|
||||
let n = crate::eval::val(vals, e, Kind::Unitless)?;
|
||||
Ok((id, solidcraft_sketch::SketchPattern::circular_quantity(n)?))
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
for (id, count) in counts {
|
||||
sk.resize_circular_pattern(&id, count)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move the points placed by parameters to where their expressions come to (in place). Such
|
||||
/// a point is fixed, so the solve keeps it there.
|
||||
pub fn apply_point_values(&self, vals: &std::collections::BTreeMap<String, expr::Value>, sk: &mut Sketch) -> Result<()> {
|
||||
@@ -3503,6 +3582,7 @@ impl Document {
|
||||
moved_from: Option<&Sketch>,
|
||||
analysis: solidcraft_sketch::Analysis,
|
||||
) -> Result<solidcraft_sketch::SolveReport> {
|
||||
self.apply_pattern_quantities(vals, sk)?;
|
||||
self.apply_dimension_values(vals, sk)?;
|
||||
let was = |sk: &Sketch| moved_from.and_then(|from| sk.positions_in(from));
|
||||
let offsets = !sk.offsets.is_empty() || !sk.freeform_offsets.is_empty();
|
||||
@@ -3523,10 +3603,10 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// Density of the default material (g/cm³), as the default.
|
||||
/// The built-in material library: (name, density), and each material's colour.
|
||||
#[path = "../../../assets/materials/library.rs"]
|
||||
mod materials;
|
||||
pub use materials::{DEFAULT_DENSITY, MATERIALS, material_color};
|
||||
pub use materials::{DEFAULT_DENSITY, DEFAULT_MATERIAL, MATERIALS, material_color};
|
||||
|
||||
/// A physical material defined in a design (`material.create`): its name, its density and,
|
||||
/// optionally, the look it gives bodies without an appearance of their own.
|
||||
|
||||
@@ -17,6 +17,8 @@ use crate::expr::{self, Kind, Value};
|
||||
use crate::lineage::Traced;
|
||||
use crate::{AxisRef, DocError, Document, Feature, FeatureKind, HoleKind, Operation, PlaneRef, ProfileSel, Result};
|
||||
|
||||
type JointFaces = (Body, kernel::Result<Arc<[kernel::FaceInfo]>>);
|
||||
|
||||
/// A body in the evaluated model.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ModelBody {
|
||||
@@ -31,6 +33,9 @@ pub struct ModelBody {
|
||||
// Timeline snapshots are cloned before meshes are requested. Share the cell itself so
|
||||
// meshing any snapshot also warms every unchanged copy of this body.
|
||||
mesh: Arc<OnceLock<Arc<Mesh>>>,
|
||||
/// Joint face measurements use their own tolerance, independent of display quality.
|
||||
/// Shared by snapshots of this geometry; the body identity guards direct replacement.
|
||||
joint_faces: Arc<OnceLock<JointFaces>>,
|
||||
/// Lazy canonical reference triangles for a proper rigid occurrence placement.
|
||||
mesh_placement: Option<Arc<placed_mesh::PlacedMesh>>,
|
||||
/// The display plan (none when the body has no plan: a mesh body, or one the planner
|
||||
@@ -71,6 +76,7 @@ impl ModelBody {
|
||||
body,
|
||||
feature,
|
||||
mesh: Arc::new(OnceLock::new()),
|
||||
joint_faces: Arc::new(OnceLock::new()),
|
||||
mesh_placement: None,
|
||||
plan: Arc::new(Default::default()),
|
||||
plan_mesh: Arc::new(Default::default()),
|
||||
@@ -86,6 +92,19 @@ impl ModelBody {
|
||||
}
|
||||
}
|
||||
|
||||
/// Face measurements for joint origins, at their original chord tolerance. Pose changes
|
||||
/// and scratch copies share them; a geometry edit makes a new body and cache.
|
||||
pub(crate) fn joint_faces(&self) -> Result<Arc<[kernel::FaceInfo]>> {
|
||||
let measure = || {
|
||||
#[cfg(test)]
|
||||
cost_tests::count(|c| c.joint_faces += 1);
|
||||
let tol = (self.body.size() * 2e-3).max(1e-3);
|
||||
self.body.faces(tol).map(Arc::from)
|
||||
};
|
||||
let (body, faces) = self.joint_faces.get_or_init(|| (self.body.clone(), measure()));
|
||||
if body.same(&self.body) { faces.clone().map_err(Into::into) } else { measure().map_err(Into::into) }
|
||||
}
|
||||
|
||||
/// A box around the body without meshing it ([`Body::outline_bounds`]), worked out once.
|
||||
pub fn outline_bounds(&self) -> solidcraft_geom::Aabb3 {
|
||||
match self.outline.get_or_init(|| (self.body.clone(), self.body.outline_bounds())) {
|
||||
@@ -120,6 +139,9 @@ impl ModelBody {
|
||||
// finding their surface, picks naming their face, sketch planes), and a different
|
||||
// triangulation moved those and changed what designs build. A GPU viewport draws the
|
||||
// plan ([`ModelBody::display_plan`]).
|
||||
if let Some(mesh) = self.mesh.get() {
|
||||
return mesh.clone();
|
||||
}
|
||||
match self.mesh_placement.as_deref() {
|
||||
Some(placed) => self.mesh.get_or_init(|| placed.mesh()).clone(),
|
||||
None => placed_mesh::reference_mesh(&self.body, &self.mesh, self.display_tol()),
|
||||
@@ -163,6 +185,50 @@ impl ModelBody {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Identity of this body's display geometry, without meshing it: the cell its mesh is kept
|
||||
/// in, shared by every clone of the body (timeline snapshots, world states that place it
|
||||
/// unchanged) and, while it is held, by no other body.
|
||||
pub fn display_identity(&self) -> Arc<dyn std::any::Any + Send + Sync> {
|
||||
self.mesh.clone()
|
||||
}
|
||||
|
||||
/// [`ModelBody::mesh`] as a shared mesh and the move that places it: for a rigid placement
|
||||
/// of a definition, the definition's own mesh (the same one for every placement of it) and
|
||||
/// the placement; for any other body its own mesh, unmoved. The mesh moved by the matrix is
|
||||
/// `mesh()`, so a viewport can draw every placement of a part from one copy of its
|
||||
/// triangles, without making the moved copies.
|
||||
pub fn mesh_in_place(&self) -> (Arc<Mesh>, crate::Mat) {
|
||||
match self.mesh_placement.as_deref() {
|
||||
Some(placed) => placed.source_mesh(),
|
||||
None => (self.mesh(), crate::IDENTITY),
|
||||
}
|
||||
}
|
||||
|
||||
/// The box of [`ModelBody::mesh`] without making a placement's moved copy: the shared mesh's
|
||||
/// points, moved.
|
||||
pub fn mesh_bounds(&self) -> solidcraft_geom::Aabb3 {
|
||||
if let Some(mesh) = self.cached_mesh() {
|
||||
return mesh.bounds();
|
||||
}
|
||||
let (mesh, m) = self.mesh_in_place();
|
||||
if crate::is_identity(&m) {
|
||||
return mesh.bounds();
|
||||
}
|
||||
mesh.positions.iter().fold(solidcraft_geom::Aabb3::EMPTY, |mut b, p| {
|
||||
b.add(crate::apply_point(&m, *p));
|
||||
b
|
||||
})
|
||||
}
|
||||
|
||||
/// [`ModelBody::plan_mesh_at`] as a shared mesh and the move that places it, as
|
||||
/// [`ModelBody::mesh_in_place`] does for the reference mesh.
|
||||
pub fn plan_mesh_in_place_at(&self, q: crate::display_quality::DisplayQuality) -> Option<(Arc<Mesh>, crate::Mat)> {
|
||||
match self.mesh_placement.as_deref() {
|
||||
Some(placed) => placed.source_plan_mesh(q, &self.name),
|
||||
None => Some((self.plan_mesh_at(q)?, crate::IDENTITY)),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`ModelBody::plan_mesh`] if it is made already; otherwise it is made on a worker thread
|
||||
/// (once, however often this is asked) and `None` comes back meanwhile, so a caller drawing a
|
||||
/// frame (a face highlighted under the pointer) is not held up by a large body's plan
|
||||
@@ -650,6 +716,10 @@ pub struct ModelState {
|
||||
/// Shared between states: a feature that does not change a sketch keeps it as it was.
|
||||
pub sketches: Vec<Arc<SolvedSketch>>,
|
||||
pub body_counter: usize,
|
||||
/// Body names the design used and must not use again (a name a `body.rename` freed, #4957).
|
||||
/// The counter starts over at every evaluation, so without these a design that is evaluated
|
||||
/// again after a rename hands the freed name to the next body.
|
||||
pub reserved_body_names: std::collections::BTreeSet<String>,
|
||||
/// Sheet metal bodies (by body name) and their flat patterns.
|
||||
pub sheets: Vec<crate::sheet::SheetBody>,
|
||||
/// Construction planes, axes and points as evaluated (by feature).
|
||||
@@ -685,6 +755,9 @@ impl ModelState {
|
||||
self.sketch_keys.get(&s.feature).hash(&mut h);
|
||||
}
|
||||
self.body_counter.hash(&mut h);
|
||||
for n in &self.reserved_body_names {
|
||||
n.hash(&mut h);
|
||||
}
|
||||
for (feature, points) in &self.blend_edges {
|
||||
feature.hash(&mut h);
|
||||
for p in points {
|
||||
@@ -950,6 +1023,11 @@ pub struct FeatureResult {
|
||||
pub stale: bool,
|
||||
#[serde(skip)]
|
||||
fingerprint: u64,
|
||||
/// Taken from a saved model that did not keep this feature's state (#3220): `state` only
|
||||
/// stands in for it until [`Model::evaluate`] evaluates the feature again, which
|
||||
/// [`Model::adopt`] does before the model is used.
|
||||
#[serde(skip)]
|
||||
unkept: bool,
|
||||
#[serde(skip)]
|
||||
pub state: Arc<ModelState>,
|
||||
}
|
||||
@@ -977,6 +1055,11 @@ pub struct Model {
|
||||
kept: KeptSlot,
|
||||
/// The imported texts' digests the last `evaluate` fingerprinted the features with.
|
||||
step_digests: StepDigests,
|
||||
/// The document context the last `evaluate` ran in ([`context_digest`]).
|
||||
context: u64,
|
||||
/// Results another copy of this model evaluated (ahead, off the window thread) that
|
||||
/// `evaluate` takes for features they match ([`Model::prime`]), with that copy's context.
|
||||
primed: Option<(u64, Arc<std::collections::HashMap<u64, FeatureResult>>)>,
|
||||
}
|
||||
|
||||
/// Parameters a feature uses directly (inputs, sketch dimensions and the expressions of a
|
||||
@@ -1143,8 +1226,17 @@ fn evaluation_values(doc: &Document) -> (BTreeMap<String, Value>, BTreeMap<Strin
|
||||
|
||||
/// A feature's fingerprint in the chain after `prev`: its own inputs, where it is placed, and
|
||||
/// the sheet metal or plastic rules it takes defaults from.
|
||||
fn chain_fingerprint(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, prev: u64, rolled_back: bool, steps: &StepDigests) -> u64 {
|
||||
let mut fp = fingerprint(prev, f, vals, rolled_back, steps) ^ placement_digest(doc, f);
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn chain_fingerprint(
|
||||
doc: &Document,
|
||||
vals: &BTreeMap<String, Value>,
|
||||
f: &Feature,
|
||||
prev: u64,
|
||||
rolled_back: bool,
|
||||
steps: &StepDigests,
|
||||
st: Option<&ModelState>,
|
||||
) -> u64 {
|
||||
let mut fp = fingerprint(prev, f, vals, rolled_back, steps) ^ placement_digest(doc, f, st);
|
||||
// Sheet metal features also depend on the rules (and the parameters they use).
|
||||
if f.kind.is_sheet() {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
@@ -1229,9 +1321,157 @@ fn cache_key(f: &Feature, chain_fp: u64, local: u64, ctx: u64, state: &ModelStat
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Where components are placed, for a sketch that sees other components' geometry (on a face,
|
||||
/// or with projections): it is resolved in its own component's view of them.
|
||||
fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
/// What a feature reads of placements: components (their first placement), occurrence paths
|
||||
/// and bodies in its timeline input, whose geometry it finds in its evaluating frame, or all.
|
||||
#[derive(Default)]
|
||||
struct PlacementReads {
|
||||
all: bool,
|
||||
components: std::collections::BTreeSet<u64>,
|
||||
paths: Vec<Vec<u64>>,
|
||||
bodies: std::collections::BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl PlacementReads {
|
||||
fn is_empty(&self) -> bool {
|
||||
!self.all && self.components.is_empty() && self.paths.is_empty() && self.bodies.is_empty()
|
||||
}
|
||||
|
||||
/// A body's complete placement in the feature's input state, including its offset. Later
|
||||
/// transfers do not change where an earlier feature read it.
|
||||
fn placed_body(&mut self, st: Option<&ModelState>, name: &str) {
|
||||
if st.is_some_and(|st| st.body(name).is_some()) {
|
||||
self.bodies.insert(name.to_owned());
|
||||
} else {
|
||||
self.all = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// The same target selection as `apply_traced`, expanded only over the timeline input.
|
||||
fn drawn_targets(&mut self, st: Option<&ModelState>, f: &Feature) {
|
||||
let Some(st) = st else {
|
||||
self.all = true;
|
||||
return;
|
||||
};
|
||||
let (_, targets) = feature_op(f);
|
||||
let names = if targets.is_empty() { &f.participants } else { &targets };
|
||||
let all = names.len() == 1 && names.first().is_some_and(|n| n == "*");
|
||||
if all || names.is_empty() {
|
||||
self.bodies.extend(st.bodies.iter().filter(|b| all || b.component == f.component || b.feature == f.id).map(|b| b.name.clone()));
|
||||
} else {
|
||||
for name in names {
|
||||
self.placed_body(Some(st), name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The component of body `name` in `st`; every component when the body is not there (a
|
||||
/// projection then takes the body nearest its point, wherever that is).
|
||||
fn body(&mut self, doc: &Document, st: Option<&ModelState>, name: &str) {
|
||||
match st.and_then(|st| st.body(name)) {
|
||||
Some(b) => {
|
||||
self.components.insert(doc.model_body_component(b));
|
||||
}
|
||||
None => self.all = true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The component of sketch `id`, and the root when it holds wires fixed in the world (they
|
||||
/// are read through the reader's whole placement).
|
||||
fn sketch(&mut self, doc: &Document, id: u64) {
|
||||
self.components.insert(doc.feature(id).map(|f| f.component).unwrap_or(0));
|
||||
if doc.sketch(id).is_ok_and(|sk| sk.wires.iter().any(|w| w.space == solidcraft_sketch::WireSpace::World)) {
|
||||
self.components.insert(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn link(&mut self, doc: &Document, st: Option<&ModelState>, source: &solidcraft_sketch::LinkSource) {
|
||||
use solidcraft_sketch::LinkSource as L;
|
||||
match source {
|
||||
L::Edge { body, .. } | L::Face { body, .. } | L::Body { body } | L::Vertex { body, .. } | L::Iso { body, .. } | L::Spun { body, .. } => {
|
||||
self.body(doc, st, body)
|
||||
}
|
||||
L::SketchCurve { sketch, .. } | L::SketchPoint { sketch, .. } => self.sketch(doc, *sketch),
|
||||
L::OnSurface { sketch, body, .. } => {
|
||||
self.sketch(doc, *sketch);
|
||||
self.body(doc, st, body);
|
||||
}
|
||||
L::Intersection { a, b } => {
|
||||
self.link(doc, st, a);
|
||||
self.link(doc, st, b);
|
||||
}
|
||||
// In the sketch's own frame: the component's origin, axes and planes, its own axes, text.
|
||||
L::Origin | L::Axis { .. } | L::SketchAxis { .. } | L::Plane { .. } | L::Text { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame of an occurrence path from the root (its occurrences' transforms in turn); `None`
|
||||
/// when the path no longer leads anywhere.
|
||||
fn path_frame(doc: &Document, path: &[u64]) -> Option<Mat> {
|
||||
if path.len() > crate::MAX_PLACEMENT_DEPTH + 1 {
|
||||
return None;
|
||||
}
|
||||
let (mut m, mut c) = (crate::IDENTITY, 0);
|
||||
for id in path {
|
||||
let o = doc.occurrences.iter().find(|o| o.id == *id && o.parent == c)?;
|
||||
m = crate::mat_mul(&m, &o.transform);
|
||||
c = o.component;
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Hash a placement on a grid far finer than anything a design shows ([`QUANTUM`] mm, a
|
||||
/// thousandth of that for the rotation), so the same relative placement reached through
|
||||
/// different whole-assembly motions (each rounding a little differently) reads the same.
|
||||
fn hash_placement(m: &Mat, h: &mut impl Hasher) {
|
||||
for (col, column) in m.iter().enumerate() {
|
||||
let q = if col == 3 { QUANTUM } else { QUANTUM * 1e-3 };
|
||||
for v in column {
|
||||
let r = (v / q).round();
|
||||
// -0 and 0 are the same place.
|
||||
(if r == 0.0 { 0.0f64 } else { r }).to_bits().hash(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the geometry a feature reads from other components is placed, as its own component
|
||||
/// sees it: the placement of each component (or occurrence path) it reads relative to its own
|
||||
/// frame. A feature that reads nothing outside its own frame gives 0, so moving parts around
|
||||
/// (a joint driven, an occurrence moved) leaves it as it was; one that reads a part moved
|
||||
/// relative to it (a sketch projecting a face of another part, To Object, a mirror of a placed
|
||||
/// body) is evaluated again. `st` is the model the feature starts from, which says what
|
||||
/// component a body it names belongs to (without it, every component is taken as read).
|
||||
/// [`placement_digest`] of a mirror of placed bodies: its own component's frame, each placement it
|
||||
/// was picked at (the copy's component, frame and the frame it was picked in) and the offsets of
|
||||
/// the bodies it mirrors (#1938).
|
||||
fn mirror_placements_digest(doc: &Document, f: &Feature, placements: &[Vec<u64>], bodies: &[crate::BodyName]) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
doc.component_transform(f.component).iter().flatten().for_each(|x| x.to_bits().hash(&mut h));
|
||||
for placed in placements.iter().map(|path| mirror_copy_frame(doc, path)) {
|
||||
placed.is_some().hash(&mut h);
|
||||
if let Some((component, frame, picked)) = placed {
|
||||
component.hash(&mut h);
|
||||
frame.iter().chain(&picked).flatten().for_each(|x| x.to_bits().hash(&mut h));
|
||||
}
|
||||
}
|
||||
for b in bodies {
|
||||
serde_json::to_string(&doc.body_offsets.get(&b.0)).unwrap_or_default().hash(&mut h);
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
fn placement_digest(doc: &Document, f: &Feature, st: Option<&ModelState>) -> u64 {
|
||||
// No shortcut for a design without placements: its first component (placed where the
|
||||
// design is) must not change what its features read, or creating it evaluates them all.
|
||||
let mut reads = PlacementReads::default();
|
||||
let mut frame = None;
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
// A sketch, profile or path of another component is moved through both placements.
|
||||
for (sketch, _) in f.kind.profile_sels() {
|
||||
if doc.feature(sketch).is_some_and(|s| s.component != f.component) {
|
||||
reads.sketch(doc, sketch);
|
||||
}
|
||||
}
|
||||
let path = match &f.kind {
|
||||
FeatureKind::Sweep { path_sketch, .. }
|
||||
| FeatureKind::SurfaceSweep { path_sketch, .. }
|
||||
@@ -1248,10 +1488,8 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
_ => &[],
|
||||
};
|
||||
if path.as_ref().is_some_and(wired) || rails.iter().any(|r| wired(&r.sketch)) {
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
let owner = path.and_then(|path| doc.feature(path.0)).map(|feature| feature.component).unwrap_or(0);
|
||||
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
|
||||
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
if let Some(path) = path {
|
||||
reads.sketch(doc, path.0);
|
||||
}
|
||||
// A sweep's profile and the rails' components place them too.
|
||||
let profile = match &f.kind {
|
||||
@@ -1259,69 +1497,113 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
_ => None,
|
||||
};
|
||||
for sketch in profile.into_iter().chain(rails.iter().map(|r| &r.sketch)) {
|
||||
let owner = doc.feature(sketch.0).map(|feature| feature.component).unwrap_or(0);
|
||||
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
reads.sketch(doc, sketch.0);
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
// A network through 3D sketch curves reads their points through the components the
|
||||
// feature and those sketches are placed in, as a sweep's wired path does.
|
||||
if let FeatureKind::SurfaceNetwork { u, v, .. } = &f.kind {
|
||||
let sketches: Vec<crate::SketchId> = u.iter().chain(v).filter_map(|c| c.sketch).collect();
|
||||
if sketches.iter().any(wired) {
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
doc.component_transform(f.component).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
for sketch in &sketches {
|
||||
let owner = doc.feature(sketch.0).map(|feature| feature.component).unwrap_or(0);
|
||||
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
}
|
||||
// To / From Object finds its target where that target's component is placed (#928).
|
||||
let to_object = match &f.kind {
|
||||
FeatureKind::Extrude { extent, .. } => extent.to.is_some() || extent.from.is_some(),
|
||||
FeatureKind::Revolve { to, .. } => to.is_some(),
|
||||
_ => false,
|
||||
};
|
||||
// A mirror of placed bodies follows the placements it was picked at and its own component's
|
||||
// frame, not every occurrence: moving an unrelated one leaves it (and its cache) alone.
|
||||
if let FeatureKind::Mirror { placements, bodies, .. } = &f.kind
|
||||
&& !placements.is_empty()
|
||||
{
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
doc.component_transform(f.component).iter().flatten().for_each(|x| x.to_bits().hash(&mut h));
|
||||
for placed in placements.iter().map(|path| mirror_copy_frame(doc, path)) {
|
||||
placed.is_some().hash(&mut h);
|
||||
if let Some((component, frame, picked)) = placed {
|
||||
component.hash(&mut h);
|
||||
frame.iter().chain(&picked).flatten().for_each(|x| x.to_bits().hash(&mut h));
|
||||
reads.sketch(doc, sketch.0);
|
||||
}
|
||||
}
|
||||
for b in bodies {
|
||||
serde_json::to_string(&doc.body_offsets.get(&b.0)).unwrap_or_default().hash(&mut h);
|
||||
}
|
||||
match &f.kind {
|
||||
// To / From Object finds its target where that target's component is placed (#928),
|
||||
// over every body.
|
||||
FeatureKind::Extrude { extent, .. } if extent.to.is_some() || extent.from.is_some() => reads.all = true,
|
||||
FeatureKind::Revolve { to: Some(_), .. } => reads.all = true,
|
||||
// A mirror of placed bodies follows the placements it was picked at and its own component's
|
||||
// frame, not every occurrence: moving an unrelated one leaves it (and its cache) alone.
|
||||
FeatureKind::Mirror { placements, bodies, .. } if !placements.is_empty() => return mirror_placements_digest(doc, f, placements, bodies),
|
||||
// A combine moves tools placed differently from its target into the target's frame (#3383).
|
||||
FeatureKind::Combine { target, tools, .. } => {
|
||||
frame = st.and_then(|st| st.body(target)).map(|b| body_world(doc, b));
|
||||
reads.placed_body(st, target);
|
||||
for tool in tools {
|
||||
reads.placed_body(st, tool);
|
||||
}
|
||||
}
|
||||
return h.finish();
|
||||
// Joins, cuts and intersects meet their targets where those are placed (#3383).
|
||||
kind if drawn_tools_change_bodies(kind) => reads.drawn_targets(st, f),
|
||||
FeatureKind::Sketch { plane, sketch } => {
|
||||
match crate::face_anchor::plane_face_anchor(plane) {
|
||||
// Read in the frame of the placement it was drawn in, its face through the
|
||||
// placement it was picked at.
|
||||
Some(anchor) => {
|
||||
match anchor.author_frame(doc, f.component) {
|
||||
Ok(m) => frame = Some(m),
|
||||
Err(e) => e.to_string().hash(&mut h),
|
||||
}
|
||||
if anchor.path.is_empty() {
|
||||
reads.components.insert(anchor.component);
|
||||
} else {
|
||||
reads.paths.push(anchor.path.clone());
|
||||
}
|
||||
}
|
||||
// A face or edges picked on whatever body lies there.
|
||||
None if plane_reads_geometry(plane) => reads.all = true,
|
||||
None => {}
|
||||
}
|
||||
for link in &sketch.links {
|
||||
reads.link(doc, st, &link.source);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// A combine moves tools placed differently from its target into the target's frame (#3383).
|
||||
let combine = matches!(&f.kind, FeatureKind::Combine { .. });
|
||||
if !to_object && !combine && drawn_tools_change_bodies(&f.kind) {
|
||||
return drawn_tools_digest(doc, f);
|
||||
}
|
||||
if to_object || combine {
|
||||
return if doc.occurrences.is_empty() && doc.body_offsets.is_empty() { 0 } else { placements_hash(doc) };
|
||||
}
|
||||
let FeatureKind::Sketch { plane, sketch } = &f.kind else { return 0 };
|
||||
let anchor = crate::face_anchor::plane_face_anchor(plane);
|
||||
if (doc.occurrences.is_empty() && (anchor.is_none() || doc.body_offsets.is_empty()))
|
||||
|| (!matches!(plane, PlaneRef::Face { .. }) && anchor.is_none() && sketch.links.is_empty())
|
||||
{
|
||||
if reads.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
placements_hash(doc)
|
||||
let frame = frame.unwrap_or_else(|| doc.component_transform(f.component));
|
||||
let Some(inverse) = crate::mat_inverse(&frame) else { return placements_hash(doc) };
|
||||
if reads.all {
|
||||
reads.components.insert(0);
|
||||
match st {
|
||||
// The components holding what the feature can reach: a component made later, or one
|
||||
// that holds nothing yet (just created), changes nothing it reads.
|
||||
Some(st) => {
|
||||
reads.components.extend(doc.body_owners(st));
|
||||
let owner = |id: u64| doc.feature(id).map(|g| g.component);
|
||||
reads.components.extend(st.sketches.iter().filter_map(|s| owner(s.feature)));
|
||||
reads.components.extend(st.construct.iter().filter_map(|c| owner(c.feature)));
|
||||
}
|
||||
None => reads.components.extend(doc.components.iter().map(|c| c.id)),
|
||||
}
|
||||
}
|
||||
for c in &reads.components {
|
||||
c.hash(&mut h);
|
||||
// Its own component is read in its own frame.
|
||||
if *c != f.component {
|
||||
hash_placement(&crate::mat_mul(&inverse, &doc.component_transform(*c)), &mut h);
|
||||
}
|
||||
}
|
||||
for path in &reads.paths {
|
||||
path.hash(&mut h);
|
||||
match path_frame(doc, path) {
|
||||
Some(m) => hash_placement(&crate::mat_mul(&inverse, &m), &mut h),
|
||||
None => u8::MAX.hash(&mut h),
|
||||
}
|
||||
}
|
||||
for name in &reads.bodies {
|
||||
name.hash(&mut h);
|
||||
match st.and_then(|st| st.body(name)) {
|
||||
Some(b) => hash_placement(&crate::mat_mul(&inverse, &body_world(doc, b)), &mut h),
|
||||
None => u8::MAX.hash(&mut h),
|
||||
}
|
||||
}
|
||||
// Legacy and component-wide reads still need the conservative offset dependency. Body
|
||||
// reads above already include just their own offsets, in the frame that evaluates them.
|
||||
if reads.all || !reads.components.is_empty() || !reads.paths.is_empty() {
|
||||
serde_json::to_string(&doc.body_offsets).unwrap_or_default().hash(&mut h);
|
||||
}
|
||||
// Never 0: the feature reads placements.
|
||||
h.finish() | 1
|
||||
}
|
||||
|
||||
/// Every occurrence's placement and every body offset.
|
||||
/// Every occurrence's placement and every body offset (a feature read from a frame that cannot
|
||||
/// be inverted).
|
||||
fn placements_hash(doc: &Document) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
for o in &doc.occurrences {
|
||||
@@ -1419,7 +1701,8 @@ impl Model {
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
let mut prev_fp = first.checked_sub(1).and_then(|i| self.results.get(i)).map_or(0, |r| r.fingerprint);
|
||||
for (i, f) in doc.features.iter().enumerate().skip(first) {
|
||||
prev_fp = chain_fingerprint(doc, &vals, f, prev_fp, i >= marker, &steps);
|
||||
let before = self.state_before(f.id);
|
||||
prev_fp = chain_fingerprint(doc, &vals, f, prev_fp, i >= marker, &steps, Some(&before));
|
||||
if let Some(r) = self.results.get_mut(i).filter(|r| r.id == f.id) {
|
||||
r.fingerprint = prev_fp;
|
||||
}
|
||||
@@ -1460,25 +1743,47 @@ impl Model {
|
||||
let steps = StepDigests::of(doc, &self.step_digests);
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
let mut prev = 0;
|
||||
// The model each feature starts from (which components the bodies it names are in).
|
||||
let mut state = self.empty.clone();
|
||||
doc.features.iter().zip(&self.results).enumerate().all(|(i, (f, r))| {
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &steps);
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &steps, Some(&state));
|
||||
if !r.skipped {
|
||||
state = r.state.clone();
|
||||
}
|
||||
r.id == f.id && r.fingerprint == prev
|
||||
})
|
||||
}
|
||||
|
||||
/// Take the results `ahead` (a copy of this model, evaluated off this thread for the same
|
||||
/// commands) computed: until [`Model::unprime`], `evaluate` reuses each one whose feature
|
||||
/// has the same inputs here, instead of evaluating it again. Results that don't match are
|
||||
/// ignored, so a design changed meanwhile is evaluated as usual.
|
||||
pub fn prime(&mut self, ahead: &Model) {
|
||||
let results = ahead.results.iter().filter(|r| !r.skipped && !r.stale).map(|r| (r.id, r.clone())).collect();
|
||||
self.primed = Some((ahead.context, Arc::new(results)));
|
||||
}
|
||||
|
||||
/// Stop offering the results [`Model::prime`] took.
|
||||
pub fn unprime(&mut self) {
|
||||
self.primed = None;
|
||||
}
|
||||
|
||||
/// Re-evaluate the document, reusing results up to the first changed feature.
|
||||
pub fn evaluate(&mut self, doc: &Document) {
|
||||
let (vals, perr) = evaluation_values(doc);
|
||||
self.param_errors = perr;
|
||||
let mut prev_fp = 0u64;
|
||||
let mut state = self.empty.clone();
|
||||
// The names the design must not use again travel with the state (#4957).
|
||||
let mut state = std::sync::Arc::new(ModelState { reserved_body_names: doc.reserved_body_names.clone(), ..(*self.empty).clone() });
|
||||
let mut out: Vec<FeatureResult> = Vec::with_capacity(doc.features.len());
|
||||
let mut reuse = !self.no_cache;
|
||||
let mut recomputed = 0;
|
||||
let mut restored = 0;
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
let ctx = context_digest(doc, &vals);
|
||||
self.context = ctx;
|
||||
self.step_digests = StepDigests::of(doc, &self.step_digests);
|
||||
let primed = self.primed.as_ref().filter(|(context, _)| !self.no_cache && *context == ctx).map(|(_, results)| results.clone());
|
||||
// Each feature's component, for `Document::legacy_body_component` on every body after
|
||||
// every feature without searching the timeline for each (the first feature with an id).
|
||||
let mut feature_component: std::collections::HashMap<u64, u64> = std::collections::HashMap::with_capacity(doc.features.len());
|
||||
@@ -1490,13 +1795,15 @@ impl Model {
|
||||
let rolled_back = i >= marker;
|
||||
// Opening was cancelled: the rest is skipped and the result discarded.
|
||||
let cancelled = watched.step(i);
|
||||
let fp = chain_fingerprint(doc, &vals, f, prev_fp, rolled_back, &self.step_digests);
|
||||
let fp = chain_fingerprint(doc, &vals, f, prev_fp, rolled_back, &self.step_digests, Some(&state));
|
||||
prev_fp = fp;
|
||||
let current = |old: &&FeatureResult| old.id == f.id && old.fingerprint == fp && !old.stale;
|
||||
// A state the saved model did not keep is evaluated again from the one before it; the
|
||||
// results after it that the model did keep still stand (#3220).
|
||||
let unkept = reuse && self.results.get(i).filter(current).is_some_and(|old| old.unkept);
|
||||
if reuse
|
||||
&& let Some(old) = self.results.get(i)
|
||||
&& old.id == f.id
|
||||
&& old.fingerprint == fp
|
||||
&& !old.stale
|
||||
&& !unkept
|
||||
&& let Some(old) = self.results.get(i).filter(current)
|
||||
{
|
||||
if !old.skipped {
|
||||
state = old.state.clone();
|
||||
@@ -1504,7 +1811,7 @@ impl Model {
|
||||
out.push(old.clone());
|
||||
continue;
|
||||
}
|
||||
reuse = false;
|
||||
reuse = unkept && !cancelled;
|
||||
if f.suppressed || rolled_back || cancelled {
|
||||
out.push(FeatureResult {
|
||||
id: f.id,
|
||||
@@ -1516,12 +1823,13 @@ impl Model {
|
||||
skipped: true,
|
||||
stale: false,
|
||||
fingerprint: fp,
|
||||
unkept: false,
|
||||
state: state.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// The same feature on the same inputs: its earlier result.
|
||||
let key = cache_key(f, fp, fingerprint(0, f, &vals, false, &self.step_digests) ^ placement_digest(doc, f), ctx, &state);
|
||||
let key = cache_key(f, fp, fingerprint(0, f, &vals, false, &self.step_digests) ^ placement_digest(doc, f, Some(&state)), ctx, &state);
|
||||
let kept = self.kept.0.as_ref().filter(|_| !self.no_cache);
|
||||
if !self.no_cache
|
||||
&& let Some(c) = self.cache.lock().ok().and_then(|mut m| m.get(key)).or_else(|| kept.and_then(|k| k.get(key)))
|
||||
@@ -1533,6 +1841,8 @@ impl Model {
|
||||
if c.error.is_none() {
|
||||
state = c.state.clone();
|
||||
}
|
||||
// A kept result after a failed one would not be this evaluation's.
|
||||
reuse &= c.error.is_none();
|
||||
out.push(FeatureResult {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
@@ -1543,13 +1853,27 @@ impl Model {
|
||||
skipped: false,
|
||||
stale: false,
|
||||
fingerprint: fp,
|
||||
unkept: false,
|
||||
state: state.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// Evaluated ahead on these same inputs (the same chain of definitions from the start).
|
||||
if let Some(r) = primed.as_ref().and_then(|p| p.get(&f.id)).filter(|r| r.fingerprint == fp && !r.stale && !r.skipped) {
|
||||
restored += 1;
|
||||
if r.error.is_none() {
|
||||
state = r.state.clone();
|
||||
}
|
||||
reuse &= r.error.is_none();
|
||||
out.push(FeatureResult { state: state.clone(), unkept: false, ..r.clone() });
|
||||
continue;
|
||||
}
|
||||
recomputed += 1;
|
||||
let t0 = now();
|
||||
let mut next = (*state).clone();
|
||||
// The names the design must not use again travel with the state (#4957): a state
|
||||
// taken from an earlier evaluation may have been made before a rename freed one.
|
||||
next.reserved_body_names = doc.reserved_body_names.clone();
|
||||
for b in &mut next.bodies {
|
||||
b.made = crate::lineage::Made::Before;
|
||||
}
|
||||
@@ -1673,6 +1997,7 @@ impl Model {
|
||||
m.put(key, e);
|
||||
}
|
||||
}
|
||||
reuse &= error.is_none();
|
||||
out.push(FeatureResult {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
@@ -1683,6 +2008,7 @@ impl Model {
|
||||
skipped: false,
|
||||
stale: false,
|
||||
fingerprint: fp,
|
||||
unkept: false,
|
||||
state: state.clone(),
|
||||
});
|
||||
}
|
||||
@@ -1727,6 +2053,8 @@ impl Model {
|
||||
b.name = new.to_string();
|
||||
b.names = b.names.renamed();
|
||||
}
|
||||
// The names the design must not use again travel with the state (#4957).
|
||||
st.reserved_body_names = doc.reserved_body_names.clone();
|
||||
Arc::new(st)
|
||||
})
|
||||
.clone();
|
||||
@@ -1787,9 +2115,12 @@ impl Model {
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
self.step_digests = StepDigests::of(doc, &self.step_digests);
|
||||
let mut prev = 0;
|
||||
// Each feature's fingerprint reads the model it starts from, as evaluating does.
|
||||
let mut before = Arc::new(ModelState::default());
|
||||
for (i, (r, f)) in self.results.iter_mut().zip(&doc.features).enumerate() {
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &self.step_digests);
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &self.step_digests, Some(&before));
|
||||
r.fingerprint = prev;
|
||||
before = r.state.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1839,8 +2170,10 @@ pub fn select_profiles(ss: &SolvedSketch, sel: &ProfileSel) -> Result<Vec<Region
|
||||
solidcraft_sketch::without_text_counters(&ss.sketch, chosen).into_iter().map(|p| p.region).collect()
|
||||
}
|
||||
ProfileSel::Indices { indices } => {
|
||||
// A design file may list a profile more than once; it is still one profile.
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
let mut v = Vec::new();
|
||||
for i in indices {
|
||||
for i in indices.iter().filter(|i| seen.insert(**i)) {
|
||||
v.push(ps.get(*i).ok_or_else(|| DocError::Unknown(format!("profile {i} of {}", ss.name)))?.region.clone());
|
||||
}
|
||||
v
|
||||
@@ -1906,7 +2239,8 @@ fn new_name(state: &mut ModelState, f: &Feature, k: usize) -> String {
|
||||
loop {
|
||||
state.body_counter += 1;
|
||||
let n = format!("Body{}", state.body_counter);
|
||||
if state.body(&n).is_none() {
|
||||
// A name a rename freed is not handed out again (#4957).
|
||||
if state.body(&n).is_none() && !state.reserved_body_names.contains(&n) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
@@ -2093,6 +2427,33 @@ fn clear_tools_as_one(tools: Vec<Traced>) -> Vec<Traced> {
|
||||
}
|
||||
}
|
||||
|
||||
/// An intersect never deletes a body by accident (#5295): it is refused when nothing of a named
|
||||
/// target would remain, when nothing of any target would remain, or when the tools meet a target
|
||||
/// and the boolean still came out empty. Only an unnamed target the tools miss altogether is
|
||||
/// removed, as an intersect with every body does (#5160).
|
||||
pub(crate) fn empty_intersection(bodies: &[&str]) -> DocError {
|
||||
let body = match bodies {
|
||||
[one] => format!("`{one}`"),
|
||||
[first, rest @ ..] => format!("`{first}` and {} more", rest.len()),
|
||||
[] => "the target body".to_string(),
|
||||
};
|
||||
DocError::Invalid(format!("the intersection is empty: nothing of {body} would remain; check that the profile overlaps the body"))
|
||||
}
|
||||
|
||||
/// The bodies a feature's boolean takes part in, and whether they were named rather than taken
|
||||
/// by default (every body, or the feature's component).
|
||||
pub(crate) fn boolean_scope(state: &ModelState, f: &Feature, targets: &[String]) -> (bool, Vec<String>) {
|
||||
let effective = if targets.is_empty() { &f.participants } else { targets };
|
||||
let all = effective.len() == 1 && effective.first().is_some_and(|t| t == "*");
|
||||
let names = state
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|b| all || if effective.is_empty() { b.component == f.component || b.feature == f.id } else { effective.contains(&b.name) })
|
||||
.map(|b| b.name.clone())
|
||||
.collect();
|
||||
(!all && !effective.is_empty(), names)
|
||||
}
|
||||
|
||||
/// [`apply_op`] with tools whose lineage is known: the bodies it makes or changes keep theirs.
|
||||
fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Operation, targets: &[String]) -> Result<()> {
|
||||
let tools = if op == Operation::Intersect { intersection_tool_union(tools)? } else { tools };
|
||||
@@ -2100,6 +2461,7 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
|
||||
// of its component (and what it made itself): other components are left alone.
|
||||
let targets: &[String] = if targets.is_empty() { &f.participants } else { targets };
|
||||
let all = targets.len() == 1 && targets.first().is_some_and(|t| t == "*");
|
||||
let named = !all && !targets.is_empty();
|
||||
let target_idx: Vec<usize> = if all {
|
||||
(0..state.bodies.len()).collect()
|
||||
} else if targets.is_empty() {
|
||||
@@ -2234,6 +2596,24 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
if op == Operation::Intersect && acc.as_ref().is_none_or(|b| b.body.face_count() == 0) {
|
||||
let Some(mb) = state.bodies.get(ti) else { continue };
|
||||
// A tool that meets the body yet leaves nothing is a failed boolean, not an empty result.
|
||||
let mut met = false;
|
||||
if !named && !mb.body.is_mesh() {
|
||||
for t in &tools {
|
||||
if mb.body.meets(&tool_in(t, into)?.body)? {
|
||||
met = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if named || met {
|
||||
return Err(empty_intersection(&[mb.name.as_str()]));
|
||||
}
|
||||
remove.push(ti);
|
||||
continue;
|
||||
}
|
||||
match acc {
|
||||
Some(b) => {
|
||||
if let Some(mb) = state.bodies.get_mut(ti) {
|
||||
@@ -2243,6 +2623,10 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec<Traced>, op: Ope
|
||||
None => remove.push(ti),
|
||||
}
|
||||
}
|
||||
if op == Operation::Intersect && target_idx.iter().all(|i| remove.contains(i)) {
|
||||
let names: Vec<&str> = target_idx.iter().filter_map(|i| state.bodies.get(*i)).map(|b| b.name.as_str()).collect();
|
||||
return Err(empty_intersection(&names));
|
||||
}
|
||||
let mut i = 0;
|
||||
state.bodies.retain(|_| {
|
||||
let keep = !remove.contains(&i);
|
||||
@@ -3038,7 +3422,17 @@ fn blend_target(state: &ModelState, body: &Option<crate::BodyName>, edges: &[Vec
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, Vec2)> {
|
||||
/// The revolve axis in the sketch's 2D coordinates (a point on it, its direction). `construct`
|
||||
/// gives a construction axis, by name, in the sketch's 3D frame.
|
||||
pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef, construct: &dyn Fn(&str) -> Result<(Vec3, Vec3)>) -> Result<(Vec2, Vec2)> {
|
||||
let in_plane = |origin: Vec3, dir: Vec3| {
|
||||
let pl = &ss.plane;
|
||||
let d = dir.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
|
||||
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(origin).abs() > 1e-6 {
|
||||
return Err(DocError::Invalid("the revolve axis does not lie in the sketch plane".into()));
|
||||
}
|
||||
Ok((pl.to_local(origin), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
|
||||
};
|
||||
match axis {
|
||||
AxisRef::SketchLine { curve } => {
|
||||
let ci = ss.sketch.curve_index(curve).ok_or_else(|| DocError::Unknown(format!("sketch line `{curve}`")))?;
|
||||
@@ -3068,17 +3462,37 @@ pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, V
|
||||
}
|
||||
Ok((pl.to_local(Vec3::ZERO), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
|
||||
}
|
||||
AxisRef::Line { origin, dir } => {
|
||||
let pl = &ss.plane;
|
||||
let d = dir.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
|
||||
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(origin.0).abs() > 1e-6 {
|
||||
return Err(DocError::Invalid("the revolve axis does not lie in the sketch plane".into()));
|
||||
}
|
||||
Ok((pl.to_local(origin.0), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
|
||||
AxisRef::Line { origin, dir } => in_plane(origin.0, dir.0),
|
||||
AxisRef::Construction { name } => {
|
||||
let (origin, dir) = construct(name)?;
|
||||
in_plane(origin, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A construction axis `name` of the timeline so far, as evaluated, in the frame of component
|
||||
/// `comp`: a point on it and its direction. Without a document (`doc` None) it is taken in its
|
||||
/// own component's frame.
|
||||
pub(crate) fn construction_axis_in(doc: Option<&Document>, st: &ModelState, name: &str, comp: u64) -> Result<(Vec3, Vec3)> {
|
||||
let c = st
|
||||
.construct
|
||||
.iter()
|
||||
.find(|c| c.name == name)
|
||||
.ok_or_else(|| DocError::Unknown(format!("construction axis `{name}` (it must come earlier in the timeline)")))?;
|
||||
let crate::construct::ConstructGeom::Axis { origin, dir } = &c.geom else {
|
||||
return Err(DocError::Invalid(format!("`{name}` is not a construction axis")));
|
||||
};
|
||||
let Some(doc) = doc else { return Ok((*origin, *dir)) };
|
||||
let owner = doc.feature(c.feature).map_or(0, |f| f.component);
|
||||
if owner == comp || doc.occurrences.is_empty() {
|
||||
return Ok((*origin, *dir));
|
||||
}
|
||||
let inverse =
|
||||
crate::mat_inverse(&doc.component_transform(comp)).ok_or_else(|| DocError::Invalid("the feature's component frame is invalid".into()))?;
|
||||
let relative = crate::mat_mul(&inverse, &doc.component_transform(owner));
|
||||
Ok((crate::apply_point(&relative, *origin), crate::apply_vector(&relative, *dir)))
|
||||
}
|
||||
|
||||
/// Whether a drill from `p` along `d` reaches material: it starts into a body (`touches`), or,
|
||||
/// when `ahead` is set, a body lies further along `d`. A spot exactly on a surface counts only
|
||||
/// when the drill enters there, not when it leaves. Only the bodies in `scope` count.
|
||||
@@ -4113,7 +4527,7 @@ fn feature_tools_with_constructor(
|
||||
f.component,
|
||||
)?;
|
||||
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
let (o, d) = revolve_axis(&ss, axis, &|name| construction_axis_in(Some(doc), st, name, f.component))?;
|
||||
// To Object is in this feature's component frame: the model as seen from there.
|
||||
let tst = if to.is_some() { state_in_frame(doc, st, f.component) } else { std::borrow::Cow::Borrowed(st) };
|
||||
// Where the tool meets its targets is measured where they are shown (#3383).
|
||||
@@ -4704,6 +5118,7 @@ fn feature_op(f: &Feature) -> (Operation, Vec<String>) {
|
||||
FeatureKind::Pipe { operation, targets, .. } => (*operation, crate::plain(targets)),
|
||||
FeatureKind::Loft { operation, targets, .. } | FeatureKind::Sweep { operation, targets, .. } => (*operation, crate::plain(targets)),
|
||||
FeatureKind::Coil { operation, targets, .. } => (*operation, crate::plain(targets)),
|
||||
FeatureKind::Rib { .. } => (Operation::Join, Vec::new()),
|
||||
_ => (Operation::NewBody, Vec::new()),
|
||||
}
|
||||
}
|
||||
@@ -4897,7 +5312,11 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
|
||||
out.push(m);
|
||||
}
|
||||
}
|
||||
crate::PatternKind::Circular { origin, axis, count: c, angle, alignment, reversed } => {
|
||||
crate::PatternKind::Circular { origin, axis, count: c, angle, alignment, reversed, axis_feature } => {
|
||||
let (origin, axis) = match axis_feature {
|
||||
Some(name) => construction_axis_in(Some(doc), st, name, component)?,
|
||||
None => (origin.0, axis.0),
|
||||
};
|
||||
let n = count(c)?;
|
||||
let total = val(vals, angle, Kind::Angle)?;
|
||||
let full = (total.abs() - std::f64::consts::TAU).abs() < 1e-9;
|
||||
@@ -4909,7 +5328,7 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
|
||||
0.0
|
||||
};
|
||||
for i in 1..n {
|
||||
out.push(rotation(origin.0, axis.0, step * alignment.multiplier(i, *reversed)));
|
||||
out.push(rotation(origin, axis, step * alignment.multiplier(i, *reversed)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5059,6 +5478,16 @@ fn replay(
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if let FeatureKind::Rib { .. } = &src.kind {
|
||||
// A rib is built again at each copy of its sketch, against the bodies there (#4593):
|
||||
// its walls run until they meet the body, which may not lie the same way there.
|
||||
for (k, m) in mats.iter().enumerate() {
|
||||
for body in more::rib_tools(vals, st, src, Some(m))? {
|
||||
// Each copy's faces are named as the rib's, with the instance.
|
||||
let t = Traced::unnamed(body);
|
||||
copies.push(Traced { lineage: t.lineage.map(|l| l.copied(src.id, k + 1)), body: t.body });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Each copy's faces are named as the source feature's, with the instance.
|
||||
let tools = feature_tools_with_constructor(doc, vals, src, st, &mut None)?;
|
||||
@@ -6419,25 +6848,6 @@ impl Drop for ToolFrames {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`placement_digest`] of a feature with drawn tools: the placements, when it names the bodies
|
||||
/// it changes (they may lie in another component, where its tools meet them, #3383); nothing
|
||||
/// otherwise, as it then changes only bodies of its own component.
|
||||
fn drawn_tools_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
let targets = match &f.kind {
|
||||
FeatureKind::Extrude { targets, .. }
|
||||
| FeatureKind::Revolve { targets, .. }
|
||||
| FeatureKind::Loft { targets, .. }
|
||||
| FeatureKind::Sweep { targets, .. }
|
||||
| FeatureKind::Pipe { targets, .. }
|
||||
| FeatureKind::Coil { targets, .. } => !targets.is_empty(),
|
||||
_ => false,
|
||||
};
|
||||
if (!targets && f.participants.is_empty()) || (doc.occurrences.is_empty() && doc.body_offsets.is_empty()) {
|
||||
return 0;
|
||||
}
|
||||
placements_hash(doc)
|
||||
}
|
||||
|
||||
/// The matrix taking the evaluated feature's drawn tools into body `name`'s frame (`None`: the
|
||||
/// same frame).
|
||||
fn tool_frame(name: &str) -> Option<Mat> {
|
||||
|
||||
@@ -13,15 +13,16 @@ use solidcraft_sketch::Sketch;
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Work counted on this thread: bodies moved into a component's frame, STEP texts digested.
|
||||
/// Work counted on this thread: bodies moved, STEP texts digested, joint faces measured.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub(super) struct Counts {
|
||||
pub moved: usize,
|
||||
pub digested: usize,
|
||||
pub joint_faces: usize,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static COUNTS: Cell<Counts> = const { Cell::new(Counts { moved: 0, digested: 0 }) };
|
||||
static COUNTS: Cell<Counts> = const { Cell::new(Counts { moved: 0, digested: 0, joint_faces: 0 }) };
|
||||
}
|
||||
|
||||
pub(super) fn count(f: impl FnOnce(&mut Counts)) {
|
||||
@@ -39,6 +40,103 @@ fn counted(f: impl FnOnce()) -> Counts {
|
||||
COUNTS.with(Cell::get)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_face_measurements_are_shared_across_motion_samples_and_snap_kinds() {
|
||||
use crate::joints::{Joint, JointKind, JointOrigin, Snap, XRef};
|
||||
let mut doc = Document::new("joint face work");
|
||||
let part = doc.add_component(Some("Crank"), 0).unwrap();
|
||||
let occurrence = doc.occurrences.iter().find(|o| o.component == part).unwrap().id;
|
||||
let id = doc
|
||||
.add_feature(
|
||||
FeatureKind::Cylinder {
|
||||
base: point_expr(Vec3::ZERO),
|
||||
axis: Dir(Vec3::Z),
|
||||
radius: "2".into(),
|
||||
height: "10".into(),
|
||||
operation: Operation::NewBody,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
doc.feature_mut(id).unwrap().component = part;
|
||||
let mut model = Model::new();
|
||||
model.evaluate(&doc);
|
||||
let state = model.state();
|
||||
let pick = Vec3::new(0.0, 0.0, 10.0);
|
||||
let face = joints::face_name_at(&doc, &state, part, pick, false).unwrap();
|
||||
doc.assembly.joints.push(Joint {
|
||||
id: 100,
|
||||
name: "Pivot".into(),
|
||||
kind: JointKind::Revolute,
|
||||
a: JointOrigin { occurrence: 0, snap: Snap::Frame { origin: pick, z: Vec3::Z, x: Some(Vec3::X) } },
|
||||
b: JointOrigin { occurrence, snap: Snap::FaceCenter { pick, face: None, x: Some(XRef::Dir(Vec3::X)) } },
|
||||
values: vec![0.0],
|
||||
limits: Vec::new(),
|
||||
flip: true,
|
||||
offset: 0.0,
|
||||
angle: 0.0,
|
||||
suppressed: false,
|
||||
exprs: Default::default(),
|
||||
});
|
||||
// One body supplies both old nearest-face snaps and persistent named-face snaps.
|
||||
// A scratch model shares its measurements, even though each sample changes the pose.
|
||||
let snapshot = model.clone();
|
||||
let work = counted(|| {
|
||||
for name in [None, Some(face)] {
|
||||
doc.assembly.joints[0].b.snap = Snap::FaceCenter { pick, face: name, x: Some(XRef::Dir(Vec3::X)) };
|
||||
for k in 0..145 {
|
||||
let angle = (k as f64 * 2.5).to_radians();
|
||||
doc.assembly.joints[0].values[0] = angle;
|
||||
let solution = joints::solve(&doc, &snapshot.state());
|
||||
assert!(solution.errors.is_empty() && solution.conflicts.is_empty(), "{solution:?}");
|
||||
let point = apply_point(&solution.world[&occurrence], Vec3::new(5.0, 0.0, 10.0));
|
||||
assert!(point.dist(Vec3::new(5.0 * angle.cos(), 5.0 * angle.sin(), 10.0)) < 1e-9);
|
||||
doc.apply_joint_solution(&solution);
|
||||
}
|
||||
}
|
||||
});
|
||||
assert_eq!(work.joint_faces, 1, "pose-only samples must not remeasure unchanged faces: {work:?}");
|
||||
|
||||
// Editing geometry creates a fresh measurement without changing a retained snapshot.
|
||||
let FeatureKind::Cylinder { height, .. } = &mut doc.feature_mut(id).unwrap().kind else { panic!("cylinder") };
|
||||
*height = "12".into();
|
||||
model.evaluate(&doc);
|
||||
let origin = &doc.assembly.joints[0].b;
|
||||
let work = counted(|| {
|
||||
let changed = joints::resolve_origin(&doc, &model.state(), origin).unwrap();
|
||||
let retained = joints::resolve_origin(&doc, &snapshot.state(), origin).unwrap();
|
||||
assert!((changed[3][2] - 12.0).abs() < 1e-9);
|
||||
assert!((retained[3][2] - 10.0).abs() < 1e-9);
|
||||
});
|
||||
assert_eq!(work.joint_faces, 1, "only the edited body must be measured: {work:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_face_measurements_never_follow_a_replaced_body_or_stale_offset() {
|
||||
use crate::joints::{JointOrigin, Snap};
|
||||
let mut doc = Document::new("replaced joint geometry");
|
||||
let mut state = ModelState::default();
|
||||
state.bodies.push(ModelBody::new("Pin".into(), kernel::cylinder(Vec3::ZERO, Vec3::Z, 2.0, 10.0).unwrap(), 0));
|
||||
let origin = JointOrigin { occurrence: 0, snap: Snap::FaceCenter { pick: Vec3::new(0.0, 0.0, 10.0), face: None, x: None } };
|
||||
let work = counted(|| {
|
||||
joints::resolve_origin(&doc, &state, &origin).unwrap();
|
||||
});
|
||||
assert_eq!(work.joint_faces, 1);
|
||||
let retained = state.clone();
|
||||
// Body is public; a clone's old measurement must never mask a direct replacement.
|
||||
state.bodies[0].body = kernel::cylinder(Vec3::ZERO, Vec3::Z, 2.0, 12.0).unwrap();
|
||||
let changed = joints::resolve_origin(&doc, &state, &origin).unwrap();
|
||||
assert!((changed[3][2] - 12.0).abs() < 1e-9);
|
||||
let work = counted(|| {
|
||||
let old = joints::resolve_origin(&doc, &retained, &origin).unwrap();
|
||||
assert!((old[3][2] - 10.0).abs() < 1e-9);
|
||||
doc.body_offsets.insert("Pin".into(), rigid(Vec3::new(0.0, 0.0, 1.0), Vec3::ZERO, Vec3::Z, 0.0));
|
||||
let moved = joints::resolve_origin(&doc, &retained, &origin).unwrap();
|
||||
assert!((moved[3][2] - 11.0).abs() < 1e-9);
|
||||
});
|
||||
assert_eq!(work.joint_faces, 0, "offsets are applied after the shared face measurements");
|
||||
}
|
||||
|
||||
/// `parts` imported files of two boxes each, every one in its own component placed away from
|
||||
/// the origin (as inserted parts are).
|
||||
fn placed_imports(parts: usize) -> Document {
|
||||
|
||||
@@ -69,27 +69,8 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
|
||||
}
|
||||
FeatureKind::Rib { sketch, curves, thickness, depth, flip, web, normal } => {
|
||||
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
|
||||
let t = val(vals, thickness, Kind::Length)?;
|
||||
if !(t > 0.0) {
|
||||
return Err(DocError::Invalid("the rib thickness must be positive".into()));
|
||||
}
|
||||
let depth = match depth {
|
||||
Some(e) => Some(val(vals, e, Kind::Length)?)
|
||||
.filter(|d| *d > 0.0)
|
||||
.ok_or_else(|| DocError::Invalid("the rib depth must be positive".into()))
|
||||
.map(Some)?,
|
||||
None => None,
|
||||
};
|
||||
let groups: Vec<Vec<String>> = if *web { curves.iter().map(|c| vec![c.clone()]).collect() } else { vec![curves.clone()] };
|
||||
if groups.is_empty() || curves.is_empty() || curves.len() > 1000 {
|
||||
return Err(DocError::Invalid("a rib needs 1…1000 sketch curves".into()));
|
||||
}
|
||||
let mut tools = Vec::new();
|
||||
for g in &groups {
|
||||
tools.push(if *normal { rib_tool_normal(st, &ss, g, t, depth, *flip)? } else { rib_tool(st, &ss, g, t, depth, *flip)? });
|
||||
}
|
||||
FeatureKind::Rib { .. } => {
|
||||
let tools = rib_tools(vals, st, f, None)?;
|
||||
apply_op(st, f, tools, Operation::Join, &[])
|
||||
}
|
||||
FeatureKind::ReplaceFace { faces, target, body } => {
|
||||
@@ -176,6 +157,41 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
}
|
||||
|
||||
/// The walls of rib `f` (one per curve for a web), each run until it meets the bodies. `at`
|
||||
/// moves the rib's sketch first: a pattern's or mirror's copy of the rib is built again where
|
||||
/// the copy is, against the bodies there (#4593), as a hole is drilled again at each copy.
|
||||
pub(super) fn rib_tools(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature, at: Option<&crate::Mat>) -> Result<Vec<Body>> {
|
||||
let FeatureKind::Rib { sketch, curves, thickness, depth, flip, web, normal } = &f.kind else {
|
||||
return Err(DocError::Invalid(format!("{} is not a rib", f.name)));
|
||||
};
|
||||
let mut ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
|
||||
if let Some(m) = at {
|
||||
let p = ss.plane;
|
||||
ss.plane = Plane::new(apply_point(m, p.origin), crate::apply_vector(m, p.x), crate::apply_vector(m, p.y))
|
||||
.ok_or_else(|| DocError::Invalid("the rib's sketch plane at the copy".into()))?;
|
||||
}
|
||||
let t = val(vals, thickness, Kind::Length)?;
|
||||
if !(t > 0.0) {
|
||||
return Err(DocError::Invalid("the rib thickness must be positive".into()));
|
||||
}
|
||||
let depth = match depth {
|
||||
Some(e) => Some(val(vals, e, Kind::Length)?)
|
||||
.filter(|d| *d > 0.0)
|
||||
.ok_or_else(|| DocError::Invalid("the rib depth must be positive".into()))
|
||||
.map(Some)?,
|
||||
None => None,
|
||||
};
|
||||
let groups: Vec<Vec<String>> = if *web { curves.iter().map(|c| vec![c.clone()]).collect() } else { vec![curves.clone()] };
|
||||
if groups.is_empty() || curves.is_empty() || curves.len() > 1000 {
|
||||
return Err(DocError::Invalid("a rib needs 1…1000 sketch curves".into()));
|
||||
}
|
||||
let mut tools = Vec::new();
|
||||
for g in &groups {
|
||||
tools.push(if *normal { rib_tool_normal(st, &ss, g, t, depth, *flip)? } else { rib_tool(st, &ss, g, t, depth, *flip)? });
|
||||
}
|
||||
Ok(tools)
|
||||
}
|
||||
|
||||
/// Unit outward normal of the planar face through `p`: of the planar faces whose plane runs
|
||||
/// through it, the one nearest it. A face elsewhere in such a plane (the wall of a notch, the end
|
||||
/// of a rib, in line with a rim) is no pick of `p`, though its plane runs through it as exactly.
|
||||
@@ -728,7 +744,9 @@ pub(super) fn sheet_eval(
|
||||
None => p.0,
|
||||
});
|
||||
}
|
||||
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &rv, face.0, &picks)?;
|
||||
let placement = doc.component_transform(doc.model_body_component(&mb));
|
||||
let body_to_world = doc.model_body_offset(&mb).map(|offset| crate::mat_mul(&placement, offset)).unwrap_or(placement);
|
||||
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &rv, face.0, &picks, &body_to_world)?;
|
||||
for note in notes {
|
||||
append_warning(warning, note);
|
||||
}
|
||||
@@ -1327,6 +1345,9 @@ fn sheet_prism_cut(
|
||||
} else {
|
||||
let material = sh.flat_material()?;
|
||||
if material.is_empty() {
|
||||
if operation == Operation::Intersect && super::boolean_scope(st, f, targets).0 {
|
||||
return Err(super::empty_intersection(&[sh.body.as_str()]));
|
||||
}
|
||||
// Empty exact material removes geometry and metadata together. Budget or
|
||||
// topology errors propagate; they are never mistaken for an empty blank.
|
||||
plans.push((sh.body.clone(), None));
|
||||
@@ -1338,6 +1359,14 @@ fn sheet_prism_cut(
|
||||
}
|
||||
}
|
||||
}
|
||||
if operation == Operation::Intersect {
|
||||
// Flat material is exact: an empty one is a true miss, removed only while some target remains.
|
||||
let (_, scope) = super::boolean_scope(st, f, targets);
|
||||
let emptied: Vec<&str> = plans.iter().filter(|(_, r)| r.is_none()).map(|(name, _)| name.as_str()).collect();
|
||||
if !emptied.is_empty() && scope.iter().all(|b| emptied.contains(&b.as_str())) {
|
||||
return Err(super::empty_intersection(&emptied));
|
||||
}
|
||||
}
|
||||
let handled = plans.iter().map(|(name, _)| name.clone()).collect();
|
||||
if !notes.is_empty() {
|
||||
append_warning(warning, notes.join("; "));
|
||||
@@ -1714,6 +1743,21 @@ fn offset_left(p: &[Vec2], d: f64) -> Vec<Vec2> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The smallest size a plastic feature's lengths may have, in millimetres. Thinner walls and
|
||||
/// shorter arms than this are below what the solid booleans can resolve.
|
||||
const PLASTIC_MIN_SIZE: f64 = 0.01;
|
||||
|
||||
/// `v`, the feature's `what`, when it is at least [`PLASTIC_MIN_SIZE`].
|
||||
fn plastic_size(feature: &str, what: &str, v: f64) -> Result<f64> {
|
||||
if v.is_finite() && v >= PLASTIC_MIN_SIZE {
|
||||
Ok(v)
|
||||
} else {
|
||||
// Tiny values in exponent form: 1e-320 written out is hundreds of zeros.
|
||||
let got = if v != 0.0 && v.abs() < 1e-3 { format!("{v:e}") } else { format!("{v}") };
|
||||
Err(DocError::Invalid(format!("the {feature}'s {what} must be at least {PLASTIC_MIN_SIZE} mm (got {got} mm)")))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
let len = |e: &str| val(vals, e, Kind::Length);
|
||||
// The plastic rule of the body a feature goes on gives its defaults (draft, clearance).
|
||||
@@ -1738,10 +1782,8 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
body,
|
||||
} => {
|
||||
let target = body_under(st, body, position.0)?;
|
||||
let (r0, h) = (len(diameter)? / 2.0, len(height)?);
|
||||
if !(r0 > 0.0 && h > 0.0) {
|
||||
return Err(DocError::Invalid("the boss diameter and height must be positive".into()));
|
||||
}
|
||||
let r0 = plastic_size("boss", "diameter", len(diameter)?)? / 2.0;
|
||||
let h = plastic_size("boss", "height", len(height)?)?;
|
||||
let dr = match draft {
|
||||
Some(e) => val(vals, e, Kind::Angle)?,
|
||||
None => rule(&target)?.map_or(0.0, |r| r.draft),
|
||||
@@ -1784,8 +1826,10 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
None => 0.0,
|
||||
};
|
||||
let hr = h - off;
|
||||
if n > 0.0 && !(t > 0.0 && l > 0.0 && hr > 0.0) {
|
||||
return Err(DocError::Invalid("ribs need a positive thickness, length and height".into()));
|
||||
if n > 0.0 {
|
||||
plastic_size("boss", "rib_thickness", t)?;
|
||||
plastic_size("boss", "rib_length", l)?;
|
||||
plastic_size("boss", "rib height (height less rib_offset)", hr)?;
|
||||
}
|
||||
for k in 0..n as usize {
|
||||
let a = std::f64::consts::TAU * k as f64 / n;
|
||||
@@ -1816,9 +1860,9 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
}
|
||||
}
|
||||
if let Some(hd) = hole_diameter {
|
||||
let rh = len(hd)? / 2.0;
|
||||
let rh = plastic_size("boss", "hole_diameter", len(hd)?)? / 2.0;
|
||||
let depth = match hole_depth {
|
||||
Some(e) => len(e)?,
|
||||
Some(e) => plastic_size("boss", "hole_depth", len(e)?)?,
|
||||
None => h,
|
||||
};
|
||||
if !(rh > 0.0 && rh < r_top && depth > 0.0) {
|
||||
@@ -1835,14 +1879,15 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
let target = body_under(st, body, face.0)?;
|
||||
let mb = st.body(&target).cloned().ok_or_else(|| DocError::Invalid("body".into()))?;
|
||||
let n = planar_normal_at(&mb.body, face.0).ok_or_else(|| DocError::Invalid("pick the planar rim face".into()))?;
|
||||
let (w, h) = (len(width)?, len(height)?);
|
||||
let what = if *groove { "groove" } else { "lip" };
|
||||
let (w, h) = (plastic_size(what, "width", len(width)?)?, plastic_size(what, "height", len(height)?)?);
|
||||
let g = match gap {
|
||||
Some(e) => len(e)?,
|
||||
None if *groove => rule(&target)?.map_or(0.0, |r| r.clearance),
|
||||
None => 0.0,
|
||||
};
|
||||
if !(w > 0.0 && h > 0.0 && g >= 0.0) {
|
||||
return Err(DocError::Invalid("the lip width and height must be positive".into()));
|
||||
if !(g >= 0.0) {
|
||||
return Err(DocError::Invalid("the groove's gap must not be negative".into()));
|
||||
}
|
||||
let plane = Plane::from_normal(face.0, n).ok_or_else(|| DocError::Invalid("rim plane".into()))?;
|
||||
let tol = (mb.body.size() * 1e-3).max(1e-3);
|
||||
@@ -1920,9 +1965,11 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
}
|
||||
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
|
||||
let target = body_under(st, body, position.0)?;
|
||||
let (l, t, w, cd, cl) = (len(length)?, len(thickness)?, len(width)?, len(catch_depth)?, len(catch_length)?);
|
||||
if !(l > 0.0 && t > 0.0 && w > 0.0 && cd > 0.0 && cl > 0.0 && cl < l) {
|
||||
return Err(DocError::Invalid("the snap fit's sizes must be positive and the catch shorter than the arm".into()));
|
||||
let size = |what: &str, e: &str| plastic_size("snap fit", what, len(e)?);
|
||||
let (l, t, w) = (size("length", length)?, size("thickness", thickness)?, size("width", width)?);
|
||||
let (cd, cl) = (size("catch_depth", catch_depth)?, size("catch_length", catch_length)?);
|
||||
if !(cl < l) {
|
||||
return Err(DocError::Invalid("the snap fit's catch must be shorter than its arm".into()));
|
||||
}
|
||||
let (x, y, z) = local_frame(direction.0, Some(hook.0))?;
|
||||
let delta = (l * 0.05).clamp(1e-3, 0.5);
|
||||
@@ -1943,15 +1990,16 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
}
|
||||
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
|
||||
let target = body_under(st, body, position.0)?;
|
||||
let (w, h) = (len(width)?, len(height)?);
|
||||
let l = length.as_deref().map(len).transpose()?;
|
||||
let size = |what: &str, e: &str| plastic_size("rest", what, len(e)?);
|
||||
let (w, h) = (size("width", width)?, size("height", height)?);
|
||||
let l = length.as_deref().map(|e| size("length", e)).transpose()?;
|
||||
let dr = match draft {
|
||||
Some(e) => val(vals, e, Kind::Angle)?,
|
||||
None => rule(&target)?.map_or(0.0, |r| r.draft),
|
||||
};
|
||||
let t = thickness.as_deref().map(len).transpose()?;
|
||||
if !(w > 0.0 && h > 0.0 && l.is_none_or(|l| l > 0.0) && t.is_none_or(|t| t > 0.0 && 2.0 * t < w.min(l.unwrap_or(w)))) {
|
||||
return Err(DocError::Invalid("the rest's sizes must be positive and its wall thinner than half its width".into()));
|
||||
let t = thickness.as_deref().map(|e| size("thickness", e)).transpose()?;
|
||||
if t.is_some_and(|t| 2.0 * t >= w.min(l.unwrap_or(w))) {
|
||||
return Err(DocError::Invalid("the rest's wall must be thinner than half its width".into()));
|
||||
}
|
||||
if !(dr.abs() < 1.0) {
|
||||
return Err(DocError::Invalid("the rest's draft must be under 57°".into()));
|
||||
|
||||
@@ -304,7 +304,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f.component,
|
||||
)?;
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
let (o, d) = revolve_axis(&ss, axis, &|name| construction_axis_in(Some(doc), st, name, f.component))?;
|
||||
let a = val(vals, angle, Kind::Angle)?;
|
||||
let (start, sweep) = match (angle2, symmetric) {
|
||||
(Some(a2), _) => {
|
||||
|
||||
@@ -341,6 +341,12 @@ fn lex_spans(s: &str) -> Result<Vec<(Tok, usize, usize)>, DocError> {
|
||||
let mut out = Vec::new();
|
||||
// Parentheses open at this point: a comma outside them can only be a decimal comma.
|
||||
let mut depth = 0usize;
|
||||
// A single leading `=` marks the text as a formula (`=d3`, #4681) and is not part of it.
|
||||
if let Some(k) = cs.iter().position(|(_, c)| !c.is_whitespace())
|
||||
&& cs.get(k).is_some_and(|(_, c)| *c == '=')
|
||||
{
|
||||
i = k + 1;
|
||||
}
|
||||
while let Some(&(_, c)) = cs.get(i) {
|
||||
if c.is_whitespace() {
|
||||
i += 1;
|
||||
@@ -564,7 +570,9 @@ fn is_name_shaped(s: &str) -> bool {
|
||||
pub fn definition(s: &str) -> Option<(&str, &str)> {
|
||||
let (name, rhs) = s.split_once('=')?;
|
||||
let name = name.trim();
|
||||
is_name_shaped(name).then(|| (name, rhs.trim()))
|
||||
let rhs = rhs.trim();
|
||||
// `w == 3` is not a definition: a second `=` would read as a formula marker (#4681).
|
||||
(is_name_shaped(name) && !rhs.starts_with('=')).then_some((name, rhs))
|
||||
}
|
||||
|
||||
/// The length unit of a design with these units: `mm` unless they name another length unit.
|
||||
@@ -1252,8 +1260,6 @@ mod tests {
|
||||
("szerokość = 20", "szerokość", "20"),
|
||||
// Reserved names split too: the parameter check names the reason.
|
||||
("mm = 5", "mm", "5"),
|
||||
// A second `=` stays in the expression, which then fails to evaluate.
|
||||
("w == 3", "w", "= 3"),
|
||||
("w = a = 3", "w", "a = 3"),
|
||||
// An empty expression is the caller's error to report.
|
||||
("w =", "w", ""),
|
||||
@@ -1262,7 +1268,7 @@ mod tests {
|
||||
for (text, name, rhs) in splits {
|
||||
assert_eq!(definition(text), Some((name, rhs)), "{text:?}");
|
||||
}
|
||||
for text in ["20", "20 mm", "width / 2", "= 20", "=", "", "2w = 3", "a b = 3", "a-b = 3", "w.x = 3", "(w) = 3", "-w = 3"] {
|
||||
for text in ["20", "20 mm", "width / 2", "= 20", "=", "", "2w = 3", "a b = 3", "a-b = 3", "w.x = 3", "(w) = 3", "-w = 3", "w == 3"] {
|
||||
assert_eq!(definition(text), None, "{text:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,18 @@ impl FeatureKind {
|
||||
v
|
||||
}
|
||||
|
||||
/// Whether the feature names the feature `name` (the construction plane a plane is built on,
|
||||
/// a patterned or mirrored feature).
|
||||
pub fn names_feature(&self, name: &str) -> bool {
|
||||
let mut found = false;
|
||||
self.walk_refs(&mut |r| {
|
||||
if let Ref::Feature(n) = r {
|
||||
found |= n.as_ref() == name;
|
||||
}
|
||||
});
|
||||
found
|
||||
}
|
||||
|
||||
/// Point references to sketches (by id) and features (by name) at their copies, or at a
|
||||
/// feature's new name (`timeline.rename`).
|
||||
pub fn remap_refs(&mut self, sketches: &BTreeMap<u64, u64>, names: &BTreeMap<String, String>) {
|
||||
|
||||
@@ -578,6 +578,7 @@ pub fn read(s: &str) -> Result<Document> {
|
||||
validate_body_transfers(&v)?;
|
||||
FORMATS.upgrade(&mut v, version, applied.as_deref())?;
|
||||
let mut d: Document = serde_json::from_value(v).map_err(|e| DocError::Invalid(format!("design file: {e}")))?;
|
||||
check_typed_numbers(&d)?;
|
||||
let mut legacy_by_feature = std::collections::BTreeMap::<usize, Vec<usize>>::new();
|
||||
for (feature, wire) in legacy_wires {
|
||||
legacy_by_feature.entry(feature).or_default().push(wire);
|
||||
@@ -674,6 +675,9 @@ pub fn read(s: &str) -> Result<Document> {
|
||||
d.features.iter().map(|f| f.id).chain(d.components.iter().map(|c| c.id)).chain(d.occurrences.iter().map(|o| o.id)).max().unwrap_or(0);
|
||||
let floor = largest.checked_add(1).ok_or_else(|| DocError::Invalid("damaged design: the id space is exhausted".into()))?;
|
||||
d.next_id = d.next_id.max(floor).max(1);
|
||||
// These lists used to allocate independently. Keep their existing identities, but never
|
||||
// let a later deletion make them available again after loading an older design.
|
||||
d.next_id = d.next_item_id()?;
|
||||
d.format = format_string(FORMAT);
|
||||
d.format_pending = pending_names();
|
||||
Ok(d)
|
||||
@@ -1043,6 +1047,16 @@ fn check_design_numbers(v: &Value) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The range check again, on the typed design: an integer such as `9223372036854775808` passes
|
||||
/// [`check_numbers`] in the file (ids and counters are integers and may be large), but once it
|
||||
/// lands in a coordinate or size it is a float, and floats are written as floats. Integer fields
|
||||
/// (ids, counters, indexes) stay integers and are never limited, so the field's type decides
|
||||
/// what is checked, not its name.
|
||||
fn check_typed_numbers(d: &Document) -> Result<()> {
|
||||
let typed = serde_json::to_value(d).map_err(|e| DocError::Invalid(format!("design file: {e}")))?;
|
||||
check_design_numbers(&typed)
|
||||
}
|
||||
|
||||
/// Every non-integer number in the file is finite and within ±`MAX_REAL` (a damaged or hostile
|
||||
/// file is refused instead of overflowing the geometry later).
|
||||
fn check_numbers(v: &Value, depth: usize) -> Result<()> {
|
||||
@@ -2038,6 +2052,46 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn older_auxiliary_ids_advance_the_saved_counter_before_deletion() {
|
||||
use serde_json::json;
|
||||
|
||||
let origin = json!({"occurrence": 0, "snap": {"type": "point", "point": [0, 0, 0]}});
|
||||
let cases = [
|
||||
("browser_groups", json!({"id": 100, "name": "Group", "component": 0, "folder": "bodies", "items": []})),
|
||||
("timeline_groups", json!({"id": 100, "name": "Folder", "features": []})),
|
||||
(
|
||||
"canvases",
|
||||
json!({"id": 100, "name": "Canvas", "format": "png", "data": "", "pixels": [1, 1],
|
||||
"plane": {"type": "origin", "name": "XY"}, "center": [0, 0], "width": 10,
|
||||
"angle": 0, "opacity": 0.5, "flip": false, "visible": true}),
|
||||
),
|
||||
("joints", json!({"id": 100, "name": "Joint", "kind": "rigid", "a": origin, "b": origin})),
|
||||
("contact_sets", json!({"id": 100, "name": "Contact", "occurrences": []})),
|
||||
("studies", json!({"id": 100, "name": "Study", "steps": 10, "keys": []})),
|
||||
("exploded", json!({"id": 100, "name": "View", "moves": []})),
|
||||
];
|
||||
for (field, item) in cases {
|
||||
let json = edited(&Document::new("Legacy IDs"), |v| {
|
||||
v.as_object_mut().unwrap().remove("next_id");
|
||||
if matches!(field, "browser_groups" | "timeline_groups" | "canvases") {
|
||||
v[field] = json!([item]);
|
||||
} else {
|
||||
v["assembly"] = json!({field: [item]});
|
||||
}
|
||||
});
|
||||
let mut back = read(&json).unwrap_or_else(|e| panic!("{field}: {e}"));
|
||||
assert_eq!(back.next_id, 101, "{field}");
|
||||
back.browser_groups.clear();
|
||||
back.timeline_groups.clear();
|
||||
back.canvases.clear();
|
||||
back.assembly = Default::default();
|
||||
let mut reopened = read(&back.to_json()).unwrap();
|
||||
assert_eq!(reopened.allocate_id().unwrap(), 101, "{field}: deleted legacy id was forgotten");
|
||||
assert_eq!(reopened.allocate_id().unwrap(), 102, "{field}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_component_or_occurrence_ids_are_a_damaged_design() {
|
||||
let d = two_components_one_occurrence();
|
||||
@@ -2052,6 +2106,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// An integer-valued coordinate is range-checked like a float one, while ids and counters
|
||||
/// may still be large integers (solidcraft#5021).
|
||||
#[test]
|
||||
fn integer_valued_coordinates_are_range_checked_and_large_ids_still_load() {
|
||||
let mut sketch = solidcraft_sketch::Sketch::new();
|
||||
sketch.add_point(solidcraft_geom::Vec2::new(40.0, 0.0), None).unwrap();
|
||||
let mut d = Document::new("range");
|
||||
d.add_feature(crate::FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch }, None).unwrap();
|
||||
let last = |v: &Value| v["features"][0]["sketch"]["points"].as_array().map_or(0, |a| a.len() - 1);
|
||||
for huge in ["9223372036854775808", "4294967295", "2000000000", "-2000000000"] {
|
||||
let json = edited(&d, |v| {
|
||||
let i = last(v);
|
||||
v["features"][0]["sketch"]["points"][i]["pos"][1] = serde_json::from_str(huge).unwrap();
|
||||
});
|
||||
let error = read(&json).unwrap_err().to_string();
|
||||
assert!(error.contains("damaged design") && error.contains("out of range"), "{huge}: {error}");
|
||||
}
|
||||
// In range, an integer-valued coordinate is fine.
|
||||
let json = edited(&d, |v| {
|
||||
let i = last(v);
|
||||
v["features"][0]["sketch"]["points"][i]["pos"][1] = 1_000_000_000u64.into();
|
||||
});
|
||||
assert!(read(&json).is_ok());
|
||||
// Ids and counters are integers and may be past the coordinate limit.
|
||||
let json = edited(&d, |v| {
|
||||
v["features"][0]["id"] = 5_000_000_000u64.into();
|
||||
v["next_id"] = 5_000_000_001u64.into();
|
||||
});
|
||||
let back = read(&json).unwrap();
|
||||
assert_eq!(back.features[0].id, 5_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exhausted_id_space_is_an_error_not_a_panic() {
|
||||
let d = two_components_one_occurrence();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
//! A circular pattern can turn about a construction axis (`axis_feature`, its name) and a revolve
|
||||
//! or surface revolve can take one as its axis (`"type": "construction"`, with `name`); both follow
|
||||
//! the axis when it is edited. Older designs need no transformation: they name no construction
|
||||
//! axis. The last release refuses a design saved with this (the newer format number).
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Associative motion traces retain their occurrence path, point reference, joint bounds or
|
||||
//! study identity, sample count and assembly branch seeds, alongside the generated sketch.
|
||||
//! Older designs default to no associations: their saved curves remain fixed. Readers from
|
||||
//! the last release refuse the newer format number instead of silently dropping associations.
|
||||
@@ -0,0 +1,5 @@
|
||||
//! The design saves the body names a `body.rename` freed (`Body1` after the body it named was
|
||||
//! renamed), so a name the design used is not used again.
|
||||
//!
|
||||
//! A design saved before this has no such names: it keeps the names it has, and the next body
|
||||
//! it makes may take a name an earlier rename freed.
|
||||
@@ -0,0 +1,3 @@
|
||||
//! Circular sketch patterns may retain a Quantity expression alongside their evaluated count.
|
||||
//! Older designs have no expression and keep their fixed counts unchanged. The previous
|
||||
//! release must request an update rather than discard the expression on save.
|
||||
@@ -572,6 +572,8 @@ pub struct Assembly {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub studies: Vec<crate::motion::MotionStudy>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub traces: Vec<crate::motion::MotionTrace>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub exploded: Vec<crate::motion::ExplodedView>,
|
||||
}
|
||||
|
||||
@@ -597,6 +599,15 @@ impl Assembly {
|
||||
}
|
||||
}
|
||||
}
|
||||
for trace in &self.traces {
|
||||
if let crate::motion::TraceMotion::Joint { joint, from, to, .. } = &trace.motion {
|
||||
for (label, value) in [("from", from), ("to", to)] {
|
||||
if let serde_json::Value::String(e) = value {
|
||||
out.push((*joint, format!("trace {} {label}", trace.sketch), e.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -616,6 +627,15 @@ impl Assembly {
|
||||
}
|
||||
}
|
||||
}
|
||||
for trace in &mut self.traces {
|
||||
if let crate::motion::TraceMotion::Joint { from, to, .. } = &mut trace.motion {
|
||||
for value in [from, to] {
|
||||
if let serde_json::Value::String(e) = value {
|
||||
out.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -627,6 +647,7 @@ impl Assembly {
|
||||
&& !self.contact_enabled
|
||||
&& !self.contact_all
|
||||
&& self.studies.is_empty()
|
||||
&& self.traces.is_empty()
|
||||
&& self.exploded.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -731,8 +752,7 @@ fn nearest_plane(doc: &Document, st: &ModelState, o: &JointOrigin, pick: Vec3) -
|
||||
let mut best: Option<(f64, Vec3, Vec3)> = None;
|
||||
for b in component_bodies(doc, st, comp) {
|
||||
let offset = doc.model_body_offset(b);
|
||||
let tol = (b.body.size() * 2e-3).max(1e-3);
|
||||
for f in b.body.faces(tol)? {
|
||||
for f in b.joint_faces()?.iter() {
|
||||
let Some(n) = f.plane_normal else { continue };
|
||||
let (c, n) = match offset {
|
||||
Some(m) => (apply_point(m, f.centroid), apply_vector(m, n)),
|
||||
@@ -836,6 +856,15 @@ pub fn clamp(j: &Joint, q: &mut [f64]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Work requested after placing occurrences and checking their joint constraints.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Analysis {
|
||||
/// Placement, loop closure, conflicts and errors only; `Solution::dof` is empty.
|
||||
Placement,
|
||||
/// Also count each occurrence's remaining motion, including nearby singular poses.
|
||||
DegreesOfFreedom,
|
||||
}
|
||||
|
||||
/// The result of placing occurrences by their joints.
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Solution {
|
||||
@@ -852,7 +881,7 @@ pub struct Solution {
|
||||
/// Remaining degrees of freedom per occurrence (6 = free, 0 = fixed): how many independent
|
||||
/// values still move it, from the fixed occurrences, with loops kept closed and a value a
|
||||
/// motion link sets moving with the values its link chain starts from (a driven gear has its
|
||||
/// driving gear's one, a screw's travel its turn's).
|
||||
/// driving gear's one, a screw's travel its turn's). Empty for [`Analysis::Placement`].
|
||||
pub dof: BTreeMap<u64, usize>,
|
||||
/// Occurrences fixed in place (grounded, or the root's).
|
||||
pub fixed: Vec<u64>,
|
||||
@@ -943,22 +972,36 @@ fn flip_of(j: &Joint) -> Mat {
|
||||
if !j.flip { mat_mul(&m, &[[1.0, 0.0, 0.0, 0.0], [0.0, -1.0, 0.0, 0.0], [0.0, 0.0, -1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]) } else { m }
|
||||
}
|
||||
|
||||
/// Component-space frames stay fixed throughout one solve. Cache their inverses without
|
||||
/// regrouping products: reversed joints still invert the value-dependent link as before.
|
||||
struct JointFrames {
|
||||
a: Mat,
|
||||
b: Mat,
|
||||
inverse_a: Option<Mat>,
|
||||
inverse_b: Option<Mat>,
|
||||
}
|
||||
|
||||
impl JointFrames {
|
||||
fn new(a: Mat, b: Mat) -> Self {
|
||||
Self { a, b, inverse_a: mat_inverse(&a), inverse_b: mat_inverse(&b) }
|
||||
}
|
||||
|
||||
fn through(&self, joint: &Joint, values: &[f64], from_a: bool, t: &Mat) -> Option<Mat> {
|
||||
let link = mat_mul(&link_of(joint, values), self.inverse_b.as_ref()?);
|
||||
if from_a { Some(mat_mul(&mat_mul(t, &self.a), &link)) } else { Some(mat_mul(&mat_mul(t, &mat_inverse(&link)?), self.inverse_a.as_ref()?)) }
|
||||
}
|
||||
}
|
||||
|
||||
/// B's world transform from A's (or A's from B's, going the other way) for the joint's values.
|
||||
fn through(j: &Joint, (fa, fb): &(Mat, Mat), q: &[f64], from_a: bool, t: &Mat) -> Option<Mat> {
|
||||
let link = mat_mul(&link_of(j, q), &mat_inverse(fb)?);
|
||||
if from_a {
|
||||
Some(mat_mul(&mat_mul(t, fa), &link))
|
||||
} else {
|
||||
// T_A = T_B · link⁻¹ · F_a⁻¹
|
||||
Some(mat_mul(&mat_mul(t, &mat_inverse(&link)?), &mat_inverse(fa)?))
|
||||
}
|
||||
JointFrames::new(*fa, *fb).through(j, q, from_a, t)
|
||||
}
|
||||
|
||||
/// The spanning tree the solver places occurrences along, out from the fixed ones.
|
||||
struct Tree<'a> {
|
||||
joints: BTreeMap<u64, &'a Joint>,
|
||||
/// Joint frames in component coordinates (A's, B's).
|
||||
frames: BTreeMap<u64, (Mat, Mat)>,
|
||||
frames: BTreeMap<u64, JointFrames>,
|
||||
fixed: BTreeMap<u64, Mat>,
|
||||
/// Placed occurrence → (the occurrence it is placed from, the joint, from the joint's A side).
|
||||
parent: BTreeMap<u64, (u64, u64, bool)>,
|
||||
@@ -968,8 +1011,10 @@ struct Tree<'a> {
|
||||
|
||||
impl Tree<'_> {
|
||||
fn step(&self, joint: u64, from_a: bool, t: &Mat, vals: &Values) -> Option<Mat> {
|
||||
#[cfg(test)]
|
||||
tests::PLACEMENT_STEPS.with(|steps| steps.set(steps.get() + 1));
|
||||
let q = vals.get(&joint).map(Vec::as_slice).unwrap_or(&[]);
|
||||
through(self.joints.get(&joint)?, self.frames.get(&joint)?, q, from_a, t)
|
||||
self.frames.get(&joint)?.through(self.joints.get(&joint)?, q, from_a, t)
|
||||
}
|
||||
|
||||
/// Where every placed occurrence is for these values.
|
||||
@@ -1008,6 +1053,75 @@ impl Tree<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage shared by one residual callback's evaluations, scoped to its immutable tree.
|
||||
/// Keep map nodes and path capacity, but invalidate every placement before reading new values.
|
||||
struct PlacementWorkspace<'tree, 'doc> {
|
||||
tree: &'tree Tree<'doc>,
|
||||
placed: BTreeMap<u64, Option<Mat>>,
|
||||
path: Vec<(u64, u64, bool)>,
|
||||
}
|
||||
|
||||
impl<'tree, 'doc> PlacementWorkspace<'tree, 'doc> {
|
||||
fn new(tree: &'tree Tree<'doc>) -> Self {
|
||||
Self { tree, placed: BTreeMap::new(), path: Vec::new() }
|
||||
}
|
||||
|
||||
fn at<'values>(&'values mut self, values: &'values Values) -> Placements<'values, 'tree, 'doc> {
|
||||
for placed in self.placed.values_mut() {
|
||||
*placed = None;
|
||||
}
|
||||
Placements { workspace: self, values }
|
||||
}
|
||||
}
|
||||
|
||||
/// Only this guard can read placements. Its values stay immutable until it is dropped;
|
||||
/// obtaining the next guard clears validity while retaining the allocated storage.
|
||||
struct Placements<'values, 'tree, 'doc> {
|
||||
workspace: &'values mut PlacementWorkspace<'tree, 'doc>,
|
||||
values: &'values Values,
|
||||
}
|
||||
|
||||
impl Placements<'_, '_, '_> {
|
||||
fn world(&mut self, mut occurrence: u64) -> Option<Mat> {
|
||||
self.workspace.path.clear();
|
||||
let mut t = loop {
|
||||
if let Some(t) = self.workspace.placed.get(&occurrence).and_then(Option::as_ref).or_else(|| self.workspace.tree.fixed.get(&occurrence)) {
|
||||
break *t;
|
||||
}
|
||||
if self.workspace.path.len() >= self.workspace.tree.order.len() {
|
||||
return None;
|
||||
}
|
||||
let &(from, joint, from_a) = self.workspace.tree.parent.get(&occurrence)?;
|
||||
self.workspace.path.push((occurrence, joint, from_a));
|
||||
occurrence = from;
|
||||
};
|
||||
while let Some((occurrence, joint, from_a)) = self.workspace.path.pop() {
|
||||
t = self.workspace.tree.step(joint, from_a, &t, self.values)?;
|
||||
#[cfg(test)]
|
||||
if !self.workspace.placed.contains_key(&occurrence) {
|
||||
tests::PLACEMENT_INSERTS.with(|inserts| inserts.set(inserts.get() + 1));
|
||||
}
|
||||
self.workspace.placed.insert(occurrence, Some(t));
|
||||
}
|
||||
Some(t)
|
||||
}
|
||||
|
||||
fn closure_gaps(&mut self, closing: &[u64], scale: f64, out: &mut Vec<f64>) -> Option<()> {
|
||||
for id in closing {
|
||||
let (joint, frames) = (self.workspace.tree.joints.get(id)?, self.workspace.tree.frames.get(id)?);
|
||||
let tb = self.world(joint.b.occurrence)?;
|
||||
let ta = self.world(joint.a.occurrence)?;
|
||||
let want = self.workspace.tree.step(*id, true, &ta, self.values)?;
|
||||
for p in [Vec3::ZERO, Vec3::X * scale, Vec3::Y * scale] {
|
||||
let at = apply_point(&frames.b, p);
|
||||
let d = apply_point(&want, at) - apply_point(&tb, at);
|
||||
out.extend([d.x, d.y, d.z]);
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Values the loop solver moves at most in one group of loops, and loop-closing joints it takes
|
||||
/// on at most: larger ones are only checked.
|
||||
const MAX_LOOP_VALUES: usize = 64;
|
||||
@@ -1212,8 +1326,8 @@ fn loop_scale(tree: &Tree, around: &BTreeSet<u64>, vals: &Values) -> f64 {
|
||||
let joints: Aabb3 = around
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let (j, (fa, _)) = (tree.joints.get(id)?, tree.frames.get(id)?);
|
||||
Some(apply_point(&mat_mul(&tree.world(j.a.occurrence, vals)?, fa), Vec3::ZERO))
|
||||
let (j, frames) = (tree.joints.get(id)?, tree.frames.get(id)?);
|
||||
Some(apply_point(&mat_mul(&tree.world(j.a.occurrence, vals)?, &frames.a), Vec3::ZERO))
|
||||
})
|
||||
.collect();
|
||||
if joints.is_empty() { 1.0 } else { joints.diagonal().clamp(1.0, 1e6) }
|
||||
@@ -1221,18 +1335,8 @@ fn loop_scale(tree: &Tree, around: &BTreeSet<u64>, vals: &Values) -> f64 {
|
||||
|
||||
/// How far each closing joint is from meeting: three points of B's joint frame, `scale` apart,
|
||||
/// where the joint wants them against where the tree puts them (appended to `out`).
|
||||
fn closure_gaps(tree: &Tree, closing: &[u64], vals: &Values, scale: f64, out: &mut Vec<f64>) -> Option<()> {
|
||||
for id in closing {
|
||||
let (j, (_, fb)) = (tree.joints.get(id)?, tree.frames.get(id)?);
|
||||
let tb = tree.world(j.b.occurrence, vals)?;
|
||||
let want = tree.step(*id, true, &tree.world(j.a.occurrence, vals)?, vals)?;
|
||||
for p in [Vec3::ZERO, Vec3::X * scale, Vec3::Y * scale] {
|
||||
let at = apply_point(fb, p);
|
||||
let d = apply_point(&want, at) - apply_point(&tb, at);
|
||||
out.extend([d.x, d.y, d.z]);
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
fn closure_gaps(workspace: &mut PlacementWorkspace, closing: &[u64], vals: &Values, scale: f64, out: &mut Vec<f64>) -> Option<()> {
|
||||
workspace.at(vals).closure_gaps(closing, scale, out)
|
||||
}
|
||||
|
||||
/// How far a closing joint may be off before it is a conflict: its frames apart by more than
|
||||
@@ -1294,6 +1398,7 @@ fn nearby_pose(tree: &Tree, groups: &[Group], asm: &Assembly, linked: &BTreeSet<
|
||||
(v + d * if k % 2 == 0 { w } else { -w }).max(*lo).min(*hi)
|
||||
})
|
||||
.collect();
|
||||
let mut workspace = PlacementWorkspace::new(tree);
|
||||
let mut work = near.clone();
|
||||
let mut gaps = |x: &[f64]| -> Option<Vec<f64>> {
|
||||
spent += 1;
|
||||
@@ -1307,7 +1412,7 @@ fn nearby_pose(tree: &Tree, groups: &[Group], asm: &Assembly, linked: &BTreeSet<
|
||||
}
|
||||
apply_links(asm, &mut work);
|
||||
let mut out = Vec::with_capacity(closing.len() * 9);
|
||||
closure_gaps(tree, closing, &work, scale, &mut out)?;
|
||||
closure_gaps(&mut workspace, closing, &work, scale, &mut out)?;
|
||||
out.iter().all(|v| v.is_finite()).then_some(out)
|
||||
};
|
||||
if close_within(&mut gaps, &mut x, &bounds, (1e-10 * scale).min(1e-7), loose_gap(scale)).is_some()
|
||||
@@ -1422,6 +1527,7 @@ fn loop_dof(tree: &Tree, closing: &[u64], asm: &Assembly, vals: &Values, dof: &m
|
||||
let (n, gaps) = (values.len(), closers.len() * 9);
|
||||
// How many ways the occurrence moves at a pose, with the loops kept closed.
|
||||
let mut free_at = |at: &Values| -> Option<usize> {
|
||||
let mut workspace = PlacementWorkspace::new(tree);
|
||||
let mut work = at.clone();
|
||||
// The loops' gaps, then three points of the occurrence `scale` apart.
|
||||
let mut f = |x: &[f64]| -> Option<Vec<f64>> {
|
||||
@@ -1433,8 +1539,9 @@ fn loop_dof(tree: &Tree, closing: &[u64], asm: &Assembly, vals: &Values, dof: &m
|
||||
}
|
||||
apply_links(asm, &mut work);
|
||||
let mut out = Vec::with_capacity(closers.len() * 9 + 9);
|
||||
closure_gaps(tree, &closers, &work, scale, &mut out)?;
|
||||
let t = tree.world(*occ, &work)?;
|
||||
let mut placed = workspace.at(&work);
|
||||
placed.closure_gaps(&closers, scale, &mut out)?;
|
||||
let t = placed.world(*occ)?;
|
||||
for p in [Vec3::ZERO, Vec3::X * scale, Vec3::Y * scale] {
|
||||
let q = apply_point(&t, p);
|
||||
out.extend([q.x, q.y, q.z]);
|
||||
@@ -1530,6 +1637,7 @@ fn close_group(
|
||||
if !moves.is_empty() {
|
||||
first = first.min(1.0 / stride(&effective(&x0, 0.0), &effective(&x0, 1.0)));
|
||||
}
|
||||
let mut workspace = PlacementWorkspace::new(tree);
|
||||
let mut work = vals.clone();
|
||||
// How far each closing joint is from meeting with the held values `t` of the way along:
|
||||
// three points of B's joint frame, where the joint wants them against where the tree puts them.
|
||||
@@ -1540,7 +1648,7 @@ fn close_group(
|
||||
}
|
||||
place(&mut work, x, t);
|
||||
let mut out = Vec::with_capacity(closing.len() * 9);
|
||||
closure_gaps(tree, closing, &work, scale, &mut out)?;
|
||||
closure_gaps(&mut workspace, closing, &work, scale, &mut out)?;
|
||||
out.iter().all(|v| v.is_finite()).then_some(out)
|
||||
};
|
||||
// Already closed, with no held value moving. (A moving one is followed all the way: a
|
||||
@@ -1690,6 +1798,13 @@ pub fn solve(doc: &Document, st: &ModelState) -> Solution {
|
||||
/// of the other joints around the loop. The `held` values (the ones being driven) keep theirs,
|
||||
/// moving there from the values given (where the loops last closed) so the others follow them.
|
||||
pub fn solve_holding(doc: &Document, st: &ModelState, held: &Held) -> Solution {
|
||||
solve_holding_with_analysis(doc, st, held, Analysis::DegreesOfFreedom)
|
||||
}
|
||||
|
||||
/// Place occurrences and check constraints with the requested diagnostic work. Placement-only
|
||||
/// solves still close and check every loop, respect held values and limits, and report errors;
|
||||
/// they leave `Solution::dof` empty rather than returning the spanning tree's approximate count.
|
||||
pub fn solve_holding_with_analysis(doc: &Document, st: &ModelState, held: &Held, analysis: Analysis) -> Solution {
|
||||
let asm = &doc.assembly;
|
||||
let mut sol = Solution::default();
|
||||
let mut vals = joint_values(asm);
|
||||
@@ -1704,9 +1819,9 @@ pub fn solve_holding(doc: &Document, st: &ModelState, held: &Held) -> Solution {
|
||||
for id in &closing {
|
||||
let Some(j) = tree.joints.get(id) else { continue };
|
||||
let (Some(ta), Some(tb)) = (placed.get(&j.a.occurrence), placed.get(&j.b.occurrence)) else { continue };
|
||||
let (Some(want), Some((_, fb))) = (tree.step(*id, true, ta, &vals), tree.frames.get(id)) else { continue };
|
||||
let (Some(want), Some(frames)) = (tree.step(*id, true, ta, &vals), tree.frames.get(id)) else { continue };
|
||||
// Measured at the joint, where the loop solver closes it.
|
||||
let (dt, da) = mismatch(&mat_mul(&want, fb), &mat_mul(tb, fb));
|
||||
let (dt, da) = mismatch(&mat_mul(&want, &frames.b), &mat_mul(tb, &frames.b));
|
||||
if dt > CONFLICT_MM || da > CONFLICT_RAD {
|
||||
sol.conflicts.push((
|
||||
j.id,
|
||||
@@ -1719,11 +1834,15 @@ pub fn solve_holding(doc: &Document, st: &ModelState, held: &Held) -> Solution {
|
||||
sol.conflicts.push((j.id, j.name.clone(), "the loop can't stay closed on the way to these values".into()));
|
||||
}
|
||||
}
|
||||
if !closing.is_empty() {
|
||||
loop_dof(&tree, &closing, asm, &vals, &mut dof);
|
||||
}
|
||||
for o in &doc.occurrences {
|
||||
dof.entry(o.id).or_insert(6);
|
||||
if analysis == Analysis::DegreesOfFreedom {
|
||||
if !closing.is_empty() {
|
||||
loop_dof(&tree, &closing, asm, &vals, &mut dof);
|
||||
}
|
||||
for o in &doc.occurrences {
|
||||
dof.entry(o.id).or_insert(6);
|
||||
}
|
||||
} else {
|
||||
dof.clear();
|
||||
}
|
||||
placed.remove(&0);
|
||||
sol.world = placed;
|
||||
@@ -1749,7 +1868,7 @@ fn build<'a>(doc: &'a Document, st: &ModelState, vals: &Values, sol: &mut Soluti
|
||||
let joints: Vec<&Joint> = asm.joints.iter().filter(|j| !j.suppressed).take(MAX_JOINTS).collect();
|
||||
// Joint frames in component coordinates (errors leave the joint out). An origin several joints
|
||||
// share (a panel face every part sits on) is found once.
|
||||
let mut frames: BTreeMap<u64, (Mat, Mat)> = BTreeMap::new();
|
||||
let mut frames: BTreeMap<u64, JointFrames> = BTreeMap::new();
|
||||
let mut found: BTreeMap<(u64, String), std::result::Result<Mat, String>> = BTreeMap::new();
|
||||
let mut resolve = |o: &JointOrigin| -> Result<Mat> {
|
||||
let Ok(comp) = component_of(doc, o.occurrence) else { return resolve_origin(doc, st, o) };
|
||||
@@ -1770,7 +1889,7 @@ fn build<'a>(doc: &'a Document, st: &ModelState, vals: &Values, sol: &mut Soluti
|
||||
}
|
||||
match (resolve(&j.a), resolve(&j.b)) {
|
||||
(Ok(fa), Ok(fb)) => {
|
||||
frames.insert(j.id, (fa, fb));
|
||||
frames.insert(j.id, JointFrames::new(fa, fb));
|
||||
}
|
||||
(Err(e), _) | (_, Err(e)) => sol.errors.push((j.id, j.name.clone(), e.to_string())),
|
||||
}
|
||||
@@ -1953,6 +2072,138 @@ pub fn frame_of(o: Vec3, z: Vec3, x: Option<Vec3>) -> Option<Mat> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
thread_local! {
|
||||
pub(super) static PLACEMENT_STEPS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
pub(super) static PLACEMENT_INSERTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
pub(super) fn placement_tree(asm: &Assembly) -> Tree<'_> {
|
||||
Tree {
|
||||
joints: asm.joints.iter().map(|j| (j.id, j)).collect(),
|
||||
frames: asm.joints.iter().map(|j| (j.id, JointFrames::new(mat_mul(&shift(3.0, 4.0, 5.0), &rot_y(0.3)), rot_x(-0.2)))).collect(),
|
||||
fixed: BTreeMap::from([(0, IDENTITY)]),
|
||||
parent: BTreeMap::from([(1, (0, 1, true)), (2, (1, 2, false)), (3, (2, 3, true)), (4, (1, 4, true)), (99, (0, 5, true))]),
|
||||
order: vec![1, 2, 3, 4, 99],
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared ancestors are evaluated once, without visiting an unrelated assembly branch.
|
||||
/// The same arithmetic must be used after values change and on a reversed tree edge.
|
||||
#[test]
|
||||
fn residual_placements_reuse_ancestors_only_for_the_current_values() {
|
||||
let asm = chain(6, 0.2);
|
||||
let tree = placement_tree(&asm);
|
||||
let mut values = joint_values(&asm);
|
||||
let expected: BTreeMap<u64, Mat> = [1, 2, 3, 4].into_iter().map(|id| (id, tree.world(id, &values).unwrap())).collect();
|
||||
let mut workspace = PlacementWorkspace::new(&tree);
|
||||
PLACEMENT_STEPS.with(|steps| steps.set(0));
|
||||
{
|
||||
let mut placed = workspace.at(&values);
|
||||
for id in [3, 4, 3, 2, 4] {
|
||||
assert_eq!(placed.world(id), expected.get(&id).copied());
|
||||
}
|
||||
assert_eq!(placed.world(0), Some(IDENTITY));
|
||||
assert_eq!(placed.world(1234), None);
|
||||
}
|
||||
assert_eq!(PLACEMENT_STEPS.with(|steps| steps.get()), 4, "one step per needed occurrence; no unrelated branch");
|
||||
values.get_mut(&1).unwrap()[0] += 0.1;
|
||||
apply_links(&asm, &mut values);
|
||||
let changed = tree.world(3, &values).unwrap();
|
||||
assert_ne!(Some(&changed), expected.get(&3));
|
||||
let mut placed = workspace.at(&values);
|
||||
assert_eq!(placed.world(3), Some(changed), "a new residual evaluation cannot reuse the previous values' placements");
|
||||
}
|
||||
|
||||
/// Cached static inverses retain the original multiplication order for every joint kind,
|
||||
/// flip and traversal direction. A non-invertible frame remains a failed step.
|
||||
#[test]
|
||||
fn prepared_joint_frames_match_fresh_inverses() {
|
||||
fn reference(j: &Joint, fa: &Mat, fb: &Mat, q: &[f64], from_a: bool, t: &Mat) -> Option<Mat> {
|
||||
let link = mat_mul(&link_of(j, q), &mat_inverse(fb)?);
|
||||
if from_a { Some(mat_mul(&mat_mul(t, fa), &link)) } else { Some(mat_mul(&mat_mul(t, &mat_inverse(&link)?), &mat_inverse(fa)?)) }
|
||||
}
|
||||
let mut joint = chain(1, 0.0).joints.remove(0);
|
||||
let fa = mat_mul(&shift(3.0, -7.0, 11.0), &rot_y(0.3));
|
||||
let fb = mat_mul(&shift(-2.0, 5.0, 13.0), &rot_x(-0.2));
|
||||
let t = mat_mul(&shift(17.0, 19.0, -23.0), &rot_z(0.7));
|
||||
let singular = [[0.0; 4]; 4];
|
||||
for (a, b) in [(fa, fb), (singular, fb), (fa, singular)] {
|
||||
let frames = JointFrames::new(a, b);
|
||||
for kind in [
|
||||
JointKind::Rigid,
|
||||
JointKind::Revolute,
|
||||
JointKind::Slider,
|
||||
JointKind::Cylindrical,
|
||||
JointKind::PinSlot,
|
||||
JointKind::Planar,
|
||||
JointKind::Ball,
|
||||
JointKind::Parallel,
|
||||
JointKind::Perpendicular,
|
||||
JointKind::Angle,
|
||||
JointKind::PointOnPlane,
|
||||
JointKind::PointOnLine,
|
||||
JointKind::LineOnPlane,
|
||||
] {
|
||||
joint.kind = kind;
|
||||
joint.offset = 2.7;
|
||||
joint.angle = -0.41;
|
||||
for flip in [false, true] {
|
||||
joint.flip = flip;
|
||||
for angle in [0.0, 1e-6, -0.37, std::f64::consts::PI] {
|
||||
let q: Vec<f64> = (0..kind.dofs().len()).map(|i| angle * (i + 1) as f64).collect();
|
||||
for forward in [false, true] {
|
||||
assert_eq!(
|
||||
frames.through(&joint, &q, forward, &t),
|
||||
reference(&joint, &a, &b, &q, forward, &t),
|
||||
"{kind:?}, {flip}, {angle}, {forward}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The loop solver's residual entry point reuses shared ancestors for each perturbation;
|
||||
/// row order, duplicate rows, append semantics and missing-origin failure stay unchanged.
|
||||
#[test]
|
||||
fn closure_residuals_reuse_shared_ancestors_for_each_perturbation() {
|
||||
let mut asm = chain(6, 0.2);
|
||||
asm.joints[5].a.occurrence = 3;
|
||||
asm.joints[5].b.occurrence = 4;
|
||||
let tree = placement_tree(&asm);
|
||||
let mut workspace = PlacementWorkspace::new(&tree);
|
||||
let mut cold = true;
|
||||
for start in [0.2, 0.2 + 1e-6, 0.2 - 1e-6, 1.3] {
|
||||
let mut values = joint_values(&asm);
|
||||
values.get_mut(&1).unwrap()[0] = start;
|
||||
apply_links(&asm, &mut values);
|
||||
let mut expected = vec![42.0];
|
||||
for id in [6, 6] {
|
||||
let joint = tree.joints.get(&id).unwrap();
|
||||
let frames = tree.frames.get(&id).unwrap();
|
||||
let a = tree.world(joint.a.occurrence, &values).unwrap();
|
||||
let b = tree.world(joint.b.occurrence, &values).unwrap();
|
||||
let want = through(joint, &(frames.a, frames.b), values.get(&id).unwrap(), true, &a).unwrap();
|
||||
for point in [Vec3::ZERO, Vec3::X * 37.0, Vec3::Y * 37.0] {
|
||||
let at = apply_point(&frames.b, point);
|
||||
let d = apply_point(&want, at) - apply_point(&b, at);
|
||||
expected.extend([d.x, d.y, d.z]);
|
||||
}
|
||||
}
|
||||
PLACEMENT_STEPS.with(|steps| steps.set(0));
|
||||
PLACEMENT_INSERTS.with(|inserts| inserts.set(0));
|
||||
let mut actual = vec![42.0];
|
||||
assert_eq!(closure_gaps(&mut workspace, &[6, 6], &values, 37.0, &mut actual), Some(()));
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(PLACEMENT_INSERTS.with(|inserts| inserts.get()), if cold { 4 } else { 0 }, "reuse cache storage between perturbations");
|
||||
cold = false;
|
||||
assert_eq!(PLACEMENT_STEPS.with(|steps| steps.get()), 6, "four tree placements and two closing-joint steps");
|
||||
assert_eq!(closure_gaps(&mut workspace, &[u64::MAX], &values, 37.0, &mut actual), None);
|
||||
assert_eq!(actual, expected, "an unresolved joint does not insert a zero residual");
|
||||
}
|
||||
}
|
||||
|
||||
fn close(a: &Mat, b: &Mat) -> bool {
|
||||
let (t, r) = mismatch(a, b);
|
||||
t < 1e-9 && r < 1e-9
|
||||
@@ -1960,7 +2211,7 @@ mod tests {
|
||||
|
||||
/// Revolute joints 1…n (joint 1 at `start`, the others at 0) linked 1 → 2 → … → n with
|
||||
/// ratio 2, the links stored from the end of the chain; joint 3 limited to ±50.
|
||||
fn chain(n: u64, start: f64) -> Assembly {
|
||||
pub(super) fn chain(n: u64, start: f64) -> Assembly {
|
||||
let origin = JointOrigin { occurrence: 0, snap: Snap::Point { point: Vec3::ZERO, z: None, x: None } };
|
||||
let joints = (1..=n)
|
||||
.map(|id| Joint {
|
||||
|
||||
@@ -64,8 +64,7 @@ impl NamedFace<'_> {
|
||||
|
||||
/// The face's centre and outward normal (the component's coordinates), when it is planar.
|
||||
pub fn plane(&self) -> Result<Option<(Vec3, Vec3)>> {
|
||||
let tol = (self.body.body.size() * 2e-3).max(1e-3);
|
||||
let faces = self.body.body.faces(tol)?;
|
||||
let faces = self.body.joint_faces()?;
|
||||
Ok(faces.get(self.face).and_then(|f| f.plane_normal.map(|n| (apply_point(&self.offset, f.centroid), apply_vector(&self.offset, n)))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ use solidcraft_geom::Vec3;
|
||||
use solidcraft_sketch::linalg;
|
||||
|
||||
use super::{
|
||||
Assembly, Held, MAX_LOOP_VALUES, MAX_LOOP_WORK, MAX_LOOPS, Solution, Tree, Values, apply_links, build, close_within, closure_gaps, jacobian,
|
||||
joint_values, largest, least_squares, limit_bounds, link_chains, loop_groups, loop_scale, loose_gap, unknowns,
|
||||
Assembly, Held, MAX_LOOP_VALUES, MAX_LOOP_WORK, MAX_LOOPS, Solution, Tree, Values, apply_links, build, close_within, jacobian, joint_values,
|
||||
largest, least_squares, limit_bounds, link_chains, loop_groups, loop_scale, loose_gap, unknowns,
|
||||
};
|
||||
use crate::{Document, ModelState, apply_point, apply_vector, mat_mul};
|
||||
|
||||
@@ -133,8 +133,8 @@ pub fn reach(doc: &Document, st: &ModelState, r: &Reach) -> Result<Reached, Stri
|
||||
let lever = path
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let (j, (fa, _)) = (tree.joints.get(id)?, tree.frames.get(id)?);
|
||||
Some(apply_point(&mat_mul(&tree.world(j.a.occurrence, &vals)?, fa), Vec3::ZERO).dist(point?))
|
||||
let (j, frames) = (tree.joints.get(id)?, tree.frames.get(id)?);
|
||||
Some(apply_point(&mat_mul(&tree.world(j.a.occurrence, &vals)?, &frames.a), Vec3::ZERO).dist(point?))
|
||||
})
|
||||
.fold(0.0, f64::max);
|
||||
let span = point.map_or(scale, |p| scale.max(p.dist(r.to)).max(lever));
|
||||
@@ -155,7 +155,8 @@ pub fn reach(doc: &Document, st: &ModelState, r: &Reach) -> Result<Reached, Stri
|
||||
let mt = if r.axis.is_some() { 6 } else { 3 };
|
||||
let spent = Cell::new(0usize);
|
||||
let mut work = vals.clone();
|
||||
let mut f = |x: &[f64]| misses(tree, asm, r, scale, &vars, &closers, x, &mut work, &spent);
|
||||
let mut workspace = super::PlacementWorkspace::new(tree);
|
||||
let mut f = |x: &[f64]| misses(&mut workspace, asm, r, scale, &vars, &closers, x, &mut work, &spent);
|
||||
let mut x = x0.clone();
|
||||
let miss = |res: &[f64]| res.iter().take(mt).map(|v| v * v).sum::<f64>().sqrt();
|
||||
let before = f(&x0).map_or(f64::INFINITY, |r| miss(&r));
|
||||
@@ -216,7 +217,8 @@ pub fn reach(doc: &Document, st: &ModelState, r: &Reach) -> Result<Reached, Stri
|
||||
if let Some(first) = closers.first() {
|
||||
let k = drive.len();
|
||||
let mut alone = vals.clone();
|
||||
let mut g = |x: &[f64]| misses(tree, asm, r, scale, &drive, &[], x, &mut alone, &spent);
|
||||
let mut alone_workspace = super::PlacementWorkspace::new(tree);
|
||||
let mut g = |x: &[f64]| misses(&mut alone_workspace, asm, r, scale, &drive, &[], x, &mut alone, &spent);
|
||||
let (mut xo, bo, co) = (x0.get(..k).unwrap_or_default().to_vec(), bounds.get(..k).unwrap_or_default(), cap.get(..k).unwrap_or_default());
|
||||
// The loops left open, nothing to pace.
|
||||
if let Some(ro) = approach(&mut g, &|_, _| 0.0, mt, &mut xo, bo, co, tol, loose_gap(scale))
|
||||
@@ -249,7 +251,7 @@ pub fn reach(doc: &Document, st: &ModelState, r: &Reach) -> Result<Reached, Stri
|
||||
/// The target's misses with the values `vars` at `x`: the point's (mm), then the axis's (as mm
|
||||
/// across the chain's `scale`), then the gaps of the loops `closers`, weighted.
|
||||
fn misses(
|
||||
tree: &Tree,
|
||||
workspace: &mut super::PlacementWorkspace,
|
||||
asm: &Assembly,
|
||||
r: &Reach,
|
||||
scale: f64,
|
||||
@@ -269,9 +271,18 @@ fn misses(
|
||||
}
|
||||
}
|
||||
apply_links(asm, work);
|
||||
let mut out = target(tree, r, scale, work)?;
|
||||
let n = out.len();
|
||||
closure_gaps(tree, closers, work, scale, &mut out)?;
|
||||
let tree = workspace.tree;
|
||||
// An open chain has only one target placement to read. Loops share their ancestors with
|
||||
// that target, but a single placement needs no residual-local map.
|
||||
let mut out = if closers.is_empty() {
|
||||
target(tree, r, scale, work)?
|
||||
} else {
|
||||
let mut placed = workspace.at(work);
|
||||
let mut out = target_at(&placed.world(r.occurrence)?, r, scale);
|
||||
placed.closure_gaps(closers, scale, &mut out)?;
|
||||
out
|
||||
};
|
||||
let n = if r.axis.is_some() { 6 } else { 3 };
|
||||
for v in out.iter_mut().skip(n) {
|
||||
*v *= LOOP_WEIGHT;
|
||||
}
|
||||
@@ -280,14 +291,17 @@ fn misses(
|
||||
|
||||
/// The target's misses for these values: the point's (mm), then the axis's (as mm across `scale`).
|
||||
fn target(tree: &Tree, r: &Reach, scale: f64, vals: &Values) -> Option<Vec<f64>> {
|
||||
let t = tree.world(r.occurrence, vals)?;
|
||||
let d = apply_point(&t, r.point) - r.to;
|
||||
Some(target_at(&tree.world(r.occurrence, vals)?, r, scale))
|
||||
}
|
||||
|
||||
fn target_at(t: &crate::Mat, r: &Reach, scale: f64) -> Vec<f64> {
|
||||
let d = apply_point(t, r.point) - r.to;
|
||||
let mut out = vec![d.x, d.y, d.z];
|
||||
if let Some((a, z)) = r.axis {
|
||||
let w = (apply_vector(&t, a) - z) * scale;
|
||||
let w = (apply_vector(t, a) - z) * scale;
|
||||
out.extend([w.x, w.y, w.z]);
|
||||
}
|
||||
Some(out)
|
||||
out
|
||||
}
|
||||
|
||||
/// How many steps apart the values `vars` at `a` and at `b` put the `paced` values (each with its
|
||||
@@ -447,3 +461,55 @@ fn approach(
|
||||
}
|
||||
Some(r)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::tests::{PLACEMENT_INSERTS, PLACEMENT_STEPS, chain, placement_tree};
|
||||
use super::*;
|
||||
|
||||
/// Exercise the IK callback itself: removing sharing at this call site must fail even if
|
||||
/// the reusable placement helper still passes its own tests.
|
||||
#[test]
|
||||
fn ik_residual_reuses_target_and_loop_ancestors_at_the_call_site() {
|
||||
let mut asm = chain(6, 0.2);
|
||||
asm.joints[5].a.occurrence = 3;
|
||||
asm.joints[5].b.occurrence = 4;
|
||||
let tree = placement_tree(&asm);
|
||||
let base = joint_values(&asm);
|
||||
let mut workspace = super::super::PlacementWorkspace::new(&tree);
|
||||
let mut cold = true;
|
||||
for axis in [None, Some((Vec3::Y, Vec3::Z))] {
|
||||
let r = Reach { occurrence: 3, point: Vec3::new(1.0, 2.0, 3.0), to: Vec3::new(7.0, 11.0, 13.0), axis, joints: None };
|
||||
for x in [0.2, 0.2 + 1e-6, 0.2 - 1e-6, 1.3] {
|
||||
let mut reference = base.clone();
|
||||
reference.get_mut(&1).unwrap()[0] = x;
|
||||
apply_links(&asm, &mut reference);
|
||||
let mut expected = target(&tree, &r, 37.0, &reference).unwrap();
|
||||
let n = expected.len();
|
||||
super::super::closure_gaps(&mut super::super::PlacementWorkspace::new(&tree), &[6], &reference, 37.0, &mut expected).unwrap();
|
||||
for v in expected.iter_mut().skip(n) {
|
||||
*v *= LOOP_WEIGHT;
|
||||
}
|
||||
let mut work = base.clone();
|
||||
let spent = Cell::new(0);
|
||||
PLACEMENT_STEPS.with(|steps| steps.set(0));
|
||||
PLACEMENT_INSERTS.with(|inserts| inserts.set(0));
|
||||
let actual = misses(&mut workspace, &asm, &r, 37.0, &[(1, 0)], &[6], &[x], &mut work, &spent);
|
||||
assert_eq!(actual, Some(expected));
|
||||
assert_eq!(
|
||||
PLACEMENT_INSERTS.with(|inserts| inserts.get()),
|
||||
if cold { 4 } else { 0 },
|
||||
"IK must retain cache storage across evaluations"
|
||||
);
|
||||
cold = false;
|
||||
assert_eq!(spent.get(), 1, "reuse must not skip a finite-difference evaluation or bypass its work cap");
|
||||
assert_eq!(PLACEMENT_STEPS.with(|steps| steps.get()), 5, "four tree placements shared by the target and loop, plus the closing step");
|
||||
}
|
||||
let mut work = base.clone();
|
||||
let spent = Cell::new(MAX_LOOP_WORK);
|
||||
PLACEMENT_STEPS.with(|steps| steps.set(0));
|
||||
assert!(misses(&mut workspace, &asm, &r, 37.0, &[(1, 0)], &[6], &[0.2], &mut work, &spent).is_none());
|
||||
assert_eq!(PLACEMENT_STEPS.with(|steps| steps.get()), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! hold (one that later features pass through unchanged) is recorded once, as are its face names
|
||||
//! and the solved sketches the states share. Face names are kept as worked out, not as the
|
||||
//! history they were worked out from: a model taken from a file names every face as the model
|
||||
//! that was saved did.
|
||||
//! that was saved did. A file without room for every state keeps fewer
|
||||
//! ([`SavedModel::thinned`]); opening evaluates the features whose state it left out again.
|
||||
|
||||
use super::*;
|
||||
use crate::history_names::{FaceInfo, Names};
|
||||
@@ -181,6 +182,7 @@ impl Writer {
|
||||
let sketches = st.sketches.iter().map(|s| self.sketch(s)).collect::<Option<Vec<_>>>()?;
|
||||
let r = json!({
|
||||
"bodies": runs(&bodies), "sketches": runs(&sketches), "body_counter": st.body_counter,
|
||||
"reserved_body_names": st.reserved_body_names.iter().collect::<Vec<_>>(),
|
||||
"sheets": serde_json::to_value(&st.sheets).ok()?, "construct": serde_json::to_value(&st.construct).ok()?,
|
||||
"threads": serde_json::to_value(&st.threads).ok()?,
|
||||
"sketch_keys": st.sketch_keys.iter().map(|(f, k)| json!([f, hex(*k)])).collect::<Vec<_>>(),
|
||||
@@ -320,6 +322,11 @@ impl SavedModel {
|
||||
bodies: ids("bodies", model_bodies.len())?.into_iter().filter_map(|i| model_bodies.get(i).cloned()).collect(),
|
||||
sketches: ids("sketches", sketches.len())?.into_iter().filter_map(|i| sketches.get(i).cloned()).collect(),
|
||||
body_counter: index(s.get("body_counter"), usize::MAX)?,
|
||||
reserved_body_names: s
|
||||
.get("reserved_body_names")
|
||||
.and_then(Json::as_array)
|
||||
.map(|a| a.iter().filter_map(|n| n.as_str().map(str::to_string)).collect())
|
||||
.unwrap_or_default(),
|
||||
sheets: serde_json::from_value(field("sheets")?).map_err(invalid)?,
|
||||
construct: serde_json::from_value(field("construct")?).map_err(invalid)?,
|
||||
threads: serde_json::from_value(field("threads")?).map_err(invalid)?,
|
||||
@@ -353,12 +360,163 @@ impl SavedModel {
|
||||
None => Ok(Arc::new(ModelState::default())),
|
||||
}
|
||||
}
|
||||
|
||||
/// This model with fewer states, for a design file without room for all of them (#3220): the
|
||||
/// final state (what the design shows, kept whatever it costs), then the others, the slowest
|
||||
/// feature's first (the ones opening would take longest to evaluate again; of equal ones the
|
||||
/// later first), each while what it adds still fits in `budget` bytes: the bodies no state kept
|
||||
/// so far holds, and the records of the state, those bodies, their names and sketches.
|
||||
/// `pieces[i]` is what keeping body `i` stores, as ids and bytes: a piece that several bodies
|
||||
/// or states need counts once. A feature whose state is left out keeps none, and opening
|
||||
/// evaluates it again ([`Model::adopt`]). With the model, the index here of each body it keeps,
|
||||
/// and the bytes it counted: over `budget` when the final state alone is. `None` only for
|
||||
/// records it cannot read.
|
||||
pub fn thinned(&self, pieces: &[Vec<(usize, usize)>], budget: usize) -> Option<(SavedModel, Vec<usize>, usize)> {
|
||||
let list = |key: &str| self.records.get(key).and_then(Json::as_array);
|
||||
let (features, states, model_bodies) = (list("features")?, list("states")?, list("model_bodies")?);
|
||||
let (names, sketches) = (list("names")?, list("sketches")?);
|
||||
let at = |v: Option<&Json>, len: usize| v.and_then(Json::as_u64).and_then(|i| usize::try_from(i).ok()).filter(|i| *i < len);
|
||||
// The indices a state's runs list.
|
||||
let held = |s: &Json, key: &str, len: usize| -> Option<Vec<usize>> {
|
||||
let mut out = Vec::new();
|
||||
for run in s.get(key)?.as_array()? {
|
||||
let first = at(run.get(0), len)?;
|
||||
out.extend(first..first.checked_add(at(run.get(1), len.saturating_add(1))?).filter(|end| *end <= len)?);
|
||||
}
|
||||
Some(out)
|
||||
};
|
||||
let state_bodies = states.iter().map(|s| held(s, "bodies", model_bodies.len())).collect::<Option<Vec<_>>>()?;
|
||||
let state_sketches = states.iter().map(|s| held(s, "sketches", sketches.len())).collect::<Option<Vec<_>>>()?;
|
||||
let body_of = model_bodies.iter().map(|r| at(r.get("body"), self.bodies.len())).collect::<Option<Vec<_>>>()?;
|
||||
let names_of = model_bodies.iter().map(|r| at(r.get("names"), names.len())).collect::<Option<Vec<_>>>()?;
|
||||
// What each record takes in the file.
|
||||
let bytes = |list: &[Json]| list.iter().map(|r| serde_json::to_vec(r).map_or(usize::MAX, |b| b.len().saturating_add(1))).collect::<Vec<_>>();
|
||||
let (state_bytes, body_bytes) = (bytes(states.as_slice()), bytes(model_bodies.as_slice()));
|
||||
let (name_bytes, sketch_bytes) = (bytes(names.as_slice()), bytes(sketches.as_slice()));
|
||||
// What each state's feature took to evaluate, and the final state.
|
||||
let mut ms = vec![0.0f64; states.len()];
|
||||
let mut last = None;
|
||||
for f in features.iter().filter(|f| f.get("skipped").and_then(Json::as_bool) == Some(false)) {
|
||||
let s = at(f.get("state"), states.len())?;
|
||||
if let Some(t) = ms.get_mut(s) {
|
||||
*t += f.get("ms").and_then(Json::as_f64).filter(|t| t.is_finite() && *t > 0.0).unwrap_or(0.0);
|
||||
}
|
||||
last = Some(s);
|
||||
}
|
||||
let last = last?;
|
||||
let time = |s: &usize| ms.get(*s).copied().unwrap_or(0.0);
|
||||
let mut order: Vec<usize> = (0..states.len()).filter(|s| *s != last).collect();
|
||||
order.sort_by(|a, b| time(b).total_cmp(&time(a)).then(b.cmp(a)));
|
||||
let mut keep = vec![false; states.len()];
|
||||
// The records and pieces kept so far, and their bytes.
|
||||
let mut kept_bodies = vec![false; model_bodies.len()];
|
||||
let (mut names_used, mut sketches_used) = (vec![false; names.len()], vec![false; sketches.len()]);
|
||||
let mut stored = std::collections::HashSet::new();
|
||||
let mut used = 0usize;
|
||||
for s in std::iter::once(last).chain(order) {
|
||||
let (mut adds, mut new_names, mut new_sketches) = (HashMap::new(), std::collections::HashSet::new(), std::collections::HashSet::new());
|
||||
let mut records = *state_bytes.get(s)?;
|
||||
for &m in state_bodies.get(s)? {
|
||||
if kept_bodies.get(m) == Some(&false) {
|
||||
records = records.saturating_add(*body_bytes.get(m)?);
|
||||
let n = *names_of.get(m)?;
|
||||
if names_used.get(n) == Some(&false) && new_names.insert(n) {
|
||||
records = records.saturating_add(*name_bytes.get(n)?);
|
||||
}
|
||||
for &(piece, bytes) in pieces.get(*body_of.get(m)?)? {
|
||||
if !stored.contains(&piece) {
|
||||
adds.insert(piece, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for &k in state_sketches.get(s)? {
|
||||
if sketches_used.get(k) == Some(&false) && new_sketches.insert(k) {
|
||||
records = records.saturating_add(*sketch_bytes.get(k)?);
|
||||
}
|
||||
}
|
||||
let total = adds.values().fold(used.saturating_add(records), |t, b| t.saturating_add(*b));
|
||||
if total > budget && s != last {
|
||||
continue;
|
||||
}
|
||||
used = total;
|
||||
stored.extend(adds.into_keys());
|
||||
for &m in state_bodies.get(s)? {
|
||||
*kept_bodies.get_mut(m)? = true;
|
||||
}
|
||||
for n in new_names {
|
||||
*names_used.get_mut(n)? = true;
|
||||
}
|
||||
for k in new_sketches {
|
||||
*sketches_used.get_mut(k)? = true;
|
||||
}
|
||||
*keep.get_mut(s)? = true;
|
||||
}
|
||||
// What the states kept refer to, numbered again in the order it had.
|
||||
let mut kernel_used = vec![false; self.bodies.len()];
|
||||
for m in (0..model_bodies.len()).filter(|m| kept_bodies.get(*m) == Some(&true)) {
|
||||
*kernel_used.get_mut(*body_of.get(m)?)? = true;
|
||||
}
|
||||
let renumber = |used: &[bool]| -> Vec<Option<usize>> {
|
||||
let mut next = 0;
|
||||
used.iter()
|
||||
.map(|&u| {
|
||||
u.then(|| {
|
||||
next += 1;
|
||||
next - 1
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let (new_body, new_kernel, new_names) = (renumber(&kept_bodies), renumber(&kernel_used), renumber(&names_used));
|
||||
let (new_sketch, new_state) = (renumber(&sketches_used), renumber(&keep));
|
||||
let new = |map: &[Option<usize>], i: usize| map.get(i).copied().flatten();
|
||||
let kept_of = |list: &[Json], used: &[bool]| list.iter().zip(used).filter(|(_, u)| **u).map(|(v, _)| v.clone()).collect::<Vec<_>>();
|
||||
let mut out_states = Vec::new();
|
||||
for (s, st) in states.iter().enumerate().filter(|(s, _)| keep.get(*s) == Some(&true)) {
|
||||
let bodies = state_bodies.get(s)?.iter().map(|m| new(&new_body, *m)).collect::<Option<Vec<_>>>()?;
|
||||
let sketches = state_sketches.get(s)?.iter().map(|i| new(&new_sketch, *i)).collect::<Option<Vec<_>>>()?;
|
||||
let mut st = st.clone();
|
||||
let o = st.as_object_mut()?;
|
||||
o.insert("bodies".into(), json!(runs(&bodies)));
|
||||
o.insert("sketches".into(), json!(runs(&sketches)));
|
||||
out_states.push(st);
|
||||
}
|
||||
let mut out_bodies = Vec::new();
|
||||
for (m, r) in model_bodies.iter().enumerate().filter(|(m, _)| kept_bodies.get(*m) == Some(&true)) {
|
||||
let (body, named) = (new(&new_kernel, *body_of.get(m)?)?, new(&new_names, at(r.get("names"), names.len())?)?);
|
||||
let mut r = r.clone();
|
||||
let o = r.as_object_mut()?;
|
||||
o.insert("body".into(), json!(body));
|
||||
o.insert("names".into(), json!(named));
|
||||
out_bodies.push(r);
|
||||
}
|
||||
let mut out_features = Vec::with_capacity(features.len());
|
||||
for f in features {
|
||||
let state = new(&new_state, at(f.get("state"), states.len())?);
|
||||
let mut f = f.clone();
|
||||
let o = f.as_object_mut()?;
|
||||
match state {
|
||||
Some(s) => o.insert("state".into(), json!(s)),
|
||||
None => o.remove("state"),
|
||||
};
|
||||
out_features.push(f);
|
||||
}
|
||||
let records = json!({
|
||||
"features": out_features, "states": out_states, "model_bodies": out_bodies, "names": kept_of(names.as_slice(), &names_used),
|
||||
"sketches": kept_of(sketches.as_slice(), &sketches_used),
|
||||
});
|
||||
let kept: Vec<usize> = (0..self.bodies.len()).filter(|i| kernel_used.get(*i) == Some(&true)).collect();
|
||||
let bodies = kept.iter().filter_map(|i| self.bodies.get(*i).cloned()).collect();
|
||||
Some((SavedModel { records, bodies }, kept, used))
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// The model a design file kept for `doc` ([`Model::saved`]), taken as `doc`'s evaluation:
|
||||
/// every feature's result as saved, nothing evaluated. Refused (an error) unless every
|
||||
/// feature's fingerprint is the one `doc` gives it now, and every record is well formed.
|
||||
/// every feature's result as saved, nothing evaluated but the features whose state the file
|
||||
/// did not keep ([`SavedModel::thinned`]). Refused (an error) unless every feature's
|
||||
/// fingerprint is the one `doc` gives it now, and every record is well formed.
|
||||
pub fn adopt(doc: &Document, saved: &SavedModel) -> Result<Model> {
|
||||
let states = saved.states()?;
|
||||
let records = &saved.records;
|
||||
@@ -375,16 +533,24 @@ impl Model {
|
||||
let step_digests = StepDigests::of(doc, &StepDigests::default());
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
let ctx = context_digest(doc, &vals);
|
||||
let mut cache = EvalCache::default();
|
||||
let mut before = Arc::new(ModelState::default());
|
||||
// Whether `before` is the state before the feature: one the file left out only stands in.
|
||||
let mut exact = true;
|
||||
let mut prev = 0;
|
||||
let mut results = Vec::with_capacity(features.len());
|
||||
for (i, (f, r)) in doc.features.iter().zip(features).enumerate() {
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &step_digests);
|
||||
if r.get("id").and_then(Json::as_u64) != Some(f.id) || unhex(r.get("fingerprint").unwrap_or(&Json::Null))? != prev {
|
||||
let saved = unhex(r.get("fingerprint").unwrap_or(&Json::Null))?;
|
||||
// A fingerprint reads the state before its feature (where the bodies it takes lie).
|
||||
// After a state the file left out, the saved one is taken: `evaluate` works that state
|
||||
// out, checks the fingerprints after it, and evaluates again from one that differs.
|
||||
prev = if exact { chain_fingerprint(doc, &vals, f, prev, i >= marker, &step_digests, Some(&before)) } else { saved };
|
||||
if r.get("id").and_then(Json::as_u64) != Some(f.id) || saved != prev {
|
||||
return Err(bad("saved model: the document changed"));
|
||||
}
|
||||
let ms = r.get("ms").and_then(Json::as_f64).filter(|ms| ms.is_finite()).ok_or_else(|| bad("saved model: time"))?;
|
||||
// A file without room for every state keeps some features without one (#3220).
|
||||
let state =
|
||||
r.get("state").map(|s| states.get(index(Some(s), states.len())?).cloned().ok_or_else(|| bad("saved model: state"))).transpose()?;
|
||||
let result = FeatureResult {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
@@ -398,27 +564,48 @@ impl Model {
|
||||
skipped: r.get("skipped").and_then(Json::as_bool).ok_or_else(|| bad("saved model: skipped"))?,
|
||||
stale: false,
|
||||
fingerprint: prev,
|
||||
state: states.get(index(r.get("state"), states.len())?).cloned().ok_or_else(|| bad("saved model: state"))?,
|
||||
unkept: state.is_none(),
|
||||
state: state.unwrap_or_else(|| before.clone()),
|
||||
};
|
||||
// Each result where an evaluation would have put it, so an edit undone (or set back)
|
||||
// finds it again rather than evaluating the feature anew.
|
||||
if !result.skipped {
|
||||
let local = fingerprint(0, f, &vals, false, &step_digests) ^ placement_digest(doc, f);
|
||||
cache.put(
|
||||
cache_key(f, prev, local, ctx, &before),
|
||||
CachedEval {
|
||||
error: None,
|
||||
warning: result.warning.clone(),
|
||||
boolean_diagnostics: result.boolean_diagnostics.clone(),
|
||||
ms,
|
||||
bytes: result.state.retained_bytes(),
|
||||
state: result.state.clone(),
|
||||
},
|
||||
);
|
||||
exact = !result.unkept;
|
||||
before = result.state.clone();
|
||||
}
|
||||
results.push(result);
|
||||
}
|
||||
Ok(Model { results, param_errors, step_digests, cache: Arc::new(std::sync::Mutex::new(cache)), ..Model::default() })
|
||||
let mut model = Model { results, param_errors, step_digests, ..Model::default() };
|
||||
let unkept = model.results.iter().filter(|r| r.unkept).count();
|
||||
if unkept > 0 {
|
||||
// The features whose state the file did not keep, each evaluated again from the state
|
||||
// before it: nothing else in the model is missing then.
|
||||
model.evaluate(doc);
|
||||
log::info!(
|
||||
"the saved model kept the states of {} of {} features: {} evaluated again",
|
||||
model.results.len().saturating_sub(unkept),
|
||||
model.results.len(),
|
||||
model.last_recomputed
|
||||
);
|
||||
}
|
||||
// Each result where an evaluation would have put it, so an edit undone (or set back)
|
||||
// finds it again rather than evaluating the feature anew.
|
||||
let mut before = Arc::new(ModelState::default());
|
||||
if let Ok(mut cache) = model.cache.lock() {
|
||||
for (f, r) in doc.features.iter().zip(&model.results).filter(|(_, r)| !r.skipped && r.error.is_none()) {
|
||||
let local = fingerprint(0, f, &vals, false, &model.step_digests) ^ placement_digest(doc, f, Some(&before));
|
||||
cache.put(
|
||||
cache_key(f, r.fingerprint, local, ctx, &before),
|
||||
CachedEval {
|
||||
error: None,
|
||||
warning: r.warning.clone(),
|
||||
boolean_diagnostics: r.boolean_diagnostics.clone(),
|
||||
ms: r.ms,
|
||||
bytes: r.state.retained_bytes(),
|
||||
state: r.state.clone(),
|
||||
},
|
||||
);
|
||||
before = r.state.clone();
|
||||
}
|
||||
}
|
||||
Ok(model)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,29 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
/// The motion retained by an associative trace. Joint bounds keep their expressions, in
|
||||
/// command units (degrees or millimetres); studies are referenced by their stable identity.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum TraceMotion {
|
||||
Joint { joint: u64, index: usize, from: serde_json::Value, to: serde_json::Value },
|
||||
Study { study: u64 },
|
||||
}
|
||||
|
||||
/// A generated root sketch curve and the inputs needed to regenerate it. The seed values
|
||||
/// select the original assembly branch: playing or dragging the mechanism does not redefine
|
||||
/// its trace. Older saved curves have no entry here and remain ordinary fixed sketch curves.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MotionTrace {
|
||||
pub sketch: u64,
|
||||
pub wire: String,
|
||||
pub occurrence: Vec<u64>,
|
||||
pub point: crate::joints::JointOrigin,
|
||||
pub motion: TraceMotion,
|
||||
pub samples: Option<usize>,
|
||||
pub seeds: Vec<(u64, Vec<f64>)>,
|
||||
}
|
||||
|
||||
/// Occurrences whose bodies may touch but not pass through each other while joints move.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ContactSet {
|
||||
|
||||
@@ -266,10 +266,7 @@ pub fn locate_face(st: &ModelState, name: &str, p: Vec3) -> Option<FaceHit> {
|
||||
// Every body's names are read below: work out the ones not known yet on all cores first
|
||||
// (each body's names are its own, whichever thread works them out).
|
||||
if legacy_name(name).is_none() {
|
||||
let bodies: Vec<&ModelBody> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
|
||||
solidcraft_kernel::for_each_on_threads(&bodies, 16, |b| {
|
||||
let _ = naming::names(b);
|
||||
});
|
||||
naming::warm_names(st.bodies.iter().filter(|b| !b.body.is_mesh()));
|
||||
}
|
||||
locate_in(st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh()), name, p)
|
||||
}
|
||||
@@ -445,9 +442,7 @@ pub fn locate_one(st: &ModelState, body: Option<&str>, name: &str) -> Result<Fac
|
||||
st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh() && body.is_none_or(|n| b.name == n)).collect();
|
||||
// As [`locate_face`]: the names of several bodies are worked out on all cores first.
|
||||
if bodies.len() > 1 && legacy_name(name).is_none() {
|
||||
solidcraft_kernel::for_each_on_threads(&bodies, 16, |(_, b)| {
|
||||
let _ = naming::names(b);
|
||||
});
|
||||
naming::warm_names(bodies.iter().map(|(_, b)| *b));
|
||||
}
|
||||
let hit = locate_in(bodies.into_iter(), name, Vec3::ZERO).ok_or_else(|| format!("no face is named `{name}` (`model.faces` lists the names)"))?;
|
||||
if hit.split {
|
||||
|
||||
@@ -229,6 +229,23 @@ pub fn face_name_exact(b: &ModelBody, index: usize) -> bool {
|
||||
names(b).verified(index)
|
||||
}
|
||||
|
||||
/// Prepare only names not yet known before a lookup reads several bodies. A ready cell is
|
||||
/// shared by snapshots and poses: dispatching threads just to read it costs more than a lookup.
|
||||
pub(crate) fn warm_names<'a>(bodies: impl Iterator<Item = &'a ModelBody>) {
|
||||
let bodies: Vec<_> = bodies.filter(|b| b.names.names.get().is_none()).collect();
|
||||
#[cfg(test)]
|
||||
tests::record_warm_jobs(bodies.len());
|
||||
match bodies.as_slice() {
|
||||
[] => {}
|
||||
[body] => {
|
||||
let _ = names(body);
|
||||
}
|
||||
_ => solidcraft_kernel::for_each_on_threads(&bodies, 16, |b| {
|
||||
let _ = names(b);
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// A body's names, with what finding them again needs: from the history of the feature that
|
||||
/// made it where it reports one, else from geometry (unverified).
|
||||
pub(crate) fn names(b: &ModelBody) -> Arc<Names> {
|
||||
@@ -583,7 +600,9 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
|
||||
}
|
||||
FeatureKind::Revolve { sketch, axis, .. } => {
|
||||
let Some(ss) = before.sketch(sketch.0) else { return format!("F{fid}:face:{fi}") };
|
||||
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis) else { return format!("F{fid}:face:{fi}") };
|
||||
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis, &|name| crate::eval::construction_axis_in(None, before, name, 0)) else {
|
||||
return format!("F{fid}:face:{fi}");
|
||||
};
|
||||
let (o, d) = (ss.plane.to_world(o2), (ss.plane.to_world(o2 + d2) - ss.plane.to_world(o2)).normalized().unwrap_or(Vec3::Z));
|
||||
// A flat face holding the axis is an angle cap: on the sketch plane, or the far one.
|
||||
let flat = !ns.is_empty() && ns.windows(2).all(|w| w[0].cross(w[1]).len() < 1e-6) && ns.iter().all(|m| m.dot(d).abs() < 1e-6);
|
||||
@@ -870,6 +889,85 @@ pub fn face_on_body(body: &ModelBody, name: &str, p: Vec3) -> Option<(usize, Vec
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
thread_local! {
|
||||
static WARM_JOBS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
pub(super) fn record_warm_jobs(n: usize) {
|
||||
WARM_JOBS.with(|jobs| jobs.set(jobs.get() + n));
|
||||
}
|
||||
|
||||
fn take_warm_jobs() -> usize {
|
||||
WARM_JOBS.with(|jobs| jobs.replace(0))
|
||||
}
|
||||
|
||||
fn lookup_bodies() -> ModelState {
|
||||
let body = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(4., 2., 1.)).unwrap();
|
||||
let mut st = ModelState::default();
|
||||
st.bodies = (0..13).map(|i| ModelBody::new(format!("Part{i}"), body.clone(), i + 1)).collect();
|
||||
st
|
||||
}
|
||||
|
||||
fn same_hit(a: &FaceHit, b: &FaceHit) {
|
||||
assert_eq!((a.body, a.face, a.point), (b.body, b.face, b.point));
|
||||
assert_eq!((a.split, a.merged, a.verified, a.on_point, a.legacy), (b.split, b.merged, b.verified, b.on_point, b.legacy));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_face_lookups_schedule_no_name_warmup() {
|
||||
let st = lookup_bodies();
|
||||
for b in &st.bodies {
|
||||
let _ = face_names(b);
|
||||
}
|
||||
let name = face_names(&st.bodies[0])[0].clone();
|
||||
let picked = locate_face(&st, &name, Vec3::ZERO).unwrap();
|
||||
let unique = locate_one(&st, None, &name).unwrap();
|
||||
take_warm_jobs();
|
||||
// The two public lookup paths used by picked origins and named-face commands.
|
||||
for _ in 0..145 {
|
||||
same_hit(&locate_face(&st, &name, Vec3::ZERO).unwrap(), &picked);
|
||||
same_hit(&locate_one(&st, None, &name).unwrap(), &unique);
|
||||
}
|
||||
assert!(locate_face(&st, "absent:face:0", Vec3::ZERO).is_none());
|
||||
assert!(locate_one(&st, None, "absent:face:0").is_err());
|
||||
assert_eq!(take_warm_jobs(), 0, "cached names must not dispatch preparation work");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_lookup_warms_only_cold_and_invalidated_names() {
|
||||
// Exercise both entry points as the first reader of a partly named model.
|
||||
for picked in [false, true] {
|
||||
let mut st = lookup_bodies();
|
||||
let name = face_names(&st.bodies[0])[0].clone();
|
||||
let lookup = |st: &ModelState, name: &str| {
|
||||
if picked { locate_face(st, name, Vec3::ZERO).unwrap() } else { locate_one(st, None, name).unwrap() }
|
||||
};
|
||||
take_warm_jobs();
|
||||
let hit = lookup(&st, &name);
|
||||
assert_eq!(take_warm_jobs(), 12, "only the other bodies need names");
|
||||
let snapshot = st.clone();
|
||||
same_hit(&lookup(&snapshot, &name), &hit);
|
||||
assert_eq!(take_warm_jobs(), 0, "snapshots share ready names");
|
||||
|
||||
st.bodies[0].name = "Renamed".into();
|
||||
st.bodies[0].names = st.bodies[0].names.renamed();
|
||||
let renamed = lookup(&st, "Renamed:face:0");
|
||||
same_hit(&renamed, &hit);
|
||||
assert_eq!(take_warm_jobs(), 1, "a rename invalidates only its own names");
|
||||
assert!(locate_face(&st, &name, Vec3::ZERO).is_none());
|
||||
same_hit(&lookup(&snapshot, &name), &hit);
|
||||
assert_eq!(take_warm_jobs(), 0, "the retained snapshot stays ready");
|
||||
|
||||
let source = st.bodies[1].clone();
|
||||
st.bodies[1].names = NameCell::same(&source);
|
||||
same_hit(&lookup(&st, "Renamed:face:0"), &renamed);
|
||||
assert_eq!(take_warm_jobs(), 1, "a lazy moved copy is prepared once");
|
||||
assert!(Arc::ptr_eq(&names(&st.bodies[1]), &names(&source)));
|
||||
same_hit(&lookup(&st, "Renamed:face:0"), &renamed);
|
||||
assert_eq!(take_warm_jobs(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pieces lying almost level along the sort axis must keep the order they arrived in, whatever
|
||||
/// the low bits of their centres do. The centre is the mean of a tessellated boundary, so those
|
||||
/// bits depend on where the loop's polyline starts and can differ between processes — which is
|
||||
|
||||