Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cbf328bab | |||
| 3559964792 | |||
| dfb0b3e113 | |||
| cd39f96d33 | |||
| 67cbfe941e | |||
| 45286e0489 | |||
| 5f127aceb0 | |||
| e717739735 | |||
| 28d90fae50 | |||
| 7b18fd5c0f | |||
| 992466e76a | |||
| c46fd3acd4 | |||
| 7d66a4d0ab | |||
| 0b3bc6e968 | |||
| d8e7a8690a | |||
| 6b39ee3686 | |||
| a7f3c78b07 | |||
| 471e17f50e | |||
| 1891344e07 | |||
| 1e176d8503 | |||
| fac0043543 | |||
| 119c40c814 | |||
| fe3fb381b1 | |||
| 0778d24801 | |||
| 79f53a0999 | |||
| dba3a226f6 | |||
| b72165fd30 | |||
| 8e26fe9388 | |||
| c8c9534022 | |||
| aae2c5e417 | |||
| 2cc6615c2d | |||
| 9c5b3beb73 | |||
| 14acdc2a6d | |||
| bcb67cc2ec | |||
| a8e5bfca73 | |||
| b68d42e5ec | |||
| 1ca2c5299c | |||
| 7d652d0e1f | |||
| 5f98396460 | |||
| bffa846a55 | |||
| 62da9deb29 | |||
| d17bc410a9 | |||
| fe210ece6d | |||
| e8d214716d | |||
| a4d1b5b6a9 | |||
| 8589cdfb84 | |||
| 4a34a258e1 | |||
| 3a951bc8c3 | |||
| 4fa051c9fc | |||
| 31d6873f7d | |||
| 545232be37 | |||
| c1dee61482 | |||
| ad182057b9 | |||
| bfe7792592 | |||
| be2deb8c6f | |||
| cecb1d8b66 | |||
| 427bd730f0 | |||
| 10b8faeba3 | |||
| 649ab0ef91 | |||
| 8be7bb0201 | |||
| 67d5051792 | |||
| 7e8f87dc57 | |||
| 84016f036e | |||
| 5f90d06452 | |||
| 27772ddba1 | |||
| e1b49238ba | |||
| ba35509ca0 | |||
| 32f11961e9 | |||
| 4dda001210 | |||
| f49d5c2f29 | |||
| afde0f8073 | |||
| 2897d3b22e | |||
| 9bcb2ffe68 | |||
| 6af571eb40 | |||
| a5f472c23e | |||
| 34efb90489 | |||
| f060702676 | |||
| d6b1d03695 | |||
| e181015fd1 | |||
| 387e86729c | |||
| 64ff25f380 | |||
| 1dba7f7f04 | |||
| 7d9b28e20d | |||
| 48bcc83e2e | |||
| 1e3cb3e36e | |||
| 4d6664e4ca | |||
| d56dc310b0 | |||
| 4ab612ea67 | |||
| 73f06753b6 | |||
| 292177e085 | |||
| d7111c7326 | |||
| 745184f2fc | |||
| 942f11169c | |||
| 029cf29b49 | |||
| 7ad059aec9 | |||
| a2a00aa9ee | |||
| 59551c59ab | |||
| bf1b09698c | |||
| fc2142b407 | |||
| d3b2c715d3 | |||
| 09308fceb1 | |||
| 8f87509f7d | |||
| c962e5e8cd | |||
| 7df858594d | |||
| fed9605357 | |||
| bf1886e90a | |||
| dfa9afd39f | |||
| 944c658c90 | |||
| 8f8df03358 | |||
| 92fc868aa9 | |||
| 33281cf3bc | |||
| d64816437a | |||
| 8210e94b6d | |||
| f6d969f98a | |||
| 59faca3850 | |||
| 29a2f33664 | |||
| 9d4d7932ee | |||
| 54a207cb8b | |||
| 5bf5de49a5 | |||
| cee995e776 | |||
| b86c75544e | |||
| 249318cffb | |||
| 6f8ee10a86 | |||
| 30cb1238f7 | |||
| db999b2a98 | |||
| e59decda95 | |||
| aca80f2054 | |||
| 0b2efd60b9 | |||
| 54f613e4dd | |||
| ae07e36a7d | |||
| 18fe82ab26 | |||
| 372badfc1b | |||
| 1781b4d323 | |||
| cf5dc813e2 | |||
| b0c8c466b0 | |||
| 85aaed2ccb | |||
| 110efa3598 | |||
| 9fb41f8378 |
@@ -429,6 +429,20 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
|
||||
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/frame_member.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/render_environment.svg | SolidCraft contributors | original Studio icon master (64 grid): dome with a grey cube inside | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/style_shaded.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/style_wireframe.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/style_hidden_edges.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/style_translucent.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/select_faces_edges.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/select_bodies.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/select_features.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/select_parts.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/select_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/select_sketch.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/display.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/grid.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/ground_shadow.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/ambient_occlusion.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
@@ -581,6 +595,13 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
|
||||
| assets/styles/ui/themes/cga.json | SolidCraft contributors | original SolidCraft theme file using the 16 colours of the IBM Color Graphics Adapter (black, cyan, magenta, and white of its high-intensity palette 1 for the interface) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/1-bit.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/1-bit-dark.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
| `assets/locales/pending/navbar-customisation.json` | SolidCraft contributors | Original navigation bar editor translations | MIT OR Apache-2.0 |
|
||||
| `assets/locales/pending/navbar-slots.json` | SolidCraft contributors | Original navigation slot translations | MIT OR Apache-2.0 |
|
||||
| assets/locales/bs.json | Tarik Sijercic (tsijercic1) | Original Bosnian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/hr.json | Tarik Sijercic (tsijercic1) | Original Croatian translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/sr.json | Tarik Sijercic (tsijercic1) | Original Serbian (ekavian, Cyrillic) translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/finish_edit.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| `assets/locales/pending/sketch-tracking-guides.json` | SolidCraft contributors | Original alignment-guide preference labels and help text | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
|
||||
@@ -268,7 +268,9 @@ a conflict. `crates/doc/src/format/pending/README.md` has an example.
|
||||
|
||||
**At release time** the maintainer runs `scripts/fold-format-pending.sh`, which moves the pending
|
||||
files into `crates/doc/src/format/v<N>/` and fixes that number for good (see
|
||||
[docs/releasing.md](docs/releasing.md)). Never add a file to a `v<N>/` folder afterwards.
|
||||
[docs/releasing.md](docs/releasing.md)). Never add a file to a `v<N>/` folder afterwards. Drawings
|
||||
follow the same rules with their own numbers, from 1, in `crates/doc/src/drawing/format/` (see its
|
||||
`pending/README.md`), and the same script folds them.
|
||||
|
||||
## Lists many changes add to
|
||||
|
||||
@@ -287,6 +289,9 @@ and a fresh CI run:
|
||||
- **Toolbar commands without help** (`BACKLOG` in `crates/ui-egui/src/command_help.rs`): sorted,
|
||||
one id per line, and it only shrinks. Writing a command's help removes its line; a new command
|
||||
is never added to it.
|
||||
- **`ui.prefs` settings** (`docs/control-protocol.md`): one line per setting, sorted by name, in
|
||||
the list under the protocol table; add a new setting on its own line rather than editing a shared
|
||||
row.
|
||||
- **Languages** (`crates/ui-egui/src/i18n.rs`): one per line, English first, then by id (a test
|
||||
checks it).
|
||||
- **Generated files**: `THIRD-PARTY-LICENSES.txt` and `docs/features.md` are rewritten on `main`
|
||||
|
||||
@@ -4238,6 +4238,7 @@ dependencies = [
|
||||
"ureq",
|
||||
"windows-sys 0.61.2",
|
||||
"winit",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -653,7 +653,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
if let Some(s) = &scope {
|
||||
let roots: Vec<String> = s.roots().iter().map(|r| r.display().to_string()).collect();
|
||||
eprintln!(
|
||||
"solidcraft mcp: files limited to {} and the open design's folder (--allow-path DIR adds one, --allow-any-path lifts the limit)",
|
||||
"solidcraft mcp: files limited to {} and the folders of designs opened or saved here (--allow-path DIR adds one, --allow-any-path lifts the limit)",
|
||||
roots.join(", ")
|
||||
);
|
||||
let design_dir = flag(args, "--in")
|
||||
|
||||
@@ -40,6 +40,8 @@ pub enum Intent {
|
||||
Open,
|
||||
/// Add its bodies to the open design (STEP, 3MF, STL).
|
||||
Insert,
|
||||
/// Import it as a theme: it was dropped on Preferences' Import tile (#8226).
|
||||
Theme,
|
||||
}
|
||||
|
||||
/// A stored design, as the file panel lists it.
|
||||
@@ -615,13 +617,15 @@ fn choose_file(accept: &str) -> Result<js_sys::Promise, String> {
|
||||
}
|
||||
|
||||
/// Read files dropped on the page.
|
||||
pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput) {
|
||||
/// `theme`: the pointer is over Preferences' Import tile, so the files are themes.
|
||||
pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput, theme: bool) {
|
||||
let intent = if theme { Intent::Theme } else { Intent::Insert };
|
||||
for f in std::mem::take(&mut raw.dropped_files) {
|
||||
let files = files.clone();
|
||||
spawn_local(async move {
|
||||
let name = f.path().file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "dropped".into());
|
||||
match f.bytes_async().await {
|
||||
Ok(b) => files.arrive(name, b, Intent::Insert),
|
||||
Ok(b) => files.arrive(name, b, intent),
|
||||
Err(e) => files.say(format!("{name}: {e}"), true),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -271,6 +271,10 @@ mod web {
|
||||
!companion
|
||||
});
|
||||
for (name, bytes, intent) in incoming {
|
||||
if intent == Intent::Theme {
|
||||
solidcraft_ui_egui::prefs::import_theme_bytes(&mut self.app, bytes);
|
||||
continue;
|
||||
}
|
||||
let design = solidcraft_engine::io::is_design_path(&name);
|
||||
let path = if design && intent == Intent::Open {
|
||||
// A design from the computer is kept in the browser (a copy), so Save works.
|
||||
@@ -287,7 +291,7 @@ mod web {
|
||||
};
|
||||
match intent {
|
||||
Intent::Open => self.app.open_path(&path),
|
||||
Intent::Insert => self.app.drop_path(&path),
|
||||
Intent::Insert | Intent::Theme => self.app.drop_path(&path),
|
||||
}
|
||||
self.files.panel.set(false);
|
||||
}
|
||||
@@ -353,7 +357,8 @@ mod web {
|
||||
}
|
||||
let mut act: Option<(&'static str, String)> = None;
|
||||
let closed = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_files"), "folder", "Files in this browser")
|
||||
.cards(3.0)
|
||||
// Room for long names; the kit still caps the window at the browser's width.
|
||||
.cards(4.0)
|
||||
.show_body(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui_kit::button(ui, "Open from computer…", ui_kit::Button::Normal).clicked() {
|
||||
@@ -381,61 +386,62 @@ mod web {
|
||||
return;
|
||||
}
|
||||
let entries = self.files.entries.borrow().clone();
|
||||
if entries.is_empty() {
|
||||
ui.label("Designs you save appear here. They stay in this browser.");
|
||||
}
|
||||
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| {
|
||||
egui::Grid::new("opfs_files").num_columns(3).spacing([12.0, 6.0]).show(ui, |ui| {
|
||||
// The name takes the width the size and the actions leave (#8097).
|
||||
let empty = "Designs you save appear here. They stay in this browser.";
|
||||
ui_kit::name_list(ui, "opfs_files", &["Name", "Size", "Actions"], empty, |list| {
|
||||
for e in &entries {
|
||||
match &mut self.renaming {
|
||||
Some((from, to)) if *from == e.name => {
|
||||
let r = ui_kit::cell_editor(ui, ui.make_persistent_id(("stored_file_name", from.as_str())), to, "", false);
|
||||
if std::mem::take(&mut self.rename_focus) {
|
||||
r.request_focus();
|
||||
}
|
||||
if r.lost_focus() {
|
||||
act = Some(if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
("rename", to.clone())
|
||||
} else {
|
||||
("rename_cancel", String::new())
|
||||
});
|
||||
list.row(&e.name, |row| {
|
||||
match &mut self.renaming {
|
||||
Some((from, to)) if *from == e.name => row.cell(|ui| {
|
||||
let r =
|
||||
ui_kit::cell_editor(ui, ui.make_persistent_id(("stored_file_name", from.as_str())), to, "", false);
|
||||
if std::mem::take(&mut self.rename_focus) {
|
||||
r.request_focus();
|
||||
}
|
||||
if r.lost_focus() {
|
||||
act = Some(if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
("rename", to.clone())
|
||||
} else {
|
||||
("rename_cancel", String::new())
|
||||
});
|
||||
}
|
||||
}),
|
||||
_ => {
|
||||
row.name(&e.name);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
ui.label(&e.name);
|
||||
}
|
||||
}
|
||||
ui.label(format!("{:.1} KB", e.size as f64 / 1024.0));
|
||||
ui.horizontal(|ui| {
|
||||
if ui_kit::button(ui, "Open", ui_kit::Button::Small).clicked() {
|
||||
act = Some(("open", e.name.clone()));
|
||||
}
|
||||
let design = solidcraft_engine::io::is_design_path(&e.name);
|
||||
if !design
|
||||
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
|
||||
row.cell(|ui| ui.label(format!("{:.1} KB", e.size as f64 / 1024.0)));
|
||||
row.cell(|ui| {
|
||||
if ui_kit::button(ui, "Open", ui_kit::Button::Small).clicked() {
|
||||
act = Some(("open", e.name.clone()));
|
||||
}
|
||||
let design = solidcraft_engine::io::is_design_path(&e.name);
|
||||
if !design
|
||||
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("insert", e.name.clone()));
|
||||
}
|
||||
if ui_kit::button(ui, "Rename", ui_kit::Button::Small).clicked() {
|
||||
self.renaming = Some((e.name.clone(), e.name.clone()));
|
||||
self.rename_focus = true;
|
||||
}
|
||||
if ui_kit::button(ui, "Save to computer…", ui_kit::Button::Small)
|
||||
.on_hover_text("A copy in a file you choose; the stored design stays here")
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("insert", e.name.clone()));
|
||||
}
|
||||
if ui_kit::button(ui, "Rename", ui_kit::Button::Small).clicked() {
|
||||
self.renaming = Some((e.name.clone(), e.name.clone()));
|
||||
self.rename_focus = true;
|
||||
}
|
||||
if ui_kit::button(ui, "Save to computer…", ui_kit::Button::Small)
|
||||
.on_hover_text("A copy in a file you choose; the stored design stays here")
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("save_copy", e.name.clone()));
|
||||
}
|
||||
if ui_kit::button(ui, "Download", ui_kit::Button::Small).clicked() {
|
||||
act = Some(("download", e.name.clone()));
|
||||
}
|
||||
if ui_kit::button(ui, "Delete", ui_kit::Button::Small).clicked() {
|
||||
act = Some(("delete", e.name.clone()));
|
||||
}
|
||||
{
|
||||
act = Some(("save_copy", e.name.clone()));
|
||||
}
|
||||
if ui_kit::button(ui, "Download", ui_kit::Button::Small).clicked() {
|
||||
act = Some(("download", e.name.clone()));
|
||||
}
|
||||
if ui_kit::button(ui, "Delete", ui_kit::Button::Small).clicked() {
|
||||
act = Some(("delete", e.name.clone()));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -630,7 +636,8 @@ mod web {
|
||||
ctx.request_repaint_after(delay);
|
||||
}
|
||||
if self.launch.persists() {
|
||||
files::take_drops(&self.files, raw);
|
||||
let theme = solidcraft_ui_egui::prefs::theme_drop_target(&self.app, ctx);
|
||||
files::take_drops(&self.files, raw, theme);
|
||||
} else {
|
||||
raw.dropped_files.clear();
|
||||
}
|
||||
|
||||
@@ -96,6 +96,11 @@ libc = "0.2"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
|
||||
winit = { version = "0.30", default-features = false, features = ["x11"] }
|
||||
# Bringing the window forward on X11 when a close asks about unsaved designs (src/come_forward.rs):
|
||||
# the window's id, and the window manager request winit doesn't send (MIT OR Apache-2.0; both
|
||||
# already in the tree through eframe and winit).
|
||||
raw-window-handle = "0.6"
|
||||
x11rb = { version = "0.13", default-features = false }
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "3.0.11"
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
//! Bringing the window forward when a close stops at "save changes?".
|
||||
//!
|
||||
//! A close from the taskbar or a window switcher reaches the window while another app has the
|
||||
//! focus. The prompt it opens is only seen once the window comes forward, so the window asks for
|
||||
//! the focus then. A window the user is already in is left alone.
|
||||
//!
|
||||
//! winit's Focus asks as an application, which a window manager with focus-stealing prevention
|
||||
//! (KWin's default) refuses for a window used before the switch away: it only marks the taskbar
|
||||
//! entry. On X11 the request is sent again with the source EWMH gives "direct user actions"
|
||||
//! (`_NET_ACTIVE_WINDOW`, source 2), which is what this is: the prompt is the rest of the user's
|
||||
//! close. Native Wayland has no way for a window to raise itself, so there the attention request
|
||||
//! (xdg-activation) lets the compositor raise it or mark its taskbar entry.
|
||||
//!
|
||||
//! When: not at once. KWin raises the window, then the taskbar's closing context menu hands the
|
||||
//! focus back to the app that had it, a few milliseconds later, and the window would flicker
|
||||
//! forward and back. So the window asks once the menu is gone, `FIRST_AFTER` the close. It asks
|
||||
//! again only if the focus is taken back right after it came (a slow menu) or never came; a later
|
||||
//! switch is the user's own, and the window stays where the user put it.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// From the close to the first request: past the taskbar menu's focus hand-back (2 to 11 ms on
|
||||
/// KDE Plasma 6), too short to notice.
|
||||
const FIRST_AFTER: Duration = Duration::from_millis(120);
|
||||
/// After the first request, how long the window asks again when it loses the focus: short of
|
||||
/// anything the user does next.
|
||||
const KEEP_ASKING: Duration = Duration::from_millis(400);
|
||||
/// The least time between two requests.
|
||||
const AGAIN_AFTER: Duration = Duration::from_millis(100);
|
||||
/// Focus lost this soon after a request was taken back by the taskbar menu; later, by the user.
|
||||
const TAKEN_BACK_WITHIN: Duration = Duration::from_millis(50);
|
||||
|
||||
/// The window coming forward after a close that asks about unsaved designs.
|
||||
#[derive(Default)]
|
||||
pub struct ComeForward {
|
||||
/// The first request and the end of asking; `None`: not asking.
|
||||
window: Option<(Instant, Instant)>,
|
||||
/// The last request.
|
||||
last: Option<Instant>,
|
||||
/// The window has had the focus since the last request.
|
||||
held: bool,
|
||||
}
|
||||
|
||||
/// What a frame does while the window is coming forward.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Step {
|
||||
/// Ask for the focus now.
|
||||
Ask,
|
||||
/// Look again after this long.
|
||||
Wait(Duration),
|
||||
/// Done asking.
|
||||
Done,
|
||||
}
|
||||
|
||||
impl ComeForward {
|
||||
/// A close stopped at the prompt: ask for the focus shortly.
|
||||
pub fn start(&mut self, now: Instant) {
|
||||
let first = now + FIRST_AFTER;
|
||||
self.window = Some((first, first + KEEP_ASKING));
|
||||
self.last = None;
|
||||
self.held = false;
|
||||
}
|
||||
|
||||
/// Every frame: ask once the taskbar menu is gone, and again while the focus is taken back.
|
||||
pub fn tick(&mut self, ctx: &egui::Context, frame: &eframe::Frame) {
|
||||
if self.window.is_none() {
|
||||
return;
|
||||
}
|
||||
let (focused, minimized) = ctx.input(|i| (i.viewport().focused, i.viewport().minimized));
|
||||
// The app may be idle: a frame to look again.
|
||||
match self.step(Instant::now(), focused) {
|
||||
Step::Ask => {
|
||||
bring(ctx, frame, minimized);
|
||||
ctx.request_repaint_after(AGAIN_AFTER);
|
||||
}
|
||||
Step::Wait(after) => ctx.request_repaint_after(after),
|
||||
Step::Done => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn step(&mut self, now: Instant, focused: Option<bool>) -> Step {
|
||||
let Some((first, until)) = self.window else { return Step::Done };
|
||||
if now >= until {
|
||||
self.window = None;
|
||||
return Step::Done;
|
||||
}
|
||||
if now < first {
|
||||
return Step::Wait(first - now);
|
||||
}
|
||||
let Some(last) = self.last else {
|
||||
if focused == Some(true) {
|
||||
// The user went back to the window already.
|
||||
self.window = None;
|
||||
return Step::Done;
|
||||
}
|
||||
self.last = Some(now);
|
||||
return Step::Ask;
|
||||
};
|
||||
if focused == Some(true) {
|
||||
self.held = true;
|
||||
return Step::Wait(until - now);
|
||||
}
|
||||
if self.held {
|
||||
if now.duration_since(last) >= TAKEN_BACK_WITHIN {
|
||||
// The user switched away: leave the window there.
|
||||
self.window = None;
|
||||
return Step::Done;
|
||||
}
|
||||
self.held = false;
|
||||
}
|
||||
let next = last + AGAIN_AFTER;
|
||||
if now < next {
|
||||
return Step::Wait(next - now);
|
||||
}
|
||||
self.last = Some(now);
|
||||
Step::Ask
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask for the focus once.
|
||||
fn bring(ctx: &egui::Context, frame: &eframe::Frame, minimized: Option<bool>) {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
let raised = match x11_activate(frame) {
|
||||
Ok(sent) => sent,
|
||||
Err(e) => {
|
||||
log::warn!("bringing the window forward through X11: {e}");
|
||||
false
|
||||
}
|
||||
};
|
||||
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||||
let raised = {
|
||||
let _ = frame;
|
||||
false
|
||||
};
|
||||
for c in commands(minimized, raised) {
|
||||
ctx.send_viewport_cmd(c);
|
||||
}
|
||||
}
|
||||
|
||||
/// The viewport commands for a window in the background; `raised`: the X11 request went out,
|
||||
/// which also restores a minimized window.
|
||||
fn commands(minimized: Option<bool>, raised: bool) -> Vec<egui::ViewportCommand> {
|
||||
if raised {
|
||||
return vec![egui::ViewportCommand::Focus];
|
||||
}
|
||||
let mut commands = Vec::new();
|
||||
if minimized == Some(true) {
|
||||
commands.push(egui::ViewportCommand::Minimized(false));
|
||||
}
|
||||
commands.push(egui::ViewportCommand::Focus);
|
||||
commands.push(egui::ViewportCommand::RequestUserAttention(egui::UserAttentionType::Informational));
|
||||
commands
|
||||
}
|
||||
|
||||
/// Ask the X11 window manager to activate the window as a user action. `Ok(false)`: the window
|
||||
/// isn't an X11 window (native Wayland).
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn x11_activate(frame: &eframe::Frame) -> Result<bool, String> {
|
||||
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::xproto::{ClientMessageEvent, ConnectionExt, EventMask};
|
||||
|
||||
let handle = frame.window_handle().map_err(|e| e.to_string())?;
|
||||
let window = match handle.as_raw() {
|
||||
RawWindowHandle::Xlib(h) => u32::try_from(h.window).map_err(|e| e.to_string())?,
|
||||
RawWindowHandle::Xcb(h) => h.window.get(),
|
||||
_ => return Ok(false),
|
||||
};
|
||||
// A connection of its own: winit's isn't reachable, and a close that asks is rare.
|
||||
let (conn, screen) = x11rb::connect(None).map_err(|e| e.to_string())?;
|
||||
let root = conn.setup().roots.get(screen).ok_or("no X11 screen")?.root;
|
||||
let atom = conn.intern_atom(false, b"_NET_ACTIVE_WINDOW").map_err(|e| e.to_string())?.reply().map_err(|e| e.to_string())?.atom;
|
||||
// Source 2: a direct user action; the time is the server's current time.
|
||||
let event = ClientMessageEvent::new(32, window, atom, [2, x11rb::CURRENT_TIME, 0, 0, 0]);
|
||||
conn.send_event(false, root, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, event).map_err(|e| e.to_string())?;
|
||||
conn.flush().map_err(|e| e.to_string())?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AGAIN_AFTER, ComeForward, FIRST_AFTER, KEEP_ASKING, Step};
|
||||
use eframe::egui::{UserAttentionType, ViewportCommand as C};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// The first request waits until the taskbar menu has handed the focus back. The window asks
|
||||
/// again when the focus is taken back right after it came, or never came, no more often than
|
||||
/// every `AGAIN_AFTER` and only for `KEEP_ASKING`; a later switch away is the user's.
|
||||
#[test]
|
||||
fn the_window_asks_once_the_taskbar_menu_is_gone() {
|
||||
let t0 = Instant::now();
|
||||
let ms = |n: u64| Duration::from_millis(n);
|
||||
let at = |n: u64| t0 + ms(n);
|
||||
let (first, again, keep) = (FIRST_AFTER.as_millis() as u64, AGAIN_AFTER.as_millis() as u64, KEEP_ASKING.as_millis() as u64);
|
||||
let mut f = ComeForward::default();
|
||||
assert_eq!(f.step(t0, Some(false)), Step::Done, "not started");
|
||||
f.start(t0);
|
||||
assert_eq!(f.step(t0, Some(false)), Step::Wait(FIRST_AFTER), "the menu is still closing");
|
||||
assert_eq!(f.step(at(5), Some(false)), Step::Wait(ms(first - 5)));
|
||||
assert_eq!(f.step(at(first), Some(false)), Step::Ask);
|
||||
assert_eq!(f.step(at(first + 3), Some(true)), Step::Wait(ms(keep - 3)), "raised");
|
||||
// A slow menu takes the focus back 10 ms later: ask again, not before AGAIN_AFTER.
|
||||
assert_eq!(f.step(at(first + 10), Some(false)), Step::Wait(ms(again - 10)));
|
||||
assert_eq!(f.step(at(first + again), Some(false)), Step::Ask);
|
||||
assert_eq!(f.step(at(first + again + 3), Some(true)), Step::Wait(ms(keep - again - 3)));
|
||||
// The user clicks another app 200 ms later: the window stays behind.
|
||||
assert_eq!(f.step(at(first + again + 200), Some(false)), Step::Done);
|
||||
assert_eq!(f.step(at(first + again + 300), Some(false)), Step::Done);
|
||||
|
||||
// Never raised (the request was refused): ask again every AGAIN_AFTER, for KEEP_ASKING.
|
||||
f.start(t0);
|
||||
assert_eq!(f.step(at(first), None), Step::Ask, "unknown focus counts as out of focus");
|
||||
assert_eq!(f.step(at(first + again), Some(false)), Step::Ask);
|
||||
assert_eq!(f.step(at(first + keep), Some(false)), Step::Done);
|
||||
|
||||
// Back in the window before the first request: nothing to do.
|
||||
f.start(t0);
|
||||
assert_eq!(f.step(at(first), Some(true)), Step::Done);
|
||||
}
|
||||
|
||||
/// Without the X11 request: focus, and the attention mark where focus is refused; a minimized
|
||||
/// window is restored first.
|
||||
#[test]
|
||||
fn a_window_closed_from_the_background_comes_forward() {
|
||||
let forward = [C::Focus, C::RequestUserAttention(UserAttentionType::Informational)];
|
||||
assert_eq!(super::commands(Some(false), false), forward);
|
||||
assert_eq!(super::commands(None, false), forward, "unknown counts as not minimized");
|
||||
let minimized = super::commands(Some(true), false);
|
||||
assert_eq!(minimized.first(), Some(&C::Minimized(false)));
|
||||
assert_eq!(minimized.get(1..), Some(&forward[..]));
|
||||
// The window manager took the X11 request: nothing left to mark.
|
||||
assert_eq!(super::commands(Some(true), true), [C::Focus]);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
|
||||
mod app_icon;
|
||||
mod come_forward;
|
||||
mod control_server;
|
||||
mod device_lost;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -84,6 +85,8 @@ struct App {
|
||||
frames: u32,
|
||||
/// Started over the control channel: the window doesn't ask for the foreground.
|
||||
take_focus_unwanted: bool,
|
||||
/// Coming forward after a close that asks about unsaved designs.
|
||||
come_forward: come_forward::ComeForward,
|
||||
/// The graphics device: lost, it can't draw again (`App::watch_device`, #4172).
|
||||
gpu: Option<LostWatch>,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
@@ -139,7 +142,13 @@ impl eframe::App for App {
|
||||
if ctx.input(|i| i.viewport().close_requested()) && !self.solve.quit_requested {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
||||
solidcraft_ui_egui::documents::request_quit(&mut self.solve);
|
||||
// Closed from the taskbar or a window switcher while another app had the focus: the
|
||||
// "save changes?" prompt is only seen once the window comes forward.
|
||||
if !self.solve.quit_requested {
|
||||
self.come_forward.start(std::time::Instant::now());
|
||||
}
|
||||
}
|
||||
self.come_forward.tick(ctx, _frame);
|
||||
#[cfg(target_os = "macos")]
|
||||
for f in if self.solve.file_picker_busy() { Vec::new() } else { open_documents::take() } {
|
||||
self.solve.open_path_async(&f);
|
||||
@@ -1060,6 +1069,7 @@ fn main() -> eframe::Result {
|
||||
starting,
|
||||
frames: 0,
|
||||
take_focus_unwanted: control_port.is_some(),
|
||||
come_forward: come_forward::ComeForward::default(),
|
||||
gpu,
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
space_mouse,
|
||||
|
||||
@@ -27,6 +27,9 @@ error = { master = "status_error", accent = "surface" }
|
||||
warning = { master = "status_warning", accent = "mesh" }
|
||||
ground = "c_fix"
|
||||
|
||||
# Visual style: Shaded with edges is the generic style icon.
|
||||
style_shaded_edges = "style"
|
||||
|
||||
# Same finished drawing, different semantic operation. Colours live only in palette.toml.
|
||||
sketch_break = { master = "break", accent = "sketch" }
|
||||
sketch_cancel = { master = "close", accent = "sketch" }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
accessibility false
|
||||
activate false
|
||||
agent_chat false
|
||||
align false
|
||||
ambient_occlusion false
|
||||
appearance false
|
||||
arc3 false
|
||||
arc_center false
|
||||
@@ -83,6 +85,8 @@ decal false
|
||||
delete false
|
||||
delete_face false
|
||||
dimension false
|
||||
display false
|
||||
dots false
|
||||
draft false
|
||||
draft_analysis false
|
||||
drive false
|
||||
@@ -101,6 +105,7 @@ eye false
|
||||
fastener false
|
||||
fillet false
|
||||
finish false
|
||||
finish_edit false
|
||||
fit false
|
||||
fit_section false
|
||||
flange false
|
||||
@@ -110,7 +115,9 @@ folder false
|
||||
frame_member false
|
||||
gear false
|
||||
graph false
|
||||
grid false
|
||||
ground false
|
||||
ground_shadow false
|
||||
help false
|
||||
hem false
|
||||
hole false
|
||||
@@ -210,6 +217,11 @@ section false
|
||||
select false
|
||||
send false
|
||||
settings false
|
||||
settings_keyboard false
|
||||
settings_monitor false
|
||||
settings_mouse false
|
||||
settings_palette false
|
||||
settings_plug false
|
||||
sheet_rules false
|
||||
shell false
|
||||
shield false
|
||||
@@ -247,6 +259,11 @@ star false
|
||||
status_error false
|
||||
status_recovery false
|
||||
style false
|
||||
style_hidden_edges false
|
||||
style_shaded false
|
||||
style_shaded_edges false
|
||||
style_translucent false
|
||||
style_wireframe false
|
||||
suppress false
|
||||
surface false
|
||||
surface_blend false
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"agent_chat": "4a9ae0e7e2c360b4858b3175674c93d096705c92318fecd481e849f3ae650bf4",
|
||||
"agent_link": "2f20d89a0e3e9b28744910dddf7f282a1099ccd91aa160c9841c5b1d9ea9ed0f",
|
||||
"align": "05615db7dd9e4be56ae898e59a68c548b8a8447794442a9a394940651204bc4f",
|
||||
"ambient_occlusion": "973f8ef2326aeaf0face576e5a2c79c7ef3d13638b2c51a5aad199e4ef7edb02",
|
||||
"appearance": "b3162edd92291a5ef402e8499cd52be05beb2fa7eda641163c3f6e98325de2bf",
|
||||
"arc3": "fdca3d501fd557ac989376a12e9c4fa8231fde2b455c20724d1079a466153e9c",
|
||||
"arc_center": "3a5fdeb1d64fc6741f944c4435870b05fccab62293e270da692c6681155cbc51",
|
||||
@@ -88,6 +89,7 @@
|
||||
"dimension": "a926b7c64bc8ca99d8d6dcbb81b95235bd3fc34fbed049aca8b01b4d1147c15d",
|
||||
"disclosure_closed": "49a350c6f2efae78de0be96c524ebee1fd1291ea449a84224b88d37164a4e807",
|
||||
"disclosure_open": "d13ce3f97e3e362a9904fa2bf3412f3f00e4136bc23538b92d41483961d280de",
|
||||
"display": "58b45e5e2230eaf2c80f74b3be2e02a23714c90d6fb03b59ed093a1949993163",
|
||||
"dots": "600f7ff209d93145fc938bc4f6a1e34473e6b9ee22a5b4718f133bc7da5ee58d",
|
||||
"draft": "9c0fc361cacd0ad95ccd54794f4825dfc9901a83a3de752a588faec365d0ee09",
|
||||
"draft_analysis": "eb39c0f90e9cf226da6307365a76015e900dd692dc3f0fdc8f02fe717226261f",
|
||||
@@ -117,7 +119,9 @@
|
||||
"folder": "c19f5230df45a1bda1e92e89de38bba21c91e6eca0517eb15847a7712012d1e5",
|
||||
"gear": "25a1ca48a587850da1104a46005a330873ed196246e3d04e12cf61f26ad2b0d5",
|
||||
"graph": "7156caedd2dbfa2ae53da18b7a2204b8db66c45126859d36caf049a3fd4335a4",
|
||||
"grid": "084b5040d57e9d5183b3db1fad0d17a13ab5bae64676eef703cc06622f9c7438",
|
||||
"ground": "23e2471a9d6872e83028e0e219e330cef1930a4d89456a0fefa1bda8a7e3eb81",
|
||||
"ground_shadow": "a96809ed87f5047c2d7f1274908ed7b65b180613322ac9c10eb0f9d6296c1b66",
|
||||
"help": "8ce4060577483dbdb510be147d2827de50ac20029354008fca55abc0c7edf05d",
|
||||
"hem": "f9b1aafbedcf3dc90c26be308814638e0095fbe31b0647092507343fa60af011",
|
||||
"hole": "f82d47751f826e08682db847a270897474661bfe9f35b6d3678f49a203172563",
|
||||
@@ -263,6 +267,12 @@
|
||||
"screw": "5e30570785360d6a591ea22862a82f1cf62228385c1e96382536db8a453ac791",
|
||||
"section": "cdd13f99f15f6d59bfa21c747810608d6afd00cb8207faf0337928c8521e7eae",
|
||||
"select": "fdf6431270006ca7409c60c37cfed59572de04229f104ec723e6010ffa2b7163",
|
||||
"select_bodies": "8e6e61e083e6097535da3b98d41b607b40fc81b72bbd8c7fb903618c5b35c568",
|
||||
"select_components": "3e6cc847b334e92ce7ec5e5f35dfa929572e0974239790d76563ca4e20b26789",
|
||||
"select_faces_edges": "7b99b5370b1647bf8cea57df7a1053caab641c2f4a94c4d797b9679e6f38d6f1",
|
||||
"select_features": "b050bc19ce86d135cbe50998be681555110e92e179c4c7b101170c6b62c19943",
|
||||
"select_parts": "569adc5595a64187411d8ffeea69eab3b76ddfa33aeead6fd728efdd993bcccc",
|
||||
"select_sketch": "b0605b34a14d4173f7ed80d877109ec0b891a1b395bcab5743a603ed2eae240a",
|
||||
"send": "f83b6cdab792f9567a81d46d7828ed20795513c57a2c7d00fbbe6bd14d2f5848",
|
||||
"settings": "9193dd3a1007c43660d0d47b6499cb2571338b67949e1a99df112057b9bf2b8a",
|
||||
"settings_keyboard": "4968c70788094365af9c6e2b1c15b2aac45cf21212a14b8a256daf1ffe17f41a",
|
||||
@@ -309,6 +319,11 @@
|
||||
"stitch": "6c3205d64007b771ed78d6f8055867578439fb25d591563b32dbd3343c98732d",
|
||||
"stop": "36967eb4d423370d0d6c679b0a71f8d09f4fb2c73eed3e8cc50c604c44b0c32b",
|
||||
"style": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
|
||||
"style_hidden_edges": "a707dfc0bdcabe9144865f569abfcb478e493143cbd4331efc1404cf3082f6d2",
|
||||
"style_shaded": "6e5f8f9a9030ab3717f8b2192bbfd3f6b515ad398acf6cb41bc24f1298bc73b4",
|
||||
"style_shaded_edges": "6c2b04234b82ea4858c80190cc7208c60f549358253005afc35ab4e29506b578",
|
||||
"style_translucent": "c46b0cd485cfb9023514908357048b5cba9017a0c4976239da1e98f50e4ab5f9",
|
||||
"style_wireframe": "af7e575d378b5b6850bee9d3ca384efab95c8d4c876c5e3524f3ea771c4380a3",
|
||||
"suppress": "c54fbc01c12c0175d78cd63f692e8aaf73fc2cac79752b2a3ad821ac32734343",
|
||||
"surface": "ba0e839ce0ee03e7ff95a2389a5ecb20bd80c713064d9b1bd5f461f4a80d1921",
|
||||
"surface_blend": "0642f3b4af7d22f379a3c74f54c7b1c9eef0bdda19372ce4df73c0e56bdd7bbc",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Ambient occlusion</title>
|
||||
<g opacity="0.22"><path d="M13 51L51 51A38 38 0 0 0 13 13Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
|
||||
<g opacity="0.38"><path d="M13 51L40 51A27 27 0 0 0 13 24Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
|
||||
<g opacity="0.62"><path d="M13 51L29 51A16 16 0 0 0 13 35Z" fill="currentColor" stroke="currentColor" stroke-width="0" stroke-linejoin="round" stroke-linecap="round"/></g>
|
||||
<path d="M13 10L13 51L54 51" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 758 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Display</title>
|
||||
<path d="M10 22L54 22M10 42L54 42" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="24" cy="22" r="10.5" fill="#ffffff"/>
|
||||
<circle cx="42" cy="42" r="10.5" fill="#ffffff"/>
|
||||
<circle cx="24" cy="22" r="6.4" fill="currentColor"/>
|
||||
<circle cx="42" cy="42" r="6.4" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Grid</title>
|
||||
<path d="M12 12L52 12L52 52L12 52Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M25.3 12L25.3 52M38.7 12L38.7 52M12 25.3L52 25.3M12 38.7L52 38.7" fill="none" stroke="currentColor" stroke-width="4" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Ground shadow</title>
|
||||
<path d="M32 4.8 L48.1 13.9 L48.1 32.1 L32 41.2 L15.9 32.1 L15.9 13.9Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9.34 31.35A25 11 0 1 0 54.66 31.35" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Bodies</title>
|
||||
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Components</title>
|
||||
<path d="M22.91 5.75 L41.09 16.25 L22.91 26.75 L4.72 16.25ZM4.72 16.25 L22.91 26.75 L22.91 47.75 L4.72 37.25ZM41.09 16.25 L59.28 26.75 L41.09 37.25 L22.91 26.75ZM22.91 26.75 L41.09 37.25 L41.09 58.25 L22.91 47.75ZM59.28 26.75 L41.09 37.25 L41.09 58.25 L59.28 47.75Z" fill="currentColor" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M37.46 18.35L22.91 26.75L22.91 43.55" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 621 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Faces and edges</title>
|
||||
<path d="M32 6 L55 19 L32 32 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="4.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 32L9 19M32 32L55 19M32 32L32 58" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 554 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Features</title>
|
||||
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="4.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9 19L32 32M32 32L55 19M32 32L32 58" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M42 19C42 22.2 37.52 24.8 32 24.8C26.48 24.8 22 22.2 22 19C22 15.8 26.48 13.2 32 13.2C37.52 13.2 42 15.8 42 19Z" fill="currentColor" stroke="currentColor" stroke-width="0.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 632 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Parts</title>
|
||||
<path d="M22.91 5.75 L41.09 16.25 L22.91 26.75 L4.72 16.25ZM4.72 16.25 L22.91 26.75 L22.91 47.75 L4.72 37.25Z" fill="none" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M41.09 16.25 L59.28 26.75 L41.09 37.25 L22.91 26.75ZM22.91 26.75 L41.09 37.25 L41.09 58.25 L22.91 47.75ZM59.28 26.75 L41.09 37.25 L41.09 58.25 L59.28 47.75Z" fill="currentColor" stroke="currentColor" stroke-width="4.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Sketch features</title>
|
||||
<path d="M10 46C18 14 46 14 54 46" fill="none" stroke="currentColor" stroke-width="5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="10" cy="46" r="8.6" fill="#ffffff"/>
|
||||
<circle cx="54" cy="46" r="8.6" fill="#ffffff"/>
|
||||
<circle cx="32" cy="22.5" r="8.6" fill="#ffffff"/>
|
||||
<circle cx="10" cy="46" r="5.8" fill="currentColor"/>
|
||||
<circle cx="54" cy="46" r="5.8" fill="currentColor"/>
|
||||
<circle cx="32" cy="22.5" r="5.8" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 563 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Shaded with hidden edges</title>
|
||||
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 32L15.44 22.64M32 32L48.56 22.64M32 32L32 50.72" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 32L32 13.28M32 32L48.56 41.36M32 32L15.44 41.36" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="4.5 4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 619 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Shaded</title>
|
||||
<g opacity="0.5"><path d="M32 6 L55 19 L32 32 L9 19Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/></g>
|
||||
<g opacity="0.8"><path d="M9 19 L32 32 L32 58 L9 45Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/></g>
|
||||
<path d="M32 32 L55 19 L55 45 L32 58Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 583 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Translucent</title>
|
||||
<defs>
|
||||
<clipPath id="tr-clip"><path d="M32 8.6 L52.7 20.3 L52.7 43.7 L32 55.4 L11.3 43.7 L11.3 20.3Z"/></clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#tr-clip)"><path d="M-71 72L9 -8M-60.5 72L19.5 -8M-50 72L30 -8M-39.5 72L40.5 -8M-29 72L51 -8M-18.5 72L61.5 -8M-8 72L72 -8M2.5 72L82.5 -8M13 72L93 -8M23.5 72L103.5 -8M34 72L114 -8M44.5 72L124.5 -8M55 72L135 -8" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round"/></g>
|
||||
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 32L11.76 20.56M32 32L52.24 20.56M32 32L32 54.88" fill="none" stroke="#ffffff" stroke-width="6.4" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 32L11.3 20.3M32 32L52.7 20.3M32 32L32 55.4" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Wireframe</title>
|
||||
<path d="M32 6 L55 19 L55 45 L32 58 L9 45 L9 19Z" fill="none" stroke="currentColor" stroke-width="5.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 32L32 6M32 32L55 19M32 32L55 45M32 32L32 58M32 32L9 45M32 32L9 19" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Finish Edit</title>
|
||||
<path d="M50 32L14 32M25 21L14 32L25 43" fill="none" stroke="#236ead" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 237 B |
@@ -33,6 +33,7 @@
|
||||
"All open designs": "Alle åbne designs",
|
||||
"Allowed expressions, separated by semicolons": "Tilladte udtryk adskilt af semikolon",
|
||||
"Also deletes": "Sletter også",
|
||||
"Also place them from the origin (fully constrained)": "Placer dem også fra origo (fuldt fastlåst)",
|
||||
"Anchor": "Forankring",
|
||||
"Angle 2": "Vinkel 2",
|
||||
"Animate": "Animér",
|
||||
@@ -158,6 +159,7 @@
|
||||
"Designs you open or save appear here.": "Designs, du åbner eller gemmer, vises her.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Bestemmes af Til objekt. Vælg Vinkel for at ændre vinkeludstrækningen.",
|
||||
"Diameter": "Diameter",
|
||||
"Dimension new shapes": "Mål nye figurer",
|
||||
"Direction": "Retning",
|
||||
"Discard": "Kassér",
|
||||
"Distance": "Afstand",
|
||||
@@ -169,6 +171,8 @@
|
||||
"Drag to move the hole; click to remove it": "Træk for at flytte hullet; klik for at fjerne det",
|
||||
"Drive Joint": "Bevæg led",
|
||||
"Duration": "Varighed",
|
||||
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Hver ny figur får også mål fra skitsens origo, indtil intet af den er frit. En figur, der allerede er knyttet til origo eller til placeret geometri, får ingen.",
|
||||
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Hver figur, du tegner, får alle sine mål i de størrelser, du tegnede (indtastede værdier vinder). En størrelse, som skitsen allerede fastlægger, udelades.",
|
||||
"Edges": "Kanter",
|
||||
"Edit": "Redigér",
|
||||
"Edit Profile Sketch": "Redigér profilskitse",
|
||||
@@ -1190,7 +1194,9 @@
|
||||
"nav.free_orbit.help": "Musen og 3D-musen roterer gennem begge poler og bevarer rulning. Snap til akser med Shift er slået fra.",
|
||||
"nav.free_orbit.label": "Fri rotation af visningen",
|
||||
"nav.fusion.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, træk med højre roterer visningen",
|
||||
"nav.hfss.help": "Alt+træk med venstre museknap panorerer, Shift+Alt+venstre zoomer, Ctrl+træk med venstre fra et tomt område roterer visningen",
|
||||
"nav.inventor.help": "Træk med midterste museknap panorerer, Shift+midterste roterer visningen, Ctrl+midterste zoomer, træk med højre roterer visningen",
|
||||
"nav.maya.help": "Alt+træk med venstre museknap roterer visningen, Alt+midterste panorerer, Alt+højre zoomer; træk med midterste museknap panorerer også",
|
||||
"nav.openscad.help": "Træk med venstre museknap i et tomt område roterer visningen, Shift+venstre markerer med en ramme, træk i skitsegeometri redigerer den; træk med højre eller midterste museknap panorerer, Shift+højre eller Shift+midterste zoomer",
|
||||
"nav.solidworks.help": "Træk med midterste museknap roterer visningen, Ctrl+midterste panorerer, Shift+midterste zoomer, træk med højre roterer visningen",
|
||||
"nav.tinkercad.help": "Træk med højre museknap roterer visningen, træk med midterste eller Shift+højre panorerer",
|
||||
@@ -1209,6 +1215,11 @@
|
||||
"no picture": "intet billede",
|
||||
"none": "ingen",
|
||||
"parts.automatic_name": "automatisk",
|
||||
"parts.dimension.d": "Nominel diameter",
|
||||
"parts.dimension.height": "Højde",
|
||||
"parts.dimension.pitch": "Stigning",
|
||||
"parts.dimension.thickness": "Tykkelse",
|
||||
"parts.dimension.width": "Bredde",
|
||||
"pattern.alignment_2": "Justering 2",
|
||||
"pattern.alignment_hint": "Antal omfatter originalen. Centrér fordeler kopierne ligeligt; Vend vælger siden med den ekstra kopi ved et lige samlet antal.",
|
||||
"pattern.direction_1": "Retning 1",
|
||||
@@ -1388,6 +1399,13 @@
|
||||
"render.return_design": "Tilbage til Design",
|
||||
"render.save_setup": "Gem opsætning",
|
||||
"render.setup_name": "Opsætningsnavn",
|
||||
"render.shots_camera_changed": "Kameraet er ændret, siden denne optagelse blev gemt",
|
||||
"render.shots_name_help": "Indtast et unikt navn til den aktuelle optagelse.",
|
||||
"render.shots_no_result": "Ingen rendering endnu",
|
||||
"render.shots_result": "Rendering {number} · {width} × {height}",
|
||||
"render.shots_revert": "Gendan",
|
||||
"render.shots_save": "Gem optagelse",
|
||||
"render.shots_update": "Opdater optagelse",
|
||||
"render.unavailable_preview": "Denne opsætning bruger en type forhåndsvisning, der ikke er tilgængelig.",
|
||||
"render.unsaved": "Ændringer i forhåndsvisning er ikke gemt",
|
||||
"render.use_preview": "Brug integreret forhåndsvisning",
|
||||
@@ -2057,6 +2075,7 @@
|
||||
"ui.heading_utilities": "VÆRKTØJER",
|
||||
"ui.heading_utility": "VÆRKTØJ",
|
||||
"ui.heading_weldments": "SVEJSNING",
|
||||
"ui.height": "Højde",
|
||||
"ui.help": "Hjælp",
|
||||
"ui.hide": "Skjul",
|
||||
"ui.hideAxes": "Skjul akser",
|
||||
@@ -2095,6 +2114,7 @@
|
||||
"ui.language": "Sprog",
|
||||
"ui.layout_compact": "Kompakt",
|
||||
"ui.layout_floating": "Flydende",
|
||||
"ui.length": "Længde",
|
||||
"ui.length_decimals": "Længdedecimaler",
|
||||
"ui.light": "Lys",
|
||||
"ui.line": "Linje",
|
||||
@@ -2182,6 +2202,7 @@
|
||||
"ui.pinToShortcuts": "Fastgør til genveje",
|
||||
"ui.pinToToolbar": "Fastgør til værktøjslinje",
|
||||
"ui.pipe": "Rør",
|
||||
"ui.pitch": "Stigning",
|
||||
"ui.placement": "Placering",
|
||||
"ui.plane": "Plan",
|
||||
"ui.plane_at_angle": "Plan i vinkel",
|
||||
@@ -2365,6 +2386,7 @@
|
||||
"ui.visual_shaded_with_edges": "Skyggelagt med kanter",
|
||||
"ui.visual_style": "Visuel stil",
|
||||
"ui.visual_wireframe": "Trådmodel",
|
||||
"ui.width": "Bredde",
|
||||
"ui.workspace": "arbejdsområde",
|
||||
"ui.workspace_animation_description": "Eksploderede visninger, bevægelsesstudier og storyboards",
|
||||
"ui.workspace_available": "Tilgængelig",
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
"cmd.sketch_slot_center_point": "Centre Point Slot",
|
||||
"cmd.sketch_slot_center_to_center": "Centre to Centre Slot",
|
||||
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
|
||||
"prefs.sketch_tracking_guides": "Centre and midpoint alignment guides",
|
||||
"prefs.sketch_tracking_guides_note": "Hover over a circle centre or straight-edge midpoint, then move horizontally or vertically to show a temporary dotted guide.",
|
||||
"prefs.theme_follow": "Follow a theme file",
|
||||
"prefs.theme_follow_error": "Not applied: {error}. The last good theme stays on.",
|
||||
"prefs.theme_follow_launch": "Followed for this run only (--theme-file); reloaded whenever another tool rewrites it.",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"(active rule)": "(regla activa)",
|
||||
"0 turns it off": "0 lo desactiva",
|
||||
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Abre el borrador de GitHub de abajo y pega el informe copiado al portapapeles.",
|
||||
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Abre el borrador de GitHub de abajo. El título y el informe se rellenarán automáticamente.",
|
||||
@@ -26,6 +27,7 @@
|
||||
"All open designs": "Todos los diseños abiertos",
|
||||
"Allowed expressions, separated by semicolons": "Expresiones permitidas, separadas por punto y coma",
|
||||
"Also deletes": "También elimina",
|
||||
"Also place them from the origin (fully constrained)": "Situarlas también desde el origen (totalmente restringidas)",
|
||||
"Angle 2": "Ángulo 2",
|
||||
"Animate": "Animar",
|
||||
"Appearance name": "Nombre de apariencia",
|
||||
@@ -35,12 +37,16 @@
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Aplique primero la apariencia. Exporta las apariencias asignadas y los materiales creados en este diseño.",
|
||||
"Arc length": "Longitud del arco",
|
||||
"Area": "Área",
|
||||
"As Picked": "Según la selección",
|
||||
"As-Built": "Tal como se construyó",
|
||||
"Assign Rule": "Asignar regla",
|
||||
"Attachment": "Vinculación",
|
||||
"BLEND SURFACE": "SUPERFICIE DE TRANSICIÓN",
|
||||
"BOUNDARY SURFACE": "SUPERFICIE DE CONTORNO",
|
||||
"BROWSER": "EXPLORADOR",
|
||||
"Ball": "Rótula",
|
||||
"Base Height": "Altura de la base",
|
||||
"Bend Line": "Línea de plegado",
|
||||
"Bend Line Position": "Posición de la línea de plegado",
|
||||
"Bend Position": "Posición del pliegue",
|
||||
"Bend Radius": "Radio de plegado",
|
||||
@@ -48,6 +54,7 @@
|
||||
"Bends": "Pliegues",
|
||||
"Bodies": "Cuerpos",
|
||||
"Body": "Cuerpo",
|
||||
"Both": "Ambos",
|
||||
"Boundary": "Contorno",
|
||||
"Bounding box": "Caja delimitadora",
|
||||
"CHECK": "COMPROBAR",
|
||||
@@ -70,6 +77,7 @@
|
||||
"Checked": "Comprobado",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Elige un plano nuevo. La geometría y las cotas del boceto se mantienen en sus coordenadas bidimensionales.",
|
||||
"Choose…": "Elegir…",
|
||||
"Circle or sphere": "Círculo o esfera",
|
||||
"Circular": "Circular",
|
||||
"Circumference": "Circunferencia",
|
||||
"Clockwise": "Horario",
|
||||
@@ -81,6 +89,7 @@
|
||||
"Config Table": "Tabla de config.",
|
||||
"Configuration": "Configuración",
|
||||
"Configuration rules": "Reglas de configuración",
|
||||
"Constant radius": "Radio constante",
|
||||
"Contact": "Contacto",
|
||||
"Contact Sets": "Conjuntos de contacto",
|
||||
"Contact sets": "Conjuntos de contacto",
|
||||
@@ -105,6 +114,8 @@
|
||||
"Curvature (G2)": "Curvatura (G2)",
|
||||
"Curvature change (G3)": "Cambio de curvatura (G3)",
|
||||
"Customize Toolbar": "Personalizar barra de herramientas",
|
||||
"Cylindrical": "Cilíndrica",
|
||||
"Deboss": "Grabado",
|
||||
"Delete": "Eliminar",
|
||||
"Delete all": "Eliminar todo",
|
||||
"Delete rule": "Eliminar regla",
|
||||
@@ -114,6 +125,7 @@
|
||||
"Dependents": "Dependientes",
|
||||
"Designs you open or save appear here.": "Aquí aparecen los diseños que abras o guardes.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Determinado por «Hasta objeto». Elige «Ángulo» para modificar la extensión angular.",
|
||||
"Dimension new shapes": "Acotar las formas nuevas",
|
||||
"Direction": "Dirección",
|
||||
"Discard": "Descartar",
|
||||
"Do not export hidden edges": "No exportar aristas ocultas",
|
||||
@@ -122,6 +134,9 @@
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Doble clic con el botón central en la vista: Inicio (vista inicial que encuadra lo visible), Ajustar (encuadrar sin cambiar la orientación) o nada",
|
||||
"Drive Joint": "Accionar unión",
|
||||
"Duration": "Duración",
|
||||
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Cada forma nueva también se acota desde el origen del croquis hasta que no quede nada libre. Una forma ya unida al origen o a geometría situada no recibe ninguna.",
|
||||
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Cada forma que dibujas recibe todas sus cotas con los tamaños dibujados (los valores escritos tienen prioridad). Se omite un tamaño que el croquis ya fija.",
|
||||
"Edge": "Arista",
|
||||
"Edges": "Aristas",
|
||||
"Edit": "Editar",
|
||||
"Edit Profile Sketch": "Editar boceto del perfil",
|
||||
@@ -129,6 +144,7 @@
|
||||
"Enabled": "Activado",
|
||||
"End bulge": "Abombamiento final",
|
||||
"End continuity": "Continuidad final",
|
||||
"Equal Distance": "Distancia igual",
|
||||
"Explode": "Explosionar",
|
||||
"Explode Text": "Descomponer texto",
|
||||
"Export Animation…": "Exportar animación…",
|
||||
@@ -148,6 +164,8 @@
|
||||
"Face attachment lost": "Se ha perdido la vinculación de la cara",
|
||||
"Faces": "Caras",
|
||||
"Faces / edges / vertices": "Caras / aristas / vértices",
|
||||
"Faces/Body": "Caras/Cuerpo",
|
||||
"Factor must be a positive number": "El factor debe ser un número positivo",
|
||||
"Fastener": "Sujeción",
|
||||
"Favorites": "Favoritos",
|
||||
"Favourite": "Favorito",
|
||||
@@ -156,20 +174,28 @@
|
||||
"Filter": "Filtrar",
|
||||
"Find in Browser": "Buscar en el explorador",
|
||||
"Finish": "Acabado",
|
||||
"First edge": "Primera arista",
|
||||
"First point": "Primer punto",
|
||||
"Flange Gap": "Separación de pestaña",
|
||||
"Flat Pattern": "Desarrollo plano",
|
||||
"Flat size": "Tamaño del desarrollo",
|
||||
"Follow camera (Look At sketches)": "Seguir cámara (orientar hacia los bocetos)",
|
||||
"Follow the camera (Look At sketches)": "Seguir la cámara (orientar hacia los bocetos)",
|
||||
"Font": "Fuente",
|
||||
"Free Move": "Movimiento libre",
|
||||
"From Bodies": "Desde cuerpos",
|
||||
"From Snap": "Desde el enganche",
|
||||
"From edges": "Aristas de origen",
|
||||
"Full Length": "Longitud total",
|
||||
"Gap": "Separación",
|
||||
"Grid": "Cuadrícula",
|
||||
"HISTORY": "HISTORIAL",
|
||||
"Height": "Altura",
|
||||
"Hem Gap": "Separación de reborde",
|
||||
"Hidden edges": "Aristas ocultas",
|
||||
"History layout": "Disposición del historial",
|
||||
"Hole Depth": "Profundidad del agujero",
|
||||
"Hole Diameter": "Diámetro del agujero",
|
||||
"Holes": "Agujeros",
|
||||
"Holes (edges on them)": "Agujeros (aristas que contienen)",
|
||||
"Home view": "Vista de inicio",
|
||||
@@ -185,10 +211,14 @@
|
||||
"Isocurve": "Isocurva",
|
||||
"Issue title": "Título de la incidencia",
|
||||
"JOINTS": "UNIONES",
|
||||
"Joint 1": "Unión 1",
|
||||
"Joint 2": "Unión 2",
|
||||
"Joint Origin1": "Origen de unión1",
|
||||
"K Factor": "Factor K",
|
||||
"Keep (points on it)": "Conservar (puntos sobre ella)",
|
||||
"Keep Tools": "Conservar herramientas",
|
||||
"Keep of the other surface": "Conservar de la otra superficie",
|
||||
"Knife Edge": "Borde afilado",
|
||||
"LIBRARY": "BIBLIOTECA",
|
||||
"LINKS": "VÍNCULOS",
|
||||
"Later": "Más tarde",
|
||||
@@ -199,7 +229,9 @@
|
||||
"Letter Spacing": "Espaciado entre letras",
|
||||
"Library": "Biblioteca",
|
||||
"Library file": "Archivo de biblioteca",
|
||||
"Library response is missing": "Falta la respuesta de la biblioteca",
|
||||
"Line Spacing": "Interlineado",
|
||||
"Line on plane": "Línea en plano",
|
||||
"Local library": "Biblioteca local",
|
||||
"Loop": "Bucle",
|
||||
"MANAGE": "GESTIONAR",
|
||||
@@ -213,9 +245,12 @@
|
||||
"Material": "Material",
|
||||
"Material copied. Existing body assignments are unchanged.": "Material copiado. Se conservan las asignaciones actuales de los cuerpos.",
|
||||
"Max": "Máx.",
|
||||
"Max Thickness": "Espesor máx.",
|
||||
"Mid Radius": "Radio intermedio",
|
||||
"Middle double-click": "Doble clic central",
|
||||
"Min": "Mín.",
|
||||
"Min Draft": "Ángulo de salida mín.",
|
||||
"Min Thickness": "Espesor mín.",
|
||||
"Mitre": "Inglete",
|
||||
"Model Parameters": "Parámetros del modelo",
|
||||
"Modeled": "Modelado",
|
||||
@@ -230,6 +265,7 @@
|
||||
"Name": "Nombre",
|
||||
"Name on import": "Nombre al importar",
|
||||
"Nearest": "Más cercano",
|
||||
"Neutral plane": "Plano neutro",
|
||||
"New": "Nuevo",
|
||||
"New Comp.": "Nuevo comp.",
|
||||
"New Sketch": "Nuevo boceto",
|
||||
@@ -240,6 +276,7 @@
|
||||
"No matching appearances": "No hay apariencias coincidentes",
|
||||
"No matching physical materials.": "No hay materiales físicos coincidentes.",
|
||||
"No visible bodies or sketches to export.": "No hay cuerpos ni bocetos visibles para exportar.",
|
||||
"Nominal Radius": "Radio nominal",
|
||||
"None": "Ninguno",
|
||||
"Not available yet": "Todavía no disponible",
|
||||
"Notch": "Muesca",
|
||||
@@ -255,19 +292,25 @@
|
||||
"Overall length": "Longitud total",
|
||||
"Param. Graph": "Gráfico de parám.",
|
||||
"Parameter": "Parámetro",
|
||||
"Part length must be greater than zero and at most 2000 mm": "La longitud de la pieza debe ser mayor que cero y como máximo 2000 mm",
|
||||
"Parts": "Piezas",
|
||||
"Pass through": "Pasante",
|
||||
"Path": "Trayectoria",
|
||||
"Pattern": "Patrón",
|
||||
"Per Edge": "Por arista",
|
||||
"Physical material definitions": "Definiciones de materiales físicos",
|
||||
"Pick a part size": "Elige un tamaño de pieza",
|
||||
"Pick the tool bodies": "Selecciona los cuerpos herramienta",
|
||||
"Picked": "Seleccionado",
|
||||
"Picked point": "Punto seleccionado",
|
||||
"Pin-slot": "Pasador-ranura",
|
||||
"Place at the origin instead": "Colocar en el origen",
|
||||
"Planar": "Plana",
|
||||
"Plastic Rules": "Reglas de plástico",
|
||||
"Playback": "Reproducción",
|
||||
"Plot": "Graficar",
|
||||
"Point on line": "Punto en línea",
|
||||
"Point on plane": "Punto en plano",
|
||||
"Position": "Posición",
|
||||
"Position (0 to 1)": "Posición (de 0 a 1)",
|
||||
"Position (G0)": "Posición (G0)",
|
||||
@@ -277,6 +320,7 @@
|
||||
"Preview leaves the current design unchanged.": "La vista previa no modifica el diseño actual.",
|
||||
"Profiles/Curves": "Perfiles/curvas",
|
||||
"Quantity": "Cantidad",
|
||||
"Quantity must be at least 2": "La cantidad debe ser al menos 2",
|
||||
"RECENT": "RECIENTES",
|
||||
"REFERENCE": "REFERENCIA",
|
||||
"RULED SURFACE": "SUPERFICIE REGLADA",
|
||||
@@ -288,6 +332,8 @@
|
||||
"Recover unsaved design?": "¿Recuperar el diseño sin guardar?",
|
||||
"Rectangular": "Rectangular",
|
||||
"Reference face": "Cara de referencia",
|
||||
"Relief Depth": "Profundidad de alivio",
|
||||
"Relief Width": "Ancho de alivio",
|
||||
"Remove Tools": "Eliminar herramientas",
|
||||
"Remove from list": "Quitar de la lista",
|
||||
"Remove override": "Quitar anulación",
|
||||
@@ -304,6 +350,7 @@
|
||||
"Restore the inherited component or physical material appearance": "Restaurar la apariencia heredada del componente o del material físico",
|
||||
"Result": "Resultado",
|
||||
"Retake screenshot": "Repetir captura de pantalla",
|
||||
"Reveal Height": "Altura del escalón",
|
||||
"Reverse": "Invertir",
|
||||
"Reverse Direction": "Invertir dirección",
|
||||
"Revert": "Revertir",
|
||||
@@ -311,7 +358,10 @@
|
||||
"Revert Pos.": "Revertir pos.",
|
||||
"Revert Position": "Revertir posición",
|
||||
"Review the report and screenshot before opening GitHub.": "Revisa el informe y la captura antes de abrir GitHub.",
|
||||
"Revolute": "Revolución",
|
||||
"Rigid": "Rígida",
|
||||
"Rim Edge": "Arista del borde",
|
||||
"Rim face": "Cara del borde",
|
||||
"Roll History Marker Here": "Desplazar marcador de la línea de tiempo hasta aquí",
|
||||
"Round": "Redondo",
|
||||
"Rule": "Regla",
|
||||
@@ -328,11 +378,22 @@
|
||||
"Search": "Buscar",
|
||||
"Search commands": "Buscar comandos",
|
||||
"Search this library": "Buscar en esta biblioteca",
|
||||
"Second edge": "Segunda arista",
|
||||
"Second point": "Segundo punto",
|
||||
"Section": "Sección",
|
||||
"Section position": "Posición de la sección",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Seleccionar otro: mantén pulsado el botón izquierdo sobre el modelo (o haz clic derecho › Seleccionar otro…) para ver todo lo que hay bajo el cursor; vuelve a pulsar en el mismo lugar para seleccionar el siguiente elemento. Haz doble clic con el botón central para volver a Inicio (Preferencias › Navegación).",
|
||||
"Select bodies (a face picks its body)": "Selecciona cuerpos (al seleccionar una cara se selecciona su cuerpo)",
|
||||
"Select bodies or faces instead": "Selecciona cuerpos o caras en su lugar",
|
||||
"Select points of the sketch being edited": "Selecciona puntos del boceto que se está editando",
|
||||
"Select the centre the pattern turns around first": "Selecciona primero el centro alrededor del cual gira el patrón",
|
||||
"Select the curves to offset first": "Selecciona primero las curvas que se van a desfasar",
|
||||
"Select the mirror line": "Selecciona la línea de simetría",
|
||||
"Select the sketch curves or points to mirror first": "Selecciona primero las curvas o puntos del boceto que se van a reflejar",
|
||||
"Select the sketch curves or points to move first": "Selecciona primero las curvas o puntos del boceto que se van a mover",
|
||||
"Select the sketch curves or points to pattern first": "Selecciona primero las curvas o puntos del boceto para el patrón",
|
||||
"Select the sketch curves or points to scale first": "Selecciona primero las curvas o puntos del boceto que se van a escalar",
|
||||
"Select the sketch curves or points to transform first": "Selecciona primero las curvas o puntos del boceto que se van a transformar",
|
||||
"Selection filter": "Filtro de selección",
|
||||
"Semantic faces share their appearance across instances.": "Las caras semánticas comparten su apariencia entre instancias.",
|
||||
"Send your report on GitHub": "Enviar tu informe a GitHub",
|
||||
@@ -350,8 +411,11 @@
|
||||
"Sketch geometry and profiles": "Geometría y perfiles del boceto",
|
||||
"Sketch plane has no body-face attachment.": "El plano del boceto no está vinculado a ninguna cara del cuerpo.",
|
||||
"Sketch points": "Puntos del boceto",
|
||||
"Slider": "Deslizante",
|
||||
"Slot width": "Anchura de la ranura",
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft se cerró antes de guardar estos diseños. Puedes abrir su último guardado automático.",
|
||||
"Spacing 2 must be a non-zero number": "El espaciado 2 debe ser un número distinto de cero",
|
||||
"Spacing must be a non-zero number": "El espaciado debe ser un número distinto de cero",
|
||||
"Specialise": "Especializar",
|
||||
"Speed": "Velocidad",
|
||||
"Square": "Cuadrado",
|
||||
@@ -361,6 +425,7 @@
|
||||
"Start angle": "Ángulo inicial",
|
||||
"Start bulge": "Abombamiento inicial",
|
||||
"Start continuity": "Continuidad inicial",
|
||||
"Stationary Side": "Lado fijo",
|
||||
"Step": "Paso",
|
||||
"Steps": "Pasos",
|
||||
"Stitch the kept pieces": "Coser las piezas conservadas",
|
||||
@@ -369,29 +434,41 @@
|
||||
"Style": "Estilo",
|
||||
"Suppressed": "Suprimido",
|
||||
"Surface": "Superficie",
|
||||
"Surfaces": "Superficies",
|
||||
"Swap Ends": "Intercambiar extremos",
|
||||
"TRIM": "RECORTAR",
|
||||
"Tangent (G1)": "Tangencia (G1)",
|
||||
"Tangent Chain": "Cadena tangente",
|
||||
"Target Face (optional)": "Cara de destino (opcional)",
|
||||
"Tear": "Desgarro",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "El boceto discontinuo sirve de guía del plano guardado. Selecciona una cara nueva para reparar la vinculación.",
|
||||
"The design changed. Reopen Change Part Size for the component.": "El diseño ha cambiado. Vuelve a abrir Cambiar tamaño de pieza para el componente.",
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "La dirección debe ser un eje o una curva recta del boceto que esté en el plano del boceto",
|
||||
"The mirror line cannot also be mirrored": "La línea de simetría no puede reflejarse también",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "La línea de simetría debe ser una curva recta de este boceto, no un eje del origen",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Falta la instancia de autor guardada. Selecciona una posición de autor válida antes de volver a vincular.",
|
||||
"Then": "Entonces",
|
||||
"These features will be deleted:": "Se eliminarán estas operaciones:",
|
||||
"These features will fail:": "Estas operaciones fallarán:",
|
||||
"Thickness Outside": "Espesor hacia fuera",
|
||||
"Thickness Variation": "Variación de espesor",
|
||||
"Thin": "Delgado",
|
||||
"Thin Extrude": "Extrusión delgada",
|
||||
"Third plane": "Tercer plano",
|
||||
"Third point": "Tercer punto",
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "Este elemento secundario es compartido. Si lo eliminas, desaparecerá de todas las copias de su elemento principal.",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "Este elemento secundario es compartido. Los cambios de posición se aplican a todas las copias de su elemento principal.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Esta cara no permite una vinculación persistente. Elige otra cara o utiliza expresamente un plano fijo.",
|
||||
"Through point": "Por un punto",
|
||||
"To Offset": "Hasta desfase",
|
||||
"To Snap": "Hasta el enganche",
|
||||
"To edges": "Aristas de destino",
|
||||
"Tolerance": "Tolerancia",
|
||||
"Too close": "Demasiado cerca",
|
||||
"Trimming tool (plane, face or curves)": "Herramienta de recorte (plano, cara o curvas)",
|
||||
"Turn off section analysis before exporting the view.": "Desactiva el análisis de sección antes de exportar la vista.",
|
||||
"Type": "Tipo",
|
||||
"Type a distance or an angle to move by": "Escribe una distancia o un ángulo para mover",
|
||||
"UNTRIM": "ANULAR RECORTE",
|
||||
"Unit": "Unidad",
|
||||
"Updating the preview": "Actualizando vista previa",
|
||||
@@ -420,6 +497,17 @@
|
||||
"Z angle": "Ángulo Z",
|
||||
"Z distance": "Distancia Z",
|
||||
"Zebra": "Cebra",
|
||||
"`joint` must be an id or a name": "`joint` debe ser un id o un nombre",
|
||||
"`params` must be an object": "`params` debe ser un objeto",
|
||||
"`values` must be a list": "`values` debe ser una lista",
|
||||
"a chamfer needs a corner of exactly two lines": "un chaflán necesita una esquina de exactamente dos líneas",
|
||||
"a file operation is in progress": "hay una operación de archivo en curso",
|
||||
"a picked curve is no longer in a sketch": "una curva elegida ya no está en un boceto",
|
||||
"a picked curve's sketch has not been solved": "el boceto de una curva elegida no se ha resuelto",
|
||||
"a picked edge has no points": "una arista elegida no tiene puntos",
|
||||
"a picked edge is gone": "una arista elegida ya no existe",
|
||||
"a picked edge's body is gone": "el cuerpo de una arista elegida ya no existe",
|
||||
"add a key for a joint first": "añade primero una clave para una unión",
|
||||
"angle, height": "ángulo, altura",
|
||||
"appearance.dielectric_ior": "Índice de refracción dieléctrico",
|
||||
"appearance.metallic": "Metalicidad",
|
||||
@@ -428,6 +516,7 @@
|
||||
"appearance.pbr_opaque": "PBR opaco",
|
||||
"appearance.roughness": "Rugosidad",
|
||||
"appearance.surface_response": "Respuesta de la superficie",
|
||||
"bom.group_children": "Subpiezas en la fila de su pieza",
|
||||
"browser.body_name": "Cuerpo{number}",
|
||||
"browser.calibrate_title": "Calibrar: {name}",
|
||||
"browser.capture_many": "Se han movido {count} componentes, pero sus posiciones no se han capturado.",
|
||||
@@ -438,6 +527,8 @@
|
||||
"browser.move_title": "Mover: {name}",
|
||||
"browser.parameters": "Parámetros ({count})",
|
||||
"browser.precision": "Precisión: {precision}",
|
||||
"browser.sketch_attachment_lost": "{name} (fijación perdida)",
|
||||
"browser.sketch_editing": "{name} (en edición)",
|
||||
"browser.sketch_name": "Boceto{number}",
|
||||
"browser.units": "Unidades: {units}",
|
||||
"cam.flat": "Cara plana",
|
||||
@@ -447,6 +538,11 @@
|
||||
"cam.roller": "Rodillo",
|
||||
"cam.roller_pick": "Rodillo (opcional)",
|
||||
"cam.tip_pick": "Punta (opcional)",
|
||||
"choose a profile": "elige un perfil",
|
||||
"choose the cam's and the follower's joints": "elige las uniones de la leva y del seguidor",
|
||||
"choose the joints to couple": "elige las uniones que acoplar",
|
||||
"click a plane or planar face": "hacer clic en un plano o una cara plana",
|
||||
"click sketch curves": "hacer clic en curvas del boceto",
|
||||
"cmd.appearance_assign": "Apariencia",
|
||||
"cmd.appearance_library": "Biblioteca de apariencias",
|
||||
"cmd.appearance_list": "Listar apariencias",
|
||||
@@ -884,6 +980,8 @@
|
||||
"dxf.progress.write": "Escribiendo DXF (5/5)",
|
||||
"dxf.tolerance": "Tolerancia de curva",
|
||||
"dxf.tolerance_help": "Solo cuerpos visibles y bocetos mostrados. Las curvas usan segmentos rectos. Una tolerancia menor da curvas más finas y consume más tiempo y memoria.",
|
||||
"each rail's curves must be in one sketch": "las curvas de cada guía deben estar en un solo boceto",
|
||||
"feature computation ended without a result": "el cálculo de la operación terminó sin resultado",
|
||||
"file_load.built": "Construido hasta ahora: {built} de {total} operaciones.",
|
||||
"file_load.converting": "Convirtiendo sólido {done} de {total}",
|
||||
"file_load.inserting": "Insertando geometría",
|
||||
@@ -1078,16 +1176,55 @@
|
||||
"no hole": "sin agujero",
|
||||
"no interference": "sin interferencias",
|
||||
"no joint that moves yet": "todavía no hay ninguna unión móvil",
|
||||
"no motion study yet": "todavía no hay ningún estudio de movimiento",
|
||||
"no picture": "sin imagen",
|
||||
"no such joint": "no existe esa unión",
|
||||
"none": "ninguno",
|
||||
"not a move": "no es un movimiento",
|
||||
"not a number": "no es un número",
|
||||
"open a sketch first": "abre primero un boceto",
|
||||
"parts.automatic_name": "automático",
|
||||
"parts.dimension.across_flats": "Entrecaras",
|
||||
"parts.dimension.bore": "Diámetro interior",
|
||||
"parts.dimension.head_diameter": "Diámetro cabeza",
|
||||
"parts.dimension.head_height": "Altura cabeza",
|
||||
"parts.dimension.height": "Altura",
|
||||
"parts.dimension.inner": "Diámetro interior",
|
||||
"parts.dimension.outer": "Diámetro exterior",
|
||||
"parts.dimension.socket": "Hexágono interior",
|
||||
"parts.dimension.socket_depth": "Profundidad hexágono",
|
||||
"parts.dimension.thickness": "Espesor",
|
||||
"parts.dimension.width": "Anchura",
|
||||
"parts.family.bearing": "Rodamiento rígido de bolas (envolvente)",
|
||||
"parts.family.dowel_pin": "Pasador cilíndrico",
|
||||
"parts.family.hex_bolt": "Tornillo de cabeza hexagonal",
|
||||
"parts.family.hex_nut": "Tuerca hexagonal",
|
||||
"parts.family.set_screw": "Prisionero con hexágono interior, punta plana",
|
||||
"parts.family.socket_head_cap_screw": "Tornillo cilíndrico con hexágono interior",
|
||||
"parts.family.washer": "Arandela plana",
|
||||
"pattern.alignment_2": "Alineación 2",
|
||||
"pattern.alignment_hint": "Los recuentos incluyen el original. Centrar equilibra las copias; Invertir elige el lado adicional cuando el total es par.",
|
||||
"pattern.direction_1": "Dirección 1",
|
||||
"pattern.direction_2": "Dirección 2",
|
||||
"pattern.flip_direction": "Invertir dirección",
|
||||
"pick a body or some of its faces": "elige un cuerpo o algunas de sus caras",
|
||||
"pick a hole to seat it, or OK places it at the origin": "selecciona un agujero para colocarlo o pulsa Aceptar para situarlo en el origen",
|
||||
"pick a planar face": "elige una cara plana",
|
||||
"pick a plane or a face as the tool": "elige un plano o una cara como herramienta",
|
||||
"pick a size": "elige un tamaño",
|
||||
"pick edges of a body": "elige aristas de un cuerpo",
|
||||
"pick edges of one body": "elige aristas de un solo cuerpo",
|
||||
"pick edges of the holes to remove": "elige las aristas de los agujeros que quitar",
|
||||
"pick faces of one body": "elige caras de un solo cuerpo",
|
||||
"pick faces to offset": "elige las caras que desfasar",
|
||||
"pick lines of one sketch": "elige líneas de un solo boceto",
|
||||
"pick one or two vertices, edges or faces": "selecciona uno o dos vértices, aristas o caras",
|
||||
"pick one plane or face as the tool, or sketch curves": "elige un plano o una cara como herramienta, o curvas de boceto",
|
||||
"pick sections in two or more sketches": "elige secciones en dos o más bocetos",
|
||||
"pick sketch curves or edges": "elige curvas de boceto o aristas",
|
||||
"pick the face to untrim": "elige la cara a la que quitar el recorte",
|
||||
"pick what to keep": "elige qué conservar",
|
||||
"pick what to keep of the other surface": "elige qué conservar de la otra superficie",
|
||||
"prefs.cpu": "CPU (software)",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.theme_follow": "Seguir un archivo de tema",
|
||||
@@ -1720,6 +1857,26 @@
|
||||
"search.suggest": "¿Quisiste decir {label}?",
|
||||
"search.try": "Escribe el nombre de un comando, una operación de este diseño o un parámetro. Para ejecutar un comando con valores, escribe su id y un objeto JSON: {example}",
|
||||
"search.valid": "Válido",
|
||||
"select a direction (an axis, a sketch line or a straight edge) first": "selecciona primero una dirección (un eje, una línea de boceto o una arista recta)",
|
||||
"select a direction (an axis, a sketch line or a straight edge) for the profile to follow": "selecciona una dirección (un eje, una línea de boceto o una arista recta) que seguirá el perfil",
|
||||
"select a point on a component": "selecciona un punto en un componente",
|
||||
"select a profile, edges or sketch lines first": "selecciona primero un perfil, aristas o líneas de boceto",
|
||||
"select an axis": "selecciona un eje",
|
||||
"select an axis, a straight or circular edge, or a cylinder, cone or torus face first": "selecciona primero un eje, una arista recta o circular, o una cara cilíndrica, cónica o toroidal",
|
||||
"select bodies first": "selecciona primero cuerpos",
|
||||
"select corner points, or the two lines of one corner": "selecciona puntos de esquina, o las dos líneas de una esquina",
|
||||
"select one corner point, or the two lines of one corner": "selecciona un punto de esquina, o las dos líneas de una esquina",
|
||||
"select sheet edges first": "selecciona primero aristas de chapa",
|
||||
"select the bend line (a sketch line) first": "selecciona primero la línea de plegado (una línea de boceto)",
|
||||
"select the cam's profile (a sketch profile or the cam's edges)": "selecciona el perfil de la leva (un perfil de boceto o las aristas de la leva)",
|
||||
"select the corner to cut first": "selecciona primero la esquina que cortar",
|
||||
"select the corners to round first": "selecciona primero las esquinas que redondear",
|
||||
"select the fixed face first (a plate's large face)": "selecciona primero la cara fija (la cara grande de una placa)",
|
||||
"select the frame sketch's lines first": "selecciona primero las líneas del boceto del marco",
|
||||
"select the path (sketch lines across the edge)": "selecciona la trayectoria (líneas de boceto a lo largo de la arista)",
|
||||
"select the rim face first": "selecciona primero la cara del borde",
|
||||
"select the sheet edge first": "selecciona primero la arista de la chapa",
|
||||
"select two components": "selecciona dos componentes",
|
||||
"settings.display_quality_note": "Los niveles más finos muestran las caras y aristas curvas con mayor suavidad, pero tardan más en generar la malla. Solo cambia el visor, nunca el diseño ni las exportaciones.",
|
||||
"settings.frame_rate_note": "Muestra los fotogramas por segundo en la esquina inferior derecha de la vista.",
|
||||
"settings.measure_axis_distances": "Mostrar distancias X/Y/Z",
|
||||
@@ -1732,16 +1889,22 @@
|
||||
"settings.reset_intro": "Estos ajustes no funcionan en este ordenador e impedirían iniciar SolidCraft, por lo que se han cambiado:",
|
||||
"settings.reset_title": "Se han cambiado los ajustes",
|
||||
"settings.supersampling_note": "Dibuja la vista a mayor resolución y después la reduce: bordes, líneas y cuadrícula más suaves en pantallas inferiores a 4K. Consume recursos de GPU: unos 2,25× píxeles a 1,5× y 4× a 2×.",
|
||||
"sheet.flange_type_base": "Base",
|
||||
"sheet.flange_type_contour": "Contorno",
|
||||
"sheet.flange_type_edge": "Arista",
|
||||
"shortcut.json_error": "los parámetros deben estar en formato JSON: {error}",
|
||||
"shortcut.no_command": "no existe el comando `{id}`; el texto anterior al JSON debe ser un ID de comando",
|
||||
"shortcut.pin": "Fijar en la parte superior del cuadro S",
|
||||
"shortcut.unpin": "Desfijar",
|
||||
"sketch is missing": "falta el boceto",
|
||||
"sketch.degrees_of_freedom": "{count} grados de libertad",
|
||||
"sketch.dof_left": "Quedan {count} grados de libertad",
|
||||
"sketch.dof_left_one": "Queda {count} grado de libertad",
|
||||
"sketch.draw_curves": "Dibujar como {linetype}",
|
||||
"sketch.make_selection": "Convertir la selección a {linetype}",
|
||||
"sketch.open_ends": "Abiertos {count}",
|
||||
"sketch.status_conflict": "restricciones en conflicto",
|
||||
"sketch.status_fully_constrained": "totalmente restringido",
|
||||
"sketch.untied_ends": "Sin vincular {count}",
|
||||
"sketch_panel.attached": "Vinculado a {body} / {path}",
|
||||
"sketch_panel.author": "Ubicación de autoría: {path}",
|
||||
@@ -1763,7 +1926,24 @@
|
||||
"sketch_text.path_hint": "Curva del boceto que seguirá el texto: haz clic aquí y después en la curva",
|
||||
"sketch_text.use_font_file": "Usar el archivo de fuente {path}",
|
||||
"solid": "sólido",
|
||||
"the angle must be a number": "el ángulo debe ser un número",
|
||||
"the curves must be in one sketch": "las curvas deben estar en un solo boceto",
|
||||
"the direction must be an axis, a sketch line or a straight edge": "la dirección debe ser un eje, una línea de boceto o una arista recta",
|
||||
"the duration must be a number of seconds, 0.001 or more": "la duración debe ser un número de segundos, 0.001 o más",
|
||||
"the line's sketch is gone": "el boceto de la línea ya no existe",
|
||||
"the lines must be in one sketch": "las líneas deben estar en un solo boceto",
|
||||
"the lines must be in one sketch, not the section's": "las líneas deben estar en un solo boceto, no en el de la sección",
|
||||
"the parts library is empty": "la biblioteca de piezas está vacía",
|
||||
"the parts library is not available": "la biblioteca de piezas no está disponible",
|
||||
"the path curves must be in one other sketch": "las curvas de la trayectoria deben estar en un solo boceto distinto",
|
||||
"the path's sketch is gone": "el boceto de la trayectoria ya no existe",
|
||||
"the position must be a number": "la posición debe ser un número",
|
||||
"the preview failed": "falló la vista previa",
|
||||
"the profiles and curves must be in one sketch": "los perfiles y las curvas deben estar en un solo boceto",
|
||||
"the reference face must be on the edges' body": "la cara de referencia debe estar en el cuerpo de las aristas",
|
||||
"the scale must be above 0": "la escala debe ser mayor que 0",
|
||||
"the section's profiles must be in one sketch": "los perfiles de la sección deben estar en un solo boceto",
|
||||
"the two origins are on the same component": "los dos orígenes están en el mismo componente",
|
||||
"thread.automatic_size": "Tamaño automático",
|
||||
"thread.designation": "Designación",
|
||||
"thread.iso_metric": "Métrica ISO",
|
||||
@@ -1790,6 +1970,7 @@
|
||||
"trace.points": "{count} puntos a lo largo de la trayectoria",
|
||||
"trace.stopped_by_contact": "Termina en la muestra {sample}: {a} atravesaría {b}",
|
||||
"trace.stopped_by_loop": "Termina en la muestra {sample}: el bucle que pasa por {joint} no puede cerrarse más",
|
||||
"trim by sketch curves, or by one plane or face": "recorta con curvas de boceto, o con un plano o una cara",
|
||||
"ui.1_degree_of_freedom": "1 grado de libertad",
|
||||
"ui.3d_mouse_axes_reassigned": "Los ejes están reasignados en Preferencias, y eso sigue aplicándose sobre esta calibración.",
|
||||
"ui.3d_mouse_button": "Botón {number}",
|
||||
@@ -2216,6 +2397,8 @@
|
||||
"ui.design_properties.revision": "Revisión",
|
||||
"ui.diameter": "Diámetro",
|
||||
"ui.dimension": "Cota",
|
||||
"ui.direct_interaction": "Interacción directa",
|
||||
"ui.direct_interaction_note": "Trabaja directamente sobre el modelo en la vista. Pulsa una cara plana o un perfil y tira: hacia arriba extruye, hacia abajo dentro de un cuerpo corta, y el movimiento lateral fija el paso",
|
||||
"ui.direction": "Dirección",
|
||||
"ui.display_quality": "Calidad de visualización",
|
||||
"ui.display_quality_coarse": "Baja",
|
||||
@@ -2239,6 +2422,7 @@
|
||||
"ui.editJointLimits": "Editar límites de unión",
|
||||
"ui.edit_section_analysis": "Editar análisis de sección",
|
||||
"ui.ellipse": "Elipse",
|
||||
"ui.emboss_wrap_note": "Los perfiles de líneas ortogonales tangentes a un cilindro compatible se envuelven automáticamente. Elegir una pared para definir el destino.",
|
||||
"ui.end_radius": "Radio final",
|
||||
"ui.end_scale": "Escala final",
|
||||
"ui.entities": "Entidades",
|
||||
@@ -2304,6 +2488,7 @@
|
||||
"ui.heading_extrude": "EXTRUIR",
|
||||
"ui.heading_fillet": "REDONDEO",
|
||||
"ui.heading_finish_sketch": "FINALIZAR BOCETO",
|
||||
"ui.heading_form": "FORMA",
|
||||
"ui.heading_front": "FRONTAL",
|
||||
"ui.heading_hole": "AGUJERO",
|
||||
"ui.heading_home": "INICIO",
|
||||
@@ -2352,6 +2537,7 @@
|
||||
"ui.heading_utilities": "UTILIDADES",
|
||||
"ui.heading_utility": "UTILIDAD",
|
||||
"ui.heading_weldments": "SOLDADURA",
|
||||
"ui.height": "Altura",
|
||||
"ui.help": "Ayuda",
|
||||
"ui.hide": "Ocultar",
|
||||
"ui.hideAxes": "Ocultar ejes",
|
||||
@@ -2379,11 +2565,14 @@
|
||||
"ui.intersect": "Intersecar",
|
||||
"ui.isolate": "Aislar",
|
||||
"ui.join": "Unir",
|
||||
"ui.joint_play": "▶ Reproducir",
|
||||
"ui.joint_stop": "■ Detener",
|
||||
"ui.joints": "Uniones",
|
||||
"ui.keyboard_shortcuts": "Atajos de teclado",
|
||||
"ui.language": "Idioma",
|
||||
"ui.layout_compact": "Compacta",
|
||||
"ui.layout_floating": "Flotante",
|
||||
"ui.length": "Longitud",
|
||||
"ui.length_decimals": "Decimales de longitud",
|
||||
"ui.light": "Claro",
|
||||
"ui.line": "Línea",
|
||||
@@ -2391,6 +2580,7 @@
|
||||
"ui.loft": "Solevación",
|
||||
"ui.look_at": "Mirar hacia",
|
||||
"ui.look_at_anim_desc": "Vista de eje, Mirar hacia, Inicio y Ajustar",
|
||||
"ui.major_radius": "Radio mayor",
|
||||
"ui.manipulator_free_alt": "Arrastre libre del manipulador también con Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "Activado: Alt/Option o Ctrl/Cmd sobre las flechas de extrusión / manipulador desactiva el ajuste de valores. Desactivado: solo Ctrl/Cmd.",
|
||||
"ui.manufacture": "Fabricación",
|
||||
@@ -2420,8 +2610,10 @@
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "3MF con etiquetas de cara",
|
||||
"ui.mesh_format_stl": "STL",
|
||||
"ui.minor_radius": "Radio menor",
|
||||
"ui.mirror": "Simetría",
|
||||
"ui.mirror_line": "Línea de simetría",
|
||||
"ui.modifiers_either": "{first} o {second}",
|
||||
"ui.move": "Mover",
|
||||
"ui.multiple": "Múltiple",
|
||||
"ui.named_views": "Vistas guardadas",
|
||||
@@ -2520,6 +2712,7 @@
|
||||
"ui.reverse_horizontal_pan_direction": "Invertir dirección del desplazamiento horizontal",
|
||||
"ui.reverse_horizontal_pan_tip": "El desplazamiento horizontal con dos dedos y la rueda de pulgar desplazan en sentido contrario",
|
||||
"ui.reverse_zoom_direction": "Invertir dirección del zoom",
|
||||
"ui.revolutions": "Vueltas",
|
||||
"ui.revolve": "Revolución",
|
||||
"ui.ribbon_exit": "Salir",
|
||||
"ui.ribbon_labels": "Mostrar texto bajo los iconos de la cinta",
|
||||
@@ -2535,6 +2728,7 @@
|
||||
"ui.scale": "Escala",
|
||||
"ui.second_rail": "Segunda línea guía",
|
||||
"ui.section_analysis": "Análisis de sección",
|
||||
"ui.section_size": "Tamaño de la sección",
|
||||
"ui.select": "Seleccionar",
|
||||
"ui.selectOther": "Seleccionar otro…",
|
||||
"ui.selectTangentChain": "Seleccionar cadena tangente",
|
||||
@@ -2585,6 +2779,7 @@
|
||||
"ui.start": "Iniciar",
|
||||
"ui.start_over": "Volver a empezar",
|
||||
"ui.start_page": "Página de inicio",
|
||||
"ui.stitch_tolerance_hint": "estricta",
|
||||
"ui.stretch": "Estirar",
|
||||
"ui.supersampling": "Supermuestreo (SSAA)",
|
||||
"ui.supersampling_adaptive": "Solo cuando la vista está inmóvil",
|
||||
@@ -2615,6 +2810,8 @@
|
||||
"ui.to_object": "Hasta objeto",
|
||||
"ui.tool_direction": "Dirección de herramienta",
|
||||
"ui.top": "Superior",
|
||||
"ui.top_diameter": "Diámetro superior",
|
||||
"ui.top_radius": "Radio superior",
|
||||
"ui.torus": "Toro",
|
||||
"ui.total_angle": "Ángulo total",
|
||||
"ui.trackpad_pan_tip": "Los gestos del panel táctil desplazan la vista (desactivado: hacen zoom como la rueda); la rueda del ratón hace zoom, la rueda lateral desplaza horizontalmente y el gesto de pellizco siempre hace zoom",
|
||||
@@ -2640,6 +2837,7 @@
|
||||
"ui.visual_shaded_with_edges": "Sombreado con aristas",
|
||||
"ui.visual_style": "Estilo visual",
|
||||
"ui.visual_wireframe": "Estructura alámbrica",
|
||||
"ui.width": "Anchura",
|
||||
"ui.workspace": "espacio de trabajo",
|
||||
"ui.workspace_animation_description": "Vistas explosionadas, estudios de movimiento y guiones gráficos",
|
||||
"ui.workspace_available": "Disponible",
|
||||
@@ -2695,5 +2893,8 @@
|
||||
"update.up_to_date": "SolidCraft está actualizado.",
|
||||
"update.update_now": "Actualizar ahora",
|
||||
"update.you_have": "Tienes la versión {version}.",
|
||||
"use a positive nominal diameter and a pitch smaller than it": "usa un diámetro nominal positivo y un paso menor que él",
|
||||
"values must be finite numbers": "los valores deben ser números finitos",
|
||||
"values must be numbers or expressions": "los valores deben ser números o expresiones",
|
||||
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ elegir · Intro / Tab completar · Esc cerrar"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"(active rule)": "(règle active)",
|
||||
"0 turns it off": "0 le désactive",
|
||||
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Ouvrir le brouillon GitHub ci-dessous, puis coller le rapport copié dans le presse-papiers.",
|
||||
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Ouvrir le brouillon GitHub ci-dessous. Votre titre et votre rapport y seront déjà remplis.",
|
||||
@@ -35,6 +36,7 @@
|
||||
"All open designs": "Toutes les conceptions ouvertes",
|
||||
"Allowed expressions, separated by semicolons": "Expressions autorisées, séparées par des points-virgules",
|
||||
"Also deletes": "Supprime aussi",
|
||||
"Also place them from the origin (fully constrained)": "Les placer aussi depuis l'origine (entièrement contraintes)",
|
||||
"Anchor": "Ancrage",
|
||||
"Angle 2": "Angle 2",
|
||||
"Angle Display": "Affichage de l'angle",
|
||||
@@ -49,15 +51,19 @@
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Appliquez d’abord votre apparence. Exporte les apparences attribuées et les matériaux créés dans cette conception.",
|
||||
"Arc length": "Longueur d'arc",
|
||||
"Area": "Aire",
|
||||
"As Picked": "Tel que sélectionné",
|
||||
"As-Built": "Tel que construit",
|
||||
"Assign Rule": "Attribuer une règle",
|
||||
"Attachment": "Rattachement",
|
||||
"BLEND SURFACE": "SURFACE DE RACCORD",
|
||||
"BOUNDARY SURFACE": "SURFACE FRONTIÈRE",
|
||||
"BROWSER": "NAVIGATEUR",
|
||||
"Ball": "Rotule",
|
||||
"Bar": "Plat",
|
||||
"Base Height": "Hauteur de la base",
|
||||
"Bend": "Angle de pli",
|
||||
"Bend Edges": "Arêtes de pli",
|
||||
"Bend Line": "Ligne de pli",
|
||||
"Bend Line Position": "Position de la ligne de pli",
|
||||
"Bend Position": "Position du pli",
|
||||
"Bend Radius": "Rayon de pliage",
|
||||
@@ -69,6 +75,7 @@
|
||||
"Body (last if none)": "Corps (le dernier si aucun)",
|
||||
"Body to split": "Corps à scinder",
|
||||
"Body, or faces to take out": "Corps, ou faces à retirer",
|
||||
"Both": "Les deux",
|
||||
"Bottom Left": "Bas gauche",
|
||||
"Bottom Middle": "Bas centre",
|
||||
"Bottom Right": "Bas droite",
|
||||
@@ -100,6 +107,7 @@
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Choisir un nouveau plan. La géométrie et les cotes de l'esquisse restent inchangées dans ses coordonnées bidimensionnelles.",
|
||||
"Choose…": "Choisir…",
|
||||
"Circle center": "Centre du cercle",
|
||||
"Circle or sphere": "Cercle ou sphère",
|
||||
"Circular": "Circulaire",
|
||||
"Circumference": "Circonférence",
|
||||
"Clockwise": "Horaire",
|
||||
@@ -116,6 +124,7 @@
|
||||
"Config Table": "Table de config.",
|
||||
"Configuration": "Configuration",
|
||||
"Configuration rules": "Règles de configuration",
|
||||
"Constant radius": "Rayon constant",
|
||||
"Contact": "Contact",
|
||||
"Contact Sets": "Jeux de contacts",
|
||||
"Contact sets": "Jeux de contact",
|
||||
@@ -150,7 +159,9 @@
|
||||
"Custom": "Personnalisé",
|
||||
"Customize Toolbar": "Personnaliser la barre d'outils",
|
||||
"Customize…": "Personnaliser…",
|
||||
"Cylindrical": "Pivot glissant",
|
||||
"Cylindrical face": "Face cylindrique",
|
||||
"Deboss": "Embossage en creux",
|
||||
"Delete": "Supprimer",
|
||||
"Delete Selected Features…": "Supprimer les fonctions sélectionnées…",
|
||||
"Delete Selected Folders": "Supprimer les dossiers sélectionnés",
|
||||
@@ -165,6 +176,7 @@
|
||||
"Designs you open or save appear here.": "Les conceptions que vous ouvrez ou enregistrez apparaissent ici.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Déterminé par Jusqu'à l'objet. Choisir Angle pour modifier l'étendue angulaire.",
|
||||
"Diameter": "Diamètre",
|
||||
"Dimension new shapes": "Coter les nouvelles formes",
|
||||
"Direction": "Direction",
|
||||
"Discard": "Abandonner",
|
||||
"Distance": "Distance",
|
||||
@@ -176,6 +188,9 @@
|
||||
"Drag to move the hole; click to remove it": "Faire glisser pour déplacer le perçage ; cliquer pour le retirer",
|
||||
"Drive Joint": "Piloter la liaison",
|
||||
"Duration": "Durée",
|
||||
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Chaque nouvelle forme est aussi cotée depuis l'origine de l'esquisse jusqu'à ce que rien ne reste libre. Une forme déjà liée à l'origine ou à une géométrie placée n'en reçoit aucune.",
|
||||
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Chaque forme dessinée reçoit toutes ses cotes aux tailles dessinées (les valeurs saisies l'emportent). Une taille déjà fixée par l'esquisse est omise.",
|
||||
"Edge": "Arête",
|
||||
"Edges": "Arêtes",
|
||||
"Edit": "Modifier",
|
||||
"Edit Profile Sketch": "Modifier l'esquisse du profil",
|
||||
@@ -184,6 +199,7 @@
|
||||
"End Radius": "Rayon final",
|
||||
"End bulge": "Renflement de fin",
|
||||
"End continuity": "Continuité de fin",
|
||||
"Equal Distance": "Distance égale",
|
||||
"Expand": "Développer",
|
||||
"Expected result (optional)": "Résultat attendu (facultatif)",
|
||||
"Explode": "Éclater",
|
||||
@@ -211,6 +227,8 @@
|
||||
"Faces": "Faces",
|
||||
"Faces / edges / vertices": "Faces / arêtes / sommets",
|
||||
"Faces to split": "Faces à scinder",
|
||||
"Faces/Body": "Faces/Corps",
|
||||
"Factor must be a positive number": "Le facteur doit être un nombre strictement positif",
|
||||
"Fastener": "Fixation",
|
||||
"Favorites": "Favoris",
|
||||
"Favourite": "Favori",
|
||||
@@ -220,9 +238,12 @@
|
||||
"Filter…": "Filtrer…",
|
||||
"Find in Browser": "Trouver dans le navigateur",
|
||||
"Finish": "Terminer",
|
||||
"First edge": "Première arête",
|
||||
"First plane": "Premier plan",
|
||||
"First point": "Premier point",
|
||||
"Fit and G2 corners require Constant radius.": "L'ajustement et les angles G2 nécessitent un rayon constant.",
|
||||
"Fixed Face": "Face fixe",
|
||||
"Flange Gap": "Jeu de bord tombé",
|
||||
"Flange thickness": "Épaisseur d'aile",
|
||||
"Flanges": "Bords tombés",
|
||||
"Flat Pattern": "Dépliage",
|
||||
@@ -230,8 +251,10 @@
|
||||
"Follow camera (Look At sketches)": "Suivre la caméra (vue normale aux esquisses)",
|
||||
"Follow the camera (Look At sketches)": "Suivre la caméra (vue normale aux esquisses)",
|
||||
"Font": "Police",
|
||||
"Free Move": "Déplacement libre",
|
||||
"From": "De",
|
||||
"From Bodies": "À partir de corps",
|
||||
"From Snap": "Depuis l'accrochage",
|
||||
"From edges": "Arêtes de départ",
|
||||
"Full Length": "Longueur totale",
|
||||
"Full thread depth": "Profondeur de filetage complète",
|
||||
@@ -243,10 +266,13 @@
|
||||
"Halo under sketch curves": "Halo sous les courbes d'esquisse",
|
||||
"Height": "Hauteur",
|
||||
"Height From": "Hauteur depuis",
|
||||
"Hem Gap": "Jeu d'ourlet",
|
||||
"Hidden edges": "Arêtes cachées",
|
||||
"History layout": "Disposition de l’historique",
|
||||
"History names": "Noms dans l'historique",
|
||||
"Hole (optional)": "Perçage (facultatif)",
|
||||
"Hole Depth": "Profondeur du trou",
|
||||
"Hole Diameter": "Diamètre du trou",
|
||||
"Holes": "Trous",
|
||||
"Holes (edges on them)": "Trous (par leurs arêtes)",
|
||||
"Home view": "Vue d'accueil",
|
||||
@@ -269,12 +295,16 @@
|
||||
"Issue title": "Titre du ticket",
|
||||
"Items": "Éléments",
|
||||
"JOINTS": "LIAISONS",
|
||||
"Joint 1": "Liaison 1",
|
||||
"Joint 2": "Liaison 2",
|
||||
"Joint Origin1": "Origine de liaison1",
|
||||
"K Factor": "Facteur K",
|
||||
"Keep (points on it)": "À conserver (points dessus)",
|
||||
"Keep Tools": "Conserver les outils",
|
||||
"Keep of the other surface": "À conserver de l'autre surface",
|
||||
"Key a joint's value at this step": "Placer une image clé de la valeur d'une liaison à cette étape",
|
||||
"Keys": "Images clés",
|
||||
"Knife Edge": "Arête vive",
|
||||
"L-Angle": "Cornière",
|
||||
"LIBRARY": "BIBLIOTHÈQUE",
|
||||
"LINKS": "LIENS",
|
||||
@@ -287,9 +317,11 @@
|
||||
"Letter Spacing": "Espacement des lettres",
|
||||
"Library": "Bibliothèque",
|
||||
"Library file": "Fichier de bibliothèque",
|
||||
"Library response is missing": "La réponse de la bibliothèque est manquante",
|
||||
"Limits": "Limites",
|
||||
"Line Spacing": "Interligne",
|
||||
"Line Type": "Type de ligne",
|
||||
"Line on plane": "Ligne sur plan",
|
||||
"Lines": "Lignes",
|
||||
"Listing the installed fonts…": "Liste des polices installées…",
|
||||
"Load library": "Charger la bibliothèque",
|
||||
@@ -309,11 +341,14 @@
|
||||
"Material Outside": "Matière à l'extérieur",
|
||||
"Material copied. Existing body assignments are unchanged.": "Matériau copié. Les affectations existantes des corps sont inchangées.",
|
||||
"Max": "Max",
|
||||
"Max Thickness": "Épaisseur max.",
|
||||
"Maximum": "Maximum",
|
||||
"Mesh body": "Corps maillé",
|
||||
"Mid Radius": "Rayon intermédiaire",
|
||||
"Middle double-click": "Double-clic milieu",
|
||||
"Min": "Min",
|
||||
"Min Draft": "Dépouille min.",
|
||||
"Min Thickness": "Épaisseur min.",
|
||||
"Minimum": "Minimum",
|
||||
"Mirror Plane": "Plan de symétrie",
|
||||
"Mitre": "Onglet",
|
||||
@@ -333,6 +368,7 @@
|
||||
"Name": "Nom",
|
||||
"Name on import": "Nom à l'importation",
|
||||
"Nearest": "Corps le plus proche",
|
||||
"Neutral plane": "Plan neutre",
|
||||
"New": "Nouveau",
|
||||
"New Comp.": "Nouv. comp.",
|
||||
"New Sketch": "Nouvelle esquisse",
|
||||
@@ -343,6 +379,7 @@
|
||||
"No matching appearances": "Aucune apparence correspondante",
|
||||
"No matching physical materials.": "Aucun matériau physique correspondant.",
|
||||
"No visible bodies or sketches to export.": "Aucun corps ni esquisse visible à exporter.",
|
||||
"Nominal Radius": "Rayon nominal",
|
||||
"Nominal diameter": "Diamètre nominal",
|
||||
"None": "Aucun",
|
||||
"Not available yet": "Pas encore disponible",
|
||||
@@ -371,22 +408,28 @@
|
||||
"Overall length": "Longueur hors tout",
|
||||
"Param. Graph": "Graphe param.",
|
||||
"Parameter": "Paramètre",
|
||||
"Part length must be greater than zero and at most 2000 mm": "La longueur de la pièce doit être supérieure à zéro et d'au plus 2000 mm",
|
||||
"Parts": "Pièces",
|
||||
"Pass through": "Passer par",
|
||||
"Path": "Trajectoire",
|
||||
"Pattern": "Répétition",
|
||||
"Per Edge": "Par arête",
|
||||
"Physical material definitions": "Définitions de matériaux physiques",
|
||||
"Pick a part size": "Choisir une taille de pièce",
|
||||
"Pick the tool bodies": "Choisir les corps outils",
|
||||
"Picked": "Sélectionnés",
|
||||
"Picked point": "Point choisi",
|
||||
"Pin-slot": "Axe-rainure",
|
||||
"Pitch": "Pas",
|
||||
"Pivot": "Pivot",
|
||||
"Place at the origin instead": "Placer plutôt à l'origine",
|
||||
"Planar": "Plan",
|
||||
"Plastic Rules": "Règles plastique",
|
||||
"Play the joint's motion in the view": "Lire le mouvement de la liaison dans la vue",
|
||||
"Playback": "Lecture",
|
||||
"Plot": "Graphique",
|
||||
"Point on line": "Point sur ligne",
|
||||
"Point on plane": "Point sur plan",
|
||||
"Position": "Position",
|
||||
"Position (0 to 1)": "Position (0 à 1)",
|
||||
"Position (G0)": "Position (G0)",
|
||||
@@ -399,6 +442,7 @@
|
||||
"Profiles or curves": "Profils ou courbes",
|
||||
"Profiles/Curves": "Profils/Courbes",
|
||||
"Quantity": "Quantité",
|
||||
"Quantity must be at least 2": "La quantité doit être d'au moins 2",
|
||||
"RECENT": "RÉCENTS",
|
||||
"REFERENCE": "RÉFÉRENCE",
|
||||
"RULED SURFACE": "SURFACE RÉGLÉE",
|
||||
@@ -412,6 +456,8 @@
|
||||
"Rectangular": "Rectangulaire",
|
||||
"Rectangular Tube": "Tube rectangulaire",
|
||||
"Reference face": "Face de référence",
|
||||
"Relief Depth": "Profondeur de dégagement",
|
||||
"Relief Width": "Largeur de dégagement",
|
||||
"Remove Tools": "Retirer les outils",
|
||||
"Remove condition": "Supprimer la condition",
|
||||
"Remove from Folder": "Retirer du dossier",
|
||||
@@ -437,6 +483,7 @@
|
||||
"Restore this studio's tools": "Restaurer les outils de ce studio",
|
||||
"Result": "Résultat",
|
||||
"Retake screenshot": "Reprendre la capture d'écran",
|
||||
"Reveal Height": "Hauteur de décroché",
|
||||
"Reverse": "Inverser",
|
||||
"Reverse Direction": "Inverser la direction",
|
||||
"Revert": "Rétablir",
|
||||
@@ -445,12 +492,15 @@
|
||||
"Revert Position": "Rétablir la position",
|
||||
"Review the report and screenshot before opening GitHub.": "Relisez le rapport et la capture d'écran avant d'ouvrir GitHub.",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "Vérifiez votre rapport sur GitHub et cliquez sur Submit new issue pour le publier.",
|
||||
"Revolute": "Pivot",
|
||||
"Rib Length": "Longueur de nervure",
|
||||
"Rib Offset": "Décalage de nervure",
|
||||
"Rib Thickness": "Épaisseur de nervure",
|
||||
"Ribs": "Nervures",
|
||||
"Right Middle": "Milieu droite",
|
||||
"Rigid": "Rigide",
|
||||
"Rim Edge": "Arête du bord",
|
||||
"Rim face": "Face de bord",
|
||||
"Ring of tools on the right button": "Anneau d'outils sur le bouton droit",
|
||||
"Roll Back Here": "Revenir ici dans l'historique",
|
||||
"Roll History Marker Here": "Placer le marqueur d'historique ici",
|
||||
@@ -475,7 +525,9 @@
|
||||
"Search": "Rechercher",
|
||||
"Search commands": "Rechercher des commandes",
|
||||
"Search this library": "Rechercher dans cette bibliothèque",
|
||||
"Second edge": "Deuxième arête",
|
||||
"Second plane": "Second plan",
|
||||
"Second point": "Deuxième point",
|
||||
"Section": "Coupe",
|
||||
"Section (optional)": "Section (facultative)",
|
||||
"Section position": "Position de la section",
|
||||
@@ -485,6 +537,15 @@
|
||||
"Select bodies (a face picks its body)": "Sélectionner des corps (une face sélectionne son corps)",
|
||||
"Select bodies or faces instead": "Sélectionner plutôt des corps ou des faces",
|
||||
"Select edges": "Sélectionner des arêtes",
|
||||
"Select points of the sketch being edited": "Sélectionner des points de l'esquisse en cours de modification",
|
||||
"Select the centre the pattern turns around first": "Sélectionner d'abord le centre autour duquel la répétition pivote",
|
||||
"Select the curves to offset first": "Sélectionner d'abord les courbes à décaler",
|
||||
"Select the mirror line": "Sélectionner la ligne de symétrie",
|
||||
"Select the sketch curves or points to mirror first": "Sélectionner d'abord les courbes ou points d'esquisse à symétriser",
|
||||
"Select the sketch curves or points to move first": "Sélectionner d'abord les courbes ou points d'esquisse à déplacer",
|
||||
"Select the sketch curves or points to pattern first": "Sélectionner d'abord les courbes ou points d'esquisse à répéter",
|
||||
"Select the sketch curves or points to scale first": "Sélectionner d'abord les courbes ou points d'esquisse à mettre à l'échelle",
|
||||
"Select the sketch curves or points to transform first": "Sélectionner d'abord les courbes ou points d'esquisse à transformer",
|
||||
"Selection filter": "Filtre de sélection",
|
||||
"Semantic faces share their appearance across instances.": "Les faces sémantiques partagent leur apparence entre les instances.",
|
||||
"Send your report on GitHub": "Envoyer votre rapport sur GitHub",
|
||||
@@ -504,9 +565,12 @@
|
||||
"Sketch geometry and profiles": "Géométrie et profils d'esquisse",
|
||||
"Sketch plane has no body-face attachment.": "Le plan d'esquisse n'est rattaché à aucune face de corps.",
|
||||
"Sketch points": "Points d'esquisse",
|
||||
"Slider": "Glissière",
|
||||
"Slot width": "Largeur de rainure",
|
||||
"Snap to face and edge centres": "Accrocher aux centres des faces et des arêtes",
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft s'est fermé avant l'enregistrement de ces conceptions. Leur dernier enregistrement automatique peut être ouvert.",
|
||||
"Spacing 2 must be a non-zero number": "L'espacement 2 doit être un nombre non nul",
|
||||
"Spacing must be a non-zero number": "L'espacement doit être un nombre non nul",
|
||||
"Specialise": "Spécialiser",
|
||||
"Speed": "Vitesse",
|
||||
"Splitting tool": "Outil de scission",
|
||||
@@ -517,6 +581,7 @@
|
||||
"Start angle": "Angle de départ",
|
||||
"Start bulge": "Renflement de début",
|
||||
"Start continuity": "Continuité de début",
|
||||
"Stationary Side": "Côté fixe",
|
||||
"Step": "Étape",
|
||||
"Steps": "Étapes",
|
||||
"Steps to reproduce (optional)": "Étapes pour reproduire le problème (facultatives)",
|
||||
@@ -530,6 +595,7 @@
|
||||
"Suppressed": "Désactivé",
|
||||
"Surface": "Surface",
|
||||
"Surface, or faces to turn over": "Surface, ou faces à retourner",
|
||||
"Surfaces": "Surfaces",
|
||||
"Swap Ends": "Permuter les extrémités",
|
||||
"TRIM": "AJUSTER",
|
||||
"Tangent": "Tangence",
|
||||
@@ -540,24 +606,34 @@
|
||||
"Tap drill diameter": "Diamètre de l'avant-trou",
|
||||
"Target": "Cible",
|
||||
"Target Body": "Corps cible",
|
||||
"Target Face (optional)": "Face cible (facultatif)",
|
||||
"Tear": "Déchirure",
|
||||
"Technical details": "Détails techniques",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "L'esquisse en pointillés est un guide de plan enregistré. Sélectionnez une nouvelle face pour réparer le rattachement.",
|
||||
"The design changed. Reopen Change Part Size for the component.": "La conception a changé. Rouvrir Changer la taille de la pièce pour le composant.",
|
||||
"The direction must be an axis or a straight sketch curve that lies in the sketch plane": "La direction doit être un axe ou une courbe d'esquisse droite située dans le plan de l'esquisse",
|
||||
"The mirror line cannot also be mirrored": "La ligne de symétrie ne peut pas être elle-même symétrisée",
|
||||
"The mirror line must be a straight curve of this sketch, not an origin axis": "La ligne de symétrie doit être une courbe droite de cette esquisse, et non un axe de l'origine",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "L'occurrence d'auteur enregistrée est introuvable. Sélectionnez un placement d'auteur valide avant de relier.",
|
||||
"Then": "Alors",
|
||||
"These features will be deleted:": "Ces fonctions seront supprimées :",
|
||||
"These features will fail:": "Ces fonctions échoueront :",
|
||||
"Thickness Outside": "Épaisseur extérieure",
|
||||
"Thickness Variation": "Variation d'épaisseur",
|
||||
"Thin": "Mince",
|
||||
"Thin Extrude": "Extrusion mince",
|
||||
"Third plane": "Troisième plan",
|
||||
"Third point": "Troisième point",
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "Cet enfant est partagé. Le supprimer le retire de toutes les copies de son parent.",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "Cet enfant est partagé. Les changements de position s'appliquent à toutes les copies de son parent.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Cette face ne peut pas fournir de rattachement durable. Choisissez une autre face ou utilisez explicitement un plan fixe.",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Cette image n'est pas encore jointe sur GitHub. Copiez-la ou enregistrez-la, puis joignez-la sur GitHub.",
|
||||
"Thread depth": "Profondeur de filetage",
|
||||
"Thread diameter": "Diamètre de filetage",
|
||||
"Through point": "Par un point",
|
||||
"To": "À",
|
||||
"To Offset": "Jusqu'au décalage",
|
||||
"To Snap": "Vers l'accrochage",
|
||||
"To edges": "Arêtes d'arrivée",
|
||||
"Toe radius": "Rayon d'extrémité",
|
||||
"Tolerance": "Tolérance",
|
||||
@@ -571,6 +647,7 @@
|
||||
"Trimming tool (plane, face or curves)": "Outil d'ajustement (plan, face ou courbes)",
|
||||
"Turn off section analysis before exporting the view.": "Désactivez l'analyse de coupe avant d'exporter la vue.",
|
||||
"Type": "Type",
|
||||
"Type a distance or an angle to move by": "Saisir une distance ou un angle de déplacement",
|
||||
"UNTRIM": "ANNULER L'AJUSTEMENT",
|
||||
"Unit": "Unité",
|
||||
"Unsuppress": "Réactiver",
|
||||
@@ -614,6 +691,17 @@
|
||||
"Z angle": "Angle Z",
|
||||
"Z distance": "Distance Z",
|
||||
"Zebra": "Zébrures",
|
||||
"`joint` must be an id or a name": "`joint` doit être un identifiant ou un nom",
|
||||
"`params` must be an object": "`params` doit être un objet",
|
||||
"`values` must be a list": "`values` doit être une liste",
|
||||
"a chamfer needs a corner of exactly two lines": "un chanfrein nécessite un coin formé d'exactement deux lignes",
|
||||
"a file operation is in progress": "une opération sur un fichier est en cours",
|
||||
"a picked curve is no longer in a sketch": "une courbe sélectionnée ne se trouve plus dans une esquisse",
|
||||
"a picked curve's sketch has not been solved": "l'esquisse d'une courbe sélectionnée n'a pas été résolue",
|
||||
"a picked edge has no points": "une arête sélectionnée n'a aucun point",
|
||||
"a picked edge is gone": "une arête sélectionnée n'existe plus",
|
||||
"a picked edge's body is gone": "le corps d'une arête sélectionnée n'existe plus",
|
||||
"add a key for a joint first": "ajouter d'abord une clé pour une liaison",
|
||||
"ai.connected_toast": "SolidCraft est maintenant disponible dans {clients}. Redémarrez-les pour l'utiliser (Paramètres > Assistant).",
|
||||
"ai.prefs.allow_control": "Autoriser la connexion des agents IA (canal de contrôle / MCP)",
|
||||
"ai.prefs.allow_control_note": "Ouvre un canal de contrôle limité à cet ordinateur (127.0.0.1, port 27878 ou le suivant disponible). Un programme qui se connecte doit aussi présenter un jeton secret lisible uniquement par votre compte utilisateur, régénéré à chaque démarrage. Prend effet au prochain démarrage de SolidCraft. Désactivé : rien n'écoute, sauf si vous lancez SolidCraft avec --control.",
|
||||
@@ -650,6 +738,7 @@
|
||||
"appearance.pbr_opaque": "PBR opaque",
|
||||
"appearance.roughness": "Rugosité",
|
||||
"appearance.surface_response": "Réponse de la surface",
|
||||
"bom.group_children": "Sous-pièces dans la ligne de leur pièce",
|
||||
"browser.body_name": "Corps{number}",
|
||||
"browser.calibrate_title": "Calibrer : {name}",
|
||||
"browser.capture_many": "{count} composants ont été déplacés, mais leurs positions ne sont pas capturées.",
|
||||
@@ -660,6 +749,8 @@
|
||||
"browser.move_title": "Déplacer : {name}",
|
||||
"browser.parameters": "Paramètres ({count})",
|
||||
"browser.precision": "Précision : {precision}",
|
||||
"browser.sketch_attachment_lost": "{name} (attache perdue)",
|
||||
"browser.sketch_editing": "{name} (en cours de modification)",
|
||||
"browser.sketch_name": "Esquisse{number}",
|
||||
"browser.units": "Unités : {units}",
|
||||
"cam.flat": "Plateau",
|
||||
@@ -669,6 +760,11 @@
|
||||
"cam.roller": "Galet",
|
||||
"cam.roller_pick": "Galet (facultatif)",
|
||||
"cam.tip_pick": "Pointe (facultatif)",
|
||||
"choose a profile": "choisir un profilé",
|
||||
"choose the cam's and the follower's joints": "choisir les liaisons de la came et du suiveur",
|
||||
"choose the joints to couple": "choisir les liaisons à coupler",
|
||||
"click a plane or planar face": "cliquer sur un plan ou une face plane",
|
||||
"click sketch curves": "cliquer sur des courbes d'esquisse",
|
||||
"cmd.appearance_assign": "Apparence",
|
||||
"cmd.appearance_library": "Bibliothèque d'apparences",
|
||||
"cmd.appearance_list": "Lister les apparences",
|
||||
@@ -1139,6 +1235,8 @@
|
||||
"dxf.progress.write": "Écriture du DXF (5/5)",
|
||||
"dxf.tolerance": "Tolérance des courbes",
|
||||
"dxf.tolerance_help": "Corps visibles et esquisses affichées uniquement. Les courbes sont approchées par des segments de droite. Une tolérance plus faible donne des courbes plus fines, au prix de plus de temps et de mémoire.",
|
||||
"each rail's curves must be in one sketch": "les courbes de chaque rail de guidage doivent être dans une seule esquisse",
|
||||
"feature computation ended without a result": "le calcul de la fonction s'est terminé sans résultat",
|
||||
"file.open.drawing_filter": "Conceptions, fichiers CAO, maillages et dessins",
|
||||
"file_load.built": "Construit jusqu’ici : {built} entités sur {total}.",
|
||||
"file_load.converting": "Conversion du solide {done} sur {total}",
|
||||
@@ -1356,7 +1454,9 @@
|
||||
"nav.free_orbit.help": "La souris et la souris 3D tournent en passant par les deux pôles et conservent le roulis. L'alignement des axes avec Maj est désactivé.",
|
||||
"nav.free_orbit.label": "Orbite libre",
|
||||
"nav.fusion.help": "Glisser avec le milieu fait un panoramique, Maj+milieu fait orbiter, glisser avec le droit fait orbiter",
|
||||
"nav.hfss.help": "Alt+glisser gauche fait un panoramique, Maj+Alt+gauche zoome, Ctrl+glisser gauche sur un espace vide fait orbiter",
|
||||
"nav.inventor.help": "Glisser avec le milieu fait un panoramique, Maj+milieu fait orbiter, Ctrl+milieu zoome, glisser avec le droit fait orbiter",
|
||||
"nav.maya.help": "Alt+glisser gauche fait orbiter, Alt+milieu fait un panoramique, Alt+droit zoome ; glisser avec le milieu fait aussi un panoramique",
|
||||
"nav.openscad.help": "Glisser avec le gauche dans le vide fait orbiter, Maj+glisser gauche sélectionne par rectangle, glisser la géométrie d'esquisse la modifie ; glisser avec le droit ou le milieu fait un panoramique, Maj+droit ou Maj+milieu zoome",
|
||||
"nav.solidworks.help": "Glisser avec le milieu fait orbiter, Ctrl+milieu fait un panoramique, Maj+milieu zoome, glisser avec le droit fait orbiter",
|
||||
"nav.tinkercad.help": "Glisser avec le droit fait orbiter, glisser avec le milieu ou Maj+droit fait un panoramique",
|
||||
@@ -1372,9 +1472,34 @@
|
||||
"no hole": "sans perçage",
|
||||
"no interference": "aucune interférence",
|
||||
"no joint that moves yet": "aucune liaison mobile pour l'instant",
|
||||
"no motion study yet": "pas encore d'étude de mouvement",
|
||||
"no picture": "aucune image",
|
||||
"no such joint": "liaison introuvable",
|
||||
"none": "aucun",
|
||||
"not a move": "pas un déplacement",
|
||||
"not a number": "pas un nombre",
|
||||
"open a sketch first": "ouvrir d'abord une esquisse",
|
||||
"parts.automatic_name": "automatique",
|
||||
"parts.dimension.across_flats": "Sur plats",
|
||||
"parts.dimension.bore": "Alésage",
|
||||
"parts.dimension.d": "Diamètre nominal",
|
||||
"parts.dimension.head_diameter": "Diamètre de tête",
|
||||
"parts.dimension.head_height": "Hauteur de tête",
|
||||
"parts.dimension.height": "Hauteur",
|
||||
"parts.dimension.inner": "Diamètre intérieur",
|
||||
"parts.dimension.outer": "Diamètre extérieur",
|
||||
"parts.dimension.pitch": "Pas",
|
||||
"parts.dimension.socket": "Empreinte",
|
||||
"parts.dimension.socket_depth": "Profondeur empreinte",
|
||||
"parts.dimension.thickness": "Épaisseur",
|
||||
"parts.dimension.width": "Largeur",
|
||||
"parts.family.bearing": "Roulement à billes à gorge profonde (enveloppe)",
|
||||
"parts.family.dowel_pin": "Goupille cylindrique",
|
||||
"parts.family.hex_bolt": "Vis à tête hexagonale",
|
||||
"parts.family.hex_nut": "Écrou hexagonal",
|
||||
"parts.family.set_screw": "Vis sans tête à six pans creux, bout plat",
|
||||
"parts.family.socket_head_cap_screw": "Vis à tête cylindrique à six pans creux",
|
||||
"parts.family.washer": "Rondelle plate",
|
||||
"pattern.alignment_2": "Alignement 2",
|
||||
"pattern.alignment_hint": "La quantité inclut l’original. Le centrage répartit les copies de chaque côté ; si la quantité est paire, l’inversion choisit le côté qui reçoit la copie supplémentaire.",
|
||||
"pattern.direction_1": "Direction 1",
|
||||
@@ -1390,9 +1515,25 @@
|
||||
"perf.ui_time": "Temps de l'interface",
|
||||
"perf.viewport": "Vue 3D",
|
||||
"perf.worst": "pire",
|
||||
"pick a body or some of its faces": "choisir un corps ou certaines de ses faces",
|
||||
"pick a hole to seat it, or OK places it at the origin": "choisir un trou pour la loger, ou OK la place à l'origine",
|
||||
"pick a planar face": "choisir une face plane",
|
||||
"pick a plane or a face as the tool": "choisir un plan ou une face comme outil",
|
||||
"pick a size": "choisir une taille",
|
||||
"pick edges of a body": "choisir des arêtes d'un corps",
|
||||
"pick edges of one body": "choisir des arêtes d'un seul corps",
|
||||
"pick edges of the holes to remove": "choisir les arêtes des perçages à supprimer",
|
||||
"pick faces of one body": "choisir des faces d'un seul corps",
|
||||
"pick faces to offset": "choisir les faces à décaler",
|
||||
"pick lines of one sketch": "choisir des lignes d'une seule esquisse",
|
||||
"pick one or two vertices, edges or faces": "choisir un ou deux sommets, arêtes ou faces",
|
||||
"pick one plane or face as the tool, or sketch curves": "choisir un plan ou une face comme outil, ou des courbes d'esquisse",
|
||||
"pick sections in two or more sketches": "choisir des sections dans au moins deux esquisses",
|
||||
"pick sketch curves or edges": "choisir des courbes d'esquisse ou des arêtes",
|
||||
"pick the face to untrim": "choisir la face dont l'ajustement doit être annulé",
|
||||
"pick the path": "choisir la trajectoire",
|
||||
"pick what to keep": "choisir ce qu'il faut conserver",
|
||||
"pick what to keep of the other surface": "choisir ce qu'il faut conserver de l'autre surface",
|
||||
"prefs.add_item": "Ajouter l'élément",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option libère aussi les flèches du manipulateur",
|
||||
"prefs.anti_aliasing_amd_dx12": "L'anticrénelage est à 1× car la vérification au démarrage n'a rien dessiné avec le multiéchantillonnage et a réussi à 1×. Vous pouvez choisir un autre réglage ci-dessus ; redémarrez pour l'appliquer si la vérification réussit.",
|
||||
@@ -2156,6 +2297,26 @@
|
||||
"search.try": "Saisissez un nom de commande, une entité de cette conception ou un paramètre. Pour exécuter une commande avec des valeurs, saisissez son id et un objet JSON : {example}",
|
||||
"search.valid": "Valide",
|
||||
"section.off_hint": "Aucun plan choisi : OK désactive la coupe.",
|
||||
"select a direction (an axis, a sketch line or a straight edge) first": "sélectionner d'abord une direction (un axe, une ligne d'esquisse ou une arête droite)",
|
||||
"select a direction (an axis, a sketch line or a straight edge) for the profile to follow": "sélectionner une direction (un axe, une ligne d'esquisse ou une arête droite) que le profil doit suivre",
|
||||
"select a point on a component": "sélectionner un point sur un composant",
|
||||
"select a profile, edges or sketch lines first": "sélectionner d'abord un profil, des arêtes ou des lignes d'esquisse",
|
||||
"select an axis": "sélectionner un axe",
|
||||
"select an axis, a straight or circular edge, or a cylinder, cone or torus face first": "sélectionner d'abord un axe, une arête droite ou circulaire, ou une face cylindrique, conique ou torique",
|
||||
"select bodies first": "sélectionner d'abord des corps",
|
||||
"select corner points, or the two lines of one corner": "sélectionner des points de coin, ou les deux lignes d'un coin",
|
||||
"select one corner point, or the two lines of one corner": "sélectionner un point de coin, ou les deux lignes d'un coin",
|
||||
"select sheet edges first": "sélectionner d'abord des arêtes de tôle",
|
||||
"select the bend line (a sketch line) first": "sélectionner d'abord la ligne de pli (une ligne d'esquisse)",
|
||||
"select the cam's profile (a sketch profile or the cam's edges)": "sélectionner le profil de la came (un profil d'esquisse ou les arêtes de la came)",
|
||||
"select the corner to cut first": "sélectionner d'abord le coin à chanfreiner",
|
||||
"select the corners to round first": "sélectionner d'abord les coins à arrondir",
|
||||
"select the fixed face first (a plate's large face)": "sélectionner d'abord la face fixe (la grande face d'une plaque)",
|
||||
"select the frame sketch's lines first": "sélectionner d'abord les lignes de l'esquisse de la structure",
|
||||
"select the path (sketch lines across the edge)": "sélectionner la trajectoire (lignes d'esquisse qui traversent l'arête)",
|
||||
"select the rim face first": "sélectionner d'abord la face du bord",
|
||||
"select the sheet edge first": "sélectionner d'abord l'arête de la tôle",
|
||||
"select two components": "sélectionner deux composants",
|
||||
"set by the sketch's lines": "défini par les lignes de l'esquisse",
|
||||
"settings.display_quality_note": "Les niveaux plus fins affichent plus lisses les faces et arêtes courbes, mais leur maillage prend plus de temps. Seule la vue change, jamais la conception ni les exports.",
|
||||
"settings.frame_rate_details_note": "Ajoute un graphe des temps des dernières images avec les triangles dessinés superposés, un histogramme des temps d'image et ce que le GPU a dessiné. Cliquez sur la fréquence d'images dans la vue pour les afficher ou les masquer.",
|
||||
@@ -2173,17 +2334,23 @@
|
||||
"settings.window_system_details": "Détails",
|
||||
"settings.window_system_note_now": "S'applique après un redémarrage. Actuellement : {now}.",
|
||||
"settings.window_system_wayland_limited": "Certaines fonctionnalités sont limitées sous Wayland natif.",
|
||||
"sheet.flange_type_base": "Base",
|
||||
"sheet.flange_type_contour": "Contour",
|
||||
"sheet.flange_type_edge": "Arête",
|
||||
"sheet.no_body": "il n'y a pas de corps de tôlerie : commencez-en un avec Bord tombé > Base, ou transformez une plaque en tôlerie avec Convertir en tôlerie (choisissez sa grande face plane)",
|
||||
"shortcut.json_error": "les paramètres doivent être au format JSON : {error}",
|
||||
"shortcut.no_command": "la commande `{id}` n’existe pas ; le texte avant le JSON doit être un ID de commande",
|
||||
"shortcut.pin": "Épingler en haut de la boîte S",
|
||||
"shortcut.unpin": "Désépingler",
|
||||
"sketch is missing": "l'esquisse est manquante",
|
||||
"sketch.degrees_of_freedom": "{count} degrés de liberté",
|
||||
"sketch.dof_left": "{count} degrés de liberté restants",
|
||||
"sketch.dof_left_one": "{count} degré de liberté restant",
|
||||
"sketch.draw_curves": "Dessiner en {linetype}",
|
||||
"sketch.make_selection": "Convertir la sélection en {linetype}",
|
||||
"sketch.open_ends": "{count} ouvertes",
|
||||
"sketch.status_conflict": "contraintes en conflit",
|
||||
"sketch.status_fully_constrained": "entièrement contrainte",
|
||||
"sketch.untied_ends": "{count} sans coïncidence",
|
||||
"sketch_panel.attached": "Attaché à {body} / {path}",
|
||||
"sketch_panel.author": "Placement d’origine : {path}",
|
||||
@@ -2210,7 +2377,24 @@
|
||||
"sketch_text.path_hint": "Une courbe de l'esquisse que le texte suit : cliquer ici, puis sur la courbe",
|
||||
"sketch_text.use_font_file": "Utiliser le fichier de police {path}",
|
||||
"solid": "plein (sans paroi)",
|
||||
"the angle must be a number": "l'angle doit être un nombre",
|
||||
"the curves must be in one sketch": "les courbes doivent être dans une seule esquisse",
|
||||
"the direction must be an axis, a sketch line or a straight edge": "la direction doit être un axe, une ligne d'esquisse ou une arête droite",
|
||||
"the duration must be a number of seconds, 0.001 or more": "la durée doit être un nombre de secondes, d'au moins 0.001",
|
||||
"the line's sketch is gone": "l'esquisse de la ligne n'existe plus",
|
||||
"the lines must be in one sketch": "les lignes doivent être dans une seule esquisse",
|
||||
"the lines must be in one sketch, not the section's": "les lignes doivent être dans une seule esquisse, pas celle de la section",
|
||||
"the parts library is empty": "la bibliothèque de pièces est vide",
|
||||
"the parts library is not available": "la bibliothèque de pièces n'est pas disponible",
|
||||
"the path curves must be in one other sketch": "les courbes de la trajectoire doivent être dans une seule autre esquisse",
|
||||
"the path's sketch is gone": "l'esquisse de la trajectoire n'existe plus",
|
||||
"the position must be a number": "la position doit être un nombre",
|
||||
"the preview failed": "l'aperçu a échoué",
|
||||
"the profiles and curves must be in one sketch": "les profils et les courbes doivent être dans une seule esquisse",
|
||||
"the reference face must be on the edges' body": "la face de référence doit appartenir au corps des arêtes",
|
||||
"the scale must be above 0": "l'échelle doit être supérieure à 0",
|
||||
"the section's profiles must be in one sketch": "les profils de la section doivent être dans une seule esquisse",
|
||||
"the two origins are on the same component": "les deux origines se trouvent sur le même composant",
|
||||
"thread.automatic_size": "Taille automatique",
|
||||
"thread.designation": "Désignation",
|
||||
"thread.iso_metric": "Métrique ISO",
|
||||
@@ -2238,6 +2422,7 @@
|
||||
"trace.points": "{count} points le long de la trajectoire",
|
||||
"trace.stopped_by_contact": "Se termine à l'échantillon {sample} : {a} traverserait {b}",
|
||||
"trace.stopped_by_loop": "Se termine à l'échantillon {sample} : la boucle passant par {joint} ne peut plus se fermer",
|
||||
"trim by sketch curves, or by one plane or face": "ajuster avec des courbes d'esquisse, ou avec un plan ou une face",
|
||||
"ui.1_degree_of_freedom": "1 degré de liberté",
|
||||
"ui.3d_mouse_axes_reassigned": "Les axes sont réattribués dans les Préférences, et cela s'applique toujours en plus de ce calibrage.",
|
||||
"ui.3d_mouse_button": "Bouton {number}",
|
||||
@@ -2692,6 +2877,8 @@
|
||||
"ui.design_properties.revision": "Indice",
|
||||
"ui.diameter": "Diamètre",
|
||||
"ui.dimension": "Cote",
|
||||
"ui.direct_interaction": "Interaction directe",
|
||||
"ui.direct_interaction_note": "Travailler directement sur le modèle dans la vue. Appuyer sur une face plane ou un profil et tirer : vers le haut extrude, vers le bas dans un corps découpe, et un mouvement latéral règle le pas",
|
||||
"ui.direction": "Direction",
|
||||
"ui.display_quality": "Qualité d'affichage",
|
||||
"ui.display_quality_coarse": "Grossière",
|
||||
@@ -2715,6 +2902,7 @@
|
||||
"ui.editJointLimits": "Modifier les limites de liaison",
|
||||
"ui.edit_section_analysis": "Modifier l'analyse de coupe",
|
||||
"ui.ellipse": "Ellipse",
|
||||
"ui.emboss_wrap_note": "Les profils de lignes orthogonales tangents à un cylindre pris en charge s'enroulent automatiquement. Choisir une paroi pour définir une cible.",
|
||||
"ui.end_radius": "Rayon final",
|
||||
"ui.end_scale": "Échelle finale",
|
||||
"ui.entities": "Entités",
|
||||
@@ -2786,6 +2974,7 @@
|
||||
"ui.heading_extrude": "EXTRUSION",
|
||||
"ui.heading_fillet": "CONGÉ",
|
||||
"ui.heading_finish_sketch": "TERMINER L'ESQUISSE",
|
||||
"ui.heading_form": "FORME",
|
||||
"ui.heading_front": "AVANT",
|
||||
"ui.heading_hole": "PERÇAGE",
|
||||
"ui.heading_home": "ACCUEIL",
|
||||
@@ -2834,6 +3023,7 @@
|
||||
"ui.heading_utilities": "UTILITAIRES",
|
||||
"ui.heading_utility": "UTILITAIRE",
|
||||
"ui.heading_weldments": "MÉCANO-SOUDURE",
|
||||
"ui.height": "Hauteur",
|
||||
"ui.help": "Aide",
|
||||
"ui.hide": "Masquer",
|
||||
"ui.hideAxes": "Masquer les axes",
|
||||
@@ -2870,11 +3060,14 @@
|
||||
"ui.intersect": "Intersection",
|
||||
"ui.isolate": "Isoler",
|
||||
"ui.join": "Joindre",
|
||||
"ui.joint_play": "▶ Lire",
|
||||
"ui.joint_stop": "■ Arrêter",
|
||||
"ui.joints": "Liaisons",
|
||||
"ui.keyboard_shortcuts": "Raccourcis clavier",
|
||||
"ui.language": "Langue",
|
||||
"ui.layout_compact": "Compacte",
|
||||
"ui.layout_floating": "Flottante",
|
||||
"ui.length": "Longueur",
|
||||
"ui.length_decimals": "Décimales des longueurs",
|
||||
"ui.light": "Clair",
|
||||
"ui.line": "Ligne",
|
||||
@@ -2886,6 +3079,7 @@
|
||||
"ui.loft": "Lissage",
|
||||
"ui.look_at": "Vue normale",
|
||||
"ui.look_at_anim_desc": "Vue d'axe, Vue normale, Accueil et Cadrer",
|
||||
"ui.major_radius": "Grand rayon",
|
||||
"ui.manipulator_free_alt": "Glissement libre du manipulateur aussi avec Alt/Option",
|
||||
"ui.manipulator_free_alt_tip": "Activé : Alt/Option ou Ctrl/Cmd sur les flèches d’extrusion / du manipulateur désactive l’accrochage aux valeurs. Désactivé : Ctrl/Cmd uniquement.",
|
||||
"ui.manufacture": "Fabrication",
|
||||
@@ -2915,8 +3109,10 @@
|
||||
"ui.mesh_format_3mf": "3MF",
|
||||
"ui.mesh_format_3mf_tagged": "3MF avec étiquettes de faces",
|
||||
"ui.mesh_format_stl": "STL",
|
||||
"ui.minor_radius": "Petit rayon",
|
||||
"ui.mirror": "Symétrie",
|
||||
"ui.mirror_line": "Ligne de symétrie",
|
||||
"ui.modifiers_either": "{first} ou {second}",
|
||||
"ui.move": "Déplacer",
|
||||
"ui.multiple": "Multiple",
|
||||
"ui.named_views": "Vues nommées",
|
||||
@@ -2968,6 +3164,7 @@
|
||||
"ui.pinToShortcuts": "Épingler aux raccourcis",
|
||||
"ui.pinToToolbar": "Épingler à la barre d'outils",
|
||||
"ui.pipe": "Tuyau",
|
||||
"ui.pitch": "Pas",
|
||||
"ui.placement": "Placement",
|
||||
"ui.plane": "Plan",
|
||||
"ui.plane_at_angle": "Plan incliné",
|
||||
@@ -3027,6 +3224,7 @@
|
||||
"ui.reverse_horizontal_pan_direction": "Inverser le sens du panoramique horizontal",
|
||||
"ui.reverse_horizontal_pan_tip": "Le défilement horizontal à deux doigts et la molette latérale font le panoramique dans l'autre sens",
|
||||
"ui.reverse_zoom_direction": "Inverser le sens du zoom",
|
||||
"ui.revolutions": "Tours",
|
||||
"ui.revolve": "Révolution",
|
||||
"ui.ribbon_exit": "Quitter",
|
||||
"ui.ribbon_labels": "Afficher le texte sous les icônes du ruban",
|
||||
@@ -3042,6 +3240,7 @@
|
||||
"ui.scale": "Échelle",
|
||||
"ui.second_rail": "Second rail",
|
||||
"ui.section_analysis": "Analyse de coupe",
|
||||
"ui.section_size": "Taille de la section",
|
||||
"ui.select": "Sélectionner",
|
||||
"ui.selectOther": "Sélectionner un autre…",
|
||||
"ui.selectTangentChain": "Sélectionner la chaîne tangente",
|
||||
@@ -3102,6 +3301,7 @@
|
||||
"ui.start_page": "Page d’accueil",
|
||||
"ui.step_values": "Incrémenter les valeurs avec les flèches et la molette",
|
||||
"ui.step_values_tip": "Dans un champ de valeur en cours de saisie, Haut/Bas modifient un nombre simple de 1 (Maj : 10, Alt : 0,1), de même que la molette de la souris au-dessus du champ. Les expressions restent telles que saisies.",
|
||||
"ui.stitch_tolerance_hint": "serrée",
|
||||
"ui.stretch": "Étirer",
|
||||
"ui.supersampling": "Suréchantillonnage (SSAA)",
|
||||
"ui.supersampling_adaptive": "Seulement quand la vue est immobile",
|
||||
@@ -3132,6 +3332,8 @@
|
||||
"ui.to_object": "Jusqu'à l'objet",
|
||||
"ui.tool_direction": "Direction de l'outil",
|
||||
"ui.top": "Dessus",
|
||||
"ui.top_diameter": "Diamètre supérieur",
|
||||
"ui.top_radius": "Rayon supérieur",
|
||||
"ui.torus": "Tore",
|
||||
"ui.total_angle": "Angle total",
|
||||
"ui.trackpad_pan_tip": "Les gestes du pavé tactile font un panoramique (désactivé : ils zooment comme la molette) ; la molette zoome, la molette latérale fait un panoramique horizontal, le pincement zoome toujours",
|
||||
@@ -3160,6 +3362,7 @@
|
||||
"ui.visual_shaded_with_edges": "Ombré avec arêtes",
|
||||
"ui.visual_style": "Style visuel",
|
||||
"ui.visual_wireframe": "Filaire",
|
||||
"ui.width": "Largeur",
|
||||
"ui.window_system": "Système de fenêtrage",
|
||||
"ui.window_system_auto": "Automatique",
|
||||
"ui.window_system_wayland": "Wayland",
|
||||
@@ -3220,6 +3423,9 @@
|
||||
"update.up_to_date": "SolidCraft est à jour.",
|
||||
"update.update_now": "Mettre à jour maintenant",
|
||||
"update.you_have": "Vous avez la version {version}.",
|
||||
"use a positive nominal diameter and a pitch smaller than it": "utiliser un diamètre nominal positif et un pas inférieur à ce diamètre",
|
||||
"values must be finite numbers": "les valeurs doivent être des nombres finis",
|
||||
"values must be numbers or expressions": "les valeurs doivent être des nombres ou des expressions",
|
||||
"web.files.insert_drawings_hint": "Fichiers CAO, maillages, DXF ou SVG dans la conception ouverte",
|
||||
"web.files.insert_stored_drawings_hint": "Dans la conception ouverte (fichiers CAO, maillages, DXF, SVG)",
|
||||
"web.open_full": "Ouvrir dans SolidCraft",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"All open designs": "Wszystkie otwarte projekty",
|
||||
"Allowed expressions, separated by semicolons": "Dozwolone wyrażenia, rozdzielone średnikami",
|
||||
"Also deletes": "Usuwa również",
|
||||
"Also place them from the origin (fully constrained)": "Umieść je też względem początku (w pełni związane)",
|
||||
"Angle 2": "Kąt 2",
|
||||
"Animate": "Animuj",
|
||||
"App/system information and command settings are included. Review private details before sharing.": "Dołączone są informacje o aplikacji i systemie oraz ustawienia poleceń. Przed udostępnieniem sprawdź, czy nie ma tam prywatnych danych.",
|
||||
@@ -118,6 +119,7 @@
|
||||
"Describe what happened before continuing.": "Zanim przejdziesz dalej, opisz, co się stało.",
|
||||
"Designs you open or save appear here.": "Tutaj pojawiają się otwierane lub zapisywane projekty.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Wynika z opcji Do obiektu. Wybierz Kąt, aby edytować zakres kątowy.",
|
||||
"Dimension new shapes": "Wymiaruj nowe kształty",
|
||||
"Direction": "Kierunek",
|
||||
"Discard": "Odrzuć",
|
||||
"Do not export hidden edges": "Nie eksportuj krawędzi niewidocznych",
|
||||
@@ -131,6 +133,8 @@
|
||||
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Przeciągnij, aby obrócić podparcie co 5°. Przytrzymaj Alt lub Ctrl, aby obracać swobodnie.",
|
||||
"Drive Joint": "Steruj złączem",
|
||||
"Duration": "Czas trwania",
|
||||
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Każdy nowy kształt otrzymuje też wymiary od początku szkicu, aż nic nie pozostanie swobodne. Kształt już połączony z początkiem lub z umieszczoną geometrią nie otrzymuje żadnych.",
|
||||
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Każdy narysowany kształt otrzymuje wszystkie swoje wymiary w narysowanych rozmiarach (wpisane wartości mają pierwszeństwo). Rozmiar, który szkic już określa, jest pomijany.",
|
||||
"Edges": "Krawędzie",
|
||||
"Edit": "Edytuj",
|
||||
"Edit Profile Sketch": "Edytuj szkic profilu",
|
||||
@@ -1057,6 +1061,11 @@
|
||||
"no picture": "brak obrazu",
|
||||
"none": "brak",
|
||||
"parts.automatic_name": "automatycznie",
|
||||
"parts.dimension.d": "Średnica nominalna",
|
||||
"parts.dimension.height": "Wysokość",
|
||||
"parts.dimension.pitch": "Skok",
|
||||
"parts.dimension.thickness": "Grubość",
|
||||
"parts.dimension.width": "Szerokość",
|
||||
"pattern.alignment_2": "Wyrównanie 2",
|
||||
"pattern.alignment_hint": "Liczba obejmuje oryginał. Wyśrodkuj rozkłada kopie po obu stronach; Odwróć wybiera stronę z dodatkową kopią przy parzystej liczbie.",
|
||||
"pattern.direction_1": "Kierunek 1",
|
||||
@@ -1893,6 +1902,7 @@
|
||||
"ui.heading_utilities": "NARZĘDZIA",
|
||||
"ui.heading_utility": "NARZĘDZIE",
|
||||
"ui.heading_weldments": "SPAWANIE",
|
||||
"ui.height": "Wysokość",
|
||||
"ui.help": "Pomoc",
|
||||
"ui.hide": "Ukryj",
|
||||
"ui.hideAxes": "Ukryj osie",
|
||||
@@ -1931,6 +1941,7 @@
|
||||
"ui.language": "Język",
|
||||
"ui.layout_compact": "Kompaktowy",
|
||||
"ui.layout_floating": "Pływający",
|
||||
"ui.length": "Długość",
|
||||
"ui.length_decimals": "Miejsca dziesiętne długości",
|
||||
"ui.light": "Jasny",
|
||||
"ui.line": "Linia",
|
||||
@@ -2019,6 +2030,7 @@
|
||||
"ui.pinToShortcuts": "Przypnij do skrótów",
|
||||
"ui.pinToToolbar": "Przypnij do paska narzędzi",
|
||||
"ui.pipe": "Rura",
|
||||
"ui.pitch": "Skok",
|
||||
"ui.placement": "Umiejscowienie",
|
||||
"ui.placement_handles": "Uchwyty umieszczania",
|
||||
"ui.placement_handles_tip": "Uchwyty na podglądzie podparcia, występu lub zatrzasku: przeciągnij znacznik, aby przesunąć element po modelu, obróć go pierścieniem",
|
||||
@@ -2194,6 +2206,7 @@
|
||||
"ui.visual_shaded_with_edges": "Cieniowany z krawędziami",
|
||||
"ui.visual_style": "Styl wizualny",
|
||||
"ui.visual_wireframe": "Model krawędziowy",
|
||||
"ui.width": "Szerokość",
|
||||
"ui.workspace": "środowisko pracy",
|
||||
"ui.workspace_animation_description": "Widoki rozstrzelone, badania ruchu i scenorysy",
|
||||
"ui.workspace_available": "Dostępne",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"All open designs": "Todos os projetos abertos",
|
||||
"Allowed expressions, separated by semicolons": "Expressões permitidas, separadas por ponto e vírgula",
|
||||
"Also deletes": "Também exclui",
|
||||
"Also place them from the origin (fully constrained)": "Posicioná-las também a partir da origem (totalmente restritas)",
|
||||
"Angle 2": "Ângulo 2",
|
||||
"Animate": "Animar",
|
||||
"App/system information and command settings are included. Review private details before sharing.": "Informações do aplicativo/sistema e configurações estão incluídas. Revise detalhes privados antes de compartilhar.",
|
||||
@@ -145,6 +146,7 @@
|
||||
"Designs you open or save appear here.": "Os projetos que você abrir ou salvar aparecem aqui.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Determinado por Até o objeto. Escolha Ângulo para editar a extensão angular.",
|
||||
"Diameter": "Diâmetro",
|
||||
"Dimension new shapes": "Cotar novas formas",
|
||||
"Direction": "Direção",
|
||||
"Discard": "Descartar",
|
||||
"Distance": "Distância",
|
||||
@@ -155,6 +157,8 @@
|
||||
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Clique duplo com o botão do meio na vista: Início (a vista inicial, enquadrando o que é mostrado), Ajustar (enquadra o que é mostrado, mantendo a orientação) ou nada",
|
||||
"Drive Joint": "Acionar junta",
|
||||
"Duration": "Duração",
|
||||
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Cada nova forma também recebe cotas a partir da origem do esboço até nada ficar livre. Uma forma já ligada à origem ou a uma geometria posicionada não recebe nenhuma.",
|
||||
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Cada forma que você desenha recebe todas as suas cotas nos tamanhos desenhados (valores digitados têm prioridade). Um tamanho que o esboço já define é omitido.",
|
||||
"Edges": "Arestas",
|
||||
"Edit": "Editar",
|
||||
"Edit Profile Sketch": "Editar esboço do perfil",
|
||||
@@ -1159,7 +1163,9 @@
|
||||
"nav.free_orbit.label": "Órbita livre",
|
||||
"nav.free_orbit.snap_help": "Mouse e mouse 3D giram através dos dois polos e preservam a rotação da imagem. Mantenha Shift pressionado durante uma órbita por arraste para alinhar a direção da vista e a rotação da imagem.",
|
||||
"nav.fusion.help": "Arrastar com o meio desloca, Shift+meio orbita, arrastar com o direito orbita",
|
||||
"nav.hfss.help": "Alt+arrastar com o esquerdo desloca, Shift+Alt+esquerdo aplica zoom, Ctrl+arrastar com o esquerdo em área vazia orbita",
|
||||
"nav.inventor.help": "Arrastar com o meio desloca, Shift+meio orbita, Ctrl+meio aplica zoom, arrastar com o direito orbita",
|
||||
"nav.maya.help": "Alt+arrastar com o esquerdo orbita, Alt+meio desloca, Alt+direito aplica zoom; arrastar com o meio também desloca",
|
||||
"nav.openscad.help": "Arrastar com o esquerdo no espaço vazio orbita, Shift+arrastar com o esquerdo seleciona por caixa, arrastar a geometria do esboço a edita; arrastar com o direito ou o meio desloca, Shift+arrastar com o direito ou o meio aplica zoom",
|
||||
"nav.solidworks.help": "Arrastar com o meio orbita, Ctrl+meio desloca, Shift+meio aplica zoom, arrastar com o direito orbita",
|
||||
"nav.tinkercad.help": "Arrastar com o direito orbita, arrastar com o meio ou Shift+direito desloca",
|
||||
@@ -1178,6 +1184,11 @@
|
||||
"no picture": "sem imagem",
|
||||
"none": "nenhum",
|
||||
"parts.automatic_name": "automático",
|
||||
"parts.dimension.d": "Diâmetro nominal",
|
||||
"parts.dimension.height": "Altura",
|
||||
"parts.dimension.pitch": "Passo",
|
||||
"parts.dimension.thickness": "Espessura",
|
||||
"parts.dimension.width": "Largura",
|
||||
"pattern.alignment_2": "Alinhamento 2",
|
||||
"pattern.alignment_hint": "As contagens incluem o original. Centralizar equilibra as cópias; Inverter escolhe o lado extra para totais pares.",
|
||||
"pattern.direction_1": "Direção 1",
|
||||
@@ -2023,6 +2034,7 @@
|
||||
"ui.heading_utilities": "UTILITÁRIOS",
|
||||
"ui.heading_utility": "UTILITÁRIO",
|
||||
"ui.heading_weldments": "SOLDAGEM",
|
||||
"ui.height": "Altura",
|
||||
"ui.help": "Ajuda",
|
||||
"ui.hide": "Ocultar",
|
||||
"ui.hideAxes": "Ocultar eixos",
|
||||
@@ -2061,6 +2073,7 @@
|
||||
"ui.language": "Idioma",
|
||||
"ui.layout_compact": "Compacto",
|
||||
"ui.layout_floating": "Flutuante",
|
||||
"ui.length": "Comprimento",
|
||||
"ui.length_decimals": "Casas decimais de comprimento",
|
||||
"ui.light": "Claro",
|
||||
"ui.line": "Linha",
|
||||
@@ -2148,6 +2161,7 @@
|
||||
"ui.pinToShortcuts": "Fixar nos atalhos",
|
||||
"ui.pinToToolbar": "Fixar na barra de ferramentas",
|
||||
"ui.pipe": "Tubo",
|
||||
"ui.pitch": "Passo",
|
||||
"ui.placement": "Posicionamento",
|
||||
"ui.plane": "Plano",
|
||||
"ui.plane_at_angle": "Plano em ângulo",
|
||||
@@ -2322,6 +2336,7 @@
|
||||
"ui.visual_shaded_with_edges": "Sombreado com arestas",
|
||||
"ui.visual_style": "Estilo visual",
|
||||
"ui.visual_wireframe": "Aramado",
|
||||
"ui.width": "Largura",
|
||||
"ui.workspace": "espaço de trabalho",
|
||||
"ui.workspace_animation_description": "Vistas explodidas, estudos de movimento e storyboards",
|
||||
"ui.workspace_available": "Disponível",
|
||||
|
||||
@@ -22,10 +22,12 @@
|
||||
"Add Mid Radius": "Ara yarıçap ekle",
|
||||
"Add Row": "Satır ekle",
|
||||
"Add a tool": "Araç ekle",
|
||||
"Add all bodies": "Tüm gövdeleri ekle",
|
||||
"Add bend": "Büküm ekle",
|
||||
"Add condition": "Koşul ekle",
|
||||
"Add or edit rule": "Kural ekle veya düzenle",
|
||||
"Add to New Folder": "Yeni Klasöre Ekle",
|
||||
"Add visible bodies": "Görünür gövdeleri ekle",
|
||||
"Advanced": "Gelişmiş",
|
||||
"Against": "Karşı",
|
||||
"Alignment": "Hizalama",
|
||||
@@ -35,6 +37,7 @@
|
||||
"All open designs": "Tüm açık tasarımlar",
|
||||
"Allowed expressions, separated by semicolons": "Noktalı virgülle ayrılmış izin verilen ifadeler",
|
||||
"Also deletes": "Bunları da siler",
|
||||
"Also place them from the origin (fully constrained)": "Orijinden de konumlandır (tam kısıtlı)",
|
||||
"Anchor": "Çapa",
|
||||
"Angle 2": "Açı 2",
|
||||
"Angle Display": "Açı görünümü",
|
||||
@@ -56,6 +59,7 @@
|
||||
"BOUNDARY SURFACE": "SINIR YÜZEYİ",
|
||||
"BROWSER": "TARAYICI",
|
||||
"Bar": "Çubuk",
|
||||
"Base Center": "Taban Merkezi",
|
||||
"Bend": "Büküm",
|
||||
"Bend Edges": "Büküm kenarları",
|
||||
"Bend Line Position": "Büküm çizgisi konumu",
|
||||
@@ -140,6 +144,7 @@
|
||||
"Create Issue — review draft": "Sorun oluştur — taslağı gözden geçirin",
|
||||
"Create Issue…": "Sorun Oluştur…",
|
||||
"Cross curves": "Çapraz eğriler",
|
||||
"Ctrl+Enter continues on GitHub.": "Ctrl+Enter ile GitHub'da devam edilir.",
|
||||
"Cube": "Küp",
|
||||
"Current orthographic view (1:1 mm)": "Geçerli ortografik görünüm (1:1 mm)",
|
||||
"Current perspective view": "Geçerli perspektif görünüm",
|
||||
@@ -152,6 +157,7 @@
|
||||
"Customize…": "Özelleştir…",
|
||||
"Cylindrical face": "Silindirik yüz",
|
||||
"Delete": "Sil",
|
||||
"Delete Folders and Features…": "Klasörleri ve Özellikleri Sil…",
|
||||
"Delete Selected Features…": "Seçili Özellikleri Sil…",
|
||||
"Delete Selected Folders": "Seçili Klasörleri Sil",
|
||||
"Delete all": "Tümünü sil",
|
||||
@@ -165,6 +171,7 @@
|
||||
"Designs you open or save appear here.": "Açtığınız veya kaydettiğiniz tasarımlar burada görünür.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Nesneye kadar seçeneğiyle belirlenir. Açısal kapsamı düzenlemek için Açı seçeneğini seçin.",
|
||||
"Diameter": "Çap",
|
||||
"Dimension new shapes": "Yeni şekillere ölçü ver",
|
||||
"Direction": "Yön",
|
||||
"Discard": "At",
|
||||
"Distance": "Mesafe",
|
||||
@@ -181,6 +188,8 @@
|
||||
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Dayamayı 5° adımlarla çevirmek için sürükleyin. Serbestçe çevirmek için Alt veya Ctrl tuşunu basılı tutun.",
|
||||
"Drive Joint": "Bağlantıyı tahrik et",
|
||||
"Duration": "Süre",
|
||||
"Each new shape is also dimensioned from the sketch origin until nothing of it is left free. A shape already attached to the origin or to placed geometry gets none.": "Her yeni şekle, serbest bir şey kalmayana kadar çizim orijininden de ölçü verilir. Orijine veya yerleştirilmiş geometriye zaten bağlı bir şekle verilmez.",
|
||||
"Each shape you draw gets all its dimensions, at the sizes you drew (typed values win). A size the sketch already sets is left out.": "Çizdiğiniz her şekil, çizdiğiniz boyutlarda tüm ölçülerini alır (yazılan değerler önceliklidir). Çizimin zaten belirlediği bir boyut atlanır.",
|
||||
"Edges": "Kenarlar",
|
||||
"Edit": "Düzenle",
|
||||
"Edit Profile Sketch": "Profil eskizini düzenle",
|
||||
@@ -430,6 +439,7 @@
|
||||
"Remove this point": "Bu noktayı kaldır",
|
||||
"Rename Feature": "Özelliği Yeniden Adlandır",
|
||||
"Rename Folder": "Klasörü Yeniden Adlandır",
|
||||
"Render Stage": "Render Platosu",
|
||||
"Reopen designs": "Tasarımları yeniden aç",
|
||||
"Reopen the designs that were open when SolidCraft was last closed": "SolidCraft son kapatıldığında açık olan tasarımları yeniden aç",
|
||||
"Replace existing library file": "Mevcut kitaplık dosyasının üzerine yaz",
|
||||
@@ -534,6 +544,7 @@
|
||||
"Style": "Stil",
|
||||
"Summary": "Özet",
|
||||
"Suppress": "Bastır",
|
||||
"Suppress Selected Folders": "Seçili Klasörleri Bastır",
|
||||
"Suppressed": "Bastırılmış",
|
||||
"Surface": "Yüzey",
|
||||
"Surface, or faces to turn over": "Yüzey veya çevrilecek yüzler",
|
||||
@@ -581,6 +592,7 @@
|
||||
"UNTRIM": "KIRPMAYI KALDIR",
|
||||
"Unit": "Birim",
|
||||
"Unsuppress": "Bastırmayı kaldır",
|
||||
"Unsuppress Selected Folders": "Seçili Klasörlerin Bastırmasını Kaldır",
|
||||
"Updating the preview": "Önizleme güncelleniyor",
|
||||
"Use active author placement": "Etkin kaynak yerleşimini kullan",
|
||||
"Use fixed plane": "Sabit düzlem kullan",
|
||||
@@ -628,6 +640,8 @@
|
||||
"ai.prefs.background_commands": "Ajan komutlarını arka planda çalıştır",
|
||||
"ai.prefs.connect_apps": "SolidCraft'ı bu bilgisayardaki AI uygulamalarına aç",
|
||||
"ai.prefs.connect_apps_note": "SolidCraft başladığında, bu bilgisayarda bulduğu AI uygulamalarına (OpenCode, Claude Desktop, Claude Code, Cursor, Codex, Gemini CLI, Copilot CLI, Windsurf, Zed, VS Code) onu bir MCP sunucusu olarak ekler; böylece bir ajan kurulum yapmadan kullanabilir. Yalnızca solidcraft girdisi eklenir veya yolu yenilenir; değiştirdiğiniz bir girdiye dokunulmaz ve dosya önce yedeklenir. Kapalı: eklediği girdiler kaldırılır. Yardım > AI asistanlarını bağla… bunu elle yapar.",
|
||||
"ai.prefs.connect_at_start": "SolidCraft başladığında en son kullanılan ajana bağlan",
|
||||
"ai.prefs.connect_at_start_note": "SolidCraft her başladığında, en son bağlandığınız ajanı (veya varsayılanı) arka planda başlatır; böylece sohbete hazır olur. SolidCraft --control ile başlatıldığında çalışmaz. Bağlanamazsa Asistan, Bağlantı başarısız mesajını ve bir Yeniden dene düğmesini gösterir.",
|
||||
"ai.prefs.follow_camera": "Kamerayı eskizlere çevir",
|
||||
"ai.prefs.follow_camera_off": "Otomatik kamera takibi kapalı.",
|
||||
"ai.prefs.follow_camera_on": "Kamera, ajanın oluşturduğu eskizleri görecek şekilde döner.",
|
||||
@@ -635,6 +649,8 @@
|
||||
"ai.prefs.make_default": "Varsayılan olarak kullan",
|
||||
"ai.prefs.no_agents": "Henüz ajan yok. Ajan ekleyin veya yüklü ajanları bulmak için yenileyin.",
|
||||
"ai.prefs.no_saved_approvals": "Kaydedilmiş onay yok.",
|
||||
"ai.prefs.open_at_start": "SolidCraft başladığında Asistan panelini aç",
|
||||
"ai.prefs.open_at_start_note": "SolidCraft her başladığında Asistan panelini (veya kendi penceresini) gösterir. SolidCraft --control ile başlatıldığında gösterilmez.",
|
||||
"ai.prefs.override_all_warning": "Bu ajan, sormadan tasarımı kaydedebilir, üzerine yazabilir, dışa aktarabilir veya değiştirebilir.",
|
||||
"ai.prefs.permission_all": "Her şeye izin ver",
|
||||
"ai.prefs.permission_all_summary": "Kullanılabilir SolidCraft araçları sormadan çalışır.",
|
||||
@@ -679,6 +695,9 @@
|
||||
"cmd.appearance_assign": "Görünüm",
|
||||
"cmd.appearance_library": "Görünüm kitaplığı",
|
||||
"cmd.appearance_list": "Görünümleri listele",
|
||||
"cmd.appearance_paint": "Görünümü Boya",
|
||||
"cmd.appearance_pick_up": "Görünümü Al",
|
||||
"cmd.appearance_replace": "Görünümü Değiştir",
|
||||
"cmd.assembly_bom": "Malzeme listesi",
|
||||
"cmd.body_rename": "Gövdeyi yeniden adlandır",
|
||||
"cmd.browser_collapse_all": "Tümünü Daralt",
|
||||
@@ -766,6 +785,7 @@
|
||||
"cmd.document_revision": "Tasarım Revizyonu",
|
||||
"cmd.document_units": "Tasarımın birimlerini değiştir",
|
||||
"cmd.edit_checkpoint": "Kontrol noktası",
|
||||
"cmd.edit_repeat": "Son Komutu Tekrarla",
|
||||
"cmd.edit_restore_checkpoint": "Kontrol noktasını geri yükle",
|
||||
"cmd.engine_commands": "Komutları listele",
|
||||
"cmd.engine_describe": "Komutu açıkla",
|
||||
@@ -879,9 +899,11 @@
|
||||
"cmd.plastic_snap_fit": "Geçmeli bağlantı",
|
||||
"cmd.render_aging_describe": "Katmanlı Yüzey Yaşlanması",
|
||||
"cmd.render_backends": "Render arka uçları",
|
||||
"cmd.render_batch": "Tüm Çekimleri Render Et",
|
||||
"cmd.render_camera_fit": "Render Konusunu Çerçevele",
|
||||
"cmd.render_camera_get": "Render Kamerasını İncele",
|
||||
"cmd.render_cancel": "Render'ı iptal et",
|
||||
"cmd.render_compare": "Görüntüleri Karşılaştır",
|
||||
"cmd.render_effects_describe": "Mercek ve Yaratıcı Efektler",
|
||||
"cmd.render_export": "PNG dışa aktar",
|
||||
"cmd.render_hdr_inspect": "HDR Kaynağını İncele",
|
||||
@@ -897,6 +919,7 @@
|
||||
"cmd.render_library_surface_prepare": "Oluşturulmuş Yüzey Haritalarını Hazırla",
|
||||
"cmd.render_library_surface_unbind": "Yüzey Bağlamını Kaldır",
|
||||
"cmd.render_library_targets": "Render Kitaplık Hedeflerini Bul",
|
||||
"cmd.render_lights_solo": "Işığı Solo Yap",
|
||||
"cmd.render_optics_bind": "Optik Bağlamı Kaydet",
|
||||
"cmd.render_optics_unbind": "Optik Bağlamı Kaldır",
|
||||
"cmd.render_preflight": "Render isteğini kontrol et",
|
||||
@@ -1006,7 +1029,9 @@
|
||||
"cmd.sketch_spline_control_point": "Kontrol noktalı spline",
|
||||
"cmd.sketch_spline_control_point_5": "Kontrol noktalı spline (derece 5)",
|
||||
"cmd.sketch_spline_fit_point": "Uydurma noktalı spline",
|
||||
"cmd.sketch_spline_insert_point": "Spline Noktası Ekle",
|
||||
"cmd.sketch_spun_profile": "Döndürülmüş profil",
|
||||
"cmd.sketch_toggle_diameter": "Yarıçap / Çap Arasında Geçiş Yap",
|
||||
"cmd.sketch_toggle_driven": "Türetilmiş ölçüyü aç/kapat",
|
||||
"cmd.sketch_transform": "Dönüşüm",
|
||||
"cmd.sketch_trim_path": "Yol boyunca buda",
|
||||
@@ -1072,6 +1097,7 @@
|
||||
"cmd.view_rename": "Adlandırılmış görünümü yeniden adlandır",
|
||||
"cmd.view_right": "Sağ Görünüm",
|
||||
"cmd.view_save": "Yeni adlandırılmış görünüm",
|
||||
"cmd.view_shots_strip": "Çekim Şeridi",
|
||||
"cmd.view_spin": "Sürekli döndürme",
|
||||
"cmd.view_tool": "Gezinme aracı",
|
||||
"cmd.view_top": "Üst Görünüm",
|
||||
@@ -1145,6 +1171,7 @@
|
||||
"document.save_changes": "{name} içindeki değişiklikler kaydedilsin mi?",
|
||||
"document.save_copy": "Bir Kopya Kaydet…",
|
||||
"document.unsaved_changes": "{name} kaydedilmemiş değişiklikler içeriyor. Kaydetmezseniz bunlar kaybolur.",
|
||||
"drive.rigid": "{joint} rijit bir bağlantıdır: parçalarını sabit tutar, bu yüzden tahrik edilecek bir şey yoktur. Hareket etmelerini sağlamak için türünü Bağlantıyı düzenle içinde değiştirin: dönmek için Döner, kaymak için Kayar, ikisi için Silindirik.",
|
||||
"drive.stopped_by_contact": "{value} değerinde durdu: {a} ile {b} iç içe geçerdi",
|
||||
"drive.stopped_by_loop": "{value} değerinde durdu: {joint} üzerinden geçen döngü daha fazla kapatılamıyor",
|
||||
"drive.unconnected": "Hiçbir şey hareket etmiyor: {part} sabitlenmiş hiçbir parçaya bağlı değil",
|
||||
@@ -1180,28 +1207,44 @@
|
||||
"from the profile": "profilden",
|
||||
"from the rule": "kuraldan",
|
||||
"help.apache": "Apache lisansı 2.0",
|
||||
"help.appearance.assign": "Seçili gövdelere, yüzlere veya bileşenlere ağırlıklarını değiştirmeden bir renk veya malzeme görünümü verir. Parçaları ayırt etmek veya bir kaplamayı önizlemek için kullanın.",
|
||||
"help.attributions": "Atıflar (ATTRIBUTION.md)",
|
||||
"help.build": "Sürüm {version} · commit {commit} · derleme tarihi {date}",
|
||||
"help.connect_note": "SolidCraft sunucusunu yüklemek için her asistanı yeniden başlatın. Değişmeden önce her dosya .bak olarak yedeklendi.",
|
||||
"help.connect_title": "AI asistanlarını bağla",
|
||||
"help.face.delete": "Seçili yüzleri kaldırır ve komşu yüzlerin boşluğu kapatmasını sağlar ya da boşluğu yüzey olarak açık bırakır. Yuvarlatılmış bir kenarı, bir deliği veya yolda duran bir çıkıntıyı gidermek için kullanın.",
|
||||
"help.font_details": "Arayüz, egui'nin yerleşik yazı tiplerini kullanır (Ubuntu Font Licence ve MIT/Bitstream Vera lisansları altında Ubuntu Light ve Hack; OFL ve MIT altında Noto Emoji ve emoji simge yazı tipi). Eskiz metni Liberation Sans (SIL OFL 1.1) kullanır. The Crafting Apps'in isteğe bağlı yazı tipi paketi craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), SolidCraft derlemelerine dahil değildir.",
|
||||
"help.fonts": "Yazı tipleri",
|
||||
"help.licences": "Lisanslar",
|
||||
"help.material.assign": "Seçili gövdelerin neyden yapıldığını belirler: çelik, alüminyum veya ABS gibi. Kütle ve ağırlığın gerçek parçayla uyuşması için kullanın.",
|
||||
"help.mit": "MIT lisansı",
|
||||
"help.notice": "NOTICE",
|
||||
"help.parameters.change": "Tasarımın ölçülerini yöneten adlandırılmış değerlerin ve ifadelerin tablosunu açar. Bir ölçüyü tek yerden değiştirip onu kullanan her özelliğin buna uymasını sağlamak için kullanın.",
|
||||
"help.solid.align": "Seçili gövdeleri, üzerlerindeki bir nokta, yüz veya düzlem başka bir yerdeki nokta, yüz veya düzlemle buluşacak şekilde taşır. Ofseti ölçmeden bir parçayı diğerinin yanına yerleştirmek için kullanın.",
|
||||
"help.solid.chamfer": "Seçili kenarları, belirlediğiniz mesafede düz bir pahla keser. Keskin kenarları kırmak veya bir pim ya da vidanın girişini kolaylaştırmak için kullanın.",
|
||||
"help.solid.combine": "Gövdeleri birleştirir, bir hedef gövdeden çıkarır veya yalnızca paylaştıkları hacmi tutar ve araç gövdelerini tutabilir. Parçaları kaynaştırmak, bir şekli diğeriyle kesmek veya iki gövdenin örtüştüğü yeri bulmak için kullanın.",
|
||||
"help.solid.draft": "Seçili yüzleri, nötr bir düzlemden ölçülen belirlediğiniz açı kadar eğerek koniklik verir. Kalıplanan veya dökülen parçaların kalıptan kolayca çıkması için kullanın.",
|
||||
"help.solid.extrude": "Bir eskiz profilini, malzeme ekleyerek veya kaldırarak bir mesafe boyunca 3B'ye taşır. Düz bir konturu blok, kabarıklık veya cep haline getirmek için kullanın.",
|
||||
"help.solid.fillet": "Seçili kenarları, belirlediğiniz yarıçapta pürüzsüz dairesel bir uyumla yuvarlatır. Keskin kenarları sağlamlık, konfor veya görünüm için yumuşatmak amacıyla kullanın.",
|
||||
"help.solid.hole": "Bir yüzeyde seçtiğiniz noktada, belirlediğiniz çapta yuvarlak bir delik deler. Vida boşlukları, pimleri ve diğer basit yuvarlak açıklıklar için kullanın.",
|
||||
"help.solid.loft": "İki veya daha fazla eskiz profili arasında, ya da bir profille bir nokta arasında pürüzsüz bir katı karıştırır. Geçişler, huniler, saplar ve uzunluğu boyunca kesiti değişen şekiller için kullanın.",
|
||||
"help.solid.mirror": "Seçili gövdelerin veya özelliklerin bir düzlem boyunca ayna görüntüsünü kopyasını oluşturur ve orijinalle birleştirebilir. Simetrik parçalar için kullanın; yarısını modelleyip ikisini de alın.",
|
||||
"help.solid.move": "Seçili gövdeleri ya da kopyalarını, belirlediğiniz mesafe ve açıyla taşır veya döndürür. Bir gövdenin yerini değiştirmek ya da başka bir yerde ikincisini oluşturmak için kullanın.",
|
||||
"help.solid.offset_face": "Seçili yüzleri belirlediğiniz mesafe kadar içeri veya dışarı iter, çevrelerindeki yüzler de buna uyar. Eskizi düzenlemeden bir duvarı kalınlaştırmak, bir cebi derinleştirmek veya bir çıkıntıyı yükseltmek için kullanın.",
|
||||
"help.solid.pattern.circular": "Seçili nesneleri, belirlediğiniz adet ve toplam açı boyunca bir eksen etrafında yineler. Cıvata daireleri, jant kolları, kanatçıklar ve dişler için kullanın.",
|
||||
"help.solid.pattern.rectangular": "Seçili nesneleri, belirlediğiniz adet ve aralıkla bir veya iki yönde yineler. Delik, nervür, pim veya ayak dizileri ve ızgaraları için kullanın.",
|
||||
"help.solid.press_pull": "Bir yüzü sürüklerken malzeme ekler veya kaldırır, kenarları sürüklerken yuvarlatır. Doğrudan model üzerinde hızlı değişiklikler için kullanın.",
|
||||
"help.solid.remove": "Seçili gövdeleri tasarımdan, zaman çizelgesindeki bu noktadan itibaren çıkarır; önceki adımlar onları korur. Artık gerekmeyen bir kesici araç gibi yardımcı bir gövdeyi atmak için kullanın.",
|
||||
"help.solid.replace_face": "Seçili düz yüzleri bir düzleme veya paralel bir yüze taşır, gövde de onunla buluşmak için büyür veya küçülür. Bir yüzü başka bir parçayla ya da bir yapı düzlemiyle aynı hizaya getirmek için kullanın.",
|
||||
"help.solid.revolve": "Bir eskiz profilini bir eksen etrafında döndürerek yuvarlak bir katı oluşturur. Miller, topuzlar, kaseler ve bir merkez çizgisi etrafında yuvarlak olan parçalar için kullanın.",
|
||||
"help.solid.scale": "Seçili gövdeleri bir çarpanla büyütür veya küçültür; her yönde aynı ya da eksen başına ayrı olabilir. İçe aktarılan bir şekli yeniden boyutlandırmak veya büzülme payı bırakmak için kullanın.",
|
||||
"help.solid.shell": "Bir katıyı içini oyarak boşaltır, belirlediğiniz kalınlıkta duvarlar bırakır ve seçtiğiniz yüzleri açabilir. Kutular, kapaklar ve diğer ince cidarlı parçalar için kullanın.",
|
||||
"help.solid.split_body": "Bir gövdeyi bir düzlemle, başka bir gövdenin yüzüyle veya bir eskiz çizgisiyle ayrı gövdelere keser. Ayrı ayrı basılan, işlenen veya renklendirilen parçalar elde etmek için kullanın.",
|
||||
"help.solid.split_face": "Seçili yüzleri, gövdeyi kesmeden bir düzlem, başka bir gövdenin yüzü veya eskiz eğrileri boyunca böler. Bir yüzün bir kısmına kendi koniklik, renk veya ofsetini vermek için kullanın.",
|
||||
"help.solid.sweep": "Bir eskiz profilini bir yol boyunca sürükleyerek katı oluşturur. Borular, raylar, saplar ve bir eğriyi izleyen sabit kesitli parçalar için kullanın.",
|
||||
"help.third_party": "Üçüncü taraf lisansları",
|
||||
"help.timeline.compute_all": "Zaman çizelgesindeki her özelliği baştan yeniden oluşturur. Modeli tazelemek ve hangi özelliklerin başarısız olduğunu görmek için kullanın.",
|
||||
"help.timeline.delete": "Seçili özellikleri zaman çizelgesinden siler; bir eskizi silmek ondan oluşturulan özellikleri de siler. Artık istenmeyen bir adımı kaldırmak için kullanın.",
|
||||
"hole_presets.confirm_delete": "Silmeyi onayla",
|
||||
"hole_presets.delete": "Sil",
|
||||
"hole_presets.delete_prompt": "Bu kişisel ön ayar silinsin mi? Mevcut delikler değişmez.",
|
||||
@@ -1286,6 +1329,9 @@
|
||||
"install.steps_intro": "Safari web uygulamalarını kendi menüleri üzerinden yükler:",
|
||||
"install.title": "SolidCraft'ı yükle",
|
||||
"issue.body_label": "Rapor metni",
|
||||
"issue.note_changed_while_waiting": "Önizleme beklenirken ayarlar veya model değişti. Teknik ayrıntıları geçerli görünüme göre güncellemek için ekran görüntüsünü yeniden alın.",
|
||||
"issue.note_details_kept": "Ekran görüntüsü yerel olarak alındı. Düzenlediğiniz teknik ayrıntılar korundu: hâlâ geçerli görünümle uyuştuklarını kontrol edin.",
|
||||
"issue.note_details_updated": "Teknik ayrıntılar geçerli görünümle uyuşacak şekilde güncellendi. Ekran görüntüsü yerel olarak alındı. GitHub'a eklemeden önce gözden geçirin.",
|
||||
"issue.note_image_copied": "Görseli GitHub taslağına yapıştırın; görsel kopyalama kullanılamıyorsa PNG'yi kaydedin.",
|
||||
"issue.note_opened": "GitHub taslağını gözden geçirin ve ekran görüntüsünü ekleyin. GitHub'daki Submit new issue düğmesi raporu yayımlar.",
|
||||
"issue.note_opened_copied": "Rapor kopyalandı. GitHub taslağına yapıştırın, ekran görüntüsünü ekleyin, ardından GitHub'da Submit new issue düğmesine tıklayın.",
|
||||
@@ -1352,6 +1398,8 @@
|
||||
"material.editor.unknown_card": "Mühendislik özellikleri bilinmiyor. Kaydetme yalnızca atanan yoğunluğu ve mevcut görünümü koruyabilir.",
|
||||
"material.editor.user_supplied": "Kullanıcı tarafından sağlanan",
|
||||
"material.editor.wood": "Ahşap",
|
||||
"material.selection.choose": "Bir malzeme seçin",
|
||||
"material.selection.mixed": "Farklı malzemeler",
|
||||
"measure.at": "Konum",
|
||||
"measure.item": "Öğe {number}",
|
||||
"messages.ago_h": "{n} sa. önce",
|
||||
@@ -1381,8 +1429,12 @@
|
||||
"nav.free_orbit.help": "Fare ve 3B fare, her iki kutuptan geçerek döndürür ve yuvarlanmayı korur. Shift eksen hizalama devre dışı.",
|
||||
"nav.free_orbit.label": "Serbest döndürme",
|
||||
"nav.free_orbit.snap_help": "Fare ve 3B fare, her iki kutuptan geçerek döndürür ve yuvarlanmayı korur. Görünüm yönünü ve yuvarlanmayı hizalamak için döndürme sürüklemesi sırasında Shift tuşunu basılı tutun.",
|
||||
"nav.free_orbit_upright.help": "Fareyle yana sürüklemek modeli dünyanın dikey ekseni etrafında döndürür; yukarı ve aşağı sürüklemek tepe noktasını da aşarak sınırsız eğim verir.",
|
||||
"nav.free_orbit_upright.label": "Dikey eksen etrafında döndür",
|
||||
"nav.fusion.help": "Orta sürükleme kaydırır, Shift+orta döndürür, sağ sürükleme döndürür",
|
||||
"nav.hfss.help": "Alt+sol sürükleme kaydırır, Shift+Alt+sol yakınlaştırır, boş alanda Ctrl+sol sürükleme döndürür",
|
||||
"nav.inventor.help": "Orta sürükleme kaydırır, Shift+orta döndürür, Ctrl+orta yakınlaştırır, sağ sürükleme döndürür",
|
||||
"nav.maya.help": "Alt+sol sürükleme döndürür, Alt+orta kaydırır, Alt+sağ yakınlaştırır; orta sürükleme ayrıca kaydırır",
|
||||
"nav.openscad.help": "Boşlukta sol sürükleme döndürür, Shift+sol sürükleme kutu seçimi yapar, eskiz geometrisi sürükleyerek düzenlenir; sağ veya orta sürükleme kaydırır, Shift+sağ veya Shift+orta sürükleme yakınlaştırır",
|
||||
"nav.perspective.sketch": "Eskiz düzenleme dik görünüm kullanır. Perspektife geçmek için eskizi bitirin.",
|
||||
"nav.solidworks.help": "Orta sürükleme döndürür, Ctrl+orta kaydırır, Shift+orta yakınlaştırır, sağ sürükleme döndürür",
|
||||
@@ -1393,6 +1445,93 @@
|
||||
"nav.touchpad.pan": "Kaydır (Shift tuşunu basılı tutup hareket ettirin; fare tuşu olmadan)",
|
||||
"nav.touchpad.scroll": "Dokunmatik yüzey yakınlaştırmak için dikey kaydırmayı kullanır. İki parmakla kaydırma diğer ön ayarlar için kayıtlı kalır.",
|
||||
"nav.touchpad.zoom": "Yakınlaştır (dikey kaydırın veya parmaklarınızı açıp kapatın)",
|
||||
"navbar.agent_chat": "Asistan sohbet paneli",
|
||||
"navbar.agent_cursor": "Ajan imlecini göster",
|
||||
"navbar.ambient_occlusion": "Ortam oklüzyonu",
|
||||
"navbar.assistant": "Asistan",
|
||||
"navbar.custom.add_slot": "{slot} ekle",
|
||||
"navbar.custom.always": "Her zaman görünür",
|
||||
"navbar.custom.available": "{available} · çubuğa sürükleyin veya Ekle'ye basın",
|
||||
"navbar.custom.buttons": "Düğmeler",
|
||||
"navbar.custom.compact": "Kompakt",
|
||||
"navbar.custom.count": "16 yuvadan {count} tanesi",
|
||||
"navbar.custom.customise": "Gezinme çubuğunu özelleştir…",
|
||||
"navbar.custom.design": "Tasarım",
|
||||
"navbar.custom.duplicate": "Bu yuva zaten çubukta.",
|
||||
"navbar.custom.fade": "Soldur",
|
||||
"navbar.custom.full": "Başka bir yuva eklemek için bir yuvayı kaldırın.",
|
||||
"navbar.custom.hidden_design": "Tasarımda gizli",
|
||||
"navbar.custom.hidden_render": "Render'da gizli",
|
||||
"navbar.custom.hide": "Gezinme çubuğunu gizle",
|
||||
"navbar.custom.hide_idle": "Üzerine gelinceye kadar gizle",
|
||||
"navbar.custom.icon_only": "Yalnızca simge",
|
||||
"navbar.custom.icon_value": "Simge ve değer",
|
||||
"navbar.custom.icons": "Simgeler",
|
||||
"navbar.custom.idle": "Boştayken",
|
||||
"navbar.custom.intro": "3B görünümün altındaki çubukta hangi denetimlerin ve hangi sırayla olacağını seçin. Bir yuvayı taşımak için sürükleyin, kaldırmak için çubuğun dışına sürükleyin.",
|
||||
"navbar.custom.labels": "Simgeler ve etiketler",
|
||||
"navbar.custom.left": "Sola taşı",
|
||||
"navbar.custom.position": "Konum",
|
||||
"navbar.custom.position_centre": "Orta",
|
||||
"navbar.custom.position_left": "Sol",
|
||||
"navbar.custom.position_right": "Sağ",
|
||||
"navbar.custom.preview_as": "Şu şekilde önizle",
|
||||
"navbar.custom.regular": "Normal",
|
||||
"navbar.custom.remove": "Kaldır",
|
||||
"navbar.custom.remove_bar": "Çubuktan kaldır",
|
||||
"navbar.custom.render": "Render",
|
||||
"navbar.custom.reset": "Gezinme çubuğunu sıfırla",
|
||||
"navbar.custom.reset_default": "Varsayılana sıfırla",
|
||||
"navbar.custom.right": "Sağa taşı",
|
||||
"navbar.custom.select": "Yuva seçilmedi. Taşımak, kaldırmak veya görünümünü değiştirmek için çubukta bir yuva seçin.",
|
||||
"navbar.custom.separators_full": "Çubukta zaten dört ayırıcı var.",
|
||||
"navbar.custom.show": "Gezinme çubuğunu göster",
|
||||
"navbar.custom.shows": "Gösterim",
|
||||
"navbar.custom.size": "Boyut",
|
||||
"navbar.custom.stale": "Çubuk değişti. Yuvayı yeniden seçin.",
|
||||
"navbar.custom.title": "Gezinme çubuğu",
|
||||
"navbar.custom.unknown": "Bu yuva kullanılamıyor.",
|
||||
"navbar.description.ambient_occlusion": "Derinlik katmak için kıvrımları ve köşeleri koyulaştırır.",
|
||||
"navbar.description.display": "Görünüm ayarlarını barındırır: grafik hazır ayarı, ızgara, zemin gölgesi, ortam oklüzyonu ve projeksiyon.",
|
||||
"navbar.description.fit": "Seçimi çerçeveler; hiçbir şey seçili değilse tüm modeli.",
|
||||
"navbar.description.grid": "Zemin ızgarasını gösterir veya gizler.",
|
||||
"navbar.description.ground_shadow": "Modelin altındaki gölgeyi gösterir veya gizler.",
|
||||
"navbar.description.home": "Tüm modelin izometrik görünümüne döner.",
|
||||
"navbar.description.look_at": "Görünümü seçili yüze, düzleme veya açık eskize doğru çevirir.",
|
||||
"navbar.description.orbit": "Görünümü modelin etrafında döndürür.",
|
||||
"navbar.description.pan": "Görünümü yana, yukarı veya aşağı kaydırır.",
|
||||
"navbar.description.projection": "Ortografik ve perspektif görünümler arasında geçiş yapar.",
|
||||
"navbar.description.separator": "Komşu yuvaları gruplayan ince bir çizgi.",
|
||||
"navbar.description.spin": "Siz tıklayana veya bir tuşa basana kadar görünümü döndürmeye devam eder.",
|
||||
"navbar.description.visual_style": "Modelin nasıl çizileceğini değiştirir: gölgeli, tel kafes veya gizli kenarlarla.",
|
||||
"navbar.description.zoom": "Görünümü modele yaklaştırır veya uzaklaştırır.",
|
||||
"navbar.display": "Görüntü",
|
||||
"navbar.fit": "Sığdır",
|
||||
"navbar.graphics": "Grafik",
|
||||
"navbar.grid": "Izgara",
|
||||
"navbar.ground_shadow": "Zemin gölgesi",
|
||||
"navbar.home": "Ana görünüm",
|
||||
"navbar.later": "Sonra",
|
||||
"navbar.look_at": "Bak",
|
||||
"navbar.more": "Daha fazla",
|
||||
"navbar.orbit": "Yörünge",
|
||||
"navbar.orthographic": "Ortografik",
|
||||
"navbar.pan": "Kaydır",
|
||||
"navbar.perspective": "Perspektif",
|
||||
"navbar.preset": "Hazır ayar",
|
||||
"navbar.projection": "Projeksiyon",
|
||||
"navbar.projection_heading": "Projeksiyon",
|
||||
"navbar.separator": "Ayırıcı",
|
||||
"navbar.show": "Göster",
|
||||
"navbar.spin": "Sürekli döndür",
|
||||
"navbar.style.shaded": "Gölgeli",
|
||||
"navbar.style.shaded_with_edges": "Kenarlı gölgeli",
|
||||
"navbar.style.shaded_with_hidden_edges": "Gizli kenarlı gölgeli",
|
||||
"navbar.style.wireframe": "Tel kafes",
|
||||
"navbar.translucent": "Yarı saydam",
|
||||
"navbar.value_tip": "{name} · {value}",
|
||||
"navbar.visual_style": "Görsel stil",
|
||||
"navbar.zoom": "Yakınlaştır",
|
||||
"needs two joints": "iki bağlantı gerektirir",
|
||||
"network.allow_once": "Bir kez izin ver",
|
||||
"network.always_allow": "Bu sunucuya her zaman izin ver",
|
||||
@@ -1406,6 +1545,11 @@
|
||||
"no picture": "resim yok",
|
||||
"none": "yok",
|
||||
"parts.automatic_name": "otomatik",
|
||||
"parts.dimension.d": "Nominal çap",
|
||||
"parts.dimension.height": "Yükseklik",
|
||||
"parts.dimension.pitch": "Adım",
|
||||
"parts.dimension.thickness": "Kalınlık",
|
||||
"parts.dimension.width": "Genişlik",
|
||||
"pattern.alignment_2": "Hizalama 2",
|
||||
"pattern.alignment_hint": "Adet, orijinali içerir. Ortala, kopyaları iki yana dağıtır; çift adetlerde yönü ters çevirme, fazladan kopyanın hangi tarafta olacağını belirler.",
|
||||
"pattern.direction_1": "Yön 1",
|
||||
@@ -1432,6 +1576,10 @@
|
||||
"prefs.assistant": "Asistan",
|
||||
"prefs.axes": "Eksenler",
|
||||
"prefs.axis": "Eksen",
|
||||
"prefs.bodies_folders": "Gövde klasörleri",
|
||||
"prefs.bodies_folders_always": "Her zaman",
|
||||
"prefs.bodies_folders_never": "Hiçbir zaman",
|
||||
"prefs.bodies_folders_several": "Yalnızca birden çok gövde",
|
||||
"prefs.bottom": "Alt",
|
||||
"prefs.browser": "Tarayıcı",
|
||||
"prefs.built_in": "Yerleşik",
|
||||
@@ -1456,6 +1604,10 @@
|
||||
"prefs.design_note": "Yeni tasarımların neyle başladığı. Açık tasarımlar kendi ayarlarını korur.",
|
||||
"prefs.devices": "3B fare",
|
||||
"prefs.dialog_buttons": "Diyalog düğmeleri",
|
||||
"prefs.dialogs": "Diyaloglar",
|
||||
"prefs.dialogs_edge": "Görünümün kenarında",
|
||||
"prefs.dialogs_left_column": "Sol sütunda",
|
||||
"prefs.dialogs_note": "Komut diyaloglarının açılacağı yer: görünümün kenarına yerleşik veya tarayıcının ve Geçmiş'in yerine sol sütunda; bunlar diyalog kapanınca eskisi gibi geri gelir.",
|
||||
"prefs.display": "Görüntü",
|
||||
"prefs.edges": "Kenarlar ve gölgeleme",
|
||||
"prefs.extend": "Uzat",
|
||||
@@ -1515,6 +1667,8 @@
|
||||
"prefs.opaque": "Opak",
|
||||
"prefs.orbit_snap": "Bir eksene hizalamak için döndürme sırasında Shift tuşunu basılı tutun.",
|
||||
"prefs.orientation": "Yönelim",
|
||||
"prefs.origin_plane_names": "Orijin düzlemi adları",
|
||||
"prefs.origin_plane_names_note": "Tarayıcıda, diyaloglarda ve listelerde gösterilen adlar. Z yukarı olmak üzere {top} XY, {front} XZ ve {right} YZ'dir. Komutlar, betikler ve kayıtlı tasarımlar XY, XZ ve YZ adlarını korur.",
|
||||
"prefs.overall_speed": "Genel hız",
|
||||
"prefs.placement": "Yerleşim",
|
||||
"prefs.prefer_free_sketch_points": "Noktalar örtüştüğünde hareketli eskiz noktalarını tercih et",
|
||||
@@ -1528,6 +1682,8 @@
|
||||
"prefs.saving": "Otomatik kayıt ve kurtarma",
|
||||
"prefs.scale_note": "Pencerede ayrıca Ctrl + / Ctrl −. Düğmeler %100'de 28 px.",
|
||||
"prefs.scale_text": "Ölçek ve metin",
|
||||
"prefs.search": "Ayarlarda ara",
|
||||
"prefs.search_empty": "Eşleşen ayar yok",
|
||||
"prefs.select": "Seçim",
|
||||
"prefs.select_one_item": "Bir öğe seçin",
|
||||
"prefs.selecting": "Seçme",
|
||||
@@ -1625,6 +1781,7 @@
|
||||
"render.aging_target_search": "Gövde adlarını filtrele",
|
||||
"render.aging_upward": "Yukarı bakan birikim",
|
||||
"render.all_controls_help": "Tüm geçerli kontrollerin yoğun incelemesi. Bölüm sayfaları, temel isteği değiştirmeden odaklı düzenleme sağlar.",
|
||||
"render.all_shots_label": "Tüm Çekimler",
|
||||
"render.appearance_physics_separate": "Görsel görünüm yoğunluk, termal veri veya diğer mühendislik özelliklerini atamaz.",
|
||||
"render.availability_native": "Yerli kompozisyon; uygulamadan önce inceleyin",
|
||||
"render.availability_projection": "Desteklenen yansıtma; kapsama ve atlamalara bakın",
|
||||
@@ -1779,10 +1936,13 @@
|
||||
"render.effects_reset_lens": "Mercek efektlerini sıfırla",
|
||||
"render.effects_seed": "Desen tohumu",
|
||||
"render.export_display": "Dışa aktar: görüntü PNG",
|
||||
"render.export_image_label": "Görüntüyü Dışa Aktar",
|
||||
"render.export_linear": "Dışa aktar: ham doğrusal EXR",
|
||||
"render.exr_pending": "GPU ham doğrusal EXR'ı saklayabilir. Sıkıştırma, çok katmanlı görüntüler ve yardımcı render geçişleri kullanılamamaya devam ediyor.",
|
||||
"render.film_grain": "Fotoğrafik gren",
|
||||
"render.fit": "Ürünü sığdır",
|
||||
"render.fixed_camera": "Kamera",
|
||||
"render.fixed_world": "Dünya",
|
||||
"render.flare_ghosts": "Kaynak başına hayalet halka",
|
||||
"render.flare_halo": "Flare halesi",
|
||||
"render.flare_streak": "Anamorfik tarzı çizgi",
|
||||
@@ -1809,6 +1969,12 @@
|
||||
"render.hdr_primaries_srgb": "Renk alanı: sRGB/D65",
|
||||
"render.hdr_primaries_undeclared": "Kaynak renk alanını beyan etmiyor; işleyici renk gereksinimlerini inceleyin.",
|
||||
"render.hdr_resource_missing": "Bu tasarımda kaydedilen HDR kaynağı eksik.",
|
||||
"render.heading_batch": "TOPLU",
|
||||
"render.heading_frame": "ÇERÇEVE",
|
||||
"render.heading_lens": "MERCEK",
|
||||
"render.heading_results": "SONUÇLAR",
|
||||
"render.heading_shots": "ÇEKİMLER",
|
||||
"render.hide_shots_label": "Çekim şeridini gizle",
|
||||
"render.image_advanced": "Gelişmiş GPU kontrolleri",
|
||||
"render.image_cancelled": "Render iptal edildi",
|
||||
"render.image_cancelling": "İptal ediliyor",
|
||||
@@ -1840,6 +2006,7 @@
|
||||
"render.image_width": "Genişlik",
|
||||
"render.include_rig_cards": "Pasif kartları ve bayrakları dahil et",
|
||||
"render.inspect_scene": "Yakalanan sahne özetini yenile",
|
||||
"render.inspector": "Denetçi",
|
||||
"render.keep_editing": "Düzenlemeye devam et",
|
||||
"render.layer_down": "Aşağı taşı",
|
||||
"render.layer_enabled": "Katman etkin",
|
||||
@@ -1908,6 +2075,8 @@
|
||||
"render.library_unfavorite": "Sık kullanılanlardan kaldır",
|
||||
"render.library_uniform_ack": "Uzamsal veya gelişmiş efektler olmadan tek biçimli yaklaşımı kullan",
|
||||
"render.library_usable": "Çalıştırılabilir yansıtmalar",
|
||||
"render.light_anchor_pending": "Işık koordinat çerçevesini değiştirmek henüz desteklenmiyor.",
|
||||
"render.lights_fixed_to": "Işıklar şuna sabit",
|
||||
"render.linear_help": "Giderme, pozlama, ton eşleme, filtreler ve arka plandan önce 32 bit float RGBA ana görüntüyü saklar. Doğrusal sRGB/D65; kapsama ile ön çarpımlı. Ek bellek açıkça kabul edilir. Sıkıştırma veya ek geçiş yoktur.",
|
||||
"render.linear_retain": "Ham doğrusal EXR'ı sakla",
|
||||
"render.linear_unavailable": "Seçilen tamamlanmış karede saklanmış EXR yok. Render'dan önce saklamayı etkinleştirin; eski bir PNG ham ışınımı geri getiremez.",
|
||||
@@ -1993,6 +2162,7 @@
|
||||
"render.output_over_budget": "İstenen görüntü, seçilen kaynak sınırını aşıyor. Uygun bir boyut veya profil açıkça seçin; otomatik düşürme yapılmaz.",
|
||||
"render.output_ratio_refused": "Bağlı boyut, bu yöntemin boyut sınırını aşıyor. Önceki boyut korunur; oranı kilitten çıkarın veya daha küçük bir değer seçin.",
|
||||
"render.output_swap_orientation": "Genişlik ve yüksekliği değiştir",
|
||||
"render.output_tools_inspector": "Boyutu ve biçimi Çıktı denetçisinde düzenleyin.",
|
||||
"render.page_aging": "Yaşlanma ve kirlenme",
|
||||
"render.page_all": "Tüm kontroller",
|
||||
"render.page_appearance": "Görünüm",
|
||||
@@ -2004,6 +2174,7 @@
|
||||
"render.page_lighting": "Aydınlatma",
|
||||
"render.page_output": "Çıktı",
|
||||
"render.page_scene": "Sahne",
|
||||
"render.paint_label": "Boya",
|
||||
"render.passive_stage": "Zemin ve pasif sahne",
|
||||
"render.passive_stage_help": "Sonlu pasif paneller yalnızca render görüntülerine aittir. CAD gövdesi veya kütle eklemezler. Uygulamadan sonra yerleşim sabittir; ürün sınırlarıyla kesişen reddedilir. CPU dağınık sekme aydınlatması sağlamaz.",
|
||||
"render.perspective": "Perspektif kamera",
|
||||
@@ -2019,6 +2190,13 @@
|
||||
"render.physical_prepare_plan": "Oluşturma planını hazırla (uygulamaz)",
|
||||
"render.physical_separate": "Görünüm, malzeme kimliği değildir. Bunlar kaynaklı referanslardır, sertifikalı etkin özellikler değildir. Mevcut kütle, mühendislik malzemeleri ve optikler değişmez.",
|
||||
"render.physical_unknown": "Sayısal aday belirlenmemiş. Kademe, formülasyon veya yapı belirtilmelidir; bilinmez, sıfır değildir.",
|
||||
"render.pick_body": "Gövde",
|
||||
"render.pick_component": "Bileşen",
|
||||
"render.pick_face": "Yüz",
|
||||
"render.pick_help": "Gövde, Yüz veya Bileşen seçin. Temizlemek için boş alana tıklayın; Shift veya Ctrl ekler ya da seçimi tersine çevirir. Vurgular yalnızca canlı ürün görünümünde görünür.",
|
||||
"render.pick_label": "Seç",
|
||||
"render.pick_up_label": "Al",
|
||||
"render.pick_waiting": "Ürün seçimi henüz desteklenmiyor.",
|
||||
"render.post_contrast": "Kontrast",
|
||||
"render.post_gain_b": "Mavi kazancı",
|
||||
"render.post_gain_g": "Yeşil kazancı",
|
||||
@@ -2031,6 +2209,7 @@
|
||||
"render.preview": "Yerleşik önizleme",
|
||||
"render.preview_help": "Harmanlanmış saydamlıkla raster görüntü. Kırılma, HDRI ve son render henüz kullanılamıyor.",
|
||||
"render.preview_image_settings_help": "Canlı Önizleme, sabit stüdyo aydınlatmasını korur. Pozlama ve görüntü aydınlatması, Kontrol veya Başlat sırasında görüntü işine uygulanır.",
|
||||
"render.preview_lighting_help": "Canlı önizleme yalnızca doğrudan ışığı gösterir, gölge göstermez.",
|
||||
"render.preview_retry": "Canlı önizlemeyi yeniden dene",
|
||||
"render.preview_unavailable_help": "Canlı önizleme kullanılamıyor. Kaydedilmiş kurulumlar ve görüntü işi kontrolleri erişilebilir kalır. Sahneyi değiştirin veya açıkça yeniden deneyin; reddedilen sahne her karede yeniden kurulmaz.",
|
||||
"render.preview_upload": "Canlı önizleme tepe noktası yükü",
|
||||
@@ -2044,8 +2223,10 @@
|
||||
"render.radial_distortion": "Radyal bozulma",
|
||||
"render.regrade_action": "Ham ana görüntüyü yeniden derecelendir (giderme yok)",
|
||||
"render.regrade_help": "Bu sunum kontrollerini kullanarak seçilen tamamlanmış ham ana görüntüden ayrı bir PNG oluşturur. Işın izlemez, yakalanan boyutu/kamerayı/ışıkları değiştirmez veya EXR'ın üzerine yazmaz. Giderme yeniden uygulanmaz: ham ana görüntüler onun kılavuzlarını saklamaz. Türev, aynı iş kontrollerinden dışa aktarılabilir kalır.",
|
||||
"render.regrade_label": "Yeniden derecelendir",
|
||||
"render.reload_settings": "Kaydedilmiş ayarları yükle",
|
||||
"render.replace_ground": "Mevcut sahnenin değiştirilmesini onayla",
|
||||
"render.replace_label": "Değiştir",
|
||||
"render.replace_saved_surface": "Bu hedef için mevcut kaydedilmiş bağlamın yerini al",
|
||||
"render.restore_camera": "Kaydedilmiş kamerayı yükle",
|
||||
"render.result_actual_help": "Ekran pikseli başına bir görüntü pikseli.",
|
||||
@@ -2091,6 +2272,7 @@
|
||||
"render.sampling_title": "Uyarlanabilir ve geçiş örnekleme",
|
||||
"render.save_continue": "Kaydet ve devam et",
|
||||
"render.save_setup": "Kurulumu kaydet",
|
||||
"render.save_shot": "Çekimi Kaydet",
|
||||
"render.saved_maps_runtime_conflict": "Kaydedilmiş yüzey bağlamları, geçici optik geçersiz kılmalar veya kaydedilmemiş HDR ile karıştırılamaz. HDR'ı kaydedin veya geçici girdileri açıkça kaldırın.",
|
||||
"render.saved_surface_remove": "Kaydedilmiş bağlamı kaldır",
|
||||
"render.saved_surfaces": "Kaydedilmiş yüzey bağlamları",
|
||||
@@ -2110,6 +2292,9 @@
|
||||
"render.setup_runtime_unsupported": "Bu tam iş henüz kaydedilemez: haritalar veya sahne-gövde bağlamları içeriyor. Taşınabilir bir kurulum kaydetmeden önce o geçici girdileri kaldırın.",
|
||||
"render.setup_save_missing": "Komut tamamlandıktan sonra kaydedilmiş render yapılandırması bulunamadı.",
|
||||
"render.sharpening": "Detay keskinleştirme",
|
||||
"render.shell_tab_appearance": "RENDER GÖRÜNÜMÜ",
|
||||
"render.shell_tab_camera": "RENDER KAMERASI",
|
||||
"render.shell_tab_scene": "RENDER SAHNESİ",
|
||||
"render.shot_duplicate": "Kaydedilmiş çekimi çoğalt",
|
||||
"render.shot_manager": "Kaydedilmiş çekimler",
|
||||
"render.shot_new_name": "Yeni çekim adı",
|
||||
@@ -2118,11 +2303,19 @@
|
||||
"render.shot_remove_title": "Kaydedilmiş çekim kaldırılsın mı?",
|
||||
"render.shot_rename": "Kaydedilmiş çekimi yeniden adlandır",
|
||||
"render.shot_saved_only": "Önce geçerli taslağı kaydedin. Çoğalt ve yeniden adlandır, tam kaydedilmiş seçenekleri ve kaynak referanslarını korur.",
|
||||
"render.show_shots_label": "Çekim şeridini göster",
|
||||
"render.sky_ground": "Zemin · doğrusal RGB",
|
||||
"render.sky_horizon": "Ufuk · doğrusal RGB",
|
||||
"render.sky_zenith": "Tepe · doğrusal RGB",
|
||||
"render.soft_focus": "Yumuşak odak",
|
||||
"render.stage_frame": "Çerçeve",
|
||||
"render.stage_light": "Işık",
|
||||
"render.stage_look": "Görünüm",
|
||||
"render.stage_orthographic": "Ortografik",
|
||||
"render.stage_output": "Çıktı",
|
||||
"render.stage_panel_count": "Kaydedilmiş paneller",
|
||||
"render.stage_preview_note": "Entegre önizleme · doğrudan ışık",
|
||||
"render.strip_label": "Şerit",
|
||||
"render.studio_add_area": "Alan ışığı ekle",
|
||||
"render.studio_ambient": "Sabit ortam ışınımı",
|
||||
"render.studio_ambient_rgb": "Sabit ortam ışınımı (doğrusal RGB)",
|
||||
@@ -2179,6 +2372,12 @@
|
||||
"render.volume_max": "En fazla sınırlar · dünya mm",
|
||||
"render.volume_min": "En az sınırlar · dünya mm",
|
||||
"render.volume_title": "Sınırlı homojen hacim",
|
||||
"render.wait_batch": "henüz desteklenmiyor: kayıtlı çekimlerin render kuyruğu",
|
||||
"render.wait_compare": "henüz desteklenmiyor: saklanan görüntü karşılaştırması",
|
||||
"render.wait_paint": "henüz desteklenmiyor: ürün seçimi ve görünüm boyama",
|
||||
"render.wait_pick_up": "henüz desteklenmiyor: ürün seçimi ve görünüm örnekleme",
|
||||
"render.wait_replace": "henüz desteklenmiyor: bir görünümün tüm kullanımlarını değiştirme",
|
||||
"render.wait_solo": "henüz desteklenmiyor: ışık görünürlüğü ve solo denetimleri",
|
||||
"sample.bracket.about": "2 mm sac metal kuralına göre kenar flanşları ve bir kenet içeren taban flanşı",
|
||||
"sample.bracket.name": "Sac metal köşebent",
|
||||
"sample.enclosure.about": "Dudak, vida göbekleri, dayanak ve geçmeli bağlantı içeren, içi boşaltılmış blok",
|
||||
@@ -2270,6 +2469,7 @@
|
||||
"timeline.delete_after_marker_one": "Geçmiş İşaretçisinden Sonra 1 Özelliği Sil",
|
||||
"timeline.delete_after_marker_other": "Geçmiş İşaretçisinden Sonra {count} Özelliği Sil",
|
||||
"timeline.delete_folders_tip": "Özelliklerini ve iç içe klasörlerini geçmişte tut",
|
||||
"timeline.delete_folders_with_features_tip": "Klasörleri, içlerindeki her özellikle ve iç içe klasörleriyle birlikte sil",
|
||||
"timeline.delete_many": "{count} Seçili Özelliği Sil",
|
||||
"timeline.feature_error": "Hata: {message}",
|
||||
"timeline.feature_hint": "Düzenlemek için çift tıklayın · sıralamak için sürükleyin · daha fazlası için sağ tıklayın",
|
||||
@@ -2279,6 +2479,7 @@
|
||||
"timeline.move_to_group": "Özellikleri klasöre taşı",
|
||||
"timeline.rename_group": "Özellik klasörünü yeniden adlandır",
|
||||
"timeline.reorder_many": "Özellikleri yeniden sırala",
|
||||
"timeline.suppress_folders_tip": "Klasörlerdeki ve iç içe klasörlerindeki her özelliği bastır veya bastırmayı kaldır",
|
||||
"timeline.ungroup": "Özellik klasörünü kaldır",
|
||||
"to the face": "yüze kadar",
|
||||
"to the next face": "sonraki yüze kadar",
|
||||
@@ -2289,7 +2490,11 @@
|
||||
"toolbar.render_camera": "KAMERA",
|
||||
"toolbar.render_environment": "ORTAM",
|
||||
"toolbar.render_lighting": "AYDINLATMA",
|
||||
"toolbar.render_lights": "IŞIKLAR",
|
||||
"toolbar.render_result": "SONUÇ",
|
||||
"toolbar.render_scene": "SAHNE",
|
||||
"toolbar.render_stage": "PLATO",
|
||||
"toolbar.render_studio": "STÜDYO",
|
||||
"trace.points": "Yol boyunca {count} nokta",
|
||||
"trace.stopped_by_contact": "Örnek {sample} noktasında biter: {a}, {b} içinden geçecek",
|
||||
"trace.stopped_by_loop": "Örnek {sample} noktasında biter: {joint} üzerinden geçen döngü daha fazla kapanamaz",
|
||||
@@ -2468,6 +2673,7 @@
|
||||
"ui.agent_chat_import_failed": "{name}: içe aktarma başarısız oldu: {error}",
|
||||
"ui.agent_chat_intro": "Zaten kullandığınız bir kodlama ajanıyla veya seçtiğiniz bir model uç noktasıyla sohbet edin. Ajan, SolidCraft'ın araçlarıyla açık tasarım üzerinde çalışır: Değişiklikleri hemen görünür ve mesaj başına tek adımda geri alınabilir. Sormadan neleri yapabileceğini Ayarlar > AI altında belirlersiniz.",
|
||||
"ui.agent_chat_key_in_env": "anahtar ayarlı",
|
||||
"ui.agent_chat_last_missing": "En son kullanılan ajan ({agent}) artık kullanılamıyor. Bir ajan seçin, ardından Yeniden dene'ye basın.",
|
||||
"ui.agent_chat_left": "Sol",
|
||||
"ui.agent_chat_limit": "Ajan, bir mesaj için {limit} değişiklik sınırına ulaştı. Devam etmesi için başka bir mesaj gönderin.",
|
||||
"ui.agent_chat_login_found": "oturum bulundu",
|
||||
@@ -2514,6 +2720,7 @@
|
||||
"ui.agent_chat_remove": "Sıradan kaldır",
|
||||
"ui.agent_chat_remove_file": "Bu iletiden çıkar",
|
||||
"ui.agent_chat_remove_imported": "Bu iletiden çıkar; parça tasarımda kalır (Geri al onu kaldırır)",
|
||||
"ui.agent_chat_retry": "Yeniden dene",
|
||||
"ui.agent_chat_right": "Sağ",
|
||||
"ui.agent_chat_save": "Kaydet",
|
||||
"ui.agent_chat_send": "Gönder",
|
||||
@@ -2526,6 +2733,7 @@
|
||||
"ui.agent_chat_state_auth": "Oturum açılması gerekiyor",
|
||||
"ui.agent_chat_state_busy": "Çalışıyor…",
|
||||
"ui.agent_chat_state_exited": "Ajan sonlandı",
|
||||
"ui.agent_chat_state_failed": "Bağlantı başarısız",
|
||||
"ui.agent_chat_state_off": "Bağlı değil",
|
||||
"ui.agent_chat_state_ready": "Hazır",
|
||||
"ui.agent_chat_state_starting": "Başlatılıyor…",
|
||||
@@ -2829,6 +3037,7 @@
|
||||
"ui.fully_constrained": "Tam kısıtlı",
|
||||
"ui.function": "Fonksiyon",
|
||||
"ui.general": "Genel",
|
||||
"ui.gpu_memory_lowered": "Ekran kartının belleği doldu: görünüm daha düşük kalitede çiziliyor.",
|
||||
"ui.graphics": "Grafik",
|
||||
"ui.graphics_custom": "Özel",
|
||||
"ui.graphics_performance": "Performans",
|
||||
@@ -2908,6 +3117,7 @@
|
||||
"ui.heading_utilities": "ARAÇLAR",
|
||||
"ui.heading_utility": "YARDIMCI ARAÇLAR",
|
||||
"ui.heading_weldments": "KAYNAK",
|
||||
"ui.height": "Yükseklik",
|
||||
"ui.help": "Yardım",
|
||||
"ui.hide": "Gizle",
|
||||
"ui.hideAxes": "Eksenleri gizle",
|
||||
@@ -2950,6 +3160,7 @@
|
||||
"ui.language": "Dil",
|
||||
"ui.layout_compact": "Kompakt",
|
||||
"ui.layout_floating": "Kayan",
|
||||
"ui.length": "Uzunluk",
|
||||
"ui.length_decimals": "Uzunluk ondalık basamakları",
|
||||
"ui.light": "Açık",
|
||||
"ui.line": "Çizgi",
|
||||
@@ -3048,6 +3259,7 @@
|
||||
"ui.pinToShortcuts": "Kısayollara sabitle",
|
||||
"ui.pinToToolbar": "Araç çubuğuna sabitle",
|
||||
"ui.pipe": "Boru",
|
||||
"ui.pitch": "Adım",
|
||||
"ui.placement": "Yerleşim",
|
||||
"ui.placement_handles": "Yerleşim tutamaçları",
|
||||
"ui.placement_handles_tip": "Bir Dayama, Kabarıklık veya Kilit Geçme önizlemesindeki tutamaçlar: işaretçiyi model üzerinde hareket ettirmek için sürükleyin, halkasıyla döndürün",
|
||||
@@ -3099,6 +3311,7 @@
|
||||
"ui.render": "Render",
|
||||
"ui.renderer": "Çizim motoru",
|
||||
"ui.repeat": "Tekrarla",
|
||||
"ui.repeat_nothing": "Henüz tekrarlanacak bir şey yok: önce bir araç veya diyalog başlatın.",
|
||||
"ui.replace_with_link_done": "Kopyalar artık bağlı tasarımın yerleşimleri.",
|
||||
"ui.report_a_bug": "Hata bildir",
|
||||
"ui.reset": "Sıfırla",
|
||||
@@ -3254,6 +3467,7 @@
|
||||
"ui.visual_shaded_with_edges": "Kenarlarla gölgeli",
|
||||
"ui.visual_style": "Görsel stil",
|
||||
"ui.visual_wireframe": "Tel kafes",
|
||||
"ui.width": "Genişlik",
|
||||
"ui.window_system": "Pencere sistemi",
|
||||
"ui.window_system_auto": "Otomatik",
|
||||
"ui.window_system_wayland": "Wayland",
|
||||
@@ -3321,5 +3535,6 @@
|
||||
"web.open_link": "Bağlı tasarım açılsın mı?",
|
||||
"web.open_link_confirm": "Kaydedilmemiş değişiklikler var. Bağlı tasarım yeni bir sekmede açılsın mı?",
|
||||
"↑ ↓ choose · Enter / Tab complete · Esc dismiss": "↑ ↓ seç · Enter / Tab tamamla · Esc kapat",
|
||||
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ seçin · Enter yazdığınızı uygular · Esc kapatır"
|
||||
"↑ ↓ choose · Enter applies what you typed · Esc dismiss": "↑ ↓ seçin · Enter yazdığınızı uygular · Esc kapatır",
|
||||
"⌘Enter continues on GitHub.": "⌘Enter ile GitHub'da devam edilir."
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ struct U {
|
||||
acc_u: vec4<f32>, // accessibility map axes (xyz, offset)
|
||||
acc_v: vec4<f32>,
|
||||
eye: vec4<f32>, // eye position, w = 1 for perspective (0: orthographic, looking along -back)
|
||||
ao: vec4<f32>, // radius in world units (zero: disabled)
|
||||
ao: vec4<f32>, // x: radius in world units (zero: disabled), y: samples per pixel
|
||||
// analysis colours (`theme::AnalysisColors`): draft positive, between, negative; reachable,
|
||||
// blocked; curvature flat, middle, tight
|
||||
ana_c: array<vec4<f32>, 8>,
|
||||
@@ -333,9 +333,12 @@ fn occlusion(i: TOut) -> f32 {
|
||||
// the buffer runs reversed). The map is cleared to its far plane either way, so a texel
|
||||
// with nothing in it reads as 1 and the `d < 1.0` test below skips it.
|
||||
let flip = reversed();
|
||||
// Fewer samples while the view moves (#7165), spread over the same hemisphere. 32 is the
|
||||
// still picture's count, `AO_SAMPLES` in view.rs.
|
||||
let count = u32(clamp(u.ao.y, 1.0, 32.0));
|
||||
var blocked = 0.0;
|
||||
for (var j = 0u; j < 32u; j = j + 1u) {
|
||||
let f = (f32(j) + 0.5) / 32.0;
|
||||
for (var j = 0u; j < count; j = j + 1u) {
|
||||
let f = (f32(j) + 0.5) / f32(count);
|
||||
let angle = f32(j) * 2.39996323;
|
||||
let direction = t * (cos(angle) * sqrt(f)) + b * (sin(angle) * sqrt(f)) + n * sqrt(1.0 - f);
|
||||
let reach = radius * (0.2 + 0.8 * fract(f32(j) * 0.61803399 + 0.5));
|
||||
@@ -356,7 +359,7 @@ fn occlusion(i: TOut) -> f32 {
|
||||
blocked = blocked + smoothstep(0.005, 0.15, delta) * (1.0 - smoothstep(0.5, 1.0, delta));
|
||||
}
|
||||
}
|
||||
return 1.0 - 0.85 * blocked / 32.0;
|
||||
return 1.0 - 0.85 * blocked / f32(count);
|
||||
}
|
||||
|
||||
/// How fast the normal turns across the pixel (per mm).
|
||||
|
||||
@@ -8,6 +8,15 @@ const fn icon_primary(rgb: [u8; 3]) -> Color32 {
|
||||
impl Tokens {
|
||||
pub const fn light() -> Tokens {
|
||||
Tokens {
|
||||
nav_hover: Color32::from_rgb(233, 237, 242),
|
||||
nav_press: Color32::from_rgb(220, 226, 234),
|
||||
nav_dim: Color32::from_rgb(136, 149, 167),
|
||||
nav_viewport: Color32::from_rgb(238, 241, 245),
|
||||
nav_tip: Color32::from_rgb(27, 36, 48),
|
||||
nav_tip_ink: Color32::from_rgb(255, 255, 255),
|
||||
nav_tip_key_border: Color32::from_rgba_unmultiplied_const(128, 140, 160, 153),
|
||||
nav_field: Color32::from_rgb(255, 255, 255),
|
||||
|
||||
borders: crate::theme::Borders::LEGACY,
|
||||
icon_palette: None,
|
||||
file_dim: Color32::from_rgb(58, 70, 86),
|
||||
@@ -184,8 +193,10 @@ impl Tokens {
|
||||
sketch_halo: Color32::from_rgb(240, 243, 247),
|
||||
sketch_point: Color32::BLACK,
|
||||
sketch_locked: Color32::from_rgb(0, 140, 60),
|
||||
dimension: Color32::from_rgb(36, 40, 48),
|
||||
dimension_driven: Color32::from_rgb(120, 126, 138),
|
||||
// Sketch dimensions in a plum of their own, apart from the blue and black curves for
|
||||
// every colour vision mode (#7861).
|
||||
dimension: Color32::from_rgb(130, 42, 110),
|
||||
dimension_driven: Color32::from_rgb(160, 90, 120),
|
||||
sketch_point_free: Color32::from_rgb(30, 90, 200),
|
||||
section_cap: Color32::from_rgb(214, 128, 72),
|
||||
dialog_bg: Color32::from_rgb(250, 251, 252),
|
||||
@@ -314,6 +325,14 @@ impl Tokens {
|
||||
t.render_ink = Color32::from_rgb(224, 116, 174);
|
||||
t.render_soft = Color32::from_rgb(64, 35, 54);
|
||||
t.tab_on_fill_dark = Color32::BLACK;
|
||||
t.nav_hover = Color32::from_rgb(58, 65, 80);
|
||||
t.nav_press = Color32::from_rgb(70, 78, 96);
|
||||
t.nav_dim = Color32::from_rgb(124, 135, 154);
|
||||
t.nav_viewport = Color32::from_rgb(38, 43, 53);
|
||||
t.nav_tip = Color32::from_rgb(238, 241, 245);
|
||||
t.nav_tip_ink = Color32::from_rgb(27, 36, 48);
|
||||
t.nav_tip_key_border = Color32::from_rgba_unmultiplied_const(128, 140, 160, 153);
|
||||
t.nav_field = Color32::from_rgb(47, 52, 62);
|
||||
t.dialog_body = Color32::from_rgb(39, 43, 52);
|
||||
t.dialog_footer = Color32::from_rgb(47, 52, 62);
|
||||
t.dialog_rule = Color32::from_rgb(53, 59, 71);
|
||||
@@ -341,8 +360,9 @@ impl Tokens {
|
||||
t.sketch_halo = Color32::from_rgb(28, 32, 40);
|
||||
t.sketch_point = Color32::from_rgb(232, 234, 238);
|
||||
t.sketch_locked = Color32::from_rgb(96, 204, 116);
|
||||
t.dimension = Color32::from_rgb(224, 228, 234);
|
||||
t.dimension_driven = Color32::from_rgb(142, 150, 164);
|
||||
// Sketch dimensions: Light's plum, lightened for the dark view (#7861).
|
||||
t.dimension = Color32::from_rgb(224, 176, 224);
|
||||
t.dimension_driven = Color32::from_rgb(176, 104, 144);
|
||||
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
|
||||
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
|
||||
t.dialog_bg = Color32::from_rgb(39, 43, 52);
|
||||
|
||||
@@ -101,6 +101,14 @@
|
||||
"mesh_fill": "#ffffff",
|
||||
"mesh_ink": "#ffffff",
|
||||
"mesh_soft": "#000000",
|
||||
"nav_dim": "#ffffff",
|
||||
"nav_field": "#000000",
|
||||
"nav_hover": "#000000",
|
||||
"nav_press": "#ffffff",
|
||||
"nav_tip": "#ffffff",
|
||||
"nav_tip_ink": "#000000",
|
||||
"nav_tip_key_border": "#000000",
|
||||
"nav_viewport": "#000000",
|
||||
"on_accent": "#000000",
|
||||
"origin_plane": "#000000",
|
||||
"origin_plane_edge": "#ffffff",
|
||||
|
||||
@@ -101,6 +101,14 @@
|
||||
"mesh_fill": "#000000",
|
||||
"mesh_ink": "#000000",
|
||||
"mesh_soft": "#ffffff",
|
||||
"nav_dim": "#000000",
|
||||
"nav_field": "#ffffff",
|
||||
"nav_hover": "#ffffff",
|
||||
"nav_press": "#000000",
|
||||
"nav_tip": "#000000",
|
||||
"nav_tip_ink": "#ffffff",
|
||||
"nav_tip_key_border": "#ffffff",
|
||||
"nav_viewport": "#ffffff",
|
||||
"on_accent": "#ffffff",
|
||||
"origin_plane": "#ffffff",
|
||||
"origin_plane_edge": "#000000",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#efece8",
|
||||
"dialog_label": "#403d38",
|
||||
"dialog_rule": "#d9d5d0",
|
||||
"dimension": "#403d38",
|
||||
"dimension_driven": "#6c6863",
|
||||
"dimension": "#006a70",
|
||||
"dimension_driven": "#4d8a8c",
|
||||
"error": "#c71529",
|
||||
"field": "#fffdfa",
|
||||
"file_rail": "#e1ddd8",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#100b04",
|
||||
"dialog_label": "#f3ca87",
|
||||
"dialog_rule": "#3b280d",
|
||||
"dimension": "#ffe2a5",
|
||||
"dimension_driven": "#9b6922",
|
||||
"dimension": "#f5e050",
|
||||
"dimension_driven": "#a8962e",
|
||||
"error": "#fff0ca",
|
||||
"field": "#100b04",
|
||||
"form_accent": "#ca892b",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#0c223c",
|
||||
"dialog_label": "#e3f0fa",
|
||||
"dialog_rule": "#254764",
|
||||
"dimension": "#e3f0fa",
|
||||
"dimension_driven": "#94b2cb",
|
||||
"dimension": "#ffd970",
|
||||
"dimension_driven": "#c9a95a",
|
||||
"error": "#ff968d",
|
||||
"field": "#071d35",
|
||||
"form_accent": "#baade8",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#1e1e2e",
|
||||
"dialog_label": "#cdd6f4",
|
||||
"dialog_rule": "#45475a",
|
||||
"dimension": "#cdd6f4",
|
||||
"dimension_driven": "#a6adc8",
|
||||
"dimension": "#94e2d5",
|
||||
"dimension_driven": "#6c9d95",
|
||||
"error": "#f38ba8",
|
||||
"field": "#181825",
|
||||
"form_accent": "#a9b2f2",
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
"dialog_footer": "#000000",
|
||||
"dialog_label": "#55ffff",
|
||||
"dialog_rule": "#00aaaa",
|
||||
"dimension": "#ffffff",
|
||||
"dimension_driven": "#aaaaaa",
|
||||
"dimension": "#ffff55",
|
||||
"dimension_driven": "#aa5500",
|
||||
"error": "#ff5555",
|
||||
"field": "#000000",
|
||||
"file_background": "#000000",
|
||||
@@ -101,6 +101,14 @@
|
||||
"mesh_fill": "#55ff55",
|
||||
"mesh_ink": "#55ff55",
|
||||
"mesh_soft": "#000000",
|
||||
"nav_dim": "#55ffff",
|
||||
"nav_field": "#000000",
|
||||
"nav_hover": "#aa00aa",
|
||||
"nav_press": "#ff55ff",
|
||||
"nav_tip": "#ffffff",
|
||||
"nav_tip_ink": "#000000",
|
||||
"nav_tip_key_border": "#55ffff",
|
||||
"nav_viewport": "#000000",
|
||||
"on_accent": "#000000",
|
||||
"origin_plane": "#55ffff20",
|
||||
"origin_plane_edge": "#55ffff80",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#29241f",
|
||||
"dialog_label": "#c3bdb5",
|
||||
"dialog_rule": "#403930",
|
||||
"dimension": "#c3bdb5",
|
||||
"dimension_driven": "#9d9285",
|
||||
"dimension": "#5fc4c0",
|
||||
"dimension_driven": "#4f9895",
|
||||
"error": "#f85052",
|
||||
"field": "#3c352d",
|
||||
"form_accent": "#a6a2fc",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#f4eee3",
|
||||
"dialog_label": "#423e36",
|
||||
"dialog_rule": "#e0d8c9",
|
||||
"dimension": "#423e36",
|
||||
"dimension_driven": "#726a5d",
|
||||
"dimension": "#006a70",
|
||||
"dimension_driven": "#4a8688",
|
||||
"error": "#c71529",
|
||||
"field": "#fdfaf3",
|
||||
"form_accent": "#5844bc",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#ececec",
|
||||
"dialog_label": "#262628",
|
||||
"dialog_rule": "#c6c6c8",
|
||||
"dimension": "#262628",
|
||||
"dimension_driven": "#6e6e73",
|
||||
"dimension": "#00646b",
|
||||
"dimension_driven": "#3d8087",
|
||||
"error": "#d53137",
|
||||
"field": "#ffffff",
|
||||
"form_accent": "#8b62bf",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#25272b",
|
||||
"dialog_label": "#e0e2e5",
|
||||
"dialog_rule": "#484d55",
|
||||
"dimension": "#e0e2e5",
|
||||
"dimension_driven": "#a0a6b0",
|
||||
"dimension": "#5fd0c8",
|
||||
"dimension_driven": "#4a9a95",
|
||||
"error": "#ed7777",
|
||||
"field": "#1c1e21",
|
||||
"form_accent": "#a08fc4",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#061009",
|
||||
"dialog_label": "#8def9f",
|
||||
"dialog_rule": "#173b22",
|
||||
"dimension": "#8def9f",
|
||||
"dimension_driven": "#5b9b6a",
|
||||
"dimension": "#c8ff4d",
|
||||
"dimension_driven": "#7fa040",
|
||||
"error": "#d8ffdf",
|
||||
"field": "#061009",
|
||||
"form_accent": "#1faa4e",
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
"sketch_fixed": "#ebdbb2",
|
||||
"sketch_halo": "#141617",
|
||||
"sketch_locked": "#b8bb26",
|
||||
"dimension": "#ebdbb2",
|
||||
"dimension_driven": "#928374",
|
||||
"dimension": "#fabd2f",
|
||||
"dimension_driven": "#b57614",
|
||||
"sketch_point": "#ebdbb2",
|
||||
"sketch_point_free": "#83a598",
|
||||
"dialog_bg": "#3c3836",
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"sketch_fixed": "#ebdbb2",
|
||||
"sketch_halo": "#1d2021",
|
||||
"sketch_locked": "#b8bb26",
|
||||
"dimension": "#ebdbb2",
|
||||
"dimension_driven": "#928374",
|
||||
"dimension": "#fabd2f",
|
||||
"dimension_driven": "#b57614",
|
||||
"sketch_point": "#ebdbb2",
|
||||
"sketch_point_free": "#83a598",
|
||||
"dialog_bg": "#3c3836",
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"sketch_fixed": "#ebdbb2",
|
||||
"sketch_halo": "#1d2021",
|
||||
"sketch_locked": "#b8bb26",
|
||||
"dimension": "#ebdbb2",
|
||||
"dimension_driven": "#928374",
|
||||
"dimension": "#fabd2f",
|
||||
"dimension_driven": "#b57614",
|
||||
"sketch_point": "#ebdbb2",
|
||||
"sketch_point_free": "#83a598",
|
||||
"dialog_bg": "#3c3836",
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
"sketch_fixed": "#282828",
|
||||
"sketch_halo": "#fbf8e3",
|
||||
"sketch_locked": "#79740e",
|
||||
"dimension": "#3c3836",
|
||||
"dimension_driven": "#7c6f64",
|
||||
"dimension": "#1f5c40",
|
||||
"dimension_driven": "#4f7d64",
|
||||
"sketch_point": "#282828",
|
||||
"sketch_point_free": "#076678",
|
||||
"dialog_bg": "#f9f5d7",
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
"sketch_fixed": "#282828",
|
||||
"sketch_halo": "#f6edd0",
|
||||
"sketch_locked": "#79740e",
|
||||
"dimension": "#3c3836",
|
||||
"dimension_driven": "#7c6f64",
|
||||
"dimension": "#1f5c40",
|
||||
"dimension_driven": "#4f7d64",
|
||||
"sketch_point": "#282828",
|
||||
"sketch_point_free": "#076678",
|
||||
"dialog_bg": "#f2e5bc",
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
"sketch_fixed": "#282828",
|
||||
"sketch_halo": "#fcf5d8",
|
||||
"sketch_locked": "#79740e",
|
||||
"dimension": "#3c3836",
|
||||
"dimension_driven": "#7c6f64",
|
||||
"dimension": "#1f5c40",
|
||||
"dimension_driven": "#4f7d64",
|
||||
"sketch_point": "#282828",
|
||||
"sketch_point_free": "#076678",
|
||||
"dialog_bg": "#fbf1c7",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#000000",
|
||||
"dialog_label": "#ffffff",
|
||||
"dialog_rule": "#a0a0a0",
|
||||
"dimension": "#ffffff",
|
||||
"dimension_driven": "#cccccc",
|
||||
"dimension": "#ffff66",
|
||||
"dimension_driven": "#c8c850",
|
||||
"error": "#ff6666",
|
||||
"field": "#000000",
|
||||
"form_accent": "#d98af9",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#2e3440",
|
||||
"dialog_label": "#eceff4",
|
||||
"dialog_rule": "#4c566a",
|
||||
"dimension": "#eceff4",
|
||||
"dimension_driven": "#a4b0c2",
|
||||
"dimension": "#d08770",
|
||||
"dimension_driven": "#a87060",
|
||||
"error": "#bf616a",
|
||||
"field": "#242933",
|
||||
"form_accent": "#cfa7c7",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#282a36",
|
||||
"dialog_label": "#f8f8f2",
|
||||
"dialog_rule": "#50546d",
|
||||
"dimension": "#f8f8f2",
|
||||
"dimension_driven": "#a1a4c4",
|
||||
"dimension": "#8be9fd",
|
||||
"dimension_driven": "#62a3b1",
|
||||
"error": "#ff5555",
|
||||
"field": "#21222c",
|
||||
"form_accent": "#c096e8",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#1b1e20",
|
||||
"dialog_label": "#bbbec1",
|
||||
"dialog_rule": "#2f3235",
|
||||
"dimension": "#bbbec1",
|
||||
"dimension_driven": "#8f9296",
|
||||
"dimension": "#5fc4c0",
|
||||
"dimension_driven": "#4f9895",
|
||||
"error": "#f85052",
|
||||
"field": "#2c2e31",
|
||||
"file_rail": "#0f1113",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#2f343e",
|
||||
"dialog_label": "#b4bbc8",
|
||||
"dialog_rule": "#353b47",
|
||||
"dimension": "#abb2bf",
|
||||
"dimension_driven": "#7f848e",
|
||||
"dimension": "#56b6c2",
|
||||
"dimension_driven": "#3f8590",
|
||||
"error": "#e06c75",
|
||||
"field": "#1e2227",
|
||||
"form_accent": "#d283e9",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#dddddd",
|
||||
"dialog_label": "#222222",
|
||||
"dialog_rule": "#aaaaaa",
|
||||
"dimension": "#222222",
|
||||
"dimension_driven": "#666666",
|
||||
"dimension": "#00787f",
|
||||
"dimension_driven": "#4d9196",
|
||||
"error": "#bb3333",
|
||||
"field": "#ffffff",
|
||||
"form_accent": "#704e81",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#eee8d5",
|
||||
"dialog_label": "#586e75",
|
||||
"dialog_rule": "#b6b3a1",
|
||||
"dimension": "#586e75",
|
||||
"dimension_driven": "#657b83",
|
||||
"dimension": "#136059",
|
||||
"dimension_driven": "#3d8f88",
|
||||
"error": "#dc322f",
|
||||
"field": "#fdf6e3",
|
||||
"form_accent": "#6b70c3",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#c0c0c0",
|
||||
"dialog_label": "#000000",
|
||||
"dialog_rule": "#808080",
|
||||
"dimension": "#000000",
|
||||
"dimension_driven": "#808080",
|
||||
"dimension": "#ffffff",
|
||||
"dimension_driven": "#ffff00",
|
||||
"error": "#ff0000",
|
||||
"field": "#ffffff",
|
||||
"form_accent": "#800080",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"dialog_footer": "#1a1b26",
|
||||
"dialog_label": "#c0caf5",
|
||||
"dialog_rule": "#414868",
|
||||
"dimension": "#c0caf5",
|
||||
"dimension_driven": "#9aa5ce",
|
||||
"dimension": "#73daca",
|
||||
"dimension_driven": "#4f9a90",
|
||||
"error": "#f7768e",
|
||||
"field": "#16161e",
|
||||
"form_accent": "#b190ec",
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
//! Lists the design-format changes in `src/format/`: one file per change, in `v<N>/` once a
|
||||
//! release fixed its number, else in `pending/` (see `src/format/pending/README.md`). The list is
|
||||
//! Lists the format changes of each family of files versioned by `src/format.rs`: designs in
|
||||
//! `src/format/`, drawings in `src/drawing/format/` (#3890). One file per change, in `v<N>/` once a
|
||||
//! release fixed its number, else in `pending/` (see `src/format/pending/README.md`). The lists are
|
||||
//! generated so that a change adds a file and never edits a line another change also edits.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The last format whose upgrade steps are written out in `format.rs` (`JSON_STEPS`).
|
||||
const HAND_WRITTEN: u32 = 22;
|
||||
/// A family's change files: their folder, the last format before them, and the prefix of the
|
||||
/// family's lists in `format.rs`.
|
||||
struct Family {
|
||||
folder: &'static str,
|
||||
base: u32,
|
||||
list_prefix: &'static str,
|
||||
}
|
||||
|
||||
/// Every family, each read by `scripts/fold-format-pending.sh` too. A design's upgrade steps are
|
||||
/// written out in `format.rs` up to 22 (`JSON_STEPS`); a drawing's first format is 1.
|
||||
const FAMILIES: [Family; 2] =
|
||||
[Family { folder: "src/format", base: 22, list_prefix: "" }, Family { folder: "src/drawing/format", base: 1, list_prefix: "DRAWING_" }];
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("src/format");
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let mut out = String::new();
|
||||
let mut mods = String::new();
|
||||
for family in &FAMILIES {
|
||||
lists(family, &manifest, &mut out, &mut mods);
|
||||
}
|
||||
out.push_str(&mods);
|
||||
let dest = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("format_parts.rs");
|
||||
std::fs::write(dest, out).unwrap();
|
||||
}
|
||||
|
||||
/// A family's `FOLDED` and `PENDING` lists; its folded formats follow on from its base.
|
||||
fn lists(family: &Family, manifest: &Path, out: &mut String, mods: &mut String) {
|
||||
let Family { folder, base, list_prefix } = family;
|
||||
let root = manifest.join(folder);
|
||||
println!("cargo:rerun-if-changed={}", root.display());
|
||||
let mut folded: Vec<(u32, PathBuf)> = std::fs::read_dir(&root)
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", root.display()))
|
||||
@@ -21,23 +46,24 @@ fn main() {
|
||||
.collect();
|
||||
folded.sort();
|
||||
for (i, (version, _)) in folded.iter().enumerate() {
|
||||
let expected = HAND_WRITTEN + 1 + i as u32;
|
||||
assert_eq!(*version, expected, "src/format/: expected v{expected}/ next, found v{version}/ (versions are contiguous)");
|
||||
let expected = base + 1 + i as u32;
|
||||
assert_eq!(*version, expected, "{folder}/: expected v{expected}/ next, found v{version}/ (versions are contiguous)");
|
||||
}
|
||||
let mut out = String::new();
|
||||
let mut mods = String::new();
|
||||
out.push_str("/// The changes of each format after the hand-written ones, oldest first.\nconst FOLDED: &[(u32, &[Part])] = &[\n");
|
||||
// Module names start with the family's (`drawing_pending_…`), so two families' changes of one
|
||||
// name never clash.
|
||||
let module = list_prefix.to_ascii_lowercase();
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"/// The changes of each format after the base, oldest first (`{folder}/v<N>/`).\nconst {list_prefix}FOLDED: &[(u32, &[Part])] = &["
|
||||
);
|
||||
for (version, dir) in &folded {
|
||||
let _ = writeln!(out, " ({version}, &[");
|
||||
parts(dir, &format!("v{version}"), &mut out, &mut mods);
|
||||
parts(dir, &format!("{module}v{version}"), out, mods);
|
||||
out.push_str(" ]),\n");
|
||||
}
|
||||
out.push_str("];\n\n/// The changes no release has a number for yet (`src/format/pending/`).\nconst PENDING: &[Part] = &[\n");
|
||||
parts(&root.join("pending"), "pending", &mut out, &mut mods);
|
||||
out.push_str("];\n");
|
||||
out.push_str(&mods);
|
||||
let dest = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("format_parts.rs");
|
||||
std::fs::write(dest, out).unwrap();
|
||||
let _ = writeln!(out, "];\n\n/// The changes no release has a number for yet (`{folder}/pending/`).\nconst {list_prefix}PENDING: &[Part] = &[");
|
||||
parts(&root.join("pending"), &format!("{module}pending"), out, mods);
|
||||
out.push_str("];\n\n");
|
||||
}
|
||||
|
||||
/// The `Part`s of the change files in `dir`, in file-name order, and a module for each.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! a component's over the physical material's look; a body with none of these keeps the colour
|
||||
//! it was imported with.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcraft_geom::Vec3;
|
||||
@@ -129,6 +129,95 @@ impl Appearances {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.bodies.is_empty() && self.faces.is_empty() && self.components.is_empty()
|
||||
}
|
||||
|
||||
/// Bound bulk appearance operations, including designs read from untrusted files.
|
||||
pub fn check_budget(&self) -> crate::Result<()> {
|
||||
if self.bodies.len().saturating_add(self.faces.len()).saturating_add(self.components.len()) > 100_000 {
|
||||
return Err(crate::DocError::Invalid("at most 100000 appearance overrides can be processed".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build a replacement without changing the source. Equality includes the label and
|
||||
/// every optical value. Physical-material defaults are deliberately outside this scope.
|
||||
/// `None` means no change, allowing callers to preserve history and revision.
|
||||
pub fn replacing(&self, from: &Look, to: &Look, scope: Option<&AppearanceScope>) -> crate::Result<Option<(Self, usize)>> {
|
||||
self.check_budget()?;
|
||||
for look in [from, to] {
|
||||
if !look.opacity.is_finite() || !(0.0..=1.0).contains(&look.opacity) {
|
||||
return Err(crate::DocError::Invalid("opacity must be finite and in 0..1".into()));
|
||||
}
|
||||
look.opaque_surface()?;
|
||||
}
|
||||
if from == to {
|
||||
return Ok(None);
|
||||
}
|
||||
let body_in_scope = |body: &str| scope.is_none_or(|s| s.bodies.contains(body));
|
||||
let component_in_scope = |id: &u64| scope.is_none_or(|s| s.components.contains(id));
|
||||
let count = self.bodies.iter().filter(|(body, look)| body_in_scope(body) && *look == from).count()
|
||||
+ self.faces.iter().filter(|face| body_in_scope(&face.body) && &face.look == from).count()
|
||||
+ self.components.iter().filter(|(id, look)| component_in_scope(id) && *look == from).count();
|
||||
if count == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut replaced = self.clone();
|
||||
for (body, look) in &mut replaced.bodies {
|
||||
if body_in_scope(body) && look == from {
|
||||
*look = to.clone();
|
||||
}
|
||||
}
|
||||
for face in &mut replaced.faces {
|
||||
if body_in_scope(&face.body) && &face.look == from {
|
||||
face.look = to.clone();
|
||||
}
|
||||
}
|
||||
for (id, look) in &mut replaced.components {
|
||||
if component_in_scope(id) && look == from {
|
||||
*look = to.clone();
|
||||
}
|
||||
}
|
||||
Ok(Some((replaced, count)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact override containers for replacement. A body includes its face overrides;
|
||||
/// a component selects its own override, independently of the bodies it owns.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AppearanceScope {
|
||||
pub bodies: BTreeSet<String>,
|
||||
pub components: BTreeSet<u64>,
|
||||
}
|
||||
|
||||
/// A target in an evaluated model. Face indices are those of `model.faces`.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum AppearanceTarget<'a> {
|
||||
Body(&'a str),
|
||||
Face { body: &'a str, face: usize },
|
||||
Component(u64),
|
||||
}
|
||||
|
||||
/// The four override steps, in descending precedence. Imported paint is a display
|
||||
/// fallback, not an override or physical material, and is not a fifth step.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AppearanceChain {
|
||||
pub face: Option<Look>,
|
||||
pub body: Option<Look>,
|
||||
pub component: Option<Look>,
|
||||
pub material: Option<Look>,
|
||||
}
|
||||
|
||||
impl AppearanceChain {
|
||||
pub fn steps(&self) -> [(&'static str, Option<&Look>); 4] {
|
||||
[("face", self.face.as_ref()), ("body", self.body.as_ref()), ("component", self.component.as_ref()), ("material", self.material.as_ref())]
|
||||
}
|
||||
|
||||
pub fn active(&self) -> Option<&'static str> {
|
||||
self.steps().into_iter().find_map(|(step, look)| look.map(|_| step))
|
||||
}
|
||||
|
||||
pub fn resolved(&self) -> Option<&Look> {
|
||||
self.steps().into_iter().find_map(|(_, look)| look)
|
||||
}
|
||||
}
|
||||
|
||||
/// The built-in appearance library: (name, colour, opacity).
|
||||
@@ -161,15 +250,96 @@ pub fn material_look(material: &str) -> Option<Look> {
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// Read the same precedence used by body and face display resolution. Components
|
||||
/// have no material assignment of their own: the material step is present only
|
||||
/// when their directly owned bodies all have the same material look. Empty or
|
||||
/// mixed components have no material step. Root is not an assignable component.
|
||||
pub fn appearance_chain(&self, state: &crate::ModelState, target: AppearanceTarget<'_>) -> crate::Result<AppearanceChain> {
|
||||
self.appearances.check_budget()?;
|
||||
match target {
|
||||
AppearanceTarget::Body(name) | AppearanceTarget::Face { body: name, .. } => {
|
||||
let body = state.body(name).ok_or_else(|| crate::DocError::Invalid(format!("no body `{name}`")))?;
|
||||
let mut chain = self.body_appearance_chain(body.definition_name(), self.model_body_component(body));
|
||||
if let AppearanceTarget::Face { face, .. } = target {
|
||||
if face >= body.body.face_count() {
|
||||
return Err(crate::DocError::Invalid("unknown face index".into()));
|
||||
}
|
||||
let mut overrides = Vec::new();
|
||||
self.apply_face_overrides(body, &mut overrides);
|
||||
chain.face = overrides.into_iter().find_map(|(index, look)| (index == face).then_some(look));
|
||||
}
|
||||
Ok(chain)
|
||||
}
|
||||
AppearanceTarget::Component(id) => {
|
||||
if id == 0 || !self.components.iter().any(|c| c.id == id) {
|
||||
return Err(crate::DocError::Invalid("unknown or root component".into()));
|
||||
}
|
||||
if state.bodies.len() > 100_000 {
|
||||
return Err(crate::DocError::Invalid("too many bodies to read a component appearance chain".into()));
|
||||
}
|
||||
// Legacy ownership can refer to a feature. Index it once, rather
|
||||
// than scanning the timeline for each body of a large component.
|
||||
let features: BTreeMap<_, _> = self.features.iter().rev().map(|f| (f.id, f.component)).collect();
|
||||
let owner = |b: &&crate::ModelBody| {
|
||||
if b.transferred || b.source_name.is_some() {
|
||||
b.component
|
||||
} else {
|
||||
self.body_components.get(&b.name).copied().or_else(|| features.get(&b.feature).copied()).unwrap_or(0)
|
||||
}
|
||||
};
|
||||
let mut materials = state
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|b| owner(b) == id)
|
||||
.map(|b| self.materials.get(b.definition_name()).and_then(|m| self.material_look(m)));
|
||||
let material = materials.next().flatten().filter(|first| materials.all(|look| look.as_ref() == Some(first)));
|
||||
Ok(AppearanceChain { face: None, body: None, component: self.appearances.components.get(&id).cloned(), material })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn body_appearance_chain(&self, name: &str, component: u64) -> AppearanceChain {
|
||||
AppearanceChain {
|
||||
face: None,
|
||||
body: self.appearances.bodies.get(name).cloned(),
|
||||
component: (component != 0).then(|| self.appearances.components.get(&component).cloned()).flatten(),
|
||||
material: self.materials.get(name).and_then(|material| self.material_look(material)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a picked face through the same appearance chain used by the viewport.
|
||||
pub fn model_face_look(&self, body: &crate::ModelBody, face: usize) -> Option<Look> {
|
||||
self.face_colors(body).into_iter().find(|(index, _)| *index == face).map(|(_, look)| look).or_else(|| self.model_display_look(body))
|
||||
}
|
||||
|
||||
/// Body appearance including the imported fallback used by `body_color`.
|
||||
pub fn model_display_look(&self, body: &crate::ModelBody) -> Option<Look> {
|
||||
self.model_body_look(body).or_else(|| {
|
||||
let color = body.body.color().or_else(|| body.imported.as_ref().and_then(|p| p.color))?;
|
||||
Some(Look::custom(color.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8), self.model_body_opacity(body)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Distinct appearances used by an object, including its face overrides. `None` is
|
||||
/// the unassigned/imported body appearance. Repeated faces count as one object user.
|
||||
pub fn model_used_looks(&self, body: &crate::ModelBody) -> Vec<Option<Look>> {
|
||||
let faces = self.face_colors(body);
|
||||
let mut looks = Vec::new();
|
||||
if faces.len() < body.body.face_count() || faces.is_empty() {
|
||||
looks.push(self.model_display_look(body));
|
||||
}
|
||||
for (_, look) in faces {
|
||||
let look = Some(look);
|
||||
if !looks.contains(&look) {
|
||||
looks.push(look);
|
||||
}
|
||||
}
|
||||
looks
|
||||
}
|
||||
|
||||
/// Shared definition appearance for a computed occurrence body.
|
||||
pub fn model_body_look(&self, body: &crate::ModelBody) -> Option<Look> {
|
||||
let name = body.definition_name();
|
||||
self.appearances
|
||||
.bodies
|
||||
.get(name)
|
||||
.cloned()
|
||||
.or_else(|| self.appearances.components.get(&self.model_body_component(body)).cloned())
|
||||
.or_else(|| self.materials.get(name).and_then(|material| self.material_look(material)))
|
||||
self.body_appearance_chain(body.definition_name(), self.model_body_component(body)).resolved().cloned()
|
||||
}
|
||||
/// Effective body opacity, including paint carried from an imported body.
|
||||
pub fn model_body_opacity(&self, body: &crate::ModelBody) -> f64 {
|
||||
@@ -182,15 +352,7 @@ impl Document {
|
||||
/// A body's look (without face appearances): its own, else its component's, else its
|
||||
/// material's.
|
||||
pub fn body_look(&self, name: &str, feature: u64) -> Option<Look> {
|
||||
self.appearances
|
||||
.bodies
|
||||
.get(name)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
let c = self.body_component(name, feature);
|
||||
(c != 0).then(|| self.appearances.components.get(&c).cloned()).flatten()
|
||||
})
|
||||
.or_else(|| self.materials.get(name).and_then(|m| self.material_look(m)))
|
||||
self.body_appearance_chain(name, self.body_component(name, feature)).resolved().cloned()
|
||||
}
|
||||
|
||||
/// Face appearances on a body: (point on the face, look).
|
||||
@@ -201,9 +363,19 @@ impl Document {
|
||||
/// The faces of a body with appearances of their own: (B-rep face index, look). A later
|
||||
/// assignment to the same face wins.
|
||||
pub fn face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
|
||||
let mut out = self.imported_face_colors(b);
|
||||
for (index, look) in self.authored_face_colors(b) {
|
||||
out.retain(|(old, _)| *old != index);
|
||||
out.push((index, look));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Imported paint alone, after applying body-appearance precedence.
|
||||
pub(crate) fn imported_face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
|
||||
// An assigned body appearance replaces imported paint; design face appearances still win.
|
||||
let to8 = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
let mut out: Vec<(usize, Look)> = if self.appearances.bodies.contains_key(b.definition_name()) {
|
||||
if self.appearances.bodies.contains_key(b.definition_name()) {
|
||||
Vec::new()
|
||||
} else {
|
||||
match b.body.paint() {
|
||||
@@ -219,7 +391,18 @@ impl Document {
|
||||
None => Vec::new(),
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Design-authored face overrides, without imported STEP paint. Linked designs carry
|
||||
/// these independently so an inherited body look does not hide their face overrides.
|
||||
pub fn authored_face_colors(&self, b: &crate::ModelBody) -> Vec<(usize, Look)> {
|
||||
let mut out = Vec::new();
|
||||
self.apply_face_overrides(b, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn apply_face_overrides(&self, b: &crate::ModelBody, out: &mut Vec<(usize, Look)>) {
|
||||
for f in self.appearances.faces.iter().filter(|f| f.body == b.definition_name()) {
|
||||
let (p, look) = (crate::apply_point(&b.source_transform, f.point), f.look.clone());
|
||||
if f.strict_name {
|
||||
@@ -259,7 +442,6 @@ impl Document {
|
||||
out.push((i, look));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +567,28 @@ mod tests {
|
||||
assert!(restored.iter().all(|(face, look)| *look == if *face == 0 { face_look.clone() } else { Look::custom([255, 0, 0], 0.5) }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linked_paint_retains_imported_faces_without_baking_authored_overrides() {
|
||||
let mut state = crate::ModelState::default();
|
||||
state.bodies.push(imported_box());
|
||||
let body = &state.bodies[0];
|
||||
let mut doc = Document::default();
|
||||
doc.appearances.faces.push(FaceLook {
|
||||
body: body.name.clone(),
|
||||
point: Vec3::ZERO,
|
||||
look: Look::custom([0, 255, 0], 1.0),
|
||||
name: Some(crate::naming::face_names(body)[0].clone()),
|
||||
strict_name: false,
|
||||
certificate: None,
|
||||
});
|
||||
let exported = doc.painted(&state);
|
||||
assert!(exported.bodies[0].body.paint().unwrap().faces.iter().any(|face| face.color == [0., 1., 0.]));
|
||||
let linked = doc.painted_for_link(&state);
|
||||
let faces = &linked.bodies[0].body.paint().unwrap().faces;
|
||||
assert_eq!(faces.len(), body.body.faces(0.01).unwrap().len());
|
||||
assert!(faces.iter().all(|face| face.color == [1., 0., 0.] && face.opacity == 0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_appearance_overrides_imported_face_paint() {
|
||||
check_imported_appearance_precedence(&imported_box());
|
||||
@@ -400,6 +604,130 @@ mod tests {
|
||||
check_imported_appearance_precedence(&body);
|
||||
}
|
||||
|
||||
fn chain_fixture() -> (Document, crate::ModelState) {
|
||||
let mut doc = Document::new("Appearance chain");
|
||||
doc.components.push(crate::Component { id: 1, name: "Part".into(), ..Default::default() });
|
||||
doc.body_components.insert("B".into(), 1);
|
||||
let mut state = crate::ModelState::default();
|
||||
state.bodies.push(crate::ModelBody::new("B".into(), solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap(), 1));
|
||||
(doc, state)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_chain_agrees_with_display_and_definition_identity() {
|
||||
let (mut doc, mut state) = chain_fixture();
|
||||
let body_look = library("ABS - Red").unwrap();
|
||||
let face_look = library("Steel - Satin PBR").unwrap();
|
||||
doc.appearances.bodies.insert("B".into(), body_look.clone());
|
||||
doc.appearances.components.insert(1, library("ABS - Blue").unwrap());
|
||||
doc.materials.insert("B".into(), "Brass".into());
|
||||
let body = &state.bodies[0];
|
||||
let spot = crate::naming::face_spots(body)[0].unwrap().point;
|
||||
let face_name = crate::naming::face_names(body)[0].clone();
|
||||
doc.appearances.faces.push(FaceLook {
|
||||
body: "B".into(),
|
||||
point: spot,
|
||||
look: face_look.clone(),
|
||||
name: Some(face_name),
|
||||
strict_name: false,
|
||||
certificate: None,
|
||||
});
|
||||
let chain = doc.appearance_chain(&state, AppearanceTarget::Face { body: "B", face: 0 }).unwrap();
|
||||
assert_eq!(chain.active(), Some("face"));
|
||||
assert_eq!(chain.resolved(), Some(&face_look));
|
||||
assert_eq!(doc.model_face_look(body, 0).as_ref(), chain.resolved());
|
||||
let chain = doc.appearance_chain(&state, AppearanceTarget::Body("B")).unwrap();
|
||||
assert_eq!(chain.active(), Some("body"));
|
||||
assert_eq!(doc.model_body_look(body).as_ref(), chain.resolved());
|
||||
assert_eq!(doc.body_look("B", 1).as_ref(), chain.resolved());
|
||||
assert_eq!(chain.component.as_ref().unwrap().name, "ABS - Blue");
|
||||
assert_eq!(chain.material.as_ref().unwrap().name, "Brass");
|
||||
// A computed placement resolves the definition's assignments, not its label.
|
||||
state.bodies[0].name = "Occurrence/B".into();
|
||||
state.bodies[0].source_name = Some("B".into());
|
||||
state.bodies[0].component = 1;
|
||||
let chain = doc.appearance_chain(&state, AppearanceTarget::Body("Occurrence/B")).unwrap();
|
||||
assert_eq!(chain.body, Some(body_look));
|
||||
assert_eq!(chain.material.as_ref().unwrap().name, "Brass");
|
||||
assert!(doc.appearance_chain(&state, AppearanceTarget::Face { body: "Occurrence/B", face: usize::MAX }).is_err());
|
||||
assert!(doc.appearance_chain(&state, AppearanceTarget::Body("missing")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_chain_component_material_is_common_or_absent_and_imports_stay_fallbacks() {
|
||||
let (mut doc, mut state) = chain_fixture();
|
||||
state.bodies[0].imported = Some(std::sync::Arc::new(ImportedLook { color: Some([0.2, 0.4, 0.6]), ..Default::default() }));
|
||||
let body = doc.appearance_chain(&state, AppearanceTarget::Body("B")).unwrap();
|
||||
assert_eq!(body.active(), None);
|
||||
assert!(doc.model_display_look(&state.bodies[0]).is_some());
|
||||
doc.materials.insert("B".into(), "Brass".into());
|
||||
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), Some("material"));
|
||||
let mut second = state.bodies[0].clone();
|
||||
second.name = "C".into();
|
||||
doc.body_components.insert("C".into(), 1);
|
||||
state.bodies.push(second);
|
||||
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), None);
|
||||
doc.materials.insert("C".into(), "Brass".into());
|
||||
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), Some("material"));
|
||||
doc.materials.insert("C".into(), "Steel".into());
|
||||
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().material, None);
|
||||
doc.appearances.components.insert(1, library("ABS - Blue").unwrap());
|
||||
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().active(), Some("component"));
|
||||
state.bodies.clear();
|
||||
assert_eq!(doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap().material, None);
|
||||
assert!(doc.appearance_chain(&state, AppearanceTarget::Component(0)).is_err());
|
||||
assert!(doc.appearance_chain(&state, AppearanceTarget::Component(99)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_replacement_validates_values_and_design_budget_without_mutation() {
|
||||
let from = library("ABS - Red").unwrap();
|
||||
let to = library("ABS - Blue").unwrap();
|
||||
let mut appearances = Appearances::default();
|
||||
appearances.bodies.insert("B".into(), from.clone());
|
||||
for opacity in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1., 2.] {
|
||||
let mut invalid = to.clone();
|
||||
invalid.opacity = opacity;
|
||||
assert!(appearances.replacing(&from, &invalid, None).is_err());
|
||||
assert!(appearances.replacing(&invalid, &to, None).is_err());
|
||||
assert_eq!(appearances.bodies["B"], from);
|
||||
}
|
||||
let mut invalid = library("Steel - Satin PBR").unwrap();
|
||||
invalid.opacity = 0.5;
|
||||
assert!(appearances.replacing(&from, &invalid, None).is_err());
|
||||
let (changed, count) = appearances.replacing(&from, &to, None).unwrap().unwrap();
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(changed.bodies["B"], to);
|
||||
assert_eq!(appearances.bodies["B"], from);
|
||||
assert!(appearances.replacing(&from, &from, None).unwrap().is_none());
|
||||
for i in 0..100_000 {
|
||||
appearances.components.insert(i, from.clone());
|
||||
}
|
||||
assert!(appearances.replacing(&from, &to, None).is_err());
|
||||
let (mut doc, state) = chain_fixture();
|
||||
doc.appearances = appearances;
|
||||
assert!(doc.appearance_chain(&state, AppearanceTarget::Body("B")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appearance_chain_of_ten_thousand_body_component_has_four_steps() {
|
||||
let (mut doc, mut state) = chain_fixture();
|
||||
let mut body = state.bodies[0].clone();
|
||||
body.source_name = Some("B".into());
|
||||
body.component = 1;
|
||||
state.bodies = (0..10_000)
|
||||
.map(|i| {
|
||||
let mut b = body.clone();
|
||||
b.name = format!("Placed {i}");
|
||||
b
|
||||
})
|
||||
.collect();
|
||||
doc.materials.insert("B".into(), "Brass".into());
|
||||
let chain = doc.appearance_chain(&state, AppearanceTarget::Component(1)).unwrap();
|
||||
assert_eq!(chain.steps().len(), 4);
|
||||
assert_eq!(chain.active(), Some("material"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_and_materials() {
|
||||
assert_eq!(library("abs - red").map(|l| l.color), Some([196, 40, 40]));
|
||||
@@ -487,6 +815,8 @@ mod tests {
|
||||
bad.boundary = GlassBoundary::Thin { thickness_mm: 10001.0 };
|
||||
assert!(bad.validate().is_err());
|
||||
}
|
||||
// The swatch shows the transmission as sRGB codes.
|
||||
assert_eq!(find("starter.glass.smoky-neutral").unwrap().preview_color(), [160, 166, 173]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -154,10 +154,7 @@ impl VisualPreset {
|
||||
match self.recipe {
|
||||
Recipe::Opaque(r) => r.color,
|
||||
// A UI swatch of the transmission values, not rendered glass or absorption.
|
||||
Recipe::Glass(r) => r.transmission_linear_rgb.map(|v| {
|
||||
let encoded = if v <= 0.0031308 { 12.92 * v } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 };
|
||||
(encoded.clamp(0.0, 1.0) * 255.0).round() as u8
|
||||
}),
|
||||
Recipe::Glass(r) => r.transmission_linear_rgb.map(solidcraft_geom::srgb::encode8),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,48 @@ fn identity() -> Mat {
|
||||
IDENTITY
|
||||
}
|
||||
|
||||
/// How a component counts in the bill of materials of an assembly it is placed in. It is a part
|
||||
/// property (the `bom_type` key of the component's [`Metadata`](crate::Metadata)); a component
|
||||
/// without one is [`BomType::Normal`].
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BomType {
|
||||
/// A row of its own; every occurrence adds to its quantity.
|
||||
#[default]
|
||||
Normal,
|
||||
/// No row of its own: what it holds counts as if placed directly in its parent.
|
||||
Phantom,
|
||||
/// Shown, but counted nowhere, and nothing inside it is counted either.
|
||||
Reference,
|
||||
/// Bought: one row, with nothing inside it counted or listed.
|
||||
Purchased,
|
||||
/// Made as one item that doesn't come apart (a welded frame): one row in the assembly it is
|
||||
/// placed in. What it holds is listed in its own bill of materials, and its purchased parts
|
||||
/// also in the parts-only list of the assembly it is placed in.
|
||||
Inseparable,
|
||||
}
|
||||
|
||||
impl BomType {
|
||||
/// Every type, in the order commands and documentation list them.
|
||||
pub const ALL: [BomType; 5] = [BomType::Normal, BomType::Phantom, BomType::Reference, BomType::Purchased, BomType::Inseparable];
|
||||
|
||||
/// The word the design file and commands use.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
BomType::Normal => "normal",
|
||||
BomType::Phantom => "phantom",
|
||||
BomType::Reference => "reference",
|
||||
BomType::Purchased => "purchased",
|
||||
BomType::Inseparable => "inseparable",
|
||||
}
|
||||
}
|
||||
|
||||
/// The type a word names, if it names one.
|
||||
pub fn parse(word: &str) -> Option<BomType> {
|
||||
BomType::ALL.into_iter().find(|t| t.as_str() == word)
|
||||
}
|
||||
}
|
||||
|
||||
/// How deep and how many placements [`Document::placements`] walks.
|
||||
pub const MAX_PLACEMENT_DEPTH: usize = 64;
|
||||
pub const MAX_PLACEMENTS: usize = 100_000;
|
||||
|
||||
@@ -2617,13 +2617,42 @@ pub struct StandardPart {
|
||||
/// A component whose contents come from another design file (Insert Design): where that file
|
||||
/// is, and the Import feature in the component that holds its bodies (Update Links replaces
|
||||
/// them; Break Link keeps them and forgets the file).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DesignLink {
|
||||
/// The design file: relative to this design's folder once this design is saved, else as
|
||||
/// given (`/` separates folders).
|
||||
pub path: String,
|
||||
/// The Import feature holding the linked design's bodies.
|
||||
pub feature: u64,
|
||||
/// Last inherited values, by Import body slot. Differing assembly assignments are
|
||||
/// local overrides and survive a source update. Older links start with no baseline.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub inherited: Vec<LinkedBodyProperties>,
|
||||
}
|
||||
|
||||
/// The values last inherited by a linked body, not another editable material library.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LinkedBodyProperties {
|
||||
pub material: String,
|
||||
/// SHA-256 of the inherited custom definition, for equality only. The complete
|
||||
/// engineering card stays in user_materials, not duplicated once per linked body.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub material_fingerprint: Option<String>,
|
||||
/// Explicit body/component look only: material fallback must remain a fallback.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub appearance: Option<crate::appearance::Look>,
|
||||
/// Last inherited authored face overrides. None marks links saved before face
|
||||
/// inheritance; Some(empty) records that the source has no authored face overrides.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub faces: Option<Vec<crate::appearance::FaceLook>>,
|
||||
}
|
||||
|
||||
impl LinkedBodyProperties {
|
||||
pub fn fingerprint(material: &UserMaterial) -> Result<String> {
|
||||
use sha2::{Digest, Sha256};
|
||||
let bytes = serde_json::to_vec(material).map_err(|e| DocError::Invalid(e.to_string()))?;
|
||||
Ok(format!("{:x}", Sha256::digest(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A name the design's counter hands out (`Body1`).
|
||||
@@ -3131,14 +3160,22 @@ impl Document {
|
||||
/// The model with each body carrying the colour, opacity and face colours it is shown with
|
||||
/// (for export).
|
||||
pub fn painted(&self, state: &crate::ModelState) -> crate::ModelState {
|
||||
self.painted_with_faces(state, true)
|
||||
}
|
||||
|
||||
/// Linked designs carry authored face overrides as editable properties. Their STEP
|
||||
/// geometry keeps imported paint, without baking a second copy of those overrides.
|
||||
pub fn painted_for_link(&self, state: &crate::ModelState) -> crate::ModelState {
|
||||
self.painted_with_faces(state, false)
|
||||
}
|
||||
|
||||
fn painted_with_faces(&self, state: &crate::ModelState, authored_faces: bool) -> crate::ModelState {
|
||||
let mut st = state.clone();
|
||||
for b in &mut st.bodies {
|
||||
let c = self.body_color(b);
|
||||
let faces: Vec<solidcraft_kernel::FacePaint> = self
|
||||
.face_colors(b)
|
||||
.into_iter()
|
||||
.map(|(face, l)| solidcraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 })
|
||||
.collect();
|
||||
let face_colors = if authored_faces { self.face_colors(b) } else { self.imported_face_colors(b) };
|
||||
let faces: Vec<solidcraft_kernel::FacePaint> =
|
||||
face_colors.into_iter().map(|(face, l)| solidcraft_kernel::FacePaint { face, color: l.rgb(), opacity: l.opacity as f32 }).collect();
|
||||
let opacity = self.model_body_opacity(b) as f32;
|
||||
let paint = solidcraft_kernel::Paint { opacity, faces };
|
||||
if c != b.body.color() || b.body.paint() != Some(&paint) {
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
//! A drawing (#3890): sheets of paper that draw designs saved in their own files. A drawing is a
|
||||
//! file of its own, `"format": "solidcraft-drawing/N"`, versioned by the same code as designs
|
||||
//! ([`DRAWING`]); format 1 is this record.
|
||||
//!
|
||||
//! - **Sources** are the designs it draws: an id and the design file's path, relative to the
|
||||
//! drawing's folder once the drawing is saved, else as given, with `/` between folders (a linked
|
||||
//! design's [`DesignLink`](crate::DesignLink) rule). What a design was when the drawing last saw
|
||||
//! it is not saved here.
|
||||
//! - **Sheets**: the paper, a copy of its drawing border, the resolution its pictures are rendered
|
||||
//! at, the component it documents, and whether it is left out of the sheet count or of printing.
|
||||
//! The border is a copy of the profile it was made with, never a name, so a later built-in never
|
||||
//! changes a saved drawing.
|
||||
//! - **Units** follow each view's design or are one length unit for the whole drawing. The
|
||||
//! **language** is the content's, a BCP 47 tag chosen when the drawing is made, whatever the
|
||||
//! interface's is. **Properties** follow [`Metadata`]'s rules.
|
||||
//!
|
||||
//! The rules, which every later kind of record (views, annotations, tables) keeps too:
|
||||
//! - Every object has an id of [`crate::ids`]: random, so objects added side by side never take
|
||||
//! the same one, and written as 13 characters of base32. No two objects share an id, and a run
|
||||
//! never gives a removed object's id again ([`Drawing::allocate_id`]).
|
||||
//! - Every reference between records is an id and must resolve: a file with one that doesn't is
|
||||
//! damaged and refused, never patched.
|
||||
//! - 1 to [`MAX_SHEETS`] sheets and at most [`MAX_SOURCES`] sources; names of 1 to
|
||||
//! [`MAX_NAME_BYTES`] bytes; paths of 1 to [`MAX_PATH_BYTES`] bytes, without NUL.
|
||||
//! - Nothing derived is saved, and a drawing always saves as the same bytes: fields in their
|
||||
//! order, maps sorted.
|
||||
//! - A later change to the record is one file in `src/drawing/format/pending/` (its README).
|
||||
//!
|
||||
//! [`Drawing::from_json`] reads a file in this order: at most [`MAX_DRAWING_BYTES`] of text, the
|
||||
//! family's format check, every number in range, the upgrade, the typed record, then
|
||||
//! [`Drawing::validate`].
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use solidcraft_geom::drawing_sheet::{self, DrawingBorderProfile};
|
||||
|
||||
use crate::format::DRAWING;
|
||||
use crate::ids::{self, IdSource};
|
||||
use crate::{DocError, Metadata, Result, expr};
|
||||
|
||||
/// The most sheets a drawing may have.
|
||||
pub const MAX_SHEETS: usize = 1000;
|
||||
/// The most designs a drawing may draw.
|
||||
pub const MAX_SOURCES: usize = 64;
|
||||
/// The longest name of a drawing or a sheet, in bytes.
|
||||
pub const MAX_NAME_BYTES: usize = 256;
|
||||
/// The longest path to a design, in bytes.
|
||||
pub const MAX_PATH_BYTES: usize = 4096;
|
||||
/// The longest language tag: what RFC 5646 asks every reader to take (§4.4.1).
|
||||
pub const MAX_LANGUAGE_CHARS: usize = 35;
|
||||
/// The largest drawing read, as JSON text.
|
||||
pub const MAX_DRAWING_BYTES: usize = 16 << 20;
|
||||
/// The resolution a sheet's pictures (its shaded views) are rendered at unless it says, in dots
|
||||
/// per inch.
|
||||
pub const DEFAULT_DPI: f64 = 300.0;
|
||||
/// The lowest resolution a sheet may ask for.
|
||||
pub const MIN_DPI: f64 = 25.0;
|
||||
/// The highest resolution a sheet may ask for.
|
||||
pub const MAX_DPI: f64 = 2400.0;
|
||||
|
||||
/// A drawing: its sheets and the designs they draw.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Drawing {
|
||||
/// `solidcraft-drawing/N`: the format this build writes, whatever the file said.
|
||||
pub format: String,
|
||||
/// The changes of `format` this drawing has while they are pending
|
||||
/// ([`Family::pending_names`](crate::format::Family::pending_names)). Empty, and not saved,
|
||||
/// otherwise.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub format_pending: Vec<String>,
|
||||
pub name: String,
|
||||
/// The designs the drawing draws.
|
||||
pub sources: Vec<Source>,
|
||||
/// The drawing's properties and other tools' data, kept as they are.
|
||||
#[serde(default, skip_serializing_if = "Metadata::is_empty")]
|
||||
pub properties: Metadata,
|
||||
#[serde(default)]
|
||||
pub units: Units,
|
||||
/// The language of the drawing's content, a BCP 47 tag (`en-GB`).
|
||||
pub language: String,
|
||||
/// In order, each behind its own `Arc`, so an undo step shares every sheet it didn't change.
|
||||
pub sheets: Vec<Arc<Sheet>>,
|
||||
}
|
||||
|
||||
/// A design the drawing draws.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Source {
|
||||
#[serde(with = "crate::ids")]
|
||||
pub id: u64,
|
||||
/// The design file: relative to the drawing's folder once the drawing is saved, else as given
|
||||
/// (`/` separates folders).
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// The length unit a drawing gives its measurements in.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Units {
|
||||
/// Each view's design's own units.
|
||||
#[default]
|
||||
FollowModel,
|
||||
/// One length unit for the whole drawing, by its name in [`expr::UNITS`] (`mm`, `in`).
|
||||
Fixed { unit: String },
|
||||
}
|
||||
|
||||
/// One sheet of a drawing.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Sheet {
|
||||
#[serde(with = "crate::ids")]
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
/// The paper: its size and the way it lies.
|
||||
pub sheet: drawing_sheet::Sheet,
|
||||
/// A copy of the drawing border's profile (`None`: no drawing border).
|
||||
pub border: Option<DrawingBorderProfile>,
|
||||
/// The resolution its pictures (its shaded views) are rendered at, in dots per inch.
|
||||
#[serde(default = "default_dpi")]
|
||||
pub dpi: f64,
|
||||
/// The component the sheet documents, which its title block reads.
|
||||
pub documents: Option<SheetComponent>,
|
||||
#[serde(default)]
|
||||
pub excluded: Excluded,
|
||||
}
|
||||
|
||||
/// A component of one of the drawing's designs.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SheetComponent {
|
||||
/// The [`Source`]'s id.
|
||||
#[serde(with = "crate::ids")]
|
||||
pub source: u64,
|
||||
/// The component's id in that design, as the design writes it (0: the design itself).
|
||||
pub component: u64,
|
||||
}
|
||||
|
||||
/// What a sheet is left out of.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Excluded {
|
||||
/// The sheet count ("sheet 2 of 3" on the other sheets).
|
||||
pub count: bool,
|
||||
/// Printing and exporting every sheet.
|
||||
pub print: bool,
|
||||
}
|
||||
|
||||
fn default_dpi() -> f64 {
|
||||
DEFAULT_DPI
|
||||
}
|
||||
|
||||
impl Drawing {
|
||||
/// A drawing of no designs yet, named `name` and written in `language`, with one sheet named
|
||||
/// `sheet`: the paper `paper` with a copy of `border`, its id from `id_source`.
|
||||
pub fn new(
|
||||
name: &str,
|
||||
language: &str,
|
||||
sheet: &str,
|
||||
paper: drawing_sheet::Sheet,
|
||||
border: Option<DrawingBorderProfile>,
|
||||
id_source: &mut IdSource,
|
||||
) -> Result<Drawing> {
|
||||
let id = id_source.next(&BTreeSet::new())?;
|
||||
let first = Sheet { id, name: sheet.into(), sheet: paper, border, dpi: DEFAULT_DPI, documents: None, excluded: Excluded::default() };
|
||||
let drawing = Drawing {
|
||||
format: DRAWING.format_string(DRAWING.current()),
|
||||
format_pending: DRAWING.pending_names(),
|
||||
name: name.into(),
|
||||
sources: Vec::new(),
|
||||
properties: Metadata::default(),
|
||||
units: Units::default(),
|
||||
language: language.into(),
|
||||
sheets: vec![Arc::new(first)],
|
||||
};
|
||||
drawing.validate()?;
|
||||
Ok(drawing)
|
||||
}
|
||||
|
||||
/// Read a drawing file's JSON (any format this build knows; older ones are upgraded). A file
|
||||
/// that breaks a rule of the record is damaged and refused, never patched.
|
||||
pub fn from_json(s: &str) -> Result<Drawing> {
|
||||
if s.len() > MAX_DRAWING_BYTES {
|
||||
return Err(DocError::Invalid(format!("drawing too large (at most {} MiB)", MAX_DRAWING_BYTES >> 20)));
|
||||
}
|
||||
let mut v: Value = serde_json::from_str(s).map_err(|e| DocError::Invalid(format!("not a SolidCraft drawing: {e}")))?;
|
||||
let (version, applied) = DRAWING.take_version(&mut v)?;
|
||||
DRAWING.check_numbers(&v, "properties")?;
|
||||
DRAWING.upgrade(&mut v, version, applied.as_deref())?;
|
||||
let d: Drawing = serde_json::from_value(v).map_err(damaged)?;
|
||||
d.validate().map_err(damaged)?;
|
||||
Ok(Drawing { format: DRAWING.format_string(DRAWING.current()), format_pending: DRAWING.pending_names(), ..d })
|
||||
}
|
||||
|
||||
/// The drawing's JSON: the same bytes for the same drawing. JSON longer than a read takes
|
||||
/// ([`MAX_DRAWING_BYTES`]) is refused, so every drawing saved opens again: other tools'
|
||||
/// properties are kept as they came, and indenting them can make a drawing that read in
|
||||
/// under the limit save over it.
|
||||
pub fn to_json(&self) -> Result<String> {
|
||||
let text = serde_json::to_string_pretty(self).map_err(|e| DocError::Invalid(format!("drawing not saved: {e}")))?;
|
||||
require(text.len() <= MAX_DRAWING_BYTES, || format!("drawing too large to save (at most {} MiB)", MAX_DRAWING_BYTES >> 20))?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
/// An id for a new object: one no object of the drawing has, from `id_source`, which never
|
||||
/// gives one twice.
|
||||
pub fn allocate_id(&self, id_source: &mut IdSource) -> Result<u64> {
|
||||
id_source.next(&self.ids().collect())
|
||||
}
|
||||
|
||||
/// Every object's id, of every kind: what a new id must differ from. Each later kind adds its
|
||||
/// own here.
|
||||
fn ids(&self) -> impl Iterator<Item = u64> {
|
||||
self.sources.iter().map(|s| s.id).chain(self.sheets.iter().map(|s| s.id))
|
||||
}
|
||||
|
||||
/// The record's rules (the module's notes): what reading a file checks, and what every edit
|
||||
/// keeps.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
require((1..=MAX_SHEETS).contains(&self.sheets.len()), || format!("a drawing has 1 to {MAX_SHEETS} sheets"))?;
|
||||
require(self.sources.len() <= MAX_SOURCES, || format!("a drawing draws at most {MAX_SOURCES} designs"))?;
|
||||
check_name(&self.name, "the drawing's")?;
|
||||
require(is_language_tag(&self.language), || {
|
||||
format!("the language is not a BCP 47 tag of 2 to {MAX_LANGUAGE_CHARS} characters (such as `en` or `en-GB`)")
|
||||
})?;
|
||||
if let Units::Fixed { unit } = &self.units {
|
||||
// A length unit of the expression unit table, written as the table writes it.
|
||||
let length = unit.trim() == unit.as_str() && expr::unit_info(unit).is_some_and(|(kind, _)| kind == expr::Kind::Length);
|
||||
require(length, || "the drawing's unit is not a length unit (such as `mm` or `in`)".into())?;
|
||||
}
|
||||
let mut seen = BTreeSet::new();
|
||||
for id in self.ids() {
|
||||
require(id != 0, || "an object has the id 0, which is never one".into())?;
|
||||
require(seen.insert(id), || format!("two objects have the id {}", ids::encode(id)))?;
|
||||
}
|
||||
for source in &self.sources {
|
||||
let path = &source.path;
|
||||
require(!path.is_empty() && path.len() <= MAX_PATH_BYTES && !path.contains('\0'), || {
|
||||
format!("source {}: a design's path is 1 to {MAX_PATH_BYTES} bytes, without NUL", ids::encode(source.id))
|
||||
})?;
|
||||
}
|
||||
for sheet in &self.sheets {
|
||||
self.check_sheet(sheet).map_err(|e| DocError::Invalid(format!("sheet {}: {e}", ids::encode(sheet.id))))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A sheet's own rules, and that the source it documents is one of the drawing's.
|
||||
fn check_sheet(&self, sheet: &Sheet) -> Result<()> {
|
||||
check_name(&sheet.name, "its")?;
|
||||
sheet.sheet.size_mm().map_err(from_sheet)?;
|
||||
if let Some(border) = &sheet.border {
|
||||
border.validate().map_err(from_sheet)?;
|
||||
}
|
||||
require((MIN_DPI..=MAX_DPI).contains(&sheet.dpi), || format!("its pictures are {MIN_DPI} to {MAX_DPI} dpi"))?;
|
||||
match sheet.documents {
|
||||
Some(SheetComponent { source, .. }) => {
|
||||
let named = || format!("it documents source {}, which the drawing doesn't have", ids::encode(source));
|
||||
require(self.sources.iter().any(|s| s.id == source), named)
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Ok` when `ok`, else the rule `broken` names.
|
||||
fn require(ok: bool, broken: impl FnOnce() -> String) -> Result<()> {
|
||||
if ok { Ok(()) } else { Err(DocError::Invalid(broken())) }
|
||||
}
|
||||
|
||||
/// A name: 1 to [`MAX_NAME_BYTES`] bytes, not only spaces, without control characters.
|
||||
fn check_name(name: &str, whose: &str) -> Result<()> {
|
||||
require(!name.trim().is_empty() && name.len() <= MAX_NAME_BYTES && !name.chars().any(char::is_control), || {
|
||||
format!("{whose} name is 1 to {MAX_NAME_BYTES} bytes, not only spaces, without control characters")
|
||||
})
|
||||
}
|
||||
|
||||
/// A well-formed language tag, as far as a drawing needs (RFC 5646 §2.1): 2 to
|
||||
/// [`MAX_LANGUAGE_CHARS`] characters, subtags of 1 to 8 ASCII letters and digits joined by `-`,
|
||||
/// the first of 2 to 8 letters (`en`, `en-GB`, `zh-Hans-CN`). Whether the subtags are registered
|
||||
/// isn't checked, and a private-use tag alone (`x-…`) isn't taken.
|
||||
fn is_language_tag(tag: &str) -> bool {
|
||||
let mut subtags = tag.split('-');
|
||||
let first = subtags.next().unwrap_or_default();
|
||||
(2..=MAX_LANGUAGE_CHARS).contains(&tag.len())
|
||||
&& (2..=8).contains(&first.len())
|
||||
&& first.bytes().all(|b| b.is_ascii_alphabetic())
|
||||
&& subtags.all(|s| (1..=8).contains(&s.len()) && s.bytes().all(|b| b.is_ascii_alphanumeric()))
|
||||
}
|
||||
|
||||
/// A file that breaks a rule of the record.
|
||||
fn damaged(e: impl std::fmt::Display) -> DocError {
|
||||
DocError::Invalid(format!("damaged drawing: {e}"))
|
||||
}
|
||||
|
||||
fn from_sheet(e: drawing_sheet::Invalid) -> DocError {
|
||||
DocError::Invalid(e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "drawing_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,30 @@
|
||||
# Pending drawing-format changes
|
||||
|
||||
One file per change to what a drawing file saves that no release has shipped yet. Drawings
|
||||
(`"format": "solidcraft-drawing/N"`, #3890) are versioned by the same code as designs
|
||||
(`crates/doc/src/format.rs`, `Family`), from their own first format, 1. Adding a file here is the
|
||||
whole format bump: never edit the `DRAWING` family in `format.rs`, another change's file or a
|
||||
`../v<N>/` folder.
|
||||
|
||||
- Name it after the change, lowercase with dashes: `sheet-views.rs`.
|
||||
- Start it with a `//!` note: what is saved, what an older drawing gets without it, and what
|
||||
the last release does with a drawing that has it (it refuses the newer format number).
|
||||
- If older drawings must be rewritten as they open, add the step to the same file:
|
||||
|
||||
```rust
|
||||
//! …the note…
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn step(v: &mut Value) -> crate::Result<()> {
|
||||
// rewrite the drawing's JSON here
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Steps run in file-name order, after the steps of every older format.
|
||||
|
||||
While this folder has a change in it, the build writes the next drawing format number (the last
|
||||
folded one + 1, which no release reads), and lists the changes a drawing has in its
|
||||
`format_pending` field, so a later build applies only the steps that drawing is missing. At
|
||||
release time `scripts/fold-format-pending.sh` moves these files into `../v<N>/`, as it does the
|
||||
design format's (`crates/doc/src/format/pending/README.md`).
|
||||
@@ -0,0 +1,553 @@
|
||||
use serde_json::json;
|
||||
use solidcraft_geom::drawing_sheet::{Margins, Orientation, PaperSize, SheetSize};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The seed every test drawing's ids come from.
|
||||
const SEED: u64 = 3890;
|
||||
|
||||
/// A profile of its own: ISO 5457's filing margin and trimming marks, which the built-in leaves off.
|
||||
fn own_border() -> DrawingBorderProfile {
|
||||
DrawingBorderProfile { filing_margin: true, trimming_mark_mm: Some([10.0, 5.0]), ..DrawingBorderProfile::iso() }
|
||||
}
|
||||
|
||||
/// Three designs on two sheets: the first sheet (A3, the ISO border) documents component 7 of the
|
||||
/// first design; the second (A4 portrait, a border of its own, 600 dpi, left out of printing)
|
||||
/// documents the second design. The third design is on no sheet yet.
|
||||
fn plate() -> Drawing {
|
||||
let mut id_source = IdSource::seeded(SEED);
|
||||
let a3 = drawing_sheet::Sheet::standard(PaperSize::A3, Orientation::Landscape);
|
||||
let mut d = Drawing::new("Plate assembly", "en-GB", "Assembly", a3, Some(DrawingBorderProfile::iso()), &mut id_source).unwrap();
|
||||
for path in ["plate.sc", "../parts/bolt.sc", "bracket.sc"] {
|
||||
let id = d.allocate_id(&mut id_source).unwrap();
|
||||
d.sources.push(Source { id, path: path.into() });
|
||||
}
|
||||
let (plate_id, bolt_id) = (d.sources[0].id, d.sources[1].id);
|
||||
let id = d.allocate_id(&mut id_source).unwrap();
|
||||
d.sheets.push(Arc::new(Sheet {
|
||||
id,
|
||||
name: "Bolt".into(),
|
||||
sheet: drawing_sheet::Sheet::standard(PaperSize::A4, Orientation::Portrait),
|
||||
border: Some(own_border()),
|
||||
dpi: 600.0,
|
||||
documents: Some(SheetComponent { source: bolt_id, component: 0 }),
|
||||
excluded: Excluded { count: false, print: true },
|
||||
}));
|
||||
Arc::make_mut(&mut d.sheets[0]).documents = Some(SheetComponent { source: plate_id, component: 7 });
|
||||
d.properties.set(crate::metadata::Owner::Design, "author", json!("Jane Doe")).unwrap();
|
||||
d.units = Units::Fixed { unit: "in".into() };
|
||||
d.validate().unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// One A4 sheet that documents nothing, no sources: the base the caps grow from.
|
||||
fn bare() -> Drawing {
|
||||
let a4 = drawing_sheet::Sheet::standard(PaperSize::A4, Orientation::Landscape);
|
||||
Drawing::new("Bare", "en", "Sheet", a4, None, &mut IdSource::seeded(SEED)).unwrap()
|
||||
}
|
||||
|
||||
fn file(d: &Drawing) -> Value {
|
||||
serde_json::from_str(&d.to_json().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn read(file: &Value) -> Result<Drawing> {
|
||||
Drawing::from_json(&file.to_string())
|
||||
}
|
||||
|
||||
/// Why `file` is refused, or "read".
|
||||
fn refusal(file: &Value) -> String {
|
||||
read(file).map_or_else(|e| e.to_string(), |_| "read".into())
|
||||
}
|
||||
|
||||
/// Why `d`'s file, changed by `edit`, is refused, or "read".
|
||||
fn refused(d: &Drawing, edit: impl FnOnce(&mut Value)) -> String {
|
||||
let mut v = file(d);
|
||||
edit(&mut v);
|
||||
refusal(&v)
|
||||
}
|
||||
|
||||
/// A drawing reads back as it was saved, and save, open and save again writes the same bytes, as
|
||||
/// does another run from the same seed. The file holds each field under the record's name for it,
|
||||
/// ids as their 13 characters, and no counter.
|
||||
#[test]
|
||||
fn a_drawing_reads_back_as_saved_and_saves_the_same_bytes_again() {
|
||||
let d = plate();
|
||||
let saved = d.to_json().unwrap();
|
||||
let reopened = Drawing::from_json(&saved).unwrap();
|
||||
assert_eq!(reopened, d);
|
||||
assert_eq!(reopened.to_json().unwrap(), saved);
|
||||
assert_eq!(plate().to_json().unwrap(), saved, "a seeded run makes the same drawing");
|
||||
let v = file(&d);
|
||||
let [plate_id, bolt_id, bracket_id] = [0, 1, 2].map(|i| ids::encode(d.sources[i].id));
|
||||
let [assembly_id, bolt_sheet_id] = [0, 1].map(|i| ids::encode(d.sheets[i].id));
|
||||
assert_eq!(assembly_id, "1ge30wkvq38r0", "the seed's first id, as splitmix64 gives it");
|
||||
assert_eq!(v["format"], DRAWING.format_string(DRAWING.current()));
|
||||
assert_eq!(v.get("format_pending").is_some(), !DRAWING.pending_names().is_empty());
|
||||
assert_eq!(v.get("next_id"), None);
|
||||
assert_eq!((&v["name"], &v["language"]), (&json!("Plate assembly"), &json!("en-GB")));
|
||||
assert_eq!(
|
||||
v["sources"],
|
||||
json!([{"id": plate_id, "path": "plate.sc"}, {"id": bolt_id, "path": "../parts/bolt.sc"}, {"id": bracket_id, "path": "bracket.sc"}])
|
||||
);
|
||||
assert_eq!(v["properties"], json!({"author": "Jane Doe"}));
|
||||
assert_eq!(v["units"], json!({"fixed": {"unit": "in"}}));
|
||||
assert_eq!((&v["sheets"][0]["id"], &v["sheets"][0]["dpi"]), (&json!(assembly_id), &json!(300.0)));
|
||||
assert_eq!(v["sheets"][0]["sheet"], json!({"size": {"standard": "a3"}, "orientation": "landscape"}));
|
||||
assert_eq!(v["sheets"][0]["documents"], json!({"source": plate_id, "component": 7}));
|
||||
assert_eq!(v["sheets"][0]["excluded"], json!({"count": false, "print": false}));
|
||||
assert_eq!(
|
||||
v["sheets"][1],
|
||||
json!({
|
||||
"id": bolt_sheet_id, "name": "Bolt", "sheet": {"size": {"standard": "a4"}, "orientation": "portrait"},
|
||||
"border": serde_json::to_value(own_border()).unwrap(), "dpi": 600.0,
|
||||
"documents": {"source": bolt_id, "component": 0}, "excluded": {"count": false, "print": true}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// A hand-written file may leave out what has a default, and reads as a new drawing has it; the
|
||||
/// rest is needed.
|
||||
#[test]
|
||||
fn a_file_may_leave_out_what_has_a_default() {
|
||||
let minimal = json!({
|
||||
"format": DRAWING.format_string(1),
|
||||
"name": "Bracket",
|
||||
"sources": [],
|
||||
"language": "de",
|
||||
"sheets": [{"id": "0000000000001", "name": "Blatt 1", "sheet": {"size": {"standard": "a3"}}}]
|
||||
});
|
||||
let d = read(&minimal).unwrap();
|
||||
assert_eq!((d.sheets[0].id, &d.units, d.properties.is_empty()), (1, &Units::FollowModel, true));
|
||||
let sheet = &d.sheets[0];
|
||||
assert_eq!(sheet.sheet, drawing_sheet::Sheet::standard(PaperSize::A3, Orientation::Landscape));
|
||||
assert_eq!((sheet.border, sheet.dpi, sheet.documents, sheet.excluded), (None, DEFAULT_DPI, None, Excluded::default()));
|
||||
// The format as this build writes it, however the file spelt it.
|
||||
let mut spelt = minimal.clone();
|
||||
spelt["format"] = json!("solidcraft-drawing/+1");
|
||||
assert_eq!(read(&spelt).unwrap().format, DRAWING.format_string(DRAWING.current()));
|
||||
for key in ["name", "sources", "language", "sheets"] {
|
||||
let mut v = minimal.clone();
|
||||
v.as_object_mut().unwrap().remove(key);
|
||||
let why = refusal(&v);
|
||||
assert!(why.starts_with("damaged drawing: ") && why.contains(&format!("missing field `{key}`")), "{why}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every cap on both sides: sheets, sources, the bytes of paths and names (two-byte characters too).
|
||||
#[test]
|
||||
fn every_cap_holds_on_both_sides() {
|
||||
let base = file(&bare());
|
||||
let with = |key: &str, value: Value| {
|
||||
let mut v = base.clone();
|
||||
v[key] = value;
|
||||
v
|
||||
};
|
||||
let sheet = base["sheets"][0].clone();
|
||||
let sheets = |n: u64| {
|
||||
let one = |id: u64| {
|
||||
let mut s = sheet.clone();
|
||||
s["id"] = json!(ids::encode(id));
|
||||
s
|
||||
};
|
||||
Value::Array((1..=n).map(one).collect())
|
||||
};
|
||||
assert_eq!(read(&with("sheets", sheets(1000))).unwrap().sheets.len(), 1000);
|
||||
for n in [0, 1001] {
|
||||
assert_eq!(refusal(&with("sheets", sheets(n))), "damaged drawing: a drawing has 1 to 1000 sheets", "{n} sheets");
|
||||
}
|
||||
// Sources from id 1: the sheet's id is the seed's, far from them.
|
||||
let sources = |n: u64| Value::Array((1..=n).map(|id| json!({"id": ids::encode(id), "path": format!("part{id}.sc")})).collect());
|
||||
assert_eq!(read(&with("sources", sources(64))).unwrap().sources.len(), 64);
|
||||
assert_eq!(refusal(&with("sources", sources(65))), "damaged drawing: a drawing draws at most 64 designs");
|
||||
let path = |p: &str| refusal(&with("sources", json!([{"id": ids::encode(1), "path": p}])));
|
||||
for fits in ["a".repeat(4096), "é".repeat(2048), "/".into()] {
|
||||
assert_eq!(path(fits.as_str()), "read", "{} bytes", fits.len());
|
||||
}
|
||||
let rule = "damaged drawing: source 0000000000001: a design's path is 1 to 4096 bytes, without NUL";
|
||||
for broken in ["a".repeat(4097), "é".repeat(2048) + "a", String::new(), "plate\0.sc".into()] {
|
||||
assert_eq!(path(broken.as_str()), rule, "{} bytes", broken.len());
|
||||
}
|
||||
let names = |name: &str| {
|
||||
let mut sheet_named = base.clone();
|
||||
sheet_named["sheets"][0]["name"] = json!(name);
|
||||
(refusal(&with("name", json!(name))), refusal(&sheet_named))
|
||||
};
|
||||
for fits in ["x".repeat(256), "é".repeat(128), "A".into(), "Blatt 1 – Übersicht".into()] {
|
||||
assert_eq!(names(fits.as_str()), ("read".to_string(), "read".to_string()), "{} bytes", fits.len());
|
||||
}
|
||||
let rule = "name is 1 to 256 bytes, not only spaces, without control characters";
|
||||
let sheet_id = ids::encode(bare().sheets[0].id);
|
||||
for broken in ["x".repeat(257), "é".repeat(128) + "a", String::new(), " ".into(), "two\nlines".into(), "bell\u{7}".into(), "nul\0".into()] {
|
||||
let expected = (format!("damaged drawing: the drawing's {rule}"), format!("damaged drawing: sheet {sheet_id}: its {rule}"));
|
||||
assert_eq!(names(broken.as_str()), expected, "{broken:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The content language is a well-formed BCP 47 tag of 2 to 35 characters.
|
||||
#[test]
|
||||
fn the_language_is_a_bcp_47_tag_of_2_to_35_characters() {
|
||||
let base = file(&bare());
|
||||
let why = |tag: &str| {
|
||||
let mut v = base.clone();
|
||||
v["language"] = json!(tag);
|
||||
refusal(&v)
|
||||
};
|
||||
let longest = "en-abcdefgh-abcdefgh-abcdefgh-abcde";
|
||||
assert_eq!(longest.len(), 35);
|
||||
for tag in ["en", "de", "ast", "en-GB", "EN-gb", "zh-Hans-CN", "sr-Latn-RS", "es-419", longest] {
|
||||
assert_eq!(why(tag), "read", "{tag}");
|
||||
}
|
||||
let rule = "damaged drawing: the language is not a BCP 47 tag of 2 to 35 characters (such as `en` or `en-GB`)";
|
||||
let too_long = format!("{longest}f");
|
||||
let broken =
|
||||
["e", "", too_long.as_str(), "en_GB", "en-", "-en", "en--GB", "e1", "x-private", "en-abcdefghi", "abcdefghi", "日本", "en GB", "en\n"];
|
||||
for tag in broken {
|
||||
assert_eq!(why(tag), rule, "{tag:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A fixed unit is a length unit of the expression unit table, written as the table writes it.
|
||||
#[test]
|
||||
fn a_fixed_unit_is_a_length_unit_of_the_expression_table() {
|
||||
let base = file(&bare());
|
||||
let units = |units: Value| {
|
||||
let mut v = base.clone();
|
||||
v["units"] = units;
|
||||
read(&v).map(|d| d.units)
|
||||
};
|
||||
for (unit, kind, _) in expr::UNITS {
|
||||
assert_eq!(units(json!({"fixed": {"unit": unit}})).is_ok(), kind == expr::Kind::Length, "{unit}");
|
||||
}
|
||||
assert_eq!(units(json!({"fixed": {"unit": "in"}})).unwrap(), Units::Fixed { unit: "in".into() });
|
||||
assert_eq!(units(json!("follow_model")).unwrap(), Units::FollowModel);
|
||||
let rule = "damaged drawing: the drawing's unit is not a length unit (such as `mm` or `in`)";
|
||||
for unit in ["furlong", "deg", "°", "none", "", " in", "in ", "IN"] {
|
||||
assert_eq!(units(json!({"fixed": {"unit": unit}})).unwrap_err().to_string(), rule, "{unit:?}");
|
||||
}
|
||||
for broken in [json!("follow_design"), json!({"fixed": "in"}), json!({"fixed": {}}), json!(null)] {
|
||||
assert!(units(broken.clone()).is_err(), "{broken}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A sheet's border is saved as a copy of its profile, field by field, and read back as saved, so
|
||||
/// a later built-in never changes it; a profile's name in its place is refused.
|
||||
#[test]
|
||||
fn a_border_is_saved_as_a_copy_of_its_profile() {
|
||||
let v = file(&plate());
|
||||
assert_eq!(v["sheets"][0]["border"], serde_json::to_value(DrawingBorderProfile::iso()).unwrap());
|
||||
assert_eq!(v["sheets"][1]["border"], serde_json::to_value(own_border()).unwrap());
|
||||
assert_eq!(read(&v).unwrap().sheets[1].border, Some(own_border()));
|
||||
let mut edited = v.clone();
|
||||
edited["sheets"][0]["border"]["margins"]["left_mm"] = json!(15.0);
|
||||
let iso = DrawingBorderProfile::iso();
|
||||
let wider = DrawingBorderProfile { margins: Margins { left_mm: 15.0, ..iso.margins }, ..iso };
|
||||
assert_eq!(read(&edited).unwrap().sheets[0].border, Some(wider));
|
||||
edited["sheets"][0]["border"] = json!("iso");
|
||||
assert!(refusal(&edited).starts_with("damaged drawing: "));
|
||||
edited["sheets"][0]["border"] = Value::Null;
|
||||
assert_eq!(read(&edited).unwrap().sheets[0].border, None, "no drawing border");
|
||||
}
|
||||
|
||||
/// What reading refuses: each damage the rules name, refused rather than patched, and files this
|
||||
/// build can't read.
|
||||
#[test]
|
||||
fn damaged_files_are_refused_and_never_patched() {
|
||||
let d = plate();
|
||||
let plate_id = ids::encode(d.sources[0].id);
|
||||
let [assembly_id, bolt_sheet_id] = [0, 1].map(|i| ids::encode(d.sheets[i].id));
|
||||
// Two objects with one id, of one kind and of two kinds.
|
||||
assert_eq!(refused(&d, |v| v["sources"][1]["id"] = json!(plate_id)), format!("damaged drawing: two objects have the id {plate_id}"));
|
||||
assert_eq!(refused(&d, |v| v["sheets"][1]["id"] = json!(assembly_id)), format!("damaged drawing: two objects have the id {assembly_id}"));
|
||||
assert_eq!(refused(&d, |v| v["sources"][2]["id"] = json!(bolt_sheet_id)), format!("damaged drawing: two objects have the id {bolt_sheet_id}"));
|
||||
// An id that isn't one, where an object has it and where a reference names it; no number form,
|
||||
// and no counter.
|
||||
let not_an_id = "damaged drawing: an id is 13 characters of lowercase Crockford base32, not all 0";
|
||||
for text in ["0000000000000", "FZZZZZZZZZZZZ", "000000000001", "0000000000001 ", "000000000000o", "g000000000000"] {
|
||||
assert_eq!(refused(&d, |v| v["sources"][2]["id"] = json!(text)), not_an_id, "{text:?}");
|
||||
assert_eq!(refused(&d, |v| v["sheets"][1]["documents"]["source"] = json!(text)), not_an_id, "{text:?}");
|
||||
}
|
||||
assert!(refused(&d, |v| v["sources"][2]["id"] = json!(5)).starts_with("damaged drawing: invalid type"));
|
||||
assert!(refused(&d, |v| v["next_id"] = json!(6)).contains("unknown field `next_id`"));
|
||||
let mut in_memory = d.clone();
|
||||
in_memory.sources[2].id = 0;
|
||||
assert_eq!(in_memory.validate().unwrap_err().to_string(), "an object has the id 0, which is never one");
|
||||
// A reference to nothing, or to an object of another kind; a source a sheet documents removed.
|
||||
for missing in [ids::encode(99), assembly_id.clone()] {
|
||||
let expected = format!("damaged drawing: sheet {bolt_sheet_id}: it documents source {missing}, which the drawing doesn't have");
|
||||
assert_eq!(refused(&d, |v| v["sheets"][1]["documents"]["source"] = json!(missing)), expected);
|
||||
}
|
||||
let without_plate = refused(&d, |v| {
|
||||
v["sources"].as_array_mut().unwrap().remove(0);
|
||||
});
|
||||
assert_eq!(without_plate, format!("damaged drawing: sheet {assembly_id}: it documents source {plate_id}, which the drawing doesn't have"));
|
||||
// The paper, its border and its resolution, through the sheet's own rules.
|
||||
let custom = |sides: Value| refused(&d, |v| v["sheets"][0]["sheet"]["size"] = json!({"custom": {"sides_mm": sides}}));
|
||||
assert_eq!(custom(json!([5000.0, 0.5])), "read");
|
||||
for sides in [json!([5000.1, 100.0]), json!([0.0, 100.0]), json!([-1.0, 100.0])] {
|
||||
let expected = format!("damaged drawing: sheet {assembly_id}: drawing sheet: a custom side is finite, above 0 and at most 5000 mm");
|
||||
assert_eq!(custom(sides), expected);
|
||||
}
|
||||
let why = custom(json!([1e10, 100.0]));
|
||||
assert!(why.starts_with("damaged drawing: the number ") && why.ends_with(" is out of range"), "checked before the record: {why}");
|
||||
let nan = d.to_json().unwrap().replacen("600.0", "NaN", 1);
|
||||
assert!(Drawing::from_json(&nan).unwrap_err().to_string().starts_with("not a SolidCraft drawing: "), "NaN isn't JSON");
|
||||
assert_eq!(
|
||||
refused(&d, |v| v["sheets"][1]["border"]["margins"]["top_mm"] = json!(-1.0)),
|
||||
format!("damaged drawing: sheet {bolt_sheet_id}: drawing sheet: a margin is finite and 0 to 5000 mm")
|
||||
);
|
||||
for dpi in [25.0, 2400.0] {
|
||||
assert_eq!(refused(&d, |v| v["sheets"][1]["dpi"] = json!(dpi)), "read", "{dpi}");
|
||||
}
|
||||
for dpi in [24.9, 2400.1, 0.0, -300.0] {
|
||||
let expected = format!("damaged drawing: sheet {bolt_sheet_id}: its pictures are 25 to 2400 dpi");
|
||||
assert_eq!(refused(&d, |v| v["sheets"][1]["dpi"] = json!(dpi)), expected, "{dpi}");
|
||||
}
|
||||
// In memory, where NaN can be, the same rules.
|
||||
let mut in_memory = plate();
|
||||
Arc::make_mut(&mut in_memory.sheets[1]).dpi = f64::NAN;
|
||||
assert_eq!(in_memory.validate().unwrap_err().to_string(), format!("sheet {bolt_sheet_id}: its pictures are 25 to 2400 dpi"));
|
||||
let sheet = Arc::make_mut(&mut in_memory.sheets[1]);
|
||||
sheet.dpi = DEFAULT_DPI;
|
||||
sheet.sheet.size = SheetSize::Custom { sides_mm: [f64::NAN, 100.0] };
|
||||
assert!(in_memory.validate().unwrap_err().to_string().starts_with(&format!("sheet {bolt_sheet_id}: drawing sheet: a custom side")));
|
||||
// An unknown field anywhere.
|
||||
let unknown: [fn(&mut Value); 8] = [
|
||||
|v| v["views"] = json!([]),
|
||||
|v| v["sources"][0]["name"] = json!("Plate"),
|
||||
|v| v["sheets"][0]["scale"] = json!(1),
|
||||
|v| v["sheets"][0]["sheet"]["colour"] = json!("white"),
|
||||
|v| v["sheets"][0]["border"]["style"] = json!("iso"),
|
||||
|v| v["sheets"][0]["documents"]["view"] = json!(1),
|
||||
|v| v["sheets"][0]["excluded"]["export"] = json!(true),
|
||||
|v| v["units"]["fixed"]["precision"] = json!(2),
|
||||
];
|
||||
for (i, edit) in unknown.into_iter().enumerate() {
|
||||
let why = refused(&d, edit);
|
||||
assert!(why.starts_with("damaged drawing: ") && why.contains("unknown field"), "{i}: {why}");
|
||||
}
|
||||
// A newer build's file is refused as one before anything in it is read: a field this build
|
||||
// doesn't know, or a number out of its range.
|
||||
let (current, newer) = (DRAWING.current(), DRAWING.current() + 1);
|
||||
let why = refused(&d, |v| {
|
||||
v["format"] = json!(DRAWING.format_string(newer));
|
||||
v["views"] = json!([]);
|
||||
v["sheets"][0]["dpi"] = json!(1e10);
|
||||
});
|
||||
assert_eq!(
|
||||
why,
|
||||
format!(
|
||||
"this drawing was saved by a newer SolidCraft (file format {newer}; this version reads formats up to {current}): update SolidCraft to open it"
|
||||
)
|
||||
);
|
||||
// A list of pending changes means nothing beside a format made of none; beside the pending
|
||||
// number, a change this build doesn't know is a newer build's.
|
||||
let why = refused(&d, |v| v["format_pending"] = json!(["a-later-change"]));
|
||||
if DRAWING.pending_names().is_empty() {
|
||||
assert_eq!(why, "read");
|
||||
} else {
|
||||
assert!(why.contains("newer SolidCraft"), "{why}");
|
||||
}
|
||||
// A design is never read as a drawing, nor a drawing as a design.
|
||||
let design = crate::Document::new("Plate").to_json();
|
||||
let design_format = crate::format::format_string(crate::format::FORMAT);
|
||||
assert_eq!(Drawing::from_json(&design).unwrap_err().to_string(), format!("not a SolidCraft drawing (format `{design_format}`)"));
|
||||
let drawing_format = DRAWING.format_string(current);
|
||||
assert_eq!(
|
||||
crate::Document::from_json(&d.to_json().unwrap()).unwrap_err().to_string(),
|
||||
format!("not a SolidCraft design (format `{drawing_format}`)")
|
||||
);
|
||||
// Not an object, not JSON, nested too deeply, too large.
|
||||
assert_eq!(Drawing::from_json("[]").unwrap_err().to_string(), "not a SolidCraft drawing (expected a JSON object)");
|
||||
assert!(Drawing::from_json("").unwrap_err().to_string().starts_with("not a SolidCraft drawing: "));
|
||||
let deep: Value = serde_json::from_str(&format!("{}1{}", "[".repeat(100), "]".repeat(100))).unwrap();
|
||||
assert_eq!(refused(&d, |v| v["views"] = deep), "damaged drawing: nested too deeply");
|
||||
let deeper = format!("{{\"format\": \"{drawing_format}\", \"views\": {}1{}}}", "[".repeat(200), "]".repeat(200));
|
||||
assert!(Drawing::from_json(&deeper).unwrap_err().to_string().starts_with("not a SolidCraft drawing: "));
|
||||
let text = d.to_json().unwrap();
|
||||
let at_cap = text.clone() + &" ".repeat(MAX_DRAWING_BYTES - text.len());
|
||||
assert_eq!(Drawing::from_json(&at_cap).unwrap(), d);
|
||||
assert_eq!(Drawing::from_json(&(at_cap + " ")).unwrap_err().to_string(), "drawing too large (at most 16 MiB)");
|
||||
}
|
||||
|
||||
/// A run never gives an id twice: not one in use, and not a removed object's. Only another run from
|
||||
/// the same seed, which tests and replays start from the same file, can give a removed id again.
|
||||
#[test]
|
||||
fn a_run_never_gives_an_id_twice() {
|
||||
let mut id_source = IdSource::seeded(SEED);
|
||||
let a4 = drawing_sheet::Sheet::standard(PaperSize::A4, Orientation::Landscape);
|
||||
let mut d = Drawing::new("Ids", "en", "Sheet", a4, None, &mut id_source).unwrap();
|
||||
let mut given = BTreeSet::from([d.sheets[0].id]);
|
||||
for _ in 0..MAX_SOURCES {
|
||||
let id = d.allocate_id(&mut id_source).unwrap();
|
||||
d.sources.push(Source { id, path: "part.sc".into() });
|
||||
assert!(given.insert(id), "{id:#x} given twice");
|
||||
}
|
||||
let removed = d.sources.pop().unwrap().id;
|
||||
for _ in 0..10_000 {
|
||||
let id = d.allocate_id(&mut id_source).unwrap();
|
||||
assert!(id != removed && given.insert(id), "{id:#x} given twice");
|
||||
}
|
||||
// The same seed, from the start, over a file it made: every draw up to the removed id is in use.
|
||||
let reopened = Drawing::from_json(&d.to_json().unwrap()).unwrap();
|
||||
let again = reopened.allocate_id(&mut IdSource::seeded(SEED)).unwrap();
|
||||
assert!(!reopened.ids().any(|id| id == again));
|
||||
assert_eq!(again, removed);
|
||||
}
|
||||
|
||||
/// Properties follow the design metadata's rules: other tools' data and a well-known key of another
|
||||
/// kind are kept as they are, never read as numbers, while the same number elsewhere is refused.
|
||||
#[test]
|
||||
fn properties_are_kept_as_they_are() {
|
||||
let mut v = file(&plate());
|
||||
v["properties"]["com.example.pdm"] = json!({"huge": 1e300, "ids": [1, 2, 3]});
|
||||
v["properties"]["revision"] = json!(3);
|
||||
let d = read(&v).unwrap();
|
||||
assert_eq!(d.properties.get("com.example.pdm"), Some(&json!({"huge": 1e300, "ids": [1, 2, 3]})));
|
||||
assert_eq!((d.properties.revision(), d.properties.get("revision")), (None, Some(&json!(3))));
|
||||
assert_eq!(read(&file(&d)).unwrap(), d);
|
||||
v["sheets"][0]["dpi"] = json!(1e300);
|
||||
let why = refusal(&v);
|
||||
assert!(why.starts_with("damaged drawing: the number ") && why.ends_with(" is out of range"), "{why}");
|
||||
}
|
||||
|
||||
/// A drawing whose JSON would be over the read limit isn't saved. Other tools' properties are kept
|
||||
/// as they came, and indenting them can grow a file read in under the limit past it, so the save
|
||||
/// refuses rather than write a file that won't open: every drawing saved opens again.
|
||||
#[test]
|
||||
fn a_drawing_too_large_to_reopen_is_not_saved() {
|
||||
let file = |zeros: usize| {
|
||||
let items = vec!["0"; zeros].join(",");
|
||||
let sheet = r#"{"id":"0000000000001","name":"S","sheet":{"size":{"standard":"a4"}}}"#;
|
||||
let format = DRAWING.format_string(1);
|
||||
format!(
|
||||
r#"{{"format":"{format}","name":"A","sources":[],"language":"en","sheets":[{sheet}],"properties":{{"com.example":{{"items":[{items}]}}}}}}"#
|
||||
)
|
||||
};
|
||||
// The case found in review (#3891): 2,000,000 zeros read in at about 4 MB and indent past 16 MiB.
|
||||
let big = Drawing::from_json(&file(2_000_000)).unwrap();
|
||||
assert_eq!(big.to_json().unwrap_err().to_string(), "drawing too large to save (at most 16 MiB)");
|
||||
let small = Drawing::from_json(&file(100_000)).unwrap();
|
||||
assert_eq!(Drawing::from_json(&small.to_json().unwrap()).unwrap(), small);
|
||||
}
|
||||
|
||||
/// A new drawing has one sheet, its id the source's first, follows its designs' units and renders
|
||||
/// pictures at 300 dpi; a name, language or border that breaks the rules is refused.
|
||||
#[test]
|
||||
fn a_new_drawing_has_one_sheet_and_keeps_the_rules() {
|
||||
let d = bare();
|
||||
let first = IdSource::seeded(SEED).next(&BTreeSet::new()).unwrap();
|
||||
assert_eq!((d.sheets.len(), d.sheets[0].id, d.sheets[0].dpi, &d.units), (1, first, DEFAULT_DPI, &Units::FollowModel));
|
||||
assert_eq!((d.format, d.format_pending), (DRAWING.format_string(DRAWING.current()), DRAWING.pending_names()));
|
||||
let a4 = drawing_sheet::Sheet::standard(PaperSize::A4, Orientation::Landscape);
|
||||
let thin = DrawingBorderProfile { drawing_border_line_mm: 0.0, ..DrawingBorderProfile::iso() };
|
||||
let broken = [("", "en", "Sheet", None), ("Bare", "en_GB", "Sheet", None), ("Bare", "en", "\n", None), ("Bare", "en", "Sheet", Some(thin))];
|
||||
for (name, language, sheet, border) in broken {
|
||||
let made = Drawing::new(name, language, sheet, a4, border, &mut IdSource::seeded(SEED));
|
||||
assert!(made.is_err(), "{name:?} {language:?} {sheet:?} {border:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Hostile input never panics: a valid file's text cut, overwritten and given lies, and its JSON
|
||||
/// values replaced, repeated and dropped. Each read gives an error, or a drawing that keeps the
|
||||
/// rules and reads back as it saves.
|
||||
#[test]
|
||||
fn mutated_files_give_an_error_or_a_valid_drawing_never_a_panic() {
|
||||
let good = plate().to_json().unwrap();
|
||||
let tree: Value = serde_json::from_str(&good).unwrap();
|
||||
let nodes = count(&tree) as u64;
|
||||
let lies = [
|
||||
json!(null),
|
||||
json!(0),
|
||||
json!(-1),
|
||||
json!(1),
|
||||
json!(1e10),
|
||||
json!(u64::MAX),
|
||||
json!(""),
|
||||
json!("x".repeat(300)),
|
||||
json!("0000000000000"),
|
||||
json!("0000000000001"),
|
||||
json!("fzzzzzzzzzzzz"),
|
||||
json!("FZZZZZZZZZZZZ"),
|
||||
json!([]),
|
||||
json!({}),
|
||||
json!(true),
|
||||
json!("follow_model"),
|
||||
json!({"fixed": {"unit": "deg"}}),
|
||||
];
|
||||
let words = ["NaN", "1e400", "-0", "18446744073709551616", "\"", "{", "]", ",", "null", "\u{0}", "é", "o", "I"];
|
||||
let mut seed: u64 = 0x9e37_79b9_7f4a_7c15;
|
||||
let mut rnd = move || {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
seed
|
||||
};
|
||||
let mut read_back = 0;
|
||||
for i in 0..10_000 {
|
||||
let text = if i % 2 == 0 {
|
||||
let mut v = tree.clone();
|
||||
let lie = &lies[(rnd() % lies.len() as u64) as usize];
|
||||
mutate(&mut v, &mut ((rnd() % nodes) as usize), lie, rnd() % 3);
|
||||
v.to_string()
|
||||
} else {
|
||||
let mut s = good.clone().into_bytes();
|
||||
for _ in 0..1 + rnd() % 3 {
|
||||
// A run of up to 15 bytes taken out, overwritten by one byte or replaced by a lie.
|
||||
let at = (rnd() % (s.len() as u64 + 1)) as usize;
|
||||
let cut = ((rnd() % 16) as usize).min(s.len() - at);
|
||||
let byte = [(rnd() >> 56) as u8];
|
||||
let put: &[u8] = match rnd() % 3 {
|
||||
0 => &[],
|
||||
1 => &byte,
|
||||
_ => words[(rnd() % words.len() as u64) as usize].as_bytes(),
|
||||
};
|
||||
let next = [&s[..at], put, &s[at + cut..]].concat();
|
||||
s = next;
|
||||
if rnd() % 8 == 0 {
|
||||
s.truncate(at);
|
||||
}
|
||||
}
|
||||
String::from_utf8_lossy(&s).into_owned()
|
||||
};
|
||||
if let Ok(d) = Drawing::from_json(&text) {
|
||||
assert!(d.validate().is_ok() && d.ids().all(|id| id != 0), "{text}");
|
||||
assert_eq!(Drawing::from_json(&d.to_json().unwrap()).unwrap(), d, "{text}");
|
||||
read_back += 1;
|
||||
}
|
||||
}
|
||||
assert!(read_back > 50, "only {read_back} mutated files read: the mutations hardly reach the record's rules");
|
||||
}
|
||||
|
||||
/// The number of values in `v`, itself included.
|
||||
fn count(v: &Value) -> usize {
|
||||
1 + match v {
|
||||
Value::Array(a) => a.iter().map(count).sum::<usize>(),
|
||||
Value::Object(o) => o.values().map(count).sum(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the `pick`th value of `v`, depth first: `how` 1 repeats a list's first element (two
|
||||
/// objects with one id), 2 drops an object's first field, and otherwise `lie` takes its place.
|
||||
fn mutate(v: &mut Value, pick: &mut usize, lie: &Value, how: u64) -> bool {
|
||||
if *pick == 0 {
|
||||
match (v, how) {
|
||||
(Value::Array(a), 1) if !a.is_empty() => {
|
||||
let first = a[0].clone();
|
||||
a.push(first);
|
||||
}
|
||||
(Value::Object(o), 2) if !o.is_empty() => {
|
||||
let key = o.keys().next().cloned().unwrap_or_default();
|
||||
o.remove(&key);
|
||||
}
|
||||
(v, _) => *v = lie.clone(),
|
||||
}
|
||||
return true;
|
||||
}
|
||||
*pick -= 1;
|
||||
match v {
|
||||
Value::Array(a) => a.iter_mut().any(|x| mutate(x, pick, lie, how)),
|
||||
Value::Object(o) => o.values_mut().any(|x| mutate(x, pick, lie, how)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -2332,7 +2332,7 @@ impl Model {
|
||||
}
|
||||
Err(e) => Some(e.to_string().trim_start_matches("the operation failed: ").to_string()),
|
||||
};
|
||||
if !self.no_cache {
|
||||
if !self.no_cache && !watched.cancelled() {
|
||||
let e = CachedEval {
|
||||
error: error.clone(),
|
||||
warning: warning.clone(),
|
||||
|
||||
@@ -485,7 +485,7 @@ fn mirror_y(r: &Region2) -> Region2 {
|
||||
.iter()
|
||||
.map(|s| match *s {
|
||||
Seg2::Arc { center, radius, start, sweep } => Seg2::Arc { center: f(center), radius, start: -start, sweep: -sweep },
|
||||
other => sheet::map_controls(&other, f),
|
||||
other => other.map_controls(f),
|
||||
})
|
||||
.collect();
|
||||
Loop2 { segs }.ccw()
|
||||
|
||||
@@ -183,6 +183,11 @@ impl Document {
|
||||
return Err(DocError::Invalid("this face has no supported construction certificate; choose a fixed plane instead".into()));
|
||||
}
|
||||
let indices = self.configuration_face_indices(body, &target)?;
|
||||
// A copy a pattern or mirror made on the same body carries its seed's certificate,
|
||||
// which finds the seed's face: the copy is attached by its own history name below.
|
||||
if !indices.contains(&index) {
|
||||
return Err(DocError::Invalid("this face has no supported construction certificate; choose a fixed plane instead".into()));
|
||||
}
|
||||
self.coplanar_pieces(body, &indices, |_| true)?;
|
||||
Ok(target)
|
||||
});
|
||||
|
||||
@@ -232,6 +232,11 @@
|
||||
//! this build writes the next number (which no release reads), with the names of these changes
|
||||
//! in `format_pending`, so a later build applies just the steps a file is missing. At release
|
||||
//! time `scripts/fold-format-pending.sh` moves them into the next `v<N>/`.
|
||||
//!
|
||||
//! Drawings (`solidcraft-drawing/N`, #3890) are versioned by the same code ([`Family`]) from their
|
||||
//! own first format, 1 (the record in [`crate::drawing`]): each later change is one file in
|
||||
//! `src/drawing/format/`, in `v<N>/` or `pending/` as above, and the release script folds that
|
||||
//! folder too.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -244,11 +249,11 @@ pub const FORMAT: u32 = FORMATS.current();
|
||||
/// of [`Part`]s (`src/format/`, listed by `build.rs`).
|
||||
const HAND_WRITTEN: u32 = 22;
|
||||
|
||||
/// One change to the file format: a file in `src/format/v<N>/` or `src/format/pending/`.
|
||||
/// One change to a family's file format: a file in its `v<N>/` or `pending/` folder.
|
||||
pub(crate) struct Part {
|
||||
/// The file's name, which a file written while the change was pending lists.
|
||||
pub name: &'static str,
|
||||
/// How a design from before the change is rewritten as it opens, if it is.
|
||||
/// How a file from before the change is rewritten as it opens, if it is.
|
||||
pub step: Option<JsonStep>,
|
||||
}
|
||||
|
||||
@@ -257,13 +262,102 @@ include!(concat!(env!("OUT_DIR"), "/format_parts.rs"));
|
||||
/// Every format this build knows and how to upgrade a design through them.
|
||||
const FORMATS: Formats<'static> = Formats { hand: &JSON_STEPS, folded: FOLDED, pending: PENDING };
|
||||
|
||||
/// The names of the pending changes this build writes into a design (`format_pending`).
|
||||
pub fn pending_names() -> Vec<String> {
|
||||
FORMATS.pending.iter().map(|p| p.name.to_string()).collect()
|
||||
/// A kind of file whose format is versioned here: designs, and drawings (#3890). Each family has
|
||||
/// its own format tag, its own numbers and its own folder of change files (`build.rs` lists them);
|
||||
/// reading a file's format, refusing a file a newer build saved and the upgrade are the same code
|
||||
/// for every family.
|
||||
pub struct Family<'a> {
|
||||
/// What a file of the family is called in messages.
|
||||
noun: &'static str,
|
||||
/// The tag before the number in a file's `format`.
|
||||
prefix: &'static str,
|
||||
/// The tag files saved before the rename carry: read, never written (docs/compatibility.md).
|
||||
legacy_prefix: Option<&'static str>,
|
||||
/// The format of a file that names none: designs saved before format strings are format 1; a
|
||||
/// drawing always names its own.
|
||||
untagged: Option<u32>,
|
||||
formats: Formats<'a>,
|
||||
}
|
||||
|
||||
/// The upgrade path: the hand-written steps up to [`HAND_WRITTEN`], the folded formats after it
|
||||
/// (each a list of changes) and the pending changes, which make the next format.
|
||||
/// Designs (`solidcraft/N`): the upgrade steps written out up to format 22, then the change files
|
||||
/// in `src/format/`.
|
||||
pub const DESIGN: Family<'static> =
|
||||
Family { noun: "design", prefix: "solidcraft/", legacy_prefix: Some(LEGACY_FORMAT_PREFIX), untagged: Some(1), formats: FORMATS };
|
||||
|
||||
/// Drawings (`solidcraft-drawing/N`, #3890): format 1 is the first drawing record, and every later
|
||||
/// change is a file in `src/drawing/format/`.
|
||||
pub const DRAWING: Family<'static> = Family {
|
||||
noun: "drawing",
|
||||
prefix: "solidcraft-drawing/",
|
||||
legacy_prefix: None,
|
||||
untagged: None,
|
||||
formats: Formats { hand: &[], folded: DRAWING_FOLDED, pending: DRAWING_PENDING },
|
||||
};
|
||||
|
||||
impl Family<'_> {
|
||||
/// The format this build writes: the last folded one, or the next while changes are pending.
|
||||
pub const fn current(&self) -> u32 {
|
||||
self.formats.current()
|
||||
}
|
||||
|
||||
/// The names of the pending changes this build writes into a file (`format_pending`).
|
||||
pub fn pending_names(&self) -> Vec<String> {
|
||||
self.formats.pending.iter().map(|p| p.name.to_string()).collect()
|
||||
}
|
||||
|
||||
/// The format tag of format `n`.
|
||||
pub fn format_string(&self, n: u32) -> String {
|
||||
format!("{}{n}", self.prefix)
|
||||
}
|
||||
|
||||
/// The version in one of this family's format tags; `None` for any other tag, another
|
||||
/// family's included.
|
||||
pub fn version_of(&self, s: &str) -> Option<u32> {
|
||||
s.strip_prefix(self.prefix).or_else(|| self.legacy_prefix.and_then(|p| s.strip_prefix(p)))?.parse().ok()
|
||||
}
|
||||
|
||||
/// Read a file's format and take its `format_pending` out of its JSON, before anything else
|
||||
/// in it is read: a file that isn't this family's, or that a newer build saved, is refused
|
||||
/// here, so nothing a newer build saved is judged by this build's rules.
|
||||
pub fn take_version(&self, v: &mut Value) -> Result<(u32, Option<Vec<String>>)> {
|
||||
let noun = self.noun;
|
||||
let not_ours = |what: String| DocError::Invalid(format!("not a SolidCraft {noun} ({what})"));
|
||||
let Some(obj) = v.as_object() else { return Err(not_ours("expected a JSON object".into())) };
|
||||
let version = match obj.get("format") {
|
||||
None => self.untagged.ok_or_else(|| not_ours("no format".into()))?,
|
||||
Some(Value::String(f)) => self.version_of(f).ok_or_else(|| not_ours(format!("format `{f}`")))?,
|
||||
Some(_) => return Err(not_ours("format is not a string".into())),
|
||||
};
|
||||
let applied = take_names(v, "format_pending", noun)?;
|
||||
self.formats.check(noun, version, applied.as_deref())?;
|
||||
Ok((version, applied))
|
||||
}
|
||||
|
||||
/// Bring a file of format `version` up to this build's; `applied` is its `format_pending`
|
||||
/// (both from [`Family::take_version`]).
|
||||
pub fn upgrade(&self, v: &mut Value, version: u32, applied: Option<&[String]>) -> Result<()> {
|
||||
self.formats.upgrade(self.noun, v, version, applied)
|
||||
}
|
||||
|
||||
/// Every number a file of the family holds is in range ([`check_numbers`]), but for its
|
||||
/// top-level `kept` field: other tools' data (a design's `metadata`, a drawing's `properties`),
|
||||
/// kept as it is and never read as a number (#1015); parsing already bounds its nesting.
|
||||
pub(crate) fn check_numbers(&self, v: &Value, kept: &str) -> Result<()> {
|
||||
match v {
|
||||
Value::Object(m) => m.iter().filter(|(k, _)| k.as_str() != kept).try_for_each(|(_, x)| check_numbers(x, self.noun, 1)),
|
||||
v => check_numbers(v, self.noun, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The names of the pending changes this build writes into a design (`format_pending`).
|
||||
pub fn pending_names() -> Vec<String> {
|
||||
DESIGN.pending_names()
|
||||
}
|
||||
|
||||
/// A family's upgrade path: the steps written out by hand (designs' up to [`HAND_WRITTEN`]), the
|
||||
/// folded formats after them (each a list of changes) and the pending changes, which make the
|
||||
/// next format.
|
||||
struct Formats<'a> {
|
||||
hand: &'a [JsonStep],
|
||||
folded: &'a [(u32, &'a [Part])],
|
||||
@@ -300,26 +394,41 @@ impl Formats<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring a design of format `version` up to [`Formats::current`]. `applied` is the file's
|
||||
/// `format_pending`: the changes of its format it already has, when it was written while they
|
||||
/// were pending; the others' steps run first. Recognize changes across the entire history:
|
||||
/// a branch's pending change may have landed at a later number. Unknown changes are refused
|
||||
/// even after folding, and already-applied changes never run again at their later number.
|
||||
fn upgrade(&self, v: &mut Value, version: u32, applied: Option<&[String]>) -> Result<()> {
|
||||
if version == 0 || version > self.current() {
|
||||
return Err(DocError::Invalid(format!("not a SolidCraft design (format {version})")));
|
||||
/// Refuse a `noun` of format `version` that this build can't read: format 0, a newer number,
|
||||
/// or a change `applied` (its `format_pending`) names that this build doesn't have anywhere in
|
||||
/// its history, which only a newer build's file can. Changes are recognized across the entire
|
||||
/// history: a branch's pending change may have landed at a later number. A list beside a format
|
||||
/// that isn't made of changes means nothing (a tool or a test set an older number on a newer
|
||||
/// file).
|
||||
fn check(&self, noun: &str, version: u32, applied: Option<&[String]>) -> Result<()> {
|
||||
let newer = |what: String| DocError::Invalid(format!("this {noun} was saved by a newer SolidCraft ({what}): update SolidCraft to open it"));
|
||||
let current = self.current();
|
||||
if version == 0 {
|
||||
return Err(DocError::Invalid(format!("not a SolidCraft {noun} (format 0)")));
|
||||
}
|
||||
// A list beside a format that isn't made of changes means nothing (a tool or a test set
|
||||
// an older number on a newer file): it is ignored, like any field an upgrade drops.
|
||||
if version > current {
|
||||
return Err(newer(format!("file format {version}; this version reads formats up to {current}")));
|
||||
}
|
||||
let applied = applied.filter(|_| self.parts(version).is_some()).unwrap_or_default();
|
||||
match applied.iter().find(|name| !self.knows_part_name(self.canonical_applied_name(version, name))) {
|
||||
Some(unknown) => Err(newer(format!("file format {version} with `{unknown}`, which this version doesn't know"))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring a `noun` of format `version` up to [`Formats::current`]. `applied` is the file's
|
||||
/// `format_pending`: the changes of its format it already has, when it was written while they
|
||||
/// were pending; the others' steps run first. A file [`Formats::check`] refuses is refused,
|
||||
/// whoever calls, before anything is changed; already-applied changes never run again at
|
||||
/// their later number.
|
||||
fn upgrade(&self, noun: &str, v: &mut Value, version: u32, applied: Option<&[String]>) -> Result<()> {
|
||||
self.check(noun, version, applied)?;
|
||||
// A list beside a format that isn't made of changes is ignored, like any field an upgrade
|
||||
// drops.
|
||||
let applied = applied.filter(|_| self.parts(version).is_some());
|
||||
let already_applied =
|
||||
|part: &Part| applied.is_some_and(|names| names.iter().any(|name| self.canonical_applied_name(version, name) == part.name));
|
||||
if let Some(applied) = applied {
|
||||
if let Some(unknown) = applied.iter().find(|name| !self.knows_part_name(self.canonical_applied_name(version, name))) {
|
||||
return Err(DocError::Invalid(format!(
|
||||
"this design was saved by a newer SolidCraft (file format {version} with `{unknown}`, which this version doesn't know): update SolidCraft to open it"
|
||||
)));
|
||||
}
|
||||
if applied.is_some() {
|
||||
for part in self.parts(version).unwrap_or_default().iter().filter(|part| !already_applied(part)) {
|
||||
part.step.map_or(Ok(()), |step| step(v))?;
|
||||
}
|
||||
@@ -338,25 +447,26 @@ impl Formats<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a file's `format_pending` (the pending changes it has) out of its JSON.
|
||||
fn take_pending(v: &mut Value) -> Result<Option<Vec<String>>> {
|
||||
let Some(list) = v.as_object_mut().and_then(|o| o.remove("format_pending")) else { return Ok(None) };
|
||||
let invalid = || DocError::Invalid("damaged design: `format_pending` is not a short list of names".into());
|
||||
/// Take a short list of names out of a file's JSON: its `format_pending` (the pending changes it
|
||||
/// has).
|
||||
fn take_names(v: &mut Value, key: &str, noun: &str) -> Result<Option<Vec<String>>> {
|
||||
let Some(list) = v.as_object_mut().and_then(|o| o.remove(key)) else { return Ok(None) };
|
||||
let invalid = || DocError::Invalid(format!("damaged {noun}: `{key}` is not a short list of names"));
|
||||
let list = list.as_array().filter(|l| l.len() <= 1000).ok_or_else(invalid)?;
|
||||
list.iter().map(|n| n.as_str().filter(|n| n.len() <= 128).map(str::to_string).ok_or_else(invalid)).collect::<Result<_>>().map(Some)
|
||||
}
|
||||
|
||||
pub fn format_string(n: u32) -> String {
|
||||
format!("solidcraft/{n}")
|
||||
DESIGN.format_string(n)
|
||||
}
|
||||
|
||||
/// The format tag of designs saved before the rename. Compatibility shim (docs/compatibility.md):
|
||||
/// read, never written.
|
||||
pub const LEGACY_FORMAT_PREFIX: &str = "solvecraft/";
|
||||
|
||||
/// The version in a format string (`solidcraft/N`, or the legacy `solvecraft/N`).
|
||||
/// The version in a design's format string (`solidcraft/N`, or the legacy `solvecraft/N`).
|
||||
pub fn version_of(s: &str) -> Option<u32> {
|
||||
s.strip_prefix("solidcraft/").or_else(|| s.strip_prefix(LEGACY_FORMAT_PREFIX))?.parse().ok()
|
||||
DESIGN.version_of(s)
|
||||
}
|
||||
|
||||
/// JSON-level upgrades up to [`HAND_WRITTEN`]: entry `i` turns format `i + 1` into `i + 2`.
|
||||
@@ -566,23 +676,7 @@ fn v2_to_v3(v: &mut Value) -> Result<()> {
|
||||
/// Read a design file's JSON, upgrading older formats.
|
||||
pub fn read(s: &str) -> Result<Document> {
|
||||
let mut v: Value = serde_json::from_str(s).map_err(|e| DocError::Invalid(format!("not a SolidCraft design: {e}")))?;
|
||||
let Some(obj) = v.as_object() else {
|
||||
return Err(DocError::Invalid("not a SolidCraft design (expected a JSON object)".into()));
|
||||
};
|
||||
let version = match obj.get("format") {
|
||||
None => 1,
|
||||
Some(Value::String(f)) => version_of(f).ok_or_else(|| DocError::Invalid(format!("not a SolidCraft design (format `{f}`)")))?,
|
||||
Some(_) => return Err(DocError::Invalid("not a SolidCraft design (format is not a string)".into())),
|
||||
};
|
||||
let applied = take_pending(&mut v)?;
|
||||
if version == 0 {
|
||||
return Err(DocError::Invalid("not a SolidCraft design (format 0)".into()));
|
||||
}
|
||||
if version > FORMAT {
|
||||
return Err(DocError::Invalid(format!(
|
||||
"this design was saved by a newer SolidCraft (file format {version}; this version reads formats up to {FORMAT}): update SolidCraft to open it"
|
||||
)));
|
||||
}
|
||||
let (version, applied) = DESIGN.take_version(&mut v)?;
|
||||
check_design_numbers(&v)?;
|
||||
crate::config::validate_rules_json(&v)?;
|
||||
crate::config::validate_styles_json(&v)?;
|
||||
@@ -594,7 +688,7 @@ pub fn read(s: &str) -> Result<Document> {
|
||||
crate::format_offsets::validate_raw(&v)?;
|
||||
let legacy_wires = validate_wires(&v, version)?;
|
||||
validate_body_transfers(&v)?;
|
||||
FORMATS.upgrade(&mut v, version, applied.as_deref())?;
|
||||
DESIGN.upgrade(&mut v, version, applied.as_deref())?;
|
||||
let mut d: Document = serde_json::from_value(v).map_err(|e| DocError::Invalid(format!("design file: {e}")))?;
|
||||
check_typed_numbers(&d)?;
|
||||
let mut legacy_by_feature = std::collections::BTreeMap::<usize, Vec<usize>>::new();
|
||||
@@ -1060,22 +1154,22 @@ const MAX_REAL: f64 = 1e9;
|
||||
/// already bounds its nesting. Engineering SI constants have their own bounded admission
|
||||
/// (e.g. E in Pa): only the exact root user-material card field bypasses the coordinate bound.
|
||||
fn check_design_numbers(v: &Value) -> Result<()> {
|
||||
let Some(root) = v.as_object() else { return check_numbers(v, 0) };
|
||||
let Some(root) = v.as_object() else { return check_numbers(v, "design", 0) };
|
||||
for (key, value) in root {
|
||||
if key == "metadata" {
|
||||
continue;
|
||||
}
|
||||
if key != "user_materials" {
|
||||
check_numbers(value, 1)?;
|
||||
check_numbers(value, "design", 1)?;
|
||||
continue;
|
||||
}
|
||||
let Some(materials) = value.as_array() else { return check_numbers(value, 1) };
|
||||
let Some(materials) = value.as_array() else { return check_numbers(value, "design", 1) };
|
||||
if materials.len() > crate::MAX_USER_MATERIALS {
|
||||
return Err(DocError::Invalid("document exceeds 1000 user materials".into()));
|
||||
}
|
||||
for material in materials {
|
||||
let Some(fields) = material.as_object() else {
|
||||
check_numbers(material, 2)?;
|
||||
check_numbers(material, "design", 2)?;
|
||||
continue;
|
||||
};
|
||||
for (field, data) in fields {
|
||||
@@ -1084,7 +1178,7 @@ fn check_design_numbers(v: &Value) -> Result<()> {
|
||||
if field == "physical_card" && !data.is_null() {
|
||||
crate::physical_material::validate_card(data)?;
|
||||
} else {
|
||||
check_numbers(data, 3)?;
|
||||
check_numbers(data, "design", 3)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1102,21 +1196,22 @@ fn check_typed_numbers(d: &Document) -> Result<()> {
|
||||
check_design_numbers(&typed)
|
||||
}
|
||||
|
||||
/// Every non-integer number outside separately validated engineering cards is finite and within ±`MAX_REAL` (a damaged or hostile
|
||||
/// file is refused instead of overflowing the geometry later).
|
||||
fn check_numbers(v: &Value, depth: usize) -> Result<()> {
|
||||
/// Every non-integer number in a `noun`'s file, outside separately validated engineering cards,
|
||||
/// is finite and within ±`MAX_REAL` (a damaged or hostile file is refused instead of overflowing
|
||||
/// the geometry later).
|
||||
fn check_numbers(v: &Value, noun: &str, depth: usize) -> Result<()> {
|
||||
if depth > 64 {
|
||||
return Err(DocError::Invalid("damaged design: nested too deeply".into()));
|
||||
return Err(DocError::Invalid(format!("damaged {noun}: nested too deeply")));
|
||||
}
|
||||
match v {
|
||||
Value::Number(n) if !(n.is_u64() || n.is_i64()) => {
|
||||
let x = n.as_f64().unwrap_or(f64::NAN);
|
||||
if !(x.is_finite() && x.abs() <= MAX_REAL) {
|
||||
return Err(DocError::Invalid(format!("damaged design: the number {n} is out of range")));
|
||||
return Err(DocError::Invalid(format!("damaged {noun}: the number {n} is out of range")));
|
||||
}
|
||||
}
|
||||
Value::Array(a) => a.iter().try_for_each(|x| check_numbers(x, depth + 1))?,
|
||||
Value::Object(m) => m.values().try_for_each(|x| check_numbers(x, depth + 1))?,
|
||||
Value::Array(a) => a.iter().try_for_each(|x| check_numbers(x, noun, depth + 1))?,
|
||||
Value::Object(m) => m.values().try_for_each(|x| check_numbers(x, noun, depth + 1))?,
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1224,25 +1319,25 @@ mod tests {
|
||||
assert_eq!(formats.current(), next);
|
||||
for version in [1, HAND_WRITTEN, next - 1] {
|
||||
let mut v = serde_json::json!({"features": [], "assembly": {"joints": [{"flip": false}]}});
|
||||
formats.upgrade(&mut v, version, None).unwrap();
|
||||
formats.upgrade("design", &mut v, version, None).unwrap();
|
||||
assert_eq!(trace_of(&v), ["a", "b"], "from format {version}");
|
||||
// The hand-written steps ran too: format 3's joints were flipped on the way up.
|
||||
assert_eq!(v["assembly"]["joints"][0]["flip"], Value::Bool(version < 4), "from format {version}");
|
||||
}
|
||||
// A design of the pending number written before `b-second` existed gets just that step.
|
||||
let mut v = serde_json::json!({});
|
||||
formats.upgrade(&mut v, next, Some(&["a-first".to_string()])).unwrap();
|
||||
formats.upgrade("design", &mut v, next, Some(&["a-first".to_string()])).unwrap();
|
||||
assert_eq!(trace_of(&v), ["b"]);
|
||||
// One that has both, or that this build wrote, needs nothing.
|
||||
let mut v = serde_json::json!({});
|
||||
formats.upgrade(&mut v, next, Some(&["a-first".into(), "b-second".into()])).unwrap();
|
||||
formats.upgrade("design", &mut v, next, Some(&["a-first".into(), "b-second".into()])).unwrap();
|
||||
assert_eq!(trace_of(&v), Vec::<&str>::new());
|
||||
// A change this build doesn't know is a newer build's: refused, not guessed at.
|
||||
let err = formats.upgrade(&mut serde_json::json!({}), next, Some(&["c-later".into()])).unwrap_err();
|
||||
let err = formats.upgrade("design", &mut serde_json::json!({}), next, Some(&["c-later".into()])).unwrap_err();
|
||||
assert!(err.to_string().contains("newer SolidCraft"), "{err}");
|
||||
// A list on a format that isn't made of changes means nothing.
|
||||
let mut v = serde_json::json!({});
|
||||
formats.upgrade(&mut v, HAND_WRITTEN, Some(&["a-first".into()])).unwrap();
|
||||
formats.upgrade("design", &mut v, HAND_WRITTEN, Some(&["a-first".into()])).unwrap();
|
||||
assert_eq!(trace_of(&v), ["a", "b"]);
|
||||
}
|
||||
|
||||
@@ -1257,13 +1352,13 @@ mod tests {
|
||||
let formats = Formats { hand: &JSON_STEPS, folded: &folded, pending: &[] };
|
||||
assert_eq!(formats.current(), next);
|
||||
let mut old = serde_json::json!({});
|
||||
formats.upgrade(&mut old, HAND_WRITTEN, None).unwrap();
|
||||
formats.upgrade("design", &mut old, HAND_WRITTEN, None).unwrap();
|
||||
assert_eq!(trace_of(&old), ["a", "b"]);
|
||||
let mut nightly = serde_json::json!({});
|
||||
formats.upgrade(&mut nightly, next, Some(&["a-first".into()])).unwrap();
|
||||
formats.upgrade("design", &mut nightly, next, Some(&["a-first".into()])).unwrap();
|
||||
assert_eq!(trace_of(&nightly), ["b"]);
|
||||
let mut released = serde_json::json!({});
|
||||
formats.upgrade(&mut released, next, None).unwrap();
|
||||
formats.upgrade("design", &mut released, next, None).unwrap();
|
||||
assert_eq!(trace_of(&released), Vec::<&str>::new());
|
||||
}
|
||||
|
||||
@@ -1276,7 +1371,7 @@ mod tests {
|
||||
let folded = Formats { hand: &hand, folded: &[(29, &first), (30, &cards)], pending: &[] };
|
||||
for formats in [pending, folded] {
|
||||
let mut file = serde_json::json!({"trace": ["b"]});
|
||||
formats.upgrade(&mut file, 29, Some(&["physical-material-cards".into()])).unwrap();
|
||||
formats.upgrade("design", &mut file, 29, Some(&["physical-material-cards".into()])).unwrap();
|
||||
assert_eq!(trace_of(&file), ["b", "a"], "never replay existing card semantics");
|
||||
}
|
||||
}
|
||||
@@ -1288,7 +1383,7 @@ mod tests {
|
||||
let formats = Formats { hand: &hand, folded: &[(29, &first)], pending: &[] };
|
||||
let mut file = serde_json::json!({"trace": [], "sentinel": 7});
|
||||
let original = file.clone();
|
||||
let error = formats.upgrade(&mut file, 29, Some(&["physical-material-cards".into()])).unwrap_err();
|
||||
let error = formats.upgrade("design", &mut file, 29, Some(&["physical-material-cards".into()])).unwrap_err();
|
||||
assert!(error.to_string().contains("physical-material-cards"), "{error}");
|
||||
assert_eq!(file, original);
|
||||
}
|
||||
@@ -1303,7 +1398,7 @@ mod tests {
|
||||
for version in [28, 30] {
|
||||
let mut file = serde_json::json!({"trace": []});
|
||||
let original = file.clone();
|
||||
let error = formats.upgrade(&mut file, version, Some(&["physical-material-cards".into()])).unwrap_err();
|
||||
let error = formats.upgrade("design", &mut file, version, Some(&["physical-material-cards".into()])).unwrap_err();
|
||||
assert!(error.to_string().contains("physical-material-cards"), "{error}");
|
||||
assert_eq!(file, original);
|
||||
}
|
||||
@@ -1320,7 +1415,7 @@ mod tests {
|
||||
{
|
||||
let mut file = serde_json::json!({"trace": ["b"]});
|
||||
let original = file.clone();
|
||||
let error = formats.upgrade(&mut file, 29, Some(&names)).unwrap_err();
|
||||
let error = formats.upgrade("design", &mut file, 29, Some(&names)).unwrap_err();
|
||||
assert!(error.to_string().contains("unknown-card-codec"), "{error}");
|
||||
assert_eq!(file, original);
|
||||
}
|
||||
@@ -1337,7 +1432,7 @@ mod tests {
|
||||
vec!["material-engineering-cards".into(), "physical-material-cards".into()],
|
||||
] {
|
||||
let mut file = serde_json::json!({"trace": ["b"]});
|
||||
formats.upgrade(&mut file, 29, Some(&names)).unwrap();
|
||||
formats.upgrade("design", &mut file, 29, Some(&names)).unwrap();
|
||||
assert_eq!(trace_of(&file), ["b", "a"]);
|
||||
}
|
||||
}
|
||||
@@ -1350,7 +1445,7 @@ mod tests {
|
||||
let formats = Formats { hand: &hand, folded: &[(29, &first)], pending: &cards };
|
||||
let mut file = serde_json::json!({"trace": ["a"], "metadata": {"format_pending": ["physical-material-cards"]}});
|
||||
let metadata = file["metadata"].clone();
|
||||
formats.upgrade(&mut file, 29, None).unwrap();
|
||||
formats.upgrade("design", &mut file, 29, None).unwrap();
|
||||
assert_eq!(trace_of(&file), ["a", "b"]);
|
||||
assert_eq!(file["metadata"], metadata);
|
||||
}
|
||||
@@ -1365,7 +1460,7 @@ mod tests {
|
||||
let formats = Formats { hand: &[], folded: &[(2, &parts)], pending };
|
||||
let mut file = serde_json::json!({"trace": []});
|
||||
let original = file.clone();
|
||||
let err = formats.upgrade(&mut file, 2, Some(&["experimental-card".into()])).unwrap_err();
|
||||
let err = formats.upgrade("design", &mut file, 2, Some(&["experimental-card".into()])).unwrap_err();
|
||||
assert!(err.to_string().contains("experimental-card") && err.to_string().contains("update SolidCraft"), "{err}");
|
||||
assert_eq!(file, original, "refuse before applying even a known migration");
|
||||
}
|
||||
@@ -1381,14 +1476,192 @@ mod tests {
|
||||
let folded = Formats { hand: &[], folded: &[(2, &first), (3, &later)], pending: &[] };
|
||||
for formats in [pending, folded] {
|
||||
let mut experimental = serde_json::json!({"trace": ["b"]});
|
||||
formats.upgrade(&mut experimental, 2, Some(&["b-second".into()])).unwrap();
|
||||
formats.upgrade("design", &mut experimental, 2, Some(&["b-second".into()])).unwrap();
|
||||
assert_eq!(trace_of(&experimental), ["b", "a"]);
|
||||
let mut released = serde_json::json!({"trace": ["a"]});
|
||||
formats.upgrade(&mut released, 2, None).unwrap();
|
||||
formats.upgrade("design", &mut released, 2, None).unwrap();
|
||||
assert_eq!(trace_of(&released), ["a", "b"], "ordinary released files still get later migrations");
|
||||
}
|
||||
}
|
||||
|
||||
/// A design naming a pending change this build doesn't have is a newer build's, refused as
|
||||
/// such before anything else in it is checked (as a newer number is), so a newer build's
|
||||
/// fields never read as damage.
|
||||
#[test]
|
||||
fn a_newer_builds_pending_change_is_refused_before_the_design_is_checked() {
|
||||
let mut file: Value = serde_json::from_str(&Document::new("A").to_json()).unwrap();
|
||||
file["format_pending"] = serde_json::json!(["not-a-change-anyone-made"]);
|
||||
file["features"] = serde_json::json!([{"type": "emboss", "face": {"body": "Body", "at": [0, 0, 0], "a_newer_key": true}}]);
|
||||
let error = read(&file.to_string()).unwrap_err().to_string();
|
||||
if PENDING.is_empty() {
|
||||
assert!(error.contains("emboss face"), "a list beside a folded format means nothing: {error}");
|
||||
} else {
|
||||
assert_eq!(
|
||||
error,
|
||||
format!(
|
||||
"this design was saved by a newer SolidCraft (file format {FORMAT} with `not-a-change-anyone-made`, which this version doesn't know): update SolidCraft to open it"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drawings follow the same rules from their own first format: folded formats follow on from 1
|
||||
/// (`v2/`, `v3/`, …), the next number is written while changes are pending, and the build lists
|
||||
/// every change file in the drawing's folder, in file-name order.
|
||||
#[test]
|
||||
fn drawing_formats_follow_on_from_one_and_every_change_file_is_listed() {
|
||||
for (i, (version, parts)) in DRAWING_FOLDED.iter().enumerate() {
|
||||
assert_eq!(*version, 2 + i as u32);
|
||||
assert!(!parts.is_empty(), "drawing format {version} has no changes");
|
||||
}
|
||||
assert_eq!(DRAWING.current(), 1 + DRAWING_FOLDED.len() as u32 + u32::from(!DRAWING_PENDING.is_empty()));
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/drawing/format");
|
||||
let names = |dir: &str| -> Vec<String> {
|
||||
let mut names: Vec<String> = std::fs::read_dir(root.join(dir))
|
||||
.map(|d| d.filter_map(|e| e.ok()?.file_name().to_str()?.strip_suffix(".rs").map(str::to_string)).collect())
|
||||
.unwrap_or_default();
|
||||
names.sort();
|
||||
names
|
||||
};
|
||||
assert_eq!(names("pending"), DRAWING.pending_names());
|
||||
for (version, parts) in DRAWING_FOLDED {
|
||||
assert_eq!(names(&format!("v{version}")), parts.iter().map(|p| p.name).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
/// A design's format tag never reads as a drawing's, nor a drawing's as a design's, at any
|
||||
/// number: no family's tag starts another's, the legacy tag is a design's only, and each family
|
||||
/// refuses the other's files by name.
|
||||
#[test]
|
||||
fn a_design_never_reads_as_a_drawing_or_the_other_way_round() {
|
||||
let families = [&DESIGN, &DRAWING];
|
||||
for a in families {
|
||||
assert!(a.prefix.len() > 1 && a.prefix.ends_with('/'), "{}", a.prefix);
|
||||
for b in families.into_iter().filter(|b| b.prefix != a.prefix) {
|
||||
assert!(!a.prefix.starts_with(b.prefix), "{} starts {}", b.prefix, a.prefix);
|
||||
}
|
||||
}
|
||||
for n in [0, 1, FORMAT, FORMAT + 1, DRAWING.current(), DRAWING.current() + 1, u32::MAX] {
|
||||
assert_eq!(DESIGN.version_of(&DESIGN.format_string(n)), Some(n));
|
||||
assert_eq!(DRAWING.version_of(&DRAWING.format_string(n)), Some(n));
|
||||
assert_eq!(DRAWING.version_of(&DESIGN.format_string(n)), None, "{n}");
|
||||
assert_eq!(DESIGN.version_of(&DRAWING.format_string(n)), None, "{n}");
|
||||
assert_eq!(DRAWING.version_of(&format!("{LEGACY_FORMAT_PREFIX}{n}")), None, "{n}: the legacy tag is a design's");
|
||||
}
|
||||
for tag in [
|
||||
"",
|
||||
"solidcraft-drawing/",
|
||||
"solidcraft-drawing",
|
||||
"solidcraft-drawing/x",
|
||||
"solidcraft-drawing/-1",
|
||||
"solidcraft-drawing/4294967296",
|
||||
"Solidcraft-drawing/1",
|
||||
"solidcraft-drawing/1 ",
|
||||
"solidcraft-drawings/1",
|
||||
"solidcraft/drawing/1",
|
||||
] {
|
||||
assert_eq!(DRAWING.version_of(tag), None, "{tag:?}");
|
||||
}
|
||||
// The number reads as Rust reads a `u32`, a leading `+` included, as in a design's tag.
|
||||
assert_eq!(DRAWING.version_of("solidcraft-drawing/+1"), Some(1));
|
||||
let error = DRAWING.take_version(&mut serde_json::json!({"format": format_string(FORMAT)})).unwrap_err().to_string();
|
||||
assert_eq!(error, format!("not a SolidCraft drawing (format `solidcraft/{FORMAT}`)"));
|
||||
let error = read(&serde_json::json!({"format": DRAWING.format_string(1)}).to_string()).unwrap_err().to_string();
|
||||
assert_eq!(error, "not a SolidCraft design (format `solidcraft-drawing/1`)");
|
||||
}
|
||||
|
||||
/// Every refusal names its family: a design's read exactly as they did before drawings had one
|
||||
/// (the troubleshooting guide quotes them), a drawing's say "drawing". A file that names no
|
||||
/// format is a design from before format strings, never a drawing.
|
||||
#[test]
|
||||
fn refusals_name_the_family_and_a_designs_read_as_before() {
|
||||
use serde_json::json;
|
||||
fn refusal(family: &Family<'_>, mut file: Value) -> String {
|
||||
family.take_version(&mut file).map_or_else(|e| e.to_string(), |opened| format!("read as {opened:?}"))
|
||||
}
|
||||
fn tagged(family: &Family<'_>, n: u32) -> Value {
|
||||
serde_json::json!({"format": family.format_string(n)})
|
||||
}
|
||||
fn newer(noun: &str, n: u32, current: u32) -> String {
|
||||
format!(
|
||||
"this {noun} was saved by a newer SolidCraft (file format {n}; this version reads formats up to {current}): update SolidCraft to open it"
|
||||
)
|
||||
}
|
||||
assert_eq!(refusal(&DESIGN, tagged(&DESIGN, FORMAT + 1)), newer("design", FORMAT + 1, FORMAT));
|
||||
assert_eq!(read(&tagged(&DESIGN, FORMAT + 1).to_string()).unwrap_err().to_string(), newer("design", FORMAT + 1, FORMAT));
|
||||
let current = DRAWING.current();
|
||||
assert!(matches!(DRAWING.take_version(&mut tagged(&DRAWING, current)), Ok((v, None)) if v == current));
|
||||
for n in [current + 1, u32::MAX] {
|
||||
assert_eq!(refusal(&DRAWING, tagged(&DRAWING, n)), newer("drawing", n, current));
|
||||
}
|
||||
for (family, noun) in [(&DESIGN, "design"), (&DRAWING, "drawing")] {
|
||||
assert_eq!(refusal(family, tagged(family, 0)), format!("not a SolidCraft {noun} (format 0)"));
|
||||
assert_eq!(refusal(family, json!([])), format!("not a SolidCraft {noun} (expected a JSON object)"));
|
||||
assert_eq!(refusal(family, json!({"format": 1})), format!("not a SolidCraft {noun} (format is not a string)"));
|
||||
assert_eq!(refusal(family, json!({"format": ""})), format!("not a SolidCraft {noun} (format ``)"));
|
||||
// `format_pending`'s caps, on both sides of each.
|
||||
let mut file = tagged(family, 1);
|
||||
file["format_pending"] = vec!["x".repeat(128); 1000].into();
|
||||
assert!(family.take_version(&mut file).is_ok(), "{noun}: 1,000 names of 128 bytes");
|
||||
for damaged in [vec!["x".repeat(128); 1001], vec!["x".repeat(129)]] {
|
||||
let mut file = tagged(family, 1);
|
||||
file["format_pending"] = damaged.into();
|
||||
assert_eq!(refusal(family, file), format!("damaged {noun}: `format_pending` is not a short list of names"));
|
||||
}
|
||||
}
|
||||
// A change this build doesn't know, in a design of the pending number.
|
||||
let pending = [traced("a-first")];
|
||||
let design = Family { formats: Formats { hand: &JSON_STEPS, folded: FOLDED, pending: &pending }, ..DESIGN };
|
||||
let next = design.current();
|
||||
let file = json!({"format": format_string(next), "format_pending": ["c-later"]});
|
||||
assert_eq!(
|
||||
refusal(&design, file),
|
||||
format!(
|
||||
"this design was saved by a newer SolidCraft (file format {next} with `c-later`, which this version doesn't know): update SolidCraft to open it"
|
||||
)
|
||||
);
|
||||
assert!(matches!(DESIGN.take_version(&mut json!({})), Ok((1, None))));
|
||||
assert_eq!(refusal(&DRAWING, json!({})), "not a SolidCraft drawing (no format)");
|
||||
}
|
||||
|
||||
/// One drawing change pending, as a drawing's first change will be: the build writes format 2
|
||||
/// and names the change in `format_pending`. Its step runs for a drawing without the change and
|
||||
/// never for one with it; a drawing naming a change this build lacks, or of format 3, is a newer
|
||||
/// build's.
|
||||
#[test]
|
||||
fn a_pending_drawing_change_makes_format_two() {
|
||||
use serde_json::json;
|
||||
let pending = [traced("a-first")];
|
||||
let drawing = Family { formats: Formats { hand: &[], folded: &[], pending: &pending }, ..DRAWING };
|
||||
assert_eq!((drawing.current(), drawing.format_string(drawing.current())), (2, "solidcraft-drawing/2".to_string()));
|
||||
assert_eq!(drawing.pending_names(), ["a-first"]);
|
||||
let open = |mut file: Value| -> Result<Vec<String>> {
|
||||
let (version, applied) = drawing.take_version(&mut file)?;
|
||||
assert!(file.get("format_pending").is_none(), "taken out of the JSON before the record is read");
|
||||
drawing.upgrade(&mut file, version, applied.as_deref())?;
|
||||
Ok(trace_of(&file).into_iter().map(str::to_string).collect())
|
||||
};
|
||||
assert_eq!(open(json!({"format": "solidcraft-drawing/1"})).unwrap(), ["a"]);
|
||||
assert_eq!(open(json!({"format": "solidcraft-drawing/2", "format_pending": []})).unwrap(), ["a"]);
|
||||
assert_eq!(open(json!({"format": "solidcraft-drawing/2", "format_pending": ["a-first"]})).unwrap(), Vec::<String>::new());
|
||||
// A list beside format 1 means nothing: the step still runs.
|
||||
assert_eq!(open(json!({"format": "solidcraft-drawing/1", "format_pending": ["c-later"]})).unwrap(), ["a"]);
|
||||
let error = open(json!({"format": "solidcraft-drawing/2", "format_pending": ["a-first", "c-later"]})).unwrap_err().to_string();
|
||||
assert_eq!(
|
||||
error,
|
||||
"this drawing was saved by a newer SolidCraft (file format 2 with `c-later`, which this version doesn't know): update SolidCraft to open it"
|
||||
);
|
||||
let error = open(json!({"format": "solidcraft-drawing/3"})).unwrap_err().to_string();
|
||||
assert_eq!(
|
||||
error,
|
||||
"this drawing was saved by a newer SolidCraft (file format 3; this version reads formats up to 2): update SolidCraft to open it"
|
||||
);
|
||||
// An upgrade called without `take_version` refuses in the drawing's words too, format 0 included.
|
||||
let error = drawing.upgrade(&mut json!({}), 3, None).unwrap_err().to_string();
|
||||
assert!(error.starts_with("this drawing was saved by a newer SolidCraft (file format 3;"), "{error}");
|
||||
assert_eq!(drawing.upgrade(&mut json!({}), 0, None).unwrap_err().to_string(), "not a SolidCraft drawing (format 0)");
|
||||
}
|
||||
|
||||
/// `format_pending` is read and checked: a damaged list is refused, a list of changes this
|
||||
/// build doesn't have is a newer build's, and saving writes this build's own list.
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
//! Linked components retain the last inherited physical material, explicit body look and
|
||||
//! authored face overrides by imported body slot. Face baselines are optional: an older
|
||||
//! link is read again on Update Links while preserving local assignments. Authored face
|
||||
//! overrides stay separate from imported STEP paint, including under a body appearance.
|
||||
//! Source-only property edits can then update the link while
|
||||
//! differing local assignments survive. Older links have no baseline: existing assignments
|
||||
//! are preserved and missing ones can be recovered on Update Links. The last release
|
||||
//! refuses the newer design format instead of silently discarding these baselines.
|
||||