mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a06bdf5cfc | |||
| 030187452a | |||
| 198fbeb814 | |||
| 186096cbe6 | |||
| 3b982ca371 | |||
| aaa110218b | |||
| 46f80b769f | |||
| 26ca82735b | |||
| 408d30ea56 | |||
| 75261cd797 | |||
| 6f5d0038ab | |||
| 1814adb06a | |||
| 971b28c982 | |||
| f79cbc216c | |||
| 4dc1828138 | |||
| f843a3f83c | |||
| f0fd5c10b7 | |||
| 981d23d1d2 | |||
| 01648da72d | |||
| 3d3dd27439 | |||
| 6c60f1a9ed |
@@ -33,9 +33,11 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
|
||||
and calls through its function pointers), two functions in
|
||||
`apps/solidcraft/src/open_documents.rs` (macOS hands Finder files only to an Objective-C
|
||||
method), the Windows-only `apps/solidcraft/src/native_crash.rs` (the exception filter that
|
||||
records crashes inside native code such as graphics drivers) and the Windows-only
|
||||
records crashes inside native code such as graphics drivers), the Windows-only
|
||||
`apps/solidcraft/src/native_frame.rs` (the window procedure that answers the resize-border hit
|
||||
test of the window without OS decorations).
|
||||
test of the window without OS decorations) and the Windows-only
|
||||
`apps/solidcraft/src/space_mouse/windows_hid.rs` (asks Plug and Play which HID interfaces are 3D
|
||||
mice, so no other device is sent anything).
|
||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
||||
returns an error ("not supported yet: …"); it never panics.
|
||||
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
|
||||
|
||||
@@ -196,6 +196,11 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/ui/design/svg/flat/redo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/save.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_keyboard.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_monitor.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_mouse.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_palette.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_plug.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/spin.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/style.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/undo.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
@@ -336,6 +341,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/ui/design/svg/flat/send.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/stop.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/shield.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
|
||||
Generated
+18
-17
@@ -4162,7 +4162,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4193,11 +4193,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4209,7 +4209,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4218,7 +4218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4235,7 +4235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"schemars",
|
||||
@@ -4254,7 +4254,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4263,7 +4263,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"naga",
|
||||
@@ -4280,7 +4280,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
@@ -4303,7 +4303,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4323,7 +4323,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -4332,7 +4332,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4346,7 +4346,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4357,14 +4357,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4373,6 +4373,7 @@ dependencies = [
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"pollster 0.4.0",
|
||||
"proc-macro2",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4391,7 +4392,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -5921,7 +5922,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"serde_json",
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -70,6 +70,11 @@ pointer movement orbits without a mouse button, and vertical scrolling zooms. Ot
|
||||
retain their saved two-finger scroll preference; camera motion is available through
|
||||
`view.navigate` and modifier-aware `ui.move` automation.
|
||||
|
||||
UI redesign (#2016): the File/start view (#2027) uses shared light/dark navigation,
|
||||
action tiles, recent rows and sample cards, with recovery review and Save As/Export
|
||||
entry points. Document loading, sample caches and recovery safety retain their
|
||||
existing command paths.
|
||||
|
||||
M11 precursor: File → Export Current View as DXF exports shown placed bodies and sketches from the current
|
||||
camera as a 2D millimetre drawing, with hidden edges omitted or dashed. Vector occlusion splits
|
||||
partly hidden edges and includes curved silhouettes. Repeated part meshes and triangle
|
||||
|
||||
@@ -50,7 +50,11 @@ hidapi = { version = "2.6.7", default-features = false, features = ["windows-nat
|
||||
raw-window-handle = "0.6"
|
||||
# Native crash capture (src/native_crash.rs): the exception filter, the faulting module, a minidump.
|
||||
# The window's resize border and DPI size (src/native_frame.rs): the window procedure, hit test, DPI.
|
||||
# Finding 3D mice through Plug and Play without asking other devices anything (src/space_mouse/windows_hid.rs).
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Devices_DeviceAndDriverInstallation",
|
||||
"Win32_Devices_HumanInterfaceDevice",
|
||||
"Win32_Devices_Properties",
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Security",
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
mod hidraw;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod spacenavd;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_hid;
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use std::ffi::CString;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::sync::atomic::AtomicU8;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -15,8 +19,10 @@ use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use egui::Context;
|
||||
#[cfg(target_os = "macos")]
|
||||
use hidapi::DeviceInfo;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use hidapi::{DeviceInfo, HidApi};
|
||||
use hidapi::HidApi;
|
||||
use quick_xml::Reader;
|
||||
use quick_xml::events::Event;
|
||||
use solidcraft_space_mouse::Layout;
|
||||
@@ -256,8 +262,9 @@ fn at_rest(calibration: Option<Calibration>) -> Axes {
|
||||
}
|
||||
|
||||
/// The Generic Desktop "Multi-Axis Controller" usage names a 3D mouse's motion interface, whoever
|
||||
/// made it, and excludes ordinary mice and the keyboard interfaces of 3D mice.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
/// made it, and excludes ordinary mice and the keyboard interfaces of 3D mice. (Windows asks Plug
|
||||
/// and Play for the same usage: `windows_hid`.)
|
||||
#[cfg(target_os = "macos")]
|
||||
fn is_3d_mouse(info: &DeviceInfo) -> bool {
|
||||
info.usage_page() == 0x01 && info.usage() == 0x08
|
||||
}
|
||||
@@ -317,18 +324,48 @@ fn reset(ctx: &Context, shared: &Mutex<Reading>) {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// The HID interfaces that are 3D mice, by path. Windows asks Plug and Play, which sends nothing to
|
||||
/// the devices: hidapi's enumeration would ask every keyboard and mouse for its strings on each scan
|
||||
/// (#2614, `windows_hid`).
|
||||
#[cfg(target_os = "windows")]
|
||||
fn candidates(_api: &mut HidApi) -> Vec<CString> {
|
||||
windows_hid::multi_axis_controllers()
|
||||
}
|
||||
|
||||
/// The HID interfaces that are 3D mice, by path, from hidapi's enumeration (IOKit's registry).
|
||||
#[cfg(target_os = "macos")]
|
||||
fn candidates(api: &mut HidApi) -> Vec<CString> {
|
||||
if let Err(error) = api.refresh_devices() {
|
||||
log::warn!("3D mouse: device scan failed: {error}");
|
||||
return Vec::new();
|
||||
}
|
||||
api.device_list().filter(|info| is_3d_mouse(info)).map(|info| info.path().to_owned()).collect()
|
||||
}
|
||||
|
||||
/// The HID context. On Windows hidapi's own device discovery is off, so making it asks no device
|
||||
/// anything either (`candidates` finds the 3D mice). Turning discovery off panics only once a
|
||||
/// context with discovery exists; the reader is the one place the app makes a context, once.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn hid_api() -> hidapi::HidResult<HidApi> {
|
||||
HidApi::disable_device_discovery();
|
||||
HidApi::new()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn hid_api() -> hidapi::HidResult<HidApi> {
|
||||
HidApi::new()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||
let Ok(mut api) = HidApi::new() else {
|
||||
let Ok(mut api) = hid_api() else {
|
||||
log::warn!("3D mouse: HID initialization failed");
|
||||
return;
|
||||
};
|
||||
loop {
|
||||
if let Err(error) = api.refresh_devices() {
|
||||
log::warn!("3D mouse: device scan failed: {error}");
|
||||
} else if let Some((device, layout, ids)) = api.device_list().filter(|info| is_3d_mouse(info)).find_map(|info| {
|
||||
let device = info
|
||||
.open_device(&api)
|
||||
if let Some((device, layout, ids)) = candidates(&mut api).into_iter().find_map(|path| {
|
||||
let device = api
|
||||
.open_path(&path)
|
||||
.inspect_err(|error| {
|
||||
// On macOS: missing Input Monitoring permission, or the 3DxWare helper holding the device.
|
||||
// The driver having the device is the reason it cannot be opened, not a problem.
|
||||
@@ -337,6 +374,8 @@ fn reader(ctx: Context, shared: Arc<Mutex<Reading>>) {
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
// Read when the device was opened, from this device alone.
|
||||
let info = device.get_device_info().ok()?;
|
||||
let layout = device_layout(&device, info.vendor_id())?;
|
||||
Some((device, layout, (info.vendor_id(), info.product_id())))
|
||||
}) {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
//! Windows: finding 3D mice without asking any other device anything (#2614).
|
||||
//!
|
||||
//! hidapi's enumeration opens every HID interface and reads its serial number, manufacturer and
|
||||
//! product strings: on USB, three control requests to every keyboard, mouse and touchscreen. The
|
||||
//! reader repeated it every two seconds while no 3D mouse was plugged in, and on some machines the
|
||||
//! keyboard then kept dropping off the bus and the cursor jumped. Plug and Play already names each
|
||||
//! interface's top-level usage in its hardware IDs (`HID_DEVICE_UP:0001_U:0008`: Generic Desktop,
|
||||
//! Multi-Axis Controller), so the reader asks it, which sends nothing to any device, and opens only
|
||||
//! the interfaces that are 3D mice.
|
||||
//!
|
||||
//! The configuration manager has no safe API, hence the `unsafe_code` allowance on this module
|
||||
//! (the app crate denies it; see `native_crash.rs`).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::ptr::{null, null_mut};
|
||||
|
||||
use windows_sys::Win32::Devices::DeviceAndDriverInstallation::{
|
||||
CM_GET_DEVICE_INTERFACE_LIST_PRESENT, CM_Get_DevNode_PropertyW, CM_Get_Device_Interface_List_SizeW, CM_Get_Device_Interface_ListW,
|
||||
CM_Get_Device_Interface_PropertyW, CM_LOCATE_DEVNODE_NORMAL, CM_Locate_DevNodeW, CONFIGRET, CR_BUFFER_SMALL, CR_SUCCESS,
|
||||
};
|
||||
use windows_sys::Win32::Devices::HumanInterfaceDevice::GUID_DEVINTERFACE_HID;
|
||||
use windows_sys::Win32::Devices::Properties::{
|
||||
DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId, DEVPROP_TYPE_STRING, DEVPROP_TYPE_STRING_LIST, DEVPROPTYPE,
|
||||
};
|
||||
use windows_sys::Win32::Foundation::DEVPROPKEY;
|
||||
|
||||
/// The hardware ID Windows gives a top-level collection with the Generic Desktop page's
|
||||
/// Multi-Axis Controller usage.
|
||||
const MULTI_AXIS_CONTROLLER: &str = "HID_DEVICE_UP:0001_U:0008";
|
||||
/// Largest property read, in bytes: hardware IDs are a few hundred.
|
||||
const MAX_PROPERTY_BYTES: u32 = 64 * 1024;
|
||||
/// Tries at reading the interface list, which can grow between asking its size and reading it.
|
||||
const LIST_TRIES: usize = 4;
|
||||
|
||||
/// The paths of the present HID interfaces whose top-level collection is a multi-axis controller.
|
||||
pub fn multi_axis_controllers() -> Vec<CString> {
|
||||
interfaces()
|
||||
.into_iter()
|
||||
.filter(|path| {
|
||||
let Some(instance) = interface_strings(path, &DEVPKEY_Device_InstanceId).and_then(|ids| ids.into_iter().next()) else {
|
||||
return false;
|
||||
};
|
||||
devnode_strings(&instance, &DEVPKEY_Device_HardwareIds).is_some_and(|ids| is_multi_axis(&ids))
|
||||
})
|
||||
.filter_map(|path| CString::new(path).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether a device with these hardware IDs is a multi-axis controller.
|
||||
fn is_multi_axis(hardware_ids: &[String]) -> bool {
|
||||
hardware_ids.iter().any(|id| id.eq_ignore_ascii_case(MULTI_AXIS_CONTROLLER))
|
||||
}
|
||||
|
||||
/// The strings of a nul-separated list (`REG_MULTI_SZ`, or one string), without empty ones.
|
||||
fn split_multi_sz(buffer: &[u16]) -> Vec<String> {
|
||||
buffer.split(|c| *c == 0).filter(|s| !s.is_empty()).map(String::from_utf16_lossy).collect()
|
||||
}
|
||||
|
||||
/// `s` as a nul-terminated wide string.
|
||||
fn wide(s: &str) -> Vec<u16> {
|
||||
s.encode_utf16().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
/// The paths of the present HID interfaces.
|
||||
fn interfaces() -> Vec<String> {
|
||||
for _ in 0..LIST_TRIES {
|
||||
let mut len = 0u32;
|
||||
// SAFETY: `len` is a valid place for the size; the class GUID is a static; no device filter.
|
||||
let sized = unsafe { CM_Get_Device_Interface_List_SizeW(&mut len, &GUID_DEVINTERFACE_HID, null(), CM_GET_DEVICE_INTERFACE_LIST_PRESENT) };
|
||||
if sized != CR_SUCCESS || len == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut buffer = vec![0u16; len as usize];
|
||||
// SAFETY: `buffer` holds `len` wide characters, the length the call is given.
|
||||
let listed =
|
||||
unsafe { CM_Get_Device_Interface_ListW(&GUID_DEVINTERFACE_HID, null(), buffer.as_mut_ptr(), len, CM_GET_DEVICE_INTERFACE_LIST_PRESENT) };
|
||||
match listed {
|
||||
CR_SUCCESS => return split_multi_sz(&buffer),
|
||||
CR_BUFFER_SMALL => continue,
|
||||
_ => return Vec::new(),
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// A string or string-list property, read by `get` (property type, buffer, its size in bytes) in
|
||||
/// two calls: the size, then the value.
|
||||
fn property_strings(get: impl Fn(*mut DEVPROPTYPE, *mut u8, *mut u32) -> CONFIGRET) -> Option<Vec<String>> {
|
||||
let mut kind: DEVPROPTYPE = 0;
|
||||
let mut size = 0u32;
|
||||
if get(&mut kind, null_mut(), &mut size) != CR_BUFFER_SMALL || size == 0 || size > MAX_PROPERTY_BYTES {
|
||||
return None;
|
||||
}
|
||||
let mut buffer = vec![0u16; (size as usize).div_ceil(2)];
|
||||
let mut size = (buffer.len() * 2) as u32;
|
||||
if get(&mut kind, buffer.as_mut_ptr().cast(), &mut size) != CR_SUCCESS || (kind != DEVPROP_TYPE_STRING && kind != DEVPROP_TYPE_STRING_LIST) {
|
||||
return None;
|
||||
}
|
||||
Some(split_multi_sz(&buffer))
|
||||
}
|
||||
|
||||
/// A string property of the device interface at `path`.
|
||||
fn interface_strings(path: &str, key: &DEVPROPKEY) -> Option<Vec<String>> {
|
||||
let path = wide(path);
|
||||
property_strings(|kind, buffer, size| {
|
||||
// SAFETY: `path` is nul-terminated and outlives the call; `buffer` is null (asking the size)
|
||||
// or holds `*size` bytes, as `property_strings` sets up.
|
||||
unsafe { CM_Get_Device_Interface_PropertyW(path.as_ptr(), key, kind, buffer, size, 0) }
|
||||
})
|
||||
}
|
||||
|
||||
/// A string property of the device with instance ID `instance`.
|
||||
fn devnode_strings(instance: &str, key: &DEVPROPKEY) -> Option<Vec<String>> {
|
||||
let instance = wide(instance);
|
||||
let mut devnode = 0u32;
|
||||
// SAFETY: `instance` is nul-terminated and outlives the call; `devnode` is a valid place for the result.
|
||||
if unsafe { CM_Locate_DevNodeW(&mut devnode, instance.as_ptr(), CM_LOCATE_DEVNODE_NORMAL) } != CR_SUCCESS {
|
||||
return None;
|
||||
}
|
||||
property_strings(|kind, buffer, size| {
|
||||
// SAFETY: `buffer` is null (asking the size) or holds `*size` bytes, as `property_strings` sets up.
|
||||
unsafe { CM_Get_DevNode_PropertyW(devnode, key, kind, buffer, size, 0) }
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The hardware IDs Windows gives a wired 3Dconnexion device's motion interface, and a
|
||||
/// keyboard's: only the first is a multi-axis controller.
|
||||
#[test]
|
||||
fn only_the_multi_axis_usage_is_a_3d_mouse() {
|
||||
let ids = |list: &[&str]| list.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
let space_mouse = ids(&["HID\\VID_256F&PID_C635&REV_0437&MI_00", "HID\\VID_256F&UP:0001_U:0008", "HID_DEVICE_UP:0001_U:0008", "HID_DEVICE"]);
|
||||
let keyboard = ids(&[
|
||||
"HID\\VID_046D&PID_C31C&REV_6400&MI_00",
|
||||
"HID\\VID_046D&UP:0001_U:0006",
|
||||
"HID_DEVICE_SYSTEM_KEYBOARD",
|
||||
"HID_DEVICE_UP:0001_U:0006",
|
||||
"HID_DEVICE",
|
||||
]);
|
||||
let vendor = ids(&["HID\\VID_256F&UP:FF00_U:0001", "HID_DEVICE_UP:FF00_U:0001", "HID_DEVICE_UPR:FF00-FFFF", "HID_DEVICE"]);
|
||||
assert!(is_multi_axis(&space_mouse));
|
||||
assert!(is_multi_axis(&ids(&["hid_device_up:0001_u:0008"])), "IDs compare without regard to case");
|
||||
assert!(!is_multi_axis(&keyboard) && !is_multi_axis(&vendor) && !is_multi_axis(&[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nul_separated_lists_split_into_their_strings() {
|
||||
let buffer: Vec<u16> = "a\\b\0HID_DEVICE\0\0".encode_utf16().collect();
|
||||
assert_eq!(split_multi_sz(&buffer), ["a\\b", "HID_DEVICE"]);
|
||||
assert_eq!(split_multi_sz(&wide("one")), ["one"]);
|
||||
assert!(split_multi_sz(&[0, 0]).is_empty() && split_multi_sz(&[]).is_empty());
|
||||
}
|
||||
|
||||
/// The scan asks Plug and Play only: on any machine it returns, and every path it returns is a
|
||||
/// present HID interface whose hardware IDs name the multi-axis usage.
|
||||
#[test]
|
||||
fn the_scan_returns_only_multi_axis_interfaces() {
|
||||
let found = multi_axis_controllers();
|
||||
let all = interfaces();
|
||||
for path in &found {
|
||||
let path = path.to_str().unwrap_or_default();
|
||||
assert!(all.iter().any(|p| p == path), "{path}");
|
||||
let instance = interface_strings(path, &DEVPKEY_Device_InstanceId).and_then(|ids| ids.into_iter().next()).unwrap_or_default();
|
||||
assert!(devnode_strings(&instance, &DEVPKEY_Device_HardwareIds).is_some_and(|ids| is_multi_axis(&ids)), "{path}");
|
||||
}
|
||||
eprintln!("{} HID interfaces present, {} multi-axis controllers", all.len(), found.len());
|
||||
}
|
||||
}
|
||||
@@ -239,6 +239,7 @@ split_face false
|
||||
spun false
|
||||
star false
|
||||
status_error false
|
||||
status_recovery false
|
||||
style false
|
||||
suppress false
|
||||
surface_blend false
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Keyboard shortcuts</title>
|
||||
<path d="M13 16L51 16A5 5 0 0 1 56 21L56 43A5 5 0 0 1 51 48L13 48A5 5 0 0 1 8 43L8 21A5 5 0 0 1 13 16Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="20" cy="27" r="2.8" fill="currentColor"/>
|
||||
<circle cx="32" cy="27" r="2.8" fill="currentColor"/>
|
||||
<circle cx="44" cy="27" r="2.8" fill="currentColor"/>
|
||||
<path d="M20 39L44 39" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 607 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Graphics display</title>
|
||||
<path d="M13 10L51 10A5 5 0 0 1 56 15L56 37A5 5 0 0 1 51 42L13 42A5 5 0 0 1 8 37L8 15A5 5 0 0 1 13 10ZM32 42L32 54M24 54L40 54" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Mouse navigation</title>
|
||||
<path d="M32 8A13 13 0 0 1 45 21L45 43A13 13 0 0 1 19 43L19 21A13 13 0 0 1 32 8ZM32 8L32 24" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 299 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Interface palette</title>
|
||||
<path d="M32 8C18.75 8 8 18.75 8 32C8 45.25 18.75 56 32 56C37 56 39 52.5 36.5 49C34 45.5 36 42 40 42L45 42C51.1 42 56 37.1 56 31C56 19 45 8 32 8Z" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="21" cy="29" r="2.8" fill="currentColor"/>
|
||||
<circle cx="29" cy="20" r="2.8" fill="currentColor"/>
|
||||
<circle cx="41" cy="23" r="2.8" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 522 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Plugins</title>
|
||||
<path d="M24 8L24 21M40 8L40 21M16 21L48 21L48 29A16 16 0 0 1 16 29L16 21ZM32 45L32 56" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Recover unsaved design</title>
|
||||
<circle cx="32" cy="33" r="22" fill="#b9ddf8"/>
|
||||
<path d="M10 33A22 22 0 1 0 16.44 17.44L10 24" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M10 12L10 24L22 24" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M32 21L32 33L41 39" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 563 B |
@@ -99,14 +99,23 @@ pub(crate) fn breadcrumb(p: &egui::Painter, c: egui::Pos2, col: Color32) {
|
||||
}
|
||||
|
||||
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
|
||||
let s = 20.0 / 412.217;
|
||||
brand_mark_styled(p, c, 20.0, crate::theme::Tokens::get().logo, true);
|
||||
}
|
||||
|
||||
/// The same original mark as a single quiet silhouette, without face seams or outlines.
|
||||
pub(crate) fn brand_mark_silhouette(p: &egui::Painter, c: Pos2, height: f32, ink: Color32) {
|
||||
brand_mark_styled(p, c, height, [ink; 4], false);
|
||||
}
|
||||
|
||||
fn brand_mark_styled(p: &egui::Painter, c: Pos2, height: f32, colours: [Color32; 4], edges: bool) {
|
||||
let s = height / 412.217;
|
||||
let m = |pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
|
||||
// egui only fills convex shapes, so each face is a few overlapping convex pieces.
|
||||
let face = |col: Color32, pts: &[(f32, f32)]| {
|
||||
p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE));
|
||||
};
|
||||
// The faces are separated by gaps that show the application bar, not by white lines.
|
||||
let [light, mid, dark, _] = crate::theme::Tokens::get().logo;
|
||||
let [light, mid, dark, _] = colours;
|
||||
let edge_col = crate::theme::Tokens::get().app_bar;
|
||||
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
|
||||
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
|
||||
@@ -117,6 +126,9 @@ pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
|
||||
face(dark, &[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
|
||||
face(dark, &[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)]);
|
||||
face(dark, &[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)]);
|
||||
if !edges {
|
||||
return;
|
||||
}
|
||||
// White edge lines.
|
||||
let edge = Stroke::new(10.0 * s, edge_col);
|
||||
let line = |pts: &[(f32, f32)]| {
|
||||
|
||||
+42
-4
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Licence MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licence třetích stran",
|
||||
"home.back": "Zpět na {name}",
|
||||
"home.crashes": "Hlášení o pádech",
|
||||
"home.design_workspace": "Návrh",
|
||||
"home.documentation": "Dokumentace",
|
||||
"home.export": "Exportovat",
|
||||
"home.export_hint": "Vyberte formát pro aktuální návrh.",
|
||||
"home.export_json_hint": "Upravitelný návrh a historie příkazů",
|
||||
"home.export_mesh_hint": "Síť pro tisk nebo sdílení",
|
||||
"home.export_step_hint": "Přesná geometrie tělesa",
|
||||
"home.import": "Importovat…",
|
||||
"home.import_hint": "STEP, IGES nebo síť do nového návrhu",
|
||||
"home.in_this_browser": "v tomto prohlížeči",
|
||||
"home.missing": "chybí",
|
||||
"home.month_01": "led",
|
||||
"home.month_02": "úno",
|
||||
"home.month_03": "bře",
|
||||
"home.month_04": "dub",
|
||||
"home.month_05": "kvě",
|
||||
"home.month_06": "čer",
|
||||
"home.month_07": "čvc",
|
||||
"home.month_08": "srp",
|
||||
"home.month_09": "zář",
|
||||
"home.month_10": "říj",
|
||||
"home.month_11": "lis",
|
||||
"home.month_12": "pro",
|
||||
"home.new_inches": "Nový návrh v palcích",
|
||||
"home.new_inches_hint": "Palce · otevře se v pracovním prostoru Návrh",
|
||||
"home.new_mm": "Nový návrh",
|
||||
"home.new_mm_hint": "Milimetry · otevře se v pracovním prostoru Návrh",
|
||||
"home.open": "Otevřít…",
|
||||
"home.opened_days_ago": "Otevřeno před {count} dny",
|
||||
"home.opened_hours_ago": "Otevřeno před {count} h",
|
||||
"home.opened_just_now": "Otevřeno právě teď",
|
||||
"home.opened_minutes_ago": "Otevřeno před {count} min",
|
||||
"home.open_hint": "Návrh ve formátu .solidcraft ({shortcut})",
|
||||
"home.open_now": "Otevřít nyní",
|
||||
"home.opened_days_ago": "Před {count} dny",
|
||||
"home.opened_hours_ago": "Před {count} h",
|
||||
"home.opened_just_now": "Právě teď",
|
||||
"home.opened_minutes_ago": "Před {count} min",
|
||||
"home.opening": "otevírání…",
|
||||
"home.properties": "Vlastnosti návrhu",
|
||||
"home.recent": "Nedávné",
|
||||
"home.recent_empty": "Zde se zobrazí návrhy, které otevřete nebo uložíte.",
|
||||
"home.recovery_count": "Návrhy k obnovení: {count}",
|
||||
"home.recovery_empty": "Žádné návrhy k obnovení.",
|
||||
"home.recovery_hint": "SolidCraft se zavřel před uložením těchto návrhů.",
|
||||
"home.report_issue": "Nahlásit problém",
|
||||
"home.samples": "Ukázky",
|
||||
"home.settings": "Nastavení",
|
||||
"home.short_date": "{day}. {month}",
|
||||
"home.shortcuts": "Klávesové zkratky",
|
||||
"home.start_hint": "Vytvořte návrh, otevřete nedávný soubor nebo prozkoumejte ukázku.",
|
||||
"home.whats_new": "Co je nového",
|
||||
"home.yesterday": "Včera",
|
||||
"install.button_tip": "Nainstalovat SolidCraft jako aplikaci: otevře se ve vlastním okně a funguje i offline",
|
||||
"install.dock_1": "Zvolte Soubor › Přidat do Docku.",
|
||||
"install.dock_2": "Klikněte na Přidat.",
|
||||
|
||||
+42
-4
@@ -857,19 +857,57 @@
|
||||
"help.mit": "MIT-licens",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Tredjepartslicenser",
|
||||
"home.back": "Tilbage til {name}",
|
||||
"home.crashes": "Nedbrudsrapporter",
|
||||
"home.design_workspace": "Design",
|
||||
"home.documentation": "Dokumentation",
|
||||
"home.export": "Eksportér",
|
||||
"home.export_hint": "Vælg et format til det aktuelle design.",
|
||||
"home.export_json_hint": "Redigerbart design og kommandohistorik",
|
||||
"home.export_mesh_hint": "Mesh til udskrivning eller deling",
|
||||
"home.export_step_hint": "Præcis geometri for massive legemer",
|
||||
"home.import": "Importér…",
|
||||
"home.import_hint": "STEP, IGES eller mesh til et nyt design",
|
||||
"home.in_this_browser": "i denne webbrowser",
|
||||
"home.missing": "mangler",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mar.",
|
||||
"home.month_04": "apr.",
|
||||
"home.month_05": "maj",
|
||||
"home.month_06": "jun.",
|
||||
"home.month_07": "jul.",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "sep.",
|
||||
"home.month_10": "okt.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Nyt design i tommer",
|
||||
"home.new_inches_hint": "Tommer · åbner i Design",
|
||||
"home.new_mm": "Nyt design",
|
||||
"home.new_mm_hint": "Millimeter · åbner i Design",
|
||||
"home.open": "Åbn…",
|
||||
"home.opened_days_ago": "Åbnet for {count} dage siden",
|
||||
"home.opened_hours_ago": "Åbnet for {count} t siden",
|
||||
"home.opened_just_now": "Netop åbnet",
|
||||
"home.opened_minutes_ago": "Åbnet for {count} min siden",
|
||||
"home.open_hint": "Et .solidcraft-design ({shortcut})",
|
||||
"home.open_now": "Åbn nu",
|
||||
"home.opened_days_ago": "For {count} dage siden",
|
||||
"home.opened_hours_ago": "For {count} t siden",
|
||||
"home.opened_just_now": "Lige nu",
|
||||
"home.opened_minutes_ago": "For {count} min siden",
|
||||
"home.opening": "åbner…",
|
||||
"home.properties": "Designegenskaber",
|
||||
"home.recent": "Seneste",
|
||||
"home.recent_empty": "Design, du åbner eller gemmer, vises her.",
|
||||
"home.recovery_count": "Design til gendannelse: {count}",
|
||||
"home.recovery_empty": "Ingen design kan gendannes.",
|
||||
"home.recovery_hint": "SolidCraft lukkede, før disse design blev gemt.",
|
||||
"home.report_issue": "Rapportér et problem",
|
||||
"home.samples": "Eksempler",
|
||||
"home.settings": "Indstillinger",
|
||||
"home.short_date": "{day}. {month}",
|
||||
"home.shortcuts": "Tastaturgenveje",
|
||||
"home.start_hint": "Opret et design, åbn en nylig fil, eller udforsk et eksempel.",
|
||||
"home.whats_new": "Nyheder",
|
||||
"home.yesterday": "I går",
|
||||
"install.button_tip": "Installér SolidCraft som en app: Den åbner i sit eget vindue og virker offline",
|
||||
"install.dock_1": "Vælg Arkiv › Føj til Dock.",
|
||||
"install.dock_2": "Klik på Tilføj.",
|
||||
|
||||
+156
-4
@@ -14,11 +14,15 @@
|
||||
"Active": "Aktiv",
|
||||
"Active rule": "Aktive Regel",
|
||||
"Add": "Hinzufügen",
|
||||
"Add Column": "Spalte hinzufügen",
|
||||
"Add Key": "Schlüssel hinzufügen",
|
||||
"Add Mid Radius": "Mittleren Radius hinzufügen",
|
||||
"Add Row": "Zeile hinzufügen",
|
||||
"Add a tool": "Werkzeug hinzufügen",
|
||||
"Add condition": "Bedingung hinzufügen",
|
||||
"Add or edit rule": "Regel hinzufügen oder bearbeiten",
|
||||
"Add to New Folder": "Zu neuem Ordner hinzufügen",
|
||||
"Advanced": "Erweitert",
|
||||
"Against": "Gegen",
|
||||
"Alignment": "Ausrichtung",
|
||||
"All Sketch Points": "Alle Skizzenpunkte",
|
||||
@@ -31,6 +35,7 @@
|
||||
"Appearance name": "Name des Erscheinungsbilds",
|
||||
"Applied Radius": "Angewendeter Radius",
|
||||
"Apply To": "Anwenden auf",
|
||||
"Apply library appearance": "Erscheinungsbild aus der Bibliothek anwenden",
|
||||
"Apply to selection": "Auf Auswahl anwenden",
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Wenden Sie zuerst Ihr Erscheinungsbild an. Exportiert zugewiesene Erscheinungsbilder und in diesem Design erstellte Materialien.",
|
||||
"Arc length": "Bogenlänge",
|
||||
@@ -41,13 +46,18 @@
|
||||
"BLEND SURFACE": "ÜBERGANGSFLÄCHE",
|
||||
"BOUNDARY SURFACE": "BERANDUNGSFLÄCHE",
|
||||
"BROWSER": "BROWSER",
|
||||
"Bend Edges": "Biegekanten",
|
||||
"Bend Line Position": "Position der Biegelinie",
|
||||
"Bend Position": "Biegeposition",
|
||||
"Bend Radius": "Biegeradius",
|
||||
"Bend Table": "Biegetabelle",
|
||||
"Bends": "Biegungen",
|
||||
"Bodies": "Körper",
|
||||
"Bodies (optional)": "Körper (optional)",
|
||||
"Body": "Körper",
|
||||
"Body (last if none)": "Körper (letzter, wenn keiner)",
|
||||
"Body to split": "Zu teilender Körper",
|
||||
"Body, or faces to take out": "Körper oder zu entfernende Flächen",
|
||||
"Boundary": "Begrenzung",
|
||||
"Bounding box": "Hüllquader",
|
||||
"CHECK": "PRÜFEN",
|
||||
@@ -59,7 +69,9 @@
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV or JSON: updates existing parameters, adds new ones",
|
||||
"Capture All Positions": "Alle Positionen erfassen",
|
||||
"Capture Pos.": "Pos. erfassen",
|
||||
"Catch Depth": "Fangtiefe",
|
||||
"Catch Direction": "Fangrichtung",
|
||||
"Catch Length": "Fanglänge",
|
||||
"Cells": "Zellen",
|
||||
"Center X": "Mittelpunkt X",
|
||||
"Center Y": "Mittelpunkt Y",
|
||||
@@ -68,15 +80,21 @@
|
||||
"Chamfer Type": "Abschrägungstyp",
|
||||
"Check collisions": "Kollisionen prüfen",
|
||||
"Checked": "Geprüft",
|
||||
"Choose Variable radius or Spline in Type.": "Unter „Typ“ „Variable radius“ oder „Spline“ wählen.",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Wählen Sie eine neue Ebene. Skizzengeometrie und Bemaßungen bleiben in ihren zweidimensionalen Koordinaten unverändert.",
|
||||
"Choose…": "Auswählen…",
|
||||
"Circular": "Kreisförmig",
|
||||
"Circumference": "Umfang",
|
||||
"Close draft": "Entwurf schließen",
|
||||
"Collapse": "Einklappen",
|
||||
"Collisions": "Kollisionen",
|
||||
"Colour": "Farbe",
|
||||
"Command": "Befehl",
|
||||
"Comment": "Kommentar",
|
||||
"Component 1": "Komponente 1",
|
||||
"Component 2": "Komponente 2",
|
||||
"Component1": "Komponente1",
|
||||
"Components": "Komponenten",
|
||||
"Config Table": "Konfigurationstabelle",
|
||||
"Configuration": "Konfiguration",
|
||||
"Configuration rules": "Configuration rules",
|
||||
@@ -84,6 +102,7 @@
|
||||
"Contact Sets": "Kontaktsätze",
|
||||
"Contact sets": "Kontaktsätze",
|
||||
"Continuity": "Stetigkeit",
|
||||
"Control Radius": "Kontrollradius",
|
||||
"Copy Error Message": "Fehlermeldung kopieren",
|
||||
"Copy Warning Message": "Warnmeldung kopieren",
|
||||
"Copy all licences": "Alle Lizenzen kopieren",
|
||||
@@ -93,8 +112,11 @@
|
||||
"Corner Relief": "Eckenentlastung",
|
||||
"Corner Relief Shape": "Form der Eckenentlastung",
|
||||
"Counterbore depth": "Tiefe der Senkung",
|
||||
"Counterbore Ø": "Tiefenbohrung Ø",
|
||||
"Countersink angle": "Winkel der Kegelsenkung",
|
||||
"Countersink Ø": "Senkung Ø",
|
||||
"Create Issue — review draft": "Issue erstellen — Entwurf prüfen",
|
||||
"Create Issue…": "Issue erstellen…",
|
||||
"Cross curves": "Querkurven",
|
||||
"Cube": "Würfel",
|
||||
"Current orthographic view (1:1 mm)": "Aktuelle orthogonale Ansicht (1:1 mm)",
|
||||
@@ -103,7 +125,11 @@
|
||||
"Curvature (G2)": "Krümmung (G2)",
|
||||
"Curvature change (G3)": "Krümmungsänderung (G3)",
|
||||
"Customize Toolbar": "Symbolleiste anpassen",
|
||||
"Customize…": "Anpassen…",
|
||||
"Cylindrical face": "Zylindrische Fläche",
|
||||
"Delete": "Löschen",
|
||||
"Delete Selected Features…": "Ausgewählte Merkmale löschen…",
|
||||
"Delete Selected Folders": "Ausgewählte Ordner löschen",
|
||||
"Delete all": "Alle löschen",
|
||||
"Delete rule": "Regel löschen",
|
||||
"Delete the row": "Zeile löschen",
|
||||
@@ -112,8 +138,11 @@
|
||||
"Dependents": "Abhängige Elemente",
|
||||
"Designs you open or save appear here.": "Designs, die Sie öffnen oder speichern, erscheinen hier.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Bestimmt durch Bis Objekt. Wählen Sie Winkel, um den Winkelbereich zu bearbeiten.",
|
||||
"Diameter": "Durchmesser",
|
||||
"Direction": "Richtung",
|
||||
"Discard": "Verwerfen",
|
||||
"Distance": "Abstand",
|
||||
"Distance 1": "Abstand 1",
|
||||
"Do not export hidden edges": "Verdeckte Kanten nicht exportieren",
|
||||
"Don't ask again": "Nicht mehr fragen",
|
||||
"Double-click to rename": "Double-click to rename",
|
||||
@@ -125,17 +154,21 @@
|
||||
"Edit Profile Sketch": "Profile-Skizze bearbeiten",
|
||||
"Enable rule": "Regel aktivieren",
|
||||
"Enabled": "Aktiviert",
|
||||
"End Radius": "Endradius",
|
||||
"End bulge": "Endausbauchung",
|
||||
"End continuity": "Endstetigkeit",
|
||||
"Expand": "Ausklappen",
|
||||
"Explode": "Explodieren",
|
||||
"Explode Text": "Text auflösen",
|
||||
"Export Animation…": "Animation exportieren…",
|
||||
"Export Bend Table…": "Biegetabelle exportieren…",
|
||||
"Export CSV…": "CSV exportieren…",
|
||||
"Export Current View as DXF": "Aktuelle Ansicht als DXF exportieren",
|
||||
"Export Current View as DXF…": "Aktuelle Ansicht als DXF exportieren…",
|
||||
"Export DXF": "DXF exportieren",
|
||||
"Export DXF…": "DXF exportieren…",
|
||||
"Export JSON…": "JSON exportieren…",
|
||||
"Export design library...": "Designbibliothek exportieren...",
|
||||
"Export hidden edges as dashed lines": "Verdeckte Kanten gestrichelt exportieren",
|
||||
"Expression": "Ausdruck",
|
||||
"Extend to meet": "Verlängern, bis sie sich treffen",
|
||||
@@ -143,31 +176,40 @@
|
||||
"FILES": "DATEIEN",
|
||||
"FLAT": "ABWICKLUNG",
|
||||
"Face": "Fläche",
|
||||
"Face Appearance": "Flächenerscheinungsbild",
|
||||
"Face attachment lost": "Flächenanbindung verloren",
|
||||
"Faces": "Flächen",
|
||||
"Faces / edges / vertices": "Flächen / Kanten / Ecken",
|
||||
"Faces to split": "Zu teilende Flächen",
|
||||
"Fastener": "Verbindungselement",
|
||||
"Favorites": "Favoriten",
|
||||
"Favourite": "Favourite",
|
||||
"Feature": "Funktion",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Dateien aus vielen CAD-Programmen haben Y nach oben und öffnen sich hier daher auf dem Rücken liegend (Z nach oben).",
|
||||
"Filter": "Filter",
|
||||
"Filter…": "Filtern…",
|
||||
"Find in Browser": "Im Browser suchen",
|
||||
"Finish": "Oberfläche",
|
||||
"First plane": "Erste Ebene",
|
||||
"Fit and G2 corners require Constant radius.": "Passung und G2-Ecken erfordern Constant radius.",
|
||||
"Fixed Face": "Feste Fläche",
|
||||
"Flat Pattern": "Flachmuster",
|
||||
"Flat size": "Abwicklungsgröße",
|
||||
"Follow camera (Look At sketches)": "Kamera folgen (Skizzen ansehen)",
|
||||
"Follow the camera (Look At sketches)": "Der Kamera folgen (Skizzen ansehen)",
|
||||
"Font": "Schriftart",
|
||||
"From": "Von",
|
||||
"From Bodies": "Aus Körpern",
|
||||
"From edges": "Ausgangskanten",
|
||||
"Full Length": "Volle Länge",
|
||||
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2-Ecken erfordern einen konstanten Radius und Exact fit. Die Konstruktion von G2-Ecken wird noch nicht unterstützt.",
|
||||
"Gap": "Spalt",
|
||||
"Grid": "Raster",
|
||||
"HISTORY": "VERLAUF",
|
||||
"Height": "Höhe",
|
||||
"Hidden edges": "Verdeckte Kanten",
|
||||
"History layout": "Verlaufslayout",
|
||||
"Hole (optional)": "Bohrung (optional)",
|
||||
"Holes": "Löcher",
|
||||
"Holes (edges on them)": "Löcher (ihre Kanten)",
|
||||
"Home view": "Startansicht",
|
||||
@@ -178,14 +220,20 @@
|
||||
"In place (surfaces)": "An Ort und Stelle (Flächen)",
|
||||
"In plane": "In der Ebene",
|
||||
"Inside": "Innen",
|
||||
"Instant": "Sofort",
|
||||
"Interface font": "Schriftart der Oberfläche",
|
||||
"Interferes": "Überschneidet sich",
|
||||
"Invert": "Umkehren",
|
||||
"Isocurve": "Isokurve",
|
||||
"Issue title": "Issue-Titel",
|
||||
"Items": "Elemente",
|
||||
"JOINTS": "GELENKE",
|
||||
"Joint Origin1": "Gelenkursprung1",
|
||||
"Keep (points on it)": "Behalten (Punkte darauf)",
|
||||
"Keep Tools": "Werkzeuge behalten",
|
||||
"Keep of the other surface": "Vom anderen Flächenkörper behalten",
|
||||
"Key a joint's value at this step": "Gelenkwert in diesem Schritt als Schlüsselbild setzen",
|
||||
"Keys": "Schlüsselbilder",
|
||||
"LIBRARY": "BIBLIOTHEK",
|
||||
"LINKS": "VERKNÜPFUNGEN",
|
||||
"Later": "Später",
|
||||
@@ -196,9 +244,14 @@
|
||||
"Letter Spacing": "Zeichenabstand",
|
||||
"Library": "Bibliothek",
|
||||
"Library file": "Bibliotheksdatei",
|
||||
"Limits": "Grenzen",
|
||||
"Line Spacing": "Zeilenabstand",
|
||||
"Line Type": "Linientyp",
|
||||
"Listing the installed fonts…": "Installierte Schriftarten werden aufgelistet…",
|
||||
"Load library": "Bibliothek laden",
|
||||
"Local library": "Lokale Bibliothek",
|
||||
"Loop": "Schleife",
|
||||
"M5 or M5x0.5": "M5 oder M5x0.5",
|
||||
"MANAGE": "VERWALTEN",
|
||||
"MATERIALS": "MATERIALIEN",
|
||||
"MODEL": "MODELL",
|
||||
@@ -209,14 +262,20 @@
|
||||
"Matched restrictions check the final values.": "Zutreffende Einschränkungen prüfen die endgültigen Werte.",
|
||||
"Material": "Material",
|
||||
"Max": "Max",
|
||||
"Maximum": "Maximum",
|
||||
"Mesh body": "Netzkörper",
|
||||
"Mid Radius": "Mittlerer Radius",
|
||||
"Middle double-click": "Doppelklick mit mittlerer Taste",
|
||||
"Min": "Min",
|
||||
"Minimum": "Minimum",
|
||||
"Mirror Plane": "Spiegelebene",
|
||||
"Mitre": "Gehrung",
|
||||
"Model Parameters": "Modellparameter",
|
||||
"Modeled": "Modelliert",
|
||||
"More toolbar panels": "Weitere Symbolleistenbereiche",
|
||||
"Motion": "Bewegung",
|
||||
"Mouse gestures": "Mausgesten",
|
||||
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Mausgesten funktionieren nur im SolidWorks-Mausmodus (das Rad dreht, Strg + Rad verschiebt).",
|
||||
"Move / Copy": "Verschieben / Kopieren",
|
||||
"Move Type": "Verschiebungstyp",
|
||||
"Move down": "Nach unten",
|
||||
@@ -238,8 +297,11 @@
|
||||
"Not available yet": "Noch nicht verfügbar",
|
||||
"Notch": "Ausklinkung",
|
||||
"OUTPUT": "AUSGABE",
|
||||
"Of": "Freiheitsgrad",
|
||||
"On your computer": "Auf diesem Computer",
|
||||
"Opacity": "Deckkraft",
|
||||
"Open GitHub draft": "GitHub-Entwurf öffnen",
|
||||
"Open library...": "Bibliothek öffnen...",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "Öffnen Sie lokale Bibliotheken in der Desktop-App. Die Bibliothek dieses Designs können Sie hier exportieren.",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Quelloffenes parametrisches 3D-CAD in Rust. Lizenziert unter MIT OR Apache-2.0.",
|
||||
"Operator": "Operator",
|
||||
@@ -258,8 +320,10 @@
|
||||
"Pick the tool bodies": "Werkzeugkörper wählen",
|
||||
"Picked": "Ausgewählte",
|
||||
"Picked point": "Gewählter Punkt",
|
||||
"Pivot": "Drehpunkt",
|
||||
"Place at the origin instead": "Stattdessen im Ursprung platzieren",
|
||||
"Plastic Rules": "Kunststoffregeln",
|
||||
"Play the joint's motion in the view": "Die Bewegung des Gelenks in der Ansicht abspielen",
|
||||
"Playback": "Wiedergabe",
|
||||
"Plot": "Diagramm",
|
||||
"Position": "Position",
|
||||
@@ -267,14 +331,18 @@
|
||||
"Position (G0)": "Position (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "Position entlang der Kante, 0 am Anfang, 1 am anderen Ende",
|
||||
"Preferences › Design turns the offer back on": "Unter Einstellungen › Design lässt sich das Angebot wieder einschalten",
|
||||
"Preview": "Vorschau",
|
||||
"Preview Row": "Vorschauzeile",
|
||||
"Preview leaves the current design unchanged.": "Die Vorschau lässt das aktuelle Design unverändert.",
|
||||
"Profile / Edges": "Profil / Kanten",
|
||||
"Profiles or curves": "Profile oder Kurven",
|
||||
"Profiles/Curves": "Profile/Kurven",
|
||||
"Quantity": "Größe",
|
||||
"RECENT": "ZULETZT",
|
||||
"REFERENCE": "REFERENZ",
|
||||
"RULED SURFACE": "REGELFLÄCHE",
|
||||
"RULES": "REGELN",
|
||||
"Radius": "Radius",
|
||||
"Radius Fit": "Radiusanpassung",
|
||||
"Ratio": "Verhältnis",
|
||||
"Reattach selected face": "Ausgewählte Fläche neu anbinden",
|
||||
@@ -283,6 +351,8 @@
|
||||
"Rectangular": "Rechteckig",
|
||||
"Reference face": "Referenzfläche",
|
||||
"Remove Tools": "Werkzeuge entfernen",
|
||||
"Remove condition": "Bedingung entfernen",
|
||||
"Remove from Folder": "Aus Ordner entfernen",
|
||||
"Remove from list": "Aus Liste entfernen",
|
||||
"Remove override": "Überschreibung entfernen",
|
||||
"Remove private design details or paths before sharing.": "Entfernen Sie vor dem Teilen private Designdetails oder Pfade.",
|
||||
@@ -290,12 +360,16 @@
|
||||
"Remove the column": "Spalte entfernen",
|
||||
"Remove the track": "Spur entfernen",
|
||||
"Remove this point": "Diesen Punkt entfernen",
|
||||
"Rename Feature": "Merkmal umbenennen",
|
||||
"Rename Folder": "Ordner umbenennen",
|
||||
"Replace existing library file": "Vorhandene Bibliotheksdatei ersetzen",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen",
|
||||
"Reset Toolbar": "Symbolleiste zurücksetzen",
|
||||
"Resize Part": "Teil skalieren",
|
||||
"Restore View": "Ansicht wiederherstellen",
|
||||
"Restore outer boundary": "Äußere Begrenzung wiederherstellen",
|
||||
"Restore the inherited component or physical material appearance": "Das geerbte Erscheinungsbild der Komponente oder des physikalischen Materials wiederherstellen",
|
||||
"Restore this studio's tools": "Werkzeuge dieses Studios wiederherstellen",
|
||||
"Result": "Ergebnis",
|
||||
"Retake screenshot": "Screenshot erneut aufnehmen",
|
||||
"Reverse": "Umkehren",
|
||||
@@ -305,7 +379,13 @@
|
||||
"Revert Pos.": "Pos. zurücksetzen",
|
||||
"Revert Position": "Position zurücksetzen",
|
||||
"Review the report and screenshot before opening GitHub.": "Prüfen Sie Bericht und Screenshot, bevor Sie GitHub öffnen.",
|
||||
"Rib Length": "Rippenlänge",
|
||||
"Rib Offset": "Rippenversatz",
|
||||
"Rib Thickness": "Rippendicke",
|
||||
"Ribs": "Rippen",
|
||||
"Rim Edge": "Randkante",
|
||||
"Ring of tools on the right button": "Werkzeugring auf der rechten Maustaste",
|
||||
"Roll Back Here": "Hierher zurücksetzen",
|
||||
"Roll History Marker Here": "Verlaufsmarkierung hierher verschieben",
|
||||
"Round": "Rund",
|
||||
"Rule": "Rule",
|
||||
@@ -322,13 +402,18 @@
|
||||
"Search": "Suchen",
|
||||
"Search commands": "Befehle suchen",
|
||||
"Search this library": "Diese Bibliothek durchsuchen",
|
||||
"Second plane": "Zweite Ebene",
|
||||
"Section": "Schnitt",
|
||||
"Sections (one sketch each)": "Querschnitte (je eine Skizze)",
|
||||
"Select Other": "Andere auswählen",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Anderes auswählen: Halten Sie die linke Taste ruhig auf dem Modell gedrückt (oder Rechtsklick › Anderes auswählen…), um alles unter dem Cursor aufzulisten; klicken Sie erneut an dieselbe Stelle, um das nächste Element dort auszuwählen. Doppelklick mit der mittleren Taste wechselt zur Startansicht (Einstellungen › Navigation).",
|
||||
"Select bodies (a face picks its body)": "Körper auswählen (eine Fläche wählt ihren Körper)",
|
||||
"Select bodies or faces instead": "Stattdessen Körper oder Flächen auswählen",
|
||||
"Select edges": "Kanten auswählen",
|
||||
"Selection filter": "Auswahlfilter",
|
||||
"Semantic faces share their appearance across instances.": "Semantische Flächen teilen ihr Erscheinungsbild über alle Instanzen hinweg.",
|
||||
"Send your report on GitHub": "Bericht auf GitHub senden",
|
||||
"Shaded": "Schattiert",
|
||||
"Shape": "Form",
|
||||
"Sheet Rules": "Blechregeln",
|
||||
"Show Agent Cursor": "Agenten-Cursor anzeigen",
|
||||
@@ -346,6 +431,7 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft wurde beendet, bevor diese Designs gespeichert wurden. Ihr letzter Autosave kann geöffnet werden.",
|
||||
"Specialise": "Spezialisieren",
|
||||
"Speed": "Geschwindigkeit",
|
||||
"Splitting tool": "Trennwerkzeug",
|
||||
"Stand it up": "Aufrichten",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Aufrichten: eine Vierteldrehung um X, als ein Verschieben-Merkmal, das sich rückgängig machen lässt.",
|
||||
"Stand this part up?": "Dieses Teil aufrichten?",
|
||||
@@ -355,14 +441,20 @@
|
||||
"Steps": "Schritte",
|
||||
"Stitch the kept pieces": "Behaltene Teile vernähen",
|
||||
"Stitch to both bodies": "Mit beiden Körpern vernähen",
|
||||
"Studio": "Studio",
|
||||
"Study": "Untersuchung",
|
||||
"Style": "Stil",
|
||||
"Suppress": "Unterdrücken",
|
||||
"Suppressed": "Unterdrückt",
|
||||
"Surface": "Flächenkörper",
|
||||
"Surface, or faces to turn over": "Fläche oder umzudrehende Flächen",
|
||||
"Swap Ends": "Enden vertauschen",
|
||||
"TRIM": "TRIMMEN",
|
||||
"Tangent (G1)": "Tangente (G1)",
|
||||
"Tangent Chain": "Tangentenkette",
|
||||
"Tangent chain": "Tangentialkette",
|
||||
"Target": "Ziel",
|
||||
"Target Body": "Zielkörper",
|
||||
"Tear": "Reißen",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Die gestrichelte Skizze ist eine gespeicherte Ebenenführung. Wählen Sie eine neue Fläche, um die Anbindung zu reparieren.",
|
||||
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Das gespeicherte Autorvorkommen fehlt. Wählen Sie vor dem erneuten Anbinden eine gültige Autorplatzierung.",
|
||||
@@ -375,24 +467,31 @@
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "Dieses untergeordnete Element ist geteilt. Löschen entfernt es aus jeder Kopie des übergeordneten Elements.",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "Dieses untergeordnete Element ist geteilt. Positionsänderungen gelten für jede Kopie des übergeordneten Elements.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Diese Fläche kann keine dauerhafte Anbindung bieten. Wählen Sie eine andere Fläche oder verwenden Sie ausdrücklich eine feste Ebene.",
|
||||
"To": "Bis",
|
||||
"To Offset": "Bis Versatz",
|
||||
"To edges": "Zielkanten",
|
||||
"Tolerance": "Toleranz",
|
||||
"Too close": "Zu nah",
|
||||
"Tool Bodies": "Werkzeugkörper",
|
||||
"Tools": "Werkzeuge",
|
||||
"Tools, clockwise from the top": "Werkzeuge, im Uhrzeigersinn von oben",
|
||||
"Trimming tool (plane, face or curves)": "Trimmwerkzeug (Ebene, Fläche oder Kurven)",
|
||||
"Turn off section analysis before exporting the view.": "Deaktivieren Sie die Schnittanalyse, bevor Sie die Ansicht exportieren.",
|
||||
"Type": "Typ",
|
||||
"UNTRIM": "TRIMMEN AUFHEBEN",
|
||||
"Unit": "Einheit",
|
||||
"Unsuppress": "Unterdrückung aufheben",
|
||||
"Updating the preview": "Vorschau wird aktualisiert",
|
||||
"Use active author placement": "Aktive Autorplatzierung verwenden",
|
||||
"Use fixed plane": "Feste Ebene verwenden",
|
||||
"Use saved fixed plane": "Gespeicherte feste Ebene verwenden",
|
||||
"Use selected face as fixed plane": "Ausgewählte Fläche als feste Ebene verwenden",
|
||||
"Use the SolidWorks mouse mode": "SolidWorks-Mausmodus verwenden",
|
||||
"User Parameters": "Benutzerparameter",
|
||||
"VIEW": "ANSICHT",
|
||||
"VIEWS": "ANSICHTEN",
|
||||
"Value": "Value",
|
||||
"Variable Radius": "Variabler Radius",
|
||||
"Velocity": "Geschwindigkeit",
|
||||
"Vertices": "Scheitelpunkte",
|
||||
"View navigator": "Ansichtsnavigator",
|
||||
@@ -403,10 +502,18 @@
|
||||
"When": "When",
|
||||
"Width": "Breite",
|
||||
"Will fail": "Schlägt fehl",
|
||||
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "Mit eingeschalteten Mausgesten dreht die rechte Maustaste die Ansicht nicht mehr: Taste gedrückt halten und bewegen öffnet den Werkzeugring, dann die Zone eines Werkzeugs berühren. Ein einfacher Rechtsklick öffnet weiterhin das Menü.",
|
||||
"X": "X",
|
||||
"X Angle": "X-Winkel",
|
||||
"X angle": "X-Winkel",
|
||||
"X distance": "X-Abstand",
|
||||
"Y": "Y",
|
||||
"Y Angle": "Y-Winkel",
|
||||
"Y angle": "Y-Winkel",
|
||||
"Y distance": "Y-Abstand",
|
||||
"Z": "Z",
|
||||
"Z Angle": "Z-Winkel",
|
||||
"Z Distance": "Z-Abstand",
|
||||
"Z angle": "Z-Winkel",
|
||||
"Z distance": "Z-Abstand",
|
||||
"Zebra": "Zebra",
|
||||
@@ -859,19 +966,57 @@
|
||||
"help.mit": "MIT-Lizenz",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Drittanbieterlizenzen",
|
||||
"home.back": "Zurück zu {name}",
|
||||
"home.crashes": "Absturzberichte",
|
||||
"home.design_workspace": "Entwurf",
|
||||
"home.documentation": "Dokumentation",
|
||||
"home.export": "Exportieren",
|
||||
"home.export_hint": "Wählen Sie ein Format für den aktuellen Entwurf.",
|
||||
"home.export_json_hint": "Bearbeitbarer Entwurf und Befehlsverlauf",
|
||||
"home.export_mesh_hint": "Netz zum Drucken oder Teilen",
|
||||
"home.export_step_hint": "Präzise Volumengeometrie",
|
||||
"home.import": "Importieren…",
|
||||
"home.import_hint": "STEP, IGES oder Netz in einen neuen Entwurf",
|
||||
"home.in_this_browser": "in diesem Browser",
|
||||
"home.missing": "fehlt",
|
||||
"home.month_01": "Jan.",
|
||||
"home.month_02": "Feb.",
|
||||
"home.month_03": "März",
|
||||
"home.month_04": "Apr.",
|
||||
"home.month_05": "Mai",
|
||||
"home.month_06": "Juni",
|
||||
"home.month_07": "Juli",
|
||||
"home.month_08": "Aug.",
|
||||
"home.month_09": "Sept.",
|
||||
"home.month_10": "Okt.",
|
||||
"home.month_11": "Nov.",
|
||||
"home.month_12": "Dez.",
|
||||
"home.new_inches": "Neuer Entwurf in Zoll",
|
||||
"home.new_inches_hint": "Zoll · öffnet sich im Arbeitsbereich Entwurf",
|
||||
"home.new_mm": "Neuer Entwurf",
|
||||
"home.new_mm_hint": "Millimeter · öffnet sich im Arbeitsbereich Entwurf",
|
||||
"home.open": "Öffnen…",
|
||||
"home.opened_days_ago": "Vor {count} Tag(en) geöffnet",
|
||||
"home.opened_hours_ago": "Vor {count} Std. geöffnet",
|
||||
"home.opened_just_now": "Gerade eben geöffnet",
|
||||
"home.opened_minutes_ago": "Vor {count} Min. geöffnet",
|
||||
"home.open_hint": "Ein .solidcraft-Entwurf ({shortcut})",
|
||||
"home.open_now": "Jetzt öffnen",
|
||||
"home.opened_days_ago": "Vor {count} Tagen",
|
||||
"home.opened_hours_ago": "Vor {count} Std.",
|
||||
"home.opened_just_now": "Gerade eben",
|
||||
"home.opened_minutes_ago": "Vor {count} Min.",
|
||||
"home.opening": "wird geöffnet…",
|
||||
"home.properties": "Entwurfseigenschaften",
|
||||
"home.recent": "Zuletzt verwendet",
|
||||
"home.recent_empty": "Entwürfe, die Sie öffnen oder speichern, erscheinen hier.",
|
||||
"home.recovery_count": "Wiederherstellbare Entwürfe: {count}",
|
||||
"home.recovery_empty": "Keine wiederherstellbaren Entwürfe.",
|
||||
"home.recovery_hint": "SolidCraft wurde geschlossen, bevor diese Entwürfe gespeichert wurden.",
|
||||
"home.report_issue": "Problem melden",
|
||||
"home.samples": "Beispiele",
|
||||
"home.settings": "Einstellungen",
|
||||
"home.short_date": "{day}. {month}",
|
||||
"home.shortcuts": "Tastenkombinationen",
|
||||
"home.start_hint": "Erstellen Sie einen Entwurf, öffnen Sie eine zuletzt verwendete Datei oder erkunden Sie ein Beispiel.",
|
||||
"home.whats_new": "Neuigkeiten",
|
||||
"home.yesterday": "Gestern",
|
||||
"install.button_tip": "SolidCraft als App installieren: eigenes Fenster, funktioniert auch offline",
|
||||
"install.dock_1": "Wählen Sie Ablage › Zum Dock hinzufügen.",
|
||||
"install.dock_2": "Klicken Sie auf „Hinzufügen“.",
|
||||
@@ -929,8 +1074,15 @@
|
||||
"motion.to_end": "Zum Ende",
|
||||
"motion.to_start": "Zum Anfang",
|
||||
"motion.track_name": "Spur {number}",
|
||||
"nav.alias.help": "Umschalt+Alt+Linksziehen dreht, Umschalt+Alt+Mitte verschiebt, Umschalt+Alt+Rechtsziehen zoomt; mittleres Ziehen verschiebt ebenfalls und rechtes Ziehen dreht",
|
||||
"nav.blender.help": "Mittlere Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt; rechtes Ziehen lässt die Kamera unverändert",
|
||||
"nav.free_orbit.help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollneigung bei. Das Einrasten mit Umschalt ist deaktiviert.",
|
||||
"nav.free_orbit.label": "Freie Drehung",
|
||||
"nav.fusion.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, rechte Maustaste dreht",
|
||||
"nav.inventor.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, Strg+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.openscad.help": "Linksziehen auf leerem Hintergrund dreht, Umschalt+Linksziehen zieht ein Auswahlrechteck auf, Skizzengeometrie wird durch Ziehen bearbeitet; rechtes oder mittleres Ziehen verschiebt, Umschalt+rechtes oder Umschalt+mittleres Ziehen zoomt",
|
||||
"nav.solidworks.help": "Mittlere Maustaste dreht, Strg+Mitte verschiebt, Umschalt+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.tinkercad.help": "Rechte Maustaste dreht, mittlere oder Umschalt+rechte Maustaste verschiebt",
|
||||
"nav.touchpad.help": "Umschalt gedrückt halten und den Zeiger zum Verschieben bewegen; mit Alt/Option drehen. Keine Maustaste nötig. Vertikales Scrollen zoomt.",
|
||||
"nav.touchpad.label": "Touchpad",
|
||||
"nav.touchpad.orbit": "Drehen (Alt/Option halten und bewegen; ohne Maustaste)",
|
||||
|
||||
+302
-5
@@ -14,11 +14,15 @@
|
||||
"Active": "Active",
|
||||
"Active rule": "Active rule",
|
||||
"Add": "Add",
|
||||
"Add Column": "Add Column",
|
||||
"Add Key": "Add Key",
|
||||
"Add Mid Radius": "Add Mid Radius",
|
||||
"Add Row": "Add Row",
|
||||
"Add a tool": "Add a tool",
|
||||
"Add condition": "Add condition",
|
||||
"Add or edit rule": "Add or edit rule",
|
||||
"Add to New Folder": "Add to New Folder",
|
||||
"Advanced": "Advanced",
|
||||
"Against": "Against",
|
||||
"Alignment": "Alignment",
|
||||
"All Sketch Points": "All Sketch Points",
|
||||
@@ -32,6 +36,7 @@
|
||||
"Appearance name": "Appearance name",
|
||||
"Applied Radius": "Applied Radius",
|
||||
"Apply To": "Apply To",
|
||||
"Apply library appearance": "Apply library appearance",
|
||||
"Apply to selection": "Apply to selection",
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Apply your appearance first. Exports assigned appearances and materials created in this design.",
|
||||
"Arc length": "Arc length",
|
||||
@@ -42,13 +47,18 @@
|
||||
"BLEND SURFACE": "BLEND SURFACE",
|
||||
"BOUNDARY SURFACE": "BOUNDARY SURFACE",
|
||||
"BROWSER": "BROWSER",
|
||||
"Bend Edges": "Bend Edges",
|
||||
"Bend Line Position": "Bend Line Position",
|
||||
"Bend Position": "Bend Position",
|
||||
"Bend Radius": "Bend Radius",
|
||||
"Bend Table": "Bend Table",
|
||||
"Bends": "Bends",
|
||||
"Bodies": "Bodies",
|
||||
"Bodies (optional)": "Bodies (optional)",
|
||||
"Body": "Body",
|
||||
"Body (last if none)": "Body (last if none)",
|
||||
"Body to split": "Body to split",
|
||||
"Body, or faces to take out": "Body, or faces to take out",
|
||||
"Boundary": "Boundary",
|
||||
"Bounding box": "Bounding box",
|
||||
"CHECK": "CHECK",
|
||||
@@ -60,7 +70,9 @@
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV or JSON: updates existing parameters, adds new ones",
|
||||
"Capture All Positions": "Capture All Positions",
|
||||
"Capture Pos.": "Capture Pos.",
|
||||
"Catch Depth": "Catch Depth",
|
||||
"Catch Direction": "Catch Direction",
|
||||
"Catch Length": "Catch Length",
|
||||
"Cells": "Cells",
|
||||
"Center X": "Center X",
|
||||
"Center Y": "Center Y",
|
||||
@@ -69,15 +81,21 @@
|
||||
"Chamfer Type": "Chamfer Type",
|
||||
"Check collisions": "Check collisions",
|
||||
"Checked": "Checked",
|
||||
"Choose Variable radius or Spline in Type.": "Choose Variable radius or Spline in Type.",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.",
|
||||
"Choose…": "Choose…",
|
||||
"Circular": "Circular",
|
||||
"Circumference": "Circumference",
|
||||
"Close draft": "Close draft",
|
||||
"Collapse": "Collapse",
|
||||
"Collisions": "Collisions",
|
||||
"Colour": "Colour",
|
||||
"Command": "Command",
|
||||
"Comment": "Comment",
|
||||
"Component 1": "Component 1",
|
||||
"Component 2": "Component 2",
|
||||
"Component1": "Component1",
|
||||
"Components": "Components",
|
||||
"Config Table": "Config Table",
|
||||
"Configuration": "Configuration",
|
||||
"Configuration rules": "Configuration rules",
|
||||
@@ -86,6 +104,7 @@
|
||||
"Contact sets": "Contact sets",
|
||||
"Continue on GitHub": "Continue on GitHub",
|
||||
"Continuity": "Continuity",
|
||||
"Control Radius": "Control Radius",
|
||||
"Copy Error Message": "Copy Error Message",
|
||||
"Copy Warning Message": "Copy Warning Message",
|
||||
"Copy all licences": "Copy all licences",
|
||||
@@ -95,8 +114,11 @@
|
||||
"Corner Relief": "Corner Relief",
|
||||
"Corner Relief Shape": "Corner Relief Shape",
|
||||
"Counterbore depth": "Counterbore depth",
|
||||
"Counterbore Ø": "Counterbore Ø",
|
||||
"Countersink angle": "Countersink angle",
|
||||
"Countersink Ø": "Countersink Ø",
|
||||
"Create Issue — review draft": "Create Issue — review draft",
|
||||
"Create Issue…": "Create Issue…",
|
||||
"Cross curves": "Cross curves",
|
||||
"Cube": "Cube",
|
||||
"Current orthographic view (1:1 mm)": "Current orthographic view (1:1 mm)",
|
||||
@@ -106,7 +128,11 @@
|
||||
"Curvature change (G3)": "Curvature change (G3)",
|
||||
"Custom": "Custom",
|
||||
"Customize Toolbar": "Customize Toolbar",
|
||||
"Customize…": "Customize…",
|
||||
"Cylindrical face": "Cylindrical face",
|
||||
"Delete": "Delete",
|
||||
"Delete Selected Features…": "Delete Selected Features…",
|
||||
"Delete Selected Folders": "Delete Selected Folders",
|
||||
"Delete all": "Delete all",
|
||||
"Delete rule": "Delete rule",
|
||||
"Delete the row": "Delete the row",
|
||||
@@ -117,8 +143,11 @@
|
||||
"Describe what happened before continuing.": "Describe what happened before continuing.",
|
||||
"Designs you open or save appear here.": "Designs you open or save appear here.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Determined by To Object. Choose Angle to edit the angular extent.",
|
||||
"Diameter": "Diameter",
|
||||
"Direction": "Direction",
|
||||
"Discard": "Discard",
|
||||
"Distance": "Distance",
|
||||
"Distance 1": "Distance 1",
|
||||
"Do not export hidden edges": "Do not export hidden edges",
|
||||
"Don't ask again": "Don't ask again",
|
||||
"Double-click to rename": "Double-click to rename",
|
||||
@@ -130,18 +159,22 @@
|
||||
"Edit Profile Sketch": "Edit Profile Sketch",
|
||||
"Enable rule": "Enable rule",
|
||||
"Enabled": "Enabled",
|
||||
"End Radius": "End Radius",
|
||||
"End bulge": "End bulge",
|
||||
"End continuity": "End continuity",
|
||||
"Expand": "Expand",
|
||||
"Expected result (optional)": "Expected result (optional)",
|
||||
"Explode": "Explode",
|
||||
"Explode Text": "Explode Text",
|
||||
"Export Animation…": "Export Animation…",
|
||||
"Export Bend Table…": "Export Bend Table…",
|
||||
"Export CSV…": "Export CSV…",
|
||||
"Export Current View as DXF": "Export Current View as DXF",
|
||||
"Export Current View as DXF…": "Export Current View as DXF…",
|
||||
"Export DXF": "Export DXF",
|
||||
"Export DXF…": "Export DXF…",
|
||||
"Export JSON…": "Export JSON…",
|
||||
"Export design library...": "Export design library...",
|
||||
"Export hidden edges as dashed lines": "Export hidden edges as dashed lines",
|
||||
"Expression": "Expression",
|
||||
"Extend to meet": "Extend to meet",
|
||||
@@ -149,25 +182,33 @@
|
||||
"FILES": "FILES",
|
||||
"FLAT": "FLAT",
|
||||
"Face": "Face",
|
||||
"Face Appearance": "Face Appearance",
|
||||
"Face attachment lost": "Face attachment lost",
|
||||
"Faces": "Faces",
|
||||
"Faces / edges / vertices": "Faces / edges / vertices",
|
||||
"Faces to split": "Faces to split",
|
||||
"Fastener": "Fastener",
|
||||
"Favorites": "Favorites",
|
||||
"Favourite": "Favourite",
|
||||
"Feature": "Feature",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Files from many CAD programs have Y up, so they open lying on their back here (Z up).",
|
||||
"Filter": "Filter",
|
||||
"Filter…": "Filter…",
|
||||
"Find in Browser": "Find in Browser",
|
||||
"Finish": "Finish",
|
||||
"First plane": "First plane",
|
||||
"Fit and G2 corners require Constant radius.": "Fit and G2 corners require Constant radius.",
|
||||
"Fixed Face": "Fixed Face",
|
||||
"Flat Pattern": "Flat Pattern",
|
||||
"Flat size": "Flat size",
|
||||
"Follow camera (Look At sketches)": "Follow camera (Look At sketches)",
|
||||
"Follow the camera (Look At sketches)": "Follow the camera (Look At sketches)",
|
||||
"Font": "Font",
|
||||
"From": "From",
|
||||
"From Bodies": "From Bodies",
|
||||
"From edges": "From edges",
|
||||
"Full Length": "Full Length",
|
||||
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.",
|
||||
"Gap": "Gap",
|
||||
"Grid": "Grid",
|
||||
"HISTORY": "HISTORY",
|
||||
@@ -175,6 +216,7 @@
|
||||
"Hidden edges": "Hidden edges",
|
||||
"History layout": "History layout",
|
||||
"History names": "History names",
|
||||
"Hole (optional)": "Hole (optional)",
|
||||
"Holes": "Holes",
|
||||
"Holes (edges on them)": "Holes (edges on them)",
|
||||
"Home view": "Home view",
|
||||
@@ -185,14 +227,20 @@
|
||||
"In place (surfaces)": "In place (surfaces)",
|
||||
"In plane": "In plane",
|
||||
"Inside": "Inside",
|
||||
"Instant": "Instant",
|
||||
"Interface font": "Interface font",
|
||||
"Interferes": "Interferes",
|
||||
"Invert": "Invert",
|
||||
"Isocurve": "Isocurve",
|
||||
"Issue title": "Issue title",
|
||||
"Items": "Items",
|
||||
"JOINTS": "JOINTS",
|
||||
"Joint Origin1": "Joint Origin1",
|
||||
"Keep (points on it)": "Keep (points on it)",
|
||||
"Keep Tools": "Keep Tools",
|
||||
"Keep of the other surface": "Keep of the other surface",
|
||||
"Key a joint's value at this step": "Key a joint's value at this step",
|
||||
"Keys": "Keys",
|
||||
"LIBRARY": "LIBRARY",
|
||||
"LINKS": "LINKS",
|
||||
"Later": "Later",
|
||||
@@ -203,9 +251,14 @@
|
||||
"Letter Spacing": "Letter Spacing",
|
||||
"Library": "Library",
|
||||
"Library file": "Library file",
|
||||
"Limits": "Limits",
|
||||
"Line Spacing": "Line Spacing",
|
||||
"Line Type": "Line Type",
|
||||
"Listing the installed fonts…": "Listing the installed fonts…",
|
||||
"Load library": "Load library",
|
||||
"Local library": "Local library",
|
||||
"Loop": "Loop",
|
||||
"M5 or M5x0.5": "M5 or M5x0.5",
|
||||
"MANAGE": "MANAGE",
|
||||
"MATERIALS": "MATERIALS",
|
||||
"MODEL": "MODEL",
|
||||
@@ -216,14 +269,20 @@
|
||||
"Matched restrictions check the final values.": "Matched restrictions check the final values.",
|
||||
"Material": "Material",
|
||||
"Max": "Max",
|
||||
"Maximum": "Maximum",
|
||||
"Mesh body": "Mesh body",
|
||||
"Mid Radius": "Mid Radius",
|
||||
"Middle double-click": "Middle double-click",
|
||||
"Min": "Min",
|
||||
"Minimum": "Minimum",
|
||||
"Mirror Plane": "Mirror Plane",
|
||||
"Mitre": "Mitre",
|
||||
"Model Parameters": "Model Parameters",
|
||||
"Modeled": "Modeled",
|
||||
"More toolbar panels": "More toolbar panels",
|
||||
"Motion": "Motion",
|
||||
"Mouse gestures": "Mouse gestures",
|
||||
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).",
|
||||
"Move / Copy": "Move / Copy",
|
||||
"Move Type": "Move Type",
|
||||
"Move down": "Move down",
|
||||
@@ -246,8 +305,11 @@
|
||||
"Not available yet": "Not available yet",
|
||||
"Notch": "Notch",
|
||||
"OUTPUT": "OUTPUT",
|
||||
"Of": "Of",
|
||||
"On your computer": "On your computer",
|
||||
"Opacity": "Opacity",
|
||||
"Open GitHub draft": "Open GitHub draft",
|
||||
"Open library...": "Open library...",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "Open local libraries in the desktop app. You can export this design's library here.",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.",
|
||||
"Opens your browser and copies the report text for you to paste into GitHub.": "Opens your browser and copies the report text for you to paste into GitHub.",
|
||||
@@ -269,8 +331,10 @@
|
||||
"Picked": "Picked",
|
||||
"Picked point": "Picked point",
|
||||
"Pitch": "Pitch",
|
||||
"Pivot": "Pivot",
|
||||
"Place at the origin instead": "Place at the origin instead",
|
||||
"Plastic Rules": "Plastic Rules",
|
||||
"Play the joint's motion in the view": "Play the joint's motion in the view",
|
||||
"Playback": "Playback",
|
||||
"Plot": "Plot",
|
||||
"Position": "Position",
|
||||
@@ -278,14 +342,18 @@
|
||||
"Position (G0)": "Position (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "Position along the edge, 0 at the start end, 1 at the other",
|
||||
"Preferences › Design turns the offer back on": "Preferences › Design turns the offer back on",
|
||||
"Preview": "Preview",
|
||||
"Preview Row": "Preview Row",
|
||||
"Preview leaves the current design unchanged.": "Preview leaves the current design unchanged.",
|
||||
"Profile / Edges": "Profile / Edges",
|
||||
"Profiles or curves": "Profiles or curves",
|
||||
"Profiles/Curves": "Profiles/Curves",
|
||||
"Quantity": "Quantity",
|
||||
"RECENT": "RECENT",
|
||||
"REFERENCE": "REFERENCE",
|
||||
"RULED SURFACE": "RULED SURFACE",
|
||||
"RULES": "RULES",
|
||||
"Radius": "Radius",
|
||||
"Radius Fit": "Radius Fit",
|
||||
"Ratio": "Ratio",
|
||||
"Reattach selected face": "Reattach selected face",
|
||||
@@ -294,6 +362,8 @@
|
||||
"Rectangular": "Rectangular",
|
||||
"Reference face": "Reference face",
|
||||
"Remove Tools": "Remove Tools",
|
||||
"Remove condition": "Remove condition",
|
||||
"Remove from Folder": "Remove from Folder",
|
||||
"Remove from list": "Remove from list",
|
||||
"Remove override": "Remove override",
|
||||
"Remove private design details or paths before sharing.": "Remove private design details or paths before sharing.",
|
||||
@@ -301,6 +371,9 @@
|
||||
"Remove the column": "Remove the column",
|
||||
"Remove the track": "Remove the track",
|
||||
"Remove this point": "Remove this point",
|
||||
"Rename Feature": "Rename Feature",
|
||||
"Rename Folder": "Rename Folder",
|
||||
"Replace existing library file": "Replace existing library file",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Report a bug — review a screenshot and open a GitHub draft",
|
||||
"Report text copied. Paste it into GitHub and submit your report there.": "Report text copied. Paste it into GitHub and submit your report there.",
|
||||
"Reports are public. A GitHub account is required.": "Reports are public. A GitHub account is required.",
|
||||
@@ -309,6 +382,7 @@
|
||||
"Restore View": "Restore View",
|
||||
"Restore outer boundary": "Restore outer boundary",
|
||||
"Restore the inherited component or physical material appearance": "Restore the inherited component or physical material appearance",
|
||||
"Restore this studio's tools": "Restore this studio's tools",
|
||||
"Result": "Result",
|
||||
"Retake screenshot": "Retake screenshot",
|
||||
"Reverse": "Reverse",
|
||||
@@ -319,7 +393,13 @@
|
||||
"Revert Position": "Revert Position",
|
||||
"Review the report and screenshot before opening GitHub.": "Review the report and screenshot before opening GitHub.",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "Review your report on GitHub and click Submit new issue to publish it.",
|
||||
"Rib Length": "Rib Length",
|
||||
"Rib Offset": "Rib Offset",
|
||||
"Rib Thickness": "Rib Thickness",
|
||||
"Ribs": "Ribs",
|
||||
"Rim Edge": "Rim Edge",
|
||||
"Ring of tools on the right button": "Ring of tools on the right button",
|
||||
"Roll Back Here": "Roll Back Here",
|
||||
"Roll History Marker Here": "Roll History Marker Here",
|
||||
"Round": "Round",
|
||||
"Rule": "Rule",
|
||||
@@ -338,13 +418,18 @@
|
||||
"Search": "Search",
|
||||
"Search commands": "Search commands",
|
||||
"Search this library": "Search this library",
|
||||
"Second plane": "Second plane",
|
||||
"Section": "Section",
|
||||
"Sections (one sketch each)": "Sections (one sketch each)",
|
||||
"Select Other": "Select Other",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).",
|
||||
"Select bodies (a face picks its body)": "Select bodies (a face picks its body)",
|
||||
"Select bodies or faces instead": "Select bodies or faces instead",
|
||||
"Select edges": "Select edges",
|
||||
"Selection filter": "Selection filter",
|
||||
"Semantic faces share their appearance across instances.": "Semantic faces share their appearance across instances.",
|
||||
"Send your report on GitHub": "Send your report on GitHub",
|
||||
"Shaded": "Shaded",
|
||||
"Shape": "Shape",
|
||||
"Sheet Rules": "Sheet Rules",
|
||||
"Show Agent Cursor": "Show Agent Cursor",
|
||||
@@ -362,6 +447,7 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft closed before these designs were saved. Their last autosave can be opened.",
|
||||
"Specialise": "Specialise",
|
||||
"Speed": "Speed",
|
||||
"Splitting tool": "Splitting tool",
|
||||
"Stand it up": "Stand it up",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Stand it up: a quarter turn about X, as one Move feature you can undo.",
|
||||
"Stand this part up?": "Stand this part up?",
|
||||
@@ -372,16 +458,22 @@
|
||||
"Steps to reproduce (optional)": "Steps to reproduce (optional)",
|
||||
"Stitch the kept pieces": "Stitch the kept pieces",
|
||||
"Stitch to both bodies": "Stitch to both bodies",
|
||||
"Studio": "Studio",
|
||||
"Study": "Study",
|
||||
"Style": "Style",
|
||||
"Summary": "Summary",
|
||||
"Suppress": "Suppress",
|
||||
"Suppressed": "Suppressed",
|
||||
"Surface": "Surface",
|
||||
"Surface, or faces to turn over": "Surface, or faces to turn over",
|
||||
"Swap Ends": "Swap Ends",
|
||||
"TRIM": "TRIM",
|
||||
"Tangent (G1)": "Tangent (G1)",
|
||||
"Tangent Chain": "Tangent Chain",
|
||||
"Tangent chain": "Tangent chain",
|
||||
"Tap drill diameter": "Tap drill diameter",
|
||||
"Target": "Target",
|
||||
"Target Body": "Target Body",
|
||||
"Tear": "Tear",
|
||||
"Technical details": "Technical details",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "The dashed sketch is a saved plane guide. Select a new face to repair the attachment.",
|
||||
@@ -396,24 +488,31 @@
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "This child is shared. Position changes apply to every copy of its parent.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.",
|
||||
"To": "To",
|
||||
"To Offset": "To Offset",
|
||||
"To edges": "To edges",
|
||||
"Tolerance": "Tolerance",
|
||||
"Too close": "Too close",
|
||||
"Tool Bodies": "Tool Bodies",
|
||||
"Tools": "Tools",
|
||||
"Tools, clockwise from the top": "Tools, clockwise from the top",
|
||||
"Trimming tool (plane, face or curves)": "Trimming tool (plane, face or curves)",
|
||||
"Turn off section analysis before exporting the view.": "Turn off section analysis before exporting the view.",
|
||||
"Type": "Type",
|
||||
"UNTRIM": "UNTRIM",
|
||||
"Unit": "Unit",
|
||||
"Unsuppress": "Unsuppress",
|
||||
"Updating the preview": "Updating the preview",
|
||||
"Use active author placement": "Use active author placement",
|
||||
"Use fixed plane": "Use fixed plane",
|
||||
"Use saved fixed plane": "Use saved fixed plane",
|
||||
"Use selected face as fixed plane": "Use selected face as fixed plane",
|
||||
"Use the SolidWorks mouse mode": "Use the SolidWorks mouse mode",
|
||||
"User Parameters": "User Parameters",
|
||||
"VIEW": "VIEW",
|
||||
"VIEWS": "VIEWS",
|
||||
"Value": "Value",
|
||||
"Variable Radius": "Variable Radius",
|
||||
"Velocity": "Velocity",
|
||||
"Vertices": "Vertices",
|
||||
"View navigator": "View navigator",
|
||||
@@ -427,13 +526,44 @@
|
||||
"When": "When",
|
||||
"Width": "Width",
|
||||
"Will fail": "Will fail",
|
||||
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.",
|
||||
"X": "X",
|
||||
"X Angle": "X Angle",
|
||||
"X angle": "X angle",
|
||||
"X distance": "X distance",
|
||||
"Y": "Y",
|
||||
"Y Angle": "Y Angle",
|
||||
"Y angle": "Y angle",
|
||||
"Y distance": "Y distance",
|
||||
"Z": "Z",
|
||||
"Z Angle": "Z Angle",
|
||||
"Z Distance": "Z Distance",
|
||||
"Z angle": "Z angle",
|
||||
"Z distance": "Z distance",
|
||||
"Zebra": "Zebra",
|
||||
"ai.prefs.availability": "Assistant controls",
|
||||
"ai.prefs.background_commands": "Run agent commands in the background",
|
||||
"ai.prefs.follow_camera": "Turn the camera to sketches",
|
||||
"ai.prefs.follow_camera_off": "Automatic camera following is off.",
|
||||
"ai.prefs.follow_camera_on": "The camera turns to face sketches the agent creates.",
|
||||
"ai.prefs.intro": "Agents, permissions and how their work appears.",
|
||||
"ai.prefs.make_default": "Use by default",
|
||||
"ai.prefs.no_agents": "No agents yet. Add an agent or refresh to find installed agents.",
|
||||
"ai.prefs.no_saved_approvals": "No saved approvals.",
|
||||
"ai.prefs.override_all_warning": "This agent may save, overwrite, export or replace the design without asking.",
|
||||
"ai.prefs.permission_all": "Allow everything",
|
||||
"ai.prefs.permission_all_summary": "Available SolidCraft tools run without asking.",
|
||||
"ai.prefs.permission_ask": "Ask before changes",
|
||||
"ai.prefs.permission_ask_summary": "Approve every design change and file action.",
|
||||
"ai.prefs.permission_edits": "Allow model edits",
|
||||
"ai.prefs.permission_edits_summary": "Model edits run; file and destructive actions still ask.",
|
||||
"ai.prefs.permission_scope": "For SolidCraft tools only. The agent’s shell and file edits use its own permissions.",
|
||||
"ai.prefs.pointer_preview_hidden": "Pointer hidden",
|
||||
"ai.prefs.pointer_preview_speed": "Agent pointer · {speed}",
|
||||
"ai.prefs.pointer_speed": "Motion speed",
|
||||
"ai.prefs.privacy_note": "Only your chosen agent or endpoint receives the conversation. Endpoint keys are stored in your system keychain.",
|
||||
"ai.prefs.show_pointer": "Show agent pointer",
|
||||
"ai.prefs.turn_limit_hint": "Maximum design-changing tool calls the Assistant may make for one message.",
|
||||
"angle, height": "angle, height",
|
||||
"appearance.dielectric_ior": "Dielectric IOR",
|
||||
"appearance.metallic": "Metallic",
|
||||
@@ -889,19 +1019,57 @@
|
||||
"help.solid.revolve": "Spins a sketch profile around an axis to make a round solid. Use it for shafts, knobs, bowls and any part that is round about a centre line.",
|
||||
"help.solid.shell": "Hollows out a solid, leaving walls of the thickness you set, and can open the faces you pick. Use it for boxes, covers and other thin-walled parts.",
|
||||
"help.third_party": "Third-party licences",
|
||||
"home.back": "Back to {name}",
|
||||
"home.crashes": "Crash reports",
|
||||
"home.design_workspace": "Design",
|
||||
"home.documentation": "Documentation",
|
||||
"home.export": "Export",
|
||||
"home.export_hint": "Choose a format for the current design.",
|
||||
"home.export_json_hint": "Editable design and command history",
|
||||
"home.export_mesh_hint": "Mesh for printing or sharing",
|
||||
"home.export_step_hint": "Precise solid geometry",
|
||||
"home.import": "Import…",
|
||||
"home.import_hint": "STEP, IGES or mesh into a new design",
|
||||
"home.in_this_browser": "in this browser",
|
||||
"home.missing": "missing",
|
||||
"home.month_01": "Jan",
|
||||
"home.month_02": "Feb",
|
||||
"home.month_03": "Mar",
|
||||
"home.month_04": "Apr",
|
||||
"home.month_05": "May",
|
||||
"home.month_06": "Jun",
|
||||
"home.month_07": "Jul",
|
||||
"home.month_08": "Aug",
|
||||
"home.month_09": "Sep",
|
||||
"home.month_10": "Oct",
|
||||
"home.month_11": "Nov",
|
||||
"home.month_12": "Dec",
|
||||
"home.new_inches": "New design, inches",
|
||||
"home.new_inches_hint": "Inches · opens in Design",
|
||||
"home.new_mm": "New design",
|
||||
"home.new_mm_hint": "Millimetres · opens in Design",
|
||||
"home.open": "Open…",
|
||||
"home.opened_days_ago": "Opened {count} days ago",
|
||||
"home.opened_hours_ago": "Opened {count} h ago",
|
||||
"home.opened_just_now": "Opened just now",
|
||||
"home.opened_minutes_ago": "Opened {count} min ago",
|
||||
"home.open_hint": "A .solidcraft design ({shortcut})",
|
||||
"home.open_now": "Open now",
|
||||
"home.opened_days_ago": "{count} days ago",
|
||||
"home.opened_hours_ago": "{count} h ago",
|
||||
"home.opened_just_now": "Just now",
|
||||
"home.opened_minutes_ago": "{count} min ago",
|
||||
"home.opening": "opening…",
|
||||
"home.properties": "Design properties",
|
||||
"home.recent": "Recent",
|
||||
"home.recent_empty": "Designs you open or save appear here.",
|
||||
"home.recovery_count": "Recoverable designs: {count}",
|
||||
"home.recovery_empty": "No recoverable designs.",
|
||||
"home.recovery_hint": "SolidCraft closed before these designs were saved.",
|
||||
"home.report_issue": "Report an Issue",
|
||||
"home.samples": "Samples",
|
||||
"home.settings": "Settings",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Keyboard shortcuts",
|
||||
"home.start_hint": "Create a design, open a recent file, or explore a sample.",
|
||||
"home.whats_new": "What's new",
|
||||
"home.yesterday": "Yesterday",
|
||||
"install.button_tip": "Install SolidCraft as an app: it opens in its own window and works offline",
|
||||
"install.dock_1": "Choose File › Add to Dock.",
|
||||
"install.dock_2": "Click Add.",
|
||||
@@ -959,8 +1127,15 @@
|
||||
"motion.to_end": "To the end",
|
||||
"motion.to_start": "To the start",
|
||||
"motion.track_name": "Track {number}",
|
||||
"nav.alias.help": "Shift+Alt+left drag orbits, Shift+Alt+middle pans, Shift+Alt+right zooms; middle drag also pans and right drag orbits",
|
||||
"nav.blender.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone",
|
||||
"nav.free_orbit.help": "Mouse and 3D mouse rotate through both poles and preserve roll. Shift axis snapping is disabled.",
|
||||
"nav.free_orbit.label": "Free orbit",
|
||||
"nav.fusion.help": "Middle drag pans, Shift+middle orbits, right drag orbits",
|
||||
"nav.inventor.help": "Middle drag pans, Shift+middle orbits, Ctrl+middle zooms, right drag orbits",
|
||||
"nav.openscad.help": "Left drag on empty space orbits, Shift+left drag box-selects, sketch geometry drags edit; right or middle drag pans, Shift+right or Shift+middle drag zooms",
|
||||
"nav.solidworks.help": "Middle drag orbits, Ctrl+middle pans, Shift+middle zooms, right drag orbits",
|
||||
"nav.tinkercad.help": "Right drag orbits, middle or Shift+right drag pans",
|
||||
"nav.touchpad.help": "Hold Shift and move the pointer to pan; hold Alt/Option and move it to orbit. No mouse button is needed. Vertical scroll zooms.",
|
||||
"nav.touchpad.label": "Touchpad",
|
||||
"nav.touchpad.orbit": "Orbit (hold Alt/Option and move; no mouse button)",
|
||||
@@ -983,8 +1158,130 @@
|
||||
"pattern.flip_direction": "Flip direction",
|
||||
"pick a hole to seat it, or OK places it at the origin": "pick a hole to seat it, or OK places it at the origin",
|
||||
"pick one or two vertices, edges or faces": "pick one or two vertices, edges or faces",
|
||||
"prefs.add_item": "Add the item",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option also frees manipulator arrows",
|
||||
"prefs.anti_aliasing_note": "Smooths jagged edges.",
|
||||
"prefs.applies_on_restart": "Applies on restart",
|
||||
"prefs.assistant": "Assistant",
|
||||
"prefs.axes": "Axes",
|
||||
"prefs.axis": "Axis",
|
||||
"prefs.bottom": "Bottom",
|
||||
"prefs.browser": "Browser",
|
||||
"prefs.built_in": "Built in",
|
||||
"prefs.button_left": "Left drag",
|
||||
"prefs.button_middle": "Middle drag",
|
||||
"prefs.button_right": "Right drag",
|
||||
"prefs.choose_font": "Choose font file…",
|
||||
"prefs.click": "Click",
|
||||
"prefs.contact_shading": "Soft contact shading",
|
||||
"prefs.contact_shading_note": "Soft shading where parts come close.",
|
||||
"prefs.context_menu": "Context menu",
|
||||
"prefs.context_menu_ring": "Context menu · hold for the radial menu",
|
||||
"prefs.cpu": "CPU (software)",
|
||||
"prefs.crash_reports": "Open crash reports…",
|
||||
"prefs.crash_reports_note": "Reports are kept on this computer, with the design and commands that reproduce the problem.",
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "Curved surfaces",
|
||||
"prefs.curves": "Curved surfaces",
|
||||
"prefs.dead_zone_note": "The hatched band is the dead zone: motion inside it is ignored.",
|
||||
"prefs.default": "default",
|
||||
"prefs.design": "Design defaults",
|
||||
"prefs.design_note": "What new designs start with. Open designs keep their own.",
|
||||
"prefs.devices": "3D mouse",
|
||||
"prefs.display": "Display",
|
||||
"prefs.edges": "Edges and shading",
|
||||
"prefs.extend": "Extend",
|
||||
"prefs.extra_navigation": "Ctrl+left drag on empty space orbits; hold Space to pan.",
|
||||
"prefs.follows_os": "Follows the OS",
|
||||
"prefs.font": "Font",
|
||||
"prefs.font_fallback": "Glyphs the chosen font lacks fall back to the built-in font, so no label shows an empty box.",
|
||||
"prefs.font_sample": "The quick brown fox · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "The selected font could not be read.",
|
||||
"prefs.general": "General",
|
||||
"prefs.general_note": "Language, saving and updates.",
|
||||
"prefs.gesture_orbit": "Orbit",
|
||||
"prefs.gesture_pan": "Pan",
|
||||
"prefs.gesture_zoom": "Zoom",
|
||||
"prefs.graphics": "Graphics",
|
||||
"prefs.graphics_note": "Trade speed for smoothness. Each choice shows its effect on a sample.",
|
||||
"prefs.graphics_presets_note": "Presets set the choices below together.",
|
||||
"prefs.history": "History",
|
||||
"prefs.hold_stop_snapping": "Hold to stop snapping",
|
||||
"prefs.icons_only": "Icons only",
|
||||
"prefs.imported_file": "Imported file",
|
||||
"prefs.input": "Input",
|
||||
"prefs.interface": "Interface",
|
||||
"prefs.interface_language": "Interface language",
|
||||
"prefs.interface_note": "How SolidCraft looks and where its parts sit.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.labels": "Labels",
|
||||
"prefs.layout": "Layout",
|
||||
"prefs.layout_note": "Compact docks panels to the window edges. Browser translucency is available in Floating. Change the Assistant side on the Assistant page.",
|
||||
"prefs.left": "Left",
|
||||
"prefs.left_click": "Left click",
|
||||
"prefs.live_input": "Live input",
|
||||
"prefs.live_preview": "Live preview",
|
||||
"prefs.minutes": "minutes",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "Mouse and trackpad",
|
||||
"prefs.mouse_heading": "Mouse",
|
||||
"prefs.mouse_mapping": "Mouse mapping",
|
||||
"prefs.navigation": "Navigation",
|
||||
"prefs.navigation_note": "How the mouse, trackpad and 3D mouse move the view.",
|
||||
"prefs.not_on_gpu": "not on this GPU",
|
||||
"prefs.on": "On",
|
||||
"prefs.opaque": "Opaque",
|
||||
"prefs.orbit_snap": "Hold Shift during orbit to snap to an axis.",
|
||||
"prefs.orientation": "Orientation",
|
||||
"prefs.overall_speed": "Overall speed",
|
||||
"prefs.preview_choices": "Preview of your choices",
|
||||
"prefs.quality_default": "default",
|
||||
"prefs.quality_fastest": "fastest",
|
||||
"prefs.quality_slowest": "slowest",
|
||||
"prefs.quality_smoother": "smoother",
|
||||
"prefs.reports_saved": "{n} saved",
|
||||
"prefs.right_click": "Right click",
|
||||
"prefs.saving": "Autosave and recovery",
|
||||
"prefs.scale_note": "Also Ctrl + / Ctrl − in the window. Buttons are 28 px at 100%.",
|
||||
"prefs.scale_text": "Scale and text",
|
||||
"prefs.select": "Select",
|
||||
"prefs.select_one_item": "Select one item",
|
||||
"prefs.selecting": "Selecting",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift adds only; Ctrl / Cmd toggles",
|
||||
"prefs.shift_add_note": "On: Shift adds, Ctrl / Cmd toggles. Off (default): both toggle.",
|
||||
"prefs.shortcuts": "Shortcuts",
|
||||
"prefs.sketch_selection": "Sketch & selection",
|
||||
"prefs.sketch_selection_heading": "Sketch and selection",
|
||||
"prefs.sketch_selection_note": "How sketches snap and how clicks select.",
|
||||
"prefs.sketching": "Sketching",
|
||||
"prefs.snap_drawing_note": "While drawing or dragging a sketch point.",
|
||||
"prefs.source": "Source",
|
||||
"prefs.speed": "Speed",
|
||||
"prefs.stand_up_note": "After a STEP or IGES file opens with Z up, offer a quarter turn about X.",
|
||||
"prefs.status_done": "Done",
|
||||
"prefs.status_error": "Error",
|
||||
"prefs.status_selected": "Selected",
|
||||
"prefs.status_warning": "Warning",
|
||||
"prefs.supersampling_brief": "Draws the view larger and filters it down: very crisp, and heavier.",
|
||||
"prefs.tab_accent_note": "The accent follows the active tab",
|
||||
"prefs.theme_file": "theme file",
|
||||
"prefs.timeline_half_hour": "−30 min",
|
||||
"prefs.timeline_now": "now",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
"prefs.toggle_item": "Add the item, or take it out again",
|
||||
"prefs.toolbar": "Toolbar",
|
||||
"prefs.translucent": "Translucent",
|
||||
"prefs.under_browser": "Under browser",
|
||||
"prefs.under_cursor": "Under cursor",
|
||||
"prefs.units_precision": "Units and precision",
|
||||
"prefs.updates": "Updates",
|
||||
"prefs.view": "View behavior",
|
||||
"prefs.viewport": "Viewport",
|
||||
"prefs.wheel": "Wheel",
|
||||
"prefs.wheel_trackpad": "Wheel and trackpad",
|
||||
"prefs.workspace": "Workspace",
|
||||
"prefs.zoom_toward_cursor": "Zoom, toward the cursor",
|
||||
"properties.title": "Properties: {name}",
|
||||
"recovery.days_ago": "{count} days ago",
|
||||
"recovery.detail": "{file} · {features} features · autosaved {when}",
|
||||
@@ -1055,7 +1352,7 @@
|
||||
"search.suggest": "Did you mean {label}?",
|
||||
"search.try": "Type a command name, a feature in this design, or a parameter. To run a command with values, type its id and a JSON object: {example}",
|
||||
"search.valid": "Valid",
|
||||
"settings.display_quality_note": "Finer levels draw curved faces and edges smoother and take longer to mesh. Only the viewport changes, never the design or exports.",
|
||||
"settings.display_quality_note": "Only the display. Designs and exports are not affected.",
|
||||
"settings.frame_rate_note": "Shows frames per second in the view's bottom-right corner. While it is on, the view redraws continuously.",
|
||||
"settings.measure_axis_distances": "Show X/Y/Z distances",
|
||||
"settings.measure_axis_distances_help": "Show world-axis component dimensions alongside the total distance while Measure is open.",
|
||||
|
||||
+42
-4
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Licencia MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licencias de terceros",
|
||||
"home.back": "Volver a {name}",
|
||||
"home.crashes": "Informes de fallos",
|
||||
"home.design_workspace": "Diseño",
|
||||
"home.documentation": "Documentación",
|
||||
"home.export": "Exportar",
|
||||
"home.export_hint": "Elige un formato para el diseño actual.",
|
||||
"home.export_json_hint": "Diseño editable e historial de comandos",
|
||||
"home.export_mesh_hint": "Malla para imprimir o compartir",
|
||||
"home.export_step_hint": "Geometría sólida precisa",
|
||||
"home.import": "Importar…",
|
||||
"home.import_hint": "STEP, IGES o malla en un nuevo diseño",
|
||||
"home.in_this_browser": "en este navegador",
|
||||
"home.missing": "no disponible",
|
||||
"home.month_01": "ene",
|
||||
"home.month_02": "feb",
|
||||
"home.month_03": "mar",
|
||||
"home.month_04": "abr",
|
||||
"home.month_05": "may",
|
||||
"home.month_06": "jun",
|
||||
"home.month_07": "jul",
|
||||
"home.month_08": "ago",
|
||||
"home.month_09": "sept",
|
||||
"home.month_10": "oct",
|
||||
"home.month_11": "nov",
|
||||
"home.month_12": "dic",
|
||||
"home.new_inches": "Nuevo diseño en pulgadas",
|
||||
"home.new_inches_hint": "Pulgadas · se abre en Diseño",
|
||||
"home.new_mm": "Nuevo diseño",
|
||||
"home.new_mm_hint": "Milímetros · se abre en Diseño",
|
||||
"home.open": "Abrir…",
|
||||
"home.opened_days_ago": "Abierto hace {count} días",
|
||||
"home.opened_hours_ago": "Abierto hace {count} h",
|
||||
"home.opened_just_now": "Acaba de abrirse",
|
||||
"home.opened_minutes_ago": "Abierto hace {count} min",
|
||||
"home.open_hint": "Un diseño .solidcraft ({shortcut})",
|
||||
"home.open_now": "Abrir ahora",
|
||||
"home.opened_days_ago": "Hace {count} días",
|
||||
"home.opened_hours_ago": "Hace {count} h",
|
||||
"home.opened_just_now": "Ahora mismo",
|
||||
"home.opened_minutes_ago": "Hace {count} min",
|
||||
"home.opening": "abriendo…",
|
||||
"home.properties": "Propiedades del diseño",
|
||||
"home.recent": "Recientes",
|
||||
"home.recent_empty": "Los diseños que abras o guardes aparecerán aquí.",
|
||||
"home.recovery_count": "Diseños recuperables: {count}",
|
||||
"home.recovery_empty": "No hay diseños recuperables.",
|
||||
"home.recovery_hint": "SolidCraft se cerró antes de guardar estos diseños.",
|
||||
"home.report_issue": "Notificar un problema",
|
||||
"home.samples": "Ejemplos",
|
||||
"home.settings": "Configuración",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Atajos de teclado",
|
||||
"home.start_hint": "Crea un diseño, abre un archivo reciente o explora un ejemplo.",
|
||||
"home.whats_new": "Novedades",
|
||||
"home.yesterday": "Ayer",
|
||||
"install.button_tip": "Instalar SolidCraft como aplicación: se abre en una ventana propia y funciona sin conexión",
|
||||
"install.dock_1": "Elige Archivo › Añadir al Dock.",
|
||||
"install.dock_2": "Haz clic en Añadir.",
|
||||
|
||||
+42
-4
@@ -863,19 +863,57 @@
|
||||
"help.solid.revolve": "Fait tourner le profil d'une esquisse autour d'un axe pour obtenir un solide de révolution. À utiliser pour les arbres, boutons, bols et toute pièce ronde autour d'une ligne centrale.",
|
||||
"help.solid.shell": "Évide un solide en laissant des parois de l'épaisseur indiquée, et peut ouvrir les faces choisies. À utiliser pour les boîtes, couvercles et autres pièces à parois minces.",
|
||||
"help.third_party": "Licences tierces",
|
||||
"home.back": "Revenir à {name}",
|
||||
"home.crashes": "Rapports de plantage",
|
||||
"home.design_workspace": "Conception",
|
||||
"home.documentation": "Documentation",
|
||||
"home.export": "Exporter",
|
||||
"home.export_hint": "Choisissez un format pour la conception actuelle.",
|
||||
"home.export_json_hint": "Conception modifiable et historique des commandes",
|
||||
"home.export_mesh_hint": "Maillage pour l’impression ou le partage",
|
||||
"home.export_step_hint": "Géométrie solide précise",
|
||||
"home.import": "Importer…",
|
||||
"home.import_hint": "STEP, IGES ou maillage dans une nouvelle conception",
|
||||
"home.in_this_browser": "dans ce navigateur",
|
||||
"home.missing": "introuvable",
|
||||
"home.month_01": "janv.",
|
||||
"home.month_02": "févr.",
|
||||
"home.month_03": "mars",
|
||||
"home.month_04": "avr.",
|
||||
"home.month_05": "mai",
|
||||
"home.month_06": "juin",
|
||||
"home.month_07": "juil.",
|
||||
"home.month_08": "août",
|
||||
"home.month_09": "sept.",
|
||||
"home.month_10": "oct.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "déc.",
|
||||
"home.new_inches": "Nouvelle conception en pouces",
|
||||
"home.new_inches_hint": "Pouces · ouvre l’espace Conception",
|
||||
"home.new_mm": "Nouvelle conception",
|
||||
"home.new_mm_hint": "Millimètres · ouvre l’espace Conception",
|
||||
"home.open": "Ouvrir…",
|
||||
"home.opened_days_ago": "Ouvert il y a {count} j",
|
||||
"home.opened_hours_ago": "Ouvert il y a {count} h",
|
||||
"home.opened_just_now": "Ouvert à l'instant",
|
||||
"home.opened_minutes_ago": "Ouvert il y a {count} min",
|
||||
"home.open_hint": "Une conception .solidcraft ({shortcut})",
|
||||
"home.open_now": "Ouvrir maintenant",
|
||||
"home.opened_days_ago": "Il y a {count} j",
|
||||
"home.opened_hours_ago": "Il y a {count} h",
|
||||
"home.opened_just_now": "À l’instant",
|
||||
"home.opened_minutes_ago": "Il y a {count} min",
|
||||
"home.opening": "ouverture…",
|
||||
"home.properties": "Propriétés de la conception",
|
||||
"home.recent": "Récents",
|
||||
"home.recent_empty": "Les conceptions que vous ouvrez ou enregistrez apparaissent ici.",
|
||||
"home.recovery_count": "Conceptions récupérables : {count}",
|
||||
"home.recovery_empty": "Aucune conception récupérable.",
|
||||
"home.recovery_hint": "SolidCraft s’est fermé avant l’enregistrement de ces conceptions.",
|
||||
"home.report_issue": "Signaler un problème",
|
||||
"home.samples": "Exemples",
|
||||
"home.settings": "Paramètres",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Raccourcis clavier",
|
||||
"home.start_hint": "Créez une conception, ouvrez un fichier récent ou explorez un exemple.",
|
||||
"home.whats_new": "Nouveautés",
|
||||
"home.yesterday": "Hier",
|
||||
"install.button_tip": "Installer SolidCraft comme une app : il s'ouvre dans sa propre fenêtre et fonctionne hors ligne",
|
||||
"install.dock_1": "Choisissez Fichier › Ajouter au Dock.",
|
||||
"install.dock_2": "Cliquez sur Ajouter.",
|
||||
|
||||
+42
-4
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Licenza MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenzas de terceiros",
|
||||
"home.back": "Volver a {name}",
|
||||
"home.crashes": "Informes de fallos",
|
||||
"home.design_workspace": "Deseño",
|
||||
"home.documentation": "Documentación",
|
||||
"home.export": "Exportar",
|
||||
"home.export_hint": "Escolle un formato para o deseño actual.",
|
||||
"home.export_json_hint": "Deseño editable e historial de comandos",
|
||||
"home.export_mesh_hint": "Malla para imprimir ou compartir",
|
||||
"home.export_step_hint": "Xeometría sólida precisa",
|
||||
"home.import": "Importar…",
|
||||
"home.import_hint": "STEP, IGES ou malla nun novo deseño",
|
||||
"home.in_this_browser": "neste navegador",
|
||||
"home.missing": "non atopado",
|
||||
"home.month_01": "xan.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mar.",
|
||||
"home.month_04": "abr.",
|
||||
"home.month_05": "maio",
|
||||
"home.month_06": "xuño",
|
||||
"home.month_07": "xul.",
|
||||
"home.month_08": "ago.",
|
||||
"home.month_09": "set.",
|
||||
"home.month_10": "out.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Novo deseño en polgadas",
|
||||
"home.new_inches_hint": "Polgadas · ábrese en Deseño",
|
||||
"home.new_mm": "Novo deseño",
|
||||
"home.new_mm_hint": "Milímetros · ábrese en Deseño",
|
||||
"home.open": "Abrir…",
|
||||
"home.opened_days_ago": "Aberto hai {count} días",
|
||||
"home.opened_hours_ago": "Aberto hai {count} h",
|
||||
"home.opened_just_now": "Aberto agora mesmo",
|
||||
"home.opened_minutes_ago": "Aberto hai {count} min",
|
||||
"home.open_hint": "Un deseño .solidcraft ({shortcut})",
|
||||
"home.open_now": "Abrir agora",
|
||||
"home.opened_days_ago": "Hai {count} días",
|
||||
"home.opened_hours_ago": "Hai {count} h",
|
||||
"home.opened_just_now": "Agora mesmo",
|
||||
"home.opened_minutes_ago": "Hai {count} min",
|
||||
"home.opening": "abrindo…",
|
||||
"home.properties": "Propiedades do deseño",
|
||||
"home.recent": "Recentes",
|
||||
"home.recent_empty": "Os deseños que abras ou gardes aparecerán aquí.",
|
||||
"home.recovery_count": "Deseños recuperables: {count}",
|
||||
"home.recovery_empty": "Non hai deseños recuperables.",
|
||||
"home.recovery_hint": "SolidCraft pechouse antes de gardar estes deseños.",
|
||||
"home.report_issue": "Informar dunha incidencia",
|
||||
"home.samples": "Exemplos",
|
||||
"home.settings": "Configuración",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Atallos de teclado",
|
||||
"home.start_hint": "Crea un deseño, abre un ficheiro recente ou explora un exemplo.",
|
||||
"home.whats_new": "Novidades",
|
||||
"home.yesterday": "Onte",
|
||||
"install.button_tip": "Instala SolidCraft como aplicación: ábrese na súa propia xanela e funciona sen conexión",
|
||||
"install.dock_1": "Escolle Ficheiro › Engadir ao Dock.",
|
||||
"install.dock_2": "Fai clic en Engadir.",
|
||||
|
||||
+42
-4
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT licenc",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Harmadik felek licencei",
|
||||
"home.back": "Vissza ide: {name}",
|
||||
"home.crashes": "Összeomlási jelentések",
|
||||
"home.design_workspace": "Tervezés",
|
||||
"home.documentation": "Dokumentáció",
|
||||
"home.export": "Exportálás",
|
||||
"home.export_hint": "Válasszon formátumot az aktuális tervhez.",
|
||||
"home.export_json_hint": "Szerkeszthető terv és parancselőzmények",
|
||||
"home.export_mesh_hint": "Háló nyomtatáshoz vagy megosztáshoz",
|
||||
"home.export_step_hint": "Pontos testgeometria",
|
||||
"home.import": "Importálás…",
|
||||
"home.import_hint": "STEP, IGES vagy háló importálása új tervbe",
|
||||
"home.in_this_browser": "ebben a böngészőben",
|
||||
"home.missing": "hiányzik",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "febr.",
|
||||
"home.month_03": "márc.",
|
||||
"home.month_04": "ápr.",
|
||||
"home.month_05": "máj.",
|
||||
"home.month_06": "jún.",
|
||||
"home.month_07": "júl.",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "szept.",
|
||||
"home.month_10": "okt.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Új terv hüvelykben",
|
||||
"home.new_inches_hint": "Hüvelyk · a Tervezés munkaterületen nyílik meg",
|
||||
"home.new_mm": "Új terv",
|
||||
"home.new_mm_hint": "Milliméter · a Tervezés munkaterületen nyílik meg",
|
||||
"home.open": "Megnyitás…",
|
||||
"home.opened_days_ago": "Megnyitva {count} napja",
|
||||
"home.opened_hours_ago": "Megnyitva {count} órája",
|
||||
"home.opened_just_now": "Épp most megnyitva",
|
||||
"home.opened_minutes_ago": "Megnyitva {count} perce",
|
||||
"home.open_hint": "Egy .solidcraft terv ({shortcut})",
|
||||
"home.open_now": "Jelenleg megnyitva",
|
||||
"home.opened_days_ago": "{count} napja",
|
||||
"home.opened_hours_ago": "{count} órája",
|
||||
"home.opened_just_now": "Épp most",
|
||||
"home.opened_minutes_ago": "{count} perce",
|
||||
"home.opening": "megnyitás…",
|
||||
"home.properties": "Terv tulajdonságai",
|
||||
"home.recent": "Legutóbbi",
|
||||
"home.recent_empty": "A megnyitott vagy mentett tervek itt jelennek meg.",
|
||||
"home.recovery_count": "Helyreállítható tervek: {count}",
|
||||
"home.recovery_empty": "Nincs helyreállítható terv.",
|
||||
"home.recovery_hint": "A SolidCraft a tervek mentése előtt bezárult.",
|
||||
"home.report_issue": "Hiba jelentése",
|
||||
"home.samples": "Minták",
|
||||
"home.settings": "Beállítások",
|
||||
"home.short_date": "{month} {day}.",
|
||||
"home.shortcuts": "Billentyűparancsok",
|
||||
"home.start_hint": "Hozzon létre egy tervet, nyisson meg egy legutóbbi fájlt, vagy fedezzen fel egy mintát.",
|
||||
"home.whats_new": "Újdonságok",
|
||||
"home.yesterday": "Tegnap",
|
||||
"install.button_tip": "A SolidCraft telepítése alkalmazásként: saját ablakban nyílik meg, és offline is működik",
|
||||
"install.dock_1": "Válassza a Fájl › Hozzáadás a Dockhoz lehetőséget.",
|
||||
"install.dock_2": "Kattintson a Hozzáadás gombra.",
|
||||
|
||||
+42
-4
@@ -856,19 +856,57 @@
|
||||
"help.mit": "Lisensi MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Lisensi pihak ketiga",
|
||||
"home.back": "Kembali ke {name}",
|
||||
"home.crashes": "Laporan kerusakan",
|
||||
"home.design_workspace": "Desain",
|
||||
"home.documentation": "Dokumentasi",
|
||||
"home.export": "Ekspor",
|
||||
"home.export_hint": "Pilih format untuk desain saat ini.",
|
||||
"home.export_json_hint": "Desain yang dapat diedit dan riwayat perintah",
|
||||
"home.export_mesh_hint": "Mesh untuk pencetakan atau berbagi",
|
||||
"home.export_step_hint": "Geometri benda padat yang presisi",
|
||||
"home.import": "Impor…",
|
||||
"home.import_hint": "STEP, IGES, atau mesh ke dalam desain baru",
|
||||
"home.in_this_browser": "di peramban ini",
|
||||
"home.missing": "hilang",
|
||||
"home.month_01": "Jan",
|
||||
"home.month_02": "Feb",
|
||||
"home.month_03": "Mar",
|
||||
"home.month_04": "Apr",
|
||||
"home.month_05": "Mei",
|
||||
"home.month_06": "Jun",
|
||||
"home.month_07": "Jul",
|
||||
"home.month_08": "Agu",
|
||||
"home.month_09": "Sep",
|
||||
"home.month_10": "Okt",
|
||||
"home.month_11": "Nov",
|
||||
"home.month_12": "Des",
|
||||
"home.new_inches": "Desain baru dalam inci",
|
||||
"home.new_inches_hint": "Inci · terbuka di Desain",
|
||||
"home.new_mm": "Desain baru",
|
||||
"home.new_mm_hint": "Milimeter · terbuka di Desain",
|
||||
"home.open": "Buka…",
|
||||
"home.opened_days_ago": "Dibuka {count} hari lalu",
|
||||
"home.opened_hours_ago": "Dibuka {count} jam lalu",
|
||||
"home.opened_just_now": "Baru saja dibuka",
|
||||
"home.opened_minutes_ago": "Dibuka {count} mnt lalu",
|
||||
"home.open_hint": "Desain .solidcraft ({shortcut})",
|
||||
"home.open_now": "Sedang terbuka",
|
||||
"home.opened_days_ago": "{count} hari lalu",
|
||||
"home.opened_hours_ago": "{count} jam lalu",
|
||||
"home.opened_just_now": "Baru saja",
|
||||
"home.opened_minutes_ago": "{count} mnt lalu",
|
||||
"home.opening": "membuka…",
|
||||
"home.properties": "Properti desain",
|
||||
"home.recent": "Terbaru",
|
||||
"home.recent_empty": "Desain yang Anda buka atau simpan akan muncul di sini.",
|
||||
"home.recovery_count": "Desain yang dapat dipulihkan: {count}",
|
||||
"home.recovery_empty": "Tidak ada desain yang dapat dipulihkan.",
|
||||
"home.recovery_hint": "SolidCraft ditutup sebelum desain ini disimpan.",
|
||||
"home.report_issue": "Laporkan Masalah",
|
||||
"home.samples": "Contoh",
|
||||
"home.settings": "Pengaturan",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Pintasan papan ketik",
|
||||
"home.start_hint": "Buat desain, buka berkas terbaru, atau jelajahi contoh.",
|
||||
"home.whats_new": "Yang baru",
|
||||
"home.yesterday": "Kemarin",
|
||||
"install.button_tip": "Pasang SolidCraft sebagai aplikasi: terbuka di jendelanya sendiri dan berfungsi offline",
|
||||
"install.dock_1": "Pilih File › Add to Dock.",
|
||||
"install.dock_2": "Klik Add.",
|
||||
|
||||
+42
-4
@@ -860,19 +860,57 @@
|
||||
"help.mit": "Licenza MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenze di terze parti",
|
||||
"home.back": "Torna a {name}",
|
||||
"home.crashes": "Rapporti di arresto anomalo",
|
||||
"home.design_workspace": "Progettazione",
|
||||
"home.documentation": "Documentazione",
|
||||
"home.export": "Esporta",
|
||||
"home.export_hint": "Scegli un formato per il progetto attuale.",
|
||||
"home.export_json_hint": "Progetto modificabile e cronologia dei comandi",
|
||||
"home.export_mesh_hint": "Mesh per la stampa o la condivisione",
|
||||
"home.export_step_hint": "Geometria solida precisa",
|
||||
"home.import": "Importa…",
|
||||
"home.import_hint": "STEP, IGES o mesh in un nuovo progetto",
|
||||
"home.in_this_browser": "in questo browser",
|
||||
"home.missing": "non presente",
|
||||
"home.month_01": "gen",
|
||||
"home.month_02": "feb",
|
||||
"home.month_03": "mar",
|
||||
"home.month_04": "apr",
|
||||
"home.month_05": "mag",
|
||||
"home.month_06": "giu",
|
||||
"home.month_07": "lug",
|
||||
"home.month_08": "ago",
|
||||
"home.month_09": "set",
|
||||
"home.month_10": "ott",
|
||||
"home.month_11": "nov",
|
||||
"home.month_12": "dic",
|
||||
"home.new_inches": "Nuovo progetto in pollici",
|
||||
"home.new_inches_hint": "Pollici · si apre in Progettazione",
|
||||
"home.new_mm": "Nuovo progetto",
|
||||
"home.new_mm_hint": "Millimetri · si apre in Progettazione",
|
||||
"home.open": "Apri…",
|
||||
"home.opened_days_ago": "Aperto {count} giorni fa",
|
||||
"home.opened_hours_ago": "Aperto {count} h fa",
|
||||
"home.opened_just_now": "Appena aperto",
|
||||
"home.opened_minutes_ago": "Aperto {count} min fa",
|
||||
"home.open_hint": "Un progetto .solidcraft ({shortcut})",
|
||||
"home.open_now": "Apri ora",
|
||||
"home.opened_days_ago": "{count} giorni fa",
|
||||
"home.opened_hours_ago": "{count} h fa",
|
||||
"home.opened_just_now": "Adesso",
|
||||
"home.opened_minutes_ago": "{count} min fa",
|
||||
"home.opening": "apertura…",
|
||||
"home.properties": "Proprietà del progetto",
|
||||
"home.recent": "Recenti",
|
||||
"home.recent_empty": "I progetti che apri o salvi vengono visualizzati qui.",
|
||||
"home.recovery_count": "Progetti recuperabili: {count}",
|
||||
"home.recovery_empty": "Nessun progetto recuperabile.",
|
||||
"home.recovery_hint": "SolidCraft si è chiuso prima del salvataggio di questi progetti.",
|
||||
"home.report_issue": "Segnala un problema",
|
||||
"home.samples": "Esempi",
|
||||
"home.settings": "Impostazioni",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Scorciatoie da tastiera",
|
||||
"home.start_hint": "Crea un progetto, apri un file recente o esplora un esempio.",
|
||||
"home.whats_new": "Novità",
|
||||
"home.yesterday": "Ieri",
|
||||
"install.button_tip": "Installa SolidCraft come applicazione: si apre in una finestra dedicata e funziona offline",
|
||||
"install.dock_1": "Scegli File › Aggiungi al Dock.",
|
||||
"install.dock_2": "Fai clic su Aggiungi.",
|
||||
|
||||
+302
-5
@@ -14,11 +14,15 @@
|
||||
"Active": "アクティブ",
|
||||
"Active rule": "有効なルール",
|
||||
"Add": "追加",
|
||||
"Add Column": "列を追加",
|
||||
"Add Key": "キーを追加",
|
||||
"Add Mid Radius": "中間半径を追加",
|
||||
"Add Row": "行を追加",
|
||||
"Add a tool": "ツールを追加",
|
||||
"Add condition": "条件を追加",
|
||||
"Add or edit rule": "ルールを追加または編集",
|
||||
"Add to New Folder": "新しいフォルダーに追加",
|
||||
"Advanced": "詳細",
|
||||
"Against": "相手",
|
||||
"Alignment": "配置",
|
||||
"All Sketch Points": "すべてのスケッチ点",
|
||||
@@ -32,6 +36,7 @@
|
||||
"Appearance name": "外観名",
|
||||
"Applied Radius": "適用半径",
|
||||
"Apply To": "適用先",
|
||||
"Apply library appearance": "ライブラリの外観を適用",
|
||||
"Apply to selection": "選択項目に適用",
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "先に外観を適用してください。割り当てた外観と、このデザインで作成したマテリアルを書き出します。",
|
||||
"Arc length": "円弧の長さ",
|
||||
@@ -42,13 +47,18 @@
|
||||
"BLEND SURFACE": "ブレンドサーフェス",
|
||||
"BOUNDARY SURFACE": "境界サーフェス",
|
||||
"BROWSER": "ブラウザ",
|
||||
"Bend Edges": "曲げエッジ",
|
||||
"Bend Line Position": "曲げ線位置",
|
||||
"Bend Position": "曲げ位置",
|
||||
"Bend Radius": "曲げ半径",
|
||||
"Bend Table": "曲げテーブル",
|
||||
"Bends": "曲げ",
|
||||
"Bodies": "ボディ",
|
||||
"Bodies (optional)": "ボディ (オプション)",
|
||||
"Body": "ボディ",
|
||||
"Body (last if none)": "ボディ (未指定時は最後)",
|
||||
"Body to split": "分割するボディ",
|
||||
"Body, or faces to take out": "ボディ、または取り除く面",
|
||||
"Boundary": "境界",
|
||||
"Bounding box": "境界ボックス",
|
||||
"CHECK": "チェック",
|
||||
@@ -60,7 +70,9 @@
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSVまたはJSON:既存のパラメータを更新し、新しいパラメータを追加します",
|
||||
"Capture All Positions": "すべての位置をキャプチャ",
|
||||
"Capture Pos.": "位置を記録",
|
||||
"Catch Depth": "キャッチ深さ",
|
||||
"Catch Direction": "キャッチ方向",
|
||||
"Catch Length": "キャッチ長さ",
|
||||
"Cells": "セル",
|
||||
"Center X": "中心 X",
|
||||
"Center Y": "中心 Y",
|
||||
@@ -69,15 +81,21 @@
|
||||
"Chamfer Type": "面取りタイプ",
|
||||
"Check collisions": "衝突チェック",
|
||||
"Checked": "チェック済み",
|
||||
"Choose Variable radius or Spline in Type.": "タイプで Variable radius またはスプラインを選択してください。",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "新しい平面を選択してください。スケッチジオメトリと寸法は2次元座標系のまま維持されます。",
|
||||
"Choose…": "選択…",
|
||||
"Circular": "円形",
|
||||
"Circumference": "円周",
|
||||
"Close draft": "下書きを閉じる",
|
||||
"Collapse": "折りたたむ",
|
||||
"Collisions": "衝突",
|
||||
"Colour": "色",
|
||||
"Command": "コマンド",
|
||||
"Comment": "コメント",
|
||||
"Component 1": "コンポーネント 1",
|
||||
"Component 2": "コンポーネント 2",
|
||||
"Component1": "コンポーネント1",
|
||||
"Components": "コンポーネント",
|
||||
"Config Table": "構成テーブル",
|
||||
"Configuration": "構成",
|
||||
"Configuration rules": "構成ルール",
|
||||
@@ -86,6 +104,7 @@
|
||||
"Contact sets": "接触セット",
|
||||
"Continue on GitHub": "GitHubで続ける",
|
||||
"Continuity": "連続性",
|
||||
"Control Radius": "制御半径",
|
||||
"Copy Error Message": "エラーメッセージをコピー",
|
||||
"Copy Warning Message": "警告メッセージをコピー",
|
||||
"Copy all licences": "すべてのライセンスをコピー",
|
||||
@@ -95,8 +114,11 @@
|
||||
"Corner Relief": "コーナーリリーフ",
|
||||
"Corner Relief Shape": "コーナーリリーフ形状",
|
||||
"Counterbore depth": "座ぐり深さ",
|
||||
"Counterbore Ø": "座ぐり Ø",
|
||||
"Countersink angle": "皿穴角度",
|
||||
"Countersink Ø": "皿穴 Ø",
|
||||
"Create Issue — review draft": "Issueを作成 — 下書きの確認",
|
||||
"Create Issue…": "Issueを作成…",
|
||||
"Cross curves": "交差する曲線",
|
||||
"Cube": "キューブ",
|
||||
"Current orthographic view (1:1 mm)": "現在の平行投影(実寸・mm)",
|
||||
@@ -106,7 +128,11 @@
|
||||
"Curvature change (G3)": "曲率変化 (G3)",
|
||||
"Custom": "カスタム",
|
||||
"Customize Toolbar": "ツールバーのカスタマイズ",
|
||||
"Customize…": "カスタマイズ…",
|
||||
"Cylindrical face": "円筒面",
|
||||
"Delete": "削除",
|
||||
"Delete Selected Features…": "選択したフィーチャーを削除…",
|
||||
"Delete Selected Folders": "選択したフォルダーを削除",
|
||||
"Delete all": "すべて削除",
|
||||
"Delete rule": "ルールを削除",
|
||||
"Delete the row": "行を削除",
|
||||
@@ -117,8 +143,11 @@
|
||||
"Describe what happened before continuing.": "続ける前に、何が起きたか記入してください。",
|
||||
"Designs you open or save appear here.": "開いたデザインや保存したデザインがここに表示されます。",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "「対象オブジェクトまで」により決定されます。角度範囲を編集するには「角度」を選択してください。",
|
||||
"Diameter": "直径",
|
||||
"Direction": "方向",
|
||||
"Discard": "破棄",
|
||||
"Distance": "距離",
|
||||
"Distance 1": "距離 1",
|
||||
"Do not export hidden edges": "出力しない",
|
||||
"Don't ask again": "今後確認しない",
|
||||
"Double-click to rename": "ダブルクリックで名前変更",
|
||||
@@ -130,18 +159,22 @@
|
||||
"Edit Profile Sketch": "プロファイルスケッチを編集",
|
||||
"Enable rule": "ルールを有効化",
|
||||
"Enabled": "有効",
|
||||
"End Radius": "終了半径",
|
||||
"End bulge": "終了側の膨らみ",
|
||||
"End continuity": "終了側の連続性",
|
||||
"Expand": "展開",
|
||||
"Expected result (optional)": "期待した結果(任意)",
|
||||
"Explode": "分解",
|
||||
"Explode Text": "テキストを分解",
|
||||
"Export Animation…": "アニメーションをエクスポート…",
|
||||
"Export Bend Table…": "曲げテーブルを書き出す…",
|
||||
"Export CSV…": "CSVを書き出し…",
|
||||
"Export Current View as DXF": "現在の表示をDXFでエクスポート",
|
||||
"Export Current View as DXF…": "現在の表示をDXFでエクスポート…",
|
||||
"Export DXF": "DXF出力",
|
||||
"Export DXF…": "DXFをエクスポート…",
|
||||
"Export JSON…": "JSONを書き出し…",
|
||||
"Export design library...": "デザインライブラリを書き出す...",
|
||||
"Export hidden edges as dashed lines": "点線で出力",
|
||||
"Expression": "式",
|
||||
"Extend to meet": "交わるまで延長",
|
||||
@@ -149,25 +182,33 @@
|
||||
"FILES": "ファイル",
|
||||
"FLAT": "展開",
|
||||
"Face": "面",
|
||||
"Face Appearance": "面の外観",
|
||||
"Face attachment lost": "面アタッチメントが失われました",
|
||||
"Faces": "面",
|
||||
"Faces / edges / vertices": "面 / エッジ / 頂点",
|
||||
"Faces to split": "分割する面",
|
||||
"Fastener": "締結部品",
|
||||
"Favorites": "お気に入り",
|
||||
"Favourite": "お気に入り",
|
||||
"Feature": "フィーチャー",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "多くのCADではY軸を上向きとしているため、Z軸を上向きとするSolidCraftでは、読み込んだモデルが横倒しの向きで表示されることがあります。",
|
||||
"Filter": "フィルター",
|
||||
"Filter…": "フィルター…",
|
||||
"Find in Browser": "ブラウザで検索",
|
||||
"Finish": "終了",
|
||||
"First plane": "第1平面",
|
||||
"Fit and G2 corners require Constant radius.": "フィットと G2 コーナーには Constant radius が必要です。",
|
||||
"Fixed Face": "固定面",
|
||||
"Flat Pattern": "フラットパターン",
|
||||
"Flat size": "展開サイズ",
|
||||
"Follow camera (Look At sketches)": "カメラ追従(スケッチを正面視)",
|
||||
"Follow the camera (Look At sketches)": "カメラ追従(スケッチを正面視)",
|
||||
"Font": "フォント",
|
||||
"From": "開始",
|
||||
"From Bodies": "ボディから",
|
||||
"From edges": "開始側エッジ",
|
||||
"Full Length": "全長",
|
||||
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 コーナーには一定半径と Exact fit が必要です。G2 コーナーの構築はまだサポートされていません。",
|
||||
"Gap": "間隔",
|
||||
"Grid": "グリッド",
|
||||
"HISTORY": "履歴",
|
||||
@@ -175,6 +216,7 @@
|
||||
"Hidden edges": "隠線",
|
||||
"History layout": "履歴のレイアウト",
|
||||
"History names": "履歴名",
|
||||
"Hole (optional)": "穴 (オプション)",
|
||||
"Holes": "穴",
|
||||
"Holes (edges on them)": "穴(そのエッジ)",
|
||||
"Home view": "ホームビュー",
|
||||
@@ -185,14 +227,20 @@
|
||||
"In place (surfaces)": "その場で(サーフェス)",
|
||||
"In plane": "平面内",
|
||||
"Inside": "内側",
|
||||
"Instant": "即時",
|
||||
"Interface font": "インターフェースフォント",
|
||||
"Interferes": "干渉あり",
|
||||
"Invert": "反転",
|
||||
"Isocurve": "等パラメータ線",
|
||||
"Issue title": "Issueタイトル",
|
||||
"Items": "項目",
|
||||
"JOINTS": "ジョイント",
|
||||
"Joint Origin1": "ジョイント原点1",
|
||||
"Keep (points on it)": "残す部分(その上の点)",
|
||||
"Keep Tools": "ツールボディを保持",
|
||||
"Keep of the other surface": "相手のサーフェスで残す部分",
|
||||
"Key a joint's value at this step": "このステップでジョイントの値をキー設定",
|
||||
"Keys": "キー",
|
||||
"LIBRARY": "ライブラリ",
|
||||
"LINKS": "リンク",
|
||||
"Later": "後で",
|
||||
@@ -203,9 +251,14 @@
|
||||
"Letter Spacing": "文字間隔",
|
||||
"Library": "ライブラリ",
|
||||
"Library file": "ライブラリファイル",
|
||||
"Limits": "制限",
|
||||
"Line Spacing": "行間",
|
||||
"Line Type": "線種",
|
||||
"Listing the installed fonts…": "インストール済みフォントを一覧表示中…",
|
||||
"Load library": "ライブラリを読み込む",
|
||||
"Local library": "ローカルライブラリ",
|
||||
"Loop": "ループ",
|
||||
"M5 or M5x0.5": "M5 または M5x0.5",
|
||||
"MANAGE": "管理",
|
||||
"MATERIALS": "材料",
|
||||
"MODEL": "モデル",
|
||||
@@ -216,14 +269,20 @@
|
||||
"Matched restrictions check the final values.": "条件に一致する制限ルールが最終値を確認します。",
|
||||
"Material": "材料",
|
||||
"Max": "最大",
|
||||
"Maximum": "最大",
|
||||
"Mesh body": "メッシュボディ",
|
||||
"Mid Radius": "中間半径",
|
||||
"Middle double-click": "中ボタンのダブルクリック",
|
||||
"Min": "最小",
|
||||
"Minimum": "最小",
|
||||
"Mirror Plane": "ミラー平面",
|
||||
"Mitre": "留め継ぎ",
|
||||
"Model Parameters": "モデルパラメータ",
|
||||
"Modeled": "モデル化済み",
|
||||
"More toolbar panels": "その他のツールバーパネル",
|
||||
"Motion": "モーション",
|
||||
"Mouse gestures": "マウス ジェスチャ",
|
||||
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "マウス ジェスチャは SolidWorks マウス モードでのみ機能します(ホイールで回転、Ctrl + ホイールでパン)。",
|
||||
"Move / Copy": "移動 / コピー",
|
||||
"Move Type": "移動タイプ",
|
||||
"Move down": "下へ移動",
|
||||
@@ -246,8 +305,11 @@
|
||||
"Not available yet": "まだ利用できません",
|
||||
"Notch": "ノッチ",
|
||||
"OUTPUT": "出力",
|
||||
"Of": "の値",
|
||||
"On your computer": "このコンピューター上",
|
||||
"Opacity": "不透明度",
|
||||
"Open GitHub draft": "GitHubの下書きを開く",
|
||||
"Open library...": "ライブラリを開く...",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "ローカルライブラリはデスクトップアプリで開いてください。このデザインのライブラリはここで書き出せます。",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Rust製オープンソース・パラメトリック3D CAD。ライセンス: MIT または Apache-2.0。",
|
||||
"Opens your browser and copies the report text for you to paste into GitHub.": "ブラウザーを開き、GitHubに貼り付ける報告本文をコピーします。",
|
||||
@@ -269,8 +331,10 @@
|
||||
"Picked": "選択したもの",
|
||||
"Picked point": "選択した点",
|
||||
"Pitch": "ピッチ",
|
||||
"Pivot": "ピボット",
|
||||
"Place at the origin instead": "代わりに原点に配置",
|
||||
"Plastic Rules": "樹脂ルール",
|
||||
"Play the joint's motion in the view": "ビューでジョイントの動きを再生",
|
||||
"Playback": "再生",
|
||||
"Plot": "グラフ",
|
||||
"Position": "位置",
|
||||
@@ -278,14 +342,18 @@
|
||||
"Position (G0)": "位置 (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "エッジ上の位置(始点が0、終点が1)",
|
||||
"Preferences › Design turns the offer back on": "環境設定 › デザイン で再びオンにできます",
|
||||
"Preview": "プレビュー",
|
||||
"Preview Row": "行をプレビュー",
|
||||
"Preview leaves the current design unchanged.": "プレビューは現在のデザインを変更しません。",
|
||||
"Profile / Edges": "プロファイル / エッジ",
|
||||
"Profiles or curves": "プロファイルまたは曲線",
|
||||
"Profiles/Curves": "プロファイル/曲線",
|
||||
"Quantity": "量",
|
||||
"RECENT": "最近使用した項目",
|
||||
"REFERENCE": "参照",
|
||||
"RULED SURFACE": "ルールドサーフェス",
|
||||
"RULES": "ルール",
|
||||
"Radius": "半径",
|
||||
"Radius Fit": "半径フィット",
|
||||
"Ratio": "比率",
|
||||
"Reattach selected face": "選択した面を再アタッチ",
|
||||
@@ -294,6 +362,8 @@
|
||||
"Rectangular": "長方形",
|
||||
"Reference face": "参照面",
|
||||
"Remove Tools": "ツールボディを削除",
|
||||
"Remove condition": "条件を削除",
|
||||
"Remove from Folder": "フォルダーから削除",
|
||||
"Remove from list": "リストから削除",
|
||||
"Remove override": "オーバーライドを削除",
|
||||
"Remove private design details or paths before sharing.": "共有前に、非公開の設計情報やファイルパスを削除してください。",
|
||||
@@ -301,6 +371,9 @@
|
||||
"Remove the column": "列を削除",
|
||||
"Remove the track": "トラックを削除",
|
||||
"Remove this point": "この点を削除",
|
||||
"Rename Feature": "フィーチャーの名前を変更",
|
||||
"Rename Folder": "フォルダーの名前を変更",
|
||||
"Replace existing library file": "既存のライブラリファイルを置き換える",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "バグを報告 — スクリーンショットを確認してGitHub下書きを作成",
|
||||
"Report text copied. Paste it into GitHub and submit your report there.": "報告本文をコピーしました。GitHubに貼り付けて投稿してください。",
|
||||
"Reports are public. A GitHub account is required.": "報告は公開されます。GitHubアカウントが必要です。",
|
||||
@@ -309,6 +382,7 @@
|
||||
"Restore View": "ビューを復元",
|
||||
"Restore outer boundary": "外側の境界を復元",
|
||||
"Restore the inherited component or physical material appearance": "コンポーネントまたは物理マテリアルから継承した外観に戻す",
|
||||
"Restore this studio's tools": "このスタジオのツールを復元",
|
||||
"Result": "結果",
|
||||
"Retake screenshot": "スクリーンショットを再取得",
|
||||
"Reverse": "反転",
|
||||
@@ -319,7 +393,13 @@
|
||||
"Revert Position": "位置を元に戻す",
|
||||
"Review the report and screenshot before opening GitHub.": "GitHubを開く前にレポートとスクリーンショットを確認してください。",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "GitHubで内容を確認し、Submit new issueをクリックすると公開されます。",
|
||||
"Rib Length": "リブの長さ",
|
||||
"Rib Offset": "リブのオフセット",
|
||||
"Rib Thickness": "リブの厚さ",
|
||||
"Ribs": "リブ",
|
||||
"Rim Edge": "リムエッジ",
|
||||
"Ring of tools on the right button": "右ボタンのツールリング",
|
||||
"Roll Back Here": "ここまでロールバック",
|
||||
"Roll History Marker Here": "履歴マーカーをここに移動",
|
||||
"Round": "丸め",
|
||||
"Rule": "ルール",
|
||||
@@ -338,13 +418,18 @@
|
||||
"Search": "検索",
|
||||
"Search commands": "コマンドを検索",
|
||||
"Search this library": "このライブラリを検索",
|
||||
"Second plane": "第2平面",
|
||||
"Section": "断面",
|
||||
"Sections (one sketch each)": "セクション (各1スケッチ)",
|
||||
"Select Other": "その他を選択",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "その他を選択: モデル上で左ボタンを長押し(または右クリック › その他を選択…)するとカーソル下の要素が一覧表示されます。同じ場所を再度クリックすると次の要素が選択されます。中ボタンをダブルクリックするとホームビューに戻ります(環境設定 › ナビゲーション)。",
|
||||
"Select bodies (a face picks its body)": "ボディを選択(面をクリックするとそのボディが選択されます)",
|
||||
"Select bodies or faces instead": "代わりにボディまたは面を選択",
|
||||
"Select edges": "エッジを選択",
|
||||
"Selection filter": "選択フィルター",
|
||||
"Semantic faces share their appearance across instances.": "セマンティック面はインスタンス間で外観を共有します。",
|
||||
"Send your report on GitHub": "GitHubでレポートを送信",
|
||||
"Shaded": "シェーディング",
|
||||
"Shape": "形状",
|
||||
"Sheet Rules": "板金ルール",
|
||||
"Show Agent Cursor": "エージェントカーソルを表示",
|
||||
@@ -362,6 +447,7 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "これらのデザインが保存される前にSolidCraftが終了しました。最後の自動保存ファイルを開くことができます。",
|
||||
"Specialise": "特殊操作",
|
||||
"Speed": "速度",
|
||||
"Splitting tool": "分割ツール",
|
||||
"Stand it up": "正立させる",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "正立させる: 取り消し可能な1つの移動フィーチャーとして、X軸周りに90度回転します。",
|
||||
"Stand this part up?": "このパーツを正立させますか?",
|
||||
@@ -372,16 +458,22 @@
|
||||
"Steps to reproduce (optional)": "再現手順(任意)",
|
||||
"Stitch the kept pieces": "残した部分をステッチ",
|
||||
"Stitch to both bodies": "両方のボディにステッチ",
|
||||
"Studio": "スタジオ",
|
||||
"Study": "スタディ",
|
||||
"Style": "スタイル",
|
||||
"Summary": "概要",
|
||||
"Suppress": "抑制",
|
||||
"Suppressed": "抑制",
|
||||
"Surface": "サーフェス",
|
||||
"Surface, or faces to turn over": "サーフェス、または裏返す面",
|
||||
"Swap Ends": "端点を入れ替え",
|
||||
"TRIM": "トリム",
|
||||
"Tangent (G1)": "接線 (G1)",
|
||||
"Tangent Chain": "正接チェーン",
|
||||
"Tangent chain": "接線チェーン",
|
||||
"Tap drill diameter": "タップ下穴径",
|
||||
"Target": "ターゲット",
|
||||
"Target Body": "ターゲットボディ",
|
||||
"Tear": "引き裂き",
|
||||
"Technical details": "技術情報",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "破線のスケッチは保存された平面ガイドです。新しい面を選択してアタッチメントを修復してください。",
|
||||
@@ -396,24 +488,31 @@
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "この子要素は共有されています。位置の変更は親のすべてのコピーに適用されます。",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "この面は安定したアタッチメントを提供できません。別の面を選択するか、固定平面を明示的に使用してください。",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "この画像はまだGitHubに添付されていません。コピーまたは保存し、GitHubで添付してください。",
|
||||
"To": "終了",
|
||||
"To Offset": "オフセットまで",
|
||||
"To edges": "終了側エッジ",
|
||||
"Tolerance": "公差",
|
||||
"Too close": "近すぎる",
|
||||
"Tool Bodies": "ツールボディ",
|
||||
"Tools": "ツール",
|
||||
"Tools, clockwise from the top": "上から時計回りにツール",
|
||||
"Trimming tool (plane, face or curves)": "トリムツール(平面、面、または曲線)",
|
||||
"Turn off section analysis before exporting the view.": "断面解析をオフにしてからエクスポートしてください。",
|
||||
"Type": "タイプ",
|
||||
"UNTRIM": "トリム解除",
|
||||
"Unit": "単位",
|
||||
"Unsuppress": "抑制を解除",
|
||||
"Updating the preview": "プレビューを更新中",
|
||||
"Use active author placement": "アクティブな作成元配置を使用",
|
||||
"Use fixed plane": "固定平面を使用",
|
||||
"Use saved fixed plane": "保存された固定平面を使用",
|
||||
"Use selected face as fixed plane": "選択した面を固定平面として使用",
|
||||
"Use the SolidWorks mouse mode": "SolidWorks マウス モードを使用",
|
||||
"User Parameters": "ユーザーパラメータ",
|
||||
"VIEW": "表示",
|
||||
"VIEWS": "ビュー",
|
||||
"Value": "値",
|
||||
"Variable Radius": "可変半径",
|
||||
"Velocity": "速度",
|
||||
"Vertices": "頂点",
|
||||
"View navigator": "ビューナビゲーター",
|
||||
@@ -427,13 +526,44 @@
|
||||
"When": "条件 (When)",
|
||||
"Width": "幅",
|
||||
"Will fail": "失敗します",
|
||||
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "マウス ジェスチャがオンのとき、右ボタンではビューを回転しません。押したまま動かすとツールリングが開くので、ツールのゾーンに触れてください。通常の右クリックでは引き続きメニューが開きます。",
|
||||
"X": "X",
|
||||
"X Angle": "X 角度",
|
||||
"X angle": "X角度",
|
||||
"X distance": "X距離",
|
||||
"Y": "Y",
|
||||
"Y Angle": "Y 角度",
|
||||
"Y angle": "Y角度",
|
||||
"Y distance": "Y距離",
|
||||
"Z": "Z",
|
||||
"Z Angle": "Z 角度",
|
||||
"Z Distance": "Z 距離",
|
||||
"Z angle": "Z角度",
|
||||
"Z distance": "Z距離",
|
||||
"Zebra": "ゼブラ",
|
||||
"ai.prefs.availability": "アシスタントの操作",
|
||||
"ai.prefs.background_commands": "エージェントのコマンドをバックグラウンドで実行",
|
||||
"ai.prefs.follow_camera": "カメラをスケッチに向ける",
|
||||
"ai.prefs.follow_camera_off": "カメラの自動追従はオフです。",
|
||||
"ai.prefs.follow_camera_on": "エージェントが作成するスケッチにカメラを向けます。",
|
||||
"ai.prefs.intro": "エージェント、権限、作業の表示方法を設定します。",
|
||||
"ai.prefs.make_default": "既定にする",
|
||||
"ai.prefs.no_agents": "エージェントはまだありません。追加するか、更新してインストール済みのエージェントを検索してください。",
|
||||
"ai.prefs.no_saved_approvals": "保存された許可はありません。",
|
||||
"ai.prefs.override_all_warning": "このエージェントは確認せずに設計を保存、上書き、書き出し、置換できます。",
|
||||
"ai.prefs.permission_all": "すべて許可",
|
||||
"ai.prefs.permission_all_summary": "利用可能なSolidCraftツールを確認せず実行します。",
|
||||
"ai.prefs.permission_ask": "変更前に確認",
|
||||
"ai.prefs.permission_ask_summary": "設計の変更とファイル操作を毎回確認します。",
|
||||
"ai.prefs.permission_edits": "モデル編集を許可",
|
||||
"ai.prefs.permission_edits_summary": "モデル編集は自動実行。ファイル操作や破壊的な操作は確認します。",
|
||||
"ai.prefs.permission_scope": "SolidCraftツールのみに適用されます。エージェントのシェルやファイル編集には、そのエージェント自身の権限設定が適用されます。",
|
||||
"ai.prefs.pointer_preview_hidden": "ポインターは非表示",
|
||||
"ai.prefs.pointer_preview_speed": "エージェントのポインター · {speed}",
|
||||
"ai.prefs.pointer_speed": "移動速度",
|
||||
"ai.prefs.privacy_note": "会話は選択したエージェントまたはエンドポイントにのみ送信されます。エンドポイントのキーはシステムのキーチェーンに保存されます。",
|
||||
"ai.prefs.show_pointer": "エージェントのポインターを表示",
|
||||
"ai.prefs.turn_limit_hint": "アシスタントが1件のメッセージで行える、設計を変更するツール呼び出しの上限です。",
|
||||
"angle, height": "角度, 高さ",
|
||||
"appearance.dielectric_ior": "誘電体の屈折率",
|
||||
"appearance.metallic": "金属度",
|
||||
@@ -889,19 +1019,57 @@
|
||||
"help.solid.revolve": "スケッチの輪郭を軸のまわりに回転させ、丸い立体を作ります。軸、つまみ、ボウルなど、中心線まわりに丸い部品に使います。",
|
||||
"help.solid.shell": "立体の中をくり抜き、指定した厚さの壁を残します。選んだ面を開口にすることもできます。箱、カバーなどの薄肉の部品に使います。",
|
||||
"help.third_party": "サードパーティライセンス",
|
||||
"home.back": "{name} に戻る",
|
||||
"home.crashes": "クラッシュレポート",
|
||||
"home.design_workspace": "デザイン",
|
||||
"home.documentation": "ドキュメント",
|
||||
"home.export": "エクスポート",
|
||||
"home.export_hint": "現在のデザインの出力形式を選択してください。",
|
||||
"home.export_json_hint": "編集可能なデザインとコマンド履歴",
|
||||
"home.export_mesh_hint": "印刷や共有用のメッシュ",
|
||||
"home.export_step_hint": "精密なソリッド形状",
|
||||
"home.import": "インポート…",
|
||||
"home.import_hint": "STEP、IGES、またはメッシュを新しいデザインに読み込みます",
|
||||
"home.in_this_browser": "このブラウザ内",
|
||||
"home.missing": "見つかりません",
|
||||
"home.month_01": "1月",
|
||||
"home.month_02": "2月",
|
||||
"home.month_03": "3月",
|
||||
"home.month_04": "4月",
|
||||
"home.month_05": "5月",
|
||||
"home.month_06": "6月",
|
||||
"home.month_07": "7月",
|
||||
"home.month_08": "8月",
|
||||
"home.month_09": "9月",
|
||||
"home.month_10": "10月",
|
||||
"home.month_11": "11月",
|
||||
"home.month_12": "12月",
|
||||
"home.new_inches": "新しいデザイン(インチ)",
|
||||
"home.new_inches_hint": "インチ · デザインで開きます",
|
||||
"home.new_mm": "新しいデザイン",
|
||||
"home.new_mm_hint": "ミリメートル · デザインで開きます",
|
||||
"home.open": "開く…",
|
||||
"home.opened_days_ago": "{count} 日前に開きました",
|
||||
"home.opened_hours_ago": "{count} 時間前に開きました",
|
||||
"home.opened_just_now": "たった今開きました",
|
||||
"home.opened_minutes_ago": "{count} 分前に開きました",
|
||||
"home.open_hint": ".solidcraft 形式のデザイン ({shortcut})",
|
||||
"home.open_now": "今すぐ開く",
|
||||
"home.opened_days_ago": "{count} 日前",
|
||||
"home.opened_hours_ago": "{count} 時間前",
|
||||
"home.opened_just_now": "たった今",
|
||||
"home.opened_minutes_ago": "{count} 分前",
|
||||
"home.opening": "開いています…",
|
||||
"home.properties": "デザインのプロパティ",
|
||||
"home.recent": "最近使用した項目",
|
||||
"home.recent_empty": "開いたり保存したデザインがここに表示されます。",
|
||||
"home.recovery_count": "復元可能なデザイン: {count} 件",
|
||||
"home.recovery_empty": "復元可能なデザインはありません。",
|
||||
"home.recovery_hint": "これらのデザインが保存される前に SolidCraft が終了しました。",
|
||||
"home.report_issue": "問題を報告",
|
||||
"home.samples": "サンプル",
|
||||
"home.settings": "設定",
|
||||
"home.short_date": "{month}{day}日",
|
||||
"home.shortcuts": "キーボードショートカット",
|
||||
"home.start_hint": "デザインを作成するか、最近使ったファイルを開くか、サンプルをご覧ください。",
|
||||
"home.whats_new": "新機能",
|
||||
"home.yesterday": "昨日",
|
||||
"install.button_tip": "SolidCraft をアプリとしてインストールします。専用のウィンドウで開き、オフラインでも使えます",
|
||||
"install.dock_1": "「ファイル」›「Dockに追加」を選びます。",
|
||||
"install.dock_2": "「追加」をクリックします。",
|
||||
@@ -959,8 +1127,15 @@
|
||||
"motion.to_end": "最後へ",
|
||||
"motion.to_start": "最初へ",
|
||||
"motion.track_name": "トラック {number}",
|
||||
"nav.alias.help": "Shift+Alt+左ドラッグで回転、Shift+Alt+中ドラッグでパン、Shift+Alt+右ドラッグでズーム。中ドラッグでもパン、右ドラッグで回転。",
|
||||
"nav.blender.help": "中ドラッグで回転、Shift+中ドラッグでパン、Ctrl+中ドラッグでズーム。右ドラッグではカメラは動きません。",
|
||||
"nav.free_orbit.help": "マウスと3Dマウスで、極点を越えて回転し、ロールを保ちます。\nShiftによる軸スナップは無効になります。",
|
||||
"nav.free_orbit.label": "自由回転",
|
||||
"nav.fusion.help": "中ドラッグでパン、Shift+中ドラッグで回転、右ドラッグで回転。",
|
||||
"nav.inventor.help": "中ドラッグでパン、Shift+中ドラッグで回転、Ctrl+中ドラッグでズーム、右ドラッグで回転。",
|
||||
"nav.openscad.help": "空きスペースで左ドラッグすると回転、Shift+左ドラッグで矩形選択、スケッチ ジオメトリはドラッグで編集。右または中ドラッグでパン、Shift+右または Shift+中ドラッグでズーム。",
|
||||
"nav.solidworks.help": "中ドラッグで回転、Ctrl+中ドラッグでパン、Shift+中ドラッグでズーム、右ドラッグで回転。",
|
||||
"nav.tinkercad.help": "右ドラッグで回転、中ドラッグまたは Shift+右ドラッグでパン。",
|
||||
"nav.touchpad.help": "Shift を押しながらポインターを動かすとパン、Alt/Option を押しながら動かすと回転します。マウスボタンは不要です。縦スクロールでズームします。",
|
||||
"nav.touchpad.label": "タッチパッド",
|
||||
"nav.touchpad.orbit": "回転(Alt/Option を押しながら移動、マウスボタン不要)",
|
||||
@@ -983,8 +1158,130 @@
|
||||
"pattern.flip_direction": "方向を反転",
|
||||
"pick a hole to seat it, or OK places it at the origin": "配置先の穴を選択するか、[OK]を押すと原点に配置します。",
|
||||
"pick one or two vertices, edges or faces": "1つまたは2つの頂点、エッジ、面を選択",
|
||||
"prefs.add_item": "項目を追加",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option でマニピュレーターの矢印も自由に動かす",
|
||||
"prefs.anti_aliasing_note": "輪郭のぎざぎざを滑らかにします。",
|
||||
"prefs.applies_on_restart": "再起動後に適用",
|
||||
"prefs.assistant": "アシスタント",
|
||||
"prefs.axes": "軸",
|
||||
"prefs.axis": "軸",
|
||||
"prefs.bottom": "下",
|
||||
"prefs.browser": "ブラウザー",
|
||||
"prefs.built_in": "組み込み",
|
||||
"prefs.button_left": "左ドラッグ",
|
||||
"prefs.button_middle": "中央ドラッグ",
|
||||
"prefs.button_right": "右ドラッグ",
|
||||
"prefs.choose_font": "フォントファイルを選択…",
|
||||
"prefs.click": "クリック",
|
||||
"prefs.contact_shading": "柔らかな接触陰影",
|
||||
"prefs.contact_shading_note": "部品が近接する部分に柔らかな陰影を付けます。",
|
||||
"prefs.context_menu": "コンテキストメニュー",
|
||||
"prefs.context_menu_ring": "コンテキストメニュー · 長押しで放射状メニュー",
|
||||
"prefs.cpu": "CPU(ソフトウェア)",
|
||||
"prefs.crash_reports": "クラッシュレポートを開く…",
|
||||
"prefs.crash_reports_note": "問題を再現する設計とコマンドを含むレポートをこのコンピューターに保存します。",
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "曲面",
|
||||
"prefs.curves": "曲面",
|
||||
"prefs.dead_zone_note": "斜線の帯はデッドゾーンです。この範囲内の動きは無視されます。",
|
||||
"prefs.default": "既定",
|
||||
"prefs.design": "設計の既定値",
|
||||
"prefs.design_note": "新しいデザインの初期設定です。開いているデザインはそれぞれの設定を保持します。",
|
||||
"prefs.devices": "3D マウス",
|
||||
"prefs.display": "表示",
|
||||
"prefs.edges": "エッジと陰影",
|
||||
"prefs.extend": "拡張",
|
||||
"prefs.extra_navigation": "何もない場所でCtrl+左ドラッグすると回転し、Spaceを押しながらドラッグすると移動します。",
|
||||
"prefs.follows_os": "OS の設定に従う",
|
||||
"prefs.font": "フォント",
|
||||
"prefs.font_fallback": "選択したフォントにない文字は組み込みフォントで表示し、ラベルが空の四角になるのを防ぎます。",
|
||||
"prefs.font_sample": "いろはにほへと · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "選択したフォントを読み込めませんでした。",
|
||||
"prefs.general": "一般",
|
||||
"prefs.general_note": "言語、保存、更新。",
|
||||
"prefs.gesture_orbit": "回転",
|
||||
"prefs.gesture_pan": "移動",
|
||||
"prefs.gesture_zoom": "ズーム",
|
||||
"prefs.graphics": "グラフィックス",
|
||||
"prefs.graphics_note": "速度と滑らかさのバランスを調整します。各選択肢の効果をサンプルで確認できます。",
|
||||
"prefs.graphics_presets_note": "プリセットは以下の設定をまとめて変更します。",
|
||||
"prefs.history": "履歴",
|
||||
"prefs.hold_stop_snapping": "押している間スナップを解除",
|
||||
"prefs.icons_only": "アイコンのみ",
|
||||
"prefs.imported_file": "読み込んだファイル",
|
||||
"prefs.input": "入力",
|
||||
"prefs.interface": "インターフェース",
|
||||
"prefs.interface_language": "表示言語",
|
||||
"prefs.interface_note": "SolidCraft の外観と各パネルの配置。",
|
||||
"prefs.interval_minutes": "{n} 分",
|
||||
"prefs.labels": "ラベル",
|
||||
"prefs.layout": "レイアウト",
|
||||
"prefs.layout_note": "コンパクトではパネルをウィンドウの端に固定します。ブラウザーの半透明表示はフローティングで使用できます。アシスタントの配置はアシスタントページで変更します。",
|
||||
"prefs.left": "左",
|
||||
"prefs.left_click": "左クリック",
|
||||
"prefs.live_input": "現在の入力",
|
||||
"prefs.live_preview": "ライブプレビュー",
|
||||
"prefs.minutes": "分",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "マウスとトラックパッド",
|
||||
"prefs.mouse_heading": "マウス",
|
||||
"prefs.mouse_mapping": "マウスの割り当て",
|
||||
"prefs.navigation": "ナビゲーション",
|
||||
"prefs.navigation_note": "マウス、トラックパッド、3Dマウスによるビューの操作。",
|
||||
"prefs.not_on_gpu": "この GPU は非対応",
|
||||
"prefs.on": "オン",
|
||||
"prefs.opaque": "不透明",
|
||||
"prefs.orbit_snap": "回転中にShiftを押すと軸にスナップします。",
|
||||
"prefs.orientation": "方向",
|
||||
"prefs.overall_speed": "全体の速度",
|
||||
"prefs.preview_choices": "選択した設定のプレビュー",
|
||||
"prefs.quality_default": "既定",
|
||||
"prefs.quality_fastest": "最速",
|
||||
"prefs.quality_slowest": "最も低速",
|
||||
"prefs.quality_smoother": "より滑らか",
|
||||
"prefs.reports_saved": "{n} 件保存済み",
|
||||
"prefs.right_click": "右クリック",
|
||||
"prefs.saving": "自動保存と復元",
|
||||
"prefs.scale_note": "ウィンドウ内では Ctrl + / Ctrl − でも変更できます。100% のボタンは 28 px です。",
|
||||
"prefs.scale_text": "拡大率と文字",
|
||||
"prefs.select": "選択",
|
||||
"prefs.select_one_item": "項目を 1 つ選択",
|
||||
"prefs.selecting": "選択",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift は追加のみ、Ctrl / Cmd は選択を切り替え",
|
||||
"prefs.shift_add_note": "オン:Shift は追加、Ctrl / Cmd は選択を切り替えます。オフ(既定):どちらも選択を切り替えます。",
|
||||
"prefs.shortcuts": "ショートカット",
|
||||
"prefs.sketch_selection": "スケッチと選択",
|
||||
"prefs.sketch_selection_heading": "スケッチと選択",
|
||||
"prefs.sketch_selection_note": "スケッチのスナップとクリックによる選択。",
|
||||
"prefs.sketching": "スケッチ",
|
||||
"prefs.snap_drawing_note": "スケッチの描画中または点のドラッグ中に使用します。",
|
||||
"prefs.source": "入力元",
|
||||
"prefs.speed": "速度",
|
||||
"prefs.stand_up_note": "STEP または IGES ファイルを Z 上向きで開いた後、X 軸周りに 90° 回転するか確認します。",
|
||||
"prefs.status_done": "完了",
|
||||
"prefs.status_error": "エラー",
|
||||
"prefs.status_selected": "選択中",
|
||||
"prefs.status_warning": "警告",
|
||||
"prefs.supersampling_brief": "大きく描画して縮小するため、非常に鮮明になりますが負荷も増えます。",
|
||||
"prefs.tab_accent_note": "アクセント色はアクティブなタブに従います",
|
||||
"prefs.theme_file": "テーマファイル",
|
||||
"prefs.timeline_half_hour": "−30 分",
|
||||
"prefs.timeline_now": "現在",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
"prefs.toggle_item": "項目を追加、または選択から外す",
|
||||
"prefs.toolbar": "ツールバー",
|
||||
"prefs.translucent": "半透明",
|
||||
"prefs.under_browser": "ブラウザー下",
|
||||
"prefs.under_cursor": "カーソル下",
|
||||
"prefs.units_precision": "単位と精度",
|
||||
"prefs.updates": "更新",
|
||||
"prefs.view": "ビューの動作",
|
||||
"prefs.viewport": "ビューポート",
|
||||
"prefs.wheel": "ホイール",
|
||||
"prefs.wheel_trackpad": "ホイールとトラックパッド",
|
||||
"prefs.workspace": "ワークスペース",
|
||||
"prefs.zoom_toward_cursor": "カーソルに向かってズーム",
|
||||
"properties.title": "プロパティ: {name}",
|
||||
"recovery.days_ago": "{count}日前",
|
||||
"recovery.detail": "{file} · {features}フィーチャー · 自動保存: {when}",
|
||||
@@ -1055,7 +1352,7 @@
|
||||
"search.suggest": "{label} ですか?",
|
||||
"search.try": "コマンド名、このデザインのフィーチャー、またはパラメーターを入力します。値を指定して実行するには、IDとJSONオブジェクトを入力します:{example}",
|
||||
"search.valid": "有効",
|
||||
"settings.display_quality_note": "細かいほど曲面とエッジが滑らかに表示されますが、メッシュの生成に時間がかかります。変わるのはビューポートだけで、設計やエクスポートは変わりません。",
|
||||
"settings.display_quality_note": "表示だけに影響します。設計や書き出しは変わりません。",
|
||||
"settings.frame_rate_note": "ビューの右下に 1 秒あたりのフレーム数を表示します。オンの間、ビューは常に再描画されます。",
|
||||
"settings.measure_axis_distances": "計測のX・Y・Z距離を表示",
|
||||
"settings.measure_axis_distances_help": "計測ダイアログを開いている間、2点間距離に加えてワールドX・Y・Z軸方向の寸法線と距離を表示します。",
|
||||
|
||||
+42
-4
@@ -859,19 +859,57 @@
|
||||
"help.mit": "MIT 라이선스",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "타사 라이선스",
|
||||
"home.back": "{name}(으)로 돌아가기",
|
||||
"home.crashes": "충돌 보고서",
|
||||
"home.design_workspace": "설계",
|
||||
"home.documentation": "문서",
|
||||
"home.export": "내보내기",
|
||||
"home.export_hint": "현재 설계의 형식을 선택하세요.",
|
||||
"home.export_json_hint": "편집 가능한 설계 및 명령 기록",
|
||||
"home.export_mesh_hint": "인쇄 또는 공유용 메시",
|
||||
"home.export_step_hint": "정밀한 솔리드 형상",
|
||||
"home.import": "가져오기…",
|
||||
"home.import_hint": "STEP, IGES 또는 메시를 새 설계로 가져오기",
|
||||
"home.in_this_browser": "이 브라우저에 저장됨",
|
||||
"home.missing": "찾을 수 없음",
|
||||
"home.month_01": "1월",
|
||||
"home.month_02": "2월",
|
||||
"home.month_03": "3월",
|
||||
"home.month_04": "4월",
|
||||
"home.month_05": "5월",
|
||||
"home.month_06": "6월",
|
||||
"home.month_07": "7월",
|
||||
"home.month_08": "8월",
|
||||
"home.month_09": "9월",
|
||||
"home.month_10": "10월",
|
||||
"home.month_11": "11월",
|
||||
"home.month_12": "12월",
|
||||
"home.new_inches": "새 설계(인치)",
|
||||
"home.new_inches_hint": "인치 · 설계 작업 공간에서 열기",
|
||||
"home.new_mm": "새 설계",
|
||||
"home.new_mm_hint": "밀리미터 · 설계 작업 공간에서 열기",
|
||||
"home.open": "열기…",
|
||||
"home.opened_days_ago": "{count}일 전에 열림",
|
||||
"home.opened_hours_ago": "{count}시간 전에 열림",
|
||||
"home.opened_just_now": "방금 열림",
|
||||
"home.opened_minutes_ago": "{count}분 전에 열림",
|
||||
"home.open_hint": ".solidcraft 설계 ({shortcut})",
|
||||
"home.open_now": "지금 열기",
|
||||
"home.opened_days_ago": "{count}일 전",
|
||||
"home.opened_hours_ago": "{count}시간 전",
|
||||
"home.opened_just_now": "방금",
|
||||
"home.opened_minutes_ago": "{count}분 전",
|
||||
"home.opening": "여는 중…",
|
||||
"home.properties": "설계 속성",
|
||||
"home.recent": "최근 항목",
|
||||
"home.recent_empty": "열거나 저장한 설계가 여기에 표시됩니다.",
|
||||
"home.recovery_count": "복구 가능한 설계: {count}개",
|
||||
"home.recovery_empty": "복구 가능한 설계가 없습니다.",
|
||||
"home.recovery_hint": "이 설계가 저장되기 전에 SolidCraft가 종료되었습니다.",
|
||||
"home.report_issue": "문제 신고",
|
||||
"home.samples": "샘플",
|
||||
"home.settings": "설정",
|
||||
"home.short_date": "{month} {day}일",
|
||||
"home.shortcuts": "키보드 단축키",
|
||||
"home.start_hint": "설계를 만들거나, 최근 파일을 열거나, 예제를 살펴보세요.",
|
||||
"home.whats_new": "새로운 기능",
|
||||
"home.yesterday": "어제",
|
||||
"install.button_tip": "SolidCraft를 앱으로 설치합니다. 별도 창에서 열리며 오프라인에서도 작동합니다",
|
||||
"install.dock_1": "파일 › Dock에 추가를 선택합니다.",
|
||||
"install.dock_2": "추가를 클릭합니다.",
|
||||
|
||||
+42
-4
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Lesen MIT",
|
||||
"help.notice": "NOTIS",
|
||||
"help.third_party": "Lesen pihak ketiga",
|
||||
"home.back": "Kembali ke {name}",
|
||||
"home.crashes": "Laporan ranap",
|
||||
"home.design_workspace": "Reka Bentuk",
|
||||
"home.documentation": "Dokumentasi",
|
||||
"home.export": "Eksport",
|
||||
"home.export_hint": "Pilih format untuk reka bentuk semasa.",
|
||||
"home.export_json_hint": "Reka bentuk boleh sunting dan sejarah perintah",
|
||||
"home.export_mesh_hint": "Jejaring untuk pencetakan atau perkongsian",
|
||||
"home.export_step_hint": "Geometri pepejal yang tepat",
|
||||
"home.import": "Import…",
|
||||
"home.import_hint": "STEP, IGES atau jejaring ke dalam reka bentuk baharu",
|
||||
"home.in_this_browser": "dalam pelayar ini",
|
||||
"home.missing": "tiada",
|
||||
"home.month_01": "Jan",
|
||||
"home.month_02": "Feb",
|
||||
"home.month_03": "Mac",
|
||||
"home.month_04": "Apr",
|
||||
"home.month_05": "Mei",
|
||||
"home.month_06": "Jun",
|
||||
"home.month_07": "Jul",
|
||||
"home.month_08": "Ogo",
|
||||
"home.month_09": "Sep",
|
||||
"home.month_10": "Okt",
|
||||
"home.month_11": "Nov",
|
||||
"home.month_12": "Dis",
|
||||
"home.new_inches": "Reka bentuk baharu dalam inci",
|
||||
"home.new_inches_hint": "Inci · dibuka dalam Reka Bentuk",
|
||||
"home.new_mm": "Reka bentuk baharu",
|
||||
"home.new_mm_hint": "Milimeter · dibuka dalam Reka Bentuk",
|
||||
"home.open": "Buka…",
|
||||
"home.opened_days_ago": "Dibuka {count} hari lalu",
|
||||
"home.opened_hours_ago": "Dibuka {count} jam lalu",
|
||||
"home.opened_just_now": "Baru dibuka",
|
||||
"home.opened_minutes_ago": "Dibuka {count} minit lalu",
|
||||
"home.open_hint": "Reka bentuk .solidcraft ({shortcut})",
|
||||
"home.open_now": "Buka sekarang",
|
||||
"home.opened_days_ago": "{count} hari lalu",
|
||||
"home.opened_hours_ago": "{count} jam lalu",
|
||||
"home.opened_just_now": "Baru sahaja",
|
||||
"home.opened_minutes_ago": "{count} minit lalu",
|
||||
"home.opening": "membuka…",
|
||||
"home.properties": "Sifat reka bentuk",
|
||||
"home.recent": "Terkini",
|
||||
"home.recent_empty": "Reka bentuk yang anda buka atau simpan dipaparkan di sini.",
|
||||
"home.recovery_count": "Reka bentuk boleh dipulihkan: {count}",
|
||||
"home.recovery_empty": "Tiada reka bentuk yang boleh dipulihkan.",
|
||||
"home.recovery_hint": "SolidCraft ditutup sebelum reka bentuk ini disimpan.",
|
||||
"home.report_issue": "Laporkan Isu",
|
||||
"home.samples": "Contoh",
|
||||
"home.settings": "Tetapan",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Pintasan papan kekunci",
|
||||
"home.start_hint": "Cipta reka bentuk, buka fail terkini atau terokai contoh.",
|
||||
"home.whats_new": "Apa yang baharu",
|
||||
"home.yesterday": "Semalam",
|
||||
"install.button_tip": "Pasang SolidCraft: tetingkap sendiri dan boleh guna di luar talian",
|
||||
"install.dock_1": "Pilih File › Add to Dock.",
|
||||
"install.dock_2": "Klik Add.",
|
||||
|
||||
+42
-4
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT-licentie",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenties van derden",
|
||||
"home.back": "Terug naar {name}",
|
||||
"home.crashes": "Crashrapporten",
|
||||
"home.design_workspace": "Ontwerp",
|
||||
"home.documentation": "Documentatie",
|
||||
"home.export": "Exporteren",
|
||||
"home.export_hint": "Kies een indeling voor het huidige ontwerp.",
|
||||
"home.export_json_hint": "Bewerkbaar ontwerp en opdrachtgeschiedenis",
|
||||
"home.export_mesh_hint": "Mesh om af te drukken of te delen",
|
||||
"home.export_step_hint": "Nauwkeurige volumegeometrie",
|
||||
"home.import": "Importeren…",
|
||||
"home.import_hint": "STEP, IGES of een mesh in een nieuw ontwerp",
|
||||
"home.in_this_browser": "in deze browser",
|
||||
"home.missing": "ontbreekt",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mrt.",
|
||||
"home.month_04": "apr.",
|
||||
"home.month_05": "mei",
|
||||
"home.month_06": "jun.",
|
||||
"home.month_07": "jul.",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "sep.",
|
||||
"home.month_10": "okt.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Nieuw ontwerp in inches",
|
||||
"home.new_inches_hint": "Inches · opent in Ontwerp",
|
||||
"home.new_mm": "Nieuw ontwerp",
|
||||
"home.new_mm_hint": "Millimeters · opent in Ontwerp",
|
||||
"home.open": "Openen…",
|
||||
"home.opened_days_ago": "{count} dagen geleden geopend",
|
||||
"home.opened_hours_ago": "{count} u geleden geopend",
|
||||
"home.opened_just_now": "Zojuist geopend",
|
||||
"home.opened_minutes_ago": "{count} min geleden geopend",
|
||||
"home.open_hint": "Een .solidcraft-ontwerp ({shortcut})",
|
||||
"home.open_now": "Momenteel geopend",
|
||||
"home.opened_days_ago": "{count} dagen geleden",
|
||||
"home.opened_hours_ago": "{count} u geleden",
|
||||
"home.opened_just_now": "Zojuist",
|
||||
"home.opened_minutes_ago": "{count} min geleden",
|
||||
"home.opening": "openen…",
|
||||
"home.properties": "Ontwerpeigenschappen",
|
||||
"home.recent": "Recent",
|
||||
"home.recent_empty": "Ontwerpen die u opent of opslaat, verschijnen hier.",
|
||||
"home.recovery_count": "Herstelbare ontwerpen: {count}",
|
||||
"home.recovery_empty": "Geen herstelbare ontwerpen.",
|
||||
"home.recovery_hint": "SolidCraft is gesloten voordat deze ontwerpen werden opgeslagen.",
|
||||
"home.report_issue": "Probleem melden",
|
||||
"home.samples": "Voorbeelden",
|
||||
"home.settings": "Instellingen",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Sneltoetsen",
|
||||
"home.start_hint": "Maak een ontwerp, open een recent bestand of bekijk een voorbeeld.",
|
||||
"home.whats_new": "Wat is er nieuw",
|
||||
"home.yesterday": "Gisteren",
|
||||
"install.button_tip": "Installeer SolidCraft als app: deze opent in een eigen venster en werkt offline",
|
||||
"install.dock_1": "Kies Bestand › Toevoegen aan Dock.",
|
||||
"install.dock_2": "Klik op Voeg toe.",
|
||||
|
||||
+42
-4
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Licencja MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licencje stron trzecich",
|
||||
"home.back": "Wróć do {name}",
|
||||
"home.crashes": "Raporty awarii",
|
||||
"home.design_workspace": "Projektowanie",
|
||||
"home.documentation": "Dokumentacja",
|
||||
"home.export": "Eksportuj",
|
||||
"home.export_hint": "Wybierz format dla bieżącego projektu.",
|
||||
"home.export_json_hint": "Edytowalny projekt i historia poleceń",
|
||||
"home.export_mesh_hint": "Siatka do druku lub udostępniania",
|
||||
"home.export_step_hint": "Precyzyjna geometria bryłowa",
|
||||
"home.import": "Importuj…",
|
||||
"home.import_hint": "STEP, IGES lub siatka do nowego projektu",
|
||||
"home.in_this_browser": "w tej przeglądarce",
|
||||
"home.missing": "brak",
|
||||
"home.month_01": "sty",
|
||||
"home.month_02": "lut",
|
||||
"home.month_03": "mar",
|
||||
"home.month_04": "kwi",
|
||||
"home.month_05": "maj",
|
||||
"home.month_06": "cze",
|
||||
"home.month_07": "lip",
|
||||
"home.month_08": "sie",
|
||||
"home.month_09": "wrz",
|
||||
"home.month_10": "paź",
|
||||
"home.month_11": "lis",
|
||||
"home.month_12": "gru",
|
||||
"home.new_inches": "Nowy projekt w calach",
|
||||
"home.new_inches_hint": "Cale · otwiera w obszarze Projektowanie",
|
||||
"home.new_mm": "Nowy projekt",
|
||||
"home.new_mm_hint": "Milimetry · otwiera w obszarze Projektowanie",
|
||||
"home.open": "Otwórz…",
|
||||
"home.opened_days_ago": "Otwarto {count} dni temu",
|
||||
"home.opened_hours_ago": "Otwarto {count} godz. temu",
|
||||
"home.opened_just_now": "Otwarto przed chwilą",
|
||||
"home.opened_minutes_ago": "Otwarto {count} min temu",
|
||||
"home.open_hint": "Projekt .solidcraft ({shortcut})",
|
||||
"home.open_now": "Otwórz teraz",
|
||||
"home.opened_days_ago": "{count} dni temu",
|
||||
"home.opened_hours_ago": "{count} godz. temu",
|
||||
"home.opened_just_now": "Przed chwilą",
|
||||
"home.opened_minutes_ago": "{count} min temu",
|
||||
"home.opening": "otwieranie…",
|
||||
"home.properties": "Właściwości projektu",
|
||||
"home.recent": "Ostatnie",
|
||||
"home.recent_empty": "Tutaj pojawią się projekty, które otworzysz lub zapiszesz.",
|
||||
"home.recovery_count": "Projekty do odzyskania: {count}",
|
||||
"home.recovery_empty": "Brak projektów do odzyskania.",
|
||||
"home.recovery_hint": "SolidCraft zamknął się przed zapisaniem tych projektów.",
|
||||
"home.report_issue": "Zgłoś problem",
|
||||
"home.samples": "Przykłady",
|
||||
"home.settings": "Ustawienia",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Skróty klawiszowe",
|
||||
"home.start_hint": "Utwórz projekt, otwórz ostatni plik lub obejrzyj przykład.",
|
||||
"home.whats_new": "Co nowego",
|
||||
"home.yesterday": "Wczoraj",
|
||||
"install.button_tip": "Zainstaluj SolidCraft jako aplikację: otwiera się we własnym oknie i działa offline",
|
||||
"install.dock_1": "Wybierz Plik › Dodaj do Docka.",
|
||||
"install.dock_2": "Kliknij Dodaj.",
|
||||
|
||||
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Licença MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenças de terceiros",
|
||||
"home.back": "Voltar para {name}",
|
||||
"home.crashes": "Relatórios de falhas",
|
||||
"home.design_workspace": "Projeto",
|
||||
"home.documentation": "Documentação",
|
||||
"home.export": "Exportar",
|
||||
"home.export_hint": "Escolha um formato para o projeto atual.",
|
||||
"home.export_json_hint": "Projeto editável e histórico de comandos",
|
||||
"home.export_mesh_hint": "Malha para impressão ou compartilhamento",
|
||||
"home.export_step_hint": "Geometria sólida precisa",
|
||||
"home.import": "Importar…",
|
||||
"home.import_hint": "STEP, IGES ou malha em um novo projeto",
|
||||
"home.in_this_browser": "neste navegador",
|
||||
"home.missing": "ausente",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "fev.",
|
||||
"home.month_03": "mar.",
|
||||
"home.month_04": "abr.",
|
||||
"home.month_05": "mai.",
|
||||
"home.month_06": "jun.",
|
||||
"home.month_07": "jul.",
|
||||
"home.month_08": "ago.",
|
||||
"home.month_09": "set.",
|
||||
"home.month_10": "out.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dez.",
|
||||
"home.new_inches": "Novo projeto em polegadas",
|
||||
"home.new_inches_hint": "Polegadas · abre em Projeto",
|
||||
"home.new_mm": "Novo projeto",
|
||||
"home.new_mm_hint": "Milímetros · abre em Projeto",
|
||||
"home.open": "Abrir…",
|
||||
"home.opened_days_ago": "Aberto há {count} dias",
|
||||
"home.opened_hours_ago": "Aberto há {count} h",
|
||||
"home.opened_just_now": "Aberto agora mesmo",
|
||||
"home.opened_minutes_ago": "Aberto há {count} min",
|
||||
"home.open_hint": "Um projeto .solidcraft ({shortcut})",
|
||||
"home.open_now": "Abrir agora",
|
||||
"home.opened_days_ago": "Há {count} dias",
|
||||
"home.opened_hours_ago": "Há {count} h",
|
||||
"home.opened_just_now": "Agora mesmo",
|
||||
"home.opened_minutes_ago": "Há {count} min",
|
||||
"home.opening": "abrindo…",
|
||||
"home.properties": "Propriedades do projeto",
|
||||
"home.recent": "Recentes",
|
||||
"home.recent_empty": "Os projetos que você abrir ou salvar aparecerão aqui.",
|
||||
"home.recovery_count": "Projetos recuperáveis: {count}",
|
||||
"home.recovery_empty": "Nenhum projeto recuperável.",
|
||||
"home.recovery_hint": "O SolidCraft foi fechado antes que estes projetos fossem salvos.",
|
||||
"home.report_issue": "Relatar um problema",
|
||||
"home.samples": "Exemplos",
|
||||
"home.settings": "Configurações",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Atalhos de teclado",
|
||||
"home.start_hint": "Crie um projeto, abra um arquivo recente ou explore um exemplo.",
|
||||
"home.whats_new": "Novidades",
|
||||
"home.yesterday": "Ontem",
|
||||
"install.button_tip": "Instale o SolidCraft como aplicativo: ele abre em sua própria janela e funciona offline",
|
||||
"install.dock_1": "Escolha Arquivo › Adicionar ao Dock.",
|
||||
"install.dock_2": "Clique em Adicionar.",
|
||||
|
||||
+42
-4
@@ -856,19 +856,57 @@
|
||||
"help.mit": "Licența MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licențe terțe",
|
||||
"home.back": "Înapoi la {name}",
|
||||
"home.crashes": "Rapoarte de blocare",
|
||||
"home.design_workspace": "Proiectare",
|
||||
"home.documentation": "Documentație",
|
||||
"home.export": "Exportă",
|
||||
"home.export_hint": "Alege un format pentru proiectul curent.",
|
||||
"home.export_json_hint": "Proiect editabil și istoricul comenzilor",
|
||||
"home.export_mesh_hint": "Plasă pentru imprimare sau partajare",
|
||||
"home.export_step_hint": "Geometrie solidă precisă",
|
||||
"home.import": "Importă…",
|
||||
"home.import_hint": "STEP, IGES sau plasă într-un proiect nou",
|
||||
"home.in_this_browser": "în acest browser",
|
||||
"home.missing": "lipsă",
|
||||
"home.month_01": "ian.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mar.",
|
||||
"home.month_04": "apr.",
|
||||
"home.month_05": "mai",
|
||||
"home.month_06": "iun.",
|
||||
"home.month_07": "iul.",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "sept.",
|
||||
"home.month_10": "oct.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Proiect nou în inci",
|
||||
"home.new_inches_hint": "Inci · se deschide în Proiectare",
|
||||
"home.new_mm": "Proiect nou",
|
||||
"home.new_mm_hint": "Milimetri · se deschide în Proiectare",
|
||||
"home.open": "Deschide…",
|
||||
"home.opened_days_ago": "Deschis acum {count} zile",
|
||||
"home.opened_hours_ago": "Deschis acum {count} h",
|
||||
"home.opened_just_now": "Deschis chiar acum",
|
||||
"home.opened_minutes_ago": "Deschis acum {count} min",
|
||||
"home.open_hint": "Un proiect .solidcraft ({shortcut})",
|
||||
"home.open_now": "Deschis acum",
|
||||
"home.opened_days_ago": "Acum {count} zile",
|
||||
"home.opened_hours_ago": "Acum {count} h",
|
||||
"home.opened_just_now": "Chiar acum",
|
||||
"home.opened_minutes_ago": "Acum {count} min",
|
||||
"home.opening": "se deschide…",
|
||||
"home.properties": "Proprietățile proiectului",
|
||||
"home.recent": "Recente",
|
||||
"home.recent_empty": "Proiectele pe care le deschizi sau le salvezi apar aici.",
|
||||
"home.recovery_count": "Proiecte recuperabile: {count}",
|
||||
"home.recovery_empty": "Nu există proiecte recuperabile.",
|
||||
"home.recovery_hint": "SolidCraft s-a închis înainte ca aceste proiecte să fie salvate.",
|
||||
"home.report_issue": "Raportează o problemă",
|
||||
"home.samples": "Exemple",
|
||||
"home.settings": "Setări",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Scurtături de la tastatură",
|
||||
"home.start_hint": "Creează un proiect, deschide un fișier recent sau explorează un exemplu.",
|
||||
"home.whats_new": "Noutăți",
|
||||
"home.yesterday": "Ieri",
|
||||
"install.button_tip": "Instalați SolidCraft ca aplicație: se deschide în propria fereastră și funcționează offline",
|
||||
"install.dock_1": "Alegeți Fișier › Adaugă în Dock.",
|
||||
"install.dock_2": "Faceți clic pe Adaugă.",
|
||||
|
||||
+42
-4
@@ -856,19 +856,57 @@
|
||||
"help.mit": "Лицензия MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Лицензии сторонних компонентов",
|
||||
"home.back": "Назад к {name}",
|
||||
"home.crashes": "Отчёты о сбоях",
|
||||
"home.design_workspace": "Проектирование",
|
||||
"home.documentation": "Документация",
|
||||
"home.export": "Экспорт",
|
||||
"home.export_hint": "Выберите формат для текущего проекта.",
|
||||
"home.export_json_hint": "Редактируемый проект и история команд",
|
||||
"home.export_mesh_hint": "Сетка для печати или обмена",
|
||||
"home.export_step_hint": "Точная твердотельная геометрия",
|
||||
"home.import": "Импорт…",
|
||||
"home.import_hint": "STEP, IGES или сетка в новый проект",
|
||||
"home.in_this_browser": "в этом браузере",
|
||||
"home.missing": "отсутствует",
|
||||
"home.month_01": "янв.",
|
||||
"home.month_02": "февр.",
|
||||
"home.month_03": "мар.",
|
||||
"home.month_04": "апр.",
|
||||
"home.month_05": "мая",
|
||||
"home.month_06": "июн.",
|
||||
"home.month_07": "июл.",
|
||||
"home.month_08": "авг.",
|
||||
"home.month_09": "сент.",
|
||||
"home.month_10": "окт.",
|
||||
"home.month_11": "нояб.",
|
||||
"home.month_12": "дек.",
|
||||
"home.new_inches": "Новый проект в дюймах",
|
||||
"home.new_inches_hint": "Дюймы · откроется в рабочей области «Проектирование»",
|
||||
"home.new_mm": "Новый проект",
|
||||
"home.new_mm_hint": "Миллиметры · откроется в рабочей области «Проектирование»",
|
||||
"home.open": "Открыть…",
|
||||
"home.opened_days_ago": "Открыт {count} дн. назад",
|
||||
"home.opened_hours_ago": "Открыт {count} ч назад",
|
||||
"home.opened_just_now": "Открыт только что",
|
||||
"home.opened_minutes_ago": "Открыт {count} мин назад",
|
||||
"home.open_hint": "Проект .solidcraft ({shortcut})",
|
||||
"home.open_now": "Открыть сейчас",
|
||||
"home.opened_days_ago": "{count} дн. назад",
|
||||
"home.opened_hours_ago": "{count} ч назад",
|
||||
"home.opened_just_now": "Только что",
|
||||
"home.opened_minutes_ago": "{count} мин назад",
|
||||
"home.opening": "открывается…",
|
||||
"home.properties": "Свойства проекта",
|
||||
"home.recent": "Недавние",
|
||||
"home.recent_empty": "Здесь появятся проекты, которые вы откроете или сохраните.",
|
||||
"home.recovery_count": "Проектов для восстановления: {count}",
|
||||
"home.recovery_empty": "Нет проектов для восстановления.",
|
||||
"home.recovery_hint": "SolidCraft закрылся до сохранения этих проектов.",
|
||||
"home.report_issue": "Сообщить о проблеме",
|
||||
"home.samples": "Примеры",
|
||||
"home.settings": "Настройки",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Сочетания клавиш",
|
||||
"home.start_hint": "Создайте проект, откройте недавний файл или изучите пример.",
|
||||
"home.whats_new": "Что нового",
|
||||
"home.yesterday": "Вчера",
|
||||
"install.button_tip": "Установить SolidCraft как приложение: отдельное окно и работа без сети",
|
||||
"install.dock_1": "Выберите «Файл» › «Добавить в Dock».",
|
||||
"install.dock_2": "Нажмите «Добавить».",
|
||||
|
||||
+42
-4
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT-licens",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenser från tredje part",
|
||||
"home.back": "Tillbaka till {name}",
|
||||
"home.crashes": "Kraschrapporter",
|
||||
"home.design_workspace": "Konstruktion",
|
||||
"home.documentation": "Dokumentation",
|
||||
"home.export": "Exportera",
|
||||
"home.export_hint": "Välj ett format för den aktuella konstruktionen.",
|
||||
"home.export_json_hint": "Redigerbar konstruktion och kommandohistorik",
|
||||
"home.export_mesh_hint": "Mesh för utskrift eller delning",
|
||||
"home.export_step_hint": "Exakt solidgeometri",
|
||||
"home.import": "Importera…",
|
||||
"home.import_hint": "STEP, IGES eller mesh till en ny konstruktion",
|
||||
"home.in_this_browser": "i den här webbläsaren",
|
||||
"home.missing": "saknas",
|
||||
"home.month_01": "jan.",
|
||||
"home.month_02": "feb.",
|
||||
"home.month_03": "mars",
|
||||
"home.month_04": "apr.",
|
||||
"home.month_05": "maj",
|
||||
"home.month_06": "juni",
|
||||
"home.month_07": "juli",
|
||||
"home.month_08": "aug.",
|
||||
"home.month_09": "sep.",
|
||||
"home.month_10": "okt.",
|
||||
"home.month_11": "nov.",
|
||||
"home.month_12": "dec.",
|
||||
"home.new_inches": "Ny konstruktion i tum",
|
||||
"home.new_inches_hint": "Tum · öppnas i Konstruktion",
|
||||
"home.new_mm": "Ny konstruktion",
|
||||
"home.new_mm_hint": "Millimeter · öppnas i Konstruktion",
|
||||
"home.open": "Öppna…",
|
||||
"home.opened_days_ago": "Öppnades för {count} dagar sedan",
|
||||
"home.opened_hours_ago": "Öppnades för {count} h sedan",
|
||||
"home.opened_just_now": "Öppnades nyss",
|
||||
"home.opened_minutes_ago": "Öppnades för {count} min sedan",
|
||||
"home.open_hint": "En .solidcraft-konstruktion ({shortcut})",
|
||||
"home.open_now": "Öppen nu",
|
||||
"home.opened_days_ago": "För {count} dagar sedan",
|
||||
"home.opened_hours_ago": "För {count} h sedan",
|
||||
"home.opened_just_now": "Nyss",
|
||||
"home.opened_minutes_ago": "För {count} min sedan",
|
||||
"home.opening": "öppnar…",
|
||||
"home.properties": "Konstruktionsegenskaper",
|
||||
"home.recent": "Senaste",
|
||||
"home.recent_empty": "Konstruktioner som du öppnar eller sparar visas här.",
|
||||
"home.recovery_count": "Återställningsbara konstruktioner: {count}",
|
||||
"home.recovery_empty": "Inga återställningsbara konstruktioner.",
|
||||
"home.recovery_hint": "SolidCraft stängdes innan dessa konstruktioner sparades.",
|
||||
"home.report_issue": "Rapportera ett problem",
|
||||
"home.samples": "Exempel",
|
||||
"home.settings": "Inställningar",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Kortkommandon",
|
||||
"home.start_hint": "Skapa en konstruktion, öppna en nyligen använd fil eller utforska ett exempel.",
|
||||
"home.whats_new": "Nyheter",
|
||||
"home.yesterday": "I går",
|
||||
"install.button_tip": "Installera SolidCraft som en app: den öppnas i ett eget fönster och fungerar offline",
|
||||
"install.dock_1": "Välj Arkiv › Lägg till i Dock.",
|
||||
"install.dock_2": "Klicka på Lägg till.",
|
||||
|
||||
+42
-4
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT lisansı",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Üçüncü taraf lisansları",
|
||||
"home.back": "{name} tasarımına dön",
|
||||
"home.crashes": "Çökme raporları",
|
||||
"home.design_workspace": "Tasarım",
|
||||
"home.documentation": "Belgeler",
|
||||
"home.export": "Dışa aktar",
|
||||
"home.export_hint": "Geçerli tasarım için bir biçim seçin.",
|
||||
"home.export_json_hint": "Düzenlenebilir tasarım ve komut geçmişi",
|
||||
"home.export_mesh_hint": "Yazdırma veya paylaşma için ağ",
|
||||
"home.export_step_hint": "Hassas katı geometrisi",
|
||||
"home.import": "İçe aktar…",
|
||||
"home.import_hint": "STEP, IGES veya ağı yeni bir tasarıma aktar",
|
||||
"home.in_this_browser": "bu tarayıcıda",
|
||||
"home.missing": "eksik",
|
||||
"home.month_01": "Oca",
|
||||
"home.month_02": "Şub",
|
||||
"home.month_03": "Mar",
|
||||
"home.month_04": "Nis",
|
||||
"home.month_05": "May",
|
||||
"home.month_06": "Haz",
|
||||
"home.month_07": "Tem",
|
||||
"home.month_08": "Ağu",
|
||||
"home.month_09": "Eyl",
|
||||
"home.month_10": "Eki",
|
||||
"home.month_11": "Kas",
|
||||
"home.month_12": "Ara",
|
||||
"home.new_inches": "Yeni tasarım, inç",
|
||||
"home.new_inches_hint": "İnç · Tasarım alanında açılır",
|
||||
"home.new_mm": "Yeni tasarım",
|
||||
"home.new_mm_hint": "Milimetre · Tasarım alanında açılır",
|
||||
"home.open": "Aç…",
|
||||
"home.opened_days_ago": "{count} gün önce açıldı",
|
||||
"home.opened_hours_ago": "{count} sa. önce açıldı",
|
||||
"home.opened_just_now": "Az önce açıldı",
|
||||
"home.opened_minutes_ago": "{count} dk. önce açıldı",
|
||||
"home.open_hint": "Bir .solidcraft tasarımı ({shortcut})",
|
||||
"home.open_now": "Şimdi aç",
|
||||
"home.opened_days_ago": "{count} gün önce",
|
||||
"home.opened_hours_ago": "{count} sa. önce",
|
||||
"home.opened_just_now": "Az önce",
|
||||
"home.opened_minutes_ago": "{count} dk. önce",
|
||||
"home.opening": "açılıyor…",
|
||||
"home.properties": "Tasarım özellikleri",
|
||||
"home.recent": "Son kullanılanlar",
|
||||
"home.recent_empty": "Açtığınız veya kaydettiğiniz tasarımlar burada görünür.",
|
||||
"home.recovery_count": "Kurtarılabilir tasarımlar: {count}",
|
||||
"home.recovery_empty": "Kurtarılabilir tasarım yok.",
|
||||
"home.recovery_hint": "Bu tasarımlar kaydedilmeden SolidCraft kapandı.",
|
||||
"home.report_issue": "Sorun bildir",
|
||||
"home.samples": "Örnekler",
|
||||
"home.settings": "Ayarlar",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Klavye kısayolları",
|
||||
"home.start_hint": "Bir tasarım oluşturun, son kullanılan bir dosyayı açın veya bir örneği inceleyin.",
|
||||
"home.whats_new": "Yenilikler",
|
||||
"home.yesterday": "Dün",
|
||||
"install.button_tip": "SolidCraft'ı uygulama olarak yükleyin: kendi penceresinde açılır ve çevrimdışı da çalışır",
|
||||
"install.dock_1": "Dosya › Dock'a Ekle'yi seçin.",
|
||||
"install.dock_2": "“Ekle” düğmesine tıklayın.",
|
||||
|
||||
+42
-4
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Ліцензія MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Ліцензії сторонніх компонентів",
|
||||
"home.back": "Назад до {name}",
|
||||
"home.crashes": "Звіти про збої",
|
||||
"home.design_workspace": "Проєктування",
|
||||
"home.documentation": "Документація",
|
||||
"home.export": "Експортувати",
|
||||
"home.export_hint": "Виберіть формат для поточного проєкту.",
|
||||
"home.export_json_hint": "Редагований проєкт та історія команд",
|
||||
"home.export_mesh_hint": "Сітка для друку або поширення",
|
||||
"home.export_step_hint": "Точна твердотільна геометрія",
|
||||
"home.import": "Імпортувати…",
|
||||
"home.import_hint": "STEP, IGES або сітку в новий проєкт",
|
||||
"home.in_this_browser": "у цьому браузері",
|
||||
"home.missing": "відсутній",
|
||||
"home.month_01": "січ.",
|
||||
"home.month_02": "лют.",
|
||||
"home.month_03": "бер.",
|
||||
"home.month_04": "квіт.",
|
||||
"home.month_05": "трав.",
|
||||
"home.month_06": "черв.",
|
||||
"home.month_07": "лип.",
|
||||
"home.month_08": "серп.",
|
||||
"home.month_09": "вер.",
|
||||
"home.month_10": "жовт.",
|
||||
"home.month_11": "лист.",
|
||||
"home.month_12": "груд.",
|
||||
"home.new_inches": "Новий проєкт у дюймах",
|
||||
"home.new_inches_hint": "Дюйми · відкриється в робочому просторі «Проєктування»",
|
||||
"home.new_mm": "Новий проєкт",
|
||||
"home.new_mm_hint": "Міліметри · відкриється в робочому просторі «Проєктування»",
|
||||
"home.open": "Відкрити…",
|
||||
"home.opened_days_ago": "Відкрито {count} дн. тому",
|
||||
"home.opened_hours_ago": "Відкрито {count} год тому",
|
||||
"home.opened_just_now": "Щойно відкрито",
|
||||
"home.opened_minutes_ago": "Відкрито {count} хв тому",
|
||||
"home.open_hint": "Проєкт .solidcraft ({shortcut})",
|
||||
"home.open_now": "Відкрити зараз",
|
||||
"home.opened_days_ago": "{count} дн. тому",
|
||||
"home.opened_hours_ago": "{count} год тому",
|
||||
"home.opened_just_now": "Щойно",
|
||||
"home.opened_minutes_ago": "{count} хв тому",
|
||||
"home.opening": "відкриття…",
|
||||
"home.properties": "Властивості проєкту",
|
||||
"home.recent": "Недавні",
|
||||
"home.recent_empty": "Тут з’являться проєкти, які ви відкриєте або збережете.",
|
||||
"home.recovery_count": "Проєктів для відновлення: {count}",
|
||||
"home.recovery_empty": "Немає проєктів для відновлення.",
|
||||
"home.recovery_hint": "SolidCraft закрився до збереження цих проєктів.",
|
||||
"home.report_issue": "Повідомити про проблему",
|
||||
"home.samples": "Зразки",
|
||||
"home.settings": "Налаштування",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Сполучення клавіш",
|
||||
"home.start_hint": "Створіть проєкт, відкрийте нещодавній файл або перегляньте приклад.",
|
||||
"home.whats_new": "Що нового",
|
||||
"home.yesterday": "Учора",
|
||||
"install.button_tip": "Встановити SolidCraft як застосунок: він відкривається в окремому вікні й працює без мережі",
|
||||
"install.dock_1": "Виберіть «Файл» › «Додати в Dock».",
|
||||
"install.dock_2": "Натисніть «Додати».",
|
||||
|
||||
+42
-4
@@ -857,19 +857,57 @@
|
||||
"help.mit": "Giấy phép MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Giấy phép của bên thứ ba",
|
||||
"home.back": "Quay lại {name}",
|
||||
"home.crashes": "Báo cáo sự cố",
|
||||
"home.design_workspace": "Thiết kế",
|
||||
"home.documentation": "Tài liệu hướng dẫn",
|
||||
"home.export": "Xuất",
|
||||
"home.export_hint": "Chọn định dạng cho thiết kế hiện tại.",
|
||||
"home.export_json_hint": "Thiết kế có thể chỉnh sửa và lịch sử lệnh",
|
||||
"home.export_mesh_hint": "Lưới để in hoặc chia sẻ",
|
||||
"home.export_step_hint": "Hình học khối chính xác",
|
||||
"home.import": "Nhập…",
|
||||
"home.import_hint": "Nhập STEP, IGES hoặc lưới vào thiết kế mới",
|
||||
"home.in_this_browser": "trong trình duyệt này",
|
||||
"home.missing": "thiếu",
|
||||
"home.month_01": "thg 1",
|
||||
"home.month_02": "thg 2",
|
||||
"home.month_03": "thg 3",
|
||||
"home.month_04": "thg 4",
|
||||
"home.month_05": "thg 5",
|
||||
"home.month_06": "thg 6",
|
||||
"home.month_07": "thg 7",
|
||||
"home.month_08": "thg 8",
|
||||
"home.month_09": "thg 9",
|
||||
"home.month_10": "thg 10",
|
||||
"home.month_11": "thg 11",
|
||||
"home.month_12": "thg 12",
|
||||
"home.new_inches": "Thiết kế mới theo inch",
|
||||
"home.new_inches_hint": "Inch · mở trong Thiết kế",
|
||||
"home.new_mm": "Thiết kế mới",
|
||||
"home.new_mm_hint": "Milimét · mở trong Thiết kế",
|
||||
"home.open": "Mở…",
|
||||
"home.opened_days_ago": "Đã mở {count} ngày trước",
|
||||
"home.opened_hours_ago": "Đã mở {count} giờ trước",
|
||||
"home.opened_just_now": "Vừa mới mở",
|
||||
"home.opened_minutes_ago": "Đã mở {count} phút trước",
|
||||
"home.open_hint": "Một thiết kế .solidcraft ({shortcut})",
|
||||
"home.open_now": "Mở ngay",
|
||||
"home.opened_days_ago": "{count} ngày trước",
|
||||
"home.opened_hours_ago": "{count} giờ trước",
|
||||
"home.opened_just_now": "Vừa xong",
|
||||
"home.opened_minutes_ago": "{count} phút trước",
|
||||
"home.opening": "đang mở…",
|
||||
"home.properties": "Thuộc tính thiết kế",
|
||||
"home.recent": "Gần đây",
|
||||
"home.recent_empty": "Các thiết kế bạn mở hoặc lưu sẽ xuất hiện ở đây.",
|
||||
"home.recovery_count": "Thiết kế có thể khôi phục: {count}",
|
||||
"home.recovery_empty": "Không có thiết kế nào có thể khôi phục.",
|
||||
"home.recovery_hint": "SolidCraft đã đóng trước khi các thiết kế này được lưu.",
|
||||
"home.report_issue": "Báo cáo sự cố",
|
||||
"home.samples": "Mẫu thiết kế",
|
||||
"home.settings": "Cài đặt",
|
||||
"home.short_date": "{day} {month}",
|
||||
"home.shortcuts": "Phím tắt",
|
||||
"home.start_hint": "Tạo thiết kế, mở tệp gần đây hoặc khám phá mẫu.",
|
||||
"home.whats_new": "Có gì mới",
|
||||
"home.yesterday": "Hôm qua",
|
||||
"install.button_tip": "Cài đặt SolidCraft dưới dạng ứng dụng: mở trong cửa sổ riêng và hoạt động ngoại tuyến",
|
||||
"install.dock_1": "Chọn Tệp › Thêm vào Dock.",
|
||||
"install.dock_2": "Nhấp vào Thêm.",
|
||||
|
||||
@@ -856,19 +856,57 @@
|
||||
"help.mit": "MIT 授權",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "第三方授權",
|
||||
"home.back": "返回 {name}",
|
||||
"home.crashes": "當機報告",
|
||||
"home.design_workspace": "設計",
|
||||
"home.documentation": "文件",
|
||||
"home.export": "匯出",
|
||||
"home.export_hint": "選擇目前設計的格式。",
|
||||
"home.export_json_hint": "可編輯的設計與命令歷程",
|
||||
"home.export_mesh_hint": "用於列印或分享的網格",
|
||||
"home.export_step_hint": "精確的實體幾何",
|
||||
"home.import": "匯入…",
|
||||
"home.import_hint": "將 STEP、IGES 或網格匯入新設計",
|
||||
"home.in_this_browser": "在此瀏覽器中",
|
||||
"home.missing": "找不到檔案",
|
||||
"home.month_01": "1月",
|
||||
"home.month_02": "2月",
|
||||
"home.month_03": "3月",
|
||||
"home.month_04": "4月",
|
||||
"home.month_05": "5月",
|
||||
"home.month_06": "6月",
|
||||
"home.month_07": "7月",
|
||||
"home.month_08": "8月",
|
||||
"home.month_09": "9月",
|
||||
"home.month_10": "10月",
|
||||
"home.month_11": "11月",
|
||||
"home.month_12": "12月",
|
||||
"home.new_inches": "新增設計(英吋)",
|
||||
"home.new_inches_hint": "英吋 · 在設計工作區中開啟",
|
||||
"home.new_mm": "新增設計",
|
||||
"home.new_mm_hint": "公釐 · 在設計工作區中開啟",
|
||||
"home.open": "開啟…",
|
||||
"home.opened_days_ago": "{count} 天前開啟",
|
||||
"home.opened_hours_ago": "{count} 小時前開啟",
|
||||
"home.opened_just_now": "剛剛開啟",
|
||||
"home.opened_minutes_ago": "{count} 分鐘前開啟",
|
||||
"home.open_hint": ".solidcraft 設計檔案 ({shortcut})",
|
||||
"home.open_now": "目前開啟中",
|
||||
"home.opened_days_ago": "{count} 天前",
|
||||
"home.opened_hours_ago": "{count} 小時前",
|
||||
"home.opened_just_now": "剛剛",
|
||||
"home.opened_minutes_ago": "{count} 分鐘前",
|
||||
"home.opening": "正在開啟…",
|
||||
"home.properties": "設計屬性",
|
||||
"home.recent": "最近使用",
|
||||
"home.recent_empty": "您開啟或儲存的設計會顯示於此。",
|
||||
"home.recovery_count": "可復原的設計:{count} 個",
|
||||
"home.recovery_empty": "沒有可復原的設計。",
|
||||
"home.recovery_hint": "SolidCraft 在這些設計儲存前已關閉。",
|
||||
"home.report_issue": "回報問題",
|
||||
"home.samples": "範例",
|
||||
"home.settings": "設定",
|
||||
"home.short_date": "{month}{day}日",
|
||||
"home.shortcuts": "鍵盤快速鍵",
|
||||
"home.start_hint": "建立設計、開啟最近使用的檔案,或瀏覽範例。",
|
||||
"home.whats_new": "新功能",
|
||||
"home.yesterday": "昨天",
|
||||
"install.button_tip": "將 SolidCraft 安裝為應用程式:在獨立視窗中開啟,離線也能使用",
|
||||
"install.dock_1": "選擇「檔案」›「加入 Dock」。",
|
||||
"install.dock_2": "按一下「加入」。",
|
||||
|
||||
+302
-5
@@ -14,11 +14,15 @@
|
||||
"Active": "活动",
|
||||
"Active rule": "活动规则",
|
||||
"Add": "添加",
|
||||
"Add Column": "添加列",
|
||||
"Add Key": "添加按键",
|
||||
"Add Mid Radius": "添加中间半径",
|
||||
"Add Row": "添加行",
|
||||
"Add a tool": "添加工具",
|
||||
"Add condition": "添加条件",
|
||||
"Add or edit rule": "添加或编辑规则",
|
||||
"Add to New Folder": "添加到新文件夹",
|
||||
"Advanced": "高级",
|
||||
"Against": "对照",
|
||||
"Alignment": "对齐",
|
||||
"All Sketch Points": "所有草图点",
|
||||
@@ -32,6 +36,7 @@
|
||||
"Appearance name": "外观名称",
|
||||
"Applied Radius": "应用半径",
|
||||
"Apply To": "应用于",
|
||||
"Apply library appearance": "应用库外观",
|
||||
"Apply to selection": "应用到选择",
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "请先应用外观。导出已指定的外观和在此设计中创建的材质。",
|
||||
"Arc length": "弧长",
|
||||
@@ -42,13 +47,18 @@
|
||||
"BLEND SURFACE": "混合曲面",
|
||||
"BOUNDARY SURFACE": "边界曲面",
|
||||
"BROWSER": "浏览器",
|
||||
"Bend Edges": "折弯边",
|
||||
"Bend Line Position": "弯折线位置",
|
||||
"Bend Position": "折弯位置",
|
||||
"Bend Radius": "折弯半径",
|
||||
"Bend Table": "折弯表",
|
||||
"Bends": "折弯",
|
||||
"Bodies": "实体",
|
||||
"Bodies (optional)": "实体(可选)",
|
||||
"Body": "实体",
|
||||
"Body (last if none)": "实体(未指定时为最后一个)",
|
||||
"Body to split": "要分割的实体",
|
||||
"Body, or faces to take out": "实体,或要移除的面",
|
||||
"Boundary": "边界",
|
||||
"Bounding box": "边界框",
|
||||
"CHECK": "检查",
|
||||
@@ -60,7 +70,9 @@
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV 或 JSON:更新现有参数、添加新参数",
|
||||
"Capture All Positions": "捕获所有位置",
|
||||
"Capture Pos.": "捕获位置",
|
||||
"Catch Depth": "卡扣深度",
|
||||
"Catch Direction": "卡扣方向",
|
||||
"Catch Length": "卡扣长度",
|
||||
"Cells": "区域",
|
||||
"Center X": "中心 X",
|
||||
"Center Y": "中心 Y",
|
||||
@@ -69,15 +81,21 @@
|
||||
"Chamfer Type": "倒角类型",
|
||||
"Check collisions": "检查碰撞",
|
||||
"Checked": "已检查",
|
||||
"Choose Variable radius or Spline in Type.": "请在“类型”中选择 Variable radius 或样条曲线。",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "选择一个新平面。草图几何和尺寸在其二维坐标中保持不变。",
|
||||
"Choose…": "选择…",
|
||||
"Circular": "圆周",
|
||||
"Circumference": "周长",
|
||||
"Close draft": "关闭草稿",
|
||||
"Collapse": "折叠",
|
||||
"Collisions": "碰撞",
|
||||
"Colour": "颜色",
|
||||
"Command": "命令",
|
||||
"Comment": "注释",
|
||||
"Component 1": "组件 1",
|
||||
"Component 2": "组件 2",
|
||||
"Component1": "组件1",
|
||||
"Components": "组件",
|
||||
"Config Table": "配置表",
|
||||
"Configuration": "配置",
|
||||
"Configuration rules": "配置规则",
|
||||
@@ -86,6 +104,7 @@
|
||||
"Contact sets": "接触集",
|
||||
"Continue on GitHub": "在GitHub中继续",
|
||||
"Continuity": "连续性",
|
||||
"Control Radius": "控制半径",
|
||||
"Copy Error Message": "复制错误消息",
|
||||
"Copy Warning Message": "复制警告消息",
|
||||
"Copy all licences": "复制所有许可证",
|
||||
@@ -95,8 +114,11 @@
|
||||
"Corner Relief": "拐角释放",
|
||||
"Corner Relief Shape": "拐角释放形状",
|
||||
"Counterbore depth": "沉孔深度",
|
||||
"Counterbore Ø": "柱形沉孔 Ø",
|
||||
"Countersink angle": "沉头角度",
|
||||
"Countersink Ø": "锥形沉孔 Ø",
|
||||
"Create Issue — review draft": "创建 Issue——审阅草稿",
|
||||
"Create Issue…": "创建 Issue…",
|
||||
"Cross curves": "交叉曲线",
|
||||
"Cube": "立方体",
|
||||
"Current orthographic view (1:1 mm)": "当前正交视图(1:1毫米)",
|
||||
@@ -106,7 +128,11 @@
|
||||
"Curvature change (G3)": "曲率变化 (G3)",
|
||||
"Custom": "自定义",
|
||||
"Customize Toolbar": "自定义工具栏",
|
||||
"Customize…": "自定义…",
|
||||
"Cylindrical face": "圆柱面",
|
||||
"Delete": "删除",
|
||||
"Delete Selected Features…": "删除所选特征…",
|
||||
"Delete Selected Folders": "删除所选文件夹",
|
||||
"Delete all": "全部删除",
|
||||
"Delete rule": "删除规则",
|
||||
"Delete the row": "删除此行",
|
||||
@@ -117,8 +143,11 @@
|
||||
"Describe what happened before continuing.": "继续之前,请描述发生了什么。",
|
||||
"Designs you open or save appear here.": "你打开或保存的设计会显示在此处。",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "由“到对象”确定。选择“角度”可编辑角度范围。",
|
||||
"Diameter": "直径",
|
||||
"Direction": "方向",
|
||||
"Discard": "放弃",
|
||||
"Distance": "距离",
|
||||
"Distance 1": "距离 1",
|
||||
"Do not export hidden edges": "不导出隐藏线",
|
||||
"Don't ask again": "不再询问",
|
||||
"Double-click to rename": "双击重命名",
|
||||
@@ -130,18 +159,22 @@
|
||||
"Edit Profile Sketch": "编辑轮廓草图",
|
||||
"Enable rule": "启用规则",
|
||||
"Enabled": "已启用",
|
||||
"End Radius": "结束半径",
|
||||
"End bulge": "终止鼓度",
|
||||
"End continuity": "终止连续性",
|
||||
"Expand": "展开",
|
||||
"Expected result (optional)": "预期结果(可选)",
|
||||
"Explode": "爆炸",
|
||||
"Explode Text": "分解文字",
|
||||
"Export Animation…": "导出动画…",
|
||||
"Export Bend Table…": "导出折弯表…",
|
||||
"Export CSV…": "导出 CSV…",
|
||||
"Export Current View as DXF": "将当前视图导出为DXF",
|
||||
"Export Current View as DXF…": "将当前视图导出为DXF…",
|
||||
"Export DXF": "导出DXF",
|
||||
"Export DXF…": "导出DXF…",
|
||||
"Export JSON…": "导出 JSON…",
|
||||
"Export design library...": "导出设计库...",
|
||||
"Export hidden edges as dashed lines": "以虚线导出隐藏线",
|
||||
"Expression": "表达式",
|
||||
"Extend to meet": "延伸至相交",
|
||||
@@ -149,25 +182,33 @@
|
||||
"FILES": "文件",
|
||||
"FLAT": "展开",
|
||||
"Face": "面",
|
||||
"Face Appearance": "面外观",
|
||||
"Face attachment lost": "面附着丢失",
|
||||
"Faces": "面",
|
||||
"Faces / edges / vertices": "面 / 边 / 顶点",
|
||||
"Faces to split": "要分割的面",
|
||||
"Fastener": "紧固件",
|
||||
"Favorites": "收藏",
|
||||
"Favourite": "收藏",
|
||||
"Feature": "特征",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "许多 CAD 程序的文件以 Y 为上轴,因此在此处打开时会平躺(Z 朝上)。",
|
||||
"Filter": "筛选",
|
||||
"Filter…": "筛选…",
|
||||
"Find in Browser": "在浏览器中查找",
|
||||
"Finish": "完成",
|
||||
"First plane": "第一个平面",
|
||||
"Fit and G2 corners require Constant radius.": "拟合和 G2 拐角需要 Constant radius。",
|
||||
"Fixed Face": "固定面",
|
||||
"Flat Pattern": "展开图",
|
||||
"Flat size": "平板尺寸",
|
||||
"Follow camera (Look At sketches)": "跟随相机(注视草图)",
|
||||
"Follow the camera (Look At sketches)": "跟随相机(注视草图)",
|
||||
"Font": "字体",
|
||||
"From": "起点",
|
||||
"From Bodies": "从实体创建",
|
||||
"From edges": "起始边",
|
||||
"Full Length": "全长",
|
||||
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "G2 拐角需要恒定半径和 Exact fit。尚不支持构建 G2 拐角。",
|
||||
"Gap": "间隙",
|
||||
"Grid": "网格",
|
||||
"HISTORY": "历史",
|
||||
@@ -175,6 +216,7 @@
|
||||
"Hidden edges": "隐藏线",
|
||||
"History layout": "历史布局",
|
||||
"History names": "历史名称",
|
||||
"Hole (optional)": "孔(可选)",
|
||||
"Holes": "孔",
|
||||
"Holes (edges on them)": "孔(孔上的边)",
|
||||
"Home view": "主视图",
|
||||
@@ -185,14 +227,20 @@
|
||||
"In place (surfaces)": "原位(曲面)",
|
||||
"In plane": "在平面内",
|
||||
"Inside": "内侧",
|
||||
"Instant": "即时",
|
||||
"Interface font": "界面字体",
|
||||
"Interferes": "发生干涉",
|
||||
"Invert": "反转",
|
||||
"Isocurve": "等参线",
|
||||
"Issue title": "Issue 标题",
|
||||
"Items": "项目",
|
||||
"JOINTS": "关节",
|
||||
"Joint Origin1": "接头原点1",
|
||||
"Keep (points on it)": "保留(其上的点)",
|
||||
"Keep Tools": "保留工具",
|
||||
"Keep of the other surface": "另一曲面保留的部分",
|
||||
"Key a joint's value at this step": "在此步骤为接头值设置关键帧",
|
||||
"Keys": "关键帧",
|
||||
"LIBRARY": "库",
|
||||
"LINKS": "链接",
|
||||
"Later": "稍后",
|
||||
@@ -203,9 +251,14 @@
|
||||
"Letter Spacing": "字间距",
|
||||
"Library": "零件库",
|
||||
"Library file": "库文件",
|
||||
"Limits": "限制",
|
||||
"Line Spacing": "行间距",
|
||||
"Line Type": "线型",
|
||||
"Listing the installed fonts…": "正在列出已安装字体…",
|
||||
"Load library": "加载库",
|
||||
"Local library": "本地库",
|
||||
"Loop": "循环",
|
||||
"M5 or M5x0.5": "M5 或 M5x0.5",
|
||||
"MANAGE": "管理",
|
||||
"MATERIALS": "材料",
|
||||
"MODEL": "模型",
|
||||
@@ -216,14 +269,20 @@
|
||||
"Matched restrictions check the final values.": "满足条件的限制规则检查最终值。",
|
||||
"Material": "材质",
|
||||
"Max": "最大",
|
||||
"Maximum": "最大",
|
||||
"Mesh body": "网格实体",
|
||||
"Mid Radius": "中间半径",
|
||||
"Middle double-click": "中键双击",
|
||||
"Min": "最小",
|
||||
"Minimum": "最小",
|
||||
"Mirror Plane": "镜像平面",
|
||||
"Mitre": "斜接",
|
||||
"Model Parameters": "模型参数",
|
||||
"Modeled": "已建模",
|
||||
"More toolbar panels": "更多工具栏面板",
|
||||
"Motion": "运动",
|
||||
"Mouse gestures": "鼠标手势",
|
||||
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "鼠标手势仅在 SolidWorks 鼠标模式下有效(滚轮旋转,Ctrl + 滚轮平移)。",
|
||||
"Move / Copy": "移动 / 复制",
|
||||
"Move Type": "移动类型",
|
||||
"Move down": "下移",
|
||||
@@ -246,8 +305,11 @@
|
||||
"Not available yet": "暂不可用",
|
||||
"Notch": "缺口",
|
||||
"OUTPUT": "输出",
|
||||
"Of": "的值",
|
||||
"On your computer": "在此计算机上",
|
||||
"Opacity": "不透明度",
|
||||
"Open GitHub draft": "打开 GitHub 草稿",
|
||||
"Open library...": "打开库...",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "请在桌面应用中打开本地库。您可以在此导出当前设计的库。",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "使用 Rust 编写的开源参数化 3D CAD。基于 MIT OR Apache-2.0 许可。",
|
||||
"Opens your browser and copies the report text for you to paste into GitHub.": "打开浏览器,并复制报告文本以便粘贴到GitHub。",
|
||||
@@ -269,8 +331,10 @@
|
||||
"Picked": "所选",
|
||||
"Picked point": "选取的点",
|
||||
"Pitch": "螺距",
|
||||
"Pivot": "枢轴",
|
||||
"Place at the origin instead": "改为放置在原点",
|
||||
"Plastic Rules": "塑料规则",
|
||||
"Play the joint's motion in the view": "在视图中播放接头运动",
|
||||
"Playback": "播放",
|
||||
"Plot": "图表",
|
||||
"Position": "位置",
|
||||
@@ -278,14 +342,18 @@
|
||||
"Position (G0)": "位置 (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "沿边的位置,起点端为 0,另一端为 1",
|
||||
"Preferences › Design turns the offer back on": "可在 首选项 › 设计 中重新开启此提示",
|
||||
"Preview": "预览",
|
||||
"Preview Row": "预览行",
|
||||
"Preview leaves the current design unchanged.": "预览不会更改当前设计。",
|
||||
"Profile / Edges": "轮廓 / 边",
|
||||
"Profiles or curves": "轮廓或曲线",
|
||||
"Profiles/Curves": "轮廓/曲线",
|
||||
"Quantity": "物理量",
|
||||
"RECENT": "最近使用",
|
||||
"REFERENCE": "参考",
|
||||
"RULED SURFACE": "直纹曲面",
|
||||
"RULES": "规则",
|
||||
"Radius": "半径",
|
||||
"Radius Fit": "半径拟合",
|
||||
"Ratio": "比率",
|
||||
"Reattach selected face": "重新附着所选面",
|
||||
@@ -294,6 +362,8 @@
|
||||
"Rectangular": "矩形",
|
||||
"Reference face": "参考面",
|
||||
"Remove Tools": "移除工具",
|
||||
"Remove condition": "删除条件",
|
||||
"Remove from Folder": "从文件夹中移除",
|
||||
"Remove from list": "从列表移除",
|
||||
"Remove override": "移除覆盖",
|
||||
"Remove private design details or paths before sharing.": "共享前请移除私密设计细节或路径。",
|
||||
@@ -301,6 +371,9 @@
|
||||
"Remove the column": "移除此列",
|
||||
"Remove the track": "移除轨迹",
|
||||
"Remove this point": "移除此点",
|
||||
"Rename Feature": "重命名特征",
|
||||
"Rename Folder": "重命名文件夹",
|
||||
"Replace existing library file": "替换现有库文件",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "报告缺陷 — 复核截图并开启 GitHub 草稿",
|
||||
"Report text copied. Paste it into GitHub and submit your report there.": "报告文本已复制。请在GitHub中粘贴并提交。",
|
||||
"Reports are public. A GitHub account is required.": "报告将公开发布。您需要GitHub账户。",
|
||||
@@ -309,6 +382,7 @@
|
||||
"Restore View": "恢复视图",
|
||||
"Restore outer boundary": "恢复外边界",
|
||||
"Restore the inherited component or physical material appearance": "还原继承的组件或物理材质外观",
|
||||
"Restore this studio's tools": "恢复此工作室的工具",
|
||||
"Result": "结果",
|
||||
"Retake screenshot": "重新截图",
|
||||
"Reverse": "反向",
|
||||
@@ -319,7 +393,13 @@
|
||||
"Revert Position": "恢复位置",
|
||||
"Review the report and screenshot before opening GitHub.": "打开 GitHub 前请审阅报告和截图。",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "请在GitHub中检查报告,然后点击Submit new issue发布。",
|
||||
"Rib Length": "筋板长度",
|
||||
"Rib Offset": "筋板偏移",
|
||||
"Rib Thickness": "筋板厚度",
|
||||
"Ribs": "筋板",
|
||||
"Rim Edge": "轮缘边",
|
||||
"Ring of tools on the right button": "右键工具环",
|
||||
"Roll Back Here": "回滚到此处",
|
||||
"Roll History Marker Here": "将历史标记滚动到此处",
|
||||
"Round": "圆角",
|
||||
"Rule": "规则",
|
||||
@@ -338,13 +418,18 @@
|
||||
"Search": "搜索",
|
||||
"Search commands": "搜索命令",
|
||||
"Search this library": "搜索此库",
|
||||
"Second plane": "第二个平面",
|
||||
"Section": "剖面",
|
||||
"Sections (one sketch each)": "截面(每个一个草图)",
|
||||
"Select Other": "选择其他",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。双击中键转到主视图(首选项 › 导航)。",
|
||||
"Select bodies (a face picks its body)": "选择实体(单击面即选择其所属实体)",
|
||||
"Select bodies or faces instead": "改为选择实体或面",
|
||||
"Select edges": "选择边",
|
||||
"Selection filter": "选择筛选",
|
||||
"Semantic faces share their appearance across instances.": "语义面在各实例间共享外观。",
|
||||
"Send your report on GitHub": "在 GitHub 上发送报告",
|
||||
"Shaded": "着色",
|
||||
"Shape": "形状",
|
||||
"Sheet Rules": "钣金规则",
|
||||
"Show Agent Cursor": "显示代理光标",
|
||||
@@ -362,6 +447,7 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft 在这些设计保存前已关闭。可以打开它们的上次自动保存。",
|
||||
"Specialise": "专用操作",
|
||||
"Speed": "速度",
|
||||
"Splitting tool": "分割工具",
|
||||
"Stand it up": "将其立起",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "将其立起:绕 X 轴旋转四分之一周,作为一个可撤销的移动特征。",
|
||||
"Stand this part up?": "立起此零件?",
|
||||
@@ -372,16 +458,22 @@
|
||||
"Steps to reproduce (optional)": "重现步骤(可选)",
|
||||
"Stitch the kept pieces": "缝合保留的部分",
|
||||
"Stitch to both bodies": "缝合到两个实体",
|
||||
"Studio": "工作室",
|
||||
"Study": "研究",
|
||||
"Style": "样式",
|
||||
"Summary": "摘要",
|
||||
"Suppress": "抑制",
|
||||
"Suppressed": "已抑制",
|
||||
"Surface": "曲面",
|
||||
"Surface, or faces to turn over": "曲面,或要翻转的面",
|
||||
"Swap Ends": "交换端点",
|
||||
"TRIM": "修剪",
|
||||
"Tangent (G1)": "相切 (G1)",
|
||||
"Tangent Chain": "相切链",
|
||||
"Tangent chain": "相切链",
|
||||
"Tap drill diameter": "攻丝底孔直径",
|
||||
"Target": "目标",
|
||||
"Target Body": "目标实体",
|
||||
"Tear": "撕裂",
|
||||
"Technical details": "技术详情",
|
||||
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "虚线草图是已保存的平面引导。选择一个新面以修复附着。",
|
||||
@@ -396,24 +488,31 @@
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "此子项是共享的。位置更改会应用到其父项的每个副本。",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "此面无法提供持久附着。请选择其他面或显式使用固定平面。",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "此图片尚未附加到GitHub。请复制或保存图片,然后在GitHub中附加。",
|
||||
"To": "终点",
|
||||
"To Offset": "偏移",
|
||||
"To edges": "终止边",
|
||||
"Tolerance": "公差",
|
||||
"Too close": "过近",
|
||||
"Tool Bodies": "工具实体",
|
||||
"Tools": "工具",
|
||||
"Tools, clockwise from the top": "工具,从顶部顺时针排列",
|
||||
"Trimming tool (plane, face or curves)": "修剪工具(平面、面或曲线)",
|
||||
"Turn off section analysis before exporting the view.": "请关闭截面分析后再导出视图。",
|
||||
"Type": "类型",
|
||||
"UNTRIM": "取消修剪",
|
||||
"Unit": "单位",
|
||||
"Unsuppress": "取消抑制",
|
||||
"Updating the preview": "正在更新预览",
|
||||
"Use active author placement": "使用活动作者放置",
|
||||
"Use fixed plane": "使用固定平面",
|
||||
"Use saved fixed plane": "使用已保存的固定平面",
|
||||
"Use selected face as fixed plane": "使用所选面作为固定平面",
|
||||
"Use the SolidWorks mouse mode": "使用 SolidWorks 鼠标模式",
|
||||
"User Parameters": "用户参数",
|
||||
"VIEW": "视图",
|
||||
"VIEWS": "视图",
|
||||
"Value": "值",
|
||||
"Variable Radius": "可变半径",
|
||||
"Velocity": "速度",
|
||||
"Vertices": "顶点",
|
||||
"View navigator": "视图导航器",
|
||||
@@ -427,13 +526,44 @@
|
||||
"When": "条件",
|
||||
"Width": "宽度",
|
||||
"Will fail": "将失败",
|
||||
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "开启鼠标手势后,右键不再旋转视图:按住右键并移动以打开工具环,然后触碰某个工具的区域。普通右键单击仍会打开菜单。",
|
||||
"X": "X",
|
||||
"X Angle": "X 角度",
|
||||
"X angle": "X 角度",
|
||||
"X distance": "X 距离",
|
||||
"Y": "Y",
|
||||
"Y Angle": "Y 角度",
|
||||
"Y angle": "Y 角度",
|
||||
"Y distance": "Y 距离",
|
||||
"Z": "Z",
|
||||
"Z Angle": "Z 角度",
|
||||
"Z Distance": "Z 距离",
|
||||
"Z angle": "Z 角度",
|
||||
"Z distance": "Z 距离",
|
||||
"Zebra": "斑马纹",
|
||||
"ai.prefs.availability": "助手控制",
|
||||
"ai.prefs.background_commands": "在后台执行代理命令",
|
||||
"ai.prefs.follow_camera": "将相机转向草图",
|
||||
"ai.prefs.follow_camera_off": "相机自动跟随已关闭。",
|
||||
"ai.prefs.follow_camera_on": "相机将转向代理创建的草图。",
|
||||
"ai.prefs.intro": "设置代理、权限及其工作方式。",
|
||||
"ai.prefs.make_default": "设为默认",
|
||||
"ai.prefs.no_agents": "尚无代理。添加代理或刷新以查找已安装的代理。",
|
||||
"ai.prefs.no_saved_approvals": "没有已保存的批准。",
|
||||
"ai.prefs.override_all_warning": "此代理可在不询问的情况下保存、覆盖、导出或替换设计。",
|
||||
"ai.prefs.permission_all": "全部允许",
|
||||
"ai.prefs.permission_all_summary": "可用的 SolidCraft 工具无需询问即可运行。",
|
||||
"ai.prefs.permission_ask": "更改前询问",
|
||||
"ai.prefs.permission_ask_summary": "每次设计更改和文件操作均需批准。",
|
||||
"ai.prefs.permission_edits": "允许模型编辑",
|
||||
"ai.prefs.permission_edits_summary": "模型编辑自动执行;文件及破坏性操作仍需询问。",
|
||||
"ai.prefs.permission_scope": "仅适用于 SolidCraft 工具。代理的终端和文件编辑使用其自身权限。",
|
||||
"ai.prefs.pointer_preview_hidden": "指针已隐藏",
|
||||
"ai.prefs.pointer_preview_speed": "代理指针 · {speed}",
|
||||
"ai.prefs.pointer_speed": "移动速度",
|
||||
"ai.prefs.privacy_note": "对话仅发送给所选代理或端点。端点密钥保存在系统钥匙串中。",
|
||||
"ai.prefs.show_pointer": "显示代理指针",
|
||||
"ai.prefs.turn_limit_hint": "助手针对一条消息可调用的设计更改工具次数上限。",
|
||||
"angle, height": "角度、高度",
|
||||
"appearance.dielectric_ior": "电介质折射率",
|
||||
"appearance.metallic": "金属度",
|
||||
@@ -889,19 +1019,57 @@
|
||||
"help.solid.revolve": "将草图轮廓绕一条轴旋转,生成圆形的实体。用于轴、旋钮、碗等围绕中心线呈圆形的零件。",
|
||||
"help.solid.shell": "将实体掏空,留下指定厚度的壁,还可以打开所选的面。用于盒子、盖子等薄壁零件。",
|
||||
"help.third_party": "第三方许可证",
|
||||
"home.back": "返回 {name}",
|
||||
"home.crashes": "崩溃报告",
|
||||
"home.design_workspace": "设计",
|
||||
"home.documentation": "文档",
|
||||
"home.export": "导出",
|
||||
"home.export_hint": "为当前设计选择一种格式。",
|
||||
"home.export_json_hint": "可编辑的设计和命令历史记录",
|
||||
"home.export_mesh_hint": "用于打印或共享的网格",
|
||||
"home.export_step_hint": "精确的实体几何",
|
||||
"home.import": "导入…",
|
||||
"home.import_hint": "将 STEP、IGES 或网格导入新设计",
|
||||
"home.in_this_browser": "在此浏览器中",
|
||||
"home.missing": "文件不存在",
|
||||
"home.month_01": "1月",
|
||||
"home.month_02": "2月",
|
||||
"home.month_03": "3月",
|
||||
"home.month_04": "4月",
|
||||
"home.month_05": "5月",
|
||||
"home.month_06": "6月",
|
||||
"home.month_07": "7月",
|
||||
"home.month_08": "8月",
|
||||
"home.month_09": "9月",
|
||||
"home.month_10": "10月",
|
||||
"home.month_11": "11月",
|
||||
"home.month_12": "12月",
|
||||
"home.new_inches": "新建设计(英寸)",
|
||||
"home.new_inches_hint": "英寸 · 在设计工作区中打开",
|
||||
"home.new_mm": "新建设计",
|
||||
"home.new_mm_hint": "毫米 · 在设计工作区中打开",
|
||||
"home.open": "打开…",
|
||||
"home.opened_days_ago": "{count} 天前打开",
|
||||
"home.opened_hours_ago": "{count} 小时前打开",
|
||||
"home.opened_just_now": "刚刚打开",
|
||||
"home.opened_minutes_ago": "{count} 分钟前打开",
|
||||
"home.open_hint": ".solidcraft 设计文件 ({shortcut})",
|
||||
"home.open_now": "立即打开",
|
||||
"home.opened_days_ago": "{count} 天前",
|
||||
"home.opened_hours_ago": "{count} 小时前",
|
||||
"home.opened_just_now": "刚刚",
|
||||
"home.opened_minutes_ago": "{count} 分钟前",
|
||||
"home.opening": "正在打开…",
|
||||
"home.properties": "设计属性",
|
||||
"home.recent": "最近使用",
|
||||
"home.recent_empty": "您打开或保存的设计将显示在这里。",
|
||||
"home.recovery_count": "可恢复的设计: {count} 个",
|
||||
"home.recovery_empty": "没有可恢复的设计。",
|
||||
"home.recovery_hint": "SolidCraft 在这些设计保存前已关闭。",
|
||||
"home.report_issue": "报告问题",
|
||||
"home.samples": "示例",
|
||||
"home.settings": "设置",
|
||||
"home.short_date": "{month}{day}日",
|
||||
"home.shortcuts": "键盘快捷键",
|
||||
"home.start_hint": "创建设计、打开最近的文件或浏览示例。",
|
||||
"home.whats_new": "新功能",
|
||||
"home.yesterday": "昨天",
|
||||
"install.button_tip": "将 SolidCraft 安装为应用:在独立窗口中打开,离线也能使用",
|
||||
"install.dock_1": "选择“文件”›“添加到程序坞”。",
|
||||
"install.dock_2": "点按“添加”。",
|
||||
@@ -959,8 +1127,15 @@
|
||||
"motion.to_end": "到结尾",
|
||||
"motion.to_start": "到开头",
|
||||
"motion.track_name": "轨道 {number}",
|
||||
"nav.alias.help": "Shift+Alt+左键拖动旋转,Shift+Alt+中键拖动平移,Shift+Alt+右键拖动缩放;中键拖动也可平移,右键拖动旋转",
|
||||
"nav.blender.help": "中键拖动旋转,Shift+中键拖动平移,Ctrl+中键拖动缩放;右键拖动不会移动相机",
|
||||
"nav.free_orbit.help": "鼠标和3D鼠标可保持滚转并越过两个极点旋转。Shift轴吸附将被禁用。",
|
||||
"nav.free_orbit.label": "自由旋转",
|
||||
"nav.fusion.help": "中键拖动平移,Shift+中键拖动旋转,右键拖动旋转",
|
||||
"nav.inventor.help": "中键拖动平移,Shift+中键拖动旋转,Ctrl+中键拖动缩放,右键拖动旋转",
|
||||
"nav.openscad.help": "在空白处左键拖动旋转,Shift+左键拖动框选,拖动草图几何可编辑;右键或中键拖动平移,Shift+右键或 Shift+中键拖动缩放",
|
||||
"nav.solidworks.help": "中键拖动旋转,Ctrl+中键拖动平移,Shift+中键拖动缩放,右键拖动旋转",
|
||||
"nav.tinkercad.help": "右键拖动旋转,中键或 Shift+右键拖动平移",
|
||||
"nav.touchpad.help": "按住 Shift 并移动指针可平移;按住 Alt/Option 并移动可旋转。无需按鼠标按钮。垂直滚动可缩放。",
|
||||
"nav.touchpad.label": "触控板",
|
||||
"nav.touchpad.orbit": "旋转(按住 Alt/Option 并移动,无需鼠标按钮)",
|
||||
@@ -983,8 +1158,130 @@
|
||||
"pattern.flip_direction": "反转方向",
|
||||
"pick a hole to seat it, or OK places it at the origin": "选择一个孔放置,或单击确定放置在原点",
|
||||
"pick one or two vertices, edges or faces": "选择一个或两个顶点、边或面",
|
||||
"prefs.add_item": "添加项目",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option 也可自由拖动操纵器箭头",
|
||||
"prefs.anti_aliasing_note": "平滑锯齿边缘。",
|
||||
"prefs.applies_on_restart": "重启后生效",
|
||||
"prefs.assistant": "助手",
|
||||
"prefs.axes": "坐标轴",
|
||||
"prefs.axis": "轴",
|
||||
"prefs.bottom": "底部",
|
||||
"prefs.browser": "浏览器",
|
||||
"prefs.built_in": "内置",
|
||||
"prefs.button_left": "左键拖动",
|
||||
"prefs.button_middle": "中键拖动",
|
||||
"prefs.button_right": "右键拖动",
|
||||
"prefs.choose_font": "选择字体文件…",
|
||||
"prefs.click": "单击",
|
||||
"prefs.contact_shading": "柔和接触阴影",
|
||||
"prefs.contact_shading_note": "在零件相近处添加柔和阴影。",
|
||||
"prefs.context_menu": "上下文菜单",
|
||||
"prefs.context_menu_ring": "上下文菜单 · 按住显示环形菜单",
|
||||
"prefs.cpu": "CPU(软件)",
|
||||
"prefs.crash_reports": "打开崩溃报告…",
|
||||
"prefs.crash_reports_note": "报告与重现问题的设计和命令一起保存在此计算机上。",
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "曲面",
|
||||
"prefs.curves": "曲面",
|
||||
"prefs.dead_zone_note": "斜线区域为死区:此范围内的移动将被忽略。",
|
||||
"prefs.default": "默认",
|
||||
"prefs.design": "设计默认值",
|
||||
"prefs.design_note": "新建设计的初始设置。已打开的设计保留各自的设置。",
|
||||
"prefs.devices": "3D 鼠标",
|
||||
"prefs.display": "显示",
|
||||
"prefs.edges": "边缘与着色",
|
||||
"prefs.extend": "扩展",
|
||||
"prefs.extra_navigation": "在空白处按住Ctrl并用鼠标左键拖动可旋转;按住空格键可平移。",
|
||||
"prefs.follows_os": "跟随操作系统",
|
||||
"prefs.font": "字体",
|
||||
"prefs.font_fallback": "所选字体缺少的字符会使用内置字体,避免标签显示为空方框。",
|
||||
"prefs.font_sample": "字体示例 · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "无法读取所选字体。",
|
||||
"prefs.general": "常规",
|
||||
"prefs.general_note": "语言、保存和更新。",
|
||||
"prefs.gesture_orbit": "环绕",
|
||||
"prefs.gesture_pan": "平移",
|
||||
"prefs.gesture_zoom": "缩放",
|
||||
"prefs.graphics": "图形",
|
||||
"prefs.graphics_note": "在速度与平滑度之间取舍。每个选项均以示例展示效果。",
|
||||
"prefs.graphics_presets_note": "预设会同时设置以下选项。",
|
||||
"prefs.history": "历史",
|
||||
"prefs.hold_stop_snapping": "按住以停用吸附",
|
||||
"prefs.icons_only": "仅图标",
|
||||
"prefs.imported_file": "已导入文件",
|
||||
"prefs.input": "输入",
|
||||
"prefs.interface": "界面",
|
||||
"prefs.interface_language": "界面语言",
|
||||
"prefs.interface_note": "SolidCraft 的外观和面板布局。",
|
||||
"prefs.interval_minutes": "{n} 分钟",
|
||||
"prefs.labels": "标签",
|
||||
"prefs.layout": "布局",
|
||||
"prefs.layout_note": "紧凑模式将面板停靠在窗口边缘。浏览器半透明仅适用于浮动模式。助手面板的位置在助手页面中设置。",
|
||||
"prefs.left": "左侧",
|
||||
"prefs.left_click": "左键单击",
|
||||
"prefs.live_input": "实时输入",
|
||||
"prefs.live_preview": "实时预览",
|
||||
"prefs.minutes": "分钟",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "鼠标与触控板",
|
||||
"prefs.mouse_heading": "鼠标",
|
||||
"prefs.mouse_mapping": "鼠标映射",
|
||||
"prefs.navigation": "导航",
|
||||
"prefs.navigation_note": "鼠标、触控板和3D鼠标如何移动视图。",
|
||||
"prefs.not_on_gpu": "此 GPU 不支持",
|
||||
"prefs.on": "开启",
|
||||
"prefs.opaque": "不透明",
|
||||
"prefs.orbit_snap": "旋转时按住Shift可捕捉到坐标轴。",
|
||||
"prefs.orientation": "方向",
|
||||
"prefs.overall_speed": "整体速度",
|
||||
"prefs.preview_choices": "所选设置预览",
|
||||
"prefs.quality_default": "默认",
|
||||
"prefs.quality_fastest": "最快",
|
||||
"prefs.quality_slowest": "最慢",
|
||||
"prefs.quality_smoother": "更平滑",
|
||||
"prefs.reports_saved": "已保存 {n} 份",
|
||||
"prefs.right_click": "右键单击",
|
||||
"prefs.saving": "自动保存和恢复",
|
||||
"prefs.scale_note": "也可在窗口中使用 Ctrl + / Ctrl −。100% 时按钮高度为 28 像素。",
|
||||
"prefs.scale_text": "缩放与文字",
|
||||
"prefs.select": "选择",
|
||||
"prefs.select_one_item": "选择一个项目",
|
||||
"prefs.selecting": "选择",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift 仅添加;Ctrl / Cmd 切换选择",
|
||||
"prefs.shift_add_note": "开启:Shift 添加,Ctrl / Cmd 切换选择。关闭(默认):两者均切换选择。",
|
||||
"prefs.shortcuts": "快捷键",
|
||||
"prefs.sketch_selection": "草图与选择",
|
||||
"prefs.sketch_selection_heading": "草图和选择",
|
||||
"prefs.sketch_selection_note": "草图如何捕捉,以及单击如何选择。",
|
||||
"prefs.sketching": "草图",
|
||||
"prefs.snap_drawing_note": "在绘制或拖动草图点时使用。",
|
||||
"prefs.source": "来源",
|
||||
"prefs.speed": "速度",
|
||||
"prefs.stand_up_note": "以 Z 轴向上打开 STEP 或 IGES 文件后,询问是否绕 X 轴旋转 90°。",
|
||||
"prefs.status_done": "完成",
|
||||
"prefs.status_error": "错误",
|
||||
"prefs.status_selected": "已选择",
|
||||
"prefs.status_warning": "警告",
|
||||
"prefs.supersampling_brief": "以更高分辨率绘制后滤波缩小:更清晰,但负载更高。",
|
||||
"prefs.tab_accent_note": "强调色跟随当前选项卡",
|
||||
"prefs.theme_file": "主题文件",
|
||||
"prefs.timeline_half_hour": "−30 分钟",
|
||||
"prefs.timeline_now": "现在",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
"prefs.toggle_item": "添加项目,或取消选择",
|
||||
"prefs.toolbar": "工具栏",
|
||||
"prefs.translucent": "半透明",
|
||||
"prefs.under_browser": "浏览器下方",
|
||||
"prefs.under_cursor": "光标下方",
|
||||
"prefs.units_precision": "单位与精度",
|
||||
"prefs.updates": "更新",
|
||||
"prefs.view": "视图行为",
|
||||
"prefs.viewport": "视口",
|
||||
"prefs.wheel": "滚轮",
|
||||
"prefs.wheel_trackpad": "滚轮与触控板",
|
||||
"prefs.workspace": "工作区",
|
||||
"prefs.zoom_toward_cursor": "朝光标缩放",
|
||||
"properties.title": "属性:{name}",
|
||||
"recovery.days_ago": "{count} 天前",
|
||||
"recovery.detail": "{file} · {features} 个特征 · 自动保存于 {when}",
|
||||
@@ -1055,7 +1352,7 @@
|
||||
"search.suggest": "您是否想找 {label}?",
|
||||
"search.try": "输入命令名称、此设计中的特征或参数。要带值运行命令,请输入其ID和JSON对象:{example}",
|
||||
"search.valid": "有效",
|
||||
"settings.display_quality_note": "级别越精细,曲面和边显示越平滑,但网格生成耗时更长。只影响视口,不影响设计或导出。",
|
||||
"settings.display_quality_note": "仅影响显示,不影响设计和导出。",
|
||||
"settings.frame_rate_note": "在视图右下角显示每秒帧数。开启时视图会持续重绘。",
|
||||
"settings.measure_axis_distances": "显示 X/Y/Z 距离",
|
||||
"settings.measure_axis_distances_help": "测量对话框打开时,在总距离之外显示沿世界坐标轴方向的尺寸线和距离。",
|
||||
|
||||
@@ -64,7 +64,138 @@ fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c:
|
||||
return o;
|
||||
}
|
||||
|
||||
/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
|
||||
/// Shared triangles (in their part's own frame) drawn at one placement: the rows of its rigid
|
||||
/// move (`r0`..`r2`, a point's coordinates are their dot products with (p, 1)) and its colour.
|
||||
/// The vertex colour (location 2) is not read.
|
||||
@vertex
|
||||
fn vs_tri_inst(
|
||||
@location(0) p: vec3<f32>,
|
||||
@location(1) n: vec3<f32>,
|
||||
@location(3) r0: vec4<f32>,
|
||||
@location(4) r1: vec4<f32>,
|
||||
@location(5) r2: vec4<f32>,
|
||||
@location(6) c: vec4<f32>,
|
||||
) -> TOut {
|
||||
let q = vec4<f32>(p, 1.0);
|
||||
let wp = vec3<f32>(dot(r0, q), dot(r1, q), dot(r2, q));
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(wp, 1.0);
|
||||
o.n = vec3<f32>(dot(r0.xyz, n), dot(r1.xyz, n), dot(r2.xyz, n));
|
||||
o.c = c;
|
||||
o.wp = wp;
|
||||
return o;
|
||||
}
|
||||
|
||||
/// A flat-shaded placement (`vs_tri_inst_flat`): its vertices are shared between triangles, so
|
||||
/// the face's normal is the triangle's own plane, worked out per fragment. The fragment gets its
|
||||
/// position from the eye (`rp`): small near what is looked at, so the plane comes out exact at
|
||||
/// any zoom however far from the origin the part stands, and few values per vertex (a tiled GPU
|
||||
/// stores every vertex's outputs: more of them cost more than the triangles' shared vertices
|
||||
/// save).
|
||||
struct FOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) rp: vec3<f32>,
|
||||
@location(1) c: vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_tri_inst_flat(
|
||||
@location(0) p: vec3<f32>,
|
||||
@location(3) r0: vec4<f32>,
|
||||
@location(4) r1: vec4<f32>,
|
||||
@location(5) r2: vec4<f32>,
|
||||
@location(6) c: vec4<f32>,
|
||||
) -> FOut {
|
||||
let q = vec4<f32>(p, 1.0);
|
||||
let wp = vec3<f32>(dot(r0, q), dot(r1, q), dot(r2, q));
|
||||
var o: FOut;
|
||||
o.pos = u.vp * vec4<f32>(wp, 1.0);
|
||||
o.rp = wp - u.eye.xyz;
|
||||
o.c = c;
|
||||
return o;
|
||||
}
|
||||
|
||||
/// The fragment of a flat-shaded placement as the other triangles' fragments are: its normal
|
||||
/// the outward normal of its triangle (toward the eye on a front face, away on a back face, as
|
||||
/// the triangles are wound outward).
|
||||
fn flat_fragment(i: FOut, front: bool) -> TOut {
|
||||
var n = cross(dpdx(i.rp), dpdy(i.rp));
|
||||
let toward = select(u.back.xyz, -i.rp, u.eye.w > 0.5);
|
||||
n = n * inverseSqrt(max(dot(n, n), 1e-30));
|
||||
if ((dot(n, toward) < 0.0) == front) { n = -n; }
|
||||
var o: TOut;
|
||||
o.pos = i.pos;
|
||||
o.n = n;
|
||||
o.c = i.c;
|
||||
o.wp = i.rp + u.eye.xyz;
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_solid_flat(i: FOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let t = flat_fragment(i, front);
|
||||
let c = solid(t, front);
|
||||
return c;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_solid_cut_flat(i: FOut, @builtin(front_facing) front: bool) -> CutOut {
|
||||
let t = flat_fragment(i, front);
|
||||
var o: CutOut;
|
||||
o.depth = pushed(cap_depth(t.wp, t.pos.z));
|
||||
o.c = solid(t, front);
|
||||
return o;
|
||||
}
|
||||
|
||||
/// Weighted blended order-independent transparency (`view/oit.rs`): a see-through fragment's
|
||||
/// colour, premultiplied and weighted by its alpha and nearness, into the accumulation target,
|
||||
/// and its alpha into the revealage target (which keeps what is behind it by 1 - alpha).
|
||||
struct Oit {
|
||||
@location(0) accum: vec4<f32>,
|
||||
@location(1) reveal: vec4<f32>,
|
||||
};
|
||||
|
||||
fn oit(c: vec4<f32>, z: f32) -> Oit {
|
||||
let a = clamp(c.a, 0.0, 1.0);
|
||||
// Depth 0 near and 1 far, whichever way the buffer runs.
|
||||
let d = select(z, 1.0 - z, reversed());
|
||||
let w = clamp(a * max(1e-2, 3e3 * pow(1.0 - d, 3.0)), 1e-2, 3e3);
|
||||
var o: Oit;
|
||||
o.accum = vec4<f32>(c.rgb * a, a) * w;
|
||||
o.reveal = vec4<f32>(a);
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_oit(i: TOut) -> Oit {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return oit(shade(i), i.pos.z);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_oit_flat(i: FOut, @builtin(front_facing) front: bool) -> Oit {
|
||||
let t = flat_fragment(i, front);
|
||||
let o = oit(shade(t), t.pos.z);
|
||||
if (clipped(t.wp)) { discard; }
|
||||
return o;
|
||||
}
|
||||
|
||||
/// A see-through flat-shaded placement (faded or glass), shaded like `fs_tri`.
|
||||
@fragment
|
||||
fn fs_tri_flat(i: FOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let t = flat_fragment(i, front);
|
||||
let c = shade(t);
|
||||
if (clipped(t.wp)) { discard; }
|
||||
return c;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_ao_depth_flat(i: FOut, @builtin(front_facing) front: bool) -> AoDepth {
|
||||
let t = flat_fragment(i, front);
|
||||
return opaque_depth(t, front);
|
||||
}
|
||||
|
||||
/// Opaque model surfaces: cut by the section plane/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
|
||||
@fragment
|
||||
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let c = solid(i, front);
|
||||
@@ -150,6 +281,11 @@ struct AoDepth {
|
||||
|
||||
@fragment
|
||||
fn fs_ao_depth(i: TOut, @builtin(front_facing) front: bool) -> AoDepth {
|
||||
return opaque_depth(i, front);
|
||||
}
|
||||
|
||||
/// The AO prepass's depth of an opaque surface's fragment.
|
||||
fn opaque_depth(i: TOut, front: bool) -> AoDepth {
|
||||
if (clipped(i.wp) || length(i.n) < 0.5) { discard; }
|
||||
var depth = i.pos.z;
|
||||
if (!front && length(u.clip.xyz) > 0.5) { depth = cap_depth(i.wp, i.pos.z); }
|
||||
|
||||
@@ -8,6 +8,35 @@ const fn icon_primary(rgb: [u8; 3]) -> Color32 {
|
||||
impl Tokens {
|
||||
pub const fn light() -> Tokens {
|
||||
Tokens {
|
||||
file_dim: Color32::from_rgb(58, 70, 86),
|
||||
file_recovery_well: Color32::from_rgb(247, 226, 184),
|
||||
file_action_height: 32.0,
|
||||
file_back_height: 40.0,
|
||||
file_recovery_icon: 32.0,
|
||||
file_grid_gap: 14.0,
|
||||
file_background: Color32::from_rgb(247, 249, 251),
|
||||
file_surface: Color32::WHITE,
|
||||
file_border: Color32::from_rgb(224, 229, 236),
|
||||
file_image_tint: Color32::WHITE,
|
||||
file_rail: Color32::from_rgb(35, 110, 173),
|
||||
file_rail_ink: Color32::WHITE,
|
||||
file_rail_hover: Color32::from_rgb(43, 118, 180),
|
||||
file_primary: Color32::from_rgb(227, 240, 251),
|
||||
file_primary_ink: Color32::from_rgb(26, 80, 127),
|
||||
file_primary_border: Color32::from_rgb(50, 139, 211),
|
||||
file_recovery: Color32::from_rgb(255, 246, 229),
|
||||
file_recovery_ink: Color32::from_rgb(122, 74, 5),
|
||||
file_recovery_border: Color32::from_rgb(240, 210, 154),
|
||||
file_nav_width: 248.0,
|
||||
file_nav_height: 38.0,
|
||||
file_tile_height: 84.0,
|
||||
file_tile_icon: 44.0,
|
||||
file_recent_height: 52.0,
|
||||
file_sample_image_height: 84.0,
|
||||
file_content_inset_x: 40.0,
|
||||
file_content_inset_y: 32.0,
|
||||
file_section_gap: 28.0,
|
||||
font_start_title: 28.0,
|
||||
space_2: 2.0,
|
||||
space_3: 3.0,
|
||||
space_4: 4.0,
|
||||
@@ -236,6 +265,19 @@ impl Tokens {
|
||||
t.sketch_mode_tint = 0.12;
|
||||
t.sketch_footer_icon_size = 15.0;
|
||||
t.font_section_tracking = 0.04;
|
||||
t.file_dim = Color32::from_rgb(180, 187, 200);
|
||||
t.file_recovery_well = Color32::from_rgb(92, 68, 32);
|
||||
t.file_background = Color32::from_rgb(31, 35, 43);
|
||||
t.file_surface = Color32::from_rgb(39, 43, 52);
|
||||
t.file_border = Color32::from_rgb(63, 69, 82);
|
||||
t.file_rail = Color32::from_rgb(30, 80, 128);
|
||||
t.file_rail_hover = Color32::from_rgb(46, 99, 149);
|
||||
t.file_primary = Color32::from_rgb(27, 48, 72);
|
||||
t.file_primary_ink = Color32::from_rgb(156, 199, 245);
|
||||
t.file_primary_border = Color32::from_rgb(58, 136, 238);
|
||||
t.file_recovery = Color32::from_rgb(67, 51, 28);
|
||||
t.file_recovery_ink = Color32::from_rgb(245, 204, 137);
|
||||
t.file_recovery_border = Color32::from_rgb(115, 88, 43);
|
||||
t.app_bar = Color32::from_rgb(28, 31, 38);
|
||||
t.toolbar = Color32::from_rgb(39, 43, 52);
|
||||
t.panel = Color32::from_rgb(39, 43, 52);
|
||||
@@ -268,12 +310,12 @@ impl Tokens {
|
||||
t.render_ink = Color32::from_rgb(224, 116, 174);
|
||||
t.render_soft = Color32::from_rgb(64, 35, 54);
|
||||
t.tab_on_fill_dark = Color32::BLACK;
|
||||
t.dialog_body = Color32::from_rgb(59, 68, 83);
|
||||
t.dialog_body = Color32::from_rgb(39, 43, 52);
|
||||
t.dialog_footer = Color32::from_rgb(47, 52, 62);
|
||||
t.dialog_rule = Color32::from_rgb(53, 59, 71);
|
||||
t.control_fill = Color32::from_rgb(39, 43, 52);
|
||||
t.control_fill = Color32::from_rgb(28, 31, 38);
|
||||
t.control_border = Color32::from_rgb(74, 82, 98);
|
||||
t.dialog_label = Color32::from_rgb(180, 187, 200);
|
||||
t.dialog_label = t.text;
|
||||
t.selected_ink = Color32::from_rgb(156, 199, 245);
|
||||
t.selected_bg = Color32::from_rgb(42, 70, 110);
|
||||
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[1]);
|
||||
@@ -298,8 +340,8 @@ impl Tokens {
|
||||
t.dimension_driven = Color32::from_rgb(142, 150, 164);
|
||||
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
|
||||
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
|
||||
t.dialog_bg = Color32::from_rgb(59, 68, 83);
|
||||
t.dialog_border = Color32::from_rgb(72, 83, 99);
|
||||
t.dialog_bg = Color32::from_rgb(39, 43, 52);
|
||||
t.dialog_border = Color32::from_rgb(63, 69, 82);
|
||||
t.chip = Color32::from_rgb(59, 101, 168);
|
||||
t.glyph_bg = Color32::from_rgb(44, 50, 60);
|
||||
t.glyph_edge = Color32::from_rgb(200, 130, 120);
|
||||
@@ -315,7 +357,7 @@ impl Tokens {
|
||||
t.bend_up = Color32::from_rgb(110, 200, 120);
|
||||
t.bend_down = Color32::from_rgb(240, 150, 80);
|
||||
t.form_ink = t.text;
|
||||
t.form_dim = t.text_dim;
|
||||
t.form_dim = Color32::from_rgb(180, 187, 200);
|
||||
t.form_primary = t.accent;
|
||||
t.form_error = t.error;
|
||||
t.form_error_fill = t.timeline_item_error;
|
||||
|
||||
@@ -368,6 +368,8 @@ pub enum AxisRef {
|
||||
World { axis: String },
|
||||
/// Any line in the sketch plane, in world coordinates.
|
||||
Line { origin: Position, dir: Dir },
|
||||
/// A construction axis feature, by name, where it is when the revolve is evaluated.
|
||||
Construction { name: FeatureName },
|
||||
}
|
||||
|
||||
/// Persistent cylindrical Emboss selection, authored in the feature component frame.
|
||||
@@ -1854,14 +1856,16 @@ fn fair_keep_edges() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Triangles of one imported mesh body (millimetres).
|
||||
/// Triangles of one imported mesh body (millimetres). The arrays are shared by clones: every
|
||||
/// edit keeps the design as it was for undo, and a scanned part's triangles (hundreds of
|
||||
/// megabytes) are not copied for that.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MeshData {
|
||||
pub name: String,
|
||||
/// x, y, z per vertex.
|
||||
pub positions: Vec<f64>,
|
||||
pub positions: std::sync::Arc<[f64]>,
|
||||
/// Three vertex indices per triangle.
|
||||
pub triangles: Vec<u32>,
|
||||
pub triangles: std::sync::Arc<[u32]>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<[f32; 3]>,
|
||||
}
|
||||
@@ -2026,6 +2030,10 @@ pub enum PatternKind {
|
||||
alignment: solidcraft_sketch::PatternAlignment,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
reversed: bool,
|
||||
/// A construction axis the copies turn about, by name, followed when it moves;
|
||||
/// `origin` and `axis` are where it was when the pattern was made.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
axis_feature: Option<FeatureName>,
|
||||
},
|
||||
/// Copies along a chain of sketch curves: instance k moves by path(k·spacing) − path(0),
|
||||
/// measured along the path from its start.
|
||||
@@ -2618,6 +2626,33 @@ impl Default for Document {
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// The next shared identity, including auxiliary lists written by older versions or
|
||||
/// populated programmatically. Reading it does not reserve an id (used by joint previews).
|
||||
pub fn next_item_id(&self) -> Result<u64> {
|
||||
let a = &self.assembly;
|
||||
let largest = self
|
||||
.browser_groups
|
||||
.iter()
|
||||
.map(|g| g.id)
|
||||
.chain(self.timeline_groups.iter().map(|g| g.id))
|
||||
.chain(self.canvases.iter().map(|c| c.id))
|
||||
.chain(a.joints.iter().map(|j| j.id))
|
||||
.chain(a.contact_sets.iter().map(|c| c.id))
|
||||
.chain(a.studies.iter().map(|s| s.id))
|
||||
.chain(a.exploded.iter().map(|v| v.id))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let floor = largest.checked_add(1).ok_or_else(|| DocError::Invalid("too many items in the design".into()))?;
|
||||
Ok(self.next_id.max(floor).max(1))
|
||||
}
|
||||
|
||||
/// Reserve an identity in the saved counter. Removing an item never rewinds this counter.
|
||||
pub fn allocate_id(&mut self) -> Result<u64> {
|
||||
let id = self.next_item_id()?;
|
||||
self.next_id = id.checked_add(1).ok_or_else(|| DocError::Invalid("too many items in the design".into()))?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn new(name: &str) -> Self {
|
||||
Document {
|
||||
render: Default::default(),
|
||||
|
||||
+388
-91
@@ -163,6 +163,50 @@ impl ModelBody {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Identity of this body's display geometry, without meshing it: the cell its mesh is kept
|
||||
/// in, shared by every clone of the body (timeline snapshots, world states that place it
|
||||
/// unchanged) and, while it is held, by no other body.
|
||||
pub fn display_identity(&self) -> Arc<dyn std::any::Any + Send + Sync> {
|
||||
self.mesh.clone()
|
||||
}
|
||||
|
||||
/// [`ModelBody::mesh`] as a shared mesh and the move that places it: for a rigid placement
|
||||
/// of a definition, the definition's own mesh (the same one for every placement of it) and
|
||||
/// the placement; for any other body its own mesh, unmoved. The mesh moved by the matrix is
|
||||
/// `mesh()`, so a viewport can draw every placement of a part from one copy of its
|
||||
/// triangles, without making the moved copies.
|
||||
pub fn mesh_in_place(&self) -> (Arc<Mesh>, crate::Mat) {
|
||||
match self.mesh_placement.as_deref() {
|
||||
Some(placed) => placed.source_mesh(),
|
||||
None => (self.mesh(), crate::IDENTITY),
|
||||
}
|
||||
}
|
||||
|
||||
/// The box of [`ModelBody::mesh`] without making a placement's moved copy: the shared mesh's
|
||||
/// points, moved.
|
||||
pub fn mesh_bounds(&self) -> solidcraft_geom::Aabb3 {
|
||||
if let Some(mesh) = self.cached_mesh() {
|
||||
return mesh.bounds();
|
||||
}
|
||||
let (mesh, m) = self.mesh_in_place();
|
||||
if crate::is_identity(&m) {
|
||||
return mesh.bounds();
|
||||
}
|
||||
mesh.positions.iter().fold(solidcraft_geom::Aabb3::EMPTY, |mut b, p| {
|
||||
b.add(crate::apply_point(&m, *p));
|
||||
b
|
||||
})
|
||||
}
|
||||
|
||||
/// [`ModelBody::plan_mesh_at`] as a shared mesh and the move that places it, as
|
||||
/// [`ModelBody::mesh_in_place`] does for the reference mesh.
|
||||
pub fn plan_mesh_in_place_at(&self, q: crate::display_quality::DisplayQuality) -> Option<(Arc<Mesh>, crate::Mat)> {
|
||||
match self.mesh_placement.as_deref() {
|
||||
Some(placed) => placed.source_plan_mesh(q, &self.name),
|
||||
None => Some((self.plan_mesh_at(q)?, crate::IDENTITY)),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`ModelBody::plan_mesh`] if it is made already; otherwise it is made on a worker thread
|
||||
/// (once, however often this is asked) and `None` comes back meanwhile, so a caller drawing a
|
||||
/// frame (a face highlighted under the pointer) is not held up by a large body's plan
|
||||
@@ -977,6 +1021,11 @@ pub struct Model {
|
||||
kept: KeptSlot,
|
||||
/// The imported texts' digests the last `evaluate` fingerprinted the features with.
|
||||
step_digests: StepDigests,
|
||||
/// The document context the last `evaluate` ran in ([`context_digest`]).
|
||||
context: u64,
|
||||
/// Results another copy of this model evaluated (ahead, off the window thread) that
|
||||
/// `evaluate` takes for features they match ([`Model::prime`]), with that copy's context.
|
||||
primed: Option<(u64, Arc<std::collections::HashMap<u64, FeatureResult>>)>,
|
||||
}
|
||||
|
||||
/// Parameters a feature uses directly (inputs, sketch dimensions and the expressions of a
|
||||
@@ -1143,8 +1192,17 @@ fn evaluation_values(doc: &Document) -> (BTreeMap<String, Value>, BTreeMap<Strin
|
||||
|
||||
/// A feature's fingerprint in the chain after `prev`: its own inputs, where it is placed, and
|
||||
/// the sheet metal or plastic rules it takes defaults from.
|
||||
fn chain_fingerprint(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, prev: u64, rolled_back: bool, steps: &StepDigests) -> u64 {
|
||||
let mut fp = fingerprint(prev, f, vals, rolled_back, steps) ^ placement_digest(doc, f);
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn chain_fingerprint(
|
||||
doc: &Document,
|
||||
vals: &BTreeMap<String, Value>,
|
||||
f: &Feature,
|
||||
prev: u64,
|
||||
rolled_back: bool,
|
||||
steps: &StepDigests,
|
||||
st: Option<&ModelState>,
|
||||
) -> u64 {
|
||||
let mut fp = fingerprint(prev, f, vals, rolled_back, steps) ^ placement_digest(doc, f, st);
|
||||
// Sheet metal features also depend on the rules (and the parameters they use).
|
||||
if f.kind.is_sheet() {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
@@ -1229,9 +1287,157 @@ fn cache_key(f: &Feature, chain_fp: u64, local: u64, ctx: u64, state: &ModelStat
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Where components are placed, for a sketch that sees other components' geometry (on a face,
|
||||
/// or with projections): it is resolved in its own component's view of them.
|
||||
fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
/// What a feature reads of placements: components (their first placement), occurrence paths
|
||||
/// and bodies in its timeline input, whose geometry it finds in its evaluating frame, or all.
|
||||
#[derive(Default)]
|
||||
struct PlacementReads {
|
||||
all: bool,
|
||||
components: std::collections::BTreeSet<u64>,
|
||||
paths: Vec<Vec<u64>>,
|
||||
bodies: std::collections::BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl PlacementReads {
|
||||
fn is_empty(&self) -> bool {
|
||||
!self.all && self.components.is_empty() && self.paths.is_empty() && self.bodies.is_empty()
|
||||
}
|
||||
|
||||
/// A body's complete placement in the feature's input state, including its offset. Later
|
||||
/// transfers do not change where an earlier feature read it.
|
||||
fn placed_body(&mut self, st: Option<&ModelState>, name: &str) {
|
||||
if st.is_some_and(|st| st.body(name).is_some()) {
|
||||
self.bodies.insert(name.to_owned());
|
||||
} else {
|
||||
self.all = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// The same target selection as `apply_traced`, expanded only over the timeline input.
|
||||
fn drawn_targets(&mut self, st: Option<&ModelState>, f: &Feature) {
|
||||
let Some(st) = st else {
|
||||
self.all = true;
|
||||
return;
|
||||
};
|
||||
let (_, targets) = feature_op(f);
|
||||
let names = if targets.is_empty() { &f.participants } else { &targets };
|
||||
let all = names.len() == 1 && names.first().is_some_and(|n| n == "*");
|
||||
if all || names.is_empty() {
|
||||
self.bodies.extend(st.bodies.iter().filter(|b| all || b.component == f.component || b.feature == f.id).map(|b| b.name.clone()));
|
||||
} else {
|
||||
for name in names {
|
||||
self.placed_body(Some(st), name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The component of body `name` in `st`; every component when the body is not there (a
|
||||
/// projection then takes the body nearest its point, wherever that is).
|
||||
fn body(&mut self, doc: &Document, st: Option<&ModelState>, name: &str) {
|
||||
match st.and_then(|st| st.body(name)) {
|
||||
Some(b) => {
|
||||
self.components.insert(doc.model_body_component(b));
|
||||
}
|
||||
None => self.all = true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The component of sketch `id`, and the root when it holds wires fixed in the world (they
|
||||
/// are read through the reader's whole placement).
|
||||
fn sketch(&mut self, doc: &Document, id: u64) {
|
||||
self.components.insert(doc.feature(id).map(|f| f.component).unwrap_or(0));
|
||||
if doc.sketch(id).is_ok_and(|sk| sk.wires.iter().any(|w| w.space == solidcraft_sketch::WireSpace::World)) {
|
||||
self.components.insert(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn link(&mut self, doc: &Document, st: Option<&ModelState>, source: &solidcraft_sketch::LinkSource) {
|
||||
use solidcraft_sketch::LinkSource as L;
|
||||
match source {
|
||||
L::Edge { body, .. } | L::Face { body, .. } | L::Body { body } | L::Vertex { body, .. } | L::Iso { body, .. } | L::Spun { body, .. } => {
|
||||
self.body(doc, st, body)
|
||||
}
|
||||
L::SketchCurve { sketch, .. } | L::SketchPoint { sketch, .. } => self.sketch(doc, *sketch),
|
||||
L::OnSurface { sketch, body, .. } => {
|
||||
self.sketch(doc, *sketch);
|
||||
self.body(doc, st, body);
|
||||
}
|
||||
L::Intersection { a, b } => {
|
||||
self.link(doc, st, a);
|
||||
self.link(doc, st, b);
|
||||
}
|
||||
// In the sketch's own frame: the component's origin, axes and planes, its own axes, text.
|
||||
L::Origin | L::Axis { .. } | L::SketchAxis { .. } | L::Plane { .. } | L::Text { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame of an occurrence path from the root (its occurrences' transforms in turn); `None`
|
||||
/// when the path no longer leads anywhere.
|
||||
fn path_frame(doc: &Document, path: &[u64]) -> Option<Mat> {
|
||||
if path.len() > crate::MAX_PLACEMENT_DEPTH + 1 {
|
||||
return None;
|
||||
}
|
||||
let (mut m, mut c) = (crate::IDENTITY, 0);
|
||||
for id in path {
|
||||
let o = doc.occurrences.iter().find(|o| o.id == *id && o.parent == c)?;
|
||||
m = crate::mat_mul(&m, &o.transform);
|
||||
c = o.component;
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Hash a placement on a grid far finer than anything a design shows ([`QUANTUM`] mm, a
|
||||
/// thousandth of that for the rotation), so the same relative placement reached through
|
||||
/// different whole-assembly motions (each rounding a little differently) reads the same.
|
||||
fn hash_placement(m: &Mat, h: &mut impl Hasher) {
|
||||
for (col, column) in m.iter().enumerate() {
|
||||
let q = if col == 3 { QUANTUM } else { QUANTUM * 1e-3 };
|
||||
for v in column {
|
||||
let r = (v / q).round();
|
||||
// -0 and 0 are the same place.
|
||||
(if r == 0.0 { 0.0f64 } else { r }).to_bits().hash(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the geometry a feature reads from other components is placed, as its own component
|
||||
/// sees it: the placement of each component (or occurrence path) it reads relative to its own
|
||||
/// frame. A feature that reads nothing outside its own frame gives 0, so moving parts around
|
||||
/// (a joint driven, an occurrence moved) leaves it as it was; one that reads a part moved
|
||||
/// relative to it (a sketch projecting a face of another part, To Object, a mirror of a placed
|
||||
/// body) is evaluated again. `st` is the model the feature starts from, which says what
|
||||
/// component a body it names belongs to (without it, every component is taken as read).
|
||||
/// [`placement_digest`] of a mirror of placed bodies: its own component's frame, each placement it
|
||||
/// was picked at (the copy's component, frame and the frame it was picked in) and the offsets of
|
||||
/// the bodies it mirrors (#1938).
|
||||
fn mirror_placements_digest(doc: &Document, f: &Feature, placements: &[Vec<u64>], bodies: &[crate::BodyName]) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
doc.component_transform(f.component).iter().flatten().for_each(|x| x.to_bits().hash(&mut h));
|
||||
for placed in placements.iter().map(|path| mirror_copy_frame(doc, path)) {
|
||||
placed.is_some().hash(&mut h);
|
||||
if let Some((component, frame, picked)) = placed {
|
||||
component.hash(&mut h);
|
||||
frame.iter().chain(&picked).flatten().for_each(|x| x.to_bits().hash(&mut h));
|
||||
}
|
||||
}
|
||||
for b in bodies {
|
||||
serde_json::to_string(&doc.body_offsets.get(&b.0)).unwrap_or_default().hash(&mut h);
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
fn placement_digest(doc: &Document, f: &Feature, st: Option<&ModelState>) -> u64 {
|
||||
// No shortcut for a design without placements: its first component (placed where the
|
||||
// design is) must not change what its features read, or creating it evaluates them all.
|
||||
let mut reads = PlacementReads::default();
|
||||
let mut frame = None;
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
// A sketch, profile or path of another component is moved through both placements.
|
||||
for (sketch, _) in f.kind.profile_sels() {
|
||||
if doc.feature(sketch).is_some_and(|s| s.component != f.component) {
|
||||
reads.sketch(doc, sketch);
|
||||
}
|
||||
}
|
||||
let path = match &f.kind {
|
||||
FeatureKind::Sweep { path_sketch, .. }
|
||||
| FeatureKind::SurfaceSweep { path_sketch, .. }
|
||||
@@ -1248,10 +1454,8 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
_ => &[],
|
||||
};
|
||||
if path.as_ref().is_some_and(wired) || rails.iter().any(|r| wired(&r.sketch)) {
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
let owner = path.and_then(|path| doc.feature(path.0)).map(|feature| feature.component).unwrap_or(0);
|
||||
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
|
||||
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
if let Some(path) = path {
|
||||
reads.sketch(doc, path.0);
|
||||
}
|
||||
// A sweep's profile and the rails' components place them too.
|
||||
let profile = match &f.kind {
|
||||
@@ -1259,69 +1463,113 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
_ => None,
|
||||
};
|
||||
for sketch in profile.into_iter().chain(rails.iter().map(|r| &r.sketch)) {
|
||||
let owner = doc.feature(sketch.0).map(|feature| feature.component).unwrap_or(0);
|
||||
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
reads.sketch(doc, sketch.0);
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
// A network through 3D sketch curves reads their points through the components the
|
||||
// feature and those sketches are placed in, as a sweep's wired path does.
|
||||
if let FeatureKind::SurfaceNetwork { u, v, .. } = &f.kind {
|
||||
let sketches: Vec<crate::SketchId> = u.iter().chain(v).filter_map(|c| c.sketch).collect();
|
||||
if sketches.iter().any(wired) {
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
doc.component_transform(f.component).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
for sketch in &sketches {
|
||||
let owner = doc.feature(sketch.0).map(|feature| feature.component).unwrap_or(0);
|
||||
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
}
|
||||
// To / From Object finds its target where that target's component is placed (#928).
|
||||
let to_object = match &f.kind {
|
||||
FeatureKind::Extrude { extent, .. } => extent.to.is_some() || extent.from.is_some(),
|
||||
FeatureKind::Revolve { to, .. } => to.is_some(),
|
||||
_ => false,
|
||||
};
|
||||
// A mirror of placed bodies follows the placements it was picked at and its own component's
|
||||
// frame, not every occurrence: moving an unrelated one leaves it (and its cache) alone.
|
||||
if let FeatureKind::Mirror { placements, bodies, .. } = &f.kind
|
||||
&& !placements.is_empty()
|
||||
{
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
doc.component_transform(f.component).iter().flatten().for_each(|x| x.to_bits().hash(&mut h));
|
||||
for placed in placements.iter().map(|path| mirror_copy_frame(doc, path)) {
|
||||
placed.is_some().hash(&mut h);
|
||||
if let Some((component, frame, picked)) = placed {
|
||||
component.hash(&mut h);
|
||||
frame.iter().chain(&picked).flatten().for_each(|x| x.to_bits().hash(&mut h));
|
||||
reads.sketch(doc, sketch.0);
|
||||
}
|
||||
}
|
||||
for b in bodies {
|
||||
serde_json::to_string(&doc.body_offsets.get(&b.0)).unwrap_or_default().hash(&mut h);
|
||||
}
|
||||
match &f.kind {
|
||||
// To / From Object finds its target where that target's component is placed (#928),
|
||||
// over every body.
|
||||
FeatureKind::Extrude { extent, .. } if extent.to.is_some() || extent.from.is_some() => reads.all = true,
|
||||
FeatureKind::Revolve { to: Some(_), .. } => reads.all = true,
|
||||
// A mirror of placed bodies follows the placements it was picked at and its own component's
|
||||
// frame, not every occurrence: moving an unrelated one leaves it (and its cache) alone.
|
||||
FeatureKind::Mirror { placements, bodies, .. } if !placements.is_empty() => return mirror_placements_digest(doc, f, placements, bodies),
|
||||
// A combine moves tools placed differently from its target into the target's frame (#3383).
|
||||
FeatureKind::Combine { target, tools, .. } => {
|
||||
frame = st.and_then(|st| st.body(target)).map(|b| body_world(doc, b));
|
||||
reads.placed_body(st, target);
|
||||
for tool in tools {
|
||||
reads.placed_body(st, tool);
|
||||
}
|
||||
}
|
||||
return h.finish();
|
||||
// Joins, cuts and intersects meet their targets where those are placed (#3383).
|
||||
kind if drawn_tools_change_bodies(kind) => reads.drawn_targets(st, f),
|
||||
FeatureKind::Sketch { plane, sketch } => {
|
||||
match crate::face_anchor::plane_face_anchor(plane) {
|
||||
// Read in the frame of the placement it was drawn in, its face through the
|
||||
// placement it was picked at.
|
||||
Some(anchor) => {
|
||||
match anchor.author_frame(doc, f.component) {
|
||||
Ok(m) => frame = Some(m),
|
||||
Err(e) => e.to_string().hash(&mut h),
|
||||
}
|
||||
if anchor.path.is_empty() {
|
||||
reads.components.insert(anchor.component);
|
||||
} else {
|
||||
reads.paths.push(anchor.path.clone());
|
||||
}
|
||||
}
|
||||
// A face or edges picked on whatever body lies there.
|
||||
None if plane_reads_geometry(plane) => reads.all = true,
|
||||
None => {}
|
||||
}
|
||||
for link in &sketch.links {
|
||||
reads.link(doc, st, &link.source);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// A combine moves tools placed differently from its target into the target's frame (#3383).
|
||||
let combine = matches!(&f.kind, FeatureKind::Combine { .. });
|
||||
if !to_object && !combine && drawn_tools_change_bodies(&f.kind) {
|
||||
return drawn_tools_digest(doc, f);
|
||||
}
|
||||
if to_object || combine {
|
||||
return if doc.occurrences.is_empty() && doc.body_offsets.is_empty() { 0 } else { placements_hash(doc) };
|
||||
}
|
||||
let FeatureKind::Sketch { plane, sketch } = &f.kind else { return 0 };
|
||||
let anchor = crate::face_anchor::plane_face_anchor(plane);
|
||||
if (doc.occurrences.is_empty() && (anchor.is_none() || doc.body_offsets.is_empty()))
|
||||
|| (!matches!(plane, PlaneRef::Face { .. }) && anchor.is_none() && sketch.links.is_empty())
|
||||
{
|
||||
if reads.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
placements_hash(doc)
|
||||
let frame = frame.unwrap_or_else(|| doc.component_transform(f.component));
|
||||
let Some(inverse) = crate::mat_inverse(&frame) else { return placements_hash(doc) };
|
||||
if reads.all {
|
||||
reads.components.insert(0);
|
||||
match st {
|
||||
// The components holding what the feature can reach: a component made later, or one
|
||||
// that holds nothing yet (just created), changes nothing it reads.
|
||||
Some(st) => {
|
||||
reads.components.extend(doc.body_owners(st));
|
||||
let owner = |id: u64| doc.feature(id).map(|g| g.component);
|
||||
reads.components.extend(st.sketches.iter().filter_map(|s| owner(s.feature)));
|
||||
reads.components.extend(st.construct.iter().filter_map(|c| owner(c.feature)));
|
||||
}
|
||||
None => reads.components.extend(doc.components.iter().map(|c| c.id)),
|
||||
}
|
||||
}
|
||||
for c in &reads.components {
|
||||
c.hash(&mut h);
|
||||
// Its own component is read in its own frame.
|
||||
if *c != f.component {
|
||||
hash_placement(&crate::mat_mul(&inverse, &doc.component_transform(*c)), &mut h);
|
||||
}
|
||||
}
|
||||
for path in &reads.paths {
|
||||
path.hash(&mut h);
|
||||
match path_frame(doc, path) {
|
||||
Some(m) => hash_placement(&crate::mat_mul(&inverse, &m), &mut h),
|
||||
None => u8::MAX.hash(&mut h),
|
||||
}
|
||||
}
|
||||
for name in &reads.bodies {
|
||||
name.hash(&mut h);
|
||||
match st.and_then(|st| st.body(name)) {
|
||||
Some(b) => hash_placement(&crate::mat_mul(&inverse, &body_world(doc, b)), &mut h),
|
||||
None => u8::MAX.hash(&mut h),
|
||||
}
|
||||
}
|
||||
// Legacy and component-wide reads still need the conservative offset dependency. Body
|
||||
// reads above already include just their own offsets, in the frame that evaluates them.
|
||||
if reads.all || !reads.components.is_empty() || !reads.paths.is_empty() {
|
||||
serde_json::to_string(&doc.body_offsets).unwrap_or_default().hash(&mut h);
|
||||
}
|
||||
// Never 0: the feature reads placements.
|
||||
h.finish() | 1
|
||||
}
|
||||
|
||||
/// Every occurrence's placement and every body offset.
|
||||
/// Every occurrence's placement and every body offset (a feature read from a frame that cannot
|
||||
/// be inverted).
|
||||
fn placements_hash(doc: &Document) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
for o in &doc.occurrences {
|
||||
@@ -1419,7 +1667,8 @@ impl Model {
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
let mut prev_fp = first.checked_sub(1).and_then(|i| self.results.get(i)).map_or(0, |r| r.fingerprint);
|
||||
for (i, f) in doc.features.iter().enumerate().skip(first) {
|
||||
prev_fp = chain_fingerprint(doc, &vals, f, prev_fp, i >= marker, &steps);
|
||||
let before = self.state_before(f.id);
|
||||
prev_fp = chain_fingerprint(doc, &vals, f, prev_fp, i >= marker, &steps, Some(&before));
|
||||
if let Some(r) = self.results.get_mut(i).filter(|r| r.id == f.id) {
|
||||
r.fingerprint = prev_fp;
|
||||
}
|
||||
@@ -1460,12 +1709,31 @@ impl Model {
|
||||
let steps = StepDigests::of(doc, &self.step_digests);
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
let mut prev = 0;
|
||||
// The model each feature starts from (which components the bodies it names are in).
|
||||
let mut state = self.empty.clone();
|
||||
doc.features.iter().zip(&self.results).enumerate().all(|(i, (f, r))| {
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &steps);
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &steps, Some(&state));
|
||||
if !r.skipped {
|
||||
state = r.state.clone();
|
||||
}
|
||||
r.id == f.id && r.fingerprint == prev
|
||||
})
|
||||
}
|
||||
|
||||
/// Take the results `ahead` (a copy of this model, evaluated off this thread for the same
|
||||
/// commands) computed: until [`Model::unprime`], `evaluate` reuses each one whose feature
|
||||
/// has the same inputs here, instead of evaluating it again. Results that don't match are
|
||||
/// ignored, so a design changed meanwhile is evaluated as usual.
|
||||
pub fn prime(&mut self, ahead: &Model) {
|
||||
let results = ahead.results.iter().filter(|r| !r.skipped && !r.stale).map(|r| (r.id, r.clone())).collect();
|
||||
self.primed = Some((ahead.context, Arc::new(results)));
|
||||
}
|
||||
|
||||
/// Stop offering the results [`Model::prime`] took.
|
||||
pub fn unprime(&mut self) {
|
||||
self.primed = None;
|
||||
}
|
||||
|
||||
/// Re-evaluate the document, reusing results up to the first changed feature.
|
||||
pub fn evaluate(&mut self, doc: &Document) {
|
||||
let (vals, perr) = evaluation_values(doc);
|
||||
@@ -1478,7 +1746,9 @@ impl Model {
|
||||
let mut restored = 0;
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
let ctx = context_digest(doc, &vals);
|
||||
self.context = ctx;
|
||||
self.step_digests = StepDigests::of(doc, &self.step_digests);
|
||||
let primed = self.primed.as_ref().filter(|(context, _)| !self.no_cache && *context == ctx).map(|(_, results)| results.clone());
|
||||
// Each feature's component, for `Document::legacy_body_component` on every body after
|
||||
// every feature without searching the timeline for each (the first feature with an id).
|
||||
let mut feature_component: std::collections::HashMap<u64, u64> = std::collections::HashMap::with_capacity(doc.features.len());
|
||||
@@ -1490,7 +1760,7 @@ impl Model {
|
||||
let rolled_back = i >= marker;
|
||||
// Opening was cancelled: the rest is skipped and the result discarded.
|
||||
let cancelled = watched.step(i);
|
||||
let fp = chain_fingerprint(doc, &vals, f, prev_fp, rolled_back, &self.step_digests);
|
||||
let fp = chain_fingerprint(doc, &vals, f, prev_fp, rolled_back, &self.step_digests, Some(&state));
|
||||
prev_fp = fp;
|
||||
if reuse
|
||||
&& let Some(old) = self.results.get(i)
|
||||
@@ -1521,7 +1791,7 @@ impl Model {
|
||||
continue;
|
||||
}
|
||||
// The same feature on the same inputs: its earlier result.
|
||||
let key = cache_key(f, fp, fingerprint(0, f, &vals, false, &self.step_digests) ^ placement_digest(doc, f), ctx, &state);
|
||||
let key = cache_key(f, fp, fingerprint(0, f, &vals, false, &self.step_digests) ^ placement_digest(doc, f, Some(&state)), ctx, &state);
|
||||
let kept = self.kept.0.as_ref().filter(|_| !self.no_cache);
|
||||
if !self.no_cache
|
||||
&& let Some(c) = self.cache.lock().ok().and_then(|mut m| m.get(key)).or_else(|| kept.and_then(|k| k.get(key)))
|
||||
@@ -1547,6 +1817,15 @@ impl Model {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// Evaluated ahead on these same inputs (the same chain of definitions from the start).
|
||||
if let Some(r) = primed.as_ref().and_then(|p| p.get(&f.id)).filter(|r| r.fingerprint == fp && !r.stale && !r.skipped) {
|
||||
restored += 1;
|
||||
if r.error.is_none() {
|
||||
state = r.state.clone();
|
||||
}
|
||||
out.push(FeatureResult { state: state.clone(), ..r.clone() });
|
||||
continue;
|
||||
}
|
||||
recomputed += 1;
|
||||
let t0 = now();
|
||||
let mut next = (*state).clone();
|
||||
@@ -1787,9 +2066,12 @@ impl Model {
|
||||
let marker = doc.marker.unwrap_or(usize::MAX);
|
||||
self.step_digests = StepDigests::of(doc, &self.step_digests);
|
||||
let mut prev = 0;
|
||||
// Each feature's fingerprint reads the model it starts from, as evaluating does.
|
||||
let mut before = Arc::new(ModelState::default());
|
||||
for (i, (r, f)) in self.results.iter_mut().zip(&doc.features).enumerate() {
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &self.step_digests);
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &self.step_digests, Some(&before));
|
||||
r.fingerprint = prev;
|
||||
before = r.state.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3038,7 +3320,17 @@ fn blend_target(state: &ModelState, body: &Option<crate::BodyName>, edges: &[Vec
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, Vec2)> {
|
||||
/// The revolve axis in the sketch's 2D coordinates (a point on it, its direction). `construct`
|
||||
/// gives a construction axis, by name, in the sketch's 3D frame.
|
||||
pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef, construct: &dyn Fn(&str) -> Result<(Vec3, Vec3)>) -> Result<(Vec2, Vec2)> {
|
||||
let in_plane = |origin: Vec3, dir: Vec3| {
|
||||
let pl = &ss.plane;
|
||||
let d = dir.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
|
||||
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(origin).abs() > 1e-6 {
|
||||
return Err(DocError::Invalid("the revolve axis does not lie in the sketch plane".into()));
|
||||
}
|
||||
Ok((pl.to_local(origin), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
|
||||
};
|
||||
match axis {
|
||||
AxisRef::SketchLine { curve } => {
|
||||
let ci = ss.sketch.curve_index(curve).ok_or_else(|| DocError::Unknown(format!("sketch line `{curve}`")))?;
|
||||
@@ -3068,17 +3360,37 @@ pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, V
|
||||
}
|
||||
Ok((pl.to_local(Vec3::ZERO), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
|
||||
}
|
||||
AxisRef::Line { origin, dir } => {
|
||||
let pl = &ss.plane;
|
||||
let d = dir.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
|
||||
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(origin.0).abs() > 1e-6 {
|
||||
return Err(DocError::Invalid("the revolve axis does not lie in the sketch plane".into()));
|
||||
}
|
||||
Ok((pl.to_local(origin.0), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
|
||||
AxisRef::Line { origin, dir } => in_plane(origin.0, dir.0),
|
||||
AxisRef::Construction { name } => {
|
||||
let (origin, dir) = construct(name)?;
|
||||
in_plane(origin, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A construction axis `name` of the timeline so far, as evaluated, in the frame of component
|
||||
/// `comp`: a point on it and its direction. Without a document (`doc` None) it is taken in its
|
||||
/// own component's frame.
|
||||
pub(crate) fn construction_axis_in(doc: Option<&Document>, st: &ModelState, name: &str, comp: u64) -> Result<(Vec3, Vec3)> {
|
||||
let c = st
|
||||
.construct
|
||||
.iter()
|
||||
.find(|c| c.name == name)
|
||||
.ok_or_else(|| DocError::Unknown(format!("construction axis `{name}` (it must come earlier in the timeline)")))?;
|
||||
let crate::construct::ConstructGeom::Axis { origin, dir } = &c.geom else {
|
||||
return Err(DocError::Invalid(format!("`{name}` is not a construction axis")));
|
||||
};
|
||||
let Some(doc) = doc else { return Ok((*origin, *dir)) };
|
||||
let owner = doc.feature(c.feature).map_or(0, |f| f.component);
|
||||
if owner == comp || doc.occurrences.is_empty() {
|
||||
return Ok((*origin, *dir));
|
||||
}
|
||||
let inverse =
|
||||
crate::mat_inverse(&doc.component_transform(comp)).ok_or_else(|| DocError::Invalid("the feature's component frame is invalid".into()))?;
|
||||
let relative = crate::mat_mul(&inverse, &doc.component_transform(owner));
|
||||
Ok((crate::apply_point(&relative, *origin), crate::apply_vector(&relative, *dir)))
|
||||
}
|
||||
|
||||
/// Whether a drill from `p` along `d` reaches material: it starts into a body (`touches`), or,
|
||||
/// when `ahead` is set, a body lies further along `d`. A spot exactly on a surface counts only
|
||||
/// when the drill enters there, not when it leaves. Only the bodies in `scope` count.
|
||||
@@ -4113,7 +4425,7 @@ fn feature_tools_with_constructor(
|
||||
f.component,
|
||||
)?;
|
||||
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
let (o, d) = revolve_axis(&ss, axis, &|name| construction_axis_in(Some(doc), st, name, f.component))?;
|
||||
// To Object is in this feature's component frame: the model as seen from there.
|
||||
let tst = if to.is_some() { state_in_frame(doc, st, f.component) } else { std::borrow::Cow::Borrowed(st) };
|
||||
// Where the tool meets its targets is measured where they are shown (#3383).
|
||||
@@ -4897,7 +5209,11 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
|
||||
out.push(m);
|
||||
}
|
||||
}
|
||||
crate::PatternKind::Circular { origin, axis, count: c, angle, alignment, reversed } => {
|
||||
crate::PatternKind::Circular { origin, axis, count: c, angle, alignment, reversed, axis_feature } => {
|
||||
let (origin, axis) = match axis_feature {
|
||||
Some(name) => construction_axis_in(Some(doc), st, name, component)?,
|
||||
None => (origin.0, axis.0),
|
||||
};
|
||||
let n = count(c)?;
|
||||
let total = val(vals, angle, Kind::Angle)?;
|
||||
let full = (total.abs() - std::f64::consts::TAU).abs() < 1e-9;
|
||||
@@ -4909,7 +5225,7 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
|
||||
0.0
|
||||
};
|
||||
for i in 1..n {
|
||||
out.push(rotation(origin.0, axis.0, step * alignment.multiplier(i, *reversed)));
|
||||
out.push(rotation(origin, axis, step * alignment.multiplier(i, *reversed)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6419,25 +6735,6 @@ impl Drop for ToolFrames {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`placement_digest`] of a feature with drawn tools: the placements, when it names the bodies
|
||||
/// it changes (they may lie in another component, where its tools meet them, #3383); nothing
|
||||
/// otherwise, as it then changes only bodies of its own component.
|
||||
fn drawn_tools_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
let targets = match &f.kind {
|
||||
FeatureKind::Extrude { targets, .. }
|
||||
| FeatureKind::Revolve { targets, .. }
|
||||
| FeatureKind::Loft { targets, .. }
|
||||
| FeatureKind::Sweep { targets, .. }
|
||||
| FeatureKind::Pipe { targets, .. }
|
||||
| FeatureKind::Coil { targets, .. } => !targets.is_empty(),
|
||||
_ => false,
|
||||
};
|
||||
if (!targets && f.participants.is_empty()) || (doc.occurrences.is_empty() && doc.body_offsets.is_empty()) {
|
||||
return 0;
|
||||
}
|
||||
placements_hash(doc)
|
||||
}
|
||||
|
||||
/// The matrix taking the evaluated feature's drawn tools into body `name`'s frame (`None`: the
|
||||
/// same frame).
|
||||
fn tool_frame(name: &str) -> Option<Mat> {
|
||||
|
||||
@@ -304,7 +304,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f.component,
|
||||
)?;
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
let (o, d) = revolve_axis(&ss, axis, &|name| construction_axis_in(Some(doc), st, name, f.component))?;
|
||||
let a = val(vals, angle, Kind::Angle)?;
|
||||
let (start, sweep) = match (angle2, symmetric) {
|
||||
(Some(a2), _) => {
|
||||
|
||||
@@ -28,6 +28,18 @@ impl FeatureKind {
|
||||
v
|
||||
}
|
||||
|
||||
/// Whether the feature names the feature `name` (the construction plane a plane is built on,
|
||||
/// a patterned or mirrored feature).
|
||||
pub fn names_feature(&self, name: &str) -> bool {
|
||||
let mut found = false;
|
||||
self.walk_refs(&mut |r| {
|
||||
if let Ref::Feature(n) = r {
|
||||
found |= n.as_ref() == name;
|
||||
}
|
||||
});
|
||||
found
|
||||
}
|
||||
|
||||
/// Point references to sketches (by id) and features (by name) at their copies, or at a
|
||||
/// feature's new name (`timeline.rename`).
|
||||
pub fn remap_refs(&mut self, sketches: &BTreeMap<u64, u64>, names: &BTreeMap<String, String>) {
|
||||
|
||||
@@ -674,6 +674,9 @@ pub fn read(s: &str) -> Result<Document> {
|
||||
d.features.iter().map(|f| f.id).chain(d.components.iter().map(|c| c.id)).chain(d.occurrences.iter().map(|o| o.id)).max().unwrap_or(0);
|
||||
let floor = largest.checked_add(1).ok_or_else(|| DocError::Invalid("damaged design: the id space is exhausted".into()))?;
|
||||
d.next_id = d.next_id.max(floor).max(1);
|
||||
// These lists used to allocate independently. Keep their existing identities, but never
|
||||
// let a later deletion make them available again after loading an older design.
|
||||
d.next_id = d.next_item_id()?;
|
||||
d.format = format_string(FORMAT);
|
||||
d.format_pending = pending_names();
|
||||
Ok(d)
|
||||
@@ -2038,6 +2041,46 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn older_auxiliary_ids_advance_the_saved_counter_before_deletion() {
|
||||
use serde_json::json;
|
||||
|
||||
let origin = json!({"occurrence": 0, "snap": {"type": "point", "point": [0, 0, 0]}});
|
||||
let cases = [
|
||||
("browser_groups", json!({"id": 100, "name": "Group", "component": 0, "folder": "bodies", "items": []})),
|
||||
("timeline_groups", json!({"id": 100, "name": "Folder", "features": []})),
|
||||
(
|
||||
"canvases",
|
||||
json!({"id": 100, "name": "Canvas", "format": "png", "data": "", "pixels": [1, 1],
|
||||
"plane": {"type": "origin", "name": "XY"}, "center": [0, 0], "width": 10,
|
||||
"angle": 0, "opacity": 0.5, "flip": false, "visible": true}),
|
||||
),
|
||||
("joints", json!({"id": 100, "name": "Joint", "kind": "rigid", "a": origin, "b": origin})),
|
||||
("contact_sets", json!({"id": 100, "name": "Contact", "occurrences": []})),
|
||||
("studies", json!({"id": 100, "name": "Study", "steps": 10, "keys": []})),
|
||||
("exploded", json!({"id": 100, "name": "View", "moves": []})),
|
||||
];
|
||||
for (field, item) in cases {
|
||||
let json = edited(&Document::new("Legacy IDs"), |v| {
|
||||
v.as_object_mut().unwrap().remove("next_id");
|
||||
if matches!(field, "browser_groups" | "timeline_groups" | "canvases") {
|
||||
v[field] = json!([item]);
|
||||
} else {
|
||||
v["assembly"] = json!({field: [item]});
|
||||
}
|
||||
});
|
||||
let mut back = read(&json).unwrap_or_else(|e| panic!("{field}: {e}"));
|
||||
assert_eq!(back.next_id, 101, "{field}");
|
||||
back.browser_groups.clear();
|
||||
back.timeline_groups.clear();
|
||||
back.canvases.clear();
|
||||
back.assembly = Default::default();
|
||||
let mut reopened = read(&back.to_json()).unwrap();
|
||||
assert_eq!(reopened.allocate_id().unwrap(), 101, "{field}: deleted legacy id was forgotten");
|
||||
assert_eq!(reopened.allocate_id().unwrap(), 102, "{field}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_component_or_occurrence_ids_are_a_damaged_design() {
|
||||
let d = two_components_one_occurrence();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
//! A circular pattern can turn about a construction axis (`axis_feature`, its name) and a revolve
|
||||
//! or surface revolve can take one as its axis (`"type": "construction"`, with `name`); both follow
|
||||
//! the axis when it is edited. Older designs need no transformation: they name no construction
|
||||
//! axis. The last release refuses a design saved with this (the newer format number).
|
||||
@@ -380,7 +380,7 @@ impl Model {
|
||||
let mut prev = 0;
|
||||
let mut results = Vec::with_capacity(features.len());
|
||||
for (i, (f, r)) in doc.features.iter().zip(features).enumerate() {
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &step_digests);
|
||||
prev = chain_fingerprint(doc, &vals, f, prev, i >= marker, &step_digests, Some(&before));
|
||||
if r.get("id").and_then(Json::as_u64) != Some(f.id) || unhex(r.get("fingerprint").unwrap_or(&Json::Null))? != prev {
|
||||
return Err(bad("saved model: the document changed"));
|
||||
}
|
||||
@@ -403,7 +403,7 @@ impl Model {
|
||||
// Each result where an evaluation would have put it, so an edit undone (or set back)
|
||||
// finds it again rather than evaluating the feature anew.
|
||||
if !result.skipped {
|
||||
let local = fingerprint(0, f, &vals, false, &step_digests) ^ placement_digest(doc, f);
|
||||
let local = fingerprint(0, f, &vals, false, &step_digests) ^ placement_digest(doc, f, Some(&before));
|
||||
cache.put(
|
||||
cache_key(f, prev, local, ctx, &before),
|
||||
CachedEval {
|
||||
|
||||
@@ -583,7 +583,9 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
|
||||
}
|
||||
FeatureKind::Revolve { sketch, axis, .. } => {
|
||||
let Some(ss) = before.sketch(sketch.0) else { return format!("F{fid}:face:{fi}") };
|
||||
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis) else { return format!("F{fid}:face:{fi}") };
|
||||
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis, &|name| crate::eval::construction_axis_in(None, before, name, 0)) else {
|
||||
return format!("F{fid}:face:{fi}");
|
||||
};
|
||||
let (o, d) = (ss.plane.to_world(o2), (ss.plane.to_world(o2 + d2) - ss.plane.to_world(o2)).normalized().unwrap_or(Vec3::Z));
|
||||
// A flat face holding the axis is an angle cap: on the sketch plane, or the far one.
|
||||
let flat = !ns.is_empty() && ns.windows(2).all(|w| w[0].cross(w[1]).len() < 1e-6) && ns.iter().all(|m| m.dot(d).abs() < 1e-6);
|
||||
|
||||
@@ -59,6 +59,18 @@ impl PlacedMesh {
|
||||
|
||||
/// The definition's display plan mesh at quality `q`, moved here.
|
||||
pub(super) fn plan_mesh(&self, q: crate::display_quality::DisplayQuality, name: &str) -> Option<Arc<Mesh>> {
|
||||
let (source, _) = self.source_plan_mesh(q, name)?;
|
||||
Some(Arc::new(self.moved(&source)))
|
||||
}
|
||||
|
||||
/// The definition's reference mesh (shared by every placement of it) and this placement.
|
||||
pub(super) fn source_mesh(&self) -> (Arc<Mesh>, crate::Mat) {
|
||||
(reference_mesh(&self.body, &self.mesh, self.tolerance), self.transform)
|
||||
}
|
||||
|
||||
/// The definition's display plan mesh at quality `q` (shared by every placement of it) and
|
||||
/// this placement.
|
||||
pub(super) fn source_plan_mesh(&self, q: crate::display_quality::DisplayQuality, name: &str) -> Option<(Arc<Mesh>, crate::Mat)> {
|
||||
let cell = self.plan_meshes.get(q.index())?;
|
||||
let source = cell
|
||||
.get_or_init(|| {
|
||||
@@ -66,7 +78,7 @@ impl PlacedMesh {
|
||||
self.body.display_mesh_of(&plan).ok().map(Arc::new)
|
||||
})
|
||||
.clone()?;
|
||||
Some(Arc::new(self.moved(&source)))
|
||||
Some((source, self.transform))
|
||||
}
|
||||
|
||||
/// `source` (in the definition's place) moved by this placement.
|
||||
@@ -150,6 +162,44 @@ mod tests {
|
||||
assert!(Arc::ptr_eq(&mesh, &nested.mesh()));
|
||||
}
|
||||
|
||||
/// A placement hands out its definition's own meshes and its move without making the moved
|
||||
/// copy: every placement of a part gives the same mesh, and that mesh moved is the
|
||||
/// placement's own.
|
||||
#[test]
|
||||
fn placements_share_their_definitions_meshes_in_place() {
|
||||
use crate::display_quality::DisplayQuality;
|
||||
let source = ModelBody::new("Part".into(), kernel::cylinder(Vec3::ZERO, Vec3::Z, 2., 8.).unwrap(), 1);
|
||||
let placed: Vec<ModelBody> =
|
||||
[crate::rigid(Vec3::new(10., 20., 30.), Vec3::ZERO, Vec3::Z, 0.7), crate::rigid(Vec3::new(-4., 5., 1.), Vec3::ZERO, Vec3::X, 1.1)]
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let mut p = ModelBody::new("Copy".into(), kernel::transform_matrix(&source.body, *m).unwrap(), 1);
|
||||
p.mesh_placement = Some(Arc::new(PlacedMesh::new(&source, m).unwrap()));
|
||||
p
|
||||
})
|
||||
.collect();
|
||||
let (own, identity) = source.mesh_in_place();
|
||||
assert_eq!(identity, crate::IDENTITY);
|
||||
let q = DisplayQuality::parse("normal").unwrap();
|
||||
for p in &placed {
|
||||
let (mesh, m) = p.mesh_in_place();
|
||||
assert!(Arc::ptr_eq(&mesh, &own), "the definition's mesh, not a copy");
|
||||
let bounds = p.mesh_bounds();
|
||||
assert!(p.cached_mesh().is_none(), "no moved copy is made");
|
||||
let (plan_mesh, pm) = p.plan_mesh_in_place_at(q).unwrap();
|
||||
assert!(Arc::ptr_eq(&plan_mesh, &source.plan_mesh_at(q).unwrap()));
|
||||
assert_eq!(pm, m);
|
||||
let moved = p.mesh();
|
||||
let exact = moved.bounds();
|
||||
assert!(bounds.min.dist(exact.min) < 1e-9 && bounds.max.dist(exact.max) < 1e-9, "{bounds:?} vs {exact:?}");
|
||||
for (a, b) in mesh.positions.iter().zip(&moved.positions) {
|
||||
assert!(crate::apply_point(&m, *a).dist(*b) < 1e-9);
|
||||
}
|
||||
assert!(!Arc::ptr_eq(&p.display_identity(), &source.display_identity()), "each placement is its own body");
|
||||
assert!(Arc::ptr_eq(&p.display_identity(), &p.clone().display_identity()), "clones are the same body");
|
||||
}
|
||||
}
|
||||
|
||||
/// Placed parts share their definition's display plan (#3044): the definition is planned
|
||||
/// once, every placement moves that plan, and a moved plan evaluates to the definition's
|
||||
/// points moved, on every kind of surface (planes, revolved and spline surfaces).
|
||||
|
||||
@@ -89,7 +89,7 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let component = component_of(s, p, cmd)?;
|
||||
let items = keys(p, "items", cmd)?;
|
||||
let d = s.doc_mut();
|
||||
let id = super::next_free_id(cmd, d.browser_groups.iter().map(|g| g.id))?;
|
||||
let id = d.allocate_id()?;
|
||||
let name = match str_(p, "name").map(str::trim).filter(|n| !n.is_empty()) {
|
||||
Some(n) if n.len() <= 128 => n.to_string(),
|
||||
Some(_) => return Err(bad(cmd, "`name` is too long")),
|
||||
@@ -1097,12 +1097,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_next_free_id_is_one_past_the_largest_and_never_wraps() {
|
||||
use super::super::next_free_id;
|
||||
assert_eq!(next_free_id("t", std::iter::empty()).unwrap(), 1);
|
||||
assert_eq!(next_free_id("t", [3, 9, 4].into_iter()).unwrap(), 10);
|
||||
assert_eq!(next_free_id("t", [u64::MAX - 1].into_iter()).unwrap(), u64::MAX);
|
||||
assert!(next_free_id("t", [1, u64::MAX].into_iter()).is_err());
|
||||
fn exhausted_shared_ids_refuse_a_group_without_changing_the_design() {
|
||||
let mut s = Session::default();
|
||||
s.doc_mut().next_id = u64::MAX;
|
||||
let before = s.doc.to_json();
|
||||
let result = s.execute("browser.group", &json!({"folder": "bodies"}));
|
||||
assert!(result.is_err() && !matches!(result, Err(crate::EngineError::Internal(..))), "{result:?}");
|
||||
assert_eq!(s.doc.to_json(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_group_ids_are_not_reused_after_reopening() {
|
||||
let mut s = Session::default();
|
||||
let params = json!({"folder": "bodies"});
|
||||
let first = run(&mut s, "browser.group", params.clone())["group"].as_u64().unwrap();
|
||||
run(&mut s, "browser.ungroup", json!({"group": first}));
|
||||
let doc = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
*s.doc_mut() = doc;
|
||||
let second = run(&mut s, "browser.group", params)["group"].as_u64().unwrap();
|
||||
assert!(second > first, "deleted group {first} was reused as {second}");
|
||||
assert!(s.execute("browser.rename_group", &json!({"group": first, "name": "Stale reference"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -269,12 +269,14 @@ pub(super) fn clone_assembly(
|
||||
return Err(invalid("copy occurrence map points to a missing occurrence"));
|
||||
}
|
||||
unique_ids(source.assembly.joints.iter().map(|j| j.id))?;
|
||||
let mut joint_id = unique_ids(doc.assembly.joints.iter().map(|j| j.id))?;
|
||||
unique_ids(doc.assembly.joints.iter().map(|j| j.id))?;
|
||||
let aux = |a: &Assembly| {
|
||||
a.contact_sets.iter().map(|x| x.id).chain(a.studies.iter().map(|x| x.id)).chain(a.exploded.iter().map(|x| x.id)).collect::<Vec<_>>()
|
||||
};
|
||||
unique_ids(aux(&source.assembly).into_iter())?;
|
||||
let mut auxiliary_id = unique_ids(aux(&doc.assembly).into_iter())?;
|
||||
unique_ids(aux(&doc.assembly).into_iter())?;
|
||||
// Reserve from one persistent counter, committing it only when the entire copy succeeds.
|
||||
let mut item_id = doc.next_item_id()?.saturating_sub(1);
|
||||
let mut a = doc.assembly.clone();
|
||||
let mut messages = Vec::new();
|
||||
let mut origin_names = a.origins.iter().map(|o| o.name.clone()).collect();
|
||||
@@ -302,7 +304,7 @@ pub(super) fn clone_assembly(
|
||||
match (origin(&old.a, occurrences, features, bodies), origin(&old.b, occurrences, features, bodies)) {
|
||||
(Some(aa), Some(bb)) => {
|
||||
let mut item = old.clone();
|
||||
item.id = next_id(&mut joint_id)?;
|
||||
item.id = next_id(&mut item_id)?;
|
||||
item.name = unique_name(&old.name, &mut joint_names)?;
|
||||
item.a = aa;
|
||||
item.b = bb;
|
||||
@@ -354,7 +356,7 @@ pub(super) fn clone_assembly(
|
||||
}
|
||||
if let Some(ids) = old.occurrences.iter().map(|o| occurrences.get(o).copied()).collect::<Option<Vec<_>>>() {
|
||||
let mut item = old.clone();
|
||||
item.id = next_id(&mut auxiliary_id)?;
|
||||
item.id = next_id(&mut item_id)?;
|
||||
item.name = unique_name(&old.name, &mut contact_names)?;
|
||||
item.occurrences = ids;
|
||||
a.contact_sets.push(item);
|
||||
@@ -369,7 +371,7 @@ pub(super) fn clone_assembly(
|
||||
}
|
||||
if old.keys.iter().all(|k| joints.contains_key(&k.joint)) {
|
||||
let mut item = old.clone();
|
||||
item.id = next_id(&mut auxiliary_id)?;
|
||||
item.id = next_id(&mut item_id)?;
|
||||
item.name = unique_name(&old.name, &mut study_names)?;
|
||||
for key in &mut item.keys {
|
||||
key.joint = *joints.get(&key.joint).ok_or_else(|| invalid("missing copied study joint"))?;
|
||||
@@ -386,7 +388,7 @@ pub(super) fn clone_assembly(
|
||||
}
|
||||
if old.moves.iter().all(|(o, _)| occurrences.contains_key(o)) {
|
||||
let mut item = old.clone();
|
||||
item.id = next_id(&mut auxiliary_id)?;
|
||||
item.id = next_id(&mut item_id)?;
|
||||
item.name = unique_name(&old.name, &mut view_names)?;
|
||||
for (o, _) in &mut item.moves {
|
||||
*o = *occurrences.get(o).ok_or_else(|| invalid("missing copied exploded occurrence"))?;
|
||||
@@ -397,6 +399,7 @@ pub(super) fn clone_assembly(
|
||||
}
|
||||
}
|
||||
budget(&a)?;
|
||||
doc.next_id = item_id.checked_add(1).ok_or_else(|| invalid("assembly identifier space is exhausted"))?;
|
||||
doc.assembly = a;
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
@@ -76,12 +76,13 @@ fn documents() -> (Document, Document) {
|
||||
#[test]
|
||||
fn internal_relationships_receive_independent_ids_and_remapped_typed_references() {
|
||||
let (source, mut destination) = documents();
|
||||
destination.next_id = 1000;
|
||||
let report =
|
||||
clone_assembly(&mut destination, &source, &BTreeMap::from([(1, 101), (2, 102)]), &BTreeMap::from([(1, 11), (10, 110)]), &BTreeMap::new())
|
||||
.unwrap();
|
||||
let a = &destination.assembly;
|
||||
assert_eq!(a.joints.len(), 6);
|
||||
assert_eq!((a.joints[4].id, a.joints[5].id), (5, 6));
|
||||
assert_eq!((a.joints[4].id, a.joints[5].id), (1000, 1001));
|
||||
assert_eq!((a.joints[4].a.occurrence, a.joints[4].b.occurrence), (101, 102));
|
||||
assert_eq!(a.joints[4].values, source.assembly.joints[0].values);
|
||||
assert_eq!(a.joints[4].limits, source.assembly.joints[0].limits);
|
||||
@@ -90,13 +91,14 @@ fn internal_relationships_receive_independent_ids_and_remapped_typed_references(
|
||||
// Its x reference's face is the copy's too.
|
||||
assert!(matches!(&a.origins[3].origin.snap, Snap::FaceCenter { face: Some(face), x: Some(XRef::Face { face: x, .. }), .. }
|
||||
if face == "F11:start@F110.2" && x == "F110:side"));
|
||||
assert_eq!((a.links[2].a, a.links[2].b, a.links[2].ratio), (5, 6, -2.0));
|
||||
assert_eq!(a.contact_sets[2].id, 7);
|
||||
assert_eq!((a.links[2].a, a.links[2].b, a.links[2].ratio), (1000, 1001, -2.0));
|
||||
assert_eq!(a.contact_sets[2].id, 1002);
|
||||
assert_eq!(a.contact_sets[2].occurrences, vec![101, 102]);
|
||||
assert!(a.contact_sets[2].suppressed);
|
||||
assert_eq!(a.studies[2].id, 8);
|
||||
assert_eq!(a.studies[2].keys.iter().map(|k| k.joint).collect::<Vec<_>>(), vec![5, 6]);
|
||||
assert_eq!(a.exploded[2].id, 9);
|
||||
assert_eq!(a.studies[2].id, 1003);
|
||||
assert_eq!(a.studies[2].keys.iter().map(|k| k.joint).collect::<Vec<_>>(), vec![1000, 1001]);
|
||||
assert_eq!(a.exploded[2].id, 1004);
|
||||
assert_eq!(destination.next_id, 1005);
|
||||
assert_eq!(a.exploded[2].moves, vec![(101, Vec3::X * 10.0), (102, Vec3::Y * 20.0)]);
|
||||
assert_eq!(report.len(), 6);
|
||||
for kind in ["joint origin", "joint `Joint3`", "motion link", "contact set", "motion study", "exploded view"] {
|
||||
@@ -171,9 +173,9 @@ fn malformed_maps_identifier_exhaustion_and_aggregate_budgets_are_atomic() {
|
||||
invalid.assembly.links[0].ratio = f64::NAN;
|
||||
cases.push((invalid, destination, BTreeMap::from([(1, 101), (2, 102)])));
|
||||
for (source, mut destination, occurrences) in cases {
|
||||
let before = destination.assembly.clone();
|
||||
let before = destination.clone();
|
||||
assert!(clone_assembly(&mut destination, &source, &occurrences, &BTreeMap::from([(1, 11), (10, 110)]), &BTreeMap::new()).is_err());
|
||||
assert_eq!(destination.assembly, before);
|
||||
assert_eq!(destination, before);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,65 @@ struct CachedBody {
|
||||
chord: f64,
|
||||
}
|
||||
|
||||
/// Bodies [`ContactMeshes`] keeps; it starts over when full.
|
||||
const MAX_CACHED_MESHES: usize = 4 * MAX_BODIES;
|
||||
|
||||
/// A body's contact mesh and what is measured on it, made once per body content.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ContactMesh {
|
||||
mesh: Arc<Mesh>,
|
||||
chord: f64,
|
||||
/// Its thinnest width across its facet directions (the sweep's step is a quarter of it).
|
||||
thickness: f64,
|
||||
}
|
||||
|
||||
/// Contact meshes by the body's geometry ([`solidcraft_doc::ModelBody::geometry_key`]), shared
|
||||
/// by a session and its scratch copies: driving a joint again meshes no body that did not change.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ContactMeshes(HashMap<u64, ContactMesh>);
|
||||
|
||||
/// Tessellate `body` for contact checks and measure how thin it is.
|
||||
fn contact_mesh(body: &solidcraft_doc::ModelBody) -> Result<ContactMesh> {
|
||||
let tolerance = (body.body.size() * 1e-3).max(1e-3);
|
||||
let mesh = body.body.tessellate(tolerance)?;
|
||||
if mesh.triangles.len() > MAX_TRIANGLES || mesh.positions.len().saturating_add(mesh.triangles.len().saturating_mul(4)) > 200_000 {
|
||||
return Err(limit("body exceeds contact sweep tessellation budget"));
|
||||
}
|
||||
let extent = mesh.bounds().size();
|
||||
// Whole-body widths along bounded distinct facet normals catch tilted
|
||||
// plates without interpreting skinny mesh triangles as thin material.
|
||||
// Local thin walls of a wider body remain a sampling limitation.
|
||||
let mut thickness = extent.x.min(extent.y).min(extent.z);
|
||||
let mut directions: Vec<Vec3> = Vec::new();
|
||||
for triangle in &mesh.triangles {
|
||||
let Some([a, b, c]) = mesh.tri(triangle) else {
|
||||
return Err(limit("invalid contact tessellation"));
|
||||
};
|
||||
let Some(direction) = (b - a).cross(c - a).normalized() else { continue };
|
||||
if directions.len() < 32 && !directions.iter().any(|d| d.dot(direction).abs() > 1.0 - 1e-6) {
|
||||
directions.push(direction);
|
||||
}
|
||||
}
|
||||
for direction in directions {
|
||||
let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for point in &mesh.positions {
|
||||
let projected = point.dot(direction);
|
||||
low = low.min(projected);
|
||||
high = high.max(projected);
|
||||
}
|
||||
thickness = thickness.min(high - low);
|
||||
}
|
||||
if !thickness.is_finite() || thickness <= 1e-9 {
|
||||
return Err(limit("body is too thin for a bounded contact sweep"));
|
||||
}
|
||||
// Flat faces tessellate exactly; only planes known from how the face was
|
||||
// made count (an unknown face is taken as curved).
|
||||
let faces = body.body.face_count();
|
||||
let flat = faces <= 1024 && (0..faces).all(|i| body.body.face_plane(i).is_some());
|
||||
let chord = if flat { 0.0 } else { tolerance };
|
||||
Ok(ContactMesh { mesh: Arc::new(mesh), chord, thickness })
|
||||
}
|
||||
|
||||
/// A member of a contact set: (occurrence id, name).
|
||||
pub(super) type Member = (u64, String);
|
||||
|
||||
@@ -90,47 +149,24 @@ impl ContactGeometry {
|
||||
let (mesh, chord) = if let Some(cached) = meshes.get(&body.name) {
|
||||
cached.clone()
|
||||
} else {
|
||||
let tolerance = (body.body.size() * 1e-3).max(1e-3);
|
||||
let mesh = body.body.tessellate(tolerance)?;
|
||||
if mesh.triangles.len() > MAX_TRIANGLES || mesh.positions.len().saturating_add(mesh.triangles.len().saturating_mul(4)) > 200_000 {
|
||||
return Err(limit("body exceeds contact sweep tessellation budget"));
|
||||
}
|
||||
let extent = mesh.bounds().size();
|
||||
// Whole-body widths along bounded distinct facet normals catch tilted
|
||||
// plates without interpreting skinny mesh triangles as thin material.
|
||||
// Local thin walls of a wider body remain a sampling limitation.
|
||||
let mut thickness = extent.x.min(extent.y).min(extent.z);
|
||||
let mut directions: Vec<Vec3> = Vec::new();
|
||||
for triangle in &mesh.triangles {
|
||||
let Some([a, b, c]) = mesh.tri(triangle) else {
|
||||
return Err(limit("invalid contact tessellation"));
|
||||
};
|
||||
let Some(direction) = (b - a).cross(c - a).normalized() else { continue };
|
||||
if directions.len() < 32 && !directions.iter().any(|d| d.dot(direction).abs() > 1.0 - 1e-6) {
|
||||
directions.push(direction);
|
||||
let key = body.geometry_key();
|
||||
let known = key.and_then(|k| s.contact_meshes.lock().ok().and_then(|c| c.0.get(&k).cloned()));
|
||||
let made = match known {
|
||||
Some(made) => made,
|
||||
None => {
|
||||
let made = contact_mesh(body)?;
|
||||
if let (Some(k), Ok(mut cache)) = (key, s.contact_meshes.lock()) {
|
||||
if cache.0.len() >= MAX_CACHED_MESHES {
|
||||
cache.0.clear();
|
||||
}
|
||||
cache.0.insert(k, made.clone());
|
||||
}
|
||||
made
|
||||
}
|
||||
}
|
||||
for direction in directions {
|
||||
let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for point in &mesh.positions {
|
||||
let projected = point.dot(direction);
|
||||
low = low.min(projected);
|
||||
high = high.max(projected);
|
||||
}
|
||||
thickness = thickness.min(high - low);
|
||||
}
|
||||
if !thickness.is_finite() || thickness <= 1e-9 {
|
||||
return Err(limit("body is too thin for a bounded contact sweep"));
|
||||
}
|
||||
out.step = out.step.min(thickness * 0.25);
|
||||
let mesh = Arc::new(mesh);
|
||||
// Flat faces tessellate exactly; only planes known from how the face was
|
||||
// made count (an unknown face is taken as curved).
|
||||
let faces = body.body.face_count();
|
||||
let flat = faces <= 1024 && (0..faces).all(|i| body.body.face_plane(i).is_some());
|
||||
let chord = if flat { 0.0 } else { tolerance };
|
||||
meshes.insert(body.name.clone(), (mesh.clone(), chord));
|
||||
(mesh, chord)
|
||||
};
|
||||
out.step = out.step.min(made.thickness * 0.25);
|
||||
meshes.insert(body.name.clone(), (made.mesh.clone(), made.chord));
|
||||
(made.mesh, made.chord)
|
||||
};
|
||||
out.bodies.push(CachedBody {
|
||||
path: path.clone(),
|
||||
|
||||
@@ -26,6 +26,24 @@ fn rail_value(s: &Session) -> f64 {
|
||||
s.doc.assembly.joints.iter().find(|j| j.name == "Rail").unwrap().values[0]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_contact_reuses_reference_geometry_across_new_placements() {
|
||||
let mut s = slider();
|
||||
// Measure the swept-contact geometry factory, separately from other joint work.
|
||||
super::ContactGeometry::new(&s).unwrap();
|
||||
for value in [11., 12., 13., 14.] {
|
||||
let result = run(&mut s, "joint.drive", json!({"joint":"Rail","value":value}));
|
||||
let (_, work) = crate::kernel::profile::capture(|| super::ContactGeometry::new(&s).unwrap());
|
||||
assert!(result["stopped_by_contact"].is_null(), "{result}");
|
||||
assert!((rail_value(&s) - value).abs() < 1e-8);
|
||||
assert_eq!(work.tessellate_ms, 0., "unchanged contact bodies were tessellated for slider value {value}");
|
||||
}
|
||||
// Reusing geometry must still stop a genuine collision, including an endpoint beyond it.
|
||||
let result = run(&mut s, "joint.drive", json!({"joint":"Rail","value":-30}));
|
||||
assert!(result["stopped_by_contact"].is_object(), "{result}");
|
||||
assert!(rail_value(&s).abs() < 2e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slider_cannot_tunnel_to_a_collision_free_endpoint() {
|
||||
let mut s = slider();
|
||||
@@ -39,6 +57,28 @@ fn slider_cannot_tunnel_to_a_collision_free_endpoint() {
|
||||
assert!((rail_value(&s) + 100000.0).abs() < 1e-6, "disabled contact skips sweep budget");
|
||||
}
|
||||
|
||||
/// Each drive with contact on meshed every body in a contact set again: a body that did not
|
||||
/// change keeps its contact mesh across drives (and scratch copies), a changed one is meshed anew.
|
||||
#[test]
|
||||
fn contact_meshes_are_made_once_per_body() {
|
||||
let mut s = slider();
|
||||
let meshes = |s: &Session| super::ContactGeometry::new(s).unwrap().bodies.iter().map(|b| b.mesh.clone()).collect::<Vec<_>>();
|
||||
let first = meshes(&s);
|
||||
assert_eq!(first.len(), 2);
|
||||
run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 5}));
|
||||
let scratch = s.scratch();
|
||||
for again in [meshes(&s), meshes(&scratch)] {
|
||||
assert!(first.iter().zip(&again).all(|(a, b)| std::sync::Arc::ptr_eq(a, b)), "the same meshes, not tessellated again");
|
||||
}
|
||||
// A body that changed is meshed for what it is now.
|
||||
run(&mut s, "component.activate", json!({"component": "B"}));
|
||||
run(&mut s, "solid.box", json!({"length": 2, "width": 2, "height": 2, "corner": [24, 4, 10], "operation": "join"}));
|
||||
run(&mut s, "component.activate", json!({"component": 0}));
|
||||
let changed = meshes(&s);
|
||||
assert!(changed.iter().zip(&first).any(|(a, b)| !std::sync::Arc::ptr_eq(a, b)));
|
||||
assert!(changed.iter().map(|m| m.bounds().max.z).fold(0.0, f64::max) > 11.0, "the joined box is in the contact mesh");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revolute_cannot_cross_a_blocker_between_clear_endpoints() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -330,15 +330,7 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(_) => return Err(bad(cmd, "`name` must be 1…128 characters")),
|
||||
None => format!("Folder{}", s.doc.timeline_groups.len() + 1),
|
||||
};
|
||||
let id = s
|
||||
.doc
|
||||
.timeline_groups
|
||||
.iter()
|
||||
.map(|group| group.id)
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| bad(cmd, "no more timeline folder ids are available"))?;
|
||||
let id = s.doc_mut().allocate_id()?;
|
||||
let parent = match p.get("parent") {
|
||||
Some(Value::Null) | None => None,
|
||||
Some(_) => Some(timeline_group_id_field(s, p, "parent", cmd)?),
|
||||
@@ -1253,6 +1245,11 @@ fn redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if std::mem::discriminant(&new.kind) != std::mem::discriminant(&old.kind) && !(boundary(&new.kind) && boundary(&old.kind)) {
|
||||
return Err(bad(cmd, format!("{} is not a {} feature", old.name, cid)));
|
||||
}
|
||||
// The trial still holds the old feature, so a plane offset from its own name was made there,
|
||||
// and once swapped in it would be built on itself (#3567).
|
||||
if new.kind.names_feature(&old.name) {
|
||||
return Err(bad(cmd, format!("{} can't be built on itself: pick another plane or face", old.name)));
|
||||
}
|
||||
// Optional inputs can move other inputs' positions (e.g. Distance 2 goes before Taper).
|
||||
// Keep each existing model parameter attached to its semantic input, rather than its
|
||||
// old index. Constructor-assigned names for new inputs cannot reuse the old dimensions.
|
||||
|
||||
@@ -503,6 +503,20 @@ fn timeline_folders_refuse_unknown_non_contiguous_and_split_contents() {
|
||||
assert!(error.contains("not contiguous"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_timeline_group_ids_are_not_reused_after_reopening() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "name": "Block"}));
|
||||
let params = json!({"features": ["Block"]});
|
||||
let first = run(&mut s, "timeline.group", params.clone())["group"].as_u64().unwrap();
|
||||
run(&mut s, "timeline.ungroup", json!({"group": first}));
|
||||
let doc = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
*s.doc_mut() = doc;
|
||||
let second = run(&mut s, "timeline.group", params)["group"].as_u64().unwrap();
|
||||
assert!(second > first, "deleted folder {first} was reused as {second}");
|
||||
assert!(s.execute("timeline.rename_group", &json!({"group": first, "name": "Stale reference"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeline_move_many_is_atomic_and_one_undo_step() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -29,7 +29,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("solid.revolve", "Revolve", revolve)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("revolve")
|
||||
.params("axis: sketch line id | x | y (sketch axes) | X|Y|Z (world); angle?: expr (default 360 deg); angle2?: expr (Two Sides: the other side's angle); direction?: symmetric (angle split either side, independent of axis direction); convention?: sketch_normal|legacy_axis_start (default sketch_normal; legacy retains older angular starts); to?: [x,y,z] (To Object: turn until that face, vertex or point); sketch?, profiles? | face: [x,y,z] (a planar body face as the profile) and face_body?: name (whose face, where faces of several bodies meet), operation?, targets?, name?, body_name?"),
|
||||
.params("axis: sketch line id | x | y (sketch axes) | X|Y|Z (world) | construction axis name (followed when edited) | {origin, dir}; angle?: expr (default 360 deg); angle2?: expr (Two Sides: the other side's angle); direction?: symmetric (angle split either side, independent of axis direction); convention?: sketch_normal|legacy_axis_start (default sketch_normal; legacy retains older angular starts); to?: [x,y,z] (To Object: turn until that face, vertex or point); sketch?, profiles? | face: [x,y,z] (a planar body face as the profile) and face_body?: name (whose face, where faces of several bodies meet), operation?, targets?, name?, body_name?"),
|
||||
CommandSpec::new("solid.sweep", "Sweep", sweep)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("sweep")
|
||||
@@ -58,7 +58,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("solid.pattern.circular", "Circular Pattern", pattern_circ)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("pattern_circ")
|
||||
.params("features: [names] | bodies: [names] | components: [names]; axis: X|Y|Z | {origin, dir}; count; angle? (default 360 deg; partial arcs include both ends); alignment?: left|center|right (default right; original stays fixed); reversed?: bool (Flip; selects the extra side for even centered counts)"),
|
||||
.params("features: [names] | bodies: [names] | components: [names]; axis: X|Y|Z | construction axis name (followed when edited) | {origin, dir}; count; angle? (default 360 deg; partial arcs include both ends); alignment?: left|center|right (default right; original stays fixed); reversed?: bool (Flip; selects the extra side for even centered counts)"),
|
||||
CommandSpec::new("solid.pattern.path", "Pattern on Path", pattern_path)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("pattern_path")
|
||||
@@ -108,7 +108,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("construct.plane.at_angle", "Plane at Angle", plane_angle)
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("plane_angle")
|
||||
.params("base: XY|XZ|YZ|plane name; axis: X|Y|Z|{origin, dir}; angle: expr; name?"),
|
||||
.params("base: XY|XZ|YZ|plane name; axis: X|Y|Z|construction axis name (where it is now)|{origin, dir}; angle: expr; name?"),
|
||||
CommandSpec::new("solid.split_face", "Split Face", split_face)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("split_face")
|
||||
@@ -816,7 +816,7 @@ fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
None => (feature_sketch(s, p, cmd)?, None),
|
||||
};
|
||||
let axis = axis_param(p, cmd)?;
|
||||
let axis = axis_param(s, sketch, p, cmd)?;
|
||||
let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into());
|
||||
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
|
||||
let profiles = match face_profile {
|
||||
@@ -852,9 +852,10 @@ fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
add_feature(s, p, kind)
|
||||
}
|
||||
|
||||
/// A revolve axis from its parameter: a sketch line id, the sketch's x or y axis, a world axis,
|
||||
/// or {origin, dir}.
|
||||
pub(super) fn axis_param(p: &Value, cmd: &str) -> Result<AxisRef> {
|
||||
/// A revolve axis of `sketch` from its parameter: a sketch line id, the sketch's x or y axis, a
|
||||
/// world axis, a construction axis by name (followed when it is edited) or {origin, dir}. A line
|
||||
/// of the sketch wins over a construction axis of the same name.
|
||||
pub(super) fn axis_param(s: &Session, sketch: u64, p: &Value, cmd: &str) -> Result<AxisRef> {
|
||||
if let Some(o) = p.get("axis").filter(|v| v.is_object()) {
|
||||
let origin = o.get("origin").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let dir = o.get("dir").and_then(vec3).ok_or_else(|| bad(cmd, "axis needs `dir`"))?;
|
||||
@@ -863,11 +864,22 @@ pub(super) fn axis_param(p: &Value, cmd: &str) -> Result<AxisRef> {
|
||||
match str_(p, "axis") {
|
||||
Some(a @ ("X" | "Y" | "Z")) => Ok(AxisRef::World { axis: a.into() }),
|
||||
Some(a @ ("x" | "y")) => Ok(AxisRef::SketchAxis { axis: a.into() }),
|
||||
Some(name) if !is_sketch_line(s, sketch, name) && is_construction_axis(s, name) => Ok(AxisRef::Construction { name: name.into() }),
|
||||
Some(id) => Ok(AxisRef::SketchLine { curve: id.into() }),
|
||||
None => Err(bad(cmd, "`axis` must be a sketch line id, x, y (sketch axes), X, Y, Z or {origin, dir}")),
|
||||
None => Err(bad(cmd, "`axis` must be a sketch line id, x, y (sketch axes), X, Y, Z, a construction axis or {origin, dir}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `curve` is a curve of the sketch feature `sketch`.
|
||||
fn is_sketch_line(s: &Session, sketch: u64, curve: &str) -> bool {
|
||||
matches!(s.doc.feature(sketch).map(|f| &f.kind), Some(FeatureKind::Sketch { sketch, .. }) if sketch.curve_index(curve).is_some())
|
||||
}
|
||||
|
||||
/// Whether `name` is a construction axis feature of the design.
|
||||
fn is_construction_axis(s: &Session, name: &str) -> bool {
|
||||
s.doc.find_feature(name).is_some_and(|f| matches!(f.kind, FeatureKind::ConstructionAxis { .. }))
|
||||
}
|
||||
|
||||
fn radius_expr(s: &Session, p: &Value, cmd: &str) -> Result<Expr> {
|
||||
match (expr(p, "radius"), expr(p, "diameter")) {
|
||||
(Some(r), _) => Ok(r),
|
||||
@@ -1325,13 +1337,22 @@ fn source_features(s: &Session, p: &Value, cmd: &str) -> Result<Vec<String>> {
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
fn axis_line(p: &Value, cmd: &str) -> Result<(Vec3, Vec3)> {
|
||||
/// An axis line from `axis`: X, Y, Z, a construction axis by name or {origin, dir}. A construction
|
||||
/// axis comes back where it is now, with its name.
|
||||
fn axis_line(s: &Session, p: &Value, cmd: &str) -> Result<(Vec3, Vec3, Option<FeatureName>)> {
|
||||
match p.get("axis") {
|
||||
Some(Value::String(a)) => match a.to_ascii_uppercase().as_str() {
|
||||
"X" => Ok((Vec3::ZERO, Vec3::X)),
|
||||
"Y" => Ok((Vec3::ZERO, Vec3::Y)),
|
||||
"Z" => Ok((Vec3::ZERO, Vec3::Z)),
|
||||
_ => Err(bad(cmd, "axis must be X, Y, Z or {origin, dir}")),
|
||||
"X" => Ok((Vec3::ZERO, Vec3::X, None)),
|
||||
"Y" => Ok((Vec3::ZERO, Vec3::Y, None)),
|
||||
"Z" => Ok((Vec3::ZERO, Vec3::Z, None)),
|
||||
_ if is_construction_axis(s, a) => {
|
||||
let (_, _, origin, dir) = crate::view::construction_axes(s)
|
||||
.into_iter()
|
||||
.find(|(_, name, _, _)| name == a)
|
||||
.ok_or_else(|| bad(cmd, format!("construction axis `{a}` has no geometry: fix its definition first")))?;
|
||||
Ok((origin, dir, Some(a.as_str().into())))
|
||||
}
|
||||
_ => Err(bad(cmd, "axis must be X, Y, Z, a construction axis or {origin, dir}")),
|
||||
},
|
||||
Some(o @ Value::Object(_)) => {
|
||||
let origin = match o.get("origin") {
|
||||
@@ -1341,7 +1362,7 @@ fn axis_line(p: &Value, cmd: &str) -> Result<(Vec3, Vec3)> {
|
||||
})?,
|
||||
};
|
||||
let dir = o.get("dir").and_then(vec3).and_then(|d| d.normalized()).ok_or_else(|| bad(cmd, "axis needs a non-zero `dir`"))?;
|
||||
Ok((origin, dir))
|
||||
Ok((origin, dir, None))
|
||||
}
|
||||
_ => Err(bad(cmd, "`axis` is required")),
|
||||
}
|
||||
@@ -1425,7 +1446,7 @@ fn pattern_rect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn pattern_circ(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.pattern.circular";
|
||||
let (features, bodies) = pattern_sources(s, p, cmd)?;
|
||||
let (origin, axis) = axis_line(p, cmd)?;
|
||||
let (origin, axis, axis_feature) = axis_line(s, p, cmd)?;
|
||||
let count = req_expr(cmd, p, "count")?;
|
||||
check_expr(s, &count, Kind::Unitless, cmd, "count")?;
|
||||
let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into());
|
||||
@@ -1437,6 +1458,7 @@ fn pattern_circ(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
angle,
|
||||
alignment: super::alignment_param(p, "alignment", cmd)?,
|
||||
reversed: super::reversed_param(p, "reversed", cmd)?,
|
||||
axis_feature,
|
||||
};
|
||||
finish_pattern(s, p, features, pattern, bodies)
|
||||
}
|
||||
@@ -1613,7 +1635,8 @@ fn plane_offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn plane_angle(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "construct.plane.at_angle";
|
||||
let base = plane_param(s, p.get("base"), cmd)?;
|
||||
let (axis_origin, axis_dir) = axis_line(p, cmd)?;
|
||||
// The plane keeps the axis line as it is now.
|
||||
let (axis_origin, axis_dir, _) = axis_line(s, p, cmd)?;
|
||||
let angle = req_expr(cmd, p, "angle")?;
|
||||
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
|
||||
add_feature(
|
||||
|
||||
@@ -752,9 +752,7 @@ mod tests {
|
||||
let mut preview = saved_preview(&s).unwrap();
|
||||
// A checksum-valid but forged cached shape must not become editable geometry.
|
||||
for mesh in &mut preview.meshes {
|
||||
for coordinate in &mut mesh.positions {
|
||||
*coordinate *= 10.0;
|
||||
}
|
||||
mesh.positions = mesh.positions.iter().map(|c| c * 10.0).collect();
|
||||
}
|
||||
let bytes = solidcraft_io::write_design_container_with_preview(&s.doc, solidcraft_io::DesignCompression::Fast, Some(&preview)).unwrap();
|
||||
let path = std::env::temp_dir().join(format!("solidcraft-preview-{}.solidcraft", std::process::id()));
|
||||
|
||||
@@ -344,17 +344,16 @@ fn add_rigid_joints(s: &mut Session, pairs: &[(u64, u64, Mat)]) -> Result<Vec<u6
|
||||
// Too many to hold together: the parts are placed, not locked.
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut id = s.doc.assembly.joints.iter().map(|j| j.id).max().unwrap_or(0);
|
||||
let mut ids = Vec::with_capacity(pairs.len());
|
||||
let point = |m: &Mat, p: Vec3| solidcraft_doc::apply_point(m, p);
|
||||
let vector = |m: &Mat, v: Vec3| solidcraft_doc::apply_vector(m, v);
|
||||
let made: Vec<Joint> = pairs
|
||||
.iter()
|
||||
.map(|(a, b, m)| {
|
||||
id += 1;
|
||||
let id = s.doc_mut().allocate_id()?;
|
||||
ids.push(id);
|
||||
// As built: B's origin, seen from A (B's placement in A) and from B itself.
|
||||
Joint {
|
||||
Ok(Joint {
|
||||
id,
|
||||
name: format!("Rigid Group {a}:{b}"),
|
||||
kind: JointKind::Rigid,
|
||||
@@ -370,9 +369,9 @@ fn add_rigid_joints(s: &mut Session, pairs: &[(u64, u64, Mat)]) -> Result<Vec<u6
|
||||
angle: 0.0,
|
||||
suppressed: false,
|
||||
exprs: Default::default(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.collect::<Result<_>>()?;
|
||||
s.doc_mut().assembly.joints.extend(made);
|
||||
// Dozens of coinciding glyphs say nothing: the group shows in the browser instead.
|
||||
s.visibility.hidden_joints.retain(|h| !ids.contains(h));
|
||||
|
||||
@@ -576,10 +576,6 @@ fn limits_param(s: &Session, p: &Value, kind: JointKind, cmd: &str) -> Result<Ve
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn next_joint_id(s: &Session, cmd: &str) -> Result<u64> {
|
||||
super::next_free_id(cmd, s.doc.assembly.joints.iter().map(|j| j.id))
|
||||
}
|
||||
|
||||
fn add_joint(s: &mut Session, mut j: Joint, p: &Value) -> Result<Value> {
|
||||
if s.doc.assembly.joints.len() >= 10_000 {
|
||||
return Err(EngineError::Other("too many joints".into()));
|
||||
@@ -619,7 +615,7 @@ fn add_joint(s: &mut Session, mut j: Joint, p: &Value) -> Result<Value> {
|
||||
j.values = q;
|
||||
let id = j.id;
|
||||
let name = j.name.clone();
|
||||
// A new joint can take a deleted one's id; it starts shown.
|
||||
// A new joint starts shown, even if a stale visibility entry came from an older design.
|
||||
s.visibility.hidden_joints.retain(|h| *h != id);
|
||||
s.doc_mut().assembly.joints.push(j);
|
||||
let sol = resolve(s);
|
||||
@@ -652,13 +648,21 @@ fn kind_param(p: &Value, cmd: &str) -> Result<JointKind> {
|
||||
.ok_or_else(|| bad(cmd, "`type` must be rigid, revolute, slider, cylindrical, pin_slot, planar, ball, parallel, perpendicular, angle, point_on_plane, point_on_line or line_on_plane"))
|
||||
}
|
||||
|
||||
fn new_joint_name(s: &Session, p: &Value) -> String {
|
||||
str_(p, "name").map(str::to_string).unwrap_or_else(|| {
|
||||
let names: std::collections::BTreeSet<_> = s.doc.assembly.joints.iter().map(|j| j.name.as_str()).collect();
|
||||
let number = (1..=s.doc.assembly.joints.len().saturating_add(1)).find(|n| !names.contains(format!("Joint{n}").as_str())).unwrap_or(1);
|
||||
format!("Joint{number}")
|
||||
})
|
||||
}
|
||||
|
||||
fn joint(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "joint.create";
|
||||
let kind = kind_param(p, cmd)?;
|
||||
let a = origin_param(s, p.get("a"), cmd)?;
|
||||
let b = origin_param(s, p.get("b"), cmd)?;
|
||||
let id = next_joint_id(s, cmd)?;
|
||||
let name = str_(p, "name").map(str::to_string).unwrap_or_else(|| format!("Joint{id}"));
|
||||
let id = s.doc_mut().allocate_id()?;
|
||||
let name = new_joint_name(s, p);
|
||||
let offset = match p.get("offset") {
|
||||
Some(v) => value(s, v, false, cmd)?,
|
||||
None => 0.0,
|
||||
@@ -716,8 +720,8 @@ fn as_built(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let fa = side(oa)?;
|
||||
let fb = side(ob)?;
|
||||
let id = next_joint_id(s, cmd)?;
|
||||
let name = str_(p, "name").map(str::to_string).unwrap_or_else(|| format!("Joint{id}"));
|
||||
let id = s.doc_mut().allocate_id()?;
|
||||
let name = new_joint_name(s, p);
|
||||
let n = kind.dofs().len();
|
||||
let j = Joint {
|
||||
id,
|
||||
|
||||
@@ -25,6 +25,26 @@ fn near(a: Vec3, b: Vec3) -> bool {
|
||||
a.dist(b) < 1e-6
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_joint_ids_are_not_reused_after_reopening() {
|
||||
for command in ["joint.create", "joint.as_built"] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "component.create", json!({"name": "Part"}));
|
||||
let params = if command == "joint.create" {
|
||||
json!({"type": "revolute", "a": {"occurrence": 0, "point": [0, 0, 0]}, "b": {"occurrence": "Part", "point": [0, 0, 0]}})
|
||||
} else {
|
||||
json!({"type": "revolute", "a": 0, "b": "Part"})
|
||||
};
|
||||
let first = run(&mut s, command, params.clone())["joint"].as_u64().unwrap();
|
||||
run(&mut s, "joint.delete", json!({"joint": first}));
|
||||
let doc = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
*s.doc_mut() = doc;
|
||||
let second = run(&mut s, command, params)["joint"].as_u64().unwrap();
|
||||
assert!(second > first, "{command}: deleted joint {first} was reused as {second}");
|
||||
assert!(s.execute("joint.drive", &json!({"joint": first, "value": 45})).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// A hinge leaf (oracle 63's knuckle geometry): a 40 x 30 x 3 plate and an r4 knuckle along X
|
||||
/// at y = 33, z = 4.
|
||||
fn leaf(s: &mut Session, name: &str) {
|
||||
|
||||
@@ -313,13 +313,6 @@ pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
|
||||
all.iter().find(|c| c.id == id).or_else(|| all.iter().find(|c| c.id.eq_ignore_ascii_case(id))).copied()
|
||||
}
|
||||
|
||||
/// The id for a new item of a list: one past the largest id in it. Ids come from the design file,
|
||||
/// so the largest may be the largest a `u64` holds, and one past that is an error, not an overflow
|
||||
/// (a panic where overflow is checked, an id that is already taken where it is not).
|
||||
pub(crate) fn next_free_id(cmd: &str, ids: impl Iterator<Item = u64>) -> Result<u64> {
|
||||
ids.max().unwrap_or(0).checked_add(1).ok_or_else(|| crate::params::bad(cmd, "this design has no ids left to give"))
|
||||
}
|
||||
|
||||
/// The end of an unknown command's error: up to three ids like `id`, nearest first. Ids a few
|
||||
/// letters away (`solid.extrud` → `solid.extrude`) come before ids that end in the same word
|
||||
/// (`file.open` → `doc.open`). Empty when nothing is close.
|
||||
|
||||
@@ -19,7 +19,7 @@ mod analysis;
|
||||
#[path = "motion_animation.rs"]
|
||||
mod animation;
|
||||
#[path = "contact_sweep.rs"]
|
||||
mod contact_sweep;
|
||||
pub(crate) mod contact_sweep;
|
||||
#[path = "motion_sweep.rs"]
|
||||
mod sweep;
|
||||
|
||||
@@ -98,12 +98,6 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
|
||||
const MAX_ITEMS: usize = 10_000;
|
||||
|
||||
fn next_id(s: &Session, cmd: &str) -> Result<u64> {
|
||||
let a = &s.doc.assembly;
|
||||
let ids = a.contact_sets.iter().map(|c| c.id).chain(a.studies.iter().map(|m| m.id)).chain(a.exploded.iter().map(|e| e.id));
|
||||
super::next_free_id(cmd, ids)
|
||||
}
|
||||
|
||||
fn occurrences(s: &Session, p: &Value, cmd: &str) -> Result<Vec<u64>> {
|
||||
let list = p.get("occurrences").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`occurrences` must list occurrences"))?;
|
||||
if list.len() > MAX_ITEMS {
|
||||
@@ -477,7 +471,7 @@ fn new_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if s.doc.assembly.contact_sets.len() >= MAX_ITEMS {
|
||||
return Err(bad(cmd, "too many contact sets"));
|
||||
}
|
||||
let id = next_id(s, cmd)?;
|
||||
let id = s.doc_mut().allocate_id()?;
|
||||
let name = str_(p, "name").map(str::to_string).unwrap_or_else(|| format!("Contact Set{}", s.doc.assembly.contact_sets.len() + 1));
|
||||
let a = &mut s.doc_mut().assembly;
|
||||
a.contact_sets.push(ContactSet { id, name: name.clone(), occurrences: occ, suppressed: false });
|
||||
@@ -585,9 +579,13 @@ fn study(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
keys.push(StudyKey { joint, index, points });
|
||||
}
|
||||
}
|
||||
let id = next_id(s, cmd)?;
|
||||
let count = s.doc.assembly.studies.len();
|
||||
let a = &mut s.doc_mut().assembly;
|
||||
let doc = s.doc_mut();
|
||||
let id = match existing {
|
||||
Some(i) => doc.assembly.studies.get(i).ok_or_else(|| bad(cmd, "no such motion study"))?.id,
|
||||
None => doc.allocate_id()?,
|
||||
};
|
||||
let count = doc.assembly.studies.len();
|
||||
let a = &mut doc.assembly;
|
||||
let out = match existing.and_then(|i| a.studies.get_mut(i)) {
|
||||
Some(m) => {
|
||||
if let Some(n) = steps {
|
||||
@@ -1003,7 +1001,7 @@ fn explode_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if s.doc.assembly.exploded.len() >= MAX_ITEMS {
|
||||
return Err(bad(cmd, "too many exploded views"));
|
||||
}
|
||||
let id = next_id(s, cmd)?;
|
||||
let id = s.doc_mut().allocate_id()?;
|
||||
let name = str_(p, "name").map(str::to_string).unwrap_or_else(|| format!("Exploded View{}", s.doc.assembly.exploded.len() + 1));
|
||||
s.doc_mut().assembly.exploded.push(ExplodedView { id, name: name.clone(), moves: moves.clone() });
|
||||
Ok(json!({"view": id, "name": name, "moves": moves.iter().map(|(o, t)| json!({"occurrence": o, "translate": t})).collect::<Vec<_>>()}))
|
||||
|
||||
@@ -15,11 +15,444 @@ fn occ(s: &Session, component: &str) -> u64 {
|
||||
s.doc.occurrence_of(c).map(|o| o.id).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_motion_item_ids_are_not_reused_after_reopening() {
|
||||
for (create, delete, key, params) in [
|
||||
("contact.create", "contact.edit", "set", json!({"occurrences": ["A", "B"]})),
|
||||
("motion.study", "motion.delete", "study", json!({"name": "Travel", "steps": 10})),
|
||||
("explode.create", "explode.delete", "view", json!({"moves": [{"occurrence": "B", "translate": [20, 0, 0]}]})),
|
||||
] {
|
||||
let mut s = Session::default();
|
||||
for name in ["A", "B"] {
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
run(&mut s, "component.create", json!({"name": name}));
|
||||
}
|
||||
let first = run(&mut s, create, params.clone())[key].as_u64().unwrap();
|
||||
let deletion = json!({key: first, "delete": true});
|
||||
run(&mut s, delete, deletion.clone());
|
||||
let doc = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
*s.doc_mut() = doc;
|
||||
let second = run(&mut s, create, params)[key].as_u64().unwrap();
|
||||
assert!(second > first, "{create}: deleted item {first} was reused as {second}");
|
||||
assert!(s.execute(delete, &deletion).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// Where B's corner (20, 0, 0) is now.
|
||||
fn b_x(s: &Session) -> f64 {
|
||||
super::world_point(s, occ(s, "B"), Vec3::new(20.0, 0.0, 0.0)).x
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PartAuthoring {
|
||||
RootThenTransfer,
|
||||
RootThenNestedTransfer,
|
||||
InComponent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum JoinSelection {
|
||||
Named,
|
||||
Implicit,
|
||||
All,
|
||||
}
|
||||
|
||||
/// A boolean authored before a later transfer still reads the bodies in its timeline input,
|
||||
/// not their final owners. Explicit targets must not turn every occurrence into an input.
|
||||
fn joined_motion_part(authoring: PartAuthoring, selection: JoinSelection) -> Session {
|
||||
let mut s = Session::default();
|
||||
if matches!(authoring, PartAuthoring::InComponent) {
|
||||
run(&mut s, "component.create", json!({"name":"Rotor"}));
|
||||
}
|
||||
run(&mut s, "solid.box", json!({"length":10,"width":8,"height":6,"body_name":"Part","name":"Base"}));
|
||||
run(&mut s, "select.set", json!({"items":[]}));
|
||||
let mut p = json!({"length":6,"width":4,"height":4,"corner":[8,2,1],"operation":"join","name":"Boss"});
|
||||
match selection {
|
||||
JoinSelection::Named => p["targets"] = json!(["Part"]),
|
||||
JoinSelection::All => p["targets"] = json!(["*"]),
|
||||
JoinSelection::Implicit => {}
|
||||
}
|
||||
run(&mut s, "solid.box", p);
|
||||
if !matches!(authoring, PartAuthoring::InComponent) {
|
||||
let parent = if matches!(authoring, PartAuthoring::RootThenNestedTransfer) {
|
||||
run(&mut s, "component.create", json!({"name":"Group","activate":false}));
|
||||
"Group"
|
||||
} else {
|
||||
"root"
|
||||
};
|
||||
run(&mut s, "component.create", json!({"name":"Rotor","parent":parent,"activate":false}));
|
||||
run(&mut s, "component.move_bodies", json!({"bodies":["Part"],"component":"Rotor"}));
|
||||
}
|
||||
let driven = if matches!(authoring, PartAuthoring::RootThenNestedTransfer) { "Group" } else { "Rotor" };
|
||||
attach_motion_rotor(&mut s, driven, "Turn");
|
||||
s
|
||||
}
|
||||
|
||||
fn attach_motion_rotor(s: &mut Session, component: &str, joint: &str) {
|
||||
run(s, "component.activate", json!({"component":"root"}));
|
||||
run(s, "joint.as_built", json!({"type":"revolute","name":joint,"a":0,"b":component,"at":{"occurrence":0,"point":[0,0,0],"z":[0,0,1]}}));
|
||||
}
|
||||
|
||||
fn unrelated_motion_rotor(s: &mut Session) {
|
||||
run(s, "component.activate", json!({"component":"root"}));
|
||||
run(s, "component.create", json!({"name":"Unrelated"}));
|
||||
run(s, "solid.box", json!({"length":2,"width":2,"height":2,"corner":[100,100,100],"body_name":"Remote"}));
|
||||
attach_motion_rotor(s, "Unrelated", "Other turn");
|
||||
}
|
||||
|
||||
/// Test work and identity, not elapsed time. Every first visit to an angle must preserve the
|
||||
/// definitions and their warm meshes, even across full turns; returning to a cached pose alone
|
||||
/// would miss the regression. The world mesh is still in the new physical position.
|
||||
fn assert_joint_only_moves(s: &mut Session, joint: &str) {
|
||||
use std::sync::Arc;
|
||||
let definitions = s.model.state();
|
||||
let meshes: Vec<_> = definitions.bodies.iter().map(|b| b.mesh()).collect();
|
||||
for body in &s.world_state().bodies {
|
||||
std::hint::black_box(body.mesh());
|
||||
std::hint::black_box(body.display_plan());
|
||||
}
|
||||
let joints = run(s, "joint.list", json!({"quiet":true}));
|
||||
let base = joints["joints"].as_array().unwrap().iter().find(|j| j["name"] == joint).unwrap()["values"][0]["value"].as_f64().unwrap();
|
||||
for offset in [5., 10., 15., 37., 91., 365., 721., 12418., 12423., 37., 0.] {
|
||||
let angle = base + offset;
|
||||
let r = run(s, "joint.drive", json!({"joint":joint,"value":angle,"quiet":true}));
|
||||
assert_eq!(r["moved"], true, "{r}");
|
||||
assert!(r["stopped_by_loop"].is_null() && r["stopped_by_contact"].is_null(), "{r}");
|
||||
let joints = run(s, "joint.list", json!({"quiet":true}));
|
||||
let actual = joints["joints"].as_array().unwrap().iter().find(|j| j["name"] == joint).unwrap()["values"][0]["value"].as_f64().unwrap();
|
||||
assert!((actual - angle).abs() < 1e-8, "{joint} did not reach {angle}: {actual}");
|
||||
assert_eq!(s.model.last_recomputed, 0, "{joint} at {angle}: unchanged geometry was evaluated again");
|
||||
let current = s.model.state();
|
||||
assert!(Arc::ptr_eq(&definitions, ¤t), "{joint} at {angle}: definition state replaced");
|
||||
for (b, mesh) in current.bodies.iter().zip(&meshes) {
|
||||
assert!(Arc::ptr_eq(&b.mesh(), mesh), "{}: canonical mesh replaced", b.name);
|
||||
}
|
||||
let world = s.world_state();
|
||||
let (_, work) = crate::kernel::profile::capture(|| {
|
||||
for b in &world.bodies {
|
||||
let local = definitions.body(b.definition_name()).unwrap().mesh();
|
||||
let placed = b.mesh();
|
||||
assert_eq!(placed.triangles, local.triangles);
|
||||
assert_eq!(placed.tri_face, local.tri_face);
|
||||
assert_eq!(placed.positions.len(), local.positions.len());
|
||||
assert_eq!(placed.normals.len(), local.normals.len());
|
||||
for (p, q) in local.positions.iter().zip(&placed.positions) {
|
||||
assert!(crate::doc::apply_point(&b.source_transform, *p).dist(*q) < 1e-8);
|
||||
}
|
||||
for (n, q) in local.normals.iter().zip(&placed.normals) {
|
||||
assert!(crate::doc::apply_vector(&b.source_transform, *n).dist(*q) < 1e-8);
|
||||
}
|
||||
}
|
||||
});
|
||||
assert_eq!(work.tessellate_ms, 0., "{joint} at {angle}: reference tessellation ran");
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_motion_matches_full_rebuild(s: &Session) {
|
||||
let mut full = crate::doc::Model::default();
|
||||
full.no_cache = true;
|
||||
full.evaluate(&s.doc);
|
||||
assert!(full.results.iter().all(|r| r.error.is_none()), "{:?}", full.results.iter().filter_map(|r| r.error.as_ref()).collect::<Vec<_>>());
|
||||
let (cached, fresh) = (s.model.state(), full.state());
|
||||
assert_eq!(cached.bodies.len(), fresh.bodies.len());
|
||||
for (a, b) in cached.bodies.iter().zip(&fresh.bodies) {
|
||||
assert_eq!(a.name, b.name);
|
||||
assert_eq!(a.body.face_count(), b.body.face_count(), "{}", a.name);
|
||||
let (a_bounds, b_bounds) = (a.mesh().bounds(), b.mesh().bounds());
|
||||
assert!(a_bounds.min.dist(b_bounds.min) < 1e-8 && a_bounds.max.dist(b_bounds.max) < 1e-8, "{}", a.name);
|
||||
let (a_mass, b_mass) = (crate::kernel::measure(&a.body).unwrap(), crate::kernel::measure(&b.body).unwrap());
|
||||
assert!((a_mass.volume - b_mass.volume).abs() < 1e-6, "{}: {} vs {}", a.name, a_mass.volume, b_mass.volume);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_named_union_before_transfer_retains_definitions() {
|
||||
assert_joint_only_moves(&mut joined_motion_part(PartAuthoring::RootThenTransfer, JoinSelection::Named), "Turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_joined_cylinders_keep_mesh_cells_at_unseen_angles() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius":8.5,"height":8,"base":[0,32,0],"axis":[0,1,0],"operation":"new","body_name":"Crank"}));
|
||||
run(
|
||||
&mut s,
|
||||
"solid.cylinder",
|
||||
json!({"radius":2.8,"height":30,"base":[0,36,0],"axis":[0.981627183447664,0,0.1908089953765448],"operation":"join","targets":["Crank"]}),
|
||||
);
|
||||
run(&mut s, "component.create", json!({"name":"Crank","activate":false}));
|
||||
run(&mut s, "component.move_bodies", json!({"bodies":["Crank"],"component":"Crank"}));
|
||||
run(&mut s, "joint.as_built", json!({"type":"revolute","name":"Crank","a":0,"b":"Crank","at":{"occurrence":0,"point":[0,0,0],"z":[0,1,0]}}));
|
||||
assert_joint_only_moves(&mut s, "Crank");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_named_union_in_its_component_retains_definitions() {
|
||||
assert_joint_only_moves(&mut joined_motion_part(PartAuthoring::InComponent, JoinSelection::Named), "Turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_nested_body_transfer_ignores_ancestor_motion() {
|
||||
assert_joint_only_moves(&mut joined_motion_part(PartAuthoring::RootThenNestedTransfer, JoinSelection::Named), "Turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_second_occurrence_shares_the_definition() {
|
||||
let mut s = joined_motion_part(PartAuthoring::InComponent, JoinSelection::Named);
|
||||
let second = run(&mut s, "occurrence.copy", json!({"component":"Rotor","translate":[40,0,0]}))["occurrence"].as_u64().unwrap();
|
||||
run(&mut s, "joint.as_built", json!({"type":"revolute","name":"Copy turn","a":0,"b":second,"at":{"occurrence":0,"point":[40,0,0],"z":[0,0,1]}}));
|
||||
assert_eq!(s.world_state().bodies.len(), 2);
|
||||
assert_joint_only_moves(&mut s, "Copy turn");
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_implicit_union_in_its_component_retains_definitions() {
|
||||
assert_joint_only_moves(&mut joined_motion_part(PartAuthoring::InComponent, JoinSelection::Implicit), "Turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_unrelated_part_does_not_invalidate_named_union() {
|
||||
let mut s = joined_motion_part(PartAuthoring::RootThenTransfer, JoinSelection::Named);
|
||||
unrelated_motion_rotor(&mut s);
|
||||
assert_joint_only_moves(&mut s, "Other turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_future_body_is_not_an_input_of_an_earlier_wildcard_union() {
|
||||
let mut s = joined_motion_part(PartAuthoring::RootThenTransfer, JoinSelection::All);
|
||||
unrelated_motion_rotor(&mut s);
|
||||
assert_joint_only_moves(&mut s, "Other turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_empty_cache_keeps_unchanged_definition_results() {
|
||||
let mut s = joined_motion_part(PartAuthoring::RootThenTransfer, JoinSelection::Named);
|
||||
*s.model.cache.lock().unwrap() = Default::default();
|
||||
assert_joint_only_moves(&mut s, "Turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_explicit_extrude_targets_ignore_unrelated_motion() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":3,"body_name":"Base"}));
|
||||
run(&mut s, "sketch.create", json!({"plane":"XY","name":"BossOutline"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[8,2],"p1":[14,6]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch":"BossOutline","distance":3,"operation":"join","targets":["Base"],"name":"Boss"}));
|
||||
unrelated_motion_rotor(&mut s);
|
||||
assert_joint_only_moves(&mut s, "Other turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_combine_ignores_unrelated_motion() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Base"}));
|
||||
run(&mut s, "solid.box", json!({"length":2,"width":2,"height":2,"corner":[4,4,9],"body_name":"Boss","operation":"new"}));
|
||||
run(&mut s, "solid.combine", json!({"target":"Base","tools":["Boss"],"operation":"join"}));
|
||||
unrelated_motion_rotor(&mut s);
|
||||
assert_joint_only_moves(&mut s, "Other turn");
|
||||
}
|
||||
|
||||
/// A fixed tool removes the top 5 mm of the target where x=8..12. Sliding the target
|
||||
/// relative to it changes the removed volume from 100 to 200 mm³. Moving the tool together
|
||||
/// with the target preserves that volume and must preserve the computed definition too.
|
||||
fn cross_component_motion_cut(move_tool_with_target: bool, kind: &str) -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "component.create", json!({"name":"MovingTarget"}));
|
||||
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Target"}));
|
||||
run(&mut s, "component.activate", json!({"component":"root"}));
|
||||
if move_tool_with_target {
|
||||
run(&mut s, "component.create", json!({"name":"Tool"}));
|
||||
}
|
||||
run(&mut s, "solid.box", json!({"length":4,"width":12,"height":6,"corner":[8,-1,5],"operation":"cut","targets":["Target"],"name":"Notch"}));
|
||||
run(&mut s, "component.activate", json!({"component":"root"}));
|
||||
let axis = if kind == "slider" { json!([1, 0, 0]) } else { json!([0, 0, 1]) };
|
||||
run(&mut s, "joint.as_built", json!({"type":kind,"name":"Motion","a":0,"b":"MovingTarget","at":{"occurrence":0,"point":[0,0,0],"z":axis}}));
|
||||
if move_tool_with_target {
|
||||
run(&mut s, "joint.rigid_group", json!({"occurrences":["MovingTarget","Tool"]}));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn motion_target_volume(s: &Session) -> f64 {
|
||||
crate::kernel::measure(&s.model.state().body("Target").unwrap().body).unwrap().volume
|
||||
}
|
||||
|
||||
fn motion_combine_cut(kind: &str, together: bool) -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "component.create", json!({"name":"MovingTarget"}));
|
||||
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Target"}));
|
||||
run(&mut s, "component.activate", json!({"component":"root"}));
|
||||
run(&mut s, "component.create", json!({"name":"Tool"}));
|
||||
run(&mut s, "solid.box", json!({"length":4,"width":12,"height":6,"corner":[8,-1,5],"body_name":"Cutter","operation":"new"}));
|
||||
run(&mut s, "component.activate", json!({"component":"root"}));
|
||||
run(&mut s, "solid.combine", json!({"target":"Target","tools":["Cutter"],"operation":"cut"}));
|
||||
let axis = if kind == "slider" { json!([1, 0, 0]) } else { json!([0, 0, 1]) };
|
||||
run(&mut s, "joint.as_built", json!({"type":kind,"name":"Motion","a":0,"b":"MovingTarget","at":{"occurrence":0,"point":[0,0,0],"z":axis}}));
|
||||
if together {
|
||||
run(&mut s, "joint.rigid_group", json!({"occurrences":["MovingTarget","Tool"]}));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_combine_common_motion_keeps_the_target_frame() {
|
||||
let mut s = motion_combine_cut("revolute", true);
|
||||
assert_joint_only_moves(&mut s, "Motion");
|
||||
assert!((motion_target_volume(&s) - 900.).abs() < 1e-6);
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_combine_relative_motion_rebuilds_the_cut() {
|
||||
let mut s = motion_combine_cut("slider", false);
|
||||
assert!((motion_target_volume(&s) - 900.).abs() < 1e-6);
|
||||
run(&mut s, "joint.drive", json!({"joint":"Motion","value":5}));
|
||||
let volume = motion_target_volume(&s);
|
||||
assert!((volume - 800.).abs() < 1e-6, "Combine must follow relative motion: {volume}");
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_target_body_offset_rebuilds_the_cut() {
|
||||
let mut s = cross_component_motion_cut(false, "slider");
|
||||
assert!((motion_target_volume(&s) - 900.).abs() < 1e-6);
|
||||
let mut offset = crate::doc::IDENTITY;
|
||||
offset[3][0] = 5.;
|
||||
s.doc_mut().body_offsets.insert("Target".into(), offset);
|
||||
s.refresh();
|
||||
let volume = motion_target_volume(&s);
|
||||
assert!((volume - 800.).abs() < 1e-6, "the input body's offset changes the tool frame: {volume}");
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_unrelated_body_offset_keeps_named_union() {
|
||||
let mut s = joined_motion_part(PartAuthoring::RootThenTransfer, JoinSelection::Named);
|
||||
unrelated_motion_rotor(&mut s);
|
||||
let before = s.model.state();
|
||||
let mut offset = crate::doc::IDENTITY;
|
||||
offset[3][0] = 5.;
|
||||
s.doc_mut().body_offsets.insert("Remote".into(), offset);
|
||||
s.refresh();
|
||||
assert_eq!(s.model.last_recomputed, 0);
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.model.state()));
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_implicit_union_reads_its_target_body_offset() {
|
||||
let mut s = joined_motion_part(PartAuthoring::InComponent, JoinSelection::Implicit);
|
||||
let volume = |s: &Session| crate::kernel::measure(&s.model.state().body("Part").unwrap().body).unwrap().volume;
|
||||
assert!((volume(&s) - 544.).abs() < 1e-6);
|
||||
let mut offset = crate::doc::IDENTITY;
|
||||
offset[3][0] = 5.;
|
||||
s.doc_mut().body_offsets.insert("Part".into(), offset);
|
||||
s.refresh();
|
||||
assert!((volume(&s) - 480.).abs() < 1e-6, "the join's tool is now entirely inside its target: {}", volume(&s));
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_true_relative_target_motion_rebuilds_the_cut() {
|
||||
let mut s = cross_component_motion_cut(false, "slider");
|
||||
assert!((motion_target_volume(&s) - 900.).abs() < 1e-6);
|
||||
run(&mut s, "joint.drive", json!({"joint":"Motion","value":5}));
|
||||
let volume = motion_target_volume(&s);
|
||||
assert!((volume - 800.).abs() < 1e-6, "relative motion should leave 800 mm³, got {volume}");
|
||||
assert!(s.model.last_recomputed > 0, "a target moved relative to its tool");
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_shared_rigid_motion_keeps_the_cut_definition() {
|
||||
let mut s = cross_component_motion_cut(true, "slider");
|
||||
assert!((motion_target_volume(&s) - 900.).abs() < 1e-6);
|
||||
let before = s.model.state();
|
||||
run(&mut s, "joint.drive", json!({"joint":"Motion","value":5}));
|
||||
assert_eq!(s.model.last_recomputed, 0, "relative target/tool placement did not change");
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.model.state()));
|
||||
assert!((motion_target_volume(&s) - 900.).abs() < 1e-6);
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_shared_rotation_preserves_relative_cut_geometry() {
|
||||
let mut s = cross_component_motion_cut(true, "revolute");
|
||||
assert_joint_only_moves(&mut s, "Motion");
|
||||
assert!((motion_target_volume(&s) - 900.).abs() < 1e-6);
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_primed_ahead_result_does_not_replace_unchanged_geometry() {
|
||||
let mut s = joined_motion_part(PartAuthoring::RootThenTransfer, JoinSelection::Named);
|
||||
let definitions = s.model.state();
|
||||
let mesh = definitions.body("Part").unwrap().mesh();
|
||||
let commands = [("joint.drive".into(), json!({"joint":"Turn","value":5,"quiet":true}))];
|
||||
let evaluated = s.ahead().evaluate(&commands).unwrap();
|
||||
*s.model.cache.lock().unwrap() = Default::default();
|
||||
s.prime(&evaluated);
|
||||
run(&mut s, &commands[0].0, commands[0].1.clone());
|
||||
s.unprime();
|
||||
assert!(std::sync::Arc::ptr_eq(&definitions, &s.model.state()), "computing ahead must not replace an unchanged definition");
|
||||
assert!(std::sync::Arc::ptr_eq(&mesh, &s.model.state().body("Part").unwrap().mesh()));
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_save_and_reopen_keep_definitions_at_new_angles() {
|
||||
let mut s = joined_motion_part(PartAuthoring::RootThenTransfer, JoinSelection::Named);
|
||||
run(&mut s, "joint.drive", json!({"joint":"Turn","value":5}));
|
||||
let directory = std::env::temp_dir().join(format!(
|
||||
"solidcraft-joint-pose-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
|
||||
));
|
||||
std::fs::create_dir(&directory).unwrap();
|
||||
let path = directory.join("part.solidcraft");
|
||||
run(&mut s, "file.save", json!({"path":path,"cache":true,"preview":true}));
|
||||
let mut opened = Session::default();
|
||||
run(&mut opened, "doc.open", json!({"path":path}));
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
std::fs::remove_dir(&directory).unwrap();
|
||||
assert_motion_matches_full_rebuild(&opened);
|
||||
assert_joint_only_moves(&mut opened, "Turn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_undo_and_redo_match_fresh_geometry() {
|
||||
let mut s = joined_motion_part(PartAuthoring::RootThenTransfer, JoinSelection::Named);
|
||||
for angle in [5, 10] {
|
||||
run(&mut s, "joint.drive", json!({"joint":"Turn","value":angle}));
|
||||
}
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let joints = run(&mut s, "joint.list", json!({"quiet":true}));
|
||||
assert!((joints["joints"][0]["values"][0]["value"].as_f64().unwrap() - 5.).abs() < 1e-8);
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
let joints = run(&mut s, "joint.list", json!({"quiet":true}));
|
||||
assert!((joints["joints"][0]["values"][0]["value"].as_f64().unwrap() - 10.).abs() < 1e-8);
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joint_pose_geometry_edit_invalidates_the_warm_definition_mesh() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name":"span","expression":"10 mm"}));
|
||||
run(&mut s, "component.create", json!({"name":"Rotor"}));
|
||||
run(&mut s, "solid.box", json!({"length":"span","width":8,"height":6,"body_name":"Part"}));
|
||||
attach_motion_rotor(&mut s, "Rotor", "Turn");
|
||||
run(&mut s, "joint.drive", json!({"joint":"Turn","value":5}));
|
||||
let before = s.model.state().body("Part").unwrap().mesh();
|
||||
run(&mut s, "parameters.change", json!({"name":"span","expression":"12 mm"}));
|
||||
let model = s.model.state();
|
||||
let body = model.body("Part").unwrap();
|
||||
assert!(!std::sync::Arc::ptr_eq(&before, &body.mesh()));
|
||||
assert!((crate::kernel::measure(&body.body).unwrap().volume - 576.).abs() < 1e-6);
|
||||
assert_motion_matches_full_rebuild(&s);
|
||||
}
|
||||
|
||||
/// Cubes A (grounded, x 0…10) and B (x 20…30) on a slider along x: B's −x face on A's +x face
|
||||
/// at value 0, B 10 mm away to start.
|
||||
fn rail() -> Session {
|
||||
@@ -456,3 +889,112 @@ fn hostile_animation_params_are_refused() {
|
||||
assert!(root.join("in.gif").exists() && !dir.join("out").exists());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A grounded Base plate, an Arm turning on it about Z (joint `Pivot`) and a Head fixed to the
|
||||
/// arm. The Arm's slot sketch projects the arm's own edge and its axis, the Head's bore sketch
|
||||
/// an edge of the arm (they move together). With `in_context`, the slot sketch also projects an
|
||||
/// edge of the Base, across the joint.
|
||||
fn pivot(in_context: bool) -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "component.create", json!({"name": "Base"}));
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 40, "height": 10, "corner": [-20, -20, 0], "body_name": "Plate"}));
|
||||
run(&mut s, "solid.chamfer", json!({"edges": [[0, -20, 10]], "distance": 1}));
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
run(&mut s, "component.create", json!({"name": "Arm"}));
|
||||
run(&mut s, "solid.box", json!({"length": 50, "width": 8, "height": 5, "corner": [-4, -4, 10], "body_name": "Bar"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, 15], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": "Slot"}));
|
||||
run(&mut s, "sketch.project", json!({"refs": [{"edge": [20, 4, 15], "body": "Bar"}, "origin"]}));
|
||||
if in_context {
|
||||
run(&mut s, "sketch.project", json!({"refs": [{"edge": [0, 20, 10], "body": "Plate"}]}));
|
||||
}
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [20, -2], "p1": [30, 2]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Slot", "distance": -2, "operation": "cut", "name": "SlotCut"}));
|
||||
run(&mut s, "solid.chamfer", json!({"edges": [[10, -4, 15]], "distance": 0.5}));
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
run(&mut s, "component.create", json!({"name": "Head"}));
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 12, "height": 6, "corner": [36, -6, 15], "body_name": "Cap"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, 21], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": "Bore"}));
|
||||
run(&mut s, "sketch.project", json!({"refs": [{"edge": [40, 4, 15], "body": "Bar"}]}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [41, 0], "radius": 2}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Bore", "distance": -4, "operation": "cut", "name": "BoreCut"}));
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
let (base, arm, head) = (occ(&s, "Base"), occ(&s, "Arm"), occ(&s, "Head"));
|
||||
run(&mut s, "occurrence.ground", json!({"occurrence": base, "grounded": true}));
|
||||
run(
|
||||
&mut s,
|
||||
"joint.create",
|
||||
json!({"type": "revolute", "a": {"occurrence": base, "point": [0, 0, 10]}, "b": {"occurrence": arm, "point": [0, 0, 10]}, "flip": true, "name": "Pivot"}),
|
||||
);
|
||||
run(&mut s, "joint.rigid_group", json!({"occurrences": [arm, head]}));
|
||||
s
|
||||
}
|
||||
|
||||
/// Every body of `model` and `full` the same: names, faces and where their meshes lie.
|
||||
fn same_bodies(model: &solidcraft_doc::ModelState, full: &solidcraft_doc::ModelState) {
|
||||
assert_eq!(model.bodies.len(), full.bodies.len());
|
||||
for (a, b) in model.bodies.iter().zip(&full.bodies) {
|
||||
assert_eq!(a.name, b.name);
|
||||
assert_eq!(a.body.face_count(), b.body.face_count(), "{}", a.name);
|
||||
let (m, n) = (a.mesh().bounds(), b.mesh().bounds());
|
||||
assert!(m.min.dist(n.min) < 1e-9 && m.max.dist(n.max) < 1e-9, "{}: {m:?} vs {n:?}", a.name);
|
||||
}
|
||||
}
|
||||
|
||||
/// The model `s` shows, evaluated again from nothing.
|
||||
fn rebuilt(s: &Session) -> std::sync::Arc<solidcraft_doc::ModelState> {
|
||||
let mut full = solidcraft_doc::Model::default();
|
||||
full.no_cache = true;
|
||||
full.evaluate(&s.doc);
|
||||
full.state()
|
||||
}
|
||||
|
||||
/// Driving a joint only moves parts: a part that projects its own geometry, or that of a part
|
||||
/// moving with it, is not evaluated again (#1989's motion on a finished assembly evaluated the
|
||||
/// whole timeline on every drive, and every new pose missed the cache).
|
||||
#[test]
|
||||
fn driving_a_joint_moves_parts_without_evaluating_them() {
|
||||
let mut s = pivot(false);
|
||||
let model = s.model.state();
|
||||
let head = occ(&s, "Head");
|
||||
let tip = |s: &Session| super::world_point(s, head, Vec3::new(46.0, 0.0, 15.0));
|
||||
let before = tip(&s);
|
||||
for (step, angle) in ["30 deg", "75 deg", "-40 deg", "75 deg"].into_iter().enumerate() {
|
||||
let r = run(&mut s, "joint.drive", json!({"joint": "Pivot", "value": angle}));
|
||||
assert_eq!(r["moved"], true, "{r}");
|
||||
assert_eq!((s.model.last_recomputed, s.model.last_restored), (0, 0), "step {step}: nothing evaluated, nothing taken from the cache");
|
||||
assert!(std::sync::Arc::ptr_eq(&model, &s.model.state()), "the parts' bodies are the ones they were");
|
||||
}
|
||||
// The head went round with the arm: 75° about Z through the origin.
|
||||
let (sin, cos) = 75f64.to_radians().sin_cos();
|
||||
let want = Vec3::new(before.x * cos - before.y * sin, before.x * sin + before.y * cos, before.z);
|
||||
assert!(tip(&s).dist(want) < 1e-6, "{:?} vs {want:?}", tip(&s));
|
||||
same_bodies(&s.model.state(), &rebuilt(&s));
|
||||
}
|
||||
|
||||
/// A sketch that projects another part's edge is in-context design: it is evaluated again when
|
||||
/// that part moves relative to it (and gives what a full rebuild gives), not when the two move
|
||||
/// together.
|
||||
#[test]
|
||||
fn an_in_context_sketch_follows_only_the_parts_it_reads() {
|
||||
let mut s = pivot(true);
|
||||
let slot = s.doc.find_feature("Slot").map(|f| f.id).unwrap();
|
||||
let projected = |s: &Session| s.model.state().sketch(slot).map(|k| k.sketch.clone()).unwrap();
|
||||
let first = projected(&s);
|
||||
run(&mut s, "joint.drive", json!({"joint": "Pivot", "value": "30 deg"}));
|
||||
assert!(s.model.last_recomputed > 0, "the slot sketch reads the base, which turned relative to it");
|
||||
assert_ne!(projected(&s).links, first.links, "the base's edge is where the arm now sees it");
|
||||
same_bodies(&s.model.state(), &rebuilt(&s));
|
||||
// The whole assembly moved as one (both occurrences by the same turn and shift): nothing the
|
||||
// sketch reads moved relative to it.
|
||||
let model = s.model.state();
|
||||
let turn = solidcraft_doc::rigid(Vec3::new(5.0, -3.0, 2.0), Vec3::new(1.0, 2.0, 0.0), Vec3::new(0.3, 0.2, 1.0), 0.7);
|
||||
for o in &mut s.doc_mut().occurrences {
|
||||
o.transform = solidcraft_doc::mat_mul(&turn, &o.transform);
|
||||
}
|
||||
s.refresh();
|
||||
assert_eq!(s.model.last_recomputed, 0);
|
||||
assert!(std::sync::Arc::ptr_eq(&model, &s.model.state()));
|
||||
same_bodies(&s.model.state(), &rebuilt(&s));
|
||||
}
|
||||
|
||||
@@ -80,3 +80,22 @@ fn offset_plane_rejects_bad_face_picks_without_panicking() {
|
||||
assert_eq!(s.doc.features.len(), features, "{base}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #3567: an Offset Plane redefined with itself as its base was accepted while the old plane
|
||||
/// stood beside the new one, and then failed with "plane offsets nested too deeply". It is
|
||||
/// refused with a plain message, and the plane is left as it was.
|
||||
#[test]
|
||||
fn offset_plane_redefined_on_itself_is_refused() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "corner": [10, 0, 0]}));
|
||||
run(&mut s, "construct.plane.offset", json!({"base": {"face": [30, 0, 10]}, "offset": 10, "name": "Side"}));
|
||||
assert!((plane(&s, "Side").origin.y + 10.0).abs() < 1e-9, "{:?}", plane(&s, "Side"));
|
||||
let saved = s.doc.to_json();
|
||||
let redefine = |base: Value| json!({"feature": "Side", "command": "construct.plane.offset", "params": {"base": base, "offset": "7"}});
|
||||
let e = s.execute("timeline.redefine", &redefine(json!("Side"))).unwrap_err().to_string();
|
||||
assert!(e.contains("can't be built on itself"), "{e}");
|
||||
assert_eq!(s.doc.to_json(), saved);
|
||||
// The same edit on the face it was built on works.
|
||||
run(&mut s, "timeline.redefine", redefine(json!({"face": [30, 0, 10]})));
|
||||
assert!((plane(&s, "Side").origin.y + 7.0).abs() < 1e-9, "{:?}", plane(&s, "Side"));
|
||||
}
|
||||
|
||||
@@ -230,6 +230,35 @@ fn patterns_of_bodies() {
|
||||
assert!(s.execute("solid.pattern.rectangular", &json!({"bodies": ["Nope"], "dir1": [1, 0, 0], "count1": 2, "spacing1": 5})).is_err());
|
||||
}
|
||||
|
||||
/// A construction axis picked in the dialog comes as its name (#2885): the copies turn about it,
|
||||
/// and follow it when it is edited.
|
||||
#[test]
|
||||
fn circular_pattern_about_a_construction_axis_follows_it() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 2, "width": 2, "height": 2, "corner": [40, 0, 0], "body_name": "Cube"}));
|
||||
let axis = |x: f64| json!({"a": {"point": [x, 0, 0]}, "b": {"point": [x, 0, 10]}, "name": "Axis1"});
|
||||
run(&mut s, "construct.axis.two_points", axis(30.0));
|
||||
run(&mut s, "solid.pattern.circular", json!({"bodies": ["Cube"], "axis": "Axis1", "count": 4}));
|
||||
// The x and y extent of the four cubes.
|
||||
let span = |s: &Session| {
|
||||
let st = s.model.state();
|
||||
assert_eq!(st.bodies.len(), 4);
|
||||
let mut out = [f64::INFINITY, f64::NEG_INFINITY, f64::INFINITY, f64::NEG_INFINITY];
|
||||
for b in st.bodies.iter().filter_map(|mb| solidcraft_kernel::measure(&mb.body).ok()).map(|m| m.bbox) {
|
||||
out = [out[0].min(b.min.x), out[1].max(b.max.x), out[2].min(b.min.y), out[3].max(b.max.y)];
|
||||
}
|
||||
out
|
||||
};
|
||||
// About x = 30: the cube at x 40…42 also lands at 18…20, and at y ±(10…12).
|
||||
for (got, want) in span(&s).into_iter().zip([18.0, 42.0, -12.0, 12.0]) {
|
||||
assert!((got - want).abs() < 1e-6, "{got} vs {want}");
|
||||
}
|
||||
run(&mut s, "timeline.redefine", json!({"feature": "Axis1", "command": "construct.axis.two_points", "params": axis(35.0)}));
|
||||
for (got, want) in span(&s).into_iter().zip([28.0, 42.0, -7.0, 7.0]) {
|
||||
assert!((got - want).abs() < 1e-6, "{got} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patterns_of_components_place_occurrences() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -307,3 +307,56 @@ fn curved_targets_keep_only_the_material_before_the_post_for_both_axis_signs() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The construction-axis points of a feature's JSON moved along x by `dx`.
|
||||
fn shift_points(v: &mut Value, dx: f64) {
|
||||
match v {
|
||||
Value::Array(a) if a.len() == 3 && a.iter().all(Value::is_number) => a[0] = json!(a[0].as_f64().unwrap() + dx),
|
||||
Value::Array(a) => a.iter_mut().for_each(|x| shift_points(x, dx)),
|
||||
Value::Object(o) => o.values_mut().for_each(|x| shift_points(x, dx)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// A construction axis picked as the revolve axis (#2885, #4506) is saved by name, survives
|
||||
/// saving and opening, and the revolve follows the axis when it is edited.
|
||||
#[test]
|
||||
fn construction_axis_revolve_follows_the_axis() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "construct.axis.two_points", json!({"a":{"point":[0,0,0]},"b":{"point":[0,30,0]},"name":"Axis1"}));
|
||||
profile(&mut s, false);
|
||||
run(&mut s, "solid.revolve", json!({"sketch":"Profile","axis":"Axis1","angle":"180 deg","operation":"new","profiles":[0],"name":"Turn"}));
|
||||
let turn = s.doc.find_feature("Turn").unwrap();
|
||||
assert!(matches!(&turn.kind, FeatureKind::Revolve { axis: solidcraft_doc::AxisRef::Construction { name }, .. } if name == "Axis1"));
|
||||
let reopened = Document::from_json(&s.doc.to_json()).unwrap();
|
||||
assert_eq!(reopened.find_feature("Turn").unwrap().kind, turn.kind);
|
||||
let volume = |s: &Session| solidcraft_kernel::measure(&s.model.state().bodies[0].body).unwrap().volume;
|
||||
assert!((volume(&s) - 60.0 * PI).abs() < 0.05);
|
||||
assert!((volume(&Session::new(reopened)) - 60.0 * PI).abs() < 0.05);
|
||||
// Moving the axis 1 mm away from the profile widens the ring: radii 3 to 5 instead of 2 to 4.
|
||||
let mut axis = serde_json::to_value(s.doc.find_feature("Axis1").unwrap()).unwrap();
|
||||
let def = if axis.get("def").is_some() { axis.get_mut("def") } else { axis.pointer_mut("/kind/def") }.unwrap();
|
||||
shift_points(def, -1.0);
|
||||
let set = if axis.get("def").is_some() { json!({"def": axis["def"]}) } else { json!({"kind": {"def": axis["kind"]["def"]}}) };
|
||||
run(&mut s, "timeline.edit", json!({"feature":"Axis1","set":set}));
|
||||
assert!((volume(&s) - 80.0 * PI).abs() < 0.05, "{}", volume(&s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_axis_revolve_rejects_invalid_axes_atomically() {
|
||||
for (a, b, message) in [
|
||||
([0, 0, 0], [0, 0, 30], "does not lie in the sketch plane"),
|
||||
([0, 0, 1], [0, 30, 1], "does not lie in the sketch plane"),
|
||||
([3, 0, 0], [3, 30, 0], "profile crosses the revolve axis"),
|
||||
] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "construct.axis.two_points", json!({"a":{"point":a},"b":{"point":b},"name":"Axis1"}));
|
||||
profile(&mut s, false);
|
||||
let before = s.doc.to_json();
|
||||
let error =
|
||||
s.execute("solid.revolve", &json!({"sketch":"Profile","axis":"Axis1","angle":"180 deg","operation":"new"})).unwrap_err().to_string();
|
||||
assert!(error.contains(message), "{error}");
|
||||
assert_eq!(s.doc.to_json(), before);
|
||||
assert!(s.model.state().bodies.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,13 @@ fn make(s: &mut Session, p: &Value, cmd: &str, decal: bool) -> Result<Value> {
|
||||
// It belongs to the active component (its plane is in that frame) and moves with it.
|
||||
let component = s.active_component;
|
||||
let doc = s.doc_mut();
|
||||
let id = super::next_free_id(cmd, doc.canvases.iter().map(|c| c.id))?;
|
||||
let id = doc.allocate_id()?;
|
||||
let base = if decal { "Decal" } else { "Canvas" };
|
||||
let name = str_(p, "name").map(str::to_string).unwrap_or_else(|| format!("{base}{id}"));
|
||||
let name = str_(p, "name").map(str::to_string).unwrap_or_else(|| {
|
||||
let names: std::collections::BTreeSet<_> = doc.canvases.iter().map(|c| c.name.as_str()).collect();
|
||||
let number = (1..=doc.canvases.len().saturating_add(1)).find(|n| !names.contains(format!("{base}{n}").as_str())).unwrap_or(1);
|
||||
format!("{base}{number}")
|
||||
});
|
||||
doc.canvases.push(Canvas {
|
||||
id,
|
||||
name: name.clone(),
|
||||
@@ -170,6 +174,19 @@ mod tests {
|
||||
base64_encode(&b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_canvas_ids_are_not_reused_after_reopening() {
|
||||
let mut s = Session::default();
|
||||
let params = json!({"data": png(10, 10)});
|
||||
let first = s.execute("canvas.insert", ¶ms).unwrap()["canvas"].as_u64().unwrap();
|
||||
s.execute("canvas.delete", &json!({"canvas": first})).unwrap();
|
||||
let doc = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
*s.doc_mut() = doc;
|
||||
let second = s.execute("canvas.insert", ¶ms).unwrap()["canvas"].as_u64().unwrap();
|
||||
assert!(second > first, "deleted canvas {first} was reused as {second}");
|
||||
assert!(s.execute("canvas.edit", &json!({"canvas": first, "width": 500})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canvas_insert_edit_calibrate_save() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -758,6 +758,26 @@ fn refuse_infinite_corner(sk: &Sketch, p: &Value, cmd: &str) -> Result<()> {
|
||||
if at_corner { Err(bad(cmd, "an infinite line has no corner to round or cut: make it a normal line first")) } else { Ok(()) }
|
||||
}
|
||||
|
||||
/// Fillets and chamfers refuse a corner of an offset's output (#2874). The output's ends are
|
||||
/// recomputed from its source curves on every solve, so a corner cut there would be pulled back
|
||||
/// to the sharp corner, leaving full circles and an unsolved sketch.
|
||||
fn refuse_offset_corner(sk: &Sketch, p: &Value, cmd: &str) -> Result<()> {
|
||||
let output = |c: usize| sk.curves.get(c).filter(|c| sk.offset_distance(&c.id).is_some()).map(|c| c.id.clone());
|
||||
let found = match str_(p, "point").and_then(|r| sk.resolve_point(r)) {
|
||||
Some(q) => (0..sk.curves.len()).filter(|c| sk.curves.get(*c).is_some_and(|x| x.kind.uses(q))).find_map(output),
|
||||
None => ["a", "b"].iter().find_map(|k| str_(p, k).and_then(|i| sk.curve_index(i)).and_then(output)),
|
||||
};
|
||||
match found {
|
||||
Some(id) => Err(bad(
|
||||
cmd,
|
||||
format!(
|
||||
"`{id}` is an offset, and its corners follow its source curves: round or cut the source corners before offsetting, or break the offset association first"
|
||||
),
|
||||
)),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A corner of two lines: (line a, line b, corner point, far end of a, far end of b).
|
||||
fn corner(sk: &Sketch, p: &Value, cmd: &str) -> Result<(usize, usize, usize, usize, usize)> {
|
||||
let ends = |l: usize| match sk.curves.get(l).map(|c| &c.kind) {
|
||||
@@ -976,6 +996,7 @@ fn kept_json(kept: Vec<CornerKept>) -> (Vec<String>, Vec<String>) {
|
||||
/// the arc, the tangent constraints and what became of the corner point.
|
||||
fn fillet_corner(sk: &mut Sketch, p: &Value, r: f64, cmd: &str) -> Result<(usize, Vec<String>, CornerKept)> {
|
||||
refuse_infinite_corner(sk, p, cmd)?;
|
||||
refuse_offset_corner(sk, p, cmd)?;
|
||||
weld_corner(sk, p);
|
||||
let (la, lb, c, fa, fb) = match corner(sk, p, cmd) {
|
||||
Ok(x) => x,
|
||||
@@ -1086,6 +1107,7 @@ fn round_corner(sk: &mut Sketch, p: &Value, r: f64, cmd: &str) -> Result<Option<
|
||||
fn chamfer(s: &mut Session, p: &Value, cmd: &str, dists: impl FnOnce(f64) -> Result<(f64, f64)>) -> Result<Value> {
|
||||
let (out, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
refuse_infinite_corner(sk, p, cmd)?;
|
||||
refuse_offset_corner(sk, p, cmd)?;
|
||||
weld_corner(sk, p);
|
||||
let (la, lb, c, fa, fb) = corner(sk, p, cmd)?;
|
||||
let (pc, pa, pb) = (sk.point(c).unwrap_or_default(), sk.point(fa).unwrap_or_default(), sk.point(fb).unwrap_or_default());
|
||||
|
||||
@@ -1841,3 +1841,33 @@ fn trim_and_extend_refuse_an_offsets_ends_until_its_association_is_broken() {
|
||||
"{sh:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #2874: an offset's corners follow its source curves, so filleting them is refused before
|
||||
/// anything changes (the whole batch), instead of leaving full circles and an unsolved sketch.
|
||||
/// Once the offset association is broken the same corners round normally.
|
||||
#[test]
|
||||
fn fillet_refuses_an_offsets_corners_until_its_association_is_broken() {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]}));
|
||||
let out = ids(&run(&mut s, "sketch.offset", json!({"curves": ["l1", "l2", "l3", "l4"], "distance": "2 mm", "side": [-5, -5]}))["curves"]);
|
||||
assert_eq!(out, ["l5", "l6", "l7", "l8"]);
|
||||
let before = serde_json::to_value(sketch(&s)).unwrap();
|
||||
let corners = json!({"points": ["l5.start", "l5.end", "l6.end", "l7.end"], "radius": "2 mm"});
|
||||
let e = s.execute("sketch.fillet", &corners).unwrap_err().to_string();
|
||||
assert!(e.contains("offset") && e.contains("break the offset association"), "{e}");
|
||||
assert_eq!(serde_json::to_value(sketch(&s)).unwrap(), before, "a refused fillet changes nothing");
|
||||
let e = s.execute("sketch.chamfer.equal_distance", &json!({"point": "l6.end", "distance": 1})).unwrap_err().to_string();
|
||||
assert!(e.contains("offset"), "{e}");
|
||||
|
||||
run(&mut s, "sketch.offset.break", json!({"curve": "l5"}));
|
||||
let f = run(&mut s, "sketch.fillet", corners);
|
||||
assert_eq!(f["sketch"]["solved"], json!(true), "{f}");
|
||||
let sk = sketch(&s);
|
||||
let arcs = ids(&f["curves"]);
|
||||
assert_eq!(arcs.len(), 4);
|
||||
for arc in &arcs {
|
||||
let ai = sk.curve_index(arc).unwrap();
|
||||
assert!(matches!(sk.curves[ai].kind, CurveKind::Arc { .. }), "{arc} is a trimmed arc");
|
||||
assert!((sk.radius(ai).unwrap() - 2.0).abs() < 1e-7);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("surface.revolve", "Surface Revolve", surface_revolve)
|
||||
.at("SURFACE", "CREATE")
|
||||
.icon("surface_revolve")
|
||||
.params("sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); axis: sketch line id|x|y|X|Y|Z|{origin, dir}; angle?: expr (default 360 deg); angle2?: expr (the other side); symmetric?: bool; name?, body_name? (a surface body, no caps; a solid where a full turn closes it)"),
|
||||
.params("sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); axis: sketch line id|x|y|X|Y|Z|construction axis name|{origin, dir}; angle?: expr (default 360 deg); angle2?: expr (the other side); symmetric?: bool; name?, body_name? (a surface body, no caps; a solid where a full turn closes it)"),
|
||||
CommandSpec::new("surface.sweep", "Surface Sweep", surface_sweep)
|
||||
.at("SURFACE", "CREATE")
|
||||
.icon("surface_sweep")
|
||||
@@ -333,7 +333,7 @@ fn surface_revolve(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "surface.revolve";
|
||||
let sketch = feature_sketch(s, p, cmd)?;
|
||||
let (profiles, curves) = sheet_inputs(p, cmd)?;
|
||||
let axis = axis_param(p, cmd)?;
|
||||
let axis = axis_param(s, sketch, p, cmd)?;
|
||||
let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into());
|
||||
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
|
||||
let angle2 = expr(p, "angle2");
|
||||
|
||||
@@ -177,12 +177,14 @@ impl Ahead {
|
||||
}
|
||||
|
||||
/// Run `commands` (their results stay in the shared evaluation cache, and in [`Ahead::kept`]),
|
||||
/// then make the display meshes and plans of the bodies they end with: the bodies the run
|
||||
/// here finds share them, so the window draws, fits and picks without tessellating anything.
|
||||
pub fn evaluate(self, commands: &[(String, Value)]) -> Result<()> {
|
||||
let (s, _) = self.0.preview_in_place(commands)?;
|
||||
s.world_state().warm_display();
|
||||
Ok(())
|
||||
/// then make the display meshes and plans of the bodies they end with, so the window draws,
|
||||
/// fits and picks without tessellating anything. What they evaluated comes back for
|
||||
/// [`Session::prime`], so running them for real evaluates nothing again: the shared cache
|
||||
/// drops results past its budget, and a large design's states are past it.
|
||||
pub fn evaluate(self, commands: &[(String, Value)]) -> Result<Evaluated> {
|
||||
let (after, _) = self.0.preview_in_place(commands)?;
|
||||
after.world_state().warm_display();
|
||||
Ok(Evaluated(after.model))
|
||||
}
|
||||
|
||||
/// Run `commands` one after another, each reply or error in turn (what they evaluate stays
|
||||
@@ -193,6 +195,10 @@ impl Ahead {
|
||||
}
|
||||
}
|
||||
|
||||
/// The model a session copy evaluated ahead ([`Ahead::evaluate`]).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Evaluated(solidcraft_doc::Model);
|
||||
|
||||
/// What some commands would make, evaluated on a scratch copy of a session
|
||||
/// ([`Session::preview`]). The session itself is not changed.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -267,6 +273,9 @@ pub struct Session {
|
||||
/// Recent body renames: the design before and after each, and the names, so undo and redo
|
||||
/// follow them without evaluating either.
|
||||
body_renames: Vec<BodyRename>,
|
||||
/// Bodies' contact meshes (shared with scratch copies): a joint driven again meshes no body
|
||||
/// that did not change.
|
||||
contact_meshes: std::sync::Arc<std::sync::Mutex<cmd::motion::contact_sweep::ContactMeshes>>,
|
||||
/// Features copied with `timeline.copy` (pasted by `timeline.paste`).
|
||||
pub clipboard: Vec<solidcraft_doc::Feature>,
|
||||
/// Sketch geometry copied with `sketch.copy` (pasted by `sketch.paste`).
|
||||
@@ -421,6 +430,7 @@ impl Session {
|
||||
removed_later_cache: Default::default(),
|
||||
clearance_cache: Default::default(),
|
||||
body_renames: Vec::new(),
|
||||
contact_meshes: Default::default(),
|
||||
clipboard: Vec::new(),
|
||||
sketch_clipboard: None,
|
||||
section: None,
|
||||
@@ -953,6 +963,7 @@ impl Session {
|
||||
removed_later_cache: Default::default(),
|
||||
clearance_cache: self.clearance_cache.clone(),
|
||||
body_renames: Vec::new(),
|
||||
contact_meshes: self.contact_meshes.clone(),
|
||||
clipboard: self.clipboard.clone(),
|
||||
sketch_clipboard: self.sketch_clipboard.clone(),
|
||||
section: None,
|
||||
@@ -978,6 +989,19 @@ impl Session {
|
||||
self.revision = old.max(previous_revision) + 1;
|
||||
}
|
||||
|
||||
/// Offer what a copy of this session evaluated ahead for the commands about to run
|
||||
/// ([`Ahead::evaluate`]): until [`Session::unprime`], evaluating takes each feature result
|
||||
/// whose inputs match instead of computing it again (a design changed meanwhile matches
|
||||
/// less, or nothing, and is evaluated as usual).
|
||||
pub fn prime(&mut self, evaluated: &Evaluated) {
|
||||
self.model.prime(&evaluated.0);
|
||||
}
|
||||
|
||||
/// Stop offering what [`Session::prime`] offered.
|
||||
pub fn unprime(&mut self) {
|
||||
self.model.unprime();
|
||||
}
|
||||
|
||||
/// Commit commands evaluated on a scratch copy without losing this session's file,
|
||||
/// saved state, view state, checkpoints or earlier undo steps.
|
||||
pub fn accept_computed(&mut self, mut computed: Session) {
|
||||
|
||||
@@ -3242,6 +3242,39 @@ fn computed_import_preserves_file_and_earlier_undo_history() {
|
||||
assert!(s.model.state().bodies.is_empty());
|
||||
}
|
||||
|
||||
/// Commands evaluated ahead (off the window thread) and then run for real take what was
|
||||
/// evaluated: on a design whose states are past the shared cache's budget the second run
|
||||
/// evaluated everything again, so an off-thread command cost twice its time.
|
||||
#[test]
|
||||
fn commands_evaluated_ahead_are_not_evaluated_again() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "w", "expression": "40 mm"}));
|
||||
run(&mut s, "solid.box", json!({"length": "w", "width": 30, "height": 10, "body_name": "Block"}));
|
||||
run(&mut s, "solid.chamfer", json!({"edges": [[10, 0, 10]], "distance": 1}));
|
||||
run(&mut s, "solid.box", json!({"length": 5, "width": 5, "height": 20, "corner": [2, 2, 0], "operation": "cut"}));
|
||||
let commands = vec![("parameters.change".to_string(), json!({"name": "w", "expression": "50 mm"}))];
|
||||
let evaluated = s.ahead().evaluate(&commands).unwrap();
|
||||
// The shared cache dropped those results (as it does past its budget).
|
||||
s.model.cache = Default::default();
|
||||
let mut unprimed = s.scratch();
|
||||
s.prime(&evaluated);
|
||||
let r = run(&mut s, "parameters.change", json!({"name": "w", "expression": "50 mm"}));
|
||||
s.unprime();
|
||||
assert_eq!((&r["recomputed"], &r["restored"]), (&json!(0), &json!(3)), "every feature taken from the evaluation ahead: {r}");
|
||||
let r = run(&mut unprimed, "parameters.change", json!({"name": "w", "expression": "50 mm"}));
|
||||
assert_eq!(r["recomputed"], 3, "without it, all evaluated again: {r}");
|
||||
for (a, b) in s.model.state().bodies.iter().zip(&unprimed.model.state().bodies) {
|
||||
assert_eq!(a.mesh().bounds(), b.mesh().bounds());
|
||||
}
|
||||
// Inputs that differ from those evaluated ahead are evaluated, as usual.
|
||||
s.model.cache = Default::default();
|
||||
s.prime(&evaluated);
|
||||
let r = run(&mut s, "parameters.change", json!({"name": "w", "expression": "60 mm"}));
|
||||
s.unprime();
|
||||
assert_eq!(r["recomputed"], 3, "{r}");
|
||||
assert!((s.model.state().body("Block").unwrap().mesh().bounds().max.x - 60.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adopting_a_computed_component_keeps_its_warm_world_geometry() {
|
||||
let mut s = Session::default();
|
||||
@@ -3954,3 +3987,50 @@ fn cheap_commands_on_thousands_of_bodies_evaluate_nothing() {
|
||||
}
|
||||
assert_eq!(meshes(&s), shown, "every body keeps its mesh");
|
||||
}
|
||||
|
||||
/// #3397: a new, empty component changes nothing a feature reads, so creating one recomputes
|
||||
/// nothing, the first component of a design included: not a targeted cut, a Combine, a To
|
||||
/// Object extrude or a sketch on a face without a construction certificate (a legacy face
|
||||
/// plane, which reads every placed body), all of which read placements.
|
||||
#[test]
|
||||
fn creating_a_component_recomputes_nothing() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 30, "width": 30, "height": 10, "operation": "new", "body_name": "Block"}));
|
||||
run(&mut s, "solid.cylinder", json!({"diameter": 8, "height": 12, "base": [15, 15, -1], "operation": "cut", "targets": ["Block"]}));
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 30, "height": 20, "corner": [0, 0, 10], "operation": "new", "body_name": "Tab"}));
|
||||
run(&mut s, "solid.combine", json!({"target": "Block", "tools": ["Tab"], "operation": "join"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Post sketch"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [25, 25], "radius": 2}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Post sketch", "to": [25, 25, 10], "operation": "new", "body_name": "Post"}));
|
||||
// A plate whose top face loses its certificate: a patterned cut, a pad joined flush with
|
||||
// the top, then one more cut. Extruding that face makes a sketch on a legacy face plane.
|
||||
run(&mut s, "solid.box", json!({"length": 100, "width": 100, "height": 10, "corner": [200, 0, 0], "operation": "new", "body_name": "Plate"}));
|
||||
run(
|
||||
&mut s,
|
||||
"solid.cylinder",
|
||||
json!({"diameter": 4, "height": 12, "base": [210, 10, -1], "operation": "cut", "targets": ["Plate"], "name": "Pilot"}),
|
||||
);
|
||||
run(
|
||||
&mut s,
|
||||
"solid.pattern.rectangular",
|
||||
json!({"features": ["Pilot"], "dir1": [1, 0, 0], "count1": 4, "spacing1": 20, "dir2": [0, 1, 0], "count2": 4, "spacing2": 20}),
|
||||
);
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": "5 mm", "name": "Pad sketch"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [260, 60], "p1": [295, 95]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Pad sketch", "distance": 5, "operation": "join", "targets": ["Plate"]}));
|
||||
run(&mut s, "solid.box", json!({"length": 5, "width": 100, "height": 12, "corner": [199, 0, -1], "operation": "cut", "targets": ["Plate"]}));
|
||||
run(&mut s, "solid.extrude", json!({"face": [240, 40, 10], "distance": 1, "operation": "join", "name": "Face layer"}));
|
||||
let legacy_face = s
|
||||
.doc
|
||||
.features
|
||||
.iter()
|
||||
.any(|f| matches!(&f.kind, solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Face { anchor: None, .. }, .. }));
|
||||
assert!(legacy_face, "the face extrude's sketch is on a legacy face plane");
|
||||
for name in ["First", "Second"] {
|
||||
run(&mut s, "component.create", json!({"name": name}));
|
||||
assert_eq!(s.model.last_recomputed, 0, "creating {name} recomputed a feature");
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,31 @@ fn imported_box() -> Session {
|
||||
Session::new(doc)
|
||||
}
|
||||
|
||||
/// An edit keeps the design as it was for undo, and an imported mesh's triangles are shared
|
||||
/// with that copy, not copied (a scan's are hundreds of megabytes): after an edit and its undo
|
||||
/// the document holds the same arrays. They still save as plain arrays.
|
||||
#[test]
|
||||
fn edits_share_an_imported_meshs_triangles_with_undo() {
|
||||
let mut s = imported_box();
|
||||
let arrays = |s: &Session| match &s.doc.features[0].kind {
|
||||
FeatureKind::MeshImport { meshes, .. } => (meshes[0].positions.clone(), meshes[0].triangles.clone()),
|
||||
other => panic!("{other:?}"),
|
||||
};
|
||||
let (positions, triangles) = arrays(&s);
|
||||
run(&mut s, "parameters.add", json!({"name":"size","expression":"2","unit":""}));
|
||||
let (p, t) = arrays(&s);
|
||||
assert!(std::sync::Arc::ptr_eq(&p, &positions) && std::sync::Arc::ptr_eq(&t, &triangles), "shared after an edit");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let (p, t) = arrays(&s);
|
||||
assert!(std::sync::Arc::ptr_eq(&p, &positions) && std::sync::Arc::ptr_eq(&t, &triangles), "shared after undo");
|
||||
let saved =
|
||||
serde_json::to_value(MeshData { name: "M".into(), positions: vec![1.5, 2.0, 3.0].into(), triangles: vec![0, 0, 0].into(), color: None })
|
||||
.unwrap();
|
||||
assert_eq!(saved, json!({"name":"M","positions":[1.5,2.0,3.0],"triangles":[0,0,0]}));
|
||||
let back: MeshData = serde_json::from_value(saved).unwrap();
|
||||
assert_eq!(&back.positions[..], &[1.5, 2.0, 3.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_conversion_is_parametric_undoable_and_editable_after_cold_reopen() {
|
||||
let mut s = imported_box();
|
||||
|
||||
@@ -64,6 +64,25 @@ fn total_volume(s: &mut Session) -> f64 {
|
||||
run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imported_joints_do_not_reuse_deleted_ids() {
|
||||
let path = frame_file("joint-ids");
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "file.insert_step", json!({"path": path}));
|
||||
let first: Vec<_> = s.doc.assembly.joints.iter().map(|j| j.id).collect();
|
||||
assert!(!first.is_empty());
|
||||
for id in &first {
|
||||
run(&mut s, "joint.delete", json!({"joint": id}));
|
||||
}
|
||||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||||
run(&mut s, "file.insert_step", json!({"path": path}));
|
||||
assert!(!s.doc.assembly.joints.is_empty());
|
||||
for joint in &s.doc.assembly.joints {
|
||||
assert!(!first.contains(&joint.id), "import reused deleted joint {}", joint.id);
|
||||
assert!(joint.id < s.doc.next_id, "import did not reserve joint {}", joint.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// `up: "y"` turns the file's bodies a quarter turn about X, Y becoming Z: the frame's height
|
||||
/// lies along Z, and what was its depth now runs towards -Y.
|
||||
#[test]
|
||||
|
||||
@@ -110,7 +110,7 @@ pub fn bounds(s: &Session) -> Aabb3 {
|
||||
|
||||
/// [`bounds`], with the browser's Sketches folder shown (`sketches`) or hidden.
|
||||
pub fn visible_bounds(s: &Session, sketches: bool) -> Aabb3 {
|
||||
bounds_by(s, sketches, |body| body.mesh().bounds())
|
||||
bounds_by(s, sketches, |body| body.mesh_bounds())
|
||||
}
|
||||
|
||||
/// [`visible_bounds`] without meshing anything: a body not meshed yet counts with its outline
|
||||
|
||||
@@ -27,6 +27,7 @@ pub mod network;
|
||||
mod plane;
|
||||
mod profile;
|
||||
pub mod section;
|
||||
pub mod simplify;
|
||||
pub mod spline_space;
|
||||
pub mod tess;
|
||||
pub mod tspline;
|
||||
|
||||
@@ -0,0 +1,551 @@
|
||||
//! Triangle mesh simplification, for drawing a large mesh coarser where it is small on screen
|
||||
//! (display levels of detail). Display only: nothing measured, exported or picked reads a
|
||||
//! simplified mesh.
|
||||
//!
|
||||
//! Edge collapse ordered by quadric error (Garland and Heckbert, "Surface Simplification Using
|
||||
//! Quadric Error Metrics", SIGGRAPH 1997): each vertex carries the sum of the squared-distance
|
||||
//! quadrics of the planes of its faces; the edge whose two ends merge at the point of least
|
||||
//! summed squared distance goes first. A collapse that would fold a face over (its normal turning
|
||||
//! by more than about 80°) or join two sheets of the surface (the ends sharing more neighbours
|
||||
//! than the faces between them) is skipped. Open boundaries carry extra planes across them, so
|
||||
//! they keep their place.
|
||||
//!
|
||||
//! [`levels`] simplifies once, keeping a copy of the mesh each time it reaches the next target
|
||||
//! triangle count, with its error measured: how far its vertices and the centres of its triangles
|
||||
//! lie from the original surface, at most.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BinaryHeap;
|
||||
|
||||
use crate::Vec3;
|
||||
|
||||
/// Most triangles [`levels`] simplifies (more: no levels).
|
||||
pub const MAX_TRIANGLES: usize = 50_000_000;
|
||||
|
||||
/// How much more an open boundary's planes count than a face's.
|
||||
const BOUNDARY_WEIGHT: f64 = 10.0;
|
||||
/// A collapse may turn a face's normal by at most this (cosine): about 80°.
|
||||
const MIN_TURN_COS: f64 = 0.2;
|
||||
|
||||
/// A simplified copy of a mesh.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Level {
|
||||
pub positions: Vec<Vec3>,
|
||||
pub triangles: Vec<[u32; 3]>,
|
||||
/// How far (mm) its vertices and triangle centres lie from the original surface, at most.
|
||||
pub error: f64,
|
||||
}
|
||||
|
||||
/// The mesh (`positions`, and `triangles` indexing them; corners at the same position are one
|
||||
/// vertex) simplified to each of `targets` triangles in turn (largest first), one [`Level`] per
|
||||
/// target reached. Simplification stops early when no edge can collapse any more; the levels made
|
||||
/// by then are returned, but none that is not at least a fifth smaller than the one before it.
|
||||
/// Triangles with an index out of range or a point not finite are left out; an empty or oversized
|
||||
/// mesh ([`MAX_TRIANGLES`]) has no levels.
|
||||
pub fn levels(positions: &[Vec3], triangles: &[[u32; 3]], targets: &[usize]) -> Vec<Level> {
|
||||
if triangles.is_empty() || triangles.len() > MAX_TRIANGLES {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(mut m) = Collapser::new(positions, triangles) else { return Vec::new() };
|
||||
let original = Surface::new(&m.pos, &m.tris);
|
||||
let mut out: Vec<Level> = Vec::new();
|
||||
let mut previous = m.live_triangles;
|
||||
for &target in targets {
|
||||
if target >= previous {
|
||||
continue;
|
||||
}
|
||||
m.collapse_to(target);
|
||||
if m.live_triangles * 5 > previous * 4 {
|
||||
break;
|
||||
}
|
||||
previous = m.live_triangles;
|
||||
let (mut level, seeds) = m.level();
|
||||
// Never less than a coarser level's own error.
|
||||
level.error = original.deviation(&level, &seeds).max(out.last().map_or(0.0, |l| l.error));
|
||||
out.push(level);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The original surface, for measuring how far a level strays from it.
|
||||
struct Surface {
|
||||
tris: Vec<[Vec3; 3]>,
|
||||
bvh: crate::Bvh,
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
fn new(pos: &[[f64; 3]], tris: &[[u32; 3]]) -> Surface {
|
||||
let tris: Vec<[Vec3; 3]> = tris.iter().map(|t| t.map(|i| pos.get(i as usize).map_or(Vec3::ZERO, |p| Vec3::new(p[0], p[1], p[2])))).collect();
|
||||
let boxes: Vec<crate::Aabb3> = tris
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let mut b = crate::Aabb3::EMPTY;
|
||||
t.iter().for_each(|p| b.add(*p));
|
||||
b
|
||||
})
|
||||
.collect();
|
||||
Surface { bvh: crate::Bvh::new(&boxes), tris }
|
||||
}
|
||||
|
||||
fn to_triangle(&self, p: Vec3, t: u32) -> f64 {
|
||||
self.tris.get(t as usize).map_or(f64::INFINITY, |[a, b, c]| crate::compare::closest_on_triangle(p, *a, *b, *c).dist(p))
|
||||
}
|
||||
|
||||
/// How far `p` lies from the surface, when that is more than `beyond`; otherwise
|
||||
/// `beyond` or less (the search stops at the first triangle within `beyond`). `near`: an
|
||||
/// original triangle `p` is likely near (where it came from), so the search starts close.
|
||||
fn distance_beyond(&self, p: Vec3, beyond: f64, near: u32) -> f64 {
|
||||
let best = std::cell::Cell::new(self.to_triangle(p, near));
|
||||
let gap = |b: &crate::Aabb3| {
|
||||
let d = |v: f64, lo: f64, hi: f64| (lo - v).max(v - hi).max(0.0);
|
||||
Vec3::new(d(p.x, b.min.x, b.max.x), d(p.y, b.min.y, b.max.y), d(p.z, b.min.z, b.max.z)).len()
|
||||
};
|
||||
self.bvh.visit(|b| best.get() > beyond && gap(b) < best.get(), |t| best.set(best.get().min(self.to_triangle(p, t))));
|
||||
best.get()
|
||||
}
|
||||
|
||||
/// How far `level`'s vertices and triangle centres lie from the surface, at most (`seeds`:
|
||||
/// an original triangle each vertex came from).
|
||||
fn deviation(&self, level: &Level, seeds: &[u32]) -> f64 {
|
||||
let at = |i: u32| level.positions.get(i as usize).copied().unwrap_or(Vec3::ZERO);
|
||||
let seed = |i: u32| seeds.get(i as usize).copied().unwrap_or(0);
|
||||
let vertices = level.positions.iter().copied().zip(seeds.iter().copied());
|
||||
let centres = level.triangles.iter().map(|t| {
|
||||
let [a, b, c] = t.map(at);
|
||||
((a + b + c) * (1.0 / 3.0), seed(t[0]))
|
||||
});
|
||||
let worst = vertices.chain(centres).fold(0.0, |worst: f64, (p, near)| worst.max(self.distance_beyond(p, worst, near)));
|
||||
if worst.is_finite() { worst } else { 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// A symmetric 4 × 4 quadric: a a, a b, a c, a d, b b, b c, b d, c c, c d, d d.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct Quadric([f64; 10]);
|
||||
|
||||
impl Quadric {
|
||||
/// The squared distance to the plane n · p + d = 0 (`n` unit), times `w`.
|
||||
fn plane(n: [f64; 3], d: f64, w: f64) -> Quadric {
|
||||
let [a, b, c] = n;
|
||||
Quadric([a * a * w, a * b * w, a * c * w, a * d * w, b * b * w, b * c * w, b * d * w, c * c * w, c * d * w, d * d * w])
|
||||
}
|
||||
|
||||
fn add(&mut self, o: &Quadric) {
|
||||
for (x, y) in self.0.iter_mut().zip(o.0) {
|
||||
*x += y;
|
||||
}
|
||||
}
|
||||
|
||||
fn sum(&self, o: &Quadric) -> Quadric {
|
||||
let mut q = *self;
|
||||
q.add(o);
|
||||
q
|
||||
}
|
||||
|
||||
/// The summed squared distance at `p`.
|
||||
fn at(&self, p: [f64; 3]) -> f64 {
|
||||
let [aa, ab, ac, ad, bb, bc, bd, cc, cd, dd] = self.0;
|
||||
let [x, y, z] = p;
|
||||
(aa * x * x
|
||||
+ 2.0 * ab * x * y
|
||||
+ 2.0 * ac * x * z
|
||||
+ 2.0 * ad * x
|
||||
+ bb * y * y
|
||||
+ 2.0 * bc * y * z
|
||||
+ 2.0 * bd * y
|
||||
+ cc * z * z
|
||||
+ 2.0 * cd * z
|
||||
+ dd)
|
||||
.max(0.0)
|
||||
}
|
||||
|
||||
/// The point of least summed squared distance, when it is well defined.
|
||||
fn best(&self) -> Option<[f64; 3]> {
|
||||
let [aa, ab, ac, ad, bb, bc, bd, cc, cd, _] = self.0;
|
||||
let det = aa * (bb * cc - bc * bc) - ab * (ab * cc - bc * ac) + ac * (ab * bc - bb * ac);
|
||||
let scale = (aa.abs() + bb.abs() + cc.abs()).powi(3);
|
||||
if !(det.abs() > 1e-12 * scale) {
|
||||
return None;
|
||||
}
|
||||
let (r0, r1, r2) = (-ad, -bd, -cd);
|
||||
let x = (r0 * (bb * cc - bc * bc) - ab * (r1 * cc - bc * r2) + ac * (r1 * bc - bb * r2)) / det;
|
||||
let y = (aa * (r1 * cc - bc * r2) - r0 * (ab * cc - bc * ac) + ac * (ab * r2 - r1 * ac)) / det;
|
||||
let z = (aa * (bb * r2 - r1 * bc) - ab * (ab * r2 - r1 * ac) + r0 * (ab * bc - bb * ac)) / det;
|
||||
[x, y, z].iter().all(|v| v.is_finite()).then_some([x, y, z])
|
||||
}
|
||||
}
|
||||
|
||||
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
|
||||
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||
}
|
||||
|
||||
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
fn unit(a: [f64; 3]) -> Option<[f64; 3]> {
|
||||
let l = dot(a, a).sqrt();
|
||||
(l > 0.0 && l.is_finite()).then(|| [a[0] / l, a[1] / l, a[2] / l])
|
||||
}
|
||||
|
||||
/// A candidate collapse: its cost, its ends, and their versions when it was costed (a later
|
||||
/// change to either end makes it stale).
|
||||
#[derive(Clone, Copy)]
|
||||
struct Candidate {
|
||||
cost: f64,
|
||||
u: u32,
|
||||
v: u32,
|
||||
stamps: (u32, u32),
|
||||
}
|
||||
|
||||
impl PartialEq for Candidate {
|
||||
fn eq(&self, o: &Self) -> bool {
|
||||
self.cmp(o) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
impl Eq for Candidate {}
|
||||
impl PartialOrd for Candidate {
|
||||
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(o))
|
||||
}
|
||||
}
|
||||
impl Ord for Candidate {
|
||||
/// The cheapest first (the heap pops the greatest), ties by the ends, so the result never
|
||||
/// depends on the heap's order.
|
||||
fn cmp(&self, o: &Self) -> Ordering {
|
||||
o.cost.total_cmp(&self.cost).then_with(|| (o.u, o.v).cmp(&(self.u, self.v)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A quick hasher for welding by position (the keys are coordinates' bits).
|
||||
#[derive(Default)]
|
||||
struct BitsHasher(u64);
|
||||
|
||||
impl std::hash::Hasher for BitsHasher {
|
||||
fn finish(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
for b in bytes {
|
||||
self.write_u64(u64::from(*b));
|
||||
}
|
||||
}
|
||||
fn write_u64(&mut self, x: u64) {
|
||||
self.0 = (self.0.rotate_left(5) ^ x).wrapping_mul(0x51_7c_c1_b7_27_22_0a_95);
|
||||
}
|
||||
}
|
||||
|
||||
/// The lists a collapse works with, kept between collapses.
|
||||
#[derive(Default)]
|
||||
struct Scratch {
|
||||
at_u: Vec<u32>,
|
||||
at_v: Vec<u32>,
|
||||
shared: Vec<u32>,
|
||||
nu: Vec<u32>,
|
||||
nv: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Scratch {
|
||||
fn clear(&mut self) {
|
||||
for l in [&mut self.at_u, &mut self.at_v, &mut self.shared, &mut self.nu, &mut self.nv] {
|
||||
l.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Collapser {
|
||||
pos: Vec<[f64; 3]>,
|
||||
quadric: Vec<Quadric>,
|
||||
alive: Vec<bool>,
|
||||
stamp: Vec<u32>,
|
||||
tris: Vec<[u32; 3]>,
|
||||
tri_alive: Vec<bool>,
|
||||
/// The triangles at each vertex (dead ones are dropped lazily).
|
||||
around: Vec<Vec<u32>>,
|
||||
/// An original triangle at each vertex (where it came from), to start a search near it.
|
||||
seed: Vec<u32>,
|
||||
/// Lists a collapse reuses.
|
||||
scratch: Scratch,
|
||||
heap: BinaryHeap<Candidate>,
|
||||
live_triangles: usize,
|
||||
}
|
||||
|
||||
impl Collapser {
|
||||
fn new(positions: &[Vec3], triangles: &[[u32; 3]]) -> Option<Collapser> {
|
||||
// Weld corners at the same position.
|
||||
let mut index: std::collections::HashMap<[u64; 3], u32, std::hash::BuildHasherDefault<BitsHasher>> =
|
||||
std::collections::HashMap::with_capacity_and_hasher(positions.len() / 2, Default::default());
|
||||
let mut remap: Vec<Option<u32>> = Vec::with_capacity(positions.len());
|
||||
let mut pos: Vec<[f64; 3]> = Vec::new();
|
||||
for p in positions {
|
||||
if !p.is_finite() {
|
||||
remap.push(None);
|
||||
continue;
|
||||
}
|
||||
let next = u32::try_from(pos.len()).ok()?;
|
||||
let key = [(p.x + 0.0).to_bits(), (p.y + 0.0).to_bits(), (p.z + 0.0).to_bits()];
|
||||
remap.push(Some(*index.entry(key).or_insert_with(|| {
|
||||
pos.push([p.x, p.y, p.z]);
|
||||
next
|
||||
})));
|
||||
}
|
||||
drop(index);
|
||||
let mut tris: Vec<[u32; 3]> = Vec::with_capacity(triangles.len());
|
||||
for t in triangles {
|
||||
let r = |i: u32| remap.get(i as usize).copied().flatten();
|
||||
let (Some(a), Some(b), Some(c)) = (r(t[0]), r(t[1]), r(t[2])) else { continue };
|
||||
if a != b && b != c && c != a {
|
||||
tris.push([a, b, c]);
|
||||
}
|
||||
}
|
||||
if tris.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let n = pos.len();
|
||||
let mut quadric = vec![Quadric::default(); n];
|
||||
let mut around: Vec<Vec<u32>> = vec![Vec::new(); n];
|
||||
let mut sides: Vec<(u32, u32, u32)> = Vec::with_capacity(tris.len() * 3);
|
||||
for (ti, t) in tris.iter().enumerate() {
|
||||
let ti = u32::try_from(ti).ok()?;
|
||||
let [a, b, c] = t.map(|i| pos.get(i as usize).copied().unwrap_or_default());
|
||||
if let Some(nrm) = unit(cross(sub(b, a), sub(c, a))) {
|
||||
let q = Quadric::plane(nrm, -dot(nrm, a), 1.0);
|
||||
for &i in t {
|
||||
if let Some(x) = quadric.get_mut(i as usize) {
|
||||
x.add(&q);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (k, &i) in t.iter().enumerate() {
|
||||
if let Some(list) = around.get_mut(i as usize) {
|
||||
list.push(ti);
|
||||
}
|
||||
let j = t[(k + 1) % 3];
|
||||
sides.push((i.min(j), i.max(j), ti));
|
||||
}
|
||||
}
|
||||
// Open boundaries (sides of one triangle only): a plane across each, weighted, so the
|
||||
// boundary keeps its place.
|
||||
sides.sort_unstable();
|
||||
let mut k = 0;
|
||||
while k < sides.len() {
|
||||
let (a, b, t) = sides[k];
|
||||
let mut e = k + 1;
|
||||
while e < sides.len() && sides[e].0 == a && sides[e].1 == b {
|
||||
e += 1;
|
||||
}
|
||||
if e - k == 1
|
||||
&& let (Some(pa), Some(pb), Some(tri)) = (pos.get(a as usize), pos.get(b as usize), tris.get(t as usize))
|
||||
{
|
||||
let [p0, p1, p2] = tri.map(|i| pos.get(i as usize).copied().unwrap_or_default());
|
||||
let face = cross(sub(p1, p0), sub(p2, p0));
|
||||
if let Some(across) = unit(cross(sub(*pb, *pa), face)) {
|
||||
let q = Quadric::plane(across, -dot(across, *pa), BOUNDARY_WEIGHT);
|
||||
for i in [a, b] {
|
||||
if let Some(x) = quadric.get_mut(i as usize) {
|
||||
x.add(&q);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
k = e;
|
||||
}
|
||||
let live_triangles = tris.len();
|
||||
let seed: Vec<u32> = around.iter().map(|list| list.first().copied().unwrap_or(0)).collect();
|
||||
let mut m = Collapser {
|
||||
alive: vec![true; n],
|
||||
stamp: vec![0; n],
|
||||
tri_alive: vec![true; tris.len()],
|
||||
heap: BinaryHeap::with_capacity(sides.len() / 2 + 1),
|
||||
pos,
|
||||
quadric,
|
||||
tris,
|
||||
around,
|
||||
seed,
|
||||
scratch: Scratch::default(),
|
||||
live_triangles,
|
||||
};
|
||||
let mut last = None;
|
||||
for &(a, b, _) in &sides {
|
||||
if last != Some((a, b)) {
|
||||
m.offer(a, b);
|
||||
last = Some((a, b));
|
||||
}
|
||||
}
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Where `u` and `v` would merge, and its cost.
|
||||
fn target(&self, u: u32, v: u32) -> Option<([f64; 3], f64)> {
|
||||
let (qu, qv) = (self.quadric.get(u as usize)?, self.quadric.get(v as usize)?);
|
||||
let q = qu.sum(qv);
|
||||
let (pu, pv) = (*self.pos.get(u as usize)?, *self.pos.get(v as usize)?);
|
||||
let mid = [(pu[0] + pv[0]) * 0.5, (pu[1] + pv[1]) * 0.5, (pu[2] + pv[2]) * 0.5];
|
||||
// The best point when it lies near the edge; else the best of its ends and middle.
|
||||
let reach = dot(sub(pu, pv), sub(pu, pv)) * 4.0;
|
||||
let best = q.best().filter(|p| dot(sub(*p, mid), sub(*p, mid)) <= reach);
|
||||
best.into_iter().chain([pu, pv, mid]).map(|p| (p, q.at(p))).min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
}
|
||||
|
||||
fn offer(&mut self, u: u32, v: u32) {
|
||||
let Some((_, cost)) = self.target(u, v) else { return };
|
||||
let stamps = (self.stamp.get(u as usize).copied().unwrap_or(0), self.stamp.get(v as usize).copied().unwrap_or(0));
|
||||
self.heap.push(Candidate { cost, u, v, stamps });
|
||||
}
|
||||
|
||||
/// The live vertices sharing a live triangle with `v`.
|
||||
fn neighbours(&self, v: u32, out: &mut Vec<u32>) {
|
||||
out.clear();
|
||||
out.extend(
|
||||
self.around
|
||||
.get(v as usize)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|t| self.tri_alive.get(**t as usize).copied().unwrap_or(false))
|
||||
.filter_map(|t| self.tris.get(*t as usize))
|
||||
.flat_map(|t| t.iter().copied())
|
||||
.filter(|i| *i != v),
|
||||
);
|
||||
out.sort_unstable();
|
||||
out.dedup();
|
||||
}
|
||||
|
||||
fn collapse_to(&mut self, target: usize) {
|
||||
while self.live_triangles > target {
|
||||
let Some(c) = self.heap.pop() else { return };
|
||||
let current = |i: u32| self.alive.get(i as usize).copied().unwrap_or(false);
|
||||
let stamp = |i: u32| self.stamp.get(i as usize).copied().unwrap_or(u32::MAX);
|
||||
if !current(c.u) || !current(c.v) || (stamp(c.u), stamp(c.v)) != c.stamps {
|
||||
continue;
|
||||
}
|
||||
self.collapse(c.u, c.v);
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge `v` into `u` at their best point, unless that would fold a face over or join two
|
||||
/// sheets of the surface.
|
||||
fn collapse(&mut self, u: u32, v: u32) {
|
||||
let Some((p, _)) = self.target(u, v) else { return };
|
||||
let live = |t: &u32| self.tri_alive.get(*t as usize).copied().unwrap_or(false);
|
||||
let mut s = std::mem::take(&mut self.scratch);
|
||||
s.clear();
|
||||
s.at_v.extend(self.around.get(v as usize).into_iter().flatten().copied().filter(live));
|
||||
s.at_u.extend(self.around.get(u as usize).into_iter().flatten().copied().filter(live));
|
||||
s.shared.extend(s.at_v.iter().copied().filter(|t| self.tris.get(*t as usize).is_some_and(|tri| tri.contains(&u))));
|
||||
let done = self.collapse_with(u, v, p, &mut s);
|
||||
self.scratch = s;
|
||||
if done {
|
||||
self.reoffer(u);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Collapser::collapse`] with its lists in `s`: whether it collapsed.
|
||||
fn collapse_with(&mut self, u: u32, v: u32, p: [f64; 3], s: &mut Scratch) -> bool {
|
||||
if s.shared.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// Two sheets meeting only at these ends would be joined.
|
||||
self.neighbours(u, &mut s.nu);
|
||||
self.neighbours(v, &mut s.nv);
|
||||
let common = s.nu.iter().filter(|x| s.nv.binary_search(x).is_ok()).count();
|
||||
if common > s.shared.len() {
|
||||
return false;
|
||||
}
|
||||
// No face folds over.
|
||||
for &t in s.at_u.iter().chain(&s.at_v).filter(|t| !s.shared.contains(t)) {
|
||||
let Some(tri) = self.tris.get(t as usize) else { continue };
|
||||
let corners = tri.map(|i| self.pos.get(i as usize).copied().unwrap_or_default());
|
||||
let moved = tri.map(|i| if i == u || i == v { p } else { self.pos.get(i as usize).copied().unwrap_or_default() });
|
||||
let before = cross(sub(corners[1], corners[0]), sub(corners[2], corners[0]));
|
||||
let after = cross(sub(moved[1], moved[0]), sub(moved[2], moved[0]));
|
||||
let (lb, la) = (dot(before, before).sqrt(), dot(after, after).sqrt());
|
||||
if !(la > 0.0) || (lb > 0.0 && dot(before, after) < MIN_TURN_COS * lb * la) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for t in &s.shared {
|
||||
if let Some(a) = self.tri_alive.get_mut(*t as usize) {
|
||||
*a = false;
|
||||
}
|
||||
self.live_triangles = self.live_triangles.saturating_sub(1);
|
||||
}
|
||||
let mut list = self.around.get_mut(u as usize).map(std::mem::take).unwrap_or_default();
|
||||
let tri_alive = &self.tri_alive;
|
||||
list.retain(|t| tri_alive.get(*t as usize).copied().unwrap_or(false));
|
||||
for &t in s.at_v.iter().filter(|t| !s.shared.contains(t)) {
|
||||
if let Some(tri) = self.tris.get_mut(t as usize) {
|
||||
for i in tri.iter_mut() {
|
||||
if *i == v {
|
||||
*i = u;
|
||||
}
|
||||
}
|
||||
list.push(t);
|
||||
}
|
||||
}
|
||||
if let Some(slot) = self.around.get_mut(u as usize) {
|
||||
*slot = list;
|
||||
}
|
||||
if let Some(slot) = self.around.get_mut(v as usize) {
|
||||
*slot = Vec::new();
|
||||
}
|
||||
let qv = self.quadric.get(v as usize).copied().unwrap_or_default();
|
||||
if let Some(q) = self.quadric.get_mut(u as usize) {
|
||||
q.add(&qv);
|
||||
}
|
||||
if let Some(x) = self.pos.get_mut(u as usize) {
|
||||
*x = p;
|
||||
}
|
||||
if let Some(a) = self.alive.get_mut(v as usize) {
|
||||
*a = false;
|
||||
}
|
||||
for i in [u, v] {
|
||||
if let Some(stamp) = self.stamp.get_mut(i as usize) {
|
||||
*stamp = stamp.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Cost again every edge at `u` (it moved).
|
||||
fn reoffer(&mut self, u: u32) {
|
||||
let mut around = std::mem::take(&mut self.scratch.nu);
|
||||
self.neighbours(u, &mut around);
|
||||
for &w in &around {
|
||||
self.offer(u.min(w), u.max(w));
|
||||
}
|
||||
self.scratch.nu = around;
|
||||
}
|
||||
|
||||
/// The mesh as it is now, and an original triangle each of its vertices came from.
|
||||
fn level(&self) -> (Level, Vec<u32>) {
|
||||
let mut index: Vec<Option<u32>> = vec![None; self.pos.len()];
|
||||
let mut out = Level::default();
|
||||
let mut seeds = Vec::new();
|
||||
out.triangles.reserve(self.live_triangles);
|
||||
for (t, alive) in self.tris.iter().zip(&self.tri_alive) {
|
||||
if !alive {
|
||||
continue;
|
||||
}
|
||||
let mut tri = [0u32; 3];
|
||||
for (k, &i) in t.iter().enumerate() {
|
||||
let Some(slot) = index.get_mut(i as usize) else { continue };
|
||||
tri[k] = *slot.get_or_insert_with(|| {
|
||||
let p = self.pos.get(i as usize).copied().unwrap_or_default();
|
||||
out.positions.push(Vec3::new(p[0], p[1], p[2]));
|
||||
seeds.push(self.seed.get(i as usize).copied().unwrap_or(0));
|
||||
u32::try_from(out.positions.len() - 1).unwrap_or(u32::MAX)
|
||||
});
|
||||
}
|
||||
out.triangles.push(tri);
|
||||
}
|
||||
(out, seeds)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "simplify_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,158 @@
|
||||
use super::*;
|
||||
|
||||
/// A sphere of radius `r` as a scan reads it: `n` × 2`n` quads, each triangle's corners its own
|
||||
/// points (the same position repeated where triangles meet).
|
||||
fn sphere(r: f64, n: u32) -> (Vec<Vec3>, Vec<[u32; 3]>) {
|
||||
let at = |i: u32, j: u32| {
|
||||
let (t, p) = (std::f64::consts::PI * f64::from(i) / f64::from(n), std::f64::consts::TAU * f64::from(j) / f64::from(2 * n));
|
||||
let (st, ct, sp, cp) = (t.sin(), t.cos(), p.sin(), p.cos());
|
||||
// The poles exactly, and the seam the same as the start, so corners weld.
|
||||
let (st, sp, cp) = (if i == 0 || i == n { 0.0 } else { st }, if j == 2 * n { 0.0 } else { sp }, if j == 2 * n { 1.0 } else { cp });
|
||||
Vec3::new(r * st * cp, r * st * sp, r * ct)
|
||||
};
|
||||
let (mut pos, mut tris) = (Vec::new(), Vec::new());
|
||||
let mut tri = |a: Vec3, b: Vec3, c: Vec3| {
|
||||
let k = pos.len() as u32;
|
||||
pos.extend([a, b, c]);
|
||||
tris.push([k, k + 1, k + 2]);
|
||||
};
|
||||
for i in 0..n {
|
||||
for j in 0..2 * n {
|
||||
let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
|
||||
if i > 0 {
|
||||
tri(a, c, d);
|
||||
}
|
||||
if i + 1 < n {
|
||||
tri(a, b, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
(pos, tris)
|
||||
}
|
||||
|
||||
fn volume(l: &Level) -> f64 {
|
||||
l.triangles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let [a, b, c] = t.map(|i| l.positions[i as usize]);
|
||||
a.dot(b.cross(c)) / 6.0
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Each side of a closed surface is shared by exactly two triangles, once each way.
|
||||
fn closed(l: &Level) -> bool {
|
||||
let mut sides: Vec<(u32, u32)> = l.triangles.iter().flat_map(|t| [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])]).collect();
|
||||
sides.sort_unstable();
|
||||
let n = sides.len();
|
||||
sides.dedup();
|
||||
sides.len() == n && sides.iter().all(|(a, b)| sides.binary_search(&(*b, *a)).is_ok())
|
||||
}
|
||||
|
||||
/// A finely meshed sphere comes down to each target, closed, with its volume and shape kept
|
||||
/// within the error each level reports, and the errors grow as the levels coarsen.
|
||||
#[test]
|
||||
fn a_sphere_comes_down_to_each_target_within_its_error() {
|
||||
let (pos, tris) = sphere(50.0, 100);
|
||||
let full = tris.len();
|
||||
let made = levels(&pos, &tris, &[full / 4, full / 16, full / 64]);
|
||||
assert_eq!(made.len(), 3);
|
||||
let exact = 4.0 / 3.0 * std::f64::consts::PI * 50.0f64.powi(3);
|
||||
let mut last = 0.0;
|
||||
for (l, target) in made.iter().zip([full / 4, full / 16, full / 64]) {
|
||||
assert!(l.triangles.len() <= target, "{} > {target}", l.triangles.len());
|
||||
assert!(closed(l), "closed and consistently wound");
|
||||
assert!(l.error >= last, "the error grows: {} after {last}", l.error);
|
||||
last = l.error;
|
||||
// Each point within the error of the sphere (its faces' planes lie within their sag of it).
|
||||
let sag = 50.0 * (1.0 - (std::f64::consts::PI / 100.0).cos());
|
||||
for p in &l.positions {
|
||||
assert!((p.len() - 50.0).abs() <= l.error + sag + 1e-9, "{} from the centre, error {}", p.len(), l.error);
|
||||
}
|
||||
assert!((volume(l) - exact).abs() < exact * 0.02, "volume {} vs {exact}", volume(l));
|
||||
// A 600-triangle sphere of radius 50 bows in by about 0.4 at its triangles' centres at
|
||||
// best; finer levels a fraction of that.
|
||||
assert!(l.error < 1.0, "error {}", l.error);
|
||||
}
|
||||
// The same mesh simplifies the same way.
|
||||
let again = levels(&pos, &tris, &[full / 4, full / 16, full / 64]);
|
||||
assert!(made.iter().zip(&again).all(|(a, b)| a.triangles == b.triangles && a.positions == b.positions));
|
||||
}
|
||||
|
||||
/// A flat sheet comes down to a few triangles with no error, and its open outline stays put.
|
||||
#[test]
|
||||
fn a_flat_sheet_keeps_its_outline() {
|
||||
let n = 40u32;
|
||||
let at = |i: u32, j: u32| Vec3::new(f64::from(i), f64::from(j) * 0.5, 0.0);
|
||||
let (mut pos, mut tris) = (Vec::new(), Vec::new());
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
for t in [[at(i, j), at(i + 1, j), at(i + 1, j + 1)], [at(i, j), at(i + 1, j + 1), at(i, j + 1)]] {
|
||||
let k = pos.len() as u32;
|
||||
pos.extend(t);
|
||||
tris.push([k, k + 1, k + 2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let made = levels(&pos, &tris, &[tris.len() / 16]);
|
||||
let l = made.first().unwrap();
|
||||
assert!(l.triangles.len() <= tris.len() / 16);
|
||||
assert!(l.error < 1e-6, "flat: {}", l.error);
|
||||
assert!(l.positions.iter().all(|p| p.z.abs() < 1e-9));
|
||||
let b = l.positions.iter().fold(crate::Aabb3::EMPTY, |mut b, p| {
|
||||
b.add(*p);
|
||||
b
|
||||
});
|
||||
assert!(b.min.dist(Vec3::ZERO) < 1e-9 && b.max.dist(Vec3::new(40.0, 20.0, 0.0)) < 1e-9, "{b:?}");
|
||||
let area: f64 = l
|
||||
.triangles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let [a, b, c] = t.map(|i| l.positions[i as usize]);
|
||||
(b - a).cross(c - a).len() / 2.0
|
||||
})
|
||||
.sum();
|
||||
assert!((area - 800.0).abs() < 1e-6, "the sheet is still whole: {area}");
|
||||
}
|
||||
|
||||
/// Input from a file is hostile: points that are not finite, indices out of range, triangles
|
||||
/// with a repeated corner and empty meshes are left out, never a panic.
|
||||
#[test]
|
||||
fn hostile_meshes_never_panic() {
|
||||
let p = |x: f64, y: f64| Vec3::new(x, y, 0.0);
|
||||
assert!(levels(&[], &[], &[1]).is_empty());
|
||||
assert!(levels(&[p(0.0, 0.0)], &[[0, 0, 0]], &[1]).is_empty());
|
||||
assert!(levels(&[p(f64::NAN, 0.0), p(1.0, 0.0), p(0.0, 1.0)], &[[0, 1, 2]], &[0]).is_empty());
|
||||
assert!(levels(&[p(0.0, 0.0), p(1.0, 0.0), p(0.0, 1.0)], &[[0, 1, 99]], &[0]).is_empty());
|
||||
let (pos, mut tris) = sphere(5.0, 12);
|
||||
tris.push([0, 1, u32::MAX]);
|
||||
tris.push([2, 2, 3]);
|
||||
let made = levels(&pos, &tris, &[usize::MAX, 100, 0]);
|
||||
assert!(made.iter().all(|l| l.triangles.iter().flatten().all(|i| (*i as usize) < l.positions.len())));
|
||||
}
|
||||
|
||||
/// Timing, not a check (`SOLIDCRAFT_BENCH_STL=part.stl cargo test --release -p solidcraft-geom
|
||||
/// --lib simplify_timing -- --ignored --nocapture`): the levels of a binary STL.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn simplify_timing() {
|
||||
let Some(path) = std::env::var_os("SOLIDCRAFT_BENCH_STL") else { return };
|
||||
let bytes = std::fs::read(path).unwrap();
|
||||
let n = u32::from_le_bytes(bytes[80..84].try_into().unwrap()) as usize;
|
||||
let f = |o: usize| f64::from(f32::from_le_bytes(bytes[o..o + 4].try_into().unwrap()));
|
||||
let (mut pos, mut tris) = (Vec::with_capacity(n * 3), Vec::with_capacity(n));
|
||||
for t in 0..n {
|
||||
let o = 84 + t * 50 + 12;
|
||||
let k = pos.len() as u32;
|
||||
for v in 0..3 {
|
||||
pos.push(Vec3::new(f(o + v * 12), f(o + v * 12 + 4), f(o + v * 12 + 8)));
|
||||
}
|
||||
tris.push([k, k + 1, k + 2]);
|
||||
}
|
||||
let started = std::time::Instant::now();
|
||||
let made = levels(&pos, &tris, &[n / 4, n / 16, n / 64]);
|
||||
eprintln!("{n} triangles: levels in {:.2} s", started.elapsed().as_secs_f64());
|
||||
for l in &made {
|
||||
eprintln!(" {} triangles, error {:.3} mm", l.triangles.len(), l.error);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ use wgpu::util::DeviceExt;
|
||||
|
||||
pub mod capture;
|
||||
mod pbr;
|
||||
pub mod pick;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod select;
|
||||
pub mod view;
|
||||
|
||||
@@ -1,503 +0,0 @@
|
||||
//! The pick buffer: which model edges are visible at each pixel of the viewport, for picking on a
|
||||
//! large model without testing every edge. Surfaces are drawn into the depth buffer only; then
|
||||
//! every edge segment is drawn as a line carrying its id, depth tested against them (lifted
|
||||
//! toward the eye by the picker's slack, the depth by which an edge may lie behind a face and
|
||||
//! still count as visible) and written into an `R32Uint` id texture (0: no edge).
|
||||
//!
|
||||
//! The pass is drawn once per view (camera, viewport size, model), and only small tiles around
|
||||
//! the cursor are read back, so a pointer moving over a still view reads a few kilobytes and
|
||||
//! draws nothing. Read-backs arrive a frame or two later ([`PickResults`]); until a tile is in,
|
||||
//! the picker answers on the CPU. The ids are hints: the picker still runs its exact tests on
|
||||
//! the edges a tile names.
|
||||
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
/// The side of a read-back tile, in pixels (a multiple of 64, so rows need no padding).
|
||||
pub const TILE: u32 = 128;
|
||||
/// Most tiles kept from one view.
|
||||
const KEEP: usize = 6;
|
||||
/// Most read-backs in flight.
|
||||
const IN_FLIGHT: usize = 3;
|
||||
/// Bytes per surface vertex (position) and per edge vertex (position, id).
|
||||
const FACE_VERTEX: u64 = 12;
|
||||
const EDGE_VERTEX: u64 = 16;
|
||||
const UNIFORM_SIZE: u64 = 96;
|
||||
|
||||
const SHADER: &str = r#"
|
||||
struct U {
|
||||
vp: mat4x4<f32>,
|
||||
eye: vec4<f32>, // eye position, w = 1 for perspective
|
||||
back: vec4<f32>, // toward the eye (orthographic), w = how far edges are lifted toward it
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> u: U;
|
||||
|
||||
@vertex
|
||||
fn vs_face(@location(0) p: vec3<f32>) -> @builtin(position) vec4<f32> {
|
||||
return u.vp * vec4<f32>(p, 1.0);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_face() -> @location(0) u32 {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
struct EOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) @interpolate(flat) id: u32,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_edge(@location(0) p: vec3<f32>, @location(1) id: u32) -> EOut {
|
||||
var toward = u.back.xyz;
|
||||
if (u.eye.w > 0.5) {
|
||||
let v = u.eye.xyz - p;
|
||||
let l = length(v);
|
||||
if (l > 0.0) {
|
||||
toward = v / l;
|
||||
}
|
||||
}
|
||||
var o: EOut;
|
||||
o.pos = u.vp * vec4<f32>(p + toward * u.back.w, 1.0);
|
||||
o.id = id;
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_edge(i: EOut) -> @location(0) u32 {
|
||||
return i.id;
|
||||
}
|
||||
"#;
|
||||
|
||||
/// What the pass draws: surfaces (for depth) and edge segments with their ids (from 1).
|
||||
#[derive(Default, Clone)]
|
||||
pub struct PickScene {
|
||||
faces: Vec<u8>,
|
||||
edges: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PickScene {
|
||||
pub fn triangle(&mut self, t: [[f32; 3]; 3]) {
|
||||
for p in t {
|
||||
for c in p {
|
||||
self.faces.extend_from_slice(&c.to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Segment `a`–`b` of the edge `id` (0 is no edge: not drawn).
|
||||
pub fn segment(&mut self, a: [f32; 3], b: [f32; 3], id: u32) {
|
||||
if id == 0 {
|
||||
return;
|
||||
}
|
||||
for p in [a, b] {
|
||||
for c in p {
|
||||
self.edges.extend_from_slice(&c.to_le_bytes());
|
||||
}
|
||||
self.edges.extend_from_slice(&id.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.edges.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// The view the pass is drawn for.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PickView {
|
||||
/// Changes whenever anything below or the scene changes.
|
||||
pub key: u64,
|
||||
/// Column-major view-projection (as [`crate::view::Frame::view_proj`]).
|
||||
pub view_proj: [[f32; 4]; 4],
|
||||
/// Eye position, and 1 for a perspective camera (0: orthographic).
|
||||
pub eye: [f32; 4],
|
||||
/// Direction toward the eye (orthographic).
|
||||
pub back: [f32; 3],
|
||||
/// How far edges are lifted toward the eye (model units).
|
||||
pub lift: f32,
|
||||
/// The viewport in physical pixels.
|
||||
pub size: [u32; 2],
|
||||
}
|
||||
|
||||
/// Edge ids of a block of pixels of one view.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PickTile {
|
||||
pub key: u64,
|
||||
pub origin: [u32; 2],
|
||||
pub size: [u32; 2],
|
||||
/// Row-major, `size[0] * size[1]`.
|
||||
pub ids: Vec<u32>,
|
||||
}
|
||||
|
||||
impl PickTile {
|
||||
/// Whether the tile is of view `key` and holds every pixel of `[x0, y0, x1, y1]` (inclusive)
|
||||
/// that lies inside a viewport of `view` pixels.
|
||||
pub fn covers(&self, key: u64, view: [u32; 2], [x0, y0, x1, y1]: [i64; 4]) -> bool {
|
||||
let clamp = |v: i64, n: u32| v.clamp(0, i64::from(n.max(1)) - 1);
|
||||
let (x0, x1, y0, y1) = (clamp(x0, view[0]), clamp(x1, view[0]), clamp(y0, view[1]), clamp(y1, view[1]));
|
||||
let [ox, oy] = self.origin.map(i64::from);
|
||||
let [w, h] = self.size.map(i64::from);
|
||||
self.key == key && x0 >= ox && y0 >= oy && x1 < ox + w && y1 < oy + h
|
||||
}
|
||||
|
||||
/// The ids within `[x0, y0, x1, y1]` (inclusive; outside the tile: none), each once.
|
||||
pub fn ids_in(&self, [x0, y0, x1, y1]: [i64; 4]) -> Vec<u32> {
|
||||
let [ox, oy] = self.origin.map(i64::from);
|
||||
let [w, h] = self.size.map(i64::from);
|
||||
let mut out = Vec::new();
|
||||
for y in y0.max(oy)..=y1.min(oy + h - 1) {
|
||||
for x in x0.max(ox)..=x1.min(ox + w - 1) {
|
||||
if let Some(&id) = self.ids.get(((y - oy) * w + (x - ox)) as usize)
|
||||
&& id != 0
|
||||
{
|
||||
out.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort_unstable();
|
||||
out.dedup();
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// The tiles read back so far (of the latest view only), newest last.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct PickResults {
|
||||
pub tiles: Vec<PickTile>,
|
||||
}
|
||||
|
||||
impl PickResults {
|
||||
/// The newest tile of view `key` covering `window`.
|
||||
pub fn tile(&self, key: u64, view: [u32; 2], window: [i64; 4]) -> Option<&PickTile> {
|
||||
self.tiles.iter().rev().find(|t| t.covers(key, view, window))
|
||||
}
|
||||
}
|
||||
|
||||
pub type PickOut = Arc<Mutex<PickResults>>;
|
||||
|
||||
/// The tile (origin and size) around pixel `centre` of a viewport of `view` pixels.
|
||||
pub fn tile_around(centre: [u32; 2], view: [u32; 2]) -> ([u32; 2], [u32; 2]) {
|
||||
let size = [TILE.min(view[0]), TILE.min(view[1])];
|
||||
let origin = [0, 1].map(|k| centre[k].saturating_sub(size[k] / 2).min(view[k].saturating_sub(size[k])));
|
||||
(origin, size)
|
||||
}
|
||||
|
||||
const NEW: u8 = 0;
|
||||
const MAPPING: u8 = 1;
|
||||
const READY: u8 = 2;
|
||||
const FAILED: u8 = 3;
|
||||
|
||||
/// A tile being copied out of the id texture.
|
||||
struct Pending {
|
||||
key: u64,
|
||||
origin: [u32; 2],
|
||||
size: [u32; 2],
|
||||
bytes_per_row: u32,
|
||||
buffer: wgpu::Buffer,
|
||||
/// The frame whose commands copy it (it is mapped once they are submitted).
|
||||
frame: u64,
|
||||
state: Arc<AtomicU8>,
|
||||
}
|
||||
|
||||
struct Target {
|
||||
size: [u32; 2],
|
||||
ids: wgpu::Texture,
|
||||
ids_view: wgpu::TextureView,
|
||||
depth_view: wgpu::TextureView,
|
||||
}
|
||||
|
||||
struct Uploaded {
|
||||
key: u64,
|
||||
faces: Option<(wgpu::Buffer, u32)>,
|
||||
edges: Option<(wgpu::Buffer, u32)>,
|
||||
}
|
||||
|
||||
/// The pass's pipelines, its textures and the read-backs in flight, on one device.
|
||||
pub struct PickBuffer {
|
||||
face: wgpu::RenderPipeline,
|
||||
edge: wgpu::RenderPipeline,
|
||||
uniform: wgpu::Buffer,
|
||||
bind: wgpu::BindGroup,
|
||||
scene: Option<Uploaded>,
|
||||
target: Option<Target>,
|
||||
/// The view drawn into the textures.
|
||||
drawn: Option<u64>,
|
||||
pending: Vec<Pending>,
|
||||
frame: u64,
|
||||
}
|
||||
|
||||
const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||
const IDS: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
|
||||
|
||||
impl PickBuffer {
|
||||
pub fn new(device: &wgpu::Device) -> PickBuffer {
|
||||
let module =
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("sc_pick"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
|
||||
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("sc_pick"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("sc_pick"),
|
||||
bind_group_layouts: &[Some(&bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("sc_pick_u"),
|
||||
size: UNIFORM_SIZE,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("sc_pick"),
|
||||
layout: &bgl,
|
||||
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
|
||||
});
|
||||
const FA: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x3];
|
||||
const EA: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![0 => Float32x3, 1 => Uint32];
|
||||
let pipeline = |label: &str,
|
||||
vs: &str,
|
||||
fs: &str,
|
||||
stride: u64,
|
||||
attributes: &'static [wgpu::VertexAttribute],
|
||||
topology: wgpu::PrimitiveTopology,
|
||||
write: bool,
|
||||
mask: wgpu::ColorWrites| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some(vs),
|
||||
buffers: &[Some(wgpu::VertexBufferLayout { array_stride: stride, step_mode: wgpu::VertexStepMode::Vertex, attributes })],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState { topology, ..Default::default() },
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: DEPTH,
|
||||
depth_write_enabled: Some(write),
|
||||
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some(fs),
|
||||
targets: &[Some(wgpu::ColorTargetState { format: IDS, blend: None, write_mask: mask })],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let face =
|
||||
pipeline("sc_pick_face", "vs_face", "fs_face", FACE_VERTEX, &FA, wgpu::PrimitiveTopology::TriangleList, true, wgpu::ColorWrites::empty());
|
||||
let edge = pipeline("sc_pick_edge", "vs_edge", "fs_edge", EDGE_VERTEX, &EA, wgpu::PrimitiveTopology::LineList, false, wgpu::ColorWrites::ALL);
|
||||
PickBuffer { face, edge, uniform, bind, scene: None, target: None, drawn: None, pending: Vec::new(), frame: 0 }
|
||||
}
|
||||
|
||||
/// Before the frame is submitted: collect the tiles read back, then draw `view` of `scene`
|
||||
/// (`scene_key` names it; it is uploaded when that changes) unless it is drawn already, and
|
||||
/// start reading back the tile around pixel `centre` unless it is in or on its way. The tiles
|
||||
/// land in `out` a frame or two later; keep frames coming until then.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn prepare(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
scene_key: u64,
|
||||
scene: &PickScene,
|
||||
view: &PickView,
|
||||
centre: Option<[u32; 2]>,
|
||||
out: &PickOut,
|
||||
) {
|
||||
self.frame += 1;
|
||||
self.collect(device, out);
|
||||
let [w, h] = view.size;
|
||||
let max = device.limits().max_texture_dimension_2d;
|
||||
if w == 0 || h == 0 || w > max || h > max {
|
||||
return;
|
||||
}
|
||||
if self.scene.as_ref().is_none_or(|s| s.key != scene_key) {
|
||||
let limit = device.limits().max_buffer_size;
|
||||
let upload = |label: &str, bytes: &[u8], stride: u64| {
|
||||
let n = bytes.len() as u64 / stride;
|
||||
(n > 0 && (bytes.len() as u64) <= limit && n <= u64::from(u32::MAX)).then(|| {
|
||||
(
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some(label),
|
||||
contents: bytes,
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
}),
|
||||
n as u32,
|
||||
)
|
||||
})
|
||||
};
|
||||
self.scene = Some(Uploaded {
|
||||
key: scene_key,
|
||||
faces: upload("sc_pick_faces", &scene.faces, FACE_VERTEX),
|
||||
edges: upload("sc_pick_edges", &scene.edges, EDGE_VERTEX),
|
||||
});
|
||||
self.drawn = None;
|
||||
}
|
||||
if self.target.as_ref().is_none_or(|t| t.size != view.size) {
|
||||
let texture = |label: &str, format, usage| {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage,
|
||||
view_formats: &[],
|
||||
})
|
||||
};
|
||||
let ids = texture("sc_pick_ids", IDS, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC);
|
||||
let depth = texture("sc_pick_depth", DEPTH, wgpu::TextureUsages::RENDER_ATTACHMENT);
|
||||
let (ids_view, depth_view) = (ids.create_view(&Default::default()), depth.create_view(&Default::default()));
|
||||
self.target = Some(Target { size: view.size, ids, ids_view, depth_view });
|
||||
self.drawn = None;
|
||||
}
|
||||
let (Some(target), Some(scene)) = (&self.target, &self.scene) else { return };
|
||||
if self.drawn != Some(view.key) {
|
||||
queue.write_buffer(&self.uniform, 0, &uniform_bytes(view));
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("sc_pick"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &target.ids_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &target.depth_view,
|
||||
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_bind_group(0, &self.bind, &[]);
|
||||
for (pipeline, part) in [(&self.face, &scene.faces), (&self.edge, &scene.edges)] {
|
||||
if let Some((buffer, n)) = part {
|
||||
pass.set_pipeline(pipeline);
|
||||
pass.set_vertex_buffer(0, buffer.slice(..));
|
||||
pass.draw(0..*n, 0..1);
|
||||
}
|
||||
}
|
||||
drop(pass);
|
||||
self.drawn = Some(view.key);
|
||||
// Tiles of other views no longer answer.
|
||||
self.pending.retain(|p| p.key == view.key);
|
||||
if let Ok(mut o) = out.lock() {
|
||||
o.tiles.retain(|t| t.key == view.key);
|
||||
}
|
||||
}
|
||||
let Some(centre) = centre else { return };
|
||||
let (origin, size) = tile_around(centre, view.size);
|
||||
let known = out.lock().is_ok_and(|o| o.tiles.iter().any(|t| t.key == view.key && t.origin == origin && t.size == size))
|
||||
|| self.pending.iter().any(|p| p.key == view.key && p.origin == origin && p.size == size);
|
||||
if known || self.pending.len() >= IN_FLIGHT {
|
||||
return;
|
||||
}
|
||||
let bytes_per_row = (size[0] * 4).div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("sc_pick_tile"),
|
||||
size: u64::from(bytes_per_row) * u64::from(size[1]),
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
encoder.copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &target.ids,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d { x: origin[0], y: origin[1], z: 0 },
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(bytes_per_row), rows_per_image: Some(size[1]) },
|
||||
},
|
||||
wgpu::Extent3d { width: size[0], height: size[1], depth_or_array_layers: 1 },
|
||||
);
|
||||
self.pending.push(Pending { key: view.key, origin, size, bytes_per_row, buffer, frame: self.frame, state: Arc::new(AtomicU8::new(NEW)) });
|
||||
}
|
||||
|
||||
/// Whether read-backs are still on their way (keep frames coming).
|
||||
pub fn waiting(&self) -> bool {
|
||||
!self.pending.is_empty()
|
||||
}
|
||||
|
||||
/// Maps the copies earlier frames submitted and moves the mapped ones into `out`.
|
||||
fn collect(&mut self, device: &wgpu::Device, out: &PickOut) {
|
||||
let frame = self.frame;
|
||||
for p in &self.pending {
|
||||
if p.frame < frame && p.state.load(Ordering::Acquire) == NEW {
|
||||
p.state.store(MAPPING, Ordering::Release);
|
||||
let state = p.state.clone();
|
||||
p.buffer.slice(..).map_async(wgpu::MapMode::Read, move |r| state.store(if r.is_ok() { READY } else { FAILED }, Ordering::Release));
|
||||
}
|
||||
}
|
||||
if self.pending.iter().any(|p| p.state.load(Ordering::Acquire) == MAPPING) {
|
||||
let _ = device.poll(wgpu::PollType::Poll);
|
||||
}
|
||||
let mut done = Vec::new();
|
||||
self.pending.retain(|p| match p.state.load(Ordering::Acquire) {
|
||||
READY => {
|
||||
if let Ok(bytes) = p.buffer.slice(..).get_mapped_range() {
|
||||
let [w, h] = p.size;
|
||||
let mut ids = Vec::with_capacity(w as usize * h as usize);
|
||||
for row in bytes.chunks(p.bytes_per_row as usize).take(h as usize) {
|
||||
ids.extend(row.as_chunks::<4>().0.iter().take(w as usize).map(|c| u32::from_le_bytes(*c)));
|
||||
}
|
||||
done.push(PickTile { key: p.key, origin: p.origin, size: p.size, ids });
|
||||
}
|
||||
p.buffer.unmap();
|
||||
false
|
||||
}
|
||||
FAILED => false,
|
||||
_ => true,
|
||||
});
|
||||
if !done.is_empty()
|
||||
&& let Ok(mut o) = out.lock()
|
||||
{
|
||||
for t in done {
|
||||
if self.drawn == Some(t.key) && t.ids.len() == t.size[0] as usize * t.size[1] as usize {
|
||||
o.tiles.retain(|x| x.key == t.key);
|
||||
o.tiles.push(t);
|
||||
}
|
||||
}
|
||||
let excess = o.tiles.len().saturating_sub(KEEP);
|
||||
o.tiles.drain(..excess);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn uniform_bytes(v: &PickView) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(UNIFORM_SIZE as usize);
|
||||
for col in v.view_proj {
|
||||
for x in col {
|
||||
b.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
}
|
||||
for x in v.eye.into_iter().chain(v.back).chain([v.lift]) {
|
||||
b.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "pick_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,79 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
/// Looking down −z at x, y in −10..10 (orthographic): depth 0 at z = 50, 1 at z = −50.
|
||||
const VIEW: [[f32; 4]; 4] = [[0.1, 0.0, 0.0, 0.0], [0.0, 0.1, 0.0, 0.0], [0.0, 0.0, -0.01, 0.0], [0.0, 0.0, 0.5, 1.0]];
|
||||
const SIZE: u32 = 64;
|
||||
|
||||
/// The pixel of model point (x, y).
|
||||
fn px(x: f32, y: f32) -> [i64; 2] {
|
||||
[((x / 10.0 + 1.0) * 0.5 * SIZE as f32) as i64, ((1.0 - y / 10.0) * 0.5 * SIZE as f32) as i64]
|
||||
}
|
||||
|
||||
/// A square face at z = 0 (x, y in −5..5), and three edges: 1 on it, 2 well behind it, 3 behind
|
||||
/// it by less than the lift.
|
||||
fn scene() -> PickScene {
|
||||
let mut s = PickScene::default();
|
||||
let (a, b, c, d) = ([-5.0, -5.0, 0.0], [5.0, -5.0, 0.0], [5.0, 5.0, 0.0], [-5.0, 5.0, 0.0]);
|
||||
s.triangle([a, b, c]);
|
||||
s.triangle([a, c, d]);
|
||||
s.segment([-8.0, 0.0, 0.0], [8.0, 0.0, 0.0], 1);
|
||||
s.segment([2.0, -8.0, -3.0], [2.0, 8.0, -3.0], 2);
|
||||
s.segment([-2.0, -8.0, -0.2], [-2.0, 8.0, -0.2], 3);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pick_buffer_names_the_edges_seen_at_each_pixel() {
|
||||
let (_, device, queue) = match crate::open_headless() {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
assert!(std::env::var_os("SOLIDCRAFT_REQUIRE_GPU").is_none(), "a GPU is required here: {e}");
|
||||
eprintln!("skipping: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut pb = PickBuffer::new(&device);
|
||||
let out = PickOut::default();
|
||||
let view = PickView { key: 7, view_proj: VIEW, eye: [0.0, 0.0, 100.0, 0.0], back: [0.0, 0.0, 1.0], lift: 0.5, size: [SIZE, SIZE] };
|
||||
let scene = scene();
|
||||
let all = [0, 0, SIZE as i64 - 1, SIZE as i64 - 1];
|
||||
for _ in 0..8 {
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
pb.prepare(&device, &queue, &mut encoder, 1, &scene, &view, Some([SIZE / 2, SIZE / 2]), &out);
|
||||
queue.submit([encoder.finish()]);
|
||||
let _ = device.poll(wgpu::PollType::wait_indefinitely());
|
||||
if out.lock().unwrap().tile(7, [SIZE, SIZE], all).is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let results = out.lock().unwrap();
|
||||
let tile = results.tile(7, [SIZE, SIZE], all).expect("the tile is read back within a few frames");
|
||||
let at = |x: f32, y: f32| {
|
||||
let [px, py] = px(x, y);
|
||||
tile.ids_in([px - 1, py - 1, px + 1, py + 1])
|
||||
};
|
||||
assert_eq!(at(0.0, 0.0), vec![1], "the edge on the face");
|
||||
assert_eq!(at(2.0, 3.0), Vec::<u32>::new(), "the edge behind the face is hidden");
|
||||
assert_eq!(at(2.0, 7.5), vec![2], "and seen where the face does not cover it");
|
||||
assert_eq!(at(-2.0, 3.0), vec![3], "an edge behind the face by less than the lift counts as seen");
|
||||
assert_eq!(at(3.5, 3.5), Vec::<u32>::new(), "the face itself names no edge");
|
||||
assert!(results.tile(8, [SIZE, SIZE], all).is_none(), "another view's tile does not answer");
|
||||
drop(results);
|
||||
assert!(!pb.waiting());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiles_cover_the_window_around_the_cursor_inside_the_view() {
|
||||
assert_eq!(tile_around([10, 10], [1000, 800]), ([0, 0], [TILE, TILE]));
|
||||
assert_eq!(tile_around([500, 400], [1000, 800]), ([500 - TILE / 2, 400 - TILE / 2], [TILE, TILE]));
|
||||
assert_eq!(tile_around([999, 799], [1000, 800]), ([1000 - TILE, 800 - TILE], [TILE, TILE]));
|
||||
assert_eq!(tile_around([30, 20], [50, 40]), ([0, 0], [50, 40]));
|
||||
let t = PickTile { key: 1, origin: [100, 100], size: [TILE, TILE], ids: vec![0; (TILE * TILE) as usize] };
|
||||
assert!(t.covers(1, [1000, 800], [110, 110, 120, 120]));
|
||||
assert!(!t.covers(1, [1000, 800], [90, 110, 120, 120]));
|
||||
assert!(!t.covers(2, [1000, 800], [110, 110, 120, 120]));
|
||||
// A window past the view's border needs only the pixels inside it.
|
||||
let corner = PickTile { key: 1, origin: [0, 0], size: [TILE, TILE], ids: vec![5; (TILE * TILE) as usize] };
|
||||
assert!(corner.covers(1, [1000, 800], [-8, -8, 8, 8]));
|
||||
assert_eq!(corner.ids_in([-8, -8, 8, 8]), vec![5]);
|
||||
}
|
||||
+1049
-65
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
//! bits for portable integer texel reads. GLSL cannot load from a depth texture, and float
|
||||
//! colour attachments are not available on every GL adapter.
|
||||
|
||||
use super::{Batches, color_layout, depth_compare, far_depth, planned_layout, tri_layout};
|
||||
use super::{Batches, TriSource, color_layout, depth_compare, far_depth, instance_layout, planned_layout, shared_layout, tri_layout};
|
||||
|
||||
pub(super) struct Resources {
|
||||
pub layout: wgpu::BindGroupLayout,
|
||||
@@ -16,6 +16,8 @@ pub(super) struct Resources {
|
||||
size: [u32; 2],
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
planned_pipeline: wgpu::RenderPipeline,
|
||||
instanced_pipeline: wgpu::RenderPipeline,
|
||||
instanced_flat_pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
fn texture(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, size: [u32; 2]) -> (wgpu::TextureView, wgpu::TextureView, wgpu::BindGroup) {
|
||||
@@ -64,14 +66,19 @@ impl Resources {
|
||||
});
|
||||
let built = [Some(tri_layout())];
|
||||
let evaluated = [Some(planned_layout()), Some(color_layout())];
|
||||
let pipeline = |planned: bool| {
|
||||
let placed = [Some(shared_layout()), Some(instance_layout())];
|
||||
let pipeline = |source: TriSource| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("sc_ao_prepass"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module,
|
||||
entry_point: Some("vs_tri"),
|
||||
buffers: if planned { &evaluated } else { &built },
|
||||
entry_point: Some(source.entry()),
|
||||
buffers: match source {
|
||||
TriSource::Built => &built[..],
|
||||
TriSource::Planned => &evaluated[..],
|
||||
TriSource::Instanced | TriSource::InstancedFlat => &placed[..],
|
||||
},
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
@@ -85,7 +92,7 @@ impl Resources {
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module,
|
||||
entry_point: Some("fs_ao_depth"),
|
||||
entry_point: Some(&source.fragment("fs_ao_depth")),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: wgpu::TextureFormat::R32Uint,
|
||||
blend: None,
|
||||
@@ -97,10 +104,12 @@ impl Resources {
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let planned_pipeline = pipeline(true);
|
||||
let pipeline = pipeline(false);
|
||||
let planned_pipeline = pipeline(TriSource::Planned);
|
||||
let instanced_pipeline = pipeline(TriSource::Instanced);
|
||||
let instanced_flat_pipeline = pipeline(TriSource::InstancedFlat);
|
||||
let pipeline = pipeline(TriSource::Built);
|
||||
let (depth, values, bind) = texture(device, &layout, [1, 1]);
|
||||
Self { layout, bind, depth, values, size: [1, 1], pipeline, planned_pipeline }
|
||||
Self { layout, bind, depth, values, size: [1, 1], pipeline, planned_pipeline, instanced_pipeline, instanced_flat_pipeline }
|
||||
}
|
||||
|
||||
pub fn prepare(
|
||||
@@ -159,6 +168,9 @@ impl Resources {
|
||||
pass.set_index_buffer(draw.mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.draw_indexed(0..draw.mesh.index_count, 0, 0..1);
|
||||
}
|
||||
for draw in batch.instanced.iter().filter(|d| d.look == super::InstanceLook::Opaque) {
|
||||
draw.draw(&mut pass, &self.instanced_pipeline, &self.instanced_flat_pipeline);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
//! Weighted blended order-independent transparency (McGuire and Bavoil, "Weighted Blended
|
||||
//! Order-Independent Transparency", JCGT 2013) for the model's see-through surfaces: glass (an
|
||||
//! appearance's opacity) and faded context, instanced or not.
|
||||
//!
|
||||
//! Each see-through fragment adds its colour, weighted by its alpha and its depth, into an
|
||||
//! accumulation target, and multiplies its transparency into a revealage target; a full-screen
|
||||
//! pass then lays the weighted average over what is behind it, by how much is covered. Nothing is
|
||||
//! sorted, on the CPU or the GPU, however many see-through triangles there are. The targets share
|
||||
//! the frame's depth (tested, not written), so the opaque model hides what lies behind it.
|
||||
|
||||
use super::{TriSource, depth_compare, instance_layout, shared_layout, tri_layout};
|
||||
|
||||
/// The accumulated colour (premultiplied, weighted) and alpha.
|
||||
const ACCUM: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
/// How much of what is behind still shows (1: all of it).
|
||||
const REVEAL: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm;
|
||||
|
||||
const COMPOSITE: &str = r#"
|
||||
@group(0) @binding(0) var accum_t: texture_2d<f32>;
|
||||
@group(0) @binding(1) var reveal_t: texture_2d<f32>;
|
||||
@vertex
|
||||
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
|
||||
let x = f32((i << 1u) & 2u);
|
||||
let y = f32(i & 2u);
|
||||
return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
|
||||
}
|
||||
@fragment
|
||||
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let p = vec2<i32>(pos.xy);
|
||||
let reveal = textureLoad(reveal_t, p, 0).r;
|
||||
let accum = textureLoad(accum_t, p, 0);
|
||||
if (reveal >= 1.0) { discard; }
|
||||
return vec4<f32>(accum.rgb / max(accum.a, 1e-5), 1.0 - reveal);
|
||||
}
|
||||
"#;
|
||||
|
||||
/// The pipelines, made once per target kind.
|
||||
pub(super) struct Pipelines {
|
||||
/// Built triangles ([`tri_layout`]), and instanced meshes smooth and flat.
|
||||
pub built: wgpu::RenderPipeline,
|
||||
pub instanced: wgpu::RenderPipeline,
|
||||
pub instanced_flat: wgpu::RenderPipeline,
|
||||
composite: wgpu::RenderPipeline,
|
||||
bgl: wgpu::BindGroupLayout,
|
||||
}
|
||||
|
||||
/// The targets, kept while the frame's size stays.
|
||||
pub(super) struct Targets {
|
||||
pub size: [u32; 2],
|
||||
accum: wgpu::TextureView,
|
||||
accum_resolve: Option<wgpu::TextureView>,
|
||||
reveal: wgpu::TextureView,
|
||||
reveal_resolve: Option<wgpu::TextureView>,
|
||||
bind: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
impl Pipelines {
|
||||
/// The pipelines for frames of `format` with `depth` and `samples`, using the viewport's
|
||||
/// shader `module` and uniform `layout`; `None` where the device cannot blend into a float
|
||||
/// target (the model's see-through surfaces are then sorted as before).
|
||||
pub fn new(
|
||||
device: &wgpu::Device,
|
||||
module: &wgpu::ShaderModule,
|
||||
layout: &wgpu::PipelineLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
depth: Option<wgpu::TextureFormat>,
|
||||
samples: u32,
|
||||
) -> Option<Pipelines> {
|
||||
let scopes = [wgpu::ErrorFilter::OutOfMemory, wgpu::ErrorFilter::Internal, wgpu::ErrorFilter::Validation].map(|f| device.push_error_scope(f));
|
||||
let reversed = super::reversed_depth(depth);
|
||||
let samples = samples.max(1);
|
||||
let multisample = wgpu::MultisampleState { count: samples, mask: !0, alpha_to_coverage_enabled: false };
|
||||
let add =
|
||||
wgpu::BlendComponent { src_factor: wgpu::BlendFactor::One, dst_factor: wgpu::BlendFactor::One, operation: wgpu::BlendOperation::Add };
|
||||
let keep = wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::Zero,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrc,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
};
|
||||
let targets = [
|
||||
Some(wgpu::ColorTargetState {
|
||||
format: ACCUM,
|
||||
blend: Some(wgpu::BlendState { color: add, alpha: add }),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
}),
|
||||
Some(wgpu::ColorTargetState {
|
||||
format: REVEAL,
|
||||
blend: Some(wgpu::BlendState { color: keep, alpha: keep }),
|
||||
write_mask: wgpu::ColorWrites::RED,
|
||||
}),
|
||||
];
|
||||
let built_buffers = [Some(tri_layout())];
|
||||
let placed = [Some(shared_layout()), Some(instance_layout())];
|
||||
let pipeline = |label: &str, source: TriSource| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module,
|
||||
entry_point: Some(source.entry()),
|
||||
buffers: if source == TriSource::Built { &built_buffers[..] } else { &placed[..] },
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
|
||||
depth_stencil: depth.map(|format| wgpu::DepthStencilState {
|
||||
format,
|
||||
depth_write_enabled: Some(false),
|
||||
depth_compare: Some(depth_compare(reversed, wgpu::CompareFunction::LessEqual)),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
}),
|
||||
multisample,
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module,
|
||||
entry_point: Some(&source.fragment("fs_oit")),
|
||||
targets: &targets,
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let built = pipeline("sc_oit", TriSource::Built);
|
||||
let instanced = pipeline("sc_oit_instanced", TriSource::Instanced);
|
||||
let instanced_flat = pipeline("sc_oit_instanced_flat", TriSource::InstancedFlat);
|
||||
let texture = |binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
};
|
||||
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("sc_oit"), entries: &[texture(0), texture(1)] });
|
||||
let composite_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("sc_oit_composite"),
|
||||
bind_group_layouts: &[Some(&bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let composite_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("sc_oit_composite"),
|
||||
source: wgpu::ShaderSource::Wgsl(COMPOSITE.into()),
|
||||
});
|
||||
// Over the frame (premultiplied over transparent black): the average colour by how much
|
||||
// is covered.
|
||||
let over = wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::SrcAlpha,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
};
|
||||
let composite = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("sc_oit_composite"),
|
||||
layout: Some(&composite_layout),
|
||||
vertex: wgpu::VertexState { module: &composite_module, entry_point: Some("vs"), buffers: &[], compilation_options: Default::default() },
|
||||
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
|
||||
depth_stencil: None,
|
||||
multisample,
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &composite_module,
|
||||
entry_point: Some("fs"),
|
||||
targets: &[Some(wgpu::ColorTargetState { format, blend: Some(over), write_mask: wgpu::ColorWrites::ALL })],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let mut failed = None;
|
||||
for scope in scopes.into_iter().rev() {
|
||||
if let Some(e) = crate::popped(scope.pop()) {
|
||||
failed.get_or_insert(e.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(e) = failed {
|
||||
log::info!("see-through surfaces are sorted (no order-independent transparency here: {e})");
|
||||
return None;
|
||||
}
|
||||
Some(Pipelines { built, instanced, instanced_flat, composite, bgl })
|
||||
}
|
||||
|
||||
/// The targets for a frame of `size` pixels at `samples` per pixel.
|
||||
pub fn targets(&self, device: &wgpu::Device, size: [u32; 2], samples: u32) -> Targets {
|
||||
let samples = samples.max(1);
|
||||
let texture = |label: &str, format, samples, usage| {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
size: wgpu::Extent3d { width: size[0].max(1), height: size[1].max(1), depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: samples,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage,
|
||||
view_formats: &[],
|
||||
})
|
||||
.create_view(&Default::default())
|
||||
};
|
||||
let read = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
|
||||
let pair = |label: &str, format| {
|
||||
if samples > 1 {
|
||||
(texture(label, format, samples, wgpu::TextureUsages::RENDER_ATTACHMENT), Some(texture(label, format, 1, read)))
|
||||
} else {
|
||||
(texture(label, format, 1, read), None)
|
||||
}
|
||||
};
|
||||
let (accum, accum_resolve) = pair("sc_oit_accum", ACCUM);
|
||||
let (reveal, reveal_resolve) = pair("sc_oit_reveal", REVEAL);
|
||||
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("sc_oit"),
|
||||
layout: &self.bgl,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(accum_resolve.as_ref().unwrap_or(&accum)) },
|
||||
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(reveal_resolve.as_ref().unwrap_or(&reveal)) },
|
||||
],
|
||||
});
|
||||
Targets { size, accum, accum_resolve, reveal, reveal_resolve, bind }
|
||||
}
|
||||
|
||||
/// The pass that gathers the see-through surfaces (`draw` records them), against `depth`.
|
||||
pub fn accumulate(
|
||||
&self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
t: &Targets,
|
||||
depth: Option<&wgpu::TextureView>,
|
||||
stencil: bool,
|
||||
draw: impl FnOnce(&mut wgpu::RenderPass<'static>),
|
||||
) {
|
||||
let attach = |view, resolve_target, clear| {
|
||||
Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
depth_slice: None,
|
||||
resolve_target,
|
||||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(clear), store: wgpu::StoreOp::Store },
|
||||
})
|
||||
};
|
||||
let mut pass = encoder
|
||||
.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("sc_oit"),
|
||||
color_attachments: &[
|
||||
attach(&t.accum, t.accum_resolve.as_ref(), wgpu::Color::TRANSPARENT),
|
||||
attach(&t.reveal, t.reveal_resolve.as_ref(), wgpu::Color::WHITE),
|
||||
],
|
||||
depth_stencil_attachment: depth.map(|view| wgpu::RenderPassDepthStencilAttachment {
|
||||
view,
|
||||
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
stencil_ops: stencil.then_some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
})
|
||||
.forget_lifetime();
|
||||
pass.set_viewport(0.0, 0.0, t.size[0] as f32, t.size[1] as f32, 0.0, 1.0);
|
||||
draw(&mut pass);
|
||||
}
|
||||
|
||||
/// Lay what was gathered over the frame's `color` (resolved into `resolve`).
|
||||
pub fn composite(&self, encoder: &mut wgpu::CommandEncoder, t: &Targets, color: &wgpu::TextureView, resolve: Option<&wgpu::TextureView>) {
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("sc_oit_composite"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: color,
|
||||
depth_slice: None,
|
||||
resolve_target: resolve,
|
||||
ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
pass.set_pipeline(&self.composite);
|
||||
pass.set_bind_group(0, &t.bind, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
@@ -326,10 +326,10 @@ fn encode_mesh(mesh: &MeshData) -> Result<Vec<u8>> {
|
||||
bytes.extend_from_slice(MESH_MAGIC);
|
||||
bytes.extend_from_slice(&(mesh.positions.len() as u64).to_le_bytes());
|
||||
bytes.extend_from_slice(&(mesh.triangles.len() as u64).to_le_bytes());
|
||||
for value in &mesh.positions {
|
||||
for value in mesh.positions.iter() {
|
||||
bytes.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
for value in &mesh.triangles {
|
||||
for value in mesh.triangles.iter() {
|
||||
bytes.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
Ok(bytes)
|
||||
@@ -536,7 +536,7 @@ fn read_preview<R: Read + Seek>(reader: &mut PackageReader<R>) -> Result<DesignP
|
||||
return Err(invalid("overlapping preview bodies"));
|
||||
}
|
||||
end = end.checked_add(length).ok_or_else(|| invalid("preview overflow"))?;
|
||||
let mut mesh = MeshData { name: String::new(), positions: Vec::new(), triangles: Vec::new(), color: None };
|
||||
let mut mesh = MeshData { name: String::new(), positions: Vec::new().into(), triangles: Vec::new().into(), color: None };
|
||||
decode_mesh(data.get(offset..end).ok_or_else(|| invalid("truncated preview"))?, &mut mesh)?;
|
||||
triangles = triangles.saturating_add(mesh.triangles.len() / 3);
|
||||
if triangles > MAX_PREVIEW_TRIANGLES {
|
||||
@@ -971,8 +971,8 @@ mod tests {
|
||||
fn mesh() -> MeshData {
|
||||
MeshData {
|
||||
name: "Precise mesh".into(),
|
||||
positions: vec![0.123456789012345, -0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||
triangles: vec![0, 1, 2],
|
||||
positions: vec![0.123456789012345, -0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0].into(),
|
||||
triangles: vec![0, 1, 2].into(),
|
||||
color: Some([0.25, 0.5, 0.75]),
|
||||
}
|
||||
}
|
||||
@@ -995,8 +995,9 @@ mod tests {
|
||||
other.name = "Same triangles, separate appearance".into();
|
||||
other.color = None;
|
||||
doc.add_feature(FeatureKind::MeshImport { file: "example.obj".into(), meshes: vec![mesh(), other] }, None).unwrap();
|
||||
let id = doc.allocate_id().unwrap();
|
||||
doc.canvases.push(solidcraft_doc::canvas::Canvas {
|
||||
id: 99,
|
||||
id,
|
||||
name: "Reference".into(),
|
||||
format: "png".into(),
|
||||
data: solidcraft_doc::canvas::base64_encode(b"original image bytes"),
|
||||
@@ -1128,7 +1129,7 @@ mod tests {
|
||||
assert!(read_design_preview(&package(&entries)).is_none());
|
||||
}
|
||||
let mut too_large = mesh();
|
||||
too_large.triangles = vec![0; (MAX_PREVIEW_TRIANGLES + 1) * 3];
|
||||
too_large.triangles = vec![0; (MAX_PREVIEW_TRIANGLES + 1) * 3].into();
|
||||
let bytes = write_design_container_with_preview(&doc, DesignCompression::Fast, Some(&DesignPreview { meshes: vec![too_large] })).unwrap();
|
||||
assert!(read_design_preview(&bytes).is_none());
|
||||
assert_eq!(crate::read_design(&bytes).unwrap(), doc);
|
||||
|
||||
@@ -101,10 +101,16 @@ impl GltfImport {
|
||||
if let solidcraft_doc::FeatureKind::MeshImport { meshes, .. } = &mut self.feature.kind {
|
||||
for (m, slot) in meshes.iter_mut().zip(&self.plan.bodies) {
|
||||
let Some(frame) = slot.and_then(|s| world.get(s).copied().flatten()) else { continue };
|
||||
for p in m.positions.as_chunks_mut::<3>().0 {
|
||||
let q = solidcraft_doc::apply_point(&frame, Vec3::new(p[0], p[1], p[2]));
|
||||
*p = [q.x, q.y, q.z];
|
||||
}
|
||||
m.positions = m
|
||||
.positions
|
||||
.as_chunks::<3>()
|
||||
.0
|
||||
.iter()
|
||||
.flat_map(|p| {
|
||||
let q = solidcraft_doc::apply_point(&frame, Vec3::new(p[0], p[1], p[2]));
|
||||
[q.x, q.y, q.z]
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
self.plan = MeshPlan::default();
|
||||
|
||||
@@ -508,8 +508,8 @@ outer
|
||||
doc.set_param("width", "40", Some("mm"), None).unwrap();
|
||||
let mesh = solidcraft_doc::MeshData {
|
||||
name: "Preview".into(),
|
||||
positions: vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||
triangles: vec![0, 1, 2],
|
||||
positions: vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0].into(),
|
||||
triangles: vec![0, 1, 2].into(),
|
||||
color: None,
|
||||
};
|
||||
let preview = crate::DesignPreview { meshes: vec![mesh] };
|
||||
|
||||
@@ -39,6 +39,9 @@ winreg = "0.56"
|
||||
[dev-dependencies]
|
||||
# Parse the source guard structurally, including code after inline test modules.
|
||||
syn = { workspace = true, features = ["visit"] }
|
||||
# The catalog guard reads every UI source file as Rust tokens; already in the tree through
|
||||
# crates/derive and the workspace lockfile.
|
||||
proc-macro2 = { workspace = true }
|
||||
|
||||
# Pictures pasted into the agent chat; already in the tree, with the same features, through
|
||||
# egui-winit's clipboard.
|
||||
|
||||
@@ -883,20 +883,6 @@ fn desktop_body(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn login_chip(ui: &mut egui::Ui, login: &str) {
|
||||
let t = Tokens::get();
|
||||
let (text, color) = match login {
|
||||
"signed_in" => (tr("ui.agent_chat_signed_in"), t.accent),
|
||||
"key" => (tr("ui.agent_chat_key_in_env"), t.accent),
|
||||
"signed_out" => (tr("ui.agent_chat_not_signed_in"), t.warning),
|
||||
_ => return,
|
||||
};
|
||||
egui::Frame::NONE.stroke(Stroke::new(1.0, color)).corner_radius(8.0).inner_margin(egui::Margin::symmetric(6, 1)).show(ui, |ui| {
|
||||
ui.label(RichText::new(text).color(color).size(crate::theme::Tokens::get().font_small));
|
||||
});
|
||||
}
|
||||
|
||||
/// The header's second row: the agent this conversation uses (a compact switcher), the agent's
|
||||
/// own mode when it has modes, and the state of its login.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -101,86 +101,143 @@ fn test_line(ui: &mut egui::Ui, test: &Option<&Test>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The AI page of Settings.
|
||||
/// The Assistant page of Settings. Copy stays close to its control; full security details
|
||||
/// remain accessible on hover, and high-risk permissions always retain a visible warning.
|
||||
pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let r = ui_kit::checkbox(ui, tr("ui.ai_show_features"), &mut app.preferences.ai_features);
|
||||
crate::scenario::publish_handle("prefs:ai_features", r.rect.center());
|
||||
ui_kit::text(ui, tr("ui.ai_show_features_note"), ui_kit::Text::Note("features"));
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
ui_kit::text(ui, tr("ui.agent_chat_desktop_only"), ui_kit::Text::Note("desktop"));
|
||||
return;
|
||||
}
|
||||
// Any agent's control requests, the chat's or an outside MCP client's (#1989).
|
||||
let r = ui_kit::checkbox(ui, tr("ui.agent_off_thread"), &mut app.preferences.agent_off_thread);
|
||||
crate::scenario::publish_handle("prefs:agent_off_thread", r.rect.center());
|
||||
ui_kit::text(ui, tr("ui.agent_off_thread_note"), ui_kit::Text::Note("off_thread"));
|
||||
if !app.preferences.ai_features {
|
||||
return;
|
||||
}
|
||||
let r = ui_kit::checkbox(ui, tr("ui.ai_toolbar_button"), &mut app.preferences.ai_toolbar_button);
|
||||
crate::scenario::publish_handle("prefs:ai_toolbar_button", r.rect.center());
|
||||
ui_kit::text(ui, tr("ui.ai_toolbar_button_note"), ui_kit::Text::Note("toolbar"));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if app.agent_chat.detection.is_none() && app.agent_chat.detecting.is_none() {
|
||||
refresh(app, &ui.ctx().clone());
|
||||
}
|
||||
agents_section(app, ui);
|
||||
}
|
||||
ui_kit::section(ui, "permissions", tr("ui.ai_permissions"), true, |ui| {
|
||||
for mode in Mode::ALL {
|
||||
let r = ui_kit::list_picker(ui, mode.id(), mode.label(), app.preferences.ai_permissions == mode);
|
||||
crate::scenario::publish_handle(&format!("prefs:ai_mode:{}", mode.id()), r.rect.center());
|
||||
if r.clicked() {
|
||||
app.preferences.ai_permissions = mode;
|
||||
ui_kit::settings_heading(ui, tr("prefs.assistant"), tr("ai.prefs.intro"));
|
||||
ui_kit::settings_section(ui, "assistant_availability", tr("ai.prefs.availability"), |ui| {
|
||||
let response =
|
||||
ui_kit::checkbox(ui, tr("ui.ai_show_features"), &mut app.preferences.ai_features).on_hover_text(tr("ui.ai_show_features_note"));
|
||||
crate::scenario::publish_handle("prefs:ai_features", response.rect.center());
|
||||
if !cfg!(target_arch = "wasm32") {
|
||||
if app.preferences.ai_features {
|
||||
let response = ui_kit::checkbox(ui, tr("ui.ai_toolbar_button"), &mut app.preferences.ai_toolbar_button)
|
||||
.on_hover_text(tr("ui.ai_toolbar_button_note"));
|
||||
crate::scenario::publish_handle("prefs:ai_toolbar_button", response.rect.center());
|
||||
}
|
||||
ui_kit::text(
|
||||
ui,
|
||||
tr(match mode {
|
||||
Mode::Ask => "ui.ai_mode_ask_note",
|
||||
Mode::Edits => "ui.ai_mode_edits_note",
|
||||
Mode::All => "ui.ai_mode_all_note",
|
||||
}),
|
||||
ui_kit::Text::Note(mode.id()),
|
||||
);
|
||||
let response = ui_kit::checkbox(ui, tr("ai.prefs.background_commands"), &mut app.preferences.agent_off_thread)
|
||||
.on_hover_text(tr("ui.agent_off_thread_note"));
|
||||
crate::scenario::publish_handle("prefs:agent_off_thread", response.rect.center());
|
||||
}
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
ui_kit::text(ui, tr("ui.agent_chat_desktop_only"), ui_kit::Text::Hint("desktop"));
|
||||
}
|
||||
});
|
||||
if !cfg!(target_arch = "wasm32") && app.preferences.ai_features {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if app.agent_chat.detection.is_none() && app.agent_chat.detecting.is_none() {
|
||||
refresh(app, &ui.ctx().clone());
|
||||
}
|
||||
agents_section(app, ui);
|
||||
}
|
||||
permissions_section(app, ui);
|
||||
panel_section(app, ui);
|
||||
}
|
||||
pointer_section(app, ui);
|
||||
}
|
||||
|
||||
fn permissions_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui_kit::settings_section(ui, "permissions", tr("ui.ai_permissions"), |ui| {
|
||||
let mut selected = Mode::ALL.iter().position(|mode| *mode == app.preferences.ai_permissions).unwrap_or(0);
|
||||
let choices = [
|
||||
ui_kit::SettingsChoice {
|
||||
key: "prefs:ai_mode:ask",
|
||||
title: tr("ai.prefs.permission_ask"),
|
||||
summary: tr("ai.prefs.permission_ask_summary"),
|
||||
detail: tr("ui.ai_mode_ask_note"),
|
||||
},
|
||||
ui_kit::SettingsChoice {
|
||||
key: "prefs:ai_mode:edits",
|
||||
title: tr("ai.prefs.permission_edits"),
|
||||
summary: tr("ai.prefs.permission_edits_summary"),
|
||||
detail: tr("ui.ai_mode_edits_note"),
|
||||
},
|
||||
ui_kit::SettingsChoice {
|
||||
key: "prefs:ai_mode:all",
|
||||
title: tr("ai.prefs.permission_all"),
|
||||
summary: tr("ai.prefs.permission_all_summary"),
|
||||
detail: tr("ui.ai_mode_all_note"),
|
||||
},
|
||||
];
|
||||
if ui_kit::settings_choices(ui, &choices, &mut selected)
|
||||
&& let Some(mode) = Mode::ALL.get(selected)
|
||||
{
|
||||
app.preferences.ai_permissions = *mode;
|
||||
}
|
||||
if app.preferences.ai_permissions == Mode::All {
|
||||
ui_kit::text(ui, tr("ui.ai_mode_all_warning"), ui_kit::Text::Report("permissions_warning", true));
|
||||
}
|
||||
ui_kit::text(ui, tr("ui.ai_modes_scope"), ui_kit::Text::Note("scope"));
|
||||
ui_kit::section(ui, "always_allowed", tr("ui.ai_always_allowed"), true, |ui| {
|
||||
let remembered: Vec<_> =
|
||||
app.preferences.agent_always_allow.iter().filter(|(_, v)| !v.is_empty()).map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
ui_kit::text(ui, tr("ai.prefs.permission_scope"), ui_kit::Text::Hint("scope")).on_hover_text(tr("ui.ai_modes_scope"));
|
||||
let remembered: Vec<_> = app
|
||||
.preferences
|
||||
.agent_always_allow
|
||||
.iter()
|
||||
.filter(|(_, tools)| !tools.is_empty())
|
||||
.map(|(agent, tools)| (agent.clone(), tools.clone()))
|
||||
.collect();
|
||||
ui_kit::section_count(ui, "always_allowed", tr("ui.ai_always_allowed"), false, Some(remembered.len()), |ui| {
|
||||
if remembered.is_empty() {
|
||||
ui_kit::text(ui, tr("ui.ai_always_allowed_none"), ui_kit::Text::Note("none"));
|
||||
ui_kit::text(ui, tr("ai.prefs.no_saved_approvals"), ui_kit::Text::Hint("none")).on_hover_text(tr("ui.ai_always_allowed_none"));
|
||||
}
|
||||
let names: std::collections::HashMap<_, _> = agents::entries(app).into_iter().map(|e| (e.id, e.name)).collect();
|
||||
let names: std::collections::HashMap<_, _> = agents::entries(app).into_iter().map(|entry| (entry.id, entry.name)).collect();
|
||||
for (agent, tools) in remembered {
|
||||
ui_kit::label_actions(ui, names.get(&agent).unwrap_or(&agent), |ui| {
|
||||
if ui_kit::button(ui, tr("ui.ai_reset"), ui_kit::Button::Small).clicked() {
|
||||
app.preferences.agent_always_allow.remove(&agent);
|
||||
}
|
||||
let t = crate::theme::Tokens::get();
|
||||
ui_kit::settings_block(ui, names.get(&agent).unwrap_or(&agent), t.space_24 * 6.0 + t.space_6, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui_kit::button(ui, tr("ui.ai_reset"), ui_kit::Button::Small).clicked() {
|
||||
app.preferences.agent_always_allow.remove(&agent);
|
||||
}
|
||||
ui_kit::settings_short_note(ui, &tools.join(", "));
|
||||
});
|
||||
});
|
||||
ui_kit::text(ui, &tools.join(", "), ui_kit::Text::Report(&agent, false));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui_kit::section(ui, "panel", tr("ui.ai_panel"), true, |ui| {
|
||||
ui_kit::row(ui, tr("ui.agent_chat_side"), |ui| {
|
||||
let p = &mut app.preferences;
|
||||
let mut selected = if p.agent_chat_popped { 2 } else { usize::from(p.agent_chat_side != "left") };
|
||||
let options = [tr("ui.agent_chat_left"), tr("ui.agent_chat_right"), tr("ui.agent_chat_own_window")];
|
||||
if ui_kit::segmented(ui, &mut selected, &options, Default::default()) {
|
||||
p.agent_chat_popped = selected == 2;
|
||||
if selected < 2 {
|
||||
p.agent_chat_side = if selected == 0 { "left" } else { "right" }.into();
|
||||
}
|
||||
|
||||
fn panel_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let t = crate::theme::Tokens::get();
|
||||
ui_kit::settings_section(ui, "panel", tr("ui.ai_panel"), |ui| {
|
||||
ui_kit::settings_form(ui, t.space_24 * 6.0 + t.space_6, |ui| {
|
||||
ui_kit::row(ui, tr("ui.agent_chat_side"), |ui| {
|
||||
let p = &mut app.preferences;
|
||||
let mut selected = if p.agent_chat_popped { 2 } else { usize::from(p.agent_chat_side != "left") };
|
||||
let options = [tr("ui.agent_chat_left"), tr("ui.agent_chat_right"), tr("ui.agent_chat_own_window")];
|
||||
if ui_kit::segmented(ui, &mut selected, &options, ui_kit::Segments { intrinsic: true, ..Default::default() }) {
|
||||
p.agent_chat_popped = selected == 2;
|
||||
if selected < 2 {
|
||||
p.agent_chat_side = if selected == 0 { "left" } else { "right" }.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ui_kit::row(ui, tr("ui.agent_chat_turn_limit"), |ui| {
|
||||
ui_kit::settings_number(ui, &mut app.preferences.agent_chat_turn_limit, 1..=200, 1.0).on_hover_text(tr("ai.prefs.turn_limit_hint"));
|
||||
});
|
||||
});
|
||||
ui_kit::row(ui, tr("ui.agent_chat_turn_limit"), |ui| {
|
||||
ui_kit::number_field(ui, &mut app.preferences.agent_chat_turn_limit, 1..=200, 1.0, "");
|
||||
});
|
||||
ui_kit::text(ui, tr("ui.agent_chat_privacy_note"), ui_kit::Text::Note("privacy"));
|
||||
ui_kit::text(ui, tr("ai.prefs.privacy_note"), ui_kit::Text::Hint("privacy"));
|
||||
});
|
||||
}
|
||||
|
||||
fn pointer_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui_kit::settings_section(ui, "agent_pointer", tr("ui.agent_cursor"), |ui| {
|
||||
ui_kit::settings_pair(
|
||||
ui,
|
||||
|ui| {
|
||||
ui_kit::checkbox(ui, tr("ai.prefs.show_pointer"), &mut app.ui.agent_cursor.show)
|
||||
.on_hover_text(crate::i18n::tr_label("Show the pointer an AI agent moves (#35)"));
|
||||
ui.add_enabled_ui(app.ui.agent_cursor.show, |ui| {
|
||||
let speed_label_width = ui_kit::ContentWidth::label(ui, tr("ai.prefs.pointer_speed")) + crate::theme::Tokens::get().space_4;
|
||||
ui_kit::settings_block(ui, tr("ai.prefs.pointer_speed"), speed_label_width, |ui| {
|
||||
crate::agent_cursor::speed_slider(ui, &mut app.ui.agent_cursor.speed);
|
||||
});
|
||||
ui_kit::checkbox(ui, tr("ai.prefs.follow_camera"), &mut app.ui.agent_cursor.follow_camera)
|
||||
.on_hover_text(crate::i18n::tr_label("Follow the camera (Look At sketches)"));
|
||||
});
|
||||
(app.ui.agent_cursor.show, app.ui.agent_cursor.speed.label(), app.ui.agent_cursor.follow_camera)
|
||||
},
|
||||
|ui, (visible, speed, follow)| ui_kit::settings_pointer_preview(ui, visible, crate::i18n::tr_label(&speed), follow),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,65 +252,88 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let default = if all.iter().any(|e| &e.id == d) { Some(d.clone()) } else { all.first().map(|e| e.id.clone()) };
|
||||
let mut remove: Option<String> = None;
|
||||
let mut test: Option<agents::Entry> = None;
|
||||
ui_kit::section(ui, "agents", tr("ui.ai_agents"), true, |ui| {
|
||||
let t = crate::theme::Tokens::get();
|
||||
ui_kit::settings_section(ui, "agents", tr("ui.ai_agents"), |ui| {
|
||||
if app.agent_chat.detecting.is_some() {
|
||||
ui_kit::text(ui, tr("ui.agent_chat_detecting"), ui_kit::Text::Note("detecting"));
|
||||
ui_kit::text(ui, tr("ui.agent_chat_detecting"), ui_kit::Text::Hint("detecting"));
|
||||
} else if all.is_empty() {
|
||||
ui_kit::text(ui, tr("ui.agent_chat_none_found"), ui_kit::Text::Note("none"));
|
||||
ui_kit::empty_box(ui, "assistant_agents", tr("ai.prefs.no_agents"));
|
||||
}
|
||||
for e in &all {
|
||||
ui_kit::section(ui, &e.id, &e.name, true, |ui| {
|
||||
ui_kit::text(ui, &e.detail(), ui_kit::Text::Report("detail", false));
|
||||
super::panel::login_chip(ui, e.login);
|
||||
if !e.found {
|
||||
ui_kit::text(ui, tr("ui.ai_added"), ui_kit::Text::Note("added"));
|
||||
for (index, entry) in all.iter().enumerate() {
|
||||
ui.push_id(&entry.id, |ui| {
|
||||
if index > 0 {
|
||||
ui_kit::divider(ui, "agent_divider");
|
||||
}
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if ui_kit::list_picker(ui, &e.id, tr("ui.ai_default"), default.as_deref() == Some(e.id.as_str())).clicked() {
|
||||
app.preferences.agent_default = e.id.clone();
|
||||
}
|
||||
});
|
||||
ui_kit::row(ui, tr("ui.ai_permissions"), |ui| {
|
||||
let cur = app.preferences.agent_permissions.get(&e.id).copied();
|
||||
let shown = cur.map_or_else(|| tr("ui.ai_mode_global").to_string(), |m| m.label().to_string());
|
||||
let mut pick: Option<Option<Mode>> = None;
|
||||
ui_kit::dropdown_menu(ui, ("sc_ai_mode", &e.id), &shown, |ui| {
|
||||
if ui_kit::list_picker(ui, "global", tr("ui.ai_mode_global"), cur.is_none()).clicked() {
|
||||
pick = Some(None);
|
||||
ui_kit::settings_block(ui, &entry.name, t.space_24 * 6.0 + t.space_6, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let is_default = default.as_deref() == Some(entry.id.as_str());
|
||||
let choice = ui_kit::button(
|
||||
ui,
|
||||
tr(if is_default { "ui.ai_default" } else { "ai.prefs.make_default" }),
|
||||
if is_default { ui_kit::Button::SmallSelected } else { ui_kit::Button::Small },
|
||||
);
|
||||
crate::scenario::publish_target(&format!("dialog:list:{}", entry.id), choice.rect);
|
||||
if choice.clicked() {
|
||||
app.preferences.agent_default = entry.id.clone();
|
||||
}
|
||||
for m in Mode::ALL {
|
||||
if ui_kit::list_picker(ui, m.id(), m.label(), cur == Some(m)).clicked() {
|
||||
pick = Some(Some(m));
|
||||
}
|
||||
let test_button = ui_kit::button(ui, tr("ui.ai_test"), ui_kit::Button::Small);
|
||||
crate::scenario::publish_handle(&format!("prefs:ai_test:{}", entry.id), test_button.rect.center());
|
||||
if test_button.on_hover_text(tr(if entry.kind == Kind::Acp { "ui.ai_test_acp_tip" } else { "ui.ai_test_api_tip" })).clicked()
|
||||
{
|
||||
test = Some(entry.clone());
|
||||
}
|
||||
if !entry.found && ui_kit::button(ui, tr("ui.ai_remove"), ui_kit::Button::Ghost).clicked() {
|
||||
remove = Some(entry.id.clone());
|
||||
}
|
||||
match entry.login {
|
||||
"signed_in" => ui_kit::settings_badge(ui, tr("ui.agent_chat_signed_in"), false),
|
||||
"key" => ui_kit::settings_badge(ui, tr("ui.agent_chat_key_in_env"), false),
|
||||
"signed_out" => ui_kit::settings_badge(ui, tr("ui.agent_chat_not_signed_in"), true),
|
||||
_ => {}
|
||||
}
|
||||
if !entry.found {
|
||||
ui_kit::settings_badge(ui, tr("ui.ai_added"), false);
|
||||
}
|
||||
});
|
||||
match pick {
|
||||
Some(Some(m)) => {
|
||||
app.preferences.agent_permissions.insert(e.id.clone(), m);
|
||||
}
|
||||
Some(None) => {
|
||||
app.preferences.agent_permissions.remove(&e.id);
|
||||
}
|
||||
None => {}
|
||||
ui_kit::settings_short_note(ui, &entry.detail());
|
||||
ui_kit::settings_form(ui, t.form_label_width + t.space_16, |ui| {
|
||||
ui_kit::row(ui, tr("ui.ai_permissions"), |ui| {
|
||||
let current = app.preferences.agent_permissions.get(&entry.id).copied();
|
||||
let shown = current.map_or_else(|| tr("ui.ai_mode_global").to_owned(), |mode| mode.label().to_owned());
|
||||
let mut pick: Option<Option<Mode>> = None;
|
||||
ui_kit::dropdown_menu(ui, ("sc_ai_mode", &entry.id), &shown, |ui| {
|
||||
if ui_kit::list_picker(ui, "global", tr("ui.ai_mode_global"), current.is_none()).clicked() {
|
||||
pick = Some(None);
|
||||
}
|
||||
for mode in Mode::ALL {
|
||||
if ui_kit::list_picker(ui, mode.id(), mode.label(), current == Some(mode)).clicked() {
|
||||
pick = Some(Some(mode));
|
||||
}
|
||||
}
|
||||
});
|
||||
match pick {
|
||||
Some(Some(mode)) => {
|
||||
app.preferences.agent_permissions.insert(entry.id.clone(), mode);
|
||||
}
|
||||
Some(None) => {
|
||||
app.preferences.agent_permissions.remove(&entry.id);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
});
|
||||
});
|
||||
if app.preferences.agent_permissions.get(&entry.id) == Some(&Mode::All) {
|
||||
ui_kit::text(ui, tr("ai.prefs.override_all_warning"), ui_kit::Text::Report("agent_permissions_warning", true))
|
||||
.on_hover_text(tr("ui.ai_mode_all_warning"));
|
||||
}
|
||||
test_line(ui, &app.agent_chat.settings.tests.get(&entry.id));
|
||||
});
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let b = ui_kit::button(ui, tr("ui.ai_test"), ui_kit::Button::Normal);
|
||||
crate::scenario::publish_handle(&format!("prefs:ai_test:{}", e.id), b.rect.center());
|
||||
if b.on_hover_text(tr(if e.kind == Kind::Acp { "ui.ai_test_acp_tip" } else { "ui.ai_test_api_tip" })).clicked() {
|
||||
test = Some(e.clone());
|
||||
}
|
||||
if !e.found && ui_kit::button(ui, tr("ui.ai_remove"), ui_kit::Button::Normal).clicked() {
|
||||
remove = Some(e.id.clone());
|
||||
}
|
||||
});
|
||||
test_line(ui, &app.agent_chat.settings.tests.get(&e.id));
|
||||
});
|
||||
}
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let b = ui_kit::button(ui, tr("ui.ai_add_agent"), ui_kit::Button::Normal);
|
||||
crate::scenario::publish_handle("prefs:ai_add", b.rect.center());
|
||||
if b.clicked() {
|
||||
let button = ui_kit::icon_label_button(ui, tr("ui.ai_add_agent"), "plus", true, false);
|
||||
crate::scenario::publish_handle("prefs:ai_add", button.rect.center());
|
||||
if button.clicked() {
|
||||
open_add(app);
|
||||
}
|
||||
if ui_kit::button(ui, tr("ui.agent_chat_refresh"), ui_kit::Button::Enabled(app.agent_chat.detecting.is_none())).clicked() {
|
||||
|
||||
@@ -108,12 +108,12 @@ fn the_dialog_holds_its_inputs_while_ok_computes() {
|
||||
// OK computing, until the test lets it finish.
|
||||
let (finish, done) = std::sync::mpsc::channel();
|
||||
let cmds = h.app.dialog.as_ref().and_then(|d| crate::dialogs::apply_commands(&h.app, d).ok()).unwrap();
|
||||
h.app.applying = Some(crate::dialogs::Applying { done, started: crate::now_ms(), cmds, seen: true });
|
||||
h.app.applying = Some(crate::dialogs::Applying::computing(done, cmds));
|
||||
// A click on the square picked would take it out of the profiles.
|
||||
h.step(&json!({"click": {"world": [10, 10, 10]}})).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(h.app.dialog.as_ref().map(|d| d.inputs[0].items.clone()), Some(picked), "no pick while computing");
|
||||
finish.send(()).unwrap();
|
||||
finish.send(None).unwrap();
|
||||
until_applied(&mut h);
|
||||
assert!(h.app.dialog.is_none());
|
||||
h.step(&json!({"expect": {"bodies": 1, "volume": 13000}})).unwrap();
|
||||
@@ -129,8 +129,8 @@ fn inputs_changed_while_ok_computes_are_computed_off_the_thread_too() {
|
||||
let features = h.app.session.doc.features.len();
|
||||
// OK computing other commands than the dialog's, until the test lets it finish.
|
||||
let (finish, done) = std::sync::mpsc::channel();
|
||||
h.app.applying = Some(crate::dialogs::Applying { done, started: crate::now_ms(), cmds: Vec::new(), seen: true });
|
||||
finish.send(()).unwrap();
|
||||
h.app.applying = Some(crate::dialogs::Applying::computing(done, Vec::new()));
|
||||
finish.send(None).unwrap();
|
||||
h.frames(1);
|
||||
assert!(h.app.applying.is_some(), "the dialog's own commands are computing");
|
||||
assert_eq!(h.app.session.doc.features.len(), features, "nothing applied on the window thread");
|
||||
|
||||
@@ -75,7 +75,6 @@ const ALLOWED_LITERALS: &[(&str, usize, &str)] = &[
|
||||
("dialogs_motion.rs", 1, "a white outline"),
|
||||
("gizmo.rs", 2, "the gizmo's neutral grey ball"),
|
||||
("icons.rs", 1, "the white tint that carries an icon image's opacity"),
|
||||
("home.rs", 2, "thumbnail tints"),
|
||||
("ref_images.rs", 1, "a reference image's opacity tint"),
|
||||
("theme_files.rs", 1, "a colour read from a theme file, which sets tokens"),
|
||||
("titlebar.rs", 2, "the window close button's red hover (the system's convention) and white ink"),
|
||||
|
||||
@@ -379,6 +379,35 @@ const SHADED: [&str; 2] = ["analysis.draft", "analysis.access"];
|
||||
/// Tokens no colour vision mode needs to keep apart, and why. A new token fails
|
||||
/// `every_token_is_measured_or_carries_no_meaning` until it is in a group above or here.
|
||||
const NO_MEANING: &[(&str, &str)] = &[
|
||||
("file_dim", "File view secondary text, WCAG guarded in theme tests"),
|
||||
("file_recovery_well", "decorative recovery icon ground"),
|
||||
("file_action_height", "layout dimension"),
|
||||
("file_back_height", "layout dimension"),
|
||||
("file_recovery_icon", "layout dimension"),
|
||||
("file_grid_gap", "layout dimension"),
|
||||
("file_nav_width", "layout dimension"),
|
||||
("file_nav_height", "layout dimension"),
|
||||
("file_tile_height", "layout dimension"),
|
||||
("file_tile_icon", "layout dimension"),
|
||||
("file_recent_height", "layout dimension"),
|
||||
("file_sample_image_height", "layout dimension"),
|
||||
("file_content_inset_x", "layout dimension"),
|
||||
("file_content_inset_y", "layout dimension"),
|
||||
("file_section_gap", "layout dimension"),
|
||||
("font_start_title", "layout dimension"),
|
||||
("file_background", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_surface", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_image_tint", "neutral image multiplication; model pixels retain their original colours"),
|
||||
("file_border", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_rail", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_rail_ink", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_rail_hover", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_primary", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_primary_ink", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_primary_border", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_recovery", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_recovery_ink", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("file_recovery_border", "File view chrome; paired text contrast is guarded in theme tests"),
|
||||
("space_2", "layout dimension"),
|
||||
("space_3", "layout dimension"),
|
||||
("space_4", "layout dimension"),
|
||||
@@ -846,7 +875,7 @@ fn the_sections_messages_are_in_every_catalog() {
|
||||
let ids: Vec<&str> = ColorVision::ALL
|
||||
.into_iter()
|
||||
.flat_map(|m| [m.label_id(), m.help_id()])
|
||||
.chain(crate::prefs::COLOUR_VISION_SAMPLE)
|
||||
.chain(crate::ui_kit::COLOUR_VISION_SAMPLE)
|
||||
.chain(["ui.colour_vision", "ui.colour_vision_mode", "ui.colour_vision_sample", "ui.colour_vision_note"])
|
||||
.collect();
|
||||
for (lang, text) in [
|
||||
@@ -984,22 +1013,22 @@ fn random_values_never_panic_and_only_the_four_ids_are_taken() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_puts_the_mode_back_ok_keeps_it_and_restore_defaults_resets_graphics_only() {
|
||||
fn cancel_puts_the_mode_back_ok_keeps_it_and_restore_defaults_resets_interface_only() {
|
||||
let mut app = app();
|
||||
let control = |app: &mut crate::SolveApp, p: serde_json::Value| crate::prefs::control(app, &p).unwrap();
|
||||
control(&mut app, json!({"open": "Graphics"}));
|
||||
control(&mut app, json!({"open": "Interface"}));
|
||||
control(&mut app, json!({"color_vision": "protan"}));
|
||||
assert_eq!(app.preferences.color_vision, ColorVision::Protan);
|
||||
control(&mut app, json!({"button": "cancel"}));
|
||||
assert_eq!(app.preferences.color_vision, ColorVision::Off);
|
||||
control(&mut app, json!({"open": "Graphics"}));
|
||||
control(&mut app, json!({"open": "Interface"}));
|
||||
control(&mut app, json!({"color_vision": "tritan", "button": "ok"}));
|
||||
assert_eq!(app.preferences.color_vision, ColorVision::Tritan);
|
||||
// Restore Defaults on another page leaves it.
|
||||
control(&mut app, json!({"open": "General"}));
|
||||
control(&mut app, json!({"button": "defaults"}));
|
||||
assert_eq!(app.preferences.color_vision, ColorVision::Tritan);
|
||||
control(&mut app, json!({"open": "Graphics"}));
|
||||
control(&mut app, json!({"open": "Interface"}));
|
||||
control(&mut app, json!({"button": "defaults"}));
|
||||
assert_eq!(app.preferences.color_vision, ColorVision::Off);
|
||||
control(&mut app, json!({"button": "ok"}));
|
||||
|
||||
@@ -17,6 +17,11 @@ pub struct CrashUi {
|
||||
}
|
||||
|
||||
impl CrashUi {
|
||||
/// Cached report count; rendering preferences never reads the filesystem.
|
||||
pub(crate) fn saved_count(&self) -> usize {
|
||||
self.reports.len()
|
||||
}
|
||||
|
||||
/// Read the reports again (when the window opens or changes; never every frame).
|
||||
pub fn refresh(&mut self) {
|
||||
let dir = self.dir_path();
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
//! The command is evaluated on a copy of the session ([`solidcraft_engine::Session::ahead`]),
|
||||
//! which leaves what it computes in the evaluation cache the session shares. Done within the
|
||||
//! app's wait ([`crate::dialogs::APPLY_AT_ONCE`]), the command just runs; otherwise the window
|
||||
//! goes on, saying it computes, and the command runs for real once the evaluation is in, finding
|
||||
//! its features in the cache. Commands asked for meanwhile wait their turn; a marker moved again
|
||||
//! goes on, saying it computes, and the command runs for real once the evaluation is in, taking
|
||||
//! the features it evaluated ([`solidcraft_engine::Session::prime`]) rather than computing them
|
||||
//! again. Commands asked for meanwhile wait their turn; a marker moved again
|
||||
//! replaces the move still waiting, so a drag computes where it ends rather than every slot it
|
||||
//! crossed.
|
||||
//!
|
||||
@@ -24,7 +25,9 @@ use crate::SolveApp;
|
||||
|
||||
/// A command computing off the window thread.
|
||||
pub struct Deferred {
|
||||
done: std::sync::mpsc::Receiver<()>,
|
||||
done: std::sync::mpsc::Receiver<Option<solidcraft_engine::Evaluated>>,
|
||||
/// What it evaluated, once in (`Some(None)`: it failed, or its thread is gone).
|
||||
evaluated: Option<Option<solidcraft_engine::Evaluated>>,
|
||||
/// When it started (ms).
|
||||
pub(crate) started: f64,
|
||||
command: (String, Value),
|
||||
@@ -33,11 +36,26 @@ pub struct Deferred {
|
||||
}
|
||||
|
||||
impl Deferred {
|
||||
fn finished(&self) -> bool {
|
||||
!matches!(self.done.try_recv(), Err(std::sync::mpsc::TryRecvError::Empty))
|
||||
pub(crate) fn finished(&mut self) -> bool {
|
||||
if self.evaluated.is_none() {
|
||||
match self.done.try_recv() {
|
||||
Ok(evaluated) => self.evaluated = Some(evaluated),
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => self.evaluated = Some(None),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => {}
|
||||
}
|
||||
}
|
||||
self.evaluated.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// How a command asked to run off the window thread goes.
|
||||
enum Start {
|
||||
/// It computes off the window thread.
|
||||
Later(Deferred),
|
||||
/// It runs now, with what was evaluated for it ahead (if anything).
|
||||
Now(Option<solidcraft_engine::Evaluated>),
|
||||
}
|
||||
|
||||
/// A delete's check computing off the window thread: each check command's reply, in order.
|
||||
pub struct DeleteCheck {
|
||||
done: std::sync::mpsc::Receiver<Vec<Result<Value, String>>>,
|
||||
@@ -105,14 +123,24 @@ impl SolveApp {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
match self.begin_deferred(id, ¶ms) {
|
||||
Some(job) => {
|
||||
Start::Later(job) => {
|
||||
self.deferred = Some(job);
|
||||
Ok(Value::Null)
|
||||
}
|
||||
None => self.run(id, params),
|
||||
Start::Now(evaluated) => self.run_evaluated(id, params, evaluated.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a command for real, taking the features `evaluated` computed for it ahead.
|
||||
fn run_evaluated(&mut self, id: &str, params: Value, evaluated: Option<&solidcraft_engine::Evaluated>) -> Result<Value, String> {
|
||||
if let Some(evaluated) = evaluated {
|
||||
self.session.prime(evaluated);
|
||||
}
|
||||
let r = self.run(id, params);
|
||||
self.session.unprime();
|
||||
r
|
||||
}
|
||||
|
||||
/// Since when a command, or a delete's check, has been computing off the window thread (ms),
|
||||
/// if one is.
|
||||
pub fn computing_since(&self) -> Option<f64> {
|
||||
@@ -175,7 +203,7 @@ impl SolveApp {
|
||||
|
||||
/// Each frame: a command whose evaluation is in runs, then the next one waiting starts.
|
||||
pub(crate) fn poll_deferred(&mut self, ctx: &egui::Context) {
|
||||
if let Some(job) = &self.deferred {
|
||||
if let Some(job) = &mut self.deferred {
|
||||
if !job.finished() {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
return;
|
||||
@@ -187,7 +215,7 @@ impl SolveApp {
|
||||
// The design changed meanwhile: computed again for the design as it is now.
|
||||
let _ = self.run_heavy(&id, params);
|
||||
} else if !superseded {
|
||||
let _ = self.run(&id, params);
|
||||
let _ = self.run_evaluated(&id, params, job.evaluated.flatten().as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,33 +229,35 @@ impl SolveApp {
|
||||
/// ([`SolveApp::apply_off_thread`]), a file operation in progress, no threads (the web), or
|
||||
/// done within the wait.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn begin_deferred(&self, id: &str, params: &Value) -> Option<Deferred> {
|
||||
let wait = self.apply_off_thread?;
|
||||
fn begin_deferred(&self, id: &str, params: &Value) -> Start {
|
||||
let Some(wait) = self.apply_off_thread else { return Start::Now(None) };
|
||||
if self.file_load.busy() || self.file_picker.is_some() {
|
||||
return None;
|
||||
return Start::Now(None);
|
||||
}
|
||||
let ahead = self.session.ahead();
|
||||
let command = (id.to_string(), params.clone());
|
||||
let commands = vec![command.clone()];
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::Builder::new()
|
||||
.name("history".into())
|
||||
.spawn(move || {
|
||||
let _ = ahead.evaluate(&commands);
|
||||
let _ = tx.send(());
|
||||
})
|
||||
.ok()?;
|
||||
let spawned = std::thread::Builder::new().name("history".into()).spawn(move || {
|
||||
let _ = tx.send(ahead.evaluate(&commands).ok());
|
||||
});
|
||||
if spawned.is_err() {
|
||||
return Start::Now(None);
|
||||
}
|
||||
// Zero explicitly requests off-thread application, even if a fast worker
|
||||
// finishes before this thread is scheduled again. Apply on the next frame.
|
||||
if wait.is_zero() || matches!(rx.recv_timeout(wait), Err(std::sync::mpsc::RecvTimeoutError::Timeout)) {
|
||||
Some(Deferred { done: rx, started: crate::now_ms(), command, revision: self.session.revision })
|
||||
} else {
|
||||
None
|
||||
if !wait.is_zero() {
|
||||
match rx.recv_timeout(wait) {
|
||||
Ok(evaluated) => return Start::Now(evaluated),
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return Start::Now(None),
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
|
||||
}
|
||||
}
|
||||
Start::Later(Deferred { done: rx, evaluated: None, started: crate::now_ms(), command, revision: self.session.revision })
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn begin_deferred(&self, _id: &str, _params: &Value) -> Option<Deferred> {
|
||||
None
|
||||
fn begin_deferred(&self, _id: &str, _params: &Value) -> Start {
|
||||
Start::Now(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,12 +437,14 @@ struct ShellChain {
|
||||
faces: Vec<Sel>,
|
||||
}
|
||||
|
||||
/// OK computing off the window thread: the dialog's commands evaluated on a scratch copy of the
|
||||
/// session, which leaves the feature in the evaluation cache the session shares. Once that is
|
||||
/// in, OK runs them for real and finds the feature there; meanwhile the window stays live and
|
||||
/// the dialog says it is computing.
|
||||
/// OK computing off the window thread: the dialog's commands evaluated on a copy of the
|
||||
/// session. Once that is in, OK runs them for real, taking what the copy evaluated
|
||||
/// ([`solidcraft_engine::Session::prime`]) rather than computing it again; meanwhile the window
|
||||
/// stays live and the dialog says it is computing.
|
||||
pub struct Applying {
|
||||
pub(crate) done: std::sync::mpsc::Receiver<()>,
|
||||
pub(crate) done: std::sync::mpsc::Receiver<Option<solidcraft_engine::Evaluated>>,
|
||||
/// What it evaluated, once in (`Some(None)`: it failed, or its worker is gone).
|
||||
evaluated: Option<Option<solidcraft_engine::Evaluated>>,
|
||||
/// When OK was pressed (ms).
|
||||
pub(crate) started: f64,
|
||||
/// The commands being computed.
|
||||
@@ -453,17 +455,48 @@ pub struct Applying {
|
||||
}
|
||||
|
||||
impl Applying {
|
||||
/// The evaluation is in (or its worker ended).
|
||||
fn finished(&self) -> bool {
|
||||
!matches!(self.done.try_recv(), Err(std::sync::mpsc::TryRecvError::Empty))
|
||||
/// OK computing `cmds`, in once `done` says what was evaluated (tests stand in for the worker).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn computing(done: std::sync::mpsc::Receiver<Option<solidcraft_engine::Evaluated>>, cmds: Vec<(String, Value)>) -> Self {
|
||||
Applying { done, evaluated: None, started: crate::now_ms(), cmds, seen: true }
|
||||
}
|
||||
|
||||
/// The evaluation is in and the job has been in a frame. A job that has just started waits
|
||||
/// one frame: the window thread may be held up between starting it and the check, and the
|
||||
/// feature must not appear where the dialog says it is computing.
|
||||
fn ready(&self) -> bool {
|
||||
fn ready(&mut self) -> bool {
|
||||
self.seen && self.finished()
|
||||
}
|
||||
|
||||
/// The evaluation is in (or its worker ended).
|
||||
fn finished(&mut self) -> bool {
|
||||
if self.evaluated.is_none() {
|
||||
match self.done.try_recv() {
|
||||
Ok(evaluated) => self.evaluated = Some(evaluated),
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => self.evaluated = Some(None),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => {}
|
||||
}
|
||||
}
|
||||
self.evaluated.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// How OK goes.
|
||||
enum Begun {
|
||||
/// It computes off the window thread.
|
||||
Later(Applying),
|
||||
/// It runs now, with what was evaluated for it (if anything).
|
||||
Now(Option<solidcraft_engine::Evaluated>),
|
||||
}
|
||||
|
||||
/// Run the dialog's commands, taking the features `evaluated` computed for them ahead.
|
||||
fn run_evaluated(app: &mut SolveApp, d: &Dialog, evaluated: Option<&solidcraft_engine::Evaluated>) -> Result<(), String> {
|
||||
if let Some(evaluated) = evaluated {
|
||||
app.session.prime(evaluated);
|
||||
}
|
||||
let r = run_dialog(app, d);
|
||||
app.session.unprime();
|
||||
r
|
||||
}
|
||||
|
||||
/// How long the desktop app's OK waits for its evaluation before it lets the window go on and
|
||||
@@ -471,41 +504,44 @@ impl Applying {
|
||||
/// applies at once, as it always did.
|
||||
pub const APPLY_AT_ONCE: std::time::Duration = std::time::Duration::from_millis(150);
|
||||
|
||||
/// Start OK's evaluation off the window thread, for a feature dialog. `None` when OK should
|
||||
/// Start OK's evaluation off the window thread, for a feature dialog. `Now` when OK should
|
||||
/// just run: not a feature, not asked for ([`SolveApp::apply_off_thread`]), no threads (the
|
||||
/// web), or done within the wait.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn begin_applying(app: &SolveApp, d: &Dialog) -> Option<Applying> {
|
||||
let wait = app.apply_off_thread?;
|
||||
fn begin_applying(app: &SolveApp, d: &Dialog) -> Begun {
|
||||
let Some(wait) = app.apply_off_thread else { return Begun::Now(None) };
|
||||
if !d.previews() {
|
||||
return None;
|
||||
return Begun::Now(None);
|
||||
}
|
||||
let cmds = apply_commands(app, d).ok()?;
|
||||
let scratch = app.session.scratch();
|
||||
let Ok(cmds) = apply_commands(app, d) else { return Begun::Now(None) };
|
||||
let ahead = app.session.ahead();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let computed = cmds.clone();
|
||||
#[cfg(test)]
|
||||
let gate = app.apply_gate.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("apply".into())
|
||||
.spawn(move || {
|
||||
let _ = scratch.preview(&computed);
|
||||
#[cfg(test)]
|
||||
if let Some(gate) = gate {
|
||||
let _ = gate.lock().unwrap().recv();
|
||||
}
|
||||
let _ = tx.send(());
|
||||
})
|
||||
.ok()?;
|
||||
let spawned = std::thread::Builder::new().name("apply".into()).spawn(move || {
|
||||
let evaluated = ahead.evaluate(&computed).ok();
|
||||
#[cfg(test)]
|
||||
if let Some(gate) = gate {
|
||||
let _ = gate.lock().unwrap().recv();
|
||||
}
|
||||
let _ = tx.send(evaluated);
|
||||
});
|
||||
if spawned.is_err() {
|
||||
return Begun::Now(None);
|
||||
}
|
||||
match rx.recv_timeout(wait) {
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Some(Applying { done: rx, started: crate::now_ms(), cmds, seen: false }),
|
||||
_ => None,
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
Begun::Later(Applying { done: rx, evaluated: None, started: crate::now_ms(), cmds, seen: false })
|
||||
}
|
||||
Ok(evaluated) => Begun::Now(evaluated),
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Begun::Now(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn begin_applying(_app: &SolveApp, _d: &Dialog) -> Option<Applying> {
|
||||
None
|
||||
fn begin_applying(_app: &SolveApp, _d: &Dialog) -> Begun {
|
||||
Begun::Now(None)
|
||||
}
|
||||
|
||||
/// The commands a dialog's inputs make (or why they make none), its picks, and the design's
|
||||
@@ -2443,7 +2479,6 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
plane["fixed"] = json!(true);
|
||||
match app.run("sketch.create", json!({"plane":plane})) {
|
||||
Ok(_) => {
|
||||
app.pre_sketch_cam = Some(app.cam);
|
||||
look_at_sketch(app);
|
||||
cancel = true;
|
||||
then_start = d.then_start;
|
||||
@@ -2471,11 +2506,11 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
Kind::Text(k) => Some(crate::sketch_text::apply(app, k)),
|
||||
_ => match begin_applying(app, &d) {
|
||||
// Computing: the dialog stays, saying so, until it is in.
|
||||
Some(job) => {
|
||||
Begun::Later(job) => {
|
||||
app.applying = Some(job);
|
||||
None
|
||||
}
|
||||
None => Some(run_dialog(app, &d)),
|
||||
Begun::Now(evaluated) => Some(run_evaluated(app, &d, evaluated.as_ref())),
|
||||
},
|
||||
};
|
||||
match run {
|
||||
@@ -2487,22 +2522,22 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
// OK's evaluation is in: run it, the feature taken from the cache. Inputs changed meanwhile
|
||||
// OK's evaluation is in: run it, taking what was evaluated. Inputs changed meanwhile
|
||||
// (nothing should change them) are computed in turn, off the window thread too: run here,
|
||||
// they would hold the window for the whole boolean.
|
||||
// A job that has just started is not in yet: it waits a frame, so the dialog says it is
|
||||
// computing before the feature appears.
|
||||
let ready = app.applying.as_ref().is_some_and(Applying::ready);
|
||||
let ready = app.applying.as_mut().is_some_and(Applying::ready);
|
||||
if let Some(job) = &mut app.applying {
|
||||
job.seen = true;
|
||||
}
|
||||
if ready && !cancel {
|
||||
let computed = app.applying.take().map(|job| job.cmds);
|
||||
let (computed, evaluated) = app.applying.take().map(|job| (Some(job.cmds), job.evaluated.flatten())).unwrap_or_default();
|
||||
let current = apply_commands(app, &d).ok();
|
||||
let again = if current.is_some() && current != computed { begin_applying(app, &d) } else { None };
|
||||
let again = if current.is_some() && current != computed { begin_applying(app, &d) } else { Begun::Now(evaluated) };
|
||||
match again {
|
||||
Some(job) => app.applying = Some(job),
|
||||
None => match run_dialog(app, &d) {
|
||||
Begun::Later(job) => app.applying = Some(job),
|
||||
Begun::Now(evaluated) => match run_evaluated(app, &d, evaluated.as_ref()) {
|
||||
Ok(()) => {
|
||||
cancel = !apply_again;
|
||||
applied = true;
|
||||
@@ -2778,9 +2813,7 @@ pub fn start_sketch(app: &mut SolveApp, sel: &Sel) -> Result<(), String> {
|
||||
{
|
||||
params["flip"] = json!(flip);
|
||||
}
|
||||
let before = app.cam;
|
||||
app.run("sketch.create", params).map_err(|error| crate::friendly_error("sketch.create", &error))?;
|
||||
app.pre_sketch_cam = Some(before);
|
||||
look_at_sketch(app);
|
||||
// On a face: look at the picked point, not the plane's origin (a corner of the face),
|
||||
// which left the face filling the view off-centre like a big square.
|
||||
@@ -4081,7 +4114,9 @@ pub(crate) fn plane_sel(s: &Session, pl: &PlaneRef) -> Option<Sel> {
|
||||
match pl {
|
||||
PlaneRef::Origin { name } => Some(Sel::Plane { name: name.clone() }),
|
||||
PlaneRef::Construction { name } => Some(Sel::Plane { name: name.to_string() }),
|
||||
PlaneRef::Custom { plane } | PlaneRef::Face { plane, .. } => face_sel(s, plane.origin),
|
||||
PlaneRef::Custom { plane } => face_sel(s, plane.origin),
|
||||
// The point picked on the face: the plane's origin can lie off the face (#3567).
|
||||
PlaneRef::Face { at, .. } => face_sel(s, *at),
|
||||
PlaneRef::Flipped { base, .. } => plane_sel(s, base),
|
||||
_ => None,
|
||||
}
|
||||
@@ -4207,6 +4242,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
AxisRef::World { axis } => Some(Sel::Axis { name: axis.to_ascii_uppercase() }),
|
||||
AxisRef::SketchAxis { axis } => Some(Sel::Axis { name: axis.to_ascii_lowercase() }),
|
||||
AxisRef::SketchLine { curve } => Some(Sel::SketchCurve { id: curve.clone() }),
|
||||
AxisRef::Construction { name } => Some(Sel::Axis { name: name.to_string() }),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(inp) = d.inputs.get_mut(1) {
|
||||
@@ -4538,12 +4574,14 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
}
|
||||
d
|
||||
}
|
||||
PatternKind::Circular { origin, axis, count, angle, alignment, reversed } => {
|
||||
PatternKind::Circular { origin, axis, count, angle, alignment, reversed, axis_feature } => {
|
||||
let mut d = start("solid.pattern.circular")?;
|
||||
d.extra.insert("alignment".into(), json!(alignment));
|
||||
d.extra.insert("reversed".into(), json!(reversed));
|
||||
d.kind = Kind::PatternCirc { count: count.to_string(), angle: angle.to_string() };
|
||||
match world_axis(axis.0).filter(|_| origin.len() < 1e-9) {
|
||||
// The construction axis it turns about, else an origin axis or its line.
|
||||
let named = axis_feature.as_ref().map(|n| n.to_string());
|
||||
match named.or_else(|| world_axis(axis.0).filter(|_| origin.len() < 1e-9)) {
|
||||
Some(name) => {
|
||||
if let Some(inp) = d.inputs.get_mut(1) {
|
||||
inp.items = vec![Sel::Axis { name }];
|
||||
|
||||
@@ -1007,7 +1007,7 @@ pub fn animate(app: &SolveApp, k: &Asm, cmds: &mut Vec<(String, Value)>) {
|
||||
None
|
||||
}
|
||||
// As-built joints start at zero: drive the new one after it is made.
|
||||
None => Some(next_joint_id(&app.session)),
|
||||
None => app.session.doc.next_item_id().ok(),
|
||||
},
|
||||
};
|
||||
if let Some(id) = drive {
|
||||
@@ -1015,10 +1015,6 @@ pub fn animate(app: &SolveApp, k: &Asm, cmds: &mut Vec<(String, Value)>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn next_joint_id(s: &Session) -> u64 {
|
||||
s.doc.assembly.joints.iter().map(|j| j.id).max().unwrap_or(0) + 1
|
||||
}
|
||||
|
||||
/// Joint and drive previews: the commands run on a scratch copy and the placed model before and
|
||||
/// after is compared, so the components that move show where they go (`None`: not such
|
||||
/// commands).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user