Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dda74755e | |||
| 605e0a935c | |||
| 5a09a9c49e |
@@ -317,6 +317,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/ui/design/svg/model/circle_ttt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/coil.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/colored_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/colored_components_off.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/combine.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/component.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
|
||||
@@ -786,19 +787,6 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
|
||||
| assets/locales/pending/theme-blur-ui.json | SolidCraft contributors | Original blur controls, notices and translations | MIT OR Apache-2.0 |
|
||||
| crates/ui-egui/tests/fixtures/theme-blur.json | SolidCraft contributors | Original theme for blur option scenarios and manager review | MIT OR Apache-2.0 |
|
||||
| assets/locales/ka.json | David Machakhelidze (devius) | Original Georgian translation of SolidCraft's English catalog, written with Claude Code from the meaning of each English string; terminology and doubtful strings decided by a native Georgian speaker; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark.svg | SolidCraft contributors | static SolidCraft mark: the five faces and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg (the final frame of assets/styles/web/loading-logo.svg), cropped to the mark's bounds; original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-mono-black.svg | SolidCraft contributors | one-colour (black) version of assets/branding/logo/solidcraft-mark.svg, same paths; original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-mono-white.svg | SolidCraft contributors | one-colour (white) version of assets/branding/logo/solidcraft-mark.svg, same paths; original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-512.png | SolidCraft contributors | transparent 512 px render of assets/branding/logo/solidcraft-mark.svg (cairosvg); original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-1024.png | SolidCraft contributors | transparent 1024 px render of assets/branding/logo/solidcraft-mark.svg (cairosvg); original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-mark-2048.png | SolidCraft contributors | transparent 2048 px render of assets/branding/logo/solidcraft-mark.svg (cairosvg); original artwork | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-wordmark.svg | SolidCraft contributors | the name SolidCraft set like the loading logo's wordmark (bold, +0.02 em tracking, #15499e) in IBM Plex Sans Bold from assets/fonts/ibm-plex-sans (SIL OFL 1.1, IBM Corp.), outlined to paths; no font file is embedded | MIT OR Apache-2.0 |
|
||||
| assets/branding/logo/solidcraft-lockup-horizontal.svg | SolidCraft contributors | assets/branding/logo/solidcraft-mark.svg beside assets/branding/logo/solidcraft-wordmark.svg, in the proportions of assets/branding/web/source/og.png; original artwork | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/onyx.json | SolidCraft contributors | original Onyx stone-grey palette, option overlays and semantic icon-role mapping; existing SolidCraft artwork is unchanged | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/ui-tour/window.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (`solidcraft --sample`, default dark theme, software renderer, 1280x800) with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/book/src/media/ui-tour/navigate.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (`--sample`, default dark theme, software renderer) orbiting, panning and zooming, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/sketch-tool-hints.json | carlymx | Original sketch tool hints and engine error translations | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/primitive-pick-field-tips.json | SolidCraft contributors | Original primitive pick-field tooltip translations | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
@@ -850,7 +838,7 @@ into THIRD-PARTY-LICENSES.txt when `main` regenerates it.
|
||||
## FreeCAD and Rhino import library
|
||||
|
||||
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
|
||||
0.6.7 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
|
||||
0.6.3 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
|
||||
`cadmpeg-container`, `cadmpeg-codec-freecad`, `cadmpeg-codec-rhino` and `cadmpeg-codec-step`
|
||||
in native and browser builds (`crates/io/src/native_cad.rs`). Only those open-format codecs are built: no
|
||||
default features, so cadmpeg's codecs for commercial CAD formats and its ShapeManager/ACIS
|
||||
|
||||
@@ -88,8 +88,6 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
|
||||
|
||||
- `crates/geom`: vectors, planes, meshes, measures.
|
||||
- `crates/sketch`: our own constraint solver, plus profile finding.
|
||||
- `crates/dynamics`: multibody dynamics: rigid bodies, joint constraints, gravity and springs,
|
||||
integrated in time (RFC #9719). It depends only on `geom`.
|
||||
- `crates/kernel`: the B-rep kernel boundary. It wraps [truck](https://github.com/ricosjp/truck),
|
||||
and it's the only crate allowed to touch truck. Fillets, chamfers, shell/offset, Delete Face,
|
||||
Split Face, STEP/IGES read and write, and face provenance are our own code here. Patched
|
||||
@@ -474,16 +472,6 @@ the same area), first resolve the command registry and source conflicts, then ru
|
||||
The generator tests cover stable ordering and additions that leave existing lines
|
||||
unchanged. The initial switch to sorted rows is a one-time larger diff.
|
||||
|
||||
## Pictures in the user guide
|
||||
|
||||
The user guide's screenshots and short clips (`docs/book/src/media/`) are captured from the real
|
||||
app by `examples/demo/doc_shots.py`, one function per figure. Each run starts a fresh app with its
|
||||
own settings, so your preferences never show. When a change makes a figure out of date, run
|
||||
`python3 examples/demo/doc_shots.py <page or shot>` (it needs Xvfb, ffmpeg, Pillow and a release
|
||||
build) and commit the new file. A new figure is a new function there, an ATTRIBUTION row in
|
||||
`attribution/pending/`, and a `<video>` (clips) or image link on the page. Keep a still under
|
||||
about 250 KB and a clip under about 1 MB.
|
||||
|
||||
## Licence
|
||||
|
||||
By contributing you agree that your contribution is licensed as MIT OR Apache-2.0, like the rest
|
||||
|
||||
@@ -564,9 +564,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-codec-freecad"
|
||||
version = "0.6.7"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fe0ed1935ace2c203b87107a064818dec2bb2ec2f250c4ad121767f3da75d1b"
|
||||
checksum = "3c4a8fa61748cbb3ce80838b00a8cebfe553460583941f5aba6e2b97c16b4d37"
|
||||
dependencies = [
|
||||
"cadmpeg-container",
|
||||
"cadmpeg-core",
|
||||
@@ -580,9 +580,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-codec-rhino"
|
||||
version = "0.6.7"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd2b76bd1d9301c949fbd731fd22cff9d37d10d1f481c754b557c2187bd0171"
|
||||
checksum = "d7af41440d4c1d69e88e612de02cc3c9bad79d03429531e640ac924fef53c6bf"
|
||||
dependencies = [
|
||||
"cadmpeg-container",
|
||||
"cadmpeg-core",
|
||||
@@ -599,9 +599,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-codec-step"
|
||||
version = "0.6.7"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cbce33ed681a28b62d8b01ae64cca2b01f11f31b9a3e5e2455b9ffde915712"
|
||||
checksum = "35e1b6d17cf26b31f59f2edab8ef5c29d2a33216c581fd913f012ad4869bd559"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cadmpeg-container",
|
||||
@@ -616,9 +616,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-container"
|
||||
version = "0.6.7"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03f0d1c22b875294c5fbf4a4738aec8f8c0a4ad035eb28ece404f1dec21f371a"
|
||||
checksum = "5ab7bcef48ac7480b4917820272d85cd2c9533e17904e15b0e4d22f19a6045bd"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"crc32fast",
|
||||
@@ -628,9 +628,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-core"
|
||||
version = "0.6.7"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "795bdc6e2a10ecc0627138874c17e2948a3925cd22c6c6b0c2e4f6621d92f89c"
|
||||
checksum = "e86bb7a40521832504bb1f5953a1c706b2c52aad73bf6d84e1b35e5cb0beae3f"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"memchr",
|
||||
@@ -643,9 +643,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-ir"
|
||||
version = "0.6.7"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5592036516aaf2574b159a86a3d1ae872dc245261de86e4c0ce6773fed00a249"
|
||||
checksum = "b570a1a8c579e407a9fd7bb10fa1900f8827f1d578b8fcb4ea2a056b5f323c72"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cadmpeg-core",
|
||||
@@ -658,9 +658,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-registry"
|
||||
version = "0.6.7"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc163233d5f664337267192848505854de6aeed4843181adf87f58baddcee9a0"
|
||||
checksum = "b2be23469a9bca0b684f1811118fffb581ebd32ded08e6cc8b0716a41f8c1377"
|
||||
dependencies = [
|
||||
"cadmpeg-codec-freecad",
|
||||
"cadmpeg-codec-rhino",
|
||||
@@ -4153,17 +4153,6 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-dynamics"
|
||||
version = "0.40.0-rc.2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcore-record",
|
||||
"solidcraft-geom",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.40.0-rc.2"
|
||||
@@ -4286,11 +4275,9 @@ dependencies = [
|
||||
name = "solidcraft-render"
|
||||
version = "0.40.0-rc.2"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"image",
|
||||
"moxcms",
|
||||
"png",
|
||||
"robust",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
|
||||
@@ -22,7 +22,6 @@ solidcraft-kernel = { path = "crates/kernel" }
|
||||
solidcraft-smlib = { path = "crates/smlib" }
|
||||
solidcraft-step = { path = "crates/step" }
|
||||
solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-dynamics = { path = "crates/dynamics" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
solidcraft-io = { path = "crates/io" }
|
||||
@@ -82,9 +81,9 @@ gethostname = "1"
|
||||
# Pinned: its IR and codec APIs change between minor versions. Open formats only: no default
|
||||
# features, so cadmpeg's codecs for commercial formats (f3d, sldprt, inventor, catia, creo, nx)
|
||||
# and its ShapeManager/ACIS `sat` codec are not compiled in. `step` is the writer we restate through.
|
||||
cadmpeg-core = "=0.6.7"
|
||||
cadmpeg-ir = "=0.6.7"
|
||||
cadmpeg-registry = { version = "=0.6.7", default-features = false, features = ["fcstd", "rhino", "step"] }
|
||||
cadmpeg-core = "=0.6.3"
|
||||
cadmpeg-ir = "=0.6.3"
|
||||
cadmpeg-registry = { version = "=0.6.3", default-features = false, features = ["fcstd", "rhino", "step"] }
|
||||
|
||||
# fonts (sketch text outlines; MIT OR Apache-2.0, pure Rust)
|
||||
ttf-parser = { version = "0.25", default-features = false, features = ["std", "variable-fonts"] }
|
||||
|
||||
@@ -57,13 +57,13 @@ bytemuck_derive 1.12.1 (Zlib OR Apache-2.0 OR MIT)
|
||||
byteorder 1.5.0 (Unlicense OR MIT)
|
||||
byteorder-lite 0.1.0 (Unlicense OR MIT)
|
||||
bytes 1.12.1 (MIT)
|
||||
cadmpeg-codec-freecad 0.6.7 (Apache-2.0)
|
||||
cadmpeg-codec-rhino 0.6.7 (Apache-2.0)
|
||||
cadmpeg-codec-step 0.6.7 (Apache-2.0)
|
||||
cadmpeg-container 0.6.7 (Apache-2.0)
|
||||
cadmpeg-core 0.6.7 (Apache-2.0)
|
||||
cadmpeg-ir 0.6.7 (Apache-2.0)
|
||||
cadmpeg-registry 0.6.7 (Apache-2.0)
|
||||
cadmpeg-codec-freecad 0.6.3 (Apache-2.0)
|
||||
cadmpeg-codec-rhino 0.6.3 (Apache-2.0)
|
||||
cadmpeg-codec-step 0.6.3 (Apache-2.0)
|
||||
cadmpeg-container 0.6.3 (Apache-2.0)
|
||||
cadmpeg-core 0.6.3 (Apache-2.0)
|
||||
cadmpeg-ir 0.6.3 (Apache-2.0)
|
||||
cadmpeg-registry 0.6.3 (Apache-2.0)
|
||||
calloop 0.13.0 (MIT)
|
||||
calloop 0.14.5 (MIT)
|
||||
calloop-wayland-source 0.3.0 (MIT)
|
||||
@@ -15978,13 +15978,13 @@ accesskit_windows 0.32.1: MIT OR Apache-2.0 (https://github.com/AccessKit/access
|
||||
accesskit_winit 0.32.2: Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
block2 0.5.1: MIT (https://github.com/madsmtm/objc2)
|
||||
block2 0.6.2: MIT (https://github.com/madsmtm/objc2)
|
||||
cadmpeg-codec-freecad 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-rhino 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-step 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-container 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-core 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-ir 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-registry 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-freecad 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-rhino 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-step 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-container 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-core 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-ir 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-registry 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
clipboard-win 5.4.1: BSL-1.0 (https://github.com/DoumanAsh/clipboard-win)
|
||||
dispatch 0.2.0: MIT (http://github.com/SSheldon/rust-dispatch)
|
||||
dispatch2 0.3.1: Zlib OR Apache-2.0 OR MIT (https://github.com/madsmtm/objc2)
|
||||
|
||||
@@ -25,7 +25,6 @@ solidcraft-engine = { workspace = true }
|
||||
solidcraft-ui-egui = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
url = "2"
|
||||
percent-encoding = "2"
|
||||
|
||||
@@ -34,6 +33,7 @@ eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
serde_json = { workspace = true }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "Location", "Navigator", "StorageManager",
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemGetDirectoryOptions", "FileSystemWritableFileStream",
|
||||
|
||||
@@ -553,7 +553,7 @@ mod web {
|
||||
/// Open a design remembered from an earlier session (a recent one, after a reload): ask once
|
||||
/// for access to the file (call in the click), read it into the engine's files and queue it
|
||||
/// to open. A file that is gone is forgotten and dropped from the recent designs.
|
||||
pub fn restore(files: &Rc<Files>, path: &str, on_opened: Option<Box<dyn FnOnce()>>) {
|
||||
pub fn restore(files: &Rc<Files>, path: &str) {
|
||||
let Some((id, name)) = parse(path) else { return };
|
||||
let (files, path, id, name) = (files.clone(), path.to_string(), id.to_string(), name.to_string());
|
||||
spawn_local(async move {
|
||||
@@ -568,9 +568,6 @@ mod web {
|
||||
files.opened.borrow_mut().push(path);
|
||||
files.wake();
|
||||
touch(&id).await;
|
||||
if let Some(done) = on_opened {
|
||||
done();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let gone = e.contains("NotFoundError") || e.contains("could not be found") || e.contains("no longer remembers");
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
//! and go out as downloads (exports, and "Download design"). Where the File System Access API
|
||||
//! exists a design also lives on the computer's disk, saved and opened through the browser's file
|
||||
//! dialogs (`disk.rs`). An autosave of unsaved work is kept in OPFS and offered again after a
|
||||
//! reload or a crash. With "keep unsaved work in this browser" on, each open design's unsaved
|
||||
//! changes are kept there instead, one copy per design (`keep.rs`, #10220).
|
||||
//! reload or a crash.
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
@@ -14,7 +13,7 @@ use std::rc::Rc;
|
||||
use js_sys::{Array, Function, Object, Reflect, Uint8Array};
|
||||
use solidcraft_engine::io::vfs;
|
||||
|
||||
use crate::{disk, keep};
|
||||
use crate::disk;
|
||||
use wasm_bindgen::{JsCast as _, JsValue, closure::Closure};
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
use web_sys::{
|
||||
@@ -80,16 +79,6 @@ pub struct Files {
|
||||
pub recovery: RefCell<Option<(String, Option<String>)>>,
|
||||
/// The app's context, to wake it when a browser call finishes.
|
||||
pub ctx: RefCell<Option<egui::Context>>,
|
||||
/// This page's id: the copies it keeps are named after it (`keep::entry_id`).
|
||||
pub keep_page: RefCell<String>,
|
||||
/// Kept copies stored by this page, and the design revision each holds once its write ended.
|
||||
pub kept: RefCell<std::collections::HashMap<String, Option<u64>>>,
|
||||
/// Kept copies being written or removed now (one browser call at a time per copy).
|
||||
pub keeping: RefCell<std::collections::HashSet<String>>,
|
||||
/// When (ms) the last write of a copy failed, to wait before trying again (`keep::may_write`).
|
||||
pub kept_failed: RefCell<std::collections::HashMap<String, f64>>,
|
||||
/// Copies earlier pages left, newer than their design's last save: offered at start.
|
||||
pub keep_offered: RefCell<Vec<keep::Entry>>,
|
||||
}
|
||||
|
||||
impl Files {
|
||||
@@ -299,9 +288,6 @@ pub fn start(files: &Rc<Files>) {
|
||||
let _ = JsFuture::from(p).await;
|
||||
});
|
||||
}
|
||||
let page = keep::page_id(js_sys::Date::now(), js_sys::Math::random());
|
||||
hold_page_lock(&page);
|
||||
*files.keep_page.borrow_mut() = page;
|
||||
let files = files.clone();
|
||||
spawn_local(async move {
|
||||
match disk::known_ids().await {
|
||||
@@ -314,7 +300,6 @@ pub fn start(files: &Rc<Files>) {
|
||||
files.say("This page can't use the browser's file storage (it needs https or localhost): designs are kept in local storage, a few MB at most.", false);
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
let kept = kept_copies(&all);
|
||||
let has_recovery = all.iter().any(|(n, _, _)| n == RECOVERY);
|
||||
let legacy = (!has_recovery).then(|| LEGACY_RECOVERY.into_iter().find(|l| all.iter().any(|(n, _, _)| n == l))).flatten();
|
||||
for (name, bytes, modified) in all {
|
||||
@@ -330,7 +315,7 @@ pub fn start(files: &Rc<Files>) {
|
||||
*files.recovery.borrow_mut() = Some((n, meta["path"].as_str().map(str::to_string)));
|
||||
continue;
|
||||
}
|
||||
if name != RECOVERY && keep::parse_name(&name).is_none() {
|
||||
if name != RECOVERY {
|
||||
entries.push(Entry { name: name.clone(), size: bytes.len() as u64, modified });
|
||||
}
|
||||
vfs::insert(&format!("{OPFS}{name}"), bytes);
|
||||
@@ -341,7 +326,6 @@ pub fn start(files: &Rc<Files>) {
|
||||
if !vfs::exists(&format!("{OPFS}{RECOVERY}")) {
|
||||
*files.recovery.borrow_mut() = None;
|
||||
}
|
||||
sort_kept(&files, kept, &live_pages().await);
|
||||
}
|
||||
Err(e) => files.say(format!("browser storage: {e}"), true),
|
||||
}
|
||||
@@ -359,7 +343,7 @@ pub fn flush(files: &Rc<Files>) {
|
||||
{
|
||||
let mut e = files.entries.borrow_mut();
|
||||
e.retain(|x| x.name != name);
|
||||
if name != RECOVERY && name != RECOVERY_META && keep::parse_name(&name).is_none() {
|
||||
if name != RECOVERY && name != RECOVERY_META {
|
||||
e.push(Entry { name: name.clone(), size: bytes.len() as u64, modified: js_sys::Date::now() });
|
||||
e.sort_by_key(|a| a.name.to_lowercase());
|
||||
}
|
||||
@@ -489,15 +473,7 @@ pub fn recovery_path() -> String {
|
||||
/// A recovery file lives in OPFS, but its relative links belong to the original design's
|
||||
/// folder (which can be on disk). Resolve them before Open sees the temporary file location.
|
||||
pub fn prepare_recovery(original_path: Option<&str>) -> Result<String, String> {
|
||||
prepare_copy(recovery_path(), original_path)
|
||||
}
|
||||
|
||||
/// The same for a kept copy (#10220).
|
||||
pub fn prepare_kept(id: &str, original_path: Option<&str>) -> Result<String, String> {
|
||||
prepare_copy(format!("{OPFS}{}", keep::design_file(id)), original_path)
|
||||
}
|
||||
|
||||
fn prepare_copy(path: String, original_path: Option<&str>) -> Result<String, String> {
|
||||
let path = recovery_path();
|
||||
let bytes = vfs::read(&path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| e.to_string())?;
|
||||
let mut doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?;
|
||||
solidcraft_engine::resolve_design_links(&mut doc, original_path);
|
||||
@@ -670,149 +646,3 @@ pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput, theme: bool) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Hold this page's browser lock while it runs, so other tabs know its kept copies are in use
|
||||
/// (`keep.rs`). Browsers without the Web Locks API hold none; their copies are then offered by
|
||||
/// any later page.
|
||||
fn hold_page_lock(page: &str) {
|
||||
let Some(locks) = web_locks() else { return };
|
||||
let Ok(request) = Reflect::get(&locks, &"request".into()).and_then(|f| f.dyn_into::<Function>().map_err(JsValue::from)) else { return };
|
||||
// A promise that never settles: the lock is let go only when the page goes.
|
||||
let hold = Closure::<dyn FnMut(JsValue) -> js_sys::Promise>::new(|_lock: JsValue| js_sys::Promise::new(&mut |_, _| {}));
|
||||
let name = format!("{}{page}", keep::LOCK_PREFIX);
|
||||
if let Err(e) = request.call2(&locks, &name.into(), hold.as_ref().unchecked_ref()) {
|
||||
log::info!("keep: no page lock: {}", err(e));
|
||||
}
|
||||
// The callback lives as long as the page.
|
||||
hold.forget();
|
||||
}
|
||||
|
||||
fn web_locks() -> Option<JsValue> {
|
||||
let nav = web_sys::window()?.navigator();
|
||||
Reflect::get(&nav, &"locks".into()).ok().filter(|l| l.is_object())
|
||||
}
|
||||
|
||||
/// The pages still open (in this browser profile) that hold their lock.
|
||||
async fn live_pages() -> Vec<String> {
|
||||
let query = async {
|
||||
let locks = web_locks().ok_or("no Web Locks")?;
|
||||
let f: Function = Reflect::get(&locks, &"query".into()).map_err(err)?.dyn_into().map_err(|_| "no query".to_string())?;
|
||||
let p: js_sys::Promise = f.call0(&locks).map_err(err)?.dyn_into().map_err(|_| "no promise".to_string())?;
|
||||
let state = JsFuture::from(p).await.map_err(err)?;
|
||||
let held: Array = Reflect::get(&state, &"held".into()).map_err(err)?.dyn_into().map_err(|_| "no list".to_string())?;
|
||||
Ok::<_, String>(
|
||||
held.iter()
|
||||
.take(10_000)
|
||||
.filter_map(|l| Reflect::get(&l, &"name".into()).ok()?.as_string())
|
||||
.filter_map(|n| n.strip_prefix(keep::LOCK_PREFIX).map(str::to_string))
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
query.await.unwrap_or_else(|e| {
|
||||
log::info!("keep: open pages unknown ({e})");
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
|
||||
/// The kept copies among the stored files: (copy, when its design's file was last written), and
|
||||
/// the ids of designs stored without their metadata.
|
||||
struct Found {
|
||||
copies: Vec<(keep::Entry, Option<f64>)>,
|
||||
orphans: Vec<String>,
|
||||
}
|
||||
|
||||
fn kept_copies(all: &[(String, Vec<u8>, f64)]) -> Found {
|
||||
let modified = |path: &str| path.strip_prefix(OPFS).and_then(|n| all.iter().find(|(name, _, _)| name == n)).map(|(_, _, m)| *m);
|
||||
let mut copies = Vec::new();
|
||||
let mut designs = Vec::new();
|
||||
for (name, bytes, _) in all {
|
||||
match keep::parse_name(name) {
|
||||
Some((id, keep::Part::Meta)) => {
|
||||
if let Some(e) = keep::Entry::parse(id, bytes) {
|
||||
let saved = e.path.as_deref().and_then(modified);
|
||||
copies.push((e, saved));
|
||||
}
|
||||
}
|
||||
Some((id, keep::Part::Design)) => designs.push(id.to_string()),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
// A copy is whole only with its design.
|
||||
copies.retain(|(e, _)| designs.contains(&e.id));
|
||||
let orphans = designs.into_iter().filter(|id| !copies.iter().any(|(e, _)| &e.id == id)).collect();
|
||||
Found { copies, orphans }
|
||||
}
|
||||
|
||||
/// Offer the copies earlier pages left that hold unsaved work, and remove those their design's
|
||||
/// later save made stale (and parts without the rest), leaving open pages' copies alone.
|
||||
fn sort_kept(files: &Rc<Files>, found: Found, live: &[String]) {
|
||||
let mut offered = Vec::new();
|
||||
let mut gone = Vec::new();
|
||||
for (e, saved) in found.copies {
|
||||
match keep::fate(&e, live, saved) {
|
||||
keep::Fate::Offer => offered.push(e),
|
||||
keep::Fate::Drop => gone.push(e.id),
|
||||
keep::Fate::Leave => {}
|
||||
}
|
||||
}
|
||||
gone.extend(found.orphans.into_iter().filter(|id| !keep::page_of(id).is_some_and(|p| live.iter().any(|l| l == p))));
|
||||
keep::order_offered(&mut offered);
|
||||
*files.keep_offered.borrow_mut() = offered;
|
||||
for id in gone {
|
||||
remove_kept(files, &id);
|
||||
}
|
||||
}
|
||||
|
||||
/// The path (in the engine's files) a kept copy opens from.
|
||||
pub fn kept_path(id: &str) -> String {
|
||||
format!("{OPFS}{}", keep::design_file(id))
|
||||
}
|
||||
|
||||
/// Write the copy of a design with unsaved changes (`bytes`, described by `entry`) that holds
|
||||
/// its revision `revision`. Its metadata goes last, so a copy is never offered half written.
|
||||
pub fn write_kept(files: &Rc<Files>, entry: &keep::Entry, bytes: Vec<u8>, revision: u64) {
|
||||
let id = entry.id.clone();
|
||||
if !files.keeping.borrow_mut().insert(id.clone()) {
|
||||
return;
|
||||
}
|
||||
files.kept.borrow_mut().entry(id.clone()).or_insert(None);
|
||||
let meta = entry.to_json();
|
||||
let files = files.clone();
|
||||
spawn_local(async move {
|
||||
let r = async {
|
||||
write_file(&keep::design_file(&id), &bytes).await?;
|
||||
write_file(&keep::meta_file(&id), meta.as_bytes()).await
|
||||
};
|
||||
match r.await {
|
||||
Ok(()) => {
|
||||
files.kept.borrow_mut().insert(id.clone(), Some(revision));
|
||||
files.kept_failed.borrow_mut().remove(&id);
|
||||
}
|
||||
Err(e) => {
|
||||
files.kept_failed.borrow_mut().insert(id.clone(), js_sys::Date::now());
|
||||
files.say(format!("keeping unsaved work in this browser: {e}"), true)
|
||||
}
|
||||
}
|
||||
files.keeping.borrow_mut().remove(&id);
|
||||
files.wake();
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove kept copy `id` (its design was saved, closed, discarded or restored).
|
||||
pub fn remove_kept(files: &Rc<Files>, id: &str) {
|
||||
let id = id.to_string();
|
||||
if !files.keeping.borrow_mut().insert(id.clone()) {
|
||||
return;
|
||||
}
|
||||
let _ = vfs::remove(&kept_path(&id));
|
||||
files.kept_failed.borrow_mut().remove(&id);
|
||||
let files = files.clone();
|
||||
spawn_local(async move {
|
||||
// The metadata first: what's left without it is never offered.
|
||||
let _ = remove_file(&keep::meta_file(&id)).await;
|
||||
let _ = remove_file(&keep::design_file(&id)).await;
|
||||
files.kept.borrow_mut().remove(&id);
|
||||
files.keeping.borrow_mut().remove(&id);
|
||||
files.wake();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
//! Keep unsaved work in this browser (#10220): the decisions, in plain Rust (tested natively).
|
||||
//!
|
||||
//! With Preferences › General › "Keep unsaved work in this browser instead of asking when the tab
|
||||
//! closes" on, each open design with unsaved changes has a copy in the browser's private storage:
|
||||
//! `keep-<id>.sc` (the design) and `keep-<id>.json` (its name, its file and when it last
|
||||
//! changed). The copy is written shortly after each change ([`due`]) and again when the page is
|
||||
//! hidden, so it is already current when the tab closes; the page then doesn't ask through the
|
||||
//! browser's "leave site?" box ([`asks_before_unload`]). A design that is saved, closed or
|
||||
//! discarded has its copy removed. The next visit offers the copies newer than their design's
|
||||
//! last save ([`fate`]) in SolidCraft's own window, answered with [`plan`].
|
||||
//!
|
||||
//! An id is `<page>-<n>`: the page that wrote it (each page load has its own) and the design's
|
||||
//! number in that page. A page holds a browser lock named after it while it runs, so another tab
|
||||
//! never offers, or removes, the copies of a page that is still open.
|
||||
#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// The start of every kept file's name.
|
||||
pub const PREFIX: &str = "keep-";
|
||||
/// The browser lock a page holds while it runs: this, then its page id.
|
||||
pub const LOCK_PREFIX: &str = "solidcraft.keep.";
|
||||
/// A copy is written once the design has been still this long (ms)…
|
||||
pub const QUIET_MS: f64 = 800.0;
|
||||
/// …or at the latest this long after the first change it doesn't hold yet (ms).
|
||||
pub const MAX_WAIT_MS: f64 = 4000.0;
|
||||
/// The most copies offered at once (the rest wait for the next visit).
|
||||
pub const MAX_OFFERED: usize = 32;
|
||||
const MAX_ID: usize = 64;
|
||||
const MAX_NAME: usize = 256;
|
||||
const MAX_PATH: usize = 4096;
|
||||
/// Copies' metadata is small: a damaged or hostile one is not read.
|
||||
pub const MAX_META_BYTES: usize = 64 * 1024;
|
||||
|
||||
/// Ids are short and safe in a file name: ASCII letters, digits and dashes, with a page part.
|
||||
pub fn valid_id(id: &str) -> bool {
|
||||
!id.is_empty() && id.len() <= MAX_ID && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') && page_of(id).is_some()
|
||||
}
|
||||
|
||||
/// The page that wrote copy `id`.
|
||||
pub fn page_of(id: &str) -> Option<&str> {
|
||||
id.rsplit_once('-').map(|(page, _)| page).filter(|p| !p.is_empty())
|
||||
}
|
||||
|
||||
/// This page's id, from the time it started (ms since 1970) and a random number in 0..1:
|
||||
/// letters and digits only.
|
||||
pub fn page_id(now_ms: f64, random: f64) -> String {
|
||||
let now = if now_ms.is_finite() && now_ms > 0.0 { now_ms.min(1e15) as u64 } else { 0 };
|
||||
let random = if random.is_finite() { (random.clamp(0.0, 1.0) * f64::from(0xff_ffff)) as u64 } else { 0 };
|
||||
format!("{now:x}{random:06x}")
|
||||
}
|
||||
|
||||
/// The copy of design `uid` (`Session::uid`) open in page `page`.
|
||||
pub fn entry_id(page: &str, uid: u64) -> String {
|
||||
format!("{page}-{uid}")
|
||||
}
|
||||
|
||||
/// The stored file names of copy `id`: its design and its metadata.
|
||||
pub fn design_file(id: &str) -> String {
|
||||
format!("{PREFIX}{id}.{}", solidcraft_engine::io::DESIGN_EXT)
|
||||
}
|
||||
pub fn meta_file(id: &str) -> String {
|
||||
format!("{PREFIX}{id}.json")
|
||||
}
|
||||
|
||||
/// Which part of a copy a stored file is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Part {
|
||||
Design,
|
||||
Meta,
|
||||
}
|
||||
|
||||
/// A stored file's copy id and part; `None` for every other file.
|
||||
pub fn parse_name(name: &str) -> Option<(&str, Part)> {
|
||||
let rest = name.strip_prefix(PREFIX)?;
|
||||
let (id, part) = if let Some(id) = rest.strip_suffix(".json") {
|
||||
(id, Part::Meta)
|
||||
} else {
|
||||
(rest.strip_suffix(solidcraft_engine::io::DESIGN_EXT)?.strip_suffix('.')?, Part::Design)
|
||||
};
|
||||
valid_id(id).then_some((id, part))
|
||||
}
|
||||
|
||||
/// A kept copy, as its metadata describes it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Entry {
|
||||
pub id: String,
|
||||
/// The design's name.
|
||||
pub name: String,
|
||||
/// Its file (`None`: never saved).
|
||||
pub path: Option<String>,
|
||||
/// When the design last changed (ms since 1970).
|
||||
pub changed_at: f64,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn to_json(&self) -> String {
|
||||
json!({"name": self.name, "path": self.path, "changed_at": self.changed_at}).to_string()
|
||||
}
|
||||
|
||||
/// Read copy `id`'s metadata. Anything malformed, oversized or not finite is no copy.
|
||||
pub fn parse(id: &str, bytes: &[u8]) -> Option<Entry> {
|
||||
if !valid_id(id) || bytes.len() > MAX_META_BYTES {
|
||||
return None;
|
||||
}
|
||||
let v: Value = serde_json::from_slice(bytes).ok()?;
|
||||
let name = v.get("name")?.as_str()?.chars().take(MAX_NAME).collect::<String>();
|
||||
let path = match v.get("path") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(p) => Some(p.as_str().filter(|p| !p.is_empty() && p.len() <= MAX_PATH)?.to_string()),
|
||||
};
|
||||
let changed_at = v.get("changed_at")?.as_f64().filter(|t| t.is_finite() && *t >= 0.0)?;
|
||||
Some(Entry { id: id.to_string(), name, path, changed_at })
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a copy that last changed at `changed_at` holds work its design's file doesn't:
|
||||
/// the file was last written at `saved_at` (ms since 1970; `None` or unknown: never saved, or
|
||||
/// the time can't be read, so the copy is kept rather than lost).
|
||||
pub fn newer_than_save(changed_at: f64, saved_at: Option<f64>) -> bool {
|
||||
match saved_at {
|
||||
Some(saved) if saved.is_finite() && saved > 0.0 => changed_at > saved,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the next visit does with a copy it finds.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Fate {
|
||||
/// Offer it: "Restore unsaved design…?".
|
||||
Offer,
|
||||
/// Its design was saved after it: remove it without asking.
|
||||
Drop,
|
||||
/// Its page is still open (another tab): leave it alone.
|
||||
Leave,
|
||||
}
|
||||
|
||||
/// The copy `entry`, whose design's file was last written at `saved_at`, while the pages
|
||||
/// `live` are open.
|
||||
pub fn fate(entry: &Entry, live: &[String], saved_at: Option<f64>) -> Fate {
|
||||
if page_of(&entry.id).is_some_and(|p| live.iter().any(|l| l == p)) {
|
||||
Fate::Leave
|
||||
} else if newer_than_save(entry.changed_at, saved_at) {
|
||||
Fate::Offer
|
||||
} else {
|
||||
Fate::Drop
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies to offer first: the most recently changed, at most [`MAX_OFFERED`].
|
||||
pub fn order_offered(entries: &mut Vec<Entry>) {
|
||||
entries.sort_by(|a, b| b.changed_at.total_cmp(&a.changed_at).then_with(|| a.id.cmp(&b.id)));
|
||||
entries.truncate(MAX_OFFERED);
|
||||
}
|
||||
|
||||
/// Whether a design's copy is due to be written at `now`: the design last changed at
|
||||
/// `last_change`, and the copy has lacked a change since `first_unwritten` (all ms).
|
||||
pub fn due(now: f64, first_unwritten: f64, last_change: f64) -> bool {
|
||||
now - last_change >= QUIET_MS || now - first_unwritten >= MAX_WAIT_MS
|
||||
}
|
||||
|
||||
/// How long after a failed write of a design's copy it isn't tried again (ms).
|
||||
pub const RETRY_MS: f64 = 30_000.0;
|
||||
|
||||
/// Whether a copy whose write last failed at `failed_at` (ms; `None`: it hasn't) may be written
|
||||
/// at `now`: not before [`RETRY_MS`] has passed, so a full or denied store isn't hit every tick.
|
||||
pub fn may_write(now: f64, failed_at: Option<f64>) -> bool {
|
||||
failed_at.is_none_or(|t| !(now >= t) || now - t >= RETRY_MS)
|
||||
}
|
||||
|
||||
/// Whether `wanted` is the design now open, given the open design's path.
|
||||
pub fn is_open(open: Option<&str>, wanted: &str) -> bool {
|
||||
open == Some(wanted)
|
||||
}
|
||||
|
||||
/// Whether leaving the page asks through the browser's "leave site?" box: there is unsaved work,
|
||||
/// and either the user still wants to be asked or it isn't all kept in the browser yet (with the
|
||||
/// keep preference off it never is, so there the box always stays).
|
||||
pub fn asks_before_unload(persists: bool, keep: bool, also_ask: bool, any_unsaved: bool, all_kept: bool) -> bool {
|
||||
persists && any_unsaved && (also_ask || !(keep && all_kept))
|
||||
}
|
||||
|
||||
/// An answer in the "Restore unsaved design?" window.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Answer {
|
||||
/// Open the copy, with its unsaved changes, as the design it was (saving goes to its file).
|
||||
Restore,
|
||||
/// Open the design's file as last saved; the copy goes.
|
||||
OpenSaved,
|
||||
/// The copy goes.
|
||||
Discard,
|
||||
}
|
||||
|
||||
/// What an answer does.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Plan {
|
||||
/// Open the copy (and, once it is open, give it back its name and file).
|
||||
pub open_copy: bool,
|
||||
/// Open this saved file.
|
||||
pub open_saved: Option<String>,
|
||||
/// Remove the copy.
|
||||
pub drop_copy: bool,
|
||||
}
|
||||
|
||||
/// What `answer` does to copy `entry`; `saved_exists`: its file can still be opened. `None`: the
|
||||
/// answer isn't offered for it (Open saved version of a design never saved, or gone).
|
||||
pub fn plan(answer: Answer, entry: &Entry, saved_exists: bool) -> Option<Plan> {
|
||||
match answer {
|
||||
Answer::Restore => Some(Plan { open_copy: true, open_saved: None, drop_copy: true }),
|
||||
Answer::OpenSaved => {
|
||||
let path = entry.path.clone().filter(|_| saved_exists)?;
|
||||
Some(Plan { open_copy: false, open_saved: Some(path), drop_copy: true })
|
||||
}
|
||||
Answer::Discard => Some(Plan { open_copy: false, open_saved: None, drop_copy: true }),
|
||||
}
|
||||
}
|
||||
|
||||
/// The answers the window offers for `entry`, in order.
|
||||
pub fn answers(entry: &Entry, saved_exists: bool) -> Vec<Answer> {
|
||||
[Answer::Restore, Answer::OpenSaved, Answer::Discard].into_iter().filter(|a| plan(*a, entry, saved_exists).is_some()).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn entry(id: &str, changed_at: f64, path: Option<&str>) -> Entry {
|
||||
Entry { id: id.into(), name: "Bracket".into(), path: path.map(str::to_string), changed_at }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_write_waits_before_the_next_try() {
|
||||
assert!(may_write(1000.0, None));
|
||||
assert!(!may_write(1000.0 + RETRY_MS - 1.0, Some(1000.0)));
|
||||
assert!(may_write(1000.0 + RETRY_MS, Some(1000.0)));
|
||||
// A clock that went back doesn't block for good.
|
||||
assert!(may_write(10.0, Some(1000.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_design_counts_as_opened_only_at_its_own_path() {
|
||||
assert!(is_open(Some("a.sc"), "a.sc"));
|
||||
assert!(!is_open(Some("b.sc"), "a.sc"));
|
||||
assert!(!is_open(None, "a.sc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_are_named_by_page_and_design_and_nothing_else_reads_as_one() {
|
||||
let page = page_id(1_700_000_000_000.0, 0.5);
|
||||
assert!(page.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
let id = entry_id(&page, 7);
|
||||
assert!(valid_id(&id));
|
||||
assert_eq!(page_of(&id), Some(page.as_str()));
|
||||
assert_eq!(parse_name(&design_file(&id)), Some((id.as_str(), Part::Design)));
|
||||
assert_eq!(parse_name(&meta_file(&id)), Some((id.as_str(), Part::Meta)));
|
||||
for other in ["part.sc", "recovery.sc", "recovery.json", "keep-.sc", "keep-x.sc", "keep-a/b-1.sc", "keep-a-1.step", "keep-a-1sc"] {
|
||||
assert_eq!(parse_name(other), None, "{other}");
|
||||
}
|
||||
// Hostile clocks still give a usable id.
|
||||
for (now, random) in [(f64::NAN, f64::NAN), (f64::INFINITY, -1.0), (-5.0, 9.0)] {
|
||||
assert!(valid_id(&entry_id(&page_id(now, random), 1)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_round_trips_and_hostile_metadata_is_no_copy() {
|
||||
let e = entry("a1-3", 1234.0, Some("/opfs/Bracket.sc"));
|
||||
assert_eq!(Entry::parse("a1-3", e.to_json().as_bytes()), Some(e.clone()));
|
||||
let never_saved = entry("a1-4", 99.0, None);
|
||||
assert_eq!(Entry::parse("a1-4", never_saved.to_json().as_bytes()), Some(never_saved));
|
||||
for bad in [
|
||||
"",
|
||||
"[]",
|
||||
r#"{"name":"x","changed_at":"soon"}"#,
|
||||
r#"{"name":"x","changed_at":-1}"#,
|
||||
r#"{"name":"x","changed_at":1e999}"#,
|
||||
r#"{"name":"x","path":7,"changed_at":1}"#,
|
||||
r#"{"name":"x","path":"","changed_at":1}"#,
|
||||
r#"{"changed_at":1}"#,
|
||||
] {
|
||||
assert_eq!(Entry::parse("a1-3", bad.as_bytes()), None, "{bad}");
|
||||
}
|
||||
assert_eq!(Entry::parse("../x", e.to_json().as_bytes()), None);
|
||||
assert_eq!(Entry::parse("a1-3", &vec![b' '; MAX_META_BYTES + 1]), None);
|
||||
let long = json!({"name": "n".repeat(10_000), "changed_at": 1}).to_string();
|
||||
assert_eq!(Entry::parse("a1-3", long.as_bytes()).map(|e| e.name.len()), Some(MAX_NAME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_copy_newer_than_the_last_save_is_offered_an_older_one_dropped_an_open_pages_left() {
|
||||
let e = entry("p1-1", 2000.0, Some("/opfs/Bracket.sc"));
|
||||
// Changed after the file was last written: offered.
|
||||
assert_eq!(fate(&e, &[], Some(1000.0)), Fate::Offer);
|
||||
// Saved since (in this tab or another): nothing to offer.
|
||||
assert_eq!(fate(&e, &[], Some(2000.0)), Fate::Drop);
|
||||
assert_eq!(fate(&e, &[], Some(3000.0)), Fate::Drop);
|
||||
// Never saved, or the save time can't be read: kept rather than lost.
|
||||
for saved in [None, Some(0.0), Some(f64::NAN), Some(f64::INFINITY), Some(-4.0)] {
|
||||
assert_eq!(fate(&e, &[], saved), Fate::Offer, "{saved:?}");
|
||||
}
|
||||
// Its page is still open in another tab: neither offered nor removed.
|
||||
assert_eq!(fate(&e, &["p1".into()], Some(1000.0)), Fate::Leave);
|
||||
assert_eq!(fate(&e, &["p1".into()], Some(3000.0)), Fate::Leave);
|
||||
assert_eq!(fate(&e, &["p2".into()], Some(1000.0)), Fate::Offer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_newest_copies_are_offered_first_and_at_most_a_windowful() {
|
||||
let mut list: Vec<Entry> = (0..40).map(|i| entry(&format!("p-{i}"), f64::from(i), None)).collect();
|
||||
order_offered(&mut list);
|
||||
assert_eq!(list.len(), MAX_OFFERED);
|
||||
assert_eq!(list.first().map(|e| e.id.as_str()), Some("p-39"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_copy_is_written_once_the_design_is_still_or_after_the_longest_wait() {
|
||||
// Still changing, not waited long: not yet.
|
||||
assert!(!due(1000.0, 900.0, 900.0));
|
||||
// Still for QUIET_MS: due.
|
||||
assert!(due(900.0 + QUIET_MS, 900.0, 900.0));
|
||||
// Changing all the time: due after MAX_WAIT_MS anyway.
|
||||
assert!(!due(5000.0, 2000.0, 4900.0));
|
||||
assert!(due(2000.0 + MAX_WAIT_MS, 2000.0, 2000.0 + MAX_WAIT_MS - 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_browser_asks_before_unload_unless_told_not_to_and_every_copy_is_current() {
|
||||
// Keeping off: exactly as before, unsaved work asks, whatever the second option says.
|
||||
assert!(asks_before_unload(true, false, true, true, true));
|
||||
assert!(asks_before_unload(true, false, true, true, false));
|
||||
assert!(asks_before_unload(true, false, false, true, true));
|
||||
// Keeping on, asking on (the default): both, the box and the copy.
|
||||
assert!(asks_before_unload(true, true, true, true, true));
|
||||
// Keeping on, asking off, every unsaved design kept: no browser box.
|
||||
assert!(!asks_before_unload(true, true, false, true, true));
|
||||
// Keeping on, asking off, a copy still being written (or failed): the browser still asks.
|
||||
assert!(asks_before_unload(true, true, false, true, false));
|
||||
// Nothing unsaved, or an embedded page that keeps nothing: never.
|
||||
assert!(!asks_before_unload(true, true, true, false, false));
|
||||
assert!(!asks_before_unload(true, false, true, false, false));
|
||||
assert!(!asks_before_unload(false, false, true, true, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_open_saved_and_discard_each_do_one_thing_and_always_remove_the_copy() {
|
||||
let saved = entry("p-1", 5.0, Some("/opfs/Bracket.sc"));
|
||||
assert_eq!(plan(Answer::Restore, &saved, true), Some(Plan { open_copy: true, open_saved: None, drop_copy: true }));
|
||||
assert_eq!(
|
||||
plan(Answer::OpenSaved, &saved, true),
|
||||
Some(Plan { open_copy: false, open_saved: Some("/opfs/Bracket.sc".into()), drop_copy: true })
|
||||
);
|
||||
assert_eq!(plan(Answer::Discard, &saved, true), Some(Plan { open_copy: false, open_saved: None, drop_copy: true }));
|
||||
assert_eq!(answers(&saved, true), [Answer::Restore, Answer::OpenSaved, Answer::Discard]);
|
||||
// A design never saved, or whose file is gone, has no saved version to open.
|
||||
let never = entry("p-2", 5.0, None);
|
||||
assert_eq!(plan(Answer::OpenSaved, &never, true), None);
|
||||
assert_eq!(plan(Answer::OpenSaved, &saved, false), None);
|
||||
assert_eq!(answers(&never, false), [Answer::Restore, Answer::Discard]);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
//! Runs the same [`solidcraft_ui_egui::SolveApp`] as the desktop app through eframe's web runner
|
||||
//! (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release` from this
|
||||
//! directory (output in `dist/web`). URL flags: `?webgl` forces WebGL2 (remembered until
|
||||
//! `?webgpu`), `?sample` opens the sample part, `?embed=editor` runs it as an editing embed in a
|
||||
//! host page (no application bar or start page; nothing kept in the browser; #8517).
|
||||
//! `?webgpu`), `?sample` opens the sample part.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -19,8 +18,6 @@ mod easyprint;
|
||||
mod files;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod install;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod keep;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod linked;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
@@ -45,8 +42,6 @@ fn query_flag(query: &str, name: &str) -> bool {
|
||||
struct Launch {
|
||||
open: Option<String>,
|
||||
embed: bool,
|
||||
/// `?embed=editor` (#8517): an editing embed in a host page that keeps the files itself.
|
||||
editor: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
@@ -57,19 +52,16 @@ impl Launch {
|
||||
for (key, value) in url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"open" if launch.open.is_none() => launch.open = Some(value.into_owned()),
|
||||
"embed" if value == "editor" => launch.editor = true,
|
||||
"embed" => embed = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
launch.embed = embed && launch.open.is_some() && !launch.editor;
|
||||
launch.embed = embed && launch.open.is_some();
|
||||
launch
|
||||
}
|
||||
|
||||
/// The browser's own storage (designs, autosave, recovery, preferences) is used: not when
|
||||
/// embedded, where the host keeps what is kept.
|
||||
fn persists(&self) -> bool {
|
||||
!self.embed && !self.editor
|
||||
!self.embed
|
||||
}
|
||||
|
||||
fn full_app_url(&self, mut url: url::Url) -> String {
|
||||
@@ -186,7 +178,6 @@ mod web {
|
||||
use crate::api::Api;
|
||||
use crate::disk::{self, SaveTarget};
|
||||
use crate::files::{self, Files, Intent};
|
||||
use crate::keep;
|
||||
use crate::space_mouse::SpaceMouse;
|
||||
|
||||
const CANVAS_ID: &str = "solidcraft_canvas";
|
||||
@@ -246,9 +237,7 @@ mod web {
|
||||
}
|
||||
|
||||
/// The browser's "leave site? changes you made may not be saved" prompt while a design has
|
||||
/// unsaved changes (the page can't show its own dialog when it is being closed). With "keep
|
||||
/// unsaved work in this browser" on and "still ask" off, there is nothing to ask once every
|
||||
/// unsaved design's copy is current (`keep::asks_before_unload`, #10220).
|
||||
/// unsaved changes (the page can't show its own dialog when it is being closed).
|
||||
fn ask_before_unload(window: &web_sys::Window) {
|
||||
let on_unload = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::BeforeUnloadEvent)>::new(|e: web_sys::BeforeUnloadEvent| {
|
||||
if UNSAVED.with(std::cell::Cell::get) {
|
||||
@@ -261,50 +250,6 @@ mod web {
|
||||
on_unload.forget();
|
||||
}
|
||||
|
||||
/// With "keep unsaved work in this browser" on, the page brings every unsaved design's copy up
|
||||
/// to date when it is hidden or goes (`pagehide`, `visibilitychange`): the browser may not
|
||||
/// wait for that write, so the copies are written shortly after each change as well
|
||||
/// (`Shell::keep_tick`, #10220).
|
||||
fn keep_when_hidden(window: &web_sys::Window, runner: &eframe::WebRunner) {
|
||||
let keep_now = {
|
||||
let runner = runner.clone();
|
||||
move || {
|
||||
if let Some(mut shell) = runner.app_mut::<Shell>() {
|
||||
shell.keep_tick(None, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
let on_hide = {
|
||||
let keep_now = keep_now.clone();
|
||||
wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::Event)>::new(move |_: web_sys::Event| keep_now())
|
||||
};
|
||||
let on_visibility = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::Event)>::new(move |_: web_sys::Event| {
|
||||
let hidden = web_sys::window()
|
||||
.and_then(|w| w.document())
|
||||
.and_then(|d| js_sys::Reflect::get(&d, &"visibilityState".into()).ok())
|
||||
.and_then(|v| v.as_string())
|
||||
.is_some_and(|v| v == "hidden");
|
||||
if hidden {
|
||||
keep_now();
|
||||
}
|
||||
});
|
||||
let _ = window.add_event_listener_with_callback("pagehide", on_hide.as_ref().unchecked_ref());
|
||||
if let Some(document) = window.document() {
|
||||
let _ = document.add_event_listener_with_callback("visibilitychange", on_visibility.as_ref().unchecked_ref());
|
||||
}
|
||||
// The listeners live as long as the page.
|
||||
on_hide.forget();
|
||||
on_visibility.forget();
|
||||
}
|
||||
|
||||
/// One open design followed for its kept copy: its revision, when that last changed, and since
|
||||
/// when its copy has lacked a change (`None`: current, or being written).
|
||||
struct Track {
|
||||
revision: u64,
|
||||
last_change: f64,
|
||||
first_unwritten: Option<f64>,
|
||||
}
|
||||
|
||||
/// eframe cancels the browser's action for every Tab it gets, so the browser couldn't switch
|
||||
/// tabs while the app has the keyboard. Its tab-switching keys stop here, on the way down to
|
||||
/// the canvas, before eframe sees them; the browser then acts on them as usual.
|
||||
@@ -377,10 +322,6 @@ mod web {
|
||||
/// The settings this tab wrote or loaded last, and those another tab wrote since (#8644).
|
||||
prefs_writer: crate::PrefsWriter,
|
||||
other_tab_prefs: PrefsInbox,
|
||||
/// The open designs followed for their kept copy, by `Session::uid` (#10220).
|
||||
keep_track: std::collections::HashMap<u64, Track>,
|
||||
/// Every unsaved design's kept copy is current (and the preference is on).
|
||||
all_kept: bool,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
@@ -495,14 +436,8 @@ mod web {
|
||||
self.sync_recents();
|
||||
}
|
||||
let now = ctx.input(|i| i.time);
|
||||
// With "keep unsaved work in this browser" on, each design's own copy takes over from
|
||||
// the single autosave (#10220).
|
||||
if self.app.preferences.keep_unsaved_in_browser && self.autosaved.is_some() {
|
||||
files::autosave(&self.files, None);
|
||||
self.autosaved = None;
|
||||
}
|
||||
// Unsaved work is kept (at most every AUTOSAVE_S); saved work needs no autosave.
|
||||
if self.files.ready.get() && self.files.recovery.borrow().is_none() && !self.app.preferences.keep_unsaved_in_browser {
|
||||
if self.files.ready.get() && self.files.recovery.borrow().is_none() {
|
||||
let s = &self.app.session;
|
||||
let changed = s.is_dirty() && self.autosaved != Some(s.revision);
|
||||
let wait = AUTOSAVE_S - (now - self.autosaved_at);
|
||||
@@ -521,175 +456,6 @@ mod web {
|
||||
if !self.files.ready.get() || !self.files.incoming.borrow().is_empty() {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
}
|
||||
self.all_kept = self.keep_tick(Some(ctx), false);
|
||||
}
|
||||
|
||||
/// Keep each open design's unsaved changes in the browser, when the preference asks for
|
||||
/// it: a design's copy is written once it has been still a moment (`keep::due`), or at
|
||||
/// once with `now` (the page is being hidden); a copy whose design is saved or closed, or
|
||||
/// with the preference off, goes. Answers whether every unsaved design's copy is current.
|
||||
fn keep_tick(&mut self, ctx: Option<&egui::Context>, now: bool) -> bool {
|
||||
if !self.launch.persists() || !self.files.ready.get() {
|
||||
return false;
|
||||
}
|
||||
let Shell { app, files, keep_track, .. } = self;
|
||||
let on = app.preferences.keep_unsaved_in_browser;
|
||||
let time = js_sys::Date::now();
|
||||
let page = files.keep_page.borrow().clone();
|
||||
let mut wanted = std::collections::HashSet::new();
|
||||
let mut all_kept = true;
|
||||
let mut wake: Option<f64> = None;
|
||||
let mut error = None;
|
||||
let mut soon = |ms: f64| wake = Some(wake.map_or(ms, |w: f64| w.min(ms)));
|
||||
if on {
|
||||
for s in solidcraft_ui_egui::documents::open_sessions(app).into_iter().filter(|s| s.is_dirty()) {
|
||||
let id = keep::entry_id(&page, s.uid);
|
||||
wanted.insert(s.uid);
|
||||
let t = keep_track.entry(s.uid).or_insert(Track { revision: s.revision, last_change: time, first_unwritten: None });
|
||||
if t.revision != s.revision {
|
||||
t.revision = s.revision;
|
||||
t.last_change = time;
|
||||
}
|
||||
if files.kept.borrow().get(&id).copied().flatten() == Some(s.revision) {
|
||||
t.first_unwritten = None;
|
||||
continue;
|
||||
}
|
||||
all_kept = false;
|
||||
if files.keeping.borrow().contains(&id) {
|
||||
soon(100.0);
|
||||
continue;
|
||||
}
|
||||
let first = *t.first_unwritten.get_or_insert(time);
|
||||
let failed = files.kept_failed.borrow().get(&id).copied();
|
||||
if !keep::may_write(time, failed) {
|
||||
// The last write failed: try again after a while.
|
||||
soon(keep::RETRY_MS);
|
||||
continue;
|
||||
}
|
||||
if now || keep::due(time, first, t.last_change) {
|
||||
match solidcraft_engine::io::write_design_container(&s.design()) {
|
||||
Ok(bytes) => {
|
||||
let entry = keep::Entry { id, name: s.doc.name.clone(), path: s.path.clone(), changed_at: t.last_change };
|
||||
files::write_kept(files, &entry, bytes, s.revision);
|
||||
t.first_unwritten = None;
|
||||
}
|
||||
Err(e) => {
|
||||
files.kept_failed.borrow_mut().insert(id, time);
|
||||
error = Some(format!("keeping unsaved work in this browser: {e}"));
|
||||
}
|
||||
}
|
||||
soon(100.0);
|
||||
} else {
|
||||
soon((keep::QUIET_MS - (time - t.last_change)).min(keep::MAX_WAIT_MS - (time - first)).max(16.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
// This page's copies no design needs any more: saved, closed, or the preference off.
|
||||
let stale: Vec<String> = files
|
||||
.kept
|
||||
.borrow()
|
||||
.keys()
|
||||
.filter(|id| keep::page_of(id) == Some(page.as_str()))
|
||||
.filter(|id| !wanted.iter().any(|uid| keep::entry_id(&page, *uid) == **id))
|
||||
.cloned()
|
||||
.collect();
|
||||
for id in stale {
|
||||
files::remove_kept(files, &id);
|
||||
soon(100.0);
|
||||
}
|
||||
keep_track.retain(|uid, _| wanted.contains(uid));
|
||||
if let Some(e) = error {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
if let (Some(ctx), Some(ms)) = (ctx, wake) {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(ms / 1000.0));
|
||||
}
|
||||
on && all_kept
|
||||
}
|
||||
|
||||
/// Offer the copies an earlier visit kept (#10220): "Restore unsaved design: <name>,
|
||||
/// changed <time>?", one window for all of them, each with Restore, Open saved version
|
||||
/// (when it has a file) and Discard.
|
||||
fn keep_prompt(&mut self, ctx: &egui::Context) {
|
||||
let offered = self.files.keep_offered.borrow().clone();
|
||||
if offered.is_empty() || !self.files.ready.get() {
|
||||
return;
|
||||
}
|
||||
let now = (js_sys::Date::now() / 1000.0) as u64;
|
||||
let rows: Vec<_> = offered
|
||||
.iter()
|
||||
.map(|e| solidcraft_ui_egui::recovery_ui::BrowserCopy {
|
||||
name: &e.name,
|
||||
when: solidcraft_ui_egui::recovery_ui::ago_between(now, (e.changed_at / 1000.0) as u64),
|
||||
open_saved: keep::answers(e, self.saved_exists(e)).contains(&keep::Answer::OpenSaved),
|
||||
})
|
||||
.collect();
|
||||
let act = solidcraft_ui_egui::recovery_ui::browser_copies_window(ctx, &self.app.preferences.language, &rows);
|
||||
let Some((index, answer)) = act else { return };
|
||||
let Some(entry) = offered.get(index) else { return };
|
||||
let answer = match answer {
|
||||
solidcraft_ui_egui::recovery_ui::CopyAnswer::Restore => keep::Answer::Restore,
|
||||
solidcraft_ui_egui::recovery_ui::CopyAnswer::OpenSaved => keep::Answer::OpenSaved,
|
||||
solidcraft_ui_egui::recovery_ui::CopyAnswer::Discard => keep::Answer::Discard,
|
||||
};
|
||||
self.keep_answer(entry, answer);
|
||||
}
|
||||
|
||||
/// Whether a kept copy's design file can still be opened: in the browser's storage, or a
|
||||
/// file on the computer this browser remembers.
|
||||
fn saved_exists(&self, e: &keep::Entry) -> bool {
|
||||
e.path.as_deref().is_some_and(|p| vfs::exists(p) || disk::parse(p).is_some_and(|(id, _)| self.files.disk_known.borrow().contains(id)))
|
||||
}
|
||||
|
||||
/// Act on an answer in the "Restore unsaved design?" window (`keep::plan`).
|
||||
fn keep_answer(&mut self, entry: &keep::Entry, answer: keep::Answer) {
|
||||
let Some(plan) = keep::plan(answer, entry, self.saved_exists(entry)) else { return };
|
||||
if plan.open_copy {
|
||||
let copy = match files::prepare_kept(&entry.id, entry.path.as_deref()) {
|
||||
Ok(copy) => copy,
|
||||
Err(error) => {
|
||||
self.app.set_status(error, true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.app.open_path(©);
|
||||
if self.app.session.path.as_deref() != Some(copy.as_str()) {
|
||||
// Not opened (the status says why): it stays offered.
|
||||
return;
|
||||
}
|
||||
// It is its own design again (saving goes to its file), with its changes unsaved.
|
||||
self.app.session.path = entry.path.clone();
|
||||
self.app.session.doc_mut().name = entry.name.clone();
|
||||
self.app.session.mark_unsaved();
|
||||
self.app.home.open = false;
|
||||
}
|
||||
if let Some(path) = &plan.open_saved {
|
||||
if vfs::exists(path) {
|
||||
self.app.open_path(path);
|
||||
if !keep::is_open(self.app.session.path.as_deref(), path) {
|
||||
// Not opened (the status says why): the copy stays offered.
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// A file on the computer: the browser asks for access again, in this click.
|
||||
// The copy goes once the file has opened, not before: if access is refused,
|
||||
// the picker is dismissed or the file is gone, it stays offered.
|
||||
let (files, id) = (self.files.clone(), entry.id.clone());
|
||||
disk::restore(
|
||||
&self.files,
|
||||
path,
|
||||
Some(Box::new(move || {
|
||||
files.keep_offered.borrow_mut().retain(|e| e.id != id);
|
||||
files::remove_kept(&files, &id);
|
||||
})),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if plan.drop_copy {
|
||||
self.files.keep_offered.borrow_mut().retain(|e| e.id != entry.id);
|
||||
files::remove_kept(&self.files, &entry.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// The start page's recent designs are the stored ones: the remembered order first (those
|
||||
@@ -926,13 +692,6 @@ mod web {
|
||||
files::autosave(&self.files, None);
|
||||
}
|
||||
|
||||
/// Whether leaving the page asks through the browser's own box (`keep::asks_before_unload`).
|
||||
fn asks_before_unload(&self) -> bool {
|
||||
let any = solidcraft_ui_egui::documents::any_dirty(&self.app);
|
||||
let prefs = &self.app.preferences;
|
||||
keep::asks_before_unload(self.launch.persists(), prefs.keep_unsaved_in_browser, prefs.keep_unsaved_also_asks, any, self.all_kept)
|
||||
}
|
||||
|
||||
/// Whether the shell already has the on-demand face the live language needs.
|
||||
fn script_font_ready(&self) -> bool {
|
||||
match solidcraft_ui_egui::fonts::web_script_font_kind(solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language)) {
|
||||
@@ -1044,7 +803,7 @@ mod web {
|
||||
self.api.poll();
|
||||
self.space_mouse.logic(&mut self.app, ctx);
|
||||
self.files_tick(ctx);
|
||||
UNSAVED.with(|u| u.set(self.asks_before_unload()));
|
||||
UNSAVED.with(|u| u.set(solidcraft_ui_egui::documents::any_dirty(&self.app)));
|
||||
DESIGN_SHOWN.with(|d| d.set(solidcraft_ui_egui::toolbar::design_shown(&self.app)));
|
||||
}
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
@@ -1064,7 +823,7 @@ mod web {
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.linked.tick(&mut self.app, ui.ctx());
|
||||
UNSAVED.with(|u| u.set(self.asks_before_unload()));
|
||||
UNSAVED.with(|u| u.set(self.launch.persists() && solidcraft_ui_egui::documents::any_dirty(&self.app)));
|
||||
if self.launch.embed {
|
||||
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
|
||||
@@ -1079,15 +838,10 @@ mod web {
|
||||
if self.launch.persists() {
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
self.keep_prompt(&ctx);
|
||||
}
|
||||
self.watch_text_for_font(&ctx);
|
||||
self.store_prefs(&ctx);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
||||
Some(&mut *self)
|
||||
}
|
||||
fn persist_egui_memory(&self) -> bool {
|
||||
// eframe keeps this under one fixed key: only stable uses it, so a pre-release never rewrites it.
|
||||
self.launch.persists() && crate::channel::current().is_empty()
|
||||
@@ -1162,8 +916,7 @@ mod web {
|
||||
{
|
||||
create.instance_descriptor.backends = eframe::wgpu::Backends::GL;
|
||||
}
|
||||
let runner = eframe::WebRunner::new();
|
||||
let result = runner
|
||||
let result = eframe::WebRunner::new()
|
||||
.start(
|
||||
canvas,
|
||||
options,
|
||||
@@ -1175,7 +928,7 @@ mod web {
|
||||
}
|
||||
let (panel, opener, saver, shown) = (files.clone(), files.clone(), files.clone(), files.clone());
|
||||
let (restorable, restorer) = (files.clone(), files.clone());
|
||||
let services = if !launch.persists() { Services::default() } else { Services {
|
||||
let services = if launch.embed { Services::default() } else { Services {
|
||||
// Open shows the browser's open dialog for a design where it has one
|
||||
// (the file keeps its handle, so Save writes back to it), else the
|
||||
// browser's files window. A command that reads another kind of file
|
||||
@@ -1207,7 +960,7 @@ mod web {
|
||||
can_restore: Some(Box::new(move |path: &str| {
|
||||
disk::parse(path).is_some_and(|(id, _)| restorable.disk_known.borrow().contains(id))
|
||||
})),
|
||||
restore_file: Some(Box::new(move |path: &str| disk::restore(&restorer, path, None))),
|
||||
restore_file: Some(Box::new(move |path: &str| disk::restore(&restorer, path))),
|
||||
download_design: Some(Box::new(files::download)),
|
||||
easyprint_upload: Some(std::sync::Arc::new(crate::easyprint::upload)),
|
||||
browser_files: Some(Box::new(move || shown.panel.set(true))),
|
||||
@@ -1219,7 +972,6 @@ mod web {
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
|
||||
app.embedded = launch.embed;
|
||||
app.embed_editor = launch.editor;
|
||||
// Listening before reading: settings another tab writes from now on are
|
||||
// newer than those read here.
|
||||
let other_tab_prefs = PrefsInbox::default();
|
||||
@@ -1274,18 +1026,10 @@ mod web {
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
|
||||
prefs_writer: crate::PrefsWriter::default(),
|
||||
other_tab_prefs,
|
||||
keep_track: Default::default(),
|
||||
all_kept: false,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
if result.is_ok()
|
||||
&& crate::Launch::parse(&query()).persists()
|
||||
&& let Some(window) = web_sys::window()
|
||||
{
|
||||
keep_when_hidden(&window, &runner);
|
||||
}
|
||||
if let Some(el) = document.get_element_by_id(LOADING_ID) {
|
||||
match result {
|
||||
Ok(()) => dismiss_loading(el),
|
||||
@@ -1338,17 +1082,6 @@ mod tests {
|
||||
assert!(full.fragment().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embed_editor_needs_no_design_link_and_keeps_nothing_in_the_browser() {
|
||||
let editor = Launch::parse("?embed=editor");
|
||||
assert!(editor.editor && !editor.embed && !editor.persists());
|
||||
let linked = Launch::parse("?embed=editor&open=https://example.org/a.sc");
|
||||
assert!(linked.editor && !linked.embed, "an editing embed may open a linked design, still editable");
|
||||
let viewer = Launch::parse("?embed=viewer&open=https://example.org/a.sc");
|
||||
assert!(viewer.embed && !viewer.editor, "any other value is the viewer embed, as before");
|
||||
assert!(!Launch::parse("?other=editor").editor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_links_refuse_hostile_schemes_credentials_and_sizes() {
|
||||
assert!(design_url("https://example.org/arm.solidcraft?download=1", "http://solidcraft.io").is_ok());
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
//! Is the file chooser that opens the desktop's own? (#10159)
|
||||
//!
|
||||
//! `rfd` opens the file chooser through the XDG desktop portal; without the portal it falls
|
||||
//! back to zenity, a window that does not belong to the app and that the app never mentions.
|
||||
//! The desktop's own chooser is the portal one, so the app has to know when it is not the one
|
||||
//! that opens, and say so in the session log.
|
||||
//!
|
||||
//! The session bus is asked once, on a thread of its own: a bus that answers late must not
|
||||
//! hold the window. What it answered is kept, and read when a file is opened.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
|
||||
/// The portal's bus name: the desktop's own file chooser lives behind it.
|
||||
const PORTAL: &str = "org.freedesktop.portal.Desktop";
|
||||
|
||||
/// The bus has not answered yet.
|
||||
const UNKNOWN: u8 = 0;
|
||||
/// There is no session bus to reach the portal on.
|
||||
const NO_SESSION_BUS: u8 = 1;
|
||||
/// The bus is there but the portal does not answer on it.
|
||||
const NO_PORTAL_SERVICE: u8 = 2;
|
||||
/// The bus is there and the portal answers on it.
|
||||
const PORTAL_THERE: u8 = 3;
|
||||
|
||||
/// What the session bus said about the portal.
|
||||
static BUS: AtomicU8 = AtomicU8::new(UNKNOWN);
|
||||
/// A file chooser is opening: whether it is the desktop's own is to be said.
|
||||
static OPENING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Ask the session bus, on a thread of its own, whether the portal is there.
|
||||
pub fn start(ctx: &egui::Context) {
|
||||
let thread_ctx = ctx.clone();
|
||||
let probe = move || {
|
||||
let answer = match session_bus() {
|
||||
None => NO_SESSION_BUS,
|
||||
Some(connection) => match portal_on(&connection) {
|
||||
true => PORTAL_THERE,
|
||||
false => NO_PORTAL_SERVICE,
|
||||
},
|
||||
};
|
||||
BUS.store(answer, Ordering::Relaxed);
|
||||
// The answer is in: a file chooser waiting for it can be told now.
|
||||
thread_ctx.request_repaint();
|
||||
};
|
||||
if let Err(e) = std::thread::Builder::new().name("desktop-chooser".into()).spawn(probe) {
|
||||
log::warn!("file chooser: no thread to ask the session bus: {e}");
|
||||
// Nothing will answer: the desktop's own chooser cannot be told apart from the start.
|
||||
BUS.store(PORTAL_THERE, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// The session bus, when there is one to talk to.
|
||||
fn session_bus() -> Option<zbus::blocking::Connection> {
|
||||
zbus::blocking::Connection::session().ok()
|
||||
}
|
||||
|
||||
/// Whether the portal answers on the bus: it is either running on it or waiting to be started.
|
||||
fn portal_on(connection: &zbus::blocking::Connection) -> bool {
|
||||
let Ok(bus) = zbus::blocking::fdo::DBusProxy::new(connection) else { return false };
|
||||
let named = |names: Vec<zbus::names::OwnedBusName>| names.iter().any(|name| name.as_str() == PORTAL);
|
||||
// A portal that is running is on the bus; one that is not running yet is still there when
|
||||
// the bus can start it, which is what the portal's service file is for.
|
||||
match (bus.list_names().map(named), bus.list_activatable_names().map(named)) {
|
||||
(Ok(running), Ok(startable)) => running || startable,
|
||||
(Ok(running), Err(_)) => running,
|
||||
(Err(_), Ok(startable)) => startable,
|
||||
// The bus does not answer: nothing answers on it.
|
||||
(Err(_), Err(_)) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// A file chooser is opening: ask for a frame, so that the reason it is not the desktop's own
|
||||
/// is said (#10159).
|
||||
pub fn note_opening(ctx: &egui::Context) {
|
||||
OPENING.store(true, Ordering::Relaxed);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// The reason the file chooser that is opening is not the desktop's own, if it is not.
|
||||
///
|
||||
/// The decision is the one the test states: without a session bus there is no bus to reach the
|
||||
/// portal on; with a bus but no portal on it, `rfd` falls back to zenity; and under XWayland the
|
||||
/// window is an X11 one while the session is a Wayland one, which the portal refuses to parent
|
||||
/// its chooser to.
|
||||
pub fn take_reason() -> Option<&'static str> {
|
||||
if !OPENING.load(Ordering::Relaxed) {
|
||||
return None;
|
||||
}
|
||||
let (dbus_session, portal_service) = match BUS.load(Ordering::Relaxed) {
|
||||
NO_SESSION_BUS => (false, false),
|
||||
NO_PORTAL_SERVICE => (true, false),
|
||||
PORTAL_THERE => (true, true),
|
||||
// The bus has not answered yet: wait for it, and say nothing yet.
|
||||
_ => return None,
|
||||
};
|
||||
OPENING.store(false, Ordering::Relaxed);
|
||||
let wayland_display = std::env::var_os("WAYLAND_DISPLAY").is_some();
|
||||
let x11_display = super::window_system_now() == Some("x11");
|
||||
super::desktop_chooser_fallback(dbus_session, portal_service, wayland_display, x11_display)
|
||||
}
|
||||
@@ -76,7 +76,8 @@ fn command(
|
||||
reason: &str,
|
||||
) -> std::process::Command {
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(args).env(LOST_ENV, lost_at.to_string());
|
||||
// The notice of a hang that restarted this launch isn't the next one's (`App::watch_for_a_hang`).
|
||||
command.args(args).env(LOST_ENV, lost_at.to_string()).env_remove(startup::HUNG_ENV);
|
||||
match self::reason(Some(reason)) {
|
||||
Some(reason) => command.env(REASON_ENV, reason),
|
||||
// Not one inherited from an earlier loss.
|
||||
|
||||
@@ -23,8 +23,6 @@ mod app_icon;
|
||||
mod come_forward;
|
||||
mod control_server;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod desktop_chooser;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod desktop_theme;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
mod desktop_transparency;
|
||||
@@ -95,6 +93,12 @@ struct App {
|
||||
come_forward: come_forward::ComeForward,
|
||||
/// The graphics device: lost, it can't draw again (`App::watch_device`, #4172).
|
||||
gpu: Option<LostWatch>,
|
||||
/// The window's backend, and whether it is the software renderer.
|
||||
renderer: Option<(eframe::wgpu::Backend, bool)>,
|
||||
/// A GPU that hangs after the window is up (`App::watch_for_a_hang`).
|
||||
hang: startup::HangWatch,
|
||||
/// The GPU hung: how the next launch starts, once this one has closed.
|
||||
restart: Option<startup::Restart>,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
space_mouse: space_mouse::SpaceMouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -215,12 +219,14 @@ impl eframe::App for App {
|
||||
solidcraft_ui_egui::ai_connect::begin(&mut self.solve);
|
||||
solidcraft_ui_egui::agent_chat::begin(&mut self.solve);
|
||||
}
|
||||
startup::first_frame_drawn();
|
||||
if let Some(dir) = self.starting.take() {
|
||||
startup::started(&dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.watch_device();
|
||||
self.watch_for_a_hang(ctx);
|
||||
// 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
|
||||
@@ -244,12 +250,6 @@ impl eframe::App for App {
|
||||
menu.tick(&mut self.solve, ctx);
|
||||
}
|
||||
self.solve.logic(ctx);
|
||||
// The file chooser that opened is not the desktop's own: the user has to be told (#10159).
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(reason) = desktop_chooser::take_reason() {
|
||||
let text = self.solve.tr(reason).to_string();
|
||||
self.solve.set_status(text, false);
|
||||
}
|
||||
self.dialog_folder.design(self.solve.session.path.as_deref());
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
if !self.solve.file_picker_busy() {
|
||||
@@ -303,8 +303,6 @@ impl eframe::App for App {
|
||||
log::warn!("a picture was still being drawn as the app closed");
|
||||
}
|
||||
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
// A GPU path tracer setup still running did not stop the app (#9236).
|
||||
solidcraft_ui_egui::render_workspace::close(&mut self.solve);
|
||||
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
|
||||
// Before a restart (`Drop for App`) starts the next instance, which would otherwise get
|
||||
// nothing from the 3D mouse driver (`solidcraft_3dx_client::stop`).
|
||||
@@ -329,11 +327,22 @@ impl Drop for App {
|
||||
// The window's GPU surface is still to be dropped, and on a lost device that may
|
||||
// never return (#9044).
|
||||
device_lost::end();
|
||||
} else if let Some((restart, (backend, _))) = self.restart.zip(self.renderer) {
|
||||
restart_after_hang(restart, backend);
|
||||
} else if self.solve.graphics.restart {
|
||||
// Preferences: to try graphics settings, or to go back from them (#4015). The next
|
||||
// start has begun: this one leaves at once. Tearing down a window's swap chain, device
|
||||
// and pipelines can hang or crash in a graphics driver (#9044), on the very API that
|
||||
// the user is leaving, and a crash here would be taken for one of the new start.
|
||||
relaunch(None);
|
||||
control_server::remove_token_file();
|
||||
device_lost::end();
|
||||
}
|
||||
// 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);
|
||||
command.args(&args);
|
||||
// This launch's notices aren't the new version's.
|
||||
command.args(&args).env_remove(startup::HUNG_ENV);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -374,6 +383,109 @@ impl App {
|
||||
gpu.reason = reason;
|
||||
self.solve.quit_requested = true;
|
||||
}
|
||||
|
||||
/// The GPU hung (frames wait out their acquire, or the surface refuses its configuration, while
|
||||
/// the user is there, #3321): close, keeping unsaved changes for recovery as a forced quit
|
||||
/// does (the window, white, can't ask about them), and start again on another renderer once
|
||||
/// closed ([`restart_after_hang`]). Windows only: elsewhere a hidden window's frames can wait
|
||||
/// as long (Wayland, macOS).
|
||||
fn watch_for_a_hang(&mut self, ctx: &egui::Context) {
|
||||
if !cfg!(target_os = "windows") {
|
||||
return;
|
||||
}
|
||||
let (now, input) = ctx.input(|i| {
|
||||
let input = i.events.iter().any(|e| {
|
||||
matches!(
|
||||
e,
|
||||
egui::Event::PointerMoved(_) | egui::Event::PointerButton { .. } | egui::Event::Key { .. } | egui::Event::MouseWheel { .. }
|
||||
)
|
||||
});
|
||||
(i.time, input)
|
||||
});
|
||||
let stalled = eframe::egui_wgpu::stalled_frames();
|
||||
let refused = eframe::egui_wgpu::refused_for().map(|d| d.as_secs_f64());
|
||||
if !self.hang.hung(now, input, stalled, refused) {
|
||||
return;
|
||||
}
|
||||
let why = match refused {
|
||||
Some(s) if s > 0.0 => format!("the surface refused its configuration for {s:.0} s"),
|
||||
_ => format!("{stalled} frames in a row waited out their acquire"),
|
||||
};
|
||||
let trial = self.solve.graphics.trial.is_some();
|
||||
let Some(restart) = self.renderer.and_then(|(backend, cpu)| startup::after_hang(backend, cpu, trial)) else {
|
||||
log::error!("graphics: {why}, on the software renderer");
|
||||
return;
|
||||
};
|
||||
log::error!("graphics: {why}; the GPU hung, restarting: {restart:?}");
|
||||
eprintln!("SolidCraft graphics: the GPU stopped responding; restarting ({restart:?})");
|
||||
if let startup::Restart::Backend(backend) = restart {
|
||||
remember_backend(backend);
|
||||
}
|
||||
self.restart = Some(restart);
|
||||
self.solve.quit_requested = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start SolidCraft again after the GPU hung on `hung`: on the backend [`remember_backend`]
|
||||
/// saved, or as it was (settings on trial, which the next start drops).
|
||||
fn restart_after_hang(restart: startup::Restart, hung: eframe::wgpu::Backend) {
|
||||
match restart {
|
||||
startup::Restart::Backend(_) => relaunch(Some(hung)),
|
||||
startup::Restart::Again => relaunch(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start SolidCraft again once this one has closed, with the same arguments. `hung`: the backend
|
||||
/// the GPU hung on, which the new start's notice names; `WGPU_BACKEND` is then not passed on, as
|
||||
/// it would win over the backend [`remember_backend`] saved and start the one that hung again. A
|
||||
/// start-up retry's backend never is: it would win over the saved or tried settings.
|
||||
fn relaunch(hung: Option<eframe::wgpu::Backend>) {
|
||||
let Ok(exe) = std::env::current_exe() else { return };
|
||||
let mut command = std::process::Command::new(exe);
|
||||
// This launch's notices and start-up retry aren't the next one's.
|
||||
command
|
||||
.args(std::env::args_os().skip(1))
|
||||
.env_remove(startup::HUNG_ENV)
|
||||
.env_remove(startup::RETRY_BACKEND_ENV)
|
||||
.env_remove(startup::FALLBACK_NOTE_ENV)
|
||||
.env_remove(device_lost::LOST_ENV);
|
||||
if let Some(hung) = hung {
|
||||
command.env(startup::HUNG_ENV, format!("{hung:?}")).env_remove(startup::WGPU_BACKEND_ENV);
|
||||
}
|
||||
#[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: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep `backend` as `graphics.json`'s `backend` (its other settings stay). Written to a new file
|
||||
/// that then replaces the old one: this runs as the GPU hangs, when the machine may reset next,
|
||||
/// and a half-written file would lose the anti-aliasing setting too.
|
||||
fn remember_backend(backend: &str) {
|
||||
let Some(path) = graphics_path() else { return };
|
||||
let mut graphics = graphics().filter(serde_json::Value::is_object).unwrap_or_else(|| serde_json::json!({}));
|
||||
graphics["backend"] = serde_json::json!(backend);
|
||||
if let Err(e) = write_replacing(&path, graphics.to_string().as_bytes()) {
|
||||
log::error!("graphics: keeping the {backend} backend in {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
/// Write `bytes` to `path` all at once: to a file next to it, flushed to disk, then renamed over it.
|
||||
fn write_replacing(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
|
||||
use std::io::Write;
|
||||
let mut name = path.file_name().unwrap_or_default().to_os_string();
|
||||
name.push(".new");
|
||||
let new = path.with_file_name(name);
|
||||
let mut file = std::fs::File::create(&new)?;
|
||||
file.write_all(bytes)?;
|
||||
file.sync_all()?;
|
||||
drop(file);
|
||||
std::fs::rename(&new, path)
|
||||
}
|
||||
|
||||
/// Where the next file dialog opens (#7756): the folder of the file last chosen in one, or of the
|
||||
@@ -481,19 +593,12 @@ fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom:
|
||||
|
||||
fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services {
|
||||
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
|
||||
// The file chooser that opens may not be the desktop's own: the user is told when it does
|
||||
// not (#10159).
|
||||
#[cfg(target_os = "linux")]
|
||||
let chooser_ctx = cc.egui_ctx.clone();
|
||||
let open_parent = parent.clone();
|
||||
let program_parent = parent.clone();
|
||||
let file_parent = parent.clone();
|
||||
let (open_folder, file_folder, save_folder) = (folder.clone(), folder.clone(), folder.clone());
|
||||
Services {
|
||||
pick_open: Some(Box::new(move |first: &[&str]| {
|
||||
// The chooser that opens may not be the desktop's own (#10159).
|
||||
#[cfg(target_os = "linux")]
|
||||
desktop_chooser::note_opening(&chooser_ctx);
|
||||
// The file type the command reads comes first, so the picker opens on it (#1801).
|
||||
let mut dialog = open_folder.start(open_parent.clone());
|
||||
if !first.is_empty() {
|
||||
@@ -802,29 +907,8 @@ fn graphics() -> Option<serde_json::Value> {
|
||||
graphics_path().and_then(|p| std::fs::read_to_string(p).ok()).and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||
}
|
||||
|
||||
fn restore_preferences(app: &mut SolveApp, stored_ui: Option<&str>, saved_graphics: Option<&serde_json::Value>, safe_mode: bool) {
|
||||
if let Some(p) = stored_ui.filter(|_| !safe_mode) {
|
||||
app.load_prefs(p);
|
||||
}
|
||||
// Even safe mode edits the real graphics.json. Keep its choices in Preferences so an
|
||||
// anti-aliasing change cannot erase them; safe mode's renderer still uses its own defaults.
|
||||
restore_graphics_preferences(app, saved_graphics);
|
||||
}
|
||||
|
||||
/// Restore startup choices after loading the separately saved UI preferences.
|
||||
fn restore_graphics_preferences(app: &mut SolveApp, saved: Option<&serde_json::Value>) {
|
||||
app.preferences.graphics_adapter = saved.and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str)).unwrap_or_default().to_string();
|
||||
// graphics.json chooses the window before app.ron is loaded. Reflect that saved choice,
|
||||
// not the current session's window system (which an environment override may change).
|
||||
app.preferences.window_system = saved
|
||||
.and_then(|v| v.get("window_system").and_then(serde_json::Value::as_str))
|
||||
.map(str::trim)
|
||||
.filter(|choice| matches!(*choice, "wayland" | "x11"))
|
||||
.unwrap_or("auto")
|
||||
.to_string();
|
||||
}
|
||||
|
||||
/// The graphics backend preference (`graphics.json`'s `backend`: `auto`, `dx12`, `vulkan`, `gl`).
|
||||
/// The graphics backend preference (`graphics.json`'s `backend`: `auto`, `dx12`, `vulkan`, `gl`,
|
||||
/// or `cpu`, the software renderer).
|
||||
fn backend_preference() -> Option<String> {
|
||||
graphics().and_then(|v| v.get("backend").and_then(serde_json::Value::as_str).map(str::to_string))
|
||||
}
|
||||
@@ -888,7 +972,7 @@ const DEPTH_BITS: u8 = 24;
|
||||
const FLOAT_DEPTH_BITS: u8 = 32;
|
||||
/// The section cap marks where it shows in the stencil.
|
||||
const STENCIL_BITS: u8 = 8;
|
||||
const MSAA: u16 = 4;
|
||||
const MSAA: u16 = startup::DEFAULT_MSAA;
|
||||
|
||||
/// `custom_titlebar`: no OS decorations, the application bar is the title bar.
|
||||
fn app_viewport(custom_titlebar: bool) -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
@@ -1009,28 +1093,6 @@ fn main() -> eframe::Result {
|
||||
safe_mode = true;
|
||||
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 {
|
||||
let reason = std::env::var(device_lost::REASON_ENV).ok();
|
||||
if let Some(reason) = device_lost::reason(reason.as_deref()) {
|
||||
log::warn!("graphics: the last launch lost its device ({reason})");
|
||||
}
|
||||
notes.push(device_lost::note(cpu_renderer, reason.as_deref()));
|
||||
}
|
||||
if safe_mode {
|
||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||
}
|
||||
let fallback_note = std::env::var(startup::FALLBACK_NOTE_ENV).ok().map(|s| s.chars().take(4096).collect::<String>());
|
||||
if let Some(note) = &fallback_note {
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
notes.push(note.clone());
|
||||
}
|
||||
let saved_samples = if safe_mode { None } else { msaa() };
|
||||
let wgpu_options = wgpu_options(cpu_renderer, if safe_mode { None } else { backend_preference() }.as_deref());
|
||||
let depth_bits = if !safe_mode && startup::float_depth_everywhere(&wgpu_options) { FLOAT_DEPTH_BITS } else { DEPTH_BITS };
|
||||
// Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md).
|
||||
let crashed = solidcraft_engine::crash::default_dir()
|
||||
.map(|dir| {
|
||||
@@ -1042,8 +1104,73 @@ fn main() -> eframe::Result {
|
||||
solidcraft_engine::crash::install(dir, build)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// The recovery folder also holds the reopen guard marker (#5064); read once, reused below.
|
||||
let recovery_dir = solidcraft_engine::recovery::default_dir();
|
||||
// The graphics API and anti-aliasing: the saved ones, or ones tried from Preferences (#4015).
|
||||
let mut graphics = startup::graphics_start(graphics().as_ref(), safe_mode);
|
||||
if let Some(file) = graphics.rewrite.take() {
|
||||
let written = graphics_path().is_some_and(|path| match write_replacing(&path, file.to_string().as_bytes()) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
log::error!("graphics: writing {}: {e}", path.display());
|
||||
false
|
||||
}
|
||||
});
|
||||
// A trial that can't be marked as started would be tried at every start.
|
||||
if !written && matches!(graphics.trial, startup::Trial::Trying(_)) {
|
||||
graphics.trial = startup::Trial::None;
|
||||
}
|
||||
}
|
||||
notes.extend(graphics.note());
|
||||
// The last launch crashed on an API Preferences warns about on this system: back to Automatic.
|
||||
let last_crashed = crashed.iter().any(|r| startup::launch_crashed(&r.kind));
|
||||
if !safe_mode
|
||||
&& graphics.trial == startup::Trial::None
|
||||
&& let Some(auto) = startup::after_crash(&graphics.saved, last_crashed, || {
|
||||
startup::apis_that_freeze(cfg!(target_os = "windows"), startup::hardware_adapters_once)
|
||||
})
|
||||
{
|
||||
notes.push(startup::after_crash_note(&graphics.saved.backend));
|
||||
remember_backend(&auto.backend);
|
||||
graphics.saved = auto;
|
||||
}
|
||||
let current = graphics.current().clone();
|
||||
let trying = matches!(graphics.trial, startup::Trial::Trying(_));
|
||||
// The software renderer: safe mode, SOLIDCRAFT_RENDERER, or kept after the GPU hung on every
|
||||
// API (`startup::after_hang`).
|
||||
let cpu_renderer = safe_mode
|
||||
|| startup::cpu_renderer_env(std::env::var(startup::RENDERER_ENV).ok().as_deref())
|
||||
|| startup::cpu_renderer_env(Some(¤t.backend));
|
||||
// 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 {
|
||||
let reason = std::env::var(device_lost::REASON_ENV).ok();
|
||||
if let Some(reason) = device_lost::reason(reason.as_deref()) {
|
||||
log::warn!("graphics: the last launch lost its device ({reason})");
|
||||
}
|
||||
notes.push(device_lost::note(cpu_renderer, reason.as_deref()));
|
||||
}
|
||||
// The launch before this one closed because its GPU hung (`App::watch_for_a_hang`).
|
||||
if let Some(hung) = std::env::var(startup::HUNG_ENV).ok().filter(|v| !v.trim().is_empty()) {
|
||||
notes.push(startup::hang_note(&hung, safe_mode, backend_preference().as_deref()));
|
||||
}
|
||||
if safe_mode {
|
||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||
}
|
||||
let fallback_note = std::env::var(startup::FALLBACK_NOTE_ENV).ok().map(|s| s.chars().take(4096).collect::<String>());
|
||||
if let Some(note) = &fallback_note {
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
notes.push(note.clone());
|
||||
}
|
||||
// Anti-aliasing on trial is asked for; a saved one only once Graphics changed it (`msaa`).
|
||||
let saved_samples = if safe_mode {
|
||||
None
|
||||
} else if trying {
|
||||
Some(current.msaa)
|
||||
} else {
|
||||
msaa()
|
||||
};
|
||||
let wgpu_options = wgpu_options(cpu_renderer, (!safe_mode).then_some(current.backend.as_str()));
|
||||
let depth_bits = if !safe_mode && startup::float_depth_everywhere(&wgpu_options) { FLOAT_DEPTH_BITS } else { DEPTH_BITS };
|
||||
// A crash in native code (a graphics driver) says where it happened: the exception or signal
|
||||
// and the module on Windows and macOS, and on every system the startup step the app was in.
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -1115,6 +1242,15 @@ fn main() -> eframe::Result {
|
||||
#[cfg(target_os = "macos")]
|
||||
let _driver_client = DriverClient;
|
||||
mark("run_native");
|
||||
if trying {
|
||||
// A new graphics API that hangs before the window draws leaves nothing to click on.
|
||||
startup::watch_trial_start(startup::TRIAL_START_LIMIT, || {
|
||||
eprintln!("SolidCraft graphics: the settings on trial drew no window in {} s; starting again", startup::TRIAL_START_LIMIT.as_secs());
|
||||
relaunch(None);
|
||||
control_server::remove_token_file();
|
||||
device_lost::end();
|
||||
});
|
||||
}
|
||||
let result = eframe::run_native(
|
||||
"SolidCraft",
|
||||
options,
|
||||
@@ -1126,10 +1262,6 @@ fn main() -> eframe::Result {
|
||||
solidcraft_ui_egui::fonts::load_system_fonts_in_background(&cc.egui_ctx);
|
||||
mark("fonts");
|
||||
let dialog_folder = DialogFolder::default();
|
||||
// The file chooser that opens may not be the desktop's own: the session bus is asked
|
||||
// now, so the answer is there when a file is opened (#10159).
|
||||
#[cfg(target_os = "linux")]
|
||||
desktop_chooser::start(&cc.egui_ctx);
|
||||
let mut app = SolveApp::new(Session::default(), services(cc, &dialog_folder));
|
||||
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
|
||||
app.custom_titlebar = custom_titlebar;
|
||||
@@ -1150,8 +1282,9 @@ fn main() -> eframe::Result {
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.cpu_meshing = safe_mode;
|
||||
app.graphics_fallback = fallback_note.clone();
|
||||
let stored_ui = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k)));
|
||||
restore_preferences(&mut app, stored_ui.as_deref(), graphics().as_ref(), safe_mode);
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
if let Some(path) = &theme_file
|
||||
&& let Err(e) = solidcraft_ui_egui::prefs::follow_theme_file_for_run(&mut app, path)
|
||||
{
|
||||
@@ -1164,6 +1297,23 @@ fn main() -> eframe::Result {
|
||||
lost_at: None,
|
||||
reason: String::new(),
|
||||
});
|
||||
if !safe_mode {
|
||||
app.preferences.graphics_adapter =
|
||||
crate::graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string)).unwrap_or_default();
|
||||
}
|
||||
// Preferences shows the graphics settings this start uses, saved or on trial (#4015).
|
||||
app.preferences.graphics_api = current.backend.clone();
|
||||
app.preferences.msaa = u32::from(current.msaa);
|
||||
let saved = solidcraft_ui_egui::prefs::GraphicsChoice { api: graphics.saved.backend.clone(), msaa: u32::from(graphics.saved.msaa) };
|
||||
app.graphics = solidcraft_ui_egui::prefs::Graphics {
|
||||
apis: startup::GRAPHICS_APIS.to_vec(),
|
||||
freezes: startup::apis_that_freeze(cfg!(target_os = "windows"), startup::hardware_adapters_once),
|
||||
env_override: std::env::var(startup::WGPU_BACKEND_ENV).ok().filter(|v| !v.trim().is_empty()),
|
||||
trial: trying.then(|| solidcraft_ui_egui::prefs::GraphicsTrial { previous: saved.clone(), shown: 0.0, last_frame: None }),
|
||||
saved,
|
||||
restart: false,
|
||||
};
|
||||
let mut renderer = None;
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
app.services.graphics_adapters = rs
|
||||
.available_adapters
|
||||
@@ -1187,6 +1337,7 @@ fn main() -> eframe::Result {
|
||||
let scene_samples = scene_samples(info.backend, u32::from(samples), &supported);
|
||||
eprintln!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
|
||||
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
|
||||
renderer = Some((rs.adapter.get_info().backend, cpu_renderer));
|
||||
let scene_samples = app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
|
||||
app.msaa_supported = supported;
|
||||
app.preferences.msaa = u32::from(samples);
|
||||
@@ -1236,25 +1387,8 @@ fn main() -> eframe::Result {
|
||||
};
|
||||
// No design given: the designs open at the last quit, when the preference asks (#5542).
|
||||
// Not when driven over the control channel, whose clients expect a fresh design.
|
||||
// A design that stopped the last session while it was being reopened is skipped this
|
||||
// time, so one bad file cannot brick every launch (#5064).
|
||||
app.reopen_guard_dir = recovery_dir.clone();
|
||||
let files = if files.is_empty() && !sample && control_port.is_none() {
|
||||
let (reopen, skipped) = solidcraft_ui_egui::documents::reopen_plan(&app, recovery_dir.as_deref());
|
||||
if !skipped.is_empty() {
|
||||
let names: Vec<String> = skipped
|
||||
.iter()
|
||||
.map(|p| std::path::Path::new(p).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| p.clone()))
|
||||
.collect();
|
||||
notes.push(format!(
|
||||
"The last session stopped while reopening {}. It was not reopened this time; open it yourself if you need it.",
|
||||
names.join(", ")
|
||||
));
|
||||
}
|
||||
reopen
|
||||
} else {
|
||||
files
|
||||
};
|
||||
let files =
|
||||
if files.is_empty() && !sample && control_port.is_none() { solidcraft_ui_egui::documents::designs_to_reopen(&app) } else { files };
|
||||
for f in &files {
|
||||
app.open_path_async(f);
|
||||
}
|
||||
@@ -1270,9 +1404,9 @@ fn main() -> eframe::Result {
|
||||
app.home.show(rev);
|
||||
}
|
||||
// Autosave, and offer what crashed sessions left behind.
|
||||
if let Some(dir) = recovery_dir.as_ref() {
|
||||
if let Some(dir) = solidcraft_engine::recovery::default_dir() {
|
||||
let every = if app.autosave_minutes > 0.0 { app.autosave_minutes * 60.0 } else { f64::MAX / 4.0 };
|
||||
solidcraft_ui_egui::recovery_ui::start(&mut app, dir, std::time::Duration::from_secs_f64(every.min(1e9)));
|
||||
solidcraft_ui_egui::recovery_ui::start(&mut app, &dir, std::time::Duration::from_secs_f64(every.min(1e9)));
|
||||
// Safe mode keeps autosaving but offers nothing: the entries wait for a normal start.
|
||||
if safe_mode {
|
||||
app.recoverable.clear();
|
||||
@@ -1309,6 +1443,9 @@ fn main() -> eframe::Result {
|
||||
take_focus_unwanted: control_port.is_some(),
|
||||
come_forward: come_forward::ComeForward::default(),
|
||||
gpu,
|
||||
renderer,
|
||||
hang: startup::HangWatch::default(),
|
||||
restart: None,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
space_mouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -1320,7 +1457,7 @@ fn main() -> eframe::Result {
|
||||
);
|
||||
control_server::remove_token_file();
|
||||
match result {
|
||||
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, started, settings.as_deref()),
|
||||
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, started, trying, settings.as_deref()),
|
||||
r => r,
|
||||
}
|
||||
}
|
||||
@@ -1336,8 +1473,8 @@ fn startup_step(step: &'static str, detail: &str) {
|
||||
}
|
||||
|
||||
/// The renderer settings: with `cpu`, the software adapter (WARP, lavapipe, llvmpipe). The
|
||||
/// backends come from `startup::choose_backends` (on Windows D3D12 first, and only the backend
|
||||
/// in use is started); `preference` is `graphics.json`'s `backend`.
|
||||
/// backends come from `startup::choose_backends` (on Windows D3D12 first, Vulkan on AMD
|
||||
/// integrated GPUs, and only the backend in use is started); `preference` is `graphics.json`'s `backend`.
|
||||
fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuConfiguration {
|
||||
let mut options = eframe::egui_wgpu::WgpuConfiguration::default();
|
||||
if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
|
||||
@@ -1345,7 +1482,7 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
// A retry applies only to this launch. WGPU_BACKEND always has priority.
|
||||
let retry = std::env::var(startup::RETRY_BACKEND_ENV).ok();
|
||||
let preference = if cpu { preference } else { retry.as_deref().or(preference) };
|
||||
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::has_hardware_adapter) {
|
||||
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::hardware_adapters_once) {
|
||||
eprintln!("SolidCraft graphics: starting {backends:?}");
|
||||
setup.instance_descriptor.backends = backends;
|
||||
}
|
||||
@@ -1369,35 +1506,53 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
}
|
||||
|
||||
/// A failed renderer returns here before app creation. A fresh process avoids reusing winit's
|
||||
/// event loop or a failed device; each retry advances through the APIs, then software once.
|
||||
fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backends, settings: Option<&std::path::Path>) -> eframe::Result {
|
||||
/// event loop or a failed device; each retry advances through the APIs, then software once. With
|
||||
/// graphics settings on trial (`trial`), once more as usual instead: that start drops them and
|
||||
/// goes back to the saved ones.
|
||||
fn no_window(
|
||||
error: eframe::Error,
|
||||
tried_cpu: bool,
|
||||
failed: eframe::wgpu::Backends,
|
||||
trial: bool,
|
||||
settings: Option<&std::path::Path>,
|
||||
) -> eframe::Result {
|
||||
log::error!("the window could not open: {error}");
|
||||
eprintln!("SolidCraft: the window could not open: {error}");
|
||||
// The retry below runs as a child that this process waits for, for as long as it runs: not a
|
||||
// start that never draws.
|
||||
startup::first_frame_drawn();
|
||||
// This failure was handled, so neither the marker nor the journal should call it a crash.
|
||||
if let Some(dir) = settings {
|
||||
startup::started(dir);
|
||||
}
|
||||
solidcraft_engine::crash::end_session();
|
||||
let explicit = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
|
||||
if let Some(next) = startup::fallback_backend(cfg!(target_os = "windows"), explicit, tried_cpu, failed)
|
||||
let next = if trial { None } else { startup::fallback_backend(cfg!(target_os = "windows"), explicit, tried_cpu, failed) };
|
||||
if (trial || next.is_some())
|
||||
&& let Ok(exe) = std::env::current_exe()
|
||||
{
|
||||
let previous = std::env::var(startup::FALLBACK_NOTE_ENV).unwrap_or_default();
|
||||
let note = startup::fallback_note(&previous, failed, next, &error.to_string());
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(std::env::args_os().skip(1)).env(startup::FALLBACK_NOTE_ENV, note);
|
||||
if next == "cpu" {
|
||||
command.env(startup::RENDERER_ENV, "cpu").env_remove(startup::RETRY_BACKEND_ENV);
|
||||
command.args(std::env::args_os().skip(1)).env_remove(startup::HUNG_ENV);
|
||||
if let Some(next) = next {
|
||||
let previous = std::env::var(startup::FALLBACK_NOTE_ENV).unwrap_or_default();
|
||||
let note = startup::fallback_note(&previous, failed, next, &error.to_string());
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
command.env(startup::FALLBACK_NOTE_ENV, note);
|
||||
if next == "cpu" {
|
||||
command.env(startup::RENDERER_ENV, "cpu").env_remove(startup::RETRY_BACKEND_ENV);
|
||||
} else {
|
||||
command.env(startup::RETRY_BACKEND_ENV, next);
|
||||
}
|
||||
} else {
|
||||
command.env(startup::RETRY_BACKEND_ENV, next);
|
||||
eprintln!("SolidCraft: trying again with the saved graphics settings");
|
||||
command.env_remove(startup::RETRY_BACKEND_ENV).env_remove(startup::FALLBACK_NOTE_ENV);
|
||||
}
|
||||
match command.status() {
|
||||
Ok(status) if status.success() => return Ok(()),
|
||||
// The final try showed its own message.
|
||||
Ok(_) => return Err(error),
|
||||
Err(e) => log::error!("could not start again on {next}: {e}"),
|
||||
Err(e) => log::error!("could not start again on {}: {e}", next.unwrap_or("the saved graphics settings")),
|
||||
}
|
||||
}
|
||||
let message = if explicit {
|
||||
@@ -1460,47 +1615,6 @@ fn prefer_x11(display: bool, wayland_display: bool, desktop: Option<&str>, wayla
|
||||
display && !(niri && wayland_display)
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
const CHOOSER_NO_SESSION_BUS: &str = "file.open.portal_fallback.no_session_bus";
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
const CHOOSER_NO_PORTAL_SERVICE: &str = "file.open.portal_fallback.no_portal_service";
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
const CHOOSER_XWAYLAND: &str = "file.open.portal_fallback.xwayland";
|
||||
|
||||
/// Whether the chooser that opens is NOT the desktop's own, and why.
|
||||
///
|
||||
/// `rfd` opens the file chooser through the XDG desktop portal; when the portal is not there it
|
||||
/// falls back to zenity, which opens a window that does not belong to SolidCraft and that the
|
||||
/// application never mentions. The application has to know that: this decision is what the warning
|
||||
/// is built on.
|
||||
///
|
||||
/// * `dbus_session`: the session bus is there (`DBUS_SESSION_BUS_ADDRESS`).
|
||||
/// * `portal_service`: the file chooser portal answers on it.
|
||||
/// * `wayland_display`: a Wayland session runs (`WAYLAND_DISPLAY`).
|
||||
/// * `x11_display`: the window is an X11 one (`DISPLAY`, or XWayland).
|
||||
///
|
||||
/// `None` means the desktop's own chooser is the one that opens and there is nothing to say;
|
||||
/// `Some` carries the reason to report.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
fn desktop_chooser_fallback(dbus_session: bool, portal_service: bool, wayland_display: bool, x11_display: bool) -> Option<&'static str> {
|
||||
// Without a session bus there is no bus to reach the portal on, so the chooser is not the
|
||||
// desktop's own whatever else is there.
|
||||
if !dbus_session {
|
||||
return Some(CHOOSER_NO_SESSION_BUS);
|
||||
}
|
||||
// The bus is there but the portal does not answer on it: `rfd` falls back to zenity.
|
||||
if !portal_service {
|
||||
return Some(CHOOSER_NO_PORTAL_SERVICE);
|
||||
}
|
||||
// Under XWayland the window is an X11 one while the session is a Wayland one, and the portal
|
||||
// refuses to parent its chooser to an X11 window: `rfd` falls back to zenity.
|
||||
if wayland_display && x11_display {
|
||||
return Some(CHOOSER_XWAYLAND);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix, not(target_os = "macos")))]
|
||||
mod backend_tests {
|
||||
use super::prefer_x11;
|
||||
@@ -1536,30 +1650,6 @@ mod backend_tests {
|
||||
assert!(prefer_x11(true, true, Some("niri"), over(Some("x11"))));
|
||||
}
|
||||
|
||||
/// The file chooser must say when it is NOT the desktop's own: `rfd` falls back to zenity
|
||||
/// when the XDG portal is not there, and the user gets a foreign window with no explanation.
|
||||
#[test]
|
||||
fn the_file_picker_says_when_it_is_not_the_desktops_own_chooser() {
|
||||
use super::desktop_chooser_fallback;
|
||||
// The normal case: the portal is there, the desktop's own chooser opens, nothing to say.
|
||||
assert_eq!(desktop_chooser_fallback(true, true, true, false), None);
|
||||
// No session bus: the portal can not be reached at all, so `rfd` falls back to zenity.
|
||||
assert!(
|
||||
desktop_chooser_fallback(false, true, true, false).is_some(),
|
||||
"without a D-Bus session bus the file chooser is not the desktop's own: the user has to be told"
|
||||
);
|
||||
// The bus is there but the portal service is not: same fallback, same warning.
|
||||
assert!(
|
||||
desktop_chooser_fallback(true, false, true, false).is_some(),
|
||||
"without the desktop portal service the file chooser is not the desktop's own: the user has to be told"
|
||||
);
|
||||
// XWayland: the parent window is an X11 one and the portal refuses it, so `rfd` falls back.
|
||||
assert!(
|
||||
desktop_chooser_fallback(true, true, true, true).is_some(),
|
||||
"under XWayland the portal rejects the parent window: the user has to be told"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_desktops_title_bar_only_when_asked_for() {
|
||||
use super::titlebar_choice;
|
||||
@@ -1837,88 +1927,6 @@ mod viewport_tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod msaa_tests {
|
||||
/// Safe mode uses default UI settings but edits the real graphics.json for the next start.
|
||||
#[test]
|
||||
fn safe_mode_preserves_graphics_choices_when_changing_antialiasing() {
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_ui_egui::scenario::Harness;
|
||||
|
||||
for choice in ["wayland", "x11"] {
|
||||
let mut h = Harness::new();
|
||||
let path = h.dir.join("graphics.json");
|
||||
let saved = json!({"window_system": choice, "adapter": "Selected GPU", "backend": "vulkan", "msaa": 8});
|
||||
std::fs::write(&path, saved.to_string()).unwrap();
|
||||
h.app.services.graphics_path = Some(path.to_string_lossy().into_owned());
|
||||
let defaults = h.app.preferences.clone();
|
||||
let stored_ui = json!({"preferences": {"window_system": "auto", "graphics_adapter": "Stale GPU", "msaa": 2,
|
||||
"display_quality": "fine"}})
|
||||
.to_string();
|
||||
super::restore_preferences(&mut h.app, Some(&stored_ui), Some(&saved), true);
|
||||
|
||||
assert_eq!(h.app.preferences.window_system, choice);
|
||||
assert_eq!(h.app.preferences.graphics_adapter, "Selected GPU");
|
||||
assert_eq!(h.app.preferences.msaa, defaults.msaa, "safe mode ignores saved anti-aliasing");
|
||||
assert_eq!(h.app.preferences.display_quality, defaults.display_quality, "safe mode ignores saved UI settings");
|
||||
assert_eq!(serde_json::from_str::<Value>(&std::fs::read_to_string(&path).unwrap()).unwrap(), saved, "startup does not write settings");
|
||||
|
||||
assert_eq!(h.call("ui.prefs", json!({"open": "Graphics"}))["ok"], true);
|
||||
assert_eq!(h.call("ui.prefs", json!({"msaa": 1}))["ok"], true);
|
||||
assert_eq!(h.call("ui.prefs", json!({"button": "ok"}))["ok"], true);
|
||||
let after: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(after, json!({"window_system": choice, "adapter": "Selected GPU", "backend": "vulkan", "msaa": 1}));
|
||||
|
||||
let mut normal = Harness::new();
|
||||
super::restore_preferences(&mut normal.app, Some(&stored_ui), Some(&after), false);
|
||||
assert_eq!(normal.app.preferences.window_system, choice);
|
||||
assert_eq!(normal.app.preferences.graphics_adapter, "Selected GPU");
|
||||
assert_eq!(normal.app.preferences.display_quality, "fine", "normal startup still restores UI settings");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_invalid_startup_window_system_uses_automatic() {
|
||||
use serde_json::json;
|
||||
|
||||
for saved in [None, Some(json!(null)), Some(json!({})), Some(json!({"window_system": 7})), Some(json!({"window_system": "unknown"}))] {
|
||||
let mut app = super::SolveApp::new(super::Session::default(), super::Services::default());
|
||||
app.preferences.window_system = "wayland".into();
|
||||
super::restore_graphics_preferences(&mut app, saved.as_ref());
|
||||
assert_eq!(app.preferences.window_system, "auto", "{saved:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A manual graphics.json choice wins over stale app.ron preferences and survives an
|
||||
/// unrelated graphics edit (#10116), including on a fresh launch with no saved UI state.
|
||||
#[test]
|
||||
fn startup_window_system_survives_changing_antialiasing() {
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_ui_egui::scenario::Harness;
|
||||
|
||||
for choice in ["wayland", "x11", "auto", " wayland ", " x11 "] {
|
||||
for stale in [None, Some("auto"), Some("wayland"), Some("x11")] {
|
||||
let mut h = Harness::new();
|
||||
if let Some(stale) = stale {
|
||||
h.app.load_prefs(&json!({"preferences": {"window_system": stale}}).to_string());
|
||||
}
|
||||
let path = h.dir.join("graphics.json");
|
||||
std::fs::write(&path, json!({"window_system": choice, "backend": "vulkan", "adapter": "Selected GPU", "msaa": 4}).to_string())
|
||||
.unwrap();
|
||||
h.app.services.graphics_path = Some(path.to_string_lossy().into_owned());
|
||||
let saved: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
super::restore_graphics_preferences(&mut h.app, Some(&saved));
|
||||
|
||||
let result = h.call("ui.prefs", json!({"msaa": 2}));
|
||||
assert_eq!(result["ok"], true, "{result}");
|
||||
let after: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(after["window_system"], choice.trim(), "saved choice {choice:?}, stale UI choice {stale:?}");
|
||||
assert_eq!(result["result"]["window_system"], choice.trim());
|
||||
assert_eq!(after["msaa"], 2);
|
||||
assert_eq!(after["backend"], "vulkan");
|
||||
assert_eq!(after["adapter"], "Selected GPU");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_launch_adopts_the_previous_release_settings() {
|
||||
let d = std::env::temp_dir().join(format!("solidcraft-adopt-settings-{}", std::process::id()));
|
||||
|
||||
@@ -40,10 +40,10 @@ fn connect_to(path: &Path) -> Option<UnixStream> {
|
||||
|
||||
/// The axes of one record, or `None` for a record that is not motion.
|
||||
///
|
||||
/// The daemon reports x to the right, y up and z away from the user; our raw HID frame is x
|
||||
/// right, y towards the user and z down. Swap and negate y/z and ry/rz to match that frame.
|
||||
/// With nothing set in `spnavcfg` the two readers give the same motion; what the user swaps
|
||||
/// or inverts there then carries over.
|
||||
/// The daemon's frame is right-handed with x to the right, y away from the user and z up; ours is
|
||||
/// the device's raw one, x right, y towards the user and z down, so y and z (and the rotations
|
||||
/// about them) change sign. With nothing set in `spnavcfg` the two give the same motion; what
|
||||
/// the user swaps or inverts there then carries over.
|
||||
fn decode(record: &[u8; RECORD_BYTES]) -> Option<Axes> {
|
||||
let mut data = [0_i32; 8];
|
||||
for (slot, bytes) in data.iter_mut().zip(record.as_chunks::<4>().0) {
|
||||
@@ -51,8 +51,8 @@ fn decode(record: &[u8; RECORD_BYTES]) -> Option<Axes> {
|
||||
}
|
||||
let [kind, x, y, z, rx, ry, rz, _period] = data;
|
||||
(kind == 0).then(|| Axes {
|
||||
translation: [unit(x), unit(-i64::from(z)), unit(-i64::from(y))],
|
||||
rotation: [unit(rx), unit(-i64::from(rz)), unit(-i64::from(ry))],
|
||||
translation: [unit(x), unit(-i64::from(y)), unit(-i64::from(z))],
|
||||
rotation: [unit(rx), unit(-i64::from(ry)), unit(-i64::from(rz))],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,28 +149,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn motion_records_become_axes_in_the_devices_raw_frame() {
|
||||
// Each daemon axis reaches only its matching raw HID axis, in both directions.
|
||||
for (daemon_axis, raw_axis, sign) in [(0, 0, 1), (1, 2, -1), (2, 1, -1), (3, 3, 1), (4, 5, -1), (5, 4, -1)] {
|
||||
for value in [-500, 500] {
|
||||
let mut data = [0; 8];
|
||||
data[daemon_axis + 1] = value;
|
||||
let mut expected = [0; 6];
|
||||
expected[raw_axis] = sign * (value * 350 / 500) as i16;
|
||||
assert_eq!(decode(&record(data)), Some(Axes::from(expected)), "daemon axis {daemon_axis}, value {value}");
|
||||
}
|
||||
}
|
||||
// Full deflection of the daemon's x, y and z is the device's full scale, and y and z flip.
|
||||
assert_eq!(decode(&record([0, 500, 500, 500, 0, 0, 0, 8])), Some(Axes { translation: [350, -350, -350], rotation: [0; 3] }));
|
||||
// Rotations follow translations: right-handed in both frames, so ry and rz flip as y and z do.
|
||||
assert_eq!(decode(&record([0, 0, 0, 0, 250, 250, 250, 8])), Some(Axes { translation: [0; 3], rotation: [175, -175, -175] }));
|
||||
// Rest is a motion record of zeros.
|
||||
assert_eq!(decode(&record([0, 0, 0, 0, 0, 0, 0, 1200])), Some(Axes::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_spnavcfg_swaps_reaches_the_view() {
|
||||
// A push away is daemon z (raw y); swapping Y/Z makes it a lift (raw z).
|
||||
// The same swap changes a sideways tilt (daemon rz, raw ry) into a twist (raw rz).
|
||||
let before = decode(&record([0, 0, 0, 500, 0, 0, 250, 8]));
|
||||
let after = decode(&record([0, 0, 500, 0, 0, 250, 0, 8]));
|
||||
assert_eq!(before, Some(Axes { translation: [0, -350, 0], rotation: [0, -175, 0] }));
|
||||
assert_eq!(after, Some(Axes { translation: [0, 0, -350], rotation: [0, 0, -175] }));
|
||||
// The daemon sends a push away as y. With Y and Z swapped in spnavcfg it sends the same push
|
||||
// as z, and the view gets a lift (raw z) instead of the zoom (raw y).
|
||||
let before = decode(&record([0, 0, 500, 0, 0, 0, 0, 8])).map(|axes| axes.translation);
|
||||
let after = decode(&record([0, 0, 0, 500, 0, 0, 0, 8])).map(|axes| axes.translation);
|
||||
assert_eq!(before, Some([0, -350, 0]));
|
||||
assert_eq!(after, Some([0, 0, -350]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -185,7 +179,7 @@ mod tests {
|
||||
// Nothing overflows on the extremes of `i32`.
|
||||
let extreme = decode(&record([0, i32::MIN, i32::MIN, i32::MAX, i32::MAX, i32::MIN, i32::MIN, -1]));
|
||||
assert_eq!(extreme.map(|axes| axes.translation[0]), Some(i16::MIN));
|
||||
assert_eq!(extreme.map(|axes| axes.translation), Some([i16::MIN, i16::MIN, i16::MAX]));
|
||||
assert_eq!(extreme.map(|axes| axes.translation[1]), Some(i16::MAX));
|
||||
assert_eq!(extreme.map(|axes| axes.rotation[0]), Some(i16::MAX));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
//! - **A renderer that cannot start**: on Windows, try the remaining graphics APIs in order,
|
||||
//! then the software renderer (`SOLIDCRAFT_RENDERER=cpu`). An explicit `WGPU_BACKEND` fails
|
||||
//! cleanly without switching. Exhausted retries show a message box with the error.
|
||||
//! - **A GPU that hangs after the window is up** (Windows, [`HangWatch`]): frames that each wait
|
||||
//! out their acquire while the user is there. The app closes, keeping unsaved changes for
|
||||
//! recovery, and starts again on the next API (D3D12 → Vulkan → OpenGL → the software
|
||||
//! renderer), kept in `graphics.json`.
|
||||
//! - **Graphics settings tried from Preferences** (the graphics API, anti-aliasing; [`graphics_start`]):
|
||||
//! the start that tries them asks to keep them. One that never says so (a crash, a hang, the
|
||||
//! question timed out) leaves the trial marked as started, and the next start drops it.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -109,6 +116,46 @@ pub fn begin(dir: &Path) -> u32 {
|
||||
before
|
||||
}
|
||||
|
||||
/// How long a start that tries graphics settings has to draw its first frame (`watch_trial_start`).
|
||||
pub const TRIAL_START_LIMIT: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
|
||||
static FIRST_FRAME: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// The first frame was drawn (or the start handed over to a retry): `watch_trial_start` stands down.
|
||||
pub fn first_frame_drawn() {
|
||||
FIRST_FRAME.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// A start that tries a new graphics API can hang inside the driver before any window draws (a
|
||||
/// black or frozen window, no question, nothing to click). If [`first_frame_drawn`] isn't called
|
||||
/// within `limit`, run `give_up` once (it starts again with the saved settings: the trial is
|
||||
/// marked as started, so the next start drops it). Returns the thread, for tests.
|
||||
pub fn watch_trial_start(limit: std::time::Duration, give_up: impl FnOnce() + Send + 'static) -> Option<std::thread::JoinHandle<()>> {
|
||||
watch_start(&FIRST_FRAME, limit, give_up)
|
||||
}
|
||||
|
||||
fn watch_start(
|
||||
drawn: &'static std::sync::atomic::AtomicBool,
|
||||
limit: std::time::Duration,
|
||||
give_up: impl FnOnce() + Send + 'static,
|
||||
) -> Option<std::thread::JoinHandle<()>> {
|
||||
std::thread::Builder::new()
|
||||
.name("solidcraft-trial-start".into())
|
||||
.spawn(move || {
|
||||
let begun = std::time::Instant::now();
|
||||
while begun.elapsed() < limit {
|
||||
if drawn.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50).min(limit));
|
||||
}
|
||||
if !drawn.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
give_up();
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// The window is up: this launch did not fail to start.
|
||||
pub fn started(dir: &Path) {
|
||||
let _ = std::fs::remove_file(dir.join(MARKER));
|
||||
@@ -188,8 +235,14 @@ pub const WGPU_BACKEND_ENV: &str = "WGPU_BACKEND";
|
||||
/// together and prefer Vulkan, and starting Vulkan loads every installed Vulkan driver: Intel's
|
||||
/// (`igvk64.dll`) crashed SolidCraft at startup that way, inside the driver, before anything
|
||||
/// could be reported. D3D12 is Windows' own graphics API, the one its drivers are tested with.
|
||||
/// `has_hardware(backends)` says whether those backends have an adapter that isn't software
|
||||
/// (it is only asked about the backends being tried, one at a time).
|
||||
/// `hardware(backends)` lists those backends' adapters that aren't software (it is only asked
|
||||
/// about the backends being tried, one at a time).
|
||||
///
|
||||
/// One exception: when D3D12's only hardware adapters are AMD integrated GPUs and Vulkan has a
|
||||
/// hardware adapter, Vulkan. On those (Ryzen 5400U, 5600U, 5700G) D3D12 hung the driver a few
|
||||
/// seconds into the first frames and Windows reset it (a black screen, then a crash, #1789,
|
||||
/// #2806); 0.11.0, which preferred Vulkan, worked there, and `WGPU_BACKEND=vulkan` runs it
|
||||
/// smoothly (#3321).
|
||||
///
|
||||
/// - `WGPU_BACKEND` set (`env_set`): `None`, whatever it says wins.
|
||||
/// - The software renderer (`cpu`, safe mode): D3D12 alone on Windows (WARP); `None` elsewhere.
|
||||
@@ -202,7 +255,7 @@ pub fn choose_backends(
|
||||
env_set: bool,
|
||||
cpu: bool,
|
||||
preference: Option<&str>,
|
||||
mut has_hardware: impl FnMut(eframe::wgpu::Backends) -> bool,
|
||||
mut hardware: impl FnMut(eframe::wgpu::Backends) -> Vec<HardwareAdapter>,
|
||||
) -> Option<eframe::wgpu::Backends> {
|
||||
use eframe::wgpu::Backends;
|
||||
if env_set {
|
||||
@@ -221,7 +274,150 @@ pub fn choose_backends(
|
||||
if !windows {
|
||||
return None;
|
||||
}
|
||||
Some(WINDOWS_ORDER.into_iter().find(|b| has_hardware(*b)).unwrap_or(Backends::DX12))
|
||||
for b in WINDOWS_ORDER {
|
||||
let found = hardware(b);
|
||||
if found.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if b == Backends::DX12 && found.iter().all(HardwareAdapter::hangs_on_d3d12) && !hardware(Backends::VULKAN).is_empty() {
|
||||
log::warn!("graphics: Vulkan rather than D3D12 on an AMD integrated GPU (#3321)");
|
||||
return Some(Backends::VULKAN);
|
||||
}
|
||||
return Some(b);
|
||||
}
|
||||
Some(Backends::DX12)
|
||||
}
|
||||
|
||||
/// A hardware (not software) adapter a backend offers, as [`choose_backends`] needs it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct HardwareAdapter {
|
||||
/// The PCI vendor id.
|
||||
pub vendor: u32,
|
||||
pub device_type: eframe::wgpu::DeviceType,
|
||||
}
|
||||
|
||||
/// AMD's PCI vendor id.
|
||||
const AMD: u32 = 0x1002;
|
||||
|
||||
impl HardwareAdapter {
|
||||
/// An AMD integrated GPU, whose D3D12 driver hung on SolidCraft's first frames (#3321).
|
||||
pub fn hangs_on_d3d12(&self) -> bool {
|
||||
self.vendor == AMD && self.device_type == eframe::wgpu::DeviceType::IntegratedGpu
|
||||
}
|
||||
}
|
||||
|
||||
/// Frames in a row that stalled ([`eframe::egui_wgpu::stalled_frames`]: each waited out wgpu's
|
||||
/// one-second acquire) after which the GPU is taken as hung.
|
||||
pub const STALLED_FRAMES_FOR_RESTART: u32 = 5;
|
||||
/// Seconds the window's surface may refuse its configuration ([`eframe::egui_wgpu::refused_for`])
|
||||
/// while the user is there before the GPU is taken as hung: after a reset, a surface that stays
|
||||
/// unsupported leaves a blank window. Refusals that clear up (a resize, a display waking up)
|
||||
/// take a moment, not this long.
|
||||
pub const REFUSED_S_FOR_RESTART: f64 = 10.0;
|
||||
/// Stalled frames count only this many seconds after the user's input: a hung GPU leaves a white
|
||||
/// window the user clicks at, while a switched-off display can hold frames back with nobody there.
|
||||
pub const STALL_INPUT_WINDOW_S: f64 = 10.0;
|
||||
/// Set on the launch that replaces one whose GPU hung: the hung backend, for the notice.
|
||||
pub const HUNG_ENV: &str = "SOLIDCRAFT_GPU_HUNG";
|
||||
|
||||
/// Watches for a hung GPU after the window is up (#3321). The failed-start count misses it: frames
|
||||
/// still come, a second apart, so the start counts as good.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HangWatch {
|
||||
last_input: Option<f64>,
|
||||
/// When the user came back: the first input after none for [`STALL_INPUT_WINDOW_S`].
|
||||
arrived: f64,
|
||||
/// The stalled streak when the user came back. Those frames stalled with nobody there (a
|
||||
/// display switched off holds them back too), and the input that wakes the display comes
|
||||
/// before the next frame shows whether the GPU is fine, so only stalls after it count.
|
||||
stalled_before: u32,
|
||||
hung: bool,
|
||||
}
|
||||
|
||||
impl HangWatch {
|
||||
/// Per frame, at `now` seconds: whether the user gave input this frame, the frames in a row
|
||||
/// that stalled, and the seconds the surface has refused its configuration. True once, the
|
||||
/// frame the GPU is taken as hung.
|
||||
pub fn hung(&mut self, now: f64, input: bool, stalled: u32, refused_s: Option<f64>) -> bool {
|
||||
if stalled < self.stalled_before {
|
||||
// The streak broke: every stall since came after the user was back.
|
||||
self.stalled_before = 0;
|
||||
}
|
||||
if input {
|
||||
if !self.user_there(now) {
|
||||
self.arrived = now;
|
||||
self.stalled_before = stalled;
|
||||
}
|
||||
self.last_input = Some(now);
|
||||
}
|
||||
if self.hung || !self.user_there(now) {
|
||||
return false;
|
||||
}
|
||||
// A surface refused since before the user came back counts from then.
|
||||
let refused = refused_s.map_or(0.0, |r| r.min(now - self.arrived));
|
||||
self.hung = stalled.saturating_sub(self.stalled_before) >= STALLED_FRAMES_FOR_RESTART || refused >= REFUSED_S_FOR_RESTART;
|
||||
self.hung
|
||||
}
|
||||
|
||||
fn user_there(&self, now: f64) -> bool {
|
||||
self.last_input.is_some_and(|t| (0.0..=STALL_INPUT_WINDOW_S).contains(&(now - t)))
|
||||
}
|
||||
}
|
||||
|
||||
/// How a launch whose GPU hung starts again.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Restart {
|
||||
/// On this backend (`graphics.json`'s `backend`), kept for later launches too.
|
||||
Backend(&'static str),
|
||||
/// As it was: the hang came with graphics settings on trial, which the next start drops
|
||||
/// ([`graphics_start`]).
|
||||
Again,
|
||||
}
|
||||
|
||||
/// The restart after the GPU hung on `backend` (`cpu`: the software renderer, which has nothing
|
||||
/// to fall back to; `trial`: with graphics settings on trial, which the next start drops). Each
|
||||
/// hang moves on to the next of [`WINDOWS_ORDER`], then to the software renderer, kept in
|
||||
/// `graphics.json` for later launches too, so a GPU hangs at most once on each: a safe-mode
|
||||
/// restart would leave the backend that hung saved for the next normal start. D3D12 goes to
|
||||
/// Vulkan, which runs the GPUs whose D3D12 driver hung (#1789); it isn't probed here, as loading
|
||||
/// Vulkan's drivers into this process is what crashed once (`WINDOWS_ORDER`): the new launch
|
||||
/// finds out, and falls back to software if it has no GPU.
|
||||
pub fn after_hang(backend: eframe::wgpu::Backend, cpu: bool, trial: bool) -> Option<Restart> {
|
||||
match backend {
|
||||
_ if trial => Some(Restart::Again),
|
||||
_ if cpu => None,
|
||||
eframe::wgpu::Backend::Dx12 => Some(Restart::Backend("vulkan")),
|
||||
eframe::wgpu::Backend::Vulkan => Some(Restart::Backend("gl")),
|
||||
_ => Some(Restart::Backend(SOFTWARE_BACKEND)),
|
||||
}
|
||||
}
|
||||
|
||||
/// `graphics.json`'s `backend` for the software renderer, kept after the GPU hung on every API.
|
||||
pub const SOFTWARE_BACKEND: &str = "cpu";
|
||||
|
||||
/// The notice on the launch after the GPU hung on `hung` (a backend's name): this one is in safe
|
||||
/// mode, or on the `backend` preference.
|
||||
pub fn hang_note(hung: &str, safe_mode: bool, backend: Option<&str>) -> String {
|
||||
let now = match backend {
|
||||
_ if safe_mode => "in safe mode".to_string(),
|
||||
Some(b) => format!("on {}, which it keeps using (the backend in graphics.json)", api_name(b)),
|
||||
None => "with another renderer".to_string(),
|
||||
};
|
||||
let unsaved = if safe_mode { "the next normal start offers unsaved changes" } else { "unsaved changes are offered for recovery" };
|
||||
format!("The graphics driver stopped responding on {}, so SolidCraft restarted {now}; {unsaved}.", api_name(hung))
|
||||
}
|
||||
|
||||
/// A backend's name (`Dx12`, `vulkan`, …) as people know the graphics API.
|
||||
fn api_name(backend: &str) -> &str {
|
||||
match backend.trim().to_ascii_lowercase().as_str() {
|
||||
"" | "auto" => "Automatic",
|
||||
"dx12" => "Direct3D 12",
|
||||
"vulkan" => "Vulkan",
|
||||
"gl" => "OpenGL",
|
||||
"metal" => "Metal",
|
||||
"cpu" | "software" => "the software renderer",
|
||||
_ => backend,
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal, process-local retry choice; never written to the graphics preference.
|
||||
@@ -252,6 +448,164 @@ pub fn fallback_note(previous: &str, failed: eframe::wgpu::Backends, next: &str,
|
||||
line.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(4096).collect()
|
||||
}
|
||||
|
||||
/// The graphics APIs Preferences offers on this system besides Automatic, as `graphics.json`'s
|
||||
/// `backend` names them (#4015). None on macOS, which has Metal alone.
|
||||
pub const GRAPHICS_APIS: &[&str] = if cfg!(target_os = "windows") {
|
||||
&["dx12", "vulkan", "gl"]
|
||||
} else if cfg!(target_os = "macos") {
|
||||
&[]
|
||||
} else {
|
||||
&["vulkan", "gl"]
|
||||
};
|
||||
|
||||
/// The APIs Preferences warns about on this system: D3D12 when its GPUs are all AMD integrated
|
||||
/// ones, whose driver it froze, at times until the computer was restarted (#3321).
|
||||
pub fn apis_that_freeze(windows: bool, hardware: impl FnOnce(eframe::wgpu::Backends) -> Vec<HardwareAdapter>) -> Vec<&'static str> {
|
||||
if !windows {
|
||||
return Vec::new();
|
||||
}
|
||||
let found = hardware(eframe::wgpu::Backends::DX12);
|
||||
if !found.is_empty() && found.iter().all(HardwareAdapter::hangs_on_d3d12) { vec!["dx12"] } else { Vec::new() }
|
||||
}
|
||||
|
||||
/// The saved settings to start with after the last launch crashed (`crashed`) on `saved`: an API
|
||||
/// Preferences warns about on this system (`freezes`, [`apis_that_freeze`]) goes back to Automatic.
|
||||
/// AMD's integrated GPUs crashed on D3D12 once a design opened, after the start and its trial had
|
||||
/// gone fine and the user kept it (#4015). `None`: they stay.
|
||||
pub fn after_crash(saved: &GraphicsChoice, crashed: bool, freezes: impl FnOnce() -> Vec<&'static str>) -> Option<GraphicsChoice> {
|
||||
let chosen: Vec<&str> = saved.backend.split(',').map(str::trim).filter(|b| !b.is_empty() && *b != "auto").collect();
|
||||
if !crashed || chosen.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let freezes = freezes();
|
||||
chosen.iter().any(|b| freezes.contains(b)).then(|| GraphicsChoice { backend: "auto".into(), msaa: saved.msaa })
|
||||
}
|
||||
|
||||
/// The notice when [`after_crash`] put `backend` (a backend's name) back to Automatic.
|
||||
pub fn after_crash_note(backend: &str) -> String {
|
||||
format!(
|
||||
"SolidCraft closed unexpectedly on {}, which can freeze this computer's graphics, so it went back to Automatic. Preferences can choose it again.",
|
||||
api_name(backend)
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether a crash report `kind` ([`solidcraft_engine::crash::Summary`]) means the launch ended:
|
||||
/// a panic nothing caught or an unclean exit, not a command or kernel call recovered from.
|
||||
pub fn launch_crashed(kind: &str) -> bool {
|
||||
matches!(kind, "panic" | "unclean_exit")
|
||||
}
|
||||
|
||||
/// [`hardware_adapters`], keeping D3D12's: the start asks for them ([`choose_backends`]), then
|
||||
/// Preferences ([`apis_that_freeze`]).
|
||||
pub fn hardware_adapters_once(backends: eframe::wgpu::Backends) -> Vec<HardwareAdapter> {
|
||||
static D3D12: std::sync::OnceLock<Vec<HardwareAdapter>> = std::sync::OnceLock::new();
|
||||
if backends == eframe::wgpu::Backends::DX12 { D3D12.get_or_init(|| hardware_adapters(backends)).clone() } else { hardware_adapters(backends) }
|
||||
}
|
||||
|
||||
/// Anti-aliasing samples when `graphics.json` has none (or one that isn't 1, 2, 4 or 8).
|
||||
pub const DEFAULT_MSAA: u16 = 4;
|
||||
|
||||
/// Graphics settings a launch starts with (`graphics.json`'s `backend` and `msaa`).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GraphicsChoice {
|
||||
/// `auto`, or the backends to start (`dx12`, `vulkan`, `gl`, or a comma list).
|
||||
pub backend: String,
|
||||
pub msaa: u16,
|
||||
}
|
||||
|
||||
impl GraphicsChoice {
|
||||
/// The choice in a JSON object, with `fallback`'s values where it has none (or a bad one).
|
||||
fn read(json: Option<&serde_json::Value>, fallback: &GraphicsChoice) -> GraphicsChoice {
|
||||
let backend = json.and_then(|v| v.get("backend")).and_then(serde_json::Value::as_str).map(str::trim).filter(|b| !b.is_empty());
|
||||
let msaa = json
|
||||
.and_then(|v| v.get("msaa"))
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.and_then(|m| u16::try_from(m).ok())
|
||||
.filter(|m| [1, 2, 4, 8].contains(m));
|
||||
GraphicsChoice { backend: backend.map_or_else(|| fallback.backend.clone(), str::to_string), msaa: msaa.unwrap_or(fallback.msaa) }
|
||||
}
|
||||
|
||||
/// As the notice names it, with only what differs from `other` ("Vulkan, 8× anti-aliasing").
|
||||
fn describe(&self, other: &GraphicsChoice) -> String {
|
||||
let mut parts = Vec::new();
|
||||
if self.backend != other.backend || self.msaa == other.msaa {
|
||||
parts.push(api_name(&self.backend).to_string());
|
||||
}
|
||||
if self.msaa != other.msaa {
|
||||
parts.push(format!("{}× anti-aliasing", self.msaa));
|
||||
}
|
||||
parts.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings tried from Preferences, as a launch finds them (#4015).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Trial {
|
||||
None,
|
||||
/// This launch tries them: it starts with them and asks whether to keep them.
|
||||
Trying(GraphicsChoice),
|
||||
/// The launch that tried them ended without keeping them: they are dropped.
|
||||
Failed(GraphicsChoice),
|
||||
}
|
||||
|
||||
/// `graphics.json` as a launch reads it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct GraphicsStart {
|
||||
/// The settings kept for every launch.
|
||||
pub saved: GraphicsChoice,
|
||||
pub trial: Trial,
|
||||
/// The file to write back before the window opens, when it changes: the trial marked as
|
||||
/// started, or dropped.
|
||||
pub rewrite: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl GraphicsStart {
|
||||
/// The settings this launch starts with.
|
||||
pub fn current(&self) -> &GraphicsChoice {
|
||||
match &self.trial {
|
||||
Trial::Trying(tried) => tried,
|
||||
_ => &self.saved,
|
||||
}
|
||||
}
|
||||
|
||||
/// The notice when a trial was dropped.
|
||||
pub fn note(&self) -> Option<String> {
|
||||
let Trial::Failed(tried) = &self.trial else { return None };
|
||||
Some(format!(
|
||||
"The graphics settings tried at the last start ({}) weren't kept, so SolidCraft went back to {}.",
|
||||
tried.describe(&self.saved),
|
||||
self.saved.describe(tried)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read `graphics.json` (`file`) at start. Preferences writes settings to try as its `trial`
|
||||
/// object, next to the saved `backend` and `msaa`. The first launch to find it tries them and
|
||||
/// marks it `started`; the app removes the trial when the user keeps the settings (they become
|
||||
/// the saved ones) or goes back. A trial already started belongs to a launch that never got
|
||||
/// that far, so it is dropped. Safe mode leaves a trial that hasn't started for the next start.
|
||||
pub fn graphics_start(file: Option<&serde_json::Value>, safe_mode: bool) -> GraphicsStart {
|
||||
let defaults = GraphicsChoice { backend: "auto".into(), msaa: DEFAULT_MSAA };
|
||||
let saved = GraphicsChoice::read(file, &defaults);
|
||||
let Some(trial) = file.and_then(|f| f.get("trial")).filter(|t| t.is_object()) else {
|
||||
return GraphicsStart { saved, trial: Trial::None, rewrite: None };
|
||||
};
|
||||
let tried = GraphicsChoice::read(Some(trial), &saved);
|
||||
let started = trial.get("started").and_then(serde_json::Value::as_bool).unwrap_or(false);
|
||||
let mut rewrite = file.cloned().unwrap_or_default();
|
||||
if started {
|
||||
if let Some(o) = rewrite.as_object_mut() {
|
||||
o.remove("trial");
|
||||
}
|
||||
return GraphicsStart { saved, trial: Trial::Failed(tried), rewrite: Some(rewrite) };
|
||||
}
|
||||
if safe_mode {
|
||||
return GraphicsStart { saved, trial: Trial::None, rewrite: None };
|
||||
}
|
||||
rewrite["trial"]["started"] = serde_json::json!(true);
|
||||
GraphicsStart { saved, trial: Trial::Trying(tried), rewrite: Some(rewrite) }
|
||||
}
|
||||
|
||||
/// The device feature for a 32-bit float depth buffer with a stencil (reversed depth, #1434).
|
||||
pub const FLOAT_DEPTH_STENCIL: eframe::wgpu::Features = eframe::wgpu::Features::DEPTH32FLOAT_STENCIL8;
|
||||
|
||||
@@ -272,8 +626,8 @@ pub fn float_depth_everywhere(options: &eframe::egui_wgpu::WgpuConfiguration) ->
|
||||
/// The order backends are tried in on Windows.
|
||||
pub const WINDOWS_ORDER: [eframe::wgpu::Backends; 3] = [eframe::wgpu::Backends::DX12, eframe::wgpu::Backends::VULKAN, eframe::wgpu::Backends::GL];
|
||||
|
||||
/// Whether `backends` (started on their own, nothing else) have a hardware adapter.
|
||||
pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
|
||||
/// The hardware adapters of `backends` (started on their own, nothing else).
|
||||
pub fn hardware_adapters(backends: eframe::wgpu::Backends) -> Vec<HardwareAdapter> {
|
||||
let mut desc = eframe::wgpu::InstanceDescriptor::new_without_display_handle();
|
||||
desc.backends = backends;
|
||||
desc.flags = eframe::wgpu::InstanceFlags::from_build_config().with_env();
|
||||
@@ -282,7 +636,12 @@ pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
|
||||
let adapters = pollster::block_on(instance.enumerate_adapters(backends));
|
||||
let found: Vec<String> = adapters.iter().map(|a| format!("{} ({:?})", a.get_info().name, a.get_info().device_type)).collect();
|
||||
log::info!("graphics: {backends:?} adapters: {found:?}");
|
||||
adapters.iter().any(|a| a.get_info().device_type != eframe::wgpu::DeviceType::Cpu)
|
||||
adapters
|
||||
.iter()
|
||||
.map(|a| a.get_info())
|
||||
.filter(|i| i.device_type != eframe::wgpu::DeviceType::Cpu)
|
||||
.map(|i| HardwareAdapter { vendor: i.vendor, device_type: i.device_type })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The environment variable that switches wgpu's indirect-call validation on or off.
|
||||
@@ -311,18 +670,13 @@ pub const SYSTEM_VERSION_COMPAT_ENV: &str = "SYSTEM_VERSION_COMPAT";
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const OCLP_NVRAM_KEY: &str = "4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102:OCLP-Version";
|
||||
|
||||
/// Where macOS keeps its `nvram` tool.
|
||||
#[cfg(target_os = "macos")]
|
||||
const NVRAM_TOOL: &str = "/usr/sbin/nvram";
|
||||
|
||||
/// `true` if the OCLP NVRAM key is present on this system.
|
||||
///
|
||||
/// Runs `/usr/sbin/nvram <key>` and treats exit 0 as present. The full path keeps a `PATH` that
|
||||
/// lacks `/usr/sbin`, or lists a look-alike first, from changing the answer. Any error (missing
|
||||
/// binary, permission denied, key absent) returns `false`: this is a best-effort probe.
|
||||
/// Runs `nvram <key>` and treats exit 0 as present. Any error (missing binary,
|
||||
/// permission denied, key absent) returns `false` — this is a best-effort probe.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn oclp_nvram_present() -> bool {
|
||||
std::process::Command::new(NVRAM_TOOL).arg(OCLP_NVRAM_KEY).output().map(|o| o.status.success()).unwrap_or(false)
|
||||
std::process::Command::new("nvram").arg(OCLP_NVRAM_KEY).output().map(|o| o.status.success()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether to re-launch with `SYSTEM_VERSION_COMPAT=1`: it is not set yet and OCLP shows through
|
||||
@@ -339,8 +693,8 @@ fn compat_wanted(compat_set: bool, nvram: impl FnOnce() -> bool) -> bool {
|
||||
///
|
||||
/// `SYSTEM_VERSION_COMPAT=1` must be present **before** the process starts: setting it
|
||||
/// with `std::env::set_var` inside `main` is too late because the macOS frameworks have
|
||||
/// already queried the OS version by then, so the program starts itself again with the variable
|
||||
/// set and waits for that child.
|
||||
/// already queried the OS version by then. Re-launching is the only safe approach that
|
||||
/// works without `unsafe` (a true `execve` replacement would require it).
|
||||
///
|
||||
/// Returns `Ok(false)` immediately when:
|
||||
/// - `SYSTEM_VERSION_COMPAT` is already set (re-launched process, no loop), or
|
||||
@@ -408,6 +762,34 @@ mod tests {
|
||||
["Integrated", "Discrete small", "Discrete large"].into_iter().map(|name| AdapterChoice { name: name.into(), compatible: true }).collect()
|
||||
}
|
||||
|
||||
/// A start on trial that draws no frame in time gives up once; one that draws stands down
|
||||
/// (#4015).
|
||||
#[test]
|
||||
fn a_trial_start_that_draws_nothing_gives_up_once() {
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::time::Duration;
|
||||
static HUNG: AtomicBool = AtomicBool::new(false);
|
||||
static HUNG_GAVE_UP: AtomicU32 = AtomicU32::new(0);
|
||||
watch_start(&HUNG, Duration::from_millis(150), || {
|
||||
HUNG_GAVE_UP.fetch_add(1, Ordering::SeqCst);
|
||||
})
|
||||
.map(|t| t.join())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(HUNG_GAVE_UP.load(Ordering::SeqCst), 1);
|
||||
|
||||
static DRAWN: AtomicBool = AtomicBool::new(false);
|
||||
static DRAWN_GAVE_UP: AtomicU32 = AtomicU32::new(0);
|
||||
let watch = watch_start(&DRAWN, Duration::from_millis(400), || {
|
||||
DRAWN_GAVE_UP.fetch_add(1, Ordering::SeqCst);
|
||||
})
|
||||
.unwrap();
|
||||
std::thread::sleep(Duration::from_millis(60));
|
||||
DRAWN.store(true, Ordering::SeqCst);
|
||||
watch.join().unwrap();
|
||||
assert_eq!(DRAWN_GAVE_UP.load(Ordering::SeqCst), 0, "the window drew: nothing to give up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_file_size_parses_sizes_and_refuses_bad_ones() {
|
||||
assert_eq!(max_file_size(Some("2GB")), Ok(2 << 30));
|
||||
@@ -567,21 +949,32 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
/// A discrete NVIDIA GPU, and AMD's integrated one (#3321).
|
||||
const NVIDIA: HardwareAdapter = HardwareAdapter { vendor: 0x10de, device_type: eframe::wgpu::DeviceType::DiscreteGpu };
|
||||
const AMD_IGPU: HardwareAdapter = HardwareAdapter { vendor: AMD, device_type: eframe::wgpu::DeviceType::IntegratedGpu };
|
||||
|
||||
/// A probe where the backends `with` have `gpus`, and the others none.
|
||||
fn probe_with(
|
||||
with: &'static [eframe::wgpu::Backends],
|
||||
gpus: &'static [HardwareAdapter],
|
||||
) -> impl FnMut(eframe::wgpu::Backends) -> Vec<HardwareAdapter> {
|
||||
use eframe::wgpu::Backends;
|
||||
move |b: Backends| {
|
||||
assert!(b == Backends::DX12 || b == Backends::VULKAN || b == Backends::GL, "one backend at a time: {b:?}");
|
||||
if with.contains(&b) { gpus.to_vec() } else { Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_starts_d3d12_first_and_vulkan_only_when_d3d12_has_no_gpu() {
|
||||
use eframe::wgpu::Backends;
|
||||
let probe = |with: &'static [Backends]| {
|
||||
move |b: Backends| {
|
||||
assert!(b == Backends::DX12 || b == Backends::VULKAN || b == Backends::GL, "one backend at a time: {b:?}");
|
||||
with.contains(&b)
|
||||
}
|
||||
};
|
||||
let probe = |with: &'static [Backends]| probe_with(with, &[NVIDIA]);
|
||||
// D3D12 works: Vulkan and OpenGL are never even asked about (starting Vulkan loads
|
||||
// its drivers, which is what crashed).
|
||||
let mut asked = Vec::new();
|
||||
let chosen = choose_backends(true, false, false, None, |b| {
|
||||
asked.push(b);
|
||||
b == Backends::DX12
|
||||
if b == Backends::DX12 { vec![NVIDIA] } else { Vec::new() }
|
||||
});
|
||||
assert_eq!(chosen, Some(Backends::DX12));
|
||||
assert_eq!(asked, [Backends::DX12]);
|
||||
@@ -596,6 +989,158 @@ mod tests {
|
||||
assert_eq!(choose_backends(true, false, false, Some(""), probe(&[Backends::GL])), Some(Backends::GL));
|
||||
}
|
||||
|
||||
/// AMD integrated GPUs hung under D3D12 at the first frames, the driver reset and the app
|
||||
/// crashed (#1789, #2806); they run on Vulkan (#3321).
|
||||
#[test]
|
||||
fn an_amd_integrated_gpu_starts_vulkan_rather_than_d3d12() {
|
||||
use eframe::wgpu::Backends;
|
||||
let all = &[Backends::DX12, Backends::VULKAN, Backends::GL];
|
||||
assert_eq!(choose_backends(true, false, false, None, probe_with(all, &[AMD_IGPU])), Some(Backends::VULKAN));
|
||||
// Without a Vulkan GPU: D3D12 still, rather than OpenGL or software.
|
||||
assert_eq!(choose_backends(true, false, false, None, probe_with(&[Backends::DX12], &[AMD_IGPU])), Some(Backends::DX12));
|
||||
// Another GPU beside it (a laptop's discrete one): D3D12, which can pick that one.
|
||||
assert_eq!(choose_backends(true, false, false, None, probe_with(all, &[AMD_IGPU, NVIDIA])), Some(Backends::DX12));
|
||||
// A discrete AMD GPU keeps D3D12.
|
||||
let discrete = &[HardwareAdapter { vendor: AMD, device_type: eframe::wgpu::DeviceType::DiscreteGpu }];
|
||||
assert_eq!(choose_backends(true, false, false, None, probe_with(all, discrete)), Some(Backends::DX12));
|
||||
// The preference and WGPU_BACKEND still win, and other systems are left to wgpu.
|
||||
let never = |b: Backends| -> Vec<HardwareAdapter> { panic!("no probing expected, asked about {b:?}") };
|
||||
assert_eq!(choose_backends(true, false, false, Some("dx12"), never), Some(Backends::DX12));
|
||||
assert_eq!(choose_backends(true, true, false, None, never), None);
|
||||
assert_eq!(choose_backends(false, false, false, None, never), None);
|
||||
}
|
||||
|
||||
/// After the window was up, D3D12 hung the GPU: frames came a second apart and never showed,
|
||||
/// a white window with no crash (#1789, #3321). Five such frames while the user is there
|
||||
/// restart the app, D3D12 on Vulkan and anything else in safe mode.
|
||||
#[test]
|
||||
fn a_gpu_that_hangs_after_the_window_is_up_restarts_on_another_renderer() {
|
||||
use eframe::wgpu::Backend;
|
||||
let stalled = STALLED_FRAMES_FOR_RESTART;
|
||||
let mut watch = HangWatch::default();
|
||||
assert!(!watch.hung(0.0, true, 0, None));
|
||||
assert!(!watch.hung(4.0, false, stalled - 1, None), "not yet");
|
||||
assert!(watch.hung(5.0, false, stalled, None), "input 5 s ago");
|
||||
assert!(!watch.hung(6.0, true, stalled + 1, None), "once");
|
||||
// Nobody at the computer (a display switched off holds frames back too): no restart.
|
||||
let mut away = HangWatch::default();
|
||||
assert!(!away.hung(100.0, false, stalled, None));
|
||||
assert!(!away.hung(200.0, false, 100, None));
|
||||
// The input that wakes the display comes before a frame shows the GPU is fine: the
|
||||
// stalls from before don't count, and the frames on time that follow end the streak.
|
||||
let mut woke = HangWatch::default();
|
||||
assert!(!woke.hung(0.0, true, 0, None));
|
||||
assert!(!woke.hung(STALL_INPUT_WINDOW_S + 1.0, false, 30, None));
|
||||
assert!(!woke.hung(STALL_INPUT_WINDOW_S + 2.0, true, 30, None), "back at a healthy app");
|
||||
assert!(!woke.hung(STALL_INPUT_WINDOW_S + 3.0, true, 0, None));
|
||||
let later: Vec<bool> = (1..=stalled).map(|n| woke.hung(STALL_INPUT_WINDOW_S + 3.0 + f64::from(n), true, n, None)).collect();
|
||||
assert_eq!(later.last(), Some(&true), "then it hangs: {later:?}");
|
||||
// Back at a GPU that hung while nobody was there: five more stalls.
|
||||
let mut back = HangWatch::default();
|
||||
assert!(!back.hung(100.0, true, 30, None));
|
||||
assert!(!back.hung(103.0, true, 30 + stalled - 1, None));
|
||||
assert!(back.hung(104.0, true, 30 + stalled, None));
|
||||
// A surface refused for good after a reset never reaches the acquire: a blank window.
|
||||
let mut refused = HangWatch::default();
|
||||
assert!(!refused.hung(0.0, true, 0, Some(60.0)), "refused since before the user came");
|
||||
assert!(!refused.hung(REFUSED_S_FOR_RESTART - 1.0, true, 0, Some(60.0 + REFUSED_S_FOR_RESTART - 1.0)));
|
||||
assert!(refused.hung(REFUSED_S_FOR_RESTART, true, 0, Some(60.0 + REFUSED_S_FOR_RESTART)));
|
||||
let mut resized = HangWatch::default();
|
||||
assert!(!resized.hung(0.0, true, 0, Some(0.5)), "a moment's refusal");
|
||||
|
||||
// Each API hangs at most once: D3D12, then Vulkan, then OpenGL, then software.
|
||||
assert_eq!(after_hang(Backend::Dx12, false, false), Some(Restart::Backend("vulkan")));
|
||||
assert_eq!(after_hang(Backend::Vulkan, false, false), Some(Restart::Backend("gl")));
|
||||
assert_eq!(after_hang(Backend::Gl, false, false), Some(Restart::Backend(SOFTWARE_BACKEND)));
|
||||
assert_eq!(after_hang(Backend::Dx12, true, false), None, "software: nothing to fall back to");
|
||||
assert!(cpu_renderer_env(Some(SOFTWARE_BACKEND)), "the saved software renderer starts as such");
|
||||
// Settings on trial: start again as it was, and the next start drops them (#4015).
|
||||
assert_eq!(after_hang(Backend::Vulkan, false, true), Some(Restart::Again));
|
||||
assert_eq!(after_hang(Backend::Dx12, true, true), Some(Restart::Again));
|
||||
let note = hang_note("Dx12", false, Some("vulkan"));
|
||||
assert!(note.contains("on Direct3D 12") && note.contains("on Vulkan") && note.contains("recovery"), "{note}");
|
||||
assert!(hang_note("Vulkan", true, Some("gl")).contains("in safe mode"));
|
||||
assert!(hang_note("Gl", false, Some("cpu")).contains("on the software renderer, which it keeps using"));
|
||||
}
|
||||
|
||||
/// Settings tried from Preferences (#4015): the first start tries them and marks the trial
|
||||
/// started; one that finds it started drops it and says so; safe mode leaves it waiting.
|
||||
#[test]
|
||||
fn graphics_settings_on_trial_are_tried_once_and_dropped_unless_kept() {
|
||||
use serde_json::json;
|
||||
let saved = GraphicsChoice { backend: "dx12".into(), msaa: 4 };
|
||||
let vulkan_8x = GraphicsChoice { backend: "vulkan".into(), msaa: 8 };
|
||||
// No file, or one without a trial: the saved settings (defaults where missing), unchanged.
|
||||
let none = graphics_start(None, false);
|
||||
assert_eq!(none.current(), &GraphicsChoice { backend: "auto".into(), msaa: DEFAULT_MSAA });
|
||||
assert_eq!((none.trial, none.rewrite), (Trial::None, None));
|
||||
let plain = graphics_start(Some(&json!({"backend": "dx12", "msaa": 16})), false);
|
||||
assert_eq!(plain.current(), &saved, "16 isn't a sample count: the default");
|
||||
assert_eq!(plain.rewrite, None);
|
||||
|
||||
// The first start tries the trial and marks it started.
|
||||
let file = json!({"backend": "dx12", "msaa": 4, "trial": {"backend": "vulkan", "msaa": 8}});
|
||||
let first = graphics_start(Some(&file), false);
|
||||
assert_eq!(first.trial, Trial::Trying(vulkan_8x.clone()));
|
||||
assert_eq!(first.current(), &vulkan_8x);
|
||||
assert_eq!(first.saved, saved);
|
||||
assert_eq!(first.note(), None);
|
||||
let marked = first.rewrite.clone().unwrap();
|
||||
assert_eq!(marked["trial"]["started"], json!(true));
|
||||
assert_eq!((marked["backend"].clone(), marked["msaa"].clone()), (json!("dx12"), json!(4)), "the saved settings stay");
|
||||
|
||||
// That start never kept them (it crashed, hung, or the question timed out): the next one
|
||||
// drops the trial, starts with the saved settings and says what happened.
|
||||
let next = graphics_start(Some(&marked), false);
|
||||
assert_eq!(next.trial, Trial::Failed(vulkan_8x));
|
||||
assert_eq!(next.current(), &saved);
|
||||
assert_eq!(next.rewrite, Some(json!({"backend": "dx12", "msaa": 4})));
|
||||
let note = next.note().unwrap();
|
||||
assert!(note.contains("(Vulkan, 8× anti-aliasing)") && note.contains("back to Direct3D 12, 4× anti-aliasing"), "{note}");
|
||||
// Only what differs is named; Automatic by name.
|
||||
let api_only = graphics_start(Some(&json!({"msaa": 4, "trial": {"backend": "gl", "started": true}})), false);
|
||||
assert_eq!(
|
||||
api_only.note().unwrap(),
|
||||
"The graphics settings tried at the last start (OpenGL) weren't kept, so SolidCraft went back to Automatic."
|
||||
);
|
||||
|
||||
// Safe mode leaves a trial that hasn't started for the next normal start, but still drops
|
||||
// one that has.
|
||||
let safe = graphics_start(Some(&file), true);
|
||||
assert_eq!((safe.current(), &safe.trial, &safe.rewrite), (&saved, &Trial::None, &None));
|
||||
assert!(matches!(graphics_start(Some(&marked), true).trial, Trial::Failed(_)));
|
||||
// Every API Preferences offers is a backend the start understands.
|
||||
for api in GRAPHICS_APIS {
|
||||
assert!(!eframe::wgpu::Backends::from_comma_list(api).is_empty(), "{api}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_warn_about_d3d12_only_where_it_froze_the_gpu() {
|
||||
let with = |adapters: Vec<HardwareAdapter>| move |_: eframe::wgpu::Backends| adapters;
|
||||
assert_eq!(apis_that_freeze(true, with(vec![AMD_IGPU])), vec!["dx12"]);
|
||||
assert!(apis_that_freeze(true, with(vec![AMD_IGPU, NVIDIA])).is_empty(), "a graphics card besides");
|
||||
assert!(apis_that_freeze(true, with(vec![NVIDIA])).is_empty());
|
||||
assert!(apis_that_freeze(true, with(Vec::new())).is_empty(), "WARP alone");
|
||||
assert!(apis_that_freeze(false, |_| panic!("only Windows has D3D12")).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_crash_on_an_api_that_freezes_this_gpu_goes_back_to_automatic() {
|
||||
let d3d12 = GraphicsChoice { backend: "dx12".into(), msaa: 1 };
|
||||
let freezes = || vec!["dx12"];
|
||||
assert_eq!(after_crash(&d3d12, true, freezes), Some(GraphicsChoice { backend: "auto".into(), msaa: 1 }), "anti-aliasing stays");
|
||||
assert_eq!(after_crash(&d3d12, false, freezes), None, "no crash");
|
||||
assert_eq!(after_crash(&d3d12, true, Vec::new), None, "D3D12 doesn't freeze this GPU");
|
||||
let vulkan = GraphicsChoice { backend: "vulkan".into(), msaa: 4 };
|
||||
assert_eq!(after_crash(&vulkan, true, freezes), None);
|
||||
let auto = GraphicsChoice { backend: "auto".into(), msaa: 4 };
|
||||
assert_eq!(after_crash(&auto, true, || panic!("Automatic isn't probed")), None);
|
||||
assert!(launch_crashed("unclean_exit") && launch_crashed("panic"));
|
||||
assert!(!launch_crashed("command_panic") && !launch_crashed("kernel_panic_recovered"));
|
||||
assert!(after_crash_note("dx12").contains("on Direct3D 12"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_windows_backends_fall_back_in_order_and_stop_after_software() {
|
||||
use eframe::wgpu::Backends as B;
|
||||
@@ -622,7 +1167,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
choose_backends(windows, true, cpu, Some("vulkan"), |_| {
|
||||
probed = true;
|
||||
false
|
||||
Vec::new()
|
||||
}),
|
||||
None
|
||||
);
|
||||
@@ -647,7 +1192,7 @@ mod tests {
|
||||
#[test]
|
||||
fn wgpu_backend_the_preference_and_the_software_renderer_override_the_order() {
|
||||
use eframe::wgpu::Backends;
|
||||
let never = |b: Backends| -> bool { panic!("no probing expected, asked about {b:?}") };
|
||||
let never = |b: Backends| -> Vec<HardwareAdapter> { panic!("no probing expected, asked about {b:?}") };
|
||||
// WGPU_BACKEND: wgpu reads it itself, nothing is probed.
|
||||
assert_eq!(choose_backends(true, true, false, Some("gl"), never), None);
|
||||
assert_eq!(choose_backends(true, true, true, None, never), None);
|
||||
@@ -660,7 +1205,7 @@ mod tests {
|
||||
assert_eq!(choose_backends(true, false, false, Some("dx12,vulkan"), never), Some(Backends::DX12 | Backends::VULKAN));
|
||||
assert_eq!(choose_backends(false, false, false, Some("vulkan"), never), Some(Backends::VULKAN));
|
||||
// An unknown preference is the default order.
|
||||
assert_eq!(choose_backends(true, false, false, Some("glide"), |b| b == Backends::DX12), Some(Backends::DX12));
|
||||
assert_eq!(choose_backends(true, false, false, Some("glide"), probe_with(&[Backends::DX12], &[NVIDIA])), Some(Backends::DX12));
|
||||
// Elsewhere: wgpu's own choice.
|
||||
assert_eq!(choose_backends(false, false, false, None, never), None);
|
||||
assert_eq!(choose_backends(false, false, false, Some("auto"), never), None);
|
||||
@@ -673,10 +1218,10 @@ mod tests {
|
||||
// D3D12 never loads its OpenGL driver into the test process, and AMD's (`atio6axx.dll`)
|
||||
// aborted the process as it exited (#1156).
|
||||
if cfg!(target_os = "windows") {
|
||||
let _ = choose_backends(true, false, false, None, has_hardware_adapter);
|
||||
let _ = choose_backends(true, false, false, None, hardware_adapters);
|
||||
} else {
|
||||
for b in WINDOWS_ORDER {
|
||||
let _ = has_hardware_adapter(b);
|
||||
let _ = hardware_adapters(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ assets/
|
||||
ui/design/tools/ # authoring bases, checks and included review app
|
||||
ui/source/ # Rust viewport glyphs, markers and navigation
|
||||
branding/web/source/og.png # shared social preview card
|
||||
branding/logo/ # print/web logo files; guide in branding/README.md
|
||||
styles/ui/palettes.rs # light/dark defaults
|
||||
styles/web/loading.css # web loading/canvas styles
|
||||
appearances/library.rs # built-in visual presets
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# SolidCraft brand kit
|
||||
|
||||
Logo files for stickers, shirts, the web and social posts. Everything in `logo/` is drawn
|
||||
from the app icon (`assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg`),
|
||||
which is also the last frame of the loading animation (`assets/styles/web/loading-logo.svg`).
|
||||
The SVGs are plain paths: no scripts, fonts, animation or external links, so any printer can use them.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Use it for |
|
||||
|---|---|
|
||||
| `logo/solidcraft-mark.svg` | The mark in full colour. First choice for stickers, print and the web. |
|
||||
| `logo/solidcraft-mark-mono-black.svg` | One-colour print on light material: screen-printed shirts, stamps, engraving, vinyl. |
|
||||
| `logo/solidcraft-mark-mono-white.svg` | One-colour print on dark material, such as a white print on a dark shirt. |
|
||||
| `logo/solidcraft-mark-512.png`, `-1024.png`, `-2048.png` | Transparent square PNGs for places that don't take SVG: avatars, social posts, slides, print shops that ask for a bitmap (send the 2048). |
|
||||
| `logo/solidcraft-wordmark.svg` | The name on its own, outlined to paths. |
|
||||
| `logo/solidcraft-lockup-horizontal.svg` | Mark and name side by side: headers, banners, shirt fronts, wide stickers. |
|
||||
| `web/source/og.png` | The existing link preview card for the docs site. |
|
||||
|
||||
The gaps between the faces are part of the logo. In the colour and one-colour files they are
|
||||
transparent, so the material shows through. For a die-cut sticker, cut around the outer shape
|
||||
and let the backing colour (white is best) fill the gaps.
|
||||
|
||||
## Colours
|
||||
|
||||
The mark's five faces each use a two-stop gradient from the icon source:
|
||||
|
||||
| Face | From | To |
|
||||
|---|---|---|
|
||||
| Top | `#51adff` | `#2b83f1` |
|
||||
| Left | `#258df5` | `#1461da` |
|
||||
| Right | `#1b66cf` | `#124596` |
|
||||
| Lower step (top) | `#459fff` | `#2379e7` |
|
||||
| Upper step (inner side) | `#1d63c6` | `#15499e` |
|
||||
|
||||
Other colours, all already used in the project:
|
||||
|
||||
| Colour | Hex | Where it comes from |
|
||||
|---|---|---|
|
||||
| Wordmark blue | `#15499e` | loading logo `--word` |
|
||||
| Interface accent | `#2678da` | light theme `accent` token (`assets/styles/ui/palettes.rs`) |
|
||||
| Light background | `#e8ebf0` | web loading screen (`assets/styles/web/loading.css`) |
|
||||
| Dark background | `#1f232b` | dark theme `file_background` token |
|
||||
| Black / white | `#000000` / `#ffffff` | one-colour versions only |
|
||||
|
||||
If a printer can't do gradients, use the one-colour files, or fill every face with `#2678da`.
|
||||
|
||||
## Clear space and size
|
||||
|
||||
- Keep empty space around the mark of at least one fifth of its height on every side. Treat
|
||||
the lockup the same way, measured from the mark.
|
||||
- Smallest mark: 16 px tall on screen, 12 mm tall in print. Below that the gaps fill in.
|
||||
- Smallest lockup: 120 px wide on screen, 40 mm wide in print.
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Don't recolour it outside the colours above.
|
||||
- Don't stretch, squash, rotate, skew or crop it. Scale it evenly.
|
||||
- Don't add shadows, outlines, glows, bevels or other effects.
|
||||
- Don't fill or close the gaps between the faces, or move the faces apart.
|
||||
- Don't rearrange the lockup, retype the name in another font, or put the mark on a busy
|
||||
background where it loses contrast.
|
||||
|
||||
## Name and logo
|
||||
|
||||
The SolidCraft code and these files are licensed MIT OR Apache-2.0 (see the [README](../../README.md#licence)),
|
||||
so you may copy and change them. The name SolidCraft and this logo identify the SolidCraft
|
||||
project. Use them to refer to the project, for example in an article, a talk, or a sticker for a
|
||||
meetup. If you publish a fork or a modified build, give it its own name and logo so people
|
||||
don't mistake it for SolidCraft.
|
||||
|
||||
The wordmark is set in IBM Plex Sans Bold (SIL Open Font License 1.1, the interface font in
|
||||
`assets/fonts/ibm-plex-sans/`) and outlined, so you don't need the font to print it.
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 2748.71 904.436" width="2749" height="904" role="img"><title>SolidCraft</title><defs><linearGradient id="sc1" x1="619.14" y1="179.734" x2="694.687" y2="396.761" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d63c6"/><stop offset="1" stop-color="#15499e"/></linearGradient><linearGradient id="sc2" x1="619.14" y1="413.244" x2="790.839" y2="605.547" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#459fff"/><stop offset="1" stop-color="#2379e7"/></linearGradient><linearGradient id="sc3" x1="632.876" y1="399.508" x2="783.971" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1b66cf"/><stop offset="1" stop-color="#124596"/></linearGradient><linearGradient id="sc4" x1="207.063" y1="303.357" x2="426.837" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#258df5"/><stop offset="1" stop-color="#1461da"/></linearGradient><linearGradient id="sc5" x1="385.63" y1="53.364" x2="522.989" y2="510.769" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#51adff"/><stop offset="1" stop-color="#2b83f1"/></linearGradient></defs><g><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z" fill="url(#sc1)"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z" fill="url(#sc2)"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z" fill="url(#sc3)"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z" fill="url(#sc4)"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z" fill="url(#sc5)"/></g><path fill="#15499e" transform="translate(1092.78 632.20) scale(0.36281)" d="M303 12Q212 12 147.5 -20Q83 -52 38 -101L138 -202Q173 -162 217.5 -142Q262 -122 313 -122Q369 -122 396 -145Q423 -168 423 -206Q423 -225 416.5 -240.5Q410 -256 392.5 -266.5Q375 -277 342 -281L273 -290Q200 -300 152.5 -326.5Q105 -353 82 -396.5Q59 -440 59 -498Q59 -561 90.5 -608.5Q122 -656 180.5 -683Q239 -710 321 -710Q400 -710 460 -684.5Q520 -659 563 -612L462 -510Q437 -539 401.5 -557.5Q366 -576 312 -576Q261 -576 236 -558Q211 -540 211 -508Q211 -484 219.5 -469Q228 -454 246.5 -446Q265 -438 294 -433L363 -422Q435 -411 482 -385Q529 -359 552 -317Q575 -275 575 -215Q575 -148 543.5 -97Q512 -46 451 -17Q390 12 303 12ZM926 12Q849 12 794 -21.5Q739 -55 709.5 -117Q680 -179 680 -263Q680 -348 709.5 -409Q739 -470 794 -503.5Q849 -537 926 -537Q1003 -537 1058 -503.5Q1113 -470 1142.5 -409Q1172 -348 1172 -263Q1172 -179 1142.5 -117Q1113 -55 1058 -21.5Q1003 12 926 12ZM926 -105Q970 -105 994 -132.5Q1018 -160 1018 -209V-316Q1018 -366 994 -393Q970 -420 926 -420Q883 -420 858.5 -393Q834 -366 834 -316V-209Q834 -160 858.5 -132.5Q883 -105 926 -105ZM1511 0H1444Q1368 0 1332.5 -37Q1297 -74 1297 -144V-740H1445V-115H1511ZM1694 -581Q1649 -581 1628.5 -602Q1608 -623 1608 -654V-676Q1608 -708 1628.5 -728.5Q1649 -749 1694 -749Q1739 -749 1759.5 -728.5Q1780 -708 1780 -676V-654Q1780 -623 1759.5 -602Q1739 -581 1694 -581ZM1620 0V-525H1768V0ZM2248 0V-88H2243Q2232 -45 2192.5 -16.5Q2153 12 2102 12Q2036 12 1991 -19.5Q1946 -51 1923 -112.5Q1900 -174 1900 -263Q1900 -352 1923 -413Q1946 -474 1991 -505.5Q2036 -537 2102 -537Q2153 -537 2192.5 -509Q2232 -481 2243 -437H2248V-740H2396V0ZM2151 -108Q2179 -108 2201 -117Q2223 -126 2235.5 -143.5Q2248 -161 2248 -185V-340Q2248 -364 2235.5 -381.5Q2223 -399 2201 -408Q2179 -417 2151 -417Q2109 -417 2081.5 -387.5Q2054 -358 2054 -308V-217Q2054 -168 2081.5 -138Q2109 -108 2151 -108ZM2844 12Q2750 12 2680.5 -28Q2611 -68 2573 -147Q2535 -226 2535 -344Q2535 -462 2573 -543.5Q2611 -625 2680.5 -667.5Q2750 -710 2844 -710Q2940 -710 3003.5 -670Q3067 -630 3105 -546L2974 -478Q2961 -522 2930.5 -548.5Q2900 -575 2844 -575Q2777 -575 2736.5 -531.5Q2696 -488 2696 -405V-293Q2696 -211 2736.5 -167Q2777 -123 2844 -123Q2900 -123 2933.5 -153.5Q2967 -184 2983 -227L3107 -155Q3068 -76 3004 -32Q2940 12 2844 12ZM3373 0H3225V-525H3373V-411H3378Q3384 -440 3400 -466Q3416 -492 3443.5 -508.5Q3471 -525 3512 -525H3538V-387H3501Q3458 -387 3429.5 -379.5Q3401 -372 3387 -355Q3373 -338 3373 -307ZM4105 0H4023Q3996 0 3972.5 -13.5Q3949 -27 3935 -53.5Q3921 -80 3921 -117V-130L3953 -92H3917Q3905 -41 3864 -14.5Q3823 12 3763 12Q3684 12 3642 -30.5Q3600 -73 3600 -141Q3600 -197 3627.5 -233Q3655 -269 3705.5 -287Q3756 -305 3825 -305H3908V-338Q3908 -376 3888 -398.5Q3868 -421 3821 -421Q3777 -421 3751 -402Q3725 -383 3708 -359L3620 -437Q3652 -484 3701 -510.5Q3750 -537 3832 -537Q3943 -537 3999.5 -487.5Q4056 -438 4056 -345V-115H4105ZM3908 -221H3836Q3793 -221 3771 -206Q3749 -191 3749 -162V-147Q3749 -119 3767 -105Q3785 -91 3818 -91Q3843 -91 3863 -98Q3883 -105 3895.5 -120Q3908 -135 3908 -159ZM4250 0V-410H4176V-525H4250V-587Q4250 -663 4290.5 -701.5Q4331 -740 4407 -740H4503V-625H4398V-525H4503V-410H4398V0ZM4877 0H4789Q4713 0 4673 -39Q4633 -78 4633 -153V-410H4559V-525H4596Q4626 -525 4637 -539.5Q4648 -554 4648 -581V-667H4781V-525H4885V-410H4781V-115H4877Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 32 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 791.188 904.436" width="791" height="904" role="img"><title>SolidCraft</title><g fill="#000000"><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 612 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 791.188 904.436" width="791" height="904" role="img"><title>SolidCraft</title><g fill="#ffffff"><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 612 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 791.188 904.436" width="791" height="904" role="img"><title>SolidCraft</title><defs><linearGradient id="sc1" x1="619.14" y1="179.734" x2="694.687" y2="396.761" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d63c6"/><stop offset="1" stop-color="#15499e"/></linearGradient><linearGradient id="sc2" x1="619.14" y1="413.244" x2="790.839" y2="605.547" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#459fff"/><stop offset="1" stop-color="#2379e7"/></linearGradient><linearGradient id="sc3" x1="632.876" y1="399.508" x2="783.971" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1b66cf"/><stop offset="1" stop-color="#124596"/></linearGradient><linearGradient id="sc4" x1="207.063" y1="303.357" x2="426.837" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#258df5"/><stop offset="1" stop-color="#1461da"/></linearGradient><linearGradient id="sc5" x1="385.63" y1="53.364" x2="522.989" y2="510.769" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#51adff"/><stop offset="1" stop-color="#2b83f1"/></linearGradient></defs><g><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z" fill="url(#sc1)"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z" fill="url(#sc2)"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z" fill="url(#sc3)"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z" fill="url(#sc4)"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z" fill="url(#sc5)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="38.0 -749.0 4847.0 761.0" width="4847" height="761" role="img"><title>SolidCraft</title><path fill="#15499e" d="M303 12Q212 12 147.5 -20Q83 -52 38 -101L138 -202Q173 -162 217.5 -142Q262 -122 313 -122Q369 -122 396 -145Q423 -168 423 -206Q423 -225 416.5 -240.5Q410 -256 392.5 -266.5Q375 -277 342 -281L273 -290Q200 -300 152.5 -326.5Q105 -353 82 -396.5Q59 -440 59 -498Q59 -561 90.5 -608.5Q122 -656 180.5 -683Q239 -710 321 -710Q400 -710 460 -684.5Q520 -659 563 -612L462 -510Q437 -539 401.5 -557.5Q366 -576 312 -576Q261 -576 236 -558Q211 -540 211 -508Q211 -484 219.5 -469Q228 -454 246.5 -446Q265 -438 294 -433L363 -422Q435 -411 482 -385Q529 -359 552 -317Q575 -275 575 -215Q575 -148 543.5 -97Q512 -46 451 -17Q390 12 303 12ZM926 12Q849 12 794 -21.5Q739 -55 709.5 -117Q680 -179 680 -263Q680 -348 709.5 -409Q739 -470 794 -503.5Q849 -537 926 -537Q1003 -537 1058 -503.5Q1113 -470 1142.5 -409Q1172 -348 1172 -263Q1172 -179 1142.5 -117Q1113 -55 1058 -21.5Q1003 12 926 12ZM926 -105Q970 -105 994 -132.5Q1018 -160 1018 -209V-316Q1018 -366 994 -393Q970 -420 926 -420Q883 -420 858.5 -393Q834 -366 834 -316V-209Q834 -160 858.5 -132.5Q883 -105 926 -105ZM1511 0H1444Q1368 0 1332.5 -37Q1297 -74 1297 -144V-740H1445V-115H1511ZM1694 -581Q1649 -581 1628.5 -602Q1608 -623 1608 -654V-676Q1608 -708 1628.5 -728.5Q1649 -749 1694 -749Q1739 -749 1759.5 -728.5Q1780 -708 1780 -676V-654Q1780 -623 1759.5 -602Q1739 -581 1694 -581ZM1620 0V-525H1768V0ZM2248 0V-88H2243Q2232 -45 2192.5 -16.5Q2153 12 2102 12Q2036 12 1991 -19.5Q1946 -51 1923 -112.5Q1900 -174 1900 -263Q1900 -352 1923 -413Q1946 -474 1991 -505.5Q2036 -537 2102 -537Q2153 -537 2192.5 -509Q2232 -481 2243 -437H2248V-740H2396V0ZM2151 -108Q2179 -108 2201 -117Q2223 -126 2235.5 -143.5Q2248 -161 2248 -185V-340Q2248 -364 2235.5 -381.5Q2223 -399 2201 -408Q2179 -417 2151 -417Q2109 -417 2081.5 -387.5Q2054 -358 2054 -308V-217Q2054 -168 2081.5 -138Q2109 -108 2151 -108ZM2844 12Q2750 12 2680.5 -28Q2611 -68 2573 -147Q2535 -226 2535 -344Q2535 -462 2573 -543.5Q2611 -625 2680.5 -667.5Q2750 -710 2844 -710Q2940 -710 3003.5 -670Q3067 -630 3105 -546L2974 -478Q2961 -522 2930.5 -548.5Q2900 -575 2844 -575Q2777 -575 2736.5 -531.5Q2696 -488 2696 -405V-293Q2696 -211 2736.5 -167Q2777 -123 2844 -123Q2900 -123 2933.5 -153.5Q2967 -184 2983 -227L3107 -155Q3068 -76 3004 -32Q2940 12 2844 12ZM3373 0H3225V-525H3373V-411H3378Q3384 -440 3400 -466Q3416 -492 3443.5 -508.5Q3471 -525 3512 -525H3538V-387H3501Q3458 -387 3429.5 -379.5Q3401 -372 3387 -355Q3373 -338 3373 -307ZM4105 0H4023Q3996 0 3972.5 -13.5Q3949 -27 3935 -53.5Q3921 -80 3921 -117V-130L3953 -92H3917Q3905 -41 3864 -14.5Q3823 12 3763 12Q3684 12 3642 -30.5Q3600 -73 3600 -141Q3600 -197 3627.5 -233Q3655 -269 3705.5 -287Q3756 -305 3825 -305H3908V-338Q3908 -376 3888 -398.5Q3868 -421 3821 -421Q3777 -421 3751 -402Q3725 -383 3708 -359L3620 -437Q3652 -484 3701 -510.5Q3750 -537 3832 -537Q3943 -537 3999.5 -487.5Q4056 -438 4056 -345V-115H4105ZM3908 -221H3836Q3793 -221 3771 -206Q3749 -191 3749 -162V-147Q3749 -119 3767 -105Q3785 -91 3818 -91Q3843 -91 3863 -98Q3883 -105 3895.5 -120Q3908 -135 3908 -159ZM4250 0V-410H4176V-525H4250V-587Q4250 -663 4290.5 -701.5Q4331 -740 4407 -740H4503V-625H4398V-525H4503V-410H4398V0ZM4877 0H4789Q4713 0 4673 -39Q4633 -78 4633 -153V-410H4559V-525H4596Q4626 -525 4637 -539.5Q4648 -554 4648 -581V-667H4781V-525H4885V-410H4781V-115H4877Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -60,6 +60,7 @@ circle_ttt false
|
||||
close false
|
||||
coil false
|
||||
colored_components false
|
||||
colored_components_off false
|
||||
combine false
|
||||
compare false
|
||||
component false
|
||||
@@ -160,7 +161,6 @@ min_radius false
|
||||
mirror false
|
||||
motion_link false
|
||||
move false
|
||||
nav_settings false
|
||||
new false
|
||||
normal false
|
||||
offset false
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Disable component colors</title>
|
||||
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#328bd3"/>
|
||||
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#236ead"/>
|
||||
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#b9ddf8"/>
|
||||
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
|
||||
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
|
||||
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
|
||||
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#328bd3"/>
|
||||
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#236ead"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#b9ddf8"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -79,6 +79,17 @@ pub(crate) fn snap_hint(painter: &egui::Painter, c: Pos2, kind: SnapKind, col: C
|
||||
painter.circle_filled(c, 1.3, col);
|
||||
}
|
||||
|
||||
/// A placed hole's drag handle: a ring round the hole's centre with a cross through it, filled
|
||||
/// while it is hovered or dragged.
|
||||
pub(crate) fn hole_handle(painter: &egui::Painter, c: Pos2, col: Color32, hot: bool) {
|
||||
let ic = Tokens::get().icons;
|
||||
let r = 6.0;
|
||||
painter.circle_filled(c, r + 3.5, ic.marker_halo);
|
||||
painter.circle(c, r, if hot { col } else { col.gamma_multiply(0.35) }, Stroke::new(1.6, col));
|
||||
let ink = Stroke::new(1.3, if hot { ic.highlight } else { col });
|
||||
painter.line_segment([c - vec2(r + 3.0, 0.0), c + vec2(r + 3.0, 0.0)], ink);
|
||||
painter.line_segment([c - vec2(0.0, r + 3.0), c + vec2(0.0, r + 3.0)], ink);
|
||||
}
|
||||
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
Some(match name {
|
||||
"Coincident" => "c_coincident",
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
"Bottom Right": "أسفل اليمين",
|
||||
"Boundary": "الحد",
|
||||
"Bounding box": "الصندوق المحيط",
|
||||
"Bridge": "جسر",
|
||||
"CHECK": "تحقق",
|
||||
"COMPONENTS": "المكوّنات",
|
||||
"CONFIGURATIONS": "التكوينات",
|
||||
@@ -964,7 +963,8 @@
|
||||
"cmd.inspect_analysis": "تحليل العرض",
|
||||
"cmd.inspect_center_of_mass": "مركز الكتلة",
|
||||
"cmd.inspect_clearance": "الخلوص",
|
||||
"cmd.inspect_colored_components": "ألوان المكوّنات",
|
||||
"cmd.inspect_colored_components": "تفعيل ألوان المكوّنات",
|
||||
"cmd.inspect_colored_components_disable": "تعطيل ألوان المكوّنات",
|
||||
"cmd.inspect_compare": "مقارنة الأجسام",
|
||||
"cmd.inspect_compare_picture": "مقارنة بصورة مرجعية",
|
||||
"cmd.inspect_continuity": "تحليل الاستمرارية",
|
||||
@@ -1296,7 +1296,6 @@
|
||||
"cmd.timeline_stop": "إيقاف السجل",
|
||||
"cmd.timeline_suppress": "تعطيل الميزة",
|
||||
"cmd.ui.assistant": "إظهار المساعد أو إخفاؤه",
|
||||
"cmd.view.navigator": "متصفح العرض المخصص",
|
||||
"cmd.view.render_browser": "متصفح التصيير",
|
||||
"cmd.view.render_dock": "نقل لوحة التصيير",
|
||||
"cmd.view.render_focus": "عرض تركيز التصيير",
|
||||
@@ -1395,7 +1394,6 @@
|
||||
"dialog.computing": "جارٍ الحساب… {seconds} ث",
|
||||
"dialog.could_not_be_checked": "تعذّر فحصه",
|
||||
"dialog.custom_axis": "↖ محور مخصص",
|
||||
"dialog.cylinder_face_tip": "وجه النموذج أو رأسه الذي تقوم عليه الأسطوانة: انقر هنا ثم انقر عليه في العرض. إذا تُرك فارغًا، تحدد X وY وZ موضعها.",
|
||||
"dialog.dimension_insert_hint": "انقر على بُعد قائد آخر لإدراج اسمه",
|
||||
"dialog.dimension_name": "البُعد: {name}",
|
||||
"dialog.dimension_step_down": "إنقاص بمقدار {step}",
|
||||
@@ -1404,8 +1402,6 @@
|
||||
"dialog.edit_title": "تعديل {title}",
|
||||
"dialog.no_shown_body": "لا يوجد جسم ظاهر لتغييره: أظهر جسمًا، أو اختر «جسم جديد»",
|
||||
"dialog.pattern_sketch_not_object": "اختر أجسامًا أو ميزات: الرسم ليس أيًّا منهما. كرّر منحنياته بأداة النمط أو المرآة داخل الرسم، أو ابثقه أولًا.",
|
||||
"dialog.primitive_point_tip": "نقطة تملأ X وY وZ: انقر هنا ثم على رأس أو حافة أو وجه من النموذج، أو على نقطة من رسم، أو على نقطة الأصل.",
|
||||
"dialog.reference_edge_tip": "حافة مستقيمة من الوجه المختار تُقاس منها المسافة أدناه: انقر هنا ثم على الحافة.",
|
||||
"dialog.select": "↖ تحديد",
|
||||
"dialog.selected_count": "↖ {count} محدد",
|
||||
"dialog.selection_constraint": "قيد {id}",
|
||||
@@ -1449,9 +1445,6 @@
|
||||
"each rail's curves must be in one sketch": "يجب أن تكون منحنيات كل دليل في رسم واحد",
|
||||
"feature computation ended without a result": "انتهى حساب الميزة دون نتيجة",
|
||||
"file.open.drawing_filter": "التصاميم وملفات CAD والشبكات والرسومات",
|
||||
"file.open.portal_fallback.no_portal_service": "منتقي الملفات الذي فُتح ليس تابعًا لسطح المكتب: خدمة منتقي الملفات في سطح المكتب غير متاحة.",
|
||||
"file.open.portal_fallback.no_session_bus": "منتقي الملفات الذي فُتح ليس تابعًا لسطح المكتب: لا يوجد ناقل جلسات D-Bus.",
|
||||
"file.open.portal_fallback.xwayland": "منتقي الملفات الذي فُتح ليس تابعًا لسطح المكتب: في XWayland ترفض البوابة النافذة الأصلية.",
|
||||
"file_load.built": "بُني حتى الآن: {built} من {total} ميزة.",
|
||||
"file_load.converting": "تحويل الجسم الصلب {done} من {total}",
|
||||
"file_load.inserting": "إدراج الهندسة",
|
||||
@@ -1612,7 +1605,7 @@
|
||||
"help.parts.resize": "يغيّر مقاس قطعة قياسية مدرجة أو طولها ويعيد بناءها، وتبقيها مفاصلها مثبتة في مكانها. استخدمه عندما يتبين أن برغيًا أطول من اللازم أو أن ثقبًا يحتاج إلى مقاس آخر.",
|
||||
"help.plastic.assign_rule": "يعيّن قاعدة بلاستيك لأجسام، فتأخذ ميزاتها البلاستيكية ميلها وخلوصها منها كقيم افتراضية. استخدمه لإعداد قطعة مقولبة مرة واحدة لكل ميزاتها.",
|
||||
"help.plastic.boss": "يضيف نتوءًا لمسمار على وجه: عمودًا مع ثقب اختياري وميل وتقويس عند الجذر وأضلاع تقوية. استخدمه لنقاط البراغي في الهياكل المقولبة أو المطبوعة.",
|
||||
"help.plastic.lip": "يضيف شفة أو أخدودًا مطابقًا على طول إطار جسم مفرَّغ (فرّغ الجسم بفتح أحد وجوهه أولًا، ثم اختر الإطار المحيط بتلك الفتحة). استخدمه ليتمركز نصفا الهيكل أحدهما على الآخر ويُغلقا بإحكام.",
|
||||
"help.plastic.lip": "يضيف شفة أو أخدودًا مطابقًا على طول حافة جسم مفرَّغ. استخدمه ليتمركز نصفا الهيكل أحدهما على الآخر ويُغلقا بإحكام.",
|
||||
"help.plastic.manage_rules": "ينشئ قواعد البلاستيك ويعدّلها: المادة وسماكة الجدار وأنصاف الأقطار والخلوص والميل. استخدمها للحفاظ على اتساق ميزات القطعة المقولبة.",
|
||||
"help.plastic.rest": "يضيف مسندًا على وجه: وسادة مستديرة أو مستطيلة، مصمتة أو مجوفة، مع ميل. استخدمه كدعامة أو مُباعِد في هيكل مقولب.",
|
||||
"help.plastic.snap_fit": "يضيف خطاف وصلة كبس على وجه، بطوله وسماكته ونتوء التثبيت. استخدمه لتثبيت القطع معًا بلا براغي.",
|
||||
@@ -2042,8 +2035,6 @@
|
||||
"nav.blender.help": "السحب بالزر الأوسط يدوّر العرض، وShift مع الأوسط يُزيحه، وCtrl مع الأوسط يكبّره؛ والسحب بالأيمن لا يحرك الكاميرا",
|
||||
"nav.creo.help": "السحب بالزر الأوسط يدوّر العرض، وShift مع الأوسط يُزيحه، وCtrl مع السحب العمودي بالأوسط يكبّره أو يصغّره (السحب لأسفل للتكبير)؛ وتدوير العجلة يكبّره أو يصغّره",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "يستمر السحب بالزر الأوسط أو الأيمن في التدوير أو التحريك أو التكبير كما بدأ حتى تحرير الزر، مهما ضُغطت المفاتيح أو حُرّرت في أثناء ذلك. عند الإيقاف: تقرر المفاتيح المضغوطة في كل لحظة من السحب.",
|
||||
"nav.drag_keeps_mode.label": "يحتفظ السحب بطريقة التنقل التي بدأ بها",
|
||||
"nav.free_orbit.help": "الفأرة والفأرة ثلاثية الأبعاد تدوّران العرض عبر القطبين وتحافظان على اللف. يُعطَّل التقاط المحاور بـ Shift.",
|
||||
"nav.free_orbit.label": "تدوير حر",
|
||||
"nav.free_orbit.snap_help": "الفأرة والفأرة ثلاثية الأبعاد تدوّران العرض عبر القطبين وتحافظان على اللف. اضغط مطولًا على Shift أثناء سحب التدوير لالتقاط اتجاه العرض واللف.",
|
||||
@@ -2280,9 +2271,6 @@
|
||||
"prefs.auto_operation_note": "ضم أو جسم جديد أو قص، حسب ما تلامسه المادة.",
|
||||
"prefs.axes": "المحاور",
|
||||
"prefs.axis": "المحور",
|
||||
"prefs.background_choose": "اختيار صورة…",
|
||||
"prefs.background_image": "الصورة",
|
||||
"prefs.background_unreadable": "تعذرت قراءة صورة الخلفية",
|
||||
"prefs.blur_cost": "تكلفة العرض النسبية. زيادة الأشرطة تعني عادةً مزيدًا من العمل.",
|
||||
"prefs.blur_device": "غير متاح على جهاز الرسوميات أو مسار العرض هذا",
|
||||
"prefs.blur_guard": "خُفّضت جودة التمويه للحفاظ على استجابة الإطارات. لم يتغير نصف القطر.",
|
||||
@@ -2304,8 +2292,6 @@
|
||||
"prefs.browser_eye_left": "أيقونة الإظهار بجانب سهم الطي",
|
||||
"prefs.browser_eye_left_note": "تعرض صفوف المستعرض أيقونة الإظهار/الإخفاء بجانب سهم الطي كما كانت من قبل، بدلًا من عمود عند نهاية الصفوف.",
|
||||
"prefs.browser_reveal_on_hover": "إظهار عند مرور المؤشر",
|
||||
"prefs.browser_rows_start_closed": "صفوف المستعرض تبدأ مغلقة",
|
||||
"prefs.browser_rows_start_closed_note": "تبدأ المواضع والمجلدات في المستعرض مغلقة، فيعرض التصميم الذي يضم مكونات كثيرة صفوفه العليا. يُفتح كل صف بسهمه أو بـ«توسيع الكل» أو بـ«البحث في المستعرض» الذي يفتح الصفوف المؤدية إلى الجسم فقط، ويغلق من جديد الصفوف التي فتحها البحث السابق. اختيار جسم في العرض ثلاثي الأبعاد يترك المستعرض كما هو. عند إيقافه تبدأ المجلدات مفتوحة، وتبدأ المواضع كما يحدّد خيار «المكوّنات مفتوحة من البداية»، ويفتح الاختيار في العرض ثلاثي الأبعاد المستعرضَ حتى صف الجسم، كما كان من قبل.",
|
||||
"prefs.browser_show_note": "«ظاهر» يُبقي المستعرض على الشاشة. «إظهار عند مرور المؤشر» يُخفيه حتى يمر المؤشر فوق مكانه أو فوق الشريط الرفيع عند حافته اليسرى، ويبقى مفتوحًا ما دام المؤشر داخله أو كانت قائمة منه مفتوحة.",
|
||||
"prefs.browser_shown": "ظاهر",
|
||||
"prefs.built_in": "مدمج",
|
||||
@@ -2351,8 +2337,8 @@
|
||||
"prefs.dialogs_edge": "عند حافة العرض",
|
||||
"prefs.dialogs_left_column": "في العمود الأيسر",
|
||||
"prefs.dialogs_note": "مكان فتح حوارات الأوامر: مثبتة عند حافة العرض، أو في العمود الأيسر مكان المستعرض والسجل، اللذين يعودان كما كانا عند إغلاق الحوار.",
|
||||
"prefs.dimension_step_angle": "ما تضيفه أسهم النافذة والسهمان لأعلى ولأسفل والعجلة إلى الزاوية",
|
||||
"prefs.dimension_step_length": "ما تضيفه أسهم النافذة والسهمان لأعلى ولأسفل والعجلة إلى الطول",
|
||||
"prefs.dimension_step_angle": "ما تضيفه أسهم النافذة وأعلى/أسفل والعجلة إلى الزاوية",
|
||||
"prefs.dimension_step_length": "ما تضيفه أسهم النافذة وأعلى/أسفل والعجلة إلى الطول",
|
||||
"prefs.dimension_steps": "خطوات النافذة",
|
||||
"prefs.dimension_values": "قيم الأبعاد",
|
||||
"prefs.dimension_values_note": "مكان كتابة قيمة البعد. للنافذة أسهم تغيّرها بخطوات؛ تضع أداة البعد النص أولًا.",
|
||||
@@ -2437,10 +2423,6 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "سهما التدوير المنحنيان عند زاوية المكعب يدوران العرض داخل مستوى الشاشة. عند التشغيل: سهم التدوير مع عقارب الساعة يدور عكسها، وسهم العكس يدور معها. عند الإيقاف: يدور كل سهم بالعرض حسب انحنائه.",
|
||||
"prefs.invert_navigator_arrows": "عكس أسهم تدوير مكعب العرض",
|
||||
"prefs.invert_navigator_arrows_note": "تدوّر الأسهم المحيطة بالمكعب الكاميرا في الاتجاه الذي تشير إليه، كما في Fusion وInventor: السهم الموجود على اليسار يشير إلى اليمين ويدوّر الكاميرا إلى اليمين. عند الإيقاف: يدوّر السهم المكعب نحو الجهة التي يقع فيها، فيدوّر السهم الأيسر الكاميرا إلى اليسار. لا يتأثر سهما الدوران المنحنيان.",
|
||||
"prefs.keep_unsaved_also_asks": "السؤال أيضًا عند إغلاق علامة التبويب",
|
||||
"prefs.keep_unsaved_also_asks_tip": "عند إيقاف هذا الخيار، تُغلق علامة التبويب دون مربع المتصفح بعد تحديث نسخة كل تصميم غير محفوظ.",
|
||||
"prefs.keep_unsaved_in_browser": "الاحتفاظ بالعمل غير المحفوظ في هذا المتصفح",
|
||||
"prefs.keep_unsaved_in_browser_tip": "تُحدَّث التغييرات غير المحفوظة لكل تصميم مفتوح باستمرار في التخزين الخاص بهذا المتصفح. وتعرض الزيارة التالية استعادتها.",
|
||||
"prefs.keep_work_on_tab_switch": "إبقاء مربعات الحوار والأدوات مفتوحة عند التبديل بين التصاميم",
|
||||
"prefs.keep_work_on_tab_switch_note": "تحتفظ كل علامة تبويب تصميم بمربع الحوار أو الأداة المفتوحة، بما حددته وكتبته فيها، وبعلامة تبويب شريط الأدوات. وتجدها كما هي عند العودة إلى التصميم. عند الإيقاف: يؤدي التبديل بين التصاميم إلى إلغائها وعرض علامة التبويب «النموذج».",
|
||||
"prefs.labels": "التسميات",
|
||||
@@ -2572,8 +2554,6 @@
|
||||
"prefs.section_hatch_by_part": "حسب الجزء",
|
||||
"prefs.section_hatch_one": "نمط واحد",
|
||||
"prefs.select": "تحديد",
|
||||
"prefs.select_hidden_edges": "تحديد الحواف المخفية في نمط «مظلل مع الحواف المخفية»",
|
||||
"prefs.select_hidden_edges_help": "يتيح تحديد الحواف عبر الأوجه عند تفعيل نمط «مظلل مع الحواف المخفية»، بما في ذلك عند استخدام التقويس. عند الإيقاف يبقى سلوك التحديد كما في v0.30.1. تظل الأجسام المخفية غير قابلة للتحديد.",
|
||||
"prefs.select_one_item": "تحديد عنصر واحد",
|
||||
"prefs.select_other_hidden": "تحديد العناصر الهندسية المخفية باستخدام «تحديد عنصر آخر»",
|
||||
"prefs.select_other_hidden_help": "عند التفعيل: تضمين الأجسام والمخططات المخفية والحواف المحجوبة قرب المؤشر. عند التعطيل: استخدام القائمة الأصلية للعناصر الهندسية المرئية أسفل المؤشر.",
|
||||
@@ -2598,8 +2578,6 @@
|
||||
"prefs.sketch.click_selects": "النقر يحدد",
|
||||
"prefs.sketch.conflict_colour": "لون التعارض",
|
||||
"prefs.sketch.drawing": "الرسم",
|
||||
"prefs.sketch.ellipse_axes": "رسم القطوع الناقصة مع محاورها",
|
||||
"prefs.sketch.ellipse_axes_note": "المحوران الأكبر والأصغر كخطوط مساعدة، لتحديد أبعاد أنصاف المحاور والمحاور. عند الإيقاف: القطع الناقص وحده، ويعطي D عليه نصف قطره الأصغر أينما سُحبت القيمة، مرسومًا ومُعلَّمًا بـ R كما في السابق.",
|
||||
"prefs.sketch.exit_keep": "إبقاء",
|
||||
"prefs.sketch.exit_previous": "العودة إلى العرض السابق",
|
||||
"prefs.sketch.exit_unless_rotated": "إلا إذا تم التدوير",
|
||||
@@ -2769,12 +2747,6 @@
|
||||
"recovery.just_now": "الآن",
|
||||
"recovery.min_ago": "قبل {count} دقيقة",
|
||||
"recovery.never_saved": "لم يُحفظ قط",
|
||||
"recovery.open_saved": "فتح النسخة المحفوظة",
|
||||
"recovery.restore": "استعادة",
|
||||
"recovery.restore_intro": "التغييرات التي لم تُحفظ عند آخر إغلاق لهذه الصفحة محفوظة في هذا المتصفح.",
|
||||
"recovery.restore_row": "{name}، تغيّر {when}",
|
||||
"recovery.restore_title": "استعادة التصاميم غير المحفوظة؟",
|
||||
"recovery.restore_title_one": "استعادة التصميم غير المحفوظ: {name}، تغيّر {when}؟",
|
||||
"refused.change": "تعذّر تغيير {body}.",
|
||||
"refused.cut": "تعذّر قطع {body}.",
|
||||
"refused.face_curved": "أحد أوجهه المنحنية لا يمكن استخدامه في النمذجة.",
|
||||
@@ -2789,9 +2761,9 @@
|
||||
"render.adaptive_absolute": "الحد الأدنى المطلق لخطأ RGB",
|
||||
"render.adaptive_coverage": "هدف خطأ التغطية",
|
||||
"render.adaptive_enable": "توزيع العيّنات باستخدام عيّنات استكشافية مستقلة",
|
||||
"render.adaptive_help": "تُهمل العينات الاستطلاعية المستقلة، ثم يُحدد لكل بكسل 16 عينة إنتاج على الأقل حتى قيمة العينات لكل بكسل. تشمل حدود موارد CPU المرحلتين. يوجه تباين RGB والتغطية التوزيع؛ وقد تبقى ضوضاء في البكسلات التي بلغت الحد. يحصي التقرير النهائي العينات الفعلية.",
|
||||
"render.adaptive_help": "على GPU فقط. تُستبعد العيّنات الاستكشافية المستقلة، ثم تُوزَّع 16 عيّنة إنتاجية أو أكثر لكل بكسل حتى قيمة «عيّنات لكل بكسل». يوجّه تباين RGB والتغطية هذا التوزيع؛ قد تفوت المسارات النادرة وقد تبقى البكسلات المحدودة مشوشة. يحصي التقرير النهائي العمل الفعلي. لا ضمان معتمد للتشويش أو للزمن.",
|
||||
"render.adaptive_minimum": "الحد الأدنى للعيّنات الإنتاجية",
|
||||
"render.adaptive_pending": "يتوفر التوزيع التكيفي بعينات استطلاعية مستقلة على CPU وGPU. تتطلب طبقات فرينل الشرطية GPU. لا يضمن التوزيع الإحصائي التقارب.",
|
||||
"render.adaptive_pending": "التوزيع التكيّفي على GPU بعيّنات استكشافية مستقلة وطبقات فرينل الشرطية متاحة. التقارب المعتمد وضمانات الأحداث النادرة والنقل التكيّفي على CPU غير متاحة بعد.",
|
||||
"render.adaptive_pilots": "العيّنات الاستكشافية لكل بكسل",
|
||||
"render.adaptive_relative": "هدف خطأ RGB النسبي",
|
||||
"render.add_ground": "إضافة أرضية ملائمة إلى الإعداد",
|
||||
@@ -2852,7 +2824,7 @@
|
||||
"render.appearance_editor.replace": "استبدال الخصائص البصرية المحفوظة لهذا الجسم",
|
||||
"render.appearance_editor.revalidate": "إعادة التحقق من هذا الجسم",
|
||||
"render.appearance_editor.save": "حفظ هذه الخصائص البصرية",
|
||||
"render.appearance_editor.save_reason": "لحفظ الزجاج، فعّله على جسم حالي يستخدم CPU أو GPU. للطلاء الشفاف استخدم حفظ الطلاء مع اللقطة. تبقى ملفات الصور المحلية مؤقتة.",
|
||||
"render.appearance_editor.save_reason": "يتطلب حفظ هذه الخصائص البصرية زجاجًا فيزيائيًا على جسم حالي في بطاقة الرسومات. تبقى الطبقة الشفافة وخرائط الصور المحلية مؤقتة.",
|
||||
"render.appearance_editor.saved": "محفوظ مع هذه اللقطة",
|
||||
"render.appearance_editor.saved_state": "هذه الخصائص البصرية محفوظة مع هذه اللقطة.",
|
||||
"render.appearance_editor.scatter_reason": "التشتت الداخلي يتطلب حدًّا لجسم مصمت مغلق.",
|
||||
@@ -2948,7 +2920,7 @@
|
||||
"render.children.copy_request": "نسخ JSON الطلب",
|
||||
"render.children.coverage_error": "خطأ التغطية",
|
||||
"render.children.cpu": "تتبّع الأشعة على المعالج",
|
||||
"render.children.denoise": "تقليل تشويش الصورة",
|
||||
"render.children.denoise": "تقليل التشويش في الصورة النهائية",
|
||||
"render.children.depth": "عمق المسار",
|
||||
"render.children.distortion": "التشويه",
|
||||
"render.children.edge_samples": "عينات الحواف",
|
||||
@@ -3080,7 +3052,7 @@
|
||||
"render.cpu_point_help": "حتى ثمانية أضواء نقطية تُضاف إلى الإضاءة الاتجاهية المختارة. المسافات بالمليمتر؛ ويجب ألا يتجاوز الحد الأدنى المسافة المرجعية.",
|
||||
"render.cpu_primary_samples": "عيّنات الحواف لكل بكسل",
|
||||
"render.cpu_progressive": "CPU تدريجي",
|
||||
"render.cpu_progressive_help": "تُجمع عينات كاميرا CPU الجديدة ويُعرض كل تمرير مكتمل. تستخدم الانعكاسات المحددة محاولة واحدة لكل عينة كاميرا؛ ويحتفظ الزجاج المعتمد والتباين الاتجاهي المستوي بحدودهما الحالية. تتشارك العينات الاستطلاعية التكيفية والإنتاج ميزانية ملف CPU.",
|
||||
"render.cpu_progressive_help": "يُراكم مُكامل CPU الحالي على تمريرات من عيّنة أساسية واحدة. الانعكاسات المختارة تتطلب عيّنة واحدة؛ والأضواء المساحية والعوازل وتباين الخواص تتطلب «صورة واحدة».",
|
||||
"render.cpu_progressive_limits": "بحد أقصى 1,048,576 بكسل و16,777,216 عملية تقييم بكسل × عيّنة. تغيير العيّنات لا يغيّر حجم الصورة تلقائيًا أبدًا.",
|
||||
"render.cpu_progressive_seed": "بذرة عيّنات CPU",
|
||||
"render.cpu_progressive_spp": "عيّنات لكل بكسل",
|
||||
@@ -3256,7 +3228,7 @@
|
||||
"render.export_display": "تصدير: PNG للعرض",
|
||||
"render.export_image_label": "تصدير الصورة",
|
||||
"render.export_linear": "تصدير: EXR خطي خام",
|
||||
"render.exr_pending": "يمكن لتتبع الأشعة على CPU وتتبع المسارات على GPU الاحتفاظ بـ EXR خطي خام. الضغط والصور متعددة الطبقات وتمريرات التصيير المساعدة غير متاحة بعد.",
|
||||
"render.exr_pending": "يستطيع GPU الاحتفاظ بـ EXR خطي خام. الضغط والصور متعددة الطبقات وتمريرات التصيير المساعدة غير متاحة بعد.",
|
||||
"render.film_grain": "حبيبات فوتوغرافية",
|
||||
"render.fit": "ملاءمة المنتج",
|
||||
"render.fixed_camera": "الكاميرا",
|
||||
@@ -3296,11 +3268,9 @@
|
||||
"render.frame_wide": "16:9 · عريض",
|
||||
"render.gpu_depth": "أقصى عمق للمسار",
|
||||
"render.gpu_options_help": "تُحفظ العيّنات والعمق والبذرة والتعريض مع هذا الإعداد. تُحفظ طريقة الصورة النشطة فقط.",
|
||||
"render.gpu_path_crashed": "أُغلق SolidCraft أثناء إعداده على محوّل الرسوميات هذا في المرة السابقة، على الأرجح داخل برنامج تشغيل الرسوميات، لذا لن يُعاد إعداده تلقائيًا. استخدم تتبع الأشعة على المعالج المركزي بدلًا منه، أو حاول مرة أخرى (يعيد تحديث برنامج التشغيل المحاولة أيضًا).",
|
||||
"render.gpu_path_failed": "متتبع المسارات على GPU غير متاح",
|
||||
"render.gpu_path_first_setup": "قد يستغرق الإعداد الأول على وحدة GPU هذه عدة دقائق؛ ويبقى SolidCraft قابلًا للاستخدام أثناءها. تعيد المرات اللاحقة استخدام المظللات المترجمة حيث يحتفظ بها برنامج التشغيل.",
|
||||
"render.gpu_path_preparing": "متتبع المسارات على GPU: ترجمة المظللات…",
|
||||
"render.gpu_path_retry": "حاول مرة أخرى",
|
||||
"render.gpu_seed": "البذرة العشوائية",
|
||||
"render.gpu_unavailable": "تتبع المسارات على GPU غير جاهز على هذا الجهاز.",
|
||||
"render.ground_apply": "ملاءمة الأرضية وحفظها",
|
||||
@@ -3619,7 +3589,7 @@
|
||||
"render.method_cpu": "تتبع الأشعة على CPU",
|
||||
"render.method_gpu": "تتبع المسارات على GPU",
|
||||
"render.method_limit_raster": "التنقيط يستخدم معاينة الاستوديو المعتمة الثابتة. اختر طريقة تتبع أشعة صريحة لعناصر التحكم البصرية المدعومة.",
|
||||
"render.method_raster": "تصيير نقطي على CPU (تخطيطي)",
|
||||
"render.method_raster": "تنقيط على CPU",
|
||||
"render.navigation_help": "تتحرك الكاميرا كما في التصميم (التفضيلات › التنقل). «حفظ الإعداد» يسجّل الكاميرا الحالية.",
|
||||
"render.nested_enable": "عوازل متداخلة معتمدة",
|
||||
"render.nested_help": "حتى 16 طبقة حاوية؛ يجب أن يكون كل أصل محدبًا وكل فرع داخله تمامًا. التقاطعات والأسطح المتلامسة والصفائح الرقيقة ونفاذ الظل المستقيم تُرفض. حالة وجود الكاميرا في الداخل تتبع التسلسل المعتمد. لا يُنشئ سطح سائل ولا يدمج حدود CAD المتلامسة.",
|
||||
@@ -3647,7 +3617,7 @@
|
||||
"render.output_exr_unavailable": "إعداد مخرجات EXR غير متاح هنا. تصدير EXR الخطي يتطلب صورة GPU مكتملة بنسخة رئيسية خطية محتفظ بها.",
|
||||
"render.output_fit": "ملاءمة",
|
||||
"render.output_format": "الصيغة",
|
||||
"render.output_format_status": "PNG هو صورة العرض المكتملة بالضبط. يحتفظ EXR الاختياري على CPU أو GPU بالإشعاع الخام الخطي للمشهد بصيغة FLOAT32 قبل مرشحات العرض. لا يستبدل التصدير أبدًا الصورة بذاكرة المعاينة المؤقتة أو بمهمة مختلفة.",
|
||||
"render.output_format_status": "PNG هو صورة العرض المكتملة بالضبط. EXR الاختياري على GPU يحتفظ بالإشعاع الخام الخطي للمشهد بصيغة FLOAT32 قبل مرشحات العرض. التصدير لا يستبدل أبدًا ذاكرة المعاينة المؤقتة أو مهمة أخرى.",
|
||||
"render.output_grading": "تدرّج الألوان",
|
||||
"render.output_image": "الصورة",
|
||||
"render.output_largest": "الأقصى {width} × {height}",
|
||||
@@ -3732,7 +3702,7 @@
|
||||
"render.quality_custom": "مخصص",
|
||||
"render.quality_draft": "مسودة",
|
||||
"render.quality_high": "عالية",
|
||||
"render.quality_projection_help": "تغيّر هذه المراجعة الدقة وميزانية العينات وعمق المسار والبذرة وتقليل التشويش فقط. تُحفظ الإعدادات الأخرى؛ ولا تثبت الميزانية تقارب الصورة.",
|
||||
"render.quality_projection_help": "إعدادات Q الجاهزة تتضمن متطلبات مستقبلية. هذا الإجراء يُسقط فقط ميزانية PNG مدعومة بعيّنات ثابتة، ويُغفل صراحةً الإيقاف التكيّفي والإخراج الخطي الخام وحدود النفاذ المستقلة.",
|
||||
"render.quality_review": "مراجعة ميزانية العيّنات",
|
||||
"render.queue_pending": "لا توجد في هذه الواجهة قائمة إنتاج تلقائية ولا مُصيّر للرسوم المتحركة. تبقى الصور الثابتة المكتملة قابلة للتصدير كلٌّ على حدة.",
|
||||
"render.radial_assign": "تعيين مظهر",
|
||||
@@ -3763,7 +3733,7 @@
|
||||
"render.result_navigation": "الإزاحة والتكبير يتبعان تفضيلات التنقل. Space مع السحب يُزيح العرض.",
|
||||
"render.result_open": "فتح الصورة المكتملة في اللوحة الرئيسية.",
|
||||
"render.result_preview": "المعاينة الحية",
|
||||
"render.result_provisional": "صورة مؤقتة",
|
||||
"render.result_provisional": "صورة CPU مؤقتة",
|
||||
"render.result_provisional_help": "نقطة التحقق هذه غير قابلة للتصدير. يحتفظ التصدير بآخر نتيجة مكتملة؛ والإلغاء يعود إلى تلك النتيجة.",
|
||||
"render.result_reduced_display": "قُلّصت معاينة العرض إلى حد الجهاز أو الذاكرة المؤقتة لمنطقة العرض. يحتفظ PNG المحفوظ والتصدير بالدقة الكاملة؛ والفحص الأصلي بنسبة 100% غير متاح لهذا العرض المقلّص.",
|
||||
"render.result_retained": "تُعرض آخر صورة مكتملة؛ ولم يستبدلها تصيير أحدث.",
|
||||
@@ -3927,7 +3897,7 @@
|
||||
"render.stage_panels.prepare_plan": "تجهيز خطة الإنشاء",
|
||||
"render.stage_panels.projects": "ما يُسقط",
|
||||
"render.stage_panels.quality_ack": "أقرّ بالميزانية الثابتة وما تُغفله",
|
||||
"render.stage_panels.quality_omissions": "لا تضبط هذه الميزانية أهداف الخطأ التكيفي أو حدود النفاذ المستقلة أو قواعد الروليت الروسي أو الاحتفاظ بالأصل الخام، ولا تضمن تقارب الصورة.",
|
||||
"render.stage_panels.quality_omissions": "التوقف التكيّفي وأهداف الخطأ، وعمق نفاذ منفصل، وقواعد الروليت الروسية، والإخراج الخطي الخام، ونسخة EXR الأصلية، وشهادة التقارب.",
|
||||
"render.stage_panels.reference": "مرجع",
|
||||
"render.stage_panels.references": "مراجع",
|
||||
"render.stage_panels.remove_optics": "إزالة البصريات",
|
||||
@@ -3971,7 +3941,7 @@
|
||||
"render.stage_panels.subtitle_environment": "بيئة HDR أصلية · المشهد بأكمله",
|
||||
"render.stage_panels.subtitle_maps": "خرائط مكانية مُعدّة · معاينة مؤقتة",
|
||||
"render.stage_panels.subtitle_physical": "خطة للقراءة فقط · لا تُنشأ مادة ولا تُعيَّن",
|
||||
"render.stage_panels.subtitle_quality": "ميزانية عينات GPU · الحقول المدرجة فقط",
|
||||
"render.stage_panels.subtitle_quality": "ميزانية GPU ثابتة · تُغفل المتطلبات المستقبلية",
|
||||
"render.stage_panels.subtitle_rig": "طقم إضاءة مُعدّ · باعثات وبطاقات سلبية",
|
||||
"render.stage_panels.subtitle_saved_maps": "خرائط مُعدّة · محفوظة مع هذه اللقطة",
|
||||
"render.stage_panels.subtitle_stage": "أرضية ومنصة فيزيائيتان · ملائمتان للمنتج",
|
||||
@@ -3996,7 +3966,7 @@
|
||||
"render.studio_area_help": "حتى أربعة مستطيلات أحادية الجانب. نصفا الضلعين المتعامدين U وV يحددان الحجم؛ وحاصل U × V يواجه الجهة المضاءة. الإحداثيات بمليمترات فضاء العالم.",
|
||||
"render.studio_center": "المركز X, Y, Z (mm)",
|
||||
"render.studio_denoise": "تقليل التشويش في الصورة النهائية",
|
||||
"render.studio_denoise_help": "تستخدم الصور التدريجية والنهائية مرشحًا يحافظ على الحواف؛ وقد تصبح التفاصيل الدقيقة أنعم. لا يُطبّق المرشح على الزجاج والأسطح ذات الانعكاس الحاد.",
|
||||
"render.studio_denoise_help": "مرشح نهائي يراعي الحواف؛ وقد تلين التفاصيل الدقيقة. الزجاج والأسطح المرآوية الحادة لا يمر عليها المرشح.",
|
||||
"render.studio_direction": "الاتجاه نحو الشمس (X, Y, Z)",
|
||||
"render.studio_environment": "البيئة",
|
||||
"render.studio_environment_bounds": "يجب أن يكون دوران البيئة قيمة محدودة ضمن ±1,000,000 راديان والشدة بين 0 و100.",
|
||||
@@ -4147,7 +4117,7 @@
|
||||
"settings.display_quality_note": "المستويات الأدق ترسم الأوجه والحواف المنحنية بنعومة أكبر وتستغرق وقتًا أطول لإنشاء الشبكة. يتغير العرض فقط، لا التصميم ولا الملفات المصدّرة.",
|
||||
"settings.double_click_components": "النقر المزدوج على مكوّن لتنشيطه",
|
||||
"settings.frame_rate_details_note": "يضيف رسمًا لأزمنة الإطارات الأخيرة مع المثلثات المرسومة فوقه، ومدرّجًا تكراريًا لأزمنة الإطارات، وما رسمته وحدة GPU. انقر معدل الإطارات في العرض لإظهارها أو إخفائها.",
|
||||
"settings.frame_rate_note": "يعرض الإطارات في الثانية في الزاوية اليمنى السفلية من العرض.",
|
||||
"settings.frame_rate_note": "يعرض الإطارات في الثانية في الزاوية اليمنى السفلية من العرض. أثناء تفعيله يُعاد رسم العرض باستمرار.",
|
||||
"settings.insert_lists_open_designs": "إدراج تصميم يسرد التصاميم المفتوحة أولًا",
|
||||
"settings.measure_axis_distances": "مسافات المحاور في القياس",
|
||||
"settings.measure_axis_distances_help": "إظهار مركّبات الأبعاد على محاور العالم بجانب المسافة الكلية أثناء فتح أداة القياس.",
|
||||
@@ -4198,32 +4168,6 @@
|
||||
"sketch.dof_left": "تبقّت {count} درجات حرية",
|
||||
"sketch.dof_left_one": "تبقّت {count} درجة حرية",
|
||||
"sketch.draw_curves": "الرسم كـ {linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "يحتاج المنحنى المغلق إلى نقطتي تقاطع لقصّه",
|
||||
"sketch.error.nothing_to_break": "لا يوجد هنا ما يمكن كسره",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` منحنى إزاحة، وزواياه تتبع منحنياته المصدر: قوّس زوايا المنحنيات المصدر أو اشطفها قبل الإزاحة، أو فك ارتباط الإزاحة أولًا",
|
||||
"sketch.error.offset_trim": "`{id}` منحنى إزاحة، وأطرافه تتبع منحنياته المصدر: عدّل المنحنيات المصدر بدلًا من ذلك، أو فك ارتباط الإزاحة أولًا",
|
||||
"sketch.hint.arc.center_point": "قوس: انقر المركز ثم البداية، ودُر في أي اتجاه وانقر النهاية، أو اكتب زاوية القوس بإشارة موجبة أو سالبة",
|
||||
"sketch.hint.arc.three_point": "قوس: انقر البداية ثم النهاية، ثم نقطة على القوس",
|
||||
"sketch.hint.arc3d": "قوس ثلاثي الأبعاد: انقر البداية ثم النهاية، ثم نقطة على القوس (يمكن أن تقع النقاط خارج مستوى الرسم)",
|
||||
"sketch.hint.break": "كسر: انقر على منحنى في الموضع الذي يُقسم عنده",
|
||||
"sketch.hint.circle.center": "دائرة: انقر المركز، ثم نقطة على الدائرة",
|
||||
"sketch.hint.constraint.generic": "قيد: اختر عناصر الرسم",
|
||||
"sketch.hint.constraint.horizontal_vertical": "أفقي/رأسي: اختر خطًا، أو نقطتين، أو نقطة وخطًا؛ اضغط Shift مع الاستمرار لاختيار منتصف خط",
|
||||
"sketch.hint.constraint.midpoint": "نقطة المنتصف: اختر نقطة وخطًا، أو خطين لتوسيط الثاني على الأول",
|
||||
"sketch.hint.dimension": "البُعد: اختر عنصرًا أو عنصرين واكتب القيمة (أو انقر لوضعه أولًا)؛ انقر على بُعد لتعديله؛ اضغط Enter للتطبيق",
|
||||
"sketch.hint.extend": "تمديد: انقر على منحنى قرب الطرف المراد تمديده (Shift: انقر منحنيين لتمديدهما معًا حتى نقطة التقائهما)",
|
||||
"sketch.hint.fallback": "انقر في الرسم",
|
||||
"sketch.hint.line": "خط: انقر النقاط؛ انقر بالزر الأيمن أو اضغط Esc للإنهاء",
|
||||
"sketch.hint.line3d": "خط ثلاثي الأبعاد: انقر نقاطًا في الفضاء؛ انقر بالزر الأيمن أو اضغط Esc للإنهاء (يمكن أن تقع النقاط خارج مستوى الرسم)",
|
||||
"sketch.hint.open_done": "تم الفتح",
|
||||
"sketch.hint.point3d": "نقطة ثلاثية الأبعاد: انقر حيث توضع النقطة ثلاثية الأبعاد",
|
||||
"sketch.hint.rectangle.center": "مستطيل من المركز: انقر المركز، ثم زاوية",
|
||||
"sketch.hint.rectangle.rounded": "مستطيل مستدير الزوايا: انقر زاويتين متقابلتين (اضغط Tab للانتقال إلى خانة نصف القطر وكتابة نصف قطر الزوايا)",
|
||||
"sketch.hint.rectangle.three_point": "مستطيل بثلاث نقاط: انقر نقطتين على أحد الأضلاع، ثم العرض",
|
||||
"sketch.hint.rectangle.two_point": "مستطيل: انقر زاويتين متقابلتين",
|
||||
"sketch.hint.slot": "شق: انقر الطرفين، ثم العرض",
|
||||
"sketch.hint.spline3d": "منحنى شريحي ثلاثي الأبعاد: انقر نقاط الملاءمة في الفضاء ثلاثي الأبعاد؛ انقر بالزر الأيمن أو اضغط Esc للإنهاء",
|
||||
"sketch.hint.trim": "قص: انقر على الجزء المراد إزالته من المنحنى، أو اسحب عبر الأجزاء المراد إزالتها",
|
||||
"sketch.make_selection": "تحويل التحديد إلى {linetype}",
|
||||
"sketch.open_ends": "مفتوحة {count}",
|
||||
"sketch.status_conflict": "تعارض القيود",
|
||||
@@ -4445,7 +4389,6 @@
|
||||
"ui.3d_sketch": "رسم ثلاثي الأبعاد",
|
||||
"ui.activate": "تنشيط",
|
||||
"ui.active_component": "المكوّن النشط",
|
||||
"ui.addToQuickAccess": "إضافة إلى الوصول السريع",
|
||||
"ui.add_configuration": "إضافة تكوين",
|
||||
"ui.add_parameter": "إضافة معامل",
|
||||
"ui.agent_chat": "محادثة الوكيل",
|
||||
@@ -5074,7 +5017,6 @@
|
||||
"ui.heading_construct": "إنشاء مرجعي",
|
||||
"ui.heading_create": "إنشاء",
|
||||
"ui.heading_create_sketch": "إنشاء رسم",
|
||||
"ui.heading_custom_view_navigator": "متصفح العرض المخصص",
|
||||
"ui.heading_cylinder": "أسطوانة",
|
||||
"ui.heading_design": "التصميم",
|
||||
"ui.heading_draft": "ميل السحب",
|
||||
@@ -5361,7 +5303,6 @@
|
||||
"ui.quality": "الجودة",
|
||||
"ui.quantity": "الكمية",
|
||||
"ui.quantity_2": "الكمية 2",
|
||||
"ui.quick_access_full": "يتسع شريط الوصول السريع لـ {count} أمرًا؛ أزل أحدها لإضافة آخر",
|
||||
"ui.quit": "خروج",
|
||||
"ui.radial_context_menu": "قائمة سياقية دائرية",
|
||||
"ui.radius": "نصف القطر",
|
||||
@@ -5379,7 +5320,6 @@
|
||||
"ui.relink_not_linked": "هذا المكوّن غير مرتبط بتصميم مصدر.",
|
||||
"ui.relink_select_one": "حدد مكوّنًا مرتبطًا واحدًا أو إحدى نسخه أولًا.",
|
||||
"ui.relink_source_all": "إعادة ربط المصدر (كل النسخ)…",
|
||||
"ui.removeFromQuickAccess": "إزالة من الوصول السريع",
|
||||
"ui.removeFromToolbar": "إزالة من شريط الأدوات",
|
||||
"ui.removeSection": "إزالة المقطع",
|
||||
"ui.render": "التصيير",
|
||||
@@ -5388,8 +5328,6 @@
|
||||
"ui.repeat_nothing": "لا يوجد ما يُكرَّر بعد: ابدأ أداة أو مربع حوار أولًا.",
|
||||
"ui.replace_with_link_done": "أصبحت النسخ الآن مواضع للتصميم المرتبط.",
|
||||
"ui.report_a_bug": "الإبلاغ عن خطأ",
|
||||
"ui.report_characters_left_one": "الأحرف المتبقية: {count}",
|
||||
"ui.report_characters_left_other": "الأحرف المتبقية: {count}",
|
||||
"ui.reset": "إعادة تعيين",
|
||||
"ui.reset_all": "إعادة تعيين الكل",
|
||||
"ui.reset_the_settings_on_this_page": "إعادة تعيين إعدادات هذه الصفحة",
|
||||
@@ -5674,14 +5612,6 @@
|
||||
"values must be numbers or expressions": "يجب أن تكون القيم أعدادًا أو تعابير",
|
||||
"view.auto_ortho_faces": "منظوري أثناء التدوير، ومتعامد على الأوجه",
|
||||
"view.render_inspector": "مفتش التصيير",
|
||||
"view_gizmo.arrows": "أسهم التدوير",
|
||||
"view_gizmo.home": "زر العرض الرئيسي",
|
||||
"view_gizmo.no_such_body": "لا يوجد في هذا التصميم جسم بهذا الاسم.",
|
||||
"view_gizmo.note": "اختر جسمًا في العرض. يُحفظ مع تفضيلاتك وملفات تعريف الإعدادات، ويظهر في كل تصميم. انقر أحد أوجهه للنظر إليه مباشرة.",
|
||||
"view_gizmo.texture": "الخامة",
|
||||
"view_gizmo.themed": "حسب السمة",
|
||||
"view_gizmo.too_detailed": "مفصّل أكثر مما يناسب متصفح العرض (أكثر من 10,000 مثلث)",
|
||||
"view_gizmo.too_thin": "طويل ورفيع أكثر مما يناسب متصفح العرض: يجب ألا يقل أرفع جانب عن خُمس أطول جانب",
|
||||
"web.files.insert_drawings_hint": "ملفات CAD أو شبكات أو DXF أو SVG إلى التصميم المفتوح",
|
||||
"web.files.insert_stored_drawings_hint": "إلى التصميم المفتوح (ملفات CAD، شبكات، DXF، SVG)",
|
||||
"web.open_full": "فتح في SolidCraft",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Analiza dostupnosti",
|
||||
"cmd.inspect_center_of_mass": "Centar mase",
|
||||
"cmd.inspect_clearance": "Provjera zazora",
|
||||
"cmd.inspect_colored_components": "Omogući boje komponenti",
|
||||
"cmd.inspect_colored_components_disable": "Onemogući boje komponenti",
|
||||
"cmd.inspect_compare": "Uporedi tijela",
|
||||
"cmd.inspect_compare_picture": "Uporedi sliku",
|
||||
"cmd.inspect_continuity": "Analiza kontinuiteta",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Analýza přístupnosti",
|
||||
"cmd.inspect_center_of_mass": "Těžiště",
|
||||
"cmd.inspect_clearance": "Vůle",
|
||||
"cmd.inspect_colored_components": "Zapnout barvy komponent",
|
||||
"cmd.inspect_colored_components_disable": "Vypnout barvy komponent",
|
||||
"cmd.inspect_compare": "Porovnat tělesa",
|
||||
"cmd.inspect_compare_picture": "Porovnat obrázek",
|
||||
"cmd.inspect_continuity": "Analýza spojitosti",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Tilgængelighedsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massecentrum",
|
||||
"cmd.inspect_clearance": "Frigangskontrol",
|
||||
"cmd.inspect_colored_components": "Aktivér komponentfarver",
|
||||
"cmd.inspect_colored_components_disable": "Deaktivér komponentfarver",
|
||||
"cmd.inspect_compare": "Sammenlign emner",
|
||||
"cmd.inspect_compare_picture": "Sammenlign billede",
|
||||
"cmd.inspect_continuity": "Kontinuitetsanalyse",
|
||||
|
||||
@@ -960,6 +960,8 @@
|
||||
"cmd.inspect_accessibility": "Zugänglichkeitsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massenschwerpunkt",
|
||||
"cmd.inspect_clearance": "Freiraum",
|
||||
"cmd.inspect_colored_components": "Komponentenfarben einschalten",
|
||||
"cmd.inspect_colored_components_disable": "Komponentenfarben ausschalten",
|
||||
"cmd.inspect_compare": "Körper vergleichen",
|
||||
"cmd.inspect_continuity": "Kontinuitätsprüfung",
|
||||
"cmd.inspect_curvature_comb": "Krümmungskamm-Analyse",
|
||||
@@ -1289,7 +1291,6 @@
|
||||
"cmd.timeline_stop": "Verlauf anhalten",
|
||||
"cmd.timeline_suppress": "Merkmal unterdrücken",
|
||||
"cmd.ui.assistant": "Assistenten ein-/ausblenden",
|
||||
"cmd.view.navigator": "Eigener Ansichtsnavigator",
|
||||
"cmd.view.render_browser": "Render-Browser",
|
||||
"cmd.view.render_dock": "Render-Panel verschieben",
|
||||
"cmd.view.render_focus": "Render-Fokusansicht",
|
||||
@@ -1388,7 +1389,6 @@
|
||||
"dialog.computing": "Wird berechnet… {seconds} s",
|
||||
"dialog.could_not_be_checked": "nicht prüfbar",
|
||||
"dialog.custom_axis": "↖ Eigene Achse",
|
||||
"dialog.cylinder_face_tip": "Die Fläche oder der Eckpunkt des Modells, auf dem der Zylinder steht: hier klicken, dann in der Ansicht darauf klicken. Leer gelassen platzieren ihn X, Y und Z.",
|
||||
"dialog.dimension_insert_hint": "Doppelklicken Sie auf eine andere steuernde Bemaßung, um ihren Namen einzufügen",
|
||||
"dialog.dimension_name": "Bemaßung: {name}",
|
||||
"dialog.dimension_step_down": "Um {step} verringern",
|
||||
@@ -1397,8 +1397,6 @@
|
||||
"dialog.edit_title": "{title} bearbeiten",
|
||||
"dialog.no_shown_body": "Kein sichtbarer Körper zum Ändern vorhanden. Blenden Sie einen Körper ein oder wählen Sie „Neuer Körper“.",
|
||||
"dialog.pattern_sketch_not_object": "Körper oder Funktionen wählen: Eine Skizze ist keines von beiden. Ihre Kurven mit Muster oder Spiegeln in der Skizze wiederholen oder sie zuerst extrudieren.",
|
||||
"dialog.primitive_point_tip": "Ein Punkt, der X, Y und Z füllt: hier klicken, dann einen Eckpunkt, eine Kante oder Fläche des Modells, einen Punkt einer Skizze oder den Ursprung anklicken.",
|
||||
"dialog.reference_edge_tip": "Eine gerade Kante der gewählten Fläche, von der aus der Abstand darunter gemessen wird: hier klicken, dann die Kante anklicken.",
|
||||
"dialog.select": "↖ Auswählen",
|
||||
"dialog.selected_count": "↖ {count} ausgewählt",
|
||||
"dialog.selection_constraint": "Bedingung {id}",
|
||||
@@ -1441,9 +1439,6 @@
|
||||
"each rail's curves must be in one sketch": "die Kurven jeder Führungskurve müssen in derselben Skizze liegen",
|
||||
"feature computation ended without a result": "die Berechnung des Merkmals wurde ohne Ergebnis beendet",
|
||||
"file.open.drawing_filter": "Designs, CAD-Dateien, Netze und Zeichnungen",
|
||||
"file.open.portal_fallback.no_portal_service": "Die geöffnete Dateiauswahl ist nicht die des Desktops: Der Dateiauswahl-Dienst des Desktops ist nicht verfügbar.",
|
||||
"file.open.portal_fallback.no_session_bus": "Die geöffnete Dateiauswahl ist nicht die des Desktops: Es gibt keinen D-Bus-Sitzungsbus.",
|
||||
"file.open.portal_fallback.xwayland": "Die geöffnete Dateiauswahl ist nicht die des Desktops: Unter XWayland lehnt das Portal das Elternfenster ab.",
|
||||
"file_load.built": "Bisher erstellt: {built} von {total} Merkmalen.",
|
||||
"file_load.converting": "Volumenkörper {done} von {total} wird konvertiert",
|
||||
"file_load.inserting": "Geometrie wird eingefügt",
|
||||
@@ -2022,8 +2017,6 @@
|
||||
"nav.blender.help": "Mittlere Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt; rechtes Ziehen lässt die Kamera unverändert",
|
||||
"nav.creo.help": "Ziehen mit der mittleren Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt beim vertikalen Ziehen (nach unten zum Vergrößern); das Mausrad zoomt",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "Ein Ziehen mit der mittleren oder rechten Taste dreht, verschiebt oder zoomt weiter wie begonnen, bis die Taste losgelassen wird, egal welche Tasten dabei gedrückt oder losgelassen werden. Aus: Die gehaltenen Tasten entscheiden in jedem Moment des Ziehens.",
|
||||
"nav.drag_keeps_mode.label": "Ziehen behält die Navigation bei, mit der es begann",
|
||||
"nav.free_orbit.help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollneigung bei. Das Einrasten mit Umschalt ist deaktiviert.",
|
||||
"nav.free_orbit.label": "Freie Drehung",
|
||||
"nav.free_orbit.snap_help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollausrichtung bei. Halten Sie beim Ziehen zum Drehen die Umschalttaste gedrückt, um Blickrichtung und Rollausrichtung einrasten zu lassen.",
|
||||
@@ -2281,8 +2274,6 @@
|
||||
"prefs.browser_eye_left": "Auge neben dem Aufklapppfeil",
|
||||
"prefs.browser_eye_left_note": "Browserzeilen zeigen ihr Auge zum Ein- und Ausblenden wie bisher neben dem Aufklapppfeil statt in einer Spalte am rechten Zeilenende.",
|
||||
"prefs.browser_reveal_on_hover": "Beim Überfahren einblenden",
|
||||
"prefs.browser_rows_start_closed": "Browser-Zeilen beginnen geschlossen",
|
||||
"prefs.browser_rows_start_closed_note": "Platzierungen und Ordner im Browser beginnen geschlossen, sodass ein Entwurf mit vielen Komponenten seine obersten Zeilen zeigt. Jede öffnet sich mit ihrem Pfeil, „Alle ausklappen“ oder „Im Browser suchen“, das nur die Zeilen bis zu seinem Körper öffnet und die von der letzten Suche geöffneten wieder schließt. Ein in der 3D-Ansicht gewählter Körper lässt den Browser, wie er ist. Ist es aus, beginnen die Ordner geöffnet, Platzierungen so, wie „Komponenten zu Beginn aufgeklappt“ es festlegt, und eine Auswahl in der 3D-Ansicht öffnet den Browser bis zu ihrem Körper, wie bisher.",
|
||||
"prefs.browser_show_note": "Angezeigt lässt den Browser auf dem Bildschirm. Beim Überfahren einblenden blendet ihn aus, bis der Zeiger über seinen Platz oder die schmale Lasche an seinem linken Rand kommt; er bleibt offen, solange der Zeiger darin ist oder ein Menü daraus offen ist.",
|
||||
"prefs.browser_shown": "Angezeigt",
|
||||
"prefs.built_in": "Integriert",
|
||||
@@ -2540,8 +2531,6 @@
|
||||
"prefs.search": "Einstellungen suchen",
|
||||
"prefs.search_empty": "Keine passenden Einstellungen",
|
||||
"prefs.select": "Auswählen",
|
||||
"prefs.select_hidden_edges": "Verdeckte Kanten in der schattierten Ansicht mit verdeckten Kanten auswählen",
|
||||
"prefs.select_hidden_edges_help": "Kanten durch Flächen auswählen, wenn die schattierte Ansicht mit verdeckten Kanten aktiv ist, auch zum Verrunden. Aus behält das Auswahlverhalten von v0.30.1 bei. Ausgeblendete Körper bleiben ausgeschlossen.",
|
||||
"prefs.select_one_item": "Ein Element auswählen",
|
||||
"prefs.select_other_hidden": "Verborgene Geometrie mit „Andere auswählen“ auswählen",
|
||||
"prefs.select_other_hidden_help": "Ein: Verborgene Körper, Skizzen und verdeckte Kanten nahe dem Zeiger einbeziehen. Aus: Die ursprüngliche Liste sichtbarer Geometrie direkt unter dem Zeiger verwenden.",
|
||||
@@ -2566,8 +2555,6 @@
|
||||
"prefs.sketch.click_selects": "Klick wählt",
|
||||
"prefs.sketch.conflict_colour": "Konfliktfarbe",
|
||||
"prefs.sketch.drawing": "Zeichnen",
|
||||
"prefs.sketch.ellipse_axes": "Ellipsen mit ihren Achsen zeichnen",
|
||||
"prefs.sketch.ellipse_axes_note": "Haupt- und Nebenachse als Konstruktionslinien, damit Halbachsen und Achsen bemaßt werden können. Aus: nur die Ellipse, und D darauf ergibt ihren Nebenradius, wohin der Wert auch gezogen wird, gezeichnet und mit R beschriftet wie bisher.",
|
||||
"prefs.sketch.exit_keep": "Beibehalten",
|
||||
"prefs.sketch.exit_previous": "Zur Ansicht davor zurückkehren",
|
||||
"prefs.sketch.exit_unless_rotated": "Außer bei Drehung",
|
||||
@@ -2751,9 +2738,9 @@
|
||||
"render.adaptive_absolute": "Absolute RGB-Fehleruntergrenze",
|
||||
"render.adaptive_coverage": "Zielwert für den Abdeckungsfehler",
|
||||
"render.adaptive_enable": "Samples mithilfe unabhängiger Pilot-Samples zuteilen",
|
||||
"render.adaptive_help": "Unabhängige Pilotsamples werden verworfen und bestimmen dann mindestens 16 Produktionssamples pro Pixel bis zum Wert Samples pro Pixel. CPU-Ressourcengrenzen umfassen beide Phasen. RGB- und Abdeckungsvarianz steuern die Verteilung; Pixel am Limit können verrauscht bleiben. Der Abschlussbericht zählt die tatsächlichen Samples.",
|
||||
"render.adaptive_help": "Nur GPU. Unabhängige Pilot-Samples werden verworfen. Anschließend werden pro Pixel 16 oder mehr Produktions-Samples zugeteilt, bis zu den Samples pro Pixel. Die Zuteilung richtet sich nach der RGB- und Abdeckungsvarianz; seltene Pfade können übersehen werden, und Pixel, die ihr Limit erreicht haben, können verrauscht bleiben. Der Abschlussbericht zählt den tatsächlichen Aufwand. Keine zertifizierte Garantie für ein bestimmtes Rauschniveau oder eine bestimmte Renderzeit.",
|
||||
"render.adaptive_minimum": "Minimale Produktions-Samples",
|
||||
"render.adaptive_pending": "Adaptive Verteilung mit unabhängigen Pilotsamples ist für CPU und GPU verfügbar. Bedingte Fresnel-Schichten erfordern die GPU. Die statistische Verteilung bestätigt keine Konvergenz.",
|
||||
"render.adaptive_pending": "Die adaptive Zuteilung mit unabhängigen GPU-Pilot-Samples und bedingte Fresnel-Schichten sind verfügbar. Zertifizierte Konvergenz, Garantien für seltene Ereignisse und adaptiver Lichttransport auf der CPU bleiben nicht verfügbar.",
|
||||
"render.adaptive_pilots": "Pilot-Samples pro Pixel",
|
||||
"render.adaptive_relative": "Zielwert für den relativen RGB-Fehler",
|
||||
"render.add_ground": "Add fitted ground to setup",
|
||||
@@ -2814,7 +2801,7 @@
|
||||
"render.appearance_editor.replace": "Gespeicherte Optik dieses Körpers ersetzen",
|
||||
"render.appearance_editor.revalidate": "Diesen Körper erneut prüfen",
|
||||
"render.appearance_editor.save": "Diese Optik speichern",
|
||||
"render.appearance_editor.save_reason": "Zum Speichern Glas an einem aktuellen CPU- oder GPU-Körper aktivieren. Für Klarlack „Klarlack in Aufnahme speichern“ verwenden. Lokale Bilddateien bleiben temporär.",
|
||||
"render.appearance_editor.save_reason": "Zum Speichern wird physikalisches Glas auf einem aktuellen GPU-Körper benötigt. Klarlack und lokale Bildtexturen bleiben vorübergehend.",
|
||||
"render.appearance_editor.saved": "Mit dieser Aufnahme gespeichert",
|
||||
"render.appearance_editor.saved_state": "Diese Optik ist mit dieser Aufnahme gespeichert.",
|
||||
"render.appearance_editor.scatter_reason": "Innere Streuung benötigt einen geschlossenen Körper.",
|
||||
@@ -2910,7 +2897,7 @@
|
||||
"render.children.copy_request": "Anfrage-JSON kopieren",
|
||||
"render.children.coverage_error": "Abdeckungsfehler",
|
||||
"render.children.cpu": "CPU-Raytracing",
|
||||
"render.children.denoise": "Bildrauschen reduzieren",
|
||||
"render.children.denoise": "Rauschen im fertigen Bild reduzieren",
|
||||
"render.children.depth": "Pfadtiefe",
|
||||
"render.children.distortion": "Verzeichnung",
|
||||
"render.children.edge_samples": "Kanten-Samples",
|
||||
@@ -3042,7 +3029,7 @@
|
||||
"render.cpu_point_help": "Bis zu acht Punktlichter, zusätzlich zur gewählten Richtungsbeleuchtung. Abstände in mm; der Mindestabstand darf den Referenzabstand nicht überschreiten.",
|
||||
"render.cpu_primary_samples": "Kanten-Samples pro Pixel",
|
||||
"render.cpu_progressive": "CPU progressiv",
|
||||
"render.cpu_progressive_help": "Neue CPU-Kamerasamples sammeln und jeden vollständigen Durchgang zeigen. Ausgewählte Reflexionen verwenden einen Versuch pro Kamerasample; zertifiziertes Glas und planare Anisotropie behalten ihre bisherigen Grenzen. Adaptive Pilot- und Produktionssamples teilen sich das CPU-Profilbudget.",
|
||||
"render.cpu_progressive_help": "Den vorhandenen CPU-Integrator in Durchgängen mit je einem Primär-Sample akkumulieren. Ausgewählte Reflexionen erfordern ein Sample; Flächenlichter, Dielektrika und Anisotropie erfordern „Einzelbild“.",
|
||||
"render.cpu_progressive_limits": "Höchstens 1.048.576 Pixel und 16.777.216 Auswertungen von Pixel × Sample. Das Ändern der Samples passt die Bildgröße nie automatisch an.",
|
||||
"render.cpu_progressive_seed": "Startwert für das CPU-Sampling",
|
||||
"render.cpu_progressive_spp": "Samples pro Pixel",
|
||||
@@ -3218,7 +3205,7 @@
|
||||
"render.export_display": "Export: Anzeige-PNG",
|
||||
"render.export_image_label": "Bild exportieren",
|
||||
"render.export_linear": "Export: lineares Roh-EXR",
|
||||
"render.exr_pending": "CPU-Raytracing und GPU-Pathtracing können rohe lineare EXR-Dateien behalten. Kompression, mehrschichtige Bilder und zusätzliche Renderpässe bleiben nicht verfügbar.",
|
||||
"render.exr_pending": "Die GPU kann rohe lineare EXR-Dateien behalten. Kompression, mehrschichtige Bilder und zusätzliche Renderpässe bleiben nicht verfügbar.",
|
||||
"render.film_grain": "Fotografisches Korn",
|
||||
"render.fit": "Produkt einpassen",
|
||||
"render.fixed_camera": "Kamera",
|
||||
@@ -3258,11 +3245,9 @@
|
||||
"render.frame_wide": "16:9 · Breitbild",
|
||||
"render.gpu_depth": "Maximale Pfadtiefe",
|
||||
"render.gpu_options_help": "Samples, Tiefe, Startwert und Belichtung werden mit diesem Setup gespeichert. Es wird nur die aktive Bildmethode gespeichert.",
|
||||
"render.gpu_path_crashed": "SolidCraft wurde beim letzten Einrichten auf diesem Grafikadapter beendet, wahrscheinlich im Grafiktreiber, und richtet ihn deshalb nicht von selbst erneut ein. Verwenden Sie stattdessen Raytracing auf der CPU oder versuchen Sie es erneut (nach einem Treiberupdate wird es ebenfalls erneut versucht).",
|
||||
"render.gpu_path_failed": "GPU-Pathtracer nicht verfügbar",
|
||||
"render.gpu_path_first_setup": "Das erste Setup auf dieser GPU kann mehrere Minuten dauern; SolidCraft bleibt währenddessen bedienbar. Spätere Starts verwenden die kompilierten Shader erneut, sofern der Treiber sie behält.",
|
||||
"render.gpu_path_preparing": "GPU-Pathtracer: Shader werden kompiliert…",
|
||||
"render.gpu_path_retry": "Erneut versuchen",
|
||||
"render.gpu_seed": "Zufallsstartwert",
|
||||
"render.gpu_unavailable": "GPU-Pathtracing ist auf diesem Gerät nicht bereit.",
|
||||
"render.ground_apply": "Boden anpassen und speichern",
|
||||
@@ -3581,7 +3566,7 @@
|
||||
"render.method_cpu": "CPU-Raytracing",
|
||||
"render.method_gpu": "GPU-Pathtracing",
|
||||
"render.method_limit_raster": "Die Rasterung verwendet die feste undurchsichtige Studiovorschau. Wählen Sie ausdrücklich eine Raytracing-Methode für die unterstützten optischen Einstellungen.",
|
||||
"render.method_raster": "CPU-Rasterung (schematisch)",
|
||||
"render.method_raster": "CPU-Rasterung",
|
||||
"render.navigation_help": "Die Kamera bewegt sich wie im Konstruieren (Einstellungen › Navigation). Setup speichern übernimmt die aktuelle Kamera.",
|
||||
"render.nested_enable": "Zertifizierte verschachtelte Dielektrika",
|
||||
"render.nested_help": "Bis zu 16 umschließende Schichten; jedes übergeordnete Element muss konvex und jedes untergeordnete strikt darin enthalten sein. Überschneidungen, sich berührende Flächen, dünne Schichten und die Transmission gerader Schattenstrahlen werden abgelehnt. Der Zustand „Kamera im Inneren“ folgt der zertifizierten Hierarchie. Erzeugt keine Flüssigkeitsoberfläche und verschmilzt keine sich berührenden CAD-Grenzflächen.",
|
||||
@@ -3609,7 +3594,7 @@
|
||||
"render.output_exr_unavailable": "EXR-Ausgabeeinstellungen sind hier nicht verfügbar. Linearer EXR-Export erfordert ein fertiges GPU-Bild mit gespeichertem linearem Original.",
|
||||
"render.output_fit": "Einpassen",
|
||||
"render.output_format": "Format",
|
||||
"render.output_format_status": "PNG enthält das exakte fertige Anzeigebild. Optionales CPU/GPU-EXR bewahrt rohe lineare FLOAT32-Strahldichte vor Anzeigefiltern. Der Export verwendet nie den Vorschaucache oder einen anderen Auftrag.",
|
||||
"render.output_format_status": "PNG enthält das exakte fertige Anzeigebild. Optionales GPU-EXR bewahrt rohe lineare FLOAT32-Strahldichte vor Filtern. Export ersetzt nie durch Vorschaucache oder anderen Auftrag.",
|
||||
"render.output_grading": "Farbkorrektur",
|
||||
"render.output_image": "Bild",
|
||||
"render.output_largest": "{width} × {height} max.",
|
||||
@@ -3694,7 +3679,7 @@
|
||||
"render.quality_custom": "Benutzerdefiniert",
|
||||
"render.quality_draft": "Entwurf",
|
||||
"render.quality_high": "Hoch",
|
||||
"render.quality_projection_help": "Diese Prüfung ändert nur Auflösung, Abtastbudget, Pfadtiefe, Startwert und Rauschreduzierung. Andere Einstellungen bleiben erhalten; das Budget garantiert keine Konvergenz.",
|
||||
"render.quality_projection_help": "Q-Vorgaben enthalten künftige Anforderungen. Diese Aktion projiziert nur ein unterstütztes PNG-Budget mit festen Samples und lässt adaptives Stoppen, rohe lineare Ausgabe und unabhängige Transmissionslimits ausdrücklich aus.",
|
||||
"render.quality_review": "Sampling-Budget prüfen",
|
||||
"render.queue_pending": "In dieser Oberfläche gibt es keine automatische Produktionswarteschlange und keinen Animations-Renderer. Fertige Einzelbilder bleiben separat exportierbar.",
|
||||
"render.radial_assign": "Erscheinungsbild zuweisen",
|
||||
@@ -3725,7 +3710,7 @@
|
||||
"render.result_navigation": "Verschieben und Zoomen folgen den Navigationseinstellungen. Leertaste + Ziehen verschiebt.",
|
||||
"render.result_open": "Das fertige Bild in der Hauptansicht öffnen.",
|
||||
"render.result_preview": "Live-Vorschau",
|
||||
"render.result_provisional": "Vorläufiges Bild",
|
||||
"render.result_provisional": "Vorläufiges CPU-Bild",
|
||||
"render.result_provisional_help": "Dieser Zwischenstand ist nicht exportierbar. Der Export behält das letzte fertige Ergebnis; ein Abbruch kehrt zu diesem Ergebnis zurück.",
|
||||
"render.result_reduced_display": "Anzeigevorschau auf die Geräte-/Cachegrenze reduziert. PNG und Export behalten die volle Auflösung; native 100%-Ansicht ist hier nicht verfügbar.",
|
||||
"render.result_retained": "Das zuletzt fertiggestellte Bild wird angezeigt; ein neueres Rendering hat es noch nicht ersetzt.",
|
||||
@@ -3889,7 +3874,7 @@
|
||||
"render.stage_panels.prepare_plan": "Erstellungsplan vorbereiten",
|
||||
"render.stage_panels.projects": "Projiziert",
|
||||
"render.stage_panels.quality_ack": "Ich bestätige das feste Budget und seine Auslassungen",
|
||||
"render.stage_panels.quality_omissions": "Dieses Budget legt keine adaptiven Fehlerziele, eigenen Transmissionsgrenzen, Russian-Roulette-Regeln oder Speicherung des linearen Masters fest und garantiert keine Konvergenz.",
|
||||
"render.stage_panels.quality_omissions": "Adaptives Stoppen und Fehlerziele, separate Transmissionstiefe, Russian-Roulette-Regeln, lineare Rohausgabe, EXR-Master und Konvergenznachweis.",
|
||||
"render.stage_panels.reference": "Referenz",
|
||||
"render.stage_panels.references": "Referenzen",
|
||||
"render.stage_panels.remove_optics": "Optik entfernen",
|
||||
@@ -3933,7 +3918,7 @@
|
||||
"render.stage_panels.subtitle_environment": "Originale HDR-Umgebung · gesamte Szene",
|
||||
"render.stage_panels.subtitle_maps": "Erstellte räumliche Texturen · temporäre Vorschau",
|
||||
"render.stage_panels.subtitle_physical": "Schreibgeschützter Plan · kein Material erstellt oder zugewiesen",
|
||||
"render.stage_panels.subtitle_quality": "GPU-Abtastbudget · nur aufgeführte Einstellungen",
|
||||
"render.stage_panels.subtitle_quality": "Festes GPU-Budget · zukünftige Anforderungen ausgelassen",
|
||||
"render.stage_panels.subtitle_rig": "Erstellter Lichtaufbau · Lichtquellen und passive Flächen",
|
||||
"render.stage_panels.subtitle_saved_maps": "Erstellte Texturen · mit dieser Aufnahme gespeichert",
|
||||
"render.stage_panels.subtitle_stage": "Physischer Boden und Bühne · an das Produkt angepasst",
|
||||
@@ -3958,7 +3943,7 @@
|
||||
"render.studio_area_help": "Bis zu vier einseitige Rechtecke. Orthogonale Halbkanten U und V bestimmen die Größe; U × V zeigt zur beleuchteten Seite. Die Koordinaten sind Weltmillimeter.",
|
||||
"render.studio_center": "Mittelpunkt X, Y, Z (mm)",
|
||||
"render.studio_denoise": "Rauschen im fertigen Bild reduzieren",
|
||||
"render.studio_denoise_help": "Kantenerhaltender Filter für progressive und fertige Bilder; feine Details können weicher werden. Glas und scharfe spiegelnde Oberflächen umgehen den Filter.",
|
||||
"render.studio_denoise_help": "Abschließender kantenerhaltender Filter; feine Details können weicher werden. Glas und scharfe spiegelnde Oberflächen umgehen den Filter.",
|
||||
"render.studio_direction": "Richtung zur Sonne (X, Y, Z)",
|
||||
"render.studio_environment": "Umgebung",
|
||||
"render.studio_environment_bounds": "Die Umgebungsdrehung muss endlich und innerhalb von ±1.000.000 Radiant liegen, die Intensität zwischen 0 und 100.",
|
||||
@@ -5004,7 +4989,6 @@
|
||||
"ui.heading_construct": "KONSTRUKTION",
|
||||
"ui.heading_create": "ERSTELLEN",
|
||||
"ui.heading_create_sketch": "SKIZZE ERSTELLEN",
|
||||
"ui.heading_custom_view_navigator": "EIGENER ANSICHTSNAVIGATOR",
|
||||
"ui.heading_cylinder": "ZYLINDER",
|
||||
"ui.heading_design": "DESIGN",
|
||||
"ui.heading_draft": "ABZIEHEN",
|
||||
@@ -5312,8 +5296,6 @@
|
||||
"ui.repeat_nothing": "Noch nichts zu wiederholen: Starten Sie zuerst ein Werkzeug oder einen Dialog.",
|
||||
"ui.replace_with_link_done": "Die Kopien sind jetzt Platzierungen des verknüpften Designs.",
|
||||
"ui.report_a_bug": "Fehler melden",
|
||||
"ui.report_characters_left_one": "{count} Zeichen übrig",
|
||||
"ui.report_characters_left_other": "{count} Zeichen übrig",
|
||||
"ui.reset": "Zurücksetzen",
|
||||
"ui.reset_all": "Alle zurücksetzen",
|
||||
"ui.reset_the_settings_on_this_page": "Einstellungen auf dieser Seite zurücksetzen",
|
||||
@@ -5595,14 +5577,6 @@
|
||||
"values must be numbers or expressions": "Werte müssen Zahlen oder Ausdrücke sein",
|
||||
"view.auto_ortho_faces": "Perspektivisch beim Drehen, orthografisch auf Flächen",
|
||||
"view.render_inspector": "Render-Inspektor",
|
||||
"view_gizmo.arrows": "Drehpfeile",
|
||||
"view_gizmo.home": "Schaltfläche „Ausgangsansicht“",
|
||||
"view_gizmo.no_such_body": "Diese Konstruktion hat keinen Körper mit diesem Namen.",
|
||||
"view_gizmo.note": "Wählen Sie einen Körper in der Ansicht. Er wird mit Ihren Einstellungen und Einstellungsprofilen gespeichert und in jeder Konstruktion angezeigt. Klicken Sie auf eine seiner Flächen, um direkt darauf zu blicken.",
|
||||
"view_gizmo.texture": "Textur",
|
||||
"view_gizmo.themed": "Design",
|
||||
"view_gizmo.too_detailed": "Zu detailliert für den Ansichtsnavigator (über 10.000 Dreiecke)",
|
||||
"view_gizmo.too_thin": "Zu lang und dünn für den Ansichtsnavigator: Die dünnste Seite muss mindestens ein Fünftel der längsten sein",
|
||||
"web.files.insert_drawings_hint": "CAD-Dateien, Netze, DXF oder SVG in das geöffnete Design",
|
||||
"web.files.insert_stored_drawings_hint": "In das geöffnete Design (CAD-Dateien, Netze, DXF, SVG)",
|
||||
"web.open_full": "In SolidCraft öffnen",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"cmd.component_cycle_color": "Cycle Component Colour",
|
||||
"cmd.construct_point_center": "Point At Centre Of Circle/Sphere/Torus",
|
||||
"cmd.inspect_center_of_mass": "Centre of Mass",
|
||||
"cmd.inspect_colored_components": "Component Colours",
|
||||
"cmd.inspect_colored_components": "Enable Component Colours",
|
||||
"cmd.inspect_colored_components_disable": "Disable Component Colours",
|
||||
"cmd.parameters_favorite": "Favourite Parameter",
|
||||
"cmd.render_aging_describe": "Layered Surface Aging",
|
||||
"cmd.render_effects_describe": "Lens and Creative Effects",
|
||||
@@ -86,7 +87,6 @@
|
||||
"prefs.max_file_size_web": "The web app opens STEP and import files up to 512 MB and designs up to 256 MB: a browser tab can't hold larger files. The desktop app can raise these limits.",
|
||||
"prefs.megabytes": "MB",
|
||||
"prefs.network_locations_note": "For organisations that keep designs on file servers. Off: only servers you allow.",
|
||||
"prefs.select_hidden_edges_help": "Select edges through faces when Shaded with hidden edges is active, including for Fillet. Off keeps v0.30.1 selection behaviour. Hidden bodies stay unselectable.",
|
||||
"prefs.sketch.center_arc": "Centre-point arc follows the cursor",
|
||||
"prefs.sketch.conflict_colour": "Conflict colour",
|
||||
"prefs.sketch.tracking_guides": "Centre and midpoint guides",
|
||||
@@ -235,10 +235,6 @@
|
||||
"render.studio_area_help": "Up to four one-sided rectangles. Orthogonal half-edges U and V define size; U × V faces the lit side. Coordinates are world-space millimetres.",
|
||||
"render.studio_center": "Centre X, Y, Z (mm)",
|
||||
"render.studio_hdr_help": "Radiance .hdr, 2:1 panorama, up to 2048 × 1024. EXR is unavailable. The colour assumption applies only when primaries are unspecified.",
|
||||
"sketch.hint.arc.center_point": "Arc: click centre and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.circle.center": "Circle: click the centre, then a point on the circle",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to centre the second on the first",
|
||||
"sketch.hint.rectangle.center": "Centre rectangle: click the centre, then a corner",
|
||||
"sketch_text.align_center": "Centre",
|
||||
"ui.3d_mouse_kind_cap": "Return to Centre",
|
||||
"ui.3d_mouse_looks_like_cap": "The reading stays put while you hold it, the way a Return to Centre device does.",
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
"Bottom Right": "Bottom Right",
|
||||
"Boundary": "Boundary",
|
||||
"Bounding box": "Bounding box",
|
||||
"Bridge": "Bridge",
|
||||
"CHECK": "CHECK",
|
||||
"COMPONENTS": "COMPONENTS",
|
||||
"CONFIGURATIONS": "CONFIGURATIONS",
|
||||
@@ -964,7 +963,8 @@
|
||||
"cmd.inspect_analysis": "View Analysis",
|
||||
"cmd.inspect_center_of_mass": "Center of Mass",
|
||||
"cmd.inspect_clearance": "Clearance",
|
||||
"cmd.inspect_colored_components": "Component Colors",
|
||||
"cmd.inspect_colored_components": "Enable Component Colors",
|
||||
"cmd.inspect_colored_components_disable": "Disable Component Colors",
|
||||
"cmd.inspect_compare": "Compare Bodies",
|
||||
"cmd.inspect_compare_picture": "Compare Picture",
|
||||
"cmd.inspect_continuity": "Continuity Analysis",
|
||||
@@ -1172,7 +1172,7 @@
|
||||
"cmd.sketch_arc_tangent": "Tangent Arc",
|
||||
"cmd.sketch_arc_three_point": "3-Point Arc",
|
||||
"cmd.sketch_auto_constrain": "AutoConstrain",
|
||||
"cmd.sketch_auto_constrain_from_datum": "AutoConstrain from Datum",
|
||||
"cmd.sketch_auto_constrain_from_datum": "AutoConstrain from datum",
|
||||
"cmd.sketch_auto_project": "Auto-Project Edges on Reference",
|
||||
"cmd.sketch_blend_curve": "Blend Curve",
|
||||
"cmd.sketch_cancel": "Cancel Sketch",
|
||||
@@ -1186,7 +1186,7 @@
|
||||
"cmd.sketch_circle_two_point": "2-Point Circle",
|
||||
"cmd.sketch_circle_two_tangent": "2-Tangent Circle",
|
||||
"cmd.sketch_constrainer": "Sketch Constrainer",
|
||||
"cmd.sketch_constraint_fix": "Fix/Unfix",
|
||||
"cmd.sketch_constraint_fix": "Fix/UnFix",
|
||||
"cmd.sketch_constraint_midpoint": "MidPoint",
|
||||
"cmd.sketch_construction": "Normal/Construction",
|
||||
"cmd.sketch_copy": "Copy Sketch Entities",
|
||||
@@ -1296,7 +1296,6 @@
|
||||
"cmd.timeline_stop": "Stop History",
|
||||
"cmd.timeline_suppress": "Suppress Feature",
|
||||
"cmd.ui.assistant": "Toggle Assistant",
|
||||
"cmd.view.navigator": "Custom View Navigator",
|
||||
"cmd.view.render_browser": "Render Browser",
|
||||
"cmd.view.render_dock": "Move Render Dock",
|
||||
"cmd.view.render_focus": "Render Focus View",
|
||||
@@ -1312,13 +1311,13 @@
|
||||
"cmd.view_front": "Front View",
|
||||
"cmd.view_graphics_preset": "Graphics Preset",
|
||||
"cmd.view_grid": "Grid",
|
||||
"cmd.view_ground_shadow": "Ground Shadow",
|
||||
"cmd.view_home": "Home View",
|
||||
"cmd.view_ground_shadow": "Ground shadow",
|
||||
"cmd.view_home": "Home view",
|
||||
"cmd.view_isometric": "Isometric View",
|
||||
"cmd.view_left": "Left View",
|
||||
"cmd.view_list": "List Named Views",
|
||||
"cmd.view_navigate": "Navigate View",
|
||||
"cmd.view_navigation_bar": "Navigation Bar",
|
||||
"cmd.view_navigation_bar": "Navigation bar",
|
||||
"cmd.view_next_tab": "Next Tab",
|
||||
"cmd.view_normal_to": "Normal to Selection",
|
||||
"cmd.view_previous_tab": "Previous Tab",
|
||||
@@ -1327,10 +1326,10 @@
|
||||
"cmd.view_right": "Right View",
|
||||
"cmd.view_save": "New Named View",
|
||||
"cmd.view_shots_strip": "Shots Strip",
|
||||
"cmd.view_spin": "Continuous Orbit",
|
||||
"cmd.view_tool": "Navigation Tool",
|
||||
"cmd.view_spin": "Continuous orbit",
|
||||
"cmd.view_tool": "Navigation tool",
|
||||
"cmd.view_top": "Top View",
|
||||
"cmd.view_visual_style": "Visual Style",
|
||||
"cmd.view_visual_style": "Visual style",
|
||||
"color.blue": "Blue",
|
||||
"color.copy": "Click to copy color values",
|
||||
"color.edit": "Click to edit color",
|
||||
@@ -1395,7 +1394,6 @@
|
||||
"dialog.computing": "Computing… {seconds} s",
|
||||
"dialog.could_not_be_checked": "could not be checked",
|
||||
"dialog.custom_axis": "↖ Custom Axis",
|
||||
"dialog.cylinder_face_tip": "The face or vertex of the model the cylinder stands on: click here, then click it in the view. Left empty, X, Y and Z place it.",
|
||||
"dialog.dimension_insert_hint": "Click another driving dimension to insert its name",
|
||||
"dialog.dimension_name": "Dimension: {name}",
|
||||
"dialog.dimension_step_down": "Down by {step}",
|
||||
@@ -1404,8 +1402,6 @@
|
||||
"dialog.edit_title": "EDIT {title}",
|
||||
"dialog.no_shown_body": "No shown body to change: show one, or choose New Body",
|
||||
"dialog.pattern_sketch_not_object": "Pick bodies or features: a sketch is neither. Repeat its curves with the sketch's own Pattern or Mirror, or extrude it first.",
|
||||
"dialog.primitive_point_tip": "A point that fills X, Y and Z: click here, then click a vertex, edge or face of the model, a point of a sketch, or the origin.",
|
||||
"dialog.reference_edge_tip": "A straight edge of the picked face to measure the distance below from: click here, then click the edge.",
|
||||
"dialog.select": "↖ Select",
|
||||
"dialog.selected_count": "↖ {count} selected",
|
||||
"dialog.selection_constraint": "Constraint {id}",
|
||||
@@ -1449,9 +1445,6 @@
|
||||
"each rail's curves must be in one sketch": "each rail's curves must be in one sketch",
|
||||
"feature computation ended without a result": "feature computation ended without a result",
|
||||
"file.open.drawing_filter": "Designs, CAD files, meshes and drawings",
|
||||
"file.open.portal_fallback.no_portal_service": "The file chooser that opened is not the desktop's own: the desktop portal service is not available.",
|
||||
"file.open.portal_fallback.no_session_bus": "The file chooser that opened is not the desktop's own: there is no D-Bus session bus.",
|
||||
"file.open.portal_fallback.xwayland": "The file chooser that opened is not the desktop's own: under XWayland the portal refuses the parent window.",
|
||||
"file_load.built": "Built so far: {built} of {total} features.",
|
||||
"file_load.converting": "Converting solid {done} of {total}",
|
||||
"file_load.inserting": "Inserting geometry",
|
||||
@@ -2042,8 +2035,6 @@
|
||||
"nav.blender.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone",
|
||||
"nav.creo.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle drag vertically zooms (down to zoom in); scroll the wheel to zoom",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "A middle or right drag goes on orbiting, panning or zooming as it began until the button is released, whatever keys are pressed or let go meanwhile. Off: the keys held decide at every moment of the drag.",
|
||||
"nav.drag_keeps_mode.label": "A drag keeps the navigation it started with",
|
||||
"nav.free_orbit.help": "Mouse and 3D mouse rotate through both poles and preserve roll. Shift axis snapping is disabled.",
|
||||
"nav.free_orbit.label": "Free orbit",
|
||||
"nav.free_orbit.snap_help": "Mouse and 3D mouse rotate through both poles and preserve roll. Hold Shift during an orbit drag to snap the view direction and roll.",
|
||||
@@ -2280,9 +2271,6 @@
|
||||
"prefs.auto_operation_note": "Join, New Body or Cut, from what the material touches.",
|
||||
"prefs.axes": "Axes",
|
||||
"prefs.axis": "Axis",
|
||||
"prefs.background_choose": "Choose picture…",
|
||||
"prefs.background_image": "Picture",
|
||||
"prefs.background_unreadable": "Could not read the background picture",
|
||||
"prefs.blur_cost": "Relative rendering cost. More bars usually mean more work.",
|
||||
"prefs.blur_device": "Unavailable on this graphics device or rendering path",
|
||||
"prefs.blur_guard": "Blur quality was reduced to keep frames responsive. The radius is unchanged.",
|
||||
@@ -2304,8 +2292,6 @@
|
||||
"prefs.browser_eye_left": "Eye beside the fold arrow",
|
||||
"prefs.browser_eye_left_note": "Browser rows show their show/hide eye next to the fold arrow, as before, instead of in a column at the right end of the rows.",
|
||||
"prefs.browser_reveal_on_hover": "Reveal on hover",
|
||||
"prefs.browser_rows_start_closed": "Browser rows start closed",
|
||||
"prefs.browser_rows_start_closed_note": "Placements and folders in the Browser start closed, so a design with many components shows its top-level rows. Each opens with its arrow, Expand All or Find in Browser, which opens just the rows down to its body and closes again the ones the last find opened. A body picked in the 3D view leaves the Browser as it is. When off, folders start open, placements start as Components start open says, and a pick in the 3D view opens the Browser down to its body, as before.",
|
||||
"prefs.browser_show_note": "Shown keeps the browser on screen. Reveal on hover keeps it out of sight until the pointer comes over its place or the thin tab at its left edge, and it stays open while the pointer is inside it or a menu from it is open.",
|
||||
"prefs.browser_shown": "Shown",
|
||||
"prefs.built_in": "Built in",
|
||||
@@ -2437,10 +2423,6 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "The two curved arrows at the cube's corner roll the view within the screen plane. On: the clockwise arrow rolls counterclockwise and the counterclockwise arrow rolls clockwise. Off: each arrow rolls the view the way it curls.",
|
||||
"prefs.invert_navigator_arrows": "Invert the view cube rotation arrows",
|
||||
"prefs.invert_navigator_arrows_note": "The arrows around the cube orbit the camera the way they point, as in Fusion and Inventor: the arrow on the left points right and swings the camera right. Off: an arrow rotates the cube toward the side it sits on, so the left arrow swings the camera left. The curved roll arrows are not affected.",
|
||||
"prefs.keep_unsaved_also_asks": "Still ask when the tab closes",
|
||||
"prefs.keep_unsaved_also_asks_tip": "With this off, the tab closes without the browser's box once every unsaved design's copy is current.",
|
||||
"prefs.keep_unsaved_in_browser": "Keep unsaved work in this browser",
|
||||
"prefs.keep_unsaved_in_browser_tip": "Each open design's unsaved changes are kept up to date in this browser's private storage. The next visit offers to restore them.",
|
||||
"prefs.keep_work_on_tab_switch": "Keep dialogs and tools open when switching designs",
|
||||
"prefs.keep_work_on_tab_switch_note": "Each design tab keeps its open dialog or tool, with what you picked and typed in it, and its toolbar tab. They are there again when you come back to the design. Off: switching designs cancels them and shows the Model tab.",
|
||||
"prefs.labels": "Labels",
|
||||
@@ -2572,8 +2554,6 @@
|
||||
"prefs.section_hatch_by_part": "By part",
|
||||
"prefs.section_hatch_one": "One style",
|
||||
"prefs.select": "Select",
|
||||
"prefs.select_hidden_edges": "Select hidden edges in Shaded with hidden edges",
|
||||
"prefs.select_hidden_edges_help": "Select edges through faces when Shaded with hidden edges is active, including for Fillet. Off keeps v0.30.1 selection behavior. Hidden bodies stay unselectable.",
|
||||
"prefs.select_one_item": "Select one item",
|
||||
"prefs.select_other_hidden": "Select hidden geometry with Select Other",
|
||||
"prefs.select_other_hidden_help": "On: include hidden bodies, sketches and occluded edges near the pointer. Off: use the original list of visible geometry under the pointer.",
|
||||
@@ -2598,8 +2578,6 @@
|
||||
"prefs.sketch.click_selects": "Click selects",
|
||||
"prefs.sketch.conflict_colour": "Conflict color",
|
||||
"prefs.sketch.drawing": "Drawing",
|
||||
"prefs.sketch.ellipse_axes": "Draw ellipses with their axes",
|
||||
"prefs.sketch.ellipse_axes_note": "The major and minor axes as construction lines, so the half-axes and axes can be dimensioned. Off: the ellipse alone, and D on it gives its minor radius wherever the value is pulled, drawn and labelled R as before.",
|
||||
"prefs.sketch.exit_keep": "Keep",
|
||||
"prefs.sketch.exit_previous": "Return to the view before",
|
||||
"prefs.sketch.exit_unless_rotated": "Unless turned",
|
||||
@@ -2769,12 +2747,6 @@
|
||||
"recovery.just_now": "just now",
|
||||
"recovery.min_ago": "{count} min ago",
|
||||
"recovery.never_saved": "never saved",
|
||||
"recovery.open_saved": "Open saved version",
|
||||
"recovery.restore": "Restore",
|
||||
"recovery.restore_intro": "Changes that weren't saved when this page last closed are kept in this browser.",
|
||||
"recovery.restore_row": "{name}, changed {when}",
|
||||
"recovery.restore_title": "Restore unsaved designs?",
|
||||
"recovery.restore_title_one": "Restore unsaved design: {name}, changed {when}?",
|
||||
"refused.change": "Couldn't change {body}.",
|
||||
"refused.cut": "Couldn't cut {body}.",
|
||||
"refused.face_curved": "One of its curved faces can't be used for modelling.",
|
||||
@@ -2789,9 +2761,9 @@
|
||||
"render.adaptive_absolute": "Absolute RGB error floor",
|
||||
"render.adaptive_coverage": "Coverage error target",
|
||||
"render.adaptive_enable": "Allocate samples with independent pilots",
|
||||
"render.adaptive_help": "Independent pilot samples are discarded, then choose 16 or more production samples per pixel up to Samples per pixel. CPU resource limits include both stages. RGB and coverage variance guide allocation; capped pixels can remain noisy. The final report counts actual samples.",
|
||||
"render.adaptive_help": "GPU only. Independent pilot samples are discarded, then allocate 16 or more production samples per pixel up to Samples per pixel. RGB and coverage variance guide allocation; rare paths may be missed and capped pixels may remain noisy. The final report counts actual work. No certified noise or time guarantee.",
|
||||
"render.adaptive_minimum": "Minimum production samples",
|
||||
"render.adaptive_pending": "Independent-pilot adaptive allocation is available for CPU and GPU. Conditional Fresnel strata require GPU. Statistical allocation does not certify convergence.",
|
||||
"render.adaptive_pending": "GPU independent-pilot adaptive allocation and conditional Fresnel strata are available. Certified convergence, rare-event guarantees and CPU adaptive transport remain unavailable.",
|
||||
"render.adaptive_pilots": "Pilot samples per pixel",
|
||||
"render.adaptive_relative": "Relative RGB error target",
|
||||
"render.add_ground": "Add fitted ground to setup",
|
||||
@@ -2852,7 +2824,7 @@
|
||||
"render.appearance_editor.replace": "Replace this body's saved optics",
|
||||
"render.appearance_editor.revalidate": "Revalidate this body",
|
||||
"render.appearance_editor.save": "Save these optics",
|
||||
"render.appearance_editor.save_reason": "To save Glass, enable it on a current CPU or GPU body. Use Save coat to shot for clear coat. Local image files remain temporary.",
|
||||
"render.appearance_editor.save_reason": "Saving these optics needs physical glass on a current GPU body. Coat and local image maps remain temporary.",
|
||||
"render.appearance_editor.saved": "Saved with this shot",
|
||||
"render.appearance_editor.saved_state": "These optics are saved with this shot.",
|
||||
"render.appearance_editor.scatter_reason": "Interior scattering needs a closed solid boundary.",
|
||||
@@ -2948,7 +2920,7 @@
|
||||
"render.children.copy_request": "Copy request JSON",
|
||||
"render.children.coverage_error": "Coverage error",
|
||||
"render.children.cpu": "CPU ray tracing",
|
||||
"render.children.denoise": "Reduce image noise",
|
||||
"render.children.denoise": "Reduce noise in the finished image",
|
||||
"render.children.depth": "Path depth",
|
||||
"render.children.distortion": "Distortion",
|
||||
"render.children.edge_samples": "Edge samples",
|
||||
@@ -3080,7 +3052,7 @@
|
||||
"render.cpu_point_help": "Up to eight point lights, added to the selected directional lighting. Distances are in mm; the minimum must not exceed the reference distance.",
|
||||
"render.cpu_primary_samples": "Edge samples per pixel",
|
||||
"render.cpu_progressive": "CPU progressive",
|
||||
"render.cpu_progressive_help": "Accumulate fresh CPU camera samples and show every complete pass. Selected reflections use one attempt per camera sample; certified glass and planar anisotropy keep their existing limits. Adaptive pilots and production share the CPU profile budget.",
|
||||
"render.cpu_progressive_help": "Accumulate the existing CPU integrator in passes of one primary sample. Selected reflections require one sample; area lights, dielectrics and anisotropy require Single image.",
|
||||
"render.cpu_progressive_limits": "At most 1,048,576 pixels and 16,777,216 pixel × sample evaluations. Changing samples never resizes the image automatically.",
|
||||
"render.cpu_progressive_seed": "CPU sampling seed",
|
||||
"render.cpu_progressive_spp": "samples per pixel",
|
||||
@@ -3256,7 +3228,7 @@
|
||||
"render.export_display": "Export: display PNG",
|
||||
"render.export_image_label": "Export Image",
|
||||
"render.export_linear": "Export: raw linear EXR",
|
||||
"render.exr_pending": "CPU ray tracing and GPU path tracing can retain raw linear EXR. Compression, multilayer images and auxiliary render passes remain unavailable.",
|
||||
"render.exr_pending": "GPU can retain raw linear EXR. Compression, multilayer images and auxiliary render passes remain unavailable.",
|
||||
"render.film_grain": "Photographic grain",
|
||||
"render.fit": "Fit Product",
|
||||
"render.fixed_camera": "Camera",
|
||||
@@ -3296,11 +3268,9 @@
|
||||
"render.frame_wide": "16:9 · Widescreen",
|
||||
"render.gpu_depth": "Maximum path depth",
|
||||
"render.gpu_options_help": "Samples, depth, seed and exposure are saved with this setup. Only the active image method is saved.",
|
||||
"render.gpu_path_crashed": "SolidCraft closed while setting it up on this graphics adapter last time, most likely inside the graphics driver, so it is not set up again by itself. Choose CPU ray tracing instead, or try again (a driver update also tries again).",
|
||||
"render.gpu_path_failed": "GPU path tracer unavailable",
|
||||
"render.gpu_path_first_setup": "The first setup on this GPU can take several minutes; SolidCraft stays usable meanwhile. Later starts reuse the compiled shaders where the driver keeps them.",
|
||||
"render.gpu_path_preparing": "GPU path tracer: compiling shaders…",
|
||||
"render.gpu_path_retry": "Try again",
|
||||
"render.gpu_seed": "Random seed",
|
||||
"render.gpu_unavailable": "GPU path tracing is not ready on this device.",
|
||||
"render.ground_apply": "Fit and save ground",
|
||||
@@ -3619,7 +3589,7 @@
|
||||
"render.method_cpu": "CPU ray tracing",
|
||||
"render.method_gpu": "GPU path tracing",
|
||||
"render.method_limit_raster": "Raster uses the fixed opaque studio preview. Select an explicit raytracing method for supported optical controls.",
|
||||
"render.method_raster": "CPU raster (schematic)",
|
||||
"render.method_raster": "CPU raster",
|
||||
"render.navigation_help": "The camera moves as in Design (Preferences › Navigation). Save Setup records the current camera.",
|
||||
"render.nested_enable": "Certified nested dielectrics",
|
||||
"render.nested_help": "Up to 16 containing layers; every parent must be convex and every child strictly inside it. Intersections, touching surfaces, thin sheets and straight-shadow transmission refuse. Camera-inside state follows the certified hierarchy. Does not create a liquid surface or merge touching CAD boundaries.",
|
||||
@@ -3647,7 +3617,7 @@
|
||||
"render.output_exr_unavailable": "EXR output setup is unavailable here. Linear EXR export requires a completed GPU image with a retained linear master.",
|
||||
"render.output_fit": "Fit",
|
||||
"render.output_format": "Format",
|
||||
"render.output_format_status": "PNG is the exact completed display image. Optional CPU/GPU EXR retains raw scene-linear FLOAT32 radiance before display filters. Export never substitutes the preview cache or a different job.",
|
||||
"render.output_format_status": "PNG is the exact completed display image. Optional GPU EXR retains raw scene-linear FLOAT32 radiance before display filters. Export never substitutes the preview cache or a different job.",
|
||||
"render.output_grading": "Grading",
|
||||
"render.output_image": "Image",
|
||||
"render.output_largest": "{width} × {height} max",
|
||||
@@ -3732,7 +3702,7 @@
|
||||
"render.quality_custom": "Custom",
|
||||
"render.quality_draft": "Draft",
|
||||
"render.quality_high": "High",
|
||||
"render.quality_projection_help": "This review changes only resolution, sample budget, path depth, seed and noise reduction. Other settings are kept; the budget does not certify convergence.",
|
||||
"render.quality_projection_help": "Q presets include future requirements. This action projects only a supported fixed-sample PNG budget and explicitly omits adaptive stopping, raw linear output and independent transmission limits.",
|
||||
"render.quality_review": "Review sampling budget",
|
||||
"render.queue_pending": "There is no automatic production queue or animation renderer in this UI. Completed still images remain separately exportable.",
|
||||
"render.radial_assign": "Assign Appearance",
|
||||
@@ -3763,7 +3733,7 @@
|
||||
"render.result_navigation": "Pan and zoom use Navigation preferences. Space + drag pans.",
|
||||
"render.result_open": "Open the completed image in the main canvas.",
|
||||
"render.result_preview": "Live Preview",
|
||||
"render.result_provisional": "Provisional image",
|
||||
"render.result_provisional": "Provisional CPU image",
|
||||
"render.result_provisional_help": "This checkpoint is not exportable. Export keeps the last completed result; cancellation returns to that result.",
|
||||
"render.result_reduced_display": "Display preview reduced to the viewport device/cache limit. The retained PNG and export keep full resolution; native 100% inspection is unavailable for this reduced view.",
|
||||
"render.result_retained": "Showing the last completed image; a newer render has not replaced it.",
|
||||
@@ -3927,7 +3897,7 @@
|
||||
"render.stage_panels.prepare_plan": "Prepare creation plan",
|
||||
"render.stage_panels.projects": "Projects",
|
||||
"render.stage_panels.quality_ack": "I acknowledge the fixed budget and its omissions",
|
||||
"render.stage_panels.quality_omissions": "This budget does not set adaptive error targets, independent transmission limits, Russian roulette rules or raw-master retention, and does not guarantee convergence.",
|
||||
"render.stage_panels.quality_omissions": "Adaptive stopping and error targets, separate transmission depth, Russian roulette rules, raw linear output, EXR master and convergence certification.",
|
||||
"render.stage_panels.reference": "Reference",
|
||||
"render.stage_panels.references": "References",
|
||||
"render.stage_panels.remove_optics": "Remove optics",
|
||||
@@ -3971,7 +3941,7 @@
|
||||
"render.stage_panels.subtitle_environment": "Original HDR environment · whole scene",
|
||||
"render.stage_panels.subtitle_maps": "Authored spatial maps · temporary preview",
|
||||
"render.stage_panels.subtitle_physical": "Read-only plan · no material created or assigned",
|
||||
"render.stage_panels.subtitle_quality": "GPU sampling budget · listed fields only",
|
||||
"render.stage_panels.subtitle_quality": "Fixed GPU budget · future requirements omitted",
|
||||
"render.stage_panels.subtitle_rig": "Authored light rig · emitters and passive cards",
|
||||
"render.stage_panels.subtitle_saved_maps": "Authored maps · saved with this shot",
|
||||
"render.stage_panels.subtitle_stage": "Physical ground and stage · fitted to the product",
|
||||
@@ -3996,7 +3966,7 @@
|
||||
"render.studio_area_help": "Up to four one-sided rectangles. Orthogonal half-edges U and V define size; U × V faces the lit side. Coordinates are world-space millimeters.",
|
||||
"render.studio_center": "Center X, Y, Z (mm)",
|
||||
"render.studio_denoise": "Reduce noise in the finished image",
|
||||
"render.studio_denoise_help": "Progressive and final images use an edge-aware filter; fine detail may soften. Glass and sharp specular surfaces bypass it.",
|
||||
"render.studio_denoise_help": "Final edge-aware filter; fine detail can soften. Glass and sharp specular surfaces bypass the filter.",
|
||||
"render.studio_direction": "Direction toward sun (X, Y, Z)",
|
||||
"render.studio_environment": "Environment",
|
||||
"render.studio_environment_bounds": "Environment rotation must be finite within ±1,000,000 radians and intensity between 0 and 100.",
|
||||
@@ -4198,32 +4168,6 @@
|
||||
"sketch.dof_left": "{count} degrees of freedom left",
|
||||
"sketch.dof_left_one": "{count} degree of freedom left",
|
||||
"sketch.draw_curves": "Draw as {linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "Convert the selection to {linetype}",
|
||||
"sketch.open_ends": "Open {count}",
|
||||
"sketch.status_conflict": "constraints conflict",
|
||||
@@ -4445,7 +4389,6 @@
|
||||
"ui.3d_sketch": "3D Sketch",
|
||||
"ui.activate": "Activate",
|
||||
"ui.active_component": "Active component",
|
||||
"ui.addToQuickAccess": "Add to Quick Access",
|
||||
"ui.add_configuration": "Add configuration",
|
||||
"ui.add_parameter": "Add parameter",
|
||||
"ui.agent_chat": "Agent chat",
|
||||
@@ -5074,7 +5017,6 @@
|
||||
"ui.heading_construct": "CONSTRUCT",
|
||||
"ui.heading_create": "CREATE",
|
||||
"ui.heading_create_sketch": "CREATE SKETCH",
|
||||
"ui.heading_custom_view_navigator": "CUSTOM VIEW NAVIGATOR",
|
||||
"ui.heading_cylinder": "CYLINDER",
|
||||
"ui.heading_design": "DESIGN",
|
||||
"ui.heading_draft": "DRAFT",
|
||||
@@ -5361,7 +5303,6 @@
|
||||
"ui.quality": "Quality",
|
||||
"ui.quantity": "Quantity",
|
||||
"ui.quantity_2": "Quantity 2",
|
||||
"ui.quick_access_full": "The quick-access bar holds {count} commands; remove one to add another",
|
||||
"ui.quit": "Quit",
|
||||
"ui.radial_context_menu": "Radial context menu",
|
||||
"ui.radius": "Radius",
|
||||
@@ -5379,7 +5320,6 @@
|
||||
"ui.relink_not_linked": "This component is not linked to a source design.",
|
||||
"ui.relink_select_one": "Select one linked component or one of its instances first.",
|
||||
"ui.relink_source_all": "Relink Source (All Instances)…",
|
||||
"ui.removeFromQuickAccess": "Remove from Quick Access",
|
||||
"ui.removeFromToolbar": "Remove from Toolbar",
|
||||
"ui.removeSection": "Remove Section",
|
||||
"ui.render": "Render",
|
||||
@@ -5388,8 +5328,6 @@
|
||||
"ui.repeat_nothing": "Nothing to repeat yet: start a tool or dialog first.",
|
||||
"ui.replace_with_link_done": "The copies are now placements of the linked design.",
|
||||
"ui.report_a_bug": "Report a bug",
|
||||
"ui.report_characters_left_one": "{count} character left",
|
||||
"ui.report_characters_left_other": "{count} characters left",
|
||||
"ui.reset": "Reset",
|
||||
"ui.reset_all": "Reset All",
|
||||
"ui.reset_the_settings_on_this_page": "Reset the settings on this page",
|
||||
@@ -5674,14 +5612,6 @@
|
||||
"values must be numbers or expressions": "values must be numbers or expressions",
|
||||
"view.auto_ortho_faces": "Perspective while orbiting, ortho on faces",
|
||||
"view.render_inspector": "Render Inspector",
|
||||
"view_gizmo.arrows": "Rotation arrows",
|
||||
"view_gizmo.home": "Home button",
|
||||
"view_gizmo.no_such_body": "This design has no body of that name.",
|
||||
"view_gizmo.note": "Pick a body in the view. It is kept with your preferences and settings profiles, and shown in every design. Click one of its faces to look straight at it.",
|
||||
"view_gizmo.texture": "Texture",
|
||||
"view_gizmo.themed": "Themed",
|
||||
"view_gizmo.too_detailed": "Too detailed for the view navigator (over 10,000 triangles)",
|
||||
"view_gizmo.too_thin": "Too long and thin for the view navigator: its thinnest side must be at least a fifth of its longest",
|
||||
"web.files.insert_drawings_hint": "CAD files, meshes, DXF or SVG into the open design",
|
||||
"web.files.insert_stored_drawings_hint": "Into the open design (CAD files, meshes, DXF, SVG)",
|
||||
"web.open_full": "Open in SolidCraft",
|
||||
|
||||
@@ -956,6 +956,8 @@
|
||||
"cmd.inspect_accessibility": "Analyse d'accessibilité",
|
||||
"cmd.inspect_center_of_mass": "Centre de gravité",
|
||||
"cmd.inspect_clearance": "Jeu",
|
||||
"cmd.inspect_colored_components": "Activer les couleurs des composants",
|
||||
"cmd.inspect_colored_components_disable": "Désactiver les couleurs des composants",
|
||||
"cmd.inspect_compare": "Comparer les corps",
|
||||
"cmd.inspect_compare_picture": "Comparer les images",
|
||||
"cmd.inspect_continuity": "Analyse de continuité",
|
||||
@@ -1363,14 +1365,11 @@
|
||||
"dialog.computing": "Calcul… {seconds} s",
|
||||
"dialog.could_not_be_checked": "non vérifiable",
|
||||
"dialog.custom_axis": "↖ Axe personnalisé",
|
||||
"dialog.cylinder_face_tip": "La face ou le sommet du modèle sur lequel le cylindre est posé : cliquer ici, puis dessus dans la vue. Laissé vide, X, Y et Z le placent.",
|
||||
"dialog.dimension_insert_hint": "Double-cliquez sur une autre cote pilotante pour insérer son nom",
|
||||
"dialog.dimension_name": "Cote : {name}",
|
||||
"dialog.edit_title": "MODIFIER {title}",
|
||||
"dialog.no_shown_body": "Aucun corps affiché à modifier : affichez-en un ou choisissez Nouveau corps",
|
||||
"dialog.pattern_sketch_not_object": "Choisissez des corps ou des fonctions : une esquisse n'est ni l'un ni l'autre. Répétez ses courbes avec la Répétition ou la Symétrie de l'esquisse, ou extrudez-la d'abord.",
|
||||
"dialog.primitive_point_tip": "Un point qui remplit X, Y et Z : cliquer ici, puis sur un sommet, une arête ou une face du modèle, sur un point d'une esquisse ou sur l'origine.",
|
||||
"dialog.reference_edge_tip": "Une arête droite de la face choisie, depuis laquelle la distance ci-dessous est mesurée : cliquer ici, puis sur l'arête.",
|
||||
"dialog.select": "↖ Sélectionner",
|
||||
"dialog.selected_count": "↖ {count} sélectionné(s)",
|
||||
"dialog.selection_constraint": "Contrainte {id}",
|
||||
@@ -3002,11 +3001,9 @@
|
||||
"render.frame_wide": "16:9 · Écran large",
|
||||
"render.gpu_depth": "Profondeur maximale des chemins",
|
||||
"render.gpu_options_help": "Les échantillons, la profondeur, la graine et l'exposition sont enregistrés avec ce préréglage. Seule la méthode d'image active est enregistrée.",
|
||||
"render.gpu_path_crashed": "SolidCraft s’est fermé pendant sa dernière préparation sur cet adaptateur graphique, très probablement dans le pilote graphique ; il n’est donc plus préparé de lui-même. Utilisez le lancer de rayons sur le CPU, ou réessayez (une mise à jour du pilote réessaie aussi).",
|
||||
"render.gpu_path_failed": "Traceur de chemins GPU indisponible",
|
||||
"render.gpu_path_first_setup": "La première initialisation sur ce GPU peut prendre plusieurs minutes ; SolidCraft reste utilisable pendant ce temps. Les démarrages suivants réutilisent les shaders compilés lorsque le pilote les conserve.",
|
||||
"render.gpu_path_preparing": "Traceur de chemins GPU : compilation des shaders…",
|
||||
"render.gpu_path_retry": "Réessayer",
|
||||
"render.gpu_seed": "Graine aléatoire",
|
||||
"render.gpu_unavailable": "Le tracé de chemins GPU n'est pas prêt sur cet appareil.",
|
||||
"render.ground_apply": "Ajuster et enregistrer le sol",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Análise de accesibilidade",
|
||||
"cmd.inspect_center_of_mass": "Centro de masas",
|
||||
"cmd.inspect_clearance": "Folga",
|
||||
"cmd.inspect_colored_components": "Activar as cores dos compoñentes",
|
||||
"cmd.inspect_colored_components_disable": "Desactivar as cores dos compoñentes",
|
||||
"cmd.inspect_compare": "Comparar corpos",
|
||||
"cmd.inspect_compare_picture": "Comparar a imaxe",
|
||||
"cmd.inspect_continuity": "Análise de continuidade",
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
"Bottom Right": "למטה מימין",
|
||||
"Boundary": "גבול",
|
||||
"Bounding box": "תיבה תוחמת",
|
||||
"Bridge": "גשר",
|
||||
"CHECK": "בדיקה",
|
||||
"COMPONENTS": "רכיבים",
|
||||
"CONFIGURATIONS": "תצורות",
|
||||
@@ -964,7 +963,8 @@
|
||||
"cmd.inspect_analysis": "ניתוח תצוגה",
|
||||
"cmd.inspect_center_of_mass": "מרכז מסה",
|
||||
"cmd.inspect_clearance": "מרווח",
|
||||
"cmd.inspect_colored_components": "צבעי רכיבים",
|
||||
"cmd.inspect_colored_components": "הפעלת צבעי רכיבים",
|
||||
"cmd.inspect_colored_components_disable": "ביטול צבעי רכיבים",
|
||||
"cmd.inspect_compare": "השוואת גופים",
|
||||
"cmd.inspect_compare_picture": "השוואה לתמונת ייחוס",
|
||||
"cmd.inspect_continuity": "ניתוח רציפות",
|
||||
@@ -1296,7 +1296,6 @@
|
||||
"cmd.timeline_stop": "עצירת ציר הזמן",
|
||||
"cmd.timeline_suppress": "השבתת פיצ'ר",
|
||||
"cmd.ui.assistant": "הצגה או הסתרה של העוזר",
|
||||
"cmd.view.navigator": "ניווט תצוגה מותאם אישית",
|
||||
"cmd.view.render_browser": "דפדפן רינדור",
|
||||
"cmd.view.render_dock": "הזז את עגינת הרינדור",
|
||||
"cmd.view.render_focus": "תצוגת מיקוד רינדור",
|
||||
@@ -1395,7 +1394,6 @@
|
||||
"dialog.computing": "מחשב… {seconds} שנ'",
|
||||
"dialog.could_not_be_checked": "לא ניתן היה לבדוק",
|
||||
"dialog.custom_axis": "↖ ציר מותאם",
|
||||
"dialog.cylinder_face_tip": "הפאה או הקודקוד של המודל שעליהם הגליל ניצב: יש ללחוץ כאן ואז עליהם בתצוגה. אם נשאר ריק, X, Y ו-Z ממקמים אותו.",
|
||||
"dialog.dimension_insert_hint": "יש ללחוץ על מידה מניעה אחרת כדי להוסיף את שמה",
|
||||
"dialog.dimension_name": "מידה: {name}",
|
||||
"dialog.dimension_step_down": "הפחתה ב־{step}",
|
||||
@@ -1404,8 +1402,6 @@
|
||||
"dialog.edit_title": "עריכת {title}",
|
||||
"dialog.no_shown_body": "אין גוף מוצג לשינוי: הציגו גוף, או בחרו „גוף חדש”",
|
||||
"dialog.pattern_sketch_not_object": "בחרו גופים או פיצ'רים: סקיצה אינה אף אחד מהם. חזרו על העקומות שלה בעזרת המערך או השיקוף של הסקיצה עצמה, או בצעו לה שליפה קודם.",
|
||||
"dialog.primitive_point_tip": "נקודה שממלאת את X, Y ו-Z: יש ללחוץ כאן ואז על קודקוד, מקצוע או פאה של המודל, על נקודה של סקיצה, או על הראשית.",
|
||||
"dialog.reference_edge_tip": "מקצוע ישר של הפאה שנבחרה שממנו נמדד המרחק שלמטה: יש ללחוץ כאן ואז על המקצוע.",
|
||||
"dialog.select": "↖ בחירה",
|
||||
"dialog.selected_count": "↖ {count} נבחרו",
|
||||
"dialog.selection_constraint": "אילוץ {id}",
|
||||
@@ -1449,9 +1445,6 @@
|
||||
"each rail's curves must be in one sketch": "עקומות כל מסילה חייבות להיות בסקיצה אחת",
|
||||
"feature computation ended without a result": "חישוב הפיצ'ר הסתיים ללא תוצאה",
|
||||
"file.open.drawing_filter": "תכנים, קובצי CAD, רשתות משולשים ושרטוטים",
|
||||
"file.open.portal_fallback.no_portal_service": "בורר הקבצים שנפתח אינו של שולחן העבודה: שירות בחירת הקבצים של שולחן העבודה אינו זמין.",
|
||||
"file.open.portal_fallback.no_session_bus": "בורר הקבצים שנפתח אינו של שולחן העבודה: אין אוטובוס הפעלה של D-Bus.",
|
||||
"file.open.portal_fallback.xwayland": "בורר הקבצים שנפתח אינו של שולחן העבודה: תחת XWayland הפורטל דוחה את חלון האב.",
|
||||
"file_load.built": "נבנו עד כה: {built} מתוך {total} פיצ'רים.",
|
||||
"file_load.converting": "המרת גוף מוצק {done} מתוך {total}",
|
||||
"file_load.inserting": "הוספת גאומטריה",
|
||||
@@ -2042,8 +2035,6 @@
|
||||
"nav.blender.help": "גרירה אמצעית מסובבת, Shift וגרירה אמצעית מזיזים, Ctrl וגרירה אמצעית מבצעים זום; גרירה ימנית לא מזיזה את המצלמה",
|
||||
"nav.creo.help": "גרירה בלחצן האמצעי מסובבת את התצוגה, Shift והלחצן האמצעי מזיזים אותה, Ctrl וגרירה אנכית בלחצן האמצעי משנים את הזום (גרירה מטה להגדלה); גלילת הגלגלת משנה גם היא את הזום",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "גרירה בכפתור האמצעי או הימני ממשיכה לסובב, להזיז או לשנות זום כפי שהתחילה עד שמשחררים את הכפתור, בלי קשר למקשים שנלחצים או משתחררים בינתיים. כבוי: המקשים הלחוצים קובעים בכל רגע של הגרירה.",
|
||||
"nav.drag_keeps_mode.label": "גרירה שומרת על הניווט שבו התחילה",
|
||||
"nav.free_orbit.help": "העכבר והעכבר התלת־ממדי מסובבים דרך שני הקטבים ושומרים על הגלגול. הצמדת הצירים עם Shift מושבתת.",
|
||||
"nav.free_orbit.label": "סיבוב חופשי",
|
||||
"nav.free_orbit.snap_help": "העכבר והעכבר התלת־ממדי מסובבים דרך שני הקטבים ושומרים על הגלגול. יש להחזיק את Shift בזמן גרירת סיבוב כדי להצמיד את כיוון התצוגה ואת הגלגול.",
|
||||
@@ -2280,9 +2271,6 @@
|
||||
"prefs.auto_operation_note": "איחוד, גוף חדש או חיתוך, לפי מה שהחומר נוגע בו.",
|
||||
"prefs.axes": "צירים",
|
||||
"prefs.axis": "ציר",
|
||||
"prefs.background_choose": "בחירת תמונה…",
|
||||
"prefs.background_image": "תמונה",
|
||||
"prefs.background_unreadable": "לא ניתן לקרוא את תמונת הרקע",
|
||||
"prefs.blur_cost": "עלות עיבוד יחסית. יותר פסים מציינים בדרך כלל יותר עבודה.",
|
||||
"prefs.blur_device": "לא זמין בהתקן הגרפי או בנתיב העיבוד הזה",
|
||||
"prefs.blur_guard": "איכות הטשטוש הופחתה כדי לשמור על תגובתיות הפריימים. הרדיוס לא השתנה.",
|
||||
@@ -2304,8 +2292,6 @@
|
||||
"prefs.browser_eye_left": "סמל העין ליד חץ הקיפול",
|
||||
"prefs.browser_eye_left_note": "שורות עץ התכן מציגות את סמל העין ליד חץ הקיפול, כמו קודם, במקום בעמודה בקצה הימני של השורות.",
|
||||
"prefs.browser_reveal_on_hover": "הצגה בריחוף",
|
||||
"prefs.browser_rows_start_closed": "שורות עץ התכן מוצגות סגורות",
|
||||
"prefs.browser_rows_start_closed_note": "מיקומים ותיקיות בעץ התכן מוצגים סגורים, כך שתכן עם רכיבים רבים מציג את השורות העליונות שלו. כל שורה נפתחת בחץ שלה, ב„הרחבת הכול” או ב„איתור בעץ התכן”, שפותח רק את השורות שמובילות לגוף שלו וסוגר שוב את אלה שהאיתור הקודם פתח. גוף שנבחר בתצוגה התלת־ממדית משאיר את עץ התכן כפי שהוא. כשהאפשרות כבויה התיקיות מוצגות פתוחות, המיקומים מוצגים כפי שקובעת האפשרות „רכיבים פתוחים מההתחלה”, ובחירה בתצוגה התלת־ממדית פותחת את עץ התכן עד לגוף שלה, כמו קודם.",
|
||||
"prefs.browser_show_note": "״מוצג״ משאיר את הדפדפן על המסך. ״הצגה בריחוף״ מסתיר אותו עד שהמצביע מגיע למקומו או ללשונית הדקה בקצהו השמאלי, והוא נשאר פתוח כל עוד המצביע בתוכו או שתפריט ממנו פתוח.",
|
||||
"prefs.browser_shown": "מוצג",
|
||||
"prefs.built_in": "מובנה",
|
||||
@@ -2437,10 +2423,6 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "שני החצים המעוגלים בפינת הקובייה מסובבים את התצוגה במישור המסך. פועל: החץ עם כיוון השעון מסובב נגד כיוון השעון, והחץ נגד כיוון השעון מסובב עם כיוון השעון. כבוי: כל חץ מסובב את התצוגה לפי העקום שלו.",
|
||||
"prefs.invert_navigator_arrows": "היפוך חצי סיבוב קוביית התצוגה",
|
||||
"prefs.invert_navigator_arrows_note": "החצים סביב הקובייה מחוויים את המצלמה לכיוון שהחץ מצביע, כמו ב-Fusion ו-Inventor: החץ שמשמאל מצביע ימימה ומניע את המצלמה ימימה. כבוי: החץ מסובב את הקובייה אל הצד שבו הוא יושב, כך שחץ שמאל מניע את המצלמה שמאלה. חצי הגלילה המעוגלים אינם מושפעים.",
|
||||
"prefs.keep_unsaved_also_asks": "לשאול גם כשהכרטיסייה נסגרת",
|
||||
"prefs.keep_unsaved_also_asks_tip": "כשאפשרות זו כבויה, הכרטיסייה נסגרת ללא חלון הדפדפן לאחר שהעותק של כל תכן שלא נשמר עודכן.",
|
||||
"prefs.keep_unsaved_in_browser": "לשמור עבודה שלא נשמרה בדפדפן זה",
|
||||
"prefs.keep_unsaved_in_browser_tip": "השינויים שלא נשמרו בכל תכן פתוח נשמרים מעודכנים באחסון הפרטי של דפדפן זה. בביקור הבא תוצע שחזורם.",
|
||||
"prefs.keep_work_on_tab_switch": "השארת תיבות דו־שיח וכלים פתוחים במעבר בין עיצובים",
|
||||
"prefs.keep_work_on_tab_switch_note": "כל לשונית עיצוב שומרת את תיבת הדו־שיח או הכלי הפתוחים בה, עם מה שבחרת והקלדת בהם, ואת לשונית סרגל הכלים שלה. הם מחכים לך כשתחזור לעיצוב. כבוי: מעבר בין עיצובים מבטל אותם ומציג את הלשונית מודל.",
|
||||
"prefs.labels": "תוויות",
|
||||
@@ -2572,8 +2554,6 @@
|
||||
"prefs.section_hatch_by_part": "לפי חלק",
|
||||
"prefs.section_hatch_one": "סגנון אחד",
|
||||
"prefs.select": "בחירה",
|
||||
"prefs.select_hidden_edges": "בחירת קצוות מוסתרים בתצוגה מוצללת עם קצוות מוסתרים",
|
||||
"prefs.select_hidden_edges_help": "מאפשר לבחור קצוות דרך פאות בתצוגה מוצללת עם קצוות מוסתרים, גם עבור Fillet. כיבוי שומר על אופן הבחירה של v0.30.1. גופים שהוסתרו עדיין אינם ניתנים לבחירה.",
|
||||
"prefs.select_one_item": "בחירת פריט אחד",
|
||||
"prefs.select_other_hidden": "בחירת גאומטריה מוסתרת באמצעות בחירה אחרת",
|
||||
"prefs.select_other_hidden_help": "מופעל: כולל גופים, סקיצות וקצוות מוסתרים בקרבת הסמן. כבוי: שימוש ברשימה המקורית של גאומטריה גלויה מתחת לסמן.",
|
||||
@@ -2598,8 +2578,6 @@
|
||||
"prefs.sketch.click_selects": "לחיצה בוחרת",
|
||||
"prefs.sketch.conflict_colour": "צבע התנגשות",
|
||||
"prefs.sketch.drawing": "שרטוט",
|
||||
"prefs.sketch.ellipse_axes": "שרטוט אליפסות עם הצירים שלהן",
|
||||
"prefs.sketch.ellipse_axes_note": "הציר הראשי והציר המשני כקווי עזר, כדי שאפשר יהיה לתת מידות לחצאי הצירים ולצירים. כבוי: האליפסה בלבד, ו-D עליה נותן את הרדיוס המשני שלה בכל מקום שאליו נגרר הערך, משורטט ומסומן R כמו קודם.",
|
||||
"prefs.sketch.exit_keep": "שמירה",
|
||||
"prefs.sketch.exit_previous": "חזרה לתצוגה הקודמת",
|
||||
"prefs.sketch.exit_unless_rotated": "אלא אם סובבה",
|
||||
@@ -2769,12 +2747,6 @@
|
||||
"recovery.just_now": "זה עתה",
|
||||
"recovery.min_ago": "לפני {count} דק'",
|
||||
"recovery.never_saved": "לא נשמר מעולם",
|
||||
"recovery.open_saved": "פתיחת הגרסה השמורה",
|
||||
"recovery.restore": "שחזור",
|
||||
"recovery.restore_intro": "שינויים שלא נשמרו כשדף זה נסגר לאחרונה שמורים בדפדפן זה.",
|
||||
"recovery.restore_row": "{name}, שונה {when}",
|
||||
"recovery.restore_title": "לשחזר תכנים שלא נשמרו?",
|
||||
"recovery.restore_title_one": "לשחזר תכן שלא נשמר: {name}, שונה {when}?",
|
||||
"refused.change": "לא ניתן היה לשנות את {body}.",
|
||||
"refused.cut": "לא ניתן היה לחתוך את {body}.",
|
||||
"refused.face_curved": "אחת הפאות המעוקלות שלו אינה ניתנת לשימוש במידול.",
|
||||
@@ -2789,9 +2761,9 @@
|
||||
"render.adaptive_absolute": "רצפת שגיאת RGB מוחלטת",
|
||||
"render.adaptive_coverage": "יעד שגיאת כיסוי",
|
||||
"render.adaptive_enable": "הקצאת דגימות באמצעות דגימות גישוש עצמאיות",
|
||||
"render.adaptive_help": "דגימות החלוץ העצמאיות נזרקות, ואז נקבעות לכל פיקסל לפחות 16 דגימות הפקה, עד לערך דגימות לפיקסל. מגבלות משאבי CPU כוללות את שני השלבים. השונות ב-RGB ובכיסוי מנחה את ההקצאה; פיקסלים שהגיעו לתקרה עשויים להישאר רועשים. הדוח הסופי מונה את הדגימות בפועל.",
|
||||
"render.adaptive_help": "GPU בלבד. דגימות הגישוש העצמאיות מושלכות, ואז מוקצות 16 דגימות הפקה או יותר לכל פיקסל עד לערך „דגימות לכל פיקסל”. השונות של RGB ושל הכיסוי מכוונת את ההקצאה; מסלולים נדירים עלולים להיות מוחמצים ופיקסלים שהגיעו לתקרה עשויים להישאר רועשים. הדוח הסופי סופר את העבודה בפועל. אין ערובה מאושרת לרמת הרעש או לזמן.",
|
||||
"render.adaptive_minimum": "מינימום דגימות הפקה",
|
||||
"render.adaptive_pending": "הקצאה מסתגלת עם דגימות חלוץ עצמאיות זמינה ב-CPU וב-GPU. שכבות פרנל מותנות דורשות GPU. ההקצאה הסטטיסטית אינה מבטיחה התכנסות.",
|
||||
"render.adaptive_pending": "הקצאה אדפטיבית ב־GPU עם דגימות גישוש עצמאיות ושכבות פרנל מותנות זמינות. התכנסות מאושרת, ערובה לאירועים נדירים והעברה אדפטיבית ב־CPU עדיין אינן זמינות.",
|
||||
"render.adaptive_pilots": "דגימות גישוש לכל פיקסל",
|
||||
"render.adaptive_relative": "יעד שגיאת RGB יחסית",
|
||||
"render.add_ground": "הוספת קרקע מותאמת להגדרה",
|
||||
@@ -2852,7 +2824,7 @@
|
||||
"render.appearance_editor.replace": "החלף את האופטיקה השמורה של גוף זה",
|
||||
"render.appearance_editor.revalidate": "אמת מחדש גוף זה",
|
||||
"render.appearance_editor.save": "שמור אופטיקה זו",
|
||||
"render.appearance_editor.save_reason": "כדי לשמור זכוכית, יש להפעיל אותה בגוף CPU או GPU עדכני. לציפוי שקוף יש להשתמש בשמירת הציפוי בתצלום. קובצי תמונה מקומיים נשארים זמניים.",
|
||||
"render.appearance_editor.save_reason": "שמירת האופטיקה הזו דורשת זכוכית פיזיקלית בגוף GPU עדכני. הציפוי ומפות התמונה המקומיות נשארים זמניים.",
|
||||
"render.appearance_editor.saved": "נשמר עם הצילום הזה",
|
||||
"render.appearance_editor.saved_state": "האופטיקה הזו נשמרת עם הצילום הזה.",
|
||||
"render.appearance_editor.scatter_reason": "פיזור פנימי דורש גבול של גוף מלא סגור.",
|
||||
@@ -2948,7 +2920,7 @@
|
||||
"render.children.copy_request": "העתק JSON של הבקשה",
|
||||
"render.children.coverage_error": "שגיאת כיסוי",
|
||||
"render.children.cpu": "מעקב קרניים במעבד",
|
||||
"render.children.denoise": "הפחתת רעש בתמונה",
|
||||
"render.children.denoise": "הפחת רעש בתמונה המוגמרת",
|
||||
"render.children.depth": "עומק נתיב",
|
||||
"render.children.distortion": "עיוות",
|
||||
"render.children.edge_samples": "דגימות קצה",
|
||||
@@ -3080,7 +3052,7 @@
|
||||
"render.cpu_point_help": "עד שמונה אורות נקודתיים, המתווספים לתאורה הכיוונית שנבחרה. המרחקים במילימטרים; המינימום אינו יכול לעלות על מרחק הייחוס.",
|
||||
"render.cpu_primary_samples": "דגימות קצה לכל פיקסל",
|
||||
"render.cpu_progressive": "CPU הדרגתי",
|
||||
"render.cpu_progressive_help": "נאספות דגימות מצלמת CPU חדשות ומוצג כל מעבר שהושלם. השתקפויות נבחרות משתמשות בניסיון אחד לכל דגימת מצלמה; זכוכית מאושרת ואנאיזוטרופיה מישורית שומרות על מגבלותיהן הקיימות. דגימות החלוץ וההפקה המסתגלות חולקות את תקציב פרופיל CPU.",
|
||||
"render.cpu_progressive_help": "צבירה של אינטגרטור ה־CPU הקיים במעברים של דגימה ראשית אחת. החזרות שנבחרו דורשות דגימה אחת; אורות שטח, דיאלקטריים ואנאיזוטרופיה דורשים תמונה בודדת.",
|
||||
"render.cpu_progressive_limits": "לכל היותר 1,048,576 פיקסלים ו־16,777,216 הערכות פיקסל × דגימה. שינוי הדגימות לעולם אינו משנה את גודל התמונה אוטומטית.",
|
||||
"render.cpu_progressive_seed": "זרע דגימה ב־CPU",
|
||||
"render.cpu_progressive_spp": "דגימות לכל פיקסל",
|
||||
@@ -3256,7 +3228,7 @@
|
||||
"render.export_display": "ייצוא: PNG לתצוגה",
|
||||
"render.export_image_label": "ייצוא תמונה",
|
||||
"render.export_linear": "ייצוא: EXR ליניארי גולמי",
|
||||
"render.exr_pending": "עקיבת קרניים ב־CPU ועקיבת נתיבים ב־GPU יכולות לשמור EXR ליניארי גולמי. דחיסה, תמונות רב־שכבתיות ומעברי הדמיה נלווים עדיין אינם זמינים.",
|
||||
"render.exr_pending": "GPU יכול לשמור EXR ליניארי גולמי. דחיסה, תמונות רב־שכבתיות ומעברי הדמיה נלווים עדיין אינם זמינים.",
|
||||
"render.film_grain": "גרעיניות צילומית",
|
||||
"render.fit": "התאמת המוצר לתצוגה",
|
||||
"render.fixed_camera": "מצלמה",
|
||||
@@ -3296,11 +3268,9 @@
|
||||
"render.frame_wide": "16:9 · רחב",
|
||||
"render.gpu_depth": "עומק מסלול מרבי",
|
||||
"render.gpu_options_help": "הדגימות, העומק, הזרע והחשיפה נשמרים עם ההגדרה הזאת. נשמרת רק שיטת התמונה הפעילה.",
|
||||
"render.gpu_path_crashed": "SolidCraft נסגר בזמן ההכנה על מתאם גרפי זה בפעם הקודמת, ככל הנראה בתוך מנהל ההתקן הגרפי, ולכן ההכנה לא תתבצע שוב מעצמה. יש להשתמש במעקב קרניים על המעבד במקום זאת, או לנסות שוב (עדכון מנהל ההתקן גם מנסה שוב).",
|
||||
"render.gpu_path_failed": "מעקב המסלולים ב־GPU אינו זמין",
|
||||
"render.gpu_path_first_setup": "ההגדרה הראשונה ב־GPU הזה עשויה להימשך כמה דקות; SolidCraft נשאר זמין לשימוש בינתיים. הפעלות מאוחרות יותר משתמשות שוב בשיידרים שהודרו, כשמנהל ההתקן שומר אותן.",
|
||||
"render.gpu_path_preparing": "מעקב מסלולים ב־GPU: הידור שיידרים…",
|
||||
"render.gpu_path_retry": "לנסות שוב",
|
||||
"render.gpu_seed": "זרע אקראי",
|
||||
"render.gpu_unavailable": "מעקב מסלולים ב־GPU אינו מוכן במכשיר הזה.",
|
||||
"render.ground_apply": "התאמת הקרקע ושמירתה",
|
||||
@@ -3619,7 +3589,7 @@
|
||||
"render.method_cpu": "מעקב קרניים ב־CPU",
|
||||
"render.method_gpu": "מעקב מסלולים ב־GPU",
|
||||
"render.method_limit_raster": "רסטר משתמש בתצוגה המקדימה הקבועה של הסטודיו האטום. לפקדים האופטיים הנתמכים יש לבחור שיטת מעקב קרניים מפורשת.",
|
||||
"render.method_raster": "רסטר ב־CPU (סכמטי)",
|
||||
"render.method_raster": "רסטר ב־CPU",
|
||||
"render.navigation_help": "המצלמה נעה כמו בתכן (העדפות › ניווט). שמירת ההגדרה מתעדת את המצלמה הנוכחית.",
|
||||
"render.nested_enable": "דיאלקטריים מקוננים מאושרים",
|
||||
"render.nested_help": "עד 16 שכבות מכילות; כל הורה חייב להיות קמור וכל צאצא חייב להיות בתוכו לגמרי. חיתוכים, משטחים נוגעים, יריעות דקות והעברת צל ישרה נדחים. מצב מצלמה בפנים עוקב אחר ההיררכיה המאושרת. אינו יוצר משטח נוזל ואינו ממזג גבולות CAD נוגעים.",
|
||||
@@ -3647,7 +3617,7 @@
|
||||
"render.output_exr_unavailable": "הגדרת פלט EXR אינה זמינה כאן. ייצוא EXR לינארי דורש תמונת GPU שהושלמה עם מאסטר לינארי שנשמר.",
|
||||
"render.output_fit": "התאם",
|
||||
"render.output_format": "פורמט",
|
||||
"render.output_format_status": "PNG הוא תמונת התצוגה הגמורה המדויקת. EXR אופציונלי ב־CPU או ב־GPU שומר קרינה גולמית ליניארית של הסצנה ב־FLOAT32 לפני מסנני התצוגה. הייצוא לעולם אינו מחליף אותה במטמון התצוגה המקדימה או במשימה אחרת.",
|
||||
"render.output_format_status": "PNG הוא תמונת התצוגה הגמורה המדויקת. EXR אופציונלי ב־GPU שומר קרינה גולמית ליניארית של הסצנה ב־FLOAT32 לפני מסנני התצוגה. הייצוא לעולם אינו מחליף אותה במטמון התצוגה המקדימה או במשימה אחרת.",
|
||||
"render.output_grading": "גריידינג",
|
||||
"render.output_image": "תמונה",
|
||||
"render.output_largest": "מקס׳ {width} × {height}",
|
||||
@@ -3732,7 +3702,7 @@
|
||||
"render.quality_custom": "מותאם",
|
||||
"render.quality_draft": "טיוטה",
|
||||
"render.quality_high": "גבוהה",
|
||||
"render.quality_projection_help": "בדיקה זו משנה רק רזולוציה, תקציב דגימות, עומק נתיב, זרע והפחתת רעש. ההגדרות האחרות נשמרות; התקציב אינו מעיד על התכנסות התמונה.",
|
||||
"render.quality_projection_help": "פרופילי Q מוכנים כוללים דרישות עתידיות. פעולה זו משליכה רק תקציב PNG נתמך בדגימות קבועות, ומשמיטה במפורש עצירה אדפטיבית, פלט ליניארי גולמי ומגבלות העברה עצמאיות.",
|
||||
"render.quality_review": "סקירת תקציב הדגימה",
|
||||
"render.queue_pending": "בממשק זה אין תור הפקה אוטומטי ואין מנוע הדמיה לאנימציה. תמונות סטילס גמורות נשארות ניתנות לייצוא כל אחת בנפרד.",
|
||||
"render.radial_assign": "הקצה מראה",
|
||||
@@ -3763,7 +3733,7 @@
|
||||
"render.result_navigation": "הזזה וזום משתמשים בהעדפות הניווט. רווח + גרירה מזיזים.",
|
||||
"render.result_open": "פתיחת התמונה הגמורה בקנבס הראשי.",
|
||||
"render.result_preview": "תצוגה מקדימה חיה",
|
||||
"render.result_provisional": "תמונה זמנית",
|
||||
"render.result_provisional": "תמונת CPU זמנית",
|
||||
"render.result_provisional_help": "נקודת ביקורת זו אינה ניתנת לייצוא. הייצוא שומר את התוצאה הגמורה האחרונה; ביטול חוזר לתוצאה הזאת.",
|
||||
"render.result_reduced_display": "התצוגה המקדימה הוקטנה למגבלת המכשיר או המטמון של אזור התצוגה. ה־PNG השמור והייצוא שומרים על רזולוציה מלאה; בדיקה ברזולוציה המקורית (100%) אינה זמינה בתצוגה המוקטנת הזאת.",
|
||||
"render.result_retained": "מוצגת התמונה הגמורה האחרונה; הדמיה חדשה יותר לא החליפה אותה.",
|
||||
@@ -3927,7 +3897,7 @@
|
||||
"render.stage_panels.prepare_plan": "הכנת תוכנית יצירה",
|
||||
"render.stage_panels.projects": "מקרין",
|
||||
"render.stage_panels.quality_ack": "ידוע לי על התקציב הקבוע והפרטים המושמטים בו",
|
||||
"render.stage_panels.quality_omissions": "תקציב זה אינו קובע יעדי שגיאה אדפטיביים, מגבלות מעבר אור נפרדות, כללי רולטה רוסית או שמירת מקור גולמי, ואינו מבטיח התכנסות של התמונה.",
|
||||
"render.stage_panels.quality_omissions": "עצירה מסתגלת ויעדי שגיאה, עומק העברה נפרד, כללי רולטה רוסית, פלט ליניארי גולמי, קובץ מקור EXR ואישור התכנסות.",
|
||||
"render.stage_panels.reference": "ייחוס",
|
||||
"render.stage_panels.references": "ייחוסים",
|
||||
"render.stage_panels.remove_optics": "הסרת האופטיקה",
|
||||
@@ -3971,7 +3941,7 @@
|
||||
"render.stage_panels.subtitle_environment": "סביבת HDR מקורית · הסצנה כולה",
|
||||
"render.stage_panels.subtitle_maps": "מפות מרחביות מעוצבות · תצוגה מקדימה זמנית",
|
||||
"render.stage_panels.subtitle_physical": "תוכנית לקריאה בלבד · לא נוצר או משויך חומר",
|
||||
"render.stage_panels.subtitle_quality": "תקציב דגימה ב־GPU · השדות המפורטים בלבד",
|
||||
"render.stage_panels.subtitle_quality": "תקציב GPU קבוע · דרישות עתידיות מושמטות",
|
||||
"render.stage_panels.subtitle_rig": "מערך תאורה מעוצב · פולטי אור וכרטיסים סבילים",
|
||||
"render.stage_panels.subtitle_saved_maps": "מפות מעוצבות · נשמרות עם צילום זה",
|
||||
"render.stage_panels.subtitle_stage": "קרקע ובמה פיזיות · מותאמות למוצר",
|
||||
@@ -3996,7 +3966,7 @@
|
||||
"render.studio_area_help": "עד ארבעה מלבנים חד־צדדיים. חצאי הצלעות הניצבים U ו־V קובעים את הגודל; U × V פונה אל הצד המואר. הקואורדינטות במילימטרים של מרחב העולם.",
|
||||
"render.studio_center": "מרכז X, Y, Z (mm)",
|
||||
"render.studio_denoise": "הפחתת רעש בתמונה הסופית",
|
||||
"render.studio_denoise_help": "תמונות הדרגתיות וסופיות משתמשות במסנן שומר קצוות; פרטים עדינים עלולים להתרכך. זכוכית ומשטחים בעלי החזר חד אינם עוברים סינון.",
|
||||
"render.studio_denoise_help": "מסנן סופי שמתחשב בקצוות; פרטים עדינים עלולים להתרכך. זכוכית ומשטחים ספקולריים חדים עוקפים את המסנן.",
|
||||
"render.studio_direction": "כיוון אל השמש (X, Y, Z)",
|
||||
"render.studio_environment": "סביבה",
|
||||
"render.studio_environment_bounds": "סיבוב הסביבה חייב להיות סופי בטווח ±1,000,000 רדיאנים והעוצמה בין 0 ל־100.",
|
||||
@@ -4198,32 +4168,6 @@
|
||||
"sketch.dof_left": "נותרו {count} דרגות חופש",
|
||||
"sketch.dof_left_one": "נותרה {count} דרגת חופש",
|
||||
"sketch.draw_curves": "שרטוט כ־{linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "המרת הבחירה ל־{linetype}",
|
||||
"sketch.open_ends": "קצוות פתוחים: {count}",
|
||||
"sketch.status_conflict": "אילוצים סותרים",
|
||||
@@ -4445,7 +4389,6 @@
|
||||
"ui.3d_sketch": "סקיצה תלת־ממדית",
|
||||
"ui.activate": "הפעלה",
|
||||
"ui.active_component": "רכיב פעיל",
|
||||
"ui.addToQuickAccess": "הוספה לגישה מהירה",
|
||||
"ui.add_configuration": "הוספת תצורה",
|
||||
"ui.add_parameter": "הוספת פרמטר",
|
||||
"ui.agent_chat": "צ'אט עם סוכן",
|
||||
@@ -5074,7 +5017,6 @@
|
||||
"ui.heading_construct": "עזר",
|
||||
"ui.heading_create": "יצירה",
|
||||
"ui.heading_create_sketch": "יצירת סקיצה",
|
||||
"ui.heading_custom_view_navigator": "ניווט תצוגה מותאם אישית",
|
||||
"ui.heading_cylinder": "גליל",
|
||||
"ui.heading_design": "תכן",
|
||||
"ui.heading_draft": "שיפוע יציקה",
|
||||
@@ -5361,7 +5303,6 @@
|
||||
"ui.quality": "איכות",
|
||||
"ui.quantity": "כמות",
|
||||
"ui.quantity_2": "כמות 2",
|
||||
"ui.quick_access_full": "סרגל הגישה המהירה מכיל {count} פקודות; יש להסיר אחת כדי להוסיף אחרת",
|
||||
"ui.quit": "יציאה",
|
||||
"ui.radial_context_menu": "תפריט הקשר מעגלי",
|
||||
"ui.radius": "רדיוס",
|
||||
@@ -5379,7 +5320,6 @@
|
||||
"ui.relink_not_linked": "רכיב זה אינו מקושר לתכן מקור.",
|
||||
"ui.relink_select_one": "יש לבחור תחילה רכיב מקושר אחד או אחד המופעים שלו.",
|
||||
"ui.relink_source_all": "קישור מחדש למקור (כל המופעים)…",
|
||||
"ui.removeFromQuickAccess": "הסרה מהגישה המהירה",
|
||||
"ui.removeFromToolbar": "הסרה מסרגל הכלים",
|
||||
"ui.removeSection": "הסרת מקטע",
|
||||
"ui.render": "הדמיה",
|
||||
@@ -5388,8 +5328,6 @@
|
||||
"ui.repeat_nothing": "אין עדיין מה לחזור עליו: התחל קודם כלי או תיבת דו־שיח.",
|
||||
"ui.replace_with_link_done": "העותקים הם עכשיו מיקומים של התכן המקושר.",
|
||||
"ui.report_a_bug": "דיווח על באג",
|
||||
"ui.report_characters_left_one": "תווים שנותרו: {count}",
|
||||
"ui.report_characters_left_other": "תווים שנותרו: {count}",
|
||||
"ui.reset": "איפוס",
|
||||
"ui.reset_all": "איפוס הכול",
|
||||
"ui.reset_the_settings_on_this_page": "איפוס ההגדרות בדף זה",
|
||||
@@ -5674,14 +5612,6 @@
|
||||
"values must be numbers or expressions": "הערכים חייבים להיות מספרים או ביטויים",
|
||||
"view.auto_ortho_faces": "פרספקטיבה בזמן סיבוב, אורתוגרפי על פאות",
|
||||
"view.render_inspector": "מפקח רינדור",
|
||||
"view_gizmo.arrows": "חיצי סיבוב",
|
||||
"view_gizmo.home": "לחצן תצוגת בית",
|
||||
"view_gizmo.no_such_body": "אין בעיצוב הזה גוף בשם הזה.",
|
||||
"view_gizmo.note": "בחרו גוף בתצוגה. הוא נשמר עם ההעדפות ופרופילי ההגדרות שלכם ומוצג בכל עיצוב. לחצו על אחת מפאותיו כדי להביט בה ישירות.",
|
||||
"view_gizmo.texture": "מרקם",
|
||||
"view_gizmo.themed": "לפי ערכת הנושא",
|
||||
"view_gizmo.too_detailed": "מפורט מדי לניווט התצוגה (יותר מ-10,000 משולשים)",
|
||||
"view_gizmo.too_thin": "ארוך ודק מדי לניווט התצוגה: הצד הדק ביותר חייב להיות לפחות חמישית מהצד הארוך ביותר",
|
||||
"web.files.insert_drawings_hint": "קובצי CAD, רשתות משולשים, DXF או SVG אל התכן הפתוח",
|
||||
"web.files.insert_stored_drawings_hint": "אל התכן הפתוח (קובצי CAD, רשתות משולשים, DXF, SVG)",
|
||||
"web.open_full": "פתיחה ב־SolidCraft",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Analiza dostupnosti",
|
||||
"cmd.inspect_center_of_mass": "Središte mase",
|
||||
"cmd.inspect_clearance": "Provjera zazora",
|
||||
"cmd.inspect_colored_components": "Omogući boje komponenti",
|
||||
"cmd.inspect_colored_components_disable": "Onemogući boje komponenti",
|
||||
"cmd.inspect_compare": "Usporedi tijela",
|
||||
"cmd.inspect_compare_picture": "Usporedi sliku",
|
||||
"cmd.inspect_continuity": "Analiza kontinuiteta",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Hozzáférhetőség-elemzés",
|
||||
"cmd.inspect_center_of_mass": "Tömegközéppont",
|
||||
"cmd.inspect_clearance": "Hézag",
|
||||
"cmd.inspect_colored_components": "Komponensszínek bekapcsolása",
|
||||
"cmd.inspect_colored_components_disable": "Komponensszínek kikapcsolása",
|
||||
"cmd.inspect_compare": "Testek összehasonlítása",
|
||||
"cmd.inspect_compare_picture": "Kép összehasonlítása",
|
||||
"cmd.inspect_continuity": "Folytonosságelemzés",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Analisis Aksesibilitas",
|
||||
"cmd.inspect_center_of_mass": "Pusat Massa",
|
||||
"cmd.inspect_clearance": "Kelonggaran",
|
||||
"cmd.inspect_colored_components": "Aktifkan Warna Komponen",
|
||||
"cmd.inspect_colored_components_disable": "Nonaktifkan Warna Komponen",
|
||||
"cmd.inspect_compare": "Bandingkan Bodi",
|
||||
"cmd.inspect_compare_picture": "Bandingkan dengan Gambar",
|
||||
"cmd.inspect_continuity": "Analisis Kontinuitas",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Analisi di accessibilità",
|
||||
"cmd.inspect_center_of_mass": "Centro di massa",
|
||||
"cmd.inspect_clearance": "Distanza libera",
|
||||
"cmd.inspect_colored_components": "Attiva colori dei componenti",
|
||||
"cmd.inspect_colored_components_disable": "Disattiva colori dei componenti",
|
||||
"cmd.inspect_compare": "Confronta corpi",
|
||||
"cmd.inspect_compare_picture": "Confronta immagine",
|
||||
"cmd.inspect_continuity": "Analisi di continuità",
|
||||
@@ -1364,14 +1366,11 @@
|
||||
"dialog.computing": "Calcolo in corso… {seconds} s",
|
||||
"dialog.could_not_be_checked": "impossibile verificare",
|
||||
"dialog.custom_axis": "↖ Asse personalizzato",
|
||||
"dialog.cylinder_face_tip": "La faccia o il vertice del modello su cui poggia il cilindro: fai clic qui, poi su di esso nella vista. Se resta vuoto, lo posizionano X, Y e Z.",
|
||||
"dialog.dimension_insert_hint": "Fai doppio clic su un’altra quota di controllo per inserirne il nome",
|
||||
"dialog.dimension_name": "Quota: {name}",
|
||||
"dialog.edit_title": "MODIFICA {title}",
|
||||
"dialog.no_shown_body": "Nessun corpo visibile da modificare: mostrane uno o scegli Nuovo corpo",
|
||||
"dialog.pattern_sketch_not_object": "Seleziona corpi o lavorazioni: uno schizzo non è nessuno dei due. Ripeti le sue curve con Ripetizione o Specchia dello schizzo stesso, oppure estrudilo prima.",
|
||||
"dialog.primitive_point_tip": "Un punto che compila X, Y e Z: fai clic qui, poi su un vertice, uno spigolo o una faccia del modello, su un punto di uno schizzo oppure sull'origine.",
|
||||
"dialog.reference_edge_tip": "Uno spigolo rettilineo della faccia scelta da cui si misura la distanza sotto: fai clic qui, poi sullo spigolo.",
|
||||
"dialog.select": "↖ Seleziona",
|
||||
"dialog.selected_count": "↖ Selezionati: {count}",
|
||||
"dialog.selection_constraint": "Vincolo {id}",
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
"Bottom Right": "右下",
|
||||
"Boundary": "境界",
|
||||
"Bounding box": "境界ボックス",
|
||||
"Bridge": "ブリッジ",
|
||||
"CHECK": "チェック",
|
||||
"COMPONENTS": "コンポーネント",
|
||||
"CONFIGURATIONS": "構成",
|
||||
@@ -964,7 +963,8 @@
|
||||
"cmd.inspect_analysis": "表示解析",
|
||||
"cmd.inspect_center_of_mass": "重心",
|
||||
"cmd.inspect_clearance": "クリアランス",
|
||||
"cmd.inspect_colored_components": "コンポーネントの色分け",
|
||||
"cmd.inspect_colored_components": "コンポーネントの色分けを有効化",
|
||||
"cmd.inspect_colored_components_disable": "コンポーネントの色分けを無効化",
|
||||
"cmd.inspect_compare": "ボディを比較",
|
||||
"cmd.inspect_compare_picture": "参考画像と比較",
|
||||
"cmd.inspect_continuity": "連続性解析",
|
||||
@@ -1296,7 +1296,6 @@
|
||||
"cmd.timeline_stop": "履歴を停止",
|
||||
"cmd.timeline_suppress": "フィーチャーを抑制",
|
||||
"cmd.ui.assistant": "アシスタントの表示切り替え",
|
||||
"cmd.view.navigator": "カスタム ビューナビゲーター",
|
||||
"cmd.view.render_browser": "Renderブラウザー",
|
||||
"cmd.view.render_dock": "レンダードックを移動",
|
||||
"cmd.view.render_focus": "Renderフォーカス表示",
|
||||
@@ -1395,7 +1394,6 @@
|
||||
"dialog.computing": "計算中… {seconds} 秒",
|
||||
"dialog.could_not_be_checked": "チェックできません",
|
||||
"dialog.custom_axis": "↖ カスタム軸",
|
||||
"dialog.cylinder_face_tip": "円柱が立つモデルの面または頂点: ここをクリックしてから、ビューでそれをクリック。空のままなら X、Y、Z で配置します。",
|
||||
"dialog.dimension_insert_hint": "別の駆動寸法をクリックして名前を挿入",
|
||||
"dialog.dimension_name": "寸法: {name}",
|
||||
"dialog.dimension_step_down": "{step} 減らす",
|
||||
@@ -1404,8 +1402,6 @@
|
||||
"dialog.edit_title": "{title}を編集",
|
||||
"dialog.no_shown_body": "変更できる表示中のボディがありません。ボディを表示するか、新規ボディを選択してください",
|
||||
"dialog.pattern_sketch_not_object": "ボディかフィーチャーを選んでください。スケッチはどちらでもありません。曲線はスケッチ内のパターンかミラーで繰り返すか、先に押し出してください。",
|
||||
"dialog.primitive_point_tip": "X、Y、Z を埋める点: ここをクリックしてから、モデルの頂点、エッジ、面、スケッチの点、または原点をクリック。",
|
||||
"dialog.reference_edge_tip": "下の距離を測る基準となる、選んだ面の直線エッジ: ここをクリックしてからエッジをクリック。",
|
||||
"dialog.select": "↖ 選択",
|
||||
"dialog.selected_count": "↖ {count} 個選択中",
|
||||
"dialog.selection_constraint": "拘束 {id}",
|
||||
@@ -1449,9 +1445,6 @@
|
||||
"each rail's curves must be in one sketch": "各レールの曲線は1つのスケッチ内にある必要があります",
|
||||
"feature computation ended without a result": "フィーチャーの計算が結果なしで終了しました",
|
||||
"file.open.drawing_filter": "デザイン、CADファイル、メッシュ、図面",
|
||||
"file.open.portal_fallback.no_portal_service": "開いたファイル選択はデスクトップのものではありません。デスクトップのファイル選択サービスが利用できません。",
|
||||
"file.open.portal_fallback.no_session_bus": "開いたファイル選択はデスクトップのものではありません。D-Bus セッションがありません。",
|
||||
"file.open.portal_fallback.xwayland": "開いたファイル選択はデスクトップのものではありません。XWayland ではポータルが親ウィンドウを拒否します。",
|
||||
"file_load.built": "構築済み: 全{total}フィーチャー中{built}件。",
|
||||
"file_load.converting": "ソリッドを変換中: {done}/{total}",
|
||||
"file_load.inserting": "ジオメトリを挿入中",
|
||||
@@ -2042,8 +2035,6 @@
|
||||
"nav.blender.help": "中ドラッグで回転、Shift+中ドラッグでパン、Ctrl+中ドラッグでズーム。右ドラッグではカメラは動きません。",
|
||||
"nav.creo.help": "マウスの中ボタンドラッグで回転、Shift+中ボタンで平行移動、Ctrl+中ボタンの上下ドラッグでズーム(下方向で拡大)。ホイールの回転でもズーム",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "中ボタンまたは右ボタンのドラッグは、途中でキーを押したり離したりしても、ボタンを離すまで開始時の回転・パン・ズームを続けます。オフにすると、ドラッグ中の各時点で押しているキーによって決まります。",
|
||||
"nav.drag_keeps_mode.label": "ドラッグは開始時のナビゲーションを維持",
|
||||
"nav.free_orbit.help": "マウスと3Dマウスで、極点を越えて回転し、ロールを保ちます。\nShiftによる軸スナップは無効になります。",
|
||||
"nav.free_orbit.label": "自由回転",
|
||||
"nav.free_orbit.snap_help": "マウスと3Dマウスで両極を越えて回転でき、ロールも保持されます。オービットのドラッグ中にShiftを押すと、視線方向とロールがスナップします。",
|
||||
@@ -2280,9 +2271,6 @@
|
||||
"prefs.auto_operation_note": "材料が接触する対象に応じて結合、新規ボディ、切り取りを選びます。",
|
||||
"prefs.axes": "軸",
|
||||
"prefs.axis": "軸",
|
||||
"prefs.background_choose": "画像を選択…",
|
||||
"prefs.background_image": "画像",
|
||||
"prefs.background_unreadable": "背景画像を読み込めませんでした",
|
||||
"prefs.blur_cost": "相対的な描画負荷です。バーが多いほど、通常は処理量が増えます。",
|
||||
"prefs.blur_device": "このグラフィックスデバイスまたは描画経路では利用できません",
|
||||
"prefs.blur_guard": "画面の応答性を保つため、ぼかしの品質を下げました。半径は変更されていません。",
|
||||
@@ -2304,8 +2292,6 @@
|
||||
"prefs.browser_eye_left": "折りたたみ矢印の隣の目のアイコン",
|
||||
"prefs.browser_eye_left_note": "ブラウザーの各行の表示切り替えを、行の右端の列ではなく、以前のように折りたたみ矢印の隣に表示します。",
|
||||
"prefs.browser_reveal_on_hover": "ホバーで表示",
|
||||
"prefs.browser_rows_start_closed": "ブラウザーの行を閉じた状態で始める",
|
||||
"prefs.browser_rows_start_closed_note": "ブラウザーの配置とフォルダーを閉じた状態で表示するため、多数のコンポーネントを含むデザインでは最上位の行だけが表示されます。各行は矢印、「すべて展開」または「ブラウザで検索」で開きます。「ブラウザで検索」はボディまでの行だけを開き、前回の検索で開いた行を再び閉じます。3Dビューでボディを選んでもブラウザーはそのままです。オフのときはフォルダーが開いた状態で始まり、配置は「コンポーネントを最初から展開」の設定に従い、3Dビューで選ぶとブラウザーがそのボディまで開きます(以前と同じ)。",
|
||||
"prefs.browser_show_note": "「表示」はブラウザーを常に画面に表示します。「ホバーで表示」はポインターがその位置か左端の細いタブに来るまで隠し、ポインターが中にある間やそのメニューが開いている間は表示したままにします。",
|
||||
"prefs.browser_shown": "表示",
|
||||
"prefs.built_in": "組み込み",
|
||||
@@ -2437,10 +2423,6 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "キューブの隅にある2つのカーブした矢印は、画面内でビューをロールします。オン: 時計回りの矢印が反時計回りに、反時計回りの矢印が時計回りにロールします。オフ: 各矢印は、そのカーブの向きにビューをロールします。",
|
||||
"prefs.invert_navigator_arrows": "ビューキューブの回転矢印を反転",
|
||||
"prefs.invert_navigator_arrows_note": "キューブの周りの矢印が、矢印の指す方向にカメラを回します(Fusion や Inventor と同じ動き):左の矢印は右を指し、カメラを右に振ります。オフ: 矢印は、そのある面の方へキューブを回すため、左の矢印はカメラを左に振ります。カーブしたロール矢印は影響を受けません。",
|
||||
"prefs.keep_unsaved_also_asks": "タブを閉じるときにも確認する",
|
||||
"prefs.keep_unsaved_also_asks_tip": "オフにすると、未保存の各デザインのコピーが最新であれば、ブラウザーの確認なしでタブが閉じます。",
|
||||
"prefs.keep_unsaved_in_browser": "未保存の作業をこのブラウザーに保持する",
|
||||
"prefs.keep_unsaved_in_browser_tip": "開いている各デザインの未保存の変更を、このブラウザーのプライベートストレージに常に最新の状態で保持します。次回のアクセス時に復元を提案します。",
|
||||
"prefs.keep_work_on_tab_switch": "デザインを切り替えてもダイアログとツールを開いたままにする",
|
||||
"prefs.keep_work_on_tab_switch_note": "各デザインのタブが、開いているダイアログまたはツール(選択や入力した内容を含む)とツールバーのタブを保持します。デザインに戻ると元のまま表示されます。オフ: デザインを切り替えるとそれらはキャンセルされ、モデル タブが表示されます。",
|
||||
"prefs.labels": "ラベル",
|
||||
@@ -2572,8 +2554,6 @@
|
||||
"prefs.section_hatch_by_part": "部品ごと",
|
||||
"prefs.section_hatch_one": "単一スタイル",
|
||||
"prefs.select": "選択",
|
||||
"prefs.select_hidden_edges": "隠線表示で隠れた辺を選択する",
|
||||
"prefs.select_hidden_edges_help": "「Shaded with hidden edges」で面の向こうの辺を選択し、Filletにも使用できます。オフではv0.30.1相当の選択動作になります。非表示のボディは選択対象になりません。",
|
||||
"prefs.select_one_item": "項目を 1 つ選択",
|
||||
"prefs.select_other_hidden": "「他を選択」で非表示のジオメトリを選択",
|
||||
"prefs.select_other_hidden_help": "オン: ポインター付近の非表示のボディやスケッチ、遮られたエッジを含めます。オフ: ポインター直下の表示中のジオメトリだけを元のリストに表示します。",
|
||||
@@ -2598,8 +2578,6 @@
|
||||
"prefs.sketch.click_selects": "クリックで選択",
|
||||
"prefs.sketch.conflict_colour": "競合色",
|
||||
"prefs.sketch.drawing": "作図",
|
||||
"prefs.sketch.ellipse_axes": "楕円を軸と一緒に描く",
|
||||
"prefs.sketch.ellipse_axes_note": "長軸と短軸をコンストラクション線として描き、半軸と軸に寸法を付けられるようにします。オフ: 楕円のみを描き、D で短半径を寸法にします。値をどこへ引いても、これまでどおり R 付きで描かれます。",
|
||||
"prefs.sketch.exit_keep": "維持",
|
||||
"prefs.sketch.exit_previous": "開始前のビューに戻る",
|
||||
"prefs.sketch.exit_unless_rotated": "回転した場合を除く",
|
||||
@@ -2769,12 +2747,6 @@
|
||||
"recovery.just_now": "たった今",
|
||||
"recovery.min_ago": "{count}分前",
|
||||
"recovery.never_saved": "未保存",
|
||||
"recovery.open_saved": "保存済みのバージョンを開く",
|
||||
"recovery.restore": "復元",
|
||||
"recovery.restore_intro": "このページを前回閉じたときに保存されていなかった変更は、このブラウザーに保持されています。",
|
||||
"recovery.restore_row": "{name}、{when}に変更",
|
||||
"recovery.restore_title": "未保存のデザインを復元しますか?",
|
||||
"recovery.restore_title_one": "未保存のデザイン {name}({when}に変更)を復元しますか?",
|
||||
"refused.change": "{body} を変更できませんでした。",
|
||||
"refused.cut": "{body} をカットできませんでした。",
|
||||
"refused.face_curved": "曲面の 1 つがモデリングに使えません。",
|
||||
@@ -2789,9 +2761,9 @@
|
||||
"render.adaptive_absolute": "RGB 絶対誤差の下限",
|
||||
"render.adaptive_coverage": "被覆率の誤差目標",
|
||||
"render.adaptive_enable": "独立した予備サンプルで配分",
|
||||
"render.adaptive_help": "独立したパイロットサンプルは破棄され、ピクセルごとに16以上、「ピクセルあたりのサンプル数」以下の本番サンプル数を決めます。CPUのリソース上限には両段階が含まれます。RGBとカバレッジの分散から配分し、上限に達したピクセルにはノイズが残ることがあります。最終レポートには実際のサンプル数を記録します。",
|
||||
"render.adaptive_help": "GPU 専用。独立した予備サンプルは破棄され、各ピクセルに最小16から設定上限までの本サンプルを配分します。RGB と被覆率の分散を目安にしますが、まれな経路を見逃したり上限でノイズが残ったりします。最終レポートは実際の処理量を示します。ノイズや時間を保証しません。",
|
||||
"render.adaptive_minimum": "最小本サンプル数",
|
||||
"render.adaptive_pending": "独立したパイロットによる適応配分はCPUとGPUで利用できます。条件付きフレネル層にはGPUが必要です。統計的な配分は収束を保証しません。",
|
||||
"render.adaptive_pending": "GPU の独立予備サンプルによる適応配分と条件付きフレネル層化が利用できます。収束の認証、まれな事象の保証、CPU 適応輸送は未対応です。",
|
||||
"render.adaptive_pilots": "ピクセルごとの予備サンプル",
|
||||
"render.adaptive_relative": "RGB 相対誤差の目標",
|
||||
"render.add_ground": "フィットした床をセットアップに追加",
|
||||
@@ -2852,7 +2824,7 @@
|
||||
"render.appearance_editor.replace": "このボディの保存済み光学設定を置換",
|
||||
"render.appearance_editor.revalidate": "このボディを再検証",
|
||||
"render.appearance_editor.save": "この光学設定を保存",
|
||||
"render.appearance_editor.save_reason": "ガラスを保存するには、現在のCPUまたはGPUボディで有効にしてください。クリアコートは「コートをショットに保存」を使用します。ローカル画像ファイルは一時設定のままです。",
|
||||
"render.appearance_editor.save_reason": "保存には現在のGPUボディの物理ガラスが必要です。コートとローカル画像マップは一時設定のままです。",
|
||||
"render.appearance_editor.saved": "このショットに保存",
|
||||
"render.appearance_editor.saved_state": "この光学設定はこのショットに保存されています。",
|
||||
"render.appearance_editor.scatter_reason": "内部散乱には閉じたソリッド境界が必要です。",
|
||||
@@ -2948,7 +2920,7 @@
|
||||
"render.children.copy_request": "リクエストJSONをコピー",
|
||||
"render.children.coverage_error": "カバレッジ誤差",
|
||||
"render.children.cpu": "CPUレイトレーシング",
|
||||
"render.children.denoise": "画像のノイズを低減",
|
||||
"render.children.denoise": "完成画像のノイズを低減",
|
||||
"render.children.depth": "パス深度",
|
||||
"render.children.distortion": "歪み",
|
||||
"render.children.edge_samples": "エッジサンプル",
|
||||
@@ -3080,7 +3052,7 @@
|
||||
"render.cpu_point_help": "選択した平行光に最大8個の点光源を追加できます。距離はmmで、最小距離は基準距離以下にしてください。",
|
||||
"render.cpu_primary_samples": "輪郭サンプル数/ピクセル",
|
||||
"render.cpu_progressive": "CPUプログレッシブ",
|
||||
"render.cpu_progressive_help": "新しいCPUカメラサンプルを蓄積し、完了したすべてのパスを表示します。選択した反射はカメラサンプルごとに1回評価します。認証済みガラスと平面異方性には従来の制限が適用されます。適応サンプリングのパイロットと本番は同じCPUプロファイル予算を使います。",
|
||||
"render.cpu_progressive_help": "既存のCPU積分器を、一次サンプル1個ずつのパスで累積します。選択した反射は1サンプルが必要です。面光源、誘電体、異方性には固定画像を使用してください。",
|
||||
"render.cpu_progressive_limits": "最大1,048,576ピクセル、ピクセル×サンプルの合計は16,777,216以下です。サンプル数を変えても画像サイズは自動変更されません。",
|
||||
"render.cpu_progressive_seed": "CPUサンプリングシード",
|
||||
"render.cpu_progressive_spp": "サンプル/ピクセル",
|
||||
@@ -3256,7 +3228,7 @@
|
||||
"render.export_display": "書き出し:表示PNG",
|
||||
"render.export_image_label": "画像を書き出す",
|
||||
"render.export_linear": "書き出し:生の線形EXR",
|
||||
"render.exr_pending": "CPUレイトレーシングとGPUパストレーシングでは生の線形EXRを保持できます。圧縮、複数レイヤー、補助パスは未実装です。",
|
||||
"render.exr_pending": "GPUでは線形EXRを保持できます。圧縮、複数レイヤー、補助パスは未実装です。",
|
||||
"render.film_grain": "写真粒子",
|
||||
"render.fit": "製品を画面に合わせる",
|
||||
"render.fixed_camera": "カメラ",
|
||||
@@ -3296,11 +3268,9 @@
|
||||
"render.frame_wide": "16:9 · ワイドスクリーン",
|
||||
"render.gpu_depth": "最大パス深度",
|
||||
"render.gpu_options_help": "サンプル数、深度、シード、露出をこの設定に保存します。保存するのは選択中の画像方式だけです。",
|
||||
"render.gpu_path_crashed": "前回このグラフィックアダプターでの準備中に SolidCraft が終了しました。グラフィックドライバー内で終了した可能性が高いため、自動では再度準備しません。代わりに CPU レイトレーシングを使うか、もう一度試してください(ドライバーを更新した場合も再試行されます)。",
|
||||
"render.gpu_path_failed": "GPUパストレーサーを利用できません",
|
||||
"render.gpu_path_first_setup": "このGPUでの初回セットアップには数分かかることがあります。その間もSolidCraftは使用できます。以降の起動では、ドライバーが保持していればコンパイル済みのシェーダーを再利用します。",
|
||||
"render.gpu_path_preparing": "GPUパストレーサー:シェーダーをコンパイル中…",
|
||||
"render.gpu_path_retry": "もう一度試す",
|
||||
"render.gpu_seed": "乱数シード",
|
||||
"render.gpu_unavailable": "このデバイスでは GPU パストレーシングの準備ができていません。",
|
||||
"render.ground_apply": "床をフィットして保存",
|
||||
@@ -3619,7 +3589,7 @@
|
||||
"render.method_cpu": "CPU レイトレーシング",
|
||||
"render.method_gpu": "GPU パストレーシング",
|
||||
"render.method_limit_raster": "ラスターは固定の不透明スタジオプレビューを使用します。対応する光学設定には明示的なレイトレーシング方式を選択してください。",
|
||||
"render.method_raster": "CPU ラスター(模式図)",
|
||||
"render.method_raster": "CPU ラスター",
|
||||
"render.navigation_help": "カメラはデザインと同じように動きます(環境設定 › ナビゲーション)。「セットアップを保存」で現在のカメラを記録します。",
|
||||
"render.nested_enable": "検証済み入れ子誘電体",
|
||||
"render.nested_help": "最大16層。親は凸形状、子は厳密に内部である必要があります。交差・接触・薄膜・直線透過影は拒否します。内部カメラは検証した階層を使います。液面の生成や接触するCAD境界の結合は行いません。",
|
||||
@@ -3647,7 +3617,7 @@
|
||||
"render.output_exr_unavailable": "ここではEXR出力設定を利用できません。リニアEXR書き出しには、リニアマスターを保持した完成GPU画像が必要です。",
|
||||
"render.output_fit": "全体表示",
|
||||
"render.output_format": "形式",
|
||||
"render.output_format_status": "PNGは完了した表示画像そのものです。CPU/GPUの任意EXRは表示フィルター前の生の線形FLOAT32放射輝度を保持します。プレビューキャッシュや別ジョブへの置換はありません。",
|
||||
"render.output_format_status": "PNGは完了した表示画像そのものです。GPUの任意EXRは表示フィルター前の生の線形FLOAT32放射輝度を保持します。プレビューキャッシュや別ジョブへの置換はありません。",
|
||||
"render.output_grading": "カラーグレーディング",
|
||||
"render.output_image": "画像",
|
||||
"render.output_largest": "{width} × {height} 最大",
|
||||
@@ -3732,7 +3702,7 @@
|
||||
"render.quality_custom": "カスタム",
|
||||
"render.quality_draft": "ドラフト",
|
||||
"render.quality_high": "高品質",
|
||||
"render.quality_projection_help": "この確認で変更するのは、解像度、サンプル予算、パス深度、シード、ノイズ低減だけです。他の設定は維持され、収束は保証されません。",
|
||||
"render.quality_projection_help": "Qプリセットには将来の要件も含まれます。この操作は対応する固定サンプルPNG予算のみを適用し、適応的停止、生の線形出力、独立した透過制限は含みません。",
|
||||
"render.quality_review": "サンプリング予算を確認",
|
||||
"render.queue_pending": "このUIには自動の本番キューやアニメーションレンダラーはありません。完了した静止画像は個別に書き出せます。",
|
||||
"render.radial_assign": "外観を割り当て",
|
||||
@@ -3763,7 +3733,7 @@
|
||||
"render.result_navigation": "移動とズームはナビゲーション設定に従います。スペースキーを押しながらドラッグすると移動します。",
|
||||
"render.result_open": "完成した画像をメインキャンバスに表示します。",
|
||||
"render.result_preview": "ライブプレビュー",
|
||||
"render.result_provisional": "仮画像",
|
||||
"render.result_provisional": "CPU仮画像",
|
||||
"render.result_provisional_help": "この中間画像はエクスポートできません。最後に完成した結果をエクスポートします。キャンセルするとその結果に戻ります。",
|
||||
"render.result_reduced_display": "表示プレビューをビューポート機器またはキャッシュの上限に縮小しています。保持したPNGと書き出しは元の解像度です。この縮小表示ではネイティブ100%表示はできません。",
|
||||
"render.result_retained": "最後に完成した画像を表示しています。新しいレンダリングにはまだ置き換わっていません。",
|
||||
@@ -3927,7 +3897,7 @@
|
||||
"render.stage_panels.prepare_plan": "作成計画を準備",
|
||||
"render.stage_panels.projects": "投影",
|
||||
"render.stage_panels.quality_ack": "固定予算と省略事項を確認しました",
|
||||
"render.stage_panels.quality_omissions": "この予算は、適応サンプリングの誤差目標、独立した透過制限、ロシアンルーレット規則、リニアマスターの保持を設定しません。収束も保証しません。",
|
||||
"render.stage_panels.quality_omissions": "適応停止と誤差目標、個別の透過深度、ロシアンルーレット規則、線形RAW出力、EXRマスター、収束認証。",
|
||||
"render.stage_panels.reference": "参照",
|
||||
"render.stage_panels.references": "参照",
|
||||
"render.stage_panels.remove_optics": "光学設定を削除",
|
||||
@@ -3971,7 +3941,7 @@
|
||||
"render.stage_panels.subtitle_environment": "オリジナルHDR環境・シーン全体",
|
||||
"render.stage_panels.subtitle_maps": "作成済み空間マップ・一時プレビュー",
|
||||
"render.stage_panels.subtitle_physical": "読み取り専用計画・マテリアルの作成や割り当てなし",
|
||||
"render.stage_panels.subtitle_quality": "GPUサンプリング予算 · 記載された設定のみ",
|
||||
"render.stage_panels.subtitle_quality": "固定GPU予算・将来の要件は省略",
|
||||
"render.stage_panels.subtitle_rig": "作成済みライトリグ・光源と受動カード",
|
||||
"render.stage_panels.subtitle_saved_maps": "作成済みマップ・このショットに保存",
|
||||
"render.stage_panels.subtitle_stage": "物理的な地面とステージ・製品にフィット",
|
||||
@@ -3996,7 +3966,7 @@
|
||||
"render.studio_area_help": "最大 4 個の片面矩形です。直交する半辺 U と V でサイズを定め、U × V が発光方向になります。座標はワールド空間の mm です。",
|
||||
"render.studio_center": "中心 X, Y, Z (mm)",
|
||||
"render.studio_denoise": "完成画像のノイズを低減",
|
||||
"render.studio_denoise_help": "プログレッシブ画像と完成画像に輪郭を保持するフィルターを適用します。微細な描写が柔らかくなることがあります。ガラスと鋭い鏡面反射は対象外です。",
|
||||
"render.studio_denoise_help": "輪郭を保持する最終フィルターです。微細な描写が柔らかくなることがあります。ガラスと鋭い鏡面反射は対象外です。",
|
||||
"render.studio_direction": "太陽に向かう方向 (X, Y, Z)",
|
||||
"render.studio_environment": "環境",
|
||||
"render.studio_environment_bounds": "環境回転は±1,000,000ラジアン以内の有限値、強度は0~100でなければなりません。",
|
||||
@@ -4198,32 +4168,6 @@
|
||||
"sketch.dof_left": "残り自由度 {count}",
|
||||
"sketch.dof_left_one": "残り自由度 {count}",
|
||||
"sketch.draw_curves": "{linetype}として描く",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "選択を{linetype}にする",
|
||||
"sketch.open_ends": "開放 {count}",
|
||||
"sketch.status_conflict": "拘束が競合しています",
|
||||
@@ -4445,7 +4389,6 @@
|
||||
"ui.3d_sketch": "3D スケッチ",
|
||||
"ui.activate": "アクティブ化",
|
||||
"ui.active_component": "アクティブなコンポーネント",
|
||||
"ui.addToQuickAccess": "クイックアクセスに追加",
|
||||
"ui.add_configuration": "構成を追加",
|
||||
"ui.add_parameter": "パラメーターを追加",
|
||||
"ui.agent_chat": "エージェントチャット",
|
||||
@@ -5074,7 +5017,6 @@
|
||||
"ui.heading_construct": "構築",
|
||||
"ui.heading_create": "作成",
|
||||
"ui.heading_create_sketch": "スケッチを作成",
|
||||
"ui.heading_custom_view_navigator": "カスタム ビューナビゲーター",
|
||||
"ui.heading_cylinder": "円柱",
|
||||
"ui.heading_design": "デザイン",
|
||||
"ui.heading_draft": "勾配",
|
||||
@@ -5361,7 +5303,6 @@
|
||||
"ui.quality": "品質",
|
||||
"ui.quantity": "数量",
|
||||
"ui.quantity_2": "数量 2",
|
||||
"ui.quick_access_full": "クイックアクセスバーに置けるコマンドは {count} 個までです。追加するには 1 つ削除してください",
|
||||
"ui.quit": "終了",
|
||||
"ui.radial_context_menu": "パイ メニュー",
|
||||
"ui.radius": "半径",
|
||||
@@ -5379,7 +5320,6 @@
|
||||
"ui.relink_not_linked": "このコンポーネントは参照元の設計にリンクされていません。",
|
||||
"ui.relink_select_one": "リンクされたコンポーネントまたはそのインスタンスを1つ選択してください。",
|
||||
"ui.relink_source_all": "参照元を再リンク(すべてのインスタンス)…",
|
||||
"ui.removeFromQuickAccess": "クイックアクセスから削除",
|
||||
"ui.removeFromToolbar": "ツールバーから削除",
|
||||
"ui.removeSection": "セクションを解除",
|
||||
"ui.render": "レンダリング",
|
||||
@@ -5388,8 +5328,6 @@
|
||||
"ui.repeat_nothing": "まだ繰り返すものがありません。先にツールかダイアログを開始してください。",
|
||||
"ui.replace_with_link_done": "コピーはリンクされたデザインの配置になりました。",
|
||||
"ui.report_a_bug": "不具合を報告",
|
||||
"ui.report_characters_left_one": "残り {count} 文字",
|
||||
"ui.report_characters_left_other": "残り {count} 文字",
|
||||
"ui.reset": "リセット",
|
||||
"ui.reset_all": "すべてリセット",
|
||||
"ui.reset_the_settings_on_this_page": "このページの設定をリセット",
|
||||
@@ -5674,14 +5612,6 @@
|
||||
"values must be numbers or expressions": "値は数値または式にしてください",
|
||||
"view.auto_ortho_faces": "回転中は透視投影、面を正面に見ると平行投影",
|
||||
"view.render_inspector": "レンダーインスペクター",
|
||||
"view_gizmo.arrows": "回転矢印",
|
||||
"view_gizmo.home": "ホーム ボタン",
|
||||
"view_gizmo.no_such_body": "このデザインにその名前のボディはありません。",
|
||||
"view_gizmo.note": "ビューでボディを選択してください。ボディは環境設定と設定プロファイルに保存され、すべてのデザインで表示されます。面をクリックすると、その面を正面から見ます。",
|
||||
"view_gizmo.texture": "テクスチャ",
|
||||
"view_gizmo.themed": "テーマ",
|
||||
"view_gizmo.too_detailed": "ビューナビゲーターには細かすぎます (10,000 三角形を超えています)",
|
||||
"view_gizmo.too_thin": "ビューナビゲーターには細長すぎます: 最も薄い辺が最も長い辺の 5 分の 1 以上である必要があります",
|
||||
"web.files.insert_drawings_hint": "CADファイル、メッシュ、DXF、SVGを開いているデザインに挿入",
|
||||
"web.files.insert_stored_drawings_hint": "開いているデザインに挿入(CADファイル、メッシュ、DXF、SVG)",
|
||||
"web.open_full": "SolidCraft で開く",
|
||||
|
||||
@@ -963,7 +963,8 @@
|
||||
"cmd.inspect_analysis": "ანალიზის ნახვა",
|
||||
"cmd.inspect_center_of_mass": "მასის ცენტრი",
|
||||
"cmd.inspect_clearance": "ღრეჩო",
|
||||
"cmd.inspect_colored_components": "კომპონენტების ფერები",
|
||||
"cmd.inspect_colored_components": "კომპონენტების ფერების ჩართვა",
|
||||
"cmd.inspect_colored_components_disable": "კომპონენტების ფერების გამორთვა",
|
||||
"cmd.inspect_compare": "სხეულების შედარება",
|
||||
"cmd.inspect_compare_picture": "სურათთან შედარება",
|
||||
"cmd.inspect_continuity": "უწყვეტობის ანალიზი",
|
||||
|
||||
@@ -1133,7 +1133,6 @@
|
||||
"cmd.timeline_stop": "기록 재생 중지",
|
||||
"cmd.timeline_suppress": "특성 비활성화",
|
||||
"cmd.ui.assistant": "도우미 표시 전환",
|
||||
"cmd.view.navigator": "사용자 지정 뷰 탐색기",
|
||||
"cmd.view.render_browser": "렌더 브라우저",
|
||||
"cmd.view.render_dock": "렌더 도크 이동",
|
||||
"cmd.view.render_focus": "렌더 집중 보기",
|
||||
@@ -1206,7 +1205,6 @@
|
||||
"dialog.computing": "계산 중… {seconds}초",
|
||||
"dialog.could_not_be_checked": "확인할 수 없음",
|
||||
"dialog.custom_axis": "↖ 사용자 정의 축",
|
||||
"dialog.cylinder_face_tip": "원기둥이 놓일 모델의 면 또는 꼭짓점: 여기를 클릭한 다음 뷰에서 그것을 클릭. 비워 두면 X, Y, Z로 배치합니다.",
|
||||
"dialog.dimension_insert_hint": "다른 구동 치수를 두 번 클릭하여 이름 삽입",
|
||||
"dialog.dimension_name": "치수: {name}",
|
||||
"dialog.dimension_step_down": "{step} 줄이기",
|
||||
@@ -1214,8 +1212,6 @@
|
||||
"dialog.dimension_step_up": "{step} 늘리기",
|
||||
"dialog.edit_title": "{title} 편집",
|
||||
"dialog.pattern_sketch_not_object": "바디나 피처를 선택하세요. 스케치는 둘 다 아닙니다. 곡선은 스케치 안의 패턴이나 대칭 복사로 반복하거나, 먼저 돌출하세요.",
|
||||
"dialog.primitive_point_tip": "X, Y, Z를 채우는 점: 여기를 클릭한 다음 모델의 꼭짓점, 모서리, 면, 스케치의 점 또는 원점을 클릭.",
|
||||
"dialog.reference_edge_tip": "아래 거리를 재는 기준이 되는, 선택한 면의 직선 모서리: 여기를 클릭한 다음 모서리를 클릭.",
|
||||
"dialog.select": "↖ 선택",
|
||||
"dialog.selected_count": "↖ {count}개 선택됨",
|
||||
"dialog.selection_constraint": "구속 조건 {id}",
|
||||
@@ -1244,9 +1240,6 @@
|
||||
"dxf.tolerance_help": "표시된 바디와 스케치만 내보내며 곡선은 직선 구간으로 근사합니다. 허용 오차가 작을수록 곡선이 매끄러워지지만 처리 시간과 메모리 사용량이 늘어납니다.",
|
||||
"each rail's curves must be in one sketch": "각 레일의 곡선은 하나의 스케치 안에 있어야 합니다",
|
||||
"feature computation ended without a result": "특성 계산이 결과 없이 끝났습니다",
|
||||
"file.open.portal_fallback.no_portal_service": "열린 파일 선택은 데스크톱의 것이 아닙니다: 데스크톱의 파일 선택 서비스를 사용할 수 없습니다.",
|
||||
"file.open.portal_fallback.no_session_bus": "열린 파일 선택은 데스크톱의 것이 아닙니다: D-Bus 세션 버스가 없습니다.",
|
||||
"file.open.portal_fallback.xwayland": "열린 파일 선택은 데스크톱의 것이 아닙니다: XWayland에서는 포털이 부모 창을 거부합니다.",
|
||||
"file_load.built": "지금까지 빌드됨: 피처 {total}개 중 {built}개.",
|
||||
"file_load.converting": "솔리드 변환 중 {done}/{total}",
|
||||
"file_load.inserting": "기하 삽입 중",
|
||||
@@ -1468,8 +1461,6 @@
|
||||
"nav.blender.help": "가운데 드래그는 회전, Shift+가운데 드래그는 이동, Ctrl+가운데 드래그는 확대/축소입니다. 오른쪽 드래그는 카메라에 영향을 주지 않습니다",
|
||||
"nav.creo.help": "가운데 버튼을 끌면 회전, Shift+가운데 버튼은 이동, Ctrl+가운데 버튼을 위아래로 끌면 확대/축소합니다(아래로 끌면 확대). 휠을 굴려도 확대/축소합니다",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "가운데 또는 오른쪽 버튼 드래그는 도중에 키를 누르거나 떼어도 버튼을 놓을 때까지 시작할 때의 회전, 이동 또는 확대/축소를 계속합니다. 끄면 드래그하는 동안 누르고 있는 키가 매 순간 결정합니다.",
|
||||
"nav.drag_keeps_mode.label": "드래그는 시작할 때의 탐색 방식을 유지",
|
||||
"nav.free_orbit.help": "마우스와 3D 마우스로 롤을 유지하며 양 극점을 넘어 회전합니다. Shift 축 스냅은 비활성화됩니다.",
|
||||
"nav.free_orbit.label": "자유 회전",
|
||||
"nav.free_orbit.snap_help": "마우스와 3D 마우스로 양쪽 극점을 지나 회전할 수 있으며 롤 각도가 유지됩니다. 궤도 회전 드래그 중 Shift를 누르면 보기 방향과 롤 각도가 스냅됩니다.",
|
||||
@@ -1690,8 +1681,6 @@
|
||||
"prefs.browser_components_open_note": "브라우저의 모든 구성요소가 이전처럼 펼쳐진 상태로 시작합니다. 끄면 직접 펼칠 때까지 접혀 있습니다.",
|
||||
"prefs.browser_eye_left": "접기 화살표 옆의 눈 아이콘",
|
||||
"prefs.browser_eye_left_note": "브라우저 행의 표시 아이콘을 행 오른쪽 끝의 열이 아니라 이전처럼 접기 화살표 옆에 표시합니다.",
|
||||
"prefs.browser_rows_start_closed": "브라우저 행을 접힌 상태로 시작",
|
||||
"prefs.browser_rows_start_closed_note": "브라우저의 배치와 폴더가 접힌 상태로 시작하므로, 컴포넌트가 많은 디자인은 최상위 행만 보여 줍니다. 각 행은 화살표, '모두 펼치기' 또는 '브라우저에서 찾기'로 엽니다. '브라우저에서 찾기'는 바디까지의 행만 열고, 지난번 찾기로 연 행은 다시 접습니다. 3D 뷰에서 바디를 골라도 브라우저는 그대로입니다. 끄면 폴더가 펼친 상태로 시작하고, 배치는 '구성요소를 펼친 상태로 시작' 설정을 따르며, 3D 뷰에서 고르면 브라우저가 그 바디까지 열립니다(이전과 같음).",
|
||||
"prefs.built_in": "기본 제공",
|
||||
"prefs.button_left": "왼쪽 드래그",
|
||||
"prefs.button_middle": "가운데 드래그",
|
||||
@@ -1900,8 +1889,6 @@
|
||||
"prefs.search": "설정 검색",
|
||||
"prefs.search_empty": "일치하는 설정이 없습니다",
|
||||
"prefs.select": "선택",
|
||||
"prefs.select_hidden_edges": "숨은 모서리가 표시된 음영 뷰에서 숨은 모서리 선택",
|
||||
"prefs.select_hidden_edges_help": "숨은 모서리가 표시된 음영 뷰에서 면 뒤의 모서리를 선택하여 필렛에 사용할 수 있습니다. 끄면 v0.30.1의 선택 동작을 유지합니다. 숨긴 바디는 선택되지 않습니다.",
|
||||
"prefs.select_one_item": "항목 하나 선택",
|
||||
"prefs.select_other_hidden": "다른 항목 선택으로 숨겨진 형상 선택",
|
||||
"prefs.select_other_hidden_help": "켜짐: 포인터 근처의 숨겨진 바디, 스케치 및 가려진 모서리를 포함합니다. 꺼짐: 포인터 바로 아래에 보이는 형상만 포함하는 기존 목록을 사용합니다.",
|
||||
@@ -1926,8 +1913,6 @@
|
||||
"prefs.sketch.click_selects": "클릭으로 선택",
|
||||
"prefs.sketch.conflict_colour": "충돌 색상",
|
||||
"prefs.sketch.drawing": "그리기",
|
||||
"prefs.sketch.ellipse_axes": "타원을 축과 함께 그리기",
|
||||
"prefs.sketch.ellipse_axes_note": "장축과 단축을 구성선으로 그려 반축과 축에 치수를 지정할 수 있습니다. 끄면: 타원만 그리며, D는 값을 어디로 끌든 단반경을 주며, 이전처럼 R로 표시되어 그려집니다.",
|
||||
"prefs.sketch.exit_keep": "유지",
|
||||
"prefs.sketch.exit_previous": "이전 뷰로 돌아가기",
|
||||
"prefs.sketch.exit_unless_rotated": "회전하지 않았다면",
|
||||
@@ -2120,7 +2105,7 @@
|
||||
"render.appearance_editor.replace": "이 바디의 저장된 광학 설정 교체",
|
||||
"render.appearance_editor.revalidate": "이 바디 다시 검증",
|
||||
"render.appearance_editor.save": "이 광학 설정 저장",
|
||||
"render.appearance_editor.save_reason": "유리를 저장하려면 현재 CPU 또는 GPU 바디에서 활성화하세요. 클리어 코트는 샷에 코트 저장을 사용하세요. 로컬 이미지 파일은 임시 설정으로 유지됩니다.",
|
||||
"render.appearance_editor.save_reason": "저장하려면 현재 GPU 바디에 물리 유리가 필요합니다. 코트와 로컬 이미지 맵은 임시로 유지됩니다.",
|
||||
"render.appearance_editor.saved": "이 샷에 저장됨",
|
||||
"render.appearance_editor.saved_state": "이 광학 설정은 이 샷과 함께 저장되어 있습니다.",
|
||||
"render.appearance_editor.scatter_reason": "내부 산란에는 닫힌 솔리드 경계가 필요합니다.",
|
||||
@@ -2168,7 +2153,7 @@
|
||||
"render.children.copy_request": "요청 JSON 복사",
|
||||
"render.children.coverage_error": "커버리지 오차",
|
||||
"render.children.cpu": "CPU 레이 트레이싱",
|
||||
"render.children.denoise": "이미지 노이즈 줄이기",
|
||||
"render.children.denoise": "완성 이미지의 노이즈 줄이기",
|
||||
"render.children.depth": "경로 깊이",
|
||||
"render.children.distortion": "왜곡",
|
||||
"render.children.edge_samples": "모서리 샘플",
|
||||
@@ -2389,7 +2374,7 @@
|
||||
"render.error_visible_solid": "여기서 미리 보려면 디자인에서 보이는 솔리드를 만드세요.",
|
||||
"render.exit_focus": "집중 보기 종료",
|
||||
"render.export_image_label": "이미지 내보내기",
|
||||
"render.exr_pending": "CPU 레이 트레이싱과 GPU 패스 트레이싱은 원본 선형 EXR을 보존할 수 있습니다. 압축, 다중 레이어 이미지 및 보조 렌더 패스는 아직 사용할 수 없습니다.",
|
||||
"render.exr_pending": "GPU는 원본 선형 EXR을 보존할 수 있습니다. 압축, 다중 레이어 이미지 및 보조 렌더 패스는 아직 사용할 수 없습니다.",
|
||||
"render.fit": "제품에 맞추기",
|
||||
"render.fixed_camera": "카메라",
|
||||
"render.fixed_world": "월드",
|
||||
@@ -2639,7 +2624,7 @@
|
||||
"render.output_exr_unavailable": "여기서는 EXR 출력을 설정할 수 없습니다. 선형 EXR 내보내기에는 선형 원본을 보관한 완료된 GPU 이미지가 필요합니다.",
|
||||
"render.output_fit": "맞춤",
|
||||
"render.output_format": "형식",
|
||||
"render.output_format_status": "PNG는 완료된 표시 이미지 그대로입니다. 선택적 CPU/GPU EXR은 표시 필터 전 선형 FLOAT32 방사 휘도를 보존합니다. 미리보기 캐시나 다른 작업으로 바꾸어 내보내지 않습니다.",
|
||||
"render.output_format_status": "PNG는 완료된 표시 이미지 그대로입니다. 선택적 GPU EXR은 표시 필터 전 선형 FLOAT32 방사 휘도를 보존합니다. 미리보기 캐시나 다른 작업으로 바꾸어 내보내지 않습니다.",
|
||||
"render.output_grading": "색 보정",
|
||||
"render.output_image": "이미지",
|
||||
"render.output_largest": "{width} × {height} 최대",
|
||||
@@ -2689,7 +2674,6 @@
|
||||
"render.quality_custom": "사용자 지정",
|
||||
"render.quality_draft": "초안",
|
||||
"render.quality_high": "높음",
|
||||
"render.quality_projection_help": "이 검토에서는 해상도, 샘플 예산, 경로 깊이, 시드 및 노이즈 제거만 변경합니다. 다른 설정은 유지되며 수렴은 보장되지 않습니다.",
|
||||
"render.radial_assign": "외관 지정",
|
||||
"render.radial_compare": "비교",
|
||||
"render.radial_fit": "제품 맞추기",
|
||||
@@ -2832,7 +2816,7 @@
|
||||
"render.stage_panels.prepare_plan": "생성 계획 준비",
|
||||
"render.stage_panels.projects": "투영",
|
||||
"render.stage_panels.quality_ack": "고정 예산과 생략 사항을 확인했습니다",
|
||||
"render.stage_panels.quality_omissions": "이 예산은 적응형 오차 목표, 독립적인 투과 한도, 러시안 룰렛 규칙 또는 선형 마스터 보존을 설정하지 않으며 수렴을 보장하지 않습니다.",
|
||||
"render.stage_panels.quality_omissions": "적응형 중지 및 오차 목표, 별도 투과 깊이, 러시안 룰렛 규칙, 원시 선형 출력, EXR 마스터 및 수렴 인증.",
|
||||
"render.stage_panels.reference": "참조",
|
||||
"render.stage_panels.references": "참조",
|
||||
"render.stage_panels.remove_optics": "광학 설정 제거",
|
||||
@@ -2876,7 +2860,7 @@
|
||||
"render.stage_panels.subtitle_environment": "원본 HDR 환경 · 전체 장면",
|
||||
"render.stage_panels.subtitle_maps": "제작된 공간 맵 · 임시 미리 보기",
|
||||
"render.stage_panels.subtitle_physical": "읽기 전용 계획 · 재질 생성 또는 할당 없음",
|
||||
"render.stage_panels.subtitle_quality": "GPU 샘플링 예산 · 표시된 설정만 변경",
|
||||
"render.stage_panels.subtitle_quality": "고정 GPU 예산 · 향후 요구 사항 생략",
|
||||
"render.stage_panels.subtitle_rig": "제작된 조명 리그 · 광원 및 수동 카드",
|
||||
"render.stage_panels.subtitle_saved_maps": "제작된 맵 · 이 샷에 저장",
|
||||
"render.stage_panels.subtitle_stage": "물리적 바닥 및 스테이지 · 제품에 맞춤",
|
||||
@@ -2893,7 +2877,6 @@
|
||||
"render.state_in_progress": "이미지 생성 중",
|
||||
"render.state_model": "모델 미리보기",
|
||||
"render.strip_label": "스트립",
|
||||
"render.studio_denoise_help": "프로그레시브 이미지와 완성 이미지에 경계를 보존하는 필터를 적용합니다. 미세한 디테일이 부드러워질 수 있습니다. 유리와 날카로운 정반사 표면은 필터를 거치지 않습니다.",
|
||||
"render.studio_tiles_require_gpu": "스튜디오에는 GPU 경로 추적이 필요합니다. 출력 편집기에서 GPU를 선택하세요.",
|
||||
"render.studios_environments": "스튜디오 및 환경",
|
||||
"render.tone_aces_fitted": "ACES 근사",
|
||||
@@ -3801,7 +3784,6 @@
|
||||
"ui.heading_construct": "구성",
|
||||
"ui.heading_create": "만들기",
|
||||
"ui.heading_create_sketch": "스케치 만들기",
|
||||
"ui.heading_custom_view_navigator": "사용자 지정 뷰 탐색기",
|
||||
"ui.heading_cylinder": "원통",
|
||||
"ui.heading_design": "디자인",
|
||||
"ui.heading_draft": "구배",
|
||||
@@ -4329,13 +4311,5 @@
|
||||
"values must be finite numbers": "값은 유한한 숫자여야 합니다",
|
||||
"values must be numbers or expressions": "값은 숫자 또는 식이어야 합니다",
|
||||
"view.render_inspector": "렌더 검사기",
|
||||
"view_gizmo.arrows": "회전 화살표",
|
||||
"view_gizmo.home": "홈 버튼",
|
||||
"view_gizmo.no_such_body": "이 디자인에 그런 이름의 바디가 없습니다.",
|
||||
"view_gizmo.note": "뷰에서 바디를 선택하십시오. 바디는 환경설정과 설정 프로필에 저장되어 모든 디자인에 표시됩니다. 면을 클릭하면 그 면을 정면으로 봅니다.",
|
||||
"view_gizmo.texture": "텍스처",
|
||||
"view_gizmo.themed": "테마",
|
||||
"view_gizmo.too_detailed": "뷰 탐색기에 너무 세밀합니다 (삼각형 10,000개 초과)",
|
||||
"view_gizmo.too_thin": "뷰 탐색기에 너무 가늘고 깁니다: 가장 얇은 변이 가장 긴 변의 5분의 1 이상이어야 합니다",
|
||||
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ 선택 · Enter / Tab 완성 · Esc 닫기"
|
||||
}
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Analisis Kebolehcapaian",
|
||||
"cmd.inspect_center_of_mass": "Pusat Jisim",
|
||||
"cmd.inspect_clearance": "Kelegaan",
|
||||
"cmd.inspect_colored_components": "Aktifkan Warna Komponen",
|
||||
"cmd.inspect_colored_components_disable": "Nyahaktifkan Warna Komponen",
|
||||
"cmd.inspect_compare": "Bandingkan Jasad",
|
||||
"cmd.inspect_compare_picture": "Banding dengan Imej",
|
||||
"cmd.inspect_continuity": "Analisis Keselanjaran",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Tilgjengelighetsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massesenter",
|
||||
"cmd.inspect_clearance": "Klaring",
|
||||
"cmd.inspect_colored_components": "Aktiver komponentfarger",
|
||||
"cmd.inspect_colored_components_disable": "Deaktiver komponentfarger",
|
||||
"cmd.inspect_compare": "Sammenlign kropper",
|
||||
"cmd.inspect_compare_picture": "Sammenlign bilde",
|
||||
"cmd.inspect_continuity": "Kontinuitetsanalyse",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Toegankelijkheidsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Zwaartepunt",
|
||||
"cmd.inspect_clearance": "Speling",
|
||||
"cmd.inspect_colored_components": "Componentkleuren inschakelen",
|
||||
"cmd.inspect_colored_components_disable": "Componentkleuren uitschakelen",
|
||||
"cmd.inspect_compare": "Lichamen vergelijken",
|
||||
"cmd.inspect_compare_picture": "Afbeelding vergelijken",
|
||||
"cmd.inspect_continuity": "Continuïteitsanalyse",
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"ai.prefs.instructions": "Custom instructions",
|
||||
"ai.prefs.instructions_note": "Sent to the agent at the start of every chat, after SolidCraft's own instructions: how you like to work, rules to keep to. A change applies from the next chat (Connect or New chat)."
|
||||
},
|
||||
"ar": {
|
||||
"ai.prefs.instructions": "تعليمات مخصصة",
|
||||
"ai.prefs.instructions_note": "تُرسَل إلى الوكيل في بداية كل محادثة، بعد تعليمات SolidCraft الخاصة: طريقتك المفضلة في العمل والقواعد التي يجب الالتزام بها. يسري أي تغيير من المحادثة التالية (اتصال أو محادثة جديدة)."
|
||||
},
|
||||
"he": {
|
||||
"ai.prefs.instructions": "הוראות מותאמות אישית",
|
||||
"ai.prefs.instructions_note": "נשלחות לסוכן בתחילת כל צ'אט, אחרי ההוראות של SolidCraft עצמה: דרך העבודה המועדפת עליך וכללים שיש להקפיד עליהם. שינוי חל מהצ'אט הבא (התחברות או צ'אט חדש)."
|
||||
},
|
||||
"ja": {
|
||||
"ai.prefs.instructions": "カスタム指示",
|
||||
"ai.prefs.instructions_note": "各チャットの開始時に、SolidCraft 自身の指示の後に続けてエージェントへ送られます。作業の進め方や守ってほしいルールを書いてください。変更は次のチャット(接続または新しいチャット)から適用されます。"
|
||||
},
|
||||
"vi": {
|
||||
"ai.prefs.instructions": "Hướng dẫn tùy chỉnh",
|
||||
"ai.prefs.instructions_note": "Được gửi cho agent khi bắt đầu mỗi cuộc trò chuyện, sau hướng dẫn riêng của SolidCraft: cách bạn muốn làm việc, các quy tắc cần tuân theo. Thay đổi có hiệu lực từ cuộc trò chuyện tiếp theo (Kết nối hoặc Cuộc trò chuyện mới)."
|
||||
},
|
||||
"zh": {
|
||||
"ai.prefs.instructions": "自定义指令",
|
||||
"ai.prefs.instructions_note": "在每次对话开始时发送给代理,位于 SolidCraft 自身的指令之后:你喜欢的工作方式、需要遵守的规则。更改从下一次对话(连接或新对话)起生效。"
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.aging_raytrace_help": "يُطبّق التعتيق في تتبع الأشعة على المعالج وتتبع المسارات على المعالج الرسومي. يبقى التصيير النقطي تخطيطياً."
|
||||
},
|
||||
"de": {
|
||||
"render.aging_raytrace_help": "Alterung wird beim CPU-Raytracing und GPU-Pathtracing angewendet. Raster bleibt eine schematische Darstellung."
|
||||
},
|
||||
"en": {
|
||||
"render.aging_raytrace_help": "Aging is applied by CPU ray tracing and GPU path tracing. Raster keeps schematic rendering."
|
||||
},
|
||||
"en-GB": {
|
||||
"render.aging_raytrace_help": "Ageing is applied by CPU ray tracing and GPU path tracing. Raster keeps schematic rendering."
|
||||
},
|
||||
"he": {
|
||||
"render.aging_raytrace_help": "התיישנות מוחלת במעקב קרניים במעבד ובמעקב מסלולים במעבד הגרפי. רינדור רסטר נשאר סכמטי."
|
||||
},
|
||||
"ja": {
|
||||
"render.aging_raytrace_help": "エイジングはCPUレイトレーシングとGPUパストレーシングで適用されます。ラスタは模式的な描画を維持します。"
|
||||
},
|
||||
"ko": {
|
||||
"render.aging_raytrace_help": "에이징은 CPU 광선 추적과 GPU 경로 추적에 적용됩니다. 래스터는 도식적인 렌더링을 유지합니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.aging_raytrace_help": "Hiệu ứng lão hoá được áp dụng khi dò tia bằng CPU và dò đường đi bằng GPU. Kết xuất raster giữ kiểu sơ đồ."
|
||||
},
|
||||
"zh": {
|
||||
"render.aging_raytrace_help": "老化效果适用于CPU光线追踪和GPU路径追踪。光栅保持示意渲染。"
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_volume_enable_path": "تفعيل الإضاءة غير المباشرة",
|
||||
"render.cpu_volume_enable_path_plan": "تتبع أشعة CPU · عمق المسار {depth} · {spp} عينة/بكسل · بذرة {seed} · ظلال متتبعة. يحتفظ بالغلاف الجوي وميزانية الصورة المحددين.",
|
||||
"render.cpu_volume_help": "يستخدم الغلاف الجوي على CPU الإضاءة غير المباشرة. يحافظ الانطفاء الصفري على الصندوق المحفوظ دون ضباب.",
|
||||
"render.cpu_volume_method_adopt": "تتبع أشعة CPU مع إضاءة غير مباشرة",
|
||||
"render.cpu_volume_method_incompatible": "لا يمكن نقل إعدادات الإضاءة أو أخذ العينات أو الغلاف الجوي هذه إلى هذه الطريقة. تبقى إعداداتك محفوظة.",
|
||||
"render.cpu_volume_path_required": "فعّل الإضاءة غير المباشرة لاستخدام الغلاف الجوي على CPU.",
|
||||
"render.volume_denoise_required": "أوقف تقليل الضوضاء لاستخدام الغلاف الجوي.",
|
||||
"render.volume_glass_required": "لا يمكن الجمع بين الغلاف الجوي والزجاج في هذا الإعداد.",
|
||||
"render.volume_lighting_required": "اختر بيئة أو مجموعة إضاءة محددة لتصيير الغلاف الجوي.",
|
||||
"render.volume_single_stratum_required": "استخدم عينة نفاذ واحدة لتصيير الغلاف الجوي."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_volume_enable_path": "Indirekte Beleuchtung einschalten",
|
||||
"render.cpu_volume_enable_path_plan": "CPU-Raytracing · Pfadtiefe {depth} · {spp} Samples/Pixel · Seed {seed} · Raytracing-Schatten. Atmosphäre und Bildbudget bleiben erhalten.",
|
||||
"render.cpu_volume_help": "CPU-Atmosphäre nutzt indirekte Beleuchtung. Bei Extinktion null bleibt die gespeicherte Box ohne Nebel erhalten.",
|
||||
"render.cpu_volume_method_adopt": "CPU-Raytracing mit indirekter Beleuchtung",
|
||||
"render.cpu_volume_method_incompatible": "Diese Licht-, Sampling- oder Atmosphäreinstellungen lassen sich nicht auf diese Methode übertragen. Die Einstellungen bleiben erhalten.",
|
||||
"render.cpu_volume_path_required": "Indirekte Beleuchtung einschalten, um CPU-Atmosphäre zu nutzen.",
|
||||
"render.volume_denoise_required": "Für Atmosphäre die Rauschreduzierung ausschalten.",
|
||||
"render.volume_glass_required": "Atmosphäre und Glas können in dieser Einstellung nicht kombiniert werden.",
|
||||
"render.volume_lighting_required": "Für Atmosphäre eine Umgebung oder ein eigenes Lichtrig wählen.",
|
||||
"render.volume_single_stratum_required": "Für Atmosphäre ein Transmissionssample verwenden."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_volume_enable_path": "Enable Indirect lighting",
|
||||
"render.cpu_volume_enable_path_plan": "CPU ray tracing · path depth {depth} · {spp} samples/pixel · seed {seed} · traced shadows. Keeps the authored atmosphere and image budget.",
|
||||
"render.cpu_volume_help": "CPU Atmosphere uses Indirect lighting. Zero extinction keeps the saved box without fog.",
|
||||
"render.cpu_volume_method_adopt": "CPU ray tracing with indirect lighting",
|
||||
"render.cpu_volume_method_incompatible": "These light, sampling or atmosphere settings cannot be transferred to this method. Your settings are kept.",
|
||||
"render.cpu_volume_path_required": "Enable Indirect lighting to use CPU Atmosphere.",
|
||||
"render.volume_denoise_required": "Turn off noise reduction to use Atmosphere.",
|
||||
"render.volume_glass_required": "Atmosphere cannot be combined with Glass in this setup.",
|
||||
"render.volume_lighting_required": "Choose an environment or an explicit light rig to render Atmosphere.",
|
||||
"render.volume_single_stratum_required": "Use one transmission sample to render Atmosphere."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_volume_enable_path": "הפעלת תאורה עקיפה",
|
||||
"render.cpu_volume_enable_path_plan": "מעקב קרניים ב־CPU · עומק מסלול {depth} · {spp} דגימות לפיקסל · זרע {seed} · צללים במעקב קרניים. האטמוספרה ותקציב התמונה שנקבעו נשמרים.",
|
||||
"render.cpu_volume_help": "אטמוספרה ב־CPU משתמשת בתאורה עקיפה. הכחדה אפסית שומרת את התיבה ללא ערפל.",
|
||||
"render.cpu_volume_method_adopt": "מעקב קרניים ב־CPU עם תאורה עקיפה",
|
||||
"render.cpu_volume_method_incompatible": "לא ניתן להעביר הגדרות תאורה, דגימה או אטמוספרה אלה לשיטה זו. ההגדרות שלך נשמרות.",
|
||||
"render.cpu_volume_path_required": "יש להפעיל תאורה עקיפה כדי להשתמש באטמוספרה ב־CPU.",
|
||||
"render.volume_denoise_required": "יש לכבות הפחתת רעש כדי להשתמש באטמוספרה.",
|
||||
"render.volume_glass_required": "לא ניתן לשלב אטמוספרה וזכוכית בהגדרה זו.",
|
||||
"render.volume_lighting_required": "יש לבחור סביבה או מערך תאורה מפורש כדי לרנדר אטמוספרה.",
|
||||
"render.volume_single_stratum_required": "יש להשתמש בדגימת מעבר אחת כדי לרנדר אטמוספרה."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_volume_enable_path": "間接照明を有効にする",
|
||||
"render.cpu_volume_enable_path_plan": "CPUレイトレーシング · パス深度{depth} · {spp}サンプル/ピクセル · シード{seed} · レイトレースシャドウ。大気と画像予算の設定を保持します。",
|
||||
"render.cpu_volume_help": "CPUの大気は間接照明を使用します。消散係数がゼロでも、霧のないボックスとして設定は保存されます。",
|
||||
"render.cpu_volume_method_adopt": "間接照明を使うCPUレイトレーシング",
|
||||
"render.cpu_volume_method_incompatible": "この照明、サンプリング、大気の設定はこの方式に引き継げません。設定は保持されます。",
|
||||
"render.cpu_volume_path_required": "CPUの大気を使うには間接照明を有効にしてください。",
|
||||
"render.volume_denoise_required": "大気を使うにはノイズ低減をオフにしてください。",
|
||||
"render.volume_glass_required": "この設定では大気とガラスを併用できません。",
|
||||
"render.volume_lighting_required": "大気を描画するには環境または明示的なライト構成を選んでください。",
|
||||
"render.volume_single_stratum_required": "大気を描画するには透過サンプル数を1にしてください。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_volume_enable_path": "간접 조명 켜기",
|
||||
"render.cpu_volume_enable_path_plan": "CPU 레이 트레이싱 · 경로 깊이 {depth} · 픽셀당 {spp}샘플 · 시드 {seed} · 레이 트레이싱 그림자. 작성한 대기와 이미지 예산을 유지합니다.",
|
||||
"render.cpu_volume_help": "CPU 대기는 간접 조명을 사용합니다. 소광 계수가 0이면 저장된 상자는 안개 없이 유지됩니다.",
|
||||
"render.cpu_volume_method_adopt": "간접 조명을 사용하는 CPU 레이 트레이싱",
|
||||
"render.cpu_volume_method_incompatible": "이 조명, 샘플링 또는 대기 설정을 이 방식으로 옮길 수 없습니다. 설정은 유지됩니다.",
|
||||
"render.cpu_volume_path_required": "CPU 대기를 사용하려면 간접 조명을 켜세요.",
|
||||
"render.volume_denoise_required": "대기를 사용하려면 노이즈 감소를 끄세요.",
|
||||
"render.volume_glass_required": "이 설정에서는 대기와 유리를 함께 사용할 수 없습니다.",
|
||||
"render.volume_lighting_required": "대기를 렌더링하려면 환경 또는 명시적인 조명 구성을 선택하세요.",
|
||||
"render.volume_single_stratum_required": "대기를 렌더링하려면 투과 샘플을 1로 설정하세요."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_volume_enable_path": "Bật chiếu sáng gián tiếp",
|
||||
"render.cpu_volume_enable_path_plan": "Dò tia CPU · độ sâu đường đi {depth} · {spp} mẫu/pixel · hạt giống {seed} · bóng dò tia. Giữ nguyên khí quyển và ngân sách ảnh đã đặt.",
|
||||
"render.cpu_volume_help": "Khí quyển trên CPU dùng chiếu sáng gián tiếp. Hệ số suy giảm bằng không vẫn giữ hộp đã lưu mà không tạo sương.",
|
||||
"render.cpu_volume_method_adopt": "Dò tia CPU với chiếu sáng gián tiếp",
|
||||
"render.cpu_volume_method_incompatible": "Không thể chuyển các thiết lập đèn, lấy mẫu hoặc khí quyển này sang phương pháp này. Thiết lập của bạn được giữ nguyên.",
|
||||
"render.cpu_volume_path_required": "Bật chiếu sáng gián tiếp để dùng Khí quyển trên CPU.",
|
||||
"render.volume_denoise_required": "Tắt giảm nhiễu để dùng Khí quyển.",
|
||||
"render.volume_glass_required": "Không thể kết hợp Khí quyển và Kính trong thiết lập này.",
|
||||
"render.volume_lighting_required": "Chọn môi trường hoặc bộ đèn tường minh để kết xuất Khí quyển.",
|
||||
"render.volume_single_stratum_required": "Dùng một mẫu truyền sáng để kết xuất Khí quyển."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_volume_enable_path": "启用间接照明",
|
||||
"render.cpu_volume_enable_path_plan": "CPU 光线追踪 · 路径深度 {depth} · 每像素 {spp} 个采样 · 种子 {seed} · 追踪阴影。保留已编写的大气和图像预算。",
|
||||
"render.cpu_volume_help": "CPU 大气使用间接照明。消光系数为零时,已保存的包围盒仍会保留,但不会产生雾。",
|
||||
"render.cpu_volume_method_adopt": "使用间接照明的 CPU 光线追踪",
|
||||
"render.cpu_volume_method_incompatible": "这些灯光、采样或大气设置无法转移到此方式。您的设置会保留。",
|
||||
"render.cpu_volume_path_required": "启用间接照明以使用 CPU 大气。",
|
||||
"render.volume_denoise_required": "关闭降噪以使用大气。",
|
||||
"render.volume_glass_required": "此设置无法同时使用大气和玻璃。",
|
||||
"render.volume_lighting_required": "选择环境或显式灯光配置以渲染大气。",
|
||||
"render.volume_single_stratum_required": "将透射采样数设为 1 以渲染大气。"
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_constant_direct_help": "بدون الإضاءة غير المباشرة، يمكن أن يظهر هذا المصدر الثابت في الانعكاسات وعبر الزجاج. لكنه لا يضيء الأسطح المعتمة. فعّل الإضاءة غير المباشرة لاستخدامه في إضاءة المشهد.",
|
||||
"render.cpu_constant_enable_indirect_plan": "عمق المسار {depth}؛ أخذ عينات تدريجي بمعدل {spp} عينة/بكسل (البذرة {seed})؛ شعاع أولي واحد لكل عينة؛ ظلال متتبعة. يحتفظ بحجم الصورة وميزانية CPU والإعدادات الأخرى.",
|
||||
"render.cpu_indirect_method_required": "اختر تتبع أشعة CPU لتفعيل وضع الإضاءة غير المباشرة هذا."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_constant_direct_help": "Ohne indirekte Beleuchtung kann diese konstante Quelle in Reflexionen und durch Glas sichtbar sein. Sie beleuchtet keine undurchsichtigen Flächen. Schalte indirekte Beleuchtung ein, um sie als Szenenlicht zu nutzen.",
|
||||
"render.cpu_constant_enable_indirect_plan": "Pfadtiefe {depth}; progressives Sampling mit {spp} Samples/Pixel (Seed {seed}); ein Primärstrahl pro Sample; Raytracing-Schatten. Bildgröße, CPU-Budget und andere Einstellungen bleiben erhalten.",
|
||||
"render.cpu_indirect_method_required": "Wähle CPU-Raytracing, um diesen Modus für indirekte Beleuchtung einzuschalten."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_constant_direct_help": "Without Indirect lighting, this constant source can appear in reflections and through Glass. It does not light opaque surfaces. Enable Indirect lighting to use it for scene lighting.",
|
||||
"render.cpu_constant_enable_indirect_plan": "Path depth {depth}; progressive sampling at {spp} samples/pixel (seed {seed}); one primary ray per sample; traced shadows. Keeps your image size, CPU budget and other settings.",
|
||||
"render.cpu_indirect_method_required": "Choose CPU ray tracing to enable this Indirect lighting mode."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_constant_direct_help": "ללא תאורה עקיפה, מקור קבוע זה עשוי להופיע בהשתקפויות ודרך זכוכית. הוא אינו מאיר משטחים אטומים. יש להפעיל תאורה עקיפה כדי להשתמש בו לתאורת הסצנה.",
|
||||
"render.cpu_constant_enable_indirect_plan": "עומק מסלול {depth}; דגימה הדרגתית עם {spp} דגימות לפיקסל (זרע {seed}); קרן ראשית אחת לכל דגימה; צללים במעקב קרניים. גודל התמונה, תקציב ה־CPU וההגדרות האחרות נשמרים.",
|
||||
"render.cpu_indirect_method_required": "יש לבחור מעקב קרניים ב־CPU כדי להפעיל מצב זה של תאורה עקיפה."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_constant_direct_help": "間接照明を使わない場合、この一定の光源は反射やガラス越しに見えることがありますが、不透明な面は照らしません。シーンの照明に使うには、間接照明を有効にしてください。",
|
||||
"render.cpu_constant_enable_indirect_plan": "パスの深さ {depth}、ピクセルあたり {spp} サンプルのプログレッシブサンプリング(シード {seed})、各サンプルに一次レイ1本、レイトレーシングの影を使用します。画像サイズ、CPU予算、その他の設定は保持します。",
|
||||
"render.cpu_indirect_method_required": "この間接照明モードを有効にするには、CPUレイトレーシングを選択してください。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_constant_direct_help": "간접 조명이 꺼져 있으면 이 일정한 광원은 반사나 유리를 통해 보일 수 있지만 불투명한 표면을 밝히지는 않습니다. 장면 조명으로 사용하려면 간접 조명을 켜세요.",
|
||||
"render.cpu_constant_enable_indirect_plan": "경로 깊이 {depth}, 픽셀당 {spp}개 샘플의 점진적 샘플링(시드 {seed}), 샘플당 기본 광선 1개, 추적된 그림자를 사용합니다. 이미지 크기, CPU 예산 및 다른 설정을 유지합니다.",
|
||||
"render.cpu_indirect_method_required": "이 간접 조명 모드를 켜려면 CPU 레이 트레이싱을 선택하세요."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_constant_direct_help": "Khi tắt Chiếu sáng gián tiếp, nguồn sáng đồng nhất này có thể xuất hiện trong phản xạ và qua Kính nhưng không chiếu sáng các bề mặt đục. Bật Chiếu sáng gián tiếp để dùng nguồn này chiếu sáng cảnh.",
|
||||
"render.cpu_constant_enable_indirect_plan": "Độ sâu đường đi {depth}; lấy mẫu tăng dần với {spp} mẫu/pixel (hạt giống {seed}); một tia sơ cấp cho mỗi mẫu; bóng đổ dò tia. Giữ kích thước ảnh, ngân sách CPU và các cài đặt khác.",
|
||||
"render.cpu_indirect_method_required": "Chọn dò tia CPU để bật chế độ Chiếu sáng gián tiếp này."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_constant_direct_help": "未启用间接照明时,此恒定光源可能出现在反射中或透过玻璃可见,但不会照亮不透明表面。要用它照亮场景,请启用间接照明。",
|
||||
"render.cpu_constant_enable_indirect_plan": "路径深度 {depth};每像素 {spp} 个样本的渐进采样(种子 {seed});每个样本一条主光线;光线追踪阴影。保留图像尺寸、CPU预算和其他设置。",
|
||||
"render.cpu_indirect_method_required": "请选择CPU光线追踪以启用此间接照明模式。"
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_glass_enable_path": "تشغيل الإضاءة غير المباشرة",
|
||||
"render.cpu_path_depth": "عمق المسار",
|
||||
"render.cpu_path_help": "تحسب الضوء المرتد بين الأسطح المعتمة والمار عبر أشكال الزجاج المدعومة. تتطلب التصيير التدريجي وظلال تتبع الأشعة. يحسب عمق المسار الأسطح المضاءة وحدود الزجاج وأحداث التشتت بدءاً من أول تقاطع (1–16). يقتصر العمق 1 على الإضاءة المباشرة للأسطح المعتمة؛ وتسمح القيم الأعلى بمزيد من الارتدادات أو عبور الزجاج وقد تستغرق وقتاً أطول. أوقف الإضاءة غير المباشرة لاستخدام الانعكاسات المخصصة أو التباين الاتجاهي.",
|
||||
"render.cpu_path_lighting": "إضاءة غير مباشرة",
|
||||
"render.cpu_path_optics_reason": "تبقى إعداداتك كما هي. أوقف الإضاءة غير المباشرة لاستخدام الانعكاسات المخصصة أو التباين الاتجاهي."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_glass_enable_path": "Indirekte Beleuchtung einschalten",
|
||||
"render.cpu_path_depth": "Pfadtiefe",
|
||||
"render.cpu_path_help": "Berechnet Licht, das zwischen undurchsichtigen Flächen und durch unterstützte Glasformen läuft. Erfordert progressives Rendern und Raytracing-Schatten. Die Pfadtiefe zählt beleuchtete Flächen, Glasgrenzen und Streuereignisse ab dem ersten Treffer (1–16). Tiefe 1 liefert direkte Beleuchtung auf undurchsichtigen Flächen; höhere Werte erlauben mehr Reflexionen oder Glasdurchgänge und können länger dauern. Für eigene Reflexionen oder Anisotropie die indirekte Beleuchtung ausschalten.",
|
||||
"render.cpu_path_lighting": "Indirekte Beleuchtung",
|
||||
"render.cpu_path_optics_reason": "Die Einstellungen bleiben erhalten. Für eigene Reflexionen oder Anisotropie die indirekte Beleuchtung ausschalten."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_glass_enable_path": "Enable Indirect lighting",
|
||||
"render.cpu_path_depth": "Path depth",
|
||||
"render.cpu_path_help": "Adds light bouncing between opaque surfaces and through supported Glass shapes. Requires progressive rendering and traced shadows. Path depth counts shaded surfaces, Glass boundaries and scattering events, starting with the first hit (1–16). Depth 1 gives direct lighting on opaque surfaces; higher values allow more bounces or Glass crossings and may take longer. Turn off Indirect lighting to use custom reflections or anisotropy.",
|
||||
"render.cpu_path_lighting": "Indirect lighting",
|
||||
"render.cpu_path_optics_reason": "Your settings are kept. Turn off Indirect lighting to use custom reflections or anisotropy."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_glass_enable_path": "הפעלת תאורה עקיפה",
|
||||
"render.cpu_path_depth": "עומק מסלול",
|
||||
"render.cpu_path_help": "מחשבת אור שמוחזר בין משטחים אטומים ועובר דרך צורות זכוכית נתמכות. נדרשים רינדור הדרגתי וצללים במעקב קרניים. עומק המסלול סופר משטחים מוצללים, גבולות זכוכית ואירועי פיזור החל מהמפגש הראשון (1–16). עומק 1 נותן תאורה ישירה למשטחים אטומים; ערכים גבוהים מאפשרים יותר החזרות או מעברים בזכוכית ועשויים לדרוש יותר זמן. יש לכבות תאורה עקיפה כדי להשתמש בהשתקפויות מותאמות או באנאיזוטרופיה.",
|
||||
"render.cpu_path_lighting": "תאורה עקיפה",
|
||||
"render.cpu_path_optics_reason": "ההגדרות שלך נשמרות. יש לכבות תאורה עקיפה כדי להשתמש בהשתקפויות מותאמות או באנאיזוטרופיה."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_glass_enable_path": "間接照明を有効にする",
|
||||
"render.cpu_path_depth": "パス深度",
|
||||
"render.cpu_path_help": "不透明な面の間で反射し、対応するガラス形状を通る光を計算します。プログレッシブ描画とレイトレーシングの影が必要です。パス深度は最初の交点から、照明を計算する面、ガラス境界、散乱イベントを数えます(1~16)。深度1は不透明面の直接照明のみです。値を上げると反射やガラスの通過を増やせますが、時間がかかる場合があります。独自の反射設定や異方性を使うには間接照明をオフにしてください。",
|
||||
"render.cpu_path_lighting": "間接照明",
|
||||
"render.cpu_path_optics_reason": "設定は保持されます。独自の反射設定や異方性を使うには間接照明をオフにしてください。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_glass_enable_path": "간접 조명 켜기",
|
||||
"render.cpu_path_depth": "경로 깊이",
|
||||
"render.cpu_path_help": "불투명 표면 사이에서 반사되고 지원되는 유리 형상을 통과하는 빛을 계산합니다. 점진적 렌더링과 레이 추적 그림자가 필요합니다. 경로 깊이는 첫 교점부터 조명이 계산되는 표면, 유리 경계 및 산란 이벤트를 셉니다(1–16). 깊이 1은 불투명 표면의 직접 조명만 계산합니다. 값을 높이면 반사나 유리 통과가 늘어나며 시간이 더 걸릴 수 있습니다. 사용자 반사 설정이나 이방성을 사용하려면 간접 조명을 끄세요.",
|
||||
"render.cpu_path_lighting": "간접 조명",
|
||||
"render.cpu_path_optics_reason": "설정은 유지됩니다. 사용자 반사 설정이나 이방성을 사용하려면 간접 조명을 끄세요."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_glass_enable_path": "Bật Chiếu sáng gián tiếp",
|
||||
"render.cpu_path_depth": "Độ sâu đường truyền",
|
||||
"render.cpu_path_help": "Tính ánh sáng phản xạ giữa các bề mặt đục và truyền qua các hình kính được hỗ trợ. Cần kết xuất lũy tiến và bóng dò tia. Độ sâu đường đi đếm các bề mặt được chiếu sáng, ranh giới kính và các sự kiện tán xạ từ giao điểm đầu tiên (1–16). Độ sâu 1 chỉ chiếu sáng trực tiếp bề mặt đục; giá trị cao hơn cho phép thêm lần phản xạ hoặc xuyên kính và có thể lâu hơn. Tắt Chiếu sáng gián tiếp để dùng phản xạ tùy chỉnh hoặc dị hướng.",
|
||||
"render.cpu_path_lighting": "Chiếu sáng gián tiếp",
|
||||
"render.cpu_path_optics_reason": "Các thiết lập được giữ nguyên. Tắt Chiếu sáng gián tiếp để dùng phản xạ tùy chỉnh hoặc dị hướng."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_glass_enable_path": "启用间接照明",
|
||||
"render.cpu_path_depth": "路径深度",
|
||||
"render.cpu_path_help": "计算在不透明表面之间反射并穿过受支持玻璃形状的光。需要渐进渲染和光线追踪阴影。路径深度从首次相交开始计算着色表面、玻璃边界和散射事件(1–16)。深度 1 只计算不透明表面的直接照明;更高的值允许更多反射或玻璃穿越,也可能耗时更长。使用自定义反射或各向异性时,请关闭间接照明。",
|
||||
"render.cpu_path_lighting": "间接照明",
|
||||
"render.cpu_path_optics_reason": "设置会保留。使用自定义反射或各向异性时,请关闭间接照明。"
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"render.cpu_quality_budget_help": "Changes size, production sample cap, seed and CPU resource profile. Camera, lens, lighting, GI depth and effects are kept.",
|
||||
"render.cpu_quality_progressive_required": "Enable progressive sampling below before choosing a CPU budget.",
|
||||
"render.cpu_quality_summary": "{width} × {height} · {samples} sample cap · seed {seed} · {profile}"
|
||||
},
|
||||
"ar": {
|
||||
"render.cpu_quality_budget_help": "يغير الحجم وحد عينات الإنتاج والبذرة وملف موارد CPU. تُحفظ الكاميرا والعدسة والإضاءة وعمق GI والتأثيرات.",
|
||||
"render.cpu_quality_progressive_required": "فعّل أخذ العينات التدريجي أدناه قبل اختيار ميزانية CPU.",
|
||||
"render.cpu_quality_summary": "{width} × {height} · حد العينات {samples} · البذرة {seed} · {profile}"
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_quality_budget_help": "Ändert Größe, Obergrenze der Produktionssamples, Startwert und CPU-Ressourcenprofil. Kamera, Objektiv, Beleuchtung, GI-Tiefe und Effekte bleiben erhalten.",
|
||||
"render.cpu_quality_progressive_required": "Aktiviere unten progressive Samples, bevor du ein CPU-Budget auswählst.",
|
||||
"render.cpu_quality_summary": "{width} × {height} · Sample-Grenze {samples} · Startwert {seed} · {profile}"
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_quality_budget_help": "משנה גודל, תקרת דגימות הפקה, זרע ופרופיל משאבי CPU. המצלמה, העדשה, התאורה, עומק GI והאפקטים נשמרים.",
|
||||
"render.cpu_quality_progressive_required": "יש להפעיל דגימה מתקדמת למטה לפני בחירת תקציב CPU.",
|
||||
"render.cpu_quality_summary": "{width} × {height} · תקרת דגימות {samples} · זרע {seed} · {profile}"
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_quality_budget_help": "サイズ、本番サンプル数の上限、シード、CPUリソースプロファイルを変更します。カメラ、レンズ、照明、GI深度、エフェクトは維持されます。",
|
||||
"render.cpu_quality_progressive_required": "CPU予算を選ぶ前に、下でプログレッシブサンプリングを有効にしてください。",
|
||||
"render.cpu_quality_summary": "{width} × {height} · サンプル上限 {samples} · シード {seed} · {profile}"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_quality_budget_help": "크기, 제작 샘플 상한, 시드와 CPU 리소스 프로필을 변경합니다. 카메라, 렌즈, 조명, GI 깊이와 효과는 유지됩니다.",
|
||||
"render.cpu_quality_progressive_required": "CPU 예산을 선택하기 전에 아래에서 점진적 샘플링을 활성화하세요.",
|
||||
"render.cpu_quality_summary": "{width} × {height} · 샘플 상한 {samples} · 시드 {seed} · {profile}"
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_quality_budget_help": "Thay đổi kích thước, trần mẫu sản xuất, hạt giống và cấu hình tài nguyên CPU. Giữ nguyên máy ảnh, ống kính, ánh sáng, độ sâu GI và hiệu ứng.",
|
||||
"render.cpu_quality_progressive_required": "Bật lấy mẫu tiến dần bên dưới trước khi chọn ngân sách CPU.",
|
||||
"render.cpu_quality_summary": "{width} × {height} · trần mẫu {samples} · hạt giống {seed} · {profile}"
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_quality_budget_help": "更改尺寸、正式样本上限、种子和CPU资源配置。保留相机、镜头、照明、GI深度和效果。",
|
||||
"render.cpu_quality_progressive_required": "选择CPU预算前,请先在下方启用渐进采样。",
|
||||
"render.cpu_quality_summary": "{width} × {height} · 样本上限 {samples} · 种子 {seed} · {profile}"
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.appearance_editor.discard_saved_edit": "تجاهل التعديلات البصرية غير المحفوظة",
|
||||
"render.cpu_subsurface_denoise_reason": "يتطلب التشتت داخل الزجاج على CPU إيقاف إزالة الضجيج. تبقى إعداداتك محفوظة.",
|
||||
"render.cpu_subsurface_help": "يغيّر التشتت اتجاه الضوء داخل الزجاج المغلق. تتطلب المعدلات الموجبة إضاءة غير مباشرة مع إيقاف إزالة الضجيج. المعدل هو عدد التصادمات لكل مم؛ لا يضيف الصفر أي تشتت. يفضّل انحياز الاتجاه الموجب الأمام، والسالب الخلف.",
|
||||
"render.material_cpu_glass_only": "تتطلب خرائط ملفات الصور المؤقتة GPU. يمكن لـCPU استخدام خرائط الأسطح المضمنة المحفوظة.",
|
||||
"render.material_cpu_optics_limits": "شغّل الإضاءة غير المباشرة لأشكال الزجاج المغلقة المدعومة والتداخل المتحقق منه والتشتت داخل الزجاج. يمكن للصفائح الرقيقة تمرير الضوء للظلال. وإلا فاستخدم صفيحة مستوية أو صندوقًا محاذيًا للمحاور. تظل الأشكال المتلامسة والظلال النافذة عبر الزجاج المغلق غير مدعومة. تبقى قيمك محفوظة.",
|
||||
"render.material_cpu_saved_runtime_conflict": "لا يمكن الجمع بين البصريات المحفوظة على CPU والارتباطات البصرية المؤقتة السابقة. احفظ البصريات التي راجعتها أو أزل الارتباط المؤقت المتعارض.",
|
||||
"render.material_raytrace_required": "يتطلب الزجاج الفيزيائي تتبع الأشعة على CPU أو تتبع المسارات على GPU.",
|
||||
"render.material_saved_edit_pending": "هذه التعديلات البصرية غير محفوظة. احفظ القيم التي راجعتها، أو أزل التعيين المحفوظ، أو تجاهل التعديلات. لا يمكن للارتباطات المؤقتة السابقة والمؤثرات غير المدعومة تجاوز الهدف المحفوظ."
|
||||
},
|
||||
"de": {
|
||||
"render.appearance_editor.discard_saved_edit": "Ungespeicherte Optikänderungen verwerfen",
|
||||
"render.cpu_subsurface_denoise_reason": "Streuung im Glas auf der CPU benötigt ausgeschaltete Rauschreduzierung. Die Einstellungen bleiben erhalten.",
|
||||
"render.cpu_subsurface_help": "Streuung lenkt Licht im Inneren geschlossenen Glases um. Positive Raten benötigen indirekte Beleuchtung ohne Rauschreduzierung. Die Rate zählt Kollisionen pro mm; 0 fügt keine Streuung hinzu. Positive Richtungswerte bevorzugen vorwärts, negative rückwärts.",
|
||||
"render.material_cpu_glass_only": "Temporäre Bilddatei-Karten benötigen die GPU. Die CPU kann gespeicherte integrierte Oberflächenkarten verwenden.",
|
||||
"render.material_cpu_optics_limits": "Indirekte Beleuchtung für unterstützte geschlossene Glasformen, geprüfte Verschachtelung und Streuung im Inneren einschalten. Dünne Scheiben können Licht für Schatten durchlassen. Sonst eine ebene Scheibe oder achsenparallele Box verwenden. Berührende Formen und durch geschlossenes Glas übertragene Schatten werden weiterhin nicht unterstützt. Die Werte bleiben erhalten.",
|
||||
"render.material_cpu_saved_runtime_conflict": "Gespeicherte CPU-Optik kann nicht mit temporären optischen Bindungen kombiniert werden. Geprüfte Optik speichern oder die temporäre Bindung entfernen.",
|
||||
"render.material_raytrace_required": "Physikalisches Glas benötigt CPU-Raytracing oder GPU-Pathtracing.",
|
||||
"render.material_saved_edit_pending": "Diese Optikänderungen sind ungespeichert. Geprüfte Werte speichern, die gespeicherte Zuordnung entfernen oder Änderungen verwerfen. Temporäre Bindungen und nicht unterstützte Effekte können das gespeicherte Ziel nicht überschreiben."
|
||||
},
|
||||
"en": {
|
||||
"render.appearance_editor.discard_saved_edit": "Discard unsaved optical edits",
|
||||
"render.cpu_subsurface_denoise_reason": "CPU subsurface scattering needs Denoise off. Your settings are kept.",
|
||||
"render.cpu_subsurface_help": "Scattering redirects light inside closed Glass. Positive rates need Indirect lighting with Denoise off. The rate is collisions per mm; 0 adds no scattering. Positive phase bias favors forward directions; negative favors backward.",
|
||||
"render.material_cpu_glass_only": "Temporary image-file maps require GPU. CPU can use saved built-in surface maps.",
|
||||
"render.material_cpu_optics_limits": "Turn on Indirect lighting for supported closed Glass shapes, certified nesting and subsurface scattering. Thin sheets can pass shadows. Otherwise use a planar sheet or axis-aligned box. Touching shapes and transmitted shadows through closed Glass remain unsupported. Your values are kept.",
|
||||
"render.material_cpu_saved_runtime_conflict": "Saved CPU optics cannot combine with legacy temporary optical bindings. Save the reviewed optics or remove the conflicting temporary binding.",
|
||||
"render.material_raytrace_required": "Physical Glass requires CPU ray tracing or GPU path tracing.",
|
||||
"render.material_saved_edit_pending": "These optical edits are unsaved. Save reviewed values, remove the saved assignment, or discard the edits. Legacy temporary bindings and unsupported effects cannot override the saved target."
|
||||
},
|
||||
"he": {
|
||||
"render.appearance_editor.discard_saved_edit": "ביטול שינויים אופטיים שלא נשמרו",
|
||||
"render.cpu_subsurface_denoise_reason": "פיזור בתוך זכוכית ב־CPU דורש הסרת רעש כבויה. ההגדרות נשמרות.",
|
||||
"render.cpu_subsurface_help": "פיזור משנה את כיוון האור בתוך זכוכית סגורה. קצב חיובי דורש תאורה עקיפה והסרת רעש כבויה. הקצב הוא מספר התנגשויות למ״מ; 0 אינו מוסיף פיזור. הטיית כיוון חיובית מעדיפה קדימה, ושלילית אחורה.",
|
||||
"render.material_cpu_glass_only": "מפות זמניות מקובצי תמונה דורשות GPU. ב־CPU אפשר להשתמש במפות משטח מובנות שנשמרו.",
|
||||
"render.material_cpu_optics_limits": "יש להפעיל תאורה עקיפה לצורות זכוכית סגורות נתמכות, לקינון שנבדק ולפיזור בתוך הזכוכית. יריעות דקות יכולות להעביר אור לצללים. אחרת יש להשתמש ביריעה מישורית או בתיבה מקבילה לצירים. צורות נוגעות וצללים העוברים בזכוכית סגורה עדיין אינם נתמכים. הערכים נשמרים.",
|
||||
"render.material_cpu_saved_runtime_conflict": "אי אפשר לשלב הגדרות אופטיות שמורות ב־CPU עם הקישורים האופטיים הזמניים הישנים. יש לשמור את ההגדרות שנבדקו או להסיר את הקישור הזמני המתנגש.",
|
||||
"render.material_raytrace_required": "זכוכית פיזיקלית דורשת עקיבת קרניים ב־CPU או עקיבת נתיבים ב־GPU.",
|
||||
"render.material_saved_edit_pending": "השינויים האופטיים האלה לא נשמרו. יש לשמור את הערכים שנבדקו, להסיר את ההקצאה השמורה או לבטל את השינויים. קישורים זמניים ישנים ואפקטים שאינם נתמכים אינם יכולים לעקוף את היעד השמור."
|
||||
},
|
||||
"ja": {
|
||||
"render.appearance_editor.discard_saved_edit": "未保存の光学設定の変更を破棄",
|
||||
"render.cpu_subsurface_denoise_reason": "CPUの内部散乱ではノイズ除去をオフにしてください。設定値は保持されます。",
|
||||
"render.cpu_subsurface_help": "散乱は閉じたガラスの内部で光の方向を変えます。正の値には間接照明を使い、ノイズ除去をオフにします。率は1mmあたりの衝突回数です。0では散乱を加えません。方向の偏りが正なら前方、負なら後方を優先します。",
|
||||
"render.material_cpu_glass_only": "一時画像ファイルのマップにはGPUが必要です。CPUでは保存済みの組み込み表面マップを使用できます。",
|
||||
"render.material_cpu_optics_limits": "対応する閉じたガラス形状、検証済みの入れ子形状、内部散乱には間接照明をオンにしてください。薄いシートは影に光を透過できます。それ以外は平面シートか軸平行の箱を使います。接触する形状と閉じたガラスを通る透過影には未対応です。値は保持されます。",
|
||||
"render.material_cpu_saved_runtime_conflict": "保存済みCPU光学設定と一時的な光学割り当ては併用できません。確認した設定を保存するか、競合する一時割り当てを削除してください。",
|
||||
"render.material_raytrace_required": "物理ガラスにはCPUレイトレーシングまたはGPUパストレーシングが必要です。",
|
||||
"render.material_saved_edit_pending": "光学設定の変更は未保存です。確認した値を保存するか、保存済み割り当てを削除するか、変更を破棄してください。一時割り当てや未対応の効果で保存済み対象を上書きできません。"
|
||||
},
|
||||
"ko": {
|
||||
"render.appearance_editor.discard_saved_edit": "저장하지 않은 광학 편집 버리기",
|
||||
"render.cpu_subsurface_denoise_reason": "CPU 내부 산란을 사용하려면 노이즈 제거를 끄세요. 설정은 유지됩니다.",
|
||||
"render.cpu_subsurface_help": "산란은 닫힌 유리 내부에서 빛의 방향을 바꿉니다. 양수 비율에는 간접광을 사용하고 노이즈 제거를 끄세요. 비율은 mm당 충돌 횟수이며, 0은 산란을 추가하지 않습니다. 방향 편향이 양수이면 전방, 음수이면 후방을 선호합니다.",
|
||||
"render.material_cpu_glass_only": "임시 이미지 파일 맵에는 GPU가 필요합니다. CPU는 저장된 내장 표면 맵을 사용할 수 있습니다.",
|
||||
"render.material_cpu_optics_limits": "지원되는 닫힌 유리 형상, 검증된 중첩 및 내부 산란에는 간접 조명을 켜세요. 얇은 시트는 그림자에 빛을 통과시킬 수 있습니다. 그 외에는 평면 시트나 축 정렬 상자를 사용하세요. 서로 닿는 형상과 닫힌 유리를 통과하는 투과 그림자는 아직 지원하지 않습니다. 값은 유지됩니다.",
|
||||
"render.material_cpu_saved_runtime_conflict": "저장된 CPU 광학 설정은 임시 광학 바인딩과 함께 사용할 수 없습니다. 검토한 설정을 저장하거나 충돌하는 임시 바인딩을 제거하세요.",
|
||||
"render.material_raytrace_required": "물리 유리에는 CPU 레이 트레이싱이나 GPU 패스 트레이싱이 필요합니다.",
|
||||
"render.material_saved_edit_pending": "광학 편집을 아직 저장하지 않았습니다. 검토한 값을 저장하거나 저장된 할당을 제거하거나 편집을 버리세요. 임시 바인딩과 지원하지 않는 효과는 저장된 대상을 덮어쓸 수 없습니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.appearance_editor.discard_saved_edit": "Hủy chỉnh sửa quang học chưa lưu",
|
||||
"render.cpu_subsurface_denoise_reason": "Tán xạ bên trong kính trên CPU cần tắt Khử nhiễu. Các cài đặt được giữ nguyên.",
|
||||
"render.cpu_subsurface_help": "Tán xạ đổi hướng ánh sáng bên trong kính kín. Tốc độ dương cần Chiếu sáng gián tiếp và tắt Khử nhiễu. Tốc độ là số va chạm mỗi mm; 0 không thêm tán xạ. Độ lệch hướng dương ưu tiên phía trước, âm ưu tiên phía sau.",
|
||||
"render.material_cpu_glass_only": "Bản đồ từ tệp ảnh tạm thời yêu cầu GPU. CPU có thể dùng bản đồ bề mặt tích hợp đã lưu.",
|
||||
"render.material_cpu_optics_limits": "Bật Chiếu sáng gián tiếp cho hình kính kín được hỗ trợ, cấu trúc lồng nhau đã kiểm chứng và tán xạ bên trong kính. Tấm mỏng có thể truyền ánh sáng cho bóng. Nếu không, dùng tấm phẳng hoặc hộp thẳng theo trục. Chưa hỗ trợ hình chạm nhau và bóng truyền qua kính kín. Các giá trị được giữ nguyên.",
|
||||
"render.material_cpu_saved_runtime_conflict": "Không thể kết hợp quang học CPU đã lưu với các liên kết quang học tạm thời kiểu cũ. Hãy lưu quang học đã xem xét hoặc xóa liên kết tạm thời xung đột.",
|
||||
"render.material_raytrace_required": "Kính vật lý yêu cầu dò tia trên CPU hoặc dò đường đi trên GPU.",
|
||||
"render.material_saved_edit_pending": "Các chỉnh sửa quang học này chưa được lưu. Hãy lưu các giá trị đã xem xét, xóa phép gán đã lưu hoặc hủy chỉnh sửa. Liên kết tạm thời kiểu cũ và hiệu ứng chưa được hỗ trợ không thể ghi đè đích đã lưu."
|
||||
},
|
||||
"zh": {
|
||||
"render.appearance_editor.discard_saved_edit": "放弃未保存的光学编辑",
|
||||
"render.cpu_subsurface_denoise_reason": "CPU 内部散射需要关闭降噪。设置会保留。",
|
||||
"render.cpu_subsurface_help": "散射会改变闭合玻璃内部的光线方向。正散射率需要开启间接光照并关闭降噪。散射率表示每毫米的碰撞次数;0 不增加散射。方向偏置为正时偏向前方,为负时偏向后方。",
|
||||
"render.material_cpu_glass_only": "临时图像文件贴图需要GPU。CPU可以使用已保存的内置表面贴图。",
|
||||
"render.material_cpu_optics_limits": "启用间接光照可使用受支持的闭合玻璃形状、经验证的嵌套结构及内部散射。薄片可以透射阴影。否则请使用平面薄片或轴对齐盒体。仍不支持相互接触的形状及穿过闭合玻璃的透射阴影。设置值会保留。",
|
||||
"render.material_cpu_saved_runtime_conflict": "已保存的CPU光学设置不能与临时光学绑定组合。请保存已审阅的设置或移除冲突的临时绑定。",
|
||||
"render.material_raytrace_required": "物理玻璃需要CPU光线追踪或GPU路径追踪。",
|
||||
"render.material_saved_edit_pending": "这些光学编辑尚未保存。请保存已审阅的值、移除已保存的分配或放弃编辑。临时绑定和不支持的效果不能覆盖已保存的目标。"
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_maps_saved_help": "يمكن لتصيير CPU استخدام خرائط الأسطح المعتمة المحفوظة: اختر سطحًا هنا ثم احفظ خرائطه في اللقطة. مع الإضاءة غير المباشرة، يمكن استخدام الزجاج في جسم آخر؛ لا يمكن استخدام الزجاج في الجسم ذي الخرائط. يجب إيقاف الانعكاسات المختارة والتباين الاتجاهي والتأثيرات الإجرائية. تتطلب مسارات ملفات الخرائط المؤقتة GPU."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_maps_saved_help": "CPU-Renderings können gespeicherte Karten undurchsichtiger Flächen verwenden: Oberfläche wählen und die Karten in der Aufnahme speichern. Mit indirekter Beleuchtung darf ein anderer Körper Glas verwenden; der Körper mit Karten darf kein Glas sein. Ausgewählte Reflexionen, Anisotropie und prozedurale Effekte ausschalten. Temporäre Kartendateipfade verwenden die GPU."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_maps_saved_help": "CPU renders can use saved opaque maps: choose a surface here, then Save maps to shot. With Indirect lighting, Glass can be on another body; a mapped body cannot use Glass. Selected reflections, anisotropy and procedural surface effects must be off. Temporary map file paths use GPU."
|
||||
},
|
||||
"es": {
|
||||
"render.cpu_maps_saved_help": "Los renders CPU pueden usar mapas opacos guardados: elige una superficie y guarda sus mapas en la toma. Con Iluminación indirecta, otro cuerpo puede usar Vidrio; el cuerpo con mapas no puede usar Vidrio. Desactiva los reflejos seleccionados, la anisotropía y los efectos procedurales. Las rutas temporales de archivos de mapas usan GPU."
|
||||
},
|
||||
"fr": {
|
||||
"render.cpu_maps_saved_help": "Le rendu CPU accepte les cartes opaques enregistrées : choisissez une surface, puis enregistrez ses cartes dans la vue. Avec l’éclairage indirect, un autre corps peut utiliser Verre ; le corps avec les cartes ne le peut pas. Désactivez les réflexions sélectionnées, l’anisotropie et les effets procéduraux. Les fichiers temporaires de cartes utilisent le GPU."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_maps_saved_help": "רינדור CPU יכול להשתמש במפות אטומות שמורות: בחרו כאן משטח ושמרו את המפות בתצלום. עם תאורה עקיפה, גוף אחר יכול להשתמש בזכוכית; הגוף בעל המפות אינו יכול להשתמש בזכוכית. יש לכבות החזרות שנבחרו, אנאיזוטרופיה ואפקטים פרוצדורליים. נתיבים זמניים לקובצי מפות משתמשים ב־GPU."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_maps_saved_help": "CPUレンダリングは保存された不透明サーフェスマップを使えます。サーフェスを選び、ショットにマップを保存してください。間接光を使う場合、別のボディにはガラスを使えますが、マップを持つボディには使えません。選択した反射、異方性、プロシージャル効果はオフにします。一時的なマップファイルにはGPUを使います。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_maps_saved_help": "CPU 렌더링은 저장한 불투명 표면 맵을 사용할 수 있습니다. 표면을 선택하고 샷에 맵을 저장하세요. 간접광을 사용하면 다른 바디에 유리를 사용할 수 있지만, 맵이 있는 바디에는 유리를 사용할 수 없습니다. 선택 반사, 비등방성 및 절차적 효과는 끄세요. 임시 맵 파일 경로는 GPU를 사용합니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_maps_saved_help": "Kết xuất CPU hỗ trợ bản đồ bề mặt đục đã lưu: chọn bề mặt rồi lưu bản đồ vào ảnh chụp. Với chiếu sáng gián tiếp, thân khác có thể dùng kính; thân có bản đồ không thể dùng kính. Tắt phản xạ được chọn, dị hướng và hiệu ứng thủ tục. Đường dẫn tạm tới tệp bản đồ dùng GPU."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_maps_saved_help": "CPU 渲染可使用已保存的不透明表面贴图:在此选择表面,再将贴图保存到镜头。使用间接光照时,其他实体可以使用玻璃;带有贴图的实体不能使用玻璃。请关闭选定的反射、各向异性和程序化表面效果。临时贴图文件路径使用 GPU。"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_denoise_help": "يقلل التشويش في كل تحديث للصورة. قد ينعّم التفاصيل الدقيقة وحواف عمق المجال. يبقى EXR الخام دون تغيير. تستخدم تعديلات الصورة المكتملة صورتها الخام ولا تطبّق تقليل التشويش مرة أخرى."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_denoise_help": "Verringert das Rauschen bei jeder Bildaktualisierung. Feine Details und Übergänge der Tiefenschärfe können weicher werden. Die EXR-Rohdaten bleiben unverändert. Nachträgliche Anpassungen eines fertigen Bildes verwenden dessen Rohbild und wenden die Rauschreduzierung nicht erneut an."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_denoise_help": "Reduces noise in every image update. It may soften fine detail and depth-of-field edges. Raw EXR is unchanged. Adjusting a completed image uses its raw image and does not apply noise reduction again."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_denoise_help": "מפחיתה רעש בכל עדכון תמונה. היא עלולה לרכך פרטים עדינים וקצוות של עומק שדה. קובץ EXR הגולמי אינו משתנה. התאמות של תמונה שהושלמה משתמשות בתמונה הגולמית שלה ואינן מחילות שוב הפחתת רעש."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_denoise_help": "画像が更新されるたびにノイズを減らします。細部や被写界深度によるぼけの境界が柔らかくなる場合があります。RAW EXRは変更されません。完成画像の調整にはそのRAW画像を使用し、ノイズ低減は再適用しません。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_denoise_help": "이미지가 갱신될 때마다 노이즈를 줄입니다. 미세한 디테일과 피사계 심도 경계가 부드러워질 수 있습니다. 원본 EXR은 바뀌지 않습니다. 완성된 이미지를 조정할 때는 해당 원본 이미지를 사용하며 노이즈 감소를 다시 적용하지 않습니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_denoise_help": "Giảm nhiễu mỗi khi ảnh được cập nhật. Có thể làm mềm chi tiết nhỏ và biên vùng mờ do độ sâu trường ảnh. EXR thô không thay đổi. Khi điều chỉnh ảnh đã hoàn tất, hệ thống dùng ảnh thô của ảnh đó và không áp dụng giảm nhiễu lần nữa."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_denoise_help": "每次图像更新时减少噪点,可能使细节和景深模糊边缘变柔。原始EXR保持不变。调整已完成的图像时使用其原始图像,不会再次降噪。"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_studio_enable": "تفعيل الإضاءة غير المباشرة لهذا التوزيع",
|
||||
"render.cpu_studio_enable_apply": "تفعيل الإضاءة غير المباشرة وتطبيقها",
|
||||
"render.cpu_studio_indirect_help": "يحتاج توزيع الشمس هذا إلى إضاءة غير مباشرة بعمق 8 وظلال متتبعة وعينة كاميرا واحدة لكل مرور. إذا كان التصيير التدريجي معطلاً، يبدأ بـ 16 عينة لكل بكسل وبذرة 0. تُحفظ الإعدادات التدريجية الحالية وملف الموارد. تمنع الإعدادات غير المتوافقة التطبيق دون تغييرها.",
|
||||
"render.cpu_studio_radiance_limit": "سطوع المصدر المساحي في هذا التوزيع هو {radiance}، ويتجاوز حد CPU البالغ 64. تُحفظ الإضاءة المؤلَّفة؛ اختر توزيعاً آخر أو GPU."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_studio_enable": "Indirekte Beleuchtung für dieses Lichtset aktivieren",
|
||||
"render.cpu_studio_enable_apply": "Indirekte Beleuchtung aktivieren und anwenden",
|
||||
"render.cpu_studio_indirect_help": "Dieses Sonnen-Lichtset benötigt indirekte Beleuchtung mit Tiefe 8, verfolgte Schatten und eine Kamerastichprobe pro Durchlauf. Ist progressives Rendern ausgeschaltet, beginnt es mit 16 Stichproben pro Pixel und Startwert 0. Vorhandene progressive Einstellungen und das Ressourcenprofil bleiben erhalten. Unvereinbare Einstellungen verhindern die Anwendung, ohne sie zu ändern.",
|
||||
"render.cpu_studio_radiance_limit": "Dieses Lichtset hat eine Flächenstrahldichte von {radiance}, über der CPU-Grenze von 64. Die vorgegebene Beleuchtung bleibt erhalten; wählen Sie ein anderes Lichtset oder GPU."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_studio_enable": "Enable indirect lighting for this rig",
|
||||
"render.cpu_studio_enable_apply": "Enable indirect lighting and apply",
|
||||
"render.cpu_studio_indirect_help": "This Sun rig needs indirect lighting at depth 8, traced shadows and one camera sample per pass. If progressive rendering is off, it starts at 16 samples per pixel with seed 0. Existing progressive settings and the resource profile are kept. Incompatible settings stop the apply without changing them.",
|
||||
"render.cpu_studio_radiance_limit": "This rig has area radiance {radiance}, above the CPU limit of 64. Its authored lighting is kept; choose another rig or GPU."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_studio_enable": "הפעלת תאורה עקיפה עבור מערך זה",
|
||||
"render.cpu_studio_enable_apply": "הפעלת תאורה עקיפה והחלה",
|
||||
"render.cpu_studio_indirect_help": "מערך השמש הזה דורש תאורה עקיפה בעומק 8, צללים מחושבים ודגימת מצלמה אחת בכל מעבר. אם רינדור הדרגתי כבוי, הוא מתחיל ב־16 דגימות לפיקסל עם זרע 0. הגדרות הדרגתיות קיימות ופרופיל המשאבים נשמרים. הגדרות שאינן תואמות עוצרות את ההחלה בלי לשנותן.",
|
||||
"render.cpu_studio_radiance_limit": "עוצמת מקור השטח במערך זה היא {radiance}, מעל מגבלת CPU של 64. התאורה המקורית נשמרת; בחרו מערך אחר או GPU."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_studio_enable": "このリグで間接照明を有効にする",
|
||||
"render.cpu_studio_enable_apply": "間接照明を有効にして適用",
|
||||
"render.cpu_studio_indirect_help": "この太陽リグには深度8の間接照明、追跡した影、パスごとに1つのカメラサンプルが必要です。プログレッシブ描画がオフなら、ピクセルごと16サンプル、シード0で開始します。既存のプログレッシブ設定とリソースプロファイルは維持します。非対応の設定がある場合は変更せず適用を中止します。",
|
||||
"render.cpu_studio_radiance_limit": "このリグの面光源の放射輝度は{radiance}で、CPU上限64を超えます。作成された照明は維持します。別のリグかGPUを選んでください。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_studio_enable": "이 리그의 간접 조명 켜기",
|
||||
"render.cpu_studio_enable_apply": "간접 조명을 켜고 적용",
|
||||
"render.cpu_studio_indirect_help": "이 태양 리그에는 깊이 8의 간접 조명, 추적된 그림자, 패스당 카메라 샘플 1개가 필요합니다. 점진적 렌더링이 꺼져 있으면 픽셀당 샘플 16개와 시드 0으로 시작합니다. 기존 점진적 설정과 리소스 프로필은 유지됩니다. 호환되지 않는 설정은 변경하지 않고 적용을 중단합니다.",
|
||||
"render.cpu_studio_radiance_limit": "이 리그의 면 광원 방사 휘도 {radiance}은(는) CPU 한도 64를 넘습니다. 작성된 조명은 유지됩니다. 다른 리그나 GPU를 선택하세요."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_studio_enable": "Bật chiếu sáng gián tiếp cho bộ đèn này",
|
||||
"render.cpu_studio_enable_apply": "Bật chiếu sáng gián tiếp và áp dụng",
|
||||
"render.cpu_studio_indirect_help": "Bộ đèn Mặt Trời này cần chiếu sáng gián tiếp với độ sâu 8, bóng được truy vết và một mẫu máy ảnh mỗi lượt. Nếu kết xuất tăng dần đang tắt, bắt đầu với 16 mẫu mỗi điểm ảnh và hạt giống 0. Giữ nguyên thiết lập tăng dần và hồ sơ tài nguyên hiện có. Thiết lập không tương thích sẽ ngăn áp dụng mà không thay đổi chúng.",
|
||||
"render.cpu_studio_radiance_limit": "Độ chói bức xạ đèn diện tích của bộ này là {radiance}, vượt giới hạn CPU 64. Giữ nguyên ánh sáng đã tạo; hãy chọn bộ đèn khác hoặc GPU."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_studio_enable": "为此灯组启用间接光照",
|
||||
"render.cpu_studio_enable_apply": "启用间接光照并应用",
|
||||
"render.cpu_studio_indirect_help": "此太阳灯组需要深度为8的间接光照、追踪阴影,以及每轮一个相机采样。如果渐进渲染关闭,则以每像素16个采样和种子0开始。保留现有渐进设置和资源配置。不兼容的设置会阻止应用,且不会被更改。",
|
||||
"render.cpu_studio_radiance_limit": "此灯组的面光源辐射亮度为{radiance},超过CPU上限64。保留原有灯光,请选择其他灯组或GPU。"
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_celestial_adopt": "تتبع الأشعة على المعالج مع إضاءة غير مباشرة · عمق المسار {depth} · {spp} عينة لكل بكسل · ظلال متتبعة · بذرة {seed} · عينة أولية واحدة",
|
||||
"render.cpu_celestial_adopt_label": "تتبع الأشعة على المعالج مع إضاءة غير مباشرة",
|
||||
"render.cpu_celestial_path_required": "تستخدم الشمس المحدودة والسماء المتدرجة الإضاءة غير المباشرة على المعالج.",
|
||||
"render.cpu_celestial_switch": "اختر تتبع الأشعة على المعالج مع الإضاءة غير المباشرة للاحتفاظ بهذه الشمس أو السماء.",
|
||||
"render.cpu_celestial_unportable": "أبقِ تتبع المسارات على المعالج الرسومي لإعدادات عينات النفاذ الحالية.",
|
||||
"render.cpu_sky_bounds": "يجب أن يكون دوران السماء محدودًا ضمن ±1,000,000 راديان، وأن تكون الشدة بين 0 و100."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_celestial_adopt": "CPU-Raytracing mit indirekter Beleuchtung · Pfadtiefe {depth} · {spp} SPP · verfolgte Schatten · Seed {seed} · 1 Primärsample",
|
||||
"render.cpu_celestial_adopt_label": "CPU-Raytracing mit indirekter Beleuchtung",
|
||||
"render.cpu_celestial_path_required": "Endliche Sonne und Verlaufshimmel verwenden indirekte CPU-Beleuchtung.",
|
||||
"render.cpu_celestial_switch": "Wähle CPU-Raytracing mit indirekter Beleuchtung, um diese Sonne oder diesen Himmel beizubehalten.",
|
||||
"render.cpu_celestial_unportable": "Behalte GPU-Pathtracing für die aktuellen Transmissions-Sample-Einstellungen bei.",
|
||||
"render.cpu_sky_bounds": "Die Himmelsrotation muss endlich innerhalb ±1.000.000 Radiant liegen; die Intensität muss 0–100 sein."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_celestial_adopt": "CPU ray tracing with indirect lighting · path depth {depth} · {spp} SPP · traced shadows · seed {seed} · 1 primary sample",
|
||||
"render.cpu_celestial_adopt_label": "CPU ray tracing with indirect lighting",
|
||||
"render.cpu_celestial_path_required": "Finite Sun and gradient Sky use CPU indirect lighting.",
|
||||
"render.cpu_celestial_switch": "Choose CPU ray tracing with indirect lighting to keep this Sun or Sky.",
|
||||
"render.cpu_celestial_unportable": "Keep GPU path tracing for the current transmission sample settings.",
|
||||
"render.cpu_sky_bounds": "Sky rotation must be finite within ±1,000,000 radians; intensity must be 0–100."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_celestial_adopt": "מעקב קרניים במעבד עם תאורה עקיפה · עומק מסלול {depth} · {spp} דגימות לפיקסל · צללים במעקב · זרע {seed} · דגימה ראשית אחת",
|
||||
"render.cpu_celestial_adopt_label": "מעקב קרניים במעבד עם תאורה עקיפה",
|
||||
"render.cpu_celestial_path_required": "שמש בעלת גודל סופי ושמיים מדורגים משתמשים בתאורה עקיפה במעבד.",
|
||||
"render.cpu_celestial_switch": "כדי לשמור על השמש או השמיים האלה, יש לבחור מעקב קרניים במעבד עם תאורה עקיפה.",
|
||||
"render.cpu_celestial_unportable": "יש לשמור על מעקב מסלולים במעבד הגרפי עבור הגדרות דגימות ההעברה הנוכחיות.",
|
||||
"render.cpu_sky_bounds": "סיבוב השמיים חייב להיות סופי בטווח ±1,000,000 רדיאנים, והעוצמה חייבת להיות בטווח 0–100."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_celestial_adopt": "間接照明付き CPU レイトレーシング · パス深度 {depth} · {spp} SPP · 追跡シャドウ · シード {seed} · プライマリサンプル1個",
|
||||
"render.cpu_celestial_adopt_label": "間接照明付き CPU レイトレーシング",
|
||||
"render.cpu_celestial_path_required": "有限の太陽とグラデーション空は CPU の間接照明を使用します。",
|
||||
"render.cpu_celestial_switch": "この太陽や空を維持するには、間接照明を使用する CPU レイトレーシングを選択してください。",
|
||||
"render.cpu_celestial_unportable": "現在の透過サンプル設定には GPU パストレーシングを使用してください。",
|
||||
"render.cpu_sky_bounds": "空の回転は ±1,000,000 ラジアン以内の有限値、強度は 0~100 にしてください。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_celestial_adopt": "간접 조명을 사용하는 CPU 레이 트레이싱 · 경로 깊이 {depth} · {spp} SPP · 추적 그림자 · 시드 {seed} · 기본 샘플1개",
|
||||
"render.cpu_celestial_adopt_label": "간접 조명을 사용하는 CPU 레이 트레이싱",
|
||||
"render.cpu_celestial_path_required": "유한 태양과 그라데이션 하늘은 CPU 간접 조명을 사용합니다.",
|
||||
"render.cpu_celestial_switch": "이 태양 또는 하늘을 유지하려면 간접 조명을 사용하는 CPU 레이 트레이싱을 선택하세요.",
|
||||
"render.cpu_celestial_unportable": "현재 투과 샘플 설정에는 GPU 경로 추적을 유지하세요.",
|
||||
"render.cpu_sky_bounds": "하늘 회전은 ±1,000,000 라디안 이내의 유한 값이어야 하며 강도는 0~100이어야 합니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_celestial_adopt": "Dò tia CPU với chiếu sáng gián tiếp · độ sâu đường đi {depth} · {spp} SPP · bóng đổ dò tia · seed {seed} · 1 mẫu chính",
|
||||
"render.cpu_celestial_adopt_label": "Dò tia CPU với chiếu sáng gián tiếp",
|
||||
"render.cpu_celestial_path_required": "Mặt Trời hữu hạn và bầu trời chuyển sắc dùng chiếu sáng gián tiếp CPU.",
|
||||
"render.cpu_celestial_switch": "Chọn dò tia CPU với chiếu sáng gián tiếp để giữ Mặt Trời hoặc bầu trời này.",
|
||||
"render.cpu_celestial_unportable": "Giữ dò đường đi GPU cho cài đặt mẫu truyền sáng hiện tại.",
|
||||
"render.cpu_sky_bounds": "Góc xoay bầu trời phải hữu hạn trong ±1.000.000 radian; cường độ phải từ 0–100."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_celestial_adopt": "CPU 光线追踪与间接照明 · 路径深度 {depth} · {spp} SPP · 追踪阴影 · 种子 {seed} · 1 个主样本",
|
||||
"render.cpu_celestial_adopt_label": "CPU 光线追踪与间接照明",
|
||||
"render.cpu_celestial_path_required": "有限太阳和渐变天空使用 CPU 间接照明。",
|
||||
"render.cpu_celestial_switch": "选择带间接照明的 CPU 光线追踪,以保留此太阳或天空。",
|
||||
"render.cpu_celestial_unportable": "当前透射采样设置请继续使用 GPU 路径追踪。",
|
||||
"render.cpu_sky_bounds": "天空旋转必须为 ±1,000,000 弧度内的有限值;强度必须为 0–100。"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.shot_lens_cpu_progressive_reason": "فعّل أخذ العينات التدريجي في قسم «أخذ العينات» قبل تمكين عمق المجال على CPU.",
|
||||
"render.shot_lens_raytrace_reason": "يتوفر عمق المجال مع تتبع الأشعة على CPU وتتبع المسارات على GPU."
|
||||
},
|
||||
"de": {
|
||||
"render.shot_lens_cpu_progressive_reason": "Vor der CPU-Schärfentiefe unter Sampling die progressive Abtastung übernehmen.",
|
||||
"render.shot_lens_raytrace_reason": "Schärfentiefe ist für CPU-Raytracing und GPU-Pathtracing verfügbar."
|
||||
},
|
||||
"en": {
|
||||
"render.shot_lens_cpu_progressive_reason": "Adopt progressive sampling in Sampling before enabling CPU depth of field.",
|
||||
"render.shot_lens_raytrace_reason": "Depth of field is available for CPU ray tracing and GPU path tracing."
|
||||
},
|
||||
"he": {
|
||||
"render.shot_lens_cpu_progressive_reason": "יש להפעיל דגימה הדרגתית בחלונית הדגימה לפני הפעלת עומק שדה ב־CPU.",
|
||||
"render.shot_lens_raytrace_reason": "עומק שדה זמין בעקיבת קרניים ב־CPU ובעקיבת נתיבים ב־GPU."
|
||||
},
|
||||
"ja": {
|
||||
"render.shot_lens_cpu_progressive_reason": "CPUの被写界深度を有効にするには、サンプリングでプログレッシブ方式を適用してください。",
|
||||
"render.shot_lens_raytrace_reason": "被写界深度はCPUレイトレーシングとGPUパストレーシングで利用できます。"
|
||||
},
|
||||
"ko": {
|
||||
"render.shot_lens_cpu_progressive_reason": "CPU 피사계 심도를 켜기 전에 샘플링에서 점진적 샘플링을 적용하세요.",
|
||||
"render.shot_lens_raytrace_reason": "피사계 심도는 CPU 레이 트레이싱과 GPU 패스 트레이싱에서 사용할 수 있습니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.shot_lens_cpu_progressive_reason": "Bật lấy mẫu lũy tiến trong mục Lấy mẫu trước khi bật độ sâu trường ảnh trên CPU.",
|
||||
"render.shot_lens_raytrace_reason": "Độ sâu trường ảnh khả dụng với dò tia trên CPU và dò đường đi trên GPU."
|
||||
},
|
||||
"zh": {
|
||||
"render.shot_lens_cpu_progressive_reason": "启用CPU景深前,请在采样中应用渐进式采样。",
|
||||
"render.shot_lens_raytrace_reason": "CPU光线追踪和GPU路径追踪均支持景深。"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_transparent_help": "يزيل PNG الشفاف الخلفية ويحافظ على تغطية الحواف. يجب أن تبقى عتامة الأجسام والأوجه عند 100%."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_transparent_help": "Transparentes PNG entfernt den Hintergrund und erhält die Kantenabdeckung. Körper- und Flächendeckkraft müssen 100 % bleiben."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_transparent_help": "Transparent PNG removes the background and keeps edge coverage. Body and face opacity must stay at 100%."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_transparent_help": "PNG שקוף מסיר את הרקע ושומר על כיסוי הקצוות. אטימות הגופים והפאות חייבת להישאר 100%."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_transparent_help": "透明PNGは背景を除き、輪郭の被覆率を保持します。ボディと面の不透明度は100%にしてください。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_transparent_help": "투명 PNG는 배경을 제거하고 가장자리 커버리지를 유지합니다. 바디와 면의 불투명도는 100%여야 합니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_transparent_help": "PNG trong suốt loại bỏ nền và giữ độ phủ ở biên. Độ đục của thân và mặt phải giữ ở 100%."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_transparent_help": "透明PNG移除背景并保留边缘覆盖率。实体和面的不透明度必须保持100%。"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.directional_sun_limit": "تدعم إضاءة GPU ثمانية مصادر ضوء اتجاهية إجمالاً، من بينها شمس واحدة بظلال ناعمة كحد أقصى. انسخ ضوءاً اتجاهياً بظلال حادة لإضافة مصدر بعيد آخر."
|
||||
},
|
||||
"de": {
|
||||
"render.directional_sun_limit": "Die GPU unterstützt insgesamt acht gerichtete Lichtquellen, darunter höchstens eine weiche Sonne. Dupliziere ein hartes gerichtetes Licht für eine weitere entfernte Quelle."
|
||||
},
|
||||
"en": {
|
||||
"render.directional_sun_limit": "GPU lighting supports eight directional sources in total, including at most one soft Sun. Duplicate a hard directional light to add another distant source."
|
||||
},
|
||||
"he": {
|
||||
"render.directional_sun_limit": "תאורת GPU תומכת בשמונה מקורות אור כיווניים בסך הכול, ובהם לכל היותר שמש אחת בעלת צללים רכים. שכפלו אור כיווני בעל צללים חדים כדי להוסיף מקור מרוחק נוסף."
|
||||
},
|
||||
"ja": {
|
||||
"render.directional_sun_limit": "GPUでは指向性光源を合計8個まで使用でき、柔らかい影の太陽は1個までです。遠方の光源を追加するには、硬い影の指向性ライトを複製してください。"
|
||||
},
|
||||
"ko": {
|
||||
"render.directional_sun_limit": "GPU는 부드러운 태양 하나를 포함해 방향성 광원을 총 8개까지 지원합니다. 원거리 광원을 추가하려면 선명한 그림자의 방향성 조명을 복제하세요."
|
||||
},
|
||||
"vi": {
|
||||
"render.directional_sun_limit": "GPU hỗ trợ tổng cộng tám nguồn sáng định hướng, trong đó có tối đa một Mặt trời với bóng mềm. Nhân đôi đèn định hướng có bóng sắc để thêm nguồn sáng ở xa."
|
||||
},
|
||||
"zh": {
|
||||
"render.directional_sun_limit": "GPU 总共支持八个平行光源,其中最多一个为柔和太阳光。复制硬阴影平行光可添加另一个远处光源。"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.gpu_path_not_started": "لم يبدأ إعداد متتبع المسارات على المعالج الرسومي بعد."
|
||||
},
|
||||
"de": {
|
||||
"render.gpu_path_not_started": "Die Einrichtung des GPU-Pfadtracers wurde noch nicht gestartet."
|
||||
},
|
||||
"en": {
|
||||
"render.gpu_path_not_started": "GPU path tracer setup has not started."
|
||||
},
|
||||
"he": {
|
||||
"render.gpu_path_not_started": "הגדרת מעקב המסלולים ב-GPU טרם התחילה."
|
||||
},
|
||||
"ja": {
|
||||
"render.gpu_path_not_started": "GPUパストレーサーのセットアップはまだ開始されていません。"
|
||||
},
|
||||
"ko": {
|
||||
"render.gpu_path_not_started": "GPU 경로 추적기 설정이 아직 시작되지 않았습니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.gpu_path_not_started": "Quá trình thiết lập bộ dò đường đi GPU chưa bắt đầu."
|
||||
},
|
||||
"zh": {
|
||||
"render.gpu_path_not_started": "GPU路径追踪器的设置尚未开始。"
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.spot_axis": "اتجاه انبعاث الضوء",
|
||||
"render.spot_gpu_help": "تُقاس الزوايا من اتجاه انبعاث الضوء. يتلاشى الضوء بين الزاويتين الداخلية والخارجية. تتطلب نطاقات التلاشي الضيقة جداً التصيير باستخدام CPU.",
|
||||
"render.spot_help": "تُقاس الزوايا من اتجاه انبعاث الضوء. يتلاشى الضوء بين الزاويتين الداخلية والخارجية.",
|
||||
"render.spot_inner": "نصف الزاوية الداخلية",
|
||||
"render.spot_outer": "نصف الزاوية الخارجية"
|
||||
},
|
||||
"de": {
|
||||
"render.spot_axis": "Abstrahlrichtung",
|
||||
"render.spot_gpu_help": "Die Winkel werden von der Abstrahlrichtung aus gemessen. Zwischen innerem und äußerem Winkel wird das Licht schwächer. Sehr schmale Übergänge erfordern CPU-Rendering.",
|
||||
"render.spot_help": "Die Winkel werden von der Abstrahlrichtung aus gemessen. Zwischen innerem und äußerem Winkel wird das Licht schwächer.",
|
||||
"render.spot_inner": "Innerer Halbwinkel",
|
||||
"render.spot_outer": "Äußerer Halbwinkel"
|
||||
},
|
||||
"en": {
|
||||
"render.spot_axis": "Emission direction",
|
||||
"render.spot_gpu_help": "Angles are measured from the emission direction. Light fades between the inner and outer angles. Very narrow fade ranges require CPU rendering.",
|
||||
"render.spot_help": "Angles are measured from the emission direction. Light fades between the inner and outer angles.",
|
||||
"render.spot_inner": "Inner half-angle",
|
||||
"render.spot_outer": "Outer half-angle"
|
||||
},
|
||||
"he": {
|
||||
"render.spot_axis": "כיוון פליטת האור",
|
||||
"render.spot_gpu_help": "הזוויות נמדדות מכיוון פליטת האור. האור דועך בין הזווית הפנימית לחיצונית. טווחי דעיכה צרים מאוד דורשים רינדור באמצעות CPU.",
|
||||
"render.spot_help": "הזוויות נמדדות מכיוון פליטת האור. האור דועך בין הזווית הפנימית לחיצונית.",
|
||||
"render.spot_inner": "חצי זווית פנימית",
|
||||
"render.spot_outer": "חצי זווית חיצונית"
|
||||
},
|
||||
"ja": {
|
||||
"render.spot_axis": "光の照射方向",
|
||||
"render.spot_gpu_help": "角度は光の照射方向から測ります。内側の角度から外側の角度にかけて光が弱まります。減衰範囲が非常に狭い場合はCPUレンダリングが必要です。",
|
||||
"render.spot_help": "角度は光の照射方向から測ります。内側の角度から外側の角度にかけて光が弱まります。",
|
||||
"render.spot_inner": "内側の半角",
|
||||
"render.spot_outer": "外側の半角"
|
||||
},
|
||||
"ko": {
|
||||
"render.spot_axis": "빛의 방출 방향",
|
||||
"render.spot_gpu_help": "각도는 빛의 방출 방향을 기준으로 측정합니다. 안쪽 각도와 바깥쪽 각도 사이에서 빛이 약해집니다. 감쇠 범위가 매우 좁으면 CPU 렌더링이 필요합니다.",
|
||||
"render.spot_help": "각도는 빛의 방출 방향을 기준으로 측정합니다. 안쪽 각도와 바깥쪽 각도 사이에서 빛이 약해집니다.",
|
||||
"render.spot_inner": "안쪽 반각",
|
||||
"render.spot_outer": "바깥쪽 반각"
|
||||
},
|
||||
"vi": {
|
||||
"render.spot_axis": "Hướng phát sáng",
|
||||
"render.spot_gpu_help": "Các góc được đo từ hướng phát sáng. Ánh sáng yếu dần giữa góc trong và góc ngoài. Dải chuyển tiếp rất hẹp cần kết xuất bằng CPU.",
|
||||
"render.spot_help": "Các góc được đo từ hướng phát sáng. Ánh sáng yếu dần giữa góc trong và góc ngoài.",
|
||||
"render.spot_inner": "Nửa góc trong",
|
||||
"render.spot_outer": "Nửa góc ngoài"
|
||||
},
|
||||
"zh": {
|
||||
"render.spot_axis": "光线发射方向",
|
||||
"render.spot_gpu_help": "角度以光线发射方向为基准测量。光线在内角和外角之间逐渐减弱。极窄的衰减范围需要使用CPU渲染。",
|
||||
"render.spot_help": "角度以光线发射方向为基准测量。光线在内角和外角之间逐渐减弱。",
|
||||
"render.spot_inner": "内半角",
|
||||
"render.spot_outer": "外半角"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"ar": {
|
||||
"settings.graphics_api_env": "يحدده متغير البيئة WGPU_BACKEND ({value})",
|
||||
"settings.graphics_api_freezes": "على بطاقات AMD الرسومية المدمجة، قد تؤدي {api} إلى تجمد الشاشة، وأحيانًا حتى إعادة تشغيل الكمبيوتر. يستخدم الخيار التلقائي Vulkan هنا.",
|
||||
"settings.graphics_applies_on_restart": "تُطبّق هذه الإعدادات عند تشغيل SolidCraft مجددًا. سيسألك حينها إن كنت تريد الاحتفاظ بها، ويعود إلى الإعدادات الحالية إن لم توافق.",
|
||||
"settings.graphics_msaa": "تنعيم الحواف بمقدار {samples}×",
|
||||
"settings.graphics_restart_now": "إعادة التشغيل الآن",
|
||||
"settings.graphics_trial_body": "بدأ SolidCraft باستخدام {settings}. دون إجابة، سيعود خلال {seconds} ثانية إلى {previous}.",
|
||||
"settings.graphics_trial_first": "احتفظ بإعدادات الرسومات قيد التجربة أو ارجع عنها أولًا",
|
||||
"settings.graphics_trial_keep": "احتفاظ",
|
||||
"settings.graphics_trial_revert": "رجوع",
|
||||
"settings.graphics_trial_title": "هل تريد الاحتفاظ بإعدادات الرسومات هذه؟",
|
||||
"ui.graphics_api": "واجهة برمجة الرسومات",
|
||||
"ui.graphics_api_auto": "تلقائي"
|
||||
},
|
||||
"en": {
|
||||
"settings.graphics_api_env": "Set by the WGPU_BACKEND environment variable ({value})",
|
||||
"settings.graphics_api_freezes": "On AMD integrated graphics, {api} can freeze the screen, sometimes until the computer is restarted. Automatic uses Vulkan here.",
|
||||
"settings.graphics_applies_on_restart": "These apply when SolidCraft starts again. It then asks whether to keep them, and goes back to the current ones if you don't.",
|
||||
"settings.graphics_msaa": "{samples}× anti-aliasing",
|
||||
"settings.graphics_restart_now": "Restart now",
|
||||
"settings.graphics_trial_body": "SolidCraft started with {settings}. Without an answer, in {seconds} s it goes back to {previous}.",
|
||||
"settings.graphics_trial_first": "Keep or go back from the graphics settings being tried first",
|
||||
"settings.graphics_trial_keep": "Keep",
|
||||
"settings.graphics_trial_revert": "Go back",
|
||||
"settings.graphics_trial_title": "Keep these graphics settings?",
|
||||
"ui.graphics_api": "Graphics API",
|
||||
"ui.graphics_api_auto": "Automatic"
|
||||
},
|
||||
"he": {
|
||||
"settings.graphics_api_env": "נקבע על ידי משתנה הסביבה WGPU_BACKEND ({value})",
|
||||
"settings.graphics_api_freezes": "בגרפיקה המשולבת של AMD, {api} עלול להקפיא את המסך, לפעמים עד להפעלה מחדש של המחשב. האפשרות האוטומטית משתמשת כאן ב־Vulkan.",
|
||||
"settings.graphics_applies_on_restart": "הגדרות אלה יחולו בהפעלה הבאה של SolidCraft. אז תופיע שאלה אם לשמור אותן, ואם לא, ההגדרות הנוכחיות ישוחזרו.",
|
||||
"settings.graphics_msaa": "החלקת קצוות פי {samples}",
|
||||
"settings.graphics_restart_now": "הפעלה מחדש כעת",
|
||||
"settings.graphics_trial_body": "SolidCraft הופעל עם {settings}. ללא תשובה, בעוד {seconds} שניות הוא יחזור אל {previous}.",
|
||||
"settings.graphics_trial_first": "יש לשמור את הגדרות הגרפיקה שבניסיון או לחזור מהן תחילה",
|
||||
"settings.graphics_trial_keep": "שמירה",
|
||||
"settings.graphics_trial_revert": "חזרה",
|
||||
"settings.graphics_trial_title": "לשמור את הגדרות הגרפיקה האלה?",
|
||||
"ui.graphics_api": "ממשק הגרפיקה",
|
||||
"ui.graphics_api_auto": "אוטומטי"
|
||||
},
|
||||
"ja": {
|
||||
"settings.graphics_api_env": "環境変数 WGPU_BACKEND で指定されています ({value})",
|
||||
"settings.graphics_api_freezes": "AMD の内蔵グラフィックスでは、{api} で画面が固まり、コンピューターを再起動するまで戻らないことがあります。自動ではここで Vulkan を使います。",
|
||||
"settings.graphics_applies_on_restart": "これらは SolidCraft の次回起動時に適用されます。起動後に設定を維持するか確認し、維持しない場合は現在の設定に戻ります。",
|
||||
"settings.graphics_msaa": "アンチエイリアス {samples}×",
|
||||
"settings.graphics_restart_now": "今すぐ再起動",
|
||||
"settings.graphics_trial_body": "SolidCraft は {settings} で起動しました。応答がない場合、{seconds} 秒後に {previous} に戻ります。",
|
||||
"settings.graphics_trial_first": "先に、試用中のグラフィックス設定を維持するか戻すかを選んでください",
|
||||
"settings.graphics_trial_keep": "維持",
|
||||
"settings.graphics_trial_revert": "元に戻す",
|
||||
"settings.graphics_trial_title": "このグラフィックス設定を維持しますか?",
|
||||
"ui.graphics_api": "グラフィックス API",
|
||||
"ui.graphics_api_auto": "自動"
|
||||
},
|
||||
"vi": {
|
||||
"settings.graphics_api_env": "Được đặt bởi biến môi trường WGPU_BACKEND ({value})",
|
||||
"settings.graphics_api_freezes": "Trên đồ họa tích hợp AMD, {api} có thể làm màn hình bị treo, đôi khi cho đến khi khởi động lại máy tính. Tự động sử dụng Vulkan ở đây.",
|
||||
"settings.graphics_applies_on_restart": "Các thiết lập này có hiệu lực khi SolidCraft khởi động lại. Khi đó ứng dụng sẽ hỏi có giữ chúng không và trở về thiết lập hiện tại nếu bạn không giữ.",
|
||||
"settings.graphics_msaa": "Khử răng cưa {samples}×",
|
||||
"settings.graphics_restart_now": "Khởi động lại ngay",
|
||||
"settings.graphics_trial_body": "SolidCraft đã khởi động với {settings}. Nếu không trả lời, sau {seconds} giây ứng dụng sẽ trở về {previous}.",
|
||||
"settings.graphics_trial_first": "Trước tiên hãy giữ hoặc quay lại từ các thiết lập đồ họa đang thử",
|
||||
"settings.graphics_trial_keep": "Giữ",
|
||||
"settings.graphics_trial_revert": "Quay lại",
|
||||
"settings.graphics_trial_title": "Giữ các thiết lập đồ họa này?",
|
||||
"ui.graphics_api": "API đồ họa",
|
||||
"ui.graphics_api_auto": "Tự động"
|
||||
},
|
||||
"zh": {
|
||||
"settings.graphics_api_env": "由环境变量 WGPU_BACKEND 设置({value})",
|
||||
"settings.graphics_api_freezes": "在 AMD 集成显卡上,{api} 可能导致屏幕冻结,有时需要重启电脑才能恢复。自动选项在这里使用 Vulkan。",
|
||||
"settings.graphics_applies_on_restart": "这些设置在 SolidCraft 下次启动时生效。启动后会询问是否保留;如果不保留,将恢复为当前设置。",
|
||||
"settings.graphics_msaa": "{samples}× 抗锯齿",
|
||||
"settings.graphics_restart_now": "立即重启",
|
||||
"settings.graphics_trial_body": "SolidCraft 已使用 {settings} 启动。如果没有回应,将在 {seconds} 秒后恢复为 {previous}。",
|
||||
"settings.graphics_trial_first": "请先决定保留还是撤销正在试用的图形设置",
|
||||
"settings.graphics_trial_keep": "保留",
|
||||
"settings.graphics_trial_revert": "恢复",
|
||||
"settings.graphics_trial_title": "保留这些图形设置吗?",
|
||||
"ui.graphics_api": "图形 API",
|
||||
"ui.graphics_api_auto": "自动"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.adaptive.pilot_progress": "تمريرات تمهيدية تكيفية · {current}/{target}",
|
||||
"render.adaptive.preview_help": "توجّه الصور التمهيدية توزيع العينات ولا تدخل في الصورة النهائية. تستخدم تكرارات التصيير ميزانية منفصلة لكل بكسل؛ ولا يمثّل عدد التكرارات عددًا موحدًا من العينات لكل بكسل.",
|
||||
"render.adaptive.production_progress": "التصيير التكيفي · {current}/{target} تكرارات",
|
||||
"render.live.adaptive": "مباشر · {width} × {height} · {progress}"
|
||||
},
|
||||
"de": {
|
||||
"render.adaptive.pilot_progress": "Adaptive Pilotphase · {current}/{target} Durchgänge",
|
||||
"render.adaptive.preview_help": "Pilotbilder bestimmen die Verteilung und sind vom Endbild ausgeschlossen. Produktionsiterationen nutzen ein eigenes Budget pro Pixel; die Iterationszahl ist keine einheitliche Samplezahl pro Pixel.",
|
||||
"render.adaptive.production_progress": "Adaptive Produktion · {current}/{target} Iterationen",
|
||||
"render.live.adaptive": "Live · {width} × {height} · {progress}"
|
||||
},
|
||||
"en": {
|
||||
"render.adaptive.pilot_progress": "Adaptive pilots · {current}/{target} passes",
|
||||
"render.adaptive.preview_help": "Pilot images guide allocation and are excluded from the final image. Production iterations use a separate budget for each pixel; the iteration count is not uniform samples per pixel.",
|
||||
"render.adaptive.production_progress": "Adaptive production · {current}/{target} iterations",
|
||||
"render.live.adaptive": "Live · {width} × {height} · {progress}"
|
||||
},
|
||||
"he": {
|
||||
"render.adaptive.pilot_progress": "מעברים מקדימים אדפטיביים · {current}/{target}",
|
||||
"render.adaptive.preview_help": "התמונות המקדימות מנחות את הקצאת הדגימות ואינן נכללות בתמונה הסופית. איטרציות ההדמיה משתמשות בתקציב נפרד לכל פיקסל; מספר האיטרציות אינו מספר דגימות אחיד לכל פיקסל.",
|
||||
"render.adaptive.production_progress": "הדמיה אדפטיבית · {current}/{target} איטרציות",
|
||||
"render.live.adaptive": "חי · {width} × {height} · {progress}"
|
||||
},
|
||||
"ja": {
|
||||
"render.adaptive.pilot_progress": "適応パイロット · {current}/{target} パス",
|
||||
"render.adaptive.preview_help": "パイロット画像は配分の決定に使われ、最終画像には含まれません。本処理はピクセルごとに別の予算を使うため、反復回数は全ピクセル共通のサンプル数ではありません。",
|
||||
"render.adaptive.production_progress": "適応本処理 · {current}/{target} 回",
|
||||
"render.live.adaptive": "ライブ · {width} × {height} · {progress}"
|
||||
},
|
||||
"ko": {
|
||||
"render.adaptive.pilot_progress": "적응형 파일럿 · {current}/{target} 패스",
|
||||
"render.adaptive.preview_help": "파일럿 이미지는 예산 배분에 사용되며 최종 이미지에서 제외됩니다. 본 처리는 픽셀마다 별도의 예산을 사용하므로 반복 횟수는 모든 픽셀의 동일한 샘플 수를 뜻하지 않습니다.",
|
||||
"render.adaptive.production_progress": "적응형 본 처리 · {current}/{target} 회",
|
||||
"render.live.adaptive": "라이브 · {width} × {height} · {progress}"
|
||||
},
|
||||
"vi": {
|
||||
"render.adaptive.pilot_progress": "Các lượt thăm dò thích ứng · {current}/{target}",
|
||||
"render.adaptive.preview_help": "Ảnh thăm dò định hướng việc phân bổ mẫu và không được đưa vào ảnh cuối cùng. Các vòng kết xuất sử dụng ngân sách riêng cho từng pixel; số vòng không phải số mẫu đồng đều trên mỗi pixel.",
|
||||
"render.adaptive.production_progress": "Kết xuất thích ứng · {current}/{target} vòng lặp",
|
||||
"render.live.adaptive": "Trực tiếp · {width} × {height} · {progress}"
|
||||
},
|
||||
"zh": {
|
||||
"render.adaptive.pilot_progress": "自适应预采样 · {current}/{target} 轮",
|
||||
"render.adaptive.preview_help": "预采样图像用于分配预算,不计入最终图像。正式采样为每个像素使用独立预算;迭代次数不表示所有像素具有相同的采样数。",
|
||||
"render.adaptive.production_progress": "自适应正式采样 · {current}/{target} 轮迭代",
|
||||
"render.live.adaptive": "实时 · {width} × {height} · {progress}"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.batch_current_settings_help": "يستخدم إعدادات الصورة الحالية مع كاميرا كل لقطة محفوظة، لقطة واحدة في كل مرة."
|
||||
},
|
||||
"de": {
|
||||
"render.batch_current_settings_help": "Verwendet die aktuellen Bildeinstellungen mit jeder gespeicherten Aufnahmekamera, eine Aufnahme nach der anderen."
|
||||
},
|
||||
"en": {
|
||||
"render.batch_current_settings_help": "Uses the current image settings with each saved shot camera, one shot at a time."
|
||||
},
|
||||
"he": {
|
||||
"render.batch_current_settings_help": "משתמש בהגדרות התמונה הנוכחיות עם המצלמה של כל שוט שמור, שוט אחד בכל פעם."
|
||||
},
|
||||
"ja": {
|
||||
"render.batch_current_settings_help": "現在の画像設定と各保存済みショットのカメラで、1ショットずつレンダリングします。"
|
||||
},
|
||||
"ko": {
|
||||
"render.batch_current_settings_help": "현재 이미지 설정과 저장된 각 샷의 카메라를 사용해 한 번에 한 샷씩 렌더링합니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.batch_current_settings_help": "Dùng thiết lập ảnh hiện tại với máy ảnh của từng góc chụp đã lưu, lần lượt từng góc chụp."
|
||||
},
|
||||
"zh": {
|
||||
"render.batch_current_settings_help": "使用当前图像设置和各个已保存镜头的相机,逐个渲染镜头。"
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"cmd.render.coat.bind": "حفظ الطلاء الشفاف",
|
||||
"cmd.render.coat.unbind": "إزالة الطلاء الشفاف",
|
||||
"render.coat_discard": "تجاهل تعديل الطلاء",
|
||||
"render.coat_pending_edit": "لم يُحفظ تعديل الطلاء هذا. احفظه مع اللقطة أو تجاهل التعديل.",
|
||||
"render.coat_remove": "إزالة الطلاء المحفوظ",
|
||||
"render.coat_remove_note": "يزيل طلاء هذا الهدف من اللقطة. تبقى الهندسة والمظاهر والمواد الفيزيائية دون تغيير.",
|
||||
"render.coat_remove_title": "إزالة الطلاء الشفاف المحفوظ؟",
|
||||
"render.coat_revalidate_saved": "تغيّر الطلاء المحفوظ. تجاهل هذا التعديل وأعد فتح الهدف قبل تعديله مجددًا.",
|
||||
"render.coat_save": "حفظ الطلاء مع اللقطة",
|
||||
"render.coat_saved_help": "يضيف الطلاء الشفاف طبقة لامعة إلى الأسطح المعتمة. احفظه مع اللقطة. لا يمكن استخدام الزجاج على الهدف نفسه، ولا تجاوزات الانعكاس المخصص والتباين الاتجاهي على CPU."
|
||||
},
|
||||
"de": {
|
||||
"cmd.render.coat.bind": "Klarlack speichern",
|
||||
"cmd.render.coat.unbind": "Klarlack entfernen",
|
||||
"render.coat_discard": "Klarlackänderung verwerfen",
|
||||
"render.coat_pending_edit": "Diese Klarlackänderung ist ungespeichert. Mit der Aufnahme speichern oder verwerfen.",
|
||||
"render.coat_remove": "Gespeicherten Klarlack entfernen",
|
||||
"render.coat_remove_note": "Entfernt den Klarlack dieses Ziels aus der Aufnahme. Geometrie, Aussehen und physikalische Materialien bleiben unverändert.",
|
||||
"render.coat_remove_title": "Gespeicherten Klarlack entfernen?",
|
||||
"render.coat_revalidate_saved": "Der gespeicherte Klarlack wurde geändert. Diese Änderung verwerfen und das Ziel vor der nächsten Bearbeitung erneut öffnen.",
|
||||
"render.coat_save": "Klarlack in Aufnahme speichern",
|
||||
"render.coat_saved_help": "Klarlack fügt undurchsichtigen Flächen eine glänzende Schicht hinzu. Mit der Aufnahme speichern. Glas kann nicht dasselbe Ziel verwenden. Auf der CPU dürfen auch benutzerdefinierte Reflexions- und Anisotropieüberschreibungen dieses Ziel nicht verwenden."
|
||||
},
|
||||
"en": {
|
||||
"cmd.render.coat.bind": "Save Clear Coat",
|
||||
"cmd.render.coat.unbind": "Remove Clear Coat",
|
||||
"render.coat_discard": "Discard coat edit",
|
||||
"render.coat_pending_edit": "This coat edit is unsaved. Save it with the shot or discard it.",
|
||||
"render.coat_remove": "Remove saved coat",
|
||||
"render.coat_remove_note": "Removes this target’s coat from the shot. Geometry, appearances and physical materials stay unchanged.",
|
||||
"render.coat_remove_title": "Remove saved clear coat?",
|
||||
"render.coat_revalidate_saved": "The saved coat changed. Discard this edit and reopen the target before editing it again.",
|
||||
"render.coat_save": "Save coat to shot",
|
||||
"render.coat_saved_help": "Clear coat adds a glossy layer to opaque surfaces. Save it with the shot. Glass cannot share this target. CPU custom reflection and anisotropy overrides cannot share it either."
|
||||
},
|
||||
"he": {
|
||||
"cmd.render.coat.bind": "שמירת ציפוי שקוף",
|
||||
"cmd.render.coat.unbind": "הסרת ציפוי שקוף",
|
||||
"render.coat_discard": "ביטול עריכת הציפוי",
|
||||
"render.coat_pending_edit": "שינוי הציפוי הזה לא נשמר. יש לשמור אותו עם התצלום או לבטל את השינוי.",
|
||||
"render.coat_remove": "הסרת ציפוי שמור",
|
||||
"render.coat_remove_note": "מסיר את הציפוי של היעד הזה מהתצלום. הגאומטריה, המראה והחומרים הפיזיקליים אינם משתנים.",
|
||||
"render.coat_remove_title": "להסיר את הציפוי השקוף השמור?",
|
||||
"render.coat_revalidate_saved": "הציפוי השמור השתנה. יש לבטל את העריכה הזו ולפתוח מחדש את היעד לפני עריכה נוספת.",
|
||||
"render.coat_save": "שמירת הציפוי בתצלום",
|
||||
"render.coat_saved_help": "ציפוי שקוף מוסיף שכבה מבריקה למשטחים אטומים. יש לשמור אותו עם התצלום. זכוכית אינה יכולה להשתמש באותו יעד, וגם לא דריסות של השתקפות מותאמת אישית ואנאיזוטרופיה ב־CPU."
|
||||
},
|
||||
"ja": {
|
||||
"cmd.render.coat.bind": "クリアコートを保存",
|
||||
"cmd.render.coat.unbind": "クリアコートを削除",
|
||||
"render.coat_discard": "コートの編集を破棄",
|
||||
"render.coat_pending_edit": "クリアコートの変更は未保存です。ショットに保存するか破棄してください。",
|
||||
"render.coat_remove": "保存済みコートを削除",
|
||||
"render.coat_remove_note": "この対象のコートをショットから削除します。形状、外観、物理材料は変更されません。",
|
||||
"render.coat_remove_title": "保存済みクリアコートを削除しますか?",
|
||||
"render.coat_revalidate_saved": "保存済みコートが変更されています。この編集を破棄し、対象を開き直してから編集してください。",
|
||||
"render.coat_save": "コートをショットに保存",
|
||||
"render.coat_saved_help": "クリアコートは不透明な表面に光沢のある層を追加します。ショットに保存してください。同じ対象にガラスは使用できません。CPUのカスタム反射や異方性の上書きも、この対象と併用できません。"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.render.coat.bind": "클리어 코트 저장",
|
||||
"cmd.render.coat.unbind": "클리어 코트 제거",
|
||||
"render.coat_discard": "코트 편집 버리기",
|
||||
"render.coat_pending_edit": "이 코트 편집은 저장되지 않았습니다. 샷에 저장하거나 편집을 버리세요.",
|
||||
"render.coat_remove": "저장된 코트 제거",
|
||||
"render.coat_remove_note": "이 대상의 코트를 샷에서 제거합니다. 형상, 외관 및 물리 재료는 변경되지 않습니다.",
|
||||
"render.coat_remove_title": "저장된 클리어 코트를 제거할까요?",
|
||||
"render.coat_revalidate_saved": "저장된 코트가 변경되었습니다. 이 편집을 버리고 대상을 다시 연 뒤 편집하세요.",
|
||||
"render.coat_save": "샷에 코트 저장",
|
||||
"render.coat_saved_help": "클리어 코트는 불투명 표면에 광택 층을 추가합니다. 샷에 저장하세요. 같은 대상에 유리를 사용할 수 없습니다. CPU 사용자 지정 반사 또는 이방성 재정의도 이 대상을 함께 사용할 수 없습니다."
|
||||
},
|
||||
"vi": {
|
||||
"cmd.render.coat.bind": "Lưu lớp phủ bóng",
|
||||
"cmd.render.coat.unbind": "Xóa lớp phủ bóng",
|
||||
"render.coat_discard": "Hủy chỉnh sửa lớp phủ",
|
||||
"render.coat_pending_edit": "Chỉnh sửa lớp phủ này chưa được lưu. Hãy lưu cùng cảnh chụp hoặc hủy chỉnh sửa.",
|
||||
"render.coat_remove": "Xóa lớp phủ đã lưu",
|
||||
"render.coat_remove_note": "Xóa lớp phủ của đích này khỏi cảnh chụp. Hình học, diện mạo và vật liệu vật lý không thay đổi.",
|
||||
"render.coat_remove_title": "Xóa lớp phủ bóng đã lưu?",
|
||||
"render.coat_revalidate_saved": "Lớp phủ đã lưu đã thay đổi. Hãy hủy chỉnh sửa này và mở lại đích trước khi sửa tiếp.",
|
||||
"render.coat_save": "Lưu lớp phủ vào cảnh chụp",
|
||||
"render.coat_saved_help": "Lớp phủ bóng thêm một lớp sáng bóng lên bề mặt đục. Hãy lưu cùng cảnh chụp. Kính không thể dùng chung đích này. Ghi đè phản xạ tùy chỉnh và tính dị hướng CPU cũng không thể dùng chung đích."
|
||||
},
|
||||
"zh": {
|
||||
"cmd.render.coat.bind": "保存清漆",
|
||||
"cmd.render.coat.unbind": "移除清漆",
|
||||
"render.coat_discard": "放弃清漆编辑",
|
||||
"render.coat_pending_edit": "清漆编辑尚未保存。请保存到镜头或放弃编辑。",
|
||||
"render.coat_remove": "移除已保存清漆",
|
||||
"render.coat_remove_note": "从镜头中移除此目标的清漆。几何、外观和物理材料保持不变。",
|
||||
"render.coat_remove_title": "移除已保存的清漆?",
|
||||
"render.coat_revalidate_saved": "已保存的清漆发生了变化。请放弃此编辑,重新打开目标后再编辑。",
|
||||
"render.coat_save": "将清漆保存到镜头",
|
||||
"render.coat_saved_help": "清漆为不透明表面添加光泽层。请随镜头保存。同一目标不能使用玻璃,也不能使用CPU自定义反射或各向异性覆盖。"
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.check_glass_cpu": "يدعم CPU صفائح زجاج مستوية وصناديق محاذية للمحاور بعد التحقق منها. تتطلب الشبكات المغلقة العامة والتعشيق والتشتت الداخلي إضاءة غير مباشرة. لا تزال الظلال النافذة عبر الزجاج المغلق غير مدعومة.",
|
||||
"render.check_glass_gpu": "يدعم GPU أوساط زجاج منفصلة متحققًا منها، أو تعشيقًا متحققًا منه عند تفعيله صراحةً. يتطلب التعشيق أغلفة خارجية محدبة وأجسامًا داخلية محتواة بالكامل وحدودًا غير متلامسة.",
|
||||
"render.check_media_remaining": "لا تزال تأثيرات الزجاج الخشن وشبكات الكثافة والإزاحة والنقل الطيفي غير متاحة.",
|
||||
"render.check_media_scope": "يتطلب التشتت الداخلي النشط وسطًا زجاجيًا مغلقًا مع إيقاف نفاذ أشعة الظل المستقيمة وإزالة الضجيج. ويتطلب GPU أيضًا طبقة عينات فرينل واحدة. لا يمكن جمع الغلاف الجوي الخارجي النشط مع الزجاج أو إزالة ضجيج الأسطح.",
|
||||
"render.cpu_material_certification": "يتحقق فحص CPU من هندسة الزجاج المدعومة قبل التصيير. يستخدم الزجاج المغلق المسافة الهندسية."
|
||||
},
|
||||
"de": {
|
||||
"render.check_glass_cpu": "Die CPU unterstützt geprüfte ebene Glasscheiben und achsenparallele Boxen. Allgemeine geschlossene Netze, Verschachtelung und innere Streuung erfordern indirekte Beleuchtung. Durch geschlossenes Glas übertragene Schatten bleiben nicht unterstützt.",
|
||||
"render.check_glass_gpu": "Die GPU unterstützt geprüfte getrennte Glasmedien oder ausdrücklich aktivierte geprüfte Verschachtelung. Dafür müssen die äußeren Körper konvex und die inneren streng enthalten sein; Grenzen dürfen sich nicht berühren.",
|
||||
"render.check_media_remaining": "Raues Glas, Dichtegitter, Displacement und spektraler Lichttransport bleiben nicht verfügbar.",
|
||||
"render.check_media_scope": "Aktive innere Streuung benötigt ein geschlossenes Glasmedium ohne gerade Schattenübertragung und ohne Rauschreduzierung. Die GPU benötigt außerdem eine Fresnel-Schicht. Aktive äußere Atmosphäre kann nicht mit Glas oder Oberflächen-Rauschreduzierung kombiniert werden.",
|
||||
"render.cpu_material_certification": "Die CPU-Prüfung validiert unterstützte Glasgeometrie vor dem Rendern. Geschlossenes Glas verwendet geometrische Abstände."
|
||||
},
|
||||
"en": {
|
||||
"render.check_glass_cpu": "CPU supports certified planar Glass sheets and axis-aligned boxes. General closed meshes, nesting and interior scattering require Indirect lighting. Closed transmitted shadows remain unsupported.",
|
||||
"render.check_glass_gpu": "GPU supports certified disjoint Glass or explicit certified nesting. Nesting requires convex parents, strictly contained children and no touching boundaries.",
|
||||
"render.check_media_remaining": "Rough Glass, density grids, displacement and spectral transport remain unavailable.",
|
||||
"render.check_media_scope": "Active interior scattering needs a closed Glass medium, straight shadow transmission off and Denoise off. GPU also needs one Fresnel stratum. Active exterior Atmosphere cannot combine with Glass or surface Denoise.",
|
||||
"render.cpu_material_certification": "CPU Check validates supported Glass geometry before rendering. Closed Glass uses geometric distance."
|
||||
},
|
||||
"es": {
|
||||
"render.cpu_material_certification": "La comprobación de CPU valida la geometría de vidrio admitida antes de renderizar. El vidrio cerrado usa la distancia geométrica."
|
||||
},
|
||||
"fr": {
|
||||
"render.cpu_material_certification": "La vérification CPU valide la géométrie de verre prise en charge avant le rendu. Le verre fermé utilise la distance géométrique."
|
||||
},
|
||||
"he": {
|
||||
"render.check_glass_cpu": "ה־CPU תומך בלוחות זכוכית מישוריים ובתיבות מקבילות לצירים שעברו אימות. רשתות סגורות כלליות, קינון ופיזור פנימי דורשים תאורה עקיפה. העברת צל דרך זכוכית סגורה עדיין אינה נתמכת.",
|
||||
"render.check_glass_gpu": "ה־GPU תומך בנפחי זכוכית נפרדים שעברו אימות או בקינון מאומת שמופעל במפורש. קינון דורש גופים חיצוניים קמורים, גופים פנימיים המוכלים בתוכם ממש וגבולות שאינם נוגעים זה בזה.",
|
||||
"render.check_media_remaining": "זכוכית מחוספסת, רשתות צפיפות, הסטת משטח והעברת אור ספקטרלית עדיין אינן זמינות.",
|
||||
"render.check_media_scope": "פיזור פנימי פעיל דורש נפח זכוכית סגור, העברת קרני צל ישרות כבויה והסרת רעש כבויה. ה־GPU דורש גם שכבת פרנל אחת. אי אפשר לשלב אטמוספירה חיצונית פעילה עם זכוכית או עם הסרת רעש משטחית.",
|
||||
"render.cpu_material_certification": "בדיקת CPU מאמתת את גאומטריית הזכוכית הנתמכת לפני הרינדור. זכוכית סגורה משתמשת במרחק גאומטרי."
|
||||
},
|
||||
"ja": {
|
||||
"render.check_glass_cpu": "CPUは検証済みの平面ガラス薄板と軸に平行な箱に対応します。一般の閉じたメッシュ、入れ子、内部散乱には間接照明が必要です。閉じたガラスを通る透過影には未対応です。",
|
||||
"render.check_glass_gpu": "GPUは検証済みの分離したガラス、または明示的に有効にした検証済みの入れ子に対応します。入れ子には凸形状の親、厳密に内包された子、接触しない境界が必要です。",
|
||||
"render.check_media_remaining": "粗いガラス、密度グリッド、ディスプレイスメント、分光輸送には未対応です。",
|
||||
"render.check_media_scope": "有効な内部散乱には閉じたガラス媒質を使い、直線の影透過とノイズ除去をオフにします。GPUではフレネル層を1つにします。有効な外部の大気はガラスや表面ノイズ除去と併用できません。",
|
||||
"render.cpu_material_certification": "CPUの確認では、レンダリング前に対応するガラス形状を検証します。閉じたガラスには形状から得られる距離を使います。"
|
||||
},
|
||||
"ko": {
|
||||
"render.check_glass_cpu": "CPU는 검증된 평면 유리판과 축 정렬 상자를 지원합니다. 일반적인 닫힌 메시, 중첩 및 내부 산란에는 간접광이 필요합니다. 닫힌 유리를 통과하는 투과 그림자는 아직 지원하지 않습니다.",
|
||||
"render.check_glass_gpu": "GPU는 검증된 분리 유리 또는 명시적으로 활성화한 검증된 중첩을 지원합니다. 중첩에는 볼록한 바깥 형상, 완전히 내부에 포함된 안쪽 형상 및 서로 닿지 않는 경계가 필요합니다.",
|
||||
"render.check_media_remaining": "거친 유리, 밀도 격자, 디스플레이스먼트 및 분광 광전달은 아직 지원하지 않습니다.",
|
||||
"render.check_media_scope": "활성 내부 산란에는 닫힌 유리 매질이 필요하며 직선 그림자 투과와 노이즈 제거를 꺼야 합니다. GPU에는 프레넬 계층도 하나만 필요합니다. 활성 외부 대기는 유리 또는 표면 노이즈 제거와 함께 사용할 수 없습니다.",
|
||||
"render.cpu_material_certification": "CPU 검사는 렌더링 전에 지원되는 유리 형상을 검증합니다. 닫힌 유리는 기하학적 거리를 사용합니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.check_glass_cpu": "CPU hỗ trợ các tấm kính phẳng và hộp thẳng theo trục đã được kiểm chứng. Lưới kín tổng quát, cấu trúc lồng nhau và tán xạ bên trong cần Chiếu sáng gián tiếp. Chưa hỗ trợ bóng truyền qua kính kín.",
|
||||
"render.check_glass_gpu": "GPU hỗ trợ các khối kính tách rời đã được kiểm chứng hoặc cấu trúc lồng nhau được kiểm chứng và bật rõ ràng. Cấu trúc lồng nhau cần lớp bao lồi, vật thể con nằm hoàn toàn bên trong và các ranh giới không chạm nhau.",
|
||||
"render.check_media_remaining": "Chưa hỗ trợ kính nhám, lưới mật độ, dịch chuyển bề mặt và truyền sáng quang phổ.",
|
||||
"render.check_media_scope": "Tán xạ bên trong đang hoạt động cần môi trường kính kín, tắt truyền tia bóng thẳng và tắt Khử nhiễu. GPU cũng cần một tầng Fresnel. Không thể kết hợp Khí quyển bên ngoài đang hoạt động với kính hoặc Khử nhiễu bề mặt.",
|
||||
"render.cpu_material_certification": "Kiểm tra trên CPU xác thực hình học thủy tinh được hỗ trợ trước khi kết xuất. Thủy tinh kín sử dụng khoảng cách hình học."
|
||||
},
|
||||
"zh": {
|
||||
"render.check_glass_cpu": "CPU 支持经验证的平面玻璃薄片和轴对齐盒体。一般闭合网格、嵌套和内部散射需要间接光照。仍不支持穿过闭合玻璃的透射阴影。",
|
||||
"render.check_glass_gpu": "GPU 支持经验证的互不相交玻璃介质,或明确启用的经验证嵌套。嵌套要求外层物体为凸体、内层物体严格包含于其中,且边界互不接触。",
|
||||
"render.check_media_remaining": "仍不支持粗糙玻璃、密度网格、位移和光谱传输。",
|
||||
"render.check_media_scope": "启用内部散射需要闭合玻璃介质,并关闭直线阴影透射和降噪。GPU 还需要只使用一个菲涅耳分层。启用的外部大气不能与玻璃或表面降噪同时使用。",
|
||||
"render.cpu_material_certification": "CPU 检查会在渲染前验证所支持的玻璃几何体。封闭玻璃使用几何距离。"
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_opaque_hint": "يستخدم تتبع الأشعة على CPU خلفية معتمة.",
|
||||
"render.output_master_unavailable": "لتصدير EXR، اختر صورة مكتملة تحتوي على بيانات EXR خام. فعّل EXR الخام قبل تصيير صور جديدة.",
|
||||
"render.output_presentation_limits": "تدعم مؤثرات الصور على CPU وإخراج EXR صوراً بعرض وارتفاع لا يتجاوزان 8192 بكسل، وبإجمالي لا يتجاوز 16,777,216 بكسل.",
|
||||
"render.output_raytrace_only": "تتطلب مؤثرات الصور تتبع الأشعة على CPU أو تتبع المسارات على GPU.",
|
||||
"render.presentation_banner": "تعمل مؤثرات إخراج الصورة مع تتبع الأشعة على CPU وGPU. فتح هذه الصفحة لا يغيّر الطريقة مطلقًا."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_opaque_hint": "CPU-Raytracing verwendet einen undurchsichtigen Hintergrund.",
|
||||
"render.output_master_unavailable": "Zum EXR-Export ein fertiges Bild mit EXR-Rohdaten auswählen. EXR-Rohdaten vor dem Rendern neuer Bilder aktivieren.",
|
||||
"render.output_presentation_limits": "CPU-Bildeffekte und EXR-Ausgabe unterstützen Bilder mit bis zu 8192 Pixeln Breite oder Höhe und insgesamt höchstens 16.777.216 Pixeln.",
|
||||
"render.output_raytrace_only": "Bildeffekte erfordern CPU-Raytracing oder GPU-Pathtracing.",
|
||||
"render.presentation_banner": "Bildeffekte funktionieren mit CPU- und GPU-Raytracing. Das Öffnen dieser Seite ändert die Methode nicht."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_opaque_hint": "CPU ray tracing uses an opaque background.",
|
||||
"render.output_master_unavailable": "Select a completed image with raw EXR data to export EXR. Enable raw EXR before rendering new images.",
|
||||
"render.output_presentation_limits": "CPU image effects and EXR output support images up to 8192 pixels wide or high, with at most 16,777,216 pixels in total.",
|
||||
"render.output_raytrace_only": "Image effects require CPU ray tracing or GPU path tracing.",
|
||||
"render.presentation_banner": "Presentation effects work with CPU and GPU ray tracing. Opening this page never changes the method."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_opaque_hint": "עקיבת קרניים ב־CPU משתמשת ברקע אטום.",
|
||||
"render.output_master_unavailable": "כדי לייצא EXR, יש לבחור תמונה שהושלמה ומכילה נתוני EXR גולמיים. יש להפעיל EXR גולמי לפני רינדור תמונות חדשות.",
|
||||
"render.output_presentation_limits": "אפקטים של תמונה ב־CPU ופלט EXR תומכים בתמונות ברוחב ובגובה של עד 8192 פיקסלים, ועד 16,777,216 פיקסלים בסך הכול.",
|
||||
"render.output_raytrace_only": "אפקטים של תמונה דורשים עקיבת קרניים ב־CPU או עקיבת נתיבים ב־GPU.",
|
||||
"render.presentation_banner": "אפקטים של תצוגת התמונה פועלים בעקיבת קרניים ב־CPU וב־GPU. פתיחת דף זה לעולם אינה משנה את השיטה."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_opaque_hint": "CPUレイトレーシングでは不透明な背景を使用します。",
|
||||
"render.output_master_unavailable": "EXRを書き出すには、RAW EXRデータを持つ完成画像を選択してください。新しい画像は、レンダリング前にRAW EXRを有効にしてください。",
|
||||
"render.output_presentation_limits": "CPUの画像効果とEXR出力は、幅・高さとも8192ピクセル以下、合計16,777,216ピクセル以下の画像に対応します。",
|
||||
"render.output_raytrace_only": "画像効果にはCPUレイトレーシングまたはGPUパストレーシングが必要です。",
|
||||
"render.presentation_banner": "画像効果はCPUとGPUのレイトレーシングで使用できます。このページを開いても方式は変わりません。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_opaque_hint": "CPU 레이 트레이싱은 불투명한 배경을 사용합니다.",
|
||||
"render.output_master_unavailable": "EXR을 내보내려면 원본 EXR 데이터가 있는 완성 이미지를 선택하세요. 새 이미지를 렌더링하기 전에 원본 EXR을 활성화하세요.",
|
||||
"render.output_presentation_limits": "CPU 이미지 효과와 EXR 출력은 너비와 높이가 각각 최대 8192픽셀이고 총 16,777,216픽셀 이하인 이미지를 지원합니다.",
|
||||
"render.output_raytrace_only": "이미지 효과에는 CPU 레이 트레이싱 또는 GPU 패스 트레이싱이 필요합니다.",
|
||||
"render.presentation_banner": "이미지 효과는 CPU 및 GPU 레이 트레이싱에서 사용할 수 있습니다. 이 페이지를 열어도 방식이 바뀌지 않습니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_opaque_hint": "Dò tia trên CPU sử dụng nền không trong suốt.",
|
||||
"render.output_master_unavailable": "Để xuất EXR, hãy chọn ảnh đã hoàn tất có dữ liệu EXR thô. Bật EXR thô trước khi kết xuất ảnh mới.",
|
||||
"render.output_presentation_limits": "Hiệu ứng ảnh trên CPU và đầu ra EXR hỗ trợ ảnh có chiều rộng và chiều cao tối đa 8192 pixel, với tổng cộng không quá 16.777.216 pixel.",
|
||||
"render.output_raytrace_only": "Hiệu ứng ảnh yêu cầu dò tia trên CPU hoặc dò đường đi trên GPU.",
|
||||
"render.presentation_banner": "Các hiệu ứng hình ảnh hoạt động với dò tia trên CPU và GPU. Mở trang này không bao giờ thay đổi phương pháp."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_opaque_hint": "CPU光线追踪使用不透明背景。",
|
||||
"render.output_master_unavailable": "要导出EXR,请选择包含原始EXR数据的已完成图像。渲染新图像前请先启用原始EXR。",
|
||||
"render.output_presentation_limits": "CPU图像效果和EXR输出支持宽度、高度均不超过8192像素且总像素数不超过16,777,216的图像。",
|
||||
"render.output_raytrace_only": "图像效果需要CPU光线追踪或GPU路径追踪。",
|
||||
"render.presentation_banner": "图像效果支持CPU和GPU光线追踪。打开此页面不会更改渲染方式。"
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"prefs.render_legacy_image_defaults": "استخدام الإعدادات الافتراضية السابقة لصور التصيير",
|
||||
"prefs.render_legacy_image_defaults_help": "الاحتفاظ بسير العمل اليدوي السابق: إعدادات جديدة بالتصيير النقطي على CPU، وأخذ العينات الثابت لتتبع الأشعة على CPU، وفتح النتائج يدويًا. إعادة ملاءمة العرض عند دخول التصيير، وتبديل اللقطات دون استعادة كاميراتها، واستخدام التأطير والتنقل السابقين. تعطيل التحديث التلقائي للصورة المباشرة. لا تتغير الإعدادات المحفوظة."
|
||||
},
|
||||
"de": {
|
||||
"prefs.render_legacy_image_defaults": "Bisherige Render-Bildvorgaben verwenden",
|
||||
"prefs.render_legacy_image_defaults_help": "Den bisherigen manuellen Ablauf beibehalten: neue Setups mit CPU-Raster, feste Abtastung beim CPU-Raytracing und manuell geöffnete Ergebnisse. Beim Wechsel zu Render die Ansicht neu einpassen, Aufnahmen ohne Wiederherstellung ihrer Kamera wechseln und die bisherige Bildausrichtung und Navigation verwenden. Automatische Live-Bildrückmeldungen deaktivieren. Gespeicherte Setups bleiben unverändert."
|
||||
},
|
||||
"en": {
|
||||
"prefs.render_legacy_image_defaults": "Use previous Render image defaults",
|
||||
"prefs.render_legacy_image_defaults_help": "Keep the previous manual workflow: new setups use CPU raster, CPU ray tracing keeps fixed sampling, and you open rendered results manually. Refit on Render entry, switch shots without restoring their cameras, and use the previous framing and navigation. Disable automatic live image feedback. Saved setups are unchanged."
|
||||
},
|
||||
"he": {
|
||||
"prefs.render_legacy_image_defaults": "שימוש בברירות המחדל הקודמות לתמונות הדמיה",
|
||||
"prefs.render_legacy_image_defaults_help": "שמירה על תהליך העבודה הידני הקודם: הגדרות חדשות ברסטר CPU, דגימה קבועה בעקיבת קרניים ב־CPU ופתיחת התוצאות ידנית. התאמת התצוגה מחדש בכניסה להדמיה, מעבר בין צילומים בלי לשחזר את המצלמות שלהם ושימוש במסגור ובניווט הקודמים. עדכון אוטומטי של התמונה החיה מושבת. הגדרות שמורות אינן משתנות."
|
||||
},
|
||||
"ja": {
|
||||
"prefs.render_legacy_image_defaults": "以前のレンダー画像の既定値を使用",
|
||||
"prefs.render_legacy_image_defaults_help": "以前の手動の画像ワークフローを使用します。新しいセットアップはCPUラスタ、CPUレイトレーシングは固定サンプリングとし、結果は手動で開きます。レンダーに入るたびにビューをフィットし、ショットを切り替えても保存カメラを復元せず、以前のフレーミングとナビゲーションを使用します。自動のライブ画像更新は無効にします。保存済みのセットアップは変更しません。"
|
||||
},
|
||||
"ko": {
|
||||
"prefs.render_legacy_image_defaults": "이전 렌더 이미지 기본값 사용",
|
||||
"prefs.render_legacy_image_defaults_help": "이전의 수동 이미지 작업 방식을 유지합니다. 새 설정은 CPU 래스터를 사용하고 CPU 광선 추적은 고정 샘플링을 사용하며 결과는 직접 엽니다. 렌더에 들어갈 때 뷰를 다시 맞추고, 샷을 전환해도 저장된 카메라를 복원하지 않으며, 이전의 프레이밍과 탐색 방식을 사용합니다. 자동 실시간 이미지 피드백을 끕니다. 저장된 설정은 변경하지 않습니다.",
|
||||
"render.method_raster": "CPU 래스터 (도식)",
|
||||
"render.result_provisional": "임시 이미지"
|
||||
},
|
||||
"vi": {
|
||||
"prefs.render_legacy_image_defaults": "Dùng mặc định ảnh Kết xuất trước đây",
|
||||
"prefs.render_legacy_image_defaults_help": "Giữ quy trình hình ảnh thủ công trước đây: thiết lập mới dùng raster CPU, dò tia CPU dùng lấy mẫu cố định và mở kết quả thủ công. Căn vừa lại khung nhìn khi vào Kết xuất, chuyển góc chụp mà không khôi phục camera đã lưu, đồng thời dùng cách lấy khung hình và điều hướng trước đây. Tắt phản hồi ảnh trực tiếp tự động. Các thiết lập đã lưu không thay đổi."
|
||||
},
|
||||
"zh": {
|
||||
"prefs.render_legacy_image_defaults": "使用以前的渲染图像默认值",
|
||||
"prefs.render_legacy_image_defaults_help": "保留以前的手动图像流程:新设置使用 CPU 光栅渲染,CPU 光线追踪使用固定采样,并手动打开结果。进入渲染时重新适配视图,切换镜头时不恢复其保存的相机,并使用以前的取景和导航方式。禁用自动实时图像反馈。已保存的设置保持不变。"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.light_space_world": "أضواء بإحداثيات العالم",
|
||||
"render.light_space_world_help": "تستخدم مواضع الأضواء واتجاهاتها إحداثيات العالم. لا يؤدي تدوير الكاميرا حول النموذج إلى تحريك الأضواء."
|
||||
},
|
||||
"de": {
|
||||
"render.light_space_world": "Lichter in Weltkoordinaten",
|
||||
"render.light_space_world_help": "Lichtpositionen und -richtungen verwenden Weltkoordinaten. Beim Umkreisen mit der Kamera bleiben die Lichter an ihrer Position."
|
||||
},
|
||||
"en": {
|
||||
"render.light_space_world": "World-space lights",
|
||||
"render.light_space_world_help": "Light positions and directions use world coordinates. Orbiting the camera does not move them."
|
||||
},
|
||||
"he": {
|
||||
"render.light_space_world": "אורות בקואורדינטות העולם",
|
||||
"render.light_space_world_help": "מיקומי האורות וכיווניהם משתמשים בקואורדינטות העולם. סיבוב המצלמה סביב המודל אינו מזיז את האורות."
|
||||
},
|
||||
"ja": {
|
||||
"render.light_space_world": "ワールド座標のライト",
|
||||
"render.light_space_world_help": "ライトの位置と方向はワールド座標を使用します。カメラを周回させてもライトは移動しません。"
|
||||
},
|
||||
"ko": {
|
||||
"render.light_space_world": "월드 좌표의 조명",
|
||||
"render.light_space_world_help": "조명의 위치와 방향은 월드 좌표를 사용합니다. 카메라를 회전해도 조명은 움직이지 않습니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.light_space_world": "Đèn trong hệ tọa độ thế giới",
|
||||
"render.light_space_world_help": "Vị trí và hướng của đèn dùng hệ tọa độ thế giới. Xoay camera quanh mô hình không làm đèn di chuyển."
|
||||
},
|
||||
"zh": {
|
||||
"render.light_space_world": "世界坐标光源",
|
||||
"render.light_space_world_help": "光源的位置和方向使用世界坐标。环绕旋转相机不会移动光源。"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"cmd.view.render_live": "إيقاف التصيير المباشر مؤقتًا أو استئنافه"
|
||||
},
|
||||
"de": {
|
||||
"cmd.view.render_live": "Live-Rendering pausieren oder fortsetzen"
|
||||
},
|
||||
"en": {
|
||||
"cmd.view.render_live": "Pause or Resume Live Render"
|
||||
},
|
||||
"he": {
|
||||
"cmd.view.render_live": "השהיה או חידוש של הדמיה חיה"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.view.render_live": "ライブレンダーを一時停止・再開"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.view.render_live": "실시간 렌더링 일시 중지 또는 재개"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.view.render_live": "Tạm dừng hoặc tiếp tục kết xuất trực tiếp"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.view.render_live": "暂停或继续实时渲染"
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.live.final_changed": "تغيّر طلب التصيير أثناء إيقاف التصيير المباشر. ابدأ الصورة النهائية من جديد.",
|
||||
"render.live.final_waiting": "جارٍ تحضير الصورة النهائية · تحرير موارد التصيير المباشر",
|
||||
"render.live.invalid_size": "يتطلب التصيير المباشر عرضًا وارتفاعًا موجبين للإخراج.",
|
||||
"render.live.pause": "إيقاف المباشر مؤقتًا",
|
||||
"render.live.paused": "مباشر · متوقف مؤقتًا",
|
||||
"render.live.paused_outdated": "مباشر · متوقف مؤقتًا · غير محدّث",
|
||||
"render.live.quality_help": "جودة تفاعلية: بحد أقصى 512 بكسل للضلع الأطول. تستخدم المصيّر المحدد ومؤثراته المدعومة. تبقى دقة الصورة النهائية وإعداداتها وتصديرها مستقلة.",
|
||||
"render.live.refused": "التصيير المباشر غير متاح: {reason}",
|
||||
"render.live.resume": "استئناف المباشر",
|
||||
"render.live.samples": "مباشر · {width} × {height} · {current}/{target} عينة",
|
||||
"render.live.stopped": "توقف التصيير المباشر",
|
||||
"render.live.updating": "مباشر · جودة تفاعلية (حتى 512 بكسل) · جارٍ التحديث",
|
||||
"render.live.waiting_canvas": "في انتظار لوحة التصيير",
|
||||
"render.live.worker_stopped": "توقفت مهمة التقاط التصيير المباشر قبل إرجاع نتيجة"
|
||||
},
|
||||
"de": {
|
||||
"render.live.final_changed": "Die Render-Anfrage hat sich beim Anhalten des Live-Renderings geändert. Starten Sie das finale Bild erneut.",
|
||||
"render.live.final_waiting": "Finales Bild vorbereiten · Live-Render-Ressourcen freigeben",
|
||||
"render.live.invalid_size": "Live-Rendering benötigt eine positive Ausgabebreite und -höhe.",
|
||||
"render.live.pause": "Live pausieren",
|
||||
"render.live.paused": "Live · Pausiert",
|
||||
"render.live.paused_outdated": "Live · Pausiert · Veraltet",
|
||||
"render.live.quality_help": "Interaktive Qualität: höchstens 512 Pixel an der längsten Kante. Verwendet den gewählten Renderer und seine unterstützten Effekte. Endauflösung, Einstellungen und Export bleiben getrennt.",
|
||||
"render.live.refused": "Live nicht verfügbar: {reason}",
|
||||
"render.live.resume": "Live fortsetzen",
|
||||
"render.live.samples": "Live · {width} × {height} · {current}/{target} Samples",
|
||||
"render.live.stopped": "Live-Rendering angehalten",
|
||||
"render.live.updating": "Live · Interaktive Qualität (bis 512 px) · Aktualisierung",
|
||||
"render.live.waiting_canvas": "Warten auf die Render-Ansicht",
|
||||
"render.live.worker_stopped": "Der Aufnahmeprozess für das Live-Rendering wurde vor der Rückgabe eines Ergebnisses beendet"
|
||||
},
|
||||
"en": {
|
||||
"render.live.final_changed": "The render request changed while live rendering was stopping. Start the final image again.",
|
||||
"render.live.final_waiting": "Preparing final image · releasing live-render resources",
|
||||
"render.live.invalid_size": "Live rendering needs a positive output width and height.",
|
||||
"render.live.pause": "Pause live",
|
||||
"render.live.paused": "Live · Paused",
|
||||
"render.live.paused_outdated": "Live · Paused · Out of date",
|
||||
"render.live.quality_help": "Interactive quality: at most 512 pixels on the longest edge. Uses the selected renderer and its supported effects. Final resolution, settings and export remain separate.",
|
||||
"render.live.refused": "Live unavailable: {reason}",
|
||||
"render.live.resume": "Resume live",
|
||||
"render.live.samples": "Live · {width} × {height} · {current}/{target} samples",
|
||||
"render.live.stopped": "Live rendering stopped",
|
||||
"render.live.updating": "Live · Interactive quality (up to 512 px) · Updating",
|
||||
"render.live.waiting_canvas": "Waiting for the Render canvas",
|
||||
"render.live.worker_stopped": "The live-render capture worker stopped before returning a result"
|
||||
},
|
||||
"he": {
|
||||
"render.live.final_changed": "בקשת ההדמיה השתנתה בזמן עצירת ההדמיה החיה. יש להתחיל את התמונה הסופית מחדש.",
|
||||
"render.live.final_waiting": "מכין תמונה סופית · משחרר משאבי הדמיה חיה",
|
||||
"render.live.invalid_size": "הדמיה חיה דורשת רוחב וגובה פלט גדולים מאפס.",
|
||||
"render.live.pause": "השהיית הדמיה חיה",
|
||||
"render.live.paused": "חי · מושהה",
|
||||
"render.live.paused_outdated": "חי · מושהה · לא מעודכן",
|
||||
"render.live.quality_help": "איכות אינטראקטיבית: לכל היותר 512 פיקסלים בצד הארוך. משתמשת במנוע ההדמיה שנבחר ובאפקטים הנתמכים בו. רזולוציית התמונה הסופית, ההגדרות והייצוא נשארים נפרדים.",
|
||||
"render.live.refused": "הדמיה חיה אינה זמינה: {reason}",
|
||||
"render.live.resume": "חידוש הדמיה חיה",
|
||||
"render.live.samples": "חי · {width} × {height} · {current}/{target} דגימות",
|
||||
"render.live.stopped": "ההדמיה החיה נעצרה",
|
||||
"render.live.updating": "חי · איכות אינטראקטיבית (עד 512 פיקסלים) · מתעדכן",
|
||||
"render.live.waiting_canvas": "ממתין למשטח ההדמיה",
|
||||
"render.live.worker_stopped": "תהליך הלכידה של ההדמיה החיה נעצר לפני שהחזיר תוצאה"
|
||||
},
|
||||
"ja": {
|
||||
"render.live.final_changed": "ライブレンダリングの停止中にリクエストが変更されました。最終画像をもう一度開始してください。",
|
||||
"render.live.final_waiting": "最終画像を準備中 · ライブレンダーのリソースを解放中",
|
||||
"render.live.invalid_size": "ライブレンダリングには正の出力幅と高さが必要です。",
|
||||
"render.live.pause": "ライブを一時停止",
|
||||
"render.live.paused": "ライブ · 一時停止中",
|
||||
"render.live.paused_outdated": "ライブ · 一時停止中 · 更新が必要",
|
||||
"render.live.quality_help": "インタラクティブ品質: 長辺は最大512ピクセルです。選択したレンダラーと対応する効果を使用します。最終解像度、設定、書き出しは別に保持します。",
|
||||
"render.live.refused": "ライブを利用できません: {reason}",
|
||||
"render.live.resume": "ライブを再開",
|
||||
"render.live.samples": "ライブ · {width} × {height} · {current}/{target} サンプル",
|
||||
"render.live.stopped": "ライブレンダリングを停止しました",
|
||||
"render.live.updating": "ライブ · インタラクティブ品質(最大512 px)· 更新中",
|
||||
"render.live.waiting_canvas": "レンダーキャンバスを待機中",
|
||||
"render.live.worker_stopped": "ライブレンダリングのキャプチャーワーカーが結果を返す前に停止しました"
|
||||
},
|
||||
"ko": {
|
||||
"render.live.final_changed": "라이브 렌더링을 중지하는 동안 렌더 요청이 변경되었습니다. 최종 이미지를 다시 시작하세요.",
|
||||
"render.live.final_waiting": "최종 이미지 준비 중 · 라이브 렌더 리소스 해제 중",
|
||||
"render.live.invalid_size": "라이브 렌더링에는 양수의 출력 너비와 높이가 필요합니다.",
|
||||
"render.live.pause": "라이브 일시 중지",
|
||||
"render.live.paused": "라이브 · 일시 중지됨",
|
||||
"render.live.paused_outdated": "라이브 · 일시 중지됨 · 업데이트 필요",
|
||||
"render.live.quality_help": "대화형 품질: 가장 긴 변은 최대 512픽셀입니다. 선택한 렌더러와 지원하는 효과를 사용합니다. 최종 해상도, 설정 및 내보내기는 별도로 유지됩니다.",
|
||||
"render.live.refused": "라이브 사용 불가: {reason}",
|
||||
"render.live.resume": "라이브 재개",
|
||||
"render.live.samples": "라이브 · {width} × {height} · {current}/{target} 샘플",
|
||||
"render.live.stopped": "라이브 렌더링 중지됨",
|
||||
"render.live.updating": "라이브 · 대화형 품질(최대 512 px) · 업데이트 중",
|
||||
"render.live.waiting_canvas": "렌더 캔버스 대기 중",
|
||||
"render.live.worker_stopped": "라이브 렌더 캡처 작업이 결과를 반환하기 전에 중지되었습니다"
|
||||
},
|
||||
"vi": {
|
||||
"render.live.final_changed": "Yêu cầu kết xuất đã thay đổi trong lúc dừng kết xuất trực tiếp. Hãy bắt đầu lại ảnh cuối cùng.",
|
||||
"render.live.final_waiting": "Đang chuẩn bị ảnh cuối · Giải phóng tài nguyên kết xuất trực tiếp",
|
||||
"render.live.invalid_size": "Kết xuất trực tiếp yêu cầu chiều rộng và chiều cao đầu ra lớn hơn không.",
|
||||
"render.live.pause": "Tạm dừng trực tiếp",
|
||||
"render.live.paused": "Trực tiếp · Tạm dừng",
|
||||
"render.live.paused_outdated": "Trực tiếp · Tạm dừng · Chưa cập nhật",
|
||||
"render.live.quality_help": "Chất lượng tương tác: tối đa 512 pixel ở cạnh dài nhất. Sử dụng bộ kết xuất đã chọn và các hiệu ứng được bộ đó hỗ trợ. Độ phân giải cuối, thiết lập và thao tác xuất vẫn độc lập.",
|
||||
"render.live.refused": "Kết xuất trực tiếp không khả dụng: {reason}",
|
||||
"render.live.resume": "Tiếp tục trực tiếp",
|
||||
"render.live.samples": "Trực tiếp · {width} × {height} · {current}/{target} mẫu",
|
||||
"render.live.stopped": "Kết xuất trực tiếp đã dừng",
|
||||
"render.live.updating": "Trực tiếp · Chất lượng tương tác (tối đa 512 px) · Đang cập nhật",
|
||||
"render.live.waiting_canvas": "Đang chờ khung vẽ Kết xuất",
|
||||
"render.live.worker_stopped": "Luồng thu nhận dữ liệu cho kết xuất trực tiếp đã dừng trước khi trả về kết quả"
|
||||
},
|
||||
"zh": {
|
||||
"render.live.final_changed": "停止实时渲染时渲染请求已更改。请重新开始最终图像渲染。",
|
||||
"render.live.final_waiting": "正在准备最终图像 · 释放实时渲染资源",
|
||||
"render.live.invalid_size": "实时渲染需要正的输出宽度和高度。",
|
||||
"render.live.pause": "暂停实时渲染",
|
||||
"render.live.paused": "实时 · 已暂停",
|
||||
"render.live.paused_outdated": "实时 · 已暂停 · 已过时",
|
||||
"render.live.quality_help": "交互质量:最长边最多为 512 像素。使用所选渲染器及其支持的效果。最终分辨率、设置和导出保持独立。",
|
||||
"render.live.refused": "实时渲染不可用:{reason}",
|
||||
"render.live.resume": "恢复实时渲染",
|
||||
"render.live.samples": "实时 · {width} × {height} · {current}/{target} 次采样",
|
||||
"render.live.stopped": "实时渲染已停止",
|
||||
"render.live.updating": "实时 · 交互质量(最多 512 px)· 正在更新",
|
||||
"render.live.waiting_canvas": "正在等待渲染画布",
|
||||
"render.live.worker_stopped": "实时渲染捕获工作线程在返回结果之前已停止"
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"render.batch_owner_limit": "At most 32 document shot queues can be retained. Finish or cancel an existing queue first.",
|
||||
"render.batch_preparing": "Preparing next shot",
|
||||
"render.batch_waiting_worker": "Waiting for render preparation",
|
||||
"render.preparation_cancelled": "Render preparation cancelled",
|
||||
"render.preparation_checking": "Checking render",
|
||||
"render.preparation_retirement_failed": "Render preparation cleanup failed. This window cannot prepare another image.",
|
||||
"render.preparation_retiring": "Finishing render preparation",
|
||||
"render.preparation_unavailable": "Render preparation unavailable"
|
||||
},
|
||||
"de": {
|
||||
"render.batch_owner_limit": "Es können höchstens 32 Dokument-Bildfolgen vorgehalten werden. Schließen Sie zuerst eine vorhandene Folge ab oder brechen Sie sie ab.",
|
||||
"render.batch_preparing": "Nächste Aufnahme wird vorbereitet",
|
||||
"render.batch_waiting_worker": "Warten auf die Rendering-Vorbereitung",
|
||||
"render.preparation_cancelled": "Rendering-Vorbereitung abgebrochen",
|
||||
"render.preparation_checking": "Rendering wird geprüft",
|
||||
"render.preparation_retirement_failed": "Die Bereinigung der Rendering-Vorbereitung ist fehlgeschlagen. Dieses Fenster kann kein weiteres Bild vorbereiten.",
|
||||
"render.preparation_retiring": "Rendering-Vorbereitung wird abgeschlossen",
|
||||
"render.preparation_unavailable": "Rendering-Vorbereitung nicht verfügbar"
|
||||
},
|
||||
"ja": {
|
||||
"render.batch_owner_limit": "保持できるドキュメントのショットキューは最大32個です。既存のキューを完了するかキャンセルしてください。",
|
||||
"render.batch_preparing": "次のショットを準備中",
|
||||
"render.batch_waiting_worker": "レンダリングの準備待ち",
|
||||
"render.preparation_cancelled": "レンダリングの準備をキャンセルしました",
|
||||
"render.preparation_checking": "レンダリングを確認中",
|
||||
"render.preparation_retirement_failed": "レンダリング準備の後処理に失敗しました。このウィンドウでは別の画像を準備できません。",
|
||||
"render.preparation_retiring": "レンダリングの準備を終了中",
|
||||
"render.preparation_unavailable": "レンダリングの準備を利用できません"
|
||||
},
|
||||
"ko": {
|
||||
"render.batch_owner_limit": "문서 샷 대기열은 최대 32개까지 유지할 수 있습니다. 기존 대기열을 완료하거나 취소하세요.",
|
||||
"render.batch_preparing": "다음 샷 준비 중",
|
||||
"render.batch_waiting_worker": "렌더링 준비 대기 중",
|
||||
"render.preparation_cancelled": "렌더링 준비가 취소되었습니다",
|
||||
"render.preparation_checking": "렌더링 확인 중",
|
||||
"render.preparation_retirement_failed": "렌더링 준비 정리에 실패했습니다. 이 창에서는 다른 이미지를 준비할 수 없습니다.",
|
||||
"render.preparation_retiring": "렌더링 준비 마무리 중",
|
||||
"render.preparation_unavailable": "렌더링 준비를 사용할 수 없음"
|
||||
},
|
||||
"zh": {
|
||||
"render.batch_owner_limit": "最多可保留32个文档的镜头队列。请先完成或取消现有队列。",
|
||||
"render.batch_preparing": "正在准备下一个镜头",
|
||||
"render.batch_waiting_worker": "等待渲染准备",
|
||||
"render.preparation_cancelled": "渲染准备已取消",
|
||||
"render.preparation_checking": "正在检查渲染",
|
||||
"render.preparation_retirement_failed": "渲染准备清理失败。此窗口无法再准备其他图像。",
|
||||
"render.preparation_retiring": "正在完成渲染准备",
|
||||
"render.preparation_unavailable": "渲染准备不可用"
|
||||
},
|
||||
"ar": {
|
||||
"render.batch_owner_limit": "يمكن الاحتفاظ بقوائم لقطات 32 مستندًا كحد أقصى. أكمل قائمة موجودة أو ألغها أولًا.",
|
||||
"render.batch_preparing": "جارٍ إعداد اللقطة التالية",
|
||||
"render.batch_waiting_worker": "في انتظار إعداد التصيير",
|
||||
"render.preparation_cancelled": "أُلغي إعداد التصيير",
|
||||
"render.preparation_checking": "جارٍ التحقق من التصيير",
|
||||
"render.preparation_retirement_failed": "تعذّر تنظيف موارد إعداد التصيير. لا يمكن لهذه النافذة إعداد صورة أخرى.",
|
||||
"render.preparation_retiring": "جارٍ إنهاء إعداد التصيير",
|
||||
"render.preparation_unavailable": "إعداد التصيير غير متاح"
|
||||
},
|
||||
"he": {
|
||||
"render.batch_owner_limit": "ניתן לשמור תורי צילומים של 32 מסמכים לכל היותר. יש להשלים או לבטל תור קיים תחילה.",
|
||||
"render.batch_preparing": "מכין את הצילום הבא",
|
||||
"render.batch_waiting_worker": "ממתין להכנת הרינדור",
|
||||
"render.preparation_cancelled": "הכנת הרינדור בוטלה",
|
||||
"render.preparation_checking": "בודק רינדור",
|
||||
"render.preparation_retirement_failed": "ניקוי משאבי הכנת הרינדור נכשל. לא ניתן להכין תמונה נוספת בחלון זה.",
|
||||
"render.preparation_retiring": "מסיים את הכנת הרינדור",
|
||||
"render.preparation_unavailable": "הכנת הרינדור אינה זמינה"
|
||||
},
|
||||
"vi": {
|
||||
"render.batch_owner_limit": "Chỉ có thể giữ hàng đợi cảnh của tối đa 32 tài liệu. Hãy hoàn tất hoặc hủy một hàng đợi hiện có trước.",
|
||||
"render.batch_preparing": "Đang chuẩn bị cảnh tiếp theo",
|
||||
"render.batch_waiting_worker": "Đang chờ chuẩn bị kết xuất",
|
||||
"render.preparation_cancelled": "Đã hủy chuẩn bị kết xuất",
|
||||
"render.preparation_checking": "Đang kiểm tra kết xuất",
|
||||
"render.preparation_retirement_failed": "Không thể giải phóng tài nguyên chuẩn bị kết xuất. Cửa sổ này không thể chuẩn bị ảnh khác.",
|
||||
"render.preparation_retiring": "Đang hoàn tất chuẩn bị kết xuất",
|
||||
"render.preparation_unavailable": "Không thể chuẩn bị kết xuất"
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.cpu_fixed_retained": "تم الاحتفاظ بأخذ العينات الثابت",
|
||||
"render.cpu_progressive_adopt": "استخدام أخذ العينات التدريجي",
|
||||
"render.cpu_progressive_adopt_help": "يستخدم أخذ العينات التدريجي عينة كاميرا واحدة لكل تمريرة. يبقى حجم الصورة والأضواء وإعدادات الانعكاس دون تغيير.",
|
||||
"render.cpu_progressive_always": "أخذ العينات التدريجي مفعّل دائمًا",
|
||||
"render.cpu_progressive_reflection_adopt": "اضبط محاولات الانعكاس المحددة على 1 قبل تفعيل أخذ العينات التدريجي."
|
||||
},
|
||||
"de": {
|
||||
"render.cpu_fixed_retained": "Feste Abtastung beibehalten",
|
||||
"render.cpu_progressive_adopt": "Progressive Abtastung verwenden",
|
||||
"render.cpu_progressive_adopt_help": "Die progressive Abtastung verwendet ein Kamerasample pro Durchlauf. Bildgröße, Lichtquellen und Reflexionseinstellungen bleiben unverändert.",
|
||||
"render.cpu_progressive_always": "Progressive Abtastung ist immer aktiv",
|
||||
"render.cpu_progressive_reflection_adopt": "Setzen Sie die Reflexionsversuche ausgewählter Körper auf 1, bevor Sie die progressive Abtastung übernehmen."
|
||||
},
|
||||
"en": {
|
||||
"render.cpu_fixed_retained": "Fixed sampling retained",
|
||||
"render.cpu_progressive_adopt": "Use progressive sampling",
|
||||
"render.cpu_progressive_adopt_help": "Adopting progressive uses one camera sample per pass. Image size, lights and reflection settings stay unchanged.",
|
||||
"render.cpu_progressive_always": "Progressive sampling is always on",
|
||||
"render.cpu_progressive_reflection_adopt": "Set selected reflection attempts to 1 before adopting progressive sampling."
|
||||
},
|
||||
"he": {
|
||||
"render.cpu_fixed_retained": "הדגימה הקבועה נשמרה",
|
||||
"render.cpu_progressive_adopt": "שימוש בדגימה הדרגתית",
|
||||
"render.cpu_progressive_adopt_help": "מעבר לדגימה הדרגתית משתמש בדגימת מצלמה אחת לכל מעבר. גודל התמונה, האורות והגדרות ההחזר נשארים ללא שינוי.",
|
||||
"render.cpu_progressive_always": "דגימה הדרגתית פעילה תמיד",
|
||||
"render.cpu_progressive_reflection_adopt": "יש לקבוע את מספר ניסיונות ההחזר שנבחרו ל־1 לפני המעבר לדגימה הדרגתית."
|
||||
},
|
||||
"ja": {
|
||||
"render.cpu_fixed_retained": "固定サンプリングを維持",
|
||||
"render.cpu_progressive_adopt": "プログレッシブサンプリングを使用",
|
||||
"render.cpu_progressive_adopt_help": "切り替えると各パスで1つのカメラサンプルを使用します。画像サイズ、照明、反射設定は変わりません。",
|
||||
"render.cpu_progressive_always": "プログレッシブサンプリングは常に有効です",
|
||||
"render.cpu_progressive_reflection_adopt": "切り替える前に、選択した反射の試行回数を1に設定してください。"
|
||||
},
|
||||
"ko": {
|
||||
"render.cpu_fixed_retained": "고정 샘플링 유지",
|
||||
"render.cpu_progressive_adopt": "점진적 샘플링 사용",
|
||||
"render.cpu_progressive_adopt_help": "전환하면 패스마다 카메라 샘플 하나를 사용합니다. 이미지 크기, 조명 및 반사 설정은 유지됩니다.",
|
||||
"render.cpu_progressive_always": "점진적 샘플링은 항상 켜져 있습니다",
|
||||
"render.cpu_progressive_reflection_adopt": "전환하기 전에 선택한 반사의 시도 횟수를 1로 설정하세요."
|
||||
},
|
||||
"vi": {
|
||||
"render.cpu_fixed_retained": "Đã giữ lấy mẫu cố định",
|
||||
"render.cpu_progressive_adopt": "Dùng lấy mẫu lũy tiến",
|
||||
"render.cpu_progressive_adopt_help": "Lấy mẫu lũy tiến sử dụng một mẫu máy ảnh cho mỗi lượt. Kích thước ảnh, nguồn sáng và thiết lập phản xạ không thay đổi.",
|
||||
"render.cpu_progressive_always": "Lấy mẫu lũy tiến luôn bật",
|
||||
"render.cpu_progressive_reflection_adopt": "Đặt số lần thử phản xạ đã chọn thành 1 trước khi chuyển sang lấy mẫu lũy tiến."
|
||||
},
|
||||
"zh": {
|
||||
"render.cpu_fixed_retained": "保留固定采样",
|
||||
"render.cpu_progressive_adopt": "使用渐进采样",
|
||||
"render.cpu_progressive_adopt_help": "启用后每轮使用一个相机样本。图像大小、灯光和反射设置保持不变。",
|
||||
"render.cpu_progressive_always": "渐进采样始终开启",
|
||||
"render.cpu_progressive_reflection_adopt": "启用渐进采样前,请将所选反射的尝试次数设为1。"
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.quality_budget_help": "يضبط الحجم والعينات والعمق والبذرة وتقليل التشويش. تُحفظ إعدادات الصورة الأخرى.",
|
||||
"render.quality_denoise_off": "تقليل التشويش: متوقف",
|
||||
"render.quality_denoise_on": "تقليل التشويش: مفعّل"
|
||||
},
|
||||
"de": {
|
||||
"render.quality_budget_help": "Legt Größe, Samples, Tiefe, Startwert und Rauschreduzierung fest. Andere Bildeinstellungen bleiben erhalten.",
|
||||
"render.quality_denoise_off": "Rauschreduzierung: Aus",
|
||||
"render.quality_denoise_on": "Rauschreduzierung: Ein"
|
||||
},
|
||||
"en": {
|
||||
"render.quality_budget_help": "Sets size, samples, depth, seed and noise reduction. Other image settings are kept.",
|
||||
"render.quality_denoise_off": "Noise reduction: Off",
|
||||
"render.quality_denoise_on": "Noise reduction: On"
|
||||
},
|
||||
"he": {
|
||||
"render.quality_budget_help": "קובע גודל, דגימות, עומק, זרע והפחתת רעש. הגדרות התמונה האחרות נשמרות.",
|
||||
"render.quality_denoise_off": "הפחתת רעש: כבויה",
|
||||
"render.quality_denoise_on": "הפחתת רעש: פעילה"
|
||||
},
|
||||
"ja": {
|
||||
"render.quality_budget_help": "サイズ、サンプル数、深度、シード、ノイズ低減を設定します。他の画像設定は維持されます。",
|
||||
"render.quality_denoise_off": "ノイズ低減:オフ",
|
||||
"render.quality_denoise_on": "ノイズ低減:オン"
|
||||
},
|
||||
"ko": {
|
||||
"render.quality_budget_help": "크기, 샘플 수, 깊이, 시드 및 노이즈 제거를 설정합니다. 다른 이미지 설정은 유지됩니다.",
|
||||
"render.quality_denoise_off": "노이즈 제거: 꺼짐",
|
||||
"render.quality_denoise_on": "노이즈 제거: 켜짐"
|
||||
},
|
||||
"vi": {
|
||||
"render.quality_budget_help": "Đặt kích thước, số mẫu, độ sâu, hạt giống và giảm nhiễu. Giữ nguyên các thiết lập ảnh khác.",
|
||||
"render.quality_denoise_off": "Giảm nhiễu: Tắt",
|
||||
"render.quality_denoise_on": "Giảm nhiễu: Bật"
|
||||
},
|
||||
"zh": {
|
||||
"render.quality_budget_help": "设置尺寸、采样数、深度、种子和降噪。其他图像设置保持不变。",
|
||||
"render.quality_denoise_off": "降噪:关闭",
|
||||
"render.quality_denoise_on": "降噪:开启"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.quality_summary": "{width} × {height} · {samples} عينة · العمق {depth} · البذرة {seed} · {denoise}"
|
||||
},
|
||||
"de": {
|
||||
"render.quality_summary": "{width} × {height} · {samples} Samples · Tiefe {depth} · Startwert {seed} · {denoise}"
|
||||
},
|
||||
"en": {
|
||||
"render.quality_summary": "{width} × {height} · {samples} samples · depth {depth} · seed {seed} · {denoise}"
|
||||
},
|
||||
"he": {
|
||||
"render.quality_summary": "{width} × {height} · {samples} דגימות · עומק {depth} · זרע {seed} · {denoise}"
|
||||
},
|
||||
"ja": {
|
||||
"render.quality_summary": "{width} × {height} · {samples} サンプル · 深度 {depth} · シード {seed} · {denoise}"
|
||||
},
|
||||
"ko": {
|
||||
"render.quality_summary": "{width} × {height} · 샘플 {samples}개 · 깊이 {depth} · 시드 {seed} · {denoise}"
|
||||
},
|
||||
"vi": {
|
||||
"render.quality_summary": "{width} × {height} · {samples} mẫu · độ sâu {depth} · hạt giống {seed} · {denoise}"
|
||||
},
|
||||
"zh": {
|
||||
"render.quality_summary": "{width} × {height} · {samples} 次采样 · 深度 {depth} · 种子 {seed} · {denoise}"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"render.live.source_current": "{method} · مباشر",
|
||||
"render.live.source_outdated": "{method} · الصورة السابقة",
|
||||
"render.live.source_outdated_help": "يعرض آخر صورة ملتقطة أثناء تحديث التصيير المباشر أو عند عدم توفره. قد تختلف عن المشهد أو إعدادات الصورة الحالية.",
|
||||
"render.live.source_settings_help": "افتح «الإخراج» لتعديل المصيّر وإعدادات الصورة."
|
||||
},
|
||||
"de": {
|
||||
"render.live.source_current": "{method} · Live",
|
||||
"render.live.source_outdated": "{method} · Vorheriges Bild",
|
||||
"render.live.source_outdated_help": "Zeigt das zuletzt aufgenommene Bild, während das Live-Rendering aktualisiert wird oder nicht verfügbar ist. Es kann von der aktuellen Szene oder den Bildeinstellungen abweichen.",
|
||||
"render.live.source_settings_help": "Ausgabe öffnen, um Renderer und Bildeinstellungen zu bearbeiten."
|
||||
},
|
||||
"en": {
|
||||
"render.live.source_current": "{method} · Live",
|
||||
"render.live.source_outdated": "{method} · Previous image",
|
||||
"render.live.source_outdated_help": "Shows the last captured image while live rendering updates or is unavailable. It may differ from the current scene or image settings.",
|
||||
"render.live.source_settings_help": "Open Output to edit the renderer and image settings."
|
||||
},
|
||||
"he": {
|
||||
"render.live.source_current": "{method} · חי",
|
||||
"render.live.source_outdated": "{method} · תמונה קודמת",
|
||||
"render.live.source_outdated_help": "מציג את התמונה האחרונה שנלכדה בזמן שההדמיה החיה מתעדכנת או אינה זמינה. היא עשויה להיות שונה מהסצנה או מהגדרות התמונה הנוכחיות.",
|
||||
"render.live.source_settings_help": "יש לפתוח את חלונית הפלט כדי לערוך את מנוע ההדמיה ואת הגדרות התמונה."
|
||||
},
|
||||
"ja": {
|
||||
"render.live.source_current": "{method} · ライブ",
|
||||
"render.live.source_outdated": "{method} · 前の画像",
|
||||
"render.live.source_outdated_help": "ライブレンダリングの更新中や利用できない間は、最後に取得した画像を表示します。現在のシーンや画像設定とは異なる場合があります。",
|
||||
"render.live.source_settings_help": "出力を開いてレンダラーと画像設定を編集します。"
|
||||
},
|
||||
"ko": {
|
||||
"render.live.source_current": "{method} · 라이브",
|
||||
"render.live.source_outdated": "{method} · 이전 이미지",
|
||||
"render.live.source_outdated_help": "라이브 렌더링이 업데이트 중이거나 사용할 수 없는 동안 마지막으로 캡처한 이미지를 표시합니다. 현재 장면 또는 이미지 설정과 다를 수 있습니다.",
|
||||
"render.live.source_settings_help": "출력을 열어 렌더러와 이미지 설정을 편집합니다."
|
||||
},
|
||||
"vi": {
|
||||
"render.live.source_current": "{method} · Trực tiếp",
|
||||
"render.live.source_outdated": "{method} · Ảnh trước",
|
||||
"render.live.source_outdated_help": "Hiển thị ảnh đã chụp gần nhất trong khi kết xuất trực tiếp đang cập nhật hoặc không khả dụng. Ảnh có thể khác cảnh hoặc thiết lập ảnh hiện tại.",
|
||||
"render.live.source_settings_help": "Mở mục Đầu ra để sửa bộ kết xuất và thiết lập ảnh."
|
||||
},
|
||||
"zh": {
|
||||
"render.live.source_current": "{method} · 实时",
|
||||
"render.live.source_outdated": "{method} · 上一幅图像",
|
||||
"render.live.source_outdated_help": "实时渲染更新或不可用时显示上次捕获的图像。它可能与当前场景或图像设置不同。",
|
||||
"render.live.source_settings_help": "打开输出以编辑渲染器和图像设置。"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"prefs.layout_page.hover_panel_menus": "تبديل قوائم اللوحات عند مرور المؤشر",
|
||||
"prefs.layout_page.hover_panel_menus_help": "انقر على اسم لوحة في الشريط لفتح قائمتها. مرّر المؤشر فوق اسم لوحة أخرى للتبديل. حرّك المؤشر خارج الشريط والقائمة المفتوحة لإغلاقها."
|
||||
},
|
||||
"de": {
|
||||
"prefs.layout_page.hover_panel_menus": "Bereichsmenüs beim Darüberfahren wechseln",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Klicken Sie auf einen Bereichsnamen, um das Menü zu öffnen. Bewegen Sie den Zeiger auf einen anderen Bereichsnamen, um das Menü zu wechseln. Außerhalb des Menübands und des geöffneten Menüs schließt es sich."
|
||||
},
|
||||
"en": {
|
||||
"prefs.layout_page.hover_panel_menus": "Switch panel menus on hover",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Click a ribbon panel name to open its menu. Hover another panel name to switch. Move outside the ribbon and open menu to close it."
|
||||
},
|
||||
"he": {
|
||||
"prefs.layout_page.hover_panel_menus": "מעבר בין תפריטי לוחות בריחוף",
|
||||
"prefs.layout_page.hover_panel_menus_help": "לחצו על שם לוח ברצועת הכלים כדי לפתוח את התפריט שלו. העבירו את הסמן מעל שם של לוח אחר כדי לעבור לתפריט שלו. הזיזו את הסמן אל מחוץ לרצועת הכלים ולתפריט הפתוח כדי לסגור אותו."
|
||||
},
|
||||
"ja": {
|
||||
"prefs.layout_page.hover_panel_menus": "ポイントしてパネルメニューを切り替える",
|
||||
"prefs.layout_page.hover_panel_menus_help": "リボンのパネル名をクリックしてメニューを開きます。別のパネル名にポインターを合わせると切り替わります。リボンと開いているメニューの外に移動すると閉じます。"
|
||||
},
|
||||
"ko": {
|
||||
"prefs.layout_page.hover_panel_menus": "포인터를 올려 패널 메뉴 전환",
|
||||
"prefs.layout_page.hover_panel_menus_help": "리본의 패널 이름을 클릭하여 메뉴를 엽니다. 다른 패널 이름에 포인터를 올리면 메뉴가 전환됩니다. 리본과 열린 메뉴 밖으로 포인터를 이동하면 메뉴가 닫힙니다."
|
||||
},
|
||||
"vi": {
|
||||
"prefs.layout_page.hover_panel_menus": "Chuyển menu nhóm khi di chuột",
|
||||
"prefs.layout_page.hover_panel_menus_help": "Nhấp vào tên nhóm trên dải công cụ để mở menu. Di chuột lên tên nhóm khác để chuyển menu. Di chuyển con trỏ ra ngoài dải công cụ và menu đang mở để đóng menu."
|
||||
},
|
||||
"zh": {
|
||||
"prefs.layout_page.hover_panel_menus": "悬停切换面板菜单",
|
||||
"prefs.layout_page.hover_panel_menus_help": "点击功能区的面板名称打开菜单。将指针悬停在另一面板名称上即可切换菜单。将指针移到功能区和已打开菜单之外即可关闭菜单。"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "The origin axis is perpendicular to the sketch plane, so it has no line in the plane"
|
||||
},
|
||||
"ar": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "محور الأصل عمودي على مستوى الرسم، لذا ليس له خط في المستوى"
|
||||
},
|
||||
"de": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Die Ursprungsachse steht senkrecht auf der Skizzenebene und hat daher keine Linie in ihr"
|
||||
},
|
||||
"he": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "ציר הראשית ניצב למישור הסקיצה, ולכן אין לו קו במישור"
|
||||
},
|
||||
"ja": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "原点軸はスケッチ平面に垂直なため、平面内に線がありません"
|
||||
},
|
||||
"ru": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Ось начала координат перпендикулярна плоскости эскиза, поэтому линии в этой плоскости нет"
|
||||
},
|
||||
"vi": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "Trục gốc vuông góc với mặt phẳng phác thảo nên không có đường thẳng trong mặt phẳng này"
|
||||
},
|
||||
"zh": {
|
||||
"The origin axis is perpendicular to the sketch plane, so it has no line in the plane": "原点轴垂直于草图平面,因此在该平面内没有直线"
|
||||
}
|
||||
}
|
||||
@@ -934,6 +934,8 @@
|
||||
"cmd.inspect_accessibility": "Analiza dostępności",
|
||||
"cmd.inspect_center_of_mass": "Środek masy",
|
||||
"cmd.inspect_clearance": "Prześwit",
|
||||
"cmd.inspect_colored_components": "Włącz kolory komponentów",
|
||||
"cmd.inspect_colored_components_disable": "Wyłącz kolory komponentów",
|
||||
"cmd.inspect_compare": "Porównaj bryły",
|
||||
"cmd.inspect_continuity": "Analiza ciągłości",
|
||||
"cmd.inspect_curvature_comb": "Analiza grzebienia krzywizny",
|
||||
@@ -1984,8 +1986,6 @@
|
||||
"prefs.shortcuts": "Skróty",
|
||||
"prefs.single_surface_analysis": "Używaj jednej analizy powierzchni",
|
||||
"prefs.single_surface_analysis_help": "Gdy wyłączone, cieniowanie powierzchni i Ciągłość mogą być włączone jednocześnie. Włączenie zachowuje ostatnio uruchomioną lub zmienioną analizę.",
|
||||
"prefs.sketch.ellipse_axes": "Rysuj elipsy z osiami",
|
||||
"prefs.sketch.ellipse_axes_note": "Oś wielka i mała jako linie konstrukcyjne, aby można było wymiarować półosie i osie. Wyłączone: sama elipsa, a D na niej daje promień małej półosi, gdziekolwiek przeciągnąć wartość, rysowany i oznaczony R jak dotąd.",
|
||||
"prefs.sketch_align_points": "Wyrównuj do innych punktów szkicu podczas rysowania",
|
||||
"prefs.sketch_align_points_note": "Podczas rysowania punkt wyrównuje się jednocześnie poziomo z jednym punktem szkicu i pionowo z innym, z kropkowaną linią pomocniczą, a kliknięcie zachowuje oba wyrównania. Wyłączone: tylko z poprzednim punktem, po jednej osi.",
|
||||
"prefs.sketch_conflict_colour": "Geometria w konflikcie w kolorze konfliktu",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Analiza accesibilității",
|
||||
"cmd.inspect_center_of_mass": "Centru de masă",
|
||||
"cmd.inspect_clearance": "Joc",
|
||||
"cmd.inspect_colored_components": "Activează culorile componentelor",
|
||||
"cmd.inspect_colored_components_disable": "Dezactivează culorile componentelor",
|
||||
"cmd.inspect_compare": "Compară corpurile",
|
||||
"cmd.inspect_compare_picture": "Compară imaginea",
|
||||
"cmd.inspect_continuity": "Analiza continuității",
|
||||
|
||||
@@ -956,6 +956,8 @@
|
||||
"cmd.inspect_accessibility": "Анализ доступности",
|
||||
"cmd.inspect_center_of_mass": "Центр масс",
|
||||
"cmd.inspect_clearance": "Зазор",
|
||||
"cmd.inspect_colored_components": "Включить цвета компонентов",
|
||||
"cmd.inspect_colored_components_disable": "Отключить цвета компонентов",
|
||||
"cmd.inspect_compare": "Сравнить тела",
|
||||
"cmd.inspect_compare_picture": "Сравнить с изображением",
|
||||
"cmd.inspect_continuity": "Анализ непрерывности",
|
||||
@@ -1720,7 +1722,7 @@
|
||||
"help.view.tool": "Закрепляет инструмент камеры (орбиту, панорамирование или масштаб), так что перетаскивание левой кнопкой мыши двигает вид, пока вы его не отключите. Используйте на трекпаде или без средней кнопки мыши.",
|
||||
"help.view.visual_style": "Переключает способ отрисовки модели: тонированная с рёбрами или без них, каркас или со скрытыми рёбрами. Используйте каркас, чтобы заглянуть внутрь, а тонированный вид — для чистого вида.",
|
||||
"history.sketch_under_constrained": "Недоопределён (степеней свободы: {count})",
|
||||
"history.sketch_under_constrained_one": "Недоопределён ({count} степень свободы)",
|
||||
"history.sketch_under_constrained_one": "Недоопределён (степеней свободы: {count})",
|
||||
"hole_presets.confirm_delete": "Подтвердить удаление",
|
||||
"hole_presets.delete": "Удалить",
|
||||
"hole_presets.delete_prompt": "Удалить этот личный пресет? Существующие отверстия не изменятся.",
|
||||
@@ -2133,7 +2135,7 @@
|
||||
"prefs.applies_on_restart": "Применяется после перезапуска",
|
||||
"prefs.assistant": "Ассистент",
|
||||
"prefs.auto_operation": "Выбирать операцию автоматически",
|
||||
"prefs.auto_operation_note": "Выбор «Объединить», «Новое тело» или «Вырезать» по тому, чего касается материал.",
|
||||
"prefs.auto_operation_note": "Окна операций сами выбирают «Объединить», «Новое тело» или «Вырезать» в зависимости от того, чего касается новый материал, пока вы не выберете операцию вручную. Выкл.: окна сохраняют ту операцию, с которой открылись.",
|
||||
"prefs.axes": "Оси",
|
||||
"prefs.axis": "Ось",
|
||||
"prefs.bodies_folders": "Папки тел",
|
||||
@@ -2372,7 +2374,7 @@
|
||||
"prefs.stand_up_unknown.ask": "Спрашивать в диалоговом окне",
|
||||
"prefs.stand_up_unknown.note": "Показывать уведомление",
|
||||
"prefs.stand_up_unknown.off": "Не предлагать",
|
||||
"prefs.stand_up_unknown_note": "Как делается предложение, когда в файле не указана известная программа.",
|
||||
"prefs.stand_up_unknown_note": "Как делается предложение, когда в заголовке файла не указана программа, о которой известно, что она сохраняет с осью Y вверх. Уведомление показывается, только если файл, судя по всему, лежит на спине; «Спрашивать в диалоговом окне» спрашивает о каждом таком файле, как раньше.",
|
||||
"prefs.status_bar": "Строка состояния",
|
||||
"prefs.status_bar_note": "Тонкая полоса вдоль нижнего края окна показывает, что изменяется, состояние открытого эскиза его цветом и единицы дизайна; щелчок по единицам изменяет их. Состояние эскиза при этом больше не показывается в углу вида.",
|
||||
"prefs.status_done": "Готово",
|
||||
@@ -3876,7 +3878,7 @@
|
||||
"status.navigation": "Назначение кнопок мыши: {mapping}",
|
||||
"status.navigation_tip": "Как мышь двигает вид. Щёлкните, чтобы выбрать другое назначение",
|
||||
"status.under_constrained": "Недоопределён (степеней свободы: {count})",
|
||||
"status.under_constrained_one": "Недоопределён ({count} степень свободы)",
|
||||
"status.under_constrained_one": "Недоопределён (степеней свободы: {count})",
|
||||
"status.units_tip": "Единицы дизайна. Щёлкните, чтобы изменить",
|
||||
"the angle must be a number": "угол должен быть числом",
|
||||
"the curves must be in one sketch": "кривые должны быть в одном эскизе",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Анализа доступности",
|
||||
"cmd.inspect_center_of_mass": "Центар масе",
|
||||
"cmd.inspect_clearance": "Провера зазора",
|
||||
"cmd.inspect_colored_components": "Омогући боје компоненти",
|
||||
"cmd.inspect_colored_components_disable": "Онемогући боје компоненти",
|
||||
"cmd.inspect_compare": "Упореди тела",
|
||||
"cmd.inspect_compare_picture": "Упореди слику",
|
||||
"cmd.inspect_continuity": "Анализа континуитета",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Tillgänglighetsanalys",
|
||||
"cmd.inspect_center_of_mass": "Masscentrum",
|
||||
"cmd.inspect_clearance": "Frigång",
|
||||
"cmd.inspect_colored_components": "Aktivera komponentfärger",
|
||||
"cmd.inspect_colored_components_disable": "Inaktivera komponentfärger",
|
||||
"cmd.inspect_compare": "Jämför kroppar",
|
||||
"cmd.inspect_compare_picture": "Jämför bild",
|
||||
"cmd.inspect_continuity": "Kontinuitetsanalys",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "Erişilebilirlik analizi",
|
||||
"cmd.inspect_center_of_mass": "Kütle merkezi",
|
||||
"cmd.inspect_clearance": "Açıklık",
|
||||
"cmd.inspect_colored_components": "Bileşen Renklerini Etkinleştir",
|
||||
"cmd.inspect_colored_components_disable": "Bileşen Renklerini Devre Dışı Bırak",
|
||||
"cmd.inspect_compare": "Gövdeleri karşılaştır",
|
||||
"cmd.inspect_compare_picture": "Görüntüyle karşılaştır",
|
||||
"cmd.inspect_continuity": "Süreklilik kontrolü",
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
"Bottom Right": "Dưới cùng bên phải",
|
||||
"Boundary": "Biên dạng bao",
|
||||
"Bounding box": "Hộp bao ngoài",
|
||||
"Bridge": "Cầu nối",
|
||||
"CHECK": "KIỂM TRA",
|
||||
"COMPONENTS": "CHI TIẾT",
|
||||
"CONFIGURATIONS": "CẤU HÌNH",
|
||||
@@ -964,7 +963,8 @@
|
||||
"cmd.inspect_analysis": "Phân tích khung nhìn",
|
||||
"cmd.inspect_center_of_mass": "Trọng tâm",
|
||||
"cmd.inspect_clearance": "Khoảng cách an toàn",
|
||||
"cmd.inspect_colored_components": "Màu cụm chi tiết",
|
||||
"cmd.inspect_colored_components": "Bật màu cụm chi tiết",
|
||||
"cmd.inspect_colored_components_disable": "Tắt màu cụm chi tiết",
|
||||
"cmd.inspect_compare": "So sánh các thân khối",
|
||||
"cmd.inspect_compare_picture": "So sánh với ảnh tham chiếu",
|
||||
"cmd.inspect_continuity": "Phân tích độ liên tục mặt",
|
||||
@@ -1296,7 +1296,6 @@
|
||||
"cmd.timeline_stop": "Dừng lịch sử",
|
||||
"cmd.timeline_suppress": "Tạm nén tính năng",
|
||||
"cmd.ui.assistant": "Bật/tắt trợ lý",
|
||||
"cmd.view.navigator": "Khối điều hướng góc nhìn tùy chỉnh",
|
||||
"cmd.view.render_browser": "Trình duyệt kết xuất",
|
||||
"cmd.view.render_dock": "Di chuyển neo kết xuất",
|
||||
"cmd.view.render_focus": "Chế độ xem tập trung kết xuất",
|
||||
@@ -1395,7 +1394,6 @@
|
||||
"dialog.computing": "Đang tính toán… {seconds} s",
|
||||
"dialog.could_not_be_checked": "không thể kiểm tra",
|
||||
"dialog.custom_axis": "↖ Trục tùy chỉnh",
|
||||
"dialog.cylinder_face_tip": "Bề mặt hoặc đỉnh của mô hình mà khối trụ đứng trên đó: nhấp vào đây, sau đó nhấp vào nó trong khung nhìn. Để trống thì X, Y và Z đặt vị trí.",
|
||||
"dialog.dimension_insert_hint": "Nhấp vào một kích thước điều khiển khác để chèn tên của nó",
|
||||
"dialog.dimension_name": "Kích thước: {name}",
|
||||
"dialog.dimension_step_down": "Giảm {step}",
|
||||
@@ -1404,8 +1402,6 @@
|
||||
"dialog.edit_title": "CHỈNH SỬA {title}",
|
||||
"dialog.no_shown_body": "Không có thân khối đang hiển thị để thay đổi: hãy hiển thị một thân khối, hoặc chọn “Tạo mới Thân khối”",
|
||||
"dialog.pattern_sketch_not_object": "Hãy chọn thân khối hoặc tính năng: bản phác thảo không phải là cả hai. Lặp lại các đường cong của nó bằng Mảng hoặc Lấy đối xứng trong chính bản phác thảo, hoặc đùn khối nó trước.",
|
||||
"dialog.primitive_point_tip": "Một điểm điền vào X, Y và Z: nhấp vào đây, sau đó nhấp vào một đỉnh, cạnh hoặc bề mặt của mô hình, một điểm của bản phác thảo, hoặc gốc tọa độ.",
|
||||
"dialog.reference_edge_tip": "Một cạnh thẳng của bề mặt đã chọn, từ đó đo khoảng cách bên dưới: nhấp vào đây, sau đó nhấp vào cạnh.",
|
||||
"dialog.select": "↖ Chọn",
|
||||
"dialog.selected_count": "↖ {count} đã chọn",
|
||||
"dialog.selection_constraint": "Ràng buộc {id}",
|
||||
@@ -1449,9 +1445,6 @@
|
||||
"each rail's curves must be in one sketch": "các đường cong của mỗi đường dẫn hướng phải nằm trong cùng một phác thảo",
|
||||
"feature computation ended without a result": "việc tính toán đặc trưng kết thúc mà không có kết quả",
|
||||
"file.open.drawing_filter": "Thiết kế, tệp CAD, lưới đa giác và bản vẽ",
|
||||
"file.open.portal_fallback.no_portal_service": "Bộ chọn tệp đã mở không phải của riêng desktop: dịch vụ chọn tệp của desktop không khả dụng.",
|
||||
"file.open.portal_fallback.no_session_bus": "Bộ chọn tệp đã mở không phải của riêng desktop: không có bus phiên D-Bus.",
|
||||
"file.open.portal_fallback.xwayland": "Bộ chọn tệp đã mở không phải của riêng desktop: dưới XWayland, portal từ chối cửa sổ cha.",
|
||||
"file_load.built": "Đã dựng được: {built} trên {total} tính năng.",
|
||||
"file_load.converting": "Đang chuyển đổi khối đặc {done} trên {total}",
|
||||
"file_load.inserting": "Đang chèn hình học",
|
||||
@@ -2042,8 +2035,6 @@
|
||||
"nav.blender.help": "Kéo chuột giữa để xoay, Shift+chuột giữa để trượt, Ctrl+chuột giữa để thu phóng; kéo chuột phải giữ nguyên camera",
|
||||
"nav.creo.help": "Kéo nút giữa để xoay, Shift+nút giữa để di chuyển, Ctrl+kéo nút giữa theo chiều dọc để thu phóng (kéo xuống để phóng to); lăn con lăn để thu phóng",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "Kéo bằng nút giữa hoặc nút phải sẽ tiếp tục xoay, di chuyển hoặc thu phóng như lúc bắt đầu cho đến khi nhả nút, dù trong lúc đó có nhấn hay nhả phím nào. Tắt: các phím đang giữ quyết định ở mọi thời điểm của thao tác kéo.",
|
||||
"nav.drag_keeps_mode.label": "Thao tác kéo giữ nguyên kiểu điều hướng lúc bắt đầu",
|
||||
"nav.free_orbit.help": "Chuột và chuột 3D xoay tự do qua cả hai cực và giữ nguyên độ lăn nghiêng (Roll). Bắt dính trục bằng phím Shift sẽ bị tắt.",
|
||||
"nav.free_orbit.label": "Xoay góc nhìn tự do",
|
||||
"nav.free_orbit.snap_help": "Chuột và chuột 3D xoay qua cả hai cực và giữ nguyên góc nghiêng cuộn. Giữ Shift trong khi kéo xoay quỹ đạo để bắt dính hướng nhìn và góc nghiêng cuộn.",
|
||||
@@ -2280,9 +2271,6 @@
|
||||
"prefs.auto_operation_note": "Nối, Vật thể mới hoặc Cắt, tùy vào phần vật liệu tiếp xúc.",
|
||||
"prefs.axes": "Các trục tọa độ",
|
||||
"prefs.axis": "Trục tọa độ",
|
||||
"prefs.background_choose": "Chọn ảnh…",
|
||||
"prefs.background_image": "Ảnh",
|
||||
"prefs.background_unreadable": "Không đọc được ảnh nền",
|
||||
"prefs.blur_cost": "Chi phí kết xuất tương đối. Nhiều thanh hơn thường có nghĩa là cần xử lý nhiều hơn.",
|
||||
"prefs.blur_device": "Không khả dụng trên thiết bị đồ họa hoặc đường kết xuất này",
|
||||
"prefs.blur_guard": "Chất lượng làm mờ đã giảm để duy trì độ phản hồi của khung hình. Bán kính không thay đổi.",
|
||||
@@ -2304,8 +2292,6 @@
|
||||
"prefs.browser_eye_left": "Biểu tượng mắt cạnh mũi tên thu gọn",
|
||||
"prefs.browser_eye_left_note": "Các hàng trong cây thư mục hiển thị biểu tượng mắt bật/tắt cạnh mũi tên thu gọn như trước đây, thay vì trong một cột ở đầu bên phải của các hàng.",
|
||||
"prefs.browser_reveal_on_hover": "Hiện khi di chuột",
|
||||
"prefs.browser_rows_start_closed": "Các dòng trong cây thư mục đóng sẵn",
|
||||
"prefs.browser_rows_start_closed_note": "Vị trí đặt và thư mục trong cây thư mục bắt đầu ở trạng thái đóng, nên thiết kế có nhiều thành phần chỉ hiện các dòng cấp trên cùng. Mỗi dòng mở bằng mũi tên của nó, “Mở rộng tất cả” hoặc “Tìm trong cây đối tượng”, lệnh này chỉ mở các dòng dẫn tới thân khối và đóng lại các dòng mà lần tìm trước đã mở. Chọn một thân khối trong khung nhìn 3D không làm thay đổi cây thư mục. Khi tắt, các thư mục mở sẵn, vị trí đặt theo tùy chọn “Thành phần mở sẵn”, và việc chọn trong khung nhìn 3D mở cây thư mục tới thân khối đó, như trước đây.",
|
||||
"prefs.browser_show_note": "Hiển thị giữ trình duyệt trên màn hình. Hiện khi di chuột ẩn nó cho đến khi con trỏ đến vị trí của nó hoặc thẻ mảnh ở cạnh trái, và nó vẫn mở khi con trỏ ở bên trong hoặc một menu của nó đang mở.",
|
||||
"prefs.browser_shown": "Hiển thị",
|
||||
"prefs.built_in": "Mặc định tích hợp",
|
||||
@@ -2437,10 +2423,6 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "Hai mũi tên cong ở góc khối cuộn khung nhìn trong mặt phẳng màn hình. Khi bật: mũi tên theo chiều kim đồng hồ cuộn ngược chiều kim đồng hồ và ngược lại. Khi tắt: mỗi mũi tên cuộn khung nhìn theo chiều cong của nó.",
|
||||
"prefs.invert_navigator_arrows": "Đảo ngược mũi tên xoay của khối điều hướng",
|
||||
"prefs.invert_navigator_arrows_note": "Các mũi tên quanh khối xoay camera theo hướng mũi tên chỉ, như trong Fusion và Inventor: mũi tên bên trái chỉ sang phải và đưa camera sang phải. Khi tắt: mũi tên xoay khối về phía cạnh chứa nó, nên mũi tên bên trái đưa camera sang trái. Hai mũi tên cuộn cong không bị ảnh hưởng.",
|
||||
"prefs.keep_unsaved_also_asks": "Vẫn hỏi khi đóng thẻ",
|
||||
"prefs.keep_unsaved_also_asks_tip": "Khi tắt, thẻ sẽ đóng mà không có hộp thoại của trình duyệt khi bản sao của mọi thiết kế chưa lưu đã được cập nhật.",
|
||||
"prefs.keep_unsaved_in_browser": "Giữ công việc chưa lưu trong trình duyệt này",
|
||||
"prefs.keep_unsaved_in_browser_tip": "Các thay đổi chưa lưu của mỗi thiết kế đang mở được cập nhật liên tục trong bộ nhớ riêng của trình duyệt này. Lần truy cập sau sẽ đề nghị khôi phục chúng.",
|
||||
"prefs.keep_work_on_tab_switch": "Giữ hộp thoại và công cụ mở khi chuyển giữa các thiết kế",
|
||||
"prefs.keep_work_on_tab_switch_note": "Mỗi thẻ thiết kế giữ hộp thoại hoặc công cụ đang mở, cùng những gì bạn đã chọn và nhập trong đó, và thẻ thanh công cụ của nó. Chúng vẫn còn đó khi bạn quay lại thiết kế. Tắt: chuyển thiết kế sẽ hủy chúng và hiển thị thẻ Mô hình.",
|
||||
"prefs.labels": "Nhãn chữ",
|
||||
@@ -2572,8 +2554,6 @@
|
||||
"prefs.section_hatch_by_part": "Theo chi tiết",
|
||||
"prefs.section_hatch_one": "Một kiểu",
|
||||
"prefs.select": "Chọn",
|
||||
"prefs.select_hidden_edges": "Chọn cạnh bị che trong chế độ tô bóng có cạnh ẩn",
|
||||
"prefs.select_hidden_edges_help": "Cho phép chọn cạnh xuyên qua bề mặt khi dùng chế độ tô bóng có cạnh ẩn, kể cả cho lệnh Fillet. Tắt để giữ cách chọn của v0.30.1. Các khối đã ẩn vẫn không thể chọn.",
|
||||
"prefs.select_one_item": "Chọn một mục",
|
||||
"prefs.select_other_hidden": "Chọn hình học ẩn bằng Chọn đối tượng khác",
|
||||
"prefs.select_other_hidden_help": "Bật: bao gồm các khối, bản phác thảo ẩn và cạnh bị che gần con trỏ. Tắt: dùng danh sách ban đầu chỉ gồm hình học đang hiển thị ngay dưới con trỏ.",
|
||||
@@ -2598,8 +2578,6 @@
|
||||
"prefs.sketch.click_selects": "Nhấp để chọn",
|
||||
"prefs.sketch.conflict_colour": "Màu xung đột",
|
||||
"prefs.sketch.drawing": "Vẽ",
|
||||
"prefs.sketch.ellipse_axes": "Vẽ hình elip kèm các trục",
|
||||
"prefs.sketch.ellipse_axes_note": "Trục lớn và trục nhỏ dưới dạng đường gióng dựng hình, để có thể ghi kích thước bán trục và trục. Tắt: chỉ vẽ hình elip, và D trên nó cho bán kính nhỏ dù kéo giá trị đến đâu, được vẽ và ghi nhãn R như trước.",
|
||||
"prefs.sketch.exit_keep": "Giữ nguyên",
|
||||
"prefs.sketch.exit_previous": "Trở về góc nhìn trước đó",
|
||||
"prefs.sketch.exit_unless_rotated": "Trừ khi đã xoay",
|
||||
@@ -2769,12 +2747,6 @@
|
||||
"recovery.just_now": "vừa xong",
|
||||
"recovery.min_ago": "{count} phút trước",
|
||||
"recovery.never_saved": "chưa từng lưu",
|
||||
"recovery.open_saved": "Mở phiên bản đã lưu",
|
||||
"recovery.restore": "Khôi phục",
|
||||
"recovery.restore_intro": "Các thay đổi chưa được lưu khi trang này đóng lần trước được giữ trong trình duyệt này.",
|
||||
"recovery.restore_row": "{name}, thay đổi {when}",
|
||||
"recovery.restore_title": "Khôi phục các thiết kế chưa lưu?",
|
||||
"recovery.restore_title_one": "Khôi phục thiết kế chưa lưu: {name}, thay đổi {when}?",
|
||||
"refused.change": "Không thể thay đổi {body}.",
|
||||
"refused.cut": "Không thể cắt {body}.",
|
||||
"refused.face_curved": "Một trong các mặt cong của nó không dùng được để mô hình hóa.",
|
||||
@@ -2789,9 +2761,9 @@
|
||||
"render.adaptive_absolute": "Ngưỡng lỗi RGB tuyệt đối",
|
||||
"render.adaptive_coverage": "Mục tiêu lỗi độ phủ (Coverage error)",
|
||||
"render.adaptive_enable": "Phân bổ mẫu bằng các mẫu thử độc lập",
|
||||
"render.adaptive_help": "Các mẫu thăm dò độc lập được loại bỏ, sau đó chọn ít nhất 16 mẫu sản xuất cho mỗi điểm ảnh, tối đa bằng Số mẫu mỗi điểm ảnh. Giới hạn tài nguyên CPU bao gồm cả hai giai đoạn. Phương sai RGB và độ phủ hướng dẫn phân bổ; điểm ảnh đạt trần vẫn có thể nhiễu. Báo cáo cuối đếm số mẫu thực tế.",
|
||||
"render.adaptive_help": "Chỉ dành cho GPU. Các mẫu thử độc lập sẽ bị loại bỏ, sau đó phân bổ tối thiểu 16 mẫu chính thức trên mỗi pixel cho đến khi đạt giới hạn mẫu. Phương sai RGB và độ phủ chỉ dùng để định hướng phân bổ; các đường truyền tia hiếm có thể bị bỏ sót và pixel đạt giới hạn vẫn có thể còn nhiễu. Báo cáo cuối cùng ghi nhận khối lượng tính toán thực tế. Không đảm bảo tuyệt đối về độ nhiễu hay thời gian.",
|
||||
"render.adaptive_minimum": "Số mẫu chính thức tối thiểu",
|
||||
"render.adaptive_pending": "Phân bổ thích ứng bằng mẫu thăm dò độc lập có trên CPU và GPU. Các tầng Fresnel có điều kiện cần GPU. Phân bổ thống kê không chứng nhận sự hội tụ.",
|
||||
"render.adaptive_pending": "GPU hỗ trợ phân bổ thích ứng qua mẫu thử độc lập và phân tầng Fresnel có điều kiện. Chưa hỗ trợ kiểm định hội tụ, đảm bảo sự kiện hiếm và truyền dẫn thích ứng trên CPU.",
|
||||
"render.adaptive_pilots": "Số mẫu thử trên mỗi pixel",
|
||||
"render.adaptive_relative": "Mục tiêu lỗi RGB tương đối",
|
||||
"render.add_ground": "Thêm mặt sàn khớp vào thiết lập",
|
||||
@@ -2852,7 +2824,7 @@
|
||||
"render.appearance_editor.replace": "Thay thuộc tính quang học đã lưu của thân này",
|
||||
"render.appearance_editor.revalidate": "Kiểm tra lại thân này",
|
||||
"render.appearance_editor.save": "Lưu các thuộc tính quang học này",
|
||||
"render.appearance_editor.save_reason": "Để lưu Kính, hãy bật nó trên thân CPU hoặc GPU hiện hành. Với lớp phủ bóng, dùng Lưu lớp phủ vào cảnh chụp. Các tệp ảnh cục bộ vẫn tạm thời.",
|
||||
"render.appearance_editor.save_reason": "Lưu các thuộc tính quang học này cần kính vật lý trên một thân GPU hiện hành. Lớp phủ và bản đồ ảnh cục bộ vẫn tạm thời.",
|
||||
"render.appearance_editor.saved": "Lưu cùng cảnh chụp này",
|
||||
"render.appearance_editor.saved_state": "Các thuộc tính quang học này được lưu cùng cảnh chụp này.",
|
||||
"render.appearance_editor.scatter_reason": "Tán xạ bên trong cần biên là khối đặc kín.",
|
||||
@@ -2948,7 +2920,7 @@
|
||||
"render.children.copy_request": "Sao chép JSON yêu cầu",
|
||||
"render.children.coverage_error": "Sai số độ phủ",
|
||||
"render.children.cpu": "Dò tia bằng CPU",
|
||||
"render.children.denoise": "Giảm nhiễu ảnh",
|
||||
"render.children.denoise": "Giảm nhiễu trong ảnh hoàn chỉnh",
|
||||
"render.children.depth": "Độ sâu đường đi",
|
||||
"render.children.distortion": "Méo",
|
||||
"render.children.edge_samples": "Mẫu cạnh",
|
||||
@@ -3080,7 +3052,7 @@
|
||||
"render.cpu_point_help": "Tối đa tám nguồn sáng điểm, được bổ sung vào ánh sáng định hướng đã chọn. Khoảng cách tính bằng mm; khoảng cách tối thiểu không được vượt quá khoảng cách tham chiếu.",
|
||||
"render.cpu_primary_samples": "Số mẫu cạnh trên mỗi pixel",
|
||||
"render.cpu_progressive": "Kết xuất lũy tiến trên CPU",
|
||||
"render.cpu_progressive_help": "Tích lũy các mẫu máy ảnh CPU mới và hiển thị từng lượt hoàn chỉnh. Phản xạ được chọn dùng một lần thử cho mỗi mẫu máy ảnh; kính được chứng nhận và dị hướng phẳng giữ các giới hạn hiện có. Mẫu thăm dò và mẫu sản xuất thích ứng dùng chung ngân sách cấu hình CPU.",
|
||||
"render.cpu_progressive_help": "Tích lũy bộ tích phân CPU hiện có theo từng lượt một mẫu chính. Các phản xạ đã chọn yêu cầu một mẫu; nguồn sáng vùng, chất điện môi và bất đẳng hướng yêu cầu chế độ 'Hình ảnh đơn'.",
|
||||
"render.cpu_progressive_limits": "Tối đa 1.048.576 pixel và tổng số tính toán (pixel × mẫu) không quá 16.777.216. Thay đổi số mẫu không bao giờ tự động đổi kích thước ảnh.",
|
||||
"render.cpu_progressive_seed": "Hạt giống lấy mẫu CPU",
|
||||
"render.cpu_progressive_spp": "Mẫu trên mỗi pixel",
|
||||
@@ -3256,7 +3228,7 @@
|
||||
"render.export_display": "Xuất: ảnh PNG hiển thị",
|
||||
"render.export_image_label": "Xuất ảnh",
|
||||
"render.export_linear": "Xuất: ảnh EXR tuyến tính thô",
|
||||
"render.exr_pending": "Dò tia trên CPU và dò đường đi trên GPU có thể giữ lại EXR tuyến tính thô. Tính năng nén, ảnh nhiều lớp và các lượt kết xuất phụ trợ vẫn chưa khả dụng.",
|
||||
"render.exr_pending": "GPU có thể giữ lại tệp EXR tuyến tính thô. Tính năng nén, ảnh nhiều lớp và các kênh kết xuất phụ trợ vẫn chưa được triển khai.",
|
||||
"render.film_grain": "Hạt nhiễu nhiếp ảnh (Film Grain)",
|
||||
"render.fit": "Thu phóng vừa sản phẩm",
|
||||
"render.fixed_camera": "Máy ảnh",
|
||||
@@ -3296,11 +3268,9 @@
|
||||
"render.frame_wide": "16:9 · Màn ảnh rộng",
|
||||
"render.gpu_depth": "Độ sâu đường truyền tia tối đa",
|
||||
"render.gpu_options_help": "Số mẫu, độ sâu, hạt giống và độ phơi sáng được lưu cùng thiết lập này. Chỉ lưu phương thức tạo ảnh đang hoạt động.",
|
||||
"render.gpu_path_crashed": "Lần trước SolidCraft đã đóng khi đang thiết lập nó trên bộ điều hợp đồ họa này, nhiều khả năng là trong trình điều khiển đồ họa, nên nó sẽ không tự thiết lập lại. Hãy dùng dò tia trên CPU, hoặc thử lại (cập nhật trình điều khiển cũng sẽ thử lại).",
|
||||
"render.gpu_path_failed": "Bộ dò đường truyền tia GPU không khả dụng",
|
||||
"render.gpu_path_first_setup": "Lần thiết lập đầu tiên trên GPU này có thể mất vài phút; SolidCraft vẫn sử dụng được bình thường. Các lần khởi động sau sẽ tái sử dụng mã đổ bóng đã biên dịch nếu trình điều khiển lưu giữ chúng.",
|
||||
"render.gpu_path_preparing": "Bộ dò đường truyền tia GPU: đang biên dịch mã đổ bóng…",
|
||||
"render.gpu_path_retry": "Thử lại",
|
||||
"render.gpu_seed": "Hạt giống ngẫu nhiên",
|
||||
"render.gpu_unavailable": "Dò đường truyền tia GPU chưa sẵn sàng trên thiết bị này.",
|
||||
"render.ground_apply": "Khớp và lưu mặt sàn",
|
||||
@@ -3619,7 +3589,7 @@
|
||||
"render.method_cpu": "Dò tia trên CPU",
|
||||
"render.method_gpu": "Dò đường truyền tia trên GPU (Path Tracing)",
|
||||
"render.method_limit_raster": "Quét mảng (Raster) dùng bản xem trước studio đục cố định. Chọn phương thức dò tia rõ ràng để dùng các điều khiển quang học được hỗ trợ.",
|
||||
"render.method_raster": "Raster trên CPU (dạng sơ đồ)",
|
||||
"render.method_raster": "Quét mảng CPU (Raster)",
|
||||
"render.navigation_help": "Camera di chuyển tương tự như trong Thiết kế (Tùy chọn cài đặt › Điều hướng). Lưu thiết lập sẽ ghi lại camera hiện tại.",
|
||||
"render.nested_enable": "Chất điện môi lồng nhau đã xác thực",
|
||||
"render.nested_help": "Tối đa 16 lớp chứa nhau; mỗi khối cha phải là khối lồi và mọi khối con phải nằm hoàn toàn bên trong. Từ chối giao cắt, bề mặt tiếp xúc, tấm mỏng và bóng đổ thẳng. Trạng thái camera bên trong tuân theo hệ thống phân cấp đã xác thực. Không tạo bề mặt chất lỏng hoặc hợp nhất các ranh giới CAD tiếp xúc.",
|
||||
@@ -3647,7 +3617,7 @@
|
||||
"render.output_exr_unavailable": "Thiết lập đầu ra EXR không khả dụng ở đây. Xuất EXR tuyến tính cần ảnh GPU đã hoàn tất với bản gốc tuyến tính được giữ lại.",
|
||||
"render.output_fit": "Vừa khít",
|
||||
"render.output_format": "Định dạng",
|
||||
"render.output_format_status": "PNG là ảnh hiển thị hoàn chỉnh chính xác. EXR tùy chọn trên CPU hoặc GPU giữ lại suất bức xạ FLOAT32 thô trong không gian tuyến tính của cảnh trước các bộ lọc hiển thị. Xuất không bao giờ thay thế ảnh bằng bộ nhớ đệm xem trước hoặc tác vụ khác.",
|
||||
"render.output_format_status": "PNG là hình ảnh hiển thị hoàn chỉnh chính xác. EXR tùy chọn trên GPU giữ lại suất bức xạ FLOAT32 tuyến tính cảnh thô trước các bộ lọc hiển thị. Xuất không bao giờ thay thế bằng bộ nhớ đệm xem trước hoặc tác vụ khác.",
|
||||
"render.output_grading": "Chỉnh màu",
|
||||
"render.output_image": "Ảnh",
|
||||
"render.output_largest": "Tối đa {width} × {height}",
|
||||
@@ -3732,7 +3702,7 @@
|
||||
"render.quality_custom": "Tùy chỉnh",
|
||||
"render.quality_draft": "Bản nháp",
|
||||
"render.quality_high": "Cao",
|
||||
"render.quality_projection_help": "Bản xem xét này chỉ thay đổi độ phân giải, ngân sách mẫu, độ sâu đường đi, hạt giống và giảm nhiễu. Các thiết lập khác được giữ nguyên; ngân sách không chứng nhận ảnh đã hội tụ.",
|
||||
"render.quality_projection_help": "Các thiết lập sẵn Q bao gồm các yêu cầu tương lai. Hành động này chỉ chiếu định mức mẫu PNG cố định được hỗ trợ và lược bỏ rõ ràng việc dừng thích ứng, đầu ra tuyến tính thô và các giới hạn truyền dẫn độc lập.",
|
||||
"render.quality_review": "Xem xét định mức lấy mẫu",
|
||||
"render.queue_pending": "Không có hàng đợi sản xuất tự động hay bộ kết xuất hoạt hình trong giao diện này. Ảnh tĩnh đã hoàn thành vẫn có thể xuất riêng lẻ.",
|
||||
"render.radial_assign": "Gán diện mạo",
|
||||
@@ -3763,7 +3733,7 @@
|
||||
"render.result_navigation": "Thu phóng và lia theo tùy chọn Điều hướng. Nhấn phím cách + kéo rê để lia.",
|
||||
"render.result_open": "Mở hình ảnh đã hoàn thành trong khung vẽ chính.",
|
||||
"render.result_preview": "Xem trước trực tiếp",
|
||||
"render.result_provisional": "Ảnh tạm",
|
||||
"render.result_provisional": "Hình ảnh CPU tạm thời",
|
||||
"render.result_provisional_help": "Điểm kiểm tra trung gian này không thể xuất. Lệnh xuất giữ lại kết quả hoàn thành gần nhất; hủy bỏ sẽ quay lại kết quả đó.",
|
||||
"render.result_reduced_display": "Bản xem trước hiển thị bị thu nhỏ theo giới hạn bộ nhớ đệm/thiết bị của khung nhìn. Tệp PNG được giữ lại và tệp xuất vẫn giữ nguyên độ phân giải đầy đủ; chế độ kiểm tra gốc 100% không khả dụng cho chế độ xem thu nhỏ này.",
|
||||
"render.result_retained": "Đang hiển thị hình ảnh hoàn thành gần nhất; kết xuất mới hơn chưa thay thế nó.",
|
||||
@@ -3927,7 +3897,7 @@
|
||||
"render.stage_panels.prepare_plan": "Chuẩn bị kế hoạch tạo",
|
||||
"render.stage_panels.projects": "Chiếu",
|
||||
"render.stage_panels.quality_ack": "Tôi xác nhận ngân sách cố định và các mục bị lược bỏ",
|
||||
"render.stage_panels.quality_omissions": "Ngân sách này không đặt mục tiêu sai số thích ứng, giới hạn truyền sáng độc lập, quy tắc roulette Nga hay việc lưu giữ bản gốc thô, và không bảo đảm ảnh hội tụ.",
|
||||
"render.stage_panels.quality_omissions": "Dừng thích ứng và mục tiêu sai số, độ sâu truyền dẫn riêng, quy tắc roulette Nga, đầu ra tuyến tính thô, bản gốc EXR và chứng nhận hội tụ.",
|
||||
"render.stage_panels.reference": "Tham chiếu",
|
||||
"render.stage_panels.references": "Các tham chiếu",
|
||||
"render.stage_panels.remove_optics": "Xóa cài đặt quang học",
|
||||
@@ -3971,7 +3941,7 @@
|
||||
"render.stage_panels.subtitle_environment": "Môi trường HDR nguyên bản · toàn bộ cảnh",
|
||||
"render.stage_panels.subtitle_maps": "Bản đồ không gian đã tạo · xem trước tạm thời",
|
||||
"render.stage_panels.subtitle_physical": "Kế hoạch chỉ đọc · không tạo hay gán vật liệu",
|
||||
"render.stage_panels.subtitle_quality": "Ngân sách lấy mẫu GPU · chỉ các trường được liệt kê",
|
||||
"render.stage_panels.subtitle_quality": "Ngân sách GPU cố định · lược bỏ các yêu cầu tương lai",
|
||||
"render.stage_panels.subtitle_rig": "Bộ đèn chiếu sáng đã thiết kế · bộ phát sáng và tấm thụ động",
|
||||
"render.stage_panels.subtitle_saved_maps": "Bản đồ đã tạo · lưu cùng khung hình này",
|
||||
"render.stage_panels.subtitle_stage": "Mặt sàn và sân khấu vật lý · khớp với sản phẩm",
|
||||
@@ -3996,7 +3966,7 @@
|
||||
"render.studio_area_help": "Tối đa bốn hình chữ nhật một mặt. Nửa cạnh trực giao U và V xác định kích thước; U × V hướng về phía chiếu sáng. Tọa độ tính theo milimét không gian thế giới.",
|
||||
"render.studio_center": "Tâm X, Y, Z (mm)",
|
||||
"render.studio_denoise": "Giảm nhiễu trong ảnh thành phẩm",
|
||||
"render.studio_denoise_help": "Ảnh lũy tiến và ảnh cuối sử dụng bộ lọc bảo toàn biên; chi tiết nhỏ có thể bị làm mềm. Kính và bề mặt phản xạ sắc nét không qua bộ lọc này.",
|
||||
"render.studio_denoise_help": "Bộ lọc cuối cùng bảo toàn cạnh; chi tiết tinh xảo có thể bị làm mềm. Thủy tinh và bề mặt phản xạ gương sắc nét bỏ qua bộ lọc này.",
|
||||
"render.studio_direction": "Hướng về phía mặt trời (X, Y, Z)",
|
||||
"render.studio_environment": "Môi trường",
|
||||
"render.studio_environment_bounds": "Góc xoay môi trường phải hữu hạn trong khoảng ±1.000.000 radian và cường độ từ 0 đến 100.",
|
||||
@@ -4198,32 +4168,6 @@
|
||||
"sketch.dof_left": "Còn {count} bậc tự do",
|
||||
"sketch.dof_left_one": "Còn {count} bậc tự do",
|
||||
"sketch.draw_curves": "Vẽ dưới dạng {linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "Chuyển đổi vùng chọn sang {linetype}",
|
||||
"sketch.open_ends": "Đầu hở: {count}",
|
||||
"sketch.status_conflict": "ràng buộc xung đột",
|
||||
@@ -4445,7 +4389,6 @@
|
||||
"ui.3d_sketch": "3D Phác thảo",
|
||||
"ui.activate": "Kích hoạt",
|
||||
"ui.active_component": "Cụm chi tiết đang kích hoạt",
|
||||
"ui.addToQuickAccess": "Thêm vào Truy cập nhanh",
|
||||
"ui.add_configuration": "Thêm cấu hình",
|
||||
"ui.add_parameter": "Thêm tham số",
|
||||
"ui.agent_chat": "Trò chuyện với AI Agent",
|
||||
@@ -5074,7 +5017,6 @@
|
||||
"ui.heading_construct": "DỰNG HÌNH",
|
||||
"ui.heading_create": "TẠO MỚI",
|
||||
"ui.heading_create_sketch": "TẠO PHÁC THẢO",
|
||||
"ui.heading_custom_view_navigator": "KHỐI ĐIỀU HƯỚNG GÓC NHÌN TÙY CHỈNH",
|
||||
"ui.heading_cylinder": "HÌNH TRỤ",
|
||||
"ui.heading_design": "THIẾT KẾ",
|
||||
"ui.heading_draft": "ĐỘ DỐC THOÁT KHUÔN",
|
||||
@@ -5361,7 +5303,6 @@
|
||||
"ui.quality": "Chất lượng",
|
||||
"ui.quantity": "Số lượng",
|
||||
"ui.quantity_2": "Số lượng 2",
|
||||
"ui.quick_access_full": "Thanh truy cập nhanh chứa tối đa {count} lệnh; hãy gỡ một lệnh để thêm lệnh khác",
|
||||
"ui.quit": "Thoát ứng dụng",
|
||||
"ui.radial_context_menu": "Menu ngữ cảnh dạng vòng (Radial)",
|
||||
"ui.radius": "Bán kính",
|
||||
@@ -5379,7 +5320,6 @@
|
||||
"ui.relink_not_linked": "Chi tiết này chưa được liên kết với thiết kế nguồn nào.",
|
||||
"ui.relink_select_one": "Hãy chọn trước một chi tiết được liên kết hoặc một thể hiện của nó.",
|
||||
"ui.relink_source_all": "Liên kết lại nguồn (tất cả thể hiện)…",
|
||||
"ui.removeFromQuickAccess": "Gỡ khỏi Truy cập nhanh",
|
||||
"ui.removeFromToolbar": "Xóa khỏi thanh công cụ",
|
||||
"ui.removeSection": "Xóa mặt cắt",
|
||||
"ui.render": "Kết xuất hình ảnh",
|
||||
@@ -5388,8 +5328,6 @@
|
||||
"ui.repeat_nothing": "Chưa có gì để lặp lại: hãy bắt đầu một công cụ hoặc hộp thoại trước.",
|
||||
"ui.replace_with_link_done": "Các bản sao hiện đã trở thành các vị trí đặt của thiết kế liên kết.",
|
||||
"ui.report_a_bug": "Báo cáo lỗi",
|
||||
"ui.report_characters_left_one": "Còn {count} ký tự",
|
||||
"ui.report_characters_left_other": "Còn {count} ký tự",
|
||||
"ui.reset": "Đặt lại",
|
||||
"ui.reset_all": "Đặt lại tất cả",
|
||||
"ui.reset_the_settings_on_this_page": "Đặt lại các cài đặt trên trang này",
|
||||
@@ -5674,14 +5612,6 @@
|
||||
"values must be numbers or expressions": "các giá trị phải là số hoặc biểu thức",
|
||||
"view.auto_ortho_faces": "Phối cảnh khi xoay quỹ đạo, trực giao khi nhìn trực diện mặt",
|
||||
"view.render_inspector": "Bảng thuộc tính kết xuất",
|
||||
"view_gizmo.arrows": "Mũi tên xoay",
|
||||
"view_gizmo.home": "Nút góc nhìn mặc định (Home)",
|
||||
"view_gizmo.no_such_body": "Thiết kế này không có thân khối nào mang tên đó.",
|
||||
"view_gizmo.note": "Chọn một thân khối trong khung nhìn. Thân khối được lưu cùng tùy chọn và hồ sơ cài đặt của bạn, và hiển thị trong mọi thiết kế. Nhấp vào một mặt của nó để nhìn thẳng vào mặt đó.",
|
||||
"view_gizmo.texture": "Kết cấu",
|
||||
"view_gizmo.themed": "Theo chủ đề",
|
||||
"view_gizmo.too_detailed": "Quá chi tiết cho khối điều hướng góc nhìn (hơn 10.000 tam giác)",
|
||||
"view_gizmo.too_thin": "Quá dài và mảnh cho khối điều hướng góc nhìn: cạnh mỏng nhất phải ít nhất bằng một phần năm cạnh dài nhất",
|
||||
"web.files.insert_drawings_hint": "Tệp CAD, lưới đa giác, DXF hoặc SVG vào thiết kế đang mở",
|
||||
"web.files.insert_stored_drawings_hint": "Vào thiết kế đang mở (tệp CAD, lưới đa giác, DXF, SVG)",
|
||||
"web.open_full": "Mở trong SolidCraft",
|
||||
|
||||
@@ -957,6 +957,8 @@
|
||||
"cmd.inspect_accessibility": "可達性分析",
|
||||
"cmd.inspect_center_of_mass": "質心",
|
||||
"cmd.inspect_clearance": "間隙",
|
||||
"cmd.inspect_colored_components": "啟用元件顏色",
|
||||
"cmd.inspect_colored_components_disable": "停用元件顏色",
|
||||
"cmd.inspect_compare": "比較實體",
|
||||
"cmd.inspect_compare_picture": "與圖片比較",
|
||||
"cmd.inspect_continuity": "連續性分析",
|
||||
@@ -1364,14 +1366,11 @@
|
||||
"dialog.computing": "正在計算… {seconds} 秒",
|
||||
"dialog.could_not_be_checked": "無法檢查",
|
||||
"dialog.custom_axis": "↖ 自訂軸",
|
||||
"dialog.cylinder_face_tip": "圓柱體所立的模型面或頂點:先點擊此處,再在視圖中點擊它。留空時由 X、Y、Z 放置。",
|
||||
"dialog.dimension_insert_hint": "點擊其他驅動尺寸以插入其名稱",
|
||||
"dialog.dimension_name": "尺寸:{name}",
|
||||
"dialog.edit_title": "編輯 {title}",
|
||||
"dialog.no_shown_body": "沒有可變更的已顯示實體:請顯示一個,或選擇「新建實體」",
|
||||
"dialog.pattern_sketch_not_object": "請選取實體或特徵:草圖兩者皆非。請用草圖自己的「陣列」或「鏡射」重複其曲線,或先將它擠出。",
|
||||
"dialog.primitive_point_tip": "填入 X、Y、Z 的點:先點擊此處,再點擊模型的頂點、邊或面、草圖中的點,或原點。",
|
||||
"dialog.reference_edge_tip": "所選面上的一條直邊,下方的距離從它量起:先點擊此處,再點擊該邊。",
|
||||
"dialog.select": "↖ 選擇",
|
||||
"dialog.selected_count": "↖ 已選擇 {count} 個",
|
||||
"dialog.selection_constraint": "拘束 {id}",
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
"Bottom Right": "右下",
|
||||
"Boundary": "边界",
|
||||
"Bounding box": "边界框",
|
||||
"Bridge": "桥接",
|
||||
"CHECK": "检查",
|
||||
"COMPONENTS": "组件",
|
||||
"CONFIGURATIONS": "配置",
|
||||
@@ -964,7 +963,8 @@
|
||||
"cmd.inspect_analysis": "视图分析",
|
||||
"cmd.inspect_center_of_mass": "质心",
|
||||
"cmd.inspect_clearance": "间隙",
|
||||
"cmd.inspect_colored_components": "组件颜色",
|
||||
"cmd.inspect_colored_components": "启用组件颜色",
|
||||
"cmd.inspect_colored_components_disable": "禁用组件颜色",
|
||||
"cmd.inspect_compare": "比较实体",
|
||||
"cmd.inspect_compare_picture": "比较参考图",
|
||||
"cmd.inspect_continuity": "连续性分析",
|
||||
@@ -1296,7 +1296,6 @@
|
||||
"cmd.timeline_stop": "停止历史播放",
|
||||
"cmd.timeline_suppress": "抑制特征",
|
||||
"cmd.ui.assistant": "切换助手显示",
|
||||
"cmd.view.navigator": "自定义视图导航器",
|
||||
"cmd.view.render_browser": "渲染浏览器",
|
||||
"cmd.view.render_dock": "移动渲染停靠面板",
|
||||
"cmd.view.render_focus": "渲染专注视图",
|
||||
@@ -1395,7 +1394,6 @@
|
||||
"dialog.computing": "正在计算… {seconds} 秒",
|
||||
"dialog.could_not_be_checked": "无法检查",
|
||||
"dialog.custom_axis": "↖ 自定义轴",
|
||||
"dialog.cylinder_face_tip": "圆柱体所立的模型面或顶点:先单击此处,再在视图中单击它。留空时由 X、Y、Z 放置。",
|
||||
"dialog.dimension_insert_hint": "单击其他驱动尺寸以插入其名称",
|
||||
"dialog.dimension_name": "尺寸:{name}",
|
||||
"dialog.dimension_step_down": "减少 {step}",
|
||||
@@ -1404,8 +1402,6 @@
|
||||
"dialog.edit_title": "编辑 {title}",
|
||||
"dialog.no_shown_body": "没有可更改的显示实体:请显示一个实体,或选择新建实体",
|
||||
"dialog.pattern_sketch_not_object": "请选择实体或特征:草图两者都不是。请用草图自身的阵列或镜像重复其曲线,或先将其拉伸。",
|
||||
"dialog.primitive_point_tip": "填入 X、Y、Z 的点:先单击此处,再单击模型的顶点、边或面、草图中的点,或原点。",
|
||||
"dialog.reference_edge_tip": "所选面上的一条直边,下方的距离从它量起:先单击此处,再单击该边。",
|
||||
"dialog.select": "↖ 选择",
|
||||
"dialog.selected_count": "↖ 已选择 {count} 个",
|
||||
"dialog.selection_constraint": "约束 {id}",
|
||||
@@ -1449,9 +1445,6 @@
|
||||
"each rail's curves must be in one sketch": "每条导轨的曲线必须位于同一个草图中",
|
||||
"feature computation ended without a result": "特征计算结束时没有返回结果",
|
||||
"file.open.drawing_filter": "设计、CAD 文件、网格和图纸",
|
||||
"file.open.portal_fallback.no_portal_service": "打开的文件选择器并非桌面自带:桌面的文件选择服务不可用。",
|
||||
"file.open.portal_fallback.no_session_bus": "打开的文件选择器并非桌面自带:没有 D-Bus 会话总线。",
|
||||
"file.open.portal_fallback.xwayland": "打开的文件选择器并非桌面自带:在 XWayland 下,门户会拒绝父窗口。",
|
||||
"file_load.built": "已构建:{built}/{total} 个特征。",
|
||||
"file_load.converting": "正在转换实体 {done}/{total}",
|
||||
"file_load.inserting": "正在插入几何体",
|
||||
@@ -2042,8 +2035,6 @@
|
||||
"nav.blender.help": "中键拖动旋转,Shift+中键拖动平移,Ctrl+中键拖动缩放;右键拖动不会移动相机",
|
||||
"nav.creo.help": "中键拖动旋转,Shift+中键平移,Ctrl+中键上下拖动缩放(向下放大);滚动滚轮也可缩放",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.drag_keeps_mode.help": "中键或右键拖动会保持开始时的旋转、平移或缩放,直到松开按键,期间按下或松开任何键都不会改变。关闭后:拖动的每一刻都由当时按住的键决定。",
|
||||
"nav.drag_keeps_mode.label": "拖动保持开始时的导航方式",
|
||||
"nav.free_orbit.help": "鼠标和3D鼠标可保持滚转并越过两个极点旋转。Shift轴吸附将被禁用。",
|
||||
"nav.free_orbit.label": "自由旋转",
|
||||
"nav.free_orbit.snap_help": "鼠标和3D鼠标可越过两极旋转,并保留画面旋转角度。在拖动环绕时按住Shift,可吸附视图方向和画面旋转角度。",
|
||||
@@ -2280,9 +2271,6 @@
|
||||
"prefs.auto_operation_note": "根据材料接触的对象选择合并、新建实体或切割。",
|
||||
"prefs.axes": "坐标轴",
|
||||
"prefs.axis": "轴",
|
||||
"prefs.background_choose": "选择图片…",
|
||||
"prefs.background_image": "图片",
|
||||
"prefs.background_unreadable": "无法读取背景图片",
|
||||
"prefs.blur_cost": "相对渲染开销。条形越多,通常意味着处理工作越多。",
|
||||
"prefs.blur_device": "此图形设备或渲染路径不支持此功能",
|
||||
"prefs.blur_guard": "已降低模糊质量,以保持画面响应。模糊半径未改变。",
|
||||
@@ -2304,8 +2292,6 @@
|
||||
"prefs.browser_eye_left": "折叠箭头旁的眼睛图标",
|
||||
"prefs.browser_eye_left_note": "浏览器各行的显示切换像以前一样位于折叠箭头旁,而不是在行右端的一列中。",
|
||||
"prefs.browser_reveal_on_hover": "悬停时显示",
|
||||
"prefs.browser_rows_start_closed": "浏览器中的行默认折叠",
|
||||
"prefs.browser_rows_start_closed_note": "浏览器中的放置和文件夹默认折叠,因此包含大量组件的设计只显示顶层行。每一行可通过其箭头、“全部展开”或“在浏览器中查找”打开;“在浏览器中查找”只展开通往该实体的行,并重新折叠上次查找展开的行。在 3D 视图中选取实体不会改变浏览器。关闭时文件夹默认展开,放置按“组件初始展开”的设置显示,在 3D 视图中选取会使浏览器展开到该实体,与以前相同。",
|
||||
"prefs.browser_show_note": "“显示”让浏览器始终留在屏幕上。“悬停时显示”会将其隐藏,直到指针移到它的位置或其左边缘的细标签上;指针在其中或其菜单打开时,它保持打开。",
|
||||
"prefs.browser_shown": "显示",
|
||||
"prefs.built_in": "内置",
|
||||
@@ -2437,10 +2423,6 @@
|
||||
"prefs.invert_curved_roll_arrows_note": "立方体角落的两个弯曲箭头在屏幕平面内翻滚视图。开启:顺时针箭头逆时针翻滚,逆时针箭头顺时针翻滚。关闭:每个箭头按其弯曲方向翻滚视图。",
|
||||
"prefs.invert_navigator_arrows": "反转视图立方体的旋转箭头",
|
||||
"prefs.invert_navigator_arrows_note": "立方体周围的箭头按箭头所指的方向转动相机,与 Fusion 和 Inventor 相同:左侧箭头指向右,相机向右转动。关闭:箭头把立方体转向它所在的那一面,因此左侧箭头使相机向左转动。弯曲的翻滚箭头不受影响。",
|
||||
"prefs.keep_unsaved_also_asks": "关闭标签页时仍然询问",
|
||||
"prefs.keep_unsaved_also_asks_tip": "关闭此项后,只要每个未保存设计的副本都是最新的,标签页就会直接关闭,不显示浏览器的提示框。",
|
||||
"prefs.keep_unsaved_in_browser": "在此浏览器中保留未保存的工作",
|
||||
"prefs.keep_unsaved_in_browser_tip": "每个打开的设计的未保存更改都会在此浏览器的私有存储中保持最新。下次访问时会提示恢复。",
|
||||
"prefs.keep_work_on_tab_switch": "切换设计时保持对话框和工具打开",
|
||||
"prefs.keep_work_on_tab_switch_note": "每个设计标签页保留其打开的对话框或工具(包括其中已选择和输入的内容)以及工具栏选项卡。回到该设计时它们仍在原处。关闭时:切换设计会取消它们并显示“模型”选项卡。",
|
||||
"prefs.labels": "标签",
|
||||
@@ -2572,8 +2554,6 @@
|
||||
"prefs.section_hatch_by_part": "按零件",
|
||||
"prefs.section_hatch_one": "单一样式",
|
||||
"prefs.select": "选择",
|
||||
"prefs.select_hidden_edges": "在带隐藏边线的着色视图中选择隐藏边",
|
||||
"prefs.select_hidden_edges_help": "在带隐藏边线的着色视图中,可穿过面选择边并用于圆角。关闭时保留v0.30.1的选择行为。隐藏的实体仍不可选择。",
|
||||
"prefs.select_one_item": "选择一个项目",
|
||||
"prefs.select_other_hidden": "使用“选择其他”选择隐藏几何体",
|
||||
"prefs.select_other_hidden_help": "开启:包括指针附近的隐藏实体、草图和被遮挡的边。关闭:恢复原来的列表,仅包含指针正下方的可见几何体。",
|
||||
@@ -2598,8 +2578,6 @@
|
||||
"prefs.sketch.click_selects": "单击选择",
|
||||
"prefs.sketch.conflict_colour": "冲突颜色",
|
||||
"prefs.sketch.drawing": "绘制",
|
||||
"prefs.sketch.ellipse_axes": "绘制椭圆时带上轴线",
|
||||
"prefs.sketch.ellipse_axes_note": "将长轴和短轴绘制为构造线,以便标注半轴和轴。关闭:仅绘制椭圆,对其按 D 得到短半径,无论将数值拖到何处,都像以前一样绘制并标注 R。",
|
||||
"prefs.sketch.exit_keep": "保持",
|
||||
"prefs.sketch.exit_previous": "返回之前的视图",
|
||||
"prefs.sketch.exit_unless_rotated": "除非已旋转",
|
||||
@@ -2769,12 +2747,6 @@
|
||||
"recovery.just_now": "刚刚",
|
||||
"recovery.min_ago": "{count} 分钟前",
|
||||
"recovery.never_saved": "从未保存",
|
||||
"recovery.open_saved": "打开已保存的版本",
|
||||
"recovery.restore": "恢复",
|
||||
"recovery.restore_intro": "此页面上次关闭时未保存的更改保留在此浏览器中。",
|
||||
"recovery.restore_row": "{name},更改于{when}",
|
||||
"recovery.restore_title": "恢复未保存的设计?",
|
||||
"recovery.restore_title_one": "恢复未保存的设计:{name},更改于{when}?",
|
||||
"refused.change": "无法更改 {body}。",
|
||||
"refused.cut": "无法切割 {body}。",
|
||||
"refused.face_curved": "它的一个曲面无法用于建模。",
|
||||
@@ -2789,9 +2761,9 @@
|
||||
"render.adaptive_absolute": "RGB 绝对误差下限",
|
||||
"render.adaptive_coverage": "覆盖率误差目标",
|
||||
"render.adaptive_enable": "使用独立预采样分配样本",
|
||||
"render.adaptive_help": "独立的先导样本会被丢弃,随后为每个像素分配至少16个、不超过“每像素样本数”的正式样本。CPU资源限制包含两个阶段。RGB与覆盖率方差决定分配,达到上限的像素仍可能有噪点。最终报告记录实际样本数。",
|
||||
"render.adaptive_help": "仅限 GPU。丢弃独立预采样后,每像素分配至少16个正式样本,最多达到每像素样本数上限。RGB 和覆盖率方差仅用于指导分配;可能遗漏罕见路径,达到上限后仍可能有噪声。最终报告记录实际工作量,不保证噪声或耗时。",
|
||||
"render.adaptive_minimum": "最少正式样本数",
|
||||
"render.adaptive_pending": "CPU和GPU均支持独立先导样本的自适应分配。条件菲涅耳分层需要GPU。统计分配不保证收敛。",
|
||||
"render.adaptive_pending": "GPU 支持独立预采样自适应分配和条件菲涅耳分层。尚不支持认证收敛、罕见事件保证及 CPU 自适应光传输。",
|
||||
"render.adaptive_pilots": "每像素预采样数",
|
||||
"render.adaptive_relative": "RGB 相对误差目标",
|
||||
"render.add_ground": "向设置添加适配地面",
|
||||
@@ -2852,7 +2824,7 @@
|
||||
"render.appearance_editor.replace": "替换此实体已保存的光学设置",
|
||||
"render.appearance_editor.revalidate": "重新验证此实体",
|
||||
"render.appearance_editor.save": "保存这些光学设置",
|
||||
"render.appearance_editor.save_reason": "要保存玻璃,请在当前CPU或GPU实体上启用它。清漆请使用“将清漆保存到镜头”。本地图像文件仍为临时设置。",
|
||||
"render.appearance_editor.save_reason": "保存需要当前 GPU 实体上的物理玻璃。清漆和本地图像贴图仍为临时设置。",
|
||||
"render.appearance_editor.saved": "随此镜头保存",
|
||||
"render.appearance_editor.saved_state": "这些光学设置已随此镜头保存。",
|
||||
"render.appearance_editor.scatter_reason": "内部散射需要封闭实体边界。",
|
||||
@@ -2948,7 +2920,7 @@
|
||||
"render.children.copy_request": "复制请求JSON",
|
||||
"render.children.coverage_error": "覆盖误差",
|
||||
"render.children.cpu": "CPU光线追踪",
|
||||
"render.children.denoise": "降低图像噪点",
|
||||
"render.children.denoise": "降低成品图像噪点",
|
||||
"render.children.depth": "路径深度",
|
||||
"render.children.distortion": "畸变",
|
||||
"render.children.edge_samples": "边缘采样",
|
||||
@@ -3080,7 +3052,7 @@
|
||||
"render.cpu_point_help": "最多可在所选平行光照明基础上添加8个点光源。距离单位为mm,最小距离不得大于参考距离。",
|
||||
"render.cpu_primary_samples": "每像素边缘采样",
|
||||
"render.cpu_progressive": "CPU渐进渲染",
|
||||
"render.cpu_progressive_help": "累积新的CPU相机样本并显示每个完整的渲染轮次。选定反射在每个相机样本中尝试一次;经认证的玻璃和平面各向异性保留原有限制。自适应先导样本和正式样本共享CPU配置预算。",
|
||||
"render.cpu_progressive_help": "使用现有CPU积分器,每轮累积一个主采样。所选反射需要单采样;面光源、电介质和各向异性需要固定图像模式。",
|
||||
"render.cpu_progressive_limits": "最多1,048,576像素,像素×采样总数不超过16,777,216。更改采样数不会自动调整图像尺寸。",
|
||||
"render.cpu_progressive_seed": "CPU采样种子",
|
||||
"render.cpu_progressive_spp": "每像素采样数",
|
||||
@@ -3256,7 +3228,7 @@
|
||||
"render.export_display": "导出:显示 PNG",
|
||||
"render.export_image_label": "导出图像",
|
||||
"render.export_linear": "导出:原始线性 EXR",
|
||||
"render.exr_pending": "CPU 光线追踪和 GPU 路径追踪可保留原始线性 EXR;压缩、多层和辅助通道仍未实现。",
|
||||
"render.exr_pending": "GPU 可保留线性 EXR;压缩、多层和辅助通道仍未实现。",
|
||||
"render.film_grain": "摄影颗粒",
|
||||
"render.fit": "适配产品视图",
|
||||
"render.fixed_camera": "相机",
|
||||
@@ -3296,11 +3268,9 @@
|
||||
"render.frame_wide": "16:9 · 宽屏",
|
||||
"render.gpu_depth": "最大路径深度",
|
||||
"render.gpu_options_help": "采样数、深度、种子和曝光会随此配置保存。仅保存当前图像方法。",
|
||||
"render.gpu_path_crashed": "上次在此图形适配器上设置时 SolidCraft 关闭了,很可能是在图形驱动程序内部,因此不会再自动设置。请改用 CPU 光线追踪,或重试(更新驱动程序后也会重试)。",
|
||||
"render.gpu_path_failed": "GPU 路径追踪器不可用",
|
||||
"render.gpu_path_first_setup": "首次在此 GPU 上设置可能需要几分钟,期间 SolidCraft 仍可正常使用。之后启动时,如果驱动程序保留了已编译的着色器,将直接复用。",
|
||||
"render.gpu_path_preparing": "GPU 路径追踪器:正在编译着色器…",
|
||||
"render.gpu_path_retry": "重试",
|
||||
"render.gpu_seed": "随机种子",
|
||||
"render.gpu_unavailable": "此设备上的 GPU 路径追踪尚未就绪。",
|
||||
"render.ground_apply": "适配并保存地面",
|
||||
@@ -3619,7 +3589,7 @@
|
||||
"render.method_cpu": "CPU 光线追踪",
|
||||
"render.method_gpu": "GPU 路径追踪",
|
||||
"render.method_limit_raster": "光栅化使用固定的不透明工作室预览;要使用受支持的光学控件,请明确选择光线追踪方式。",
|
||||
"render.method_raster": "CPU 光栅(示意图)",
|
||||
"render.method_raster": "CPU 光栅",
|
||||
"render.navigation_help": "相机的操作与设计中相同(首选项 › 导航)。保存设置会记录当前相机。",
|
||||
"render.nested_enable": "经验证的嵌套介质",
|
||||
"render.nested_help": "最多16层包含关系。父体必须凸,子体必须严格位于内部。拒绝相交、接触、薄片和直线透射阴影。内部相机使用经验证的层次结构。不会创建液面或合并接触的CAD边界。",
|
||||
@@ -3647,7 +3617,7 @@
|
||||
"render.output_exr_unavailable": "此处无法设置 EXR 输出。线性 EXR 导出需要保留线性母版的已完成 GPU 图像。",
|
||||
"render.output_fit": "适合窗口",
|
||||
"render.output_format": "格式",
|
||||
"render.output_format_status": "PNG 是完成的显示图像。可选 CPU/GPU EXR 保留显示滤镜前的原始线性 FLOAT32 辐亮度;导出不会替换成预览缓存或其他任务。",
|
||||
"render.output_format_status": "PNG 是完成的显示图像。可选 GPU EXR 保留显示滤镜前的原始线性 FLOAT32 辐亮度;导出不会替换成预览缓存或其他任务。",
|
||||
"render.output_grading": "调色",
|
||||
"render.output_image": "图像",
|
||||
"render.output_largest": "{width} × {height} 最大",
|
||||
@@ -3732,7 +3702,7 @@
|
||||
"render.quality_custom": "自定义",
|
||||
"render.quality_draft": "草稿",
|
||||
"render.quality_high": "高",
|
||||
"render.quality_projection_help": "此检查仅更改分辨率、采样预算、路径深度、种子和降噪。其他设置保持不变,预算不保证收敛。",
|
||||
"render.quality_projection_help": "Q 预设包含未来需求。此操作仅映射受支持的固定样本 PNG 预算,明确省略自适应停止、原始线性输出和独立透射限制。",
|
||||
"render.quality_review": "审阅采样预算",
|
||||
"render.queue_pending": "此界面没有自动生产队列或动画渲染器;已完成的静止图像仍可单独导出。",
|
||||
"render.radial_assign": "指定外观",
|
||||
@@ -3763,7 +3733,7 @@
|
||||
"render.result_navigation": "平移和缩放遵循导航设置。按住空格键并拖动可平移。",
|
||||
"render.result_open": "在主画布中打开已完成的图像。",
|
||||
"render.result_preview": "实时预览",
|
||||
"render.result_provisional": "临时图像",
|
||||
"render.result_provisional": "CPU临时图像",
|
||||
"render.result_provisional_help": "此中间结果无法导出。导出始终使用最后完成的结果;取消后会返回该结果。",
|
||||
"render.result_reduced_display": "显示预览已缩小到视口设备或缓存限制。保留的 PNG 和导出仍为完整分辨率;此缩小视图不能进行原生 100% 检查。",
|
||||
"render.result_retained": "正在显示最后完成的图像;较新的渲染尚未替换它。",
|
||||
@@ -3927,7 +3897,7 @@
|
||||
"render.stage_panels.prepare_plan": "准备创建计划",
|
||||
"render.stage_panels.projects": "投影",
|
||||
"render.stage_panels.quality_ack": "我确认此固定预算及其省略内容",
|
||||
"render.stage_panels.quality_omissions": "此预算不设置自适应误差目标、独立透射限制、俄罗斯轮盘规则或线性母版保留,也不保证收敛。",
|
||||
"render.stage_panels.quality_omissions": "自适应停止和误差目标、独立透射深度、俄罗斯轮盘规则、原始线性输出、EXR 母版及收敛认证。",
|
||||
"render.stage_panels.reference": "参考",
|
||||
"render.stage_panels.references": "参考",
|
||||
"render.stage_panels.remove_optics": "移除光学设置",
|
||||
@@ -3971,7 +3941,7 @@
|
||||
"render.stage_panels.subtitle_environment": "原创 HDR 环境 · 整个场景",
|
||||
"render.stage_panels.subtitle_maps": "创作的空间贴图 · 临时预览",
|
||||
"render.stage_panels.subtitle_physical": "只读计划 · 未创建或分配材料",
|
||||
"render.stage_panels.subtitle_quality": "GPU 采样预算 · 仅更改列出的设置",
|
||||
"render.stage_panels.subtitle_quality": "固定 GPU 预算 · 省略未来要求",
|
||||
"render.stage_panels.subtitle_rig": "创作的灯光装置 · 发光体和被动卡片",
|
||||
"render.stage_panels.subtitle_saved_maps": "创作的贴图 · 保存在此镜头中",
|
||||
"render.stage_panels.subtitle_stage": "物理地面和舞台 · 适配产品",
|
||||
@@ -3996,7 +3966,7 @@
|
||||
"render.studio_area_help": "最多四个单面矩形。正交半边 U 和 V 定义尺寸;U × V 指向发光侧。坐标采用世界空间毫米。",
|
||||
"render.studio_center": "中心 X, Y, Z(mm)",
|
||||
"render.studio_denoise": "降低最终图像噪点",
|
||||
"render.studio_denoise_help": "渐进图像和最终图像使用保留边缘的滤波,但可能柔化细节。玻璃与锐利镜面不参与滤波。",
|
||||
"render.studio_denoise_help": "最终滤波保留边缘,但可能柔化细节。玻璃与锐利镜面不参与滤波。",
|
||||
"render.studio_direction": "朝向太阳的方向 (X, Y, Z)",
|
||||
"render.studio_environment": "环境",
|
||||
"render.studio_environment_bounds": "环境旋转必须为±1,000,000弧度以内的有限值,强度必须在0至100之间。",
|
||||
@@ -4198,32 +4168,6 @@
|
||||
"sketch.dof_left": "剩余 {count} 个自由度",
|
||||
"sketch.dof_left_one": "剩余 {count} 个自由度",
|
||||
"sketch.draw_curves": "绘制为 {linetype}",
|
||||
"sketch.error.closed_curve_two_crossings": "a closed curve needs two crossings to trim",
|
||||
"sketch.error.nothing_to_break": "nothing to break here",
|
||||
"sketch.error.offset_fillet_chamfer": "`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first",
|
||||
"sketch.error.offset_trim": "`{id}` is an offset, and its ends follow its source curves: edit the source curves instead, or break the offset association first",
|
||||
"sketch.hint.arc.center_point": "Arc: click center and start, go round either way and click the end, or type a signed sweep",
|
||||
"sketch.hint.arc.three_point": "Arc: click start, end, then a point on the arc",
|
||||
"sketch.hint.arc3d": "3D Arc: click start, end, then a point on the arc (points may leave the sketch plane)",
|
||||
"sketch.hint.break": "Break: click a curve where it should split",
|
||||
"sketch.hint.circle.center": "Circle: click the center, then a point on the circle",
|
||||
"sketch.hint.constraint.generic": "Constraint: pick the sketch entities",
|
||||
"sketch.hint.constraint.horizontal_vertical": "Horizontal/Vertical: pick a line, two points, or a point and a line; hold Shift to pick a line midpoint",
|
||||
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to center the second on the first",
|
||||
"sketch.hint.dimension": "Dimension: pick one or two entities and type the value (or click to place it first); click a dimension to edit it; Enter applies",
|
||||
"sketch.hint.extend": "Extend: click a curve near the end to extend (Shift: click two curves to extend both to where they meet)",
|
||||
"sketch.hint.fallback": "Click in the sketch",
|
||||
"sketch.hint.line": "Line: click points; right-click or Esc to finish",
|
||||
"sketch.hint.line3d": "3D Line: click world points; right-click or Esc to finish (points may leave the sketch plane)",
|
||||
"sketch.hint.open_done": "Open done",
|
||||
"sketch.hint.point3d": "3D Point: click where a 3D point goes",
|
||||
"sketch.hint.rectangle.center": "Center rectangle: click the center, then a corner",
|
||||
"sketch.hint.rectangle.rounded": "Rounded rectangle: click two opposite corners (Tab to the radius box to type the corner radius)",
|
||||
"sketch.hint.rectangle.three_point": "3-point rectangle: click two points of one edge, then the width",
|
||||
"sketch.hint.rectangle.two_point": "Rectangle: click two opposite corners",
|
||||
"sketch.hint.slot": "Slot: click both ends, then the width",
|
||||
"sketch.hint.spline3d": "3D Spline: click fit points in 3D; right-click or Esc to finish",
|
||||
"sketch.hint.trim": "Trim: click the piece of a curve to remove, or drag across the pieces to remove",
|
||||
"sketch.make_selection": "将选择转换为 {linetype}",
|
||||
"sketch.open_ends": "开放 {count}",
|
||||
"sketch.status_conflict": "约束冲突",
|
||||
@@ -4445,7 +4389,6 @@
|
||||
"ui.3d_sketch": "3D 草图",
|
||||
"ui.activate": "激活",
|
||||
"ui.active_component": "活动组件",
|
||||
"ui.addToQuickAccess": "添加到快速访问",
|
||||
"ui.add_configuration": "添加配置",
|
||||
"ui.add_parameter": "添加参数",
|
||||
"ui.agent_chat": "智能体聊天",
|
||||
@@ -5074,7 +5017,6 @@
|
||||
"ui.heading_construct": "构造",
|
||||
"ui.heading_create": "创建",
|
||||
"ui.heading_create_sketch": "创建草图",
|
||||
"ui.heading_custom_view_navigator": "自定义视图导航器",
|
||||
"ui.heading_cylinder": "圆柱体",
|
||||
"ui.heading_design": "设计",
|
||||
"ui.heading_draft": "拔模",
|
||||
@@ -5361,7 +5303,6 @@
|
||||
"ui.quality": "质量",
|
||||
"ui.quantity": "数量",
|
||||
"ui.quantity_2": "数量 2",
|
||||
"ui.quick_access_full": "快速访问栏最多容纳 {count} 个命令;请先移除一个再添加",
|
||||
"ui.quit": "退出",
|
||||
"ui.radial_context_menu": "环形上下文菜单",
|
||||
"ui.radius": "半径",
|
||||
@@ -5379,7 +5320,6 @@
|
||||
"ui.relink_not_linked": "此组件未链接到源设计。",
|
||||
"ui.relink_select_one": "请先选择一个链接组件或其实例。",
|
||||
"ui.relink_source_all": "重新链接源文件(所有实例)…",
|
||||
"ui.removeFromQuickAccess": "从快速访问中移除",
|
||||
"ui.removeFromToolbar": "从工具栏移除",
|
||||
"ui.removeSection": "移除剖面",
|
||||
"ui.render": "渲染",
|
||||
@@ -5388,8 +5328,6 @@
|
||||
"ui.repeat_nothing": "还没有可重复的操作:请先启动一个工具或对话框。",
|
||||
"ui.replace_with_link_done": "这些副本现在是链接设计的放置。",
|
||||
"ui.report_a_bug": "报告缺陷",
|
||||
"ui.report_characters_left_one": "剩余 {count} 个字符",
|
||||
"ui.report_characters_left_other": "剩余 {count} 个字符",
|
||||
"ui.reset": "重置",
|
||||
"ui.reset_all": "全部重置",
|
||||
"ui.reset_the_settings_on_this_page": "重置此页面上的设置",
|
||||
@@ -5674,14 +5612,6 @@
|
||||
"values must be numbers or expressions": "值必须是数字或表达式",
|
||||
"view.auto_ortho_faces": "旋转时透视,正对表面时正交",
|
||||
"view.render_inspector": "渲染检查器",
|
||||
"view_gizmo.arrows": "旋转箭头",
|
||||
"view_gizmo.home": "主页按钮",
|
||||
"view_gizmo.no_such_body": "此设计中没有该名称的实体。",
|
||||
"view_gizmo.note": "在视图中选择一个实体。它随首选项和设置配置文件保存,并在每个设计中显示。单击它的一个面即可正视该面。",
|
||||
"view_gizmo.texture": "纹理",
|
||||
"view_gizmo.themed": "主题",
|
||||
"view_gizmo.too_detailed": "对视图导航器而言过于精细(超过 10,000 个三角形)",
|
||||
"view_gizmo.too_thin": "对视图导航器而言过于细长:最薄的一边至少要有最长一边的五分之一",
|
||||
"web.files.insert_drawings_hint": "将 CAD 文件、网格、DXF 或 SVG 插入当前设计",
|
||||
"web.files.insert_stored_drawings_hint": "插入当前设计(CAD 文件、网格、DXF、SVG)",
|
||||
"web.open_full": "在 SolidCraft 中打开",
|
||||
|
||||
@@ -366,13 +366,6 @@ fn fs_ao_depth(i: TOut, @builtin(front_facing) front: bool) -> AoDepth {
|
||||
return opaque_depth(i, front);
|
||||
}
|
||||
|
||||
// The virtual ground preserves the viewport background; only contact occlusion is blended.
|
||||
@fragment
|
||||
fn fs_ground_ao(i: TOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 1.0 - occlusion(i));
|
||||
}
|
||||
|
||||
/// The AO prepass's depth of an opaque surface's fragment.
|
||||
fn opaque_depth(i: TOut, front: bool) -> AoDepth {
|
||||
if (clipped(i.wp) || length(i.n) < 0.5) { discard; }
|
||||
|
||||
@@ -1,667 +0,0 @@
|
||||
{
|
||||
"order": 260,
|
||||
"provenance": "Original SolidCraft stone-grey palette and semantic icon-role mapping",
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Onyx",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#9eafbd",
|
||||
"accent_soft": "#3b4852",
|
||||
"agent_busy": "#9eafbd",
|
||||
"agent_ok": "#a5b9a5",
|
||||
"app_bar": "#1c1e20",
|
||||
"app_bar_hover": "#dededc16",
|
||||
"app_bar_hover_button": "#dededc1c",
|
||||
"app_bar_hover_strong": "#dededc28",
|
||||
"app_bar_hover_subtle": "#dededc0c",
|
||||
"app_bar_separator": "#383c3f",
|
||||
"app_bar_tab_active": "#303235",
|
||||
"app_bar_text": "#dededc",
|
||||
"axis_x": "#d7938f",
|
||||
"axis_y": "#a5b9a5",
|
||||
"axis_z": "#98b2c8",
|
||||
"bend_down": "#b8a7c3",
|
||||
"bend_up": "#a5b9a5",
|
||||
"body": "#93989c",
|
||||
"body_edge": "#60666b",
|
||||
"body_selected": "#3b4852",
|
||||
"border": "#3d4145",
|
||||
"box_window": "#9eafbd",
|
||||
"chip": "#758b9b",
|
||||
"construction_geom": "#b9a78e",
|
||||
"construction_plane": "#b9a78e24",
|
||||
"continuity": [
|
||||
"#a5b9a5",
|
||||
"#98b2c8",
|
||||
"#c7b28b",
|
||||
"#b8a7c3"
|
||||
],
|
||||
"control_border": "#4a4f54",
|
||||
"control_fill": "#303235",
|
||||
"cube_border": "#50565b",
|
||||
"cube_face": "#34373a",
|
||||
"cube_hover": "#53616c",
|
||||
"cube_text": "#dededc",
|
||||
"dialog_bg": "#2b2d2f",
|
||||
"dialog_body": "#2b2d2f",
|
||||
"dialog_border": "#3d4145",
|
||||
"dialog_footer": "#27292b",
|
||||
"dialog_label": "#dededc",
|
||||
"dialog_rule": "#383c3f",
|
||||
"dimension": "#c7b28b",
|
||||
"dimension_driven": "#a59474",
|
||||
"error": "#d7938f",
|
||||
"field": "#1c1e20",
|
||||
"form_accent": "#9d8faa",
|
||||
"form_dim": "#c6c8c9",
|
||||
"form_error": "#d7938f",
|
||||
"form_error_fill": "#493334",
|
||||
"form_fill": "#b9abc5",
|
||||
"form_ink": "#dededc",
|
||||
"form_primary": "#9eafbd",
|
||||
"form_primary_fill": "#9eafbd",
|
||||
"form_soft": "#343039",
|
||||
"form_success": "#a5b9a5",
|
||||
"form_success_fill": "#344139",
|
||||
"form_tab_ink": "#b9abc5",
|
||||
"glyph_bg": "#34373a",
|
||||
"glyph_edge": "#d7938f",
|
||||
"glyph_text": "#d7938f",
|
||||
"grid": "#252729",
|
||||
"grid_major": "#2e3235",
|
||||
"hover": "#3b4852",
|
||||
"hover_edge": "#b1c1ce",
|
||||
"hover_edge_halo": "#181a1c",
|
||||
"hover_profile": "#9eafbd30",
|
||||
"hover_profile_edge": "#b1c1ce",
|
||||
"icon": "#dededc",
|
||||
"icons.agent": "#b8a7c3",
|
||||
"icons.highlight": "#dededc",
|
||||
"icons.marker_halo": "#1c1e20cc",
|
||||
"interference": "#d7938f",
|
||||
"joint_axis": "#98b2c8",
|
||||
"joint_manipulator": "#9eafbd",
|
||||
"logo": [
|
||||
"#c9d4dc",
|
||||
"#9eafbd",
|
||||
"#647986"
|
||||
],
|
||||
"manipulator": "#9eafbd",
|
||||
"mesh_accent": "#a29772",
|
||||
"mesh_fill": "#beb48d",
|
||||
"mesh_ink": "#beb48d",
|
||||
"mesh_soft": "#34332b",
|
||||
"nav_dim": "#b5b7b8",
|
||||
"nav_field": "#1c1e20",
|
||||
"nav_hover": "#43494f",
|
||||
"nav_press": "#59636c",
|
||||
"nav_tip": "#2b2d2f",
|
||||
"nav_tip_ink": "#dededc",
|
||||
"nav_tip_key_border": "#3b3e41",
|
||||
"nav_viewport": "#27292b",
|
||||
"on_accent": "#17191b",
|
||||
"origin_plane": "#b9a78e2c",
|
||||
"origin_plane_edge": "#b9a78e94",
|
||||
"origin_plane_hover": "#9eafbd60",
|
||||
"origin_point": "#b5b7b8",
|
||||
"overlay": "#2b2d2f",
|
||||
"panel": "#2b2d2f",
|
||||
"panel_header": "#303235",
|
||||
"preview_add": "#9eafbd",
|
||||
"preview_cut": "#d7938f68",
|
||||
"preview_new": "#c7b28b",
|
||||
"preview_sketch": "#a5b9a5",
|
||||
"profile_fill": "#9eafbd28",
|
||||
"render_accent": "#a88e88",
|
||||
"render_fill": "#c1a49e",
|
||||
"render_ink": "#c1a49e",
|
||||
"render_soft": "#38302f",
|
||||
"rubber_band": "#9eafbd",
|
||||
"section_cap": "#c7b28b",
|
||||
"sel_edge": "#b1c1ce",
|
||||
"sel_edge_rim": "#d0d9df",
|
||||
"sel_face": "#3b4852",
|
||||
"sel_profile": "#3b4852",
|
||||
"sel_profile_edge": "#b1c1ce",
|
||||
"sel_vertex": "#9eafbd",
|
||||
"selected_bg": "#3b4852",
|
||||
"selected_ink": "#dededc",
|
||||
"sheet_fill": "#9eb9bd",
|
||||
"sheet_ink": "#9eb9bd",
|
||||
"sheet_soft": "#29363a",
|
||||
"sketch": "#9eafbd",
|
||||
"sketch_accent": "#b2b995",
|
||||
"sketch_centerline": "#b5b7b8",
|
||||
"sketch_conflict": "#d7938f",
|
||||
"sketch_construction": "#b5b7b8",
|
||||
"sketch_fixed": "#dededc",
|
||||
"sketch_halo": "#181a1c",
|
||||
"sketch_ink": "#b2b995",
|
||||
"sketch_locked": "#a5b9a5",
|
||||
"sketch_lost": "#d7938f",
|
||||
"sketch_point": "#dededc",
|
||||
"sketch_point_free": "#9eafbd",
|
||||
"sketch_projected": "#b8a7c3",
|
||||
"sketch_soft": "#32352c",
|
||||
"surface_accent": "#a6947d",
|
||||
"surface_fill": "#c0ae96",
|
||||
"surface_ink": "#c0ae96",
|
||||
"surface_soft": "#35322e",
|
||||
"tab_on_fill_dark": "#17191b",
|
||||
"text": "#dededc",
|
||||
"text_dim": "#c6c8c9",
|
||||
"timeline": "#2b2d2f",
|
||||
"timeline_item": "#303235",
|
||||
"timeline_item_error": "#493334",
|
||||
"timeline_marker": "#b5b7b8",
|
||||
"toolbar": "#2b2d2f",
|
||||
"viewport_bottom": "#1b1d1f",
|
||||
"viewport_top": "#222426",
|
||||
"warning": "#c7b28b"
|
||||
},
|
||||
"radii": {
|
||||
"radius_sm": 3,
|
||||
"radius_md": 6,
|
||||
"radius_lg": 8,
|
||||
"radius_xl": 10,
|
||||
"control_radius": 4,
|
||||
"sketch_mode_radius": 6
|
||||
},
|
||||
"icon_palette": {
|
||||
"ink": "#1b1d1f",
|
||||
"line": "#d7d9d8",
|
||||
"passive-1": "#b9bdc0",
|
||||
"passive-2": "#8d9398",
|
||||
"passive-3": "#646c72",
|
||||
"guide": "#a2a8ac",
|
||||
"solid": [
|
||||
"#c9d4dc",
|
||||
"#b2c1cc",
|
||||
"#9eafbd"
|
||||
],
|
||||
"surface": [
|
||||
"#d6cab8",
|
||||
"#c0ae96",
|
||||
"#a6947d"
|
||||
],
|
||||
"mesh": [
|
||||
"#d4ccad",
|
||||
"#beb48d",
|
||||
"#a29772"
|
||||
],
|
||||
"form": [
|
||||
"#d0c5d9",
|
||||
"#b9abc5",
|
||||
"#9d8faa"
|
||||
],
|
||||
"sketch": [
|
||||
"#c9cfb3",
|
||||
"#b2b995",
|
||||
"#979f78"
|
||||
],
|
||||
"constraint": [
|
||||
"#e1b7b2",
|
||||
"#d7938f",
|
||||
"#b77875"
|
||||
],
|
||||
"construction": [
|
||||
"#d2c6b4",
|
||||
"#b9a78e",
|
||||
"#9f8c73"
|
||||
]
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"id": "opacity",
|
||||
"type": "range",
|
||||
"default": 80,
|
||||
"min": 60,
|
||||
"max": 100
|
||||
},
|
||||
{
|
||||
"id": "blur",
|
||||
"type": "range",
|
||||
"default": 60,
|
||||
"max": 60
|
||||
},
|
||||
{
|
||||
"id": "tab_colours",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": "tab_fade_ms",
|
||||
"type": "range",
|
||||
"default": 240
|
||||
},
|
||||
{
|
||||
"id": "accent",
|
||||
"type": "choice",
|
||||
"default": "slate",
|
||||
"label": {
|
||||
"en": "Accent",
|
||||
"de": "Akzent",
|
||||
"ja": "アクセント",
|
||||
"ko": "강조색",
|
||||
"zh": "强调色"
|
||||
},
|
||||
"help": {
|
||||
"en": "Selection, highlights and Model controls; workspace and status colours keep their meaning.",
|
||||
"de": "Auswahl, Hervorhebungen und Modell-Steuerung; Arbeitsbereichs- und Statusfarben behalten ihre Bedeutung.",
|
||||
"ja": "選択・強調・モデルの操作色。ワークスペースや状態の色の意味は変えません。",
|
||||
"ko": "선택, 강조 및 모델 조작의 색상입니다. 작업 공간과 상태 색상의 의미는 유지됩니다.",
|
||||
"zh": "更改选择、高亮与模型控件的色调;工作空间和状态色保持原有含义。"
|
||||
},
|
||||
"values": [
|
||||
{
|
||||
"id": "slate",
|
||||
"label": {
|
||||
"en": "Slate blue",
|
||||
"de": "Schieferblau",
|
||||
"ja": "石板の青",
|
||||
"ko": "슬레이트 블루",
|
||||
"zh": "岩蓝"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "silver",
|
||||
"label": {
|
||||
"en": "Silver",
|
||||
"de": "Silber",
|
||||
"ja": "シルバー",
|
||||
"ko": "은색",
|
||||
"zh": "银灰"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sage",
|
||||
"label": {
|
||||
"en": "Sage",
|
||||
"de": "Salbei",
|
||||
"ja": "セージ",
|
||||
"ko": "세이지",
|
||||
"zh": "鼠尾草绿"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "contrast",
|
||||
"type": "choice",
|
||||
"default": "soft",
|
||||
"label": {
|
||||
"en": "Contrast",
|
||||
"de": "Kontrast",
|
||||
"ja": "コントラスト",
|
||||
"ko": "대비",
|
||||
"zh": "对比度"
|
||||
},
|
||||
"help": {
|
||||
"en": "Soft stone layers or stronger separation between panels and fields.",
|
||||
"de": "Weiche Steinschichten oder stärkere Trennung von Flächen und Feldern.",
|
||||
"ja": "柔らかな石の階調か、パネルと入力欄を明確に分ける階調を選びます。",
|
||||
"ko": "부드러운 석재 톤 또는 패널과 입력란을 더 뚜렷하게 구분합니다.",
|
||||
"zh": "选择柔和的石色层次,或增强面板与输入框的区分。"
|
||||
},
|
||||
"values": [
|
||||
{
|
||||
"id": "soft",
|
||||
"label": {
|
||||
"en": "Soft",
|
||||
"de": "Weich",
|
||||
"ja": "ソフト",
|
||||
"ko": "부드럽게",
|
||||
"zh": "柔和"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "defined",
|
||||
"label": {
|
||||
"en": "Defined",
|
||||
"de": "Deutlich",
|
||||
"ja": "くっきり",
|
||||
"ko": "뚜렷하게",
|
||||
"zh": "分明"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "brightness",
|
||||
"type": "range",
|
||||
"effect": "surface_brightness",
|
||||
"default": 100,
|
||||
"min": 60,
|
||||
"max": 120,
|
||||
"step": 1,
|
||||
"unit": "%",
|
||||
"label": {
|
||||
"en": "Brightness",
|
||||
"de": "Helligkeit",
|
||||
"ja": "明るさ",
|
||||
"ko": "밝기",
|
||||
"zh": "亮度"
|
||||
},
|
||||
"help": {
|
||||
"en": "Adjust stone surfaces, viewport backgrounds and grid brightness. 100% keeps the authored tones; text and semantic colours stay unchanged.",
|
||||
"de": "Helligkeit von Steinflächen, Ansichtsflächen und Raster. 100% erhält die ursprünglichen Töne; Text und Bedeutungsfarben bleiben unverändert.",
|
||||
"ja": "石調の面・ビューポート背景・グリッドの明るさを調整します。100%は元の階調です。文字と意味のある色は変えません。",
|
||||
"ko": "석재 표면·뷰포트 배경·격자의 밝기를 조절합니다. 100%는 원래 톤을 유지하며 글자와 의미 색상은 바꾸지 않습니다.",
|
||||
"zh": "调整石色表面、视口背景和网格的亮度。100%保留原有色调;文字和语义颜色保持不变。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "foreground_colours",
|
||||
"type": "choice",
|
||||
"default": "vibrant",
|
||||
"label": {
|
||||
"en": "Foreground colours",
|
||||
"de": "Vordergrundfarben",
|
||||
"ja": "前景の色",
|
||||
"ko": "전경 색상",
|
||||
"zh": "前景颜色"
|
||||
},
|
||||
"help": {
|
||||
"en": "Vibrant strengthens axes, icons and guides. Muted keeps the original quiet palette. Accent controls selection.",
|
||||
"de": "Leuchtend verstärkt Achsen, Symbole und Hilfen. Gedämpft behält die ruhige ursprüngliche Palette. Akzent steuert die Auswahl.",
|
||||
"ja": "鮮やかは軸・アイコン・作図ガイドを強調します。落ち着いた配色は元の控えめな色を保ちます。選択の色はアクセントで設定します。",
|
||||
"ko": "선명함은 축·아이콘·가이드를 강조합니다. 차분함은 기존의 은은한 팔레트를 유지합니다. 선택 색상은 강조색 설정을 따릅니다.",
|
||||
"zh": "鲜艳增强轴、图标和辅助线的色彩。柔和保留原有的沉静配色。选择颜色仍由强调色控制。"
|
||||
},
|
||||
"values": [
|
||||
{
|
||||
"id": "muted",
|
||||
"label": {
|
||||
"en": "Muted",
|
||||
"de": "Gedämpft",
|
||||
"ja": "落ち着いた",
|
||||
"ko": "차분함",
|
||||
"zh": "柔和"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vibrant",
|
||||
"label": {
|
||||
"en": "Vibrant",
|
||||
"de": "Leuchtend",
|
||||
"ja": "鮮やか",
|
||||
"ko": "선명함",
|
||||
"zh": "鲜艳"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"overlays": [
|
||||
{
|
||||
"when": {
|
||||
"opt": "contrast",
|
||||
"eq": "defined"
|
||||
},
|
||||
"colors": {
|
||||
"app_bar": "#151719",
|
||||
"panel": "#25282a",
|
||||
"dialog_bg": "#25282a",
|
||||
"dialog_body": "#25282a",
|
||||
"dialog_footer": "#202325",
|
||||
"panel_header": "#373b3e",
|
||||
"field": "#171a1c",
|
||||
"control_fill": "#383d41",
|
||||
"border": "#4e555b",
|
||||
"dialog_border": "#4e555b",
|
||||
"dialog_rule": "#444b51",
|
||||
"control_border": "#626c73",
|
||||
"text": "#eeeeec",
|
||||
"text_dim": "#ced1d2",
|
||||
"form_ink": "#eeeeec",
|
||||
"form_dim": "#ced1d2",
|
||||
"dialog_label": "#eeeeec",
|
||||
"toolbar": "#25282a",
|
||||
"timeline": "#25282a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"opt": "accent",
|
||||
"eq": "silver"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "#b7bdc2",
|
||||
"manipulator": "#b7bdc2",
|
||||
"joint_manipulator": "#b7bdc2",
|
||||
"preview_add": "#b7bdc2",
|
||||
"rubber_band": "#b7bdc2",
|
||||
"box_window": "#b7bdc2",
|
||||
"form_primary": "#b7bdc2",
|
||||
"form_primary_fill": "#b7bdc2",
|
||||
"sketch": "#b7bdc2",
|
||||
"sketch_point_free": "#b7bdc2",
|
||||
"sel_vertex": "#b7bdc2",
|
||||
"accent_soft": "#454b50",
|
||||
"hover": "#454b50",
|
||||
"selected_bg": "#454b50",
|
||||
"sel_face": "#454b50",
|
||||
"sel_profile": "#454b50",
|
||||
"body_selected": "#454b50",
|
||||
"sel_edge": "#d5dadf",
|
||||
"sel_profile_edge": "#d5dadf",
|
||||
"hover_edge": "#d5dadf",
|
||||
"hover_profile_edge": "#d5dadf",
|
||||
"sel_edge_rim": "#dde0e3",
|
||||
"hover_profile": "#b7bdc230",
|
||||
"origin_plane_hover": "#b7bdc260",
|
||||
"profile_fill": "#b7bdc228",
|
||||
"cube_hover": "#454b50",
|
||||
"chip": "#b7bdc2",
|
||||
"logo": [
|
||||
"#dde0e3",
|
||||
"#c9cfd4",
|
||||
"#b7bdc2"
|
||||
]
|
||||
},
|
||||
"icon_palette": {
|
||||
"solid": [
|
||||
"#dde0e3",
|
||||
"#c9cfd4",
|
||||
"#b7bdc2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"opt": "accent",
|
||||
"eq": "sage"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "#aabcae",
|
||||
"manipulator": "#aabcae",
|
||||
"joint_manipulator": "#aabcae",
|
||||
"preview_add": "#aabcae",
|
||||
"rubber_band": "#aabcae",
|
||||
"box_window": "#aabcae",
|
||||
"form_primary": "#aabcae",
|
||||
"form_primary_fill": "#aabcae",
|
||||
"sketch": "#aabcae",
|
||||
"sketch_point_free": "#aabcae",
|
||||
"sel_vertex": "#aabcae",
|
||||
"accent_soft": "#414d45",
|
||||
"hover": "#414d45",
|
||||
"selected_bg": "#414d45",
|
||||
"sel_face": "#414d45",
|
||||
"sel_profile": "#414d45",
|
||||
"body_selected": "#414d45",
|
||||
"sel_edge": "#c2d2c5",
|
||||
"sel_profile_edge": "#c2d2c5",
|
||||
"hover_edge": "#c2d2c5",
|
||||
"hover_profile_edge": "#c2d2c5",
|
||||
"sel_edge_rim": "#d1ddd4",
|
||||
"hover_profile": "#aabcae30",
|
||||
"origin_plane_hover": "#aabcae60",
|
||||
"profile_fill": "#aabcae28",
|
||||
"cube_hover": "#414d45",
|
||||
"chip": "#aabcae",
|
||||
"logo": [
|
||||
"#d1ddd4",
|
||||
"#becec2",
|
||||
"#aabcae"
|
||||
]
|
||||
},
|
||||
"icon_palette": {
|
||||
"solid": [
|
||||
"#d1ddd4",
|
||||
"#becec2",
|
||||
"#aabcae"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"opt": "foreground_colours",
|
||||
"eq": "vibrant"
|
||||
},
|
||||
"colors": {
|
||||
"axis_x": "#ff6d79",
|
||||
"axis_y": "#4fd681",
|
||||
"axis_z": "#4da7ff",
|
||||
"agent_busy": "#4da7ff",
|
||||
"agent_ok": "#4fd681",
|
||||
"warning": "#ffd166",
|
||||
"error": "#ff6d79",
|
||||
"form_error": "#ff6d79",
|
||||
"form_success": "#4fd681",
|
||||
"glyph_edge": "#ff6d79",
|
||||
"glyph_text": "#ff6d79",
|
||||
"interference": "#ff6d79",
|
||||
"sketch_conflict": "#ff6d79",
|
||||
"sketch_lost": "#ff6d79",
|
||||
"sketch_locked": "#4fd681",
|
||||
"sketch_projected": "#c78bff",
|
||||
"dimension": "#ffd166",
|
||||
"dimension_driven": "#ffb166",
|
||||
"construction_geom": "#ffb166",
|
||||
"sketch_centerline": "#b5b7b8",
|
||||
"sketch_construction": "#b5b7b8",
|
||||
"joint_axis": "#42d9dd",
|
||||
"bend_down": "#c78bff",
|
||||
"bend_up": "#4fd681",
|
||||
"preview_cut": "#ff6d7968",
|
||||
"preview_new": "#ffd166",
|
||||
"preview_sketch": "#4fd681",
|
||||
"section_cap": "#ffd166",
|
||||
"sketch_accent": "#4fd681",
|
||||
"sketch_ink": "#4fd681",
|
||||
"surface_accent": "#ffb166",
|
||||
"surface_fill": "#ffb166",
|
||||
"surface_ink": "#ffb166",
|
||||
"mesh_accent": "#ffd166",
|
||||
"mesh_fill": "#ffd166",
|
||||
"mesh_ink": "#ffd166",
|
||||
"form_accent": "#c78bff",
|
||||
"form_fill": "#c78bff",
|
||||
"form_tab_ink": "#c78bff",
|
||||
"sheet_fill": "#63cbd5",
|
||||
"sheet_ink": "#63cbd5",
|
||||
"render_accent": "#ff6d79",
|
||||
"render_fill": "#ff6d79",
|
||||
"render_ink": "#ff6d79",
|
||||
"icons.agent": "#c78bff",
|
||||
"continuity": [
|
||||
"#4fd681",
|
||||
"#4da7ff",
|
||||
"#ffd166",
|
||||
"#c78bff"
|
||||
],
|
||||
"origin_plane_edge": "#ffb16694",
|
||||
"sheet_soft": "#283a3e"
|
||||
},
|
||||
"icon_palette": {
|
||||
"ink": "#1b1d1f",
|
||||
"line": "#d7d9d8",
|
||||
"passive-1": "#d3d8dc",
|
||||
"passive-2": "#b2bbc2",
|
||||
"passive-3": "#929fa9",
|
||||
"guide": "#b4c8d6",
|
||||
"solid": [
|
||||
"#82c1ff",
|
||||
"#62b2ff",
|
||||
"#4da7ff"
|
||||
],
|
||||
"surface": [
|
||||
"#ffc894",
|
||||
"#ffba78",
|
||||
"#ffb166"
|
||||
],
|
||||
"mesh": [
|
||||
"#ffdf94",
|
||||
"#ffd778",
|
||||
"#ffd166"
|
||||
],
|
||||
"form": [
|
||||
"#d8aeff",
|
||||
"#ce99ff",
|
||||
"#c78bff"
|
||||
],
|
||||
"sketch": [
|
||||
"#84e2a7",
|
||||
"#64db90",
|
||||
"#4fd681"
|
||||
],
|
||||
"constraint": [
|
||||
"#ff99a1",
|
||||
"#ff7f89",
|
||||
"#ff6d79"
|
||||
],
|
||||
"construction": [
|
||||
"#ffc894",
|
||||
"#ffba78",
|
||||
"#ffb166"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"tabs": {
|
||||
"MODEL": {
|
||||
"viewport_top": "#24292d",
|
||||
"viewport_bottom": "#1b2023",
|
||||
"panel": "#303438"
|
||||
},
|
||||
"SURFACE": {
|
||||
"viewport_top": "#292622",
|
||||
"viewport_bottom": "#211e1b",
|
||||
"panel": "#36332f"
|
||||
},
|
||||
"MESH": {
|
||||
"viewport_top": "#282820",
|
||||
"viewport_bottom": "#202019",
|
||||
"panel": "#35352e"
|
||||
},
|
||||
"FORM": {
|
||||
"viewport_top": "#26232b",
|
||||
"viewport_bottom": "#1e1b22",
|
||||
"panel": "#343137"
|
||||
},
|
||||
"SHEET METAL": {
|
||||
"viewport_top": "#22282a",
|
||||
"viewport_bottom": "#1b2022",
|
||||
"panel": "#303638"
|
||||
},
|
||||
"RENDER": {
|
||||
"viewport_top": "#2b2423",
|
||||
"viewport_bottom": "#221c1b",
|
||||
"panel": "#373130"
|
||||
},
|
||||
"SKETCH": {
|
||||
"viewport_top": "#252921",
|
||||
"viewport_bottom": "#1d211a",
|
||||
"panel": "#33362f"
|
||||
}
|
||||
},
|
||||
"borders": {
|
||||
"primary_inset": {
|
||||
"width": 0.5,
|
||||
"gap": 1,
|
||||
"color": "#dce2e70e"
|
||||
}
|
||||
},
|
||||
"requires": 2
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
| assets/locales/pending/ribbon-panel-hover.json | SolidCraft contributors | Original ribbon panel menu preference labels and translations | MIT OR Apache-2.0 |
|
||||
@@ -2,46 +2,6 @@
|
||||
use crate::{DocError, Document, Mat, ModelBody, ModelState, Result};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// A profile moved into the component receiving its own body is read through the recorded
|
||||
/// transfer frame. Reading its live placement would move the source geometry when that
|
||||
/// component moves, then move the transferred body a second time. All outputs of a constructor
|
||||
/// must reach the sketch's component through the same frame; independent cross-component
|
||||
/// references retain their ordinary live placement.
|
||||
pub(crate) fn profile_frame(doc: &Document, feature: &crate::Feature, sketch: u64) -> Option<Mat> {
|
||||
if !matches!(
|
||||
feature.kind,
|
||||
crate::FeatureKind::Extrude { operation: crate::Operation::NewBody, .. }
|
||||
| crate::FeatureKind::Revolve { operation: crate::Operation::NewBody, .. }
|
||||
) || feature.body_names.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let owner = doc.feature(sketch)?.component;
|
||||
if owner == feature.component {
|
||||
return None;
|
||||
}
|
||||
let mut frames: std::collections::BTreeMap<&str, (u64, Mat)> =
|
||||
feature.body_names.iter().map(|name| (name.as_str(), (feature.component, crate::IDENTITY))).collect();
|
||||
let start = doc.feature_index(feature.id)?.saturating_add(1);
|
||||
for step in doc.features.iter().take(doc.marker.unwrap_or(usize::MAX)).skip(start).filter(|step| !step.suppressed) {
|
||||
let crate::FeatureKind::BodyTransfer { bodies, from, to, transform } = &step.kind else { continue };
|
||||
for name in bodies {
|
||||
if let Some((component, frame)) = frames.get_mut(name.as_ref()) {
|
||||
if *component != *from || !valid_transfer_matrix(transform) {
|
||||
return None;
|
||||
}
|
||||
*component = *to;
|
||||
*frame = crate::mat_mul(transform, frame);
|
||||
}
|
||||
}
|
||||
if frames.values().all(|(component, _)| *component == owner) {
|
||||
let (_, first) = frames.values().next()?;
|
||||
return frames.values().all(|(_, frame)| frame == first).then(|| crate::mat_inverse(first)).flatten();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn valid_transfer_matrix(m: &Mat) -> bool {
|
||||
crate::is_placement(m)
|
||||
}
|
||||
|
||||
@@ -89,45 +89,20 @@ pub fn format_value(value: f64, kind: expr::Kind, unit: &str, decimals: u8, f: &
|
||||
if unit_kind != kind {
|
||||
return Err(DocError::Invalid("number display unit has a different kind".into()));
|
||||
}
|
||||
format_display(value / scale, scale, decimals, f, None)
|
||||
format_display(value / scale, scale, decimals, f)
|
||||
}
|
||||
|
||||
/// Format a unit-less number with the same validation and decimal rounding policy.
|
||||
pub fn format_number(x: f64, decimals: u8, f: &NumberFormat) -> Result<Shown> {
|
||||
format_display(x, 1.0, decimals, f, None)
|
||||
format_display(x, 1.0, decimals, f)
|
||||
}
|
||||
|
||||
/// Format an internal value with a rounding step in the selected display unit.
|
||||
/// The step is a shortest round-trip decimal, finite, positive and at most 1e6.
|
||||
/// Input decimal preparation precedes step rounding; requested decimal precision follows it.
|
||||
/// Both rounding operations use `f.ties`. The original value is never changed.
|
||||
/// The exact snapped display magnitude must be at most 1e12. An out-of-range snap
|
||||
/// returns [`DocError::Invalid`] before final decimal-place rounding, even if that
|
||||
/// final rounding would bring the displayed value back into range.
|
||||
pub fn format_value_with_step(value: f64, kind: expr::Kind, unit: &str, decimals: u8, step: f64, f: &NumberFormat) -> Result<Shown> {
|
||||
let (unit_kind, scale) = expr::unit_info(unit).ok_or_else(|| DocError::Invalid("unknown number display unit".into()))?;
|
||||
if unit_kind != kind {
|
||||
return Err(DocError::Invalid("number display unit has a different kind".into()));
|
||||
}
|
||||
format_display(value / scale, scale, decimals, f, Some(step))
|
||||
}
|
||||
|
||||
/// Format a unit-less number with the step policy of [`format_value_with_step`].
|
||||
/// Decimal precision can round a step multiple again; `Shown.value` always matches the text.
|
||||
/// Out-of-range snapped magnitudes are rejected before final decimal-place rounding.
|
||||
pub fn format_number_with_step(x: f64, decimals: u8, step: f64, f: &NumberFormat) -> Result<Shown> {
|
||||
format_display(x, 1.0, decimals, f, Some(step))
|
||||
}
|
||||
|
||||
fn format_display(x: f64, scale: f64, decimals: u8, f: &NumberFormat, step: Option<f64>) -> Result<Shown> {
|
||||
fn format_display(x: f64, scale: f64, decimals: u8, f: &NumberFormat) -> Result<Shown> {
|
||||
// The unit table supplies a positive finite scale. Its decimal step must be at most
|
||||
// 1e-9 internal units; the precision floor can make the effective tie window narrower.
|
||||
let unit_resolution = (scale.log10() + 9.0).ceil().max(0.0) as usize;
|
||||
let resolution = unit_resolution.max(usize::from(decimals) + 3);
|
||||
let (rounded, value) = match step {
|
||||
Some(step) => round_decimal_step(x, step, decimals, resolution, f.ties)?,
|
||||
None => round_decimal(x, decimals, resolution, f.ties)?,
|
||||
};
|
||||
let (rounded, value) = round_decimal(x, decimals, resolution, f.ties)?;
|
||||
let (integer, fraction) = rounded.split_once('.').unwrap_or((&rounded, ""));
|
||||
let fraction = if f.trailing_zeros { fraction } else { fraction.trim_end_matches('0') };
|
||||
let integer = if !f.leading_zero && integer == "0" && !fraction.is_empty() { "" } else { integer };
|
||||
@@ -146,25 +121,16 @@ fn format_display(x: f64, scale: f64, decimals: u8, f: &NumberFormat, step: Opti
|
||||
Ok(Shown { text, value })
|
||||
}
|
||||
|
||||
/// Validate and prepare the magnitude once, identically for both rounding paths.
|
||||
fn prepare_decimal(x: f64, decimals: u8, resolution: usize) -> Result<String> {
|
||||
/// Prepare a bounded magnitude, then round decimal digits, never a binary tie comparison.
|
||||
fn round_decimal(x: f64, decimals: u8, resolution: usize, ties: Ties) -> Result<(String, f64)> {
|
||||
if decimals > 8 {
|
||||
return Err(DocError::Invalid("number precision must be at most 8 decimal places".into()));
|
||||
}
|
||||
if !x.is_finite() || x.abs() > 1e12 {
|
||||
return Err(DocError::Invalid("display value must be finite with magnitude at most 1e12".into()));
|
||||
}
|
||||
Ok(format!("{:.*}", resolution, x.abs()))
|
||||
}
|
||||
|
||||
/// Prepare a bounded magnitude, then round decimal digits, never a binary tie comparison.
|
||||
fn round_decimal(x: f64, decimals: u8, resolution: usize, ties: Ties) -> Result<(String, f64)> {
|
||||
let prepared = prepare_decimal(x, decimals, resolution)?;
|
||||
round_prepared(&prepared, x.is_sign_negative(), decimals, ties)
|
||||
}
|
||||
|
||||
fn round_prepared(prepared: &str, negative: bool, decimals: u8, ties: Ties) -> Result<(String, f64)> {
|
||||
let (integer, fraction) = prepared.split_once('.').unwrap_or((prepared, ""));
|
||||
let prepared = format!("{:.*}", resolution, x.abs());
|
||||
let (integer, fraction) = prepared.split_once('.').unwrap_or((&prepared, ""));
|
||||
let places = usize::from(decimals);
|
||||
let mut digits: Vec<u8> = integer.bytes().chain(fraction.bytes().take(places)).collect();
|
||||
let discarded = fraction.as_bytes().get(places..).unwrap_or_default();
|
||||
@@ -200,81 +166,10 @@ fn round_prepared(prepared: &str, negative: bool, decimals: u8, ties: Ties) -> R
|
||||
}
|
||||
let rounded = String::from_utf8(digits).map_err(|_| DocError::Invalid("invalid prepared digits".into()))?;
|
||||
let magnitude = rounded.parse::<f64>().map_err(|_| DocError::Invalid("invalid rounded number".into()))?;
|
||||
let negative = negative && magnitude != 0.0;
|
||||
let negative = x.is_sign_negative() && magnitude != 0.0;
|
||||
Ok((rounded, if negative { -magnitude } else { magnitude }))
|
||||
}
|
||||
|
||||
/// Divide decimal digits by a small decimal coefficient, retaining the full quotient.
|
||||
/// A shortest f64 coefficient has at most 17 significant digits. With the unit table's
|
||||
/// resolution <= 15, a scaled divisor is <= 1e21, so remainder * 10 fits u128.
|
||||
/// At most 324 appended zeros are needed, even for the smallest positive subnormal.
|
||||
fn round_decimal_step(x: f64, step: f64, decimals: u8, resolution: usize, ties: Ties) -> Result<(String, f64)> {
|
||||
if !step.is_finite() || step <= 0.0 || step > 1e6 {
|
||||
return Err(DocError::Invalid("number rounding step must be finite, positive and at most 1e6".into()));
|
||||
}
|
||||
let prepared = prepare_decimal(x, decimals, resolution)?;
|
||||
let scientific = format!("{step:e}");
|
||||
let (mantissa, exponent) = scientific.split_once('e').ok_or_else(|| DocError::Invalid("invalid decimal step".into()))?;
|
||||
let fraction = mantissa.split_once('.').map_or(0, |(_, f)| f.len());
|
||||
let coefficient = mantissa.replace('.', "").parse::<u128>().map_err(|_| DocError::Invalid("invalid step coefficient".into()))?;
|
||||
let exponent = exponent.parse::<i32>().map_err(|_| DocError::Invalid("invalid step exponent".into()))? - fraction as i32;
|
||||
let shift = exponent + resolution as i32;
|
||||
let divisor = if shift > 0 { 10_u128.checked_pow(shift as u32).and_then(|n| coefficient.checked_mul(n)) } else { Some(coefficient) }
|
||||
.ok_or_else(|| DocError::Invalid("rounding step scale overflow".into()))?;
|
||||
let mut remainder = 0_u128;
|
||||
let mut quotient = Vec::new();
|
||||
for digit in prepared.bytes().filter(|d| *d != b'.').chain(std::iter::repeat_n(b'0', (-shift).max(0) as usize)) {
|
||||
remainder = remainder * 10 + u128::from(digit - b'0');
|
||||
quotient.push((remainder / divisor) as u8);
|
||||
remainder %= divisor;
|
||||
}
|
||||
let up = remainder * 2 > divisor
|
||||
|| (remainder * 2 == divisor && (ties == Ties::HalfAwayFromZero || quotient.last().is_some_and(|d| !d.is_multiple_of(2))));
|
||||
// Exact quotient * coefficient, with the rounding increment folded into the carry.
|
||||
let mut carry = if up { coefficient } else { 0 };
|
||||
let mut product = Vec::new();
|
||||
for digit in quotient.iter().rev() {
|
||||
let n = u128::from(*digit) * coefficient + carry;
|
||||
product.push(b'0' + (n % 10) as u8);
|
||||
carry = n / 10;
|
||||
}
|
||||
while carry > 0 {
|
||||
product.push(b'0' + (carry % 10) as u8);
|
||||
carry /= 10;
|
||||
}
|
||||
product.reverse();
|
||||
let product = String::from_utf8(product).map_err(|_| DocError::Invalid("invalid step digits".into()))?;
|
||||
let mut magnitude = product.trim_start_matches('0').to_string();
|
||||
if magnitude.is_empty() {
|
||||
magnitude.push('0');
|
||||
}
|
||||
if exponent >= 0 {
|
||||
if magnitude != "0" {
|
||||
magnitude.extend(std::iter::repeat_n('0', exponent as usize));
|
||||
}
|
||||
} else {
|
||||
let places = (-exponent) as usize;
|
||||
if magnitude.len() <= places {
|
||||
magnitude = format!("{}{}", "0".repeat(places + 1 - magnitude.len()), magnitude);
|
||||
}
|
||||
magnitude.insert(magnitude.len() - places, '.');
|
||||
}
|
||||
let (integer, fraction) = magnitude.split_once('.').unwrap_or((&magnitude, ""));
|
||||
let whole = integer.parse::<u64>().map_err(|_| DocError::Invalid("rounded display magnitude overflow".into()))?;
|
||||
if whole > 1_000_000_000_000 || (whole == 1_000_000_000_000 && fraction.bytes().any(|d| d != b'0')) {
|
||||
return Err(DocError::Invalid("rounded display magnitude must be at most 1e12".into()));
|
||||
}
|
||||
// round_prepared needs at least the requested fraction digits, including for integer steps.
|
||||
let missing = usize::from(decimals).saturating_sub(fraction.len());
|
||||
if missing > 0 {
|
||||
if !magnitude.contains('.') {
|
||||
magnitude.push('.');
|
||||
}
|
||||
magnitude.extend(std::iter::repeat_n('0', missing));
|
||||
}
|
||||
round_prepared(&magnitude, x.is_sign_negative(), decimals, ties)
|
||||
}
|
||||
|
||||
fn grouped_digits(text: &mut String, digits: &str, integer: bool, groups: bool) {
|
||||
for (i, digit) in digits.chars().enumerate() {
|
||||
if groups && digits.len() >= 5 && i > 0 && if integer { (digits.len() - i).is_multiple_of(3) } else { i.is_multiple_of(3) } {
|
||||
@@ -576,214 +471,6 @@ mod number_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_ties_and_decimal_fractions() {
|
||||
for (x, step, dp, even, away) in [
|
||||
(1.25, 0.5, 1, "1.0", "1.5"),
|
||||
(1.75, 0.5, 1, "2.0", "2.0"),
|
||||
(12.5, 5.0, 0, "10", "15"),
|
||||
(25.0, 10.0, 0, "20", "30"),
|
||||
(0.125, 0.05, 2, "0.10", "0.15"),
|
||||
(0.375, 0.25, 2, "0.50", "0.50"),
|
||||
(0.15, 0.1, 1, "0.2", "0.2"),
|
||||
(0.25, 0.1, 1, "0.2", "0.3"),
|
||||
(1.25, 0.5, 0, "1", "2"),
|
||||
] {
|
||||
for (ties, expected) in [(Ties::HalfEven, even), (Ties::HalfAwayFromZero, away)] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
for sign in [1.0, -1.0] {
|
||||
let shown = format_number_with_step(sign * x, dp, step, &f).unwrap();
|
||||
assert_eq!(shown.text, if sign < 0.0 { format!("−{expected}") } else { expected.into() });
|
||||
assert_eq!(shown.value, sign * expected.parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_prepares_display_before_division_and_keeps_boundary_neighbors() {
|
||||
for ties in [Ties::HalfEven, Ties::HalfAwayFromZero] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
for x in [1.75_f64.next_down(), 1.75, 1.75_f64.next_up(), 1.7499999996] {
|
||||
assert_eq!(format_number_with_step(x, 1, 0.5, &f).unwrap().text, "2.0");
|
||||
}
|
||||
for x in [1.25_f64.next_down(), 1.25, 1.25_f64.next_up()] {
|
||||
let expected = if ties == Ties::HalfEven { "1.0" } else { "1.5" };
|
||||
assert_eq!(format_number_with_step(x, 1, 0.5, &f).unwrap().text, expected);
|
||||
assert_eq!(format_number_with_step(-x, 1, 0.5, &f).unwrap().text, format!("−{expected}"));
|
||||
}
|
||||
assert_eq!(format_number_with_step(1.7499999994, 1, 0.5, &f).unwrap().text, "1.5");
|
||||
assert_eq!(format_number_with_step(1.7500000006, 1, 0.5, &f).unwrap().text, "2.0");
|
||||
}
|
||||
let f = NumberFormat::default();
|
||||
// The precision floor narrows the preparation window at eight decimals.
|
||||
assert_eq!(format_number_with_step(1.7499999996, 8, 0.5, &f).unwrap().text, "1.50000000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_tiny_values_large_magnitudes_and_exact_final_digits() {
|
||||
let f = NumberFormat::default();
|
||||
for step in [f64::from_bits(1), f64::MIN_POSITIVE, 1e-200, 1e-20, 1e-8] {
|
||||
for x in [0.0, -0.0, f64::from_bits(1), -1e11, 1e11, 34947628153.461784] {
|
||||
let shown = format_number_with_step(x, 5, step, &f).unwrap();
|
||||
assert!(shown.value.is_finite());
|
||||
assert_eq!(shown.value, shown.text.replace('−', "-").parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
assert_eq!(format_number_with_step(1e12, 8, f64::from_bits(1), &f).unwrap().text, "1000000000000.00000000");
|
||||
assert_eq!(format_number_with_step(34947628153.461784, 5, 1e-8, &f).unwrap().text, "34947628153.46178");
|
||||
// A tiny step can move an exact decimal tie by less than any f64 ULP.
|
||||
// Keep those digits through the second rounding rather than parsing the snap to f64.
|
||||
assert_eq!(format_number_with_step(1.25, 1, 3e-323, &f).unwrap().text, "1.3");
|
||||
assert_eq!(format_number_with_step(-1.25, 1, 3e-323, &f).unwrap().text, "−1.3");
|
||||
for step in [0.5, 10.0, 1e6] {
|
||||
for x in [-0.0, -0.1] {
|
||||
let shown = format_number_with_step(x, 2, step, &f).unwrap();
|
||||
assert_eq!(shown.text, "0.00");
|
||||
assert_eq!(shown.value.to_bits(), 0.0_f64.to_bits());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_extreme_coefficients_have_independent_expected_digits() {
|
||||
// Shortest decimals: largest subnormal = 2225073858507201 * 10^-323,
|
||||
// smallest normal = 22250738585072014 * 10^-324. For x=1.25, integer
|
||||
// division leaves remainders 2221948386734081 and 354296156594724.
|
||||
// Twice the first is above its divisor, twice the second below: the
|
||||
// snap is just above/below 1.25, so final rounding gives 1.3/1.2 in BOTH modes.
|
||||
// Other expected digits were calculated with independent rational arithmetic.
|
||||
for (step, quarter) in [(f64::MIN_POSITIVE.next_down(), "1.3"), (f64::MIN_POSITIVE, "1.2")] {
|
||||
for ties in [Ties::HalfEven, Ties::HalfAwayFromZero] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
for (x, dp, expected) in [
|
||||
(1.25, 1, quarter),
|
||||
(99999999999.75, 0, "100000000000"),
|
||||
(99999999999.75, 1, "99999999999.7"),
|
||||
(99999999999.75, 2, "99999999999.75"),
|
||||
(34947628153.461784, 2, "34947628153.46"),
|
||||
(34947628153.461784, 5, "34947628153.46178"),
|
||||
] {
|
||||
for sign in [1.0, -1.0] {
|
||||
let shown = format_number_with_step(sign * x, dp, step, &f).unwrap();
|
||||
let text = if sign < 0.0 { format!("−{expected}") } else { expected.into() };
|
||||
assert_eq!(shown.text, text, "x={x:?}, step={step:e}, dp={dp}, ties={ties:?}");
|
||||
assert_eq!(shown.value, sign * expected.parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_and_final_decimal_rounding_carry_independently() {
|
||||
// 9.975 / .05 = 199.5: both modes increment the step index to 200.
|
||||
// 9.96 / .03 = 332 exactly: only the final rounding carries to 10.0.
|
||||
// 1.26 / .1 = 12.6: the snap is 1.3, then zero decimals give 1 (not 1.26).
|
||||
// 1.49 / .5 = 2.98: the snap is 1.5, then zero decimals give 2 (not 1).
|
||||
for (x, step, dp, even, away) in [
|
||||
(9.975, 0.05, 2, "10.00", "10.00"),
|
||||
(9.96, 0.03, 1, "10.0", "10.0"),
|
||||
(1.26, 0.1, 0, "1", "1"),
|
||||
(1.49, 0.5, 0, "2", "2"),
|
||||
(100000000000.25, 0.1, 0, "100000000000", "100000000000"),
|
||||
(100000000000.25, 0.1, 1, "100000000000.2", "100000000000.3"),
|
||||
(100000000000.25, 0.1, 2, "100000000000.20", "100000000000.30"),
|
||||
(999999999999.75, 0.25, 0, "1000000000000", "1000000000000"),
|
||||
] {
|
||||
for (ties, expected) in [(Ties::HalfEven, even), (Ties::HalfAwayFromZero, away)] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
for sign in [1.0, -1.0] {
|
||||
let shown = format_number_with_step(sign * x, dp, step, &f).unwrap();
|
||||
assert_eq!(shown.text, if sign < 0.0 { format!("−{expected}") } else { expected.into() });
|
||||
assert_eq!(shown.value, sign * expected.parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_rejects_invalid_inputs_and_out_of_range_snap() {
|
||||
let f = NumberFormat::default();
|
||||
for step in [0.0, -0.0, -0.5, f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1e6_f64.next_up()] {
|
||||
assert!(matches!(format_number_with_step(1.0, 2, step, &f), Err(DocError::Invalid(_))));
|
||||
assert!(matches!(format_value_with_step(1.0, Length, "mm", 2, step, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
for x in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1e12_f64.next_up(), -1e12_f64.next_up()] {
|
||||
assert!(matches!(format_number_with_step(x, 2, 0.5, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
for dp in [9, 255] {
|
||||
assert!(matches!(format_number_with_step(1.0, dp, 0.5, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
for x in [1e12, -1e12] {
|
||||
for ties in [Ties::HalfEven, Ties::HalfAwayFromZero] {
|
||||
let f = NumberFormat { ties, ..f };
|
||||
for step in [600_000.0, 0.6] {
|
||||
// The .6 snap has magnitude 1e12 + .2: reject it even though
|
||||
// zero decimal places would otherwise bring it back to 1e12.
|
||||
assert!(matches!(format_number_with_step(x, 0, step, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
}
|
||||
assert_eq!(format_number_with_step(x, 0, 1e6, &f).unwrap().value, x);
|
||||
}
|
||||
for (kind, unit) in [(Length, "deg"), (Angle, "mm"), (Length, "unknown")] {
|
||||
assert!(matches!(format_value_with_step(1.0, kind, unit, 2, 0.5, &f), Err(DocError::Invalid(_))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_units_options_and_input_preservation() {
|
||||
let f = NumberFormat::default();
|
||||
let input: f64 = 25.4 * 1.25;
|
||||
let bits = input.to_bits();
|
||||
assert_eq!(format_value_with_step(input, Length, "in", 1, 0.5, &f).unwrap().text, "1.0");
|
||||
assert_eq!(input.to_bits(), bits);
|
||||
assert_eq!(format_value_with_step(std::f64::consts::PI / 6.0, Angle, "deg", 1, 5.0, &f).unwrap().text, "30.0");
|
||||
assert_eq!(format_value_with_step(1e15, Length, "m", 1, 10.0, &f).unwrap().value, 1e12);
|
||||
let styled = NumberFormat { marker: DecimalMarker::Comma, groups: true, ..f };
|
||||
assert_eq!(format_number_with_step(-12345.25, 2, 0.5, &styled).unwrap().text, "−12\u{202f}345,00");
|
||||
let trim = NumberFormat { leading_zero: false, trailing_zeros: false, ..styled };
|
||||
assert_eq!(format_number_with_step(-0.6, 3, 0.5, &trim).unwrap().text, "−,5");
|
||||
assert_eq!(format_number_with_step(1.23456789, 8, 1e-8, &styled).unwrap().text, "1,234\u{202f}567\u{202f}89");
|
||||
for (distance, expected) in [(0.4, "1.0"), (0.6, "0.5")] {
|
||||
assert_eq!(format_value_with_step((0.75 - distance * 1e-4) * 1e-6, Length, "nm", 1, 0.5, &f).unwrap().text, expected);
|
||||
}
|
||||
for step in [1e6_f64.next_down(), 0.12345678901234567, 2.2250738585072014e-308] {
|
||||
let shown = format_number_with_step(12345.6789, 8, step, &f).unwrap();
|
||||
assert_eq!(shown.value, shown.text.parse::<f64>().unwrap());
|
||||
}
|
||||
// Explicitly keep both historical policies, including their different decimal ties.
|
||||
assert_eq!(format_number(2.665, 2, &f).unwrap().text, "2.66");
|
||||
assert_eq!(length_number(2.665, "mm", 2), "2.67");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_seeded_integer_reference_and_hostile_domain() {
|
||||
let mut seed = 0x4896_1138_0abc_def1;
|
||||
for _ in 0..20_000 {
|
||||
// Independent bounded integer oracle: x and step are thousandths; dp=3.
|
||||
let n = random(&mut seed) % 1_000_000;
|
||||
let s = random(&mut seed) % 100_000 + 1;
|
||||
for ties in [Ties::HalfEven, Ties::HalfAwayFromZero] {
|
||||
let f = NumberFormat { ties, ..NumberFormat::default() };
|
||||
let q = n / s;
|
||||
let r = n % s;
|
||||
let up = 2 * r > s || (2 * r == s && (ties == Ties::HalfAwayFromZero || q % 2 == 1));
|
||||
let expected = (q + u64::from(up)) * s;
|
||||
let text = format!("{}.{:03}", expected / 1000, expected % 1000);
|
||||
let shown = format_number_with_step(n as f64 / 1000.0, 3, s as f64 / 1000.0, &f).unwrap();
|
||||
assert_eq!(shown.text, text);
|
||||
// Exercise arbitrary positive subnormals and normal steps without quotient overflow.
|
||||
let step = f64::from_bits(random(&mut seed));
|
||||
if step.is_finite() && step > 0.0 && step <= 1e6 {
|
||||
let shown = format_number_with_step(n as f64, 3, step, &f).unwrap();
|
||||
assert!(shown.value.is_finite() && shown.value >= 0.0);
|
||||
assert_eq!(shown.value, shown.text.parse::<f64>().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn random(state: &mut u64) -> u64 {
|
||||
*state ^= *state << 13;
|
||||
*state ^= *state >> 7;
|
||||
|
||||
@@ -452,14 +452,8 @@ pub enum AxisRef {
|
||||
SketchAxis { axis: String },
|
||||
/// World X / Y / Z axis through the origin.
|
||||
World { axis: String },
|
||||
/// Any line in the sketch plane, in world coordinates. The origin's coordinates are length
|
||||
/// expressions, so the axis follows parameter changes.
|
||||
Line {
|
||||
#[serde(with = "point_expr")]
|
||||
#[walk(placement)]
|
||||
origin: PointExpr,
|
||||
dir: Dir,
|
||||
},
|
||||
/// Any line in the sketch plane, in world coordinates.
|
||||
Line { origin: Position, dir: Dir },
|
||||
/// An edge or round face of the model (a straight or circular edge, a cylinder, cone or
|
||||
/// torus face), found again on each evaluation: [`crate::construct::model_axis`].
|
||||
Model { geo: GeoRef },
|
||||
@@ -1050,8 +1044,6 @@ pub enum FeatureKind {
|
||||
Split {
|
||||
body: BodyName,
|
||||
plane: PlaneRef,
|
||||
// The tool face is picked (and named), so the split follows it (#11674).
|
||||
#[walk(picks_faces)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
tool: Option<FaceAt>,
|
||||
/// A straight line of this sketch as the tool (#1552): the plane through it along the
|
||||
@@ -2418,12 +2410,6 @@ pub enum HoleKind {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[walk(angle = "Tip angle")]
|
||||
tip_angle: Option<Expr>,
|
||||
/// Printing aid: two bridge layers this thick below the counterbore's floor (a slot along
|
||||
/// X, then a square), so a printer bridges the step instead of sagging into it. Absent:
|
||||
/// a plain counterbore.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[walk(length = "Bridge layer")]
|
||||
bridge: Option<Expr>,
|
||||
},
|
||||
Countersink {
|
||||
#[walk(length = "Countersink diameter")]
|
||||
|
||||
@@ -659,7 +659,7 @@ impl ModelState {
|
||||
}
|
||||
|
||||
/// A digest of everything later features can read: bodies (by content identity), sketches,
|
||||
/// the body counter, threads, sheet bodies and construction geometry.
|
||||
/// the body counter, threads and sheet bodies.
|
||||
fn digest(&self) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
for b in &self.bodies {
|
||||
@@ -701,19 +701,12 @@ impl ModelState {
|
||||
}
|
||||
serde_json::to_string(&self.threads).unwrap_or_default().hash(&mut h);
|
||||
serde_json::to_string(&self.sheets).unwrap_or_default().hash(&mut h);
|
||||
// Construction axes, planes and points so far: features find them here by name, and a
|
||||
// result taken from the cache brings back the ones it was made with, so one moved past
|
||||
// a feature must change the key (#9544). A state without any hashes as before.
|
||||
if !self.construct.is_empty() {
|
||||
serde_json::to_string(&self.construct).unwrap_or_default().hash(&mut h);
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// After a feature evaluated to this state from `before` with cache key `key`: bodies it
|
||||
/// made or changed take that key, sketches it made a digest of their solved geometry (see
|
||||
/// [`quantize`]); the rest keep theirs.
|
||||
/// `f` must have gone through [`naming_feature`]: face naming reads no parameter values.
|
||||
fn stamp(&mut self, doc: &Document, before: &Arc<ModelState>, f: &Feature, key: u64, mats: Vec<Mat>) {
|
||||
let feature = f.id;
|
||||
let depth = before.bodies.iter().filter_map(|b| b.names.depth()).max().unwrap_or(0) + 1;
|
||||
@@ -1091,8 +1084,8 @@ pub struct Model {
|
||||
pub no_cache: bool,
|
||||
/// Results kept besides the cache ([`Model::keep`]).
|
||||
kept: KeptSlot,
|
||||
/// Embedded imports' content digests from the last evaluation.
|
||||
import_digests: ImportDigests,
|
||||
/// The imported texts' digests the last `evaluate` fingerprinted the features with.
|
||||
step_digests: StepDigests,
|
||||
/// The address of the shared document whose fingerprints the results were last given in
|
||||
/// full, with [`fingerprints_digest`] of them then ([`Model::pinned_prefix`]). Only an
|
||||
/// address: a reference to the document would make every in-place edit of it a copy.
|
||||
@@ -1131,7 +1124,7 @@ fn param_names(f: &Feature) -> Vec<String> {
|
||||
names
|
||||
}
|
||||
|
||||
fn fingerprint(doc: &Document, prev: u64, f: &Feature, vals: &BTreeMap<String, Value>, rolled_back: bool, steps: &ImportDigests) -> u64 {
|
||||
fn fingerprint(doc: &Document, prev: u64, f: &Feature, vals: &BTreeMap<String, Value>, rolled_back: bool, steps: &StepDigests) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
prev.hash(&mut h);
|
||||
rolled_back.hash(&mut h);
|
||||
@@ -1152,73 +1145,34 @@ fn fingerprint(doc: &Document, prev: u64, f: &Feature, vals: &BTreeMap<String, V
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Content digests for embedded imports. Shared, unchanged arrays/text are read once;
|
||||
/// feature metadata is still fingerprinted on every evaluation. Rebuilding this map prunes
|
||||
/// removed features and never trusts an address without retaining and checking its Arc.
|
||||
/// The digests of the Import features' STEP texts, by feature id, with the text each was taken
|
||||
/// from. Every evaluation fingerprints every feature, and writing out a design's imported files
|
||||
/// each time made every command as slow as the files are large: a text is shared (`Arc`), so
|
||||
/// its digest is taken once and kept while the feature holds that same text.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ImportDigests {
|
||||
steps: std::collections::HashMap<u64, (Arc<str>, u64)>,
|
||||
meshes: std::collections::HashMap<u64, Vec<MeshDigest>>,
|
||||
}
|
||||
struct StepDigests(std::collections::HashMap<u64, (Arc<str>, u64)>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct MeshDigest {
|
||||
positions: Arc<[f64]>,
|
||||
triangles: Arc<[u32]>,
|
||||
digest: u64,
|
||||
}
|
||||
|
||||
impl MeshDigest {
|
||||
fn matches(&self, mesh: &crate::MeshData) -> bool {
|
||||
Arc::ptr_eq(&self.positions, &mesh.positions) && Arc::ptr_eq(&self.triangles, &mesh.triangles)
|
||||
}
|
||||
}
|
||||
|
||||
impl ImportDigests {
|
||||
fn of(doc: &Document, known: &ImportDigests) -> ImportDigests {
|
||||
let mut out = ImportDigests::default();
|
||||
for f in &doc.features {
|
||||
match &f.kind {
|
||||
FeatureKind::Import { step, .. } => {
|
||||
let digest = known.get(f.id, step).unwrap_or_else(|| step_text_digest(step));
|
||||
out.steps.insert(f.id, (step.clone(), digest));
|
||||
}
|
||||
FeatureKind::MeshImport { meshes, .. } => {
|
||||
let digests = meshes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, mesh)| {
|
||||
let digest = known.mesh(f.id, index, mesh).unwrap_or_else(|| mesh_content_digest(mesh));
|
||||
MeshDigest { positions: mesh.positions.clone(), triangles: mesh.triangles.clone(), digest }
|
||||
})
|
||||
.collect();
|
||||
out.meshes.insert(f.id, digests);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
impl StepDigests {
|
||||
/// The digests of `doc`'s imported texts, reusing `known` for texts it has seen.
|
||||
fn of(doc: &Document, known: &StepDigests) -> StepDigests {
|
||||
let texts = doc.features.iter().filter_map(|f| match &f.kind {
|
||||
FeatureKind::Import { step, .. } => Some((f.id, step)),
|
||||
_ => None,
|
||||
});
|
||||
StepDigests(
|
||||
texts
|
||||
.map(|(id, step)| {
|
||||
let digest = known.get(id, step).unwrap_or_else(|| step_text_digest(step));
|
||||
(id, (step.clone(), digest))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The digest of `step`, when it is the text feature `id` was last seen with.
|
||||
fn get(&self, id: u64, step: &Arc<str>) -> Option<u64> {
|
||||
self.steps.get(&id).filter(|(text, _)| Arc::ptr_eq(text, step)).map(|(_, digest)| *digest)
|
||||
self.0.get(&id).filter(|(text, _)| Arc::ptr_eq(text, step)).map(|(_, digest)| *digest)
|
||||
}
|
||||
|
||||
fn mesh(&self, id: u64, index: usize, mesh: &crate::MeshData) -> Option<u64> {
|
||||
self.meshes.get(&id)?.get(index).filter(|cached| cached.matches(mesh)).map(|cached| cached.digest)
|
||||
}
|
||||
}
|
||||
|
||||
fn mesh_content_digest(mesh: &crate::MeshData) -> u64 {
|
||||
#[cfg(test)]
|
||||
cost_tests::count(|c| c.mesh_digested += 1);
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
mesh.positions.len().hash(&mut h);
|
||||
for coordinate in mesh.positions.iter() {
|
||||
coordinate.to_bits().hash(&mut h);
|
||||
}
|
||||
mesh.triangles.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
fn step_text_digest(text: &str) -> u64 {
|
||||
@@ -1254,12 +1208,12 @@ fn quantize(sk: &mut solidcraft_sketch::Sketch) {
|
||||
}
|
||||
|
||||
/// A feature as fingerprinted: its definition, a sketch's stored solve quantized, an imported
|
||||
/// file's text or mesh payload by its content digest.
|
||||
/// file's text by its digest.
|
||||
/// Hash [`feature_json`] of `f` into `h`. Every evaluation fingerprints every feature, and in a
|
||||
/// design built command by command almost all of them are the same as last time: the text of a
|
||||
/// feature is kept (by id) with the feature it was written from, and written again only when the
|
||||
/// feature differs. Embedded imports also depend on their content digests, so their small
|
||||
/// metadata-and-digest representation is written each time without comparing large arrays.
|
||||
/// feature differs. An Import's text also depends on its STEP digest, so it is written each time
|
||||
/// as before.
|
||||
///
|
||||
/// The texts are shared by every thread: a command evaluated ahead runs on a new worker thread
|
||||
/// each time (the desktop's deferred commands and delete checks, the control channel's ahead
|
||||
@@ -1267,10 +1221,10 @@ fn quantize(sk: &mut solidcraft_sketch::Sketch) {
|
||||
/// feature again. The lock is held only to look up or store an entry, never while a feature is
|
||||
/// compared or written; an entry is the same whichever thread wrote it, since the text is a
|
||||
/// function of the feature alone.
|
||||
fn hash_feature_json(f: &Feature, steps: &ImportDigests, h: &mut impl Hasher) {
|
||||
fn hash_feature_json(f: &Feature, steps: &StepDigests, h: &mut impl Hasher) {
|
||||
type Written = std::collections::HashMap<u64, Arc<(Feature, String)>>;
|
||||
static WRITTEN: std::sync::LazyLock<std::sync::Mutex<Written>> = std::sync::LazyLock::new(Default::default);
|
||||
if matches!(f.kind, FeatureKind::Import { .. } | FeatureKind::MeshImport { .. }) {
|
||||
if matches!(f.kind, FeatureKind::Import { .. }) {
|
||||
feature_json(f, steps).hash(h);
|
||||
return;
|
||||
}
|
||||
@@ -1293,21 +1247,7 @@ fn hash_feature_json(f: &Feature, steps: &ImportDigests, h: &mut impl Hasher) {
|
||||
written.insert(f.id, entry);
|
||||
}
|
||||
|
||||
fn feature_json(f: &Feature, steps: &ImportDigests) -> String {
|
||||
if matches!(&f.kind, FeatureKind::MeshImport { .. }) {
|
||||
let mut g = f.clone();
|
||||
let mut digests = Vec::new();
|
||||
if let FeatureKind::MeshImport { meshes, .. } = &mut g.kind {
|
||||
for (index, mesh) in meshes.iter_mut().enumerate() {
|
||||
digests.push(steps.mesh(f.id, index, mesh).unwrap_or_else(|| mesh_content_digest(mesh)));
|
||||
mesh.positions = Arc::from([]);
|
||||
mesh.triangles = Arc::from([]);
|
||||
}
|
||||
}
|
||||
// Keep the complete definition (including mesh order, names and colors) without
|
||||
// writing the large numeric arrays out again. Equal content in fresh Arcs hashes alike.
|
||||
return serde_json::to_string(&(g, digests)).unwrap_or_default();
|
||||
}
|
||||
fn feature_json(f: &Feature, steps: &StepDigests) -> String {
|
||||
if let FeatureKind::Import { step, .. } = &f.kind {
|
||||
let mut g = f.clone();
|
||||
if let FeatureKind::Import { step: text, .. } = &mut g.kind {
|
||||
@@ -1382,12 +1322,6 @@ impl Document {
|
||||
pub fn evaluation_values(&self) -> BTreeMap<String, Value> {
|
||||
evaluation_values(self).0
|
||||
}
|
||||
|
||||
/// A placement point read as the features read it: the parameters, and a bare number
|
||||
/// beside them in the design's length unit (#9015).
|
||||
pub fn eval_point(&self, p: &crate::PointExpr) -> Result<Vec3> {
|
||||
point(&self.evaluation_values(), p)
|
||||
}
|
||||
}
|
||||
|
||||
/// The values features are evaluated with: the parameters, and the design's length unit.
|
||||
@@ -1410,7 +1344,7 @@ fn chain_fingerprint(
|
||||
f: &Feature,
|
||||
prev: u64,
|
||||
rolled_back: bool,
|
||||
steps: &ImportDigests,
|
||||
steps: &StepDigests,
|
||||
st: Option<&ModelState>,
|
||||
) -> u64 {
|
||||
let mut fp = fingerprint(doc, prev, f, vals, rolled_back, steps) ^ placement_digest(doc, f, st);
|
||||
@@ -1488,7 +1422,7 @@ fn context_digest(doc: &Document, vals: &BTreeMap<String, Value>) -> u64 {
|
||||
for f in doc.features.iter().filter(|f| {
|
||||
matches!(f.kind, FeatureKind::ConstructionPlane { .. } | FeatureKind::ConstructionAxis { .. } | FeatureKind::ConstructionPoint { .. })
|
||||
}) {
|
||||
fingerprint(doc, 0, f, vals, false, &ImportDigests::default()).hash(&mut h);
|
||||
fingerprint(doc, 0, f, vals, false, &StepDigests::default()).hash(&mut h);
|
||||
}
|
||||
doc.units.hash(&mut h);
|
||||
serde_json::to_string(&doc.sheet).unwrap_or_default().hash(&mut h);
|
||||
@@ -1716,13 +1650,6 @@ fn placement_digest(doc: &Document, f: &Feature, st: Option<&ModelState>) -> u64
|
||||
for (sketch, _) in f.kind.profile_sels() {
|
||||
if doc.feature(sketch).is_some_and(|s| s.component != f.component) {
|
||||
reads.sketch(doc, sketch);
|
||||
// A later transfer can preserve this profile's historical frame. Changes to that
|
||||
// boundary (including suppression and rollback) must invalidate the constructor.
|
||||
if let Some(relative) = crate::body_transfer::profile_frame(doc, f, sketch) {
|
||||
"transferred profile".hash(&mut h);
|
||||
sketch.hash(&mut h);
|
||||
hash_placement(&relative, &mut h);
|
||||
}
|
||||
}
|
||||
}
|
||||
let path = match &f.kind {
|
||||
@@ -1988,7 +1915,7 @@ impl Model {
|
||||
}
|
||||
// The fingerprints from the first stored sketch on.
|
||||
let vals = evaluation_values(doc).0;
|
||||
let steps = ImportDigests::of(doc, &self.import_digests);
|
||||
let steps = StepDigests::of(doc, &self.step_digests);
|
||||
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) {
|
||||
@@ -2038,7 +1965,7 @@ impl Model {
|
||||
return false;
|
||||
}
|
||||
let vals = evaluation_values(doc).0;
|
||||
let steps = ImportDigests::of(doc, &self.import_digests);
|
||||
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).
|
||||
@@ -2103,7 +2030,7 @@ impl Model {
|
||||
let previous_context = self.context;
|
||||
let mut rolled_results = std::mem::take(&mut self.rolled_results);
|
||||
self.context = ctx;
|
||||
self.import_digests = ImportDigests::of(doc, &self.import_digests);
|
||||
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).
|
||||
@@ -2138,7 +2065,7 @@ impl Model {
|
||||
};
|
||||
// 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.import_digests, Some(&state));
|
||||
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
|
||||
@@ -2214,7 +2141,7 @@ impl Model {
|
||||
}
|
||||
// The same feature on the same inputs: its earlier result.
|
||||
let key =
|
||||
cache_key(f, fp, fingerprint(doc, 0, f, &vals, false, &self.import_digests) ^ placement_digest(doc, f, Some(&state)), ctx, &state);
|
||||
cache_key(f, fp, fingerprint(doc, 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
|
||||
@@ -2349,9 +2276,7 @@ impl Model {
|
||||
}
|
||||
_ => mats,
|
||||
};
|
||||
// A revolve axis origin that follows parameters names its faces from its values.
|
||||
let named = naming_feature(&vals, f);
|
||||
next.stamp(doc, &state, &named, key, mats);
|
||||
next.stamp(doc, &state, f, key, mats);
|
||||
// Evaluated again only for what came before it (a mirror moved by a drag).
|
||||
if let Some(old) = self.results.get(i).filter(|old| old.id == f.id) {
|
||||
let made = old.state.bodies.iter().filter(|b| b.feature == f.id).map(|b| (b.name.as_str(), b));
|
||||
@@ -2570,7 +2495,7 @@ impl Model {
|
||||
pub(crate) fn refingerprint_from(&mut self, doc: &Document, start: usize) {
|
||||
self.placement_inputs = None;
|
||||
self.fingerprinted = None;
|
||||
self.import_digests = ImportDigests::of(doc, &self.import_digests);
|
||||
self.step_digests = StepDigests::of(doc, &self.step_digests);
|
||||
let start = start.min(self.results.len()).min(doc.features.len());
|
||||
if start == self.results.len().min(doc.features.len()) {
|
||||
return;
|
||||
@@ -2582,7 +2507,7 @@ impl Model {
|
||||
// Each feature's fingerprint reads the model it starts from, as evaluating does.
|
||||
let mut before = last.map_or_else(|| Arc::new(ModelState::default()), |r| r.state.clone());
|
||||
for (i, (r, f)) in self.results.iter_mut().zip(&doc.features).enumerate().skip(start) {
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &self.import_digests, Some(&before));
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &self.step_digests, Some(&before));
|
||||
r.fingerprint = prev;
|
||||
before = r.state.clone();
|
||||
}
|
||||
@@ -3005,67 +2930,13 @@ fn clear_tools_as_one(tools: Vec<Traced>) -> Result<Vec<Traced>> {
|
||||
return Ok(tools);
|
||||
}
|
||||
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
|
||||
let one = match kernel::union_apart(&bodies)? {
|
||||
Some(one) => Some(one),
|
||||
// The kernel only compares the tools' boxes, so copies of a through hole close together
|
||||
// on a circle (their boxes overlap) seem to meet (#9007). Their exact shapes may still
|
||||
// be apart.
|
||||
None if tools_clearly_apart(&bodies) => kernel::join_apart(&bodies).ok().flatten(),
|
||||
None => None,
|
||||
};
|
||||
#[cfg(test)]
|
||||
cost_tests::count(|c| c.cut_tools += one.as_ref().map_or(tools.len(), |_| 1));
|
||||
Ok(match one {
|
||||
Ok(match kernel::union_apart(&bodies)? {
|
||||
// Each piece's faces keep the names of the tool it is.
|
||||
Some(one) => vec![Traced::step(one, &tools.iter().collect::<Vec<_>>())],
|
||||
None => tools,
|
||||
})
|
||||
}
|
||||
|
||||
/// Most pairs of tools whose boxes meet that [`tools_clearly_apart`] tests.
|
||||
const MAX_CLOSE_TOOL_PAIRS: usize = 4096;
|
||||
|
||||
/// Whether no two of `bodies` meet or touch, decided on their exact shapes. Only pairs whose
|
||||
/// exact boxes, grown by twice the bodies' tolerances (as [`kernel::apart`] grows them), meet
|
||||
/// are tested, by [`apart_by_union`]. `false` whenever that cannot be shown: a mesh body, a body
|
||||
/// without an exact box, too many pairs to test, a union that fails.
|
||||
fn tools_clearly_apart(bodies: &[Body]) -> bool {
|
||||
if bodies.len() < 2 || bodies.len() > 10_000 || bodies.iter().any(Body::is_mesh) {
|
||||
return false;
|
||||
}
|
||||
let Some(boxes) = bodies.iter().map(more::exact_box).collect::<Option<Vec<_>>>() else { return false };
|
||||
let mut order: Vec<usize> = (0..boxes.len()).collect();
|
||||
order.sort_by(|&i, &j| boxes.get(i).map_or(0.0, |b| b.min.x).total_cmp(&boxes.get(j).map_or(0.0, |b| b.min.x)));
|
||||
let mut close = Vec::new();
|
||||
for (k, &i) in order.iter().enumerate() {
|
||||
let Some(a) = boxes.get(i) else { return false };
|
||||
for &j in order.iter().skip(k + 1) {
|
||||
let Some(b) = boxes.get(j) else { return false };
|
||||
if b.min.x > a.max.x {
|
||||
break;
|
||||
}
|
||||
if more::boxes_apart(&Some(*a), &Some(*b)) {
|
||||
continue;
|
||||
}
|
||||
if close.len() == MAX_CLOSE_TOOL_PAIRS {
|
||||
return false;
|
||||
}
|
||||
close.push((i, j));
|
||||
}
|
||||
}
|
||||
close.into_iter().all(|(i, j)| matches!((bodies.get(i), bodies.get(j)), (Some(a), Some(b)) if apart_by_union(a, b)))
|
||||
}
|
||||
|
||||
/// Whether the exact union of `a` and `b` comes out in two pieces that share no face, edge or
|
||||
/// vertex. Crossing and nested bodies come out in one piece, touching ones in one or in two that
|
||||
/// share an edge or a face.
|
||||
fn apart_by_union(a: &Body, b: &Body) -> bool {
|
||||
match kernel::boolean(a, b, BoolOp::Union) {
|
||||
Ok(Some(u)) => u.record().material_regions().len() == 2 && u.region_contacts().is_ok_and(|c| c.is_empty()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -3668,11 +3539,10 @@ pub fn hole_on_surface(st: &ModelState, p: Vec3, dir: Vec3, scope: &[String]) ->
|
||||
/// The faces of the model near `p`: for each, how far `p` is from it, the outward normal there
|
||||
/// (the surface's own normal for planes, cylinders and spheres, turned the way the face looks out
|
||||
/// of its body; the nearest mesh facet's otherwise) and, for a plane, how far `p` is from the
|
||||
/// plane itself. Only faces within `reach` of `p`, nearest first, of the bodies in `scope`
|
||||
/// (every body when it is empty).
|
||||
fn faces_through(st: &ModelState, p: Vec3, reach: f64, scope: &[String]) -> Vec<(f64, Vec3, Option<f64>)> {
|
||||
/// plane itself. Only faces within `reach` of `p`, nearest first.
|
||||
fn faces_through(st: &ModelState, p: Vec3, reach: f64) -> Vec<(f64, Vec3, Option<f64>)> {
|
||||
let mut out: Vec<(f64, Vec3, Option<f64>)> = Vec::new();
|
||||
for b in st.bodies.iter().filter(|b| !b.body.is_mesh() && in_hole_scope(scope, b)) {
|
||||
for b in st.bodies.iter().filter(|b| !b.body.is_mesh()) {
|
||||
let m = b.mesh();
|
||||
// The nearest facet of each face of this body.
|
||||
let mut near: BTreeMap<u32, (f64, Vec3)> = BTreeMap::new();
|
||||
@@ -3718,29 +3588,13 @@ fn faces_through(st: &ModelState, p: Vec3, reach: f64, scope: &[String]) -> Vec<
|
||||
out
|
||||
}
|
||||
|
||||
/// The faces near `p` a hole placed there goes by (as [`faces_through`]): those of the bodies in
|
||||
/// `scope`, the hole's own, when one of them passes within `on` of `p`, else those of every body.
|
||||
/// Where a plate's face touches another body both faces pass through `p`, and a hole that drills
|
||||
/// the plate goes by the plate's face, never the other body's (#12066). A hole placed on another
|
||||
/// body's face, to drill through it into its own body behind, still goes by that face.
|
||||
fn hole_faces_through(st: &ModelState, p: Vec3, reach: f64, on: f64, scope: &[String]) -> Vec<(f64, Vec3, Option<f64>)> {
|
||||
if !scope.is_empty() {
|
||||
let own = faces_through(st, p, reach, scope);
|
||||
if own.first().is_some_and(|(d, _, _)| *d <= on) {
|
||||
return own;
|
||||
}
|
||||
}
|
||||
faces_through(st, p, reach, &[])
|
||||
}
|
||||
|
||||
/// The way a hole placed at `p` drills: into the material, against the outward normal of the
|
||||
/// face `p` lies on. A point on no face (a planar face's centroid outside it, say) takes the
|
||||
/// nearest planar face whose plane it lies in, else the face nearest it within its body's pick
|
||||
/// tolerance. None when no face is near. Where the faces of several bodies pass through `p`, those
|
||||
/// of the bodies in `scope` (the bodies the hole drills; empty for every body) decide.
|
||||
pub fn hole_direction_at(st: &ModelState, p: Vec3, scope: &[String]) -> Option<Vec3> {
|
||||
/// tolerance. None when no face is near.
|
||||
pub fn hole_direction_at(st: &ModelState, p: Vec3) -> Option<Vec3> {
|
||||
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
|
||||
let faces = hole_faces_through(st, p, f64::INFINITY, size * 1e-4, scope);
|
||||
let faces = faces_through(st, p, f64::INFINITY);
|
||||
if let Some((d, n, _)) = faces.first()
|
||||
&& *d <= size * 1e-4
|
||||
{
|
||||
@@ -3757,13 +3611,10 @@ pub fn hole_direction_at(st: &ModelState, p: Vec3, scope: &[String]) -> Option<V
|
||||
/// starts on (as designs saved before format 18 can say: their direction was taken from any face
|
||||
/// whose plane passed near the point, such as the other side of a thin plate or a face of another
|
||||
/// body at the same height), it is mirrored in the face it starts on, so it goes in at the same
|
||||
/// angle. A direction that enters a face there, or a spot on no face, is kept. Where the faces of
|
||||
/// the hole's own bodies (`scope`, see [`hole_drill_scope`]) pass through the spot, only they
|
||||
/// count: a hole that names the plate it drills, saved with the direction into a body touching
|
||||
/// the plate there, drills the plate (#12066).
|
||||
fn hole_dir_into_face(st: &ModelState, p: Vec3, dir: Vec3, scope: &[String]) -> Vec3 {
|
||||
/// angle. A direction that enters a face there, or a spot on no face, is kept.
|
||||
fn hole_dir_into_face(st: &ModelState, p: Vec3, dir: Vec3) -> Vec3 {
|
||||
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
|
||||
let faces = hole_faces_through(st, p, size * 1e-5, size * 1e-5, scope);
|
||||
let faces = faces_through(st, p, size * 1e-5);
|
||||
if faces.is_empty() || faces.iter().any(|(_, n, _)| n.dot(dir) < -1e-9) {
|
||||
return dir;
|
||||
}
|
||||
@@ -4235,26 +4086,10 @@ fn blend_body(
|
||||
Ok((Traced::step(nb.with_preserved_planar_sources(&mb.body)?, &[&from]), used, tapered))
|
||||
}
|
||||
|
||||
/// `f` as the naming of its faces reads it: a revolve's explicit axis origin written as the values
|
||||
/// it has now (the naming has no parameter values to read an expression with).
|
||||
fn naming_feature<'a>(vals: &BTreeMap<String, Value>, f: &'a Feature) -> std::borrow::Cow<'a, Feature> {
|
||||
if let FeatureKind::Revolve { axis: AxisRef::Line { origin, .. }, .. } = &f.kind
|
||||
&& let Ok(o) = point(vals, origin)
|
||||
{
|
||||
let mut g = f.clone();
|
||||
if let FeatureKind::Revolve { axis: AxisRef::Line { origin, .. }, .. } = &mut g.kind {
|
||||
*origin = [format!("{} mm", o.x).into(), format!("{} mm", o.y).into(), format!("{} mm", o.z).into()];
|
||||
}
|
||||
return std::borrow::Cow::Owned(g);
|
||||
}
|
||||
std::borrow::Cow::Borrowed(f)
|
||||
}
|
||||
|
||||
/// The revolve axis in the sketch's plane coordinates (a point and a direction). `st` is the model
|
||||
/// an axis on an edge or face of it is found in; `construct` gives a construction axis, by name, in
|
||||
/// the sketch's 3D frame; `vals` are the parameter values an explicit axis's origin is read with.
|
||||
/// the sketch's 3D frame.
|
||||
pub(crate) fn revolve_axis(
|
||||
vals: &BTreeMap<String, Value>,
|
||||
ss: &SolvedSketch,
|
||||
axis: &AxisRef,
|
||||
st: &ModelState,
|
||||
@@ -4289,7 +4124,7 @@ pub(crate) fn revolve_axis(
|
||||
}
|
||||
Ok((pl.to_local(Vec3::ZERO), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
|
||||
}
|
||||
AxisRef::Line { origin, dir } => line_in_sketch_plane(ss, point(vals, origin)?, dir.0),
|
||||
AxisRef::Line { origin, dir } => line_in_sketch_plane(ss, origin.0, dir.0),
|
||||
AxisRef::Model { geo } => {
|
||||
let (o, d) = crate::construct::model_axis(st, geo)?;
|
||||
line_in_sketch_plane(ss, o, d)
|
||||
@@ -4398,13 +4233,6 @@ fn hole_scope(f: &Feature) -> &[String] {
|
||||
}
|
||||
}
|
||||
|
||||
/// The bodies whose faces decide which way a hole placed at a position drills, where they pass
|
||||
/// through it: the bodies it cuts when it names them, so a body touching the face there can't
|
||||
/// turn it round (#12066). Empty (every body) otherwise, and for `["*"]`.
|
||||
fn hole_drill_scope(f: &Feature) -> &[String] {
|
||||
if f.participants.iter().any(|b| b == "*") { &[] } else { &f.participants }
|
||||
}
|
||||
|
||||
fn in_hole_scope(scope: &[String], b: &ModelBody) -> bool {
|
||||
scope.is_empty() || scope.contains(&b.name)
|
||||
}
|
||||
@@ -4430,7 +4258,7 @@ fn hole_spots(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature) -> R
|
||||
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
let at = hole_on_surface(st, single_hole_position(vals, st, position, at, points)?, dir, &[]);
|
||||
let at = with_references(vals, st, f, at, dir, true)?;
|
||||
let dir = hole_dir_into_face(st, at, dir, hole_drill_scope(f));
|
||||
let dir = hole_dir_into_face(st, at, dir);
|
||||
return Ok((vec![hole_on_surface(st, at, dir, &[])], dir));
|
||||
}
|
||||
};
|
||||
@@ -4478,7 +4306,7 @@ fn hole_spots_raw(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Feature)
|
||||
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
let p = single_hole_position(vals, st, position, at, points)?;
|
||||
let p = if hole_references(f).is_empty() { p } else { with_references(vals, st, f, hole_on_surface(st, p, dir, &[]), dir, false)? };
|
||||
Ok((vec![p], hole_dir_into_face(st, hole_on_surface(st, p, dir, &[]), dir, hole_drill_scope(f))))
|
||||
Ok((vec![p], hole_dir_into_face(st, hole_on_surface(st, p, dir, &[]), dir)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4807,41 +4635,18 @@ fn hole_tools_and_prisms(
|
||||
match hole {
|
||||
HoleKind::Simple => tools.extend(bore(None)?),
|
||||
HoleKind::Drilled { tip_angle } => tools.extend(bore(Some(tip_angle))?),
|
||||
HoleKind::Counterbore { cb_diameter, cb_depth, tip_angle, bridge } => {
|
||||
HoleKind::Counterbore { cb_diameter, cb_depth, tip_angle } => {
|
||||
let (rc, dc) = (val(vals, cb_diameter, Kind::Length)? / 2.0, val(vals, cb_depth, Kind::Length)?);
|
||||
if !(rc > r && dc > 0.0 && -dc > bottom) {
|
||||
return Err(DocError::Invalid("the counterbore must be wider than the hole and shallower than it".into()));
|
||||
}
|
||||
// Wide and shallow first: the narrow hole then crosses the pocket floor.
|
||||
tools.push(cyl(rc, -dc, top, "cbore")?);
|
||||
if let Some(layer) = bridge {
|
||||
let layer = val(vals, layer, Kind::Length)?;
|
||||
tools.extend(bridge_tools(&plane, r, rc, dc, bottom, layer)?);
|
||||
// Through all, `bottom` is the tool's reach, not the material's: the second layer
|
||||
// must end inside a body (exact classification), short of the exit face.
|
||||
if depth.is_none() {
|
||||
let past = *position + dir * (dc + 2.0 * layer + size * 1e-6);
|
||||
let classes = st.bodies.iter().map(|b| b.body.classify_point(past)).collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
if !classes.contains(&kernel::PointClass::Inside) {
|
||||
return Err(bridge_too_deep(layer, "make the counterbore shallower or the bridge thinner"));
|
||||
}
|
||||
}
|
||||
}
|
||||
tools.extend(bore(tip_angle.as_ref())?);
|
||||
}
|
||||
HoleKind::Countersink { cs_diameter, cs_angle, tip_angle } => {
|
||||
let (rc, half) = (val(vals, cs_diameter, Kind::Length)? / 2.0, val(vals, cs_angle, Kind::Angle)? / 2.0);
|
||||
// The angle and the width are told apart: an angle out of range is not a countersink
|
||||
// too narrow (#9625). The cone's length grows as 1 / tan(half), so a near-flat angle
|
||||
// is refused at a floor the message names, rather than built kilometres long.
|
||||
const MIN_ANGLE_DEG: f64 = 0.115;
|
||||
if !(2.0 * half > MIN_ANGLE_DEG.to_radians() && half < std::f64::consts::FRAC_PI_2) {
|
||||
return Err(DocError::Invalid(format!(
|
||||
"the countersink angle must be more than {MIN_ANGLE_DEG}° and less than 180°, not {}°",
|
||||
((2.0 * half).to_degrees() * 1e6).round() / 1e6
|
||||
)));
|
||||
}
|
||||
if !(rc > r) {
|
||||
if !(rc > r && half > 1e-3 && half < std::f64::consts::FRAC_PI_2) {
|
||||
return Err(DocError::Invalid("the countersink must be wider than the hole".into()));
|
||||
}
|
||||
// One revolved tool: the cone from its rim on the face to just inside the hole
|
||||
@@ -4865,59 +4670,6 @@ fn hole_tools_and_prisms(
|
||||
Ok(tools)
|
||||
}
|
||||
|
||||
/// How much wider than the hole a bridged counterbore's slot and square are, in mm.
|
||||
const BRIDGE_CLEARANCE: f64 = 0.02;
|
||||
|
||||
/// Why a bridged counterbore's two layers of `layer` were refused: they reach past the hole, and
|
||||
/// `fix` says what to change.
|
||||
fn bridge_too_deep(layer: f64, fix: &str) -> DocError {
|
||||
DocError::Invalid(format!("the two bridge layers ({:.3} mm) must fit in the hole below the counterbore: {fix}", 2.0 * layer))
|
||||
}
|
||||
|
||||
/// The bridge layers of a counterbore printed opening down: below its floor, a slot across the
|
||||
/// whole counterbore, along X (one `layer` thick), then a square (one more), both
|
||||
/// [`BRIDGE_CLEARANCE`] wider than the hole, so each layer spans between supported edges.
|
||||
/// `plane` is the hole's, its normal out of the material; the bore crosses both layers, clear of
|
||||
/// their sides.
|
||||
fn bridge_tools(plane: &Plane, r: f64, rc: f64, dc: f64, bottom: f64, layer: f64) -> Result<Vec<(Traced, Option<more::PrimitivePrism>)>> {
|
||||
if !(layer > 0.0 && layer.is_finite()) {
|
||||
return Err(DocError::Invalid("the bridge layer must be a positive length, such as 0.2 mm".into()));
|
||||
}
|
||||
let (t1, t2) = (-dc - layer, -dc - 2.0 * layer);
|
||||
if t2 <= bottom {
|
||||
return Err(bridge_too_deep(layer, "make the hole deeper or the bridge thinner"));
|
||||
}
|
||||
// Both a hair wider than the hole, so the bore's rim doesn't touch their sides.
|
||||
let q = r + BRIDGE_CLEARANCE / 2.0;
|
||||
// The slot's ends need room on the counterbore's wall past that.
|
||||
if !(rc >= q + BRIDGE_CLEARANCE / 2.0) {
|
||||
return Err(DocError::Invalid(format!("a bridged counterbore must be at least {} mm wider than the hole", 2.0 * BRIDGE_CLEARANCE)));
|
||||
}
|
||||
// The slot along world X where the axis allows it, else along the hole plane's own x.
|
||||
let n = plane.normal();
|
||||
let x = (Vec3::X - n * Vec3::X.dot(n)).normalized().filter(|_| Vec3::X.dot(n).abs() < 0.99).unwrap_or(plane.x);
|
||||
let bp = Plane::new(plane.origin, x, n.cross(x)).ok_or_else(|| DocError::Invalid("hole axis".into()))?;
|
||||
// The slot ends on the counterbore's wall: two lines and two arcs of its circle.
|
||||
let a = (rc * rc - q * q).sqrt();
|
||||
let phi = q.atan2(a);
|
||||
let slot = solidcore_record::Loop2 {
|
||||
segs: vec![
|
||||
solidcore_record::Seg2::Line { a: Vec2::new(-a, -q), b: Vec2::new(a, -q) },
|
||||
solidcore_record::Seg2::Arc { center: Vec2::ZERO, radius: rc, start: -phi, sweep: 2.0 * phi },
|
||||
solidcore_record::Seg2::Line { a: Vec2::new(a, q), b: Vec2::new(-a, q) },
|
||||
solidcore_record::Seg2::Arc { center: Vec2::ZERO, radius: rc, start: std::f64::consts::PI - phi, sweep: 2.0 * phi },
|
||||
],
|
||||
};
|
||||
let square = solidcore_record::Loop2::polygon(&[Vec2::new(-q, -q), Vec2::new(q, -q), Vec2::new(q, q), Vec2::new(-q, q)]);
|
||||
let mut tools = Vec::new();
|
||||
for (outline, lo, hi) in [(slot, t1, -dc), (square, t2, t1)] {
|
||||
let region = Region2 { outer: outline, holes: vec![] };
|
||||
let tool = kernel::extrude(&bp, &[region], lo, hi)?.pop().ok_or_else(|| DocError::Invalid("hole tool".into()))?;
|
||||
tools.push((crate::lineage::part_tool(tool, "bridge"), None));
|
||||
}
|
||||
Ok(tools)
|
||||
}
|
||||
|
||||
/// The angular interval used by both the solid and its interactive extent handles.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RevolveExtent {
|
||||
@@ -5046,22 +4798,6 @@ struct ExtrudeConstructor {
|
||||
interval: (f64, f64),
|
||||
}
|
||||
|
||||
/// How far from its sketch plane an extrude may reach, in mm. It mirrors the limit the kernel's
|
||||
/// `finite` check in `crates/kernel/src/build.rs` puts on an extrusion's start and end (a literal
|
||||
/// there, not exported), so a too-long extrusion is refused here, by name, before the kernel is
|
||||
/// called: the kernel's own message, `end = 1000000000`, names no field (#9625).
|
||||
const MAX_EXTRUDE_REACH: f64 = 1e7;
|
||||
|
||||
/// Refuse an extrusion whose ends lie past [`MAX_EXTRUDE_REACH`] from the sketch plane.
|
||||
fn extrude_within_reach(ends: [f64; 2]) -> Result<()> {
|
||||
match ends.into_iter().find(|x| x.abs() >= MAX_EXTRUDE_REACH) {
|
||||
Some(x) => Err(DocError::Invalid(format!(
|
||||
"the extrusion is too long: its end is {x} mm from the sketch plane, past the limit of {MAX_EXTRUDE_REACH} mm"
|
||||
))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct planar caps locally before placing the completed tool.
|
||||
fn centered_extrude(
|
||||
plane: &Plane,
|
||||
@@ -5169,10 +4905,10 @@ fn feature_tools_with_constructor(
|
||||
let unnamed = |bodies: Vec<Body>| -> Vec<Traced> { bodies.into_iter().map(Traced::unnamed).collect() };
|
||||
match &f.kind {
|
||||
FeatureKind::Extrude { sketch, profiles, extent, operation, targets, thin } => {
|
||||
let ss = constructor_profile_in_frame(
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f,
|
||||
f.component,
|
||||
)?;
|
||||
let regions = match thin {
|
||||
Some(thin) => thin_regions(vals, &ss, thin)?,
|
||||
@@ -5245,7 +4981,6 @@ fn feature_tools_with_constructor(
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => 0.0,
|
||||
};
|
||||
extrude_within_reach([base, base + sign * len])?;
|
||||
let start_plane = ss.plane.offset(base);
|
||||
let caps = [
|
||||
Some(crate::lineage::extrude_cap_role(base)),
|
||||
@@ -5315,7 +5050,6 @@ fn feature_tools_with_constructor(
|
||||
lo = hs.iter().cloned().fold(lo, f64::min) - margin;
|
||||
}
|
||||
}
|
||||
extrude_within_reach([lo + off, hi + off])?;
|
||||
let untrimmed = extent.to.is_none() && extent.from.is_none() && !extent.through_all;
|
||||
let mut tools = Vec::new();
|
||||
for r in ®ions {
|
||||
@@ -5396,15 +5130,14 @@ fn feature_tools_with_constructor(
|
||||
Ok(tools)
|
||||
}
|
||||
FeatureKind::Revolve { sketch, profiles, axis, angle, angle2, symmetric, convention, to, .. } => {
|
||||
let ss = constructor_profile_in_frame(
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f,
|
||||
f.component,
|
||||
)?;
|
||||
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
let (o, d) = revolve_axis(vals, &ss, axis, &*axis_state(doc, st, f.component, axis)?, &|name| {
|
||||
construction_axis_in(Some(doc), st, name, f.component)
|
||||
})?;
|
||||
let (o, d) =
|
||||
revolve_axis(&ss, axis, &*axis_state(doc, st, f.component, 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) };
|
||||
let angles = if to.is_some() {
|
||||
@@ -5805,27 +5538,16 @@ fn sketch_in_feature_frame(doc: &Document, sketch: &SolvedSketch, consumer: u64)
|
||||
profile_in_frame(doc, sketch, consumer)
|
||||
}
|
||||
|
||||
fn constructor_profile_in_frame(doc: &Document, sketch: &SolvedSketch, feature: &Feature) -> Result<SolvedSketch> {
|
||||
match crate::body_transfer::profile_frame(doc, feature, sketch.feature) {
|
||||
Some(relative) => profile_with_transform(sketch, &relative),
|
||||
None => sketch_in_feature_frame(doc, sketch, feature.component),
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_in_frame(doc: &Document, sketch: &SolvedSketch, consumer: u64) -> Result<SolvedSketch> {
|
||||
let owner = doc.feature(sketch.feature).map(|feature| feature.component).unwrap_or(0);
|
||||
let inverse =
|
||||
crate::mat_inverse(&doc.component_transform(consumer)).ok_or_else(|| DocError::Invalid("profile consumer frame is invalid".into()))?;
|
||||
let relative = crate::mat_mul(&inverse, &doc.component_transform(owner));
|
||||
profile_with_transform(sketch, &relative)
|
||||
}
|
||||
|
||||
fn profile_with_transform(sketch: &SolvedSketch, relative: &Mat) -> Result<SolvedSketch> {
|
||||
let mut mapped = sketch.clone();
|
||||
mapped.plane = Plane::new(
|
||||
crate::apply_point(relative, sketch.plane.origin),
|
||||
crate::apply_vector(relative, sketch.plane.x),
|
||||
crate::apply_vector(relative, sketch.plane.y),
|
||||
crate::apply_point(&relative, sketch.plane.origin),
|
||||
crate::apply_vector(&relative, sketch.plane.x),
|
||||
crate::apply_vector(&relative, sketch.plane.y),
|
||||
)
|
||||
.ok_or_else(|| DocError::Invalid("profile sketch frame is invalid".into()))?;
|
||||
Ok(mapped)
|
||||
@@ -6353,6 +6075,7 @@ fn path_at(segs: &[kernel::PathSeg], d: f64) -> (Vec3, Vec3) {
|
||||
(Vec3::ZERO, Vec3::X)
|
||||
}
|
||||
|
||||
/// Whether a copy has no material to cut in any of its eligible targets.
|
||||
/// The bodies a hole cuts: those its feature names, else its component's and its own (as
|
||||
/// `apply_traced` picks a cut's targets).
|
||||
fn hole_targets<'a>(st: &'a ModelState, f: &Feature) -> Vec<&'a ModelBody> {
|
||||
@@ -6382,120 +6105,18 @@ fn hole_misses(st: &ModelState, f: &Feature, tool: &Body) -> bool {
|
||||
targets.iter().all(|b| more::boxes_apart(&tool_box, &b.body.exact_bounds().ok()))
|
||||
}
|
||||
|
||||
/// The bodies a pattern's cut copies are checked against for missing them ([`copy_misses`]).
|
||||
fn miss_scope<'a>(st: &'a ModelState, f: &'a Feature, targets: &'a [String]) -> impl Iterator<Item = &'a ModelBody> + 'a {
|
||||
fn copy_misses(st: &ModelState, f: &Feature, targets: &[String], tools: &[Traced]) -> bool {
|
||||
let targets = if targets.is_empty() { &f.participants } else { targets };
|
||||
let all = targets.len() == 1 && targets.first().is_some_and(|t| t == "*");
|
||||
st.bodies
|
||||
.iter()
|
||||
.filter(move |b| all || if targets.is_empty() { b.component == f.component || b.feature == f.id } else { targets.contains(&b.name) })
|
||||
}
|
||||
|
||||
/// Whether a copy has no material to cut in any of its eligible targets.
|
||||
fn copy_misses(st: &ModelState, f: &Feature, targets: &[String], tools: &[Traced]) -> bool {
|
||||
miss_scope(st, f, targets).all(|b| {
|
||||
tools.iter().all(|t| {
|
||||
#[cfg(test)]
|
||||
cost_tests::count(|c| c.miss_intersects += 1);
|
||||
// An inconclusive probe is not a miss: it must not reject an otherwise valid cut.
|
||||
matches!(kernel::boolean(&b.body, &t.body, BoolOp::Intersect), Ok(None))
|
||||
.filter(|b| all || if targets.is_empty() { b.component == f.component || b.feature == f.id } else { targets.contains(&b.name) })
|
||||
.all(|b| {
|
||||
tools.iter().all(|t| {
|
||||
// An inconclusive probe is not a miss: it must not reject an otherwise valid cut.
|
||||
matches!(kernel::boolean(&b.body, &t.body, BoolOp::Intersect), Ok(None))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// How many of a pattern's cut copies (`instances`, ranges of `copies`) remove no material, and
|
||||
/// how many copies there are. [`copy_misses`] intersects a copy with every target, which on a
|
||||
/// body with many holes costs far more than cutting the copy (#9007). A copy that a point test
|
||||
/// shows takes material from a target ([`shares_material`]) is not a miss, and skips it; only
|
||||
/// copies the test cannot place (in an earlier hole, on the edge, thinner than its samples) are
|
||||
/// intersected.
|
||||
fn count_copy_misses(
|
||||
st: &ModelState,
|
||||
f: &Feature,
|
||||
targets: &[String],
|
||||
copies: &[Traced],
|
||||
instances: &[std::ops::Range<usize>],
|
||||
axes: &[Vec<(Vec3, Vec3)>],
|
||||
) -> (usize, usize) {
|
||||
let scope: Vec<&ModelBody> = miss_scope(st, f, targets).collect();
|
||||
let (mut missed, mut total) = (0, 0);
|
||||
for (k, tools) in instances.iter().enumerate().filter_map(|(k, i)| Some((k, copies.get(i.clone())?))) {
|
||||
total += 1;
|
||||
let lines = axes.get(k).map_or(&[][..], Vec::as_slice);
|
||||
let takes = tools.iter().any(|t| scope.iter().any(|b| shares_material(&b.body, &t.body, lines)));
|
||||
if !takes && copy_misses(st, f, targets, tools) {
|
||||
missed += 1;
|
||||
}
|
||||
}
|
||||
(missed, total)
|
||||
}
|
||||
|
||||
/// Whether some point lies in the material of both B-rep bodies `target` and `tool`, by their
|
||||
/// exact point classification (a point on either's boundary does not count). Tried along
|
||||
/// `lines` (a hole's axis: through the wall it drills into, however thin and slanted), along the
|
||||
/// axes of the tool's round faces (a cylinder's axis, the middle of a slot's ends) and along the
|
||||
/// three lines parallel to the axes through the middle of the box both exact boxes hold (the
|
||||
/// axis of a straight hole through a plate), within that box. `false` says nothing.
|
||||
fn shares_material(target: &Body, tool: &Body, lines: &[(Vec3, Vec3)]) -> bool {
|
||||
if target.is_mesh() || tool.is_mesh() {
|
||||
return false;
|
||||
}
|
||||
let (Some(a), Some(b)) = (more::exact_box(target), more::exact_box(tool)) else { return false };
|
||||
let both = a.intersection(&b);
|
||||
if both.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let c = both.center();
|
||||
let mut all = lines.to_vec();
|
||||
all.extend(round_face_axes(tool, MAX_PROBED_FACES));
|
||||
all.extend([(c, Vec3::X), (c, Vec3::Y), (c, Vec3::Z)]);
|
||||
let inside = |b: &Body, p: Vec3| matches!(b.classify_point(p), Ok(kernel::PointClass::Inside));
|
||||
all.iter().any(|&(o, d)| {
|
||||
let Some((lo, hi)) = line_in_box(&both, o, d) else { return false };
|
||||
(0..SAMPLES_PER_LINE).any(|k| {
|
||||
let p = o + d * (lo + (hi - lo) * (k as f64 + 0.5) / SAMPLES_PER_LINE as f64);
|
||||
inside(tool, p) && inside(target, p)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Points tried on each line by [`shares_material`].
|
||||
const SAMPLES_PER_LINE: usize = 16;
|
||||
/// Most faces of a tool whose surfaces [`shares_material`] looks along.
|
||||
const MAX_PROBED_FACES: usize = 32;
|
||||
|
||||
/// The axes of the round faces among the first `max` faces of `body`, from their exact surfaces:
|
||||
/// a cylinder's, a cone's and a torus's axis, and the lines along X, Y and Z through a sphere's
|
||||
/// centre.
|
||||
fn round_face_axes(body: &Body, max: usize) -> Vec<(Vec3, Vec3)> {
|
||||
let mut lines = Vec::new();
|
||||
for face in 0..body.face_count().min(max) {
|
||||
match kernel::face_surface(body, face) {
|
||||
Some(kernel::FaceSurface::Cylinder { origin, axis, .. }) => lines.push((origin, Vec3::from(axis))),
|
||||
Some(kernel::FaceSurface::Cone { apex, axis, .. }) => lines.push((apex, Vec3::from(axis))),
|
||||
Some(kernel::FaceSurface::Torus { center, axis, .. }) => lines.push((center, Vec3::from(axis))),
|
||||
Some(kernel::FaceSurface::Sphere { center, .. }) => lines.extend([(center, Vec3::X), (center, Vec3::Y), (center, Vec3::Z)]),
|
||||
Some(kernel::FaceSurface::Plane { .. }) | None => {}
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
/// The stretch of the line `o + t d` inside `b`, as the range of `t`; `None` when it misses.
|
||||
fn line_in_box(b: &solidcraft_geom::Aabb3, o: Vec3, d: Vec3) -> Option<(f64, f64)> {
|
||||
let (mut lo, mut hi) = (f64::NEG_INFINITY, f64::INFINITY);
|
||||
for (o, d, min, max) in [(o.x, d.x, b.min.x, b.max.x), (o.y, d.y, b.min.y, b.max.y), (o.z, d.z, b.min.z, b.max.z)] {
|
||||
if d.abs() < 1e-12 {
|
||||
if o < min || o > max {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
let (a, c) = ((min - o) / d, (max - o) / d);
|
||||
lo = lo.max(a.min(c));
|
||||
hi = hi.min(a.max(c));
|
||||
}
|
||||
}
|
||||
(lo.is_finite() && hi.is_finite() && lo <= hi).then_some((lo, hi))
|
||||
}
|
||||
|
||||
/// The deepest a pattern or mirror reaches through patterns and mirrors of features (#395).
|
||||
@@ -6737,9 +6358,6 @@ fn replay(
|
||||
let targets = if present.is_empty() { targets } else { present };
|
||||
let mut copies = Vec::new();
|
||||
let mut instances = Vec::with_capacity(mats.len());
|
||||
// Each hole copy's axes (spot and direction), where the check for copies that miss looks
|
||||
// for material first.
|
||||
let mut axes: Vec<Vec<(Vec3, Vec3)>> = Vec::new();
|
||||
if let FeatureKind::Hole { diameter, depth, hole, to, .. } = &src.kind {
|
||||
// A hole is drilled again at each copy of its spot (found on the surface there), so
|
||||
// a blind hole's depth counts from the face at the copy, not from the bottom of the
|
||||
@@ -6748,10 +6366,8 @@ fn replay(
|
||||
for (k, m) in mats.iter().enumerate() {
|
||||
let d = crate::apply_vector(m, dir).normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
let start = copies.len();
|
||||
let mut on = Vec::with_capacity(spots.len());
|
||||
for p in &spots {
|
||||
let at = hole_on_surface(st, crate::apply_point(m, *p), d, hole_scope(src));
|
||||
on.push((at, d));
|
||||
let to = to.map(|t| crate::apply_point(m, t.0));
|
||||
let depth = hole_depth_to(st, &at, d, &to, depth)?;
|
||||
// Each copy's faces are named as the hole's, with the instance.
|
||||
@@ -6762,7 +6378,6 @@ fn replay(
|
||||
);
|
||||
}
|
||||
instances.push(start..copies.len());
|
||||
axes.push(on);
|
||||
}
|
||||
} else if let FeatureKind::Rib { .. } = &src.kind {
|
||||
// A rib is built again at each copy of its sketch, against the bodies there (#4593):
|
||||
@@ -6789,9 +6404,12 @@ fn replay(
|
||||
}
|
||||
}
|
||||
if op == Operation::Cut {
|
||||
let (m, t) = count_copy_misses(st, f, &targets, &copies, &instances, &axes);
|
||||
missed += m;
|
||||
total += t;
|
||||
for instance in &instances {
|
||||
if let Some(tools) = copies.get(instance.clone()) {
|
||||
total += 1;
|
||||
missed += usize::from(copy_misses(st, f, &targets, tools));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copies of a cut through sheet metal are cut-outs of its flat pattern, as the cut is
|
||||
// (#6841): each machined into the sheets in turn, then cut from the other targets.
|
||||
|
||||