Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b1971b584 |
@@ -1,20 +0,0 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
reviews:
|
||||
auto_review:
|
||||
# Dependabot bumps are checked by CI; skip CodeRabbit reviews on them.
|
||||
ignore_usernames:
|
||||
- "dependabot[bot]"
|
||||
path_instructions:
|
||||
- path: "**"
|
||||
instructions: |
|
||||
Project rule: don't change anyone's existing defaults unless the change is purely additive and belongs in the application.
|
||||
Flag any change to something a user already relies on: a default setting or preference value, what a command or tool does when used the usual way, the UI's default layout or behaviour, a keyboard or mouse shortcut, or the result an existing saved design rebuilds to.
|
||||
Don't flag purely additive changes: a new command, a new setting whose default keeps today's behaviour, or a new option that appears only when someone chooses it. Don't flag bug fixes that only turn a crash or an error into the result the feature always promised.
|
||||
When you flag one, start the comment with "Changes a default:", say what a user sees before and after, whether a setting keeps the old behaviour, and whether the PR description's `Changes a default:` line says so.
|
||||
pre_merge_checks:
|
||||
custom_checks:
|
||||
- name: "Existing defaults unchanged"
|
||||
mode: "warning"
|
||||
instructions: |
|
||||
Pass if the pull request changes no default a user already relies on (default setting or preference values, what a command or tool does when used the usual way, the UI's default layout or behaviour, shortcuts, or the result an existing saved design rebuilds to), or if its changes are purely additive.
|
||||
Fail otherwise, naming each changed default and what a user sees before and after. Also fail if the description says `Changes a default: no` while the code changes one.
|
||||
@@ -784,9 +784,6 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
|
||||
| `assets/locales/pending/sketch-offset-instances.json` | SolidCraft contributors | Original offset instance-count label and translations | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_profile.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original analytic head and shoulder geometry for settings profiles | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/4561-convert-box-mesh-with-a-zero-area-triangle/box-degenerate.stl | SolidCraft contributors | synthetic ASCII STL written by hand: a box with one zero-area triangle, for the kernel regression case of tangency-labs/solidcraft#4561 | MIT OR Apache-2.0 |
|
||||
| assets/locales/pending/theme-blur-ui.json | SolidCraft contributors | Original blur controls, notices and translations | MIT OR Apache-2.0 |
|
||||
| crates/ui-egui/tests/fixtures/theme-blur.json | SolidCraft contributors | Original theme for blur option scenarios and manager review | MIT OR Apache-2.0 |
|
||||
| assets/locales/ka.json | David Machakhelidze (devius) | Original Georgian translation of SolidCraft's English catalog, written with Claude Code from the meaning of each English string; terminology and doubtful strings decided by a native Georgian speaker; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
|
||||
@@ -4102,7 +4102,6 @@ dependencies = [
|
||||
"ureq",
|
||||
"windows-sys 0.61.2",
|
||||
"winit",
|
||||
"winreg",
|
||||
"x11rb",
|
||||
"zbus 5.19.0",
|
||||
]
|
||||
@@ -4219,7 +4218,6 @@ dependencies = [
|
||||
"gethostname",
|
||||
"gltf",
|
||||
"log",
|
||||
"png",
|
||||
"quick-xml",
|
||||
"rustix 1.1.5",
|
||||
"serde",
|
||||
@@ -4230,10 +4228,8 @@ dependencies = [
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-sketch",
|
||||
"solidcraft-testkit",
|
||||
"thiserror 2.0.21",
|
||||
"tiny-skia",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -4297,7 +4293,6 @@ dependencies = [
|
||||
"harfrust",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"solidcore-record",
|
||||
"solidcraft-geom",
|
||||
"thiserror 2.0.21",
|
||||
|
||||
@@ -173,12 +173,6 @@ and the [foundation ledger](docs/drawing-foundations.md) track the bounded imple
|
||||
packages. The first geometry increment defines validated section records in a plane-local
|
||||
frame; the ledger separates that contract from section producers and the Drawing workspace.
|
||||
|
||||
Drawing UI scaffold: the native workspace now supplies the shared four-tab ribbon,
|
||||
source Browser, compact dockable sheet strip and resizable Sheet Settings/options
|
||||
panel. Planned tools remain unavailable; only existing source services execute.
|
||||
Drawing files and paper rendering are still deferred. See the
|
||||
[UI implementation roadmap](docs/drawing-workspace-ui.md).
|
||||
|
||||
Desktop view DXF export runs in the file-operation worker with five-stage progress and
|
||||
cooperative cancellation. Curve tolerance (0.0001–1 mm, default 0.01 mm) is remembered alongside
|
||||
the hidden-edge setting. Cancelled exports preserve the model and the previous destination;
|
||||
|
||||
@@ -63,10 +63,6 @@ fn replay(report: &Value) -> Result<(), String> {
|
||||
Some(d) => super::load(d)?,
|
||||
None => solidcraft_engine::Session::default(),
|
||||
};
|
||||
// The ids the session drew from there, so the commands give the same objects the same ids.
|
||||
if let Some(seed) = script["ids"].as_u64() {
|
||||
s.seed_ids(seed);
|
||||
}
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let loc = info.location().map_or_else(|| "(unknown)".to_string(), |l| format!("{}:{}", l.file(), l.line()));
|
||||
let msg = info.payload_as_str().unwrap_or("(not a text message)").to_string();
|
||||
|
||||
@@ -460,13 +460,16 @@ fn snapshot_runs_its_script_on_the_in_design() {
|
||||
}
|
||||
}
|
||||
|
||||
/// An unknown command's error suggests the id that was meant first; other close ids (such as
|
||||
/// `drawing.open`) may follow it.
|
||||
/// An unknown command's error suggests the id that was meant.
|
||||
#[test]
|
||||
fn unknown_command_suggests_the_closest_id() {
|
||||
let output = cli().args(["exec", "file.open"]).output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("did you mean `doc.open`"), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains("did you mean `doc.open` or `panel.open`?"),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
/// #1833: `--timings` reports each command's time instead of the measurement, `--profile` adds
|
||||
|
||||
@@ -26,15 +26,6 @@
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/OFL.txt" data-target-path="fonts/craft-ui-sc" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/CraftUI-TC.otf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/OFL.txt" data-target-path="fonts/craft-ui-tc" />
|
||||
<!-- A drawing's Arabic, Korean and Chinese text: each fetched once, when a drawing needs it. -->
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-arabic/NotoSansArabic-Regular.ttf" data-target-path="fonts/noto-sans-arabic" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-arabic/OFL.txt" data-target-path="fonts/noto-sans-arabic" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-kr/NotoSansKR-Regular.otf" data-target-path="fonts/noto-sans-kr" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-kr/OFL.txt" data-target-path="fonts/noto-sans-kr" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-sc/NotoSansSC-Regular.otf" data-target-path="fonts/noto-sans-sc" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-sc/OFL.txt" data-target-path="fonts/noto-sans-sc" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-tc/NotoSansTC-Regular.otf" data-target-path="fonts/noto-sans-tc" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-tc/OFL.txt" data-target-path="fonts/noto-sans-tc" />
|
||||
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/styles/web/loading-logo.svg" />
|
||||
<!-- Installable as an app (manifest.webmanifest); the icons are the Linux exports of the app mark. -->
|
||||
|
||||
@@ -23,11 +23,6 @@ mod linked;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod space_mouse;
|
||||
|
||||
/// The folder the page serves its fonts from: index.html copies each there, and a drawing's text
|
||||
/// fetches its fonts from it (#4890).
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
const FONTS: &str = "fonts";
|
||||
|
||||
/// The browser switches tabs with Ctrl+Tab and Ctrl+Shift+Tab, so the page leaves those keys to
|
||||
/// it (#5227); plain Tab and Shift+Tab still move between the app's fields. (Ctrl+PageUp and
|
||||
/// Ctrl+PageDown already reach the browser.)
|
||||
@@ -176,7 +171,6 @@ mod web {
|
||||
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::io::vfs;
|
||||
use solidcraft_ui_egui::ui_kit::BackingTooltipExt as _;
|
||||
use solidcraft_ui_egui::{Services, SolveApp, ui_kit};
|
||||
use wasm_bindgen::JsCast as _;
|
||||
|
||||
@@ -292,8 +286,6 @@ mod web {
|
||||
const CHINESE_TC_FONT_PATH: &str = "./fonts/CraftUI-TC.otf";
|
||||
|
||||
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<(String, Vec<u8>), String>>>>;
|
||||
/// The fetches of drawing fonts that have finished, for the next frame to hand on.
|
||||
type DrawingFontsDone = Rc<std::cell::RefCell<Vec<(solidcraft_engine::sketch::fallback::WebFont, Result<Vec<u8>, String>)>>>;
|
||||
|
||||
struct Shell {
|
||||
app: SolveApp,
|
||||
@@ -324,9 +316,6 @@ mod web {
|
||||
font_fetch_kind: Option<&'static str>,
|
||||
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
|
||||
font_language: String,
|
||||
/// The fonts a drawing's text asked for, other than Japanese, each fetched once a session.
|
||||
drawing_fonts_asked: std::collections::HashSet<solidcraft_engine::sketch::fallback::WebFont>,
|
||||
drawing_fonts_done: DrawingFontsDone,
|
||||
space_mouse: SpaceMouse,
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures,
|
||||
/// The settings this tab wrote or loaded last, and those another tab wrote since (#8644).
|
||||
@@ -511,7 +500,7 @@ mod web {
|
||||
files::pick_from_disk(&self.files, Intent::Open);
|
||||
}
|
||||
if ui_kit::button(ui, "Insert from computer…", ui_kit::Button::Normal)
|
||||
.backing_on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
|
||||
.clicked()
|
||||
{
|
||||
files::pick_from_disk(&self.files, Intent::Insert);
|
||||
@@ -565,7 +554,7 @@ mod web {
|
||||
let design = solidcraft_engine::io::is_design_path(&e.name);
|
||||
if !design
|
||||
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
|
||||
.backing_on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("insert", e.name.clone()));
|
||||
@@ -575,7 +564,7 @@ mod web {
|
||||
self.rename_focus = true;
|
||||
}
|
||||
if ui_kit::button(ui, "Save to computer…", ui_kit::Button::Small)
|
||||
.backing_on_hover_text("A copy in a file you choose; the stored design stays here")
|
||||
.on_hover_text("A copy in a file you choose; the stored design stays here")
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("save_copy", e.name.clone()));
|
||||
@@ -730,10 +719,6 @@ mod web {
|
||||
self.chinese_kind = Some("zh-TW");
|
||||
}
|
||||
_ => {
|
||||
solidcraft_engine::sketch::fallback::provide_web_font(
|
||||
solidcraft_engine::sketch::fallback::WebFont::Japanese,
|
||||
bytes.clone(),
|
||||
);
|
||||
self.cjk_bytes = Some(bytes);
|
||||
}
|
||||
}
|
||||
@@ -747,9 +732,6 @@ mod web {
|
||||
_ => JAPANESE_FONT_PATH,
|
||||
};
|
||||
log::warn!("failed to fetch script font from {path}: {err}");
|
||||
if !matches!(kind, Some("zh" | "zh-TW")) {
|
||||
solidcraft_engine::sketch::fallback::web_font_failed(solidcraft_engine::sketch::fallback::WebFont::Japanese);
|
||||
}
|
||||
false
|
||||
}
|
||||
};
|
||||
@@ -785,12 +767,10 @@ mod web {
|
||||
self.font_language = effective.to_owned();
|
||||
}
|
||||
|
||||
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name), or a
|
||||
/// drawing's text waiting for one: fetch the Japanese font, so the text does not stay tofu (#720).
|
||||
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name):
|
||||
/// fetch the Japanese font, so the text does not stay tofu (#720).
|
||||
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
|
||||
let japanese = solidcraft_engine::sketch::fallback::web_fonts_wanted().contains(&solidcraft_engine::sketch::fallback::WebFont::Japanese);
|
||||
let wanted = solidcraft_ui_egui::fonts::painted_cjk(ctx) || japanese;
|
||||
if self.font.watching_text() && wanted && self.font.text_needs_font() {
|
||||
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
|
||||
self.fetch_font(ctx, "ja");
|
||||
}
|
||||
}
|
||||
@@ -811,33 +791,6 @@ mod web {
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/// The other fonts a drawing's text waits for (Arabic, Chinese, Korean): each fetched once,
|
||||
/// and handed to the text when it comes. Japanese comes with the interface's CJK font.
|
||||
fn fetch_drawing_fonts(&mut self, ctx: &egui::Context) {
|
||||
use solidcraft_engine::sketch::fallback::{self, WebFont};
|
||||
for (font, res) in self.drawing_fonts_done.borrow_mut().drain(..) {
|
||||
match res {
|
||||
Ok(bytes) => fallback::provide_web_font(font, bytes),
|
||||
Err(err) => {
|
||||
log::warn!("failed to fetch the drawing font {}/{}: {err}", crate::FONTS, font.file());
|
||||
fallback::web_font_failed(font);
|
||||
}
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
for font in fallback::web_fonts_wanted() {
|
||||
if font == WebFont::Japanese || !self.drawing_fonts_asked.insert(font) {
|
||||
continue;
|
||||
}
|
||||
let (done, ctx) = (self.drawing_fonts_done.clone(), ctx.clone());
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let res = fetch_font_bytes(&format!("./{}/{}", crate::FONTS, font.file())).await;
|
||||
done.borrow_mut().push((font, res));
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Shell {
|
||||
@@ -845,7 +798,6 @@ mod web {
|
||||
// First: a language another tab chose fetches its font this frame.
|
||||
self.load_other_tabs_prefs();
|
||||
self.tick_font(ctx);
|
||||
self.fetch_drawing_fonts(ctx);
|
||||
self.app.logic(ctx);
|
||||
self.api.poll();
|
||||
self.space_mouse.logic(&mut self.app, ctx);
|
||||
@@ -1069,8 +1021,6 @@ mod web {
|
||||
chinese_kind: None,
|
||||
font_fetch_kind: None,
|
||||
font_language,
|
||||
drawing_fonts_asked: Default::default(),
|
||||
drawing_fonts_done: Default::default(),
|
||||
space_mouse: SpaceMouse::default(),
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
|
||||
prefs_writer: crate::PrefsWriter::default(),
|
||||
@@ -1236,20 +1186,4 @@ mod tests {
|
||||
assert_eq!(settings_written(key, None, None), None, "clearing all storage leaves this tab's settings");
|
||||
assert_eq!(settings_written(key, Some("rc:solidcraft.prefs"), Some("{}".into())), None, "another channel's settings are not loaded");
|
||||
}
|
||||
|
||||
/// Each font a drawing's text can ask for on the web is one index.html copies, from an asset
|
||||
/// in the repository, into the folder the app fetches it from.
|
||||
#[test]
|
||||
fn each_drawing_font_is_copied_where_it_is_fetched() {
|
||||
let page = include_str!("../index.html");
|
||||
let here = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
for font in solidcraft_engine::sketch::fallback::WebFont::ALL {
|
||||
let served = format!("{FONTS}/{}", font.file());
|
||||
let (folder, file) = served.rsplit_once('/').unwrap_or_default();
|
||||
let copy = page.lines().find(|l| l.contains("rel=\"copy-file\"") && l.contains(&format!("/{file}\""))).unwrap_or_default();
|
||||
assert!(copy.contains(&format!("data-target-path=\"{folder}\"")), "index.html doesn't copy {served}: {copy}");
|
||||
let from = copy.split("href=\"").nth(1).and_then(|rest| rest.split('"').next()).unwrap_or_default();
|
||||
assert!(here.join(from).is_file(), "{served}: no {from}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ windows-sys = { version = "0.61", features = [
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
winreg = "0.56"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
muda = { version = "0.21.2", default-features = false }
|
||||
@@ -93,7 +92,7 @@ hidapi = { version = "2.6.7", default-features = false, features = ["macos-share
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs), and the fullscreen title-bar
|
||||
# inset (src/main.rs); all already come with eframe and rfd.
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSAccessibility", "NSApplication", "NSResponder", "NSWindow", "NSWorkspace"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
|
||||
block2 = "0.6"
|
||||
# Native crash capture (src/native_crash_macos.rs): the signal handler, the faulting module
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
//! Cached accessibility signal, observed only while a theme offers blur. No UI-thread IO.
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU8, Ordering},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Reading {
|
||||
needed: AtomicBool,
|
||||
started: AtomicBool,
|
||||
value: AtomicU8,
|
||||
}
|
||||
|
||||
pub fn cached(ctx: &egui::Context) -> Box<dyn Fn(bool) -> Option<bool>> {
|
||||
let state = Arc::new(Reading::default());
|
||||
let ctx = ctx.clone();
|
||||
Box::new(move |needed| {
|
||||
state.needed.store(needed, Ordering::Relaxed);
|
||||
if needed && !state.started.swap(true, Ordering::Relaxed) {
|
||||
let weak = Arc::downgrade(&state);
|
||||
let ctx = ctx.clone();
|
||||
if let Err(error) = std::thread::Builder::new().name("desktop-transparency".into()).spawn(move || {
|
||||
while let Some(state) = weak.upgrade() {
|
||||
if state.needed.load(Ordering::Relaxed) {
|
||||
let code = match read() {
|
||||
Some(true) => 1,
|
||||
Some(false) => 2,
|
||||
None => 0,
|
||||
};
|
||||
if state.value.swap(code, Ordering::Relaxed) != code {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
}
|
||||
}) {
|
||||
log::warn!("desktop transparency: could not observe accessibility setting: {error}");
|
||||
}
|
||||
}
|
||||
match state.value.load(Ordering::Relaxed) {
|
||||
1 => Some(true),
|
||||
2 => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn read() -> Option<bool> {
|
||||
let key =
|
||||
winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER).open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize").ok()?;
|
||||
match key.get_value::<u32, _>("EnableTransparency").ok()? {
|
||||
0 => Some(true),
|
||||
1 => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read() -> Option<bool> {
|
||||
objc2::rc::autoreleasepool(|_| Some(objc2_app_kit::NSWorkspace::sharedWorkspace().accessibilityDisplayShouldReduceTransparency()))
|
||||
}
|
||||
@@ -24,8 +24,6 @@ mod come_forward;
|
||||
mod control_server;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod desktop_theme;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
mod desktop_transparency;
|
||||
mod device_lost;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_crash;
|
||||
@@ -541,10 +539,6 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
|
||||
desktop_dark: Some(desktop_theme::start(&cc.egui_ctx)),
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
desktop_dark: None,
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
reduce_transparency: Some(desktop_transparency::cached(&cc.egui_ctx)),
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
reduce_transparency: None,
|
||||
http_get: Some(std::sync::Arc::new(http_get)),
|
||||
http_download: Some(std::sync::Arc::new(http_download)),
|
||||
// Already an installed app: only the browser build offers to install.
|
||||
|
||||
@@ -22,7 +22,7 @@ assets/
|
||||
appearances/library.rs # built-in visual presets
|
||||
materials/library.rs # physical defaults and colours
|
||||
shaders/viewport.wgsl # original viewport shader
|
||||
fonts/<family>/ # unmodified fonts and OFL licences
|
||||
fonts/{liberation-sans,biz-udp-gothic}/ # unmodified fonts and OFL licences
|
||||
locales/{en,de,ja,ko}.json # shared offline catalogs
|
||||
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
|
||||
platforms/macos/dmg/ # disk image window: layout, dmgbuild settings, background art
|
||||
@@ -79,7 +79,7 @@ The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
|
||||
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `io.tangencylabs.solidcraft` |
|
||||
| AppImage | Uses the Linux 256 px image and `.DirIcon` alias |
|
||||
| FreeBSD | Shares Linux hicolor images and freedesktop metadata |
|
||||
| Web | HTML/Trunk and `manifest.webmanifest` stay in `apps/solidcraft-web/`; copies the Japanese font/licence to `fonts/` and the Noto Sans Arabic, KR, SC and TC fonts with their licences to `fonts/<family>/` (fetched when a drawing's text needs them), uses the collected CSS, and copies the Linux 32/256/512 px images to `icons/` for the favicon and installed app |
|
||||
| Web | HTML/Trunk and `manifest.webmanifest` stay in `apps/solidcraft-web/`; copies Japanese font/licence to `fonts/`, uses the collected CSS, and copies the Linux 32/256/512 px images to `icons/` for the favicon and installed app |
|
||||
| Docs and link previews | `xtask book` copies the shared card to the existing `/solidcraft/og.png` public path |
|
||||
|
||||
## Code and provenance
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,92 +0,0 @@
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,92 +0,0 @@
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,92 +0,0 @@
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -178,7 +178,6 @@ pattern_rect false
|
||||
perforate false
|
||||
perspective false
|
||||
pipe false
|
||||
placeholder true
|
||||
plane false
|
||||
plane_along_path false
|
||||
plane_angle false
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Align drawing annotations</title>
|
||||
<path d="M12 6L12 58" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M22.5 12L51.5 12A2.5 2.5 0 0 1 54 14.5L54 17.5A2.5 2.5 0 0 1 51.5 20L22.5 20A2.5 2.5 0 0 1 20 17.5L20 14.5A2.5 2.5 0 0 1 22.5 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M22.5 28L40.5 28A2.5 2.5 0 0 1 43 30.5L43 33.5A2.5 2.5 0 0 1 40.5 36L22.5 36A2.5 2.5 0 0 1 20 33.5L20 30.5A2.5 2.5 0 0 1 22.5 28Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M22.5 44L47.5 44A2.5 2.5 0 0 1 50 46.5L50 49.5A2.5 2.5 0 0 1 47.5 52L22.5 52A2.5 2.5 0 0 1 20 49.5L20 46.5A2.5 2.5 0 0 1 22.5 44Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 962 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Align drawing views</title>
|
||||
<path d="M10.5 24L23.5 24A2.5 2.5 0 0 1 26 26.5L26 43.5A2.5 2.5 0 0 1 23.5 46L10.5 46A2.5 2.5 0 0 1 8 43.5L8 26.5A2.5 2.5 0 0 1 10.5 24Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M39.5 12L52.5 12A2.5 2.5 0 0 1 55 14.5L55 43.5A2.5 2.5 0 0 1 52.5 46L39.5 46A2.5 2.5 0 0 1 37 43.5L37 14.5A2.5 2.5 0 0 1 39.5 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M6 52L58 52" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M17 29L17 40M46 18L46 39" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 846 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Drawing balloon</title>
|
||||
<path d="M9.5 37L29.5 37A2.5 2.5 0 0 1 32 39.5L32 52.5A2.5 2.5 0 0 1 29.5 55L9.5 55A2.5 2.5 0 0 1 7 52.5L7 39.5A2.5 2.5 0 0 1 9.5 37Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M20 45L35 30L40 25" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M15 50L19 37L28 45Z" fill="#236ead"/>
|
||||
<circle cx="45" cy="17" r="12" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 651 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Base drawing view</title>
|
||||
<path d="M9.5 10L54.5 10A2.5 2.5 0 0 1 57 12.5L57 51.5A2.5 2.5 0 0 1 54.5 54L9.5 54A2.5 2.5 0 0 1 7 51.5L7 12.5A2.5 2.5 0 0 1 9.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M17.5 19L42.5 19A2.5 2.5 0 0 1 45 21.5L45 39.5A2.5 2.5 0 0 1 42.5 42L17.5 42A2.5 2.5 0 0 1 15 39.5L15 21.5A2.5 2.5 0 0 1 17.5 19Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="30" cy="30.5" r="6" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M47 46L53 46M50 43L50 49" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 851 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Drawing centre mark</title>
|
||||
<circle cx="32" cy="32" r="18" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 6L32 21M32 27L32 37M32 43L32 58M6 32L21 32M27 32L37 32M43 32L58 32" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 420 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Check drawing</title>
|
||||
<path d="M14.5 7L49.5 7A2.5 2.5 0 0 1 52 9.5L52 54.5A2.5 2.5 0 0 1 49.5 57L14.5 57A2.5 2.5 0 0 1 12 54.5L12 9.5A2.5 2.5 0 0 1 14.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M21 19L42 19M21 29L35 29" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M27 44L37 53L57 30" fill="none" stroke="#ffffff" stroke-width="6.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M27 44L37 53L57 30" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 730 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Drawing datum</title>
|
||||
<path d="M6 53L58 53" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 52L24 38L40 38Z" fill="#236ead"/>
|
||||
<path d="M32 38L32 27" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M21.5 7L42.5 7A2.5 2.5 0 0 1 45 9.5L45 24.5A2.5 2.5 0 0 1 42.5 27L21.5 27A2.5 2.5 0 0 1 19 24.5L19 9.5A2.5 2.5 0 0 1 21.5 7Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 631 B |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Detail drawing view</title>
|
||||
<path d="M9.5 11L30.5 11A2.5 2.5 0 0 1 33 13.5L33 45.5A2.5 2.5 0 0 1 30.5 48L9.5 48A2.5 2.5 0 0 1 7 45.5L7 13.5A2.5 2.5 0 0 1 9.5 11Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M13 18L23 18L23 33L29 33" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="23" cy="30" r="9" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M31 35L39 41" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="47" cy="46" r="12" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M40 39L46 39L46 48L53 48" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1000 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Drawing dimension</title>
|
||||
<path d="M10 10L10 55M54 10L54 55" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10 14L54 14L54 36L10 36Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M20 49L44 49" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M11 49L23 43L23 55Z" fill="#236ead"/>
|
||||
<path d="M53 49L41 55L41 43Z" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 598 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Edit drawing table</title>
|
||||
<path d="M10.5 10L53.5 10A2.5 2.5 0 0 1 56 12.5L56 51.5A2.5 2.5 0 0 1 53.5 54L10.5 54A2.5 2.5 0 0 1 8 51.5L8 12.5A2.5 2.5 0 0 1 10.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10 12L54 12L54 23L10 23Z" fill="#b8c3cf"/>
|
||||
<path d="M8 24L56 24M8 39L56 39M23 24L23 54" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M25 26L54 26L54 37L25 37Z" fill="#b9ddf8"/>
|
||||
<path d="M32 32L47 32M42 27L47 32L42 37" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 741 B |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Hole callout</title>
|
||||
<circle cx="18" cy="43" r="13" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="18" cy="43" r="6" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M27 35L39 22L57 22" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M23 40L27 28L34 35Z" fill="#236ead"/>
|
||||
<circle cx="44" cy="10" r="4.5" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M39 16L49 4" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 794 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Insert drawing image</title>
|
||||
<path d="M9.5 11L54.5 11A2.5 2.5 0 0 1 57 13.5L57 50.5A2.5 2.5 0 0 1 54.5 53L9.5 53A2.5 2.5 0 0 1 7 50.5L7 13.5A2.5 2.5 0 0 1 9.5 11Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="43" cy="23" r="5" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10 45L24 27L36 41L42 34L54 47Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 622 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Drawing leader</title>
|
||||
<path d="M8.5 34L33.5 34A2.5 2.5 0 0 1 36 36.5L36 51.5A2.5 2.5 0 0 1 33.5 54L8.5 54A2.5 2.5 0 0 1 6 51.5L6 36.5A2.5 2.5 0 0 1 8.5 34Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M18 45L39 18L56 18" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M15 49L19 35L26 41Z" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 516 B |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Drawing parts list</title>
|
||||
<path d="M10.5 10L53.5 10A2.5 2.5 0 0 1 56 12.5L56 51.5A2.5 2.5 0 0 1 53.5 54L10.5 54A2.5 2.5 0 0 1 8 51.5L8 12.5A2.5 2.5 0 0 1 10.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10 12L54 12L54 23L10 23Z" fill="#b8c3cf"/>
|
||||
<path d="M8 24L56 24M8 39L56 39M23 24L23 54" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="15.5" cy="31.5" r="3.5" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="15.5" cy="46.5" r="3.5" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M30 31L48 31M30 46L48 46" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 958 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Print drawing</title>
|
||||
<path d="M18.5 7L45.5 7A2.5 2.5 0 0 1 48 9.5L48 29.5A2.5 2.5 0 0 1 45.5 32L18.5 32A2.5 2.5 0 0 1 16 29.5L16 9.5A2.5 2.5 0 0 1 18.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M8.5 24L55.5 24A2.5 2.5 0 0 1 58 26.5L58 49.5A2.5 2.5 0 0 1 55.5 52L8.5 52A2.5 2.5 0 0 1 6 49.5L6 26.5A2.5 2.5 0 0 1 8.5 24Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="48" cy="32" r="2.5" fill="#236ead" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M18.5 37L45.5 37A2.5 2.5 0 0 1 48 39.5L48 55.5A2.5 2.5 0 0 1 45.5 58L18.5 58A2.5 2.5 0 0 1 16 55.5L16 39.5A2.5 2.5 0 0 1 18.5 37Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M23 44L41 44M23 51L36 51" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Projected drawing view</title>
|
||||
<path d="M9.5 33L31.5 33A2.5 2.5 0 0 1 34 35.5L34 52.5A2.5 2.5 0 0 1 31.5 55L9.5 55A2.5 2.5 0 0 1 7 52.5L7 35.5A2.5 2.5 0 0 1 9.5 33Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="20.5" cy="44" r="5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9.5 8L31.5 8A2.5 2.5 0 0 1 34 10.5L34 19.5A2.5 2.5 0 0 1 31.5 22L9.5 22A2.5 2.5 0 0 1 7 19.5L7 10.5A2.5 2.5 0 0 1 9.5 8Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9 25L9 30M32 25L32 30M38 44L54 44" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="3 5"/>
|
||||
<path d="M44 28L44 13" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M44 7L48 14L40 14Z" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Browse drawing resources</title>
|
||||
<path d="M15 7L51 7A3 3 0 0 1 54 10L54 48A3 3 0 0 1 51 51L15 51A3 3 0 0 1 12 48L12 10A3 3 0 0 1 15 7Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10 12L46 12A3 3 0 0 1 49 15L49 53A3 3 0 0 1 46 56L10 56A3 3 0 0 1 7 53L7 15A3 3 0 0 1 10 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M14 20L42 20L42 47L14 47Z" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M14 38L42 38M28 38L28 47" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M22 27L34 27L34 36L22 36Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 933 B |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Add drawing revision</title>
|
||||
<path d="M14.5 7L49.5 7A2.5 2.5 0 0 1 52 9.5L52 54.5A2.5 2.5 0 0 1 49.5 57L14.5 57A2.5 2.5 0 0 1 12 54.5L12 9.5A2.5 2.5 0 0 1 14.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M20 19L43 19M20 29L37 29" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M43 33L58 57L28 57Z" fill="#ffffff" stroke="#ffffff" stroke-width="4.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M43 33L58 57L28 57Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 745 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Drawing revision table</title>
|
||||
<path d="M10.5 10L53.5 10A2.5 2.5 0 0 1 56 12.5L56 51.5A2.5 2.5 0 0 1 53.5 54L10.5 54A2.5 2.5 0 0 1 8 51.5L8 12.5A2.5 2.5 0 0 1 10.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10 12L54 12L54 23L10 23Z" fill="#b8c3cf"/>
|
||||
<path d="M8 24L56 24M8 39L56 39M23 24L23 54" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M15 27L10 35L20 35ZM15 42L10 50L20 50Z" fill="#236ead"/>
|
||||
<path d="M31 31L49 31M31 46L49 46" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 752 B |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Section drawing view</title>
|
||||
<path d="M8 15L56 15L56 50L37 50L37 27L27 27L27 50L8 50Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10 38L26 22M10 25L20 15M10 50L24 36M39 35L54 20M39 48L54 33M49 50L56 43" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 450 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Space drawing annotations</title>
|
||||
<path d="M28.5 9L53.5 9A2.5 2.5 0 0 1 56 11.5L56 14.5A2.5 2.5 0 0 1 53.5 17L28.5 17A2.5 2.5 0 0 1 26 14.5L26 11.5A2.5 2.5 0 0 1 28.5 9Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M28.5 28L53.5 28A2.5 2.5 0 0 1 56 30.5L56 33.5A2.5 2.5 0 0 1 53.5 36L28.5 36A2.5 2.5 0 0 1 26 33.5L26 30.5A2.5 2.5 0 0 1 28.5 28Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M28.5 47L53.5 47A2.5 2.5 0 0 1 56 49.5L56 52.5A2.5 2.5 0 0 1 53.5 55L28.5 55A2.5 2.5 0 0 1 26 52.5L26 49.5A2.5 2.5 0 0 1 28.5 47Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M15 19L15 25M15 38L15 44" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M15 14L10 21L20 21ZM15 30L10 23L20 23ZM15 33L10 40L20 40ZM15 49L10 42L20 42Z" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Update drawing</title>
|
||||
<path d="M14.5 7L49.5 7A2.5 2.5 0 0 1 52 9.5L52 54.5A2.5 2.5 0 0 1 49.5 57L14.5 57A2.5 2.5 0 0 1 12 54.5L12 9.5A2.5 2.5 0 0 1 14.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M22.5 16L41.5 16A2.5 2.5 0 0 1 44 18.5L44 28.5A2.5 2.5 0 0 1 41.5 31L22.5 31A2.5 2.5 0 0 1 20 28.5L20 18.5A2.5 2.5 0 0 1 22.5 16Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M38 35A11 11 0 1 0 45 45" fill="none" stroke="#ffffff" stroke-width="6.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M38 35A11 11 0 1 0 45 45" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M44 40L32 37L40 28Z" fill="#236ead"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 900 B |
@@ -744,22 +744,22 @@
|
||||
"ai.prefs.availability": "التوفر",
|
||||
"ai.prefs.background_commands": "تشغيل الأوامر البطيئة في الخلفية",
|
||||
"ai.prefs.connect_apps": "إضافة SolidCraft إلى تطبيقات الذكاء الاصطناعي",
|
||||
"ai.prefs.connect_apps_note": "إيقافه يزيل الإدخالات مجددًا.\n\nعند بدء تشغيل SolidCraft، يُضاف كخادم MCP إلى تطبيقات الذكاء الاصطناعي الموجودة على هذا الحاسوب (OpenCode وClaude Desktop وClaude Code وCursor وCodex وGemini CLI وCopilot CLI وWindsurf وZed وVS Code)، ليتمكن الوكيل من استخدامه دون أي إعداد. لا يُضاف إلا مُدخل solidcraft أو يُحدَّث مساره؛ ويُترك المُدخل الذي غيّرته كما هو، ويُنسخ الملف احتياطيًا أولًا. عند الإيقاف تُزال المُدخلات التي أضافها. الأمر «المساعدة > ربط مساعدي الذكاء الاصطناعي…» يفعل ذلك يدويًا.",
|
||||
"ai.prefs.connect_apps_note": "إيقافه يزيل الإدخالات مجدداً.\n\nعند بدء تشغيل SolidCraft، يُضاف كخادم MCP إلى تطبيقات الذكاء الاصطناعي الموجودة على هذا الحاسوب (OpenCode وClaude Desktop وClaude Code وCursor وCodex وGemini CLI وCopilot CLI وWindsurf وZed وVS Code)، ليتمكن الوكيل من استخدامه دون أي إعداد. لا يُضاف إلا مُدخل solidcraft أو يُحدَّث مساره؛ ويُترك المُدخل الذي غيّرته كما هو، ويُنسخ الملف احتياطيًا أولًا. عند الإيقاف تُزال المُدخلات التي أضافها. الأمر «المساعدة > ربط مساعدي الذكاء الاصطناعي…» يفعل ذلك يدويًا.",
|
||||
"ai.prefs.connect_at_start": "توصيل آخر وكيل عند البدء",
|
||||
"ai.prefs.connect_at_start_note": "يبدأ في الخلفية الوكيل الذي اتصلت به آخر مرة (أو الوكيل الافتراضي) في كل مرة يبدأ فيها SolidCraft، ليكون جاهزًا للمحادثة. لا يحدث ذلك عند بدء SolidCraft مع --control. إذا تعذّر الاتصال، يعرض المساعد «فشل الاتصال» وزر «إعادة المحاولة».",
|
||||
"ai.prefs.follow_camera": "تتبّع الكاميرا",
|
||||
"ai.prefs.follow_camera": "اتباع الكاميرا",
|
||||
"ai.prefs.follow_camera_off": "تتبّع الكاميرا التلقائي متوقف.",
|
||||
"ai.prefs.follow_camera_on": "تتجه الكاميرا نحو الرسومات التي ينشئها الوكيل.",
|
||||
"ai.prefs.intro": "وكلاء الذكاء الاصطناعي في SolidCraft.",
|
||||
"ai.prefs.make_default": "تعيين افتراضيًا",
|
||||
"ai.prefs.make_default": "تعيين كافتراضي",
|
||||
"ai.prefs.no_agents": "لا يوجد وكلاء بعد. أضف وكيلًا أو حدّث القائمة للعثور على الوكلاء المثبّتين.",
|
||||
"ai.prefs.no_saved_approvals": "لا توجد موافقات محفوظة.",
|
||||
"ai.prefs.open_at_start": "فتح المحادثة عند البدء",
|
||||
"ai.prefs.open_at_start_note": "يعرض لوحة المساعد (أو نافذتها المستقلة) في كل مرة يبدأ فيها SolidCraft. لا يحدث ذلك عند بدء SolidCraft مع --control.",
|
||||
"ai.prefs.override_all_warning": "قد يحفظ هذا الوكيل التصميم أو يكتب فوقه أو يصدّره أو يستبدله دون أن يسأل.",
|
||||
"ai.prefs.permission_all": "تنفيذ كل شيء",
|
||||
"ai.prefs.permission_all_summary": "لا يسأل أبدًا",
|
||||
"ai.prefs.permission_ask": "السؤال أولًا",
|
||||
"ai.prefs.permission_all": "فعل كل شيء",
|
||||
"ai.prefs.permission_all_summary": "لا يسأل أبداً",
|
||||
"ai.prefs.permission_ask": "السؤال أولاً",
|
||||
"ai.prefs.permission_ask_summary": "يسأل عن كل تغيير",
|
||||
"ai.prefs.permission_edits": "إجراء تعديلات",
|
||||
"ai.prefs.permission_edits_summary": "يعدّل بحرية ويسأل عن الباقي",
|
||||
@@ -1396,9 +1396,6 @@
|
||||
"dialog.custom_axis": "↖ محور مخصص",
|
||||
"dialog.dimension_insert_hint": "انقر على بُعد قائد آخر لإدراج اسمه",
|
||||
"dialog.dimension_name": "البُعد: {name}",
|
||||
"dialog.dimension_step_down": "إنقاص بمقدار {step}",
|
||||
"dialog.dimension_step_expression": "يتغير الرقم فقط بالخطوات؛ يبقى التعبير كما كُتب",
|
||||
"dialog.dimension_step_up": "زيادة بمقدار {step}",
|
||||
"dialog.edit_title": "تعديل {title}",
|
||||
"dialog.no_shown_body": "لا يوجد جسم ظاهر لتغييره: أظهر جسمًا، أو اختر «جسم جديد»",
|
||||
"dialog.pattern_sketch_not_object": "اختر أجسامًا أو ميزات: الرسم ليس أيًّا منهما. كرّر منحنياته بأداة النمط أو المرآة داخل الرسم، أو ابثقه أولًا.",
|
||||
@@ -1406,7 +1403,6 @@
|
||||
"dialog.selected_count": "↖ {count} محدد",
|
||||
"dialog.selection_constraint": "قيد {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · منحنى {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "حدد مُدخلًا لتغييره. انقر على العناصر المحددة لإزالتها؛ Ctrl/Cmd مع النقر يبدّل تحديدها أيضًا. راجع المعاينة ثم اضغط موافق.",
|
||||
"dialog.selection_midpoint": "نقطة المنتصف · {id}",
|
||||
"dialog.selection_profile": "{sketch} · مقطع {number}",
|
||||
@@ -1424,7 +1420,6 @@
|
||||
"document.read_only_save": "فُتح هذا التصميم للقراءة فقط: احفظه كملف جديد",
|
||||
"document.save_changes": "حفظ التغييرات على {name}؟",
|
||||
"document.save_copy": "حفظ نسخة…",
|
||||
"document.tab_unsaved_changes": "تغييرات غير محفوظة",
|
||||
"document.unsaved_changes": "في {name} تغييرات غير محفوظة. إن لم تحفظها فستضيع.",
|
||||
"drive.rigid": "{joint} مفصل صلب: يثبّت أجزاءه في مكانها، لذا لا يوجد ما يمكن تحريكه. لتتحرك الأجزاء، غيّر نوعه في «تعديل المفصل»: «دوراني» للدوران، «منزلق» للانزلاق، «أسطواني» لكليهما.",
|
||||
"drive.stopped_by_contact": "توقف عند {value}: سيخترق {a} الجسم {b}",
|
||||
@@ -1923,7 +1918,7 @@
|
||||
"joint.limit_loop": "{side} {value}: لا يمكن لحلقة مغلقة أن تنغلق بعده",
|
||||
"joint.limit_range": "{side}: لا تلامس ضمن {range}، تُرك كما كان",
|
||||
"joint.limit_turn": "{side} {value}: لا تلامس ضمن دورة واحدة",
|
||||
"joint.limits_overlapping": "متداخلة بالفعل هنا: {pairs}. لا توقف البحث إلا إذا ازداد تداخلها.",
|
||||
"joint.limits_overlapping": "متداخلة بالفعل هنا: {pairs}. لا توقف البحث إلا إذا تداخلت أعمق.",
|
||||
"joint.one_body": "كلا الاختيارين على {body}. اختر الثاني على قطعة أخرى.",
|
||||
"joint.one_component": "هذه القطع أجسام لمكوّن واحد: {bodies}. المفصل يحرّك مكوّنًا مقابل آخر، لذا تحتاج كل قطعة إلى مكوّن خاص بها.",
|
||||
"joint.rigid_group_one_component": "هذه الأجسام وعددها {count} كلها في مكوّن واحد (أو في التصميم نفسه): استخدم {command} أولًا.",
|
||||
@@ -1938,7 +1933,7 @@
|
||||
"joint_move.fine": "الخطوة بعيدًا عن المحور",
|
||||
"joint_move.fixed": "لا يملك هذا الجزء محور مفصل قابلًا للحركة.",
|
||||
"joint_move.help": "تحريك — اسحب سهمًا أو قوسًا. يلغى السحب الحالي بمفتاح Esc.",
|
||||
"joint_move.legacy_hint": "عند الإيقاف: لوحة التحريك السابقة.",
|
||||
"joint_move.legacy_hint": "إيقاف: لوحة التحريك السابقة.",
|
||||
"joint_move.precision": "اسحب خارج الدليل المتقطع للخطوة الدقيقة.",
|
||||
"joint_move.rotation": "الدوران من نقطة البداية",
|
||||
"joint_move.select": "اختر وجهًا أو جزءًا واحدًا من التجميع، ثم اختر تحريك.",
|
||||
@@ -2004,7 +1999,6 @@
|
||||
"material.selection.mixed": "مواد مختلفة",
|
||||
"measure.at": "عند",
|
||||
"measure.item": "العنصر {number}",
|
||||
"measure.line_to_centre": "من الخط إلى المركز",
|
||||
"measure.quick_copied": "تم نسخ القياس",
|
||||
"measure.quick_copy": "انقر لنسخ القياس",
|
||||
"measure.quick_edges": "{count} حواف",
|
||||
@@ -2246,7 +2240,7 @@
|
||||
"prefs.anti_aliasing_amd_dx12": "يستخدم تنعيم الحواف 1× لأن فحص بدء التشغيل لم يرسم شيئًا مع أخذ العينات المتعددة ونجح عند 1×. يمكنك اختيار إعداد آخر أعلاه؛ أعد التشغيل لتطبيقه إن نجح الفحص.",
|
||||
"prefs.anti_aliasing_note": "ينعّم الحواف المسننة.",
|
||||
"prefs.appearance.colour_vision_help": "يغيّر الألوان ذات الدلالة: حالات الرسم والمحاور والثنيات والتحليلات والأيقونات.",
|
||||
"prefs.appearance.desktop_only": "لتطبيق سطح المكتب فقط",
|
||||
"prefs.appearance.desktop_only": "لسطح المكتب فقط",
|
||||
"prefs.appearance.follow_help": "يعيد التحميل عندما تعيد أداة أخرى كتابة الملف.",
|
||||
"prefs.appearance.interface_scale": "مقياس الواجهة",
|
||||
"prefs.appearance.next_themes": "السمات التالية",
|
||||
@@ -2256,11 +2250,11 @@
|
||||
"prefs.appearance.theme_count": "{count} سمات",
|
||||
"prefs.appearance.theme_own": "ألوان السمة",
|
||||
"prefs.appearance.viewport_colours": "ألوان العرض ثلاثي الأبعاد",
|
||||
"prefs.appearance.viewport_logo": "منطقة العرض والشعار",
|
||||
"prefs.appearance.viewport_logo": "منفذ العرض والشعار",
|
||||
"prefs.applies_on_restart": "يُطبَّق عند إعادة التشغيل",
|
||||
"prefs.assistant": "المساعد",
|
||||
"prefs.assistant_connecting": "الاتصال",
|
||||
"prefs.assistant_follow_help": "النظر إلى الرسومات يحرّك العرض مع المؤشر.",
|
||||
"prefs.assistant_follow_help": "تحرّك عروض Look At للرسم المنظور مع المؤشر.",
|
||||
"prefs.assistant_limit": "الحد",
|
||||
"prefs.assistant_limited": "محدود",
|
||||
"prefs.assistant_pointer_speed": "سرعة المؤشر",
|
||||
@@ -2268,21 +2262,13 @@
|
||||
"prefs.assistant_seconds_aria": "الثواني",
|
||||
"prefs.assistant_use_care": "استخدم بحذر",
|
||||
"prefs.auto_operation": "اختيار العملية تلقائيًا",
|
||||
"prefs.auto_operation_note": "ضم أو جسم جديد أو قص، حسب ما تلامسه المادة.",
|
||||
"prefs.auto_operation_note": "ضم أو جسم جديد أو قطع، حسب ما تلامسه المادة.",
|
||||
"prefs.axes": "المحاور",
|
||||
"prefs.axis": "المحور",
|
||||
"prefs.blur_cost": "تكلفة العرض النسبية. زيادة الأشرطة تعني عادةً مزيدًا من العمل.",
|
||||
"prefs.blur_device": "غير متاح على جهاز الرسوميات أو مسار العرض هذا",
|
||||
"prefs.blur_guard": "خُفّضت جودة التمويه للحفاظ على استجابة الإطارات. لم يتغير نصف القطر.",
|
||||
"prefs.blur_off": "إيقاف",
|
||||
"prefs.blur_reduce_transparency": "معطّل بسبب إعداد تقليل الشفافية في النظام",
|
||||
"prefs.blur_software": "غير متاح مع الرسوميات البرمجية (gpu-software)",
|
||||
"prefs.blur_source": "غير متاح في عرض مساحة العمل هذا",
|
||||
"prefs.blur_web": "غير متاح في مسار عرض الويب هذا",
|
||||
"prefs.bodies_folders": "مجلدات الأجسام",
|
||||
"prefs.bodies_folders_always": "دائمًا",
|
||||
"prefs.bodies_folders_never": "أبدًا",
|
||||
"prefs.bodies_folders_several": "عند التعدد",
|
||||
"prefs.bodies_folders_several": "عدة",
|
||||
"prefs.bottom": "أسفل",
|
||||
"prefs.bottom_centre": "أسفل الوسط",
|
||||
"prefs.bottom_right": "أسفل اليمين",
|
||||
@@ -2337,13 +2323,6 @@
|
||||
"prefs.dialogs_edge": "عند حافة العرض",
|
||||
"prefs.dialogs_left_column": "في العمود الأيسر",
|
||||
"prefs.dialogs_note": "مكان فتح حوارات الأوامر: مثبتة عند حافة العرض، أو في العمود الأيسر مكان المستعرض والسجل، اللذين يعودان كما كانا عند إغلاق الحوار.",
|
||||
"prefs.dimension_step_angle": "ما تضيفه أسهم النافذة وأعلى/أسفل والعجلة إلى الزاوية",
|
||||
"prefs.dimension_step_length": "ما تضيفه أسهم النافذة وأعلى/أسفل والعجلة إلى الطول",
|
||||
"prefs.dimension_steps": "خطوات النافذة",
|
||||
"prefs.dimension_values": "قيم الأبعاد",
|
||||
"prefs.dimension_values_note": "مكان كتابة قيمة البعد. للنافذة أسهم تغيّرها بخطوات؛ تضع أداة البعد النص أولًا.",
|
||||
"prefs.dimension_values_view": "في العرض",
|
||||
"prefs.dimension_values_window": "في نافذة",
|
||||
"prefs.display": "العرض",
|
||||
"prefs.edges": "الحواف والتظليل",
|
||||
"prefs.extend": "توسيع",
|
||||
@@ -2381,7 +2360,7 @@
|
||||
"prefs.graphics_note": "وازن بين السرعة والنعومة. كل خيار يعرض أثره على عيّنة.",
|
||||
"prefs.graphics_page.current_view": "العرض الحالي",
|
||||
"prefs.graphics_page.fastest": "الأسرع",
|
||||
"prefs.graphics_page.fold_items": "تلخيص العناصر",
|
||||
"prefs.graphics_page.fold_items": "طي العناصر",
|
||||
"prefs.graphics_page.in_view": "داخل العرض",
|
||||
"prefs.graphics_page.live_details": "تفاصيل مباشرة",
|
||||
"prefs.graphics_page.own_mix": "مزيجك الخاص",
|
||||
@@ -2390,7 +2369,7 @@
|
||||
"prefs.graphics_page.restart": "تلزم إعادة التشغيل",
|
||||
"prefs.graphics_page.sharpest": "الأكثر وضوحًا",
|
||||
"prefs.graphics_page.single_surface_analysis": "تحليل سطح واحد",
|
||||
"prefs.graphics_page.status_chips": "شارات الحالة",
|
||||
"prefs.graphics_page.status_chips": "شرائح الحالة",
|
||||
"prefs.graphics_page.subtitle": "وازن بين السرعة والنعومة.",
|
||||
"prefs.graphics_page.supersampling": "التنعيم الفائق",
|
||||
"prefs.graphics_page.supersampling_adaptive": "فقط أثناء سكون العرض",
|
||||
@@ -2437,7 +2416,7 @@
|
||||
"prefs.layout_page.icons_labels": "الأيقونات + التسميات",
|
||||
"prefs.layout_page.ok_cancel": "موافق وإلغاء",
|
||||
"prefs.layout_page.on_hover": "عند مرور المؤشر",
|
||||
"prefs.layout_page.open": "تُفتح",
|
||||
"prefs.layout_page.open": "فتح",
|
||||
"prefs.layout_page.origin_bodies": "الأصل والأجسام في الأعلى",
|
||||
"prefs.layout_page.panels": "اللوحات",
|
||||
"prefs.layout_page.pinned_menus": "استبعاد الأوامر المثبتة من القوائم",
|
||||
@@ -2477,8 +2456,8 @@
|
||||
"prefs.navigation.cube_compact": "المكعب",
|
||||
"prefs.navigation.device": "الجهاز",
|
||||
"prefs.navigation.free": "حر",
|
||||
"prefs.navigation.orbit_style": "نمط التدوير",
|
||||
"prefs.navigation.radial_help": "اضغط مطولًا أو انقر سريعًا بالزر الأيمن لعرض حلقة من الأدوات.",
|
||||
"prefs.navigation.orbit_style": "نمط الدوران",
|
||||
"prefs.navigation.radial_help": "اضغط مطولاً أو انقر سريعاً بالزر الأيمن لعرض حلقة من الأدوات.",
|
||||
"prefs.navigation.turntable": "طاولة دوارة",
|
||||
"prefs.navigation_note": "كيف تحرك الفأرة ولوحة اللمس والفأرة ثلاثية الأبعاد العرض.",
|
||||
"prefs.navigator_fit": "ملاءمة النموذج بعد تغيير العرض من متصفح العرض",
|
||||
@@ -2496,7 +2475,7 @@
|
||||
"prefs.origin_plane_names_note": "الأسماء المعروضة في المستعرض والحوارات والقوائم. {top} هو XY و{front} هو XZ و{right} هو YZ، والمحور Z للأعلى. تحتفظ الأوامر والسكربتات والتصاميم المحفوظة بـ XY وXZ وYZ.",
|
||||
"prefs.overall_speed": "السرعة العامة",
|
||||
"prefs.parameter_list_rows": "صفوف قائمة المعاملات",
|
||||
"prefs.parameter_list_rows_tip": "الحد الأقصى للمعاملات التي يسردها السهم لأسفل في مربع تعبير فارغ.",
|
||||
"prefs.parameter_list_rows_tip": "الحد الأقصى للمعلمات التي يسردها السهم لأسفل في مربع تعبير فارغ.",
|
||||
"prefs.persistent_ribbon_pressed": "تمييز أوامر الشريط النشطة",
|
||||
"prefs.persistent_ribbon_pressed_help": "تمييز أوامر الشريط أثناء تفعيل تأثيراتها. أوقف هذا الخيار للاحتفاظ بالمظهر المحايد السابق ومساعدة الأوامر.",
|
||||
"prefs.perspective_fov": "مجال الرؤية",
|
||||
@@ -2565,16 +2544,16 @@
|
||||
"prefs.shortcuts_import": "استيراد",
|
||||
"prefs.shortcuts_intro": "مفاتيح كل أمر.",
|
||||
"prefs.shortcuts_not_assigned": "غير معيّن",
|
||||
"prefs.shortcuts_press": "اضغط مفتاحًا...",
|
||||
"prefs.shortcuts_press": "اضغط مفتاحاً...",
|
||||
"prefs.single_surface_analysis": "استخدام تحليل سطح واحد",
|
||||
"prefs.single_surface_analysis_help": "عند إيقاف هذا الخيار، يمكن إبقاء تظليل السطح وتحليل الاستمرارية مفعّلين معًا. عند تشغيله، يبقى آخر تحليل تم بدؤه أو تغييره فقط.",
|
||||
"prefs.sketch.align_points": "المحاذاة مع نقاط الرسم الأخرى",
|
||||
"prefs.sketch.align_points_note": "تتحاذى الخطوط أثناء الرسم مع النقاط على المحورين معًا.",
|
||||
"prefs.sketch.auto_dimensions": "إضافة كل الأبعاد للأشكال الجديدة",
|
||||
"prefs.sketch.auto_dimensions_note": "القيم المدخلة وكل بُعد يثبّت الشكل.",
|
||||
"prefs.sketch.auto_locate": "تحديد الموضع من أصل الرسم",
|
||||
"prefs.sketch.auto_locate": "التحديد انطلاقًا من أصل الرسم",
|
||||
"prefs.sketch.auto_locate_note": "مع إضافة كل الأبعاد.",
|
||||
"prefs.sketch.center_arc": "القوس بنقطة مركز يتبع المؤشر",
|
||||
"prefs.sketch.center_arc": "قوس المركز يتبع المؤشر",
|
||||
"prefs.sketch.click_selects": "النقر يحدد",
|
||||
"prefs.sketch.conflict_colour": "لون التعارض",
|
||||
"prefs.sketch.drawing": "الرسم",
|
||||
@@ -2589,7 +2568,7 @@
|
||||
"prefs.sketch.prefer_free_points": "تفضيل النقاط القابلة للتحريك عند تداخلها",
|
||||
"prefs.sketch.project_options": "السؤال عن خيارات الإسقاط أولًا",
|
||||
"prefs.sketch.recent_snap": "الالتقاط عند تقاطع دليلين",
|
||||
"prefs.sketch.select_other_hidden": "تحديد آخر يشمل الهندسة المخفية",
|
||||
"prefs.sketch.select_other_hidden": "اختيار آخر يشمل الهندسة المخفية",
|
||||
"prefs.sketch.shortcut_cycles_modes": "الاختصار مجددًا يبدّل الوضع",
|
||||
"prefs.sketch.shortcut_cycles_modes_tip": "عند الإيقاف: تبدأ الأداة مجددًا.",
|
||||
"prefs.sketch.snapping_guides": "الالتقاط والأدلة",
|
||||
@@ -2671,10 +2650,6 @@
|
||||
"prefs.theme_follow_off": "إيقاف",
|
||||
"prefs.theme_follow_stop": "إيقاف الاتباع",
|
||||
"prefs.theme_import_hint": "انقر لاختيار ملف سمة، أو أفلت ملفًا هنا، أو اضغط Ctrl+V (Cmd+V على Mac) بعد نسخ سمة",
|
||||
"prefs.theme_notice_dismiss": "إخفاء",
|
||||
"prefs.theme_option_blur": "تمويه خلف اللوحات",
|
||||
"prefs.theme_option_blur_help": "تمويه خلف اللوحات شبه الشفافة، بالبكسل الفعلي. الصفر يوقف التمويه.",
|
||||
"prefs.theme_option_invalid": "القيمة المحفوظة للخيار {option} غير صالحة؛ تُستخدم القيمة الافتراضية للسمة. تبقى القيمة المحفوظة كما هي.",
|
||||
"prefs.theme_option_opacity": "العتامة",
|
||||
"prefs.theme_option_opacity_help": "اضبط عتامة اللوحات العائمة. يُطبّق حد أدنى للعتامة للحفاظ على وضوح النص فوق ألوان منطقة العرض.",
|
||||
"prefs.theme_option_tab_colours": "ألوان علامات التبويب",
|
||||
@@ -2683,7 +2658,6 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "تتغيّر ألوان اللوحات ومنطقة العرض تدريجيًا عند تبديل علامات التبويب. القيمة صفر تبدّلها فورًا.",
|
||||
"prefs.theme_options_badge": "الخيارات",
|
||||
"prefs.theme_options_more": "المزيد · {count}",
|
||||
"prefs.theme_options_newer": "تتطلب هذه السمة إصدارًا أحدث من SolidCraft. بعد الترقية، أعد استيراد السمة الأصلية لاستعادة تعريفات خياراتها.",
|
||||
"prefs.theme_options_reset": "إعادة تعيين",
|
||||
"prefs.theme_options_unavailable": "غير متاح على هذا الجهاز",
|
||||
"prefs.theme_paste": "لصق السمة",
|
||||
@@ -2712,7 +2686,7 @@
|
||||
"prefs.validation.each_step": "بعد كل خطوة",
|
||||
"prefs.validation.label": "فحص النواة الكامل",
|
||||
"prefs.validation.off": "متوقف (تجريبي)",
|
||||
"prefs.validation_note": "بعد كل خطوة: يفحص كل عملية كنس وعملية منطقية وتقويس.\nمرة لكل ميزة: يفحص ناتج كل ميزة مرة واحدة.\nمتوقف (تجريبي): لا فحص.",
|
||||
"prefs.validation_note": "بعد كل خطوة: يفحص كل عملية سحب ودمج وتقويس.\nمرة لكل ميزة: يفحص ناتج كل ميزة مرة واحدة.\nمتوقف (تجريبي): لا فحص.",
|
||||
"prefs.value_boxes_in_view": "إظهار القيم بجانب مقابضها",
|
||||
"prefs.value_boxes_in_view_tip": "مسافة البثق ونصف قطر التدوير... في مربع داخل العرض.",
|
||||
"prefs.view": "سلوك العرض",
|
||||
@@ -4227,13 +4201,13 @@
|
||||
"thread.automatic_size": "حجم تلقائي",
|
||||
"thread.chamfer_depth_needed": "مطلوب لهذا الشكل",
|
||||
"thread.designation": "التسمية",
|
||||
"thread.drill_range": "مثقاب التسنين الذي يوصي به صانعو ذكور التسنين {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.drill_range": "مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.drill_range_custom": "المثقاب هو القطر الأكبر ناقص الخطوة، {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.drill_range_taper": "مثقاب التسنين الذي يوصي به صانعو ذكور التسنين {drill} مم. القطر الأصغر القياسي عند نهاية السن {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.drill_range_taper": "مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي عند نهاية السن {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
|
||||
"thread.end_square": "قائمة",
|
||||
"thread.fade_short": "1.25 P (ISO 3508)",
|
||||
"thread.fade_standard": "2.5 P (ISO 3508)",
|
||||
"thread.fits_the_face": "يناسب الوجه، مثلًا {example}",
|
||||
"thread.fits_the_face": "يناسب الوجه، مثلاً {example}",
|
||||
"thread.form_bspp": "سن أنابيب G (ISO 228-1)",
|
||||
"thread.form_bspt": "سن أنابيب مخروطي R، Rc (ISO 7-1)",
|
||||
"thread.form_npt": "سن أنابيب مخروطي NPT (ASME B1.20.1)",
|
||||
@@ -4490,7 +4464,7 @@
|
||||
"ui.agent_chat_no_images": "هذا الوكيل لا يقبل الصور",
|
||||
"ui.agent_chat_no_picture": "لا توجد صورة في الحافظة",
|
||||
"ui.agent_chat_no_tool_call": "لم يستخدم النموذج أدوات SolidCraft، لذا لم يتغيّر التصميم. اختر نموذجًا يدعم استدعاء الأدوات؛ راجع «النماذج المحلية» في دليل محادثة الوكيل.",
|
||||
"ui.agent_chat_node_too_old": "إصدار Node.js {version} قديم جدًا لهذا الوكيل. ثبّت Node.js 20.10 أو أحدث، ثم اضغط تحديث.",
|
||||
"ui.agent_chat_node_too_old": "إصدار Node.js {version} قديم جدًا لهذا الوكيل. ثبّت Node.js 20.10 أو أحدث، ثم اضغط على تحديث.",
|
||||
"ui.agent_chat_none_found": "لم يُعثر على وكيل برمجي. ثبّت أحد هذه ثم اضغط تحديث:",
|
||||
"ui.agent_chat_none_found_short": "لم يُعثر على وكيل",
|
||||
"ui.agent_chat_not_installed": "غير مثبت",
|
||||
@@ -4531,7 +4505,7 @@
|
||||
"ui.agent_chat_send_now_tip": "إيقاف خطوة الوكيل الحالية وإرسال هذه بعدها (Ctrl+Enter)",
|
||||
"ui.agent_chat_setup_body": "يتواصل SolidCraft مع وكيل برمجة مثبّت على هذا الحاسوب. اختر واحدًا من التفضيلات › الوكيل.",
|
||||
"ui.agent_chat_setup_title": "إعداد مساعد",
|
||||
"ui.agent_chat_side": "تثبيت المحادثة",
|
||||
"ui.agent_chat_side": "إرساء المحادثة",
|
||||
"ui.agent_chat_signed_in": "سجّل الدخول",
|
||||
"ui.agent_chat_state_auth": "يلزم تسجيل الدخول",
|
||||
"ui.agent_chat_state_busy": "جارٍ العمل…",
|
||||
@@ -4596,10 +4570,10 @@
|
||||
"ui.ai_key_note": "يُخزّن في سلسلة مفاتيح النظام، وليس في إعدادات SolidCraft أبدًا. اتركه فارغًا لخادم محلي.",
|
||||
"ui.ai_keychain_error": "رفضت سلسلة مفاتيح النظام: {error}",
|
||||
"ui.ai_kind": "النوع",
|
||||
"ui.ai_mode_all": "تنفيذ كل شيء",
|
||||
"ui.ai_mode_all": "فعل كل شيء",
|
||||
"ui.ai_mode_all_note": "لا شيء يسأل أولًا. تبقى تقارير الأعطال محظورة وتظل حدود المجلدات سارية.",
|
||||
"ui.ai_mode_all_warning": "يمكن للوكيل حفظ تصميمك والكتابة فوقه وتصديره واستبداله دون سؤال. استخدم هذا فقط مع وكيل تثق به، وعلى تصاميم حفظتها.",
|
||||
"ui.ai_mode_ask": "السؤال أولًا",
|
||||
"ui.ai_mode_ask": "السؤال أولاً",
|
||||
"ui.ai_mode_ask_note": "كل تغيير في التصميم وكل إجراء على الملفات ينتظر سماحك.",
|
||||
"ui.ai_mode_edits": "إجراء تعديلات",
|
||||
"ui.ai_mode_edits_note": "أوامر النمذجة تعمل دون سؤال؛ أما الحفظ والفتح والتصدير وإجراءات الملفات أو الإجراءات الهدّامة الأخرى فتسأل.",
|
||||
@@ -4770,21 +4744,21 @@
|
||||
"ui.construction.ribbon_label": "خطوط مساعدة",
|
||||
"ui.construction.side_hint": "خط إنشائي: انقر الجهة التي يمتد نحوها الخط؛ انقر بزر الفأرة الأيمن للبدء من جديد",
|
||||
"ui.context_menu": "القائمة السياقية",
|
||||
"ui.continuity_crease": "ثنية",
|
||||
"ui.continuity_crease": "تجعّد",
|
||||
"ui.continuity_curvature": "الانحناء",
|
||||
"ui.continuity_curvature_change": "تغيّر الانحناء",
|
||||
"ui.continuity_curvature_change_tip": "تغيّر الانحناء (G3): يتغير الانحناء أيضًا بسلاسة عبر خط الالتقاء.",
|
||||
"ui.continuity_curvature_tip": "متصل الانحناء (G2): تمر الانعكاسات دون انكسار.",
|
||||
"ui.continuity_g0": "G0 ثنية",
|
||||
"ui.continuity_g0": "G0 تجعّد",
|
||||
"ui.continuity_g1": "G1 مماس",
|
||||
"ui.continuity_g2": "G2 انحناء",
|
||||
"ui.continuity_gap": "فجوة",
|
||||
"ui.continuity_position": "الموضع",
|
||||
"ui.continuity_position_tip": "الموضع (G0): تلتقي الأسطح، مع ثنية عند التقائها.",
|
||||
"ui.continuity_position_tip": "الموضع (G0): تلتقي الأسطح، مع تجعّد عند التقائها.",
|
||||
"ui.continuity_smooth_curvature_change": "تغيّر انحناء سلس",
|
||||
"ui.continuity_smooth_curvature_change_tip": "تغيّر انحناء سلس (G4): حتى طريقة تغيّر الانحناء تستمر بسلاسة عبر خط الالتقاء.",
|
||||
"ui.continuity_tangent": "مماس",
|
||||
"ui.continuity_tangent_tip": "مماس (G1): لا ثنية، لكن الانحناء يقفز عبر خط الالتقاء.",
|
||||
"ui.continuity_tangent_tip": "مماس (G1): لا تجعّد، لكن الانحناء يقفز عبر خط الالتقاء.",
|
||||
"ui.copy": "نسخ",
|
||||
"ui.copy_details": "نسخ التفاصيل",
|
||||
"ui.copy_details_tip": "انسخ الرسالة كاملة للبحث عنها أو للإبلاغ عنها.",
|
||||
@@ -4810,7 +4784,6 @@
|
||||
"ui.default_modelling_orientation": "اتجاه النمذجة الافتراضي",
|
||||
"ui.delete": "حذف",
|
||||
"ui.delete_select_first": "حدّد أولًا ما تريد حذفه: الميزات أو الأجسام أو المكوّنات أو هندسة الرسم",
|
||||
"ui.dependents_select_first": "حدّد أولًا ما تريد سرد معتمداته",
|
||||
"ui.depth": "العمق",
|
||||
"ui.design": "التصميم",
|
||||
"ui.design_properties.author": "المؤلف",
|
||||
@@ -4936,7 +4909,7 @@
|
||||
"ui.extract_done": "أصبح المكوّن تصميمًا مستقلًا، مرتبطًا هنا.",
|
||||
"ui.extrude": "بثق",
|
||||
"ui.extrude_auto_to_object": "البثق إلى هدف مختار",
|
||||
"ui.extrude_auto_to_object_tip": "وجه أو رأس أو مستوى موازٍ يغيّر الامتداد إلى «إلى كائن».",
|
||||
"ui.extrude_auto_to_object_tip": "وجه أو رأس أو مستوى موازٍ يغيّر المدى إلى «إلى كائن».",
|
||||
"ui.factor": "المعامل",
|
||||
"ui.fade": "تلاشٍ",
|
||||
"ui.fade_length": "طول التلاشي",
|
||||
@@ -4966,13 +4939,9 @@
|
||||
"ui.fillet_setback": "مسافة الإرجاع",
|
||||
"ui.fillet_setback_hint": "{factor} × نصف القطر",
|
||||
"ui.fillet_setback_least": "أكثر من {least}",
|
||||
"ui.fillet_setback_tip": "حيث تبدأ رقعة الزاوية الناعمة: المسافة على طول كل حافة من الزاوية. الافتراضي {factor} ضعف نصف القطر. يجب أن تكون أكبر من أقل مسافة إرجاع، وهي التي تلتقي عندها التقويسات دون إرجاع: نصف القطر حين تكون الأوجه متعامدة.",
|
||||
"ui.fillet_setback_tip": "حيث تبدأ رقعة الزاوية الناعمة: المسافة على طول كل حافة من الزاوية. الافتراضي {factor} ضعف نصف القطر. يجب أن تكون أكبر من أقل مسافة إرجاع، حيث تلتقي الاستدارات بدونها: نصف القطر حيث تكون الأوجه متعامدة.",
|
||||
"ui.fillet_smooth_radius": "تغيّر ناعم لنصف القطر",
|
||||
"ui.fillet_turn": "الالتفاف",
|
||||
"ui.fillet_turn_mitre": "وصلة مائلة",
|
||||
"ui.fillet_turn_swept": "ملتف",
|
||||
"ui.fillet_turn_tip": "حيث يلتف تقويسان حول حافة ركن حادة بينهما، مثل التقويسات على قاعدة نتوء عند حوافه القائمة.\nوصلة مائلة: يمتد التقويسان حتى يلتقيا في وصلة مائلة.\nملتف: يتوقف التقويسان عند حافة الركن ويلتفان حولها، والركن طارة مماسة لكليهما. فقط لتقويسين بنصف قطر واحد حول حافة ركن مستقيمة ومحدبة.",
|
||||
"ui.fillet_type_choices_tip": "نصف قطر ثابت: نصف قطر واحد على طول الحافة.\nطول الوتر: يبقى عرض التقويس نفسه مهما كانت الزاوية بين الوجوه.\nنصف قطر متغيّر: نصف قطر عند كل طرف، ومزيد على طول الحافة إن أضفته.\nمتصل الانحناء: مقطع بلا انكسار في الانعكاسات حيث يلتقي بالوجوه المستوية (G2).\nمنحنى شريحي: يتبع نصف القطر على طول الحافة أنصاف أقطار تحكّم تحدّدها.",
|
||||
"ui.fillet_type_choices_tip": "نصف قطر ثابت: نصف قطر واحد على طول الحافة.\nطول الوتر: يبقى عرض التقويس نفسه مهما كانت الزاوية بين الوجوه.\nنصف قطر متغيّر: نصف قطر عند كل طرف، ومزيد على طول الحافة إن أضفته.\nمتصل الانحناء: مقطع بلا انكسار في الانعكاسات حيث يلتقي بالوجوه المستوية (G2).\nمنحنى: يتبع نصف القطر على طول الحافة أنصاف أقطار تحكّم تحدّدها.",
|
||||
"ui.fillet_type_curvature": "متصل الانحناء",
|
||||
"ui.fillet_type_tip": "المقطع عبر الحافة. متصل الانحناء: مقطع متصل الانحناء (G2) بين وجوه مستوية، فتمر الانعكاسات من الوجوه إلى التقويس دون انكسار.",
|
||||
"ui.filter_commands_or_keys": "تصفية الأوامر أو المفاتيح",
|
||||
@@ -5084,7 +5053,7 @@
|
||||
"ui.hidePlanes": "إخفاء المستويات",
|
||||
"ui.hideProfile": "إخفاء المقطع",
|
||||
"ui.hide_component_history": "إخفاء سجله",
|
||||
"ui.hide_origin_by_default": "إخفاء مجلد نقطة الأصل في التصاميم الجديدة",
|
||||
"ui.hide_origin_by_default": "إخفاء مجلد الأصل في التصاميم الجديدة",
|
||||
"ui.hide_origin_note": "تبدأ التصاميم الجديدة ومجلد نقطة الأصل مخفي. يحتفظ كل تصميم بإعداد نقطة الأصل الخاص به بمجرد إظهارها أو إخفائها.",
|
||||
"ui.history_back": "الرجوع خطوة",
|
||||
"ui.history_below_browser": "أسفل المستعرض",
|
||||
|
||||
@@ -833,8 +833,6 @@
|
||||
"cmd.appearance_pick_up": "Aussehen aufnehmen",
|
||||
"cmd.appearance_replace": "Erscheinungsbild ersetzen",
|
||||
"cmd.assembly_bom": "Stückliste",
|
||||
"cmd.assembly_bom_item_set": "Positionsnummer ändern",
|
||||
"cmd.assembly_bom_renumber": "Positionen neu nummerieren",
|
||||
"cmd.assembly_mass_properties": "Masseeigenschaften",
|
||||
"cmd.body.print_face": "Auflagefläche",
|
||||
"cmd.body_rename": "Körper umbenennen",
|
||||
@@ -1026,7 +1024,6 @@
|
||||
"cmd.motion_trace_edit": "Bewegungsbahn bearbeiten",
|
||||
"cmd.motion_trace_keep": "Bewegungsbahn behalten",
|
||||
"cmd.occurrence_align": "Instanz ausrichten",
|
||||
"cmd.occurrence_bom_type": "Stücklistentyp",
|
||||
"cmd.occurrence_copy": "Einfügen (Instanz)",
|
||||
"cmd.occurrence_drag": "Instanz ziehen",
|
||||
"cmd.occurrence_ground": "Festsetzen",
|
||||
@@ -1394,9 +1391,6 @@
|
||||
"dialog.custom_axis": "↖ Eigene Achse",
|
||||
"dialog.dimension_insert_hint": "Doppelklicken Sie auf eine andere steuernde Bemaßung, um ihren Namen einzufügen",
|
||||
"dialog.dimension_name": "Bemaßung: {name}",
|
||||
"dialog.dimension_step_down": "Um {step} verringern",
|
||||
"dialog.dimension_step_expression": "Nur eine Zahl lässt sich schrittweise ändern; ein Ausdruck bleibt, wie er eingegeben ist",
|
||||
"dialog.dimension_step_up": "Um {step} erhöhen",
|
||||
"dialog.edit_title": "{title} bearbeiten",
|
||||
"dialog.no_shown_body": "Kein sichtbarer Körper zum Ändern vorhanden. Blenden Sie einen Körper ein oder wählen Sie „Neuer Körper“.",
|
||||
"dialog.pattern_sketch_not_object": "Körper oder Funktionen wählen: Eine Skizze ist keines von beiden. Ihre Kurven mit Muster oder Spiegeln in der Skizze wiederholen oder sie zuerst extrudieren.",
|
||||
@@ -1421,7 +1415,6 @@
|
||||
"document.read_only_save": "Dieses Design wurde schreibgeschützt geöffnet: Speichern Sie es als neue Datei",
|
||||
"document.save_changes": "Änderungen in {name} speichern?",
|
||||
"document.save_copy": "Kopie speichern…",
|
||||
"document.tab_unsaved_changes": "Ungespeicherte Änderungen",
|
||||
"document.unsaved_changes": "{name} enthält ungespeicherte Änderungen. Wenn Sie nicht speichern, gehen sie verloren.",
|
||||
"drive.rigid": "{joint} ist ein starres Gelenk. Es hält die verbundenen Teile fest zusammen und ermöglicht daher keine Bewegung. Um die Teile beweglich zu machen, ändern Sie den Gelenktyp unter „Gelenk bearbeiten“: Drehgelenk für Drehbewegungen, Schubgelenk für lineare Bewegungen oder Zylindergelenk für beides.",
|
||||
"drive.stopped_by_contact": "Angehalten bei {value}: {a} würde {b} durchdringen",
|
||||
@@ -1989,7 +1982,6 @@
|
||||
"material.selection.mixed": "Unterschiedliche Materialien",
|
||||
"measure.at": "Bei",
|
||||
"measure.item": "Element {number}",
|
||||
"measure.line_to_centre": "Linie zum Mittelpunkt",
|
||||
"measure.quick_copied": "Messwert kopiert",
|
||||
"measure.quick_copy": "Klicken, um den Messwert zu kopieren",
|
||||
"measure.quick_edges": "{count} Kanten",
|
||||
@@ -2256,14 +2248,6 @@
|
||||
"prefs.auto_operation_note": "Vereinigung, Neuer Körper oder Schnitt, je nachdem, was das Material berührt.",
|
||||
"prefs.axes": "Achsen",
|
||||
"prefs.axis": "Achse",
|
||||
"prefs.blur_cost": "Relativer Renderaufwand. Mehr Balken bedeuten meist mehr Arbeit.",
|
||||
"prefs.blur_device": "Auf diesem Grafikgerät oder Renderpfad nicht verfügbar",
|
||||
"prefs.blur_guard": "Die Unschärfequalität wurde für flüssigere Bilder verringert. Der Radius bleibt unverändert.",
|
||||
"prefs.blur_off": "Aus",
|
||||
"prefs.blur_reduce_transparency": "Durch die Systemeinstellung zur Transparenzreduzierung deaktiviert",
|
||||
"prefs.blur_software": "Mit Softwaregrafik nicht verfügbar (gpu-software)",
|
||||
"prefs.blur_source": "In dieser Arbeitsbereichsansicht nicht verfügbar",
|
||||
"prefs.blur_web": "Auf diesem Web-Renderpfad nicht verfügbar",
|
||||
"prefs.bodies_folders": "Körperordner",
|
||||
"prefs.bodies_folders_always": "Immer",
|
||||
"prefs.bodies_folders_never": "Nie",
|
||||
@@ -2322,15 +2306,7 @@
|
||||
"prefs.dialogs_edge": "Am Rand der Ansicht",
|
||||
"prefs.dialogs_left_column": "In der linken Spalte",
|
||||
"prefs.dialogs_note": "Wo Befehlsdialoge öffnen: am Rand der Ansicht angedockt oder in der linken Spalte anstelle von Browser und Verlauf, die beim Schließen des Dialogs wie zuvor zurückkehren.",
|
||||
"prefs.dimension_step_angle": "Was die Pfeile des Fensters, Auf/Ab und das Mausrad zu einem Winkel addieren",
|
||||
"prefs.dimension_step_length": "Was die Pfeile des Fensters, Auf/Ab und das Mausrad zu einer Länge addieren",
|
||||
"prefs.dimension_steps": "Schritte im Fenster",
|
||||
"prefs.dimension_values": "Bemaßungswerte",
|
||||
"prefs.dimension_values_note": "Wo der Wert einer Bemaßung eingegeben wird. Ein Fenster hat Pfeile, die ihn schrittweise ändern; das Bemaßungswerkzeug platziert zuerst den Text.",
|
||||
"prefs.dimension_values_view": "In der Ansicht",
|
||||
"prefs.dimension_values_window": "In einem Fenster",
|
||||
"prefs.display": "Anzeige",
|
||||
"prefs.drawings": "Zeichnungen",
|
||||
"prefs.edges": "Kanten und Schattierung",
|
||||
"prefs.extend": "Erweitern",
|
||||
"prefs.extra_navigation": "Mit Strg und gedrückter linker Maustaste auf einer freien Fläche drehen. Leertaste gedrückt halten, um die Ansicht zu verschieben.",
|
||||
@@ -2652,10 +2628,6 @@
|
||||
"prefs.theme_follow_off": "Aus",
|
||||
"prefs.theme_follow_stop": "Nicht mehr folgen",
|
||||
"prefs.theme_import_hint": "Klicken, um eine Designdatei zu wählen, eine hierher ziehen oder mit kopiertem Design Strg+V (Cmd+V auf dem Mac) drücken",
|
||||
"prefs.theme_notice_dismiss": "Ausblenden",
|
||||
"prefs.theme_option_blur": "Unschärfe hinter Panels",
|
||||
"prefs.theme_option_blur_help": "Unschärfe hinter durchscheinenden Panels in physischen Pixeln. Null bedeutet aus.",
|
||||
"prefs.theme_option_invalid": "Der gespeicherte Wert für {option} ist ungültig; der Designstandard wird verwendet. Der gespeicherte Wert bleibt erhalten.",
|
||||
"prefs.theme_option_opacity": "Deckkraft",
|
||||
"prefs.theme_option_opacity_help": "Deckkraft schwebender Bedienfelder anpassen. Eine Mindestdeckkraft schützt die Lesbarkeit über den Ansichtsfarben.",
|
||||
"prefs.theme_option_tab_colours": "Tabfarben",
|
||||
@@ -2664,7 +2636,6 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "Panel- und Ansichtsfarben beim Tabwechsel überblenden. Null wechselt sofort.",
|
||||
"prefs.theme_options_badge": "Optionen",
|
||||
"prefs.theme_options_more": "Mehr · {count}",
|
||||
"prefs.theme_options_newer": "Dieses Design benötigt eine neuere Version von SolidCraft. Nach dem Upgrade das Originaldesign erneut importieren, um seine Optionsdefinitionen wiederherzustellen.",
|
||||
"prefs.theme_options_reset": "Zurücksetzen",
|
||||
"prefs.theme_options_unavailable": "Auf diesem Gerät nicht verfügbar",
|
||||
"prefs.theme_paste": "Design einfügen",
|
||||
@@ -4788,7 +4759,6 @@
|
||||
"ui.default_modelling_orientation": "Standard-Modellierungsorientierung",
|
||||
"ui.delete": "Löschen",
|
||||
"ui.delete_select_first": "Wählen Sie zuerst aus, was gelöscht werden soll: Features, Körper, Komponenten oder Skizzengeometrie",
|
||||
"ui.dependents_select_first": "Wählen Sie zuerst aus, wessen Abhängige aufgelistet werden sollen",
|
||||
"ui.depth": "Tiefe",
|
||||
"ui.design": "Design",
|
||||
"ui.design_properties.author": "Autor",
|
||||
@@ -4944,10 +4914,6 @@
|
||||
"ui.fillet_setback_least": "Mehr als {least}",
|
||||
"ui.fillet_setback_tip": "Wo das glatte Eckflächenstück beginnt: wie weit von der Ecke entlang jeder Kante. Standard ist {factor} × der Radius. Er muss größer sein als der kleinste Rücksprung, wo sich die Rundungen ohne ihn treffen: der Radius, wo die Flächen rechtwinklig zueinander stehen.",
|
||||
"ui.fillet_smooth_radius": "Radius glatt ändern",
|
||||
"ui.fillet_turn": "Wendungen",
|
||||
"ui.fillet_turn_mitre": "Gehrung",
|
||||
"ui.fillet_turn_swept": "Geschwenkt",
|
||||
"ui.fillet_turn_tip": "Wo zwei Rundungen um eine scharfe Eckkante zwischen ihnen herumlaufen, etwa die Rundungen am Fuß eines Zapfens an seinen senkrechten Kanten.\nGehrung: Die Rundungen laufen weiter, bis sie sich in einer Gehrung treffen.\nGeschwenkt: Die Rundungen enden an der Eckkante und schwenken um sie herum; die Ecke ist ein Torus, tangential zu beiden. Nur für Rundungen mit einem Radius um eine gerade, konvexe Eckkante.",
|
||||
"ui.fillet_type_choices_tip": "Konstanter Radius: ein Radius entlang der ganzen Kante.\nSehnenlänge: die Breite der Rundung bleibt gleich, in welchem Winkel die Flächen auch aufeinandertreffen.\nVariabler Radius: ein Radius an jedem Ende und weitere entlang der Kante, wenn Sie sie hinzufügen.\nKrümmungsstetig: ein Querschnitt ohne Knick in den Spiegelungen, wo er auf ebene Flächen trifft (G2).\nSpline: der Radius entlang der Kante folgt Steuerradien, die Sie festlegen.",
|
||||
"ui.fillet_type_curvature": "Krümmungsstetig",
|
||||
"ui.fillet_type_tip": "Der Querschnitt über die Kante. Krümmungsstetig: ein krümmungsstetiger Querschnitt (G2) zwischen ebenen Flächen, sodass Spiegelungen ohne Knick von den Flächen in die Rundung laufen.",
|
||||
@@ -4957,7 +4923,6 @@
|
||||
"ui.fine": "Fein",
|
||||
"ui.finest": "Sehr fein",
|
||||
"ui.finish_sketch": "Skizze beenden",
|
||||
"ui.first_angle": "Erster Winkel",
|
||||
"ui.fit": "Einpassen",
|
||||
"ui.fix_unfix": "Fixieren/Lösen",
|
||||
"ui.flat": "Flach",
|
||||
@@ -5258,7 +5223,6 @@
|
||||
"ui.profiles": "Profile",
|
||||
"ui.project": "Projizieren",
|
||||
"ui.project_face_edges_sketch": "Flächenkanten in neue Skizzen projizieren",
|
||||
"ui.projection_of_new_drawings": "Projektion neuer Zeichnungen",
|
||||
"ui.properties": "Eigenschaften",
|
||||
"ui.properties.copy_matrix": "Matrix kopieren",
|
||||
"ui.properties.first_moments": "Statische Momente",
|
||||
@@ -5441,7 +5405,6 @@
|
||||
"ui.text": "Text",
|
||||
"ui.theme": "Design",
|
||||
"ui.thickness": "Dicke",
|
||||
"ui.third_angle": "Dritter Winkel",
|
||||
"ui.thread": "Gewinde",
|
||||
"ui.tie_ends": "Verknüpfen",
|
||||
"ui.tie_ends_tip": "Alle unverknüpften Enden mit Abhängigkeiten versehen",
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
"help.solid.split_face": "Divides the selected faces along a plane, the face of another body or sketch curves, without cutting the body. Use it to give part of a face its own draft, colour or offset.",
|
||||
"help.third_party": "Third-party licences",
|
||||
"home.new_mm_hint": "Millimetres · opens in Design",
|
||||
"measure.line_to_centre": "Line to centre",
|
||||
"navbar.custom.anchor.bottom_centre": "Bottom centre",
|
||||
"navbar.custom.anchor.top_centre": "Top centre",
|
||||
"navbar.custom.customise": "Customise navigation bar…",
|
||||
|
||||
@@ -833,8 +833,6 @@
|
||||
"cmd.appearance_pick_up": "Pick Up Appearance",
|
||||
"cmd.appearance_replace": "Replace Appearance",
|
||||
"cmd.assembly_bom": "Bill of Materials",
|
||||
"cmd.assembly_bom_item_set": "Change Item Number",
|
||||
"cmd.assembly_bom_renumber": "Renumber Items",
|
||||
"cmd.assembly_mass_properties": "Mass Properties",
|
||||
"cmd.body.print_face": "Print Face",
|
||||
"cmd.body_rename": "Rename Body",
|
||||
@@ -934,36 +932,6 @@
|
||||
"cmd.document_metadata_set": "Change Design Properties",
|
||||
"cmd.document_revision": "Design Revision",
|
||||
"cmd.document_units": "Change Design Units",
|
||||
"cmd.drawing_balloon_add": "Add Balloon",
|
||||
"cmd.drawing_balloon_edit": "Edit Balloon",
|
||||
"cmd.drawing_balloon_inspect": "Inspect Balloon",
|
||||
"cmd.drawing_close": "Close Drawing",
|
||||
"cmd.drawing_create": "Create Drawing",
|
||||
"cmd.drawing_delete": "Delete Drawing",
|
||||
"cmd.drawing_dimension_add": "Add Drawing Dimension",
|
||||
"cmd.drawing_dimension_edit": "Edit Drawing Dimension",
|
||||
"cmd.drawing_dimension_inspect": "Inspect Drawing Dimension",
|
||||
"cmd.drawing_export": "Export Sheet",
|
||||
"cmd.drawing_inspect": "Inspect Drawing",
|
||||
"cmd.drawing_list": "List Drawings",
|
||||
"cmd.drawing_open": "Open Drawing",
|
||||
"cmd.drawing_properties": "Drawing Properties",
|
||||
"cmd.drawing_properties_remove": "Remove Drawing Property",
|
||||
"cmd.drawing_properties_set": "Change Drawing Properties",
|
||||
"cmd.drawing_revision_add": "Add Revision",
|
||||
"cmd.drawing_revision_delete": "Delete Revision",
|
||||
"cmd.drawing_revision_edit": "Edit Revision",
|
||||
"cmd.drawing_scene": "Draw Sheet",
|
||||
"cmd.drawing_set": "Drawing Settings",
|
||||
"cmd.drawing_sheet_add": "Add Sheet",
|
||||
"cmd.drawing_sheet_delete": "Delete Sheet",
|
||||
"cmd.drawing_sheet_reorder": "Reorder Sheets",
|
||||
"cmd.drawing_sheet_set": "Sheet Settings",
|
||||
"cmd.drawing_view_copy_to_sheet": "Copy View to Sheet",
|
||||
"cmd.drawing_view_edit": "Edit Drawing View",
|
||||
"cmd.drawing_view_inspect": "Inspect Drawing View",
|
||||
"cmd.drawing_view_move_to_sheet": "Move View to Sheet",
|
||||
"cmd.drawing_view_snapshot": "Drawing View Snapshot",
|
||||
"cmd.edit_checkpoint": "Checkpoint",
|
||||
"cmd.edit_repeat": "Repeat Last Command",
|
||||
"cmd.edit_restore_checkpoint": "Restore Checkpoint",
|
||||
@@ -1061,7 +1029,6 @@
|
||||
"cmd.motion_trace_edit": "Edit Motion Trace",
|
||||
"cmd.motion_trace_keep": "Keep Motion Trace",
|
||||
"cmd.occurrence_align": "Align Occurrence",
|
||||
"cmd.occurrence_bom_type": "Bill of Materials Type",
|
||||
"cmd.occurrence_copy": "Paste (Instance)",
|
||||
"cmd.occurrence_drag": "Drag Occurrence",
|
||||
"cmd.occurrence_ground": "Ground",
|
||||
@@ -1429,9 +1396,6 @@
|
||||
"dialog.custom_axis": "↖ Custom Axis",
|
||||
"dialog.dimension_insert_hint": "Click another driving dimension to insert its name",
|
||||
"dialog.dimension_name": "Dimension: {name}",
|
||||
"dialog.dimension_step_down": "Down by {step}",
|
||||
"dialog.dimension_step_expression": "Only a number steps; an expression stays as typed",
|
||||
"dialog.dimension_step_up": "Up by {step}",
|
||||
"dialog.edit_title": "EDIT {title}",
|
||||
"dialog.no_shown_body": "No shown body to change: show one, or choose New Body",
|
||||
"dialog.pattern_sketch_not_object": "Pick bodies or features: a sketch is neither. Repeat its curves with the sketch's own Pattern or Mirror, or extrude it first.",
|
||||
@@ -1439,7 +1403,6 @@
|
||||
"dialog.selected_count": "↖ {count} selected",
|
||||
"dialog.selection_constraint": "Constraint {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · curve {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "Select an input to change it. Click selected objects to remove them; Ctrl/Cmd-click also toggles them. Review the preview, then press OK.",
|
||||
"dialog.selection_midpoint": "Midpoint · {id}",
|
||||
"dialog.selection_profile": "{sketch} · profile {number}",
|
||||
@@ -1457,7 +1420,6 @@
|
||||
"document.read_only_save": "This design was opened read-only: save it as a new file",
|
||||
"document.save_changes": "Save changes to {name}?",
|
||||
"document.save_copy": "Save a Copy…",
|
||||
"document.tab_unsaved_changes": "Unsaved changes",
|
||||
"document.unsaved_changes": "{name} has unsaved changes. If you don't save, they are lost.",
|
||||
"drive.rigid": "{joint} is a rigid joint: it holds its parts still, so it has nothing to drive. To let them move, change its type in Edit Joint: Revolute to rotate, Slider to slide, Cylindrical for both.",
|
||||
"drive.stopped_by_contact": "Stopped at {value}: {a} would pass through {b}",
|
||||
@@ -1638,7 +1600,7 @@
|
||||
"help.parts.resize": "Changes the size or length of an inserted standard part and rebuilds it; its joints keep it seated. Use it when a screw turns out too long or a hole needs another size.",
|
||||
"help.plastic.assign_rule": "Assigns a plastic rule to bodies, so their plastic features take its draft and clearance as defaults. Use it to set up a moulded part once for all its features.",
|
||||
"help.plastic.boss": "Adds a screw boss on a face: a post with an optional hole, draft, root fillet and supporting ribs. Use it for screw points in moulded or printed housings.",
|
||||
"help.plastic.lip": "Adds a lip or a matching groove along the rim of a shelled body (Shell a face open first, then pick the rim around that opening). Use it so that two housing halves locate on each other and close tightly.",
|
||||
"help.plastic.lip": "Adds a lip or a matching groove along the rim of a shelled body. Use it so that two housing halves locate on each other and close tightly.",
|
||||
"help.plastic.manage_rules": "Creates and edits plastic rules: material, wall thickness, radii, clearance and draft. Use them to keep the features of a moulded part consistent.",
|
||||
"help.plastic.rest": "Adds a rest on a face: a round or rectangular pad, solid or hollow, with draft. Use it as a support or standoff in a moulded housing.",
|
||||
"help.plastic.snap_fit": "Adds a snap-fit hook on a face, with its length, thickness and catch. Use it to clip parts together without screws.",
|
||||
@@ -2037,7 +1999,6 @@
|
||||
"material.selection.mixed": "Different materials",
|
||||
"measure.at": "At",
|
||||
"measure.item": "Item {number}",
|
||||
"measure.line_to_centre": "Line to center",
|
||||
"measure.quick_copied": "Measurement copied",
|
||||
"measure.quick_copy": "Click to copy the measurement",
|
||||
"measure.quick_edges": "{count} edges",
|
||||
@@ -2304,14 +2265,6 @@
|
||||
"prefs.auto_operation_note": "Join, New Body or Cut, from what the material touches.",
|
||||
"prefs.axes": "Axes",
|
||||
"prefs.axis": "Axis",
|
||||
"prefs.blur_cost": "Relative rendering cost. More bars usually mean more work.",
|
||||
"prefs.blur_device": "Unavailable on this graphics device or rendering path",
|
||||
"prefs.blur_guard": "Blur quality was reduced to keep frames responsive. The radius is unchanged.",
|
||||
"prefs.blur_off": "Off",
|
||||
"prefs.blur_reduce_transparency": "Disabled by the system's Reduce transparency setting",
|
||||
"prefs.blur_software": "Unavailable with software graphics (gpu-software)",
|
||||
"prefs.blur_source": "Unavailable in this workspace view",
|
||||
"prefs.blur_web": "Unavailable on this web rendering path",
|
||||
"prefs.bodies_folders": "Bodies folders",
|
||||
"prefs.bodies_folders_always": "Always",
|
||||
"prefs.bodies_folders_never": "Never",
|
||||
@@ -2370,15 +2323,7 @@
|
||||
"prefs.dialogs_edge": "At the view's edge",
|
||||
"prefs.dialogs_left_column": "In the left column",
|
||||
"prefs.dialogs_note": "Where command dialogs open: docked at the view's edge, or in the left column in place of the browser and the History, which come back as they were when the dialog closes.",
|
||||
"prefs.dimension_step_angle": "What the window's arrows, Up/Down and the wheel add to an angle",
|
||||
"prefs.dimension_step_length": "What the window's arrows, Up/Down and the wheel add to a length",
|
||||
"prefs.dimension_steps": "Window steps",
|
||||
"prefs.dimension_values": "Dimension values",
|
||||
"prefs.dimension_values_note": "Where you type a dimension's value. A window has arrows that step it; the Dimension tool places the text first.",
|
||||
"prefs.dimension_values_view": "In the view",
|
||||
"prefs.dimension_values_window": "In a window",
|
||||
"prefs.display": "Display",
|
||||
"prefs.drawings": "Drawings",
|
||||
"prefs.edges": "Edges and shading",
|
||||
"prefs.extend": "Extend",
|
||||
"prefs.extra_navigation": "Ctrl+left drag on empty space orbits; hold Space to pan.",
|
||||
@@ -2705,10 +2650,6 @@
|
||||
"prefs.theme_follow_off": "Off",
|
||||
"prefs.theme_follow_stop": "Stop following",
|
||||
"prefs.theme_import_hint": "Click to choose a theme file, drop one here, or press Ctrl+V (Cmd+V on a Mac) with a theme copied",
|
||||
"prefs.theme_notice_dismiss": "Dismiss",
|
||||
"prefs.theme_option_blur": "Blur behind panels",
|
||||
"prefs.theme_option_blur_help": "Blur behind translucent panels, in physical pixels. Zero is off.",
|
||||
"prefs.theme_option_invalid": "Saved value for {option} is invalid; using the theme default. The saved value is kept.",
|
||||
"prefs.theme_option_opacity": "Opacity",
|
||||
"prefs.theme_option_opacity_help": "Adjust floating panel opacity. A readability floor protects text over viewport colors.",
|
||||
"prefs.theme_option_tab_colours": "Tab colors",
|
||||
@@ -2717,7 +2658,6 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "Fade panel and viewport colors when switching tabs. Zero switches instantly.",
|
||||
"prefs.theme_options_badge": "Options",
|
||||
"prefs.theme_options_more": "More · {count}",
|
||||
"prefs.theme_options_newer": "This theme requires a newer SolidCraft. Upgrade, then reimport the original theme to recover its option definitions.",
|
||||
"prefs.theme_options_reset": "Reset",
|
||||
"prefs.theme_options_unavailable": "Unavailable on this device",
|
||||
"prefs.theme_paste": "Paste theme",
|
||||
@@ -4844,7 +4784,6 @@
|
||||
"ui.default_modelling_orientation": "Default modeling orientation",
|
||||
"ui.delete": "Delete",
|
||||
"ui.delete_select_first": "Select what to delete first: features, bodies, components or sketch geometry",
|
||||
"ui.dependents_select_first": "Select what to list the dependents of first",
|
||||
"ui.depth": "Depth",
|
||||
"ui.design": "Design",
|
||||
"ui.design_properties.author": "Author",
|
||||
@@ -5002,10 +4941,6 @@
|
||||
"ui.fillet_setback_least": "More than {least}",
|
||||
"ui.fillet_setback_tip": "Where the smooth corner patch starts: how far along each edge from the corner. The default is {factor} × the radius. It must be more than the least setback, where the rounds meet without one: the radius where the faces are square to each other.",
|
||||
"ui.fillet_smooth_radius": "Smooth radius change",
|
||||
"ui.fillet_turn": "Turns",
|
||||
"ui.fillet_turn_mitre": "Mitre",
|
||||
"ui.fillet_turn_swept": "Swept",
|
||||
"ui.fillet_turn_tip": "Where two rounds turn round a sharp corner edge between them, such as the rounds along a boss's base at its upright edges.\nMitre: the rounds run on until they meet in a mitre.\nSwept: the rounds stop at the corner edge and turn round it, the corner a torus tangent to both. Only for rounds of one radius round a straight, convex corner edge.",
|
||||
"ui.fillet_type_choices_tip": "Constant radius: one radius all along the edge.\nChord length: the width across the round stays the same, whatever angle the faces meet at.\nVariable radius: a radius at each end, and more along the edge if you add them.\nCurvature continuous: a section without a kink in reflections where it meets flat faces (G2).\nSpline: the radius along the edge follows control radii you set.",
|
||||
"ui.fillet_type_curvature": "Curvature continuous",
|
||||
"ui.fillet_type_tip": "The section across the edge. Curvature continuous: a curvature-continuous (G2) section between planar faces, so reflections run from the faces into the round without a kink.",
|
||||
@@ -5015,7 +4950,6 @@
|
||||
"ui.fine": "Fine",
|
||||
"ui.finest": "Finest",
|
||||
"ui.finish_sketch": "Finish Sketch",
|
||||
"ui.first_angle": "First angle",
|
||||
"ui.fit": "Fit",
|
||||
"ui.fix_unfix": "Fix/Unfix",
|
||||
"ui.flat": "Flat",
|
||||
@@ -5320,7 +5254,6 @@
|
||||
"ui.profiles": "Profiles",
|
||||
"ui.project": "Project",
|
||||
"ui.project_face_edges_sketch": "Project face edges into new sketches",
|
||||
"ui.projection_of_new_drawings": "Projection of new drawings",
|
||||
"ui.properties": "Properties",
|
||||
"ui.properties.copy_matrix": "Copy matrix",
|
||||
"ui.properties.first_moments": "First moments",
|
||||
@@ -5506,7 +5439,6 @@
|
||||
"ui.text": "Text",
|
||||
"ui.theme": "Theme",
|
||||
"ui.thickness": "Thickness",
|
||||
"ui.third_angle": "Third angle",
|
||||
"ui.thread": "Thread",
|
||||
"ui.tie_ends": "Tie",
|
||||
"ui.tie_ends_tip": "Constrain all untied ends",
|
||||
|
||||
@@ -1358,7 +1358,6 @@
|
||||
"document.read_only_save": "Este diseño se abrió en solo lectura: guárdalo como un archivo nuevo",
|
||||
"document.save_changes": "¿Guardar los cambios de {name}?",
|
||||
"document.save_copy": "Guardar una copia…",
|
||||
"document.tab_unsaved_changes": "Cambios sin guardar",
|
||||
"document.unsaved_changes": "{name} tiene cambios sin guardar. Si no los guardas, se perderán.",
|
||||
"drive.rigid": "{joint} es una junta rígida: mantiene sus partes quietas, así que no tiene nada que impulsar.",
|
||||
"drive.stopped_by_contact": "Detenido en {value}: {a} atravesaría {b}",
|
||||
|
||||
@@ -1391,7 +1391,6 @@
|
||||
"document.read_only_save": "Cette conception a été ouverte en lecture seule : enregistrez-la dans un nouveau fichier",
|
||||
"document.save_changes": "Enregistrer les modifications de {name} ?",
|
||||
"document.save_copy": "Enregistrer une copie…",
|
||||
"document.tab_unsaved_changes": "Modifications non enregistrées",
|
||||
"document.unsaved_changes": "{name} contient des modifications non enregistrées. Si vous n'enregistrez pas, elles seront perdues.",
|
||||
"drive.rigid": "{joint} est une liaison rigide : elle maintient ses pièces immobiles, il n'y a donc rien à piloter. Pour les laisser bouger, changer son type dans Modifier la liaison : Revolute pour tourner, Slider pour coulisser, Cylindrical pour les deux.",
|
||||
"drive.stopped_by_contact": "Arrêté à {value} : {a} traverserait {b}",
|
||||
|
||||
@@ -1396,9 +1396,6 @@
|
||||
"dialog.custom_axis": "↖ ציר מותאם",
|
||||
"dialog.dimension_insert_hint": "יש ללחוץ על מידה מניעה אחרת כדי להוסיף את שמה",
|
||||
"dialog.dimension_name": "מידה: {name}",
|
||||
"dialog.dimension_step_down": "הפחתה ב־{step}",
|
||||
"dialog.dimension_step_expression": "רק מספר משתנה בצעדים; ביטוי נשאר כפי שנכתב",
|
||||
"dialog.dimension_step_up": "הוספה של {step}",
|
||||
"dialog.edit_title": "עריכת {title}",
|
||||
"dialog.no_shown_body": "אין גוף מוצג לשינוי: הציגו גוף, או בחרו „גוף חדש”",
|
||||
"dialog.pattern_sketch_not_object": "בחרו גופים או פיצ'רים: סקיצה אינה אף אחד מהם. חזרו על העקומות שלה בעזרת המערך או השיקוף של הסקיצה עצמה, או בצעו לה שליפה קודם.",
|
||||
@@ -1406,7 +1403,6 @@
|
||||
"dialog.selected_count": "↖ {count} נבחרו",
|
||||
"dialog.selection_constraint": "אילוץ {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · עקומה {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "יש לבחור קלט כדי לשנות אותו. לחיצה על אובייקט שנבחר מסירה אותו; גם Ctrl/Cmd+לחיצה מחליפה את מצבו. לאחר בדיקת התצוגה המקדימה יש ללחוץ על אישור.",
|
||||
"dialog.selection_midpoint": "נקודת אמצע · {id}",
|
||||
"dialog.selection_profile": "{sketch} · פרופיל {number}",
|
||||
@@ -1424,7 +1420,6 @@
|
||||
"document.read_only_save": "התכן הזה נפתח לקריאה בלבד: יש לשמור אותו כקובץ חדש",
|
||||
"document.save_changes": "לשמור את השינויים ב־{name}?",
|
||||
"document.save_copy": "שמירת עותק…",
|
||||
"document.tab_unsaved_changes": "שינויים שלא נשמרו",
|
||||
"document.unsaved_changes": "ב־{name} יש שינויים שלא נשמרו. ללא שמירה, הם יאבדו.",
|
||||
"drive.rigid": "{joint} הוא מפרק קשיח: הוא מחזיק את החלקים שלו במקומם, ולכן אין מה להניע. כדי שהחלקים יזוזו, שנו את הסוג שלו ב״עריכת מפרק״: Revolute לסיבוב, Slider להחלקה, Cylindrical לשניהם.",
|
||||
"drive.stopped_by_contact": "נעצר ב־{value}: {a} היה עובר דרך {b}",
|
||||
@@ -2004,7 +1999,6 @@
|
||||
"material.selection.mixed": "חומרים שונים",
|
||||
"measure.at": "מיקום",
|
||||
"measure.item": "פריט {number}",
|
||||
"measure.line_to_centre": "מהקו למרכז",
|
||||
"measure.quick_copied": "המדידה הועתקה",
|
||||
"measure.quick_copy": "לחיצה מעתיקה את המדידה",
|
||||
"measure.quick_edges": "{count} מקצועות",
|
||||
@@ -2271,14 +2265,6 @@
|
||||
"prefs.auto_operation_note": "איחוד, גוף חדש או חיתוך, לפי מה שהחומר נוגע בו.",
|
||||
"prefs.axes": "צירים",
|
||||
"prefs.axis": "ציר",
|
||||
"prefs.blur_cost": "עלות עיבוד יחסית. יותר פסים מציינים בדרך כלל יותר עבודה.",
|
||||
"prefs.blur_device": "לא זמין בהתקן הגרפי או בנתיב העיבוד הזה",
|
||||
"prefs.blur_guard": "איכות הטשטוש הופחתה כדי לשמור על תגובתיות הפריימים. הרדיוס לא השתנה.",
|
||||
"prefs.blur_off": "כבוי",
|
||||
"prefs.blur_reduce_transparency": "מושבת עקב הגדרת הפחתת השקיפות של המערכת",
|
||||
"prefs.blur_software": "לא זמין עם עיבוד גרפי בתוכנה (gpu-software)",
|
||||
"prefs.blur_source": "לא זמין בתצוגת סביבת העבודה הזו",
|
||||
"prefs.blur_web": "לא זמין בנתיב העיבוד הזה בדפדפן",
|
||||
"prefs.bodies_folders": "תיקיות גופים",
|
||||
"prefs.bodies_folders_always": "תמיד",
|
||||
"prefs.bodies_folders_never": "אף פעם",
|
||||
@@ -2337,13 +2323,6 @@
|
||||
"prefs.dialogs_edge": "בקצה התצוגה",
|
||||
"prefs.dialogs_left_column": "בעמודה השמאלית",
|
||||
"prefs.dialogs_note": "היכן נפתחות תיבות הדו־שיח של הפקודות: מעוגנות בקצה התצוגה, או בעמודה השמאלית במקום הדפדפן וההיסטוריה, שחוזרים כפי שהיו כשהתיבה נסגרת.",
|
||||
"prefs.dimension_step_angle": "מה שהחצים בחלון, למעלה/למטה והגלגלת מוסיפים לזווית",
|
||||
"prefs.dimension_step_length": "מה שהחצים בחלון, למעלה/למטה והגלגלת מוסיפים לאורך",
|
||||
"prefs.dimension_steps": "צעדי החלון",
|
||||
"prefs.dimension_values": "ערכי מידות",
|
||||
"prefs.dimension_values_note": "היכן מקלידים את ערך המידה. לחלון יש חצים שמשנים אותו בצעדים; כלי המידה מציב קודם את הטקסט.",
|
||||
"prefs.dimension_values_view": "בתצוגה",
|
||||
"prefs.dimension_values_window": "בחלון",
|
||||
"prefs.display": "תצוגה",
|
||||
"prefs.edges": "מקצועות והצללה",
|
||||
"prefs.extend": "הרחבה",
|
||||
@@ -2671,10 +2650,6 @@
|
||||
"prefs.theme_follow_off": "כבוי",
|
||||
"prefs.theme_follow_stop": "הפסקת המעקב",
|
||||
"prefs.theme_import_hint": "לחצו כדי לבחור קובץ ערכת נושא, שחררו קובץ כאן, או הקישו Ctrl+V (Cmd+V ב-Mac) אחרי שהעתקתם ערכת נושא",
|
||||
"prefs.theme_notice_dismiss": "הסתרה",
|
||||
"prefs.theme_option_blur": "טשטוש מאחורי לוחות",
|
||||
"prefs.theme_option_blur_help": "טשטוש מאחורי לוחות שקופים למחצה, בפיקסלים פיזיים. אפס מכבה את הטשטוש.",
|
||||
"prefs.theme_option_invalid": "הערך השמור עבור {option} אינו תקין; נעשה שימוש בברירת המחדל של ערכת הנושא. הערך השמור נשמר ללא שינוי.",
|
||||
"prefs.theme_option_opacity": "אטימות",
|
||||
"prefs.theme_option_opacity_help": "כוונון אטימות הלוחות הצפים. רמת אטימות מינימלית שומרת על קריאות הטקסט על רקע צבעי חלון התצוגה.",
|
||||
"prefs.theme_option_tab_colours": "צבעי כרטיסיות",
|
||||
@@ -2683,7 +2658,6 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "שנה בהדרגה את צבעי הלוח ואזור התצוגה בעת מעבר בין כרטיסיות. ערך אפס מבצע את המעבר מיד.",
|
||||
"prefs.theme_options_badge": "אפשרויות",
|
||||
"prefs.theme_options_more": "עוד · {count}",
|
||||
"prefs.theme_options_newer": "ערכת נושא זו דורשת גרסה חדשה יותר של SolidCraft. לאחר השדרוג יש לייבא שוב את ערכת הנושא המקורית כדי לשחזר את הגדרות האפשרויות שלה.",
|
||||
"prefs.theme_options_reset": "איפוס",
|
||||
"prefs.theme_options_unavailable": "לא זמין במכשיר זה",
|
||||
"prefs.theme_paste": "הדבקת ערכת נושא",
|
||||
@@ -4810,7 +4784,6 @@
|
||||
"ui.default_modelling_orientation": "כיוון מידול ברירת מחדל",
|
||||
"ui.delete": "מחיקה",
|
||||
"ui.delete_select_first": "בחרו קודם מה למחוק: תכונות, גופים, רכיבים או גאומטריית סקיצה",
|
||||
"ui.dependents_select_first": "בחרו קודם מה להציג את התלויים שלו",
|
||||
"ui.depth": "עומק",
|
||||
"ui.design": "תכן",
|
||||
"ui.design_properties.author": "מחבר",
|
||||
@@ -4968,10 +4941,6 @@
|
||||
"ui.fillet_setback_least": "יותר מ-{least}",
|
||||
"ui.fillet_setback_tip": "היכן מתחיל טלאי הפינה החלק: המרחק לאורך כל קצה מהפינה. ברירת המחדל היא פי {factor} מהרדיוס. עליה להיות גדולה מהנסיגה המזערית, שבה העיגולים נפגשים בלעדיה: הרדיוס כשהפאות ניצבות זו לזו.",
|
||||
"ui.fillet_smooth_radius": "שינוי רדיוס חלק",
|
||||
"ui.fillet_turn": "פניות",
|
||||
"ui.fillet_turn_mitre": "מיטר",
|
||||
"ui.fillet_turn_swept": "מסובב",
|
||||
"ui.fillet_turn_tip": "היכן ששני עיגולים מסתובבים סביב קצה פינה חד ביניהם, כמו העיגולים בבסיס בליטה ליד הקצוות הזקופים שלה.\nמיטר: העיגולים נמשכים עד שהם נפגשים במיטר.\nמסובב: העיגולים נעצרים בקצה הפינה ומסתובבים סביבו, והפינה היא טורוס המשיק לשניהם. רק לעיגולים ברדיוס אחד סביב קצה פינה ישר וקמור.",
|
||||
"ui.fillet_type_choices_tip": "רדיוס קבוע: רדיוס אחד לכל אורך הקצה.\nאורך מיתר: רוחב העיגול נשאר זהה, בכל זווית שבה הפאות נפגשות.\nרדיוס משתנה: רדיוס בכל קצה, ועוד לאורך הקצה אם מוסיפים.\nרציף בעקמומיות: חתך בלי שבר בהשתקפויות היכן שהוא פוגש פאות מישוריות (G2).\nספליין: הרדיוס לאורך הקצה עוקב אחרי רדיוסי בקרה שאתם קובעים.",
|
||||
"ui.fillet_type_curvature": "רציף בעקמומיות",
|
||||
"ui.fillet_type_tip": "החתך לרוחב הקצה. רציף בעקמומיות: חתך רציף בעקמומיות (G2) בין פאות מישוריות, כך שההשתקפויות עוברות מהפאות אל העיגול בלי שבר.",
|
||||
|
||||
@@ -1392,7 +1392,6 @@
|
||||
"document.read_only_save": "Questa progettazione è stata aperta in sola lettura: salvala come nuovo file",
|
||||
"document.save_changes": "Salvare le modifiche a {name}?",
|
||||
"document.save_copy": "Salva una copia…",
|
||||
"document.tab_unsaved_changes": "Modifiche non salvate",
|
||||
"document.unsaved_changes": "{name} contiene modifiche non salvate. Se non salvi, andranno perse.",
|
||||
"drive.rigid": "{joint} è un giunto rigido: tiene ferme le sue parti, quindi non c'è nulla da azionare. Per farle muovere, cambia il suo tipo in Modifica giunto: Rotante per ruotare, Scorrevole per scorrere, Cilindrico per entrambi.",
|
||||
"drive.stopped_by_contact": "Arrestato a {value}: {a} attraverserebbe {b}",
|
||||
|
||||
@@ -833,8 +833,6 @@
|
||||
"cmd.appearance_pick_up": "外観を取得",
|
||||
"cmd.appearance_replace": "外観を置換",
|
||||
"cmd.assembly_bom": "部品表",
|
||||
"cmd.assembly_bom_item_set": "項目番号の変更",
|
||||
"cmd.assembly_bom_renumber": "項目番号の振り直し",
|
||||
"cmd.assembly_mass_properties": "質量特性",
|
||||
"cmd.body.print_face": "造形面",
|
||||
"cmd.body_rename": "ボディの名前を変更",
|
||||
@@ -934,36 +932,6 @@
|
||||
"cmd.document_metadata_set": "デザインのプロパティを変更",
|
||||
"cmd.document_revision": "デザイン リビジョン",
|
||||
"cmd.document_units": "デザインの単位を変更",
|
||||
"cmd.drawing_balloon_add": "バルーンを追加",
|
||||
"cmd.drawing_balloon_edit": "バルーンを編集",
|
||||
"cmd.drawing_balloon_inspect": "バルーンを確認",
|
||||
"cmd.drawing_close": "図面を閉じる",
|
||||
"cmd.drawing_create": "図面を作成",
|
||||
"cmd.drawing_delete": "図面を削除",
|
||||
"cmd.drawing_dimension_add": "図面寸法を追加",
|
||||
"cmd.drawing_dimension_edit": "図面寸法を編集",
|
||||
"cmd.drawing_dimension_inspect": "図面寸法を確認",
|
||||
"cmd.drawing_export": "シートを書き出す",
|
||||
"cmd.drawing_inspect": "図面を確認",
|
||||
"cmd.drawing_list": "図面を一覧表示",
|
||||
"cmd.drawing_open": "図面を開く",
|
||||
"cmd.drawing_properties": "図面のプロパティ",
|
||||
"cmd.drawing_properties_remove": "図面のプロパティを削除",
|
||||
"cmd.drawing_properties_set": "図面のプロパティを変更",
|
||||
"cmd.drawing_revision_add": "リビジョンを追加",
|
||||
"cmd.drawing_revision_delete": "リビジョンを削除",
|
||||
"cmd.drawing_revision_edit": "リビジョンを編集",
|
||||
"cmd.drawing_scene": "シートを描画",
|
||||
"cmd.drawing_set": "図面設定",
|
||||
"cmd.drawing_sheet_add": "シートを追加",
|
||||
"cmd.drawing_sheet_delete": "シートを削除",
|
||||
"cmd.drawing_sheet_reorder": "シートを並べ替え",
|
||||
"cmd.drawing_sheet_set": "シート設定",
|
||||
"cmd.drawing_view_copy_to_sheet": "ビューをシートにコピー",
|
||||
"cmd.drawing_view_edit": "図面ビューを編集",
|
||||
"cmd.drawing_view_inspect": "図面ビューを確認",
|
||||
"cmd.drawing_view_move_to_sheet": "ビューをシートに移動",
|
||||
"cmd.drawing_view_snapshot": "図面ビューのスナップショット",
|
||||
"cmd.edit_checkpoint": "チェックポイント",
|
||||
"cmd.edit_repeat": "最後のコマンドを繰り返す",
|
||||
"cmd.edit_restore_checkpoint": "チェックポイントを復元",
|
||||
@@ -1061,7 +1029,6 @@
|
||||
"cmd.motion_trace_edit": "モーション トレースを編集",
|
||||
"cmd.motion_trace_keep": "モーション トレースを保持",
|
||||
"cmd.occurrence_align": "オカレンスを整列",
|
||||
"cmd.occurrence_bom_type": "部品表の種類",
|
||||
"cmd.occurrence_copy": "貼り付け(インスタンス)",
|
||||
"cmd.occurrence_drag": "オカレンスをドラッグ",
|
||||
"cmd.occurrence_ground": "空間固定",
|
||||
@@ -1429,9 +1396,6 @@
|
||||
"dialog.custom_axis": "↖ カスタム軸",
|
||||
"dialog.dimension_insert_hint": "別の駆動寸法をクリックして名前を挿入",
|
||||
"dialog.dimension_name": "寸法: {name}",
|
||||
"dialog.dimension_step_down": "{step} 減らす",
|
||||
"dialog.dimension_step_expression": "増減できるのは数値だけです。式はそのまま残ります",
|
||||
"dialog.dimension_step_up": "{step} 増やす",
|
||||
"dialog.edit_title": "{title}を編集",
|
||||
"dialog.no_shown_body": "変更できる表示中のボディがありません。ボディを表示するか、新規ボディを選択してください",
|
||||
"dialog.pattern_sketch_not_object": "ボディかフィーチャーを選んでください。スケッチはどちらでもありません。曲線はスケッチ内のパターンかミラーで繰り返すか、先に押し出してください。",
|
||||
@@ -1439,7 +1403,6 @@
|
||||
"dialog.selected_count": "↖ {count} 個選択中",
|
||||
"dialog.selection_constraint": "拘束 {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · 曲線 {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "入力欄を選んで変更します。選択済みのオブジェクトをクリックすると解除できます。Ctrl/Cmd を押しながらクリックしても切り替えられます。プレビューを確認してから OK を押してください。",
|
||||
"dialog.selection_midpoint": "中点 · {id}",
|
||||
"dialog.selection_profile": "{sketch} · プロファイル {number}",
|
||||
@@ -1457,7 +1420,6 @@
|
||||
"document.read_only_save": "このデザインは読み取り専用で開かれています:新しいファイルとして保存してください",
|
||||
"document.save_changes": "{name} への変更を保存しますか?",
|
||||
"document.save_copy": "コピーを保存…",
|
||||
"document.tab_unsaved_changes": "未保存の変更",
|
||||
"document.unsaved_changes": "{name} には保存されていない変更があります。保存しない場合、変更内容は失われます。",
|
||||
"drive.rigid": "{joint} は剛体ジョイントです。部品をその場に固定するため、駆動するものがありません。動かすには「ジョイントを編集」で種類を変更してください: 回転させるなら Revolute、スライドさせるなら Slider、両方なら Cylindrical。",
|
||||
"drive.stopped_by_contact": "{value} で停止: {a} が {b} を通り抜けてしまいます",
|
||||
@@ -2037,7 +1999,6 @@
|
||||
"material.selection.mixed": "異なる材料",
|
||||
"measure.at": "位置",
|
||||
"measure.item": "項目 {number}",
|
||||
"measure.line_to_centre": "線から中心まで",
|
||||
"measure.quick_copied": "測定値をコピーしました",
|
||||
"measure.quick_copy": "クリックで測定値をコピー",
|
||||
"measure.quick_edges": "エッジ {count} 本",
|
||||
@@ -2304,14 +2265,6 @@
|
||||
"prefs.auto_operation_note": "材料が接触する対象に応じて結合、新規ボディ、切り取りを選びます。",
|
||||
"prefs.axes": "軸",
|
||||
"prefs.axis": "軸",
|
||||
"prefs.blur_cost": "相対的な描画負荷です。バーが多いほど、通常は処理量が増えます。",
|
||||
"prefs.blur_device": "このグラフィックスデバイスまたは描画経路では利用できません",
|
||||
"prefs.blur_guard": "画面の応答性を保つため、ぼかしの品質を下げました。半径は変更されていません。",
|
||||
"prefs.blur_off": "オフ",
|
||||
"prefs.blur_reduce_transparency": "システムの透明度を下げる設定により無効です",
|
||||
"prefs.blur_software": "ソフトウェア描画では利用できません (gpu-software)",
|
||||
"prefs.blur_source": "このワークスペース表示では利用できません",
|
||||
"prefs.blur_web": "このウェブ描画経路では利用できません",
|
||||
"prefs.bodies_folders": "ボディフォルダー",
|
||||
"prefs.bodies_folders_always": "常に表示",
|
||||
"prefs.bodies_folders_never": "表示しない",
|
||||
@@ -2370,15 +2323,7 @@
|
||||
"prefs.dialogs_edge": "ビューの端",
|
||||
"prefs.dialogs_left_column": "左の列",
|
||||
"prefs.dialogs_note": "コマンドダイアログを開く場所:ビューの端にドッキングするか、左の列でブラウザーと履歴の代わりに表示します。ダイアログを閉じるとブラウザーと履歴は元どおりに戻ります。",
|
||||
"prefs.dimension_step_angle": "ウィンドウの矢印、上下キー、ホイールで角度に加える量",
|
||||
"prefs.dimension_step_length": "ウィンドウの矢印、上下キー、ホイールで長さに加える量",
|
||||
"prefs.dimension_steps": "ウィンドウの増減量",
|
||||
"prefs.dimension_values": "寸法値の入力",
|
||||
"prefs.dimension_values_note": "寸法値を入力する場所です。ウィンドウでは矢印で値を増減でき、寸法ツールは先に文字を配置します。",
|
||||
"prefs.dimension_values_view": "ビュー内",
|
||||
"prefs.dimension_values_window": "ウィンドウ",
|
||||
"prefs.display": "表示",
|
||||
"prefs.drawings": "図面",
|
||||
"prefs.edges": "エッジと陰影",
|
||||
"prefs.extend": "拡張",
|
||||
"prefs.extra_navigation": "何もない場所でCtrl+左ドラッグすると回転し、Spaceを押しながらドラッグすると移動します。",
|
||||
@@ -2705,10 +2650,6 @@
|
||||
"prefs.theme_follow_off": "オフ",
|
||||
"prefs.theme_follow_stop": "追従を停止",
|
||||
"prefs.theme_import_hint": "クリックしてテーマファイルを選ぶか、ここにドロップするか、テーマをコピーした状態で Ctrl+V(Mac では Cmd+V)を押します",
|
||||
"prefs.theme_notice_dismiss": "非表示",
|
||||
"prefs.theme_option_blur": "パネル背面のぼかし",
|
||||
"prefs.theme_option_blur_help": "半透明パネルの背面のぼかしを物理ピクセル単位で指定します。0でオフになります。",
|
||||
"prefs.theme_option_invalid": "{option} の保存値が無効なため、テーマの既定値を使用します。保存値は保持されます。",
|
||||
"prefs.theme_option_opacity": "不透明度",
|
||||
"prefs.theme_option_opacity_help": "フローティングパネルの不透明度を調整します。ビューポートの色に重なっても文字が読めるように下限が適用されます。",
|
||||
"prefs.theme_option_tab_colours": "タブの色",
|
||||
@@ -2717,7 +2658,6 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "タブの切り替え時にパネルとビューポートの色を徐々に変えます。0で即座に切り替えます。",
|
||||
"prefs.theme_options_badge": "オプション",
|
||||
"prefs.theme_options_more": "その他 · {count}",
|
||||
"prefs.theme_options_newer": "このテーマには新しいバージョンの SolidCraft が必要です。更新後、元のテーマを再インポートしてオプション定義を復元してください。",
|
||||
"prefs.theme_options_reset": "リセット",
|
||||
"prefs.theme_options_unavailable": "このデバイスでは利用できません",
|
||||
"prefs.theme_paste": "テーマを貼り付け",
|
||||
@@ -4844,7 +4784,6 @@
|
||||
"ui.default_modelling_orientation": "モデリングのデフォルト方向",
|
||||
"ui.delete": "削除",
|
||||
"ui.delete_select_first": "先に削除するものを選択してください: フィーチャ、ボディ、コンポーネント、スケッチ ジオメトリ",
|
||||
"ui.dependents_select_first": "先に、依存先を表示する対象を選択してください",
|
||||
"ui.depth": "深さ",
|
||||
"ui.design": "デザイン",
|
||||
"ui.design_properties.author": "作成者",
|
||||
@@ -5002,10 +4941,6 @@
|
||||
"ui.fillet_setback_least": "{least} より大きく",
|
||||
"ui.fillet_setback_tip": "なめらかなコーナーパッチが始まる位置: コーナーから各エッジに沿った距離。既定値は半径の {factor} 倍。最小セットバック (セットバックなしでラウンド同士が交わる位置。面どうしが直角なら半径) より大きくする必要があります。",
|
||||
"ui.fillet_smooth_radius": "半径をなめらかに変化",
|
||||
"ui.fillet_turn": "曲がり角",
|
||||
"ui.fillet_turn_mitre": "留め継ぎ",
|
||||
"ui.fillet_turn_swept": "回転",
|
||||
"ui.fillet_turn_tip": "2 つのフィレットが間にある鋭いコーナーエッジを回り込む所(ボスの根元のフィレットが立ち上がりエッジを回る所など)。\n留め継ぎ: フィレットは延びて留め継ぎで出会います。\n回転: フィレットはコーナーエッジで止まり、その周りを回ります。コーナーは両方に接するトーラスです。同じ半径のフィレットが、まっすぐな凸のコーナーエッジを回る場合のみ。",
|
||||
"ui.fillet_type_choices_tip": "一定の半径: エッジ全体で一つの半径。\n弦長: 面がどんな角度で交わっても、フィレットの幅が同じ。\n可変半径: 両端の半径と、追加すればエッジ沿いの半径。\n曲率連続: 平面と接する所で映り込みが折れない断面 (G2)。\nスプライン: エッジ沿いの半径が、設定した制御半径に従います。",
|
||||
"ui.fillet_type_curvature": "曲率連続",
|
||||
"ui.fillet_type_tip": "エッジを横切る断面。曲率連続: 平面の間の曲率連続 (G2) 断面で、映り込みが面からフィレットへ折れずにつながります。",
|
||||
@@ -5015,7 +4950,6 @@
|
||||
"ui.fine": "細かい",
|
||||
"ui.finest": "最も細かい",
|
||||
"ui.finish_sketch": "スケッチを終了",
|
||||
"ui.first_angle": "第一角法",
|
||||
"ui.fit": "フィット",
|
||||
"ui.fix_unfix": "固定/固定解除",
|
||||
"ui.flat": "展開",
|
||||
@@ -5320,7 +5254,6 @@
|
||||
"ui.profiles": "プロファイル",
|
||||
"ui.project": "投影",
|
||||
"ui.project_face_edges_sketch": "新規スケッチに面のエッジを自動投影",
|
||||
"ui.projection_of_new_drawings": "新規図面の投影法",
|
||||
"ui.properties": "プロパティ",
|
||||
"ui.properties.copy_matrix": "行列をコピー",
|
||||
"ui.properties.first_moments": "一次モーメント",
|
||||
@@ -5506,7 +5439,6 @@
|
||||
"ui.text": "テキスト",
|
||||
"ui.theme": "テーマ",
|
||||
"ui.thickness": "厚さ",
|
||||
"ui.third_angle": "第三角法",
|
||||
"ui.thread": "ねじ",
|
||||
"ui.tie_ends": "拘束",
|
||||
"ui.tie_ends_tip": "未拘束をすべて拘束",
|
||||
|
||||
@@ -724,8 +724,6 @@
|
||||
"cmd.appearance_pick_up": "모양 가져오기",
|
||||
"cmd.appearance_replace": "모양 바꾸기",
|
||||
"cmd.assembly_bom": "자재 명세서",
|
||||
"cmd.assembly_bom_item_set": "항목 번호 변경",
|
||||
"cmd.assembly_bom_renumber": "항목 번호 다시 매기기",
|
||||
"cmd.assembly_mass_properties": "질량 특성",
|
||||
"cmd.body.print_face": "출력 바닥면",
|
||||
"cmd.body_rename": "바디 이름 변경",
|
||||
@@ -896,7 +894,6 @@
|
||||
"cmd.motion_study": "운동 연구",
|
||||
"cmd.motion_trace": "모션 궤적",
|
||||
"cmd.motion_trace_keep": "모션 궤적 유지",
|
||||
"cmd.occurrence_bom_type": "자재 명세서 유형",
|
||||
"cmd.occurrence_copy": "붙여넣기(인스턴스)",
|
||||
"cmd.occurrence_ground": "고정",
|
||||
"cmd.occurrence_move": "인스턴스 이동",
|
||||
@@ -1210,9 +1207,6 @@
|
||||
"dialog.custom_axis": "↖ 사용자 정의 축",
|
||||
"dialog.dimension_insert_hint": "다른 구동 치수를 두 번 클릭하여 이름 삽입",
|
||||
"dialog.dimension_name": "치수: {name}",
|
||||
"dialog.dimension_step_down": "{step} 줄이기",
|
||||
"dialog.dimension_step_expression": "숫자만 증감할 수 있으며 수식은 입력한 그대로 유지됩니다",
|
||||
"dialog.dimension_step_up": "{step} 늘리기",
|
||||
"dialog.edit_title": "{title} 편집",
|
||||
"dialog.pattern_sketch_not_object": "바디나 피처를 선택하세요. 스케치는 둘 다 아닙니다. 곡선은 스케치 안의 패턴이나 대칭 복사로 반복하거나, 먼저 돌출하세요.",
|
||||
"dialog.select": "↖ 선택",
|
||||
@@ -1437,7 +1431,6 @@
|
||||
"material.selection.mixed": "서로 다른 재질",
|
||||
"measure.at": "위치",
|
||||
"measure.item": "항목 {number}",
|
||||
"measure.line_to_centre": "선에서 중심까지",
|
||||
"messages.ago_h": "{n}시간 전",
|
||||
"messages.ago_min": "{n}분 전",
|
||||
"messages.ago_s": "{n}초 전",
|
||||
@@ -1664,14 +1657,6 @@
|
||||
"prefs.auto_operation_note": "재료가 닿는 대상에 따라 결합, 새 바디 또는 자르기를 선택합니다.",
|
||||
"prefs.axes": "축",
|
||||
"prefs.axis": "축",
|
||||
"prefs.blur_cost": "상대적인 렌더링 비용입니다. 막대가 많을수록 일반적으로 더 많은 작업이 필요합니다.",
|
||||
"prefs.blur_device": "이 그래픽 장치 또는 렌더링 경로에서는 사용할 수 없습니다",
|
||||
"prefs.blur_guard": "화면의 원활한 응답을 위해 흐림 품질을 낮췄습니다. 반경은 변경되지 않습니다.",
|
||||
"prefs.blur_off": "끄기",
|
||||
"prefs.blur_reduce_transparency": "시스템의 투명도 줄이기 설정에 의해 비활성화됨",
|
||||
"prefs.blur_software": "소프트웨어 그래픽에서는 사용할 수 없습니다 (gpu-software)",
|
||||
"prefs.blur_source": "이 작업 공간 보기에서는 사용할 수 없습니다",
|
||||
"prefs.blur_web": "이 웹 렌더링 경로에서는 사용할 수 없습니다",
|
||||
"prefs.bodies_folders": "바디 폴더",
|
||||
"prefs.bodies_folders_always": "항상 표시",
|
||||
"prefs.bodies_folders_never": "표시 안 함",
|
||||
@@ -1727,15 +1712,7 @@
|
||||
"prefs.dialogs_edge": "뷰 가장자리",
|
||||
"prefs.dialogs_left_column": "왼쪽 열",
|
||||
"prefs.dialogs_note": "명령 대화 상자가 열리는 위치: 뷰 가장자리에 고정하거나, 왼쪽 열에서 브라우저와 이력 대신 표시합니다. 대화 상자를 닫으면 브라우저와 이력이 그대로 돌아옵니다.",
|
||||
"prefs.dimension_step_angle": "창의 화살표, 위/아래 키와 휠이 각도에 더하는 값",
|
||||
"prefs.dimension_step_length": "창의 화살표, 위/아래 키와 휠이 길이에 더하는 값",
|
||||
"prefs.dimension_steps": "창의 증감 단위",
|
||||
"prefs.dimension_values": "치수 값",
|
||||
"prefs.dimension_values_note": "치수 값을 입력하는 위치입니다. 창에는 값을 증감하는 화살표가 있으며, 치수 도구는 텍스트를 먼저 배치합니다.",
|
||||
"prefs.dimension_values_view": "뷰에서",
|
||||
"prefs.dimension_values_window": "창에서",
|
||||
"prefs.display": "표시",
|
||||
"prefs.drawings": "도면",
|
||||
"prefs.edges": "모서리 및 음영",
|
||||
"prefs.extend": "확장",
|
||||
"prefs.extra_navigation": "Ctrl+왼쪽 드래그로 빈 공간에서 회전하고, Space를 누른 채 이동합니다.",
|
||||
@@ -1998,10 +1975,6 @@
|
||||
"prefs.theme_follow_note": "다른 도구가 파일을 다시 쓸 때마다 다시 불러옵니다. 테마를 선택하면 따르기를 중지합니다.",
|
||||
"prefs.theme_follow_off": "끄기",
|
||||
"prefs.theme_follow_stop": "따르기 중지",
|
||||
"prefs.theme_notice_dismiss": "닫기",
|
||||
"prefs.theme_option_blur": "패널 뒤 흐림",
|
||||
"prefs.theme_option_blur_help": "반투명 패널 뒤의 흐림을 물리적 픽셀 단위로 설정합니다. 0이면 꺼집니다.",
|
||||
"prefs.theme_option_invalid": "{option}의 저장된 값이 유효하지 않아 테마 기본값을 사용합니다. 저장된 값은 유지됩니다.",
|
||||
"prefs.theme_option_opacity": "불투명도",
|
||||
"prefs.theme_option_opacity_help": "플로팅 패널의 불투명도를 조정합니다. 뷰포트 색상 위에서도 텍스트를 읽을 수 있도록 최솟값이 적용됩니다.",
|
||||
"prefs.theme_option_tab_colours": "탭 색상",
|
||||
@@ -2010,7 +1983,6 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "탭을 전환할 때 패널 및 뷰포트 색상을 서서히 변경합니다. 0이면 즉시 전환합니다.",
|
||||
"prefs.theme_options_badge": "옵션",
|
||||
"prefs.theme_options_more": "더 보기 · {count}",
|
||||
"prefs.theme_options_newer": "이 테마에는 더 최신 버전의 SolidCraft가 필요합니다. 업그레이드 후 원본 테마를 다시 가져와 옵션 정의를 복구하세요.",
|
||||
"prefs.theme_options_reset": "초기화",
|
||||
"prefs.theme_options_unavailable": "이 기기에서는 사용할 수 없음",
|
||||
"prefs.timeline_half_hour": "−30분",
|
||||
@@ -3590,7 +3562,6 @@
|
||||
"ui.default_modelling_orientation": "기본 모델링 방향",
|
||||
"ui.delete": "삭제",
|
||||
"ui.delete_select_first": "먼저 삭제할 항목을 선택하세요: 피처, 바디, 컴포넌트 또는 스케치 형상",
|
||||
"ui.dependents_select_first": "먼저 종속 항목을 표시할 대상을 선택하세요",
|
||||
"ui.depth": "깊이",
|
||||
"ui.design": "디자인",
|
||||
"ui.design_properties.author": "작성자",
|
||||
@@ -3741,10 +3712,6 @@
|
||||
"ui.fillet_setback_least": "{least}보다 크게",
|
||||
"ui.fillet_setback_tip": "부드러운 코너 패치가 시작되는 위치: 코너에서 각 모서리를 따른 거리. 기본값은 반지름의 {factor}배. 최소 셋백(셋백 없이 라운드가 만나는 곳, 면이 서로 직각이면 반지름)보다 커야 합니다.",
|
||||
"ui.fillet_smooth_radius": "반지름 부드럽게 변화",
|
||||
"ui.fillet_turn": "꺾임",
|
||||
"ui.fillet_turn_mitre": "마이터",
|
||||
"ui.fillet_turn_swept": "회전",
|
||||
"ui.fillet_turn_tip": "두 라운드가 사이의 날카로운 코너 모서리를 돌아가는 곳(보스 밑동의 라운드가 세로 모서리를 도는 곳 등).\n마이터: 라운드가 계속 이어져 마이터로 만납니다.\n회전: 라운드가 코너 모서리에서 멈추고 그 둘레를 돕니다. 코너는 둘 모두에 접하는 토러스입니다. 반지름이 같은 라운드가 곧고 볼록한 코너 모서리를 돌 때만.",
|
||||
"ui.fillet_type_choices_tip": "일정한 반지름: 모서리 전체에 하나의 반지름.\n현 길이: 면이 어떤 각도로 만나든 라운드의 폭이 같습니다.\n가변 반지름: 양 끝의 반지름과, 추가하면 모서리를 따라 더.\n곡률 연속: 평면과 만나는 곳에서 반사가 꺾이지 않는 단면(G2).\n스플라인: 모서리를 따라 반지름이 설정한 제어 반지름을 따릅니다.",
|
||||
"ui.fillet_type_curvature": "곡률 연속",
|
||||
"ui.fillet_type_tip": "모서리를 가로지르는 단면. 곡률 연속: 평면 사이의 곡률 연속(G2) 단면으로, 반사가 면에서 라운드로 꺾임 없이 이어집니다.",
|
||||
@@ -3754,7 +3721,6 @@
|
||||
"ui.fine": "세밀",
|
||||
"ui.finest": "가장 세밀",
|
||||
"ui.finish_sketch": "스케치 종료",
|
||||
"ui.first_angle": "제1각법",
|
||||
"ui.fit": "맞춤",
|
||||
"ui.fix_unfix": "고정/고정 해제",
|
||||
"ui.flat": "플랫",
|
||||
@@ -4028,7 +3994,6 @@
|
||||
"ui.profiles": "프로파일",
|
||||
"ui.project": "투영",
|
||||
"ui.project_face_edges_sketch": "면 모서리를 새 스케치로 투영",
|
||||
"ui.projection_of_new_drawings": "새 도면의 투상법",
|
||||
"ui.properties": "속성",
|
||||
"ui.properties.copy_matrix": "행렬 복사",
|
||||
"ui.properties.first_moments": "1차 모멘트",
|
||||
@@ -4188,7 +4153,6 @@
|
||||
"ui.text": "텍스트",
|
||||
"ui.theme": "테마",
|
||||
"ui.thickness": "두께",
|
||||
"ui.third_angle": "제3각법",
|
||||
"ui.thread": "나사산",
|
||||
"ui.tie_ends": "연결",
|
||||
"ui.tie_ends_tip": "연결되지 않은 모든 끝점 구속",
|
||||
|
||||
@@ -1368,9 +1368,6 @@
|
||||
"dialog.custom_axis": "↖ Aangepaste as",
|
||||
"dialog.dimension_insert_hint": "Klik op een andere aandrijvende maat om de naam ervan in te voegen",
|
||||
"dialog.dimension_name": "Maat: {name}",
|
||||
"dialog.dimension_step_down": "{step} omlaag",
|
||||
"dialog.dimension_step_expression": "Alleen een getal verspringt; een uitdrukking blijft zoals ingevoerd",
|
||||
"dialog.dimension_step_up": "{step} omhoog",
|
||||
"dialog.edit_title": "{title} BEWERKEN",
|
||||
"dialog.no_shown_body": "Geen zichtbaar lichaam om te wijzigen: toon er een of kies Nieuw lichaam",
|
||||
"dialog.pattern_sketch_not_object": "Kies lichamen of kenmerken: een schets is geen van beide. Herhaal de curven ervan met het eigen Patroon of Spiegelen van de schets, of extrudeer de schets eerst.",
|
||||
@@ -1395,7 +1392,6 @@
|
||||
"document.read_only_save": "Dit ontwerp is alleen-lezen geopend: sla het op als nieuw bestand",
|
||||
"document.save_changes": "Wijzigingen in {name} opslaan?",
|
||||
"document.save_copy": "Kopie opslaan…",
|
||||
"document.tab_unsaved_changes": "Niet-opgeslagen wijzigingen",
|
||||
"document.unsaved_changes": "{name} heeft niet-opgeslagen wijzigingen. Als u niet opslaat, gaan ze verloren.",
|
||||
"drive.rigid": "{joint} is een starre verbinding: die houdt zijn onderdelen vast en heeft dus niets om aan te sturen. Wijzig het type in Verbinding bewerken om ze te laten bewegen: Draaiend om te draaien, Schuivend om te schuiven, Cilindrisch voor beide.",
|
||||
"drive.stopped_by_contact": "Gestopt bij {value}: {a} zou door {b} heen gaan",
|
||||
@@ -2198,13 +2194,6 @@
|
||||
"prefs.dialogs_edge": "Aan de rand van de weergave",
|
||||
"prefs.dialogs_left_column": "In de linkerkolom",
|
||||
"prefs.dialogs_note": "Waar opdrachtdialogen openen: vastgezet aan de rand van de weergave, of in de linkerkolom in plaats van de browser en de geschiedenis, die terugkomen zoals ze waren wanneer de dialoog sluit.",
|
||||
"prefs.dimension_step_angle": "Wat de pijlen van het venster, Omhoog/Omlaag en het wieltje bij een hoek optellen",
|
||||
"prefs.dimension_step_length": "Wat de pijlen van het venster, Omhoog/Omlaag en het wieltje bij een lengte optellen",
|
||||
"prefs.dimension_steps": "Stappen in het venster",
|
||||
"prefs.dimension_values": "Maatwaarden",
|
||||
"prefs.dimension_values_note": "Waar de waarde van een maat wordt ingevoerd. Een venster heeft pijlen die de waarde laten verspringen; het maatgereedschap plaatst eerst de tekst.",
|
||||
"prefs.dimension_values_view": "In de weergave",
|
||||
"prefs.dimension_values_window": "In een venster",
|
||||
"prefs.display": "Weergave",
|
||||
"prefs.edges": "Randen en schaduwing",
|
||||
"prefs.extend": "Verlengen",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"export.paper.blueprint": "Blueprint Blue",
|
||||
"export.paper.prussian_blue": "Prussian Blue (Historical)"
|
||||
},
|
||||
"ar": {
|
||||
"export.paper.blueprint": "أزرق المخطط",
|
||||
"export.paper.prussian_blue": "أزرق بروسي (تاريخي)"
|
||||
},
|
||||
"de": {
|
||||
"export.paper.blueprint": "Blaupausenblau",
|
||||
"export.paper.prussian_blue": "Preußischblau (historisch)"
|
||||
},
|
||||
"he": {
|
||||
"export.paper.blueprint": "כחול שרטוט",
|
||||
"export.paper.prussian_blue": "כחול פרוסי (היסטורי)"
|
||||
},
|
||||
"ja": {
|
||||
"export.paper.blueprint": "ブループリントブルー",
|
||||
"export.paper.prussian_blue": "プルシアンブルー(歴史的)"
|
||||
},
|
||||
"ko": {
|
||||
"export.paper.blueprint": "블루프린트 블루",
|
||||
"export.paper.prussian_blue": "프러시안 블루(역사적)"
|
||||
},
|
||||
"vi": {
|
||||
"export.paper.blueprint": "Xanh bản vẽ",
|
||||
"export.paper.prussian_blue": "Xanh Phổ (lịch sử)"
|
||||
},
|
||||
"zh": {
|
||||
"export.paper.blueprint": "蓝图蓝",
|
||||
"export.paper.prussian_blue": "普鲁士蓝(历史)"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_image_add": "Add Image"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_image_add": "إضافة صورة"
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_image_add": "Bild hinzufügen"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_image_add": "הוספת תמונה"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_image_add": "画像を追加"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_image_add": "이미지 추가"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_image_add": "Thêm hình ảnh"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_image_add": "添加图像"
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_note_add": "Add Note",
|
||||
"cmd.drawing_note_edit": "Edit Note",
|
||||
"cmd.drawing_note_inspect": "Inspect Note"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_note_add": "إضافة ملاحظة",
|
||||
"cmd.drawing_note_edit": "تحرير الملاحظة",
|
||||
"cmd.drawing_note_inspect": "فحص الملاحظة"
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_note_add": "Hinweis hinzufügen",
|
||||
"cmd.drawing_note_edit": "Hinweis bearbeiten",
|
||||
"cmd.drawing_note_inspect": "Hinweis prüfen"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_note_add": "הוספת הערה",
|
||||
"cmd.drawing_note_edit": "עריכת הערה",
|
||||
"cmd.drawing_note_inspect": "בדיקת הערה"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_note_add": "注記を追加",
|
||||
"cmd.drawing_note_edit": "注記を編集",
|
||||
"cmd.drawing_note_inspect": "注記を確認"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_note_add": "주석 추가",
|
||||
"cmd.drawing_note_edit": "주석 편집",
|
||||
"cmd.drawing_note_inspect": "주석 확인"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_note_add": "Thêm ghi chú",
|
||||
"cmd.drawing_note_edit": "Sửa ghi chú",
|
||||
"cmd.drawing_note_inspect": "Kiểm tra ghi chú"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_note_add": "添加注释",
|
||||
"cmd.drawing_note_edit": "编辑注释",
|
||||
"cmd.drawing_note_inspect": "检查注释"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_balloon_repair": "Repair Balloon",
|
||||
"cmd.drawing_dimension_repair": "Repair Drawing Dimension"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_balloon_repair": "إصلاح البالون",
|
||||
"cmd.drawing_dimension_repair": "إصلاح بُعد اللوحة"
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_balloon_repair": "Positionsnummer reparieren",
|
||||
"cmd.drawing_dimension_repair": "Zeichnungsbemaßung reparieren"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_balloon_repair": "תיקון בלון",
|
||||
"cmd.drawing_dimension_repair": "תיקון מידת שרטוט"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_balloon_repair": "バルーンを修復",
|
||||
"cmd.drawing_dimension_repair": "図面寸法を修復"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_balloon_repair": "벌룬 복구",
|
||||
"cmd.drawing_dimension_repair": "도면 치수 복구"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_balloon_repair": "Sửa chữa bóng chú thích",
|
||||
"cmd.drawing_dimension_repair": "Sửa chữa kích thước bản vẽ"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_balloon_repair": "修复零件序号",
|
||||
"cmd.drawing_dimension_repair": "修复绘图尺寸"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_revise": "Issue Revision"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_revise": "إصدار مراجعة"
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_revise": "Revision herausgeben"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_revise": "הוצאת גרסה"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_revise": "リビジョンを発行"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_revise": "리비전 발행"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_revise": "Phát hành bản sửa đổi"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_revise": "发布版本"
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_revision_tag_add": "Add Revision Tag",
|
||||
"cmd.drawing_revision_tag_edit": "Edit Revision Tag",
|
||||
"cmd.drawing_revision_tag_delete": "Delete Revision Tag"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_revision_tag_add": "إضافة علامة مراجعة",
|
||||
"cmd.drawing_revision_tag_edit": "تحرير علامة المراجعة",
|
||||
"cmd.drawing_revision_tag_delete": "حذف علامة المراجعة"
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_revision_tag_add": "Revisionskennzeichen hinzufügen",
|
||||
"cmd.drawing_revision_tag_edit": "Revisionskennzeichen bearbeiten",
|
||||
"cmd.drawing_revision_tag_delete": "Revisionskennzeichen löschen"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_revision_tag_add": "הוספת תג גרסה",
|
||||
"cmd.drawing_revision_tag_edit": "עריכת תג גרסה",
|
||||
"cmd.drawing_revision_tag_delete": "מחיקת תג גרסה"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_revision_tag_add": "リビジョンタグを追加",
|
||||
"cmd.drawing_revision_tag_edit": "リビジョンタグを編集",
|
||||
"cmd.drawing_revision_tag_delete": "リビジョンタグを削除"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_revision_tag_add": "리비전 태그 추가",
|
||||
"cmd.drawing_revision_tag_edit": "리비전 태그 편집",
|
||||
"cmd.drawing_revision_tag_delete": "리비전 태그 삭제"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_revision_tag_add": "Thêm thẻ sửa đổi",
|
||||
"cmd.drawing_revision_tag_edit": "Sửa thẻ sửa đổi",
|
||||
"cmd.drawing_revision_tag_delete": "Xóa thẻ sửa đổi"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_revision_tag_add": "添加版本标记",
|
||||
"cmd.drawing_revision_tag_edit": "编辑版本标记",
|
||||
"cmd.drawing_revision_tag_delete": "删除版本标记"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_status": "Drawing Status"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_status": "حالة الرسم"
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_status": "Zeichnungsstatus"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_status": "מצב השרטוט"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_status": "図面の状態"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_status": "도면 상태"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_status": "Trạng thái bản vẽ"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_status": "绘图状态"
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_table_add": "Add Table",
|
||||
"cmd.drawing_table_delete": "Delete Table",
|
||||
"cmd.drawing_table_edit": "Edit Table",
|
||||
"cmd.drawing_table_inspect": "Inspect Table",
|
||||
"help.drawing.table.edit": "Changes a saved table on a sheet: what it lists, where it sits, its columns and its words. Use it to adjust a parts list or revision table after adding it."
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_table_add": "إضافة جدول",
|
||||
"cmd.drawing_table_delete": "حذف الجدول",
|
||||
"cmd.drawing_table_edit": "تحرير الجدول",
|
||||
"cmd.drawing_table_inspect": "فحص الجدول",
|
||||
"help.drawing.table.edit": "يغيّر جدولاً محفوظاً على ورقة: ما يعرضه ومكانه وأعمدته ونصوصه. استخدمه لضبط قائمة أجزاء أو جدول مراجعات بعد إضافته."
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_table_add": "Tabelle hinzufügen",
|
||||
"cmd.drawing_table_delete": "Tabelle löschen",
|
||||
"cmd.drawing_table_edit": "Tabelle bearbeiten",
|
||||
"cmd.drawing_table_inspect": "Tabelle prüfen",
|
||||
"help.drawing.table.edit": "Ändert eine gespeicherte Tabelle auf einem Blatt: was sie auflistet, wo sie steht, ihre Spalten und Texte. Damit passen Sie eine Stückliste oder Revisionstabelle nach dem Hinzufügen an."
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_table_add": "הוספת טבלה",
|
||||
"cmd.drawing_table_delete": "מחיקת טבלה",
|
||||
"cmd.drawing_table_edit": "עריכת טבלה",
|
||||
"cmd.drawing_table_inspect": "בדיקת טבלה",
|
||||
"help.drawing.table.edit": "משנה טבלה שמורה בגיליון: מה היא מציגה, היכן היא ממוקמת, העמודות והטקסטים שלה. השתמשו בכך כדי להתאים רשימת חלקים או טבלת גרסאות לאחר הוספתה."
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_table_add": "表を追加",
|
||||
"cmd.drawing_table_delete": "表を削除",
|
||||
"cmd.drawing_table_edit": "表を編集",
|
||||
"cmd.drawing_table_inspect": "表を確認",
|
||||
"help.drawing.table.edit": "シート上の保存済みの表の内容、位置、列、文字を変更します。部品表や改訂表を追加した後の調整に使います。"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_table_add": "표 추가",
|
||||
"cmd.drawing_table_delete": "표 삭제",
|
||||
"cmd.drawing_table_edit": "표 편집",
|
||||
"cmd.drawing_table_inspect": "표 확인",
|
||||
"help.drawing.table.edit": "시트에 저장된 표의 내용, 위치, 열, 글자를 변경합니다. 부품표나 개정표를 추가한 뒤 조정할 때 사용합니다."
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_table_add": "Thêm bảng",
|
||||
"cmd.drawing_table_delete": "Xóa bảng",
|
||||
"cmd.drawing_table_edit": "Sửa bảng",
|
||||
"cmd.drawing_table_inspect": "Kiểm tra bảng",
|
||||
"help.drawing.table.edit": "Thay đổi một bảng đã lưu trên trang: nội dung, vị trí, các cột và chữ của bảng. Dùng để chỉnh danh sách chi tiết hoặc bảng sửa đổi sau khi thêm."
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_table_add": "添加表格",
|
||||
"cmd.drawing_table_delete": "删除表格",
|
||||
"cmd.drawing_table_edit": "编辑表格",
|
||||
"cmd.drawing_table_inspect": "检查表格",
|
||||
"help.drawing.table.edit": "更改图纸页上已保存的表格的内容、位置、列和文字。添加零件表或修订表后,可用它进行调整。"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_template_apply": "Apply Drawing Template"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_template_apply": "تطبيق قالب لوحة"
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_template_apply": "Zeichnungsvorlage anwenden"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_template_apply": "החלת תבנית שרטוט"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_template_apply": "図面テンプレートを適用"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_template_apply": "도면 템플릿 적용"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_template_apply": "Áp dụng mẫu bản vẽ"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_template_apply": "应用绘图模板"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_template_save": "Save as Drawing Template"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_template_save": "حفظ كقالب لوحة"
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_template_save": "Als Zeichnungsvorlage speichern"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_template_save": "שמירה כתבנית שרטוט"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_template_save": "図面テンプレートとして保存"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_template_save": "도면 템플릿으로 저장"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_template_save": "Lưu thành mẫu bản vẽ"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_template_save": "另存为绘图模板"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"ui.template_for_new_part_drawings": "Template for new part drawings",
|
||||
"ui.template_for_new_assembly_drawings": "Template for new assembly drawings"
|
||||
},
|
||||
"ar": {
|
||||
"ui.template_for_new_part_drawings": "قالب لوحات القطع الجديدة",
|
||||
"ui.template_for_new_assembly_drawings": "قالب لوحات التجميعات الجديدة"
|
||||
},
|
||||
"de": {
|
||||
"ui.template_for_new_part_drawings": "Vorlage für neue Teilezeichnungen",
|
||||
"ui.template_for_new_assembly_drawings": "Vorlage für neue Baugruppenzeichnungen"
|
||||
},
|
||||
"he": {
|
||||
"ui.template_for_new_part_drawings": "תבנית לשרטוטי חלקים חדשים",
|
||||
"ui.template_for_new_assembly_drawings": "תבנית לשרטוטי מכלולים חדשים"
|
||||
},
|
||||
"ja": {
|
||||
"ui.template_for_new_part_drawings": "新しい部品図面のテンプレート",
|
||||
"ui.template_for_new_assembly_drawings": "新しいアセンブリ図面のテンプレート"
|
||||
},
|
||||
"ko": {
|
||||
"ui.template_for_new_part_drawings": "새 부품 도면의 템플릿",
|
||||
"ui.template_for_new_assembly_drawings": "새 어셈블리 도면의 템플릿"
|
||||
},
|
||||
"vi": {
|
||||
"ui.template_for_new_part_drawings": "Mẫu cho bản vẽ chi tiết mới",
|
||||
"ui.template_for_new_assembly_drawings": "Mẫu cho bản vẽ cụm lắp ráp mới"
|
||||
},
|
||||
"zh": {
|
||||
"ui.template_for_new_part_drawings": "新零件绘图的模板",
|
||||
"ui.template_for_new_assembly_drawings": "新装配体绘图的模板"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_template_list": "List Drawing Templates"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_template_list": "سرد قوالب اللوحات"
|
||||
},
|
||||
"de": {
|
||||
"cmd.drawing_template_list": "Zeichnungsvorlagen auflisten"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_template_list": "רשימת תבניות שרטוט"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_template_list": "図面テンプレートを一覧表示"
|
||||
},
|
||||
"ko": {
|
||||
"cmd.drawing_template_list": "도면 템플릿 목록"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_template_list": "Liệt kê mẫu bản vẽ"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_template_list": "列出绘图模板"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.drawing_view_base": "Base View",
|
||||
"cmd.drawing_view_projected": "Projected View"
|
||||
},
|
||||
"ar": {
|
||||
"cmd.drawing_view_base": "العرض الأساسي",
|
||||
"cmd.drawing_view_projected": "العرض المُسقط"
|
||||
},
|
||||
"he": {
|
||||
"cmd.drawing_view_base": "תצוגת בסיס",
|
||||
"cmd.drawing_view_projected": "תצוגה מוטלת"
|
||||
},
|
||||
"ja": {
|
||||
"cmd.drawing_view_base": "ベースビュー",
|
||||
"cmd.drawing_view_projected": "投影ビュー"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.drawing_view_base": "Chế độ xem cơ sở",
|
||||
"cmd.drawing_view_projected": "Chế độ xem chiếu"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.drawing_view_base": "基础视图",
|
||||
"cmd.drawing_view_projected": "投影视图"
|
||||
}
|
||||
}
|
||||
@@ -1,834 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"0 objects": "0 عناصر",
|
||||
"ASSEMBLY": "التجميع",
|
||||
"Add Drawing Revision": "إضافة مراجعة للوحة",
|
||||
"Add Revision": "إضافة مراجعة",
|
||||
"Add sheet (not implemented)": "إضافة ورقة (غير منفّذ)",
|
||||
"Align Annotations": "محاذاة التعليقات التوضيحية",
|
||||
"Align Notes": "محاذاة الملاحظات",
|
||||
"Align Views": "محاذاة المساقط",
|
||||
"Annotate": "إضافة تعليقات توضيحية",
|
||||
"Apply (not implemented)": "تطبيق (غير منفّذ)",
|
||||
"Auto (Theme)": "تلقائي (السمة)",
|
||||
"Balloon": "بالون",
|
||||
"Base View": "المسقط الأساسي",
|
||||
"Border & template": "الإطار والقالب",
|
||||
"Browse Drawing Resources": "تصفح موارد اللوحات",
|
||||
"CHANGES": "التغييرات",
|
||||
"Centre Mark": "علامة المركز",
|
||||
"Check Drawing": "فحص اللوحة",
|
||||
"Close drawing panel": "إغلاق لوحة إعدادات الرسم",
|
||||
"DRAWING ANNOTATE": "تعليقات اللوحة",
|
||||
"DRAWING LAYOUT": "تخطيط اللوحة",
|
||||
"DRAWING PARTS": "أجزاء اللوحة",
|
||||
"DRAWING REVIEW": "مراجعة اللوحة",
|
||||
"Datum": "مرجع",
|
||||
"Detail View": "مسقط تفصيلي",
|
||||
"Dock at bottom": "إرساء في الأسفل",
|
||||
"Dock at top": "إرساء في الأعلى",
|
||||
"Dock beneath Browser": "إرساء أسفل المتصفح",
|
||||
"Dock left": "إرساء يسارًا",
|
||||
"Dock right": "إرساء يمينًا",
|
||||
"Dock sheet tabs": "إرساء علامات تبويب الأوراق",
|
||||
"Drafting defaults": "إعدادات الرسم الافتراضية",
|
||||
"Drawing & title block": "اللوحة وخانة العنوان",
|
||||
"Drawing Dimension": "بُعد اللوحة",
|
||||
"Drawing Line": "خط اللوحة",
|
||||
"Drawing Note": "ملاحظة اللوحة",
|
||||
"Drawing defaults (not available)": "إعدادات اللوحة الافتراضية (غير متاحة)",
|
||||
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.": "لم تُنفّذ ملفات اللوحات ولا عارض الورق بعد. تعرض الأدوات المخطط لها خياراتها حيثما حُدّدت.",
|
||||
"Drawing workspace": "مساحة عمل اللوحات",
|
||||
"Edit Drawing Table": "تعديل جدول اللوحة",
|
||||
"Edit Table": "تعديل الجدول",
|
||||
"Export Drawing": "تصدير اللوحة",
|
||||
"Finish the current operation before entering Drawing": "أكمل العملية الحالية قبل الدخول إلى مساحة اللوحات",
|
||||
"Hole Callout": "بيان الثقب",
|
||||
"Image": "صورة",
|
||||
"Inherited from parent view": "موروث من المسقط الأصل",
|
||||
"Insert Image": "إدراج صورة",
|
||||
"Leader": "خط إشارة",
|
||||
"Leader Note": "ملاحظة بخط إشارة",
|
||||
"Manage sheets (not implemented)": "إدارة الأوراق (غير منفّذ)",
|
||||
"Manage sheets and docking": "إدارة الأوراق والإرساء",
|
||||
"NOTES": "الملاحظات",
|
||||
"No drawing document": "لا يوجد مستند لوحة",
|
||||
"No drawing geometry available": "لا تتوفر هندسة للوحة",
|
||||
"No drawing objects": "لا توجد عناصر في اللوحة",
|
||||
"No drawing sheet is available yet.": "لا تتوفر ورقة لوحة بعد.",
|
||||
"No drawing view available": "لا يتوفر مسقط للوحة",
|
||||
"No sheet available": "لا تتوفر ورقة",
|
||||
"Not available": "غير متاح",
|
||||
"Not configured": "غير مُعدّ",
|
||||
"Not implemented. These options describe the planned interface; no drawing data is changed.": "غير منفّذ. تصف هذه الخيارات الواجهة المخطط لها؛ لا تتغير أي بيانات للوحة.",
|
||||
"Note": "ملاحظة",
|
||||
"Paper & layout": "الورق والتخطيط",
|
||||
"Paper colour": "لون الورق",
|
||||
"Paper colour will be available when the sheet renderer is implemented.": "سيتوفر لون الورق عند تنفيذ عارض الورقة.",
|
||||
"Parent view": "المسقط الأصل",
|
||||
"Parts & Tables": "الأجزاء والجداول",
|
||||
"Parts List": "قائمة الأجزاء",
|
||||
"Print": "طباعة",
|
||||
"Print Drawing": "طباعة اللوحة",
|
||||
"Profile & units": "ملف التعريف والوحدات",
|
||||
"Projected View": "مسقط مشتق بالإسقاط",
|
||||
"RESOURCES": "الموارد",
|
||||
"REVISION": "المراجعة",
|
||||
"References": "المراجع",
|
||||
"Resources": "الموارد",
|
||||
"Return to Design to use this command.": "عُد إلى مساحة التصميم لاستخدام هذا الأمر.",
|
||||
"Review": "مراجعة",
|
||||
"Revision Table": "جدول المراجعات",
|
||||
"Revision Tbl.": "جدول المراجعات",
|
||||
"SOURCE": "المصدر",
|
||||
"SYMBOLS": "الرموز",
|
||||
"Screen appearance": "مظهر الشاشة",
|
||||
"Section View": "مسقط مقطعي",
|
||||
"Sheet Settings": "إعدادات الورقة",
|
||||
"Sheet unavailable": "الورقة غير متاحة",
|
||||
"Size & orientation": "الحجم والاتجاه",
|
||||
"Source BOM": "قائمة مواد المصدر",
|
||||
"Source Flat": "النمط المسطح للمصدر",
|
||||
"Source design": "التصميم المصدر",
|
||||
"Source properties": "خصائص المصدر",
|
||||
"Space": "توزيع المسافات",
|
||||
"Space Annotations": "توزيع المسافات بين التعليقات التوضيحية",
|
||||
"Space Notes": "توزيع المسافات بين الملاحظات",
|
||||
"TABLES": "الجداول",
|
||||
"Text style": "نمط النص",
|
||||
"Title": "العنوان",
|
||||
"Title block": "خانة العنوان",
|
||||
"UI preview": "معاينة الواجهة",
|
||||
"Units & precision": "الوحدات والدقة",
|
||||
"Update Drawing": "تحديث اللوحة",
|
||||
"View": "مسقط"
|
||||
},
|
||||
"he": {
|
||||
"0 objects": "0 עצמים",
|
||||
"ASSEMBLY": "מכלול",
|
||||
"Add Drawing Revision": "הוספת גרסה לשרטוט",
|
||||
"Add Revision": "הוספת גרסה",
|
||||
"Add sheet (not implemented)": "הוספת גיליון (טרם מומש)",
|
||||
"Align Annotations": "יישור הערות הסבר",
|
||||
"Align Notes": "יישור הערות",
|
||||
"Align Views": "יישור מבטים",
|
||||
"Annotate": "הוספת הערות הסבר",
|
||||
"Apply (not implemented)": "החלה (טרם מומש)",
|
||||
"Auto (Theme)": "אוטומטי (ערכת נושא)",
|
||||
"Balloon": "בלון",
|
||||
"Base View": "מבט בסיס",
|
||||
"Border & template": "מסגרת ותבנית",
|
||||
"Browse Drawing Resources": "עיון במשאבי שרטוט",
|
||||
"CHANGES": "שינויים",
|
||||
"Centre Mark": "סימון מרכז",
|
||||
"Check Drawing": "בדיקת שרטוט",
|
||||
"Close drawing panel": "סגירת לוח השרטוט",
|
||||
"DRAWING ANNOTATE": "הערות הסבר בשרטוט",
|
||||
"DRAWING LAYOUT": "פריסת שרטוט",
|
||||
"DRAWING PARTS": "חלקים בשרטוט",
|
||||
"DRAWING REVIEW": "סקירת שרטוט",
|
||||
"Datum": "ייחוס",
|
||||
"Detail View": "מבט מפורט",
|
||||
"Dock at bottom": "עיגון בתחתית",
|
||||
"Dock at top": "עיגון בראש החלון",
|
||||
"Dock beneath Browser": "עיגון מתחת לדפדפן",
|
||||
"Dock left": "עיגון משמאל",
|
||||
"Dock right": "עיגון מימין",
|
||||
"Dock sheet tabs": "עיגון לשוניות הגיליונות",
|
||||
"Drafting defaults": "ברירות מחדל לשרטוט",
|
||||
"Drawing & title block": "שרטוט וטבלת כותרת",
|
||||
"Drawing Dimension": "מידת שרטוט",
|
||||
"Drawing Line": "קו שרטוט",
|
||||
"Drawing Note": "הערת שרטוט",
|
||||
"Drawing defaults (not available)": "ברירות מחדל לשרטוט (לא זמינות)",
|
||||
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.": "קובצי שרטוט ומנגנון רינדור הנייר טרם מומשו. כלים מתוכננים מציגים את האפשרויות שלהם במקומות שבהם הוגדרו.",
|
||||
"Drawing workspace": "סביבת עבודה לשרטוט",
|
||||
"Edit Drawing Table": "עריכת טבלת שרטוט",
|
||||
"Edit Table": "עריכת טבלה",
|
||||
"Export Drawing": "ייצוא שרטוט",
|
||||
"Finish the current operation before entering Drawing": "יש לסיים את הפעולה הנוכחית לפני המעבר לסביבת השרטוט",
|
||||
"Hole Callout": "ציון פרטי קדח",
|
||||
"Image": "תמונה",
|
||||
"Inherited from parent view": "בירושה ממבט האב",
|
||||
"Insert Image": "הוספת תמונה",
|
||||
"Leader": "קו הפניה",
|
||||
"Leader Note": "הערה עם קו הפניה",
|
||||
"Manage sheets (not implemented)": "ניהול גיליונות (טרם מומש)",
|
||||
"Manage sheets and docking": "ניהול גיליונות ועיגון",
|
||||
"NOTES": "הערות",
|
||||
"No drawing document": "אין מסמך שרטוט",
|
||||
"No drawing geometry available": "אין גאומטריית שרטוט זמינה",
|
||||
"No drawing objects": "אין עצמים בשרטוט",
|
||||
"No drawing sheet is available yet.": "אין עדיין גיליון שרטוט זמין.",
|
||||
"No drawing view available": "אין מבט שרטוט זמין",
|
||||
"No sheet available": "אין גיליון זמין",
|
||||
"Not available": "לא זמין",
|
||||
"Not configured": "לא מוגדר",
|
||||
"Not implemented. These options describe the planned interface; no drawing data is changed.": "טרם מומש. האפשרויות האלה מתארות את הממשק המתוכנן; נתוני השרטוט אינם משתנים.",
|
||||
"Note": "הערה",
|
||||
"Paper & layout": "נייר ופריסה",
|
||||
"Paper colour": "צבע הנייר",
|
||||
"Paper colour will be available when the sheet renderer is implemented.": "צבע הנייר יהיה זמין כאשר ימומש מנגנון רינדור הגיליון.",
|
||||
"Parent view": "מבט אב",
|
||||
"Parts & Tables": "חלקים וטבלאות",
|
||||
"Parts List": "רשימת חלקים",
|
||||
"Print": "הדפסה",
|
||||
"Print Drawing": "הדפסת שרטוט",
|
||||
"Profile & units": "פרופיל ויחידות",
|
||||
"Projected View": "מבט מוטל",
|
||||
"RESOURCES": "משאבים",
|
||||
"REVISION": "גרסה",
|
||||
"References": "הפניות",
|
||||
"Resources": "משאבים",
|
||||
"Return to Design to use this command.": "יש לחזור לסביבת התכן כדי להשתמש בפקודה זו.",
|
||||
"Review": "סקירה",
|
||||
"Revision Table": "טבלת גרסאות",
|
||||
"Revision Tbl.": "טבלת גרסאות",
|
||||
"SOURCE": "מקור",
|
||||
"SYMBOLS": "סמלים",
|
||||
"Screen appearance": "מראה המסך",
|
||||
"Section View": "מבט חתך",
|
||||
"Sheet Settings": "הגדרות גיליון",
|
||||
"Sheet unavailable": "הגיליון אינו זמין",
|
||||
"Size & orientation": "גודל וכיוון",
|
||||
"Source BOM": "רשימת חלקים של המקור",
|
||||
"Source Flat": "פריסת המקור",
|
||||
"Source design": "תכן המקור",
|
||||
"Source properties": "מאפייני המקור",
|
||||
"Space": "ריווח",
|
||||
"Space Annotations": "ריווח הערות הסבר",
|
||||
"Space Notes": "ריווח הערות",
|
||||
"TABLES": "טבלאות",
|
||||
"Text style": "סגנון טקסט",
|
||||
"Title": "כותרת",
|
||||
"Title block": "טבלת כותרת",
|
||||
"UI preview": "תצוגה מקדימה של הממשק",
|
||||
"Units & precision": "יחידות ודיוק",
|
||||
"Update Drawing": "עדכון שרטוט",
|
||||
"View": "מבט"
|
||||
},
|
||||
"vi": {
|
||||
"0 objects": "0 đối tượng",
|
||||
"ASSEMBLY": "CỤM LẮP RÁP",
|
||||
"Add Drawing Revision": "Thêm lần sửa đổi bản vẽ",
|
||||
"Add Revision": "Thêm lần sửa đổi",
|
||||
"Add sheet (not implemented)": "Thêm tờ (chưa triển khai)",
|
||||
"Align Annotations": "Căn chỉnh chú thích",
|
||||
"Align Notes": "Căn chỉnh ghi chú",
|
||||
"Align Views": "Căn chỉnh hình chiếu",
|
||||
"Annotate": "Thêm chú thích",
|
||||
"Apply (not implemented)": "Áp dụng (chưa triển khai)",
|
||||
"Auto (Theme)": "Tự động (theo giao diện)",
|
||||
"Balloon": "Bóng chú thích",
|
||||
"Base View": "Hình chiếu cơ sở",
|
||||
"Border & template": "Khung viền và mẫu",
|
||||
"Browse Drawing Resources": "Duyệt tài nguyên bản vẽ",
|
||||
"CHANGES": "THAY ĐỔI",
|
||||
"Centre Mark": "Dấu tâm",
|
||||
"Check Drawing": "Kiểm tra bản vẽ",
|
||||
"Close drawing panel": "Đóng bảng bản vẽ",
|
||||
"DRAWING ANNOTATE": "CHÚ THÍCH BẢN VẼ",
|
||||
"DRAWING LAYOUT": "BỐ CỤC BẢN VẼ",
|
||||
"DRAWING PARTS": "CHI TIẾT BẢN VẼ",
|
||||
"DRAWING REVIEW": "RÀ SOÁT BẢN VẼ",
|
||||
"Datum": "Chuẩn",
|
||||
"Detail View": "Hình chiếu chi tiết",
|
||||
"Dock at bottom": "Gắn ở cạnh dưới",
|
||||
"Dock at top": "Gắn ở cạnh trên",
|
||||
"Dock beneath Browser": "Gắn bên dưới cây thư mục",
|
||||
"Dock left": "Gắn bên trái",
|
||||
"Dock right": "Gắn bên phải",
|
||||
"Dock sheet tabs": "Gắn các thẻ tờ bản vẽ",
|
||||
"Drafting defaults": "Thiết lập mặc định khi lập bản vẽ",
|
||||
"Drawing & title block": "Bản vẽ và khung tên",
|
||||
"Drawing Dimension": "Kích thước bản vẽ",
|
||||
"Drawing Line": "Đường bản vẽ",
|
||||
"Drawing Note": "Ghi chú bản vẽ",
|
||||
"Drawing defaults (not available)": "Thiết lập mặc định của bản vẽ (chưa có)",
|
||||
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.": "Tệp bản vẽ và bộ kết xuất trang giấy chưa được triển khai. Các công cụ dự kiến hiển thị tùy chọn ở những nơi đã được định nghĩa.",
|
||||
"Drawing workspace": "Không gian làm việc bản vẽ",
|
||||
"Edit Drawing Table": "Sửa bảng trong bản vẽ",
|
||||
"Edit Table": "Sửa bảng",
|
||||
"Export Drawing": "Xuất bản vẽ",
|
||||
"Finish the current operation before entering Drawing": "Hoàn tất thao tác hiện tại trước khi chuyển sang không gian Bản vẽ",
|
||||
"Hole Callout": "Chú dẫn lỗ",
|
||||
"Image": "Hình ảnh",
|
||||
"Inherited from parent view": "Kế thừa từ hình chiếu gốc",
|
||||
"Insert Image": "Chèn hình ảnh",
|
||||
"Leader": "Đường dẫn chú thích",
|
||||
"Leader Note": "Ghi chú có đường dẫn",
|
||||
"Manage sheets (not implemented)": "Quản lý tờ (chưa triển khai)",
|
||||
"Manage sheets and docking": "Quản lý tờ và vị trí gắn",
|
||||
"NOTES": "GHI CHÚ",
|
||||
"No drawing document": "Không có tài liệu bản vẽ",
|
||||
"No drawing geometry available": "Chưa có hình học bản vẽ",
|
||||
"No drawing objects": "Không có đối tượng bản vẽ",
|
||||
"No drawing sheet is available yet.": "Chưa có tờ bản vẽ nào.",
|
||||
"No drawing view available": "Chưa có hình chiếu bản vẽ",
|
||||
"No sheet available": "Chưa có tờ",
|
||||
"Not available": "Chưa có",
|
||||
"Not configured": "Chưa cấu hình",
|
||||
"Not implemented. These options describe the planned interface; no drawing data is changed.": "Chưa triển khai. Các tùy chọn này mô tả giao diện dự kiến; không có dữ liệu bản vẽ nào bị thay đổi.",
|
||||
"Note": "Ghi chú",
|
||||
"Paper & layout": "Giấy và bố cục",
|
||||
"Paper colour": "Màu giấy",
|
||||
"Paper colour will be available when the sheet renderer is implemented.": "Có thể chọn màu giấy khi bộ kết xuất tờ bản vẽ được triển khai.",
|
||||
"Parent view": "Hình chiếu gốc",
|
||||
"Parts & Tables": "Chi tiết và bảng",
|
||||
"Parts List": "Danh sách chi tiết",
|
||||
"Print": "In",
|
||||
"Print Drawing": "In bản vẽ",
|
||||
"Profile & units": "Hồ sơ và đơn vị",
|
||||
"Projected View": "Hình chiếu dẫn xuất",
|
||||
"RESOURCES": "TÀI NGUYÊN",
|
||||
"REVISION": "LẦN SỬA ĐỔI",
|
||||
"References": "Tham chiếu",
|
||||
"Resources": "Tài nguyên",
|
||||
"Return to Design to use this command.": "Quay lại không gian Thiết kế để dùng lệnh này.",
|
||||
"Review": "Rà soát",
|
||||
"Revision Table": "Bảng sửa đổi",
|
||||
"Revision Tbl.": "Bảng sửa đổi",
|
||||
"SOURCE": "NGUỒN",
|
||||
"SYMBOLS": "KÝ HIỆU",
|
||||
"Screen appearance": "Diện mạo trên màn hình",
|
||||
"Section View": "Hình cắt",
|
||||
"Sheet Settings": "Cài đặt tờ",
|
||||
"Sheet unavailable": "Tờ không khả dụng",
|
||||
"Size & orientation": "Kích thước và hướng",
|
||||
"Source BOM": "Bảng kê vật tư nguồn",
|
||||
"Source Flat": "Bản trải phẳng nguồn",
|
||||
"Source design": "Thiết kế nguồn",
|
||||
"Source properties": "Thuộc tính nguồn",
|
||||
"Space": "Giãn cách",
|
||||
"Space Annotations": "Giãn cách chú thích",
|
||||
"Space Notes": "Giãn cách ghi chú",
|
||||
"TABLES": "BẢNG",
|
||||
"Text style": "Kiểu chữ",
|
||||
"Title": "Tiêu đề",
|
||||
"Title block": "Khung tên",
|
||||
"UI preview": "Xem trước giao diện",
|
||||
"Units & precision": "Đơn vị và độ chính xác",
|
||||
"Update Drawing": "Cập nhật bản vẽ",
|
||||
"View": "Hình chiếu"
|
||||
},
|
||||
"en": {
|
||||
"0 objects": "0 objects",
|
||||
"ASSEMBLY": "ASSEMBLY",
|
||||
"Add Drawing Revision": "Add Drawing Revision",
|
||||
"Add Revision": "Add Revision",
|
||||
"Add sheet (not implemented)": "Add sheet (not implemented)",
|
||||
"Align Annotations": "Align Annotations",
|
||||
"Align Notes": "Align Notes",
|
||||
"Align Views": "Align Views",
|
||||
"Annotate": "Annotate",
|
||||
"Apply (not implemented)": "Apply (not implemented)",
|
||||
"Auto (Theme)": "Auto (Theme)",
|
||||
"Balloon": "Balloon",
|
||||
"Base View": "Base View",
|
||||
"Border & template": "Border & template",
|
||||
"Browse Drawing Resources": "Browse Drawing Resources",
|
||||
"CHANGES": "CHANGES",
|
||||
"Centre Mark": "Centre Mark",
|
||||
"Check Drawing": "Check Drawing",
|
||||
"Close drawing panel": "Close drawing panel",
|
||||
"DRAWING ANNOTATE": "DRAWING ANNOTATE",
|
||||
"DRAWING LAYOUT": "DRAWING LAYOUT",
|
||||
"DRAWING PARTS": "DRAWING PARTS",
|
||||
"DRAWING REVIEW": "DRAWING REVIEW",
|
||||
"Datum": "Datum",
|
||||
"Detail View": "Detail View",
|
||||
"Dock at bottom": "Dock at bottom",
|
||||
"Dock at top": "Dock at top",
|
||||
"Dock beneath Browser": "Dock beneath Browser",
|
||||
"Dock left": "Dock left",
|
||||
"Dock right": "Dock right",
|
||||
"Dock sheet tabs": "Dock sheet tabs",
|
||||
"Drafting defaults": "Drafting defaults",
|
||||
"Drawing & title block": "Drawing & title block",
|
||||
"Drawing Dimension": "Drawing Dimension",
|
||||
"Drawing Line": "Drawing Line",
|
||||
"Drawing Note": "Drawing Note",
|
||||
"Drawing defaults (not available)": "Drawing defaults (not available)",
|
||||
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.": "Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.",
|
||||
"Drawing workspace": "Drawing workspace",
|
||||
"Edit Drawing Table": "Edit Drawing Table",
|
||||
"Edit Table": "Edit Table",
|
||||
"Export Drawing": "Export Drawing",
|
||||
"Finish the current operation before entering Drawing": "Finish the current operation before entering Drawing",
|
||||
"Hole Callout": "Hole Callout",
|
||||
"Image": "Image",
|
||||
"Inherited from parent view": "Inherited from parent view",
|
||||
"Insert Image": "Insert Image",
|
||||
"Leader": "Leader",
|
||||
"Leader Note": "Leader Note",
|
||||
"Manage sheets (not implemented)": "Manage sheets (not implemented)",
|
||||
"Manage sheets and docking": "Manage sheets and docking",
|
||||
"NOTES": "NOTES",
|
||||
"No drawing document": "No drawing document",
|
||||
"No drawing geometry available": "No drawing geometry available",
|
||||
"No drawing objects": "No drawing objects",
|
||||
"No drawing sheet is available yet.": "No drawing sheet is available yet.",
|
||||
"No drawing view available": "No drawing view available",
|
||||
"No sheet available": "No sheet available",
|
||||
"Not available": "Not available",
|
||||
"Not configured": "Not configured",
|
||||
"Not implemented. These options describe the planned interface; no drawing data is changed.": "Not implemented. These options describe the planned interface; no drawing data is changed.",
|
||||
"Note": "Note",
|
||||
"Paper & layout": "Paper & layout",
|
||||
"Paper colour": "Paper colour",
|
||||
"Paper colour will be available when the sheet renderer is implemented.": "Paper colour will be available when the sheet renderer is implemented.",
|
||||
"Parent view": "Parent view",
|
||||
"Parts & Tables": "Parts & Tables",
|
||||
"Parts List": "Parts List",
|
||||
"Print": "Print",
|
||||
"Print Drawing": "Print Drawing",
|
||||
"Profile & units": "Profile & units",
|
||||
"Projected View": "Projected View",
|
||||
"RESOURCES": "RESOURCES",
|
||||
"REVISION": "REVISION",
|
||||
"References": "References",
|
||||
"Resources": "Resources",
|
||||
"Return to Design to use this command.": "Return to Design to use this command.",
|
||||
"Review": "Review",
|
||||
"Revision Table": "Revision Table",
|
||||
"Revision Tbl.": "Revision Tbl.",
|
||||
"SOURCE": "SOURCE",
|
||||
"SYMBOLS": "SYMBOLS",
|
||||
"Screen appearance": "Screen appearance",
|
||||
"Section View": "Section View",
|
||||
"Sheet Settings": "Sheet Settings",
|
||||
"Sheet unavailable": "Sheet unavailable",
|
||||
"Size & orientation": "Size & orientation",
|
||||
"Source BOM": "Source BOM",
|
||||
"Source Flat": "Source Flat",
|
||||
"Source design": "Source design",
|
||||
"Source properties": "Source properties",
|
||||
"Space": "Space",
|
||||
"Space Annotations": "Space Annotations",
|
||||
"Space Notes": "Space Notes",
|
||||
"TABLES": "TABLES",
|
||||
"Text style": "Text style",
|
||||
"Title": "Title",
|
||||
"Title block": "Title block",
|
||||
"UI preview": "UI preview",
|
||||
"Units & precision": "Units & precision",
|
||||
"Update Drawing": "Update Drawing",
|
||||
"View": "View"
|
||||
},
|
||||
"ja": {
|
||||
"0 objects": "0個のオブジェクト",
|
||||
"ASSEMBLY": "アセンブリ",
|
||||
"Add Drawing Revision": "図面の改訂を追加",
|
||||
"Add Revision": "改訂を追加",
|
||||
"Add sheet (not implemented)": "シートを追加(未実装)",
|
||||
"Align Annotations": "注釈を整列",
|
||||
"Align Notes": "注記を整列",
|
||||
"Align Views": "ビューを整列",
|
||||
"Annotate": "注釈",
|
||||
"Apply (not implemented)": "適用(未実装)",
|
||||
"Auto (Theme)": "自動(テーマ)",
|
||||
"Balloon": "バルーン",
|
||||
"Base View": "基準ビュー",
|
||||
"Border & template": "図枠とテンプレート",
|
||||
"Browse Drawing Resources": "図面リソースを参照",
|
||||
"CHANGES": "変更",
|
||||
"Centre Mark": "中心マーク",
|
||||
"Check Drawing": "図面をチェック",
|
||||
"Close drawing panel": "図面パネルを閉じる",
|
||||
"DRAWING ANNOTATE": "図面の注釈",
|
||||
"DRAWING LAYOUT": "図面のレイアウト",
|
||||
"DRAWING PARTS": "図面の部品",
|
||||
"DRAWING REVIEW": "図面の確認",
|
||||
"Datum": "データム",
|
||||
"Detail View": "詳細ビュー",
|
||||
"Dock at bottom": "下部にドッキング",
|
||||
"Dock at top": "上部にドッキング",
|
||||
"Dock beneath Browser": "ブラウザーの下にドッキング",
|
||||
"Dock left": "左にドッキング",
|
||||
"Dock right": "右にドッキング",
|
||||
"Dock sheet tabs": "シートタブをドッキング",
|
||||
"Drafting defaults": "製図の既定値",
|
||||
"Drawing & title block": "図面と表題欄",
|
||||
"Drawing Dimension": "図面寸法",
|
||||
"Drawing Line": "図面の線分",
|
||||
"Drawing Note": "図面の注記",
|
||||
"Drawing defaults (not available)": "図面の既定値(利用不可)",
|
||||
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.": "図面ファイルと用紙レンダラーは未実装です。予定されているツールでは、設定項目が定義済みの場合に表示されます。",
|
||||
"Drawing workspace": "図面ワークスペース",
|
||||
"Edit Drawing Table": "図面の表を編集",
|
||||
"Edit Table": "表を編集",
|
||||
"Export Drawing": "図面をエクスポート",
|
||||
"Finish the current operation before entering Drawing": "現在の操作を完了してから図面ワークスペースに移動してください",
|
||||
"Hole Callout": "穴注記",
|
||||
"Image": "画像",
|
||||
"Inherited from parent view": "親ビューから継承",
|
||||
"Insert Image": "画像を挿入",
|
||||
"Leader": "引出線",
|
||||
"Leader Note": "引出線付き注記",
|
||||
"Manage sheets (not implemented)": "シートを管理(未実装)",
|
||||
"Manage sheets and docking": "シートとドッキングを管理",
|
||||
"NOTES": "注記",
|
||||
"No drawing document": "図面ドキュメントがありません",
|
||||
"No drawing geometry available": "利用可能な図面ジオメトリがありません",
|
||||
"No drawing objects": "図面オブジェクトがありません",
|
||||
"No drawing sheet is available yet.": "図面シートはまだ利用できません。",
|
||||
"No drawing view available": "利用可能な図面ビューがありません",
|
||||
"No sheet available": "利用可能なシートがありません",
|
||||
"Not available": "利用不可",
|
||||
"Not configured": "未設定",
|
||||
"Not implemented. These options describe the planned interface; no drawing data is changed.": "未実装です。これらの設定項目は予定されているインターフェースを示すもので、図面データは変更されません。",
|
||||
"Note": "注記",
|
||||
"Paper & layout": "用紙とレイアウト",
|
||||
"Paper colour": "用紙の色",
|
||||
"Paper colour will be available when the sheet renderer is implemented.": "用紙の色はシートレンダラーの実装後に設定できます。",
|
||||
"Parent view": "親ビュー",
|
||||
"Parts & Tables": "部品と表",
|
||||
"Parts List": "部品表",
|
||||
"Print": "印刷",
|
||||
"Print Drawing": "図面を印刷",
|
||||
"Profile & units": "製図規格と単位",
|
||||
"Projected View": "投影ビュー",
|
||||
"RESOURCES": "リソース",
|
||||
"REVISION": "改訂",
|
||||
"References": "参照",
|
||||
"Resources": "リソース",
|
||||
"Return to Design to use this command.": "このコマンドを使用するには、デザインワークスペースに戻ってください。",
|
||||
"Review": "確認",
|
||||
"Revision Table": "改訂表",
|
||||
"Revision Tbl.": "改訂表",
|
||||
"SOURCE": "参照元",
|
||||
"SYMBOLS": "記号",
|
||||
"Screen appearance": "画面表示",
|
||||
"Section View": "断面ビュー",
|
||||
"Sheet Settings": "シート設定",
|
||||
"Sheet unavailable": "シートを利用できません",
|
||||
"Size & orientation": "サイズと向き",
|
||||
"Source BOM": "参照元の部品表",
|
||||
"Source Flat": "参照元の展開図",
|
||||
"Source design": "参照元のデザイン",
|
||||
"Source properties": "参照元のプロパティ",
|
||||
"Space": "等間隔",
|
||||
"Space Annotations": "注釈を等間隔に",
|
||||
"Space Notes": "注記を等間隔に",
|
||||
"TABLES": "表",
|
||||
"Text style": "文字スタイル",
|
||||
"Title": "タイトル",
|
||||
"Title block": "表題欄",
|
||||
"UI preview": "UIプレビュー",
|
||||
"Units & precision": "単位と精度",
|
||||
"Update Drawing": "図面を更新",
|
||||
"View": "ビュー"
|
||||
},
|
||||
"zh": {
|
||||
"0 objects": "0 个对象",
|
||||
"ASSEMBLY": "装配",
|
||||
"Add Drawing Revision": "添加图纸修订",
|
||||
"Add Revision": "添加修订",
|
||||
"Add sheet (not implemented)": "添加图纸页(未实现)",
|
||||
"Align Annotations": "对齐标注",
|
||||
"Align Notes": "对齐注释",
|
||||
"Align Views": "对齐视图",
|
||||
"Annotate": "标注",
|
||||
"Apply (not implemented)": "应用(未实现)",
|
||||
"Auto (Theme)": "自动(跟随主题)",
|
||||
"Balloon": "零件序号",
|
||||
"Base View": "基本视图",
|
||||
"Border & template": "图框与模板",
|
||||
"Browse Drawing Resources": "浏览制图资源",
|
||||
"CHANGES": "更改",
|
||||
"Centre Mark": "中心标记",
|
||||
"Check Drawing": "检查图纸",
|
||||
"Close drawing panel": "关闭制图面板",
|
||||
"DRAWING ANNOTATE": "图纸标注",
|
||||
"DRAWING LAYOUT": "图纸布局",
|
||||
"DRAWING PARTS": "图纸零件",
|
||||
"DRAWING REVIEW": "图纸审阅",
|
||||
"Datum": "基准",
|
||||
"Detail View": "局部放大视图",
|
||||
"Dock at bottom": "停靠到底部",
|
||||
"Dock at top": "停靠到顶部",
|
||||
"Dock beneath Browser": "停靠到浏览器下方",
|
||||
"Dock left": "停靠到左侧",
|
||||
"Dock right": "停靠到右侧",
|
||||
"Dock sheet tabs": "停靠图纸页选项卡",
|
||||
"Drafting defaults": "制图默认设置",
|
||||
"Drawing & title block": "图纸与标题栏",
|
||||
"Drawing Dimension": "图纸尺寸标注",
|
||||
"Drawing Line": "图纸线段",
|
||||
"Drawing Note": "图纸注释",
|
||||
"Drawing defaults (not available)": "图纸默认设置(不可用)",
|
||||
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.": "图纸文件和纸张渲染器尚未实现。规划中的工具会显示已定义的选项。",
|
||||
"Drawing workspace": "工程图工作区",
|
||||
"Edit Drawing Table": "编辑图纸表格",
|
||||
"Edit Table": "编辑表格",
|
||||
"Export Drawing": "导出图纸",
|
||||
"Finish the current operation before entering Drawing": "请先完成当前操作,再进入工程图工作区",
|
||||
"Hole Callout": "孔标注",
|
||||
"Image": "图像",
|
||||
"Inherited from parent view": "继承父视图",
|
||||
"Insert Image": "插入图像",
|
||||
"Leader": "引线",
|
||||
"Leader Note": "引线注释",
|
||||
"Manage sheets (not implemented)": "管理图纸页(未实现)",
|
||||
"Manage sheets and docking": "管理图纸页与停靠位置",
|
||||
"NOTES": "注释",
|
||||
"No drawing document": "没有图纸文档",
|
||||
"No drawing geometry available": "没有可用的图纸几何",
|
||||
"No drawing objects": "没有图纸对象",
|
||||
"No drawing sheet is available yet.": "目前尚无可用的图纸页。",
|
||||
"No drawing view available": "没有可用的图纸视图",
|
||||
"No sheet available": "没有可用的图纸页",
|
||||
"Not available": "不可用",
|
||||
"Not configured": "未配置",
|
||||
"Not implemented. These options describe the planned interface; no drawing data is changed.": "尚未实现。这些选项展示规划中的界面,不会更改任何图纸数据。",
|
||||
"Note": "注释",
|
||||
"Paper & layout": "纸张与布局",
|
||||
"Paper colour": "纸张颜色",
|
||||
"Paper colour will be available when the sheet renderer is implemented.": "图纸页渲染器实现后即可设置纸张颜色。",
|
||||
"Parent view": "父视图",
|
||||
"Parts & Tables": "零件与表格",
|
||||
"Parts List": "零件明细表",
|
||||
"Print": "打印",
|
||||
"Print Drawing": "打印图纸",
|
||||
"Profile & units": "制图规范与单位",
|
||||
"Projected View": "投影视图",
|
||||
"RESOURCES": "资源",
|
||||
"REVISION": "修订",
|
||||
"References": "引用",
|
||||
"Resources": "资源",
|
||||
"Return to Design to use this command.": "请返回设计工作区使用此命令。",
|
||||
"Review": "审阅",
|
||||
"Revision Table": "修订表",
|
||||
"Revision Tbl.": "修订表",
|
||||
"SOURCE": "源设计",
|
||||
"SYMBOLS": "符号",
|
||||
"Screen appearance": "屏幕外观",
|
||||
"Section View": "剖视图",
|
||||
"Sheet Settings": "图纸页设置",
|
||||
"Sheet unavailable": "图纸页不可用",
|
||||
"Size & orientation": "尺寸与方向",
|
||||
"Source BOM": "源设计 BOM",
|
||||
"Source Flat": "源设计展开图",
|
||||
"Source design": "源设计",
|
||||
"Source properties": "源设计属性",
|
||||
"Space": "均布",
|
||||
"Space Annotations": "等间距分布标注",
|
||||
"Space Notes": "等间距分布注释",
|
||||
"TABLES": "表格",
|
||||
"Text style": "文字样式",
|
||||
"Title": "标题",
|
||||
"Title block": "标题栏",
|
||||
"UI preview": "界面预览",
|
||||
"Units & precision": "单位与精度",
|
||||
"Update Drawing": "更新图纸",
|
||||
"View": "视图"
|
||||
},
|
||||
"de": {
|
||||
"0 objects": "0 Objekte",
|
||||
"ASSEMBLY": "BAUGRUPPE",
|
||||
"Add Drawing Revision": "Zeichnungsrevision hinzufügen",
|
||||
"Add Revision": "Revision hinzufügen",
|
||||
"Add sheet (not implemented)": "Blatt hinzufügen (nicht implementiert)",
|
||||
"Align Annotations": "Beschriftungen ausrichten",
|
||||
"Align Notes": "Hinweise ausrichten",
|
||||
"Align Views": "Ansichten ausrichten",
|
||||
"Annotate": "Beschriften",
|
||||
"Apply (not implemented)": "Anwenden (nicht implementiert)",
|
||||
"Auto (Theme)": "Automatisch (Design)",
|
||||
"Balloon": "Positionssymbol",
|
||||
"Base View": "Basisansicht",
|
||||
"Border & template": "Rahmen & Vorlage",
|
||||
"Browse Drawing Resources": "Zeichnungsressourcen durchsuchen",
|
||||
"CHANGES": "ÄNDERUNGEN",
|
||||
"Centre Mark": "Mittelpunktmarkierung",
|
||||
"Check Drawing": "Zeichnung prüfen",
|
||||
"Close drawing panel": "Zeichnungspanel schließen",
|
||||
"DRAWING ANNOTATE": "ZEICHNUNG BESCHRIFTEN",
|
||||
"DRAWING LAYOUT": "ZEICHNUNGSLAYOUT",
|
||||
"DRAWING PARTS": "ZEICHNUNGSTEILE",
|
||||
"DRAWING REVIEW": "ZEICHNUNGSPRÜFUNG",
|
||||
"Datum": "Bezug",
|
||||
"Detail View": "Detailansicht",
|
||||
"Dock at bottom": "Unten andocken",
|
||||
"Dock at top": "Oben andocken",
|
||||
"Dock beneath Browser": "Unter dem Browser andocken",
|
||||
"Dock left": "Links andocken",
|
||||
"Dock right": "Rechts andocken",
|
||||
"Dock sheet tabs": "Blattregister andocken",
|
||||
"Drafting defaults": "Vorgaben für die Zeichnungserstellung",
|
||||
"Drawing & title block": "Zeichnung & Schriftfeld",
|
||||
"Drawing Dimension": "Zeichnungsbemaßung",
|
||||
"Drawing Line": "Zeichnungslinie",
|
||||
"Drawing Note": "Zeichnungshinweis",
|
||||
"Drawing defaults (not available)": "Zeichnungsvorgaben (nicht verfügbar)",
|
||||
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.": "Zeichnungsdateien und die Papierdarstellung sind noch nicht implementiert. Geplante Werkzeuge zeigen ihre Optionen an, sofern diese bereits festgelegt sind.",
|
||||
"Drawing workspace": "Zeichnungsarbeitsbereich",
|
||||
"Edit Drawing Table": "Zeichnungstabelle bearbeiten",
|
||||
"Edit Table": "Tabelle bearbeiten",
|
||||
"Export Drawing": "Zeichnung exportieren",
|
||||
"Finish the current operation before entering Drawing": "Beenden Sie den aktuellen Vorgang, bevor Sie zum Zeichnungsarbeitsbereich wechseln",
|
||||
"Hole Callout": "Bohrungsangabe",
|
||||
"Image": "Bild",
|
||||
"Inherited from parent view": "Von der übergeordneten Ansicht übernommen",
|
||||
"Insert Image": "Bild einfügen",
|
||||
"Leader": "Bezugslinie",
|
||||
"Leader Note": "Hinweis mit Bezugslinie",
|
||||
"Manage sheets (not implemented)": "Blätter verwalten (nicht implementiert)",
|
||||
"Manage sheets and docking": "Blätter und Andockpositionen verwalten",
|
||||
"NOTES": "HINWEISE",
|
||||
"No drawing document": "Kein Zeichnungsdokument",
|
||||
"No drawing geometry available": "Keine Zeichnungsgeometrie verfügbar",
|
||||
"No drawing objects": "Keine Zeichnungsobjekte",
|
||||
"No drawing sheet is available yet.": "Es ist noch kein Zeichnungsblatt verfügbar.",
|
||||
"No drawing view available": "Keine Zeichnungsansicht verfügbar",
|
||||
"No sheet available": "Kein Blatt verfügbar",
|
||||
"Not available": "Nicht verfügbar",
|
||||
"Not configured": "Nicht konfiguriert",
|
||||
"Not implemented. These options describe the planned interface; no drawing data is changed.": "Nicht implementiert. Diese Optionen beschreiben die geplante Oberfläche; es werden keine Zeichnungsdaten geändert.",
|
||||
"Note": "Hinweis",
|
||||
"Paper & layout": "Papier & Layout",
|
||||
"Paper colour": "Papierfarbe",
|
||||
"Paper colour will be available when the sheet renderer is implemented.": "Die Papierfarbe wird verfügbar sein, sobald die Blattdarstellung implementiert ist.",
|
||||
"Parent view": "Übergeordnete Ansicht",
|
||||
"Parts & Tables": "Teile & Tabellen",
|
||||
"Parts List": "Teileliste",
|
||||
"Print": "Drucken",
|
||||
"Print Drawing": "Zeichnung drucken",
|
||||
"Profile & units": "Profil & Einheiten",
|
||||
"Projected View": "Projizierte Ansicht",
|
||||
"RESOURCES": "RESSOURCEN",
|
||||
"REVISION": "REVISION",
|
||||
"References": "Referenzen",
|
||||
"Resources": "Ressourcen",
|
||||
"Return to Design to use this command.": "Wechseln Sie zum Design-Arbeitsbereich zurück, um diesen Befehl zu verwenden.",
|
||||
"Review": "Prüfen",
|
||||
"Revision Table": "Revisionstabelle",
|
||||
"Revision Tbl.": "Revisionstbl.",
|
||||
"SOURCE": "QUELLE",
|
||||
"SYMBOLS": "SYMBOLE",
|
||||
"Screen appearance": "Bildschirmdarstellung",
|
||||
"Section View": "Schnittansicht",
|
||||
"Sheet Settings": "Blatteinstellungen",
|
||||
"Sheet unavailable": "Blatt nicht verfügbar",
|
||||
"Size & orientation": "Größe & Ausrichtung",
|
||||
"Source BOM": "Stückliste der Quelle",
|
||||
"Source Flat": "Flachmuster der Quelle",
|
||||
"Source design": "Quelldesign",
|
||||
"Source properties": "Eigenschaften der Quelle",
|
||||
"Space": "Abstände anpassen",
|
||||
"Space Annotations": "Beschriftungsabstände anpassen",
|
||||
"Space Notes": "Hinweisabstände anpassen",
|
||||
"TABLES": "TABELLEN",
|
||||
"Text style": "Textstil",
|
||||
"Title": "Titel",
|
||||
"Title block": "Schriftfeld",
|
||||
"UI preview": "Oberflächenvorschau",
|
||||
"Units & precision": "Einheiten & Genauigkeit",
|
||||
"Update Drawing": "Zeichnung aktualisieren",
|
||||
"View": "Ansicht"
|
||||
},
|
||||
"ko": {
|
||||
"0 objects": "객체 0개",
|
||||
"ASSEMBLY": "조립품",
|
||||
"Add Drawing Revision": "도면 리비전 추가",
|
||||
"Add Revision": "리비전 추가",
|
||||
"Add sheet (not implemented)": "시트 추가 (미구현)",
|
||||
"Align Annotations": "주석 정렬",
|
||||
"Align Notes": "메모 정렬",
|
||||
"Align Views": "뷰 정렬",
|
||||
"Annotate": "주석 달기",
|
||||
"Apply (not implemented)": "적용 (미구현)",
|
||||
"Auto (Theme)": "자동 (테마)",
|
||||
"Balloon": "항목 번호 기호",
|
||||
"Base View": "기본 뷰",
|
||||
"Border & template": "테두리 및 템플릿",
|
||||
"Browse Drawing Resources": "도면 리소스 찾아보기",
|
||||
"CHANGES": "변경 사항",
|
||||
"Centre Mark": "중심 표시",
|
||||
"Check Drawing": "도면 검사",
|
||||
"Close drawing panel": "도면 패널 닫기",
|
||||
"DRAWING ANNOTATE": "도면 주석",
|
||||
"DRAWING LAYOUT": "도면 배치",
|
||||
"DRAWING PARTS": "도면 부품",
|
||||
"DRAWING REVIEW": "도면 검토",
|
||||
"Datum": "기준",
|
||||
"Detail View": "상세 뷰",
|
||||
"Dock at bottom": "아래쪽에 고정",
|
||||
"Dock at top": "위쪽에 고정",
|
||||
"Dock beneath Browser": "브라우저 아래에 고정",
|
||||
"Dock left": "왼쪽에 고정",
|
||||
"Dock right": "오른쪽에 고정",
|
||||
"Dock sheet tabs": "시트 탭 고정",
|
||||
"Drafting defaults": "도면 작성 기본 설정",
|
||||
"Drawing & title block": "도면 및 표제란",
|
||||
"Drawing Dimension": "도면 치수",
|
||||
"Drawing Line": "도면 선",
|
||||
"Drawing Note": "도면 메모",
|
||||
"Drawing defaults (not available)": "도면 기본 설정 (사용 불가)",
|
||||
"Drawing files and the paper renderer are not implemented. Planned tools show their options where defined.": "도면 파일과 용지 렌더러는 아직 구현되지 않았습니다. 예정된 도구는 정의된 옵션이 있는 경우 해당 옵션을 표시합니다.",
|
||||
"Drawing workspace": "도면 작업 공간",
|
||||
"Edit Drawing Table": "도면 표 편집",
|
||||
"Edit Table": "표 편집",
|
||||
"Export Drawing": "도면 내보내기",
|
||||
"Finish the current operation before entering Drawing": "도면 작업 공간으로 전환하기 전에 현재 작업을 완료하세요",
|
||||
"Hole Callout": "구멍 주석",
|
||||
"Image": "이미지",
|
||||
"Inherited from parent view": "상위 뷰에서 상속됨",
|
||||
"Insert Image": "이미지 삽입",
|
||||
"Leader": "지시선",
|
||||
"Leader Note": "지시선 메모",
|
||||
"Manage sheets (not implemented)": "시트 관리 (미구현)",
|
||||
"Manage sheets and docking": "시트 및 고정 위치 관리",
|
||||
"NOTES": "메모",
|
||||
"No drawing document": "도면 문서 없음",
|
||||
"No drawing geometry available": "사용 가능한 도면 형상 없음",
|
||||
"No drawing objects": "도면 객체 없음",
|
||||
"No drawing sheet is available yet.": "아직 사용 가능한 도면 시트가 없습니다.",
|
||||
"No drawing view available": "사용 가능한 도면 뷰 없음",
|
||||
"No sheet available": "사용 가능한 시트 없음",
|
||||
"Not available": "사용 불가",
|
||||
"Not configured": "설정되지 않음",
|
||||
"Not implemented. These options describe the planned interface; no drawing data is changed.": "아직 구현되지 않았습니다. 이 옵션은 예정된 인터페이스를 설명하며 도면 데이터를 변경하지 않습니다.",
|
||||
"Note": "메모",
|
||||
"Paper & layout": "용지 및 배치",
|
||||
"Paper colour": "용지 색상",
|
||||
"Paper colour will be available when the sheet renderer is implemented.": "시트 렌더러가 구현되면 용지 색상을 설정할 수 있습니다.",
|
||||
"Parent view": "상위 뷰",
|
||||
"Parts & Tables": "부품 및 표",
|
||||
"Parts List": "부품 목록",
|
||||
"Print": "인쇄",
|
||||
"Print Drawing": "도면 인쇄",
|
||||
"Profile & units": "프로파일 및 단위",
|
||||
"Projected View": "투영 뷰",
|
||||
"RESOURCES": "리소스",
|
||||
"REVISION": "리비전",
|
||||
"References": "참조",
|
||||
"Resources": "리소스",
|
||||
"Return to Design to use this command.": "이 명령을 사용하려면 디자인 작업 공간으로 돌아가세요.",
|
||||
"Review": "검토",
|
||||
"Revision Table": "리비전 표",
|
||||
"Revision Tbl.": "리비전 표",
|
||||
"SOURCE": "원본",
|
||||
"SYMBOLS": "기호",
|
||||
"Screen appearance": "화면 모양",
|
||||
"Section View": "단면 뷰",
|
||||
"Sheet Settings": "시트 설정",
|
||||
"Sheet unavailable": "시트 사용 불가",
|
||||
"Size & orientation": "크기 및 방향",
|
||||
"Source BOM": "원본 자재 명세서",
|
||||
"Source Flat": "원본 전개도",
|
||||
"Source design": "원본 디자인",
|
||||
"Source properties": "원본 속성",
|
||||
"Space": "간격 조정",
|
||||
"Space Annotations": "주석 간격 조정",
|
||||
"Space Notes": "메모 간격 조정",
|
||||
"TABLES": "표",
|
||||
"Text style": "텍스트 스타일",
|
||||
"Title": "제목",
|
||||
"Title block": "표제란",
|
||||
"UI preview": "인터페이스 미리보기",
|
||||
"Units & precision": "단위 및 정밀도",
|
||||
"Update Drawing": "도면 업데이트",
|
||||
"View": "뷰"
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"ar": {
|
||||
"cmd.assembly_bom_item_set": "تغيير رقم البند",
|
||||
"cmd.assembly_bom_renumber": "إعادة ترقيم البنود",
|
||||
"cmd.drawing_balloon_add": "إضافة بالون",
|
||||
"cmd.drawing_balloon_edit": "تعديل البالون",
|
||||
"cmd.drawing_balloon_inspect": "فحص البالون",
|
||||
"cmd.drawing_close": "إغلاق اللوحة",
|
||||
"cmd.drawing_create": "إنشاء لوحة",
|
||||
"cmd.drawing_delete": "حذف اللوحة",
|
||||
"cmd.drawing_dimension_add": "إضافة بُعد إلى اللوحة",
|
||||
"cmd.drawing_dimension_edit": "تعديل بُعد اللوحة",
|
||||
"cmd.drawing_dimension_inspect": "فحص بُعد اللوحة",
|
||||
"cmd.drawing_export": "تصدير الورقة",
|
||||
"cmd.drawing_inspect": "فحص اللوحة",
|
||||
"cmd.drawing_list": "سرد اللوحات",
|
||||
"cmd.drawing_open": "فتح اللوحة",
|
||||
"cmd.drawing_properties": "خصائص اللوحة",
|
||||
"cmd.drawing_properties_remove": "إزالة خاصية اللوحة",
|
||||
"cmd.drawing_properties_set": "تغيير خصائص اللوحة",
|
||||
"cmd.drawing_revision_add": "إضافة مراجعة",
|
||||
"cmd.drawing_revision_delete": "حذف المراجعة",
|
||||
"cmd.drawing_revision_edit": "تعديل المراجعة",
|
||||
"cmd.drawing_scene": "رسم الورقة",
|
||||
"cmd.drawing_set": "إعدادات اللوحة",
|
||||
"cmd.drawing_sheet_add": "إضافة ورقة",
|
||||
"cmd.drawing_sheet_delete": "حذف الورقة",
|
||||
"cmd.drawing_sheet_reorder": "إعادة ترتيب الأوراق",
|
||||
"cmd.drawing_sheet_set": "إعدادات الورقة",
|
||||
"cmd.drawing_view_copy_to_sheet": "نسخ المسقط إلى ورقة",
|
||||
"cmd.drawing_view_edit": "تعديل مسقط اللوحة",
|
||||
"cmd.drawing_view_inspect": "فحص مسقط اللوحة",
|
||||
"cmd.drawing_view_move_to_sheet": "نقل المسقط إلى ورقة",
|
||||
"cmd.drawing_view_snapshot": "لقطة مسقط اللوحة",
|
||||
"cmd.occurrence_bom_type": "نوع قائمة المواد",
|
||||
"prefs.drawings": "اللوحات",
|
||||
"ui.first_angle": "الزاوية الأولى",
|
||||
"ui.projection_of_new_drawings": "إسقاط اللوحات الجديدة",
|
||||
"ui.third_angle": "الزاوية الثالثة"
|
||||
},
|
||||
"he": {
|
||||
"cmd.assembly_bom_item_set": "שינוי מספר פריט",
|
||||
"cmd.assembly_bom_renumber": "מספור מחדש של פריטים",
|
||||
"cmd.drawing_balloon_add": "הוספת בלון",
|
||||
"cmd.drawing_balloon_edit": "עריכת בלון",
|
||||
"cmd.drawing_balloon_inspect": "בדיקת בלון",
|
||||
"cmd.drawing_close": "סגירת שרטוט",
|
||||
"cmd.drawing_create": "יצירת שרטוט",
|
||||
"cmd.drawing_delete": "מחיקת שרטוט",
|
||||
"cmd.drawing_dimension_add": "הוספת מידה לשרטוט",
|
||||
"cmd.drawing_dimension_edit": "עריכת מידת שרטוט",
|
||||
"cmd.drawing_dimension_inspect": "בדיקת מידת שרטוט",
|
||||
"cmd.drawing_export": "ייצוא גיליון",
|
||||
"cmd.drawing_inspect": "בדיקת שרטוט",
|
||||
"cmd.drawing_list": "רשימת שרטוטים",
|
||||
"cmd.drawing_open": "פתיחת שרטוט",
|
||||
"cmd.drawing_properties": "מאפייני שרטוט",
|
||||
"cmd.drawing_properties_remove": "הסרת מאפיין שרטוט",
|
||||
"cmd.drawing_properties_set": "שינוי מאפייני שרטוט",
|
||||
"cmd.drawing_revision_add": "הוספת גרסה",
|
||||
"cmd.drawing_revision_delete": "מחיקת גרסה",
|
||||
"cmd.drawing_revision_edit": "עריכת גרסה",
|
||||
"cmd.drawing_scene": "ציור גיליון",
|
||||
"cmd.drawing_set": "הגדרות שרטוט",
|
||||
"cmd.drawing_sheet_add": "הוספת גיליון",
|
||||
"cmd.drawing_sheet_delete": "מחיקת גיליון",
|
||||
"cmd.drawing_sheet_reorder": "סידור גיליונות מחדש",
|
||||
"cmd.drawing_sheet_set": "הגדרות גיליון",
|
||||
"cmd.drawing_view_copy_to_sheet": "העתקת מבט לגיליון",
|
||||
"cmd.drawing_view_edit": "עריכת מבט שרטוט",
|
||||
"cmd.drawing_view_inspect": "בדיקת מבט שרטוט",
|
||||
"cmd.drawing_view_move_to_sheet": "העברת מבט לגיליון",
|
||||
"cmd.drawing_view_snapshot": "תמונת מבט שרטוט",
|
||||
"cmd.occurrence_bom_type": "סוג ברשימת החלקים",
|
||||
"prefs.drawings": "שרטוטים",
|
||||
"ui.first_angle": "זווית ראשונה",
|
||||
"ui.projection_of_new_drawings": "היטל של שרטוטים חדשים",
|
||||
"ui.third_angle": "זווית שלישית"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.assembly_bom_item_set": "Đổi số hạng mục",
|
||||
"cmd.assembly_bom_renumber": "Đánh số lại hạng mục",
|
||||
"cmd.drawing_balloon_add": "Thêm bóng chú thích",
|
||||
"cmd.drawing_balloon_edit": "Sửa bóng chú thích",
|
||||
"cmd.drawing_balloon_inspect": "Kiểm tra bóng chú thích",
|
||||
"cmd.drawing_close": "Đóng bản vẽ",
|
||||
"cmd.drawing_create": "Tạo bản vẽ",
|
||||
"cmd.drawing_delete": "Xóa bản vẽ",
|
||||
"cmd.drawing_dimension_add": "Thêm kích thước bản vẽ",
|
||||
"cmd.drawing_dimension_edit": "Sửa kích thước bản vẽ",
|
||||
"cmd.drawing_dimension_inspect": "Kiểm tra kích thước bản vẽ",
|
||||
"cmd.drawing_export": "Xuất tờ",
|
||||
"cmd.drawing_inspect": "Kiểm tra bản vẽ",
|
||||
"cmd.drawing_list": "Liệt kê bản vẽ",
|
||||
"cmd.drawing_open": "Mở bản vẽ",
|
||||
"cmd.drawing_properties": "Thuộc tính bản vẽ",
|
||||
"cmd.drawing_properties_remove": "Xóa thuộc tính bản vẽ",
|
||||
"cmd.drawing_properties_set": "Thay đổi thuộc tính bản vẽ",
|
||||
"cmd.drawing_revision_add": "Thêm lần sửa đổi",
|
||||
"cmd.drawing_revision_delete": "Xóa lần sửa đổi",
|
||||
"cmd.drawing_revision_edit": "Sửa lần sửa đổi",
|
||||
"cmd.drawing_scene": "Vẽ tờ",
|
||||
"cmd.drawing_set": "Cài đặt bản vẽ",
|
||||
"cmd.drawing_sheet_add": "Thêm tờ",
|
||||
"cmd.drawing_sheet_delete": "Xóa tờ",
|
||||
"cmd.drawing_sheet_reorder": "Sắp xếp lại các tờ",
|
||||
"cmd.drawing_sheet_set": "Cài đặt tờ",
|
||||
"cmd.drawing_view_copy_to_sheet": "Sao chép hình chiếu sang tờ",
|
||||
"cmd.drawing_view_edit": "Sửa hình chiếu bản vẽ",
|
||||
"cmd.drawing_view_inspect": "Kiểm tra hình chiếu bản vẽ",
|
||||
"cmd.drawing_view_move_to_sheet": "Chuyển hình chiếu sang tờ",
|
||||
"cmd.drawing_view_snapshot": "Ảnh chụp hình chiếu bản vẽ",
|
||||
"cmd.occurrence_bom_type": "Loại trong bảng kê vật tư",
|
||||
"prefs.drawings": "Bản vẽ",
|
||||
"ui.first_angle": "Góc chiếu thứ nhất",
|
||||
"ui.projection_of_new_drawings": "Phép chiếu của bản vẽ mới",
|
||||
"ui.third_angle": "Góc chiếu thứ ba"
|
||||
}
|
||||
}
|
||||
@@ -1347,7 +1347,6 @@
|
||||
"document.read_only_save": "Ten projekt otwarto tylko do odczytu: zapisz go jako nowy plik",
|
||||
"document.save_changes": "Zapisać zmiany w {name}?",
|
||||
"document.save_copy": "Zapisz kopię…",
|
||||
"document.tab_unsaved_changes": "Niezapisane zmiany",
|
||||
"document.unsaved_changes": "{name} ma niezapisane zmiany. Jeśli ich nie zapiszesz, zostaną utracone.",
|
||||
"drive.rigid": "{joint} to złącze sztywne: utrzymuje swoje części nieruchomo, więc nie ma czym sterować. Aby mogły się poruszać, zmień jego typ w Edytuj złącze: Obrotowe do obrotu, Przesuwne do przesuwu, Walcowe do obu.",
|
||||
"drive.stopped_by_contact": "Zatrzymano przy {value}: {a} przeszedłby przez {b}",
|
||||
|
||||
@@ -182,8 +182,6 @@
|
||||
"Designs you open or save appear here.": "Здесь появляются открытые и сохранённые дизайны.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Определяется параметром «До объекта». Выберите «Угол», чтобы задать угловой предел.",
|
||||
"Diameter": "Диаметр",
|
||||
"Did this assembly open the wrong way up?": "Эта сборка открылась не той стороной вверх?",
|
||||
"Did this part open the wrong way up?": "Эта деталь открылась не той стороной вверх?",
|
||||
"Dimension new shapes": "Проставлять размеры новых фигур",
|
||||
"Direction": "Направление",
|
||||
"Discard": "Отменить",
|
||||
@@ -608,7 +606,6 @@
|
||||
"Square": "Квадрат",
|
||||
"Stand it up": "Поставить вертикально",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Поставить вертикально: четверть оборота вокруг X одной операцией «Переместить», которую можно отменить.",
|
||||
"Stand this assembly up?": "Поставить эту сборку вертикально?",
|
||||
"Stand this part up?": "Поставить эту деталь вертикально?",
|
||||
"Start angle": "Начальный угол",
|
||||
"Start bulge": "Выпуклость в начале",
|
||||
@@ -1359,7 +1356,6 @@
|
||||
"dialog.dimension_insert_hint": "Дважды щёлкните другой управляющий размер, чтобы вставить его имя",
|
||||
"dialog.dimension_name": "Размер: {name}",
|
||||
"dialog.edit_title": "ИЗМЕНИТЬ: {title}",
|
||||
"dialog.no_shown_body": "Нет отображаемого тела для изменения: покажите тело или выберите «Новое тело»",
|
||||
"dialog.pattern_sketch_not_object": "Выберите тела или операции: эскиз — ни то, ни другое. Повторите его кривые собственным инструментом «Массив» или «Зеркало» в эскизе либо сначала выдавите его.",
|
||||
"dialog.select": "↖ Выбрать",
|
||||
"dialog.selected_count": "↖ Выбрано: {count}",
|
||||
@@ -1721,8 +1717,6 @@
|
||||
"help.view.spin": "Непрерывно вращает вид вокруг модели до следующего щелчка или нажатия клавиши. Используйте, чтобы показать модель со всех сторон.",
|
||||
"help.view.tool": "Закрепляет инструмент камеры (орбиту, панорамирование или масштаб), так что перетаскивание левой кнопкой мыши двигает вид, пока вы его не отключите. Используйте на трекпаде или без средней кнопки мыши.",
|
||||
"help.view.visual_style": "Переключает способ отрисовки модели: тонированная с рёбрами или без них, каркас или со скрытыми рёбрами. Используйте каркас, чтобы заглянуть внутрь, а тонированный вид — для чистого вида.",
|
||||
"history.sketch_under_constrained": "Недоопределён (степеней свободы: {count})",
|
||||
"history.sketch_under_constrained_one": "Недоопределён (степеней свободы: {count})",
|
||||
"hole_presets.confirm_delete": "Подтвердить удаление",
|
||||
"hole_presets.delete": "Удалить",
|
||||
"hole_presets.delete_prompt": "Удалить этот личный пресет? Существующие отверстия не изменятся.",
|
||||
@@ -2134,8 +2128,6 @@
|
||||
"prefs.anti_aliasing_note": "Сглаживает неровные края.",
|
||||
"prefs.applies_on_restart": "Применяется после перезапуска",
|
||||
"prefs.assistant": "Ассистент",
|
||||
"prefs.auto_operation": "Выбирать операцию автоматически",
|
||||
"prefs.auto_operation_note": "Окна операций сами выбирают «Объединить», «Новое тело» или «Вырезать» в зависимости от того, чего касается новый материал, пока вы не выберете операцию вручную. Выкл.: окна сохраняют ту операцию, с которой открылись.",
|
||||
"prefs.axes": "Оси",
|
||||
"prefs.axis": "Ось",
|
||||
"prefs.bodies_folders": "Папки тел",
|
||||
@@ -2357,8 +2349,6 @@
|
||||
"prefs.sketch_selection": "Эскиз и выделение",
|
||||
"prefs.sketch_selection_heading": "Эскиз и выделение",
|
||||
"prefs.sketch_selection_note": "Как работает привязка в эскизах и как щелчки выделяют объекты.",
|
||||
"prefs.sketch_state_icons": "Значки эскизов в цвете их состояния",
|
||||
"prefs.sketch_state_icons_note": "В истории и браузере значок эскиза принимает цвет его состояния (недоопределён, полностью определён или с конфликтом), а подсказка называет это состояние. Выкл.: у всех эскизов одинаковый значок.",
|
||||
"prefs.sketch_tab_always": "Всегда показывать вкладку «Эскиз»",
|
||||
"prefs.sketch_tab_always_note": "Вкладка «Эскиз» остаётся на ленте сразу после «Модель», пока не открыт ни один эскиз. Её инструменты рисования создают эскиз на выбранной плоскости или грани либо на плоскости, которую вы выберете следом, и сразу включают нужный инструмент.",
|
||||
"prefs.sketch_tracking_guides": "Направляющие выравнивания по центру и середине",
|
||||
@@ -2370,11 +2360,6 @@
|
||||
"prefs.source": "Источник",
|
||||
"prefs.speed": "Скорость",
|
||||
"prefs.stand_up_note": "После открытия файла STEP или IGES с осью Z вверх предлагать повернуть его на четверть оборота вокруг X.",
|
||||
"prefs.stand_up_unknown": "Для файлов из неизвестных программ",
|
||||
"prefs.stand_up_unknown.ask": "Спрашивать в диалоговом окне",
|
||||
"prefs.stand_up_unknown.note": "Показывать уведомление",
|
||||
"prefs.stand_up_unknown.off": "Не предлагать",
|
||||
"prefs.stand_up_unknown_note": "Как делается предложение, когда в заголовке файла не указана программа, о которой известно, что она сохраняет с осью Y вверх. Уведомление показывается, только если файл, судя по всему, лежит на спине; «Спрашивать в диалоговом окне» спрашивает о каждом таком файле, как раньше.",
|
||||
"prefs.status_bar": "Строка состояния",
|
||||
"prefs.status_bar_note": "Тонкая полоса вдоль нижнего края окна показывает, что изменяется, состояние открытого эскиза его цветом и единицы дизайна; щелчок по единицам изменяет их. Состояние эскиза при этом больше не показывается в углу вида.",
|
||||
"prefs.status_done": "Готово",
|
||||
@@ -3917,10 +3902,8 @@
|
||||
"timeline.move_to_group": "Переместить операции в папку",
|
||||
"timeline.rename_group": "Переименовать папку операций",
|
||||
"timeline.reorder_many": "Изменить порядок операций",
|
||||
"timeline.suppress_features": "Подавить операции",
|
||||
"timeline.suppress_folders_tip": "Подавить или снять подавление со всех операций в папках и вложенных папках",
|
||||
"timeline.ungroup": "Убрать папки операций",
|
||||
"timeline.unsuppress_features": "Снять подавление с операций",
|
||||
"to the face": "до грани",
|
||||
"to the next face": "до следующей грани",
|
||||
"toolbar.customize_note": "Вкладка {tab}. Отметьте команду, чтобы добавить её кнопку на панель инструментов. Можно также перетащить строку из выпадающего меню панели на панель инструментов, перетащить кнопку за пределы панели для удаления или использовать значок закрепления в строке выпадающего меню.",
|
||||
@@ -4366,8 +4349,6 @@
|
||||
"ui.cancel": "Отмена",
|
||||
"ui.canvases": "Подложки",
|
||||
"ui.center": "Центр",
|
||||
"ui.center_arc_follows_cursor": "Дуга из центра следует направлению курсора",
|
||||
"ui.center_arc_follows_cursor_tip": "Дуга из центра идёт по часовой стрелке или против неё — в ту сторону, в которую курсор обходит её центр. Выкл.: всегда против часовой стрелки от начальной точки.",
|
||||
"ui.centerline": "Осевая линия",
|
||||
"ui.chain": "Цепочка",
|
||||
"ui.chamfer": "Фаска",
|
||||
@@ -5179,13 +5160,11 @@
|
||||
"update.installed": "SolidCraft {version} установлен. Перезапустите, чтобы использовать его.",
|
||||
"update.installer_started": "Установщик запустится после закрытия SolidCraft.",
|
||||
"update.later": "Напомнить позже",
|
||||
"update.managed": "Доступно обновление: обновите SolidCraft в {manager}",
|
||||
"update.no_notes": "У этого выпуска нет примечаний.",
|
||||
"update.package_hint": "Эта копия установлена из {kind}. Скачайте новый пакет, затем установите его командой:",
|
||||
"update.prefs_dev": "Это сборка для разработки ({version}): она не проверяет обновления.",
|
||||
"update.prefs_flatpak": "Эту копию ({version}) обновляет Flatpak: выполните {command} или воспользуйтесь центром приложений. SolidCraft не проверяет обновления.",
|
||||
"update.prefs_label": "Обновления",
|
||||
"update.prefs_managed": "Это SolidCraft {version}, которым управляет {manager}. Проверка запрашивает у GitHub последний релиз и больше ничего не отправляет; обновляйте SolidCraft в {manager}.",
|
||||
"update.prefs_note": "Это SolidCraft {version} ({kind}). При проверке у GitHub запрашивается последний выпуск, больше ничего не отправляется.",
|
||||
"update.prefs_web": "Перезагрузите страницу, чтобы получить последнюю версию веб-приложения. Если сеть недоступна или обновление не успевает загрузиться, открывается сохранённая версия; незавершённая загрузка продолжится до следующей перезагрузки.",
|
||||
"update.refresh_checking": "Проверка наличия новейшего выпуска…",
|
||||
|
||||
@@ -1392,7 +1392,6 @@
|
||||
"document.read_only_save": "Den här konstruktionen öppnades skrivskyddad: spara den som en ny fil",
|
||||
"document.save_changes": "Spara ändringar i {name}?",
|
||||
"document.save_copy": "Spara en kopia…",
|
||||
"document.tab_unsaved_changes": "Osparade ändringar",
|
||||
"document.unsaved_changes": "{name} har osparade ändringar. Om du inte sparar går de förlorade.",
|
||||
"drive.rigid": "{joint} är ett stelt led: det håller sina delar stilla och har därför inget att driva. Om delarna ska kunna röra sig, ändra ledets typ i Redigera led: Roterande för att rotera, Glidande för att glida, Cylindrisk för båda.",
|
||||
"drive.stopped_by_contact": "Stoppade vid {value}: {a} skulle gå igenom {b}",
|
||||
|
||||
@@ -1396,9 +1396,6 @@
|
||||
"dialog.custom_axis": "↖ Trục tùy chỉnh",
|
||||
"dialog.dimension_insert_hint": "Nhấp vào một kích thước điều khiển khác để chèn tên của nó",
|
||||
"dialog.dimension_name": "Kích thước: {name}",
|
||||
"dialog.dimension_step_down": "Giảm {step}",
|
||||
"dialog.dimension_step_expression": "Chỉ số mới thay đổi theo bước; biểu thức giữ nguyên như đã nhập",
|
||||
"dialog.dimension_step_up": "Tăng {step}",
|
||||
"dialog.edit_title": "CHỈNH SỬA {title}",
|
||||
"dialog.no_shown_body": "Không có thân khối đang hiển thị để thay đổi: hãy hiển thị một thân khối, hoặc chọn “Tạo mới Thân khối”",
|
||||
"dialog.pattern_sketch_not_object": "Hãy chọn thân khối hoặc tính năng: bản phác thảo không phải là cả hai. Lặp lại các đường cong của nó bằng Mảng hoặc Lấy đối xứng trong chính bản phác thảo, hoặc đùn khối nó trước.",
|
||||
@@ -1406,7 +1403,6 @@
|
||||
"dialog.selected_count": "↖ {count} đã chọn",
|
||||
"dialog.selection_constraint": "Ràng buộc {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · đường cong {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "Chọn một mục đầu vào để thay đổi. Nhấp vào đối tượng đã chọn để bỏ chọn; Ctrl/Cmd-nhấp để bật/tắt chọn. Xem trước kết quả, sau đó nhấn OK.",
|
||||
"dialog.selection_midpoint": "Trung điểm · {id}",
|
||||
"dialog.selection_profile": "{sketch} · biên dạng {number}",
|
||||
@@ -1424,7 +1420,6 @@
|
||||
"document.read_only_save": "Thiết kế này được mở ở chế độ chỉ đọc: hãy lưu thành tệp mới",
|
||||
"document.save_changes": "Lưu các thay đổi đối với {name}?",
|
||||
"document.save_copy": "Lưu bản sao…",
|
||||
"document.tab_unsaved_changes": "Thay đổi chưa lưu",
|
||||
"document.unsaved_changes": "{name} có các thay đổi chưa lưu. Nếu không lưu, các thay đổi sẽ bị mất.",
|
||||
"drive.rigid": "{joint} là khớp nối cứng: nó giữ cố định các chi tiết, nên không có gì để dẫn động. Để các chi tiết chuyển động, hãy đổi loại khớp nối trong “Chỉnh sửa khớp nối”: Revolute để xoay, Slider để trượt, Cylindrical cho cả hai.",
|
||||
"drive.stopped_by_contact": "Đã dừng tại {value}: {a} sẽ xuyên qua {b}",
|
||||
@@ -2004,7 +1999,6 @@
|
||||
"material.selection.mixed": "Các vật liệu khác nhau",
|
||||
"measure.at": "Tại",
|
||||
"measure.item": "Mục {number}",
|
||||
"measure.line_to_centre": "Từ đường đến tâm",
|
||||
"measure.quick_copied": "Đã sao chép số đo",
|
||||
"measure.quick_copy": "Nhấp để sao chép số đo",
|
||||
"measure.quick_edges": "{count} cạnh",
|
||||
@@ -2271,14 +2265,6 @@
|
||||
"prefs.auto_operation_note": "Nối, Vật thể mới hoặc Cắt, tùy vào phần vật liệu tiếp xúc.",
|
||||
"prefs.axes": "Các trục tọa độ",
|
||||
"prefs.axis": "Trục tọa độ",
|
||||
"prefs.blur_cost": "Chi phí kết xuất tương đối. Nhiều thanh hơn thường có nghĩa là cần xử lý nhiều hơn.",
|
||||
"prefs.blur_device": "Không khả dụng trên thiết bị đồ họa hoặc đường kết xuất này",
|
||||
"prefs.blur_guard": "Chất lượng làm mờ đã giảm để duy trì độ phản hồi của khung hình. Bán kính không thay đổi.",
|
||||
"prefs.blur_off": "Tắt",
|
||||
"prefs.blur_reduce_transparency": "Bị tắt bởi cài đặt Giảm độ trong suốt của hệ thống",
|
||||
"prefs.blur_software": "Không khả dụng với đồ họa phần mềm (gpu-software)",
|
||||
"prefs.blur_source": "Không khả dụng trong chế độ xem không gian làm việc này",
|
||||
"prefs.blur_web": "Không khả dụng trên đường kết xuất web này",
|
||||
"prefs.bodies_folders": "Thư mục thân khối",
|
||||
"prefs.bodies_folders_always": "Luôn luôn",
|
||||
"prefs.bodies_folders_never": "Không bao giờ",
|
||||
@@ -2337,13 +2323,6 @@
|
||||
"prefs.dialogs_edge": "Ở cạnh khung nhìn",
|
||||
"prefs.dialogs_left_column": "Trong cột bên trái",
|
||||
"prefs.dialogs_note": "Nơi hộp thoại lệnh mở ra: gắn ở cạnh khung nhìn, hoặc trong cột bên trái thay cho trình duyệt và Lịch sử, hai phần này trở lại như cũ khi hộp thoại đóng.",
|
||||
"prefs.dimension_step_angle": "Lượng mà mũi tên của cửa sổ, Lên/Xuống và con lăn cộng vào một góc",
|
||||
"prefs.dimension_step_length": "Lượng mà mũi tên của cửa sổ, Lên/Xuống và con lăn cộng vào một độ dài",
|
||||
"prefs.dimension_steps": "Bước của cửa sổ",
|
||||
"prefs.dimension_values": "Giá trị kích thước",
|
||||
"prefs.dimension_values_note": "Nơi bạn nhập giá trị kích thước. Cửa sổ có mũi tên để thay đổi theo bước; công cụ Kích thước đặt văn bản trước.",
|
||||
"prefs.dimension_values_view": "Trong khung nhìn",
|
||||
"prefs.dimension_values_window": "Trong một cửa sổ",
|
||||
"prefs.display": "Hiển thị",
|
||||
"prefs.edges": "Cạnh và đổ bóng",
|
||||
"prefs.extend": "Kéo dài",
|
||||
@@ -2671,10 +2650,6 @@
|
||||
"prefs.theme_follow_off": "Tắt",
|
||||
"prefs.theme_follow_stop": "Dừng theo dõi",
|
||||
"prefs.theme_import_hint": "Nhấp để chọn tệp giao diện, thả tệp vào đây, hoặc nhấn Ctrl+V (Cmd+V trên Mac) khi đã sao chép một giao diện",
|
||||
"prefs.theme_notice_dismiss": "Ẩn",
|
||||
"prefs.theme_option_blur": "Làm mờ phía sau bảng",
|
||||
"prefs.theme_option_blur_help": "Làm mờ phía sau các bảng bán trong suốt, tính bằng pixel vật lý. Giá trị 0 là tắt.",
|
||||
"prefs.theme_option_invalid": "Giá trị đã lưu cho {option} không hợp lệ; đang dùng giá trị mặc định của giao diện. Giá trị đã lưu vẫn được giữ lại.",
|
||||
"prefs.theme_option_opacity": "Độ đục",
|
||||
"prefs.theme_option_opacity_help": "Điều chỉnh độ đục của các bảng nổi. Giới hạn độ đục tối thiểu giúp văn bản luôn dễ đọc trên nền màu của khung nhìn.",
|
||||
"prefs.theme_option_tab_colours": "Màu thẻ",
|
||||
@@ -2683,7 +2658,6 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "Chuyển dần màu của bảng điều khiển và khung nhìn khi đổi thẻ. Đặt thành 0 để chuyển ngay lập tức.",
|
||||
"prefs.theme_options_badge": "Tùy chọn",
|
||||
"prefs.theme_options_more": "Thêm · {count}",
|
||||
"prefs.theme_options_newer": "Giao diện này yêu cầu phiên bản SolidCraft mới hơn. Sau khi nâng cấp, hãy nhập lại giao diện gốc để khôi phục các định nghĩa tùy chọn.",
|
||||
"prefs.theme_options_reset": "Đặt lại",
|
||||
"prefs.theme_options_unavailable": "Không khả dụng trên thiết bị này",
|
||||
"prefs.theme_paste": "Dán giao diện",
|
||||
@@ -4810,7 +4784,6 @@
|
||||
"ui.default_modelling_orientation": "Hướng mô hình hóa mặc định",
|
||||
"ui.delete": "Xóa",
|
||||
"ui.delete_select_first": "Hãy chọn trước những gì cần xóa: tính năng, khối, thành phần hoặc hình học phác thảo",
|
||||
"ui.dependents_select_first": "Hãy chọn trước đối tượng để liệt kê các phần phụ thuộc của nó",
|
||||
"ui.depth": "Độ sâu",
|
||||
"ui.design": "Thiết kế",
|
||||
"ui.design_properties.author": "Tác giả",
|
||||
@@ -4968,10 +4941,6 @@
|
||||
"ui.fillet_setback_least": "Lớn hơn {least}",
|
||||
"ui.fillet_setback_tip": "Nơi mảng góc trơn bắt đầu: khoảng cách dọc theo mỗi cạnh tính từ góc. Mặc định là {factor} × bán kính. Giá trị phải lớn hơn khoảng lùi nhỏ nhất, tại đó các đường bo gặp nhau mà không cần lùi: bằng bán kính khi các mặt vuông góc với nhau.",
|
||||
"ui.fillet_smooth_radius": "Thay đổi bán kính trơn",
|
||||
"ui.fillet_turn": "Chỗ rẽ",
|
||||
"ui.fillet_turn_mitre": "Vát góc",
|
||||
"ui.fillet_turn_swept": "Quét vòng",
|
||||
"ui.fillet_turn_tip": "Nơi hai góc lượn rẽ quanh một cạnh góc sắc nằm giữa chúng, ví dụ các góc lượn ở chân một khối lồi tại các cạnh đứng của nó.\nVát góc: các góc lượn kéo dài cho đến khi gặp nhau theo đường vát.\nQuét vòng: các góc lượn dừng ở cạnh góc và quay quanh nó, góc là một mặt xuyến tiếp xúc với cả hai. Chỉ cho các góc lượn cùng bán kính quanh một cạnh góc thẳng và lồi.",
|
||||
"ui.fillet_type_choices_tip": "Bán kính không đổi: một bán kính dọc suốt cạnh.\nĐộ dài dây cung: độ rộng ngang đường bo giữ nguyên, bất kể góc gặp nhau của các mặt.\nBán kính thay đổi: một bán kính ở mỗi đầu, và thêm dọc cạnh nếu bạn thêm vào.\nLiên tục độ cong: mặt cắt không làm gãy phản chiếu tại nơi gặp các mặt phẳng (G2).\nSpline: bán kính dọc cạnh theo các bán kính điều khiển bạn đặt.",
|
||||
"ui.fillet_type_curvature": "Liên tục độ cong",
|
||||
"ui.fillet_type_tip": "Mặt cắt ngang qua cạnh. Liên tục độ cong: mặt cắt liên tục độ cong (G2) giữa các mặt phẳng, nên phản chiếu đi từ các mặt vào đường bo mà không bị gãy.",
|
||||
|
||||
@@ -833,8 +833,6 @@
|
||||
"cmd.appearance_pick_up": "拾取外观",
|
||||
"cmd.appearance_replace": "替换外观",
|
||||
"cmd.assembly_bom": "物料清单",
|
||||
"cmd.assembly_bom_item_set": "更改项目编号",
|
||||
"cmd.assembly_bom_renumber": "重新编号项目",
|
||||
"cmd.assembly_mass_properties": "质量属性",
|
||||
"cmd.body.print_face": "打印底面",
|
||||
"cmd.body_rename": "重命名实体",
|
||||
@@ -934,36 +932,6 @@
|
||||
"cmd.document_metadata_set": "更改设计属性",
|
||||
"cmd.document_revision": "设计版本号",
|
||||
"cmd.document_units": "更改设计单位",
|
||||
"cmd.drawing_balloon_add": "添加零件序号",
|
||||
"cmd.drawing_balloon_edit": "编辑零件序号",
|
||||
"cmd.drawing_balloon_inspect": "检查零件序号",
|
||||
"cmd.drawing_close": "关闭绘图",
|
||||
"cmd.drawing_create": "创建绘图",
|
||||
"cmd.drawing_delete": "删除绘图",
|
||||
"cmd.drawing_dimension_add": "添加绘图尺寸",
|
||||
"cmd.drawing_dimension_edit": "编辑绘图尺寸",
|
||||
"cmd.drawing_dimension_inspect": "检查绘图尺寸",
|
||||
"cmd.drawing_export": "导出图纸",
|
||||
"cmd.drawing_inspect": "检查绘图",
|
||||
"cmd.drawing_list": "列出绘图",
|
||||
"cmd.drawing_open": "打开绘图",
|
||||
"cmd.drawing_properties": "绘图属性",
|
||||
"cmd.drawing_properties_remove": "删除绘图属性",
|
||||
"cmd.drawing_properties_set": "更改绘图属性",
|
||||
"cmd.drawing_revision_add": "添加版本",
|
||||
"cmd.drawing_revision_delete": "删除版本",
|
||||
"cmd.drawing_revision_edit": "编辑版本",
|
||||
"cmd.drawing_scene": "绘制图纸",
|
||||
"cmd.drawing_set": "绘图设置",
|
||||
"cmd.drawing_sheet_add": "添加图纸",
|
||||
"cmd.drawing_sheet_delete": "删除图纸",
|
||||
"cmd.drawing_sheet_reorder": "重新排列图纸",
|
||||
"cmd.drawing_sheet_set": "图纸设置",
|
||||
"cmd.drawing_view_copy_to_sheet": "将视图复制到图纸",
|
||||
"cmd.drawing_view_edit": "编辑绘图视图",
|
||||
"cmd.drawing_view_inspect": "检查绘图视图",
|
||||
"cmd.drawing_view_move_to_sheet": "将视图移至图纸",
|
||||
"cmd.drawing_view_snapshot": "绘图视图快照",
|
||||
"cmd.edit_checkpoint": "检查点",
|
||||
"cmd.edit_repeat": "重复上一个命令",
|
||||
"cmd.edit_restore_checkpoint": "恢复检查点",
|
||||
@@ -1061,7 +1029,6 @@
|
||||
"cmd.motion_trace_edit": "编辑运动轨迹",
|
||||
"cmd.motion_trace_keep": "保留运动轨迹",
|
||||
"cmd.occurrence_align": "对齐实例",
|
||||
"cmd.occurrence_bom_type": "物料清单类型",
|
||||
"cmd.occurrence_copy": "粘贴(实例)",
|
||||
"cmd.occurrence_drag": "拖动实例",
|
||||
"cmd.occurrence_ground": "固定",
|
||||
@@ -1429,9 +1396,6 @@
|
||||
"dialog.custom_axis": "↖ 自定义轴",
|
||||
"dialog.dimension_insert_hint": "单击其他驱动尺寸以插入其名称",
|
||||
"dialog.dimension_name": "尺寸:{name}",
|
||||
"dialog.dimension_step_down": "减少 {step}",
|
||||
"dialog.dimension_step_expression": "只有数字可以增减;表达式保持不变",
|
||||
"dialog.dimension_step_up": "增加 {step}",
|
||||
"dialog.edit_title": "编辑 {title}",
|
||||
"dialog.no_shown_body": "没有可更改的显示实体:请显示一个实体,或选择新建实体",
|
||||
"dialog.pattern_sketch_not_object": "请选择实体或特征:草图两者都不是。请用草图自身的阵列或镜像重复其曲线,或先将其拉伸。",
|
||||
@@ -1439,7 +1403,6 @@
|
||||
"dialog.selected_count": "↖ 已选择 {count} 个",
|
||||
"dialog.selection_constraint": "约束 {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · 曲线 {id}",
|
||||
"dialog.selection_external_point": "{point} · {sketch}",
|
||||
"dialog.selection_guidance": "选择输入项进行修改。点击已选对象可取消选择;按住 Ctrl/Cmd 点击也可切换选择。检查预览后,再按“确定”。",
|
||||
"dialog.selection_midpoint": "中点 · {id}",
|
||||
"dialog.selection_profile": "{sketch} · 轮廓 {number}",
|
||||
@@ -1457,7 +1420,6 @@
|
||||
"document.read_only_save": "此设计以只读方式打开:请另存为新文件",
|
||||
"document.save_changes": "保存对 {name} 的更改?",
|
||||
"document.save_copy": "保存副本…",
|
||||
"document.tab_unsaved_changes": "未保存的更改",
|
||||
"document.unsaved_changes": "{name} 有未保存的更改。如果不保存,将丢失这些更改。",
|
||||
"drive.rigid": "{joint} 是刚性接头:它把零件固定在原位,因此没有可驱动的运动。要让零件运动,请在“编辑接头”中更改其类型:旋转用 Revolute,滑动用 Slider,两者兼有用 Cylindrical。",
|
||||
"drive.stopped_by_contact": "停在 {value}:{a} 会穿过 {b}",
|
||||
@@ -2037,7 +1999,6 @@
|
||||
"material.selection.mixed": "不同材料",
|
||||
"measure.at": "位置",
|
||||
"measure.item": "项目 {number}",
|
||||
"measure.line_to_centre": "线到圆心",
|
||||
"measure.quick_copied": "已复制测量值",
|
||||
"measure.quick_copy": "点击复制测量值",
|
||||
"measure.quick_edges": "{count} 条边",
|
||||
@@ -2304,14 +2265,6 @@
|
||||
"prefs.auto_operation_note": "根据材料接触的对象选择合并、新建实体或切割。",
|
||||
"prefs.axes": "坐标轴",
|
||||
"prefs.axis": "轴",
|
||||
"prefs.blur_cost": "相对渲染开销。条形越多,通常意味着处理工作越多。",
|
||||
"prefs.blur_device": "此图形设备或渲染路径不支持此功能",
|
||||
"prefs.blur_guard": "已降低模糊质量,以保持画面响应。模糊半径未改变。",
|
||||
"prefs.blur_off": "关闭",
|
||||
"prefs.blur_reduce_transparency": "已被系统的降低透明度设置禁用",
|
||||
"prefs.blur_software": "软件图形渲染不支持此功能 (gpu-software)",
|
||||
"prefs.blur_source": "此工作区视图不支持此功能",
|
||||
"prefs.blur_web": "此网页渲染路径不支持此功能",
|
||||
"prefs.bodies_folders": "实体文件夹",
|
||||
"prefs.bodies_folders_always": "始终显示",
|
||||
"prefs.bodies_folders_never": "不显示",
|
||||
@@ -2370,15 +2323,7 @@
|
||||
"prefs.dialogs_edge": "视图边缘",
|
||||
"prefs.dialogs_left_column": "左侧栏",
|
||||
"prefs.dialogs_note": "命令对话框的打开位置:停靠在视图边缘,或在左侧栏中替代浏览器和历史;关闭对话框后,浏览器和历史恢复原样。",
|
||||
"prefs.dimension_step_angle": "窗口的箭头、上下方向键和滚轮给角度增减的量",
|
||||
"prefs.dimension_step_length": "窗口的箭头、上下方向键和滚轮给长度增减的量",
|
||||
"prefs.dimension_steps": "窗口步长",
|
||||
"prefs.dimension_values": "尺寸值",
|
||||
"prefs.dimension_values_note": "输入尺寸值的位置。窗口带有可增减数值的箭头;尺寸工具会先放置文字。",
|
||||
"prefs.dimension_values_view": "在视图中",
|
||||
"prefs.dimension_values_window": "在窗口中",
|
||||
"prefs.display": "显示",
|
||||
"prefs.drawings": "绘图",
|
||||
"prefs.edges": "边缘与着色",
|
||||
"prefs.extend": "扩展",
|
||||
"prefs.extra_navigation": "在空白处按住Ctrl并用鼠标左键拖动可旋转;按住空格键可平移。",
|
||||
@@ -2705,10 +2650,6 @@
|
||||
"prefs.theme_follow_off": "关闭",
|
||||
"prefs.theme_follow_stop": "停止跟随",
|
||||
"prefs.theme_import_hint": "单击选择主题文件,拖放到此处,或复制主题后按 Ctrl+V(Mac 上为 Cmd+V)",
|
||||
"prefs.theme_notice_dismiss": "忽略",
|
||||
"prefs.theme_option_blur": "面板背后模糊",
|
||||
"prefs.theme_option_blur_help": "半透明面板背后的模糊程度,以物理像素为单位。0 表示关闭。",
|
||||
"prefs.theme_option_invalid": "{option} 的已保存值无效;正在使用主题默认值。已保存值将予以保留。",
|
||||
"prefs.theme_option_opacity": "不透明度",
|
||||
"prefs.theme_option_opacity_help": "调整浮动面板的不透明度。最低不透明度限制可确保文字在视口颜色上清晰可读。",
|
||||
"prefs.theme_option_tab_colours": "选项卡颜色",
|
||||
@@ -2717,7 +2658,6 @@
|
||||
"prefs.theme_option_tab_fade_ms_help": "切换选项卡时渐变面板和视口颜色。设为零可立即切换。",
|
||||
"prefs.theme_options_badge": "选项",
|
||||
"prefs.theme_options_more": "更多 · {count}",
|
||||
"prefs.theme_options_newer": "此主题需要更新版本的 SolidCraft。升级后,请重新导入原始主题以恢复其选项定义。",
|
||||
"prefs.theme_options_reset": "重置",
|
||||
"prefs.theme_options_unavailable": "此设备不支持",
|
||||
"prefs.theme_paste": "粘贴主题",
|
||||
@@ -4844,7 +4784,6 @@
|
||||
"ui.default_modelling_orientation": "默认建模方向",
|
||||
"ui.delete": "删除",
|
||||
"ui.delete_select_first": "请先选择要删除的内容:特征、实体、组件或草图几何",
|
||||
"ui.dependents_select_first": "请先选择要列出其依赖项的对象",
|
||||
"ui.depth": "深度",
|
||||
"ui.design": "设计",
|
||||
"ui.design_properties.author": "作者",
|
||||
@@ -5002,10 +4941,6 @@
|
||||
"ui.fillet_setback_least": "须大于 {least}",
|
||||
"ui.fillet_setback_tip": "平滑角点曲面片开始的位置:从角点沿每条边的距离。默认值为半径的 {factor} 倍。必须大于最小后退距离(不后退时圆角相交之处;面相互垂直时即半径)。",
|
||||
"ui.fillet_smooth_radius": "半径平滑变化",
|
||||
"ui.fillet_turn": "转角",
|
||||
"ui.fillet_turn_mitre": "斜接",
|
||||
"ui.fillet_turn_swept": "旋转",
|
||||
"ui.fillet_turn_tip": "两个圆角绕过它们之间尖锐的角边处,例如凸台根部的圆角绕过其竖直边。\n斜接:圆角继续延伸,直到以斜接相遇。\n旋转:圆角止于角边并绕它旋转,转角是与两者相切的圆环面。仅用于同一半径的圆角绕过笔直的凸角边时。",
|
||||
"ui.fillet_type_choices_tip": "恒定半径:整条边一个半径。\n弦长:无论面以什么角度相交,圆角宽度都相同。\n可变半径:两端各一个半径,添加后沿边还有更多。\n曲率连续:与平面相接处反射没有折痕的截面 (G2)。\n样条:沿边的半径跟随您设置的控制半径。",
|
||||
"ui.fillet_type_curvature": "曲率连续",
|
||||
"ui.fillet_type_tip": "横跨边的截面。曲率连续:平面之间的曲率连续 (G2) 截面,反射从面平滑过渡到圆角,没有折痕。",
|
||||
@@ -5015,7 +4950,6 @@
|
||||
"ui.fine": "精细",
|
||||
"ui.finest": "最精细",
|
||||
"ui.finish_sketch": "完成草图",
|
||||
"ui.first_angle": "第一角投影",
|
||||
"ui.fit": "适合",
|
||||
"ui.fix_unfix": "固定/取消固定",
|
||||
"ui.flat": "平面",
|
||||
@@ -5320,7 +5254,6 @@
|
||||
"ui.profiles": "轮廓",
|
||||
"ui.project": "投影",
|
||||
"ui.project_face_edges_sketch": "将面的边投影到新草图",
|
||||
"ui.projection_of_new_drawings": "新绘图的投影法",
|
||||
"ui.properties": "属性",
|
||||
"ui.properties.copy_matrix": "复制矩阵",
|
||||
"ui.properties.first_moments": "一阶矩",
|
||||
@@ -5506,7 +5439,6 @@
|
||||
"ui.text": "文本",
|
||||
"ui.theme": "主题",
|
||||
"ui.thickness": "厚度",
|
||||
"ui.third_angle": "第三角投影",
|
||||
"ui.thread": "螺纹",
|
||||
"ui.tie_ends": "约束",
|
||||
"ui.tie_ends_tip": "约束全部未约束端点",
|
||||
|
||||
@@ -802,9 +802,7 @@ fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(
|
||||
let sb = cb.xy / max(cb.w, 1e-6) * half;
|
||||
var dir = sb - sa;
|
||||
let len = length(dir);
|
||||
// Intel OpenGL can miscompile vector/scalar division here (#8053). Scalar
|
||||
// quotients keep the same direction and the fallback for a point.
|
||||
if (len < 1e-6) { dir = vec2<f32>(1.0, 0.0); } else { dir = vec2<f32>(dir.x / len, dir.y / len); }
|
||||
if (len < 1e-6) { dir = vec2<f32>(1.0, 0.0); } else { dir = dir / len; }
|
||||
let nrm = vec2<f32>(-dir.y, dir.x);
|
||||
// Two triangles: corners (t, side).
|
||||
var ts = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
| assets/fonts/noto-sans-arabic/NotoSansArabic-Regular.ttf | The Noto Project Authors (Noto Sans Arabic 2.013, hinted) | https://github.com/notofonts/notofonts.github.io (fonts/NotoSansArabic/hinted/ttf), unmodified; licence text in assets/fonts/noto-sans-arabic/OFL.txt; the web app's font for a drawing's Arabic text | SIL OFL 1.1 |
|
||||
| assets/fonts/noto-sans-arabic/OFL.txt | The Noto Project Authors | Unmodified accompanying font licence (https://github.com/notofonts/arabic) | SIL OFL 1.1 |
|
||||
| assets/fonts/noto-sans-kr/NotoSansKR-Regular.otf | Adobe (Noto Sans KR 2.004, Noto Sans CJK subset) | https://github.com/notofonts/noto-cjk (Sans/SubsetOTF/KR), unmodified; licence text in assets/fonts/noto-sans-kr/OFL.txt; the web app's font for a drawing's Korean text | SIL OFL 1.1 |
|
||||
| assets/fonts/noto-sans-kr/OFL.txt | Adobe | Unmodified accompanying font licence (https://github.com/notofonts/noto-cjk, Sans/LICENSE) | SIL OFL 1.1 |
|
||||
| assets/fonts/noto-sans-sc/NotoSansSC-Regular.otf | Adobe (Noto Sans SC 2.004, Noto Sans CJK subset) | https://github.com/notofonts/noto-cjk (Sans/SubsetOTF/SC), unmodified; licence text in assets/fonts/noto-sans-sc/OFL.txt; the web app's font for a drawing's simplified Chinese text | SIL OFL 1.1 |
|
||||
| assets/fonts/noto-sans-sc/OFL.txt | Adobe | Unmodified accompanying font licence (https://github.com/notofonts/noto-cjk, Sans/LICENSE) | SIL OFL 1.1 |
|
||||
| assets/fonts/noto-sans-tc/NotoSansTC-Regular.otf | Adobe (Noto Sans TC 2.004, Noto Sans CJK subset) | https://github.com/notofonts/noto-cjk (Sans/SubsetOTF/TC), unmodified; licence text in assets/fonts/noto-sans-tc/OFL.txt; the web app's font for a drawing's traditional Chinese text | SIL OFL 1.1 |
|
||||
| assets/fonts/noto-sans-tc/OFL.txt | Adobe | Unmodified accompanying font licence (https://github.com/notofonts/noto-cjk, Sans/LICENSE) | SIL OFL 1.1 |
|
||||
@@ -1,22 +0,0 @@
|
||||
| assets/icons/ui/design/svg/model/drawing_base_view.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_projected_view.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_section_view.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_detail_view.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_align_views.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_image.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_dimension.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_hole_callout.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_leader.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_datum.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_centre_mark.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_align_annotations.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_space_annotations.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_parts_list.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_balloon.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_edit_table.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_revision_table.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_update.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_check.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_revision.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_print.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/drawing_resources.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
@@ -29,18 +29,6 @@ pub fn mat_mul(a: &Mat, b: &Mat) -> Mat {
|
||||
r
|
||||
}
|
||||
|
||||
/// How much `m` scales a volume: the size of the determinant of its linear part (1 for a rigid
|
||||
/// placement).
|
||||
pub fn mat_volume_scale(m: &Mat) -> f64 {
|
||||
let c = |i: usize| m.get(i).map_or(Vec3::ZERO, |col| Vec3::new(col[0], col[1], col[2]));
|
||||
c(0).dot(c(1).cross(c(2))).abs()
|
||||
}
|
||||
|
||||
/// Why an assembly too large to walk whole is refused.
|
||||
pub(crate) fn too_many_placements() -> crate::DocError {
|
||||
crate::DocError::Invalid(format!("the assembly is deeper than {MAX_PLACEMENT_DEPTH} levels or has more than {MAX_PLACEMENTS} placements"))
|
||||
}
|
||||
|
||||
pub fn apply_point(m: &Mat, p: Vec3) -> Vec3 {
|
||||
Vec3::new(
|
||||
m[0][0] * p.x + m[1][0] * p.y + m[2][0] * p.z + m[3][0],
|
||||
@@ -57,11 +45,6 @@ pub fn apply_vector(m: &Mat, v: Vec3) -> Vec3 {
|
||||
)
|
||||
}
|
||||
|
||||
/// `p` carried by `m`: `None` when `m` flattens it.
|
||||
pub fn map_plane(m: &Mat, p: &Plane) -> Option<Plane> {
|
||||
Plane::new(apply_point(m, p.origin), apply_vector(m, p.x), apply_vector(m, p.y))
|
||||
}
|
||||
|
||||
pub fn is_identity(m: &Mat) -> bool {
|
||||
m.iter().flatten().zip(IDENTITY.iter().flatten()).all(|(a, b)| (a - b).abs() < 1e-12)
|
||||
}
|
||||
@@ -153,10 +136,6 @@ pub struct Occurrence {
|
||||
/// Grounded occurrences don't move.
|
||||
#[serde(default)]
|
||||
pub grounded: bool,
|
||||
/// How this occurrence counts in the bill of materials, in place of its component's type
|
||||
/// (`None`: as its component).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bom_type: Option<BomType>,
|
||||
}
|
||||
|
||||
fn identity() -> Mat {
|
||||
@@ -165,8 +144,7 @@ fn identity() -> Mat {
|
||||
|
||||
/// 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`]. One occurrence can count as another type
|
||||
/// ([`Occurrence::bom_type`]).
|
||||
/// 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 {
|
||||
@@ -210,16 +188,21 @@ impl BomType {
|
||||
pub const MAX_PLACEMENT_DEPTH: usize = 64;
|
||||
pub const MAX_PLACEMENTS: usize = 100_000;
|
||||
|
||||
fn map_plane(m: &Mat, p: &Plane) -> Plane {
|
||||
let (o, x, y) = (apply_point(m, p.origin), apply_vector(m, p.x), apply_vector(m, p.y));
|
||||
Plane::new(o, x, y).unwrap_or(*p)
|
||||
}
|
||||
|
||||
fn map_plane_ref(m: &Mat, p: &mut PlaneRef) {
|
||||
match p {
|
||||
PlaneRef::Custom { plane } => *plane = map_plane(m, plane).unwrap_or(*plane),
|
||||
PlaneRef::Custom { plane } => *plane = map_plane(m, plane),
|
||||
PlaneRef::Placed { origin, x_dir, y_dir } => {
|
||||
map_point_expr(m, origin);
|
||||
*x_dir = apply_vector(m, *x_dir);
|
||||
*y_dir = apply_vector(m, *y_dir);
|
||||
}
|
||||
PlaneRef::Face { plane, at, .. } => {
|
||||
*plane = map_plane(m, plane).unwrap_or(*plane);
|
||||
*plane = map_plane(m, plane);
|
||||
*at = apply_point(m, *at);
|
||||
}
|
||||
PlaneRef::Offset { base, .. } | PlaneRef::Flipped { base, .. } => map_plane_ref(m, base),
|
||||
@@ -341,22 +324,6 @@ impl Document {
|
||||
self.occurrences.iter().find(|o| o.component == component)
|
||||
}
|
||||
|
||||
/// The bill of materials type each occurrence counts as, in the order of `occurrences`: its
|
||||
/// own ([`Occurrence::bom_type`]), else its component's. Components are looked up once.
|
||||
pub fn occurrence_bom_types(&self) -> Vec<BomType> {
|
||||
let types: std::collections::HashMap<u64, BomType> =
|
||||
self.components.iter().map(|c| (c.id, c.metadata.bom_type())).filter(|(_, t)| *t != BomType::Normal).collect();
|
||||
self.occurrences.iter().map(|o| o.bom_type.or_else(|| types.get(&o.component).copied()).unwrap_or_default()).collect()
|
||||
}
|
||||
|
||||
/// The occurrences that count as reference ([`Document::occurrence_bom_types`]). A body placed
|
||||
/// through any of them is a reference part: the bill of materials counts nothing inside a
|
||||
/// reference, and a drawing draws all of it as reference.
|
||||
pub fn reference_occurrences(&self) -> std::collections::HashSet<u64> {
|
||||
let types = self.occurrences.iter().zip(self.occurrence_bom_types());
|
||||
types.filter(|(_, bom_type)| *bom_type == BomType::Reference).map(|(o, _)| o.id).collect()
|
||||
}
|
||||
|
||||
/// World transform of a component's frame (through its first occurrence and its parents').
|
||||
pub fn component_transform(&self, component: u64) -> Mat {
|
||||
transform_through(component, |c| self.occurrence_of(c))
|
||||
@@ -407,45 +374,6 @@ impl Document {
|
||||
out
|
||||
}
|
||||
|
||||
/// Whether [`Document::placements`] reaches every placement. Its walk stops at
|
||||
/// [`MAX_PLACEMENTS`], and a branch past [`MAX_PLACEMENT_DEPTH`] levels, so a placement it
|
||||
/// holds may have occurrences it never walked; the walk is whole when none does.
|
||||
pub fn placements_complete(&self) -> bool {
|
||||
let mut children = std::collections::HashMap::<u64, usize>::new();
|
||||
for o in &self.occurrences {
|
||||
*children.entry(o.parent).or_default() += 1;
|
||||
}
|
||||
let walk = self.placements();
|
||||
let reached = walk.iter().fold(0usize, |n, (c, _, _)| n.saturating_add(children.get(c).copied().unwrap_or(0)));
|
||||
walk.len() == reached.saturating_add(1)
|
||||
}
|
||||
|
||||
/// The placement `path` of `component` at `world`, and every placement below it: (component,
|
||||
/// occurrence path ids, world transform), the start first. Unlike [`Self::placements`], which
|
||||
/// stops a branch where the display does, a larger assembly is refused, never walked in part:
|
||||
/// for totals that must cover all of it (physical properties).
|
||||
pub fn placements_below(&self, component: u64, path: &[u64], world: Mat) -> crate::Result<Vec<(u64, Vec<u64>, Mat)>> {
|
||||
let mut children: std::collections::HashMap<u64, Vec<&Occurrence>> = std::collections::HashMap::new();
|
||||
for o in &self.occurrences {
|
||||
children.entry(o.parent).or_default().push(o);
|
||||
}
|
||||
let mut out = vec![(component, path.to_vec(), world)];
|
||||
let mut stack = vec![(component, path.to_vec(), world)];
|
||||
while let Some((parent, path, world)) = stack.pop() {
|
||||
for o in children.get(&parent).into_iter().flatten() {
|
||||
if path.len() >= MAX_PLACEMENT_DEPTH || out.len() > MAX_PLACEMENTS {
|
||||
return Err(too_many_placements());
|
||||
}
|
||||
let mut child = path.clone();
|
||||
child.push(o.id);
|
||||
let w = mat_mul(&world, &o.transform);
|
||||
out.push((o.component, child.clone(), w));
|
||||
stack.push((o.component, child, w));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Every body as the world shows it: its world name ("Body1", or "Body1 (Occ:2)" for a
|
||||
/// further instance), its component, the occurrence path, and the transform from the frame
|
||||
/// its geometry is in to the world.
|
||||
@@ -512,21 +440,6 @@ impl Document {
|
||||
false
|
||||
}
|
||||
|
||||
/// The virtual components that hold something, by id: a feature (a sketch, a body's, a moved
|
||||
/// body's) or a placed component. A virtual component holds nothing ([`crate::metadata::VIRTUAL`]):
|
||||
/// the engine refuses any change that puts something in one.
|
||||
pub fn virtual_holding(&self) -> Vec<u64> {
|
||||
let virtuals: std::collections::HashSet<u64> = self.components.iter().filter(|c| c.metadata.is_virtual()).map(|c| c.id).collect();
|
||||
if virtuals.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut held: Vec<u64> =
|
||||
self.features.iter().map(|f| f.component).chain(self.occurrences.iter().map(|o| o.parent)).filter(|id| virtuals.contains(id)).collect();
|
||||
held.sort_unstable();
|
||||
held.dedup();
|
||||
held
|
||||
}
|
||||
|
||||
/// Is `c` (or one of its ancestors) marked out of the History bar (#10393)? The root (0)
|
||||
/// never is: its own features always show.
|
||||
pub fn component_history_hidden(&self, mut c: u64) -> bool {
|
||||
@@ -590,7 +503,7 @@ impl Document {
|
||||
!self.occurrences.iter().any(|o| o.name == name)
|
||||
};
|
||||
let k = std::iter::once(next).chain(1..).find(free).unwrap_or(next);
|
||||
self.occurrences.push(crate::Occurrence { id, component, parent, name: format!("{cname}:{k}"), transform, grounded: false, bom_type: None });
|
||||
self.occurrences.push(crate::Occurrence { id, component, parent, name: format!("{cname}:{k}"), transform, grounded: false });
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,964 +0,0 @@
|
||||
//! The bill of materials of an assembly (the design, or one component's definition): the rows a
|
||||
//! bill of materials view lists, typed. `assembly.bom`'s JSON and CSV and the Bill of Materials
|
||||
//! window present these rows; parts lists, item numbers and balloons are to read them too.
|
||||
//!
|
||||
//! [`list`] walks the placements below the assembly once. One rule (`classify`) decides for each
|
||||
//! placement whether it is counted in a row, how what it holds is walked and what its row stands
|
||||
//! for when weighed, from the bill of materials type it counts as (its occurrence's, else its
|
||||
//! component's), the view and the component's shape. One budget of [`MAX_PLACEMENTS`] placements
|
||||
//! and [`MAX_PLACEMENT_DEPTH`] levels covers the whole list, folded parts' contents included: a
|
||||
//! larger assembly is refused, never listed in part.
|
||||
//!
|
||||
//! A row is named by its `RowKey`: the rows it is listed under and its lowest member (a
|
||||
//! component and the type it counts as there), however its part number or contents read. Its
|
||||
//! mass is what one of its parts weighs as the row stands for it (`Holds`), so the rows of any
|
||||
//! list add up to the assembly once. The keys, what rows stand for and their members stay inside
|
||||
//! this crate: a balloon reads its row's item ([`Bom::item_of`]), and item numbers are saved by key ([`numbers`]). Placements are rigid, so
|
||||
//! each definition is weighed once however often it is placed, and kept in the [`Tree`] for every list made from it. The
|
||||
//! assembly's own mass is weighed the same way, never added up from rows: a row whose definitions
|
||||
//! weigh differently has none.
|
||||
//!
|
||||
//! Materials are bodies' (`material.assign`): a row names the materials of the bodies it stands for,
|
||||
//! and a row that stands for an assembly names one only when everything in it shares it. A static
|
||||
//! material typed in for a component ([`metadata::MATERIAL`](crate::metadata::MATERIAL)) is what
|
||||
//! its rows say instead, in every view and at every level ([`Row::statics`]), and what it stands
|
||||
//! for in the one material an assembly around it shares: the bill of materials says one thing
|
||||
//! about the part everywhere. It only names: mass still comes from the bodies.
|
||||
//!
|
||||
//! A row's [`Quantity`] adds up what each of its placements means ([`quantity`]): each definition's
|
||||
//! quantity is evaluated once for every list made from the [`Tree`].
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{BomType, Component, DocError, Document, MAX_PLACEMENT_DEPTH, MAX_PLACEMENTS, ModelBody, ModelState, Result};
|
||||
|
||||
mod numbers;
|
||||
mod quantity;
|
||||
|
||||
pub use numbers::{ItemNumber, ItemNumbers, MAX_ITEM, Numbering, Order, number, record};
|
||||
pub use quantity::{Amount, Quantity, Uncounted, amount};
|
||||
|
||||
/// A bill of materials view, saved by the word commands use for it.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum View {
|
||||
/// Parts only: every part at any depth, counted through its subassemblies.
|
||||
#[default]
|
||||
#[serde(rename = "flat")]
|
||||
PartsOnly,
|
||||
/// Structured, first level: the assembly's direct contents, a subassembly as one row.
|
||||
#[serde(rename = "top")]
|
||||
FirstLevel,
|
||||
/// Structured, all levels: the assembly's direct contents, each subassembly a row with its own
|
||||
/// contents listed under it, to any depth (1, 1.1, 1.1.1).
|
||||
#[serde(rename = "all")]
|
||||
AllLevels,
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Every view, in the order commands and documentation list them.
|
||||
pub const ALL: [View; 3] = [View::PartsOnly, View::FirstLevel, View::AllLevels];
|
||||
|
||||
/// The word commands use (`assembly.bom`'s `levels`).
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
View::PartsOnly => "flat",
|
||||
View::FirstLevel => "top",
|
||||
View::AllLevels => "all",
|
||||
}
|
||||
}
|
||||
|
||||
/// The view a word names, if it names one.
|
||||
pub fn parse(word: &str) -> Option<View> {
|
||||
View::ALL.into_iter().find(|v| v.as_str() == word)
|
||||
}
|
||||
}
|
||||
|
||||
/// What one row gathers.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Group {
|
||||
/// The definitions that share a part number; a definition without one is a row of its own.
|
||||
#[default]
|
||||
PartNumber,
|
||||
/// One definition.
|
||||
Definition,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
/// Every grouping, in the order commands and documentation list them.
|
||||
pub const ALL: [Group; 2] = [Group::PartNumber, Group::Definition];
|
||||
|
||||
/// The word commands use (`assembly.bom`'s `group_by`).
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Group::PartNumber => "part_number",
|
||||
Group::Definition => "definition",
|
||||
}
|
||||
}
|
||||
|
||||
/// The grouping a word names, if it names one.
|
||||
pub fn parse(word: &str) -> Option<Group> {
|
||||
Group::ALL.into_iter().find(|g| g.as_str() == word)
|
||||
}
|
||||
}
|
||||
|
||||
/// What a bill of materials lists. The default is the design's parts only, by part number.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Request {
|
||||
/// The assembly listed: a component, or 0 for the design.
|
||||
pub source: u64,
|
||||
pub view: View,
|
||||
pub group: Group,
|
||||
/// A normal part with a part number is one row, and what it holds is listed under it as its
|
||||
/// children (as parts only lists them) instead of as rows of their own.
|
||||
pub fold: bool,
|
||||
/// Weigh each row and the assembly: every part is measured, so it is asked for.
|
||||
pub mass: bool,
|
||||
/// In all levels, what a purchased part holds is listed under it too (it is still one row,
|
||||
/// weighed whole).
|
||||
pub purchased_contents: bool,
|
||||
}
|
||||
|
||||
/// A component as one placement counts: its id and the bill of materials type it counts as there.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct Member {
|
||||
pub component: u64,
|
||||
pub bom_type: BomType,
|
||||
}
|
||||
|
||||
/// A row's identity: the key members of the rows it is listed under (outermost first; none at the
|
||||
/// top), and its own lowest member. It doesn't change with the row's part number or contents, so
|
||||
/// item numbers, overrides and balloons can follow the row through those edits.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub(crate) struct RowKey {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub(crate) parents: Vec<Member>,
|
||||
pub(crate) member: Member,
|
||||
}
|
||||
|
||||
/// What one of a row's parts stands for when it is weighed, so that the rows of any list add up
|
||||
/// to the assembly once.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub(crate) enum Holds {
|
||||
/// Its own bodies and everything placed inside it.
|
||||
All,
|
||||
/// Its own bodies alone: what it holds is listed beside it.
|
||||
Own,
|
||||
/// Everything but its purchased parts, which are listed beside it (an inseparable part in
|
||||
/// parts only).
|
||||
AllButPurchased,
|
||||
}
|
||||
|
||||
/// What one of a row's parts weighs, or the assembly.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub enum Mass {
|
||||
/// Not asked for ([`Request::mass`]).
|
||||
#[default]
|
||||
NotAsked,
|
||||
Grams(f64),
|
||||
Unknown(Unknown),
|
||||
}
|
||||
|
||||
/// Why a mass isn't known.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Unknown {
|
||||
/// Nothing in it has a body.
|
||||
NothingToWeigh,
|
||||
/// A body in it can't be measured, or measures inside out (a negative volume).
|
||||
Unmeasurable,
|
||||
/// The definitions the row gathers (one part number) weigh differently.
|
||||
Differs,
|
||||
}
|
||||
|
||||
/// A value of a row typed in by hand for its component, in place of the one worked out.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Static {
|
||||
/// [`Row::materials`], from [`metadata::MATERIAL`](crate::metadata::MATERIAL).
|
||||
Material,
|
||||
}
|
||||
|
||||
impl Static {
|
||||
/// The part property it is kept under, and its name in `assembly.bom`'s rows.
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Static::Material => crate::metadata::MATERIAL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One row of a bill of materials view.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Row {
|
||||
pub(crate) key: RowKey,
|
||||
/// Its item number among the rows it is listed with: the one saved for it, else its position
|
||||
/// from 1 (the view not numbered yet) or the next number after the saved ones ([`numbers`]).
|
||||
pub item: u32,
|
||||
/// Its item number isn't saved yet.
|
||||
pub provisional: bool,
|
||||
/// Its item number is locked: a renumber keeps it.
|
||||
pub locked: bool,
|
||||
/// Another row listed with it has the same item number.
|
||||
pub duplicate: bool,
|
||||
pub bom_type: BomType,
|
||||
/// What one of its parts stands for when weighed.
|
||||
pub(crate) stands_for: Holds,
|
||||
/// The definitions it gathers, by id (ascending), and their names, each once.
|
||||
pub components: Vec<u64>,
|
||||
pub names: Vec<String>,
|
||||
/// Its part properties: its first definition's part number (the one it gathers by), and the
|
||||
/// first description and revision its definitions give.
|
||||
pub part_number: String,
|
||||
pub description: String,
|
||||
pub revision: String,
|
||||
/// The definitions' free properties; where several give one name, the first's.
|
||||
pub properties: BTreeMap<String, String>,
|
||||
/// The physical materials of what one of its parts stands for, each once, in body order: a
|
||||
/// part's own bodies'. A row that stands for an assembly (something placed inside it is weighed
|
||||
/// with it) shows the one material all of that shares, else none. A static material replaces
|
||||
/// them.
|
||||
pub materials: Vec<String>,
|
||||
/// Its values typed in by hand ([`Static`]). Where its definitions give different ones, the
|
||||
/// first's that gives one wins, as with its description.
|
||||
pub statics: Vec<Static>,
|
||||
/// How many are listed: in all, and in one of the row it is listed under (the assembly at the
|
||||
/// top).
|
||||
pub count: u64,
|
||||
pub each: u64,
|
||||
/// What its placements add up to, when its definitions have quantities.
|
||||
pub quantity: Quantity,
|
||||
/// The placements it counts, by occurrence path from the assembly listed, in path order;
|
||||
/// empty unless the list was made with them (`list_with`).
|
||||
pub(crate) members: Vec<Vec<u64>>,
|
||||
pub mass: Mass,
|
||||
/// What one of it holds, listed under it: a folded part's contents as parts only lists them,
|
||||
/// or in all levels a subassembly's own rows.
|
||||
pub children: Vec<Row>,
|
||||
}
|
||||
|
||||
/// The assembly as a whole.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Total {
|
||||
/// Every placement listed, children included.
|
||||
pub occurrences: u64,
|
||||
/// Its own bodies and all it holds but references, weighed from the design (with the rows'
|
||||
/// measurements), not added up from rows.
|
||||
pub mass: Mass,
|
||||
}
|
||||
|
||||
/// The rows of one bill of materials view.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Bom {
|
||||
/// The assembly listed: a component, or 0 for the design.
|
||||
pub source: u64,
|
||||
/// By part number, rows without one after them by name, one part's rows by type (`list_order`);
|
||||
/// each row's children likewise. In a numbered view, by item number.
|
||||
pub rows: Vec<Row>,
|
||||
pub total: Total,
|
||||
/// Its item numbers are saved: it has been numbered ([`ItemNumbers`]).
|
||||
pub numbered: bool,
|
||||
/// Each member path, and where its row is (its position among the rows, then among each one's
|
||||
/// children): `row_of` reads it. Empty unless the list was made with its members.
|
||||
index: HashMap<Vec<u64>, Vec<usize>>,
|
||||
}
|
||||
|
||||
/// Why a placement is counted in no row of a list.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Unlisted {
|
||||
/// No placement below the assembly listed has that path.
|
||||
NotFound,
|
||||
/// It counts as a reference, or is inside one: no bill of materials counts it.
|
||||
Reference,
|
||||
/// It has no row of its own and isn't inside one: a phantom without bodies, or in parts only
|
||||
/// a subassembly listed through its parts (the assembly listed, an empty path, is none either).
|
||||
NoRow,
|
||||
}
|
||||
|
||||
impl Bom {
|
||||
/// Every row, each followed by the rows listed under it (1, 1.1, 1.2, 2…), with its item
|
||||
/// numbers from the top (`[1, 2]` reads 1.2).
|
||||
pub fn rows_deep(&self) -> Vec<(Vec<u32>, &Row)> {
|
||||
let mut out = Vec::new();
|
||||
let mut stack: Vec<(Vec<u32>, &Row)> = self.rows.iter().rev().map(|r| (vec![r.item], r)).collect();
|
||||
while let Some((item, row)) = stack.pop() {
|
||||
for child in row.children.iter().rev() {
|
||||
let mut under = item.clone();
|
||||
under.push(child.item);
|
||||
stack.push((under, child));
|
||||
}
|
||||
out.push((item, row));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The row a placement is counted in, by its occurrence path from the assembly listed: the row
|
||||
/// that lists it, else the row of the part it is inside (a nut inside a purchased bolt is
|
||||
/// counted in the bolt's). `tree` is the one the list was made from, with its members
|
||||
/// (`list_with`).
|
||||
pub(crate) fn row_of(&self, tree: &Tree, path: &[u64]) -> std::result::Result<(Vec<u32>, &Row), Unlisted> {
|
||||
if path.len() > MAX_PLACEMENT_DEPTH {
|
||||
return Err(Unlisted::NotFound);
|
||||
}
|
||||
let mut component = self.source;
|
||||
for id in path {
|
||||
let placed = tree.contents(component).iter().find(|p| p.occurrence == *id).ok_or(Unlisted::NotFound)?;
|
||||
if placed.member.bom_type == BomType::Reference {
|
||||
return Err(Unlisted::Reference);
|
||||
}
|
||||
component = placed.member.component;
|
||||
}
|
||||
// Each placement is in one row or none, so the longest member path it starts with is its row's.
|
||||
let at = (1..=path.len()).rev().find_map(|n| path.get(..n).and_then(|start| self.index.get(start)));
|
||||
at.and_then(|at| self.row_at(at)).ok_or(Unlisted::NoRow)
|
||||
}
|
||||
|
||||
/// The row at `at` (its position among the rows, then among each one's children), with its item
|
||||
/// numbers from the top.
|
||||
fn row_at(&self, at: &[usize]) -> Option<(Vec<u32>, &Row)> {
|
||||
let (first, rest) = at.split_first()?;
|
||||
let mut row = self.rows.get(*first)?;
|
||||
let mut item = vec![row.item];
|
||||
for &i in rest {
|
||||
row = row.children.get(i)?;
|
||||
item.push(row.item);
|
||||
}
|
||||
Some((item, row))
|
||||
}
|
||||
|
||||
/// The item ("1.2") of the row a placement is counted in, by its occurrence path from the top of
|
||||
/// the design: a balloon's, which is cut to start below the assembly listed. A path that doesn't
|
||||
/// run through that assembly is [`Unlisted::NotFound`]; through one of its placements, whichever,
|
||||
/// it reads the same row. `tree` is as for `row_of`.
|
||||
pub fn item_of(&self, tree: &Tree, path: &[u64]) -> std::result::Result<String, Unlisted> {
|
||||
let below = tree.below(self.source, path).ok_or(Unlisted::NotFound)?;
|
||||
self.row_of(tree, below).map(|(item, _)| item_text(&item))
|
||||
}
|
||||
|
||||
/// Where each member's row is, for `row_of`.
|
||||
fn index_members(&mut self) {
|
||||
let mut index = HashMap::new();
|
||||
let mut stack: Vec<(Vec<usize>, &Row)> = self.rows.iter().enumerate().map(|(i, r)| (vec![i], r)).collect();
|
||||
while let Some((at, row)) = stack.pop() {
|
||||
for member in &row.members {
|
||||
index.insert(member.clone(), at.clone());
|
||||
}
|
||||
for (i, child) in row.children.iter().enumerate() {
|
||||
let mut under = at.clone();
|
||||
under.push(i);
|
||||
stack.push((under, child));
|
||||
}
|
||||
}
|
||||
self.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
/// Item numbers as one reads them: `[1, 2]` is "1.2".
|
||||
pub fn item_text(item: &[u32]) -> String {
|
||||
item.iter().map(u32::to_string).collect::<Vec<_>>().join(".")
|
||||
}
|
||||
|
||||
/// The design's structure as bills of materials read it, looked up once, and what each
|
||||
/// definition weighs, measured once for every list made from it. It reads one state of the
|
||||
/// design: make another after a change.
|
||||
pub struct Tree<'a> {
|
||||
doc: &'a Document,
|
||||
/// What each component places (by its id; 0 is the root), in occurrence order.
|
||||
children: HashMap<u64, Vec<Placed>>,
|
||||
components: HashMap<u64, &'a Component>,
|
||||
/// Each component's own bodies, in body order; a component that owns none isn't here.
|
||||
bodies: HashMap<u64, Vec<&'a ModelBody>>,
|
||||
/// What one of each definition weighs, by what it stands for.
|
||||
weighed: BTreeMap<(u64, Holds), Weighed>,
|
||||
/// The materials of what one of each definition stands for, each once, in body order.
|
||||
made_of: BTreeMap<(u64, Holds), Vec<String>>,
|
||||
/// What one placement of each definition with a quantity counts: `None` when it doesn't
|
||||
/// evaluate.
|
||||
amounts: HashMap<u64, Option<Amount>>,
|
||||
}
|
||||
|
||||
/// One occurrence: its id, and the component it places with the type it counts as there.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Placed {
|
||||
occurrence: u64,
|
||||
member: Member,
|
||||
}
|
||||
|
||||
/// What one of a component weighs: grams, and how many bodies that is (none: nothing to weigh).
|
||||
/// `None` when a body in it can't be measured, or measures inside out.
|
||||
type Weighed = Option<(f64, usize)>;
|
||||
|
||||
impl<'a> Tree<'a> {
|
||||
/// The structure of `doc` with the bodies of `state`, its evaluated model.
|
||||
pub fn new(doc: &'a Document, state: &'a ModelState) -> Tree<'a> {
|
||||
let mut children: HashMap<u64, Vec<Placed>> = HashMap::new();
|
||||
for (o, bom_type) in doc.occurrences.iter().zip(doc.occurrence_bom_types()) {
|
||||
children.entry(o.parent).or_default().push(Placed { occurrence: o.id, member: Member { component: o.component, bom_type } });
|
||||
}
|
||||
let mut components: HashMap<u64, &Component> = HashMap::new();
|
||||
for c in &doc.components {
|
||||
components.entry(c.id).or_insert(c);
|
||||
}
|
||||
let mut bodies: HashMap<u64, Vec<&ModelBody>> = HashMap::new();
|
||||
for (body, owner) in state.bodies.iter().zip(doc.model_body_components(state)) {
|
||||
bodies.entry(owner).or_default().push(body);
|
||||
}
|
||||
let values = std::cell::OnceCell::new();
|
||||
let amounts = components.values().filter_map(|c| Some((c.id, quantity::amount_in(doc, &values, c.metadata.bom_quantity()?).ok()))).collect();
|
||||
Tree { doc, children, components, bodies, weighed: BTreeMap::new(), made_of: BTreeMap::new(), amounts }
|
||||
}
|
||||
|
||||
/// What a component places.
|
||||
fn contents(&self, id: u64) -> &[Placed] {
|
||||
self.children.get(&id).map_or(&[], Vec::as_slice)
|
||||
}
|
||||
|
||||
/// What of `path`, from the top of the design, runs below `source`'s first placement on it.
|
||||
fn below<'p>(&self, source: u64, path: &'p [u64]) -> Option<&'p [u64]> {
|
||||
if source == 0 {
|
||||
return Some(path);
|
||||
}
|
||||
let mut component = 0;
|
||||
for (i, id) in path.iter().enumerate() {
|
||||
component = self.contents(component).iter().find(|p| p.occurrence == *id)?.member.component;
|
||||
if component == source {
|
||||
return path.get(i + 1..);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The components placed in `id` whose parts are part of what `id` stands for as `holds` says
|
||||
/// (see `adds`), one for each placement, with the type each counts as there.
|
||||
fn adding(&self, id: u64, holds: Holds) -> Vec<Member> {
|
||||
self.contents(id).iter().filter(|p| adds(holds, p.member.bom_type)).map(|p| p.member).collect()
|
||||
}
|
||||
|
||||
/// The static material typed in for a component, if any.
|
||||
fn static_material(&self, id: u64) -> Option<&'a str> {
|
||||
self.components.get(&id).and_then(|c| c.metadata.material())
|
||||
}
|
||||
|
||||
fn own_bodies(&self, id: u64) -> &[&'a ModelBody] {
|
||||
self.bodies.get(&id).map_or(&[], Vec::as_slice)
|
||||
}
|
||||
|
||||
/// What a row of `member` gathers by: its part number when `group` asks for that and it has
|
||||
/// one, else its definition. `None` for a damaged file's occurrence of a component that isn't
|
||||
/// there, which has no row.
|
||||
fn part(&self, member: Member, group: Group) -> Option<Part<'a>> {
|
||||
let c: &'a Component = self.components.get(&member.component)?;
|
||||
Some(match c.metadata.part_number() {
|
||||
Some(pn) if group == Group::PartNumber => Part::Number(pn),
|
||||
_ => Part::Definition(member.component),
|
||||
})
|
||||
}
|
||||
|
||||
/// The rows of one level: the placements `at` (positions in `entries`), gathered as `group`
|
||||
/// says, listed under the rows `parents` names, `per` of them in all (1 for the assembly).
|
||||
/// The rows under these are gathered as `under` says.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn level(&self, listing: &Listing, paths: &mut [Vec<u64>], at: &[usize], group: Group, under: Group, parents: &[Member], per: u64) -> Vec<Row> {
|
||||
let mut grouped: BTreeMap<GroupKey<'a>, Vec<usize>> = BTreeMap::new();
|
||||
for &i in at {
|
||||
let Some(entry) = listing.entries.get(i) else { continue };
|
||||
let Some(part) = self.part(entry.member, group) else { continue };
|
||||
let kit = listing.kits.get(i).copied().unwrap_or_default();
|
||||
grouped.entry(GroupKey { part, bom_type: entry.member.bom_type, kit }).or_default().push(i);
|
||||
}
|
||||
let mut rows: Vec<Row> = grouped.values().filter_map(|members| self.row(listing, paths, members, under, parents, per)).collect();
|
||||
rows.sort_by(list_order);
|
||||
for (item, row) in (1..).zip(&mut rows) {
|
||||
row.item = item;
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
/// Each entry's kit, as a number: what it holds as its row's children read it, to any depth.
|
||||
/// Two entries have the same kit when what they hold is the same parts (by part number, else
|
||||
/// definition), counted as the same types, each holding the same again all the way down. One
|
||||
/// pass from the last entry back: an entry's children come after it, so theirs are known first.
|
||||
fn kits(&self, entries: &[Entry], kids: &[Vec<usize>]) -> Vec<u32> {
|
||||
let mut kit_ids: HashMap<Vec<u32>, u32> = HashMap::new();
|
||||
let mut placed_ids: HashMap<(Option<Part<'a>>, BomType, u32), u32> = HashMap::new();
|
||||
// Each entry as a child reads: its part, its type and its own kit.
|
||||
let mut placed = vec![0u32; entries.len()];
|
||||
let mut kits = vec![0u32; entries.len()];
|
||||
for (i, entry) in entries.iter().enumerate().rev() {
|
||||
let mut held: Vec<u32> = kids.get(i).into_iter().flatten().filter_map(|&k| placed.get(k).copied()).collect();
|
||||
held.sort_unstable();
|
||||
let next = kit_ids.len() as u32;
|
||||
let kit = *kit_ids.entry(held).or_insert(next);
|
||||
let next = placed_ids.len() as u32;
|
||||
let key = (self.part(entry.member, Group::PartNumber), entry.member.bom_type, kit);
|
||||
if let (Some(k), Some(p)) = (kits.get_mut(i), placed.get_mut(i)) {
|
||||
*k = kit;
|
||||
*p = *placed_ids.entry(key).or_insert(next);
|
||||
}
|
||||
}
|
||||
kits
|
||||
}
|
||||
|
||||
/// One row: the placements `members` (positions in `entries`) of one part, type and kit, with
|
||||
/// the rows under it gathered as `under` says. Their `paths` (when the list keeps them) move into
|
||||
/// the row: each placement is in one row.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn row(&self, listing: &Listing, paths: &mut [Vec<u64>], members: &[usize], under: Group, parents: &[Member], per: u64) -> Option<Row> {
|
||||
let listed: Vec<&Entry> = members.iter().filter_map(|&i| listing.entries.get(i)).collect();
|
||||
let first = listed.first()?;
|
||||
let mut components: Vec<u64> = listed.iter().map(|e| e.member.component).collect();
|
||||
components.sort_unstable();
|
||||
components.dedup();
|
||||
let member = Member { component: *components.first()?, bom_type: first.member.bom_type };
|
||||
let count = listed.len() as u64;
|
||||
let mut taken: Vec<Vec<u64>> = members.iter().filter_map(|&i| paths.get_mut(i).map(std::mem::take)).filter(|p| !p.is_empty()).collect();
|
||||
taken.sort_unstable();
|
||||
// What one of it holds is listed under it.
|
||||
let inside: Vec<usize> = members.iter().flat_map(|&i| listing.kids.get(i).into_iter().flatten().copied()).collect();
|
||||
let mut chain = parents.to_vec();
|
||||
chain.push(member);
|
||||
let mut row = Row {
|
||||
key: RowKey { parents: parents.to_vec(), member },
|
||||
item: 0,
|
||||
provisional: true,
|
||||
locked: false,
|
||||
duplicate: false,
|
||||
bom_type: member.bom_type,
|
||||
stands_for: first.holds,
|
||||
// Filled in by `name_materials`, which reads each definition once.
|
||||
materials: Vec::new(),
|
||||
statics: Vec::new(),
|
||||
components,
|
||||
names: Vec::new(),
|
||||
part_number: String::new(),
|
||||
description: String::new(),
|
||||
revision: String::new(),
|
||||
properties: BTreeMap::new(),
|
||||
count,
|
||||
// Rows with different kits are different rows, to any depth, so every part of the parent
|
||||
// row holds as many.
|
||||
each: count / per.max(1),
|
||||
quantity: quantity::add_up(listed.iter().map(|e| self.amounts.get(&e.member.component))),
|
||||
members: taken,
|
||||
mass: Mass::NotAsked,
|
||||
children: self.level(listing, paths, &inside, under, under, &chain, count),
|
||||
};
|
||||
for (i, c) in row.components.iter().filter_map(|id| self.components.get(id)).enumerate() {
|
||||
let m = &c.metadata;
|
||||
if i == 0 {
|
||||
row.part_number = m.part_number().unwrap_or_default().to_string();
|
||||
}
|
||||
if !row.names.contains(&c.name) {
|
||||
row.names.push(c.name.clone());
|
||||
}
|
||||
if row.description.is_empty() {
|
||||
row.description = m.description().unwrap_or_default().to_string();
|
||||
}
|
||||
if row.revision.is_empty() {
|
||||
row.revision = m.revision().unwrap_or_default().to_string();
|
||||
}
|
||||
for (k, v) in &c.properties {
|
||||
row.properties.entry(k.clone()).or_insert_with(|| v.clone());
|
||||
}
|
||||
}
|
||||
Some(row)
|
||||
}
|
||||
|
||||
/// Each row's materials, and its children's (Cameron, 2026-10-23): a part lists its own bodies';
|
||||
/// a row that stands for an assembly shows a material only when everything it stands for
|
||||
/// shares it, so a frame of steel and aluminium parts reads blank and a bought bearing of steel
|
||||
/// parts reads steel. A static material, the first its definitions give, replaces that.
|
||||
fn name_materials(&mut self, rows: &mut [Row]) -> Result<()> {
|
||||
for row in rows {
|
||||
self.name_materials(&mut row.children)?;
|
||||
let typed_in = row.components.iter().find_map(|&id| self.static_material(id));
|
||||
if let Some(material) = typed_in {
|
||||
row.materials = vec![material.to_string()];
|
||||
row.statics.push(Static::Material);
|
||||
continue;
|
||||
}
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
let mut assembly = false;
|
||||
for &c in &row.components {
|
||||
assembly |= row.stands_for != Holds::Own && !self.adding(c, row.stands_for).is_empty();
|
||||
for name in self.made_of(c, row.stands_for, 0)? {
|
||||
push_once(&mut names, name);
|
||||
}
|
||||
}
|
||||
row.materials = if assembly && names.len() > 1 { Vec::new() } else { names };
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The materials of what one of `id` stands for as `holds` says, each once, in body order: the
|
||||
/// bodies `weigh` weighs. A static material stands for all of a part placed inside it, and for
|
||||
/// a phantom's own bodies only (a phantom's row is its own bodies, so what it holds keeps its
|
||||
/// own). Each definition is read once for each kind of row; `depth` bounds the walk below a
|
||||
/// definition not read yet.
|
||||
fn made_of(&mut self, id: u64, holds: Holds, depth: usize) -> Result<Vec<String>> {
|
||||
if let Some(names) = self.made_of.get(&(id, holds)) {
|
||||
return Ok(names.clone());
|
||||
}
|
||||
if depth > MAX_PLACEMENT_DEPTH {
|
||||
return Err(too_big());
|
||||
}
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
match self.static_material(id) {
|
||||
Some(material) if !self.own_bodies(id).is_empty() => push_once(&mut names, material.to_string()),
|
||||
Some(_) => {}
|
||||
None => {
|
||||
for body in self.own_bodies(id) {
|
||||
push_once(&mut names, self.doc.body_material(&body.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
if holds != Holds::Own {
|
||||
for child in self.adding(id, holds) {
|
||||
match self.static_material(child.component) {
|
||||
Some(material) if child.bom_type != BomType::Phantom => push_once(&mut names, material.to_string()),
|
||||
_ => {
|
||||
for name in self.made_of(child.component, holds, depth + 1)? {
|
||||
push_once(&mut names, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.made_of.insert((id, holds), names.clone());
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Each row's mass, and its children's.
|
||||
fn weigh_rows(&mut self, rows: &mut [Row]) -> Result<()> {
|
||||
for row in rows {
|
||||
let weighed = row.components.iter().map(|&c| self.weigh(c, row.stands_for, 0)).collect::<Result<Vec<Weighed>>>()?;
|
||||
row.mass = agree(&weighed);
|
||||
self.weigh_rows(&mut row.children)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One of `id` as `holds` says: its own bodies, and of what is placed inside it everything that
|
||||
/// adds to it (see `adds`). Where a part is placed doesn't change what it weighs, so each
|
||||
/// definition is weighed once for each kind of row and each body measured once. `depth` bounds
|
||||
/// the walk below a definition not weighed yet.
|
||||
fn weigh(&mut self, id: u64, holds: Holds, depth: usize) -> Result<Weighed> {
|
||||
if let Some(&weighed) = self.weighed.get(&(id, holds)) {
|
||||
return Ok(weighed);
|
||||
}
|
||||
if depth > MAX_PLACEMENT_DEPTH {
|
||||
return Err(too_big());
|
||||
}
|
||||
let mut total: Weighed = Some((0.0, 0));
|
||||
if holds == Holds::Own {
|
||||
for body in self.own_bodies(id) {
|
||||
add(&mut total, self.measure(body));
|
||||
}
|
||||
} else {
|
||||
add(&mut total, self.weigh(id, Holds::Own, depth)?);
|
||||
for child in self.adding(id, holds) {
|
||||
add(&mut total, self.weigh(child.component, holds, depth + 1)?);
|
||||
}
|
||||
}
|
||||
let weighed = total.filter(|(grams, _)| grams.is_finite());
|
||||
self.weighed.insert((id, holds), weighed);
|
||||
Ok(weighed)
|
||||
}
|
||||
|
||||
/// What one body weighs, in grams: its volume, measured once for the model
|
||||
/// ([`ModelBody::volume`]), scaled by a body offset as the offset scales what is drawn (a
|
||||
/// damaged file's scaled offset weighs what is drawn).
|
||||
fn measure(&self, body: &ModelBody) -> Weighed {
|
||||
let scale = self.doc.model_body_offset(body).map_or(1.0, crate::mat_volume_scale);
|
||||
// A negative volume (a body inside out) is unknown, never a negative mass.
|
||||
body.volume().map(|v| v * scale).filter(|v| *v >= 0.0).map(|v| (v * self.doc.density(&body.name) / 1000.0, 1))
|
||||
}
|
||||
}
|
||||
|
||||
/// The order a level lists its rows in until it is numbered: by part number; rows without one
|
||||
/// after them, by name; one part's rows by type.
|
||||
fn list_order(a: &Row, b: &Row) -> Ordering {
|
||||
(a.part_number.is_empty(), &a.part_number, &a.names, &a.components, a.bom_type).cmp(&(
|
||||
b.part_number.is_empty(),
|
||||
&b.part_number,
|
||||
&b.names,
|
||||
&b.components,
|
||||
b.bom_type,
|
||||
))
|
||||
}
|
||||
|
||||
/// What a row gathers by: a part number, borrowed from its definition, or the definition.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
enum Part<'a> {
|
||||
Number(&'a str),
|
||||
Definition(u64),
|
||||
}
|
||||
|
||||
/// How one row is told from another of its level: what it gathers by, the type its parts count
|
||||
/// as, and its kit: what one of them holds as the row's children read it, to any depth (`Tree::kits`;
|
||||
/// the same for all that hold nothing listed).
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct GroupKey<'a> {
|
||||
part: Part<'a>,
|
||||
bom_type: BomType,
|
||||
kit: u32,
|
||||
}
|
||||
|
||||
/// The placements a list counts, what each folded or all-levels row holds, and each one's kit.
|
||||
struct Listing {
|
||||
entries: Vec<Entry>,
|
||||
/// What each entry holds: the entries listed as its children.
|
||||
kids: Vec<Vec<usize>>,
|
||||
kits: Vec<u32>,
|
||||
}
|
||||
|
||||
/// How a walk lists what it passes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Walk {
|
||||
/// What the view lists.
|
||||
Listed,
|
||||
/// Inside an inseparable part in parts only: just its purchased parts, which are listed
|
||||
/// beside it, at any depth.
|
||||
PurchasedOnly,
|
||||
}
|
||||
|
||||
/// What a placed component is like, as far as listing it goes.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct Shape {
|
||||
/// It places components of its own.
|
||||
contents: bool,
|
||||
/// It has bodies of its own.
|
||||
bodies: bool,
|
||||
/// It is a part with a part number in a list that folds.
|
||||
folded: bool,
|
||||
/// What it holds is listed under it if it is purchased ([`Request::purchased_contents`], all
|
||||
/// levels).
|
||||
open_purchased: bool,
|
||||
}
|
||||
|
||||
/// How one placement counts.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct Step {
|
||||
/// It is counted in a row here.
|
||||
row: bool,
|
||||
/// How what it holds is walked (`None`: not at all).
|
||||
inside: Option<Walk>,
|
||||
/// What it holds is listed as its row's children.
|
||||
kit: bool,
|
||||
/// What its row stands for when weighed.
|
||||
holds: Holds,
|
||||
}
|
||||
|
||||
/// How a placement counts in `view`: the one rule every list and every mass read. A reference
|
||||
/// counts nowhere and nothing inside it does; a phantom's own bodies, when it has any, are a row of
|
||||
/// their own (as a subassembly's are in parts only) and what it holds counts where it is placed; a purchased part is one row with nothing inside it listed (in all levels, its
|
||||
/// contents under it when asked); an inseparable part is one row, and in parts only its purchased
|
||||
/// parts are listed beside it; a normal part is a row in the structured views, in all levels with
|
||||
/// what it holds under it, and in parts only a subassembly is listed through its parts unless it
|
||||
/// has bodies of its own. A folded part is one row holding all it holds, what it holds listed under
|
||||
/// it (of an inseparable one, its purchased parts).
|
||||
fn classify(view: View, walk: Walk, bom_type: BomType, shape: Shape) -> Step {
|
||||
let nothing = Step { row: false, inside: None, kit: false, holds: Holds::All };
|
||||
let row = |holds| Step { row: true, holds, ..nothing };
|
||||
let contents = |walk| shape.contents.then_some(walk);
|
||||
match (bom_type, walk) {
|
||||
(BomType::Reference, _) => nothing,
|
||||
(BomType::Phantom, Walk::Listed) => Step { row: shape.bodies, inside: Some(Walk::Listed), kit: false, holds: Holds::Own },
|
||||
(BomType::Phantom, Walk::PurchasedOnly) => Step { inside: Some(walk), ..nothing },
|
||||
(BomType::Purchased, Walk::Listed) if view == View::AllLevels && shape.open_purchased => {
|
||||
Step { inside: contents(Walk::Listed), kit: true, ..row(Holds::All) }
|
||||
}
|
||||
(BomType::Purchased, _) => row(Holds::All),
|
||||
(_, Walk::PurchasedOnly) => Step { inside: contents(Walk::PurchasedOnly), ..nothing },
|
||||
(BomType::Inseparable, Walk::Listed) if shape.folded => Step { inside: contents(Walk::PurchasedOnly), kit: true, ..row(Holds::All) },
|
||||
(BomType::Inseparable, Walk::Listed) => match view {
|
||||
View::PartsOnly => Step { inside: contents(Walk::PurchasedOnly), ..row(Holds::AllButPurchased) },
|
||||
View::FirstLevel | View::AllLevels => row(Holds::All),
|
||||
},
|
||||
(BomType::Normal, Walk::Listed) if shape.folded => Step { inside: contents(Walk::Listed), kit: true, ..row(Holds::All) },
|
||||
(BomType::Normal, Walk::Listed) => match view {
|
||||
View::PartsOnly => Step { row: !shape.contents || shape.bodies, inside: contents(Walk::Listed), kit: false, holds: Holds::Own },
|
||||
View::FirstLevel => row(Holds::All),
|
||||
View::AllLevels => Step { inside: contents(Walk::Listed), kit: true, ..row(Holds::All) },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a placement of `bom_type` inside a part adds to what the part's row stands for: a
|
||||
/// reference adds nothing, and what the list shows beside the row (what `classify` lists inside
|
||||
/// an inseparable part) isn't weighed with it.
|
||||
fn adds(holds: Holds, bom_type: BomType) -> bool {
|
||||
match holds {
|
||||
Holds::Own => false,
|
||||
Holds::All => bom_type != BomType::Reference,
|
||||
Holds::AllButPurchased => bom_type != BomType::Reference && !classify(View::PartsOnly, Walk::PurchasedOnly, bom_type, Shape::default()).row,
|
||||
}
|
||||
}
|
||||
|
||||
/// One placement a list counts in a row.
|
||||
struct Entry {
|
||||
member: Member,
|
||||
holds: Holds,
|
||||
/// The folded placement (a position in the entries) whose children it is among.
|
||||
kit: Option<usize>,
|
||||
}
|
||||
|
||||
/// Every placement below `request.source` that `request` counts in a row, in one walk under one
|
||||
/// budget, and with `members` each one's occurrence path (else an empty path each).
|
||||
fn walk(tree: &Tree, request: &Request, members: bool) -> Result<(Vec<Entry>, Vec<Vec<u64>>)> {
|
||||
struct Frame {
|
||||
component: u64,
|
||||
depth: usize,
|
||||
/// Kept only with `members`.
|
||||
path: Vec<u64>,
|
||||
walk: Walk,
|
||||
kit: Option<usize>,
|
||||
}
|
||||
let mut entries: Vec<Entry> = Vec::new();
|
||||
let mut paths: Vec<Vec<u64>> = Vec::new();
|
||||
let mut visited = 0usize;
|
||||
let mut stack = vec![Frame { component: request.source, depth: 0, path: Vec::new(), walk: Walk::Listed, kit: None }];
|
||||
while let Some(frame) = stack.pop() {
|
||||
for placed in tree.contents(frame.component) {
|
||||
visited += 1;
|
||||
if frame.depth >= MAX_PLACEMENT_DEPTH || visited > MAX_PLACEMENTS {
|
||||
return Err(too_big());
|
||||
}
|
||||
let component = placed.member.component;
|
||||
// What a folded part holds is listed as parts only lists it, and isn't folded again; in
|
||||
// all levels what any row holds is listed in all levels again.
|
||||
let (view, folds) = match frame.kit {
|
||||
Some(_) if request.view == View::AllLevels => (View::AllLevels, false),
|
||||
Some(_) => (View::PartsOnly, false),
|
||||
None => (request.view, request.fold),
|
||||
};
|
||||
let numbered = tree.components.get(&component).is_some_and(|c| c.metadata.part_number().is_some());
|
||||
let shape = Shape {
|
||||
contents: tree.children.contains_key(&component),
|
||||
bodies: tree.bodies.contains_key(&component),
|
||||
folded: folds && numbered,
|
||||
open_purchased: request.purchased_contents,
|
||||
};
|
||||
let step = classify(view, frame.walk, placed.member.bom_type, shape);
|
||||
let path = if members { [frame.path.as_slice(), &[placed.occurrence]].concat() } else { Vec::new() };
|
||||
let here = entries.len();
|
||||
if step.row {
|
||||
entries.push(Entry { member: placed.member, holds: step.holds, kit: frame.kit });
|
||||
paths.push(path.clone());
|
||||
}
|
||||
if let Some(walk) = step.inside {
|
||||
let kit = if step.kit { Some(here) } else { frame.kit };
|
||||
stack.push(Frame { component, depth: frame.depth + 1, path, walk, kit });
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((entries, paths))
|
||||
}
|
||||
|
||||
impl Tree<'_> {
|
||||
/// Component `id` (0: the design) as a title block shows it (#3875): what one of it weighs, as
|
||||
/// a list weighs the assembly ([`Total::mass`]: its own bodies and all it holds but references),
|
||||
/// and what that is made of, as its row would say. Each is worked out once for the tree.
|
||||
pub fn component(&mut self, id: u64) -> Result<(Mass, Vec<String>)> {
|
||||
if id != 0 && !self.components.contains_key(&id) {
|
||||
return Err(DocError::Unknown(format!("component {id}")));
|
||||
}
|
||||
let mass = agree(&[self.weigh(id, Holds::All, 0)?]);
|
||||
Ok((mass, self.made_of(id, Holds::All, 0)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// The rows `request` lists, with the assembly's total; with `request.mass`, each row and the
|
||||
/// assembly weighed.
|
||||
pub fn list(tree: &mut Tree, request: &Request) -> Result<Bom> {
|
||||
list_with(tree, request, false)
|
||||
}
|
||||
|
||||
/// [`list`], with each row's members when asked (what item numbers and balloons will follow rows
|
||||
/// by, and what `row_of` reads), so a list made for its rows alone copies no paths.
|
||||
pub fn list_with(tree: &mut Tree, request: &Request, members: bool) -> Result<Bom> {
|
||||
if request.source != 0 && !tree.components.contains_key(&request.source) {
|
||||
return Err(DocError::Unknown(format!("component {}", request.source)));
|
||||
}
|
||||
let (entries, mut paths) = walk(tree, request, members)?;
|
||||
// What each folded placement holds: the placements listed as its children.
|
||||
let mut kids: Vec<Vec<usize>> = vec![Vec::new(); entries.len()];
|
||||
for (i, entry) in entries.iter().enumerate() {
|
||||
if let Some(of) = entry.kit.and_then(|k| kids.get_mut(k)) {
|
||||
of.push(i);
|
||||
}
|
||||
}
|
||||
let kits = tree.kits(&entries, &kids);
|
||||
let listing = Listing { entries, kids, kits };
|
||||
let top: Vec<usize> = listing.entries.iter().enumerate().filter(|(_, e)| e.kit.is_none()).map(|(i, _)| i).collect();
|
||||
// What a folded part holds is gathered by part number in any list; all levels gathers every
|
||||
// level as it gathers the first.
|
||||
let under = if request.view == View::AllLevels { request.group } else { Group::PartNumber };
|
||||
let mut rows = tree.level(&listing, &mut paths, &top, request.group, under, &[], 1);
|
||||
let numbered = numbers::apply(&mut rows, numbers::saved(tree.doc, request.source, request.view));
|
||||
let mut bom = Bom { source: request.source, rows, total: Total::default(), numbered, index: HashMap::new() };
|
||||
if members {
|
||||
bom.index_members();
|
||||
}
|
||||
tree.name_materials(&mut bom.rows)?;
|
||||
if request.mass {
|
||||
tree.weigh_rows(&mut bom.rows)?;
|
||||
// After the rows, so what their definitions weigh is kept: only what no row holds (the
|
||||
// assembly's own bodies) is measured again.
|
||||
bom.total.mass = agree(&[tree.weigh(request.source, Holds::All, 0)?]);
|
||||
}
|
||||
let occurrences: u64 = bom.rows_deep().iter().map(|(_, row)| row.count).sum();
|
||||
bom.total.occurrences = occurrences;
|
||||
Ok(bom)
|
||||
}
|
||||
|
||||
/// One of a row's parts, from what each of its definitions weighs: known when every one can be
|
||||
/// measured, has something to weigh, and they agree. Some with something to weigh and some with
|
||||
/// nothing differ.
|
||||
fn agree(weighed: &[Weighed]) -> Mass {
|
||||
let Some(known) = weighed.iter().copied().collect::<Option<Vec<(f64, usize)>>>() else {
|
||||
return Mass::Unknown(Unknown::Unmeasurable);
|
||||
};
|
||||
if known.iter().all(|&(_, bodies)| bodies == 0) {
|
||||
return Mass::Unknown(Unknown::NothingToWeigh);
|
||||
}
|
||||
match known.first() {
|
||||
Some(&(first, _)) if known.iter().any(|&(grams, bodies)| bodies == 0 || !same_mass(grams, first)) => Mass::Unknown(Unknown::Differs),
|
||||
Some(&(first, _)) => Mass::Grams(first),
|
||||
None => Mass::Unknown(Unknown::NothingToWeigh),
|
||||
}
|
||||
}
|
||||
|
||||
/// How closely the kernel's `measure` gives one solid's volume however it was modelled, relative to
|
||||
/// it (about 1e-4): a pin made as a cylinder and as a revolved rectangle measured 1.05e-5 apart
|
||||
/// (2026-10-26), and this is ten times that.
|
||||
pub const MEASURE_ACCURACY: f64 = 1e-4;
|
||||
|
||||
/// Two masses of one part, measured from definitions modelled apart: one when they are within the
|
||||
/// kernel's measurement accuracy of each other.
|
||||
fn same_mass(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() <= MEASURE_ACCURACY * a.abs().max(b.abs())
|
||||
}
|
||||
|
||||
/// `more` added to `total`; unknown when either is. The body count only says whether there is
|
||||
/// any, so a compact assembly with more placed bodies than a count holds saturates it.
|
||||
fn add(total: &mut Weighed, more: Weighed) {
|
||||
*total = total.zip(more).map(|((grams, bodies), (g, n))| (grams + g, bodies.saturating_add(n)));
|
||||
}
|
||||
|
||||
/// `name` added to `names` unless it is there already, so each is listed once, in first order.
|
||||
fn push_once(names: &mut Vec<String>, name: String) {
|
||||
if !names.contains(&name) {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
fn too_big() -> DocError {
|
||||
crate::assembly::too_many_placements()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "bom_tests.rs"]
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
mod tests;
|
||||
@@ -1,260 +0,0 @@
|
||||
//! Item numbers (#3874). A view of an assembly is numbered once someone numbers it
|
||||
//! (`assembly.bom.renumber`, `assembly.bom.item.set`): from then on its rows' numbers are saved in
|
||||
//! the design ([`ItemNumbers`], `assembly.item_numbers`), each under its row's key, so a row keeps
|
||||
//! its number through part-number edits. Until then a view's numbers are its rows' positions,
|
||||
//! provisional, and listing it saves nothing.
|
||||
//!
|
||||
//! A row a numbered view has no number for (a part added since) shows the highest number saved at
|
||||
//! its level plus one, provisional, and [`record`] saves it: the engine runs it after each change,
|
||||
//! in the change's undo step. A row that goes keeps its number until a renumber, so a number on a
|
||||
//! printed drawing never comes to mean another part, and a part put back gets its number again.
|
||||
//! A renumber keeps locked numbers and skips them.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Group, Member, Request, Row, RowKey, Tree, View, item_text, list, list_order};
|
||||
use crate::{DocError, Document, MAX_PLACEMENTS, Result};
|
||||
|
||||
/// The highest item number.
|
||||
pub const MAX_ITEM: u32 = 999_999;
|
||||
|
||||
/// The item numbers saved for one view of one assembly, with the list options it was numbered
|
||||
/// with: the rows it numbers are listed by them.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ItemNumbers {
|
||||
/// The assembly: a component, or 0 for the design.
|
||||
pub source: u64,
|
||||
#[serde(rename = "levels")]
|
||||
pub view: View,
|
||||
#[serde(default, rename = "group_by")]
|
||||
pub group: Group,
|
||||
#[serde(default, rename = "group_children", skip_serializing_if = "std::ops::Not::not")]
|
||||
pub fold: bool,
|
||||
#[serde(default, rename = "show_purchased_contents", skip_serializing_if = "std::ops::Not::not")]
|
||||
pub purchased_contents: bool,
|
||||
/// One for each row numbered, rows gone since included, by key.
|
||||
#[serde(default)]
|
||||
pub items: Vec<ItemNumber>,
|
||||
}
|
||||
|
||||
/// One row's saved item number.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ItemNumber {
|
||||
#[serde(flatten)]
|
||||
pub(crate) key: RowKey,
|
||||
pub number: u32,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
impl ItemNumbers {
|
||||
/// The list it numbers.
|
||||
pub fn request(&self) -> Request {
|
||||
Request { source: self.source, view: self.view, group: self.group, fold: self.fold, mass: false, purchased_contents: self.purchased_contents }
|
||||
}
|
||||
}
|
||||
|
||||
/// How [`number`] numbers a view.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Numbering {
|
||||
/// Each row without a saved number keeps the one it shows: the next at its level. Only a
|
||||
/// numbered view has any to save.
|
||||
NewRows,
|
||||
/// Every row from `start` by `step`, level by level, in `order`. Locked rows keep their
|
||||
/// numbers, which are skipped, and rows gone are forgotten.
|
||||
Renumber { start: u32, step: u32, order: Order },
|
||||
/// One row, by its item as the view shows it now (`[1, 2]` is 1.2): its number and its lock.
|
||||
/// Every other row keeps the number it shows.
|
||||
Set { row: Vec<u32>, item: Option<u32>, locked: Option<bool> },
|
||||
}
|
||||
|
||||
/// The order a renumber goes in.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Order {
|
||||
/// The list's own, by part number (`list_order`).
|
||||
#[default]
|
||||
List,
|
||||
/// The item numbers', closing the gaps.
|
||||
Item,
|
||||
}
|
||||
|
||||
/// The numbering saved for a view of an assembly, if it has been numbered.
|
||||
pub(super) fn saved(doc: &Document, source: u64, view: View) -> Option<&ItemNumbers> {
|
||||
doc.assembly.item_numbers.iter().find(|n| n.source == source && n.view == view)
|
||||
}
|
||||
|
||||
/// `rows`, as their view lists them, numbered as `saved` says: each its saved number, else the next
|
||||
/// at its level (provisional); then each level by item number, with duplicates marked. With no
|
||||
/// numbering they stay as listed, every number provisional. Whether the view is numbered.
|
||||
pub(super) fn apply(rows: &mut [Row], saved: Option<&ItemNumbers>) -> bool {
|
||||
let Some(saved) = saved else { return false };
|
||||
// A damaged file's number out of range is no number, and of a key saved twice the first holds.
|
||||
let mut numbers: HashMap<&RowKey, &ItemNumber> = HashMap::new();
|
||||
let mut highest: HashMap<Vec<Member>, u32> = HashMap::new();
|
||||
for item in saved.items.iter().filter(|i| (1..=MAX_ITEM).contains(&i.number)) {
|
||||
if numbers.contains_key(&item.key) {
|
||||
continue;
|
||||
}
|
||||
numbers.insert(&item.key, item);
|
||||
let h = highest.entry(item.key.parents.clone()).or_default();
|
||||
*h = (*h).max(item.number);
|
||||
}
|
||||
number_level(rows, &numbers, &mut highest);
|
||||
true
|
||||
}
|
||||
|
||||
/// One level of [`apply`], and the levels under it. `highest` is the highest number at each level
|
||||
/// (by its parents), saved or shown.
|
||||
fn number_level(rows: &mut [Row], numbers: &HashMap<&RowKey, &ItemNumber>, highest: &mut HashMap<Vec<Member>, u32>) {
|
||||
for row in rows.iter_mut() {
|
||||
if let Some(saved) = numbers.get(&row.key) {
|
||||
row.item = saved.number;
|
||||
row.locked = saved.locked;
|
||||
row.provisional = false;
|
||||
} else {
|
||||
let h = highest.entry(row.key.parents.clone()).or_default();
|
||||
*h = h.saturating_add(1);
|
||||
row.item = *h;
|
||||
}
|
||||
number_level(&mut row.children, numbers, highest);
|
||||
}
|
||||
rows.sort_by_key(|r| r.item);
|
||||
let mut uses: HashMap<u32, usize> = HashMap::new();
|
||||
for row in rows.iter() {
|
||||
*uses.entry(row.item).or_default() += 1;
|
||||
}
|
||||
for row in rows.iter_mut() {
|
||||
row.duplicate = uses.get(&row.item).is_some_and(|&n| n > 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// The view `request` asks for, numbered as `how` says: the numbering to save, or `None` when that
|
||||
/// changes nothing.
|
||||
pub fn number(tree: &mut Tree, request: &Request, how: Numbering) -> Result<Option<ItemNumbers>> {
|
||||
let request = Request { mass: false, ..*request };
|
||||
// Borrowed from the design, not the tree: listing needs the tree, and nothing is copied until
|
||||
// something changes.
|
||||
let doc = tree.doc;
|
||||
let old = saved(doc, request.source, request.view);
|
||||
let mut bom = list(tree, &request)?;
|
||||
let mut items: Vec<ItemNumber> = Vec::new();
|
||||
match how {
|
||||
Numbering::NewRows => {
|
||||
let Some(old) = old else { return Ok(None) };
|
||||
let room = MAX_PLACEMENTS.saturating_sub(old.items.len());
|
||||
let shown = bom.rows_deep();
|
||||
let new: Vec<ItemNumber> = shown
|
||||
.iter()
|
||||
.filter(|(_, r)| r.provisional && r.item <= MAX_ITEM)
|
||||
.take(room)
|
||||
.map(|(_, r)| ItemNumber { key: r.key.clone(), number: r.item, locked: false })
|
||||
.collect();
|
||||
if new.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
items = [old.items.as_slice(), &new].concat();
|
||||
}
|
||||
Numbering::Set { row, item, locked } => {
|
||||
if let Some(n) = item.filter(|n| !(1..=MAX_ITEM).contains(n)) {
|
||||
return Err(DocError::Invalid(format!("an item number is 1 to {MAX_ITEM}, not {n}")));
|
||||
}
|
||||
let shown = bom.rows_deep();
|
||||
let target = shown
|
||||
.iter()
|
||||
.find(|(i, _)| *i == row)
|
||||
.map(|(_, r)| r.key.clone())
|
||||
.ok_or_else(|| DocError::Unknown(format!("item {}", item_text(&row))))?;
|
||||
// Rows not listed now keep what was saved for them; listed ones the number they show.
|
||||
let listed: HashSet<&RowKey> = shown.iter().map(|(_, r)| &r.key).collect();
|
||||
items.extend(old.iter().flat_map(|o| &o.items).filter(|i| !listed.contains(&i.key)).cloned());
|
||||
items.extend(shown.iter().map(|(_, r)| ItemNumber { key: r.key.clone(), number: r.item, locked: r.locked }));
|
||||
if let Some(it) = items.iter_mut().find(|i| i.key == target) {
|
||||
it.number = item.unwrap_or(it.number);
|
||||
it.locked = locked.unwrap_or(it.locked);
|
||||
}
|
||||
}
|
||||
Numbering::Renumber { start, step, order } => {
|
||||
for (name, n) in [("start", start), ("step", step)] {
|
||||
if !(1..=MAX_ITEM).contains(&n) {
|
||||
return Err(DocError::Invalid(format!("`{name}` is 1 to {MAX_ITEM}, not {n}")));
|
||||
}
|
||||
}
|
||||
renumber(&mut bom.rows, start, step, order, &mut items)?;
|
||||
}
|
||||
}
|
||||
items.sort_by(|a, b| a.key.cmp(&b.key));
|
||||
let new = ItemNumbers {
|
||||
source: request.source,
|
||||
view: request.view,
|
||||
group: request.group,
|
||||
fold: request.fold,
|
||||
purchased_contents: request.purchased_contents,
|
||||
items,
|
||||
};
|
||||
Ok((old != Some(&new)).then_some(new))
|
||||
}
|
||||
|
||||
/// `rows` and the levels under them numbered from `start` by `step` in `order`, each level on its
|
||||
/// own, into `out`. Locked rows keep their numbers and the others skip them, so no two rows get one
|
||||
/// number.
|
||||
fn renumber(rows: &mut [Row], start: u32, step: u32, order: Order, out: &mut Vec<ItemNumber>) -> Result<()> {
|
||||
if order == Order::List {
|
||||
rows.sort_by(list_order);
|
||||
}
|
||||
let locked: HashSet<u32> = rows.iter().filter(|r| r.locked).map(|r| r.item).collect();
|
||||
let mut next = start;
|
||||
for row in rows.iter_mut() {
|
||||
if !row.locked {
|
||||
while locked.contains(&next) {
|
||||
next = next.saturating_add(step);
|
||||
}
|
||||
if next > MAX_ITEM {
|
||||
return Err(DocError::Invalid(format!("numbering from {start} by {step} passes {MAX_ITEM}")));
|
||||
}
|
||||
row.item = next;
|
||||
next = next.saturating_add(step);
|
||||
}
|
||||
out.push(ItemNumber { key: row.key.clone(), number: row.item, locked: row.locked });
|
||||
renumber(&mut row.children, start, step, order, out)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Each numbered view of the design with its new rows' numbers saved ([`Numbering::NewRows`]), and
|
||||
/// a component's views dropped once it is gone: the design's numberings to save, or `None` when
|
||||
/// none changed. A view that can't be listed now (too large) is left as it is. A view numbered
|
||||
/// twice (only a damaged file is) keeps its first numbering, the one listing reads.
|
||||
pub fn record(tree: &mut Tree) -> Option<Vec<ItemNumbers>> {
|
||||
let doc = tree.doc;
|
||||
// Each numbering that changes: its new self, or none when its component is gone or its view
|
||||
// already has one, so no view is listed twice after a change.
|
||||
let mut changes: Vec<(usize, Option<ItemNumbers>)> = Vec::new();
|
||||
let mut views = HashSet::new();
|
||||
for (i, numbering) in doc.assembly.item_numbers.iter().enumerate() {
|
||||
let gone = numbering.source != 0 && !tree.components.contains_key(&numbering.source);
|
||||
if gone || !views.insert((numbering.source, numbering.view)) {
|
||||
changes.push((i, None));
|
||||
} else if let Ok(Some(new)) = number(tree, &numbering.request(), Numbering::NewRows) {
|
||||
changes.push((i, Some(new)));
|
||||
}
|
||||
}
|
||||
if changes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut out = doc.assembly.item_numbers.clone();
|
||||
for (i, change) in changes.into_iter().rev() {
|
||||
match change {
|
||||
Some(new) => {
|
||||
if let Some(numbering) = out.get_mut(i) {
|
||||
*numbering = new;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
out.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
//! Quantities (#3874): what one placement of a component means in a bill of materials, typed in as
|
||||
//! its [`BOM_QUANTITY`](crate::metadata::BOM_QUANTITY) part property, and what a row's placements
|
||||
//! add up to. The property is an expression evaluated with the design's parameters, so a cable's
|
||||
//! length follows the design; without a unit it is a number of pieces (a pack of 4). Units come
|
||||
//! from expressions: mass, volume and area wait for their expression units (#4811).
|
||||
|
||||
use std::cell::OnceCell;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::expr::{self, Value};
|
||||
use crate::{DocError, Document, Result};
|
||||
|
||||
/// The largest quantity of one placement, in pieces or millimetres.
|
||||
const MAX_AMOUNT: f64 = 1e9;
|
||||
|
||||
/// The mass and volume units of #4811's expressions: until they reach expressions, a quantity
|
||||
/// naming one is refused for what it is, not as an unknown parameter.
|
||||
const MASS_AND_VOLUME: [&str; 7] = ["g", "kg", "lb", "ml", "mm³", "cm³", "m³"];
|
||||
|
||||
/// What one placement of a component counts.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Amount {
|
||||
Pieces(f64),
|
||||
/// Millimetres.
|
||||
Length(f64),
|
||||
}
|
||||
|
||||
/// A row's quantity: what its placements add up to.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub enum Quantity {
|
||||
/// None of its definitions has a quantity: its count says it all.
|
||||
#[default]
|
||||
Count,
|
||||
Pieces(f64),
|
||||
/// Millimetres.
|
||||
Length(f64),
|
||||
Unknown(Uncounted),
|
||||
}
|
||||
|
||||
/// Why a row's quantity isn't known.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Uncounted {
|
||||
/// Pieces and lengths can't be added (a placement without a quantity is one piece).
|
||||
Mixed,
|
||||
/// A definition's quantity doesn't evaluate: a parameter it names is in error (or missing from
|
||||
/// a damaged file).
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// Evaluate a quantity expression with `doc`'s parameters.
|
||||
pub fn amount(doc: &Document, text: &str) -> Result<Amount> {
|
||||
amount_in(doc, &OnceCell::new(), text)
|
||||
}
|
||||
|
||||
/// [`amount`] with the parameters' values shared between calls, evaluated only when an expression
|
||||
/// names one.
|
||||
pub(super) fn amount_in(doc: &Document, values: &OnceCell<BTreeMap<String, Value>>, text: &str) -> Result<Amount> {
|
||||
let value = expr::eval_with(text, &|name| match values.get_or_init(|| doc.param_values().0).get(name) {
|
||||
Some(v) => Ok(*v),
|
||||
None if MASS_AND_VOLUME.contains(&name) => Err(mass_and_volume()),
|
||||
None => Err(DocError::Expr(format!("unknown parameter `{name}`"))),
|
||||
})?;
|
||||
let amount = match (value.len, value.ang) {
|
||||
(0, 0) => Amount::Pieces(value.v),
|
||||
(1, 0) => Amount::Length(value.v),
|
||||
(2, 0) => return Err(DocError::Invalid("not supported yet: area quantities".into())),
|
||||
(3, 0) => return Err(mass_and_volume()),
|
||||
_ => return Err(DocError::Invalid(format!("a quantity is a number of pieces or a length, not {}", value.describe()))),
|
||||
};
|
||||
let (Amount::Pieces(v) | Amount::Length(v)) = amount;
|
||||
if !(v > 0.0 && v <= MAX_AMOUNT) {
|
||||
return Err(DocError::Invalid(format!("a quantity is above 0 and at most {MAX_AMOUNT:e}")));
|
||||
}
|
||||
Ok(amount)
|
||||
}
|
||||
|
||||
fn mass_and_volume() -> DocError {
|
||||
DocError::Invalid("not supported yet: mass and volume quantities need the expression units of #4811".into())
|
||||
}
|
||||
|
||||
/// What placements add up to, from each one's entry in the definitions' amounts: none without a
|
||||
/// quantity (one piece), `Some(None)` when its quantity doesn't evaluate.
|
||||
pub(super) fn add_up<'a>(amounts: impl IntoIterator<Item = Option<&'a Option<Amount>>>) -> Quantity {
|
||||
let (mut pieces, mut length, mut typed) = (None, None, false);
|
||||
for amount in amounts {
|
||||
let (sum, n): (&mut Option<f64>, f64) = match amount {
|
||||
None => (&mut pieces, 1.0),
|
||||
Some(None) => return Quantity::Unknown(Uncounted::Invalid),
|
||||
Some(Some(Amount::Pieces(n))) => (&mut pieces, *n),
|
||||
Some(Some(Amount::Length(mm))) => (&mut length, *mm),
|
||||
};
|
||||
typed |= amount.is_some();
|
||||
*sum.get_or_insert(0.0) += n;
|
||||
}
|
||||
match (pieces, length) {
|
||||
_ if !typed => Quantity::Count,
|
||||
(Some(_), Some(_)) => Quantity::Unknown(Uncounted::Mixed),
|
||||
(None, Some(mm)) => Quantity::Length(mm),
|
||||
(n, None) => Quantity::Pieces(n.unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
@@ -1,922 +0,0 @@
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::BomType::{Inseparable, Normal, Phantom, Purchased, Reference};
|
||||
use crate::{IDENTITY, Occurrence};
|
||||
|
||||
/// A new component placed once in `parent`, and that placement.
|
||||
fn add(doc: &mut Document, name: &str, parent: u64) -> (u64, u64) {
|
||||
let component = doc.add_component(Some(name), parent).unwrap();
|
||||
(component, doc.occurrences.last().unwrap().id)
|
||||
}
|
||||
|
||||
/// Another placement of `component` in `parent`.
|
||||
fn place(doc: &mut Document, component: u64, parent: u64) -> u64 {
|
||||
doc.add_occurrence(component, parent, IDENTITY).unwrap()
|
||||
}
|
||||
|
||||
/// A part property as a file holds it (reading never validates).
|
||||
fn set(doc: &mut Document, component: u64, key: &str, value: &str) {
|
||||
doc.components.iter_mut().find(|c| c.id == component).unwrap().metadata.0.insert(key.into(), json!(value));
|
||||
}
|
||||
|
||||
fn number(doc: &mut Document, component: u64, part_number: &str) {
|
||||
set(doc, component, "part_number", part_number);
|
||||
}
|
||||
|
||||
fn typed(doc: &mut Document, component: u64, bom_type: BomType) {
|
||||
set(doc, component, "bom_type", bom_type.as_str());
|
||||
}
|
||||
|
||||
/// What `request` lists for `doc` (none of whose components has bodies), with each row's members.
|
||||
fn listed(doc: &Document, request: Request) -> Result<Bom> {
|
||||
let state = ModelState::default();
|
||||
list_with(&mut Tree::new(doc, &state), &request, true)
|
||||
}
|
||||
|
||||
fn view(view: View) -> Request {
|
||||
Request { view, ..Request::default() }
|
||||
}
|
||||
|
||||
fn folded(view: View) -> Request {
|
||||
Request { view, fold: true, ..Request::default() }
|
||||
}
|
||||
|
||||
/// Each row, followed by the rows under it: item, names, type, count and each.
|
||||
fn rows(bom: &Bom) -> Vec<(String, String, BomType, u64, u64)> {
|
||||
bom.rows_deep().into_iter().map(|(item, r)| (item_text(&item), r.names.join("; "), r.bom_type, r.count, r.each)).collect()
|
||||
}
|
||||
|
||||
fn row(item: &str, names: &str, bom_type: BomType, count: u64, each: u64) -> (String, String, BomType, u64, u64) {
|
||||
(item.into(), names.into(), bom_type, count, each)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_view_and_grouping_words_are_the_commands() {
|
||||
for v in View::ALL {
|
||||
assert_eq!(View::parse(v.as_str()), Some(v));
|
||||
}
|
||||
for g in Group::ALL {
|
||||
assert_eq!(Group::parse(g.as_str()), Some(g));
|
||||
}
|
||||
assert_eq!((View::default().as_str(), Group::default().as_str()), ("flat", "part_number"), "what assembly.bom lists by default");
|
||||
for word in ["", "deep", "Flat", "part number"] {
|
||||
assert_eq!((View::parse(word), Group::parse(word)), (None, None), "{word}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_assembly_lists_nothing_and_weighs_nothing() {
|
||||
let mut doc = Document::default();
|
||||
let empty = listed(&doc, Request { mass: true, ..Request::default() }).unwrap();
|
||||
assert!(empty.rows.is_empty());
|
||||
assert_eq!(empty.total, Total { occurrences: 0, mass: Mass::Unknown(Unknown::NothingToWeigh) });
|
||||
assert_eq!(listed(&doc, Request::default()).unwrap().total.mass, Mass::NotAsked, "weighed only when asked");
|
||||
// A component that holds nothing, as the assembly listed; one that isn't there is refused.
|
||||
let (part, _) = add(&mut doc, "Part", 0);
|
||||
for v in View::ALL {
|
||||
assert!(listed(&doc, Request { source: part, view: v, ..Request::default() }).unwrap().rows.is_empty());
|
||||
}
|
||||
assert!(listed(&doc, Request { source: 999, ..Request::default() }).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_part_is_one_row_named_by_its_lowest_member() {
|
||||
let mut doc = Document::default();
|
||||
let (part, occurrence) = add(&mut doc, "Part", 0);
|
||||
for (v, stands_for) in [(View::PartsOnly, Holds::Own), (View::FirstLevel, Holds::All)] {
|
||||
let bom = listed(&doc, view(v)).unwrap();
|
||||
let [r] = bom.rows.as_slice() else { panic!("{bom:?}") };
|
||||
assert_eq!(r.key, RowKey { parents: vec![], member: Member { component: part, bom_type: Normal } });
|
||||
assert_eq!((r.item, r.count, r.each, r.stands_for, r.mass), (1, 1, 1, stands_for, Mass::NotAsked));
|
||||
assert_eq!((&r.components, &r.names, &r.members), (&vec![part], &vec!["Part".to_string()], &vec![vec![occurrence]]));
|
||||
assert!(r.children.is_empty() && r.materials.is_empty() && r.part_number.is_empty());
|
||||
assert_eq!(bom.total.occurrences, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_is_named_by_its_lowest_member_not_by_its_part_number() {
|
||||
let mut doc = Document::default();
|
||||
let (a, oa) = add(&mut doc, "A", 0);
|
||||
let (b, ob) = add(&mut doc, "B", 0);
|
||||
number(&mut doc, a, "P");
|
||||
number(&mut doc, b, "P");
|
||||
let bom = listed(&doc, Request::default()).unwrap();
|
||||
let [r] = bom.rows.as_slice() else { panic!("{bom:?}") };
|
||||
assert_eq!((r.key.member.component, &r.components, &r.members, r.part_number.as_str()), (a, &vec![a, b], &vec![vec![oa], vec![ob]], "P"));
|
||||
// Renumbering the lower one: its row goes with it, and the other's row is named by the other.
|
||||
number(&mut doc, a, "Q");
|
||||
let bom = listed(&doc, Request::default()).unwrap();
|
||||
let keys: Vec<(&str, u64)> = bom.rows.iter().map(|r| (r.part_number.as_str(), r.key.member.component)).collect();
|
||||
assert_eq!(keys, [("P", b), ("Q", a)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_part_number_counted_as_two_types_is_two_rows() {
|
||||
let mut doc = Document::default();
|
||||
let (a, _) = add(&mut doc, "Made", 0);
|
||||
let (b, _) = add(&mut doc, "Bought", 0);
|
||||
number(&mut doc, a, "P");
|
||||
number(&mut doc, b, "P");
|
||||
typed(&mut doc, b, Purchased);
|
||||
// A second placement of the made one, counted as bought: it joins the bought row.
|
||||
let second = place(&mut doc, a, 0);
|
||||
doc.occurrences.iter_mut().find(|o| o.id == second).unwrap().bom_type = Some(Purchased);
|
||||
for v in View::ALL {
|
||||
let bom = listed(&doc, view(v)).unwrap();
|
||||
assert_eq!(rows(&bom), [row("1", "Made", Normal, 1, 1), row("2", "Made; Bought", Purchased, 2, 2)], "{v:?}");
|
||||
let keys: Vec<Member> = bom.rows.iter().map(|r| r.key.member).collect();
|
||||
assert_eq!(keys, [Member { component: a, bom_type: Normal }, Member { component: a, bom_type: Purchased }]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_phantom_in_a_phantom_promotes_what_it_holds_into_the_level_it_sits_in() {
|
||||
let mut doc = Document::default();
|
||||
let (kit, okit) = add(&mut doc, "Kit", 0);
|
||||
let (set_, oset) = add(&mut doc, "Set", kit);
|
||||
let oset2 = place(&mut doc, set_, kit);
|
||||
let (screw, oscrew) = add(&mut doc, "Screw", set_);
|
||||
let oscrew2 = place(&mut doc, screw, set_);
|
||||
let alone = place(&mut doc, screw, 0);
|
||||
typed(&mut doc, kit, Phantom);
|
||||
typed(&mut doc, set_, Phantom);
|
||||
let mut paths = vec![vec![okit, oset, oscrew], vec![okit, oset, oscrew2], vec![okit, oset2, oscrew], vec![okit, oset2, oscrew2], vec![alone]];
|
||||
paths.sort();
|
||||
for v in View::ALL {
|
||||
let bom = listed(&doc, view(v)).unwrap();
|
||||
assert_eq!(rows(&bom), [row("1", "Screw", Normal, 5, 5)], "{v:?}");
|
||||
assert_eq!(bom.rows[0].members, paths);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reference_counts_nowhere_and_its_own_list_lists_what_it_holds() {
|
||||
let mut doc = Document::default();
|
||||
let (fixture, ofixture) = add(&mut doc, "Fixture", 0);
|
||||
add(&mut doc, "Pin", fixture);
|
||||
typed(&mut doc, fixture, Reference);
|
||||
for v in View::ALL {
|
||||
let bom = listed(&doc, Request { mass: true, ..view(v) }).unwrap();
|
||||
assert_eq!((bom.rows.len(), bom.total.occurrences), (0, 0), "{v:?}");
|
||||
assert_eq!(bom.total.mass, Mass::Unknown(Unknown::NothingToWeigh));
|
||||
assert_eq!(rows(&listed(&doc, Request { source: fixture, view: v, ..Request::default() }).unwrap()), [row("1", "Pin", Normal, 1, 1)]);
|
||||
}
|
||||
// An occurrence counted as a reference, its component normal: the same.
|
||||
set(&mut doc, fixture, "bom_type", "normal");
|
||||
assert_eq!(rows(&listed(&doc, Request::default()).unwrap()), [row("1", "Pin", Normal, 1, 1)]);
|
||||
doc.occurrences.iter_mut().find(|o| o.id == ofixture).unwrap().bom_type = Some(Reference);
|
||||
assert!(listed(&doc, Request::default()).unwrap().rows.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_list_walks_64_levels_and_refuses_a_65th() {
|
||||
let mut doc = Document::default();
|
||||
let mut parent = 0;
|
||||
for level in 1..=64 {
|
||||
parent = add(&mut doc, &format!("L{level}"), parent).0;
|
||||
}
|
||||
let bom = listed(&doc, Request::default()).unwrap();
|
||||
assert_eq!(rows(&bom), [row("1", "L64", Normal, 1, 1)]);
|
||||
assert_eq!(bom.rows[0].members[0].len(), 64);
|
||||
add(&mut doc, "L65", parent);
|
||||
assert!(listed(&doc, Request::default()).is_err(), "refused, never listed in part");
|
||||
// The first level only walks what the design places directly.
|
||||
assert_eq!(rows(&listed(&doc, view(View::FirstLevel)).unwrap()), [row("1", "L1", Normal, 1, 1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_placement_budget_covers_the_whole_list_folded_parts_included() {
|
||||
let mut doc = Document::default();
|
||||
let (a, _) = add(&mut doc, "A", 0);
|
||||
for _ in 1..1000 {
|
||||
place(&mut doc, a, 0);
|
||||
}
|
||||
let (b, _) = add(&mut doc, "B", a);
|
||||
for _ in 1..99 {
|
||||
place(&mut doc, b, a);
|
||||
}
|
||||
// 1,000 As holding 99 Bs each: 100,000 placements, as many as one list walks.
|
||||
let flat = listed(&doc, Request::default()).unwrap();
|
||||
assert_eq!((rows(&flat), flat.total.occurrences), (vec![row("1", "B", Normal, 99_000, 99_000)], 99_000));
|
||||
number(&mut doc, a, "A-1");
|
||||
let kit = listed(&doc, folded(View::PartsOnly)).unwrap();
|
||||
assert_eq!((rows(&kit), kit.total.occurrences), (vec![row("1", "A", Normal, 1000, 1000), row("1.1", "B", Normal, 99_000, 99)], 100_000));
|
||||
let (a_row, b_row) = (&kit.rows[0], &kit.rows[0].children[0]);
|
||||
assert_eq!(b_row.key, RowKey { parents: vec![a_row.key.member], member: Member { component: b, bom_type: Normal } });
|
||||
assert_eq!(a_row.stands_for, Holds::All, "a folded part's row is the kit");
|
||||
// One placement more, anywhere, is refused, folded or not: a folded part's contents are walked
|
||||
// under the list's budget, not one of their own.
|
||||
add(&mut doc, "C", 0);
|
||||
for request in [Request::default(), folded(View::PartsOnly), folded(View::FirstLevel)] {
|
||||
assert!(listed(&doc, request).is_err(), "{request:?}");
|
||||
}
|
||||
assert_eq!(listed(&doc, view(View::FirstLevel)).unwrap().total.occurrences, 1001, "the first level walks 1,001");
|
||||
}
|
||||
|
||||
/// Kits used to be compared as text (`"#12×1"`): a part without a part number, whose id is 12,
|
||||
/// read the same as a part numbered "#12", and a part numbered "P×1\nQ" the same as a P and a Q.
|
||||
#[test]
|
||||
fn kits_are_told_apart_by_what_they_hold_not_by_how_it_reads() {
|
||||
let mut doc = Document::default();
|
||||
let bolt = |doc: &mut Document, name: &str| {
|
||||
let (id, _) = add(doc, name, 0);
|
||||
number(doc, id, "BOLT");
|
||||
id
|
||||
};
|
||||
let (b1, b2, b3, b4) = (bolt(&mut doc, "Bolt 1"), bolt(&mut doc, "Bolt 2"), bolt(&mut doc, "Bolt 3"), bolt(&mut doc, "Bolt 4"));
|
||||
let (plain, _) = add(&mut doc, "Plain", b1);
|
||||
let (spelled, _) = add(&mut doc, "Spelled", b2);
|
||||
number(&mut doc, spelled, &format!("#{plain}"));
|
||||
let (pq, _) = add(&mut doc, "PQ", b3);
|
||||
number(&mut doc, pq, "P×1\nQ");
|
||||
let (p, _) = add(&mut doc, "P", b4);
|
||||
let (q, _) = add(&mut doc, "Q", b4);
|
||||
number(&mut doc, p, "P");
|
||||
number(&mut doc, q, "Q");
|
||||
let bolts = |doc: &Document| -> Vec<Vec<u64>> {
|
||||
let bom = listed(doc, folded(View::PartsOnly)).unwrap();
|
||||
bom.rows.iter().filter(|r| r.part_number == "BOLT").map(|r| r.components.clone()).collect()
|
||||
};
|
||||
let mut each: Vec<Vec<u64>> = bolts(&doc);
|
||||
each.sort();
|
||||
assert_eq!(each, [vec![b1], vec![b2], vec![b3], vec![b4]], "four kits, four rows");
|
||||
// The same parts by part number are one kit, whichever definitions they are.
|
||||
let b5 = bolt(&mut doc, "Bolt 5");
|
||||
let (p2, _) = add(&mut doc, "P again", b5);
|
||||
let (q2, _) = add(&mut doc, "Q again", b5);
|
||||
number(&mut doc, p2, "P");
|
||||
number(&mut doc, q2, "Q");
|
||||
assert!(bolts(&doc).contains(&vec![b4, b5]));
|
||||
let bom = listed(&doc, folded(View::PartsOnly)).unwrap();
|
||||
let both = bom.rows.iter().find(|r| r.components == [b4, b5]).unwrap();
|
||||
let inside: Vec<(&str, &[u64], u64, u64)> =
|
||||
both.children.iter().map(|c| (c.part_number.as_str(), c.components.as_slice(), c.each, c.count)).collect();
|
||||
assert_eq!(inside, [("P", &[p, p2][..], 1, 2), ("Q", &[q, q2][..], 1, 2)], "each bolt's, counted for both");
|
||||
// The same parts counted as another type are another kit: a bolt whose Q is bought is a row of its own.
|
||||
let b6 = bolt(&mut doc, "Bolt 6");
|
||||
let (p3, _) = add(&mut doc, "P third", b6);
|
||||
let (q3, bought) = add(&mut doc, "Q third", b6);
|
||||
number(&mut doc, p3, "P");
|
||||
number(&mut doc, q3, "Q");
|
||||
doc.occurrences.iter_mut().find(|o| o.id == bought).unwrap().bom_type = Some(Purchased);
|
||||
let each = bolts(&doc);
|
||||
assert!(each.contains(&vec![b4, b5]) && each.contains(&vec![b6]), "{each:?}");
|
||||
}
|
||||
|
||||
/// All levels walks under the list's one budget: 100,000 placements and 64 levels are listed, and
|
||||
/// one more of either is refused, never listed in part.
|
||||
#[test]
|
||||
fn all_levels_lists_at_each_cap_and_refuses_one_more() {
|
||||
let mut doc = Document::default();
|
||||
let (part, _) = add(&mut doc, "Part", 0);
|
||||
let base = doc.occurrences.iter().map(|o| o.id).max().unwrap_or(0);
|
||||
let placement =
|
||||
|id: u64| Occurrence { id, component: part, parent: 0, name: format!("Part:{id}"), transform: IDENTITY, grounded: false, bom_type: None };
|
||||
doc.occurrences.extend((1..MAX_PLACEMENTS as u64).map(|i| placement(base + i)));
|
||||
let all = listed(&doc, view(View::AllLevels)).unwrap();
|
||||
assert_eq!((all.rows.len(), all.total.occurrences), (1, MAX_PLACEMENTS as u64));
|
||||
doc.occurrences.push(placement(base + MAX_PLACEMENTS as u64));
|
||||
assert!(listed(&doc, view(View::AllLevels)).is_err(), "100,001 placements");
|
||||
let mut doc = Document::default();
|
||||
let mut parent = 0;
|
||||
for i in 0..MAX_PLACEMENT_DEPTH {
|
||||
parent = add(&mut doc, &format!("L{i}"), parent).0;
|
||||
}
|
||||
let all = listed(&doc, view(View::AllLevels)).unwrap();
|
||||
assert_eq!(all.rows_deep().last().map(|(item, _)| item.len()), Some(MAX_PLACEMENT_DEPTH));
|
||||
add(&mut doc, "Too deep", parent);
|
||||
assert!(listed(&doc, view(View::AllLevels)).is_err(), "65 levels");
|
||||
}
|
||||
|
||||
/// Two parts with one part number are one row only when what they hold is the same all the way
|
||||
/// down: here they differ two levels below, so in all levels they are two rows, each with its own.
|
||||
#[test]
|
||||
fn kits_are_compared_to_any_depth() {
|
||||
let mut doc = Document::default();
|
||||
let (a, _) = add(&mut doc, "A", 0);
|
||||
let (b, _) = add(&mut doc, "B", 0);
|
||||
let (x1, _) = add(&mut doc, "X1", a);
|
||||
let (x2, _) = add(&mut doc, "X2", b);
|
||||
let (bolt, _) = add(&mut doc, "Bolt", x1);
|
||||
for (id, part_number) in [(a, "P"), (b, "P"), (x1, "X"), (x2, "X"), (bolt, "BOLT")] {
|
||||
number(&mut doc, id, part_number);
|
||||
}
|
||||
let all = listed(&doc, view(View::AllLevels)).unwrap();
|
||||
assert_eq!(
|
||||
rows(&all),
|
||||
[
|
||||
row("1", "A", Normal, 1, 1),
|
||||
row("1.1", "X1", Normal, 1, 1),
|
||||
row("1.1.1", "Bolt", Normal, 1, 1),
|
||||
row("2", "B", Normal, 1, 1),
|
||||
row("2.1", "X2", Normal, 1, 1)
|
||||
],
|
||||
"two P rows, never one with children at each 0"
|
||||
);
|
||||
// The same contents two levels down: one row again.
|
||||
let (bolt2, _) = add(&mut doc, "Bolt 2", x2);
|
||||
number(&mut doc, bolt2, "BOLT");
|
||||
let all = listed(&doc, view(View::AllLevels)).unwrap();
|
||||
assert_eq!(rows(&all), [row("1", "A; B", Normal, 2, 2), row("1.1", "X1; X2", Normal, 2, 1), row("1.1.1", "Bolt; Bolt 2", Normal, 2, 1)]);
|
||||
}
|
||||
|
||||
/// What a folded part holds is listed as parts only lists it, in parts only and the first level,
|
||||
/// and isn't folded again: a numbered subassembly inside it is listed through its parts. In all
|
||||
/// levels it is listed in all levels again, so that subassembly is a row with its own rows.
|
||||
#[test]
|
||||
fn what_a_folded_part_holds_is_listed_as_parts_only_lists_it() {
|
||||
let mut doc = Document::default();
|
||||
let (machine, _) = add(&mut doc, "Machine", 0);
|
||||
let (gearbox, _) = add(&mut doc, "Gearbox", machine);
|
||||
let (shaft, _) = add(&mut doc, "Shaft", gearbox);
|
||||
place(&mut doc, shaft, gearbox);
|
||||
add(&mut doc, "Pin", gearbox);
|
||||
for (id, part_number) in [(machine, "M-1"), (gearbox, "GB-1"), (shaft, "SH-1")] {
|
||||
number(&mut doc, id, part_number);
|
||||
}
|
||||
for v in [View::PartsOnly, View::FirstLevel] {
|
||||
let bom = listed(&doc, folded(v)).unwrap();
|
||||
assert_eq!(rows(&bom), [row("1", "Machine", Normal, 1, 1), row("1.1", "Shaft", Normal, 2, 2), row("1.2", "Pin", Normal, 1, 1)], "{v:?}");
|
||||
}
|
||||
let all = listed(&doc, folded(View::AllLevels)).unwrap();
|
||||
let nested = [
|
||||
row("1", "Machine", Normal, 1, 1),
|
||||
row("1.1", "Gearbox", Normal, 1, 1),
|
||||
row("1.1.1", "Shaft", Normal, 2, 2),
|
||||
row("1.1.2", "Pin", Normal, 1, 1),
|
||||
];
|
||||
assert_eq!(rows(&all), nested);
|
||||
}
|
||||
|
||||
/// Bolt (purchased) holding a nut; Frame (inseparable) holding a plate, a purchased clip and a
|
||||
/// weld kit (phantom) of a rivet and a purchased insert; Fixture (reference) holding a pin; Set
|
||||
/// (phantom) holding a screw; Sub (normal, numbered, no bodies) holding a washer. Each is placed
|
||||
/// once.
|
||||
fn mixed() -> (Document, BTreeMap<&'static str, u64>) {
|
||||
let mut doc = Document::default();
|
||||
let mut at = BTreeMap::new();
|
||||
for (name, parent, bom_type) in [
|
||||
("Bolt", "", Purchased),
|
||||
("Nut", "Bolt", Normal),
|
||||
("Frame", "", Inseparable),
|
||||
("Plate", "Frame", Normal),
|
||||
("Clip", "Frame", Purchased),
|
||||
("Weld kit", "Frame", Phantom),
|
||||
("Rivet", "Weld kit", Normal),
|
||||
("Insert", "Weld kit", Purchased),
|
||||
("Fixture", "", Reference),
|
||||
("Pin", "Fixture", Normal),
|
||||
("Set", "", Phantom),
|
||||
("Screw", "Set", Normal),
|
||||
("Sub", "", Normal),
|
||||
("Washer", "Sub", Normal),
|
||||
] {
|
||||
let parent = doc.find_component(parent).unwrap_or(0);
|
||||
let (id, occurrence) = add(&mut doc, name, parent);
|
||||
typed(&mut doc, id, bom_type);
|
||||
at.insert(name, occurrence);
|
||||
}
|
||||
let sub = doc.find_component("Sub").unwrap();
|
||||
number(&mut doc, sub, "SUB-1");
|
||||
(doc, at)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_of_finds_the_row_a_placement_is_counted_in() {
|
||||
let (doc, at) = mixed();
|
||||
let path = |names: &[&str]| -> Vec<u64> { names.iter().map(|n| at[*n]).collect() };
|
||||
let state = ModelState::default();
|
||||
let mut tree = Tree::new(&doc, &state);
|
||||
let name = |r: std::result::Result<(Vec<u32>, &Row), Unlisted>| r.map(|(_, r)| r.names.join("; "));
|
||||
let flat = list_with(&mut tree, &Request::default(), true).unwrap();
|
||||
// Made for its rows alone, a list keeps no members.
|
||||
assert!(list(&mut tree, &Request::default()).unwrap().rows.iter().all(|r| r.members.is_empty()));
|
||||
let cases: &[(&[&str], std::result::Result<&str, Unlisted>)] = &[
|
||||
(&["Bolt"], Ok("Bolt")),
|
||||
(&["Bolt", "Nut"], Ok("Bolt")),
|
||||
(&["Frame"], Ok("Frame")),
|
||||
(&["Frame", "Plate"], Ok("Frame")),
|
||||
(&["Frame", "Clip"], Ok("Clip")),
|
||||
(&["Frame", "Weld kit"], Ok("Frame")),
|
||||
(&["Frame", "Weld kit", "Rivet"], Ok("Frame")),
|
||||
(&["Frame", "Weld kit", "Insert"], Ok("Insert")),
|
||||
(&["Fixture"], Err(Unlisted::Reference)),
|
||||
(&["Fixture", "Pin"], Err(Unlisted::Reference)),
|
||||
(&["Set"], Err(Unlisted::NoRow)),
|
||||
(&["Set", "Screw"], Ok("Screw")),
|
||||
(&["Sub"], Err(Unlisted::NoRow)),
|
||||
(&["Sub", "Washer"], Ok("Washer")),
|
||||
(&[], Err(Unlisted::NoRow)),
|
||||
(&["Nut"], Err(Unlisted::NotFound)),
|
||||
];
|
||||
for (names, found) in cases {
|
||||
assert_eq!(name(flat.row_of(&tree, &path(names))), found.map(String::from), "{names:?}");
|
||||
}
|
||||
assert_eq!(flat.row_of(&tree, &[999]).err(), Some(Unlisted::NotFound));
|
||||
assert_eq!(flat.row_of(&tree, &vec![at["Bolt"]; 65]).err(), Some(Unlisted::NotFound));
|
||||
let stands: Vec<(String, Holds)> = flat.rows.iter().map(|r| (r.names.join("; "), r.stands_for)).collect();
|
||||
let want = [
|
||||
("Bolt", Holds::All),
|
||||
("Clip", Holds::All),
|
||||
("Frame", Holds::AllButPurchased),
|
||||
("Insert", Holds::All),
|
||||
("Screw", Holds::Own),
|
||||
("Washer", Holds::Own),
|
||||
];
|
||||
assert_eq!(stands, want.map(|(n, h)| (n.to_string(), h)));
|
||||
|
||||
let top = list_with(&mut tree, &view(View::FirstLevel), true).unwrap();
|
||||
let cases: &[(&[&str], &str)] = &[(&["Sub", "Washer"], "Sub"), (&["Set", "Screw"], "Screw"), (&["Frame", "Clip"], "Frame")];
|
||||
for (names, found) in cases {
|
||||
assert_eq!(name(top.row_of(&tree, &path(names))), Ok(found.to_string()), "{names:?}");
|
||||
}
|
||||
assert!(top.rows.iter().all(|r| r.stands_for == Holds::All));
|
||||
|
||||
let kit = list_with(&mut tree, &folded(View::PartsOnly), true).unwrap();
|
||||
let (_, sub) = kit.row_of(&tree, &path(&["Sub"])).unwrap();
|
||||
let (_, washer) = kit.row_of(&tree, &path(&["Sub", "Washer"])).unwrap();
|
||||
assert_eq!((sub.names.join(""), sub.stands_for, washer.names.join("")), ("Sub".into(), Holds::All, "Washer".into()));
|
||||
assert_eq!(washer.key.parents, [sub.key.member]);
|
||||
}
|
||||
|
||||
/// Each placement is counted in one row or none, every view and grouping: the counts, the
|
||||
/// members and the total agree, and `row_of` finds each member's row.
|
||||
#[test]
|
||||
fn every_placement_is_in_one_row_or_none() {
|
||||
let (mut doc, _) = mixed();
|
||||
// More of the same: the subassembly and the bolt placed again, the washer twice in the subassembly.
|
||||
for name in ["Sub", "Bolt"] {
|
||||
let id = doc.find_component(name).unwrap();
|
||||
place(&mut doc, id, 0);
|
||||
}
|
||||
let (washer, sub) = (doc.find_component("Washer").unwrap(), doc.find_component("Sub").unwrap());
|
||||
place(&mut doc, washer, sub);
|
||||
let state = ModelState::default();
|
||||
let mut tree = Tree::new(&doc, &state);
|
||||
for v in View::ALL {
|
||||
for fold in [false, true] {
|
||||
for group in Group::ALL {
|
||||
let request = Request { view: v, fold, group, ..Request::default() };
|
||||
let bom = list_with(&mut tree, &request, true).unwrap();
|
||||
let deep = bom.rows_deep();
|
||||
let mut seen: Vec<&Vec<u64>> = deep.iter().flat_map(|(_, r)| &r.members).collect();
|
||||
assert_eq!(seen.len() as u64, bom.total.occurrences, "{request:?}");
|
||||
assert!(deep.iter().all(|(_, r)| r.count == r.members.len() as u64), "{request:?}");
|
||||
seen.sort();
|
||||
seen.dedup();
|
||||
assert_eq!(seen.len() as u64, bom.total.occurrences, "a placement in two rows: {request:?}");
|
||||
for (_, r) in &deep {
|
||||
for m in &r.members {
|
||||
assert_eq!(bom.row_of(&tree, m).map(|(_, found)| &found.key), Ok(&r.key), "{request:?} {m:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_files_structure_is_listed_or_refused_never_followed_forever() {
|
||||
let mut doc = Document::default();
|
||||
let (a, _) = add(&mut doc, "A", 0);
|
||||
let ghost =
|
||||
|id, component, parent| Occurrence { id, component, parent, name: format!("{id}"), transform: IDENTITY, grounded: false, bom_type: None };
|
||||
// An occurrence of a component that isn't there has no row and counts nowhere.
|
||||
doc.occurrences.push(ghost(900, 999, 0));
|
||||
assert_eq!(rows(&listed(&doc, Request::default()).unwrap()), [row("1", "A", Normal, 1, 1)]);
|
||||
// A component placed inside itself: refused at the depth limit, never walked forever.
|
||||
doc.occurrences.push(ghost(901, a, a));
|
||||
assert!(listed(&doc, Request::default()).is_err());
|
||||
// At the first level A is one row, but its material and its mass are what it holds: both walk inside.
|
||||
assert!(listed(&doc, Request { mass: true, ..view(View::FirstLevel) }).is_err(), "weighing it walks inside");
|
||||
assert!(listed(&doc, view(View::FirstLevel)).is_err(), "naming its material walks inside");
|
||||
}
|
||||
|
||||
/// Moved from the engine's tests with the rule (it was `row_mass`), now with the reason.
|
||||
#[test]
|
||||
fn a_row_has_a_mass_only_when_its_definitions_are_weighed_and_agree() {
|
||||
let (steel, aluminium, surface, empty): (Weighed, Weighed, Weighed, Weighed) =
|
||||
(Some((392.5, 1)), Some((6.48, 1)), Some((0.0, 1)), Some((0.0, 0)));
|
||||
assert_eq!(agree(&[steel, steel]), Mass::Grams(392.5));
|
||||
assert_eq!(agree(&[steel, aluminium]), Mass::Unknown(Unknown::Differs));
|
||||
assert_eq!(agree(&[surface, surface]), Mass::Grams(0.0), "a body that weighs nothing (a surface) is weighed");
|
||||
assert_eq!(agree(&[surface, steel]), Mass::Unknown(Unknown::Differs));
|
||||
assert_eq!(agree(&[empty]), Mass::Unknown(Unknown::NothingToWeigh), "nothing to weigh");
|
||||
assert_eq!(agree(&[steel, empty]), Mass::Unknown(Unknown::Differs), "one weighs something, one nothing");
|
||||
assert_eq!(agree(&[empty, steel]), Mass::Unknown(Unknown::Differs), "whichever comes first");
|
||||
assert_eq!(agree(&[empty, empty]), Mass::Unknown(Unknown::NothingToWeigh));
|
||||
assert_eq!(agree(&[None]), Mass::Unknown(Unknown::Unmeasurable), "not measured");
|
||||
assert_eq!(agree(&[aluminium, None, steel]), Mass::Unknown(Unknown::Unmeasurable), "whichever definition it is");
|
||||
assert_eq!(agree(&[]), Mass::Unknown(Unknown::NothingToWeigh));
|
||||
}
|
||||
|
||||
/// What a row's part stands for leaves out a reference, and an inseparable part's purchased parts,
|
||||
/// which its list shows beside it; phantoms and inseparable parts inside are weighed through.
|
||||
#[test]
|
||||
fn what_adds_to_a_row_follows_what_it_stands_for() {
|
||||
for t in BomType::ALL {
|
||||
assert!(!adds(Holds::Own, t), "{t:?}");
|
||||
assert_eq!(adds(Holds::All, t), t != Reference, "{t:?}");
|
||||
assert_eq!(adds(Holds::AllButPurchased, t), !matches!(t, Reference | Purchased), "{t:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `how` applied to the view `request` lists, its numbering saved as the engine saves it; whether
|
||||
/// anything changed.
|
||||
fn numbered(doc: &mut Document, request: Request, how: Numbering) -> bool {
|
||||
let state = ModelState::default();
|
||||
let Some(numbers) = super::number(&mut Tree::new(doc, &state), &request, how).unwrap() else { return false };
|
||||
let saved = &mut doc.assembly.item_numbers;
|
||||
saved.retain(|n| (n.source, n.view) != (numbers.source, numbers.view));
|
||||
saved.push(numbers);
|
||||
true
|
||||
}
|
||||
|
||||
/// [`record`] as the engine runs it after a change; whether anything changed.
|
||||
fn recorded(doc: &mut Document) -> bool {
|
||||
let state = ModelState::default();
|
||||
let Some(numbers) = record(&mut Tree::new(doc, &state)) else { return false };
|
||||
doc.assembly.item_numbers = numbers;
|
||||
true
|
||||
}
|
||||
|
||||
const RENUMBER: Numbering = Numbering::Renumber { start: 1, step: 1, order: Order::List };
|
||||
|
||||
/// Each row, followed by the rows under it: item, names, and its numbering flags as letters
|
||||
/// (provisional, locked, duplicate).
|
||||
fn items(bom: &Bom) -> Vec<(String, String, String)> {
|
||||
bom.rows_deep()
|
||||
.into_iter()
|
||||
.map(|(item, r)| {
|
||||
let flags = [(r.provisional, 'p'), (r.locked, 'l'), (r.duplicate, 'd')].iter().filter(|(on, _)| *on).map(|(_, c)| *c).collect();
|
||||
(item_text(&item), r.names.join("; "), flags)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn item(item: &str, names: &str, flags: &str) -> (String, String, String) {
|
||||
(item.into(), names.into(), flags.into())
|
||||
}
|
||||
|
||||
/// Parts A, B and C at the top, part numbers P1 to P3.
|
||||
fn three_parts() -> (Document, [(u64, u64); 3]) {
|
||||
let mut doc = Document::default();
|
||||
let parts = ["A", "B", "C"].map(|name| add(&mut doc, name, 0));
|
||||
for (i, (part, _)) in parts.iter().enumerate() {
|
||||
number(&mut doc, *part, &format!("P{}", i + 1));
|
||||
}
|
||||
(doc, parts)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbering_a_view_saves_its_numbers_and_new_rows_take_the_next() {
|
||||
let (mut doc, [_, (b, ob), _]) = three_parts();
|
||||
// Never numbered: positions, provisional, and nothing saved by a change.
|
||||
let bom = listed(&doc, Request::default()).unwrap();
|
||||
assert!(!bom.numbered);
|
||||
assert_eq!(items(&bom), [item("1", "A", "p"), item("2", "B", "p"), item("3", "C", "p")]);
|
||||
assert!(!recorded(&mut doc), "a design never numbered saves nothing");
|
||||
assert!(numbered(&mut doc, Request::default(), RENUMBER));
|
||||
assert!(!numbered(&mut doc, Request::default(), RENUMBER), "the same numbers again change nothing");
|
||||
let bom = listed(&doc, Request::default()).unwrap();
|
||||
assert!(bom.numbered);
|
||||
assert_eq!(items(&bom), [item("1", "A", ""), item("2", "B", ""), item("3", "C", "")]);
|
||||
// A new part, first by part number: the highest number + 1, listed by it, saved by the next change.
|
||||
let (d, _) = add(&mut doc, "D", 0);
|
||||
number(&mut doc, d, "P0");
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()).last(), Some(&item("4", "D", "p")));
|
||||
assert!(recorded(&mut doc));
|
||||
assert!(!recorded(&mut doc), "saved once");
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()).last(), Some(&item("4", "D", "")));
|
||||
// B removed leaves a gap, and back it has its number again.
|
||||
doc.occurrences.retain(|o| o.id != ob);
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()), [item("1", "A", ""), item("3", "C", ""), item("4", "D", "")]);
|
||||
assert!(!recorded(&mut doc), "a gap isn't filled");
|
||||
// Setting another row's number keeps B's: it isn't listed, but it is saved.
|
||||
assert!(numbered(&mut doc, Request::default(), Numbering::Set { row: vec![1], item: None, locked: Some(true) }));
|
||||
assert!(numbered(&mut doc, Request::default(), Numbering::Set { row: vec![1], item: None, locked: Some(false) }));
|
||||
let (e, _) = add(&mut doc, "E", 0);
|
||||
assert!(recorded(&mut doc));
|
||||
place(&mut doc, b, 0);
|
||||
let bom = listed(&doc, Request::default()).unwrap();
|
||||
assert_eq!(items(&bom), [item("1", "A", ""), item("2", "B", ""), item("3", "C", ""), item("4", "D", ""), item("5", "E", "")]);
|
||||
assert_eq!(bom.rows.last().map(|r| r.key.member.component), Some(e));
|
||||
// A renumber closing the gaps in item order, or in the list's order (by part number).
|
||||
doc.occurrences.retain(|o| o.component != b);
|
||||
assert!(numbered(&mut doc, Request::default(), Numbering::Renumber { start: 1, step: 1, order: Order::Item }));
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()), [item("1", "A", ""), item("2", "C", ""), item("3", "D", ""), item("4", "E", "")]);
|
||||
assert!(numbered(&mut doc, Request::default(), Numbering::Renumber { start: 10, step: 5, order: Order::List }));
|
||||
assert_eq!(
|
||||
items(&listed(&doc, Request::default()).unwrap()),
|
||||
[item("10", "D", ""), item("15", "A", ""), item("20", "C", ""), item("25", "E", "")]
|
||||
);
|
||||
// The renumber forgot B: back, it is new.
|
||||
place(&mut doc, b, 0);
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()).last(), Some(&item("26", "B", "p")));
|
||||
}
|
||||
|
||||
/// A view numbered twice, which only a damaged file has: listing reads the first numbering, and
|
||||
/// recording drops the second, so a change never lists the view again for it.
|
||||
#[test]
|
||||
fn a_view_numbered_twice_keeps_its_first_numbering() {
|
||||
let (mut doc, _) = three_parts();
|
||||
assert!(numbered(&mut doc, Request::default(), RENUMBER));
|
||||
let mut second = doc.assembly.item_numbers[0].clone();
|
||||
second.items.iter_mut().for_each(|i| i.number += 10);
|
||||
doc.assembly.item_numbers.push(second);
|
||||
let first = [item("1", "A", ""), item("2", "B", ""), item("3", "C", "")];
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()), first);
|
||||
assert!(recorded(&mut doc));
|
||||
assert_eq!(doc.assembly.item_numbers.len(), 1);
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()), first);
|
||||
assert!(!recorded(&mut doc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_keeps_its_number_through_a_part_number_edit() {
|
||||
let (mut doc, [(a, _), _, _]) = three_parts();
|
||||
assert!(numbered(&mut doc, Request::default(), RENUMBER));
|
||||
number(&mut doc, a, "P9");
|
||||
assert!(!recorded(&mut doc), "no new row");
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()), [item("1", "A", ""), item("2", "B", ""), item("3", "C", "")]);
|
||||
}
|
||||
|
||||
/// The defect the plan names: a renumber with locked rows never gives two rows one number.
|
||||
#[test]
|
||||
fn a_renumber_keeps_locked_numbers_and_never_gives_two_rows_one() {
|
||||
let (mut doc, _) = three_parts();
|
||||
let (d, _) = add(&mut doc, "D", 0);
|
||||
number(&mut doc, d, "P4");
|
||||
// Setting a number numbers the view first; one another row has is marked on both.
|
||||
assert!(numbered(&mut doc, Request::default(), Numbering::Set { row: vec![3], item: Some(2), locked: Some(true) }));
|
||||
assert_eq!(
|
||||
items(&listed(&doc, Request::default()).unwrap()),
|
||||
[item("1", "A", ""), item("2", "B", "d"), item("2", "C", "ld"), item("4", "D", "")]
|
||||
);
|
||||
for (start, step, want) in [(1, 1, ["1", "3", "2", "4"]), (2, 1, ["3", "4", "2", "5"]), (1, 2, ["1", "3", "2", "5"])] {
|
||||
assert!(numbered(&mut doc, Request::default(), Numbering::Renumber { start, step, order: Order::List }));
|
||||
let bom = listed(&doc, Request::default()).unwrap();
|
||||
let by_name: BTreeMap<String, (String, String)> = items(&bom).into_iter().map(|(i, n, f)| (n, (i, f))).collect();
|
||||
let got: Vec<&str> = ["A", "B", "C", "D"].iter().map(|n| by_name[*n].0.as_str()).collect();
|
||||
assert_eq!(got, want, "from {start} by {step}");
|
||||
assert!(bom.rows.iter().all(|r| !r.duplicate), "from {start} by {step}");
|
||||
assert_eq!(by_name["C"].1, "l");
|
||||
}
|
||||
// Unlocked, the next renumber moves it.
|
||||
assert!(numbered(&mut doc, Request::default(), Numbering::Set { row: vec![2], item: None, locked: Some(false) }));
|
||||
assert!(numbered(&mut doc, Request::default(), RENUMBER));
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()), [item("1", "A", ""), item("2", "B", ""), item("3", "C", ""), item("4", "D", "")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_levels_are_numbered_within_each_parent() {
|
||||
let mut doc = Document::default();
|
||||
let (frame, _) = add(&mut doc, "Frame", 0);
|
||||
let (bolt, _) = add(&mut doc, "Bolt", 0);
|
||||
number(&mut doc, frame, "F");
|
||||
number(&mut doc, bolt, "B");
|
||||
let (beam, _) = add(&mut doc, "Beam", frame);
|
||||
number(&mut doc, beam, "F1");
|
||||
let all = view(View::AllLevels);
|
||||
assert!(numbered(&mut doc, all, RENUMBER));
|
||||
assert_eq!(items(&listed(&doc, all).unwrap()), [item("1", "Bolt", ""), item("2", "Frame", ""), item("2.1", "Beam", "")]);
|
||||
// A plate in the frame is the frame's next; the design's parts-only view isn't numbered.
|
||||
let (plate, _) = add(&mut doc, "Plate", frame);
|
||||
number(&mut doc, plate, "F0");
|
||||
assert_eq!(items(&listed(&doc, all).unwrap()).last(), Some(&item("2.2", "Plate", "p")));
|
||||
assert!(recorded(&mut doc));
|
||||
assert_eq!(items(&listed(&doc, all).unwrap()).last(), Some(&item("2.2", "Plate", "")));
|
||||
assert!(!listed(&doc, Request::default()).unwrap().numbered);
|
||||
// Renumbered in the list's order, each level from the start.
|
||||
assert!(numbered(&mut doc, all, Numbering::Renumber { start: 5, step: 5, order: Order::List }));
|
||||
assert_eq!(
|
||||
items(&listed(&doc, all).unwrap()),
|
||||
[item("5", "Bolt", ""), item("10", "Frame", ""), item("10.5", "Plate", ""), item("10.10", "Beam", "")]
|
||||
);
|
||||
// A component's own list is numbered apart from the design's.
|
||||
let own = Request { source: frame, ..all };
|
||||
assert!(!listed(&doc, own).unwrap().numbered);
|
||||
assert!(numbered(&mut doc, own, RENUMBER));
|
||||
assert_eq!(items(&listed(&doc, own).unwrap()), [item("1", "Plate", ""), item("2", "Beam", "")]);
|
||||
assert_eq!(items(&listed(&doc, all).unwrap())[2], item("10.5", "Plate", ""));
|
||||
}
|
||||
|
||||
/// A numbering keeps the list options it was made with: the next change saves the rows they list.
|
||||
/// A list with other options reads the same numbers by row, the rest provisional.
|
||||
#[test]
|
||||
fn a_numbering_keeps_its_list_options_and_other_lists_read_its_numbers() {
|
||||
let (mut doc, [_, (b, _), _]) = three_parts();
|
||||
number(&mut doc, b, "P1");
|
||||
let by_definition = Request { group: Group::Definition, ..Request::default() };
|
||||
assert!(numbered(&mut doc, Request::default(), RENUMBER));
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()), [item("1", "A; B", ""), item("2", "C", "")]);
|
||||
assert_eq!(items(&listed(&doc, by_definition).unwrap()), [item("1", "A", ""), item("2", "C", ""), item("3", "B", "p")]);
|
||||
assert!(!recorded(&mut doc), "the numbering lists by part number: nothing new");
|
||||
assert!(numbered(&mut doc, by_definition, Numbering::Set { row: vec![3], item: Some(7), locked: None }));
|
||||
let saved = &doc.assembly.item_numbers;
|
||||
assert_eq!((saved.len(), saved[0].group), (1, Group::Definition), "one numbering a view, with the options last used");
|
||||
assert_eq!(items(&listed(&doc, by_definition).unwrap()), [item("1", "A", ""), item("2", "C", ""), item("7", "B", "")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_are_refused_outside_their_range_and_rows_are_found_as_shown() {
|
||||
let (mut doc, _) = three_parts();
|
||||
let refused = |doc: &mut Document, how: Numbering| {
|
||||
let state = ModelState::default();
|
||||
super::number(&mut Tree::new(doc, &state), &Request::default(), how).unwrap_err().to_string()
|
||||
};
|
||||
for (start, step) in [(0, 1), (1, 0), (MAX_ITEM + 1, 1)] {
|
||||
assert!(refused(&mut doc, Numbering::Renumber { start, step, order: Order::List }).contains("1 to 999999"), "{start} by {step}");
|
||||
}
|
||||
assert!(refused(&mut doc, Numbering::Renumber { start: MAX_ITEM - 1, step: 1, order: Order::List }).contains("passes 999999"));
|
||||
assert!(refused(&mut doc, Numbering::Renumber { start: 1, step: MAX_ITEM, order: Order::List }).contains("passes 999999"));
|
||||
assert!(refused(&mut doc, Numbering::Set { row: vec![4], item: Some(1), locked: None }).contains("item 4"));
|
||||
assert!(refused(&mut doc, Numbering::Set { row: vec![1, 1], item: Some(1), locked: None }).contains("item 1.1"));
|
||||
assert!(refused(&mut doc, Numbering::Set { row: vec![1], item: Some(0), locked: None }).contains("1 to 999999"));
|
||||
assert!(doc.assembly.item_numbers.is_empty(), "nothing saved");
|
||||
// The highest number is taken; a renumber that would pass it is refused whole.
|
||||
assert!(numbered(&mut doc, Request::default(), Numbering::Renumber { start: MAX_ITEM - 2, step: 1, order: Order::List }));
|
||||
assert_eq!(listed(&doc, Request::default()).unwrap().rows.last().map(|r| r.item), Some(MAX_ITEM));
|
||||
}
|
||||
|
||||
/// A damaged file's numbers: one out of range is no number, and of a key saved twice the first
|
||||
/// holds. A component's numbering goes with it.
|
||||
#[test]
|
||||
fn saved_numbers_are_read_leniently_and_go_with_their_component() {
|
||||
let (mut doc, [(a, _), (b, _), (c, _)]) = three_parts();
|
||||
let key = |component| RowKey { parents: vec![], member: Member { component, bom_type: Normal } };
|
||||
let saved = |key, number| ItemNumber { key, number, locked: false };
|
||||
doc.assembly.item_numbers.push(ItemNumbers {
|
||||
source: 0,
|
||||
view: View::PartsOnly,
|
||||
group: Group::PartNumber,
|
||||
fold: false,
|
||||
purchased_contents: false,
|
||||
items: vec![saved(key(a), 4), saved(key(a), 9), saved(key(b), 0), saved(key(c), MAX_ITEM + 1)],
|
||||
});
|
||||
assert_eq!(items(&listed(&doc, Request::default()).unwrap()), [item("4", "A", ""), item("5", "B", "p"), item("6", "C", "p")]);
|
||||
let (sub, _) = add(&mut doc, "Sub", 0);
|
||||
add(&mut doc, "Inner", sub);
|
||||
assert!(numbered(&mut doc, Request { source: sub, ..Request::default() }, RENUMBER));
|
||||
assert_eq!(doc.assembly.item_numbers.len(), 2);
|
||||
doc.components.retain(|c| c.id != sub);
|
||||
doc.occurrences.retain(|o| o.component != sub && o.parent != sub);
|
||||
assert!(recorded(&mut doc));
|
||||
assert_eq!(doc.assembly.item_numbers.iter().map(|n| n.source).collect::<Vec<_>>(), [0], "the design's is kept");
|
||||
}
|
||||
|
||||
/// Numbers are saved in the design under `assembly.item_numbers`, and change no geometry.
|
||||
#[test]
|
||||
fn item_numbers_are_saved_and_are_part_properties() {
|
||||
let (mut doc, _) = three_parts();
|
||||
let before = doc.clone();
|
||||
assert!(numbered(&mut doc, Request { fold: true, ..view(View::AllLevels) }, RENUMBER));
|
||||
assert!(doc.differs_only_in_part_properties(&before));
|
||||
assert!(!doc.differs_only_in_part_properties(&doc.clone()), "no change is no part-properties change");
|
||||
let json = serde_json::to_value(&doc.assembly).unwrap();
|
||||
let first = &json["item_numbers"][0];
|
||||
assert_eq!(
|
||||
(&first["source"], &first["levels"], &first["group_by"], &first["group_children"]),
|
||||
(&json!(0), &json!("all"), &json!("part_number"), &json!(true))
|
||||
);
|
||||
assert_eq!(first["items"][0], json!({"member": {"component": doc.components[0].id, "bom_type": "normal"}, "number": 1}));
|
||||
// Through the whole design, as a save writes it: a design whose assembly holds only numbers keeps them.
|
||||
assert_eq!(Document::from_json(&doc.to_json()).unwrap().assembly, doc.assembly);
|
||||
assert!(serde_json::to_value(&before.assembly).unwrap().get("item_numbers").is_none(), "a design never numbered saves nothing new");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_quantity_is_pieces_or_a_length_with_the_designs_parameters() {
|
||||
let mut doc = Document::default();
|
||||
doc.set_param("span", "100", Some("mm"), None).unwrap();
|
||||
doc.set_param("packs", "3", Some(""), None).unwrap();
|
||||
let amount = |text: &str| super::amount(&doc, text);
|
||||
assert_eq!(amount("4").unwrap(), Amount::Pieces(4.0));
|
||||
assert_eq!(amount("packs * 4").unwrap(), Amount::Pieces(12.0));
|
||||
assert_eq!(amount("2.5 m").unwrap(), Amount::Length(2500.0));
|
||||
assert_eq!(amount("10 in").unwrap(), Amount::Length(254.0));
|
||||
assert_eq!(amount("2 * span + 50").unwrap(), Amount::Length(250.0), "a bare number beside a length is mm");
|
||||
let refused = |text: &str| amount(text).unwrap_err().to_string();
|
||||
assert!(refused("90 deg").contains("a number of pieces or a length"), "{}", refused("90 deg"));
|
||||
assert!(refused("span * span").contains("not supported yet: area"));
|
||||
for mass_or_volume in ["span^3", "50 g", "0.5 kg", "2 lb", "500 ml"] {
|
||||
assert!(refused(mass_or_volume).contains("not supported yet: mass and volume"), "{mass_or_volume}: {}", refused(mass_or_volume));
|
||||
}
|
||||
for out_of_range in ["0", "-1", "-2 m", "1e9 + 1", "1001 km"] {
|
||||
assert!(refused(out_of_range).contains("above 0"), "{out_of_range}: {}", refused(out_of_range));
|
||||
}
|
||||
assert_eq!(amount("1e9").unwrap(), Amount::Pieces(1e9), "the cap itself");
|
||||
assert!(refused("cable_length").contains("unknown parameter `cable_length`"));
|
||||
assert!(amount("").is_err() && amount("2 *").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rows_quantity_adds_up_its_placements() {
|
||||
let mut doc = Document::default();
|
||||
doc.set_param("cable_length", "2.5", Some("m"), None).unwrap();
|
||||
let (cable, _) = add(&mut doc, "Cable", 0);
|
||||
set(&mut doc, cable, "bom_quantity", "cable_length");
|
||||
place(&mut doc, cable, 0);
|
||||
place(&mut doc, cable, 0);
|
||||
let (pack, _) = add(&mut doc, "Pack", 0);
|
||||
set(&mut doc, pack, "bom_quantity", "4");
|
||||
place(&mut doc, pack, 0);
|
||||
add(&mut doc, "Plate", 0);
|
||||
// Two definitions of one part number: one with a length, one a piece.
|
||||
let (hose, _) = add(&mut doc, "Hose", 0);
|
||||
let (hose_b, _) = add(&mut doc, "Hose B", 0);
|
||||
number(&mut doc, hose, "H-1");
|
||||
number(&mut doc, hose_b, "H-1");
|
||||
set(&mut doc, hose, "bom_quantity", "500 mm");
|
||||
// A subassembly placed twice holding the pack once.
|
||||
let (kit, _) = add(&mut doc, "Kit", 0);
|
||||
place(&mut doc, kit, 0);
|
||||
place(&mut doc, pack, kit);
|
||||
let quantities = |doc: &Document, request: Request| -> Vec<(String, Quantity)> {
|
||||
listed(doc, request).unwrap().rows_deep().into_iter().map(|(_, r)| (r.names.join("; "), r.quantity)).collect()
|
||||
};
|
||||
let q = quantities(&doc, view(View::PartsOnly));
|
||||
let of = |q: &[(String, Quantity)], name: &str| q.iter().find(|(n, _)| n == name).map(|(_, q)| *q);
|
||||
assert_eq!(of(&q, "Cable"), Some(Quantity::Length(7500.0)), "{q:?}");
|
||||
assert_eq!(of(&q, "Pack"), Some(Quantity::Pieces(16.0)), "two of its own and one in each kit: {q:?}");
|
||||
assert_eq!(of(&q, "Plate"), Some(Quantity::Count));
|
||||
assert_eq!(of(&q, "Hose; Hose B"), Some(Quantity::Unknown(Uncounted::Mixed)));
|
||||
// The kit's row in the top level; its own pack listed under it in all levels.
|
||||
let q = quantities(&doc, view(View::AllLevels));
|
||||
assert_eq!(q.iter().filter(|(n, _)| n == "Pack").map(|(_, q)| *q).collect::<Vec<_>>(), [Quantity::Pieces(8.0), Quantity::Pieces(8.0)], "{q:?}");
|
||||
// A placement without a quantity beside packs counts one piece: a box of 4 and a loose one.
|
||||
let (boxed, _) = add(&mut doc, "Box", 0);
|
||||
let (loose, _) = add(&mut doc, "Loose", 0);
|
||||
number(&mut doc, boxed, "K-1");
|
||||
number(&mut doc, loose, "K-1");
|
||||
set(&mut doc, boxed, "bom_quantity", "4");
|
||||
assert_eq!(of(&quantities(&doc, view(View::PartsOnly)), "Box; Loose"), Some(Quantity::Pieces(5.0)));
|
||||
// A parameter deleted (a damaged file): that row is unknown, the list still works.
|
||||
doc.params.retain(|p| p.name != "cable_length");
|
||||
let q = quantities(&doc, view(View::PartsOnly));
|
||||
assert_eq!(of(&q, "Cable"), Some(Quantity::Unknown(Uncounted::Invalid)));
|
||||
assert_eq!(of(&q, "Pack"), Some(Quantity::Pieces(16.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_virtual_component_holding_something_is_named() {
|
||||
let mut doc = Document::default();
|
||||
let (grease, _) = add(&mut doc, "Grease", 0);
|
||||
let (frame, _) = add(&mut doc, "Frame", 0);
|
||||
let (bolt, _) = add(&mut doc, "Bolt", frame);
|
||||
assert!(doc.virtual_holding().is_empty());
|
||||
set_virtual(&mut doc, grease);
|
||||
assert!(doc.virtual_holding().is_empty(), "an empty virtual component holds nothing");
|
||||
place(&mut doc, bolt, grease);
|
||||
assert_eq!(doc.virtual_holding(), [grease]);
|
||||
set_virtual(&mut doc, frame);
|
||||
assert_eq!(doc.virtual_holding(), [grease, frame]);
|
||||
}
|
||||
|
||||
fn set_virtual(doc: &mut Document, component: u64) {
|
||||
doc.components.iter_mut().find(|c| c.id == component).unwrap().metadata.0.insert("virtual".into(), json!(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_quantity_uses_its_parameters_as_any_expression_does() {
|
||||
let mut doc = Document::default();
|
||||
doc.set_param("span", "100", Some("mm"), None).unwrap();
|
||||
let (cable, _) = add(&mut doc, "Cable", 0);
|
||||
set(&mut doc, cable, "bom_quantity", "2 * span");
|
||||
assert_eq!(doc.param_users("span"), [crate::ParamUser::Quantity { component: cable, name: "Cable".into() }]);
|
||||
assert!(doc.remove_param("span").unwrap_err().to_string().contains("component Cable (quantity)"));
|
||||
assert_eq!(doc.rename_param("span", "width").unwrap(), 1);
|
||||
assert_eq!(doc.components[0].metadata.bom_quantity(), Some("2 * width"));
|
||||
assert_eq!(amount(&doc, "2 * width").unwrap(), Amount::Length(200.0));
|
||||
}
|
||||
|
||||
/// A balloon's item reads the row its part is counted in, by a path from the top of the design: in
|
||||
/// all levels with the rows it is under, in a component's list only below that component, and the
|
||||
/// reasons there is none stay apart.
|
||||
#[test]
|
||||
fn item_of_reads_a_balloons_row() {
|
||||
let (doc, at) = mixed();
|
||||
let path = |names: &[&str]| -> Vec<u64> { names.iter().map(|n| at[*n]).collect() };
|
||||
let state = ModelState::default();
|
||||
let mut tree = Tree::new(&doc, &state);
|
||||
let all = list_with(&mut tree, &Request { view: View::AllLevels, ..Request::default() }, true).unwrap();
|
||||
let (washer, _) = all.row_of(&tree, &path(&["Sub", "Washer"])).unwrap();
|
||||
assert_eq!(washer.len(), 2, "under its subassembly");
|
||||
assert_eq!(all.item_of(&tree, &path(&["Sub", "Washer"])), Ok(item_text(&washer)));
|
||||
assert!(all.item_of(&tree, &path(&["Sub", "Washer"])).unwrap().contains('.'));
|
||||
|
||||
let sub = doc.find_component("Sub").unwrap();
|
||||
let of_sub = list_with(&mut tree, &Request { source: sub, ..Request::default() }, true).unwrap();
|
||||
assert_eq!(of_sub.item_of(&tree, &path(&["Sub", "Washer"])), Ok("1".to_string()), "cut to start below Sub");
|
||||
assert_eq!(of_sub.item_of(&tree, &path(&["Sub"])), Err(Unlisted::NoRow), "the assembly listed has no row");
|
||||
assert_eq!(of_sub.item_of(&tree, &path(&["Bolt"])), Err(Unlisted::NotFound), "not through Sub");
|
||||
assert_eq!(of_sub.item_of(&tree, &[]), Err(Unlisted::NotFound));
|
||||
|
||||
assert_eq!(all.item_of(&tree, &path(&["Fixture", "Pin"])), Err(Unlisted::Reference));
|
||||
assert_eq!(all.item_of(&tree, &[999]), Err(Unlisted::NotFound));
|
||||
assert_eq!(all.item_of(&tree, &vec![at["Bolt"]; 65]), Err(Unlisted::NotFound));
|
||||
assert!(of_sub.item_of(&tree, &vec![at["Sub"]; 65]).is_err());
|
||||
assert_eq!(all.item_of(&tree, &[]), Err(Unlisted::NoRow));
|
||||
}
|
||||
@@ -12,9 +12,6 @@ pub(crate) fn shell_plane_family(planes: impl IntoIterator<Item = Option<solidco
|
||||
first.origin.is_finite() && first.x.is_finite() && first.y.is_finite() && planes.all(|plane| plane == Some(first))
|
||||
}
|
||||
|
||||
/// The longest face or edge name a saved model reference keeps, in bytes.
|
||||
pub const MAX_REFERENCE_NAME_BYTES: usize = 512;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigFaceTarget {
|
||||
@@ -41,7 +38,7 @@ impl ConfigFaceTarget {
|
||||
if self.body.is_empty()
|
||||
|| self.body.len() > 128
|
||||
|| self.name.is_empty()
|
||||
|| self.name.len() > MAX_REFERENCE_NAME_BYTES
|
||||
|| self.name.len() > 512
|
||||
|| self.origin_feature == 0
|
||||
|| [self.point.x, self.point.y, self.point.z].iter().any(|v| !v.is_finite() || v.abs() > 1e9)
|
||||
{
|
||||
@@ -56,7 +53,7 @@ pub fn validate_face_target_json(target: &Value) -> Result<()> {
|
||||
if !(4..=5).contains(&fields.len())
|
||||
|| fields.keys().any(|key| !["body", "origin_feature", "name", "point", "certificate"].contains(&key.as_str()))
|
||||
|| fields.get("body").and_then(Value::as_str).is_none_or(|body| body.is_empty() || body.len() > 128)
|
||||
|| fields.get("name").and_then(Value::as_str).is_none_or(|name| name.is_empty() || name.len() > MAX_REFERENCE_NAME_BYTES)
|
||||
|| fields.get("name").and_then(Value::as_str).is_none_or(|name| name.is_empty() || name.len() > 512)
|
||||
|| fields.get("origin_feature").and_then(Value::as_u64).is_none_or(|id| id == 0)
|
||||
|| fields.get("point").and_then(Value::as_array).is_none_or(|point| {
|
||||
point.len() != 3 || point.iter().any(|value| value.as_f64().is_none_or(|value| !value.is_finite() || value.abs() > 1e9))
|
||||
@@ -94,7 +91,7 @@ pub fn validate_styles_json(value: &Value) -> Result<()> {
|
||||
if !certificate.is_null()
|
||||
&& (face.get("strict_name").and_then(Value::as_bool) != Some(true)
|
||||
|| face.get("body").and_then(Value::as_str).is_none_or(|body| body.is_empty() || body.len() > 128)
|
||||
|| face.get("name").and_then(Value::as_str).is_none_or(|name| name.is_empty() || name.len() > MAX_REFERENCE_NAME_BYTES)
|
||||
|| face.get("name").and_then(Value::as_str).is_none_or(|name| name.is_empty() || name.len() > 512)
|
||||
|| face.get("point").and_then(Value::as_array).is_none_or(|point| {
|
||||
point.len() != 3 || point.iter().any(|value| value.as_f64().is_none_or(|value| !value.is_finite() || value.abs() > 1e9))
|
||||
}))
|
||||
|
||||
@@ -207,7 +207,7 @@ fn edge_circle(b: &ModelBody, index: usize, pts: &[Vec3]) -> Option<(Vec3, Vec3)
|
||||
}
|
||||
|
||||
/// The most edge names a vertex reference keeps.
|
||||
pub(crate) const MAX_VERTEX_EDGES: usize = 16;
|
||||
const MAX_VERTEX_EDGES: usize = 16;
|
||||
|
||||
/// Where the edges named `names` meet: the end every one of them has, nearest `at` (two edges
|
||||
/// between the same two vertices share both). `None` when an edge is gone or they share no end.
|
||||
@@ -230,32 +230,17 @@ fn named_vertex(st: &ModelState, at: Vec3, names: &[String]) -> Option<Vec3> {
|
||||
})
|
||||
})
|
||||
.collect::<Option<_>>()?;
|
||||
shared_ends(&ends).into_iter().min_by(|p, q| p.dist(at).total_cmp(&q.dist(at)))
|
||||
}
|
||||
|
||||
/// How near two points of a body are the same point: a millionth of their distance from the
|
||||
/// origin, and at least a millionth of a millimetre.
|
||||
pub(crate) fn point_tolerance(p: Vec3) -> f64 {
|
||||
1e-6 * (1.0 + p.len())
|
||||
}
|
||||
|
||||
/// Where edges meet: the ends in `ends[0]` that every other list in `ends` has too (to
|
||||
/// [`point_tolerance`]), each once. Empty when they share none.
|
||||
pub(crate) fn shared_ends<E: AsRef<[Vec3]>>(ends: &[E]) -> Vec<Vec3> {
|
||||
let tol = point_tolerance;
|
||||
let Some((first, rest)) = ends.split_first() else { return Vec::new() };
|
||||
let mut shared: Vec<Vec3> = Vec::new();
|
||||
for p in first.as_ref().iter().copied() {
|
||||
let everywhere = rest.iter().all(|e| e.as_ref().iter().any(|q| q.dist(p) <= tol(p)));
|
||||
if p.is_finite() && everywhere && !shared.iter().any(|q| q.dist(p) <= tol(p)) {
|
||||
shared.push(p);
|
||||
}
|
||||
}
|
||||
shared
|
||||
let (first, rest) = ends.split_first()?;
|
||||
let tol = |p: Vec3| 1e-6 * (1.0 + p.len());
|
||||
first
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|p| p.is_finite() && rest.iter().all(|e| e.iter().any(|q| q.dist(*p) <= tol(*p))))
|
||||
.min_by(|p, q| p.dist(at).total_cmp(&q.dist(at)))
|
||||
}
|
||||
|
||||
/// A polyline that is straight: its line.
|
||||
pub(crate) fn as_line(pts: &[Vec3]) -> Option<(Vec3, Vec3)> {
|
||||
fn as_line(pts: &[Vec3]) -> Option<(Vec3, Vec3)> {
|
||||
let (a, b) = (*pts.first()?, *pts.last()?);
|
||||
let d = (b - a).normalized()?;
|
||||
let len = (b - a).len();
|
||||
@@ -263,7 +248,7 @@ pub(crate) fn as_line(pts: &[Vec3]) -> Option<(Vec3, Vec3)> {
|
||||
}
|
||||
|
||||
/// A polyline on a circle: (centre, unit normal, radius).
|
||||
pub(crate) fn as_circle(pts: &[Vec3]) -> Option<(Vec3, Vec3, f64)> {
|
||||
fn as_circle(pts: &[Vec3]) -> Option<(Vec3, Vec3, f64)> {
|
||||
let n = pts.len();
|
||||
if n < 4 {
|
||||
return None;
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
//! The existing length display policy, shared by windowed and headless consumers.
|
||||
//!
|
||||
//! Values are stored in millimetres and displayed in any length unit of the expression unit table
|
||||
//! ([`UNITS`]): a design's five units and the others a drawing may give its measurements in. One
|
||||
//! table for both, so every length unit a design or a drawing accepts shows its own numbers. Other
|
||||
//! names retain the millimetre fallback. These functions format values, not validate them:
|
||||
//! non-finite inputs retain Rust's `NaN`, `inf` and `-inf` text. Callers that require finite
|
||||
//! measurements must validate them first.
|
||||
//! Values are stored in millimetres and displayed in the five design units. Unknown unit
|
||||
//! names retain the millimetre fallback; expression units have a separate, broader vocabulary.
|
||||
//! These functions format values, not validate them: non-finite inputs retain Rust's `NaN`,
|
||||
//! `inf` and `-inf` text. Callers that require finite measurements must validate them first.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::expr::{Kind, UNITS};
|
||||
use crate::{DocError, Result, expr};
|
||||
|
||||
/// Units per millimetre and the unit's name, for a length unit of [`UNITS`]. Other names use
|
||||
/// millimetres.
|
||||
/// Units per millimetre and the unit's name. Unknown names use millimetres.
|
||||
pub fn unit_scale(units: &str) -> (f64, &'static str) {
|
||||
match UNITS.iter().find(|(name, kind, _)| *name == units && *kind == Kind::Length) {
|
||||
Some(&(name, _, mm)) => (1.0 / mm, name),
|
||||
None => (1.0, "mm"),
|
||||
match units {
|
||||
"cm" => (0.1, "cm"),
|
||||
"m" => (0.001, "m"),
|
||||
"in" => (1.0 / 25.4, "in"),
|
||||
"ft" => (1.0 / 304.8, "ft"),
|
||||
_ => (1.0, "mm"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inch labels (`in` or `inch`) retain a meaningful fourth digit without promoting floating-point
|
||||
/// noise. `value` is already converted to the requested display unit, not a millimetre value.
|
||||
/// Inch labels retain a meaningful fourth digit without promoting floating-point noise.
|
||||
/// `value` is already converted to the requested display unit, not a millimetre value.
|
||||
pub fn length_precision(value: f64, units: &str, decimals: u8) -> usize {
|
||||
let base = usize::from(decimals.min(8));
|
||||
if unit_scale(units).0 != unit_scale("in").0 {
|
||||
if units != "in" {
|
||||
return base;
|
||||
}
|
||||
let needs_four = ((value * 10_000.0).round() - (value * 1000.0).round() * 10.0).abs() >= 0.5;
|
||||
@@ -183,7 +182,6 @@ fn grouped_digits(text: &mut String, digits: &str, integer: bool, groups: bool)
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{format_length, length_number, length_precision, unit_scale};
|
||||
use crate::expr::{Kind, UNITS};
|
||||
|
||||
#[test]
|
||||
fn all_design_units_keep_bare_and_suffixed_labels() {
|
||||
@@ -247,26 +245,9 @@ mod tests {
|
||||
assert_eq!(format_length(-0.0, "in", 2), "-0.000 in");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_length_unit_shows_its_own_numbers() {
|
||||
// Each length unit of the expression table, which a design's units and a drawing's unit
|
||||
// come from, never millimetres unless it is millimetres.
|
||||
for &(name, kind, mm) in UNITS {
|
||||
if kind != Kind::Length {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(unit_scale(name), (1.0 / mm, name), "{name}");
|
||||
let bare = if matches!(name, "in" | "inch") { "12.500" } else { "12.50" };
|
||||
assert_eq!(length_number(12.5 * mm, name, 2), bare, "{name}");
|
||||
assert_eq!(format_length(12.5 * mm, name, 2), format!("{bare} {name}"), "{name}");
|
||||
}
|
||||
assert_eq!(length_number(25.4 * 1.2345, "inch", 0), "1.2345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_display_units_keep_the_millimetre_fallback() {
|
||||
// Names that aren't a length unit of the expression table, angles included.
|
||||
for units in ["", " in ", "IN", "unsupported", "deg", "rad"] {
|
||||
for units in ["", "inch", " in ", "IN", "unsupported"] {
|
||||
assert_eq!(unit_scale(units), (1.0, "mm"), "{units}");
|
||||
assert_eq!(length_number(25.4, units, 2), "25.40", "{units}");
|
||||
assert_eq!(format_length(25.4, units, 2), "25.40 mm", "{units}");
|
||||
|
||||
@@ -504,7 +504,12 @@ impl EmbossFace {
|
||||
|
||||
/// Resolve this exact placement without expanding every assembly instance.
|
||||
pub fn body_frame(&self, doc: &Document, body: &crate::ModelBody) -> Result<crate::Mat> {
|
||||
doc.body_frame_at(body, &self.path)
|
||||
let owner = doc.model_body_component(body);
|
||||
let mut frame = Self::component_frame(doc, owner, &self.path)?;
|
||||
if let Some(offset) = doc.model_body_offset(body) {
|
||||
frame = crate::mat_mul(&frame, offset);
|
||||
}
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Resolve one saved component path without expanding other occurrences.
|
||||
@@ -729,10 +734,6 @@ pub enum FeatureKind {
|
||||
/// from the vertex instead of where the rounds meet (a constant-radius fillet only).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
setbacks: Vec<CornerSetback>,
|
||||
/// How two rounds turn where they meet at a sharp side edge: in a mitre (the default) or
|
||||
/// swept round it (a constant-radius, exact, rolling-corner fillet without setbacks only).
|
||||
#[serde(default, skip_serializing_if = "FilletTurn::is_mitre")]
|
||||
turn: FilletTurn,
|
||||
/// Round each picked edge's tangent chain with it (the default: edges continuing it
|
||||
/// smoothly at its ends, as far as they go), or only the picked edges.
|
||||
#[serde(default = "yes", skip_serializing_if = "is_yes")]
|
||||
@@ -1970,38 +1971,6 @@ impl FilletCorner {
|
||||
}
|
||||
}
|
||||
|
||||
/// How two constant rounds of one radius turn round a sharp, straight, convex side edge square to
|
||||
/// both on a plane, such as the rounds along a boss's base at its upright edges.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FilletTurn {
|
||||
/// The rounds run on to where they cross and meet in a mitre.
|
||||
#[default]
|
||||
Mitre,
|
||||
/// The rounds end at the side edge and the corner is their section revolved about it, a
|
||||
/// torus tangent to both (`solidcraft_kernel::FilletTurn::Swept`).
|
||||
Swept,
|
||||
}
|
||||
|
||||
/// Why a swept turn is refused with a fillet's other options: the kernel sweeps the turn of two
|
||||
/// constant rounds of one radius, at its own corners only.
|
||||
pub const SWEPT_TAKES: &str = "a swept corner turns two constant rounds of one radius: `turn: swept` takes `type: constant`, \
|
||||
`corner: rolling`, no `setbacks` and `fit: exact` or `reduce`";
|
||||
|
||||
impl FilletTurn {
|
||||
pub fn is_mitre(&self) -> bool {
|
||||
*self == FilletTurn::Mitre
|
||||
}
|
||||
|
||||
/// The kernel's turn.
|
||||
pub fn kernel(self) -> solidcraft_kernel::FilletTurn {
|
||||
match self {
|
||||
FilletTurn::Mitre => solidcraft_kernel::FilletTurn::Mitre,
|
||||
FilletTurn::Swept => solidcraft_kernel::FilletTurn::Swept,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How Extend continues a surface past its edges.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -3218,12 +3187,6 @@ pub struct Document {
|
||||
/// it saves.
|
||||
#[serde(default, skip_serializing_if = "Visibility::is_empty")]
|
||||
pub visibility: Visibility,
|
||||
/// The design's drawings (#3890), by name, then id ([`crate::drawing::in_design`]), each behind
|
||||
/// its own `Arc`, so an undo step shares every drawing it didn't change. Each is read by its own
|
||||
/// family's rules ([`crate::format::read`]), never as a field of the design; a container keeps
|
||||
/// each in its own entry.
|
||||
#[serde(skip_deserializing, skip_serializing_if = "Vec::is_empty")]
|
||||
pub drawings: Vec<std::sync::Arc<crate::drawing::Drawing>>,
|
||||
}
|
||||
|
||||
/// The browser's view of a design: items and folders hidden or shown, sketch profiles and
|
||||
@@ -3511,7 +3474,6 @@ impl Document {
|
||||
browser_order: Default::default(),
|
||||
named_views: Vec::new(),
|
||||
visibility: Visibility::default(),
|
||||
drawings: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3652,38 +3614,29 @@ impl Document {
|
||||
*self == normalized
|
||||
}
|
||||
|
||||
/// Only part properties changed (the fields `with_part_properties_of` names), and perhaps
|
||||
/// drawings with them, as placing a parts list saves its numbers: nothing to evaluate. The
|
||||
/// same whole-Document proof as [`Document::differs_only_in_render`].
|
||||
/// Only part properties changed (the fields `with_part_properties_of` names): nothing to
|
||||
/// evaluate. The same whole-Document proof as [`Document::differs_only_in_render`].
|
||||
pub fn differs_only_in_part_properties(&self, previous: &Document) -> bool {
|
||||
previous.with_part_properties_of(self).is_some_and(|normalized| normalized == *self)
|
||||
}
|
||||
|
||||
/// This design with `other`'s part properties, or `None` when they are this design's
|
||||
/// already (then nothing is copied). The part properties are the fields that change no
|
||||
/// geometry: the design's metadata, each component's metadata and free properties, each
|
||||
/// placement's bill of materials type and the saved item numbers. A field that joins them is
|
||||
/// compared and copied here, and nowhere else.
|
||||
/// geometry: the design's metadata, and each component's metadata and free properties. A
|
||||
/// field that joins them is compared and copied here, and nowhere else.
|
||||
fn with_part_properties_of(&self, other: &Document) -> Option<Document> {
|
||||
let same = self.metadata == other.metadata
|
||||
&& self.components.iter().zip(&other.components).all(|(c, o)| c.metadata == o.metadata && c.properties == o.properties)
|
||||
&& self.occurrences.iter().zip(&other.occurrences).all(|(c, o)| c.bom_type == o.bom_type)
|
||||
&& self.assembly.item_numbers == other.assembly.item_numbers;
|
||||
&& self.components.iter().zip(&other.components).all(|(c, o)| c.metadata == o.metadata && c.properties == o.properties);
|
||||
if same {
|
||||
return None;
|
||||
}
|
||||
let mut out = self.clone();
|
||||
out.metadata.clone_from(&other.metadata);
|
||||
out.assembly.item_numbers.clone_from(&other.assembly.item_numbers);
|
||||
out.drawings.clone_from(&other.drawings);
|
||||
// Paired by position: if the components or placements themselves differ, so do the designs.
|
||||
// Paired by position: if the components themselves differ, so do the designs.
|
||||
for (c, o) in out.components.iter_mut().zip(&other.components) {
|
||||
c.metadata.clone_from(&o.metadata);
|
||||
c.properties.clone_from(&o.properties);
|
||||
}
|
||||
for (c, o) in out.occurrences.iter_mut().zip(&other.occurrences) {
|
||||
c.bom_type = o.bom_type;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
@@ -3699,32 +3652,6 @@ impl Document {
|
||||
*self == normalized
|
||||
}
|
||||
|
||||
/// Only drawings changed: nothing to evaluate, as a drawing reads the model and never changes
|
||||
/// it. The same whole-Document proof as [`Document::differs_only_in_render`]; a model edit
|
||||
/// shares every drawing's `Arc`, so it costs nothing here.
|
||||
pub fn differs_only_in_drawings(&self, previous: &Document) -> bool {
|
||||
let shared =
|
||||
self.drawings.len() == previous.drawings.len() && self.drawings.iter().zip(&previous.drawings).all(|(a, b)| std::sync::Arc::ptr_eq(a, b));
|
||||
if shared || self.drawings == previous.drawings {
|
||||
return false;
|
||||
}
|
||||
let mut normalized = previous.clone();
|
||||
normalized.drawings = self.drawings.clone();
|
||||
*self == normalized
|
||||
}
|
||||
|
||||
/// The design's drawing `id`.
|
||||
pub fn drawing(&self, id: u64) -> Option<&std::sync::Arc<crate::drawing::Drawing>> {
|
||||
self.drawings.iter().find(|d| d.id == id)
|
||||
}
|
||||
|
||||
/// Put `drawing` in, in place of the one with its id if there is one, in the drawings' order.
|
||||
pub fn put_drawing(&mut self, drawing: std::sync::Arc<crate::drawing::Drawing>) {
|
||||
self.drawings.retain(|d| d.id != drawing.id);
|
||||
self.drawings.push(drawing);
|
||||
crate::drawing::sort(&mut self.drawings);
|
||||
}
|
||||
|
||||
/// The colour a body is shown and exported in (sRGB as 0–1 fractions): its appearance,
|
||||
/// else its component's, else its material's look, else the colour it was imported with.
|
||||
/// (Faces may have their own: `face_looks`.)
|
||||
@@ -3835,12 +3762,12 @@ impl Document {
|
||||
}
|
||||
|
||||
pub fn legacy_body_component(&self, body: &str, feature: u64) -> u64 {
|
||||
self.legacy_body_component_with(body, feature, |f| self.feature(f).map(|f| f.component))
|
||||
self.body_components.get(body).copied().unwrap_or_else(|| self.feature(feature).map(|f| f.component).unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Ownership in this evaluated timeline state, rather than a later transfer.
|
||||
pub fn model_body_component(&self, body: &crate::ModelBody) -> u64 {
|
||||
self.model_body_component_with(body, |f| self.feature(f).map(|f| f.component))
|
||||
if body.transferred || body.source_name.is_some() { body.component } else { self.legacy_body_component(&body.name, body.feature) }
|
||||
}
|
||||
|
||||
/// [`Document::model_body_component`] of every body of `state`, in its order, with the
|
||||
@@ -3865,25 +3792,17 @@ impl Document {
|
||||
/// in one pass: the timeline is looked up once, not once per body.
|
||||
pub fn body_owners(&self, state: &crate::ModelState) -> std::collections::HashSet<u64> {
|
||||
let features: std::collections::HashMap<u64, u64> = self.features.iter().rev().map(|f| (f.id, f.component)).collect();
|
||||
state.bodies.iter().map(|b| self.model_body_component_with(b, |f| features.get(&f).copied())).collect()
|
||||
}
|
||||
|
||||
/// [`Document::model_body_component`]'s rule, written once: a body the evaluation transferred,
|
||||
/// or placed from a source, keeps the component it was given; any other is
|
||||
/// [`Document::legacy_body_component`]'s. `feature_component` finds a feature's component:
|
||||
/// the timeline for one body, a map built once for many ([`Document::body_owners`]).
|
||||
fn model_body_component_with(&self, body: &crate::ModelBody, feature_component: impl FnOnce(u64) -> Option<u64>) -> u64 {
|
||||
if body.transferred || body.source_name.is_some() {
|
||||
body.component
|
||||
} else {
|
||||
self.legacy_body_component_with(&body.name, body.feature, feature_component)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Document::legacy_body_component`]'s rule, written once: the component the body was moved
|
||||
/// to, else its feature's, else the root.
|
||||
fn legacy_body_component_with(&self, body: &str, feature: u64, feature_component: impl FnOnce(u64) -> Option<u64>) -> u64 {
|
||||
self.body_components.get(body).copied().unwrap_or_else(|| feature_component(feature).unwrap_or(0))
|
||||
state
|
||||
.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
if b.transferred || b.source_name.is_some() {
|
||||
b.component
|
||||
} else {
|
||||
self.body_components.get(&b.name).copied().unwrap_or_else(|| features.get(&b.feature).copied().unwrap_or(0))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Recorded transfers already converted geometry into the new local frame.
|
||||
@@ -3891,17 +3810,6 @@ impl Document {
|
||||
if body.transferred { None } else { self.body_offsets.get(&body.name) }
|
||||
}
|
||||
|
||||
/// The frame `body`'s geometry is in, placed through the occurrence path `path` (empty: its
|
||||
/// component's own placement): the placement, then the body's offset. One placement, without
|
||||
/// expanding every assembly instance.
|
||||
pub fn body_frame_at(&self, body: &crate::ModelBody, path: &[u64]) -> Result<crate::Mat> {
|
||||
let mut frame = EmbossFace::component_frame(self, self.model_body_component(body), path)?;
|
||||
if let Some(offset) = self.model_body_offset(body) {
|
||||
frame = crate::mat_mul(&frame, offset);
|
||||
}
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Add a component under `parent` and return its id.
|
||||
pub fn add_component(&mut self, name: Option<&str>, parent: u64) -> Result<u64> {
|
||||
if parent != 0 && !self.components.iter().any(|c| c.id == parent) {
|
||||
|
||||
@@ -1,33 +1,18 @@
|
||||
//! A drawing (#3890): sheets of paper that draw a design. A drawing lives in the file of the design
|
||||
//! it draws ([`Document::drawings`](crate::Document::drawings)): a design container keeps each in
|
||||
//! its own entry, a plain JSON design inline. Each is a record of its own family,
|
||||
//! `"format": "solidcraft-drawing/N"`, versioned by the same code as designs ([`DRAWING`]), so a
|
||||
//! change to the record is a drawing change, not a design one.
|
||||
//! 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.
|
||||
//!
|
||||
//! - Each drawing has an **id**, unique among its design's drawings, and a design lists its
|
||||
//! drawings by name, then id ([`in_design`]): nothing to merge when two people add drawings.
|
||||
//! - **Sources** are the designs it draws. For now a drawing draws the design it is in: at most one
|
||||
//! source, an id and nothing else. What the design was when the drawing last saw it is not saved
|
||||
//! here.
|
||||
//! - **Sheets**: the paper, a copy of its drawing border and of its title block with the text typed
|
||||
//! over its cells' values, 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 and the
|
||||
//! block are copies of what they were made with, never names, so a later built-in never changes a
|
||||
//! saved drawing.
|
||||
//! - **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 **font** its text is set in: a font family's name, never a file, so the drawing travels
|
||||
//! (none: the built-in font; [`solidcraft_sketch::check_family`]).
|
||||
//! - **Views** of its designs on its sheets, base and projected ([`view`]), and the **projection**
|
||||
//! (first or third angle) every projected view follows.
|
||||
//! - **Dimensions** on its views, each measuring named corners or edges of the view's model
|
||||
//! ([`dimension`]).
|
||||
//! - **Balloons** on its views, each pointing at a placed part by a named edge of it ([`balloon`]).
|
||||
//! - **Images** on its sheets, each file kept once however many places it sits ([`image`]).
|
||||
//! - **Tables** on its sheets: parts lists and revision tables ([`table`]).
|
||||
//! - **Revisions**: one revision record for the whole drawing ([`RevisionRecord`]), its rows in the
|
||||
//! order they were added, the newest last.
|
||||
//!
|
||||
//! 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
|
||||
@@ -35,59 +20,35 @@
|
||||
//! 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; names of 1 to [`MAX_NAME_BYTES`] bytes.
|
||||
//! - 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, and lists of records in a fixed order (sheets as arranged; later
|
||||
//! records by id), never the order they were added in. Records listed by id read in that order
|
||||
//! whatever order a file lists them in: the order means nothing, so it is never damage.
|
||||
//! order, maps sorted.
|
||||
//! - A later change to the record is one file in `src/drawing/format/pending/` (its README).
|
||||
//!
|
||||
//! [`Drawing::from_json`] reads a drawing in this order: at most [`MAX_DRAWING_BYTES`] of text,
|
||||
//! the family's format check, every number in range, the upgrade, the typed record with its views,
|
||||
//! dimensions, balloons, images and notes by id, then [`Drawing::validate`] and each image's file
|
||||
//! ([`Drawing::check_image_files`]); [`Drawing::from_value`] the same from the format check on.
|
||||
//! [`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::borrow::Cow;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use solidcore_record::Vec2;
|
||||
use solidcraft_geom::Aabb2;
|
||||
use solidcraft_geom::cell::Projection;
|
||||
use solidcraft_geom::drawing_sheet::{self, DrawingBorderProfile};
|
||||
use solidcraft_geom::revision::RevisionRecord;
|
||||
use solidcraft_geom::title_block::TitleBlock;
|
||||
|
||||
use crate::format::DRAWING;
|
||||
use crate::ids::{self, IdSource};
|
||||
use crate::{DocError, Metadata, Result, expr};
|
||||
|
||||
pub mod balloon;
|
||||
pub mod by_id;
|
||||
pub mod dimension;
|
||||
pub mod fields;
|
||||
pub mod image;
|
||||
pub mod note;
|
||||
pub mod table;
|
||||
pub mod template;
|
||||
pub mod view;
|
||||
|
||||
use balloon::Balloon;
|
||||
use by_id::ById;
|
||||
use dimension::Dimension;
|
||||
use image::{ImageFile, SheetImage};
|
||||
use note::{Anchor, Note};
|
||||
use table::SheetTable;
|
||||
use view::View;
|
||||
|
||||
/// The most drawings a design may hold.
|
||||
pub const MAX_DRAWINGS: usize = 1000;
|
||||
/// 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.
|
||||
@@ -111,60 +72,29 @@ pub struct Drawing {
|
||||
/// otherwise.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub format_pending: Vec<String>,
|
||||
/// Unique among its design's drawings: a design container's entry for it is named by it.
|
||||
#[serde(with = "crate::ids")]
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
/// The designs the drawing draws: for now at most one, the design it is in.
|
||||
/// The designs the drawing draws.
|
||||
pub sources: Vec<Source>,
|
||||
/// The drawing's properties and other tools' data, kept as they are, behind an `Arc` as the
|
||||
/// sheets are, so an undo step shares them while they don't change.
|
||||
#[serde(default, skip_serializing_if = "no_properties")]
|
||||
pub properties: Arc<Metadata>,
|
||||
/// 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,
|
||||
/// First or third angle: what every projected view shows, and the title block's symbol.
|
||||
#[serde(default)]
|
||||
pub projection: Projection,
|
||||
/// The language of the drawing's content, a BCP 47 tag (`en-GB`).
|
||||
pub language: String,
|
||||
/// The font family its text is set in, by name; empty: the built-in font.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub font: String,
|
||||
/// In order, each behind its own `Arc`, so an undo step shares every sheet it didn't change.
|
||||
pub sheets: Vec<Arc<Sheet>>,
|
||||
/// By id ([`Drawing::add_views`]), each behind its own `Arc`, as the sheets are.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub views: Vec<Arc<View>>,
|
||||
/// By id ([`Drawing::add_dimensions`]), each behind its own `Arc`, as the views are.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub dimensions: Vec<Arc<Dimension>>,
|
||||
#[serde(default, skip_serializing_if = "ById::is_empty")]
|
||||
pub balloons: ById<Balloon>,
|
||||
/// The image files, once each however many places they sit.
|
||||
#[serde(default, skip_serializing_if = "ById::is_empty")]
|
||||
pub images: ById<ImageFile>,
|
||||
/// Where they sit on the sheets.
|
||||
#[serde(default, skip_serializing_if = "ById::is_empty")]
|
||||
pub sheet_images: ById<SheetImage>,
|
||||
/// The saved tables on its sheets: parts lists and revision tables.
|
||||
#[serde(default, skip_serializing_if = "ById::is_empty")]
|
||||
pub tables: ById<SheetTable>,
|
||||
/// The notes on its sheets and in its views.
|
||||
#[serde(default, skip_serializing_if = "ById::is_empty")]
|
||||
pub notes: ById<Note>,
|
||||
/// Behind an `Arc` as the sheets are, so an undo step shares it while it doesn't change.
|
||||
#[serde(default, skip_serializing_if = "no_revisions")]
|
||||
pub revisions: Arc<RevisionRecord>,
|
||||
}
|
||||
|
||||
/// A design the drawing draws: the design the drawing is in. Views and sheets name it by its id,
|
||||
/// so a drawing of other designs can add sources of another kind later.
|
||||
/// 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.
|
||||
@@ -189,14 +119,6 @@ pub struct Sheet {
|
||||
pub sheet: drawing_sheet::Sheet,
|
||||
/// A copy of the drawing border's profile (`None`: no drawing border).
|
||||
pub border: Option<DrawingBorderProfile>,
|
||||
/// A copy of its title block's definition (`None`: no title block), on the drawing border's
|
||||
/// lower-right corner, so only a sheet with a border has one.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title_block: Option<TitleBlock>,
|
||||
/// Text shown in place of a title block cell's value, by the cell's name: the value is still
|
||||
/// worked out, so it can be shown beside it.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub title_block_overrides: BTreeMap<String, String>,
|
||||
/// The resolution its pictures (its shaded views) are rendered at, in dots per inch.
|
||||
#[serde(default = "default_dpi")]
|
||||
pub dpi: f64,
|
||||
@@ -227,150 +149,49 @@ pub struct Excluded {
|
||||
pub print: bool,
|
||||
}
|
||||
|
||||
impl Sheet {
|
||||
/// Where the sheet's drawing goes, in paper millimetres: inside its drawing border, or the
|
||||
/// whole paper when it has none. Nothing on a sheet is saved against its border, so a change
|
||||
/// of paper regrows the border and its zones by itself.
|
||||
pub fn drawing_space(&self) -> Result<Aabb2> {
|
||||
match &self.border {
|
||||
Some(border) => Ok(border.layout(&self.sheet).map_err(from_sheet)?.drawing_border),
|
||||
None => Ok(Aabb2 { min: Vec2::ZERO, max: self.sheet.size_mm().map_err(from_sheet)? }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where its title block sits, in paper millimetres (none without one), once its drawing border
|
||||
/// and the block on it lay out on the paper: the rule a sheet is held to, as drawing them uses it.
|
||||
pub fn title_block_outline(&self) -> Result<Option<Aabb2>> {
|
||||
let border = self.border.as_ref().map(|border| border.layout(&self.sheet)).transpose().map_err(from_sheet)?;
|
||||
let Some(block) = &self.title_block else { return Ok(None) };
|
||||
let border = border.ok_or_else(|| DocError::Invalid("its title block sits on a drawing border, and it has none".into()))?;
|
||||
Ok(Some(block.layout(&border).map_err(|e| DocError::Invalid(e.to_string()))?.outline))
|
||||
}
|
||||
|
||||
/// Gives the sheet `block` (`None`: no title block). Text typed over a cell stays while `block`
|
||||
/// has a text cell of that name; answers the cells whose typed text is dropped.
|
||||
pub fn set_title_block(&mut self, block: Option<TitleBlock>) -> Vec<String> {
|
||||
let kept = |name: &String| fields::overridable(block.as_ref(), name);
|
||||
let lost = self.title_block_overrides.keys().filter(|name| !kept(name)).cloned().collect();
|
||||
self.title_block_overrides.retain(|name, _| kept(name));
|
||||
self.title_block = block;
|
||||
lost
|
||||
}
|
||||
}
|
||||
|
||||
/// What is on one of a drawing's sheets ([`Drawing::on_sheet`]), each kind by id.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct OnSheet {
|
||||
pub views: Vec<u64>,
|
||||
/// The dimensions in its views.
|
||||
pub dimensions: Vec<u64>,
|
||||
/// The balloons in its views.
|
||||
pub balloons: Vec<u64>,
|
||||
/// The images placed on it.
|
||||
pub images: Vec<u64>,
|
||||
/// The revision tags on it.
|
||||
pub tags: Vec<u64>,
|
||||
/// The tables on it.
|
||||
pub tables: Vec<u64>,
|
||||
/// The notes on it and in its views.
|
||||
pub notes: Vec<u64>,
|
||||
}
|
||||
|
||||
impl OnSheet {
|
||||
/// Nothing is on the sheet.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.views.is_empty()
|
||||
&& self.dimensions.is_empty()
|
||||
&& self.balloons.is_empty()
|
||||
&& self.images.is_empty()
|
||||
&& self.tags.is_empty()
|
||||
&& self.tables.is_empty()
|
||||
&& self.notes.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
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, and then its sheet's, come from
|
||||
/// `id_source`, the drawing's differing from every one in `taken` (its design's drawings').
|
||||
/// `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>,
|
||||
taken: &BTreeSet<u64>,
|
||||
id_source: &mut IdSource,
|
||||
) -> Result<Drawing> {
|
||||
let id = id_source.next(taken)?;
|
||||
let sheet_id = id_source.next(&BTreeSet::from([id]))?;
|
||||
let first = Sheet {
|
||||
id: sheet_id,
|
||||
name: sheet.into(),
|
||||
sheet: paper,
|
||||
border,
|
||||
title_block: None,
|
||||
title_block_overrides: BTreeMap::new(),
|
||||
dpi: DEFAULT_DPI,
|
||||
documents: None,
|
||||
excluded: Excluded::default(),
|
||||
};
|
||||
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(),
|
||||
id,
|
||||
name: name.into(),
|
||||
sources: Vec::new(),
|
||||
properties: Arc::default(),
|
||||
properties: Metadata::default(),
|
||||
units: Units::default(),
|
||||
projection: Projection::default(),
|
||||
language: language.into(),
|
||||
font: String::new(),
|
||||
sheets: vec![Arc::new(first)],
|
||||
views: Vec::new(),
|
||||
dimensions: Vec::new(),
|
||||
balloons: ById::default(),
|
||||
images: ById::default(),
|
||||
sheet_images: ById::default(),
|
||||
tables: ById::default(),
|
||||
notes: ById::default(),
|
||||
revisions: Arc::default(),
|
||||
};
|
||||
drawing.validate()?;
|
||||
Ok(drawing)
|
||||
}
|
||||
|
||||
/// Read a drawing's JSON (any format this build knows; older ones are upgraded). A 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 v: Value = serde_json::from_str(s).map_err(|e| DocError::Invalid(format!("not a SolidCraft drawing: {e}")))?;
|
||||
Drawing::from_value(v)
|
||||
}
|
||||
|
||||
/// Read a drawing from its JSON value, as a plain JSON design holds it: [`Drawing::from_json`]
|
||||
/// once the text is parsed.
|
||||
pub fn from_value(mut v: Value) -> Result<Drawing> {
|
||||
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 mut d: Drawing = serde_json::from_value(v).map_err(damaged)?;
|
||||
let (views, dimensions) = (std::mem::take(&mut d.views), std::mem::take(&mut d.dimensions));
|
||||
d.add_views(views);
|
||||
d.add_dimensions(dimensions);
|
||||
d.balloons.put_in_order();
|
||||
d.images.put_in_order();
|
||||
d.sheet_images.put_in_order();
|
||||
d.tables.put_in_order();
|
||||
d.notes.put_in_order();
|
||||
let d: Drawing = serde_json::from_value(v).map_err(damaged)?;
|
||||
d.validate().map_err(damaged)?;
|
||||
d.check_image_files().map_err(damaged)?;
|
||||
Ok(Drawing { format: DRAWING.format_string(DRAWING.current()), format_pending: DRAWING.pending_names(), ..d })
|
||||
}
|
||||
|
||||
@@ -390,105 +211,21 @@ impl Drawing {
|
||||
id_source.next(&self.ids().collect())
|
||||
}
|
||||
|
||||
/// The position of the sheet `named` names: by id, else by a name only one sheet has.
|
||||
pub fn sheet_index(&self, named: &str) -> Result<usize> {
|
||||
if let Some(i) = ids::decode(named).and_then(|id| self.sheets.iter().position(|s| s.id == id)) {
|
||||
return Ok(i);
|
||||
}
|
||||
let mut called = self.sheets.iter().enumerate().filter(|(_, s)| s.name == named).map(|(i, _)| i);
|
||||
match (called.next(), called.next()) {
|
||||
(Some(i), None) => Ok(i),
|
||||
(Some(_), Some(_)) => Err(DocError::Invalid(format!("more than one sheet is called `{named}`: give its id"))),
|
||||
(None, _) => Err(DocError::Invalid(format!("no sheet `{named}`"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `id` names an object a sheet shows: a view, a dimension or a balloon.
|
||||
pub fn has_object(&self, id: u64) -> bool {
|
||||
self.view(id).is_some() || self.dimension(id).is_some() || self.balloons.by_id(id).is_some() || self.notes.by_id(id).is_some()
|
||||
}
|
||||
|
||||
/// What is on sheet `id`: its views and the dimensions and balloons in them, and the images placed on it.
|
||||
/// Each later kind of record on a sheet adds its own here, and goes with the sheet
|
||||
/// ([`Drawing::remove_sheet`]).
|
||||
pub fn on_sheet(&self, id: u64) -> OnSheet {
|
||||
// The views are listed by id, so the sheet's are too, and so are its images, revision tags, tables and notes.
|
||||
let mut on = self.with_contents(self.views.iter().filter(|v| v.sheet == id).map(|v| v.id).collect());
|
||||
on.images = self.sheet_images.iter().filter(|i| i.sheet == id).map(|i| i.id).collect();
|
||||
on.tags = self.revisions.tags().iter().filter(|t| t.sheet == id).map(|t| t.id).collect();
|
||||
on.tables = self.tables.iter().filter(|t| t.sheet == id).map(|t| t.id).collect();
|
||||
let in_views = |n: &Note| n.anchor.view().is_some_and(|v| on.views.binary_search(&v).is_ok());
|
||||
on.notes = self.notes.iter().filter(|n| n.anchor == Anchor::Sheet(id) || in_views(n)).map(|n| n.id).collect();
|
||||
on
|
||||
}
|
||||
|
||||
/// `views`, listed by id, with the dimensions and balloons in them. An image sits on a sheet, never in a
|
||||
/// view, so there are none.
|
||||
pub fn with_contents(&self, views: Vec<u64>) -> OnSheet {
|
||||
let dimensions = self.dimensions.iter().filter(|d| views.binary_search(&d.view).is_ok()).map(|d| d.id).collect();
|
||||
let balloons = self.balloons.iter().filter(|b| views.binary_search(&b.view).is_ok()).map(|b| b.id).collect();
|
||||
let notes = self.notes.iter().filter(|n| n.anchor.view().is_some_and(|v| views.binary_search(&v).is_ok())).map(|n| n.id).collect();
|
||||
OnSheet { views, dimensions, balloons, images: Vec::new(), tags: Vec::new(), tables: Vec::new(), notes }
|
||||
}
|
||||
|
||||
/// Takes sheet `id` out with everything on it ([`Drawing::on_sheet`]), and answers what that
|
||||
/// was. A view on another sheet projected from one of its views is left without its parent,
|
||||
/// which [`Drawing::validate`] refuses: what becomes of it is the caller's to decide first.
|
||||
pub fn remove_sheet(&mut self, id: u64) -> OnSheet {
|
||||
let on = self.on_sheet(id);
|
||||
self.sheets.retain(|s| s.id != id);
|
||||
self.remove_objects(&on);
|
||||
on
|
||||
}
|
||||
|
||||
/// Takes `objects` out, each list by id: the one rule a sheet's delete and a selection's delete
|
||||
/// remove by, with the image files nothing uses any more. As in [`Drawing::remove_sheet`], what is
|
||||
/// left must still keep the record's rules.
|
||||
pub fn remove_objects(&mut self, objects: &OnSheet) {
|
||||
self.views.retain(|v| objects.views.binary_search(&v.id).is_err());
|
||||
self.dimensions.retain(|d| objects.dimensions.binary_search(&d.id).is_err());
|
||||
self.balloons.retain(|b| objects.balloons.binary_search(&b.id).is_err());
|
||||
self.sheet_images.retain(|i| objects.images.binary_search(&i.id).is_err());
|
||||
self.drop_unused_images();
|
||||
self.tables.retain(|t| objects.tables.binary_search(&t.id).is_err());
|
||||
self.notes.retain(|n| objects.notes.binary_search(&n.id).is_err());
|
||||
// A balloon outlives the list it named, and goes back to the lists it could read.
|
||||
for balloon in self.balloons.iter_mut() {
|
||||
if balloon.parts_list.is_some_and(|list| objects.tables.binary_search(&list).is_ok()) {
|
||||
Arc::make_mut(balloon).parts_list = None;
|
||||
}
|
||||
}
|
||||
if !objects.tags.is_empty() {
|
||||
Arc::make_mut(&mut self.revisions).delete_tags(&objects.tags);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
std::iter::once(self.id)
|
||||
.chain(self.sources.iter().map(|s| s.id))
|
||||
.chain(self.sheets.iter().map(|s| s.id))
|
||||
.chain(self.views.iter().map(|v| v.id))
|
||||
.chain(self.dimensions.iter().map(|d| d.id))
|
||||
.chain(self.balloons.iter().map(|b| b.id))
|
||||
.chain(self.images.iter().map(|i| i.id))
|
||||
.chain(self.sheet_images.iter().map(|i| i.id))
|
||||
.chain(self.tables.iter().map(|t| t.id))
|
||||
.chain(self.notes.iter().map(|n| n.id))
|
||||
.chain(self.revisions.ids())
|
||||
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 drawing checks, and what every edit
|
||||
/// 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() <= 1, || "a drawing draws only the design it is in".into())?;
|
||||
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`)")
|
||||
})?;
|
||||
solidcraft_sketch::check_family(&self.font).map_err(|e| DocError::Invalid(format!("the drawing's font: {e}")))?;
|
||||
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);
|
||||
@@ -499,38 +236,25 @@ impl Drawing {
|
||||
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))))?;
|
||||
}
|
||||
// One record for the whole drawing until sheets keep their own (#3874).
|
||||
require(!self.revisions.per_sheet, || "a drawing keeps one revision record for all its sheets".into())?;
|
||||
self.revisions.validate().map_err(|e| DocError::Invalid(e.to_string()))?;
|
||||
require(self.revisions.tags().iter().all(|t| self.sheets.iter().any(|s| s.id == t.sheet)), || {
|
||||
"a revision tag is on one of the drawing's sheets".into()
|
||||
})?;
|
||||
self.check_views()?;
|
||||
self.check_dimensions()?;
|
||||
self.check_tables()?;
|
||||
self.check_balloons()?;
|
||||
self.check_images()?;
|
||||
self.check_notes()
|
||||
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)?;
|
||||
sheet.title_block_outline()?;
|
||||
if let Some(block) = &sheet.title_block {
|
||||
fields::check_block(block)?;
|
||||
if let Some(border) = &sheet.border {
|
||||
border.validate().map_err(from_sheet)?;
|
||||
}
|
||||
// Each override names a text cell of its block, and its text is a cell's.
|
||||
for name in sheet.title_block_overrides.keys() {
|
||||
let named = || format!("its title block has no text cell `{name}` to override");
|
||||
require(fields::overridable(sheet.title_block.as_ref(), name), named)?;
|
||||
}
|
||||
let texts = sheet.title_block_overrides.values().map(String::as_str);
|
||||
solidcraft_geom::cell::check_text(texts).map_err(|e| DocError::Invalid(e.to_string()))?;
|
||||
require((MIN_DPI..=MAX_DPI).contains(&sheet.dpi), || format!("its pictures are {MIN_DPI} to {MAX_DPI} dpi"))?;
|
||||
match sheet.documents {
|
||||
Some(SheetComponent { source, .. }) => {
|
||||
@@ -540,89 +264,6 @@ impl Drawing {
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// How a message names this drawing: by its name and id.
|
||||
pub fn label(&self) -> String {
|
||||
format!("drawing {:?} ({})", self.name, ids::encode(self.id))
|
||||
}
|
||||
}
|
||||
|
||||
/// A design's drawings as read, each checked on its own already: in their order (by name, then id)
|
||||
/// and keeping [`check_in_design`]'s rules.
|
||||
pub fn in_design(drawings: Vec<Drawing>) -> Result<Vec<Arc<Drawing>>> {
|
||||
let mut drawings: Vec<_> = drawings.into_iter().map(Arc::new).collect();
|
||||
sort(&mut drawings);
|
||||
check_in_design(&drawings)?;
|
||||
Ok(drawings)
|
||||
}
|
||||
|
||||
/// Put a design's drawings in their order: by name, then id. Nothing stores the order, so two
|
||||
/// people adding drawings have nothing to merge.
|
||||
pub fn sort(drawings: &mut [Arc<Drawing>]) {
|
||||
drawings.sort_by(|a, b| (&a.name, a.id).cmp(&(&b.name, b.id)));
|
||||
}
|
||||
|
||||
/// The rules for a design's drawings: at most [`MAX_DRAWINGS`], and no two with one id.
|
||||
pub fn check_in_design(drawings: &[Arc<Drawing>]) -> Result<()> {
|
||||
check_count(drawings.len())?;
|
||||
let mut seen = BTreeSet::new();
|
||||
drawings.iter().try_for_each(|d| take_id(&mut seen, d.id))
|
||||
}
|
||||
|
||||
/// A drawing takes `id`, which no drawing before it in its design (`seen`) has.
|
||||
fn take_id(seen: &mut BTreeSet<u64>, id: u64) -> Result<()> {
|
||||
require(seen.insert(id), || format!("two drawings have the id {}", ids::encode(id)))
|
||||
}
|
||||
|
||||
/// A design of `count` drawings keeps the cap: what a reader checks before it reads any of them.
|
||||
pub fn check_count(count: usize) -> Result<()> {
|
||||
require(count <= MAX_DRAWINGS, || format!("a design holds at most {MAX_DRAWINGS} drawings"))
|
||||
}
|
||||
|
||||
/// Whether the drawing has no properties, so its file leaves them out.
|
||||
fn no_properties(properties: &Arc<Metadata>) -> bool {
|
||||
properties.is_empty()
|
||||
}
|
||||
|
||||
/// Adds `more` to `records`, each where its id puts it: the order every list of records after the
|
||||
/// sheets keeps (the module's rule), whatever order they come in.
|
||||
fn add_by_id<T>(records: &mut Vec<Arc<T>>, more: impl IntoIterator<Item = Arc<T>>, id: impl Fn(&T) -> u64) {
|
||||
records.extend(more);
|
||||
records.sort_by_key(|r| id(r));
|
||||
}
|
||||
|
||||
/// Whether `records` are in the order [`add_by_id`] keeps.
|
||||
fn in_id_order<T>(records: &[Arc<T>], id: impl Fn(&T) -> u64) -> bool {
|
||||
records.is_sorted_by_key(|r| id(r))
|
||||
}
|
||||
|
||||
fn no_revisions(revisions: &Arc<RevisionRecord>) -> bool {
|
||||
**revisions == RevisionRecord::default()
|
||||
}
|
||||
|
||||
/// `doc` without the drawings that break a rule, and a line naming each one left out and the
|
||||
/// rule. Reading refuses a design whole for one such drawing, so a copy that must open again (an
|
||||
/// autosave, a crash report's design) keeps the rest: the first [`MAX_DRAWINGS`], the first of
|
||||
/// two with one id, and only those keeping their own rules and small enough to save.
|
||||
pub fn openable(doc: &crate::Document) -> (Cow<'_, crate::Document>, Vec<String>) {
|
||||
let (mut seen, mut left_out) = (BTreeSet::new(), Vec::new());
|
||||
let kept: Vec<_> = doc
|
||||
.drawings
|
||||
.iter()
|
||||
.filter(|d| {
|
||||
// Its own rules and its size before its id: a drawing left out takes no id from a later one.
|
||||
let checked =
|
||||
check_count(seen.len() + 1).and_then(|()| d.validate()).and_then(|()| d.to_json().map(drop)).and_then(|()| take_id(&mut seen, d.id));
|
||||
checked.map_err(|e| left_out.push(format!("{} left out: {e}", d.label()))).is_ok()
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
if left_out.is_empty() {
|
||||
return (Cow::Borrowed(doc), left_out);
|
||||
}
|
||||
let mut doc = doc.clone();
|
||||
doc.drawings = kept;
|
||||
(Cow::Owned(doc), left_out)
|
||||
}
|
||||
|
||||
/// `Ok` when `ok`, else the rule `broken` names.
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
//! A drawing's balloons (W26, #3874): each points at a part placed in its view's model, by an edge
|
||||
//! of it, to number that part on the drawing. A balloon saves what it points at by name, never a
|
||||
//! picked point, as a dimension does ([`super::dimension`]): when the model changes it follows the
|
||||
//! part, or says what it lost, and it never moves to another part.
|
||||
//!
|
||||
//! - **The part** it numbers is the occurrence its edge's body is placed through: the full path,
|
||||
//! from the top. A body of the design itself is not a placed part, so no balloon points at one.
|
||||
//! - **The arrow** ends on the middle of its edge, whichever way the kernel runs the edge.
|
||||
//! - **The balloon** sits at a place on paper, in millimetres from its view's centre, so it moves
|
||||
//! with the view and not with the model.
|
||||
//!
|
||||
//! The number a balloon shows comes from a parts list, worked out and never saved: the one it names,
|
||||
//! else its sheet's only one, else the drawing's only one ([`Drawing::parts_list_for`]). It keeps the
|
||||
//! balloon whatever becomes of the row. A parts list's editor shows how many balloons point at each of
|
||||
//! its rows ([`balloons_by_row`]), worked out the same way.
|
||||
//!
|
||||
//! The rules, beside the record's: a balloon is on one of the drawing's views; its edge reference
|
||||
//! keeps a dimension's rules, with an occurrence path of 1 to [`crate::MAX_PLACEMENT_DEPTH`] ids; its place
|
||||
//! is on paper ([`is_paper_coordinate`]); the list it names is one of the drawing's parts lists; at
|
||||
//! most [`MAX_BALLOONS`] balloons, listed by id.
|
||||
//!
|
||||
//! [`Document::resolve_balloons`] finds each balloon's edge again as a dimension's is found
|
||||
//! ([`super::dimension::Status`]): by name, strictly, and what it can't decide it says.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_geom::drawing_view::is_paper_coordinate;
|
||||
use solidcraft_geom::paper::MAX_SIDE_MM;
|
||||
|
||||
use super::by_id::Record;
|
||||
use super::dimension::{Lookup, ModelEdge, Status, check_edge};
|
||||
use super::table::{Kind, PartsList, SheetTable};
|
||||
use super::{Drawing, require};
|
||||
use crate::bom::{Bom, Tree, item_text};
|
||||
use crate::{DocError, Document, Model, Result, ids};
|
||||
|
||||
/// The most balloons a drawing may have.
|
||||
pub const MAX_BALLOONS: usize = 10_000;
|
||||
|
||||
/// One balloon of a drawing.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Balloon {
|
||||
#[serde(with = "crate::ids")]
|
||||
pub id: u64,
|
||||
/// The view it points into and moves with.
|
||||
#[serde(with = "crate::ids")]
|
||||
pub view: u64,
|
||||
/// The edge its arrow points at. Its occurrence path is the part the balloon numbers.
|
||||
pub edge: ModelEdge,
|
||||
/// The balloon's centre, in millimetres of paper from its view's centre.
|
||||
pub at: [f64; 2],
|
||||
/// The parts list it names, when it names one.
|
||||
#[serde(default, with = "crate::ids::option", skip_serializing_if = "Option::is_none")]
|
||||
pub parts_list: Option<u64>,
|
||||
}
|
||||
|
||||
impl Balloon {
|
||||
/// The part it numbers: the occurrences its edge's body is placed through, from the top.
|
||||
pub fn occurrence(&self) -> &[u64] {
|
||||
&self.edge.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Record for Balloon {
|
||||
fn id(&self) -> u64 {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Drawing {
|
||||
/// The parts list balloon `b` reads, by its id: the one it names, else its sheet's only one, else
|
||||
/// the drawing's only one. With none to go by, the lists it could name, by id.
|
||||
pub fn parts_list_for(&self, b: &Balloon) -> std::result::Result<(u64, PartsList), Vec<u64>> {
|
||||
let list = |t: &SheetTable| match t.kind {
|
||||
Kind::PartsList(list) => Some((t.id, list)),
|
||||
Kind::Revision => None,
|
||||
};
|
||||
let lists: Vec<(u64, u64, PartsList)> = self.tables.iter().filter_map(|t| list(t).map(|(id, l)| (id, t.sheet, l))).collect();
|
||||
if let Some(named) = lists.iter().find(|(id, ..)| Some(*id) == b.parts_list) {
|
||||
return Ok((named.0, named.2));
|
||||
}
|
||||
let sheet = self.view(b.view).map(|v| v.sheet);
|
||||
let mut on_sheet = lists.iter().filter(|(_, on, _)| Some(*on) == sheet);
|
||||
if let (Some(only), None) = (on_sheet.next(), on_sheet.next()) {
|
||||
return Ok((only.0, only.2));
|
||||
}
|
||||
match lists.as_slice() {
|
||||
[only] => Ok((only.0, only.2)),
|
||||
_ => Err(lists.iter().map(|(id, ..)| *id).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The balloons that read each parts list ([`Self::parts_list_for`]), by the list's id: each
|
||||
/// balloon's list worked out once. A balloon with no list to go by reads none.
|
||||
pub fn balloons_by_list(&self) -> HashMap<u64, Vec<&Balloon>> {
|
||||
let mut by_list: HashMap<u64, Vec<&Balloon>> = HashMap::new();
|
||||
for b in &self.balloons {
|
||||
if let Ok((list, _)) = self.parts_list_for(b) {
|
||||
by_list.entry(list).or_default().push(b);
|
||||
}
|
||||
}
|
||||
by_list
|
||||
}
|
||||
|
||||
/// The balloons' rules (the module's notes). The views and the tables are checked first, so they
|
||||
/// are in id order here.
|
||||
pub(super) fn check_balloons(&self) -> Result<()> {
|
||||
self.balloons.check(MAX_BALLOONS, "balloon", |b| self.check_balloon(b))
|
||||
}
|
||||
|
||||
fn check_balloon(&self, b: &Balloon) -> Result<()> {
|
||||
require(self.view(b.view).is_some(), || format!("it is on view {}, which the drawing doesn't have", ids::encode(b.view)))?;
|
||||
if let Some(list) = b.parts_list {
|
||||
let named = || format!("it names table {}, which isn't one of the drawing's parts lists", ids::encode(list));
|
||||
require(self.tables.by_id(list).is_some_and(|t| matches!(t.kind, Kind::PartsList(_))), named)?;
|
||||
}
|
||||
check_edge(&b.edge)?;
|
||||
require(!b.edge.path.is_empty(), || "it points at a placed part: its edge's body is placed through an occurrence".into())?;
|
||||
let [x, y] = b.at;
|
||||
require(is_paper_coordinate(x) && is_paper_coordinate(y), || format!("its place is at most {MAX_SIDE_MM} mm on paper"))
|
||||
}
|
||||
}
|
||||
|
||||
/// How many of `balloons`, those that read one parts list ([`Drawing::balloons_by_list`]), point at
|
||||
/// each of its rows, by item ("1.2"): every row of `listed` (that list, made from `tree` with its
|
||||
/// members) in its order, 0 included. A balloon whose row is lost counts for none.
|
||||
pub fn balloons_by_row(balloons: &[&Balloon], listed: &Bom, tree: &Tree) -> Vec<(String, usize)> {
|
||||
let mut counts: HashMap<String, usize> = HashMap::new();
|
||||
for b in balloons {
|
||||
if let Ok(item) = listed.item_of(tree, b.occurrence()) {
|
||||
*counts.entry(item).or_default() += 1;
|
||||
}
|
||||
}
|
||||
listed
|
||||
.rows_deep()
|
||||
.iter()
|
||||
.map(|(item, _)| {
|
||||
let item = item_text(item);
|
||||
let n = counts.get(&item).copied().unwrap_or(0);
|
||||
(item, n)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A balloon as the model has it now ([`Document::resolve_balloons`]): never saved.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Resolved {
|
||||
pub status: Status,
|
||||
/// Where its arrow ends, the middle of its edge, in the design's frame: when the edge was found.
|
||||
pub point: Option<Vec3>,
|
||||
/// Its edge was found by exact history, as one edge.
|
||||
pub exact: bool,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// The balloons `wanted` of `drawing` (one of this design's drawings) as `model` (this
|
||||
/// design's) has them now: each one's edge found again by name, and the middle of it. A body is
|
||||
/// looked up and its edge names worked out once for the call, however many balloons name it.
|
||||
/// `Err` only for an id the drawing doesn't have.
|
||||
pub fn resolve_balloons(&self, model: &Model, drawing: &Drawing, wanted: &[u64]) -> Result<Vec<Resolved>> {
|
||||
let mut lookup = Lookup::new(self, model);
|
||||
wanted
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let b = drawing.balloons.by_id(*id).ok_or_else(|| DocError::Unknown(format!("balloon {}", ids::encode(*id))))?;
|
||||
let (status, point, exact) = lookup.middle(&b.edge);
|
||||
Ok(Resolved { status, point, exact })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "balloon_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,264 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_geom::drawing_sheet::{self, Orientation, PaperSize};
|
||||
use solidcraft_geom::drawing_view::NamedView;
|
||||
|
||||
use super::*;
|
||||
use crate::drawing::table::{Kind, SheetTable};
|
||||
use crate::drawing::view::{View, ViewKind, ViewPlace};
|
||||
use crate::drawing::{OnSheet, Sheet, Source};
|
||||
use crate::drawing_view::{ViewDirection, ViewOverrides};
|
||||
use crate::format::DRAWING;
|
||||
use crate::ids::IdSource;
|
||||
|
||||
const SEED: u64 = 3874;
|
||||
|
||||
fn edge(name: &str, path: Vec<u64>) -> ModelEdge {
|
||||
ModelEdge { body: "Bolt".into(), origin_feature: 4, name: name.into(), path }
|
||||
}
|
||||
|
||||
/// A drawing of one design on one A3 sheet, its front view at (150, 150), and on the view two
|
||||
/// balloons: one on a bolt placed straight in the design (occurrence 3), one on a bolt inside a
|
||||
/// sub-assembly (occurrence 7 in 3). Their ids, in the order they were added.
|
||||
fn ballooned() -> (Drawing, [u64; 2]) {
|
||||
let mut id_source = IdSource::seeded(SEED);
|
||||
let a3 = drawing_sheet::Sheet::standard(PaperSize::A3, Orientation::Landscape);
|
||||
let mut d = Drawing::new("Bolted", "en", "Sheet 1", a3, None, &BTreeSet::new(), &mut id_source).unwrap();
|
||||
let source = d.allocate_id(&mut id_source).unwrap();
|
||||
d.sources.push(Source { id: source });
|
||||
let (view, sheet) = (d.allocate_id(&mut id_source).unwrap(), d.sheets[0].id);
|
||||
let kind = ViewKind::Base { source, view: ViewDirection::Named(NamedView::Front) };
|
||||
d.add_views([Arc::new(View {
|
||||
id: view,
|
||||
sheet,
|
||||
kind,
|
||||
place: ViewPlace::At([150.0, 150.0]),
|
||||
settings: ViewOverrides { scale: Some("1:2".parse().unwrap()), ..ViewOverrides::default() },
|
||||
reference_shown_percent: 0.0,
|
||||
})]);
|
||||
let mut added = [0; 2];
|
||||
for ((path, at), id) in [(vec![3], [40.0, 25.0]), (vec![3, 7], [-35.5, 60.0])].into_iter().zip(&mut added) {
|
||||
*id = d.allocate_id(&mut id_source).unwrap();
|
||||
d.balloons.add([Arc::new(Balloon { id: *id, view, edge: edge("F1:side|F2:head", path), at, parts_list: None })]);
|
||||
}
|
||||
d.validate().unwrap();
|
||||
(d, added)
|
||||
}
|
||||
|
||||
fn file(d: &Drawing) -> Value {
|
||||
serde_json::from_str(&d.to_json().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// Balloon `id` in a drawing's file.
|
||||
fn balloon<'a>(v: &'a mut Value, id: &str) -> &'a mut Value {
|
||||
v["balloons"].as_array_mut().unwrap().iter_mut().find(|x| x["id"] == id).unwrap()
|
||||
}
|
||||
|
||||
/// 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);
|
||||
Drawing::from_json(&v.to_string()).map_or_else(|e| e.to_string(), |_| "read".into())
|
||||
}
|
||||
|
||||
/// A balloon saves its view, its edge by name with the part's occurrence path, and its place, and
|
||||
/// nothing worked out. Balloons save by id and read back as saved, the same bytes again; a file
|
||||
/// that lists them in another order reads by id. An older drawing reads with none, and a drawing
|
||||
/// with none writes none.
|
||||
#[test]
|
||||
fn balloons_save_what_they_point_at_and_read_back_as_saved() {
|
||||
let (d, added) = ballooned();
|
||||
let [placed, nested] = added.map(ids::encode);
|
||||
let view = ids::encode(d.views[0].id);
|
||||
let mut v = file(&d);
|
||||
assert_eq!(
|
||||
*balloon(&mut v, &nested),
|
||||
json!({
|
||||
"id": nested, "view": view,
|
||||
"edge": {"body": "Bolt", "origin_feature": 4, "name": "F1:side|F2:head", "path": [3, 7]},
|
||||
"at": [-35.5, 60.0]
|
||||
})
|
||||
);
|
||||
assert_eq!(balloon(&mut v, &placed)["edge"]["path"], json!([3]));
|
||||
assert_eq!(d.balloons.by_id(added[1]).unwrap().occurrence(), [3, 7], "the part it numbers is its edge's path");
|
||||
let listed: Vec<u64> = v["balloons"].as_array().unwrap().iter().map(|x| ids::decode(x["id"].as_str().unwrap()).unwrap()).collect();
|
||||
let mut by_id = added.to_vec();
|
||||
by_id.sort_unstable();
|
||||
assert_eq!(listed, by_id);
|
||||
let reopened = Drawing::from_json(&d.to_json().unwrap()).unwrap();
|
||||
assert_eq!(reopened, d);
|
||||
assert_eq!(reopened.to_json().unwrap(), d.to_json().unwrap());
|
||||
let mut other_order = v.clone();
|
||||
other_order["balloons"].as_array_mut().unwrap().reverse();
|
||||
assert_eq!(Drawing::from_json(&other_order.to_string()).unwrap(), d);
|
||||
let mut unsorted = d.clone();
|
||||
unsorted.balloons.reverse();
|
||||
assert_eq!(unsorted.validate().unwrap_err().to_string(), "a drawing lists its balloons by id");
|
||||
let mut none = d.clone();
|
||||
none.balloons.clear();
|
||||
assert_eq!(file(&none).get("balloons"), None, "no balloons, none written");
|
||||
let older = json!({
|
||||
"format": DRAWING.format_string(1), "id": "0000000000002", "name": "Old", "sources": [], "language": "en",
|
||||
"sheets": [{"id": "0000000000001", "name": "S", "sheet": {"size": {"standard": "a4"}}}]
|
||||
});
|
||||
assert!(Drawing::from_json(&older.to_string()).unwrap().balloons.is_empty());
|
||||
}
|
||||
|
||||
/// What reading refuses, each rule by name, on both sides of every cap: the view a balloon is on,
|
||||
/// its edge's names, feature and path (a placed part's: 1 to 64 occurrences, none of them 0), its
|
||||
/// place on paper, its id, how many there are, and fields it doesn't know.
|
||||
#[test]
|
||||
fn the_balloons_rules_are_kept_on_reading() {
|
||||
let (d, added) = ballooned();
|
||||
let [placed, nested] = added.map(ids::encode);
|
||||
let rule = |id: &str, rule: &str| format!("damaged drawing: balloon {id}: {rule}");
|
||||
let nothing = ids::encode(99);
|
||||
let on = format!("it is on view {nothing}, which the drawing doesn't have");
|
||||
assert_eq!(refused(&d, |v| balloon(v, &placed)["view"] = json!(nothing)), rule(&placed, &on));
|
||||
// The edge keeps a dimension's reference rules.
|
||||
let names = "a name in a reference is 1 to 512 bytes, without control characters";
|
||||
let body = |name: &str| refused(&d, |v| balloon(v, &nested)["edge"]["body"] = json!(name));
|
||||
let edge_name = |name: &str| refused(&d, |v| balloon(v, &nested)["edge"]["name"] = json!(name));
|
||||
for fits in ["x".repeat(512), "é".repeat(256)] {
|
||||
assert_eq!([body(&fits), edge_name(&fits)], ["read", "read"], "{} bytes", fits.len());
|
||||
}
|
||||
for broken in ["x".repeat(513), String::new(), "two\nlines".into()] {
|
||||
assert_eq!([body(&broken), edge_name(&broken)], [rule(&nested, names), rule(&nested, names)], "{} bytes", broken.len());
|
||||
}
|
||||
let feature = rule(&placed, "a reference names the feature that made its body");
|
||||
assert_eq!(refused(&d, |v| balloon(v, &placed)["edge"]["origin_feature"] = json!(0)), feature);
|
||||
// A placed part's path: 1 to 64 occurrences, none of them 0. A design's own body is no part.
|
||||
let path = |p: Value| refused(&d, |v| balloon(v, &nested)["edge"]["path"] = p);
|
||||
let deep = rule(&nested, "an occurrence path is at most 64 occurrences, none of them 0");
|
||||
let own = rule(&nested, "it points at a placed part: its edge's body is placed through an occurrence");
|
||||
assert_eq!(
|
||||
[path(json!([3])), path(json!(vec![3; 64])), path(json!(vec![3; 65])), path(json!([3, 0])), path(json!([]))],
|
||||
["read".to_string(), "read".into(), deep.clone(), deep, own.clone()]
|
||||
);
|
||||
let no_path = refused(&d, |v| {
|
||||
balloon(v, &nested)["edge"].as_object_mut().unwrap().remove("path");
|
||||
});
|
||||
assert_eq!(no_path, own, "no path");
|
||||
// Its place is on paper.
|
||||
let paper = rule(&placed, "its place is at most 5000 mm on paper");
|
||||
let at = |at: [f64; 2]| refused(&d, |v| balloon(v, &placed)["at"] = json!(at));
|
||||
assert_eq!([at([5000.0, -5000.0]), at([5000.001, 0.0]), at([0.0, -5000.001])], ["read".to_string(), paper.clone(), paper]);
|
||||
assert!(refused(&d, |v| balloon(v, &placed)["at"] = json!([1.0])).starts_with("damaged drawing: "), "two numbers");
|
||||
// Its id is the drawing's: never another object's.
|
||||
let view_id = ids::encode(d.views[0].id);
|
||||
assert_eq!(refused(&d, |v| balloon(v, &placed)["id"] = json!(view_id)), format!("damaged drawing: two objects have the id {view_id}"));
|
||||
// Fields it doesn't know, at every level.
|
||||
let unknown: [fn(&mut Value, &str); 3] = [
|
||||
|v, id| balloon(v, id)["item"] = json!(3),
|
||||
|v, id| balloon(v, id)["edge"]["point"] = json!([0.0, 0.0, 0.0]),
|
||||
|v, id| balloon(v, id)["shape"] = json!("circle"),
|
||||
];
|
||||
for (i, edit) in unknown.into_iter().enumerate() {
|
||||
assert!(refused(&d, |v| edit(v, &placed)).starts_with("damaged drawing: "), "{i}");
|
||||
}
|
||||
// At most MAX_BALLOONS balloons.
|
||||
let many = |n: usize| {
|
||||
let mut v = file(&d);
|
||||
let one = balloon(&mut v, &placed).clone();
|
||||
let copies = (1..=n as u64).map(|i| {
|
||||
let mut copy = one.clone();
|
||||
copy["id"] = json!(ids::encode(i));
|
||||
copy
|
||||
});
|
||||
v["balloons"] = Value::Array(copies.collect());
|
||||
Drawing::from_json(&v.to_string()).map(|d| d.balloons.len()).map_err(|e| e.to_string())
|
||||
};
|
||||
assert_eq!(many(MAX_BALLOONS), Ok(MAX_BALLOONS));
|
||||
assert_eq!(many(MAX_BALLOONS + 1), Err(format!("damaged drawing: a drawing has at most {MAX_BALLOONS} balloons")));
|
||||
}
|
||||
|
||||
/// A sheet's balloons are its views' and go with it; a new id never takes a balloon's.
|
||||
#[test]
|
||||
fn a_sheets_balloons_go_with_it() {
|
||||
let (mut d, added) = ballooned();
|
||||
let sheet = d.sheets[0].id;
|
||||
let on = d.on_sheet(sheet);
|
||||
let mut by_id = added.to_vec();
|
||||
by_id.sort_unstable();
|
||||
assert_eq!(on.balloons, by_id);
|
||||
let mut id_source = IdSource::seeded(SEED);
|
||||
for _ in 0..64 {
|
||||
let id = d.allocate_id(&mut id_source).unwrap();
|
||||
assert!(!added.contains(&id), "a new id never takes a balloon's");
|
||||
}
|
||||
let second = d.allocate_id(&mut id_source).unwrap();
|
||||
d.sheets.push(Arc::new(crate::drawing::Sheet { id: second, name: "Sheet 2".into(), ..(*d.sheets[0]).clone() }));
|
||||
assert_eq!(d.remove_sheet(sheet), on);
|
||||
d.validate().unwrap();
|
||||
assert!(d.balloons.is_empty() && d.views.is_empty());
|
||||
}
|
||||
|
||||
/// A parts list on `d`'s sheet `sheet`, with a fresh id.
|
||||
fn add_list(d: &mut Drawing, sheet: u64, id: u64) {
|
||||
use crate::drawing::SheetComponent;
|
||||
use crate::drawing::table::{MassFormat, PartsList};
|
||||
let of = SheetComponent { source: d.sources[0].id, component: 0 };
|
||||
let list =
|
||||
PartsList { of, view: Default::default(), group: Default::default(), fold: false, purchased_contents: false, mass: MassFormat::default() };
|
||||
let kind = Kind::PartsList(list);
|
||||
d.tables.add([Arc::new(SheetTable { id, sheet, table: kind.default_table(), place: kind.default_place(), kind })]);
|
||||
}
|
||||
|
||||
/// With no link a balloon reads its sheet's only list, else the drawing's only one, else none, with
|
||||
/// the choices listed; a revision table is no choice. Its link wins. Saved only when it names one.
|
||||
#[test]
|
||||
fn a_balloon_reads_the_list_it_names_else_the_only_one() {
|
||||
let (mut d, [id, _]) = ballooned();
|
||||
let sheet = d.sheets[0].id;
|
||||
let read = |d: &Drawing| d.parts_list_for(d.balloons.by_id(id).unwrap()).map(|(list, _)| list);
|
||||
assert_eq!(read(&d), Err(vec![]));
|
||||
let saved: Value = serde_json::from_str(&d.to_json().unwrap()).unwrap();
|
||||
assert_eq!(saved["balloons"][0].get("parts_list"), None);
|
||||
|
||||
let other = 1 << 40;
|
||||
let a4 = drawing_sheet::Sheet::standard(PaperSize::A4, Orientation::Landscape);
|
||||
d.sheets.push(Arc::new(Sheet { id: other, name: "Other".into(), sheet: a4, ..(*d.sheets[0]).clone() }));
|
||||
add_list(&mut d, other, 1 << 41);
|
||||
assert_eq!(read(&d), Ok(1 << 41), "the drawing's only one");
|
||||
add_list(&mut d, sheet, 1 << 42);
|
||||
assert_eq!(read(&d), Ok(1 << 42), "the sheet's only one comes first");
|
||||
add_list(&mut d, sheet, 1 << 43);
|
||||
assert_eq!(read(&d), Err(vec![1 << 41, 1 << 42, 1 << 43]), "the choices");
|
||||
assert!(d.balloons_by_list().is_empty(), "with none to go by, a balloon reads no list");
|
||||
let kind = Kind::Revision;
|
||||
d.tables.add([Arc::new(SheetTable { id: 1 << 44, sheet, table: kind.default_table(), place: kind.default_place(), kind })]);
|
||||
assert_eq!(read(&d).unwrap_err().len(), 3, "a revision table is no choice");
|
||||
|
||||
let at = d.balloons.index_of(id).unwrap();
|
||||
Arc::make_mut(&mut d.balloons[at]).parts_list = Some(1 << 41);
|
||||
assert_eq!(read(&d), Ok(1 << 41), "its link wins");
|
||||
let by_list = d.balloons_by_list();
|
||||
assert_eq!((by_list.len(), by_list[&(1 << 41)].iter().map(|b| b.id).collect::<Vec<_>>()), (1, vec![id]), "the other reads none");
|
||||
d.validate().unwrap();
|
||||
assert_eq!(Drawing::from_json(&d.to_json().unwrap()).unwrap(), d);
|
||||
|
||||
// The list goes, by its own delete or its sheet's: the balloon stays and reads what it can.
|
||||
d.remove_objects(&OnSheet { tables: vec![1 << 41], ..Default::default() });
|
||||
assert_eq!(d.balloons.by_id(id).unwrap().parts_list, None);
|
||||
d.validate().unwrap();
|
||||
assert_eq!(read(&d).unwrap_err(), vec![1 << 42, 1 << 43]);
|
||||
}
|
||||
|
||||
/// A link names one of the drawing's parts lists.
|
||||
#[test]
|
||||
fn a_balloons_link_is_to_a_parts_list() {
|
||||
let (mut d, [id, _]) = ballooned();
|
||||
let sheet = d.sheets[0].id;
|
||||
let kind = Kind::Revision;
|
||||
d.tables.add([Arc::new(SheetTable { id: 1 << 44, sheet, table: kind.default_table(), place: kind.default_place(), kind })]);
|
||||
let at = d.balloons.index_of(id).unwrap();
|
||||
for bad in [1 << 44, 99] {
|
||||
Arc::make_mut(&mut d.balloons[at]).parts_list = Some(bad);
|
||||
assert!(d.validate().unwrap_err().to_string().contains("parts lists"), "{bad}");
|
||||
}
|
||||
let mut v: Value = serde_json::from_str(&Drawing { balloons: Default::default(), ..d.clone() }.to_json().unwrap()).unwrap();
|
||||
v["balloons"] = json!([{"id": "x", "view": "x", "edge": {}, "at": [0.0, 0.0], "parts_list": 7}]);
|
||||
assert!(Drawing::from_value(v).is_err(), "not an id");
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
//! A drawing's list of one kind of record: each behind its own `Arc`, so an undo step shares what it
|
||||
//! didn't change, and by id, whatever order they came in. It holds what every such list does, so a
|
||||
//! kind's own file holds only its own rules.
|
||||
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::require;
|
||||
use crate::{DocError, Result, ids};
|
||||
|
||||
/// A record a drawing lists by its id.
|
||||
pub trait Record {
|
||||
fn id(&self) -> u64;
|
||||
}
|
||||
|
||||
/// The records of one kind, by id. It reads as the `Vec` it is, and a file holds them as one.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ById<T>(Vec<Arc<T>>);
|
||||
|
||||
impl<T> Default for ById<T> {
|
||||
fn default() -> Self {
|
||||
ById(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for ById<T> {
|
||||
type Target = Vec<Arc<T>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for ById<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoIterator for &'a ById<T> {
|
||||
type Item = &'a Arc<T>;
|
||||
type IntoIter = std::slice::Iter<'a, Arc<T>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ById<T> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Record> ById<T> {
|
||||
/// Adds `more`, each where its id puts it.
|
||||
pub fn add(&mut self, more: impl IntoIterator<Item = Arc<T>>) {
|
||||
self.0.extend(more);
|
||||
self.put_in_order();
|
||||
}
|
||||
|
||||
/// Puts what a file listed in another order where each id puts it.
|
||||
pub(super) fn put_in_order(&mut self) {
|
||||
self.0.sort_by_key(|record| record.id());
|
||||
}
|
||||
|
||||
/// Where record `id` is in the list, found by halves.
|
||||
pub fn index_of(&self, id: u64) -> Option<usize> {
|
||||
self.0.binary_search_by_key(&id, |record| record.id()).ok()
|
||||
}
|
||||
|
||||
/// Record `id`.
|
||||
pub fn by_id(&self, id: u64) -> Option<&T> {
|
||||
self.index_of(id).and_then(|at| self.0.get(at)).map(|record| &**record)
|
||||
}
|
||||
|
||||
/// The list's rules, each named by `word`: at most `most` records, by id, each keeping `rule`.
|
||||
pub(super) fn check(&self, most: usize, word: &str, rule: impl Fn(&T) -> Result<()>) -> Result<()> {
|
||||
require(self.0.len() <= most, || format!("a drawing has at most {most} {word}s"))?;
|
||||
require(self.0.is_sorted_by_key(|record| record.id()), || format!("a drawing lists its {word}s by id"))?;
|
||||
self.0.iter().try_for_each(|record| rule(record).map_err(|e| DocError::Invalid(format!("{word} {}: {e}", ids::encode(record.id())))))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "by_id_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,50 +0,0 @@
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct Thing(u64);
|
||||
|
||||
impl Record for Thing {
|
||||
fn id(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn things(ids: &[u64]) -> ById<Thing> {
|
||||
let mut list = ById::default();
|
||||
list.add(ids.iter().map(|id| Arc::new(Thing(*id))));
|
||||
list
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_go_where_their_ids_put_them_and_are_found_there() {
|
||||
let mut list = things(&[30, 10]);
|
||||
list.add([Arc::new(Thing(20))]);
|
||||
assert_eq!(list.iter().map(|t| t.0).collect::<Vec<_>>(), [10, 20, 30]);
|
||||
assert_eq!((list.index_of(20), list.by_id(30).map(|t| t.0)), (Some(1), Some(30)));
|
||||
assert_eq!((list.index_of(25), list.by_id(25)), (None, None));
|
||||
assert!(ById::<Thing>::default().is_empty() && !list.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_holds_the_plain_list() {
|
||||
let list = things(&[1, 2]);
|
||||
assert_eq!(serde_json::to_value(&list).unwrap(), json!([1, 2]));
|
||||
assert_eq!(serde_json::from_value::<ById<Thing>>(json!([1, 2])).unwrap(), list);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_rules_name_the_kind_and_the_record() {
|
||||
let fine = |_: &Thing| Ok(());
|
||||
things(&[1, 2]).check(2, "thing", fine).unwrap();
|
||||
assert!(things(&[1, 2, 3]).check(2, "thing", fine).is_err_and(|e| e.to_string().contains("at most 2 things")));
|
||||
let mut out_of_order = things(&[1, 2]);
|
||||
out_of_order.reverse();
|
||||
assert!(out_of_order.check(2, "thing", fine).is_err_and(|e| e.to_string().contains("lists its things by id")));
|
||||
out_of_order.put_in_order();
|
||||
out_of_order.check(2, "thing", fine).unwrap();
|
||||
let odd = |t: &Thing| require(t.0.is_multiple_of(2), || "it is odd".into());
|
||||
let e = things(&[2, 3]).check(2, "thing", odd).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("thing {}: it is odd", ids::encode(3))), "{e}");
|
||||
}
|
||||
@@ -1,581 +0,0 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcore_record::Vec2;
|
||||
use solidcraft_geom::drawing_sheet::{self, Orientation, PaperSize};
|
||||
use solidcraft_geom::drawing_view::NamedView;
|
||||
|
||||
use super::*;
|
||||
use crate::drawing::Source;
|
||||
use crate::drawing::view::{View, ViewKind, ViewPlace};
|
||||
use crate::drawing_view::{ViewDirection, ViewOverrides};
|
||||
use crate::format::DRAWING;
|
||||
use crate::ids::IdSource;
|
||||
|
||||
const SEED: u64 = 4896;
|
||||
/// The body every reference here is on, and the feature that made it.
|
||||
const BODY: &str = "Box";
|
||||
const FEATURE: u64 = 7;
|
||||
|
||||
fn corner(edges: &[&str]) -> ModelPoint {
|
||||
ModelPoint { body: BODY.into(), origin_feature: FEATURE, edges: edges.iter().map(|e| e.to_string()).collect(), centre: false, path: Vec::new() }
|
||||
}
|
||||
|
||||
fn edge(name: &str) -> ModelEdge {
|
||||
ModelEdge { body: BODY.into(), origin_feature: FEATURE, name: name.into(), path: Vec::new() }
|
||||
}
|
||||
|
||||
/// A drawing of one design on one A3 sheet, its front view at 1:2 at (150, 150), and on the view a
|
||||
/// linear dimension between two corners, an edge's length and an angle between two edges: their
|
||||
/// ids, in the order they were added.
|
||||
fn measured() -> (Drawing, [u64; 3]) {
|
||||
let mut id_source = IdSource::seeded(SEED);
|
||||
let a3 = drawing_sheet::Sheet::standard(PaperSize::A3, Orientation::Landscape);
|
||||
let mut d = Drawing::new("Box", "en", "Sheet 1", a3, None, &BTreeSet::new(), &mut id_source).unwrap();
|
||||
let source = d.allocate_id(&mut id_source).unwrap();
|
||||
d.sources.push(Source { id: source });
|
||||
let (view, sheet) = (d.allocate_id(&mut id_source).unwrap(), d.sheets[0].id);
|
||||
let kind = ViewKind::Base { source, view: ViewDirection::Named(NamedView::Front) };
|
||||
let settings = ViewOverrides { scale: Some("1:2".parse().unwrap()), ..ViewOverrides::default() };
|
||||
d.add_views([Arc::new(View { id: view, sheet, kind, place: ViewPlace::At([150.0, 150.0]), settings, reference_shown_percent: 0.0 })]);
|
||||
let kinds = [
|
||||
DimensionKind::Linear {
|
||||
a: corner(&["F1:front|F1:bottom", "F1:front|F1:left"]),
|
||||
b: corner(&["F1:front|F1:bottom", "F1:front|F1:right"]),
|
||||
direction: LinearDirection::Horizontal,
|
||||
reading: Reading::Projected,
|
||||
},
|
||||
DimensionKind::Length { edge: edge("F1:front|F1:top"), direction: LinearDirection::Aligned, reading: Reading::True },
|
||||
DimensionKind::Angle { a: edge("F1:front|F1:top"), b: edge("F2:chamfer|F1:front"), reading: Reading::Projected, other_side: true },
|
||||
];
|
||||
let texts = [TextPlace::Centred, TextPlace::At([0.0, -20.0]), TextPlace::At([12.5, 30.0])];
|
||||
let mut added = [0; 3];
|
||||
for ((kind, text), id) in kinds.into_iter().zip(texts).zip(&mut added) {
|
||||
*id = d.allocate_id(&mut id_source).unwrap();
|
||||
d.add_dimensions([Arc::new(Dimension { id: *id, view, kind, offset_mm: -10.0, text, decimals: 2, override_text: None, last_good: None })]);
|
||||
}
|
||||
d.validate().unwrap();
|
||||
(d, added)
|
||||
}
|
||||
|
||||
fn file(d: &Drawing) -> Value {
|
||||
serde_json::from_str(&d.to_json().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// Dimension `id` in a drawing's file.
|
||||
fn dim<'a>(v: &'a mut Value, id: &str) -> &'a mut Value {
|
||||
v["dimensions"].as_array_mut().unwrap().iter_mut().find(|x| x["id"] == id).unwrap()
|
||||
}
|
||||
|
||||
/// 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);
|
||||
Drawing::from_json(&v.to_string()).map_or_else(|e| e.to_string(), |_| "read".into())
|
||||
}
|
||||
|
||||
/// A dimension saves what it measures by name, its offset, its text's place and its decimals, and
|
||||
/// nothing it leaves out. The dimensions save by id, whatever order they were added in, and read
|
||||
/// back as saved, the same bytes again; a file that lists them in another order reads by id. An
|
||||
/// older drawing reads with none, and a drawing with none writes none.
|
||||
#[test]
|
||||
fn dimensions_save_what_they_measure_and_read_back_as_saved() {
|
||||
let (d, added) = measured();
|
||||
let [linear_id, length_id, angle_id] = added.map(ids::encode);
|
||||
let view = ids::encode(d.views[0].id);
|
||||
let mut v = file(&d);
|
||||
let box_corner = |edges: [&str; 2]| json!({"body": "Box", "origin_feature": 7, "edges": edges});
|
||||
assert_eq!(
|
||||
*dim(&mut v, &linear_id),
|
||||
json!({
|
||||
"id": linear_id, "view": view,
|
||||
"kind": {"linear": {
|
||||
"a": box_corner(["F1:front|F1:bottom", "F1:front|F1:left"]),
|
||||
"b": box_corner(["F1:front|F1:bottom", "F1:front|F1:right"]),
|
||||
"direction": "horizontal", "reading": "projected"
|
||||
}},
|
||||
"offset_mm": -10.0, "text": "centred", "decimals": 2
|
||||
})
|
||||
);
|
||||
let length = dim(&mut v, &length_id).clone();
|
||||
let top_edge = json!({"body": "Box", "origin_feature": 7, "name": "F1:front|F1:top"});
|
||||
assert_eq!(length["kind"], json!({"length": {"edge": top_edge, "direction": "aligned", "reading": "true"}}));
|
||||
assert_eq!(length["text"], json!({"at": [0.0, -20.0]}));
|
||||
assert_eq!(dim(&mut v, &angle_id)["kind"]["angle"]["other_side"], json!(true));
|
||||
let mut one_side = v.clone();
|
||||
dim(&mut one_side, &angle_id)["kind"]["angle"].as_object_mut().unwrap().remove("other_side");
|
||||
let one_side = Drawing::from_json(&one_side.to_string()).unwrap();
|
||||
assert_eq!(dim(&mut file(&one_side), &angle_id)["kind"]["angle"].get("other_side"), None, "the usual side, not written");
|
||||
let listed: Vec<u64> = v["dimensions"].as_array().unwrap().iter().map(|x| ids::decode(x["id"].as_str().unwrap()).unwrap()).collect();
|
||||
let mut by_id = added.to_vec();
|
||||
by_id.sort_unstable();
|
||||
assert_eq!(listed, by_id);
|
||||
let reopened = Drawing::from_json(&d.to_json().unwrap()).unwrap();
|
||||
assert_eq!(reopened, d);
|
||||
assert_eq!(reopened.to_json().unwrap(), d.to_json().unwrap());
|
||||
let mut other_order = v.clone();
|
||||
other_order["dimensions"].as_array_mut().unwrap().reverse();
|
||||
assert_eq!(Drawing::from_json(&other_order.to_string()).unwrap(), d);
|
||||
let mut unsorted = d.clone();
|
||||
unsorted.dimensions.reverse();
|
||||
assert_eq!(unsorted.validate().unwrap_err().to_string(), "a drawing lists its dimensions by id");
|
||||
let mut none = d.clone();
|
||||
none.dimensions.clear();
|
||||
assert_eq!(file(&none).get("dimensions"), None, "no dimensions, none written");
|
||||
let older = json!({
|
||||
"format": DRAWING.format_string(1), "id": "0000000000002", "name": "Old", "sources": [], "language": "en",
|
||||
"sheets": [{"id": "0000000000001", "name": "S", "sheet": {"size": {"standard": "a4"}}}]
|
||||
});
|
||||
assert!(Drawing::from_json(&older.to_string()).unwrap().dimensions.is_empty());
|
||||
}
|
||||
|
||||
/// What reading refuses, each rule by name, on both sides of every cap: the view a dimension is
|
||||
/// on, the names, the feature and the path of its references, a corner's edges, its direction and
|
||||
/// reading, its offset and text place, its decimals and its text, its id, how many there are, and
|
||||
/// fields it doesn't know.
|
||||
#[test]
|
||||
fn the_dimensions_rules_are_kept_on_reading() {
|
||||
let (d, added) = measured();
|
||||
let [linear_id, length_id, angle_id] = added.map(ids::encode);
|
||||
let rule = |id: &str, rule: &str| format!("damaged drawing: dimension {id}: {rule}");
|
||||
let nothing = ids::encode(99);
|
||||
let on = format!("it is on view {nothing}, which the drawing doesn't have");
|
||||
assert_eq!(refused(&d, |v| dim(v, &linear_id)["view"] = json!(nothing)), rule(&linear_id, &on));
|
||||
// Names: of the body, an edge and a corner's edges, 1 to 512 bytes without control characters.
|
||||
let names = "a name in a reference is 1 to 512 bytes, without control characters";
|
||||
let body = |name: &str| refused(&d, |v| dim(v, &length_id)["kind"]["length"]["edge"]["body"] = json!(name));
|
||||
let edge_name = |name: &str| refused(&d, |v| dim(v, &angle_id)["kind"]["angle"]["b"]["name"] = json!(name));
|
||||
let corner_edge = |name: &str| refused(&d, |v| dim(v, &linear_id)["kind"]["linear"]["a"]["edges"][1] = json!(name));
|
||||
for fits in ["x".repeat(512), "é".repeat(256), "F1:front|F1:top".into()] {
|
||||
assert_eq!([body(&fits), edge_name(&fits), corner_edge(&fits)], ["read", "read", "read"], "{} bytes", fits.len());
|
||||
}
|
||||
for broken in ["x".repeat(513), "é".repeat(256) + "a", "x".repeat(1_000_000), String::new(), "two\nlines".into(), "nul\0".into()] {
|
||||
let expected = [rule(&length_id, names), rule(&angle_id, names), rule(&linear_id, names)];
|
||||
assert_eq!([body(&broken), edge_name(&broken), corner_edge(&broken)], expected, "{} bytes", broken.len());
|
||||
}
|
||||
// A corner is where 2 to 16 named edges meet.
|
||||
let edges = |n: usize| {
|
||||
refused(&d, |v| dim(v, &linear_id)["kind"]["linear"]["b"]["edges"] = json!((0..n).map(|i| format!("F1:e{i}")).collect::<Vec<_>>()))
|
||||
};
|
||||
let meet = rule(&linear_id, "a corner is where 2 to 16 named edges meet");
|
||||
assert_eq!([edges(0), edges(1), edges(2), edges(16), edges(17)], [meet.clone(), meet.clone(), "read".into(), "read".into(), meet]);
|
||||
// A centre is of one named circle or arc, and saves as `centre`.
|
||||
let centred = |n: usize| {
|
||||
let mut v = file(&d);
|
||||
let b = &mut dim(&mut v, &linear_id)["kind"]["linear"]["b"];
|
||||
b["centre"] = json!(true);
|
||||
b["edges"] = json!((0..n).map(|i| format!("F3:e{i}")).collect::<Vec<_>>());
|
||||
let read = Drawing::from_json(&v.to_string()).map_err(|e| e.to_string())?;
|
||||
Ok::<_, String>(dim(&mut file(&read), &linear_id)["kind"]["linear"]["b"].clone())
|
||||
};
|
||||
let one = Err(rule(&linear_id, "a centre is of one named circle or arc"));
|
||||
assert_eq!([centred(0), centred(2)], [one.clone(), one]);
|
||||
assert_eq!(centred(1), Ok(json!({"body": "Box", "origin_feature": 7, "edges": ["F3:e0"], "centre": true})));
|
||||
// The feature that made the body, and the occurrence path.
|
||||
let feature = rule(&length_id, "a reference names the feature that made its body");
|
||||
assert_eq!(refused(&d, |v| dim(v, &length_id)["kind"]["length"]["edge"]["origin_feature"] = json!(0)), feature);
|
||||
let path = |p: Vec<u64>| refused(&d, |v| dim(v, &angle_id)["kind"]["angle"]["a"]["path"] = json!(p));
|
||||
let deep = rule(&angle_id, "an occurrence path is at most 64 occurrences, none of them 0");
|
||||
assert_eq!([path(vec![3; 64]), path(vec![3; 65]), path(vec![3, 0]), path(vec![])], ["read".into(), deep.clone(), deep, "read".to_string()]);
|
||||
// A direction to measure along that isn't zero, and a true length measured aligned.
|
||||
let along = |a: Value| refused(&d, |v| dim(v, &length_id)["kind"]["length"]["direction"] = json!({"along": a}));
|
||||
let zero = rule(&length_id, "the direction to measure along is zero or not finite");
|
||||
assert_eq!(along(json!([0.0, 0.0])), zero);
|
||||
assert!(along(json!([1e308, 1e308])).ends_with("is out of range"), "the file's own number check, before the record's");
|
||||
// A whole number passes the file's check but would save as one past it: the record refuses it.
|
||||
assert_eq!(along(json!([u64::MAX, 0])), rule(&length_id, "the direction to measure along is at most 1000000000 each way"));
|
||||
assert_eq!(along(json!([1_000_000_000, 1])), rule(&length_id, "a true length is measured aligned"), "1e9 each way passes");
|
||||
assert_eq!(along(json!([0.0, 2.0])), rule(&length_id, "a true length is measured aligned"), "along, read true");
|
||||
let projected_along = refused(&d, |v| {
|
||||
dim(v, &length_id)["kind"]["length"]["direction"] = json!({"along": [0.0, 2.0]});
|
||||
dim(v, &length_id)["kind"]["length"]["reading"] = json!("projected");
|
||||
});
|
||||
assert_eq!(projected_along, "read");
|
||||
let true_horizontal = refused(&d, |v| dim(v, &linear_id)["kind"]["linear"]["reading"] = json!("true"));
|
||||
assert_eq!(true_horizontal, rule(&linear_id, "a true length is measured aligned"));
|
||||
// On paper: the offset and the text's place.
|
||||
let paper = rule(&angle_id, "its offset and its text's place are at most 5000 mm on paper");
|
||||
let offset = |mm: f64| refused(&d, |v| dim(v, &angle_id)["offset_mm"] = json!(mm));
|
||||
let text = |at: [f64; 2]| refused(&d, |v| dim(v, &angle_id)["text"] = json!({"at": at}));
|
||||
assert_eq!([offset(5000.0), offset(-5000.0), offset(5000.001), offset(-5000.001)], ["read".into(), "read".into(), paper.clone(), paper.clone()]);
|
||||
assert_eq!([text([5000.0, -5000.0]), text([5000.001, 0.0]), text([0.0, -5000.001])], ["read".into(), paper.clone(), paper]);
|
||||
// Decimals, and the text shown instead of the number.
|
||||
let decimals = |n: u64| refused(&d, |v| dim(v, &angle_id)["decimals"] = json!(n));
|
||||
assert_eq!([decimals(0), decimals(6), decimals(7)], ["read".into(), "read".into(), rule(&angle_id, "it shows at most 6 decimals")]);
|
||||
assert!(decimals(256).starts_with("damaged drawing: "), "past a u8");
|
||||
let instead = |text: &str| refused(&d, |v| dim(v, &linear_id)["override_text"] = json!(text));
|
||||
let text_rule = rule(&linear_id, "the text shown instead of its number is 1 to 1024 bytes");
|
||||
assert_eq!(
|
||||
[instead("≈80"), instead(&"x".repeat(1024)), instead(""), instead(&"x".repeat(1025))],
|
||||
["read".into(), "read".into(), text_rule.clone(), text_rule]
|
||||
);
|
||||
// Its id is the drawing's: never another object's.
|
||||
let view_id = ids::encode(d.views[0].id);
|
||||
assert_eq!(refused(&d, |v| dim(v, &length_id)["id"] = json!(view_id)), format!("damaged drawing: two objects have the id {view_id}"));
|
||||
// Fields it doesn't know, at every level.
|
||||
let unknown: [fn(&mut Value, &str); 5] = [
|
||||
|v, id| dim(v, id)["label"] = json!("A"),
|
||||
|v, id| dim(v, id)["kind"]["linear"]["tolerance"] = json!(0.1),
|
||||
|v, id| dim(v, id)["kind"]["linear"]["a"]["point"] = json!([0.0, 0.0, 0.0]),
|
||||
|v, id| dim(v, id)["text"] = json!({"at": [1.0, 2.0], "angle": 3.0}),
|
||||
|v, id| dim(v, id)["kind"] = json!({"baseline": {}}),
|
||||
];
|
||||
for (i, edit) in unknown.into_iter().enumerate() {
|
||||
assert!(refused(&d, |v| edit(v, &linear_id)).starts_with("damaged drawing: "), "{i}");
|
||||
}
|
||||
// At most MAX_DIMENSIONS dimensions.
|
||||
let many = |n: usize| {
|
||||
let mut v = file(&d);
|
||||
let one = dim(&mut v, &linear_id).clone();
|
||||
let copies = (1..=n as u64).map(|i| {
|
||||
let mut copy = one.clone();
|
||||
copy["id"] = json!(ids::encode(i));
|
||||
copy
|
||||
});
|
||||
v["dimensions"] = Value::Array(copies.collect());
|
||||
Drawing::from_json(&v.to_string()).map(|d| d.dimensions.len()).map_err(|e| e.to_string())
|
||||
};
|
||||
assert_eq!(many(MAX_DIMENSIONS), Ok(MAX_DIMENSIONS));
|
||||
assert_eq!(many(MAX_DIMENSIONS + 1), Err(format!("damaged drawing: a drawing has at most {MAX_DIMENSIONS} dimensions")));
|
||||
}
|
||||
|
||||
/// Two lines at 45° read 45°, and 135° on the other side, whichever way each runs: the angle
|
||||
/// between their directions is 45° or 135° by how the kernel runs the edges.
|
||||
#[test]
|
||||
fn an_angle_between_lines_reads_the_same_whichever_way_they_run() {
|
||||
let (acute, obtuse) = (45f64.to_radians(), 135f64.to_radians());
|
||||
for between in [acute, obtuse] {
|
||||
assert!((between_lines(between, false) - acute).abs() < 1e-12, "{between}");
|
||||
assert!((between_lines(between, true) - obtuse).abs() < 1e-12, "{between}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A radial dimension saves its edge, and `diameter` only when it is one; its edge keeps an edge's
|
||||
/// rules, and it has no reading.
|
||||
#[test]
|
||||
fn a_radial_dimension_saves_its_edge_and_whether_it_is_a_diameter() {
|
||||
let (mut d, _) = measured();
|
||||
let (view, id) = (d.views[0].id, d.allocate_id(&mut IdSource::seeded(SEED + 1)).unwrap());
|
||||
let radial = |diameter| DimensionKind::Radial { edge: edge("F3:hole|F1:front"), diameter };
|
||||
let kind = radial(true);
|
||||
d.add_dimensions([Arc::new(Dimension {
|
||||
id,
|
||||
view,
|
||||
kind,
|
||||
offset_mm: 5.0,
|
||||
text: TextPlace::Centred,
|
||||
decimals: 1,
|
||||
override_text: None,
|
||||
last_good: None,
|
||||
})]);
|
||||
d.validate().unwrap();
|
||||
let radial_id = ids::encode(id);
|
||||
let mut v = file(&d);
|
||||
let hole = json!({"body": "Box", "origin_feature": 7, "name": "F3:hole|F1:front"});
|
||||
assert_eq!(dim(&mut v, &radial_id)["kind"], json!({"radial": {"edge": hole, "diameter": true}}));
|
||||
assert_eq!(Drawing::from_json(&v.to_string()).unwrap(), d);
|
||||
dim(&mut v, &radial_id)["kind"]["radial"].as_object_mut().unwrap().remove("diameter");
|
||||
let radius = Drawing::from_json(&v.to_string()).unwrap();
|
||||
assert_eq!(radius.dimension(id).map(|r| &r.kind), Some(&radial(false)));
|
||||
assert_eq!(dim(&mut file(&radius), &radial_id)["kind"]["radial"].get("diameter"), None, "a radius, not written");
|
||||
let names = format!("damaged drawing: dimension {radial_id}: a name in a reference is 1 to 512 bytes, without control characters");
|
||||
assert_eq!(refused(&d, |v| dim(v, &radial_id)["kind"]["radial"]["edge"]["name"] = json!("")), names);
|
||||
assert!(refused(&d, |v| dim(v, &radial_id)["kind"]["radial"]["reading"] = json!("true")).starts_with("damaged drawing: "));
|
||||
}
|
||||
|
||||
/// A chain saves its points in order and its direction, and keeps its rules on reading: 3 to
|
||||
/// [`MAX_CHAIN_POINTS`] points, each with a point's rules, a direction to measure along that isn't
|
||||
/// zero, no reading (it reads projected) and no text instead of its members' numbers.
|
||||
#[test]
|
||||
fn a_chain_saves_its_points_in_order_and_keeps_its_rules() {
|
||||
let (mut d, _) = measured();
|
||||
let (view, id) = (d.views[0].id, d.allocate_id(&mut IdSource::seeded(SEED + 1)).unwrap());
|
||||
let hole = |n: u64| {
|
||||
let rim = format!("F{n}:hole|F1:front");
|
||||
ModelPoint { centre: true, ..corner(&[rim.as_str()]) }
|
||||
};
|
||||
let points = vec![corner(&["F1:front|F1:bottom", "F1:front|F1:left"]), hole(3), hole(4), hole(5)];
|
||||
let kind = DimensionKind::Chain { points, direction: LinearDirection::Horizontal };
|
||||
d.add_dimensions([Arc::new(Dimension {
|
||||
id,
|
||||
view,
|
||||
kind,
|
||||
offset_mm: 8.0,
|
||||
text: TextPlace::Centred,
|
||||
decimals: 1,
|
||||
override_text: None,
|
||||
last_good: None,
|
||||
})]);
|
||||
d.validate().unwrap();
|
||||
let chain_id = ids::encode(id);
|
||||
let mut v = file(&d);
|
||||
let first = json!({"body": "Box", "origin_feature": 7, "edges": ["F1:front|F1:bottom", "F1:front|F1:left"]});
|
||||
let centre = |n: u64| json!({"body": "Box", "origin_feature": 7, "edges": [format!("F{n}:hole|F1:front")], "centre": true});
|
||||
let saved = json!({"chain": {"points": [first, centre(3), centre(4), centre(5)], "direction": "horizontal"}});
|
||||
assert_eq!(dim(&mut v, &chain_id)["kind"], saved);
|
||||
assert_eq!(Drawing::from_json(&v.to_string()).unwrap(), d);
|
||||
let rule = |rule: &str| format!("damaged drawing: dimension {chain_id}: {rule}");
|
||||
let count = |n: usize| {
|
||||
refused(&d, |v| {
|
||||
let points = &mut dim(v, &chain_id)["kind"]["chain"]["points"];
|
||||
let one = points[1].clone();
|
||||
*points = json!(vec![one; n]);
|
||||
})
|
||||
};
|
||||
let three_to = rule(&format!("a chain has 3 to {MAX_CHAIN_POINTS} points"));
|
||||
let counts = [count(0), count(2), count(3), count(MAX_CHAIN_POINTS), count(MAX_CHAIN_POINTS + 1)];
|
||||
assert_eq!(counts, [three_to.clone(), three_to.clone(), "read".into(), "read".into(), three_to]);
|
||||
let two_edges = refused(&d, |v| dim(v, &chain_id)["kind"]["chain"]["points"][2]["edges"] = json!(["F3:a", "F3:b"]));
|
||||
assert_eq!(two_edges, rule("a centre is of one named circle or arc"));
|
||||
let zero = refused(&d, |v| dim(v, &chain_id)["kind"]["chain"]["direction"] = json!({"along": [0.0, 0.0]}));
|
||||
assert_eq!(zero, rule("the direction to measure along is zero or not finite"));
|
||||
let instead = refused(&d, |v| dim(v, &chain_id)["override_text"] = json!("60"));
|
||||
assert_eq!(instead, rule("a chain shows its members' numbers, never a text instead"));
|
||||
let reading = refused(&d, |v| dim(v, &chain_id)["kind"]["chain"]["reading"] = json!("projected"));
|
||||
assert!(reading.starts_with("damaged drawing: "), "{reading}");
|
||||
}
|
||||
|
||||
/// A cylinder dimension saves its face by name, and the face keeps a reference's rules.
|
||||
#[test]
|
||||
fn a_cylinder_dimension_saves_its_face() {
|
||||
let (mut d, _) = measured();
|
||||
let (view, id) = (d.views[0].id, d.allocate_id(&mut IdSource::seeded(SEED + 1)).unwrap());
|
||||
let face = ModelFace { body: BODY.into(), origin_feature: FEATURE, name: "F4:boss".into(), path: vec![3] };
|
||||
let kind = DimensionKind::Cylinder { face };
|
||||
d.add_dimensions([Arc::new(Dimension {
|
||||
id,
|
||||
view,
|
||||
kind,
|
||||
offset_mm: 6.0,
|
||||
text: TextPlace::Centred,
|
||||
decimals: 1,
|
||||
override_text: None,
|
||||
last_good: None,
|
||||
})]);
|
||||
d.validate().unwrap();
|
||||
let cylinder = ids::encode(id);
|
||||
let mut v = file(&d);
|
||||
let saved = json!({"cylinder": {"face": {"body": "Box", "origin_feature": 7, "name": "F4:boss", "path": [3]}}});
|
||||
assert_eq!(dim(&mut v, &cylinder)["kind"], saved);
|
||||
assert_eq!(Drawing::from_json(&v.to_string()).unwrap(), d);
|
||||
let rule = |rule: &str| format!("damaged drawing: dimension {cylinder}: {rule}");
|
||||
let changed = |key: &str, value: Value| refused(&d, |v| dim(v, &cylinder)["kind"]["cylinder"]["face"][key] = value);
|
||||
assert_eq!(changed("name", json!("")), rule("a name in a reference is 1 to 512 bytes, without control characters"));
|
||||
assert_eq!(changed("origin_feature", json!(0)), rule("a reference names the feature that made its body"));
|
||||
assert_eq!(changed("path", json!([0])), rule("an occurrence path is at most 64 occurrences, none of them 0"));
|
||||
assert!(changed("edges", json!(["F4:boss"])).starts_with("damaged drawing: "), "a face has no edges");
|
||||
}
|
||||
|
||||
/// A dimension between two cylinders' silhouettes saves each one's face and side, and its
|
||||
/// direction; each face keeps a reference's rules, a side is near or far, and it reads as the view
|
||||
/// shows it (no `reading`).
|
||||
#[test]
|
||||
fn a_silhouettes_dimension_saves_each_face_and_side() {
|
||||
let (mut d, _) = measured();
|
||||
let (view, id) = (d.views[0].id, d.allocate_id(&mut IdSource::seeded(SEED + 1)).unwrap());
|
||||
let face = |name: &str| ModelFace { body: BODY.into(), origin_feature: FEATURE, name: name.into(), path: Vec::new() };
|
||||
let (a, b) = (Silhouette { face: face("F4:boss"), side: SilhouetteSide::Near }, Silhouette { face: face("F5:pin"), side: SilhouetteSide::Far });
|
||||
let kind = DimensionKind::Silhouettes { a, b, direction: LinearDirection::Vertical };
|
||||
d.add_dimensions([Arc::new(Dimension {
|
||||
id,
|
||||
view,
|
||||
kind,
|
||||
offset_mm: 6.0,
|
||||
text: TextPlace::Centred,
|
||||
decimals: 1,
|
||||
override_text: None,
|
||||
last_good: None,
|
||||
})]);
|
||||
d.validate().unwrap();
|
||||
let silhouettes = ids::encode(id);
|
||||
let mut v = file(&d);
|
||||
let face = |name: &str| json!({"body": "Box", "origin_feature": 7, "name": name});
|
||||
let saved = json!({"silhouettes": {"a": {"face": face("F4:boss"), "side": "near"}, "b": {"face": face("F5:pin"), "side": "far"}, "direction": "vertical"}});
|
||||
assert_eq!(dim(&mut v, &silhouettes)["kind"], saved);
|
||||
assert_eq!(Drawing::from_json(&v.to_string()).unwrap(), d);
|
||||
let rule = |rule: &str| format!("damaged drawing: dimension {silhouettes}: {rule}");
|
||||
let changed = |edit: &dyn Fn(&mut Value)| refused(&d, |v| edit(&mut dim(v, &silhouettes)["kind"]["silhouettes"]));
|
||||
assert_eq!(changed(&|k| k["b"]["face"]["name"] = json!("")), rule("a name in a reference is 1 to 512 bytes, without control characters"));
|
||||
assert_eq!(changed(&|k| k["a"]["face"]["origin_feature"] = json!(0)), rule("a reference names the feature that made its body"));
|
||||
assert_eq!(changed(&|k| k["direction"] = json!({"along": [0.0, 0.0]})), rule("the direction to measure along is zero or not finite"));
|
||||
let malformed: [fn(&mut Value); 3] = [|k| k["a"]["side"] = json!("left"), |k| k["reading"] = json!("true"), |k| k["b"]["edges"] = json!([])];
|
||||
for edit in malformed {
|
||||
let why = changed(&edit);
|
||||
assert!(why.starts_with("damaged drawing: "), "{why}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A direction along a picked line saves as its plane coordinates and reads back as the same
|
||||
/// direction: what W19a measures along.
|
||||
#[test]
|
||||
fn a_direction_along_a_line_saves_as_its_coordinates() {
|
||||
let along = LinearDirection::Along(Vec2::new(0.6, 0.8));
|
||||
assert_eq!(serde_json::to_value(along).unwrap(), json!({"along": [0.6, 0.8]}));
|
||||
assert_eq!(serde_json::from_value::<LinearDirection>(json!({"along": [0.6, 0.8]})).unwrap(), along);
|
||||
assert_eq!(serde_json::to_value(Reading::True).unwrap(), json!("true"));
|
||||
}
|
||||
|
||||
/// A last good value saves only when there is one, reads back as saved, and an older drawing reads
|
||||
/// with none.
|
||||
#[test]
|
||||
fn a_last_good_value_saves_only_when_there_is_one() {
|
||||
let (mut d, [linear, ..]) = measured();
|
||||
let id = ids::encode(linear);
|
||||
assert_eq!(dim(&mut file(&d), &id).get("last_good"), None, "none: not written");
|
||||
let at = d.dimension_index(linear).unwrap();
|
||||
Arc::make_mut(&mut d.dimensions[at]).last_good = Some(80.5);
|
||||
let mut v = file(&d);
|
||||
assert_eq!(dim(&mut v, &id)["last_good"], json!(80.5));
|
||||
let read = Drawing::from_json(&v.to_string()).unwrap();
|
||||
assert_eq!(read, d);
|
||||
assert_eq!(read.to_json().unwrap(), d.to_json().unwrap(), "the same bytes again");
|
||||
dim(&mut v, &id).as_object_mut().unwrap().remove("last_good");
|
||||
assert_eq!(Drawing::from_json(&v.to_string()).unwrap().dimension(linear).unwrap().last_good, None);
|
||||
}
|
||||
|
||||
/// A last good value is from 0 to the largest number a drawing saves; a file with any other is
|
||||
/// refused, and so is a record built with one that JSON can't hold.
|
||||
#[test]
|
||||
fn a_last_good_value_keeps_its_rule() {
|
||||
let (d, [linear, ..]) = measured();
|
||||
let id = ids::encode(linear);
|
||||
let with = |value: Value| refused(&d, |v| dim(v, &id)["last_good"] = value);
|
||||
for good in [json!(0.0), json!(-0.0), json!(5e-324), json!(80), json!(MAX_REAL)] {
|
||||
assert_eq!(with(good.clone()), "read", "{good}");
|
||||
}
|
||||
for bad in [json!(-5e-324), json!(-1.0)] {
|
||||
assert!(with(bad.clone()).contains("last good"), "{bad}: {}", with(bad.clone()));
|
||||
}
|
||||
// Past every drawing's largest number: the file's own check refuses it first.
|
||||
let past = MAX_REAL * (1.0 + f64::EPSILON);
|
||||
for bad in [json!(past), json!(f64::MAX), json!(-f64::MAX)] {
|
||||
assert_ne!(with(bad.clone()), "read", "{bad}");
|
||||
}
|
||||
let mut direct = d.clone();
|
||||
Arc::make_mut(&mut direct.dimensions[0]).last_good = Some(past);
|
||||
assert!(direct.validate().is_err());
|
||||
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
let mut direct = d.clone();
|
||||
Arc::make_mut(&mut direct.dimensions[0]).last_good = Some(bad);
|
||||
assert!(direct.validate().is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// What dimensions last read goes into their records by drawing and dimension, leaving out a
|
||||
/// dimension or a drawing no longer there and a value the record would refuse. A drawing with no
|
||||
/// new value stays shared with the document it was copied from, and that document keeps none.
|
||||
#[test]
|
||||
fn keep_last_good_keeps_only_what_a_record_may_save() {
|
||||
let (first, [a, b, c]) = measured();
|
||||
let second = Drawing { id: first.id + 1, ..first.clone() };
|
||||
let (one, two) = (first.id, second.id);
|
||||
let mut doc = Document::new("Box");
|
||||
doc.drawings = vec![Arc::new(first), Arc::new(second)];
|
||||
let before = doc.clone();
|
||||
let values = BTreeMap::from([
|
||||
((one, a), 80.0),
|
||||
((one, b), MAX_REAL),
|
||||
((one, c), MAX_REAL * 2.0),
|
||||
((one, 1), 5.0),
|
||||
((one, u64::MAX), 5.0),
|
||||
((two + 1, a), 5.0),
|
||||
]);
|
||||
doc.keep_last_good(&values);
|
||||
let read = |doc: &Document, drawing: usize, id| doc.drawings[drawing].dimension(id).unwrap().last_good;
|
||||
assert_eq!([read(&doc, 0, a), read(&doc, 0, b), read(&doc, 0, c)], [Some(80.0), Some(MAX_REAL), None]);
|
||||
assert_eq!([read(&doc, 1, a), read(&doc, 1, b), read(&doc, 1, c)], [None; 3], "another drawing's dimensions, with the same ids");
|
||||
assert!(Arc::ptr_eq(&doc.drawings[1], &before.drawings[1]), "no new value: still shared");
|
||||
assert_eq!(read(&before, 0, a), None, "the document it was copied from keeps none");
|
||||
doc.drawings.iter().for_each(|d| d.validate().unwrap());
|
||||
let kept = doc.clone();
|
||||
doc.keep_last_good(&values);
|
||||
assert!(Arc::ptr_eq(&doc.drawings[0], &kept.drawings[0]), "the same values again: still shared");
|
||||
}
|
||||
|
||||
/// Whatever a read hands over, at every boundary and over random bit patterns, the saved record is
|
||||
/// one a file can open: a design is never lost to its last good values.
|
||||
#[test]
|
||||
fn keep_last_good_never_writes_a_record_a_file_refuses() {
|
||||
let (d, [linear, ..]) = measured();
|
||||
let id = d.id;
|
||||
let mut doc = Document::new("Box");
|
||||
doc.drawings = vec![Arc::new(d)];
|
||||
let tricky = [0.0, -0.0, 5e-324, MAX_REAL, MAX_REAL * (1.0 + f64::EPSILON), -1.0, 1e12, f64::MAX, f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
|
||||
let mut bits = 0x9E37_79B9_7F4A_7C15_u64;
|
||||
let random = std::iter::repeat_with(|| {
|
||||
bits = bits.wrapping_mul(0x5851_F42D_4C95_7F2D).wrapping_add(1);
|
||||
f64::from_bits(bits)
|
||||
});
|
||||
for value in tricky.into_iter().chain(random.take(2000)) {
|
||||
doc.keep_last_good(&BTreeMap::from([((id, linear), value)]));
|
||||
let saved = doc.drawings[0].to_json().unwrap();
|
||||
let read = Drawing::from_json(&saved).unwrap_or_else(|e| panic!("{value}: {e}"));
|
||||
assert_eq!(read.dimension(linear).unwrap().last_good, doc.drawings[0].dimension(linear).unwrap().last_good, "{value}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A lost edge's leads, from its body's face names: the blend face that rounds it and a piece of that
|
||||
/// face (a later cut's), then the edges they bound, then the faces of the feature that made it, at most
|
||||
/// 16, no face twice. Another fillet's face rounds another edge, so it leads nowhere. A `~k` on the
|
||||
/// name finds the same blend; a name no blend rounds leads to its makers' faces only, as a face's
|
||||
/// does, and a name that doesn't read, or whose feature made nothing here, leads nowhere.
|
||||
#[test]
|
||||
fn lost_leads_list_the_blend_then_its_edges_then_the_makers_faces() {
|
||||
let blends = ["F5:blend:F1:+z|F1:-y", "F5:blend:F1:+z|F1:-y#[F1:+x,F7:+z]", "F6:blend:F1:+x|F1:+z"];
|
||||
let mut faces: Vec<String> = ["F1:+z", "F1:-y", "F1:+x"].into_iter().chain(blends).map(String::from).collect();
|
||||
faces.extend((0..40).map(|k| format!("F1:face:{k}")));
|
||||
let leads = Leads::new(BODY, &faces);
|
||||
// Edges 0, 1, 3 and 5 bound the blend or its piece; 2 and 4 don't.
|
||||
let edge_faces = [vec![0, 3], vec![1, 3], vec![0, 2], vec![3, 4], vec![2, 5], vec![4, 1]];
|
||||
let lead_face = |index: usize| Candidate::Face { body: BODY.into(), index };
|
||||
let lead_edge = |index: usize| Candidate::Edge { body: BODY.into(), index };
|
||||
let box_faces = |n: usize| [0, 1, 2].into_iter().chain(6..).take(n).map(lead_face);
|
||||
let mut expected = vec![lead_face(3), lead_face(4), lead_edge(0), lead_edge(1), lead_edge(3), lead_edge(5)];
|
||||
expected.extend(box_faces(10));
|
||||
let found = leads.for_edge("F1:+z|F1:-y", &edge_faces);
|
||||
assert_eq!(found, expected);
|
||||
assert_eq!(found.len(), MAX_CANDIDATES);
|
||||
assert_eq!(leads.for_edge("F1:+z|F1:-y~2", &edge_faces), expected, "a numbered edge's blend");
|
||||
let makers: Vec<Candidate> = box_faces(MAX_CANDIDATES).collect();
|
||||
assert_eq!(leads.for_edge("F1:-y|F1:+x", &edge_faces), makers, "no blend rounds it");
|
||||
assert_eq!(leads.for_face("F1:+z"), makers, "a face's");
|
||||
assert!(leads.for_edge("F1:+z|", &edge_faces).is_empty(), "a name that doesn't read");
|
||||
assert!(leads.for_edge("F9:+z|F9:-y", &edge_faces).is_empty(), "a feature that made nothing here");
|
||||
}
|
||||
|
||||
/// Quarter arcs of radius `r` round `c` in the plane z = 0, from `from` degrees, as a tessellated edge.
|
||||
fn arc_at(c: [f64; 2], r: f64, from: f64) -> (usize, Vec<Vec3>) {
|
||||
let at = |d: f64| Vec3::new(c[0] + r * (from + d).to_radians().cos(), c[1] + r * (from + d).to_radians().sin(), 0.0);
|
||||
(0, (0..=9).map(|i| at(f64::from(i) * 10.0)).collect())
|
||||
}
|
||||
|
||||
/// Pieces are one circle only when every one lies on it: another centre or radius, however near, is
|
||||
/// another circle, and a straight piece isn't a circle. Pieces too short to read a circle from alone
|
||||
/// are read together, so short arcs of one circle are it and of two circles aren't; a piece of only
|
||||
/// two points could be a chord, and no pieces are none.
|
||||
#[test]
|
||||
fn pieces_are_one_circle_only_when_they_all_lie_on_it() {
|
||||
let c = [40.0, 15.0];
|
||||
let first = arc_at(c, 6.0, 0.0);
|
||||
let found = one_circle(&[first.clone(), arc_at(c, 6.0, 180.0)]).unwrap();
|
||||
assert!(found.dist(Vec3::new(40.0, 15.0, 0.0)) < 1e-9, "{found:?}");
|
||||
assert!(one_circle(std::slice::from_ref(&first)).is_some());
|
||||
assert!(one_circle(&[first.clone(), arc_at([40.0 + 1e-9, 15.0], 6.0, 180.0)]).is_some(), "rounding is the same circle");
|
||||
assert!(one_circle(&[first.clone(), arc_at([40.0 + 1e-3, 15.0], 6.0, 180.0)]).is_none(), "another centre");
|
||||
assert!(one_circle(&[first.clone(), arc_at(c, 6.0 + 1e-3, 180.0)]).is_none(), "another radius");
|
||||
let above: Vec<Vec3> = arc_at(c, 6.0, 180.0).1.iter().map(|p| Vec3::new(p.x, p.y, 5.0)).collect();
|
||||
assert!(one_circle(&[first.clone(), (1, above)]).is_none(), "the same circle moved along its axis");
|
||||
let tilted: Vec<Vec3> = arc_at(c, 6.0, 180.0).1.iter().map(|p| Vec3::new(40.0, p.y, p.x - 40.0)).collect();
|
||||
assert!(one_circle(&[first.clone(), (1, tilted)]).is_none(), "round another axis");
|
||||
let line: Vec<Vec3> = (0..5).map(|i| Vec3::new(f64::from(i), 0.0, 0.0)).collect();
|
||||
assert!(one_circle(&[first.clone(), (1, line)]).is_none(), "a straight piece");
|
||||
assert!(one_circle(&[first.clone(), (1, first.1[..3].to_vec())]).is_some(), "too short to read, but on it");
|
||||
let off: Vec<Vec3> = first.1[..3].iter().map(|p| Vec3::new(p.x + 1e-3, p.y, p.z)).collect();
|
||||
assert!(one_circle(&[first.clone(), (1, off)]).is_none(), "too short to read, and off it");
|
||||
assert!(one_circle(&[first.clone(), (1, first.1[..2].to_vec())]).is_none(), "two points could be a chord");
|
||||
let short = |c: [f64; 2]| (1, arc_at(c, 6.0, 180.0).1[..3].to_vec());
|
||||
let three = (0, first.1[..3].to_vec());
|
||||
assert!(one_circle(&[three.clone(), short(c)]).is_some(), "short arcs of one circle, read together");
|
||||
assert!(one_circle(&[three, short([45.0, 15.0])]).is_none(), "short arcs of two circles of one radius");
|
||||
assert!(one_circle(&[]).is_none());
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
//! A title block's fields (#3875): the keys each source has, and what they read. Stateless: the
|
||||
//! drawing gives its own fields, its sheets' and its newest revision's, and the caller hands in the
|
||||
//! component a sheet documents with what the model says of it (its material and mass, worked out
|
||||
//! by the bill of materials). A key its source doesn't have is refused when a block is given or
|
||||
//! read; a value that isn't there, or is empty, shows [`MISSING`]. Sheet tables' cells (B3) read
|
||||
//! the same fields.
|
||||
//!
|
||||
//! - **component:** `name`, `part_number`, `description`, `revision` (its part properties),
|
||||
//! `material`, `mass`, and `properties.<name>` (its free-text properties, such as a finish);
|
||||
//! - **drawing:** `name`, `number` (its `part_number` property: the drawing's identification
|
||||
//! number; without one, the name of the component the sheet documents), `organization`, `author`,
|
||||
//! `project`, and `properties.<key>` (any of its properties);
|
||||
//! - **sheet:** `name`, `number`, `count`: sheets left out of the count are neither counted nor
|
||||
//! numbered, and such a sheet shows neither;
|
||||
//! - **revision:** the newest revision row's fields ([`Field`]);
|
||||
//! - **prompt:** any key; answers come with templates, so a prompt shows [`MISSING`] until then.
|
||||
|
||||
use solidcraft_geom::cell::{Content, MISSING, Part, Source};
|
||||
use solidcraft_geom::revision::Field;
|
||||
use solidcraft_geom::title_block::{CellKind, TitleBlock};
|
||||
|
||||
use super::Drawing;
|
||||
use crate::{DocError, Metadata, Result};
|
||||
|
||||
const COMPONENT: [&str; 6] = ["name", "part_number", "description", "revision", "material", "mass"];
|
||||
const DRAWING: [&str; 5] = ["name", "number", "organization", "author", "project"];
|
||||
const SHEET: [&str; 3] = ["name", "number", "count"];
|
||||
/// Before a property's name: the field reads that property.
|
||||
const PROPERTIES: &str = "properties.";
|
||||
|
||||
/// The component a sheet documents, as its fields read it.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Component<'a> {
|
||||
pub name: &'a str,
|
||||
/// Its part properties.
|
||||
pub metadata: &'a Metadata,
|
||||
/// Its free-text properties (the design's own: none).
|
||||
pub properties: Option<&'a std::collections::BTreeMap<String, String>>,
|
||||
/// What it is made of, as its bill of materials row would say: none when nothing is assigned.
|
||||
pub material: Option<&'a str>,
|
||||
/// What one of it weighs, written for the sheet: none when it can't be weighed.
|
||||
pub mass: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// Whether `source` has the field `key`.
|
||||
pub fn has_key(source: Source, key: &str) -> bool {
|
||||
let property = key.strip_prefix(PROPERTIES).is_some_and(|name| !name.is_empty());
|
||||
match source {
|
||||
Source::Component => COMPONENT.contains(&key) || property,
|
||||
Source::Drawing => DRAWING.contains(&key) || property,
|
||||
Source::Sheet => SHEET.contains(&key),
|
||||
Source::Revision => Field::ALL.iter().any(|f| f.key() == key),
|
||||
Source::Prompt => !key.is_empty(),
|
||||
Source::Row => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `block` has a text cell `name`: what a sheet's override may name.
|
||||
pub fn overridable(block: Option<&TitleBlock>, name: &str) -> bool {
|
||||
block.is_some_and(|block| block.cells.iter().any(|cell| cell.name == name && matches!(cell.kind, CellKind::Text(_))))
|
||||
}
|
||||
|
||||
/// Each field of `block` names a key its source has: what a sheet holds its block to.
|
||||
pub fn check_block(block: &TitleBlock) -> Result<()> {
|
||||
for cell in &block.cells {
|
||||
let CellKind::Text(text) = &cell.kind else { continue };
|
||||
for part in &text.content.0 {
|
||||
if let Part::Field { source, key, .. } = part
|
||||
&& !has_key(*source, key)
|
||||
{
|
||||
let source = serde_json::to_value(source).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default();
|
||||
return Err(DocError::Invalid(format!("cell `{}`: the {source} has no field `{key}`", cell.name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What a sheet's fields read: its drawing, its place among the drawing's sheets, and the component
|
||||
/// it documents.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Values<'a> {
|
||||
pub drawing: &'a Drawing,
|
||||
pub sheet: usize,
|
||||
pub component: Option<Component<'a>>,
|
||||
}
|
||||
|
||||
impl Values<'_> {
|
||||
/// `content` as its cell shows it: text as written, each field its value or [`MISSING`].
|
||||
pub fn text(&self, content: &Content) -> String {
|
||||
let part = |part: &Part| match part {
|
||||
Part::Text(text) => text.clone(),
|
||||
Part::Field { source, key, .. } => self.field(*source, key).filter(|v| !v.is_empty()).unwrap_or_else(|| MISSING.into()),
|
||||
};
|
||||
content.0.iter().map(part).collect()
|
||||
}
|
||||
|
||||
/// The field `key` of `source`, when it has a value.
|
||||
fn field(&self, source: Source, key: &str) -> Option<String> {
|
||||
let d = self.drawing;
|
||||
let property = |metadata: &Metadata| key.strip_prefix(PROPERTIES).and_then(|name| metadata.text(name)).map(str::to_string);
|
||||
match source {
|
||||
Source::Drawing => match key {
|
||||
"name" => Some(d.name.clone()),
|
||||
"number" => d.properties.part_number().or(self.component.map(|c| c.name)).map(str::to_string),
|
||||
"organization" | "author" | "project" => d.properties.text(key).map(str::to_string),
|
||||
_ => property(&d.properties),
|
||||
},
|
||||
Source::Sheet => {
|
||||
let sheet = d.sheets.get(self.sheet)?;
|
||||
if sheet.excluded.count {
|
||||
return (key == "name").then(|| sheet.name.clone());
|
||||
}
|
||||
let counted = |s: &&std::sync::Arc<super::Sheet>| !s.excluded.count;
|
||||
match key {
|
||||
"name" => Some(sheet.name.clone()),
|
||||
"number" => Some((d.sheets.iter().take(self.sheet).filter(counted).count() + 1).to_string()),
|
||||
"count" => Some(d.sheets.iter().filter(counted).count().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Source::Revision => {
|
||||
// The newest row in the sheet's scope: the drawing's, or the sheet's own once rows are per sheet.
|
||||
let scope = if d.revisions.per_sheet { Some(d.sheets.get(self.sheet)?.id) } else { None };
|
||||
d.revisions.newest(scope)?.field(key).map(str::to_string)
|
||||
}
|
||||
Source::Component => {
|
||||
let c = self.component?;
|
||||
match key {
|
||||
"name" => Some(c.name.to_string()),
|
||||
"material" => c.material.map(str::to_string),
|
||||
"mass" => c.mass.map(str::to_string),
|
||||
"part_number" | "description" | "revision" => c.metadata.text(key).map(str::to_string),
|
||||
_ => key.strip_prefix(PROPERTIES).and_then(|name| c.properties?.get(name)).cloned(),
|
||||
}
|
||||
}
|
||||
Source::Prompt | Source::Row => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "fields_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,140 +0,0 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use solidcraft_geom::cell::Format;
|
||||
use solidcraft_geom::drawing_sheet::{self, DrawingBorderProfile, Orientation, PaperSize};
|
||||
use solidcraft_geom::title_block::default_block;
|
||||
|
||||
use super::*;
|
||||
use crate::drawing::Sheet;
|
||||
use crate::ids::IdSource;
|
||||
use crate::metadata::Owner;
|
||||
|
||||
/// A drawing of three A3 sheets, each with the built-in block.
|
||||
fn drawing() -> Drawing {
|
||||
let mut ids = IdSource::seeded(3875);
|
||||
let a3 = drawing_sheet::Sheet::standard(PaperSize::A3, Orientation::Landscape);
|
||||
let mut d = Drawing::new("Bracket drawing", "en", "Sheet 1", a3, Some(DrawingBorderProfile::iso()), &BTreeSet::new(), &mut ids).unwrap();
|
||||
Arc::make_mut(&mut d.sheets[0]).title_block = Some(default_block());
|
||||
for n in 2..=3 {
|
||||
let id = d.allocate_id(&mut ids).unwrap();
|
||||
d.sheets.push(Arc::new(Sheet { id, name: format!("Sheet {n}"), ..(*d.sheets[0]).clone() }));
|
||||
}
|
||||
d.validate().unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn field(source: Source, key: &str) -> Part {
|
||||
Part::Field { source, key: key.into(), format: Format::default() }
|
||||
}
|
||||
|
||||
fn shown(d: &Drawing, sheet: usize, component: Option<Component>, parts: Vec<Part>) -> String {
|
||||
Values { drawing: d, sheet, component }.text(&Content(parts))
|
||||
}
|
||||
|
||||
/// The drawing's own fields: its name, its number (its part number property), its organization
|
||||
/// and any property; a property taken away shows nothing, never the value it had.
|
||||
#[test]
|
||||
fn the_drawings_fields() {
|
||||
let mut d = drawing();
|
||||
let properties = Arc::make_mut(&mut d.properties);
|
||||
properties.set(Owner::Design, "part_number", json!("DRW-0042")).unwrap();
|
||||
properties.set(Owner::Design, "organization", json!("Tangency Labs")).unwrap();
|
||||
let parts = vec![
|
||||
field(Source::Drawing, "name"),
|
||||
Part::Text(" / ".into()),
|
||||
field(Source::Drawing, "number"),
|
||||
Part::Text(" / ".into()),
|
||||
field(Source::Drawing, "organization"),
|
||||
];
|
||||
assert_eq!(shown(&d, 0, None, parts.clone()), "Bracket drawing / DRW-0042 / Tangency Labs");
|
||||
Arc::make_mut(&mut d.properties).remove("organization");
|
||||
assert_eq!(shown(&d, 0, None, parts), "Bracket drawing / DRW-0042 / ");
|
||||
assert_eq!(shown(&d, 0, None, vec![field(Source::Drawing, "properties.acme.finish")]), "");
|
||||
// Without a number of its own, the drawing is numbered by the component its sheet documents,
|
||||
// by name; without either, nothing.
|
||||
let metadata = Metadata::default();
|
||||
let bracket = Component { name: "Bracket", metadata: &metadata, properties: None, material: None, mass: None };
|
||||
let number = || vec![field(Source::Drawing, "number")];
|
||||
assert_eq!(shown(&d, 0, Some(bracket), number()), "DRW-0042", "its own number first");
|
||||
Arc::make_mut(&mut d.properties).set(Owner::Design, "part_number", json!("")).unwrap();
|
||||
assert_eq!(shown(&d, 0, Some(bracket), number()), "Bracket");
|
||||
assert_eq!(shown(&d, 0, None, number()), "");
|
||||
}
|
||||
|
||||
/// Sheets are numbered among the counted ones: the second of three is "2/3". A sheet left out of
|
||||
/// the count shows neither, and the sheets after it close up.
|
||||
#[test]
|
||||
fn sheets_are_numbered_among_the_counted_ones() {
|
||||
let mut d = drawing();
|
||||
let sheet = || vec![field(Source::Sheet, "number"), Part::Text("/".into()), field(Source::Sheet, "count")];
|
||||
assert_eq!(shown(&d, 1, None, sheet()), "2/3");
|
||||
Arc::make_mut(&mut d.sheets[1]).excluded.count = true;
|
||||
assert_eq!([0, 1, 2].map(|at| shown(&d, at, None, sheet())), ["1/2", "/", "2/2"]);
|
||||
assert_eq!(shown(&d, 1, None, vec![field(Source::Sheet, "name")]), "Sheet 2");
|
||||
}
|
||||
|
||||
/// The revision fields read the newest row; with none, nothing.
|
||||
#[test]
|
||||
fn revision_fields_read_the_newest_row() {
|
||||
let mut d = drawing();
|
||||
let date = || vec![field(Source::Revision, "revision"), Part::Text(" ".into()), field(Source::Revision, "date")];
|
||||
assert_eq!(shown(&d, 0, None, date()), " ");
|
||||
let revisions = Arc::make_mut(&mut d.revisions);
|
||||
for (id, day) in [(901, "2026-10-01"), (902, "2026-10-30")] {
|
||||
revisions.add(None, id).unwrap();
|
||||
revisions.set(id, Field::Date, day).unwrap();
|
||||
}
|
||||
assert_eq!(shown(&d, 0, None, date()), "2 2026-10-30");
|
||||
}
|
||||
|
||||
/// A component's fields: its name and part properties, its free-text properties, and the material
|
||||
/// and mass the caller worked out; without a component, or a value, or with an empty one, nothing.
|
||||
#[test]
|
||||
fn the_components_fields() {
|
||||
let d = drawing();
|
||||
let mut metadata = Metadata::default();
|
||||
metadata.set(Owner::Component, "part_number", json!("BRK-7")).unwrap();
|
||||
let properties = BTreeMap::from([("finish".to_string(), "Zinc plated".to_string())]);
|
||||
let bracket = Component { name: "Bracket", metadata: &metadata, properties: Some(&properties), material: Some("Steel"), mass: Some("0.39 kg") };
|
||||
let keys = ["name", "part_number", "material", "mass", "properties.finish", "description"];
|
||||
let read = |component| keys.map(|key| shown(&d, 0, component, vec![field(Source::Component, key)]));
|
||||
assert_eq!(read(Some(bracket)), ["Bracket", "BRK-7", "Steel", "0.39 kg", "Zinc plated", ""]);
|
||||
let unweighed = Component { material: None, mass: None, properties: None, ..bracket };
|
||||
assert_eq!(read(Some(unweighed)), ["Bracket", "BRK-7", "", "", "", ""]);
|
||||
assert_eq!(read(None), [""; 6]);
|
||||
let blank = BTreeMap::from([("finish".to_string(), String::new())]);
|
||||
let finish = vec![field(Source::Component, "properties.finish")];
|
||||
assert_eq!(shown(&d, 0, Some(Component { properties: Some(&blank), ..bracket }), finish), "");
|
||||
}
|
||||
|
||||
/// A field its source doesn't have is refused when a block is given and when a drawing is read,
|
||||
/// naming the cell; so is an override of a cell the block doesn't have or that shows no text, or
|
||||
/// one with a control character. A sheet without overrides saves none.
|
||||
#[test]
|
||||
fn unknown_fields_and_overrides_are_refused() {
|
||||
let mut block = default_block();
|
||||
let CellKind::Text(text) = &mut block.cells[8].kind else { panic!("the mass cell has text") };
|
||||
text.content = Content(vec![field(Source::Component, "weight")]);
|
||||
assert_eq!(check_block(&block).unwrap_err().to_string(), "cell `mass`: the component has no field `weight`");
|
||||
let mut d = drawing();
|
||||
let saved = d.to_json().unwrap();
|
||||
assert!(!saved.contains("title_block_overrides"));
|
||||
let mut v: serde_json::Value = serde_json::from_str(&saved).unwrap();
|
||||
v["sheets"][0]["title_block"] = serde_json::to_value(&block).unwrap();
|
||||
let refusal = Drawing::from_json(&v.to_string()).unwrap_err().to_string();
|
||||
assert!(refusal.contains("cell `mass`: the component has no field `weight`"), "{refusal}");
|
||||
// Overrides: a cell's name, a cell's text.
|
||||
Arc::make_mut(&mut d.sheets[0]).title_block_overrides.insert("material".into(), "See note 3".into());
|
||||
let saved = d.to_json().unwrap();
|
||||
assert_eq!(Drawing::from_json(&saved).unwrap(), d);
|
||||
let refusals =
|
||||
[("weight", "x", "no text cell `weight`"), ("projection", "x", "no text cell `projection`"), ("material", "a\u{7}", "control character")];
|
||||
for (cell, text, why) in refusals {
|
||||
let mut wrong = d.clone();
|
||||
Arc::make_mut(&mut wrong.sheets[0]).title_block_overrides.insert(cell.into(), text.into());
|
||||
let refusal = wrong.validate().unwrap_err().to_string();
|
||||
assert!(refusal.contains(why), "{refusal}");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# Pending drawing-format changes
|
||||
|
||||
One file per change to what a drawing saves (in its design's file) that no release has shipped
|
||||
yet. Drawings (`"format": "solidcraft-drawing/N"`, #3890) are versioned by the same code as designs
|
||||
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.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
//! A balloon can name the parts list it reads (`parts_list`, #3874), saved only when it does. An older
|
||||
//! balloon names none and reads its sheet's only list, else its drawing's. The last release refuses a
|
||||
//! drawing with this format's number.
|
||||
@@ -1,5 +0,0 @@
|
||||
//! Drawings save their balloons (`balloons`, #3874 W26): each on one of the drawing's views, pointing
|
||||
//! at a placed part by a named edge of it (the edge's body, the feature that made it, its name and
|
||||
//! the occurrences the body is placed through), with its place on paper from the view's centre. An
|
||||
//! older drawing has no balloons. The last release refuses a drawing saved with them, as a newer
|
||||
//! format.
|
||||
@@ -1,3 +0,0 @@
|
||||
//! A drawing's dimension can measure between two flat faces (`faces`, #4892): each saved by its
|
||||
//! face's name, with the direction to measure along. An older drawing has none. The last release
|
||||
//! refuses a drawing saved with one, as a newer format.
|
||||