forked from 1b-agent/opencadstudio-upstream
Merge main into Pallet-placement
Resolved conflicts: - app/update/mod.rs: keep the dock's dock_open_at_default for the XREF manager; take main's stable xref row identity (row_index(&id)). - app/view/mod.rs: keep the edge layout from app::dock; drop main's copy of the old edge-stack code (main only reformatted it there). - ui/window/layers.rs: keep main's new visible_indices next to the branch's view_panel. Files the branch only reformatted take main's version. Adds the Lao (lo-LA) translations for the new dock texts. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
136 files changed
+12408
-949
No files matched your search
@@ -1,10 +1,8 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
# Until now nothing ran the test suite on a change: the release workflows
|
# Runs the workspace tests on every push to main and on every pull request,
|
||||||
# build the app, web-check.yml type-checks the wasm target, and pull requests
|
# on Linux, Windows and macOS. A separate lint job reports rustfmt and clippy
|
||||||
# reported their own local runs. Run the workspace tests on every push to
|
# results without blocking merges until the existing tree has been cleaned up.
|
||||||
# main and on every pull request. There is deliberately no formatter check:
|
|
||||||
# the tree is not rustfmt-clean and is not going to be reformatted.
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -20,13 +18,19 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-22.04
|
name: test (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-22.04, windows-latest, macos-14]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# The same libraries the Linux release build needs (windowing, GL,
|
# The same libraries the Linux release build needs (windowing, GL,
|
||||||
# fonts); the packaging tools are left out.
|
# fonts); the packaging tools are left out.
|
||||||
- name: System libraries
|
- name: System libraries (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
@@ -37,8 +41,38 @@ jobs:
|
|||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
# LC_ALL=C: a few tests compare English message text.
|
# LC_ALL=C: a few tests compare English message text. The GPU-backed
|
||||||
|
# tests are #[ignore]d and need an adapter, so they do not run here.
|
||||||
- name: cargo test
|
- name: cargo test
|
||||||
env:
|
env:
|
||||||
LC_ALL: C
|
LC_ALL: C
|
||||||
run: cargo test --workspace --locked
|
run: cargo test --workspace --locked
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: lint (informational)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
# Non-blocking until the existing tree is cleaned up. The tree currently has
|
||||||
|
# thousands of rustfmt diffs and several clippy warnings and errors, so
|
||||||
|
# these checks report status without failing the workflow.
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: System libraries
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \
|
||||||
|
libxkbcommon-dev libwayland-dev libfontconfig1-dev libfreetype6-dev
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: cargo fmt --check
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
- name: cargo clippy
|
||||||
|
run: cargo clippy --workspace --all-targets --locked
|
||||||
@@ -511,6 +511,12 @@ jobs:
|
|||||||
codesign -d --entitlements - --xml OpenCADStudio.app/Contents/PlugIns/DWGThumbnail.appex \
|
codesign -d --entitlements - --xml OpenCADStudio.app/Contents/PlugIns/DWGThumbnail.appex \
|
||||||
| python3 -c 'import io, plistlib, sys; ent = plistlib.load(io.BytesIO(sys.stdin.buffer.read())); print("entitlements:", ent); assert ent.get("com.apple.security.app-sandbox") is True'
|
| python3 -c 'import io, plistlib, sys; ent = plistlib.load(io.BytesIO(sys.stdin.buffer.read())); print("entitlements:", ent); assert ent.get("com.apple.security.app-sandbox") is True'
|
||||||
|
|
||||||
|
# Exercise CFBundleExecutable, not just the inner application: the
|
||||||
|
# Finder launcher must preserve --mcp and the client's stdio pipes.
|
||||||
|
- name: Verify packaged MCP handshake
|
||||||
|
timeout-minutes: 2
|
||||||
|
run: python3 docs/automation/mcp_handshake.py OpenCADStudio.app/Contents/MacOS/OpenCADStudio
|
||||||
|
|
||||||
- name: Create .dmg
|
- name: Create .dmg
|
||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
|
|||||||
Generated
+5
-5
@@ -4,7 +4,7 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "OpenCADStudio"
|
name = "OpenCADStudio"
|
||||||
version = "2026.40.1"
|
version = "2026.41.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"allsorts",
|
"allsorts",
|
||||||
"ashpd",
|
"ashpd",
|
||||||
@@ -4205,7 +4205,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "opencadcodec"
|
name = "opencadcodec"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=937745d#937745d9ec1a62d252074ff41b140b92303d0dc0"
|
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=8189f83#8189f833574c544fc57a1d773591014c3a5907c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
@@ -4228,7 +4228,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "opencadgraph"
|
name = "opencadgraph"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=81bf45f#81bf45f57cbab5ff3784f293365279bf2d93f758"
|
source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=f4fdb80#f4fdb800292f53a1454466b638ae0610a2b5b7f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"opencadkernel",
|
"opencadkernel",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4237,7 +4237,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "opencadkernel"
|
name = "opencadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=86497ae#86497ae923b7d9297707aaedb712a8678815a238"
|
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=71770e1#71770e10687f2f5b96745502315f83ca90c5c83f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
"opencadcodec",
|
"opencadcodec",
|
||||||
@@ -4248,7 +4248,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "opencadkernel-constraints"
|
name = "opencadkernel-constraints"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=86497ae#86497ae923b7d9297707aaedb712a8678815a238"
|
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=71770e1#71770e10687f2f5b96745502315f83ca90c5c83f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
]
|
]
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "OpenCADStudio"
|
name = "OpenCADStudio"
|
||||||
version = "2026.40.1"
|
version = "2026.41.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
default-run = "OpenCADStudio"
|
default-run = "OpenCADStudio"
|
||||||
@@ -34,10 +34,10 @@ rfd = "0.17"
|
|||||||
clap = { version = "4", features = ["derive", "string"] }
|
clap = { version = "4", features = ["derive", "string"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "937745d", features = ["serde"] }
|
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] }
|
||||||
kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "86497ae", features = ["acis", "offset"] }
|
kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "71770e1", features = ["acis", "offset"] }
|
||||||
kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "86497ae" }
|
kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "71770e1" }
|
||||||
graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "81bf45f" }
|
graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "f4fdb80" }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<a href="docs/readme/README.it.md">Italiano</a> ·
|
<a href="docs/readme/README.it.md">Italiano</a> ·
|
||||||
<a href="docs/readme/README.ja.md">日本語</a> ·
|
<a href="docs/readme/README.ja.md">日本語</a> ·
|
||||||
<a href="docs/readme/README.ko.md">한국어</a> ·
|
<a href="docs/readme/README.ko.md">한국어</a> ·
|
||||||
|
<a href="docs/readme/README.lo.md">ລາວ</a> ·
|
||||||
<a href="docs/readme/README.pl.md">Polski</a> ·
|
<a href="docs/readme/README.pl.md">Polski</a> ·
|
||||||
<a href="docs/readme/README.ru.md">Русский</a> ·
|
<a href="docs/readme/README.ru.md">Русский</a> ·
|
||||||
<a href="docs/readme/README.zh-CN.md">简体中文</a> ·
|
<a href="docs/readme/README.zh-CN.md">简体中文</a> ·
|
||||||
@@ -136,9 +137,9 @@ The application is ad-hoc signed but is not currently notarized by Apple.
|
|||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|
||||||
Open CAD Studio can follow the system language or use any of these 21 interface languages:
|
Open CAD Studio can follow the system language or use any of these 22 interface languages:
|
||||||
|
|
||||||
> Arabic · Brazilian Portuguese · Bulgarian · Czech · Dutch · English · Finnish · French · German · Greek · Hindi · Hungarian · Italian · Japanese · Korean · Polish · Russian · Simplified Chinese · Spanish · Traditional Chinese · Turkish
|
> Arabic · Brazilian Portuguese · Bulgarian · Czech · Dutch · English · Finnish · French · German · Greek · Hindi · Hungarian · Italian · Japanese · Korean · Lao · Polish · Russian · Simplified Chinese · Spanish · Traditional Chinese · Turkish
|
||||||
|
|
||||||
Change the language from the application settings. The browser version also uses the browser's preferred locale when **System** is selected.
|
Change the language from the application settings. The browser version also uses the browser's preferred locale when **System** is selected.
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ LEN, *LENGTHEN
|
|||||||
AL, *ALIGN
|
AL, *ALIGN
|
||||||
AR, *ARRAY
|
AR, *ARRAY
|
||||||
MA, *MATCHPROP
|
MA, *MATCHPROP
|
||||||
|
VV, *QLAYER
|
||||||
ATE, *ATTEDIT
|
ATE, *ATTEDIT
|
||||||
|
|
||||||
; ── Clipboard ─────────────────────────────────────────────────────────────
|
; ── Clipboard ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -714,7 +714,7 @@ fn bench_selection_cloning(runner: &mut BenchmarkRunner) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut state = SelectionState {
|
let state = SelectionState {
|
||||||
view: SelectionView {
|
view: SelectionView {
|
||||||
vp_size: (1920.0, 1080.0),
|
vp_size: (1920.0, 1080.0),
|
||||||
},
|
},
|
||||||
@@ -2069,6 +2069,177 @@ fn bench_view_render_viewport_construction(runner: &mut BenchmarkRunner) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 19. Viewport Navigation & Spatial Candidate Scaling ─────────────────────
|
||||||
|
|
||||||
|
fn bench_navigation_latency_and_coalescing(runner: &mut BenchmarkRunner) {
|
||||||
|
if !runner.should_run("navigation")
|
||||||
|
&& !runner.should_run("nav_")
|
||||||
|
&& !runner.should_run("quadtree")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixture: populated scene with mixed entities
|
||||||
|
let n_entities = if runner.quick_mode { 1_000 } else { 5_000 };
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
for i in 0..n_entities {
|
||||||
|
let x = (i % 100) as f64 * 20.0;
|
||||||
|
let y = (i / 100) as f64 * 20.0;
|
||||||
|
let mut line = Line::new();
|
||||||
|
line.start = Vector3::new(x, y, 0.0);
|
||||||
|
line.end = Vector3::new(x + 15.0, y + 15.0, 0.0);
|
||||||
|
scene.add_entity(EntityType::Line(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: 1920.0,
|
||||||
|
height: 1080.0,
|
||||||
|
};
|
||||||
|
let mut cam = Camera::default();
|
||||||
|
|
||||||
|
// Warm up viewports
|
||||||
|
let _ = scene.build_viewports(
|
||||||
|
bounds,
|
||||||
|
codec::entities::ViewportRenderMode::Wireframe2D,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
[1.0, 1.0, 1.0, 1.0],
|
||||||
|
);
|
||||||
|
|
||||||
|
let n_pans = if runner.quick_mode { 2_000 } else { 10_000 };
|
||||||
|
let runs = 5;
|
||||||
|
|
||||||
|
// 1. Pure Camera Pan Calculation + Viewport update (matrix update & generation bump)
|
||||||
|
let mut pan_samples = Vec::with_capacity(runs);
|
||||||
|
for _ in 0..runs {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
for step in 0..n_pans {
|
||||||
|
let dx = (step % 10) as f32 * 2.0;
|
||||||
|
let dy = (step % 5) as f32 * 2.0;
|
||||||
|
cam.pan_screen(dx, dy, bounds.height);
|
||||||
|
let primitive = scene.build_viewports(
|
||||||
|
bounds,
|
||||||
|
codec::entities::ViewportRenderMode::Wireframe2D,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
[1.0, 1.0, 1.0, 1.0],
|
||||||
|
);
|
||||||
|
black_box(primitive);
|
||||||
|
}
|
||||||
|
let per_us = (t0.elapsed().as_micros() as f64) / (n_pans as f64);
|
||||||
|
pan_samples.push(per_us);
|
||||||
|
}
|
||||||
|
let median_pan_us = pan_samples[pan_samples.len() / 2];
|
||||||
|
runner.record(
|
||||||
|
"nav_camera_pan_viewport_update",
|
||||||
|
"Camera pan_screen + build_viewports (pure GPU uniform update)",
|
||||||
|
"µs",
|
||||||
|
pan_samples,
|
||||||
|
Some((1_000_000.0 / median_pan_us, "pans/s")),
|
||||||
|
Some(50.0), // Target < 50 µs (< 0.05 ms)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Spatial Hover Candidate Search (R-tree spatial query during idle cursor sweep)
|
||||||
|
let wires = scene.entity_wires();
|
||||||
|
let (view_rot, eye) = (cam.view_proj_rte(bounds), cam.eye());
|
||||||
|
let n_hover = if runner.quick_mode { 500 } else { 2_000 };
|
||||||
|
let mut hover_samples = Vec::with_capacity(runs);
|
||||||
|
for _ in 0..runs {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
for i in 0..n_hover {
|
||||||
|
let cursor = DVec3::new((i % 200) as f64 * 10.0, ((i / 2) % 200) as f64 * 10.0, 0.0);
|
||||||
|
let candidates = scene.interaction_hover_candidates_near(
|
||||||
|
Arc::clone(&wires),
|
||||||
|
cursor,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
10.0,
|
||||||
|
);
|
||||||
|
black_box(candidates);
|
||||||
|
}
|
||||||
|
let per_us = (t0.elapsed().as_micros() as f64) / (n_hover as f64);
|
||||||
|
hover_samples.push(per_us);
|
||||||
|
}
|
||||||
|
let median_hover_us = hover_samples[hover_samples.len() / 2];
|
||||||
|
runner.record(
|
||||||
|
"nav_spatial_hover_candidate_search",
|
||||||
|
"Spatial hover candidate search (R-tree candidate culling for cursor movement)",
|
||||||
|
"µs",
|
||||||
|
hover_samples,
|
||||||
|
Some((1_000_000.0 / median_hover_us, "queries/s")),
|
||||||
|
Some(30.0), // Target < 30 µs (< 0.03 ms)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Viewport Assembly Scaling (build_viewports on active camera_generation bump)
|
||||||
|
let n_frames = if runner.quick_mode { 200 } else { 1_000 };
|
||||||
|
let mut assemble_samples = Vec::with_capacity(runs);
|
||||||
|
for _ in 0..runs {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
for _ in 0..n_frames {
|
||||||
|
scene.camera_generation += 1;
|
||||||
|
let prim = scene.build_viewports(
|
||||||
|
bounds,
|
||||||
|
codec::entities::ViewportRenderMode::Wireframe2D,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
[1.0, 1.0, 1.0, 1.0],
|
||||||
|
);
|
||||||
|
black_box(prim);
|
||||||
|
}
|
||||||
|
let per_us = (t0.elapsed().as_micros() as f64) / (n_frames as f64);
|
||||||
|
assemble_samples.push(per_us);
|
||||||
|
}
|
||||||
|
let median_assemble_us = assemble_samples[assemble_samples.len() / 2];
|
||||||
|
runner.record(
|
||||||
|
"nav_viewport_assembly_scaling",
|
||||||
|
"ViewportData assembly per camera_generation change (live navigation frame prep)",
|
||||||
|
"µs",
|
||||||
|
assemble_samples,
|
||||||
|
Some((1_000_000.0 / median_assemble_us, "frames/s")),
|
||||||
|
Some(500.0), // Target < 500 µs (0.5 ms) for 5k entities
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. QuadTree Spatial Index Build
|
||||||
|
let n_tree_items = if runner.quick_mode { 2_000 } else { 100_000 };
|
||||||
|
let tree_items: Vec<(codec::Handle, [f64; 4])> = (0..n_tree_items)
|
||||||
|
.map(|i| {
|
||||||
|
// Clustered distribution: 80% of entities share tight bounding clusters
|
||||||
|
let cluster = (i % 10) as f64 * 50.0;
|
||||||
|
let offset = (i as f64 * 0.05) % 10.0;
|
||||||
|
let x = cluster + offset;
|
||||||
|
let y = cluster + offset;
|
||||||
|
(codec::Handle::from(i as u64), [x, y, x + 5.0, y + 5.0])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let tree_bounds = [0.0, 0.0, 1000.0, 1000.0];
|
||||||
|
|
||||||
|
let mut inc_samples = Vec::with_capacity(runs);
|
||||||
|
for _ in 0..runs {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let mut tree = OpenCADStudio::scene::pick::quadtree::QuadTree::new(tree_bounds);
|
||||||
|
for &(handle, aabb) in &tree_items {
|
||||||
|
tree.insert(handle, aabb);
|
||||||
|
}
|
||||||
|
black_box(tree);
|
||||||
|
inc_samples.push(t0.elapsed().as_secs_f64() * 1000.0);
|
||||||
|
}
|
||||||
|
let median_inc_ms = inc_samples[inc_samples.len() / 2];
|
||||||
|
runner.record(
|
||||||
|
"nav_spatial_quadtree_build",
|
||||||
|
"QuadTree spatial index construction across 100k clustered entities",
|
||||||
|
"ms",
|
||||||
|
inc_samples,
|
||||||
|
Some(((n_tree_items as f64) / (median_inc_ms / 1000.0), "items/s")),
|
||||||
|
Some(100.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Main Entrypoint ─────────────────────────────────────────────────────────
|
// ── Main Entrypoint ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -2106,6 +2277,7 @@ fn main() {
|
|||||||
bench_draworder_evaluation(&mut runner);
|
bench_draworder_evaluation(&mut runner);
|
||||||
bench_undo_delta_recording(&mut runner);
|
bench_undo_delta_recording(&mut runner);
|
||||||
bench_view_render_viewport_construction(&mut runner);
|
bench_view_render_viewport_construction(&mut runner);
|
||||||
|
bench_navigation_latency_and_coalescing(&mut runner);
|
||||||
|
|
||||||
runner.finish();
|
runner.finish();
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true }
|
|||||||
# Pulled in only by the `host` feature, which adds the `opencadcodec`-typed
|
# Pulled in only by the `host` feature, which adds the `opencadcodec`-typed
|
||||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "937745d", optional = true, features = ["serde"] }
|
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", optional = true, features = ["serde"] }
|
||||||
|
|
||||||
# Runtime IPC and serialization (host feature only).
|
# Runtime IPC and serialization (host feature only).
|
||||||
interprocess = { version = "2", optional = true }
|
interprocess = { version = "2", optional = true }
|
||||||
@@ -39,7 +39,7 @@ serde_json = "1"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
cargo-lock = "11"
|
cargo-lock = "11"
|
||||||
# opencadcodec is scanned at build time to generate the embedded type registry.
|
# opencadcodec is scanned at build time to generate the embedded type registry.
|
||||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "937745d", features = ["serde"] }
|
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "937745d", features = ["serde"] }
|
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] }
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|||||||
@@ -227,6 +227,14 @@ unconsumed, and the command closed rather than left armed for a second line. Geo
|
|||||||
|
|
||||||
`ocs_capture` defaults to the drawing viewport and a longest edge of 1600 pixels. Set `scope` to `window` for the full interface or change `max_dimension` between 256 and 4096.
|
`ocs_capture` defaults to the drawing viewport and a longest edge of 1600 pixels. Set `scope` to `window` for the full interface or change `max_dimension` between 256 and 4096.
|
||||||
|
|
||||||
|
With `diff: true`, an unchanged comparison returns metadata only, including
|
||||||
|
`diff.changed: false`, the hash and resource URI; it does not resend image or
|
||||||
|
resource content blocks, regardless of `delivery`. The initial baseline (also
|
||||||
|
after `reset_diff_baseline: true`) still uses the requested image delivery.
|
||||||
|
Changed captures retain the requested diff mode, including cropped patches.
|
||||||
|
Use `diff: false` or read the returned resource URI to retrieve the full image
|
||||||
|
when an unchanged comparison omitted it.
|
||||||
|
|
||||||
Clients using MCP 2026-07-28 can advertise `io.modelcontextprotocol/tasks`. OCS then returns a standard task handle when an operation is still running and accepts `tasks/get`, `tasks/update`, and `tasks/cancel`. Other clients continue to receive the existing operation status and can read it with `ocs_read`.
|
Clients using MCP 2026-07-28 can advertise `io.modelcontextprotocol/tasks`. OCS then returns a standard task handle when an operation is still running and accepts `tasks/get`, `tasks/update`, and `tasks/cancel`. Other clients continue to receive the existing operation status and can read it with `ocs_read`.
|
||||||
|
|
||||||
The source-tree protocol smoke test treats the executable as a black box and uses only the Python standard library. It checks both supported protocol styles, the published schemas, structured errors, and the four-tool surface:
|
The source-tree protocol smoke test treats the executable as a black box and uses only the Python standard library. It checks both supported protocol styles, the published schemas, structured errors, and the four-tool surface:
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Bounded stdio regression probe; point this at the packaged launcher on macOS."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import queue
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
def probe(command: list[str], timeout: float = 10) -> None:
|
||||||
|
# A separate process group lets a failed probe clean up a broken launcher
|
||||||
|
# and any GUI child it incorrectly spawned. Never touch existing editors.
|
||||||
|
with tempfile.TemporaryFile() as stderr:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[*command, "--mcp"],
|
||||||
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=stderr,
|
||||||
|
text=True, encoding="utf-8", start_new_session=os.name == "posix",
|
||||||
|
env={**os.environ, "OCS_SKIP_SCHEMA_SYNC": "1"},
|
||||||
|
)
|
||||||
|
lines = queue.Queue()
|
||||||
|
|
||||||
|
def read_stdout() -> None:
|
||||||
|
try:
|
||||||
|
for line in process.stdout:
|
||||||
|
lines.put(line)
|
||||||
|
finally:
|
||||||
|
lines.put(None)
|
||||||
|
|
||||||
|
threading.Thread(target=read_stdout, daemon=True).start()
|
||||||
|
|
||||||
|
def request(request_id: int, method: str, params: dict) -> dict:
|
||||||
|
process.stdin.write(json.dumps({
|
||||||
|
"jsonrpc": "2.0", "id": request_id,
|
||||||
|
"method": method, "params": params,
|
||||||
|
}) + "\n")
|
||||||
|
process.stdin.flush()
|
||||||
|
# stdin stays open while waiting: a server buffering until EOF
|
||||||
|
# must fail this test just like the launcher that ignores --mcp.
|
||||||
|
try:
|
||||||
|
line = lines.get(timeout=timeout)
|
||||||
|
except queue.Empty as error:
|
||||||
|
raise AssertionError(f"Timed out waiting for {method}") from error
|
||||||
|
assert line is not None, f"stdout closed before {method} responded"
|
||||||
|
response = json.loads(line)
|
||||||
|
assert response["jsonrpc"] == "2.0", response
|
||||||
|
assert response["id"] == request_id, response
|
||||||
|
assert "error" not in response, response
|
||||||
|
return response["result"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
initialized = request(1, "initialize", {
|
||||||
|
"protocolVersion": "2025-11-25", "capabilities": {},
|
||||||
|
"clientInfo": {"name": "packaged-handshake", "version": "1"},
|
||||||
|
})
|
||||||
|
assert initialized["protocolVersion"] == "2025-11-25", initialized
|
||||||
|
assert initialized["serverInfo"]["name"] == "OpenCADStudio", initialized
|
||||||
|
process.stdin.write(
|
||||||
|
'{"jsonrpc":"2.0","method":"notifications/initialized"}\n'
|
||||||
|
)
|
||||||
|
process.stdin.flush()
|
||||||
|
tools = request(2, "tools/list", {})
|
||||||
|
assert {tool["name"] for tool in tools["tools"]} == {
|
||||||
|
"ocs_sessions", "ocs_read", "ocs_execute", "ocs_capture",
|
||||||
|
}, tools
|
||||||
|
request(3, "ping", {})
|
||||||
|
process.stdin.close()
|
||||||
|
assert process.wait(timeout=timeout) == 0, process.returncode
|
||||||
|
finally:
|
||||||
|
if os.name == "posix":
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
elif process.poll() is None:
|
||||||
|
process.kill()
|
||||||
|
process.wait(timeout=5)
|
||||||
|
process.stdin.close()
|
||||||
|
process.stdout.close()
|
||||||
|
stderr.seek(0)
|
||||||
|
diagnostics = stderr.read().decode("utf-8", errors="replace")
|
||||||
|
if diagnostics:
|
||||||
|
print(diagnostics, file=sys.stderr, end="")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
probe([str(Path(sys.argv[1]).resolve())])
|
||||||
|
print("MCP initialize, tools/list, ping and EOF shutdown passed")
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="شعار Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="شعار Open CAD Studio"></p>
|
||||||
@@ -91,9 +91,9 @@ chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
|||||||
|
|
||||||
## اللغات
|
## اللغات
|
||||||
|
|
||||||
يمكن لـ Open CAD Studio اتباع لغة النظام أو استخدام إحدى لغات الواجهة التالية البالغ عددها 21 لغة:
|
يمكن لـ Open CAD Studio اتباع لغة النظام أو استخدام إحدى لغات الواجهة التالية البالغ عددها 22 لغة:
|
||||||
|
|
||||||
> العربية · البرتغالية البرازيلية · البلغارية · التشيكية · الهولندية · الإنجليزية · الفنلندية · الفرنسية · الألمانية · اليونانية · الهندية · المجرية · الإيطالية · اليابانية · الكورية · البولندية · الروسية · الصينية المبسطة · الإسبانية · الصينية التقليدية · التركية
|
> العربية · البرتغالية البرازيلية · البلغارية · التشيكية · الهولندية · الإنجليزية · الفنلندية · الفرنسية · الألمانية · اليونانية · الهندية · المجرية · الإيطالية · اليابانية · الكورية · اللاوية · البولندية · الروسية · الصينية المبسطة · الإسبانية · الصينية التقليدية · التركية
|
||||||
|
|
||||||
غيّر اللغة من إعدادات التطبيق. عند اختيار **النظام**، يستخدم إصدار المتصفح أيضاً الإعدادات المحلية المفضلة للمتصفح.
|
غيّر اللغة من إعدادات التطبيق. عند اختيار **النظام**، يستخدم إصدار المتصفح أيضاً الإعدادات المحلية المفضلة للمتصفح.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Лого на Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Лого на Open CAD Studio"></p>
|
||||||
@@ -93,9 +93,9 @@ chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
|||||||
|
|
||||||
## Езици
|
## Езици
|
||||||
|
|
||||||
Open CAD Studio може да следва системния език или да използва един от следните 21 езика на интерфейса:
|
Open CAD Studio може да следва системния език или да използва един от следните 22 езика на интерфейса:
|
||||||
|
|
||||||
> Арабски · Бразилски португалски · Български · Чешки · Нидерландски · Английски · Фински · Френски · Немски · Гръцки · Хинди · Унгарски · Италиански · Японски · Корейски · Полски · Руски · Опростен китайски · Испански · Традиционен китайски · Турски
|
> Арабски · Бразилски португалски · Български · Чешки · Нидерландски · Английски · Фински · Френски · Немски · Гръцки · Хинди · Унгарски · Италиански · Японски · Корейски · Лаоски · Полски · Руски · Опростен китайски · Испански · Традиционен китайски · Турски
|
||||||
|
|
||||||
Променете езика от настройките на приложението. Когато е избрано **Системен език**, браузърната версия също използва предпочитания локал на браузъра.
|
Променете езика от настройките на приложението. Когато е избрано **Системен език**, браузърната версия също използва предпочитания локал на браузъра.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo Open CAD Studio"></p>
|
||||||
@@ -91,9 +91,9 @@ Aplikace je podepsána ad hoc, ale v současnosti není notářsky ověřena spo
|
|||||||
|
|
||||||
## Jazyky
|
## Jazyky
|
||||||
|
|
||||||
Open CAD Studio může používat jazyk systému nebo jeden z těchto 21 jazyků rozhraní:
|
Open CAD Studio může používat jazyk systému nebo jeden z těchto 22 jazyků rozhraní:
|
||||||
|
|
||||||
> Arabština · Brazilská portugalština · Bulharština · Čeština · Nizozemština · Angličtina · Finština · Francouzština · Němčina · Řečtina · Hindština · Maďarština · Italština · Japonština · Korejština · Polština · Ruština · Zjednodušená čínština · Španělština · Tradiční čínština · Turečtina
|
> Arabština · Brazilská portugalština · Bulharština · Čeština · Nizozemština · Angličtina · Finština · Francouzština · Němčina · Řečtina · Hindština · Maďarština · Italština · Japonština · Korejština · Laoština · Polština · Ruština · Zjednodušená čínština · Španělština · Tradiční čínština · Turečtina
|
||||||
|
|
||||||
Jazyk změníte v nastavení aplikace. Pokud je vybrána možnost **Systém**, webová verze používá také preferované národní prostředí prohlížeče.
|
Jazyk změníte v nastavení aplikace. Pokud je vybrána možnost **Systém**, webová verze používá také preferované národní prostředí prohlížeče.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open-CAD-Studio-Logo"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open-CAD-Studio-Logo"></p>
|
||||||
@@ -91,9 +91,9 @@ Die Anwendung ist ad hoc signiert, wird derzeit aber nicht von Apple notarisiert
|
|||||||
|
|
||||||
## Sprachen
|
## Sprachen
|
||||||
|
|
||||||
Open CAD Studio kann der Systemsprache folgen oder eine dieser 21 Oberflächensprachen verwenden:
|
Open CAD Studio kann der Systemsprache folgen oder eine dieser 22 Oberflächensprachen verwenden:
|
||||||
|
|
||||||
> Arabisch · Brasilianisches Portugiesisch · Bulgarisch · Tschechisch · Niederländisch · Englisch · Finnisch · Französisch · Deutsch · Griechisch · Hindi · Ungarisch · Italienisch · Japanisch · Koreanisch · Polnisch · Russisch · Vereinfachtes Chinesisch · Spanisch · Traditionelles Chinesisch · Türkisch
|
> Arabisch · Brasilianisches Portugiesisch · Bulgarisch · Tschechisch · Niederländisch · Englisch · Finnisch · Französisch · Deutsch · Griechisch · Hindi · Ungarisch · Italienisch · Japanisch · Koreanisch · Laotisch · Polnisch · Russisch · Vereinfachtes Chinesisch · Spanisch · Traditionelles Chinesisch · Türkisch
|
||||||
|
|
||||||
Die Sprache lässt sich in den Anwendungseinstellungen ändern. Die Browserversion verwendet bei Auswahl von **System** ebenfalls das bevorzugte Gebietsschema des Browsers.
|
Die Sprache lässt sich in den Anwendungseinstellungen ändern. Die Browserversion verwendet bei Auswahl von **System** ebenfalls das bevorzugte Gebietsschema des Browsers.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Λογότυπο του Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Λογότυπο του Open CAD Studio"></p>
|
||||||
@@ -93,9 +93,9 @@ chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
|||||||
|
|
||||||
## Γλώσσες
|
## Γλώσσες
|
||||||
|
|
||||||
Το Open CAD Studio μπορεί να ακολουθεί τη γλώσσα του συστήματος ή να χρησιμοποιεί μία από τις ακόλουθες 21 γλώσσες περιβάλλοντος:
|
Το Open CAD Studio μπορεί να ακολουθεί τη γλώσσα του συστήματος ή να χρησιμοποιεί μία από τις ακόλουθες 22 γλώσσες περιβάλλοντος:
|
||||||
|
|
||||||
> Αραβικά · Πορτογαλικά Βραζιλίας · Βουλγαρικά · Τσεχικά · Ολλανδικά · Αγγλικά · Φινλανδικά · Γαλλικά · Γερμανικά · Ελληνικά · Χίντι · Ουγγρικά · Ιταλικά · Ιαπωνικά · Κορεατικά · Πολωνικά · Ρωσικά · Απλοποιημένα κινεζικά · Ισπανικά · Παραδοσιακά κινεζικά · Τουρκικά
|
> Αραβικά · Πορτογαλικά Βραζιλίας · Βουλγαρικά · Τσεχικά · Ολλανδικά · Αγγλικά · Φινλανδικά · Γαλλικά · Γερμανικά · Ελληνικά · Χίντι · Ουγγρικά · Ιταλικά · Ιαπωνικά · Κορεατικά · Λαοτινά · Πολωνικά · Ρωσικά · Απλοποιημένα κινεζικά · Ισπανικά · Παραδοσιακά κινεζικά · Τουρκικά
|
||||||
|
|
||||||
Αλλάξτε τη γλώσσα από τις ρυθμίσεις της εφαρμογής. Όταν είναι επιλεγμένη η **Γλώσσα συστήματος**, η διαδικτυακή έκδοση χρησιμοποιεί επίσης την προτιμώμενη γλώσσα του προγράμματος περιήγησης.
|
Αλλάξτε τη γλώσσα από τις ρυθμίσεις της εφαρμογής. Όταν είναι επιλεγμένη η **Γλώσσα συστήματος**, η διαδικτυακή έκδοση χρησιμοποιεί επίσης την προτιμώμενη γλώσσα του προγράμματος περιήγησης.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logotipo de Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logotipo de Open CAD Studio"></p>
|
||||||
@@ -91,9 +91,9 @@ La aplicación tiene firma ad hoc, pero actualmente no está notarizada por Appl
|
|||||||
|
|
||||||
## Idiomas
|
## Idiomas
|
||||||
|
|
||||||
Open CAD Studio puede seguir el idioma del sistema o usar cualquiera de estos 21 idiomas de interfaz:
|
Open CAD Studio puede seguir el idioma del sistema o usar cualquiera de estos 22 idiomas de interfaz:
|
||||||
|
|
||||||
> Árabe · Portugués de Brasil · Búlgaro · Checo · Neerlandés · Inglés · Finés · Francés · Alemán · Griego · Hindi · Húngaro · Italiano · Japonés · Coreano · Polaco · Ruso · Chino simplificado · Español · Chino tradicional · Turco
|
> Árabe · Portugués de Brasil · Búlgaro · Checo · Neerlandés · Inglés · Finés · Francés · Alemán · Griego · Hindi · Húngaro · Italiano · Japonés · Coreano · Lao · Polaco · Ruso · Chino simplificado · Español · Chino tradicional · Turco
|
||||||
|
|
||||||
Cambia el idioma en los ajustes de la aplicación. La versión web también usa la configuración regional preferida del navegador cuando se selecciona **Sistema**.
|
Cambia el idioma en los ajustes de la aplicación. La versión web también usa la configuración regional preferida del navegador cuando se selecciona **Sistema**.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studion logo"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studion logo"></p>
|
||||||
@@ -91,9 +91,9 @@ Sovellus on allekirjoitettu ad hoc -allekirjoituksella, mutta Apple ei ole täll
|
|||||||
|
|
||||||
## Kielet
|
## Kielet
|
||||||
|
|
||||||
Open CAD Studio voi seurata järjestelmän kieltä tai käyttää jotakin näistä 21 käyttöliittymäkielestä:
|
Open CAD Studio voi seurata järjestelmän kieltä tai käyttää jotakin näistä 22 käyttöliittymäkielestä:
|
||||||
|
|
||||||
> Arabia · Brasilianportugali · Bulgaria · Tšekki · Hollanti · Englanti · Suomi · Ranska · Saksa · Kreikka · Hindi · Unkari · Italia · Japani · Korea · Puola · Venäjä · Yksinkertaistettu kiina · Espanja · Perinteinen kiina · Turkki
|
> Arabia · Brasilianportugali · Bulgaria · Tšekki · Hollanti · Englanti · Suomi · Ranska · Saksa · Kreikka · Hindi · Unkari · Italia · Japani · Korea · Lao · Puola · Venäjä · Yksinkertaistettu kiina · Espanja · Perinteinen kiina · Turkki
|
||||||
|
|
||||||
Vaihda kieli sovelluksen asetuksissa. Selainversio käyttää myös selaimen ensisijaista kielialuetta, kun **Järjestelmä** on valittu.
|
Vaihda kieli sovelluksen asetuksissa. Selainversio käyttää myös selaimen ensisijaista kielialuetta, kun **Järjestelmä** on valittu.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo Open CAD Studio"></p>
|
||||||
@@ -91,9 +91,9 @@ L’application est signée de façon ad hoc, mais n’est actuellement pas nota
|
|||||||
|
|
||||||
## Langues
|
## Langues
|
||||||
|
|
||||||
Open CAD Studio peut suivre la langue du système ou utiliser l’une de ces 21 langues d’interface :
|
Open CAD Studio peut suivre la langue du système ou utiliser l’une de ces 22 langues d’interface :
|
||||||
|
|
||||||
> Arabe · Portugais du Brésil · Bulgare · Tchèque · Néerlandais · Anglais · Finnois · Français · Allemand · Grec · Hindi · Hongrois · Italien · Japonais · Coréen · Polonais · Russe · Chinois simplifié · Espagnol · Chinois traditionnel · Turc
|
> Arabe · Portugais du Brésil · Bulgare · Tchèque · Néerlandais · Anglais · Finnois · Français · Allemand · Grec · Hindi · Hongrois · Italien · Japonais · Coréen · Lao · Polonais · Russe · Chinois simplifié · Espagnol · Chinois traditionnel · Turc
|
||||||
|
|
||||||
Changez la langue dans les paramètres de l’application. La version web utilise aussi la langue préférée du navigateur lorsque **Système** est sélectionné.
|
Changez la langue dans les paramètres de l’application. La version web utilise aussi la langue préférée du navigateur lorsque **Système** est sélectionné.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio लोगो"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio लोगो"></p>
|
||||||
@@ -91,9 +91,9 @@ chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
|||||||
|
|
||||||
## भाषाएँ
|
## भाषाएँ
|
||||||
|
|
||||||
Open CAD Studio सिस्टम भाषा का अनुसरण कर सकता है या इन 21 इंटरफ़ेस भाषाओं में से किसी एक का उपयोग कर सकता है:
|
Open CAD Studio सिस्टम भाषा का अनुसरण कर सकता है या इन 22 इंटरफ़ेस भाषाओं में से किसी एक का उपयोग कर सकता है:
|
||||||
|
|
||||||
> अरबी · ब्राज़ीलियाई पुर्तगाली · बुल्गारियाई · चेक · डच · अंग्रेज़ी · फ़िनिश · फ़्रेंच · जर्मन · यूनानी · हिन्दी · हंगेरियन · इतालवी · जापानी · कोरियाई · पोलिश · रूसी · सरलीकृत चीनी · स्पैनिश · पारंपरिक चीनी · तुर्की
|
> अरबी · ब्राज़ीलियाई पुर्तगाली · बुल्गारियाई · चेक · डच · अंग्रेज़ी · फ़िनिश · फ़्रेंच · जर्मन · यूनानी · हिन्दी · हंगेरियन · इतालवी · जापानी · कोरियाई · लाओ · पोलिश · रूसी · सरलीकृत चीनी · स्पैनिश · पारंपरिक चीनी · तुर्की
|
||||||
|
|
||||||
अनुप्रयोग सेटिंग में भाषा बदलें। **सिस्टम** चुने जाने पर ब्राउज़र संस्करण भी ब्राउज़र की पसंदीदा लोकेल का उपयोग करता है।
|
अनुप्रयोग सेटिंग में भाषा बदलें। **सिस्टम** चुने जाने पर ब्राउज़र संस्करण भी ब्राउज़र की पसंदीदा लोकेल का उपयोग करता है।
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio logó"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio logó"></p>
|
||||||
@@ -91,9 +91,9 @@ Az alkalmazás ad hoc aláírással rendelkezik, de az Apple jelenleg nem hitele
|
|||||||
|
|
||||||
## Nyelvek
|
## Nyelvek
|
||||||
|
|
||||||
Az Open CAD Studio követheti a rendszer nyelvét, vagy használhatja az alábbi 21 felületi nyelv egyikét:
|
Az Open CAD Studio követheti a rendszer nyelvét, vagy használhatja az alábbi 22 felületi nyelv egyikét:
|
||||||
|
|
||||||
> Arab · Brazil portugál · Bolgár · Cseh · Holland · Angol · Finn · Francia · Német · Görög · Hindi · Magyar · Olasz · Japán · Koreai · Lengyel · Orosz · Egyszerűsített kínai · Spanyol · Hagyományos kínai · Török
|
> Arab · Brazil portugál · Bolgár · Cseh · Holland · Angol · Finn · Francia · Német · Görög · Hindi · Magyar · Olasz · Japán · Koreai · Lao · Lengyel · Orosz · Egyszerűsített kínai · Spanyol · Hagyományos kínai · Török
|
||||||
|
|
||||||
A nyelv az alkalmazás beállításaiban módosítható. **Rendszer** választásakor a böngészős változat is a böngésző előnyben részesített területi beállítását használja.
|
A nyelv az alkalmazás beállításaiban módosítható. **Rendszer** választásakor a böngészős változat is a böngésző előnyben részesített területi beállítását használja.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo di Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo di Open CAD Studio"></p>
|
||||||
@@ -91,9 +91,9 @@ L’applicazione è firmata ad hoc, ma attualmente non è autenticata da Apple.
|
|||||||
|
|
||||||
## Lingue
|
## Lingue
|
||||||
|
|
||||||
Open CAD Studio può seguire la lingua di sistema o usare una di queste 21 lingue dell’interfaccia:
|
Open CAD Studio può seguire la lingua di sistema o usare una di queste 22 lingue dell’interfaccia:
|
||||||
|
|
||||||
> Arabo · Portoghese brasiliano · Bulgaro · Ceco · Olandese · Inglese · Finlandese · Francese · Tedesco · Greco · Hindi · Ungherese · Italiano · Giapponese · Coreano · Polacco · Russo · Cinese semplificato · Spagnolo · Cinese tradizionale · Turco
|
> Arabo · Portoghese brasiliano · Bulgaro · Ceco · Olandese · Inglese · Finlandese · Francese · Tedesco · Greco · Hindi · Ungherese · Italiano · Giapponese · Coreano · Lao · Polacco · Russo · Cinese semplificato · Spagnolo · Cinese tradizionale · Turco
|
||||||
|
|
||||||
Cambia la lingua nelle impostazioni dell’applicazione. La versione browser usa anche la lingua preferita del browser quando è selezionato **Sistema**.
|
Cambia la lingua nelle impostazioni dell’applicazione. La versione browser usa anche la lingua preferita del browser quando è selezionato **Sistema**.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio ロゴ"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio ロゴ"></p>
|
||||||
@@ -91,9 +91,9 @@ chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
|||||||
|
|
||||||
## 言語
|
## 言語
|
||||||
|
|
||||||
Open CAD Studio はシステム言語に従うか、次の 21 のインターフェース言語を使用できます。
|
Open CAD Studio はシステム言語に従うか、次の 22 のインターフェース言語を使用できます。
|
||||||
|
|
||||||
> アラビア語 · ブラジルポルトガル語 · ブルガリア語 · チェコ語 · オランダ語 · 英語 · フィンランド語 · フランス語 · ドイツ語 · ギリシャ語 · ヒンディー語 · ハンガリー語 · イタリア語 · 日本語 · 韓国語 · ポーランド語 · ロシア語 · 簡体字中国語 · スペイン語 · 繁体字中国語 · トルコ語
|
> アラビア語 · ブラジルポルトガル語 · ブルガリア語 · チェコ語 · オランダ語 · 英語 · フィンランド語 · フランス語 · ドイツ語 · ギリシャ語 · ヒンディー語 · ハンガリー語 · イタリア語 · 日本語 · 韓国語 · ラオ語 · ポーランド語 · ロシア語 · 簡体字中国語 · スペイン語 · 繁体字中国語 · トルコ語
|
||||||
|
|
||||||
言語はアプリケーション設定から変更できます。**システム** を選ぶと、ブラウザ版もブラウザの優先ロケールを使用します。
|
言語はアプリケーション設定から変更できます。**システム** を選ぶと、ブラウザ版もブラウザの優先ロケールを使用します。
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio 로고"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio 로고"></p>
|
||||||
@@ -91,9 +91,9 @@ chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
|||||||
|
|
||||||
## 언어
|
## 언어
|
||||||
|
|
||||||
Open CAD Studio는 시스템 언어를 따르거나 다음 21개 인터페이스 언어 중 하나를 사용할 수 있습니다.
|
Open CAD Studio는 시스템 언어를 따르거나 다음 22개 인터페이스 언어 중 하나를 사용할 수 있습니다.
|
||||||
|
|
||||||
> 아랍어 · 브라질 포르투갈어 · 불가리아어 · 체코어 · 네덜란드어 · 영어 · 핀란드어 · 프랑스어 · 독일어 · 그리스어 · 힌디어 · 헝가리어 · 이탈리아어 · 일본어 · 한국어 · 폴란드어 · 러시아어 · 중국어 간체 · 스페인어 · 중국어 번체 · 터키어
|
> 아랍어 · 브라질 포르투갈어 · 불가리아어 · 체코어 · 네덜란드어 · 영어 · 핀란드어 · 프랑스어 · 독일어 · 그리스어 · 힌디어 · 헝가리어 · 이탈리아어 · 일본어 · 한국어 · 라오어 · 폴란드어 · 러시아어 · 중국어 간체 · 스페인어 · 중국어 번체 · 터키어
|
||||||
|
|
||||||
애플리케이션 설정에서 언어를 변경할 수 있습니다. **시스템**을 선택하면 브라우저 버전도 브라우저의 기본 로캘을 사용합니다.
|
애플리케이션 설정에서 언어를 변경할 수 있습니다. **시스템**을 선택하면 브라우저 버전도 브라우저의 기본 로캘을 사용합니다.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="ໂລໂກ້ Open CAD Studio"></p>
|
||||||
|
|
||||||
|
<h1 align="center">Open CAD Studio</h1>
|
||||||
|
|
||||||
|
<p align="center">ແອັບພລິເຄຊັນແຕ້ມແບບ 2D ແລະ ສ້າງແບບຈຳລອງ 3D ແບບໂອເພັນຊອດ ສຳລັບເດັສທັອບ ແລະ ເວັບ, ພັດທະນາດ້ວຍ Rust.</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="ລຸ້ນລ່າສຸດ" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
|
||||||
|
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="ຈຳນວນການດາວໂຫຼດ" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
|
||||||
|
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="ດາວ GitHub" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
|
||||||
|
<a href="../../LICENSE"><img alt="ລິຂະສິດ GPL-3.0" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.opencadstudio.com"><strong>ເປີດແອັບເວັບ</strong></a> ·
|
||||||
|
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>ດາວໂຫຼດແອັບເດັສທັອບ</strong></a> ·
|
||||||
|
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>ເຂົ້າຮ່ວມການສົນທະນາ</strong></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center"><img src="../../site/workspace.png" alt="ພື້ນທີ່ເຮັດວຽກ Open CAD Studio" width="100%"></p>
|
||||||
|
|
||||||
|
## ພາບລວມ
|
||||||
|
|
||||||
|
Open CAD Studio ເປັນແອັບພລິເຄຊັນຂ້າມແພລັດຟອມ ສຳລັບການແຕ້ມແບບທາງເທັກນິກ, ການຈັດໜ້າເລເອົາຕ໌ ແລະ ການສ້າງແບບຈຳລອງວັດຖຸຕັນ. ມັນອ່ານ ແລະ ຂຽນແບບແຕ້ມ DWG ແລະ DXF ໂດຍກົງ, ແລະ ລຸ້ນເດັສທັອບກັບລຸ້ນບຣາວເຊີໃຊ້ແກນການແກ້ໄຂດຽວກັນ.
|
||||||
|
|
||||||
|
ໂຄງການນີ້ກຳລັງຖືກພັດທະນາຢ່າງຕໍ່ເນື່ອງ. ກະລຸນາສຳຮອງແບບແຕ້ມທີ່ສຳຄັນໄວ້ສະເໝີ ແລະ ລາຍງານບັນຫາທີ່ສາມາດເຮັດຊ້ຳໄດ້ຜ່ານ [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
|
||||||
|
|
||||||
|
## ຈຸດເດັ່ນ
|
||||||
|
|
||||||
|
- **ເຮັດວຽກກັບແບບແຕ້ມໂດຍກົງ** — ເປີດ, ແກ້ໄຂ, ກູ້ຄືນ ແລະ ບັນທຶກໄຟລ໌ DWG ແລະ DXF ໂດຍບໍ່ຕ້ອງໃຊ້ບໍລິການແປງໄຟລ໌.
|
||||||
|
- **ການແຕ້ມ 2D ທີ່ຊັດເຈນ** — ເສັ້ນ, ໂພລີລາຍ, ເສັ້ນໂຄ້ງ, ເສັ້ນສະພລາຍ, ລວດລາຍແຮເງົາ, ສະແນັບວັດຖຸ, ການຕິດຕາມ, ເລເຢີ, ບລັອກ ແລະ ການອ້າງອີງພາຍນອກ.
|
||||||
|
- **ເຄື່ອງມືເຮັດເອກະສານ** — ຂໍ້ຄວາມ, ຂະໜາດ, ເສັ້ນຊີ້, ຄ່າຄວາມຄາດເຄື່ອນ, ຕາຕະລາງ, ພື້ນທີ່ໂມເດວ, ພື້ນທີ່ເຈ້ຍ, ວິວພອດ ແລະ ຮູບແບບການພລັອດ.
|
||||||
|
- **ການສ້າງແບບຈຳລອງ 3D ດ້ວຍເຄີເນວ** — ຮູບຊົງຕັນພື້ນຖານ, ດຶງຢືດ (Extrude), ໝູນກວາດ (Revolve), ກວາດຕາມເສັ້ນ (Sweep), ລັອບ (Loft), ການດຳເນີນການ Boolean ແລະ ການແບ່ງພື້ນຜິວວັດຖຸ ACIS.
|
||||||
|
- **ການສະແດງຜົນດ້ວຍ GPU** — ວິວພອດ 2D ແລະ 3D ທີ່ເລັ່ງດ້ວຍ `wgpu` ພ້ອມກ້ອງແບບອໍໂທກຣາຟິກ ແລະ ແບບເພີສເປັກທີບ.
|
||||||
|
- **ຂະຫຍາຍການເຮັດວຽກໄດ້** — ປລັກອິນພາຍໃນເຄື່ອງ, ສະຄຣິບຄຳສັ່ງ, ການແປງໄຟລ໌ແບບບໍ່ມີໜ້າຕ່າງ ແລະ API ອັດຕະໂນມັດແບບ JSON ຕໍ່ແຖວ.
|
||||||
|
|
||||||
|
<p align="center"><img src="../../site/modeling.png" alt="ແບບຈຳລອງ 3D ໃນ Open CAD Studio" width="100%"></p>
|
||||||
|
|
||||||
|
## ການເຮັດວຽກກັບໄຟລ໌
|
||||||
|
|
||||||
|
| ຮູບແບບ ຫຼື ການເຮັດວຽກ | ການຮອງຮັບ |
|
||||||
|
| --- | --- |
|
||||||
|
| DWG | ອ່ານ ແລະ ຂຽນ; ບັນທຶກເປັນລຸ້ນ R14 ຫາ 2018 |
|
||||||
|
| DXF | ອ່ານ ແລະ ຂຽນ; ບັນທຶກເປັນລຸ້ນ R14 ຫາ 2018 |
|
||||||
|
| BAK / SV$ | ເປີດໄຟລ໌ສຳຮອງ ແລະ ໄຟລ໌ບັນທຶກອັດຕະໂນມັດ |
|
||||||
|
| OBJ | ນຳເຂົ້າຮູບຕາໜ່າງຫຼາຍລ່ຽມ |
|
||||||
|
| LandXML | ນຳເຂົ້າຈຸດສຳຫຼວດ `CgPoint` |
|
||||||
|
| STL | ສົ່ງອອກຂໍ້ມູນຮູບຕາໜ່າງ 3D |
|
||||||
|
| STEP AP203 | ສົ່ງອອກຂໍ້ມູນຮູບຕາໜ່າງ 3D |
|
||||||
|
| PDF | ພລັອດເລເອົາຕ໌ ແລະ ຮູບຊົງທີ່ເລືອກໃນລຸ້ນເດັສທັອບ |
|
||||||
|
| CSV | ດຶງຂໍ້ມູນຄຸນສົມບັດຂອງວັດຖຸ |
|
||||||
|
| CTB / STB | ໂຫຼດ ແລະ ແກ້ໄຂຕາຕະລາງຮູບແບບການພລັອດ |
|
||||||
|
|
||||||
|
## ເດັສທັອບ ຫຼື ເວັບ?
|
||||||
|
|
||||||
|
ໃຊ້ [ແອັບເວັບ](https://www.opencadstudio.com) ເພື່ອເລີ່ມໃຊ້ທັນທີໂດຍບໍ່ຕ້ອງຕິດຕັ້ງ. ແບບແຕ້ມຈະຖືກເລືອກຜ່ານບຣາວເຊີ ແລະ ບັນທຶກເປັນໄຟລ໌ດາວໂຫຼດໃນເຄື່ອງ.
|
||||||
|
|
||||||
|
ໃຊ້ແອັບເດັສທັອບສຳລັບການຜູກໄຟລ໌ກັບລະບົບ, ຮູບຕົວຢ່າງໃນຕົວຈັດການໄຟລ໌, ການພິມຂອງລະບົບ, ການສົ່ງອອກ PDF, ປລັກອິນພາຍນອກ, ສະຄຣິບຄຳສັ່ງ ແລະ ການເຮັດວຽກອັດຕະໂນມັດແບບບໍ່ມີໜ້າຕ່າງ. ມີລຸ້ນສຳລັບ Windows, Linux ແລະ macOS ທີ່ໃຊ້ Apple Silicon.
|
||||||
|
|
||||||
|
## ການຕິດຕັ້ງ
|
||||||
|
|
||||||
|
ດາວໂຫຼດແພັກເກດທັງໝົດທີ່ໃໝ່ສຸດຈາກ [ລຸ້ນລ່າສຸດ](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
ເລືອກແພັກເກດ x86-64 ທີ່ມີລາຍເຊັນອັນໃດອັນໜຶ່ງ:
|
||||||
|
|
||||||
|
- `OpenCADStudio-*-windows-x86_64-installer.msi` — ຕົວຕິດຕັ້ງທີ່ແນະນຳ ພ້ອມທາງລັດໃນເມນູ Start, ການຜູກໄຟລ໌ DWG/DXF ແລະ ຮູບຕົວຢ່າງແບບແຕ້ມ.
|
||||||
|
- `OpenCADStudio-*-windows-x86_64-portable.exe` — ແອັບແບບພົກພາທີ່ບໍ່ຕ້ອງຕິດຕັ້ງ.
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
ດາວໂຫຼດໄຟລ໌ AppImage x86-64, ເຮັດໃຫ້ມັນສາມາດເອີ້ນໃຊ້ໄດ້ ແລະ ເປີດມັນ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
||||||
|
./OpenCADStudio-*-linux-x86_64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
ແພັກເກດ macOS ທີ່ເຜີຍແຜ່ຮອງຮັບ Apple Silicon:
|
||||||
|
|
||||||
|
1. ດາວໂຫຼດ `OpenCADStudio-*-macos-arm64.dmg`.
|
||||||
|
2. ເປີດດິສອິມເມດ ແລະ ລາກ `OpenCADStudio.app` ໄປໃສ່ໂຟນເດີ **Applications**.
|
||||||
|
3. ຖ້າ Gatekeeper ບລັອກການເປີດຄັ້ງທຳອິດ, ໃຫ້ອະນຸມັດແອັບໃນ **System Settings → Privacy & Security**.
|
||||||
|
|
||||||
|
ແອັບມີລາຍເຊັນແບບຊົ່ວຄາວ ແຕ່ປະຈຸບັນຍັງບໍ່ໄດ້ຮັບການຮັບຮອງ (notarize) ຈາກ Apple.
|
||||||
|
|
||||||
|
## ພາສາ
|
||||||
|
|
||||||
|
Open CAD Studio ສາມາດໃຊ້ພາສາຂອງລະບົບ ຫຼື ໃຊ້ພາສາໜ້າຕ່າງໃດໜຶ່ງໃນ 22 ພາສາຕໍ່ໄປນີ້:
|
||||||
|
|
||||||
|
> ອາຣັບ · ປອກຕຸຍການ (ບຣາຊິວ) · ບັນແກເຣຍ · ເຊັກ · ໂຮນລັງ · ອັງກິດ · ຟິນແລນ · ຝຣັ່ງ · ເຢຍລະມັນ · ກຣີກ · ຮິນດີ · ຮັງກາຣີ · ອີຕາລີ · ຍີ່ປຸ່ນ · ເກົາຫຼີ · ລາວ · ໂປແລນ · ຣັດເຊຍ · ຈີນຕົວຫຍໍ້ · ສະເປນ · ຈີນຕົວເຕັມ · ຕວກກີ
|
||||||
|
|
||||||
|
ປ່ຽນພາສາໄດ້ໃນການຕັ້ງຄ່າຂອງແອັບ. ເມື່ອເລືອກ **ລະບົບ**, ລຸ້ນບຣາວເຊີຈະໃຊ້ພາສາທີ່ບຣາວເຊີຕັ້ງໄວ້.
|
||||||
|
|
||||||
|
## ການສ້າງຈາກໂຄດຕົ້ນສະບັບ
|
||||||
|
|
||||||
|
### ເດັສທັອບ
|
||||||
|
|
||||||
|
ສິ່ງທີ່ຕ້ອງມີ:
|
||||||
|
|
||||||
|
- Git
|
||||||
|
- ຊຸດເຄື່ອງມື Rust ລຸ້ນສະຖຽນລ່າສຸດ
|
||||||
|
- ໄລບຣາຣີພັດທະນາດ້ານກຣາຟິກ ແລະ ຟອນຂອງແຕ່ລະແພລັດຟອມ
|
||||||
|
|
||||||
|
ໃນ Ubuntu ຫຼື Debian, ໃຫ້ຕິດຕັ້ງສ່ວນປະກອບທີ່ຈຳເປັນ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
|
||||||
|
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
|
||||||
|
libfreetype6-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
ຈາກນັ້ນສ້າງໂປຣແກຣມ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/HakanSeven12/OpenCADStudio.git
|
||||||
|
cd OpenCADStudio
|
||||||
|
cargo build --release --bin OpenCADStudio
|
||||||
|
```
|
||||||
|
|
||||||
|
ໄຟລ໌ໂປຣແກຣມຈະຖືກຂຽນໄວ້ທີ່ `target/release/OpenCADStudio` (ໃນ Windows ແມ່ນ `OpenCADStudio.exe`).
|
||||||
|
|
||||||
|
### ເວັບ
|
||||||
|
|
||||||
|
ຕິດຕັ້ງເປົ້າໝາຍ WebAssembly ແລະ ເຄື່ອງມືສ້າງໂປຣແກຣມຄັ້ງດຽວ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rustup target add wasm32-unknown-unknown
|
||||||
|
cargo install trunk wasm-bindgen-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
ເລີ່ມເຊີບເວີພັດທະນາ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
trunk serve
|
||||||
|
```
|
||||||
|
|
||||||
|
## ການເຮັດວຽກອັດຕະໂນມັດ
|
||||||
|
|
||||||
|
ໂປຣແກຣມເດັສທັອບຮອງຮັບການແປງໄຟລ໌ຄັ້ງດຽວ ແລະ ເຊີບເວີແບບບໍ່ມີໜ້າຕ່າງທີ່ເຮັດວຽກຕໍ່ເນື່ອງ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OpenCADStudio --export input.dwg output.dxf
|
||||||
|
OpenCADStudio --serve
|
||||||
|
OpenCADStudio --serve --port 4242
|
||||||
|
OpenCADStudio --mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
ເຊີບເວີແລກປ່ຽນວັດຖຸ JSON ໜຶ່ງອັນຕໍ່ແຖວຜ່ານ stdin/stdout ຫຼື TCP socket ພາຍໃນເຄື່ອງ. ເບິ່ງ [ຄູ່ມືການເຮັດວຽກອັດຕະໂນມັດ](../automation/README.md).
|
||||||
|
|
||||||
|
## ປລັກອິນ
|
||||||
|
|
||||||
|
ປລັກອິນເດັສທັອບເຮັດວຽກໃນໂປຣເຊດແຍກຕ່າງຫາກ ແລະ ສື່ສານກັບແອັບຫຼັກຜ່ານ API ປລັກອິນທີ່ມີລຸ້ນ. ລຸ້ນບຣາວເຊີບໍ່ໂຫຼດປລັກອິນພາຍໃນເຄື່ອງ.
|
||||||
|
|
||||||
|
- [ສະຖາປັດຕະຍະກຳປລັກອິນ](../plugin-architecture.md)
|
||||||
|
- [ແມ່ແບບປລັກອິນ](../plugin-template/README.md)
|
||||||
|
- [ທະບຽນປລັກອິນ](../../plugins/README.md)
|
||||||
|
|
||||||
|
## ເອກະສານໂຄງການ
|
||||||
|
|
||||||
|
- [API ການເຮັດວຽກອັດຕະໂນມັດ](../automation/README.md)
|
||||||
|
- [ສະຖາປັດຕະຍະກຳປລັກອິນ](../plugin-architecture.md)
|
||||||
|
- [ຂັ້ນຕອນການແບ່ງພື້ນຜິວ (Tessellation)](../tessellation.md)
|
||||||
|
- [ນະໂຍບາຍຄວາມປອດໄພ](../../SECURITY.md)
|
||||||
|
|
||||||
|
## ການມີສ່ວນຮ່ວມ
|
||||||
|
|
||||||
|
ຍິນດີຮັບລາຍງານຂໍ້ຜິດພາດ, pull request ທີ່ເນັ້ນຈຸດດຽວ, ການແປພາສາ, ການປັບປຸງເອກະສານ ແລະ ປລັກອິນ.
|
||||||
|
|
||||||
|
- ຄົ້ນຫາໃນ [issues](https://github.com/HakanSeven12/OpenCADStudio/issues) ທີ່ມີຢູ່ກ່ອນເປີດລາຍງານໃໝ່.
|
||||||
|
- ໃຊ້ [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) ສຳລັບຄຳຖາມ ແລະ ແນວຄິດ.
|
||||||
|
- ລາຍງານຊ່ອງໂຫວ່ດ້ານຄວາມປອດໄພແບບສ່ວນຕົວ ຕາມ [ນະໂຍບາຍຄວາມປອດໄພ](../../SECURITY.md).
|
||||||
|
|
||||||
|
## ການເຕີບໂຕຂອງໂຄງການ
|
||||||
|
|
||||||
|
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
|
||||||
|
<img alt="ດາວ ແລະ ການດາວໂຫຼດຂອງ Open CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## ສະໜັບສະໜູນໂຄງການ
|
||||||
|
|
||||||
|
ຖ້າ Open CAD Studio ຊ່ວຍວຽກຂອງທ່ານ, ທ່ານສາມາດສະໜັບສະໜູນການພັດທະນາຜ່ານ [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) ຫຼື [Patreon](https://www.patreon.com/HakanSeven12).
|
||||||
|
|
||||||
|
## ລິຂະສິດ
|
||||||
|
|
||||||
|
Open CAD Studio ເຜີຍແຜ່ພາຍໃຕ້ [GNU General Public License v3.0](../../LICENSE).
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo van Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo van Open CAD Studio"></p>
|
||||||
@@ -91,9 +91,9 @@ De toepassing is ad hoc ondertekend, maar momenteel niet door Apple genotariseer
|
|||||||
|
|
||||||
## Talen
|
## Talen
|
||||||
|
|
||||||
Open CAD Studio kan de systeemtaal volgen of een van deze 21 interfacetalen gebruiken:
|
Open CAD Studio kan de systeemtaal volgen of een van deze 22 interfacetalen gebruiken:
|
||||||
|
|
||||||
> Arabisch · Braziliaans-Portugees · Bulgaars · Tsjechisch · Nederlands · Engels · Fins · Frans · Duits · Grieks · Hindi · Hongaars · Italiaans · Japans · Koreaans · Pools · Russisch · Vereenvoudigd Chinees · Spaans · Traditioneel Chinees · Turks
|
> Arabisch · Braziliaans-Portugees · Bulgaars · Tsjechisch · Nederlands · Engels · Fins · Frans · Duits · Grieks · Hindi · Hongaars · Italiaans · Japans · Koreaans · Laotiaans · Pools · Russisch · Vereenvoudigd Chinees · Spaans · Traditioneel Chinees · Turks
|
||||||
|
|
||||||
Wijzig de taal in de toepassingsinstellingen. De browserversie gebruikt ook de voorkeurstaal van de browser wanneer **Systeem** is geselecteerd.
|
Wijzig de taal in de toepassingsinstellingen. De browserversie gebruikt ook de voorkeurstaal van de browser wanneer **Systeem** is geselecteerd.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logo Open CAD Studio"></p>
|
||||||
@@ -91,9 +91,9 @@ Aplikacja jest podpisana ad hoc, ale obecnie nie jest notaryzowana przez Apple.
|
|||||||
|
|
||||||
## Języki
|
## Języki
|
||||||
|
|
||||||
Open CAD Studio może korzystać z języka systemu lub jednego z 21 języków interfejsu:
|
Open CAD Studio może korzystać z języka systemu lub jednego z 22 języków interfejsu:
|
||||||
|
|
||||||
> Arabski · Portugalski brazylijski · Bułgarski · Czeski · Niderlandzki · Angielski · Fiński · Francuski · Niemiecki · Grecki · Hindi · Węgierski · Włoski · Japoński · Koreański · Polski · Rosyjski · Chiński uproszczony · Hiszpański · Chiński tradycyjny · Turecki
|
> Arabski · Portugalski brazylijski · Bułgarski · Czeski · Niderlandzki · Angielski · Fiński · Francuski · Niemiecki · Grecki · Hindi · Węgierski · Włoski · Japoński · Koreański · Laotański · Polski · Rosyjski · Chiński uproszczony · Hiszpański · Chiński tradycyjny · Turecki
|
||||||
|
|
||||||
Język zmienisz w ustawieniach aplikacji. Gdy wybrano **System**, wersja przeglądarkowa używa także preferowanych ustawień regionalnych przeglądarki.
|
Język zmienisz w ustawieniach aplikacji. Gdy wybrano **System**, wersja przeglądarkowa używa także preferowanych ustawień regionalnych przeglądarki.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logotipo do Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Logotipo do Open CAD Studio"></p>
|
||||||
@@ -91,9 +91,9 @@ O aplicativo possui assinatura ad hoc, mas atualmente não é notarizado pela Ap
|
|||||||
|
|
||||||
## Idiomas
|
## Idiomas
|
||||||
|
|
||||||
Open CAD Studio pode seguir o idioma do sistema ou usar um destes 21 idiomas de interface:
|
Open CAD Studio pode seguir o idioma do sistema ou usar um destes 22 idiomas de interface:
|
||||||
|
|
||||||
> Árabe · Português do Brasil · Búlgaro · Tcheco · Holandês · Inglês · Finlandês · Francês · Alemão · Grego · Hindi · Húngaro · Italiano · Japonês · Coreano · Polonês · Russo · Chinês simplificado · Espanhol · Chinês tradicional · Turco
|
> Árabe · Português do Brasil · Búlgaro · Tcheco · Holandês · Inglês · Finlandês · Francês · Alemão · Grego · Hindi · Húngaro · Italiano · Japonês · Coreano · Laosiano · Polonês · Russo · Chinês simplificado · Espanhol · Chinês tradicional · Turco
|
||||||
|
|
||||||
Altere o idioma nas configurações do aplicativo. A versão web também usa a localidade preferida do navegador quando **Sistema** está selecionado.
|
Altere o idioma nas configurações do aplicativo. A versão web também usa a localidade preferida do navegador quando **Sistema** está selecionado.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Логотип Open CAD Studio"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Логотип Open CAD Studio"></p>
|
||||||
@@ -91,9 +91,9 @@ chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
|||||||
|
|
||||||
## Языки
|
## Языки
|
||||||
|
|
||||||
Open CAD Studio может следовать языку системы или использовать любой из следующих языков интерфейса (всего 21):
|
Open CAD Studio может следовать языку системы или использовать любой из следующих языков интерфейса (всего 22):
|
||||||
|
|
||||||
> Арабский · Бразильский португальский · Болгарский · Чешский · Нидерландский · Английский · Финский · Французский · Немецкий · Греческий · Хинди · Венгерский · Итальянский · Японский · Корейский · Польский · Русский · Упрощённый китайский · Испанский · Традиционный китайский · Турецкий
|
> Арабский · Бразильский португальский · Болгарский · Чешский · Нидерландский · Английский · Финский · Французский · Немецкий · Греческий · Хинди · Венгерский · Итальянский · Японский · Корейский · Лаосский · Польский · Русский · Упрощённый китайский · Испанский · Традиционный китайский · Турецкий
|
||||||
|
|
||||||
Язык меняется в настройках приложения. При выборе **Система** версия для браузера также использует предпочтительную локаль браузера.
|
Язык меняется в настройках приложения. При выборе **Система** версия для браузера также использует предпочтительную локаль браузера.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio logosu"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio logosu"></p>
|
||||||
@@ -93,9 +93,9 @@ Uygulama geçici imzaya sahiptir ancak şu anda Apple tarafından noterlenmemiş
|
|||||||
|
|
||||||
## Diller
|
## Diller
|
||||||
|
|
||||||
Open CAD Studio sistem dilini izleyebilir veya şu 21 arayüz dilinden birini kullanabilir:
|
Open CAD Studio sistem dilini izleyebilir veya şu 22 arayüz dilinden birini kullanabilir:
|
||||||
|
|
||||||
> Arapça · Brezilya Portekizcesi · Bulgarca · Çekçe · Felemenkçe · İngilizce · Fince · Fransızca · Almanca · Yunanca · Hintçe · Macarca · İtalyanca · Japonca · Korece · Lehçe · Rusça · Basitleştirilmiş Çince · İspanyolca · Geleneksel Çince · Türkçe
|
> Arapça · Brezilya Portekizcesi · Bulgarca · Çekçe · Felemenkçe · İngilizce · Fince · Fransızca · Almanca · Yunanca · Hintçe · Macarca · İtalyanca · Japonca · Korece · Laoca · Lehçe · Rusça · Basitleştirilmiş Çince · İspanyolca · Geleneksel Çince · Türkçe
|
||||||
|
|
||||||
Dili uygulama ayarlarından değiştirin. **Sistem** seçildiğinde tarayıcı sürümü de tarayıcının tercih ettiği yerel ayarı kullanır.
|
Dili uygulama ayarlarından değiştirin. **Sistem** seçildiğinde tarayıcı sürümü de tarayıcının tercih ettiği yerel ayarı kullanır.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio 标志"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio 标志"></p>
|
||||||
@@ -91,9 +91,9 @@ chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
|||||||
|
|
||||||
## 语言
|
## 语言
|
||||||
|
|
||||||
Open CAD Studio 可以跟随系统语言,也可以使用以下 21 种界面语言:
|
Open CAD Studio 可以跟随系统语言,也可以使用以下 22 种界面语言:
|
||||||
|
|
||||||
> 阿拉伯语 · 巴西葡萄牙语 · 保加利亚语 · 捷克语 · 荷兰语 · 英语 · 芬兰语 · 法语 · 德语 · 希腊语 · 印地语 · 匈牙利语 · 意大利语 · 日语 · 韩语 · 波兰语 · 俄语 · 简体中文 · 西班牙语 · 繁体中文 · 土耳其语
|
> 阿拉伯语 · 巴西葡萄牙语 · 保加利亚语 · 捷克语 · 荷兰语 · 英语 · 芬兰语 · 法语 · 德语 · 希腊语 · 印地语 · 匈牙利语 · 意大利语 · 日语 · 韩语 · 老挝语 · 波兰语 · 俄语 · 简体中文 · 西班牙语 · 繁体中文 · 土耳其语
|
||||||
|
|
||||||
可在应用设置中更改语言。选择**系统**时,浏览器版也会使用浏览器的首选区域设置。
|
可在应用设置中更改语言。选择**系统**时,浏览器版也会使用浏览器的首选区域设置。
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
<a href="../../README.md">English</a> · <a href="README.bg.md">Български</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.el.md">Ελληνικά</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.lo.md">ລາວ</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio 標誌"></p>
|
<p align="center"><img src="../../assets/logo.svg" width="112" alt="Open CAD Studio 標誌"></p>
|
||||||
@@ -91,9 +91,9 @@ chmod +x OpenCADStudio-*-linux-x86_64.AppImage
|
|||||||
|
|
||||||
## 語言
|
## 語言
|
||||||
|
|
||||||
Open CAD Studio 可以跟隨系統語言,也可以使用以下 21 種介面語言:
|
Open CAD Studio 可以跟隨系統語言,也可以使用以下 22 種介面語言:
|
||||||
|
|
||||||
> 阿拉伯文 · 巴西葡萄牙文 · 保加利亞文 · 捷克文 · 荷蘭文 · 英文 · 芬蘭文 · 法文 · 德文 · 希臘文 · 北印度文 · 匈牙利文 · 義大利文 · 日文 · 韓文 · 波蘭文 · 俄文 · 簡體中文 · 西班牙文 · 繁體中文 · 土耳其文
|
> 阿拉伯文 · 巴西葡萄牙文 · 保加利亞文 · 捷克文 · 荷蘭文 · 英文 · 芬蘭文 · 法文 · 德文 · 希臘文 · 北印度文 · 匈牙利文 · 義大利文 · 日文 · 韓文 · 寮文 · 波蘭文 · 俄文 · 簡體中文 · 西班牙文 · 繁體中文 · 土耳其文
|
||||||
|
|
||||||
可在應用程式設定中變更語言。選擇**系統**時,瀏覽器版也會使用瀏覽器的偏好地區設定。
|
可在應用程式設定中變更語言。選擇**系統**時,瀏覽器版也會使用瀏覽器的偏好地區設定。
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1449,6 +1450,8 @@ common =
|
|||||||
.user-preferences = تفضيلات المستخدم
|
.user-preferences = تفضيلات المستخدم
|
||||||
.zoom = التكبير
|
.zoom = التكبير
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = عكس التكبير بعجلة الفأرة (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = عكس التكبير بعجلة الفأرة (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = لوحة اللمس: التمرير يحرك العرض، Ctrl+تمرير أو القرص يكبّر
|
||||||
|
.interface-scale = مقياس الواجهة
|
||||||
.zoom-factor = معامل التكبير
|
.zoom-factor = معامل التكبير
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = مقدار التكبير لكل درجة من العجلة (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = مقدار التكبير لكل درجة من العجلة (ZOOMFACTOR).
|
||||||
.text-and-dimensions = النص والأبعاد
|
.text-and-dimensions = النص والأبعاد
|
||||||
@@ -2518,6 +2521,12 @@ inquiry =
|
|||||||
.id-specify-point = ID حدد نقطة:
|
.id-specify-point = ID حدد نقطة:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: لم تُحدد كيانات. حدد الكيانات أولًا.
|
.list-no-entities-selected-select-entities-firs = LIST: لم تُحدد كيانات. حدد الكيانات أولًا.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: لم تُحدد كيانات. حدد الكيانات أولًا.
|
.massprop-no-entities-selected-select-entities = MASSPROP: لم تُحدد كيانات. حدد الكيانات أولًا.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ لا يحتوي على جسم مغلق لقياسه.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ الحجم=__ocs_fmt_1__ مركز الثقل=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = عزوم القصور الذاتي X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = جداءات القصور الذاتي XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = أنصاف أقطار الدوران X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = العزوم الرئيسية حول مركز الثقل I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE حدد الكائن المراد قياسه:
|
.measure-select-object-to-measure = MEASURE حدد الكائن المراد قياسه:
|
||||||
.measure-specify-segment-length = MEASURE حدد طول المقطع:
|
.measure-specify-segment-length = MEASURE حدد طول المقطع:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM أدخل خيارًا [مسافة/نصف قطر/زاوية/مساحة]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM أدخل خيارًا [مسافة/نصف قطر/زاوية/مساحة]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1436,6 +1437,8 @@ common =
|
|||||||
.user-preferences = Потребителски предпочитания
|
.user-preferences = Потребителски предпочитания
|
||||||
.zoom = Мащабиране
|
.zoom = Мащабиране
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Обръщане на мащабирането с колелцето (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Обръщане на мащабирането с колелцето (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Тъчпад: превъртането мести изгледа, Ctrl+превъртане или щипване мащабира
|
||||||
|
.interface-scale = Мащаб на интерфейса
|
||||||
.zoom-factor = Коефициент на мащабиране
|
.zoom-factor = Коефициент на мащабиране
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Колко мащабира едно щракване на колелцето (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Колко мащабира едно щракване на колелцето (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Текст и размери
|
.text-and-dimensions = Текст и размери
|
||||||
@@ -2505,6 +2508,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Посочи точка:
|
.id-specify-point = ID Посочи точка:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: не са избрани обекти. Първо избери обекти.
|
.list-no-entities-selected-select-entities-firs = LIST: не са избрани обекти. Първо избери обекти.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: не са избрани обекти. Първо избери обекти.
|
.massprop-no-entities-selected-select-entities = MASSPROP: не са избрани обекти. Първо избери обекти.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ няма затворено тяло за измерване.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Обем=__ocs_fmt_1__ Център на тежестта=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Инерционни моменти X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Центробежни моменти XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Радиуси на инерция X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Главни моменти спрямо центъра на тежестта I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Избери обект за измерване:
|
.measure-select-object-to-measure = MEASURE Избери обект за измерване:
|
||||||
.measure-specify-segment-length = MEASURE Посочи дължина на сегмент:
|
.measure-specify-segment-length = MEASURE Посочи дължина на сегмент:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Въведи опция [Разстояние/Радиус/Ъгъл/Площ]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Въведи опция [Разстояние/Радиус/Ъгъл/Площ]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1436,6 +1437,8 @@ common =
|
|||||||
.user-preferences = Předvolby uživatele
|
.user-preferences = Předvolby uživatele
|
||||||
.zoom = Zoom
|
.zoom = Zoom
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Obrátit zoom kolečkem myši (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Obrátit zoom kolečkem myši (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: posun posouvá pohled, Ctrl+posun nebo sevření zvětšuje
|
||||||
|
.interface-scale = Měřítko rozhraní
|
||||||
.zoom-factor = Faktor zoomu
|
.zoom-factor = Faktor zoomu
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = O kolik přiblíží jeden zub kolečka (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = O kolik přiblíží jeden zub kolečka (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Text a kóty
|
.text-and-dimensions = Text a kóty
|
||||||
@@ -2505,6 +2508,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Určete bod:
|
.id-specify-point = ID Určete bod:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: nebyly vybrány žádné entity. Nejprve vyberte entity.
|
.list-no-entities-selected-select-entities-firs = LIST: nebyly vybrány žádné entity. Nejprve vyberte entity.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: nebyly vybrány žádné entity. Nejprve vyberte entity.
|
.massprop-no-entities-selected-select-entities = MASSPROP: nebyly vybrány žádné entity. Nejprve vyberte entity.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ nemá žádné uzavřené těleso k měření.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Objem=__ocs_fmt_1__ Těžiště=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Momenty setrvačnosti X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Deviační momenty XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Poloměry setrvačnosti X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Hlavní momenty k těžišti I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Vyberte objekt k měření:
|
.measure-select-object-to-measure = MEASURE Vyberte objekt k měření:
|
||||||
.measure-specify-segment-length = MEASURE Zadejte délku segmentu:
|
.measure-specify-segment-length = MEASURE Zadejte délku segmentu:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter možnost [Vzdálenost/Poloměr/Úhel/Area]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter možnost [Vzdálenost/Poloměr/Úhel/Area]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1437,6 +1438,8 @@ common =
|
|||||||
.user-preferences = Benutzereinstellungen
|
.user-preferences = Benutzereinstellungen
|
||||||
.zoom = Zoom
|
.zoom = Zoom
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Mausradzoom umkehren (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Mausradzoom umkehren (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: Scrollen verschiebt, Strg+Scrollen oder Zusammenziehen zoomt
|
||||||
|
.interface-scale = Oberflächenskalierung
|
||||||
.zoom-factor = Zoomfaktor
|
.zoom-factor = Zoomfaktor
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Wie weit eine Radrastung zoomt (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Wie weit eine Radrastung zoomt (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Text und Bemaßung
|
.text-and-dimensions = Text und Bemaßung
|
||||||
@@ -2506,6 +2509,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Anzugeben ist:
|
.id-specify-point = ID Anzugeben ist:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: keine Objekte ausgewählt.
|
.list-no-entities-selected-select-entities-firs = LIST: keine Objekte ausgewählt.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: Keine Objekte ausgewählt.
|
.massprop-no-entities-selected-select-entities = MASSPROP: Keine Objekte ausgewählt.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ hat keinen geschlossenen Körper zum Messen.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volumen=__ocs_fmt_1__ Schwerpunkt=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Trägheitsmomente X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Deviationsmomente XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Trägheitsradien X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Hauptträgheitsmomente um den Schwerpunkt I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Objekt auswählen, um zu messen:
|
.measure-select-object-to-measure = MEASURE Objekt auswählen, um zu messen:
|
||||||
.measure-specify-segment-length = MEASURE Segmentlänge angeben:
|
.measure-specify-segment-length = MEASURE Segmentlänge angeben:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Geben Sie eine Option ein [Distance/Radius/Angle/ARea]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Geben Sie eine Option ein [Distance/Radius/Angle/ARea]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1438,6 +1439,8 @@ common =
|
|||||||
.user-preferences = Προτιμήσεις χρήστη
|
.user-preferences = Προτιμήσεις χρήστη
|
||||||
.zoom = Ζουμ
|
.zoom = Ζουμ
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Αντιστροφή ζουμ με τον τροχό (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Αντιστροφή ζουμ με τον τροχό (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Επιφάνεια αφής: η κύλιση μετακινεί, Ctrl+κύλιση ή τσίμπημα κάνει ζουμ
|
||||||
|
.interface-scale = Κλίμακα διεπαφής
|
||||||
.zoom-factor = Συντελεστής ζουμ
|
.zoom-factor = Συντελεστής ζουμ
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Πόσο ζουμάρει ένα σκαλί του τροχού (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Πόσο ζουμάρει ένα σκαλί του τροχού (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Κείμενο και διαστάσεις
|
.text-and-dimensions = Κείμενο και διαστάσεις
|
||||||
@@ -2505,6 +2508,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Ορίστε σημείο:
|
.id-specify-point = ID Ορίστε σημείο:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: δεν έχουν επιλεγεί οντότητες. Επιλέξτε πρώτα οντότητες.
|
.list-no-entities-selected-select-entities-firs = LIST: δεν έχουν επιλεγεί οντότητες. Επιλέξτε πρώτα οντότητες.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: δεν έχουν επιλεγεί οντότητες. Επιλέξτε πρώτα οντότητες.
|
.massprop-no-entities-selected-select-entities = MASSPROP: δεν έχουν επιλεγεί οντότητες. Επιλέξτε πρώτα οντότητες.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ δεν έχει κλειστό σώμα για μέτρηση.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Όγκος=__ocs_fmt_1__ Κέντρο βάρους=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Ροπές αδράνειας X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Γινόμενα αδράνειας XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Ακτίνες αδράνειας X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Κύριες ροπές ως προς το κέντρο βάρους I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Επιλέξτε αντικείμενο προς μέτρηση:
|
.measure-select-object-to-measure = MEASURE Επιλέξτε αντικείμενο προς μέτρηση:
|
||||||
.measure-specify-segment-length = MEASURE Ορίστε μήκος τμήματος:
|
.measure-specify-segment-length = MEASURE Ορίστε μήκος τμήματος:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Δώστε επιλογή [Απόσταση/Ακτίνα/Γωνία/Εμβαδόν]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Δώστε επιλογή [Απόσταση/Ακτίνα/Γωνία/Εμβαδόν]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1438,6 +1439,8 @@ common =
|
|||||||
.user-preferences = User Preferences
|
.user-preferences = User Preferences
|
||||||
.zoom = Zoom
|
.zoom = Zoom
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Reverse mouse wheel zoom (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Reverse mouse wheel zoom (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: scroll pans, Ctrl+scroll or pinch zooms
|
||||||
|
.interface-scale = Interface scale
|
||||||
.zoom-factor = Zoom factor
|
.zoom-factor = Zoom factor
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = How far one wheel notch zooms (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = How far one wheel notch zooms (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Text and Dimensions
|
.text-and-dimensions = Text and Dimensions
|
||||||
@@ -2506,6 +2509,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Specify point:
|
.id-specify-point = ID Specify point:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: no entities selected. Select entities first.
|
.list-no-entities-selected-select-entities-firs = LIST: no entities selected. Select entities first.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: no entities selected. Select entities first.
|
.massprop-no-entities-selected-select-entities = MASSPROP: no entities selected. Select entities first.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ has no closed body to measure.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Centroid=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Moments of inertia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Products of inertia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Radii of gyration X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Principal moments about centroid I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Select object to measure:
|
.measure-select-object-to-measure = MEASURE Select object to measure:
|
||||||
.measure-specify-segment-length = MEASURE Specify segment length:
|
.measure-specify-segment-length = MEASURE Specify segment length:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter an option [Distance/Radius/Angle/ARea]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter an option [Distance/Radius/Angle/ARea]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1437,6 +1438,8 @@ common =
|
|||||||
.user-preferences = Preferencias de usuario
|
.user-preferences = Preferencias de usuario
|
||||||
.zoom = Zoom
|
.zoom = Zoom
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Invertir el zoom de la rueda del ratón (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Invertir el zoom de la rueda del ratón (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Panel táctil: el desplazamiento encuadra, Ctrl+desplazamiento o pellizco hace zoom
|
||||||
|
.interface-scale = Escala de la interfaz
|
||||||
.zoom-factor = Factor de zoom
|
.zoom-factor = Factor de zoom
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Cuánto amplía un paso de la rueda (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Cuánto amplía un paso de la rueda (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Texto y acotación
|
.text-and-dimensions = Texto y acotación
|
||||||
@@ -2506,6 +2509,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Especifique un punto:
|
.id-specify-point = ID Especifique un punto:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: no hay entidades seleccionadas. Seleccione primero las entidades.
|
.list-no-entities-selected-select-entities-firs = LIST: no hay entidades seleccionadas. Seleccione primero las entidades.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: no hay entidades seleccionadas. Seleccione primero las entidades.
|
.massprop-no-entities-selected-select-entities = MASSPROP: no hay entidades seleccionadas. Seleccione primero las entidades.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ no tiene un cuerpo cerrado que medir.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volumen=__ocs_fmt_1__ Centroide=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Momentos de inercia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Productos de inercia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Radios de giro X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Momentos principales respecto al centroide I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Seleccione el objeto que desea medir:
|
.measure-select-object-to-measure = MEASURE Seleccione el objeto que desea medir:
|
||||||
.measure-specify-segment-length = MEASURE Especifique la longitud del segmento:
|
.measure-specify-segment-length = MEASURE Especifique la longitud del segmento:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Introduzca una opción [Distancia/Radio/Ángulo/Área]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Introduzca una opción [Distancia/Radio/Ángulo/Área]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1436,6 +1437,8 @@ common =
|
|||||||
.user-preferences = Käyttäjäasetukset
|
.user-preferences = Käyttäjäasetukset
|
||||||
.zoom = Zoomaus
|
.zoom = Zoomaus
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Käännä hiiren rullan zoomaus (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Käännä hiiren rullan zoomaus (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Kosketuslevy: vieritys panoroi, Ctrl+vieritys tai nipistys zoomaa
|
||||||
|
.interface-scale = Käyttöliittymän skaalaus
|
||||||
.zoom-factor = Zoomauskerroin
|
.zoom-factor = Zoomauskerroin
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Kuinka paljon yksi rullan pykälä zoomaa (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Kuinka paljon yksi rullan pykälä zoomaa (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Teksti ja mitoitus
|
.text-and-dimensions = Teksti ja mitoitus
|
||||||
@@ -2505,6 +2508,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Määritä piste:
|
.id-specify-point = ID Määritä piste:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: entiteettejä ei ole valittu. Valitse ensin entiteetit.
|
.list-no-entities-selected-select-entities-firs = LIST: entiteettejä ei ole valittu. Valitse ensin entiteetit.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: entiteettejä ei ole valittu. Valitse ensin entiteetit.
|
.massprop-no-entities-selected-select-entities = MASSPROP: entiteettejä ei ole valittu. Valitse ensin entiteetit.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ ei sisällä suljettua kappaletta mitattavaksi.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Tilavuus=__ocs_fmt_1__ Painopiste=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Hitausmomentit X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Tulohitausmomentit XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Hitaussäteet X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Päähitausmomentit painopisteen suhteen I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Valitse mitattava kohde:
|
.measure-select-object-to-measure = MEASURE Valitse mitattava kohde:
|
||||||
.measure-specify-segment-length = MEASURE Määritä segmentin pituus:
|
.measure-specify-segment-length = MEASURE Määritä segmentin pituus:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter vaihtoehto [etäisyys/säde/kulma/pinta-ala]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter vaihtoehto [etäisyys/säde/kulma/pinta-ala]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1437,6 +1438,8 @@ common =
|
|||||||
.user-preferences = Préférences utilisateur
|
.user-preferences = Préférences utilisateur
|
||||||
.zoom = Zoom
|
.zoom = Zoom
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Inverser le zoom à la molette (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Inverser le zoom à la molette (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Pavé tactile : le défilement déplace, Ctrl+défilement ou pincement zoome
|
||||||
|
.interface-scale = Échelle de l'interface
|
||||||
.zoom-factor = Facteur de zoom
|
.zoom-factor = Facteur de zoom
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = De combien zoome un cran de molette (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = De combien zoome un cran de molette (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Texte et cotation
|
.text-and-dimensions = Texte et cotation
|
||||||
@@ -2506,6 +2509,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Spécifier le point:
|
.id-specify-point = ID Spécifier le point:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST : aucune entité sélectionnée. Sélectionnez d'abord les entités.
|
.list-no-entities-selected-select-entities-firs = LIST : aucune entité sélectionnée. Sélectionnez d'abord les entités.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP : aucune entité sélectionnée. Sélectionnez d'abord les entités.
|
.massprop-no-entities-selected-select-entities = MASSPROP : aucune entité sélectionnée. Sélectionnez d'abord les entités.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ n'a aucun corps fermé à mesurer.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Centre de gravité=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Moments d'inertie X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Produits d'inertie XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Rayons de giration X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Moments principaux au centre de gravité I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Sélectionnez l'objet à mesurer :
|
.measure-select-object-to-measure = MEASURE Sélectionnez l'objet à mesurer :
|
||||||
.measure-specify-segment-length = MEASURE Spécifier la longueur du segment:
|
.measure-specify-segment-length = MEASURE Spécifier la longueur du segment:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Saisissez une option [Distance/Radius/Angle/ARea] :
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Saisissez une option [Distance/Radius/Angle/ARea] :
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1425,6 +1426,8 @@ common =
|
|||||||
.user-preferences = उपयोगकर्ता प्राथमिकताएँ
|
.user-preferences = उपयोगकर्ता प्राथमिकताएँ
|
||||||
.zoom = ज़ूम
|
.zoom = ज़ूम
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = माउस व्हील ज़ूम उलटें (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = माउस व्हील ज़ूम उलटें (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = टचपैड: स्क्रॉल पैन करता है, Ctrl+स्क्रॉल या पिंच ज़ूम करता है
|
||||||
|
.interface-scale = इंटरफ़ेस स्केल
|
||||||
.zoom-factor = ज़ूम गुणक
|
.zoom-factor = ज़ूम गुणक
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = व्हील का एक पायदान कितना ज़ूम करता है (ZOOMFACTOR)।
|
.how-far-one-wheel-notch-zooms-zoomfactor = व्हील का एक पायदान कितना ज़ूम करता है (ZOOMFACTOR)।
|
||||||
.text-and-dimensions = पाठ और आयाम
|
.text-and-dimensions = पाठ और आयाम
|
||||||
@@ -2494,6 +2497,12 @@ inquiry =
|
|||||||
.id-specify-point = ID निर्दिष्ट बिंदु:
|
.id-specify-point = ID निर्दिष्ट बिंदु:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: कोई ऑब्जेक्ट नहीं चुना गया। पहले ऑब्जेक्ट चुनें।
|
.list-no-entities-selected-select-entities-firs = LIST: कोई ऑब्जेक्ट नहीं चुना गया। पहले ऑब्जेक्ट चुनें।
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: कोई ऑब्जेक्ट नहीं चुना गया। पहले ऑब्जेक्ट चुनें।
|
.massprop-no-entities-selected-select-entities = MASSPROP: कोई ऑब्जेक्ट नहीं चुना गया। पहले ऑब्जेक्ट चुनें।
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ में मापने के लिए कोई बंद पिंड नहीं है।
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ आयतन=__ocs_fmt_1__ केन्द्रक=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = जड़त्व आघूर्ण X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = जड़त्व गुणनफल XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = घूर्णन त्रिज्याएँ X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = केन्द्रक के सापेक्ष मुख्य आघूर्ण I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE ऑब्जेक्ट को मापने के लिए चुनें:
|
.measure-select-object-to-measure = MEASURE ऑब्जेक्ट को मापने के लिए चुनें:
|
||||||
.measure-specify-segment-length = MEASURE सेगमेंट की लंबाई निर्दिष्ट करें:
|
.measure-specify-segment-length = MEASURE सेगमेंट की लंबाई निर्दिष्ट करें:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM विकल्प दर्ज करें [दूरी / त्रिज्या / कोण / क्षेत्रफल]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM विकल्प दर्ज करें [दूरी / त्रिज्या / कोण / क्षेत्रफल]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1436,6 +1437,8 @@ common =
|
|||||||
.user-preferences = Felhasználói beállítások
|
.user-preferences = Felhasználói beállítások
|
||||||
.zoom = Nagyítás
|
.zoom = Nagyítás
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Egérgörgős nagyítás megfordítása (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Egérgörgős nagyítás megfordítása (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Érintőpad: a görgetés mozgat, a Ctrl+görgetés vagy csippentés nagyít
|
||||||
|
.interface-scale = Felület mérete
|
||||||
.zoom-factor = Nagyítási tényező
|
.zoom-factor = Nagyítási tényező
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Mennyit nagyít egy görgőkattanás (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Mennyit nagyít egy görgőkattanás (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Szöveg és méretezés
|
.text-and-dimensions = Szöveg és méretezés
|
||||||
@@ -2505,6 +2508,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Pont megadása:
|
.id-specify-point = ID Pont megadása:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: nincsenek kiválasztva entitások. Először válassza ki az entitásokat.
|
.list-no-entities-selected-select-entities-firs = LIST: nincsenek kiválasztva entitások. Először válassza ki az entitásokat.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: nincsenek kiválasztva entitások. Először válassza ki az entitásokat.
|
.massprop-no-entities-selected-select-entities = MASSPROP: nincsenek kiválasztva entitások. Először válassza ki az entitásokat.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ nem tartalmaz mérhető zárt testet.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Térfogat=__ocs_fmt_1__ Súlypont=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Tehetetlenségi nyomatékok X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Deviációs nyomatékok XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Tehetetlenségi sugarak X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Fő tehetetlenségi nyomatékok a súlypontra I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Válassza ki a mérendő objektumot:
|
.measure-select-object-to-measure = MEASURE Válassza ki a mérendő objektumot:
|
||||||
.measure-specify-segment-length = MEASURE Adja meg a szegmens hosszát:
|
.measure-specify-segment-length = MEASURE Adja meg a szegmens hosszát:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter egy opció [Távolság / Sugár / Szög / Terület]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter egy opció [Távolság / Sugár / Szög / Terület]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1436,6 +1437,8 @@ common =
|
|||||||
.user-preferences = Preferenze utente
|
.user-preferences = Preferenze utente
|
||||||
.zoom = Zoom
|
.zoom = Zoom
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Inverti lo zoom con la rotellina (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Inverti lo zoom con la rotellina (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: lo scorrimento sposta la vista, Ctrl+scorrimento o pizzico esegue lo zoom
|
||||||
|
.interface-scale = Scala dell'interfaccia
|
||||||
.zoom-factor = Fattore di zoom
|
.zoom-factor = Fattore di zoom
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Di quanto ingrandisce uno scatto della rotellina (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Di quanto ingrandisce uno scatto della rotellina (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Testo e quote
|
.text-and-dimensions = Testo e quote
|
||||||
@@ -2505,6 +2508,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Specificare il punto:
|
.id-specify-point = ID Specificare il punto:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: nessuna entità selezionata. Seleziona prima le entità.
|
.list-no-entities-selected-select-entities-firs = LIST: nessuna entità selezionata. Seleziona prima le entità.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: nessuna entità selezionata. Seleziona prima le entità.
|
.massprop-no-entities-selected-select-entities = MASSPROP: nessuna entità selezionata. Seleziona prima le entità.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ non ha un corpo chiuso da misurare.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Baricentro=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Momenti d'inerzia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Prodotti d'inerzia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Raggi di girazione X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Momenti principali rispetto al baricentro I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Selezionare l'oggetto da misurare:
|
.measure-select-object-to-measure = MEASURE Selezionare l'oggetto da misurare:
|
||||||
.measure-specify-segment-length = MEASURE Specificare la lunghezza del segmento:
|
.measure-specify-segment-length = MEASURE Specificare la lunghezza del segmento:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter un'opzione [Distanza/Raggio/Angolo/ARea]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter un'opzione [Distanza/Raggio/Angolo/ARea]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1427,6 +1428,8 @@ common =
|
|||||||
.user-preferences = ユーザー設定
|
.user-preferences = ユーザー設定
|
||||||
.zoom = ズーム
|
.zoom = ズーム
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = マウス ホイールのズーム方向を反転 (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = マウス ホイールのズーム方向を反転 (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = タッチパッド: スクロールで画面移動、Ctrl+スクロールまたはピンチでズーム
|
||||||
|
.interface-scale = インターフェースの拡大率
|
||||||
.zoom-factor = ズーム倍率
|
.zoom-factor = ズーム倍率
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = ホイール 1 ノッチのズーム量 (ZOOMFACTOR)。
|
.how-far-one-wheel-notch-zooms-zoomfactor = ホイール 1 ノッチのズーム量 (ZOOMFACTOR)。
|
||||||
.text-and-dimensions = 文字と寸法
|
.text-and-dimensions = 文字と寸法
|
||||||
@@ -2496,6 +2499,12 @@ inquiry =
|
|||||||
.id-specify-point = ID 点を指定:
|
.id-specify-point = ID 点を指定:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: 図形が選択されていません。先に選択してください。
|
.list-no-entities-selected-select-entities-firs = LIST: 図形が選択されていません。先に選択してください。
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: 図形が選択されていません。先に選択してください。
|
.massprop-no-entities-selected-select-entities = MASSPROP: 図形が選択されていません。先に選択してください。
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ には計測できる閉じたボディがありません。
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ 体積=__ocs_fmt_1__ 重心=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = 慣性モーメント X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = 慣性乗積 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = 回転半径 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = 重心まわりの主慣性モーメント I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE 計測するオブジェクトを選択:
|
.measure-select-object-to-measure = MEASURE 計測するオブジェクトを選択:
|
||||||
.measure-specify-segment-length = MEASURE 区間長を指定:
|
.measure-specify-segment-length = MEASURE 区間長を指定:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM オプションを入力 [距離/半径/角度/面積]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM オプションを入力 [距離/半径/角度/面積]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1436,6 +1437,8 @@ common =
|
|||||||
.user-preferences = 사용자 환경설정
|
.user-preferences = 사용자 환경설정
|
||||||
.zoom = 줌
|
.zoom = 줌
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = 마우스 휠 줌 방향 반전 (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = 마우스 휠 줌 방향 반전 (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = 터치패드: 스크롤은 화면 이동, Ctrl+스크롤 또는 핀치는 확대/축소
|
||||||
|
.interface-scale = 인터페이스 배율
|
||||||
.zoom-factor = 줌 배율
|
.zoom-factor = 줌 배율
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = 휠 한 칸이 확대하는 정도 (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = 휠 한 칸이 확대하는 정도 (ZOOMFACTOR).
|
||||||
.text-and-dimensions = 문자 및 치수
|
.text-and-dimensions = 문자 및 치수
|
||||||
@@ -2505,6 +2508,12 @@ inquiry =
|
|||||||
.id-specify-point = ID 지점 지정:
|
.id-specify-point = ID 지점 지정:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: 선택된 엔터티가 없습니다. 먼저 엔터티를 선택하세요.
|
.list-no-entities-selected-select-entities-firs = LIST: 선택된 엔터티가 없습니다. 먼저 엔터티를 선택하세요.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: 선택된 엔터티가 없습니다. 먼저 엔터티를 선택하세요.
|
.massprop-no-entities-selected-select-entities = MASSPROP: 선택된 엔터티가 없습니다. 먼저 엔터티를 선택하세요.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ 에 측정할 닫힌 바디가 없습니다.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ 부피=__ocs_fmt_1__ 도심=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = 관성 모멘트 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = 관성 곱 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = 회전 반경 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = 도심에 대한 주 모멘트 I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE 측정할 물체 선택:
|
.measure-select-object-to-measure = MEASURE 측정할 물체 선택:
|
||||||
.measure-specify-segment-length = MEASURE 세그먼트 길이 지정:
|
.measure-specify-segment-length = MEASURE 세그먼트 길이 지정:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter 옵션 [거리/반경/각도/지역]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter 옵션 [거리/반경/각도/지역]:
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1435,6 +1436,8 @@ common =
|
|||||||
.user-preferences = Gebruikersvoorkeuren
|
.user-preferences = Gebruikersvoorkeuren
|
||||||
.zoom = Zoomen
|
.zoom = Zoomen
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Muiswielzoom omkeren (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Muiswielzoom omkeren (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: scrollen verschuift, Ctrl+scrollen of knijpen zoomt
|
||||||
|
.interface-scale = Interfaceschaal
|
||||||
.zoom-factor = Zoomfactor
|
.zoom-factor = Zoomfactor
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Hoeveel één wielklik zoomt (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Hoeveel één wielklik zoomt (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Tekst en maatvoering
|
.text-and-dimensions = Tekst en maatvoering
|
||||||
@@ -2506,6 +2509,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Specificeer punt:
|
.id-specify-point = ID Specificeer punt:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: geen objecten geselecteerd. Selecteer eerst objecten.
|
.list-no-entities-selected-select-entities-firs = LIST: geen objecten geselecteerd. Selecteer eerst objecten.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: geen objecten geselecteerd. Selecteer eerst objecten.
|
.massprop-no-entities-selected-select-entities = MASSPROP: geen objecten geselecteerd. Selecteer eerst objecten.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ heeft geen gesloten lichaam om te meten.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Zwaartepunt=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Traagheidsmomenten X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Deviatiemomenten XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Gyratiestralen X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Hoofdtraagheidsmomenten om het zwaartepunt I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Selecteer object om mee te meten:
|
.measure-select-object-to-measure = MEASURE Selecteer object om mee te meten:
|
||||||
.measure-specify-segment-length = MEASURE segmentlengte specificeren:
|
.measure-specify-segment-length = MEASURE segmentlengte specificeren:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Voer een optie in [Afstand / Straal / Hoek / Oppervlakte]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Voer een optie in [Afstand / Straal / Hoek / Oppervlakte]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1436,6 +1437,8 @@ common =
|
|||||||
.user-preferences = Preferencje użytkownika
|
.user-preferences = Preferencje użytkownika
|
||||||
.zoom = Powiększenie
|
.zoom = Powiększenie
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Odwróć powiększanie kółkiem myszy (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Odwróć powiększanie kółkiem myszy (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: przewijanie przesuwa widok, Ctrl+przewijanie lub szczypanie powiększa
|
||||||
|
.interface-scale = Skala interfejsu
|
||||||
.zoom-factor = Współczynnik powiększenia
|
.zoom-factor = Współczynnik powiększenia
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = O ile powiększa jedno kliknięcie kółka (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = O ile powiększa jedno kliknięcie kółka (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Tekst i wymiary
|
.text-and-dimensions = Tekst i wymiary
|
||||||
@@ -2505,6 +2508,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Określ punkt:
|
.id-specify-point = ID Określ punkt:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: nie wybrano żadnych elementów. Najpierw wybierz elementy.
|
.list-no-entities-selected-select-entities-firs = LIST: nie wybrano żadnych elementów. Najpierw wybierz elementy.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: nie wybrano żadnych elementów. Najpierw wybierz elementy.
|
.massprop-no-entities-selected-select-entities = MASSPROP: nie wybrano żadnych elementów. Najpierw wybierz elementy.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ nie ma zamkniętej bryły do zmierzenia.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Objętość=__ocs_fmt_1__ Środek ciężkości=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Momenty bezwładności X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Momenty dewiacyjne XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Promienie bezwładności X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Główne momenty względem środka ciężkości I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Wybierz obiekt do pomiaru:
|
.measure-select-object-to-measure = MEASURE Wybierz obiekt do pomiaru:
|
||||||
.measure-specify-segment-length = MEASURE Określ długość segmentu:
|
.measure-specify-segment-length = MEASURE Określ długość segmentu:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter opcja [Odległość/Promień/Kąt/Powierzchnia]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter opcja [Odległość/Promień/Kąt/Powierzchnia]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1437,6 +1438,8 @@ common =
|
|||||||
.user-preferences = Preferências do usuário
|
.user-preferences = Preferências do usuário
|
||||||
.zoom = Zoom
|
.zoom = Zoom
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Inverter o zoom da roda do mouse (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Inverter o zoom da roda do mouse (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: rolar move a vista, Ctrl+rolar ou pinçar aplica zoom
|
||||||
|
.interface-scale = Escala da interface
|
||||||
.zoom-factor = Fator de zoom
|
.zoom-factor = Fator de zoom
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Quanto um passo da roda amplia (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Quanto um passo da roda amplia (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Texto e cotas
|
.text-and-dimensions = Texto e cotas
|
||||||
@@ -2506,6 +2509,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Especifique o ponto:
|
.id-specify-point = ID Especifique o ponto:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: nenhuma entidade selecionada. Selecione primeiro as entidades.
|
.list-no-entities-selected-select-entities-firs = LIST: nenhuma entidade selecionada. Selecione primeiro as entidades.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: nenhuma entidade selecionada. Selecione primeiro as entidades.
|
.massprop-no-entities-selected-select-entities = MASSPROP: nenhuma entidade selecionada. Selecione primeiro as entidades.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ não tem um corpo fechado para medir.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Centroide=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Momentos de inércia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Produtos de inércia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Raios de giração X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Momentos principais em relação ao centroide I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Selecione o objeto a medir:
|
.measure-select-object-to-measure = MEASURE Selecione o objeto a medir:
|
||||||
.measure-specify-segment-length = MEASURE Especifique o comprimento do segmento:
|
.measure-specify-segment-length = MEASURE Especifique o comprimento do segmento:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Insira uma opção [Distância/Raio/Ângulo/ÁRea]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Insira uma opção [Distância/Raio/Ângulo/ÁRea]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1440,6 +1441,8 @@ common =
|
|||||||
.user-preferences = Пользовательские настройки
|
.user-preferences = Пользовательские настройки
|
||||||
.zoom = Зумирование
|
.zoom = Зумирование
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Обратить зумирование колесом мыши (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Обратить зумирование колесом мыши (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Тачпад: прокрутка панорамирует, Ctrl+прокрутка или щипок масштабирует
|
||||||
|
.interface-scale = Масштаб интерфейса
|
||||||
.zoom-factor = Коэффициент зумирования
|
.zoom-factor = Коэффициент зумирования
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Насколько приближает один щелчок колеса (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Насколько приближает один щелчок колеса (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Текст и размеры
|
.text-and-dimensions = Текст и размеры
|
||||||
@@ -2509,6 +2512,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Укажите точку:
|
.id-specify-point = ID Укажите точку:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: не выбрано ни одного объекта.
|
.list-no-entities-selected-select-entities-firs = LIST: не выбрано ни одного объекта.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: не выбрано ни одного объекта.
|
.massprop-no-entities-selected-select-entities = MASSPROP: не выбрано ни одного объекта.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ не имеет замкнутого тела для измерения.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Объём=__ocs_fmt_1__ Центр масс=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Моменты инерции X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Центробежные моменты XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Радиусы инерции X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Главные моменты относительно центра масс I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Выберите объект для измерения:
|
.measure-select-object-to-measure = MEASURE Выберите объект для измерения:
|
||||||
.measure-specify-segment-length = MEASURE Укажите длину сегмента:
|
.measure-specify-segment-length = MEASURE Укажите длину сегмента:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Введите опцию [Расстояние/Радиус/Англия/Арея]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Введите опцию [Расстояние/Радиус/Англия/Арея]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1423,6 +1424,8 @@ common =
|
|||||||
.user-preferences = Kullanıcı tercihleri
|
.user-preferences = Kullanıcı tercihleri
|
||||||
.zoom = Yakınlaştırma
|
.zoom = Yakınlaştırma
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = Fare tekerleği yakınlaştırmasını ters çevir (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = Fare tekerleği yakınlaştırmasını ters çevir (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Dokunmatik yüzey: kaydırma kaydırır, Ctrl+kaydırma veya sıkıştırma yakınlaştırır
|
||||||
|
.interface-scale = Arayüz ölçeği
|
||||||
.zoom-factor = Yakınlaştırma çarpanı
|
.zoom-factor = Yakınlaştırma çarpanı
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = Bir tekerlek adımının ne kadar yakınlaştıracağı (ZOOMFACTOR).
|
.how-far-one-wheel-notch-zooms-zoomfactor = Bir tekerlek adımının ne kadar yakınlaştıracağı (ZOOMFACTOR).
|
||||||
.text-and-dimensions = Metin ve ölçülendirme
|
.text-and-dimensions = Metin ve ölçülendirme
|
||||||
@@ -2487,6 +2490,12 @@ inquiry =
|
|||||||
.id-specify-point = ID Noktayı belirtin:
|
.id-specify-point = ID Noktayı belirtin:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST: Nesne seçilmedi. Önce nesneleri seçin.
|
.list-no-entities-selected-select-entities-firs = LIST: Nesne seçilmedi. Önce nesneleri seçin.
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP: Nesne seçilmedi. Önce nesneleri seçin.
|
.massprop-no-entities-selected-select-entities = MASSPROP: Nesne seçilmedi. Önce nesneleri seçin.
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ için ölçülecek kapalı gövde yok.
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Hacim=__ocs_fmt_1__ Ağırlık merkezi=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = Eylemsizlik momentleri X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = Eylemsizlik çarpımları XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = Jirasyon yarıçapları X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = Ağırlık merkezine göre asal momentler I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE Ölçülecek nesneyi seçin:
|
.measure-select-object-to-measure = MEASURE Ölçülecek nesneyi seçin:
|
||||||
.measure-specify-segment-length = MEASURE Parça uzunluğunu belirtin:
|
.measure-specify-segment-length = MEASURE Parça uzunluğunu belirtin:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Bir seçenek girin [Mesafe/Yarıçap/Açı/Alan]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Bir seçenek girin [Mesafe/Yarıçap/Açı/Alan]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1424,6 +1425,8 @@ common =
|
|||||||
.user-preferences = 用户首选项
|
.user-preferences = 用户首选项
|
||||||
.zoom = 缩放
|
.zoom = 缩放
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = 反转鼠标滚轮缩放方向 (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = 反转鼠标滚轮缩放方向 (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = 触控板:滚动平移,Ctrl+滚动或捏合缩放
|
||||||
|
.interface-scale = 界面缩放
|
||||||
.zoom-factor = 缩放系数
|
.zoom-factor = 缩放系数
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = 滚轮每格的缩放幅度 (ZOOMFACTOR)。
|
.how-far-one-wheel-notch-zooms-zoomfactor = 滚轮每格的缩放幅度 (ZOOMFACTOR)。
|
||||||
.text-and-dimensions = 文字和标注
|
.text-and-dimensions = 文字和标注
|
||||||
@@ -2493,6 +2496,12 @@ inquiry =
|
|||||||
.id-specify-point = ID 指定点:
|
.id-specify-point = ID 指定点:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST:没有选择实体。首先选择实体。
|
.list-no-entities-selected-select-entities-firs = LIST:没有选择实体。首先选择实体。
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP:没有选择实体。首先选择实体。
|
.massprop-no-entities-selected-select-entities = MASSPROP:没有选择实体。首先选择实体。
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ 没有可测量的封闭实体。
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ 体积=__ocs_fmt_1__ 质心=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = 惯性矩 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = 惯性积 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = 回转半径 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = 关于质心的主力矩 I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE 选择要定距等分的对象:
|
.measure-select-object-to-measure = MEASURE 选择要定距等分的对象:
|
||||||
.measure-specify-segment-length = MEASURE 指定线段长度:
|
.measure-specify-segment-length = MEASURE 指定线段长度:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM 输入选项 [距离/半径/角度/面积]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM 输入选项 [距离/半径/角度/面积]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ language =
|
|||||||
.arabic = العربية
|
.arabic = العربية
|
||||||
.japanese = 日本語
|
.japanese = 日本語
|
||||||
.korean = 한국어
|
.korean = 한국어
|
||||||
|
.lao = ລາວ
|
||||||
.czech = Čeština
|
.czech = Čeština
|
||||||
.italian = Italiano
|
.italian = Italiano
|
||||||
.finnish = Suomi
|
.finnish = Suomi
|
||||||
@@ -1436,6 +1437,8 @@ common =
|
|||||||
.user-preferences = 使用者偏好設定
|
.user-preferences = 使用者偏好設定
|
||||||
.zoom = 縮放
|
.zoom = 縮放
|
||||||
.reverse-mouse-wheel-zoom-zoomwheel = 反轉滑鼠滾輪縮放方向 (ZOOMWHEEL)
|
.reverse-mouse-wheel-zoom-zoomwheel = 反轉滑鼠滾輪縮放方向 (ZOOMWHEEL)
|
||||||
|
.touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = 觸控板:捲動平移,Ctrl+捲動或捏合縮放
|
||||||
|
.interface-scale = 介面縮放
|
||||||
.zoom-factor = 縮放係數
|
.zoom-factor = 縮放係數
|
||||||
.how-far-one-wheel-notch-zooms-zoomfactor = 滾輪每一格的縮放幅度 (ZOOMFACTOR)。
|
.how-far-one-wheel-notch-zooms-zoomfactor = 滾輪每一格的縮放幅度 (ZOOMFACTOR)。
|
||||||
.text-and-dimensions = 文字與標註
|
.text-and-dimensions = 文字與標註
|
||||||
@@ -2505,6 +2508,12 @@ inquiry =
|
|||||||
.id-specify-point = ID 指定點:
|
.id-specify-point = ID 指定點:
|
||||||
.list-no-entities-selected-select-entities-firs = LIST:沒有選擇實體。首先選擇實體。
|
.list-no-entities-selected-select-entities-firs = LIST:沒有選擇實體。首先選擇實體。
|
||||||
.massprop-no-entities-selected-select-entities = MASSPROP:沒有選擇實體。首先選擇實體。
|
.massprop-no-entities-selected-select-entities = MASSPROP:沒有選擇實體。首先選擇實體。
|
||||||
|
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ 沒有可量測的封閉實體。
|
||||||
|
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ 體積=__ocs_fmt_1__ 質心=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||||
|
.moments-of-inertia-x-4-y-4-z-4 = 慣性矩 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.products-of-inertia-xy-4-yz-4-zx-4 = 慣性積 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||||
|
.radii-of-gyration-x-4-y-4-z-4 = 迴轉半徑 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||||
|
.principal-moments-about-centroid-i-4-j-4-k-4 = 關於質心的主力矩 I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||||
.measure-select-object-to-measure = MEASURE 選擇要定距等分的物件:
|
.measure-select-object-to-measure = MEASURE 選擇要定距等分的物件:
|
||||||
.measure-specify-segment-length = MEASURE 指定線段長度:
|
.measure-specify-segment-length = MEASURE 指定線段長度:
|
||||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM 輸入選項 [距離/半徑/角度/面積]:
|
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM 輸入選項 [距離/半徑/角度/面積]:
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ Comment[ja]=2D/3Dの作図と設計のためのCADアプリ
|
|||||||
Keywords[ja]=CAD;DWG;DXF;図面;設計;工学表記;2D;3D;
|
Keywords[ja]=CAD;DWG;DXF;図面;設計;工学表記;2D;3D;
|
||||||
Comment[ko]=2D/3D 제도 및 설계를 위한 CAD 앱
|
Comment[ko]=2D/3D 제도 및 설계를 위한 CAD 앱
|
||||||
Keywords[ko]=CAD;DWG;DXF;그리기;설계;공학;2D;3D;
|
Keywords[ko]=CAD;DWG;DXF;그리기;설계;공학;2D;3D;
|
||||||
|
Comment[lo]=ແອັບພລິເຄຊັນ CAD ສຳລັບການແຕ້ມ ແລະ ອອກແບບ 2D/3D
|
||||||
|
Keywords[lo]=CAD;DWG;DXF;ຮູບແຕ້ມ;ອອກແບບ;ວິສະວະກຳ;2D;3D;
|
||||||
Comment[nl]=Een CAD-toepassing voor 2D/3D-tekenen en ontwerpen
|
Comment[nl]=Een CAD-toepassing voor 2D/3D-tekenen en ontwerpen
|
||||||
Keywords[nl]=CAD;DWG;DXF;Tekening;Ontwerp;Technisch;2D;3D;
|
Keywords[nl]=CAD;DWG;DXF;Tekening;Ontwerp;Technisch;2D;3D;
|
||||||
Comment[pl]=Aplikacja CAD do rysowania i projektowania 2D/3D
|
Comment[pl]=Aplikacja CAD do rysowania i projektowania 2D/3D
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<summary xml:lang="it-IT">Applicazione CAD 2D/3D per disegni DWG e DXF</summary>
|
<summary xml:lang="it-IT">Applicazione CAD 2D/3D per disegni DWG e DXF</summary>
|
||||||
<summary xml:lang="ja-JP">DWGとDXF図面用の2D/3D CADアプリ</summary>
|
<summary xml:lang="ja-JP">DWGとDXF図面用の2D/3D CADアプリ</summary>
|
||||||
<summary xml:lang="ko-KR">DWG 및 DXF 도면용 2D/3D CAD 앱</summary>
|
<summary xml:lang="ko-KR">DWG 및 DXF 도면용 2D/3D CAD 앱</summary>
|
||||||
|
<summary xml:lang="lo-LA">ແອັບພລິເຄຊັນ CAD 2D/3D ສຳລັບຮູບແຕ້ມ DWG ແລະ DXF</summary>
|
||||||
<summary xml:lang="nl-NL">2D/3D-CAD-toepassing voor DWG- en DXF-tekeningen</summary>
|
<summary xml:lang="nl-NL">2D/3D-CAD-toepassing voor DWG- en DXF-tekeningen</summary>
|
||||||
<summary xml:lang="pl-PL">Aplikacja CAD 2D/3D do rysunków DWG i DXF</summary>
|
<summary xml:lang="pl-PL">Aplikacja CAD 2D/3D do rysunków DWG i DXF</summary>
|
||||||
<summary xml:lang="pt-BR">Aplicativo CAD 2D/3D para desenhos DWG e DXF</summary>
|
<summary xml:lang="pt-BR">Aplicativo CAD 2D/3D para desenhos DWG e DXF</summary>
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
<p xml:lang="it-IT">Crea e modifica disegni tecnici nei formati DWG e DXF.</p>
|
<p xml:lang="it-IT">Crea e modifica disegni tecnici nei formati DWG e DXF.</p>
|
||||||
<p xml:lang="ja-JP">DWGおよびDXF形式で技術図面を作成・編集できます。</p>
|
<p xml:lang="ja-JP">DWGおよびDXF形式で技術図面を作成・編集できます。</p>
|
||||||
<p xml:lang="ko-KR">DWG 및 DXF 형식으로 기술 도면을 만들고 편집하세요.</p>
|
<p xml:lang="ko-KR">DWG 및 DXF 형식으로 기술 도면을 만들고 편집하세요.</p>
|
||||||
|
<p xml:lang="lo-LA">ສ້າງ ແລະ ແກ້ໄຂຮູບແຕ້ມເຕັກນິກໃນຮູບແບບ DWG ແລະ DXF.</p>
|
||||||
<p xml:lang="nl-NL">Maak en bewerk technische tekeningen in DWG- en DXF-indeling.</p>
|
<p xml:lang="nl-NL">Maak en bewerk technische tekeningen in DWG- en DXF-indeling.</p>
|
||||||
<p xml:lang="pl-PL">Twórz i edytuj rysunki techniczne w formatach DWG i DXF.</p>
|
<p xml:lang="pl-PL">Twórz i edytuj rysunki techniczne w formatach DWG i DXF.</p>
|
||||||
<p xml:lang="pt-BR">Crie e edite desenhos técnicos nos formatos DWG e DXF.</p>
|
<p xml:lang="pt-BR">Crie e edite desenhos técnicos nos formatos DWG e DXF.</p>
|
||||||
@@ -94,6 +96,9 @@
|
|||||||
<keyword xml:lang="ko-KR">그리기</keyword>
|
<keyword xml:lang="ko-KR">그리기</keyword>
|
||||||
<keyword xml:lang="ko-KR">설계</keyword>
|
<keyword xml:lang="ko-KR">설계</keyword>
|
||||||
<keyword xml:lang="ko-KR">공학</keyword>
|
<keyword xml:lang="ko-KR">공학</keyword>
|
||||||
|
<keyword xml:lang="lo-LA">ຮູບແຕ້ມ</keyword>
|
||||||
|
<keyword xml:lang="lo-LA">ອອກແບບ</keyword>
|
||||||
|
<keyword xml:lang="lo-LA">ວິສະວະກຳ</keyword>
|
||||||
<keyword xml:lang="nl-NL">Tekening</keyword>
|
<keyword xml:lang="nl-NL">Tekening</keyword>
|
||||||
<keyword xml:lang="nl-NL">Ontwerp</keyword>
|
<keyword xml:lang="nl-NL">Ontwerp</keyword>
|
||||||
<keyword xml:lang="nl-NL">Technisch</keyword>
|
<keyword xml:lang="nl-NL">Technisch</keyword>
|
||||||
|
|||||||
Generated
+1
-1
@@ -1264,7 +1264,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "opencadcodec"
|
name = "opencadcodec"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=937745d#937745d9ec1a62d252074ff41b140b92303d0dc0"
|
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=8189f83#8189f833574c544fc57a1d773591014c3a5907c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"name": "ພາສາລາວ",
|
||||||
|
"title": "Open CAD Studio — ໂປຣແກຣມ CAD DWG & DXF ບໍ່ເສຍຄ່າ",
|
||||||
|
"description": "ເປີດ ແລະ ບັນທຶກໄຟລ໌ DWG ແລະ DXF, ຂຽນແບບ 2 ມິຕິ ແລະ ຂຶ້ນຮູບ 3 ມິຕິ. ໂປຣແກຣມ CAD ແບບໂອເພນຊອດ ບໍ່ເສຍຄ່າ ສຳລັບບຣາວເຊີ, Windows, Linux ແລະ macOS.",
|
||||||
|
"skip": "ຂ້າມໄປຫາເນື້ອຫາ",
|
||||||
|
"home": "ໜ້າຫຼັກ Open CAD Studio",
|
||||||
|
"navigation": "ການນຳທາງ",
|
||||||
|
"language": "ພາສາ",
|
||||||
|
"capabilities": "ຄວາມສາມາດ",
|
||||||
|
"desktop": "ເດັສທັອບ",
|
||||||
|
"formats": "ຮູບແບບໄຟລ໌",
|
||||||
|
"launch": "ເປີດໃຊ້ເວັບແອັບ",
|
||||||
|
"eyebrow": "CAD ແບບໂອເພນຊອດ · ເດັສທັອບ + ເວັບ",
|
||||||
|
"hero_title": "ຮອງຮັບໄຟລ໌ DWG & DXF ໂດຍກົງ.",
|
||||||
|
"hero_emphasis": "ຂຽນແບບ 2 ມິຕິ. ຂຶ້ນຮູບ 3 ມິຕິ.",
|
||||||
|
"hero_lead": "ແຕ້ມແບບ 2 ມິຕິ, ຈັດໜ້າເຈ້ຍ ແລະ ຂຶ້ນຮູບ 3 ມິຕິ. ເຮັດວຽກຜ່ານບຣາວເຊີ ຫຼື ໃນຄອມພິວເຕີຂອງທ່ານ, ພ້ອມທັງບັນທຶກແບບແຕ້ມລົງໃນອຸປະກອນໄດ້ທັນທີ.",
|
||||||
|
"download": "ດາວໂຫຼດສະບັບເດັສທັອບ",
|
||||||
|
"license": "ສັນຍາອະນຸຍາດ GPL-3.0",
|
||||||
|
"languages": "{count} ພາສາໃນລະບົບ",
|
||||||
|
"no_account": "ເວັບແອັບ: ບໍ່ເສຍຄ່າ · ບໍ່ຕ້ອງມີບັນຊີ · ບໍ່ຕ້ອງຕິດຕັ້ງ",
|
||||||
|
"workspace_alt": "ແຜນຜັງເຕັກນິກ DWG ທີ່ເປີດຢູ່ໃນພື້ນທີ່ເຮັດວຽກຂອງ Open CAD Studio",
|
||||||
|
"native_editing": "ແກ້ໄຂໄຟລ໌ຕົ້ນສະບັບໂດຍກົງ",
|
||||||
|
"read_write": "ອ່ານ ແລະ ຂຽນ",
|
||||||
|
"one_workspace": "ພື້ນທີ່ເຮັດວຽກດຽວ",
|
||||||
|
"gpu": "ມຸມມອງປະມວນຜົນໄວດ້ວຍຮາດແວ",
|
||||||
|
"core": "ແກນກາງການແກ້ໄຂຮ່ວມກັນ",
|
||||||
|
"cap_eyebrow": "ການຂຽນແບບ ແລະ ເອກະສານປະກອບ",
|
||||||
|
"cap_title": "ຕັ້ງແຕ່ເສັ້ນທຳອິດ ຈົນຮອດການຈັດໜ້າສຳເລັດ.",
|
||||||
|
"cap_lead": "ຄຳສັ່ງ, ການກຳນົດຕຳແໜ່ງທີ່ຊັດເຈນ ແລະ ແຜ່ນແຕ້ມແບບ ລວມຢູ່ໃນພື້ນທີ່ດຽວ.",
|
||||||
|
"draft_label": "ການຂຽນແບບທີ່ຊັດເຈນ",
|
||||||
|
"draft_title": "ເລີ່ມຕົ້ນດ້ວຍຄຳສັ່ງ.",
|
||||||
|
"draft_text": "ແຕ້ມ ແລະ ແກ້ໄຂດ້ວຍລະບົບແນະນຳຄຳສັ່ງອັດຕະໂນມັດ, ຈຸດຈັບວັດຖຸ (Snap) ແລະ ການຕິດພັນຕຳແໜ່ງ. ຈັດລະບຽບແບບແຕ້ມດ້ວຍ ເລເຍີ (Layer), ບລັອກ (Block) ແລະ ໄຟລ໌ອ້າງອີງພາຍນອກ.",
|
||||||
|
"doc_label": "ເອກະສານປະກອບແບບ",
|
||||||
|
"doc_title": "ກຽມແຜ່ນແຕ້ມແບບໃຫ້ສົມບູນ.",
|
||||||
|
"doc_text": "ເພີ່ມຂໍ້ຄວາມ, ເສັ້ນບອກຂະໜາດ, ເສັ້ນຊີ້, ຄ່າຄວາມຄາດເຄື່ອນ ແລະ ຕາຕະລາງ. ຈັດວາງໜ້າເຈ້ຍດ້ວຍຊ່ອງມຸມມອງ (Viewport) ແລະ ຮູບແບບການສັ່ງພິມ.",
|
||||||
|
"open_label": "ຂະບວນການເຮັດວຽກແບບເປີດກວ້າງ",
|
||||||
|
"open_title": "ແບບແຕ້ມຍັງຄົງເປັນຂອງທ່ານສະເໝີ.",
|
||||||
|
"open_text": "ເປີດ ແລະ ບັນທຶກໄຟລ໌ DWG ແລະ DXF ໂດຍກົງໃນອຸປະກອນຂອງທ່ານ. ຊອດໂຄດເປີດເຜີຍຕໍ່ສາທາລະນະ ແລະ ເຄື່ອງມືຕ່າງໆ ນຳໃຊ້ໄດ້ໂດຍບໍ່ເສຍຄ່າ.",
|
||||||
|
"model_label": "ລະບົບ 3 ມິຕິ ດ້ວຍເຄີເນວ",
|
||||||
|
"model_title": "ສ້າງ ແລະ ດັດຮູບຮ່າງວັດຖຸແຂງ (Solid).",
|
||||||
|
"model_text": "ສ້າງຮູບຊົງພື້ນຖານ, ດຶງຢືດ (Extrude), ໝຸນຮອບ (Revolve), ແລ່ນຕາມເສັ້ນ (Sweep) ແລະ ເຊື່ອມຕໍ່ໜ້າຕັດ (Loft). ລວມ ຫຼື ຕັດວັດຖຸແຂງດ້ວຍຄຳສັ່ງບູລີນ (Boolean) ຜ່ານເຄີເນວເລຂາຄະນິດ.",
|
||||||
|
"camera": "ມຸມມອງແບບຂະໜານ ແລະ ມຸມມອງເພີສເປັກຕິບ",
|
||||||
|
"tessellation": "ການສະແດງຜົນຮູບຊົງແຂງ ACIS",
|
||||||
|
"mesh_workflow": "ນຳເຂົ້າ OBJ ແລະ ສົ່ງອອກ STL",
|
||||||
|
"model_alt": "ໂມເດວເຄື່ອງກົນ 3 ມິຕິ ໃນສອງຊ່ອງມຸມມອງຂອງ Open CAD Studio",
|
||||||
|
"model_space": "ພື້ນທີ່ໂມເດວ",
|
||||||
|
"model_caption": "ເລຂາຄະນິດຮູບຊົງແຂງ · ມຸມມອງແບບໂຕ້ຕອບ",
|
||||||
|
"editions_label": "ແກນຫຼັກດຽວ, ເລືອກເຮັດວຽກໄດ້ສອງຮູບແບບ",
|
||||||
|
"editions_title": "ເປີດໃນບຣາວເຊີ ຫຼື ຕິດຕັ້ງລົງໃນເດັສທັອບ.",
|
||||||
|
"web": "ເວັບ",
|
||||||
|
"web_title": "ເລີ່ມຕົ້ນແຕ້ມໃນບຣາວເຊີຂອງທ່ານ.",
|
||||||
|
"web_text": "ນຳໃຊ້ເວັບແອັບໄດ້ໂດຍບໍ່ຕ້ອງມີບັນຊີ ຫຼື ຕິດຕັ້ງ. ເປີດແບບແຕ້ມຈາກອຸປະກອນ, ແກ້ໄຂ ແລະ ດາວໂຫຼດຜົນງານອອກມາ.",
|
||||||
|
"web_limit": "ສຳລັບແບບແຕ້ມຂະໜາດໃຫຍ່ ແລະ ການເຮັດວຽກຮ່ວມກັບລະບົບໂດຍກົງ, ແນະນຳໃຫ້ໃຊ້ແອັບເດັສທັອບ.",
|
||||||
|
"desktop_title": "ການສັ່ງພິມ, ປລັກອິນ ແລະ ລະບົບອັດຕະໂນມັດ.",
|
||||||
|
"desktop_text": "ຮອງຮັບການເປີດໄຟລ໌ໂດຍກົງ, ຮູບຕົວຢ່າງຫຍໍ້, ການສັ່ງພິມຜ່ານລະບົບ, ການສ້າງໄຟລ໌ PDF, ປລັກອິນ, ສະຄຣິບ ແລະ ລະບົບອັດຕະໂນມັດແບບບໍ່ເປີດໜ້າຕ່າງ (Headless).",
|
||||||
|
"latest": "ດາວໂຫຼດເວີຊັນຫຼ້າສຸດ",
|
||||||
|
"release_notes": "ບັນທຶກປະຈຳເວີຊັນ",
|
||||||
|
"formats_label": "ຂະບວນການຈັດການໄຟລ໌",
|
||||||
|
"formats_title": "ເປີດ ແລະ ບັນທຶກໄຟລ໌ແບບແຕ້ມໄດ້ໂດຍກົງ.",
|
||||||
|
"formats_text": "ເປີດ ແລະ ບັນທຶກ DWG ແລະ DXF ໄດ້ໂດຍກົງ, ເລືອກເວີຊັນບັນທຶກໄດ້ຕັ້ງແຕ່ R14 ຮອດ 2018. ພ້ອມທັງຮອງຮັບໄຟລ໌ກູ້ຄືນ, ໂຄງຕາໜ່າງ (Mesh) ແລະ ການດຶງຂໍ້ມູນຄຸນສົມບັດອອກ.",
|
||||||
|
"format_versions": "ອ່ານ · ຂຽນ · R14–2018",
|
||||||
|
"recovery": "ການກູ້ຄືນແບບແຕ້ມ",
|
||||||
|
"mesh_formats": "ນຳເຂົ້າ OBJ ແລະ ສົ່ງອອກ STL",
|
||||||
|
"pdf": "ການສັ່ງພິມແບບແຕ້ມໃນເດັສທັອບ",
|
||||||
|
"csv": "ການດຶງຂໍ້ມູນຄຸນສົມບັດອອກ",
|
||||||
|
"growth_label": "ການເຕີບໃຫຍ່ຂອງໂຄງການ",
|
||||||
|
"growth_title": "ຕົວເລກສະຖິຕິຂອງໂຄງການ.",
|
||||||
|
"growth_text": "ຕິດຕາມໂຄງການຜ່ານດາວເທິງ GitHub ແລະ ຍອດດາວໂຫຼດແຕ່ລະເວີຊັນ.",
|
||||||
|
"stars": "ດາວ",
|
||||||
|
"downloads": "ຍອດດາວໂຫຼດແຕ່ລະເວີຊັນ",
|
||||||
|
"releases_link": "ເບິ່ງລາຍການເວີຊັນທີ່ປ່ອຍອອກ",
|
||||||
|
"chart_updated": "ອັບເດດເມື່ອ {date}",
|
||||||
|
"chart_downloads": "ດາວໂຫຼດ / ເວີຊັນ",
|
||||||
|
"chart_totals": "ດາວໂຫຼດ {count} ຄັ້ງ · {releases} ເວີຊັນ · ບໍ່ລວມເວີຊັນຫຼ້າສຸດ",
|
||||||
|
"faq_label": "ຄຳຖາມທີ່ພົບເລື້ອຍ",
|
||||||
|
"faq_title": "ກ່ອນເລີ່ມລົງມືແຕ້ມແບບທຳອິດ.",
|
||||||
|
"faq_free": "Open CAD Studio ນຳໃຊ້ໄດ້ໂດຍບໍ່ເສຍຄ່າແທ້ບໍ່?",
|
||||||
|
"faq_free_answer": "ແມ່ນແລ້ວ. ໂຄງການນີ້ເປັນໂອເພນຊອດ ແລະ ເຜີຍແຜ່ພາຍໃຕ້ສັນຍາອະນຸຍາດ GPL-3.0.",
|
||||||
|
"faq_web": "ຈຳເປັນຕ້ອງສ້າງບັນຊີ ຫຼື ຕິດຕັ້ງກ່ອນບໍ່?",
|
||||||
|
"faq_web_answer": "ບໍ່ຈຳເປັນ. ສາມາດເປີດເວັບແອັບຜ່ານບຣາວເຊີໄດ້ເລີຍ. ແບບແຕ້ມຈະຖືກເປີດຈາກອຸປະກອນຂອງທ່ານ ແລະ ບັນທຶກລົງໃນເຄື່ອງໂດຍກົງ.",
|
||||||
|
"faq_platforms": "ຮອງຮັບລະບົບປະຕິບັດການໃດແດ່ເທິງເດັສທັອບ?",
|
||||||
|
"faq_platforms_answer": "ມີຊຸດຕິດຕັ້ງຮອງຮັບທັງ Windows, Linux ແລະ macOS (Apple Silicon).",
|
||||||
|
"faq_production": "ສາມາດນຳໃຊ້ມັນກັບວຽກງານການຜະລິດຫຼັກໄດ້ບໍ່?",
|
||||||
|
"faq_production_answer": "ໂຄງການນີ້ຍັງຢູ່ໃນໄລຍະການພັດທະນາຢ່າງຕໍ່ເນື່ອງ. ກະລຸນາສຳຮອງຂໍ້ມູນໄວ້ສະເໝີ ແລະ ລາຍງານບັນຫາທີ່ພົບເຫັນລົງໃນ GitHub.",
|
||||||
|
"community_label": "ສ້າງຂຶ້ນຢ່າງໂປ່ງໃສ ແລະ ເປີດກວ້າງ",
|
||||||
|
"community_title": "ຮ່ວມກັນສ້າງ ແລະ ພັດທະນາ Open CAD Studio.",
|
||||||
|
"community_text": "ລາຍງານບັນຫາ, ຮ່ວມຂຽນໂຄດ ຫຼື ສະໜັບສະໜູນການພັດທະນາ.",
|
||||||
|
"github": "ເບິ່ງເທິງ GitHub",
|
||||||
|
"sponsor": "ສະໜັບສະໜູນການພັດທະນາ",
|
||||||
|
"footer": "ໂປຣແກຣມຂຽນແບບເຕັກນິກ ແລະ ສ້າງຮູບຊົງແຂງ 3 ມິຕິ ແບບໂອເພນຊອດ.",
|
||||||
|
"web_app": "ເວັບແອັບ",
|
||||||
|
"download_nav": "ດາວໂຫຼດ",
|
||||||
|
"discussions": "ກະດານສົນທະນາ"
|
||||||
|
}
|
||||||
+2
-1
@@ -80,7 +80,7 @@ const WEB_ALIAS_KEY: &str = "opencadstudio.aliases";
|
|||||||
/// migrated forward (see `introduced_at` / `migrate_aliases`). Version 1 shipped
|
/// migrated forward (see `introduced_at` / `migrate_aliases`). Version 1 shipped
|
||||||
/// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases; version 3
|
/// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases; version 3
|
||||||
/// introduced HB (HATCHTOBACK); version 4 introduced ER (EXTERNALREFERENCES).
|
/// introduced HB (HATCHTOBACK); version 4 introduced ER (EXTERNALREFERENCES).
|
||||||
const DEFAULT_ALIASES_VERSION: u32 = 5;
|
const DEFAULT_ALIASES_VERSION: u32 = 6;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
const WEB_ALIAS_VERSION_KEY: &str = "opencadstudio.aliases.version";
|
const WEB_ALIAS_VERSION_KEY: &str = "opencadstudio.aliases.version";
|
||||||
@@ -100,6 +100,7 @@ fn introduced_at(version: u32) -> &'static [(&'static str, &'static str)] {
|
|||||||
3 => &[("HB", "HATCHTOBACK")],
|
3 => &[("HB", "HATCHTOBACK")],
|
||||||
4 => &[("ER", "EXTERNALREFERENCES")],
|
4 => &[("ER", "EXTERNALREFERENCES")],
|
||||||
5 => &[("-ATT", "-ATTDEF")],
|
5 => &[("-ATT", "-ATTDEF")],
|
||||||
|
6 => &[("VV", "QLAYER")],
|
||||||
_ => &[],
|
_ => &[],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-1
@@ -876,6 +876,42 @@ impl OpenCADStudio {
|
|||||||
for h in explicit {
|
for h in explicit {
|
||||||
self.tabs[i].scene.select_entity(h, false);
|
self.tabs[i].scene.select_entity(h, false);
|
||||||
}
|
}
|
||||||
|
// Or by a window / crossing box in world XY, through the
|
||||||
|
// viewport's own box test.
|
||||||
|
if let Some(window) = req["window"].as_array() {
|
||||||
|
let corner = |index: usize| window.get(index).and_then(Value::as_f64);
|
||||||
|
let (Some(x1), Some(y1), Some(x2), Some(y2)) =
|
||||||
|
(corner(0), corner(1), corner(2), corner(3))
|
||||||
|
else {
|
||||||
|
return err("select: window needs [x1, y1, x2, y2]");
|
||||||
|
};
|
||||||
|
let crossing = req["crossing"].as_bool().unwrap_or(false);
|
||||||
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
|
||||||
|
let tile = self.tabs[i].scene.active_model_tile_bounds(vw, vh);
|
||||||
|
let bounds = iced::Rectangle {
|
||||||
|
width: tile.width,
|
||||||
|
height: tile.height,
|
||||||
|
..iced::Rectangle::default()
|
||||||
|
};
|
||||||
|
let screen = |x: f64, y: f64| {
|
||||||
|
let camera = self.tabs[i].scene.camera.borrow();
|
||||||
|
camera.project(glam::DVec3::new(x, y, 0.0), bounds)
|
||||||
|
};
|
||||||
|
let (Some(a), Some(p)) = (screen(x1, y1), screen(x2, y2)) else {
|
||||||
|
return err("select: the window is not in view");
|
||||||
|
};
|
||||||
|
let (mut handles, _) = self.box_selection_handles(
|
||||||
|
i,
|
||||||
|
&None,
|
||||||
|
bounds,
|
||||||
|
iced::Point::new(a.x, a.y),
|
||||||
|
iced::Point::new(p.x, p.y),
|
||||||
|
crossing,
|
||||||
|
);
|
||||||
|
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
||||||
|
self.tabs[i].scene.select_entities(&handles);
|
||||||
|
self.tabs[i].scene.expand_selection_for_groups(&handles);
|
||||||
|
}
|
||||||
// Or by type / layer.
|
// Or by type / layer.
|
||||||
let type_filter = req["type"].as_str();
|
let type_filter = req["type"].as_str();
|
||||||
let layer_filter = req["layer"].as_str();
|
let layer_filter = req["layer"].as_str();
|
||||||
@@ -1790,7 +1826,7 @@ mod tests {
|
|||||||
viewport.id = 2;
|
viewport.id = 2;
|
||||||
viewport.width = 100.0;
|
viewport.width = 100.0;
|
||||||
viewport.height = 50.0;
|
viewport.height = 50.0;
|
||||||
viewport.status.is_on = true;
|
viewport.turn_on();
|
||||||
scene.add_entity(codec::EntityType::Viewport(viewport));
|
scene.add_entity(codec::EntityType::Viewport(viewport));
|
||||||
for entity in scene.document.entities_mut() {
|
for entity in scene.document.entities_mut() {
|
||||||
if let codec::EntityType::Viewport(viewport) = entity {
|
if let codec::EntityType::Viewport(viewport) = entity {
|
||||||
|
|||||||
@@ -308,11 +308,23 @@ impl OpenCADStudio {
|
|||||||
// populate_missing_meshes_from_document / bumps … CALLER-SIDE
|
// populate_missing_meshes_from_document / bumps … CALLER-SIDE
|
||||||
let clipboard = self.clipboard.clone();
|
let clipboard = self.clipboard.clone();
|
||||||
let deps = self.clipboard_deps.clone();
|
let deps = self.clipboard_deps.clone();
|
||||||
|
// Into the space new entities go: the edited block, the layout's
|
||||||
|
// paper space (no viewport active), or model space.
|
||||||
|
let scene = &self.tabs[i].scene;
|
||||||
|
let layout = scene.current_layout.clone();
|
||||||
|
let space = match scene.block_edit_block {
|
||||||
|
Some(block) => PasteSpace::Block(block),
|
||||||
|
None if layout != "Model" && scene.active_viewport.is_none() => {
|
||||||
|
PasteSpace::Layout(&layout)
|
||||||
|
}
|
||||||
|
None => PasteSpace::Model,
|
||||||
|
};
|
||||||
let (by_index, handle_map) = paste_entities_kernel(
|
let (by_index, handle_map) = paste_entities_kernel(
|
||||||
&mut self.tabs[i].scene.document,
|
&mut self.tabs[i].scene.document,
|
||||||
&clipboard,
|
&clipboard,
|
||||||
&deps,
|
&deps,
|
||||||
translate.as_ref(),
|
translate.as_ref(),
|
||||||
|
space,
|
||||||
);
|
);
|
||||||
// Batched equivalent of `add_entity`'s per-add bump (same pattern as
|
// Batched equivalent of `add_entity`'s per-add bump (same pattern as
|
||||||
// `Scene::add_entities`): fresh top-level solids tessellate once here
|
// `Scene::add_entities`): fresh top-level solids tessellate once here
|
||||||
@@ -473,11 +485,22 @@ pub(crate) fn merge_block_defs(
|
|||||||
/// `recreate_groups`, `duplicate_parametric_constraints_for`,
|
/// `recreate_groups`, `duplicate_parametric_constraints_for`,
|
||||||
/// `sync_displayed_annotation_context`, undo/dirty/selection/echo/panels).
|
/// `sync_displayed_annotation_context`, undo/dirty/selection/echo/panels).
|
||||||
/// See the per-helper report in `finalize_paste`.
|
/// See the per-helper report in `finalize_paste`.
|
||||||
|
/// Where pasted entities go, the way `Scene::add_entity` routes a new one.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum PasteSpace<'a> {
|
||||||
|
Model,
|
||||||
|
/// A layout's paper space, when no viewport of it is active.
|
||||||
|
Layout(&'a str),
|
||||||
|
/// The block definition being edited (BEDIT).
|
||||||
|
Block(Handle),
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn paste_entities_kernel(
|
pub(crate) fn paste_entities_kernel(
|
||||||
doc: &mut codec::CadDocument,
|
doc: &mut codec::CadDocument,
|
||||||
entities: &[codec::EntityType],
|
entities: &[codec::EntityType],
|
||||||
deps: &crate::app::ClipboardDeps,
|
deps: &crate::app::ClipboardDeps,
|
||||||
translate: Option<&crate::command::EntityTransform>,
|
translate: Option<&crate::command::EntityTransform>,
|
||||||
|
space: PasteSpace,
|
||||||
) -> (Vec<Handle>, rustc_hash::FxHashMap<Handle, Handle>) {
|
) -> (Vec<Handle>, rustc_hash::FxHashMap<Handle, Handle>) {
|
||||||
// Shared table-record merge (also used by `merge_dependencies`): recreate
|
// Shared table-record merge (also used by `merge_dependencies`): recreate
|
||||||
// missing records with fresh handles from the target document.
|
// missing records with fresh handles from the target document.
|
||||||
@@ -515,7 +538,7 @@ pub(crate) fn paste_entities_kernel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
add_entity_clone_doc(doc, entity)
|
add_entity_clone_doc(doc, entity, space)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -557,6 +580,8 @@ pub(crate) fn paste_entities_kernel(
|
|||||||
// A pasted associative hatch follows the pasted boundary, or drops the
|
// A pasted associative hatch follows the pasted boundary, or drops the
|
||||||
// association when the boundary was not pasted with it. (#1370)
|
// association when the boundary was not pasted with it. (#1370)
|
||||||
crate::scene::remap_hatch_associations(doc, &handle_map);
|
crate::scene::remap_hatch_associations(doc, &handle_map);
|
||||||
|
// Reactors on the source drawing's objects do not travel with the paste.
|
||||||
|
crate::scene::remap_copied_reactors(doc, &handle_map);
|
||||||
let leader_links: Vec<(Handle, Handle)> = entities
|
let leader_links: Vec<(Handle, Handle)> = entities
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|source| {
|
.filter_map(|source| {
|
||||||
@@ -583,9 +608,13 @@ pub(crate) fn paste_entities_kernel(
|
|||||||
/// `Scene::add_entity_clone` — fresh inline sub-handles (INSERT attributes,
|
/// `Scene::add_entity_clone` — fresh inline sub-handles (INSERT attributes,
|
||||||
/// 3D-polyline vertices; `reset_clone_subhandles` is `scene`-private so the
|
/// 3D-polyline vertices; `reset_clone_subhandles` is `scene`-private so the
|
||||||
/// two lines are mirrored here), NULL top-level/owner handles, then
|
/// two lines are mirrored here), NULL top-level/owner handles, then
|
||||||
/// `doc.add_entity`. The `entity_mode` default (Model vs paper space) and all
|
/// adds it to `space`. Tessellation/render caching stays caller-side in the
|
||||||
/// tessellation/render caching stay caller-side in the Scene shell.
|
/// Scene shell.
|
||||||
fn add_entity_clone_doc(doc: &mut codec::CadDocument, mut entity: codec::EntityType) -> Handle {
|
fn add_entity_clone_doc(
|
||||||
|
doc: &mut codec::CadDocument,
|
||||||
|
mut entity: codec::EntityType,
|
||||||
|
space: PasteSpace,
|
||||||
|
) -> Handle {
|
||||||
match &mut entity {
|
match &mut entity {
|
||||||
codec::EntityType::Insert(ins) => {
|
codec::EntityType::Insert(ins) => {
|
||||||
for att in ins.attributes.iter_mut() {
|
for att in ins.attributes.iter_mut() {
|
||||||
@@ -601,7 +630,15 @@ fn add_entity_clone_doc(doc: &mut codec::CadDocument, mut entity: codec::EntityT
|
|||||||
}
|
}
|
||||||
entity.common_mut().handle = Handle::NULL;
|
entity.common_mut().handle = Handle::NULL;
|
||||||
entity.common_mut().owner_handle = Handle::NULL;
|
entity.common_mut().owner_handle = Handle::NULL;
|
||||||
doc.add_entity(entity).unwrap_or(Handle::NULL)
|
match space {
|
||||||
|
PasteSpace::Model => doc.add_entity(entity),
|
||||||
|
PasteSpace::Layout(layout) => doc.add_entity_to_layout(entity, layout),
|
||||||
|
PasteSpace::Block(block) => {
|
||||||
|
entity.common_mut().owner_handle = block;
|
||||||
|
doc.add_entity(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap_or(Handle::NULL)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Document-level verbatim block-definition recreate: the `&mut CadDocument`
|
/// Document-level verbatim block-definition recreate: the `&mut CadDocument`
|
||||||
@@ -1149,8 +1186,13 @@ mod paste_entities_kernel_tests {
|
|||||||
let mut dst = codec::CadDocument::new();
|
let mut dst = codec::CadDocument::new();
|
||||||
let delta = glam::DVec3::new(5.0, 7.0, 0.0);
|
let delta = glam::DVec3::new(5.0, 7.0, 0.0);
|
||||||
let translate = crate::command::EntityTransform::Translate(delta);
|
let translate = crate::command::EntityTransform::Translate(delta);
|
||||||
let (handles, handle_map) =
|
let (handles, handle_map) = paste_entities_kernel(
|
||||||
paste_entities_kernel(&mut dst, &entities, &deps, Some(&translate));
|
&mut dst,
|
||||||
|
&entities,
|
||||||
|
&deps,
|
||||||
|
Some(&translate),
|
||||||
|
crate::app::PasteSpace::Model,
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(handles.len(), 1);
|
assert_eq!(handles.len(), 1);
|
||||||
let pasted = handles[0];
|
let pasted = handles[0];
|
||||||
@@ -1189,7 +1231,13 @@ mod paste_entities_kernel_tests {
|
|||||||
let (entities, deps) = copy_to_clipboard_kernel(&seed, &[leader_handle, note]);
|
let (entities, deps) = copy_to_clipboard_kernel(&seed, &[leader_handle, note]);
|
||||||
|
|
||||||
let mut dst = codec::CadDocument::new();
|
let mut dst = codec::CadDocument::new();
|
||||||
let (handles, _) = paste_entities_kernel(&mut dst, &entities, &deps, None);
|
let (handles, _) = paste_entities_kernel(
|
||||||
|
&mut dst,
|
||||||
|
&entities,
|
||||||
|
&deps,
|
||||||
|
None,
|
||||||
|
crate::app::PasteSpace::Model,
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(handles.len(), 2);
|
assert_eq!(handles.len(), 2);
|
||||||
let pasted_leader = match dst.get_entity(handles[0]) {
|
let pasted_leader = match dst.get_entity(handles[0]) {
|
||||||
@@ -1230,7 +1278,13 @@ mod paste_entities_kernel_tests {
|
|||||||
|
|
||||||
let mut dst = codec::CadDocument::new();
|
let mut dst = codec::CadDocument::new();
|
||||||
let before: Vec<Handle> = dst.objects.keys().copied().collect();
|
let before: Vec<Handle> = dst.objects.keys().copied().collect();
|
||||||
let (handles, _) = paste_entities_kernel(&mut dst, &entities, &deps, None);
|
let (handles, _) = paste_entities_kernel(
|
||||||
|
&mut dst,
|
||||||
|
&entities,
|
||||||
|
&deps,
|
||||||
|
None,
|
||||||
|
crate::app::PasteSpace::Model,
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(handles.len(), 1);
|
assert_eq!(handles.len(), 1);
|
||||||
let pasted = handles[0];
|
let pasted = handles[0];
|
||||||
@@ -1294,8 +1348,13 @@ mod headless_reuse_proof_tests {
|
|||||||
|
|
||||||
let mut doc_b = codec::CadDocument::new();
|
let mut doc_b = codec::CadDocument::new();
|
||||||
let translate = crate::command::EntityTransform::Translate(glam::DVec3::new(5.0, 7.0, 0.0));
|
let translate = crate::command::EntityTransform::Translate(glam::DVec3::new(5.0, 7.0, 0.0));
|
||||||
let (handles, handle_map) =
|
let (handles, handle_map) = paste_entities_kernel(
|
||||||
paste_entities_kernel(&mut doc_b, &entities, &deps, Some(&translate));
|
&mut doc_b,
|
||||||
|
&entities,
|
||||||
|
&deps,
|
||||||
|
Some(&translate),
|
||||||
|
crate::app::PasteSpace::Model,
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(handles.len(), 1);
|
assert_eq!(handles.len(), 1);
|
||||||
let pasted = handles[0];
|
let pasted = handles[0];
|
||||||
@@ -1347,4 +1406,58 @@ mod headless_reuse_proof_tests {
|
|||||||
other => panic!("expected matched Line, got {other:?}"),
|
other => panic!("expected matched Line, got {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn paste_owner_override_routes_entity_into_block_record() {
|
||||||
|
let mut source = codec::CadDocument::new();
|
||||||
|
|
||||||
|
let line = codec::entities::Line::from_points(
|
||||||
|
codec::types::Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
codec::types::Vector3::new(10.0, 0.0, 0.0),
|
||||||
|
);
|
||||||
|
let source_handle = source
|
||||||
|
.add_entity(codec::EntityType::Line(line))
|
||||||
|
.expect("source line");
|
||||||
|
|
||||||
|
let (entities, deps) = copy_to_clipboard_kernel(&source, &[source_handle]);
|
||||||
|
|
||||||
|
let mut target = codec::CadDocument::new();
|
||||||
|
|
||||||
|
let block_handle = target.allocate_handle();
|
||||||
|
let mut block = codec::tables::BlockRecord::new("BEDIT_TARGET");
|
||||||
|
block.handle = block_handle;
|
||||||
|
target
|
||||||
|
.block_records
|
||||||
|
.add(block)
|
||||||
|
.expect("target block record");
|
||||||
|
|
||||||
|
let (handles, _) = paste_entities_kernel(
|
||||||
|
&mut target,
|
||||||
|
&entities,
|
||||||
|
&deps,
|
||||||
|
None,
|
||||||
|
crate::app::PasteSpace::Block(block_handle),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(handles.len(), 1);
|
||||||
|
let pasted = handles[0];
|
||||||
|
assert!(!pasted.is_null());
|
||||||
|
|
||||||
|
let entity = target.get_entity(pasted).expect("pasted entity must exist");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
entity.common().owner_handle,
|
||||||
|
block_handle,
|
||||||
|
"paste inside BEDIT must belong to the edited block record"
|
||||||
|
);
|
||||||
|
|
||||||
|
let record = target
|
||||||
|
.block_records
|
||||||
|
.get("BEDIT_TARGET")
|
||||||
|
.expect("block record must still exist");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
record.entity_handles.contains(&pasted),
|
||||||
|
"pasted entity must be registered as a member of the edited block"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ mod utilities;
|
|||||||
mod viewport;
|
mod viewport;
|
||||||
|
|
||||||
pub(crate) use clipboard::{
|
pub(crate) use clipboard::{
|
||||||
copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference,
|
copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, PasteSpace,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
@@ -204,6 +204,7 @@ impl OpenCADStudio {
|
|||||||
if was_active && self.tabs[i].active_cmd.is_none() {
|
if was_active && self.tabs[i].active_cmd.is_none() {
|
||||||
self.tabs[i].scene.set_hover_highlight(None);
|
self.tabs[i].scene.set_hover_highlight(None);
|
||||||
self.command_line.set_step_options(Vec::new());
|
self.command_line.set_step_options(Vec::new());
|
||||||
|
self.command_line.clear_contextual_suggestions();
|
||||||
self.restore_add_selected_defaults();
|
self.restore_add_selected_defaults();
|
||||||
self.tabs[i].pending_pause_tokens = None;
|
self.tabs[i].pending_pause_tokens = None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -472,6 +472,8 @@ impl OpenCADStudio {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
// Group 294: a base point the user picked.
|
// Group 294: a base point the user picked.
|
||||||
let picked_base = options.base_point.is_some();
|
let picked_base = options.base_point.is_some();
|
||||||
|
let several = profiles.len() > 1;
|
||||||
|
let picked = options;
|
||||||
let options = sweep_model::sweep_selection_options(&selection, options);
|
let options = sweep_model::sweep_selection_options(&selection, options);
|
||||||
let pending = if profiles.is_empty() {
|
let pending = if profiles.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -524,7 +526,10 @@ impl OpenCADStudio {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let deletes_path = crate::app::delobj_deletes_auxiliary(delete_objects, surface);
|
// An associative surface keeps the profile and path it reads.
|
||||||
|
let associative = surface && sweep_model::surface_associativity();
|
||||||
|
let deletes_path =
|
||||||
|
!associative && crate::app::delobj_deletes_auxiliary(delete_objects, surface);
|
||||||
if deletes_path && self.tabs[i].scene.is_layer_locked(path_handle) {
|
if deletes_path && self.tabs[i].scene.is_layer_locked(path_handle) {
|
||||||
failed += 1;
|
failed += 1;
|
||||||
continue;
|
continue;
|
||||||
@@ -537,6 +542,24 @@ impl OpenCADStudio {
|
|||||||
created,
|
created,
|
||||||
&expressions,
|
&expressions,
|
||||||
);
|
);
|
||||||
|
if associative {
|
||||||
|
// A picked base point, or the one several profiles share.
|
||||||
|
let linked = crate::command::SweepOptions {
|
||||||
|
base_point: if picked_base || several {
|
||||||
|
options.and_then(|o| o.base_point)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
..picked
|
||||||
|
};
|
||||||
|
sweep_model::link_sweep_sources(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
created,
|
||||||
|
handle,
|
||||||
|
path_handle,
|
||||||
|
&linked,
|
||||||
|
);
|
||||||
|
}
|
||||||
created
|
created
|
||||||
} else {
|
} else {
|
||||||
self.add_solid_model(
|
self.add_solid_model(
|
||||||
@@ -549,7 +572,7 @@ impl OpenCADStudio {
|
|||||||
failed += 1;
|
failed += 1;
|
||||||
} else {
|
} else {
|
||||||
created_handles.push(created);
|
created_handles.push(created);
|
||||||
if delete_profiles {
|
if delete_profiles && !associative {
|
||||||
consumed.push(handle);
|
consumed.push(handle);
|
||||||
}
|
}
|
||||||
if deletes_path {
|
if deletes_path {
|
||||||
|
|||||||
@@ -933,14 +933,17 @@ mod delobj_tests {
|
|||||||
expected,
|
expected,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert_eq!(
|
// A swept surface is associative by default (SURFACEASSOCIATIVITY 1)
|
||||||
sweep_source_presence(2, crate::command::ExtrudeMode::Surface),
|
// and keeps the profile and path it follows, whatever DELOBJ says;
|
||||||
(false, false),
|
// DELOBJ reaches surfaces only with associativity off, a profile-wide
|
||||||
);
|
// switch this parallel test leaves alone.
|
||||||
assert_eq!(
|
assert!(crate::scene::model::sweep_model::surface_associativity());
|
||||||
sweep_source_presence(3, crate::command::ExtrudeMode::Surface),
|
for value in [2, 3] {
|
||||||
(false, true),
|
assert_eq!(
|
||||||
);
|
sweep_source_presence(value, crate::command::ExtrudeMode::Surface),
|
||||||
|
(true, true),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1128,3 +1131,351 @@ mod dispatched_command_task_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// PW-1 BEDIT characterization tests.
|
||||||
|
///
|
||||||
|
/// Two groups:
|
||||||
|
/// - `locked_*`: behavior Phase 1 must preserve (save keeps edits, discard
|
||||||
|
/// restores the definition, refusals stay refused).
|
||||||
|
/// - `documenting_*`: today's questionable behavior, pinned so the refactor
|
||||||
|
/// cannot change it silently. Each one is expected to flip in Phase 1.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod bedit_characterization_tests {
|
||||||
|
use super::super::*;
|
||||||
|
use codec::entities::{Circle, Insert, Line};
|
||||||
|
use codec::types::Vector3;
|
||||||
|
use codec::EntityType;
|
||||||
|
|
||||||
|
/// Fresh document with one block definition `BLK` (one line) and one
|
||||||
|
/// model-space INSERT referencing it. Returns the INSERT handle and the
|
||||||
|
/// definition's member handles.
|
||||||
|
fn setup_block_and_insert(app: &mut OpenCADStudio) -> (Handle, Vec<Handle>) {
|
||||||
|
let _ = app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let tab = app.active_tab;
|
||||||
|
app.tabs[tab].scene.define_block_raw(
|
||||||
|
"BLK",
|
||||||
|
Vector3::ZERO,
|
||||||
|
vec![EntityType::Line(Line::from_points(
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::new(10.0, 0.0, 0.0),
|
||||||
|
))],
|
||||||
|
);
|
||||||
|
let insert = app.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.add_entity(EntityType::Insert(Insert::new(
|
||||||
|
"BLK",
|
||||||
|
Vector3::new(100.0, 0.0, 0.0),
|
||||||
|
)))
|
||||||
|
.expect("insert");
|
||||||
|
let members = block_members(app, "BLK");
|
||||||
|
(insert, members)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_members(app: &OpenCADStudio, name: &str) -> Vec<Handle> {
|
||||||
|
app.tabs[app.active_tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get(name)
|
||||||
|
.map(|br| br.entity_handles.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Definition state as (handle, Debug form) in member order. The Debug
|
||||||
|
/// form stands in for PartialEq, which EntityType does not provide.
|
||||||
|
fn definition_state(app: &OpenCADStudio, name: &str) -> Vec<(Handle, String)> {
|
||||||
|
let tab = app.active_tab;
|
||||||
|
block_members(app, name)
|
||||||
|
.into_iter()
|
||||||
|
.map(|h| {
|
||||||
|
let e = app.tabs[tab].scene.document.get_entity(h);
|
||||||
|
(h, format!("{e:?}"))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undo_depth(app: &OpenCADStudio) -> usize {
|
||||||
|
app.tabs[app.active_tab].history.undo_stack.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_bedit(app: &mut OpenCADStudio, insert: Handle) {
|
||||||
|
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", insert.value()));
|
||||||
|
assert!(
|
||||||
|
app.tabs[app.active_tab].active_block_edit.is_some(),
|
||||||
|
"setup: BEDIT_BEGIN should open a block-edit session"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_circle_in_bedit(app: &mut OpenCADStudio) -> Handle {
|
||||||
|
let tab = app.active_tab;
|
||||||
|
app.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.add_entity(EntityType::Circle(Circle::from_coords(0.0, 0.0, 0.0, 2.0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- locked: must survive Phase 1 ----
|
||||||
|
|
||||||
|
/// Locked: save keeps the entity added inside the session.
|
||||||
|
#[test]
|
||||||
|
fn locked_save_keeps_entity_added_in_session() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, _) = setup_block_and_insert(&mut app);
|
||||||
|
begin_bedit(&mut app, insert);
|
||||||
|
add_circle_in_bedit(&mut app);
|
||||||
|
let _ = app.dispatch_command("BEDIT_SAVE");
|
||||||
|
|
||||||
|
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
||||||
|
assert_eq!(
|
||||||
|
block_members(&app, "BLK").len(),
|
||||||
|
2,
|
||||||
|
"save keeps the new entity"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Locked: discard restores the definition to its entry state exactly
|
||||||
|
/// (same handles, same geometry, same order).
|
||||||
|
#[test]
|
||||||
|
fn locked_discard_restores_definition_to_entry_state() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, _) = setup_block_and_insert(&mut app);
|
||||||
|
let entry = definition_state(&app, "BLK");
|
||||||
|
begin_bedit(&mut app, insert);
|
||||||
|
add_circle_in_bedit(&mut app);
|
||||||
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||||
|
|
||||||
|
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
||||||
|
assert_eq!(definition_state(&app, "BLK"), entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Locked: refusals. A non-INSERT is rejected and no session opens.
|
||||||
|
#[test]
|
||||||
|
fn locked_bedit_refuses_non_insert_object() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let _ = app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let tab = app.active_tab;
|
||||||
|
let line = app.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.add_entity(EntityType::Line(Line::from_points(
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::new(1.0, 0.0, 0.0),
|
||||||
|
)))
|
||||||
|
.expect("line");
|
||||||
|
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", line.value()));
|
||||||
|
assert!(app.tabs[tab].active_block_edit.is_none());
|
||||||
|
assert!(app.tabs[tab].block_edits.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Locked: refusal. The `BEDIT` picker command is rejected while a
|
||||||
|
/// REFEDIT session is active and no BEDIT session opens.
|
||||||
|
#[test]
|
||||||
|
fn locked_bedit_picker_refused_while_refedit_active() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, _) = setup_block_and_insert(&mut app);
|
||||||
|
let tab = app.active_tab;
|
||||||
|
let _ = app.dispatch_command(&format!("REFEDIT_BEGIN:{}", insert.value()));
|
||||||
|
assert!(
|
||||||
|
app.tabs[tab].refedit_session.is_some(),
|
||||||
|
"setup: REFEDIT_BEGIN should open a REFEDIT session"
|
||||||
|
);
|
||||||
|
let _ = app.dispatch_command("BEDIT");
|
||||||
|
assert!(app.tabs[tab].active_block_edit.is_none());
|
||||||
|
assert!(app.tabs[tab].block_edits.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Documenting: the REFEDIT guard lives only in the `BEDIT` picker arm.
|
||||||
|
/// `BEDIT_BEGIN:<handle>` (used by the palette "Edit" action) has no
|
||||||
|
/// REFEDIT check and opens a session while REFEDIT is active. Expected to
|
||||||
|
/// flip in Phase 1 once the guard moves into the begin path.
|
||||||
|
#[test]
|
||||||
|
fn documenting_bedit_begin_bypasses_refedit_guard() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, _) = setup_block_and_insert(&mut app);
|
||||||
|
let tab = app.active_tab;
|
||||||
|
let _ = app.dispatch_command(&format!("REFEDIT_BEGIN:{}", insert.value()));
|
||||||
|
assert!(app.tabs[tab].refedit_session.is_some());
|
||||||
|
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", insert.value()));
|
||||||
|
assert!(
|
||||||
|
app.tabs[tab].active_block_edit.is_some(),
|
||||||
|
"documenting: BEDIT_BEGIN opens a session despite an active REFEDIT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Locked: refusal. An xref definition cannot be edited.
|
||||||
|
#[test]
|
||||||
|
fn locked_bedit_refuses_xref_definition() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, _) = setup_block_and_insert(&mut app);
|
||||||
|
let tab = app.active_tab;
|
||||||
|
app.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get_mut("BLK")
|
||||||
|
.expect("block record")
|
||||||
|
.flags
|
||||||
|
.is_xref = true;
|
||||||
|
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", insert.value()));
|
||||||
|
assert!(app.tabs[tab].active_block_edit.is_none());
|
||||||
|
assert!(app.tabs[tab].block_edits.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- documenting: today's behavior, expected to flip in Phase 1 ----
|
||||||
|
|
||||||
|
/// Documenting: modify an existing member, discard, then undo once. The
|
||||||
|
/// undo target is the BEDIT_BEGIN entry. Records whatever the stack does
|
||||||
|
/// with the stale before-image (ISS-02). The assertion pins the current
|
||||||
|
/// result; Phase 1 must justify any change.
|
||||||
|
#[test]
|
||||||
|
fn documenting_modify_discard_then_undo_probes_stale_before_image() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, members) = setup_block_and_insert(&mut app);
|
||||||
|
let line = members[0];
|
||||||
|
let entry = definition_state(&app, "BLK");
|
||||||
|
begin_bedit(&mut app, insert);
|
||||||
|
|
||||||
|
let tab = app.active_tab;
|
||||||
|
// Record the undo step before the mutation so the recorder captures it.
|
||||||
|
app.push_undo_snapshot(tab, "modify inside BEDIT");
|
||||||
|
app.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity_mut(line)
|
||||||
|
.map(|e| {
|
||||||
|
if let EntityType::Line(l) = e {
|
||||||
|
l.end = Vector3::new(99.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("line in definition");
|
||||||
|
app.finish_pending_history(tab);
|
||||||
|
|
||||||
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||||
|
app.finish_pending_history(tab);
|
||||||
|
assert_eq!(
|
||||||
|
definition_state(&app, "BLK"),
|
||||||
|
entry,
|
||||||
|
"discard restores entry"
|
||||||
|
);
|
||||||
|
|
||||||
|
app.undo_steps(1);
|
||||||
|
// Pinned: the state after one undo, observed on current code.
|
||||||
|
assert_eq!(
|
||||||
|
definition_state(&app, "BLK"),
|
||||||
|
entry,
|
||||||
|
"documenting: undo after discard restores the entry state (ISS-02 not reproduced here)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Documenting: undo during a session crosses the entry boundary. A
|
||||||
|
/// single undo after an in-session edit may revert the BEDIT_BEGIN step.
|
||||||
|
#[test]
|
||||||
|
fn documenting_undo_inside_session_crosses_entry_boundary() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, _) = setup_block_and_insert(&mut app);
|
||||||
|
begin_bedit(&mut app, insert);
|
||||||
|
let tab = app.active_tab;
|
||||||
|
app.push_undo_snapshot(tab, "add circle in BEDIT");
|
||||||
|
add_circle_in_bedit(&mut app);
|
||||||
|
app.finish_pending_history(tab);
|
||||||
|
assert_eq!(block_members(&app, "BLK").len(), 2);
|
||||||
|
|
||||||
|
app.undo_steps(1);
|
||||||
|
// Documenting: undo removes the circle from the entity store but leaves
|
||||||
|
// its handle in the block record, so the definition holds a dangling
|
||||||
|
// member. Expected to flip in Phase 1 (members should match the store).
|
||||||
|
let members = block_members(&app, "BLK");
|
||||||
|
assert_eq!(
|
||||||
|
members.len(),
|
||||||
|
2,
|
||||||
|
"documenting: block record keeps the undone handle"
|
||||||
|
);
|
||||||
|
let dangling = members
|
||||||
|
.iter()
|
||||||
|
.filter(|h| app.tabs[tab].scene.document.get_entity(**h).is_none())
|
||||||
|
.count();
|
||||||
|
assert_eq!(dangling, 1, "documenting: one member handle has no entity");
|
||||||
|
assert!(
|
||||||
|
app.tabs[tab].active_block_edit.is_some(),
|
||||||
|
"documenting: undo does not close the session"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Documenting: undoing the BEDIT_BEGIN step itself, while the session is
|
||||||
|
/// still open, leaves the editor open. Records whether the undo reaches
|
||||||
|
/// across the entry boundary.
|
||||||
|
#[test]
|
||||||
|
fn documenting_undo_across_entry_leaves_session_open() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, _) = setup_block_and_insert(&mut app);
|
||||||
|
let tab = app.active_tab;
|
||||||
|
begin_bedit(&mut app, insert);
|
||||||
|
app.finish_pending_history(tab);
|
||||||
|
let depth_after_begin = undo_depth(&app);
|
||||||
|
assert!(depth_after_begin >= 1, "BEDIT_BEGIN recorded an undo step");
|
||||||
|
|
||||||
|
app.undo_steps(1);
|
||||||
|
assert!(
|
||||||
|
app.tabs[tab].active_block_edit.is_some(),
|
||||||
|
"documenting: undoing BEDIT_BEGIN does not close the open session"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Documenting: BEDIT on a block while another block is open nests a
|
||||||
|
/// second session. Records the resulting stack.
|
||||||
|
#[test]
|
||||||
|
fn documenting_nested_bedit_opens_second_session() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, _) = setup_block_and_insert(&mut app);
|
||||||
|
let tab = app.active_tab;
|
||||||
|
app.tabs[tab].scene.define_block_raw(
|
||||||
|
"OTHER",
|
||||||
|
Vector3::ZERO,
|
||||||
|
vec![EntityType::Line(Line::from_points(
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::new(5.0, 0.0, 0.0),
|
||||||
|
))],
|
||||||
|
);
|
||||||
|
let other_insert = app.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.add_entity(EntityType::Insert(Insert::new(
|
||||||
|
"OTHER",
|
||||||
|
Vector3::new(200.0, 0.0, 0.0),
|
||||||
|
)))
|
||||||
|
.expect("insert");
|
||||||
|
begin_bedit(&mut app, insert);
|
||||||
|
begin_bedit(&mut app, other_insert);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[tab].block_edits.len(),
|
||||||
|
2,
|
||||||
|
"documenting: a second BEDIT pushes a nested session"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Documenting: BEDIT_BEGIN pushes an undo entry, and BEDIT_SAVE and
|
||||||
|
/// BEDIT_DISCARD push none. Phase 1 may change this.
|
||||||
|
#[test]
|
||||||
|
fn documenting_save_and_discard_push_no_undo_entry() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (insert, _) = setup_block_and_insert(&mut app);
|
||||||
|
begin_bedit(&mut app, insert);
|
||||||
|
app.finish_pending_history(app.active_tab);
|
||||||
|
let after_begin = undo_depth(&app);
|
||||||
|
|
||||||
|
let _ = app.dispatch_command("BEDIT_SAVE");
|
||||||
|
app.finish_pending_history(app.active_tab);
|
||||||
|
assert_eq!(undo_depth(&app), after_begin, "save pushes no undo entry");
|
||||||
|
|
||||||
|
begin_bedit(&mut app, insert);
|
||||||
|
app.finish_pending_history(app.active_tab);
|
||||||
|
let before_discard = undo_depth(&app);
|
||||||
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||||
|
app.finish_pending_history(app.active_tab);
|
||||||
|
assert_eq!(
|
||||||
|
undo_depth(&app),
|
||||||
|
before_discard,
|
||||||
|
"discard pushes no undo entry (not undoable)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-21
@@ -1245,30 +1245,30 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
// EXPLMODE 0 keeps references that are not uniformly and
|
// EXPLMODE 0 keeps references that are not uniformly and
|
||||||
// positively scaled whole.
|
// positively scaled whole.
|
||||||
let selected: Vec<_> = if super::blockvars::explmode(
|
let selected: Vec<_> =
|
||||||
&self.tabs[i].scene.document,
|
if super::blockvars::explmode(&self.tabs[i].scene.document) == 0 {
|
||||||
) == 0
|
selected
|
||||||
{
|
.into_iter()
|
||||||
selected
|
.filter(|(_, entity)| match entity {
|
||||||
.into_iter()
|
codec::EntityType::Insert(ins) => {
|
||||||
.filter(|(_, entity)| match entity {
|
let (x, y, z) =
|
||||||
codec::EntityType::Insert(ins) => {
|
(ins.x_scale(), ins.y_scale(), ins.z_scale());
|
||||||
let (x, y, z) = (ins.x_scale(), ins.y_scale(), ins.z_scale());
|
let uniform = x > 0.0
|
||||||
let uniform =
|
&& (x - y).abs() <= 1e-10
|
||||||
x > 0.0 && (x - y).abs() <= 1e-10 && (x - z).abs() <= 1e-10;
|
&& (x - z).abs() <= 1e-10;
|
||||||
if !uniform {
|
if !uniform {
|
||||||
self.command_line.push_output(
|
self.command_line.push_output(
|
||||||
"The object is not uniformly and positively scaled.",
|
"The object is not uniformly and positively scaled.",
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
uniform
|
||||||
}
|
}
|
||||||
uniform
|
_ => true,
|
||||||
}
|
})
|
||||||
_ => true,
|
.collect()
|
||||||
})
|
} else {
|
||||||
.collect()
|
selected
|
||||||
} else {
|
};
|
||||||
selected
|
|
||||||
};
|
|
||||||
let replacements = plan_explode(&selected, &self.tabs[i].scene.document);
|
let replacements = plan_explode(&selected, &self.tabs[i].scene.document);
|
||||||
let exploded = replacements.len();
|
let exploded = replacements.len();
|
||||||
if exploded > 0 {
|
if exploded > 0 {
|
||||||
|
|||||||
@@ -1145,6 +1145,82 @@ impl OpenCADStudio {
|
|||||||
for (handle, _) in &selected {
|
for (handle, _) in &selected {
|
||||||
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
|
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
|
||||||
use crate::entities::traits::EntityTypeOps;
|
use crate::entities::traits::EntityTypeOps;
|
||||||
|
// A solid's properties come from its B-rep: exact
|
||||||
|
// where the kernel knows the shape, a fine mesh
|
||||||
|
// otherwise — not the display mesh.
|
||||||
|
let acis = match entity {
|
||||||
|
codec::EntityType::Solid3D(solid) => Some(&solid.acis_data),
|
||||||
|
codec::EntityType::Body(body) => Some(&body.acis_data),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(acis) = acis {
|
||||||
|
let properties =
|
||||||
|
crate::scene::convert::solid3d_tess::kernel_acis_body(acis)
|
||||||
|
.and_then(|body| kernel::brep::mass_properties(&body));
|
||||||
|
let Some(p) = properties else {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::tf!(
|
||||||
|
"MASSPROP: {} has no closed body to measure.",
|
||||||
|
crate::entities::names::dxf_name(entity),
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let [x, y, z] = p.centroid;
|
||||||
|
let [ix, iy, iz] = p.moment_of_inertia;
|
||||||
|
let [pxy, pyz, pzx] = p.product_of_inertia;
|
||||||
|
let [rx, ry, rz] = p.radii_of_gyration;
|
||||||
|
let [i1, i2, i3] = p.principal_moments;
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::tf!(
|
||||||
|
"{} Volume={:.4} Centroid=({:.4},{:.4},{:.4})",
|
||||||
|
crate::entities::names::dxf_name(entity),
|
||||||
|
p.volume,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
z,
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::tf!(
|
||||||
|
"Moments of inertia X={:.4} Y={:.4} Z={:.4}",
|
||||||
|
ix,
|
||||||
|
iy,
|
||||||
|
iz,
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::tf!(
|
||||||
|
"Products of inertia XY={:.4} YZ={:.4} ZX={:.4}",
|
||||||
|
pxy,
|
||||||
|
pyz,
|
||||||
|
pzx,
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::tf!(
|
||||||
|
"Radii of gyration X={:.4} Y={:.4} Z={:.4}",
|
||||||
|
rx,
|
||||||
|
ry,
|
||||||
|
rz,
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::tf!(
|
||||||
|
"Principal moments about centroid I={:.4} J={:.4} K={:.4}",
|
||||||
|
i1,
|
||||||
|
i2,
|
||||||
|
i3,
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(props) = entity.mass_props() {
|
if let Some(props) = entity.mass_props() {
|
||||||
self.command_line.push_output(
|
self.command_line.push_output(
|
||||||
crate::tf!(
|
crate::tf!(
|
||||||
|
|||||||
@@ -649,6 +649,102 @@ impl OpenCADStudio {
|
|||||||
.push_info(crate::t!("LAYUNISO: all layers restored.").as_ref());
|
.push_info(crate::t!("LAYUNISO: all layers restored.").as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"QLAYER" => {
|
||||||
|
use crate::modules::draw::layers::quick_layer::{
|
||||||
|
QuickLayerCommand, QuickLayerEntry,
|
||||||
|
};
|
||||||
|
|
||||||
|
let layers: Vec<QuickLayerEntry> = self.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.filter(|layer| !layer.name.starts_with('*'))
|
||||||
|
.map(|layer| {
|
||||||
|
QuickLayerEntry::new(
|
||||||
|
layer.name.clone(),
|
||||||
|
layer.color.clone(),
|
||||||
|
layer.visible,
|
||||||
|
layer.frozen,
|
||||||
|
layer.locked,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let selected: Vec<codec::Handle> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.selected_entities()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(handle, _)| handle)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let command = QuickLayerCommand::new_with_entries(layers, selected);
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd if cmd.starts_with("QLAYER_APPLY_HEX ") => {
|
||||||
|
use crate::modules::draw::layers::quick_layer::QuickLayerCommand;
|
||||||
|
|
||||||
|
let encoded = cmd.trim_start_matches("QLAYER_APPLY_HEX").trim();
|
||||||
|
|
||||||
|
let Some(requested) = QuickLayerCommand::decode_layer_name(encoded) else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("QLAYER: invalid encoded layer name.").as_ref());
|
||||||
|
return Some(Task::none());
|
||||||
|
};
|
||||||
|
|
||||||
|
let resolved = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.layers
|
||||||
|
.names()
|
||||||
|
.find(|name| name.eq_ignore_ascii_case(&requested))
|
||||||
|
.map(|name| name.to_string());
|
||||||
|
|
||||||
|
let Some(layer) = resolved else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::tf!("QLAYER: no layer named \"{requested}\".").as_ref());
|
||||||
|
return Some(Task::none());
|
||||||
|
};
|
||||||
|
|
||||||
|
let handles: Vec<codec::Handle> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.selected_entities()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(handle, _)| handle)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if handles.is_empty() {
|
||||||
|
match self.set_current_layer_name(i, &layer) {
|
||||||
|
Ok(()) => {
|
||||||
|
self.command_line.push_info(
|
||||||
|
crate::tf!("QLAYER: current layer set to \"{layer}\".").as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
self.command_line.push_error(&error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let count = handles.len();
|
||||||
|
|
||||||
|
self.apply_property_op(i, "QLAYER", &handles, |app, handle| {
|
||||||
|
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
|
||||||
|
crate::scene::view::dispatch::apply_common_prop(
|
||||||
|
entity, "layer", &layer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.ribbon.active_layer = layer.clone();
|
||||||
|
|
||||||
|
self.command_line.push_info(
|
||||||
|
crate::tf!("QLAYER: moved {count} object(s) to layer \"{layer}\".")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"LAYMATCH" | "LAYMCH" => {
|
"LAYMATCH" | "LAYMCH" => {
|
||||||
use crate::modules::draw::layers::match_layer::LayMatchCommand;
|
use crate::modules::draw::layers::match_layer::LayMatchCommand;
|
||||||
let dest: Vec<_> = self.tabs[i]
|
let dest: Vec<_> = self.tabs[i]
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"PDFOSNAP",
|
"PDFOSNAP",
|
||||||
"UOSNAP",
|
"UOSNAP",
|
||||||
"FIELDDISPLAY",
|
"FIELDDISPLAY",
|
||||||
|
"SURFACEASSOCIATIVITY",
|
||||||
"FIELDEVAL",
|
"FIELDEVAL",
|
||||||
"PDFIMPORTMODE",
|
"PDFIMPORTMODE",
|
||||||
"PDFIMPORTFILTER",
|
"PDFIMPORTFILTER",
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ impl OpenCADStudio {
|
|||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(name.clone())
|
Some(name.to_uppercase())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -320,6 +320,17 @@ impl OpenCADStudio {
|
|||||||
.map(|ds| ds.dimtxsty.clone()),
|
.map(|ds| ds.dimtxsty.clone()),
|
||||||
)
|
)
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_uppercase())
|
||||||
|
.collect();
|
||||||
|
// Complex linetypes reach their text and shape styles by handle.
|
||||||
|
let linetype_styles: rustc_hash::FxHashSet<codec::Handle> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.line_types
|
||||||
|
.iter()
|
||||||
|
.flat_map(|lt| lt.elements.iter())
|
||||||
|
.filter_map(|element| element.complex.as_ref())
|
||||||
|
.map(|complex| complex.style_handle)
|
||||||
.collect();
|
.collect();
|
||||||
// Linetypes: entity references plus every layer's own
|
// Linetypes: entity references plus every layer's own
|
||||||
// linetype — a ByLayer entity reaches it through the layer.
|
// linetype — a ByLayer entity reaches it through the layer.
|
||||||
@@ -344,6 +355,7 @@ impl OpenCADStudio {
|
|||||||
.map(|l| l.line_type.clone()),
|
.map(|l| l.line_type.clone()),
|
||||||
)
|
)
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_uppercase())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Live blocks are reachable from layouts and style roots
|
// Live blocks are reachable from layouts and style roots
|
||||||
@@ -409,7 +421,9 @@ impl OpenCADStudio {
|
|||||||
.document
|
.document
|
||||||
.layers
|
.layers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|l| l.name != "0" && !used_layers.contains(&l.name))
|
.filter(|l| {
|
||||||
|
l.name != "0" && !used_layers.contains(&l.name.to_uppercase())
|
||||||
|
})
|
||||||
.map(|l| l.name.clone())
|
.map(|l| l.name.clone())
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
@@ -421,7 +435,15 @@ impl OpenCADStudio {
|
|||||||
.document
|
.document
|
||||||
.text_styles
|
.text_styles
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|s| s.name != "Standard" && !used_text_styles.contains(&s.name))
|
// Unnamed shape-file records serve complex linetypes
|
||||||
|
// and are never listed for purging.
|
||||||
|
.filter(|s| {
|
||||||
|
!s.name.is_empty()
|
||||||
|
&& !s.is_shape_file
|
||||||
|
&& !s.name.eq_ignore_ascii_case("Standard")
|
||||||
|
&& !used_text_styles.contains(&s.name.to_uppercase())
|
||||||
|
&& !linetype_styles.contains(&s.handle)
|
||||||
|
})
|
||||||
.map(|s| s.name.clone())
|
.map(|s| s.name.clone())
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
@@ -436,7 +458,7 @@ impl OpenCADStudio {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|lt| {
|
.filter(|lt| {
|
||||||
!standard.iter().any(|s| s.eq_ignore_ascii_case(<.name))
|
!standard.iter().any(|s| s.eq_ignore_ascii_case(<.name))
|
||||||
&& !used_linetypes.contains(<.name)
|
&& !used_linetypes.contains(<.name.to_uppercase())
|
||||||
})
|
})
|
||||||
.map(|lt| lt.name.clone())
|
.map(|lt| lt.name.clone())
|
||||||
.collect()
|
.collect()
|
||||||
@@ -483,16 +505,23 @@ impl OpenCADStudio {
|
|||||||
snapshot_pushed = true;
|
snapshot_pushed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply removals (mutable)
|
// Apply removals (mutable). Count what actually went: an
|
||||||
for name in &layer_remove {
|
// entry the table cannot remove by name would otherwise be
|
||||||
self.tabs[i].scene.document.layers.remove(name);
|
// listed again on every pass and never let the loop end.
|
||||||
}
|
let doc = &mut self.tabs[i].scene.document;
|
||||||
for name in &style_remove {
|
let layers_gone = layer_remove
|
||||||
self.tabs[i].scene.document.text_styles.remove(name);
|
.iter()
|
||||||
}
|
.filter(|n| doc.layers.remove(n).is_some())
|
||||||
for name in <_remove {
|
.count();
|
||||||
self.tabs[i].scene.document.line_types.remove(name);
|
let styles_gone = style_remove
|
||||||
}
|
.iter()
|
||||||
|
.filter(|n| doc.text_styles.remove(n).is_some())
|
||||||
|
.count();
|
||||||
|
let lts_gone = lt_remove
|
||||||
|
.iter()
|
||||||
|
.filter(|n| doc.line_types.remove(n).is_some())
|
||||||
|
.count();
|
||||||
|
let mut blocks_gone = 0usize;
|
||||||
for name in &block_remove {
|
for name in &block_remove {
|
||||||
// Drop the block definition's member entities (and the
|
// Drop the block definition's member entities (and the
|
||||||
// BLOCK/ENDBLK delimiters) before the record so no orphaned
|
// BLOCK/ENDBLK delimiters) before the record so no orphaned
|
||||||
@@ -509,12 +538,23 @@ impl OpenCADStudio {
|
|||||||
for h in handles {
|
for h in handles {
|
||||||
self.tabs[i].scene.document.remove_entity(h);
|
self.tabs[i].scene.document.remove_entity(h);
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.document.block_records.remove(name);
|
if self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.remove(name)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
blocks_gone += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n_layers += layers_gone;
|
||||||
|
n_styles += styles_gone;
|
||||||
|
n_lts += lts_gone;
|
||||||
|
n_blocks += blocks_gone;
|
||||||
|
if layers_gone + styles_gone + lts_gone + blocks_gone == 0 {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
n_layers += layer_remove.len();
|
|
||||||
n_styles += style_remove.len();
|
|
||||||
n_lts += lt_remove.len();
|
|
||||||
n_blocks += block_remove.len();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove draw-order tables whose owning blocks are gone.
|
// Remove draw-order tables whose owning blocks are gone.
|
||||||
@@ -1246,6 +1286,7 @@ impl OpenCADStudio {
|
|||||||
| "DGNOSNAP"
|
| "DGNOSNAP"
|
||||||
| "UOSNAP"
|
| "UOSNAP"
|
||||||
| "FIELDDISPLAY"
|
| "FIELDDISPLAY"
|
||||||
|
| "SURFACEASSOCIATIVITY"
|
||||||
| "PDFIMPORTMODE"
|
| "PDFIMPORTMODE"
|
||||||
| "PDFIMPORTFILTER"
|
| "PDFIMPORTFILTER"
|
||||||
| "PDFIMPORTLAYERS"
|
| "PDFIMPORTLAYERS"
|
||||||
@@ -1484,6 +1525,36 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
return Some(self.finish_dispatch(cmd));
|
return Some(self.finish_dispatch(cmd));
|
||||||
}
|
}
|
||||||
|
if name == "SURFACEASSOCIATIVITY" {
|
||||||
|
use crate::scene::model::sweep_model;
|
||||||
|
let current = i16::from(sweep_model::surface_associativity());
|
||||||
|
match value
|
||||||
|
.as_deref()
|
||||||
|
.map(|v| v.trim().parse::<i16>().ok().filter(|v| (0..=1).contains(v)))
|
||||||
|
{
|
||||||
|
Some(Some(mode)) => {
|
||||||
|
if current != mode {
|
||||||
|
sweep_model::set_surface_associativity(mode == 1);
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(None) => {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("Requires 0 or 1 only.").as_ref());
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"Enter new value for SURFACEASSOCIATIVITY <{current}>:"
|
||||||
|
));
|
||||||
|
self.pending_setvar = Some(name.clone());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"Enter new value for SURFACEASSOCIATIVITY <{current}>:"
|
||||||
|
));
|
||||||
|
self.pending_setvar = Some(name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(self.finish_dispatch(cmd));
|
||||||
|
}
|
||||||
if name == "FIELDDISPLAY" {
|
if name == "FIELDDISPLAY" {
|
||||||
let current = i16::from(crate::entities::field::display());
|
let current = i16::from(crate::entities::field::display());
|
||||||
match value
|
match value
|
||||||
@@ -3889,6 +3960,13 @@ fn rename_symbol(doc: &mut codec::CadDocument, ty: &str, old: &str, new: &str) -
|
|||||||
if doc.header.current_text_style_name.eq_ignore_ascii_case(old) {
|
if doc.header.current_text_style_name.eq_ignore_ascii_case(old) {
|
||||||
doc.header.current_text_style_name = new.to_string();
|
doc.header.current_text_style_name = new.to_string();
|
||||||
}
|
}
|
||||||
|
// Dimension styles name their text style; follow the rename (same
|
||||||
|
// as rename_style_storage — see #42).
|
||||||
|
for ds in doc.dim_styles.iter_mut() {
|
||||||
|
if ds.dimtxsty.eq_ignore_ascii_case(old) {
|
||||||
|
ds.dimtxsty = new.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
"DIMSTYLE" => {
|
"DIMSTYLE" => {
|
||||||
|
|||||||
@@ -814,7 +814,7 @@ impl OpenCADStudio {
|
|||||||
vp.view_height,
|
vp.view_height,
|
||||||
vp.height,
|
vp.height,
|
||||||
),
|
),
|
||||||
vp.status.is_on,
|
vp.is_on(),
|
||||||
vp.status.locked,
|
vp.status.locked,
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -206,6 +206,12 @@ pub(super) struct DocumentTab {
|
|||||||
pub(super) dyn_active: usize,
|
pub(super) dyn_active: usize,
|
||||||
pub(super) history: HistoryState,
|
pub(super) history: HistoryState,
|
||||||
pub(super) active_layer: String,
|
pub(super) active_layer: String,
|
||||||
|
/// Block references whose properties panel shows per-axis Scale X/Y/Z even
|
||||||
|
/// though the three factors are currently equal — the user unchecked the
|
||||||
|
/// "Uniform scale" box for them (#427). Per-tab: fresh documents restart
|
||||||
|
/// handle numbering, so an app-global set keyed by raw handle leaks one
|
||||||
|
/// tab's opt-out into another tab reusing the value (#47).
|
||||||
|
pub(super) props_asym_scale: std::collections::HashSet<u64>,
|
||||||
/// Node graph shown by the command line's graph button.
|
/// Node graph shown by the command line's graph button.
|
||||||
pub(super) graph: crate::ui::node_graph::Graph,
|
pub(super) graph: crate::ui::node_graph::Graph,
|
||||||
/// Currently active UCS. `None` means WCS (identity transform).
|
/// Currently active UCS. `None` means WCS (identity transform).
|
||||||
@@ -648,6 +654,7 @@ impl DocumentTab {
|
|||||||
dyn_active: 0,
|
dyn_active: 0,
|
||||||
history: HistoryState::default(),
|
history: HistoryState::default(),
|
||||||
active_layer: "0".to_string(),
|
active_layer: "0".to_string(),
|
||||||
|
props_asym_scale: std::collections::HashSet::new(),
|
||||||
graph: Default::default(),
|
graph: Default::default(),
|
||||||
active_ucs: None,
|
active_ucs: None,
|
||||||
sketch_session: None,
|
sketch_session: None,
|
||||||
|
|||||||
+20
-1
@@ -62,10 +62,29 @@ impl OpenCADStudio {
|
|||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||||
let vp_info = self.tabs[i].scene.viewport_list();
|
let vp_info = self.tabs[i].scene.viewport_list();
|
||||||
|
let names_before: Vec<String> = self.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.name.clone())
|
||||||
|
.collect();
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.sync_with_viewports(&doc_layers, vp_info);
|
.sync_with_viewports(&doc_layers, vp_info);
|
||||||
self.sync_ribbon_layers();
|
self.sync_ribbon_layers();
|
||||||
|
// The Properties palette lists layer names it read when it was built;
|
||||||
|
// a layer added, removed or renamed must reach that list too (#1664).
|
||||||
|
// Only then: the rebuild is costly for a large selection.
|
||||||
|
let names_changed = self.tabs[i].layers.layers.len() != names_before.len()
|
||||||
|
|| self.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.zip(&names_before)
|
||||||
|
.any(|(layer, before)| layer.name != *before);
|
||||||
|
if names_changed {
|
||||||
|
self.refresh_properties();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pick up layers an entity edit registered on the fly (`ensure_layer`).
|
/// Pick up layers an entity edit registered on the fly (`ensure_layer`).
|
||||||
@@ -207,7 +226,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let active_table = self.ribbon.active_table_style.clone();
|
let active_table = doc.header.current_table_style_name.clone();
|
||||||
let active_table = if table_names.contains(&active_table) {
|
let active_table = if table_names.contains(&active_table) {
|
||||||
active_table
|
active_table
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+38
-17
@@ -9,7 +9,7 @@ pub(crate) mod config;
|
|||||||
pub use automation::{export_headless, serve};
|
pub use automation::{export_headless, serve};
|
||||||
mod annotation_data;
|
mod annotation_data;
|
||||||
mod command_driver;
|
mod command_driver;
|
||||||
pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel};
|
pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel, PasteSpace};
|
||||||
pub(crate) mod commands;
|
pub(crate) mod commands;
|
||||||
pub(crate) mod dim_viewport;
|
pub(crate) mod dim_viewport;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -420,10 +420,6 @@ pub(super) struct OpenCADStudio {
|
|||||||
discussions: Vec<crate::discussions::DiscussionEntry>,
|
discussions: Vec<crate::discussions::DiscussionEntry>,
|
||||||
/// True while the boot-time Discussions refresh is still in flight.
|
/// True while the boot-time Discussions refresh is still in flight.
|
||||||
discussions_loading: bool,
|
discussions_loading: bool,
|
||||||
/// Block references whose properties panel shows per-axis Scale X/Y/Z even
|
|
||||||
/// though the three factors are currently equal — the user unchecked the
|
|
||||||
/// "Uniform scale" box for them (#427). Keyed by entity handle.
|
|
||||||
props_asym_scale: std::collections::HashSet<u64>,
|
|
||||||
/// Collapsed Properties-panel section titles. This belongs to the app,
|
/// Collapsed Properties-panel section titles. This belongs to the app,
|
||||||
/// rather than an individual document tab, so the same view preference is
|
/// rather than an individual document tab, so the same view preference is
|
||||||
/// used by every currently open drawing/project.
|
/// used by every currently open drawing/project.
|
||||||
@@ -560,6 +556,10 @@ pub(super) struct OpenCADStudio {
|
|||||||
polar_increment_deg: f32,
|
polar_increment_deg: f32,
|
||||||
/// Reverse the mouse-wheel zoom direction when true (ZOOMWHEEL = 1).
|
/// Reverse the mouse-wheel zoom direction when true (ZOOMWHEEL = 1).
|
||||||
zoom_wheel_reversed: bool,
|
zoom_wheel_reversed: bool,
|
||||||
|
/// Scroll pans and Ctrl+scroll zooms (touchpads); see `scroll_intent`.
|
||||||
|
touchpad_pan: bool,
|
||||||
|
/// Interface scale in percent, applied through the window scale factor.
|
||||||
|
ui_scale_percent: u16,
|
||||||
/// Mouse-wheel zoom sensitivity (ZOOMFACTOR), held inside
|
/// Mouse-wheel zoom sensitivity (ZOOMFACTOR), held inside
|
||||||
/// `settings::ZOOM_FACTOR_MIN..=settings::ZOOM_FACTOR_MAX`. `SETVAR`
|
/// `settings::ZOOM_FACTOR_MIN..=settings::ZOOM_FACTOR_MAX`. `SETVAR`
|
||||||
/// only sets the range the system variable itself has; the Options
|
/// only sets the range the system variable itself has; the Options
|
||||||
@@ -1121,6 +1121,14 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// Snapshot of the dialog's settings taken when it opened, restored by the
|
/// Snapshot of the dialog's settings taken when it opened, restored by the
|
||||||
/// `<previous>` list entry.
|
/// `<previous>` list entry.
|
||||||
plot_prev: Option<crate::ui::window::plot::PlotDialogState>,
|
plot_prev: Option<crate::ui::window::plot::PlotDialogState>,
|
||||||
|
/// Tab/layout that last owned the Plot dialog. Reopening Plot in the same
|
||||||
|
/// context keeps the user's last paper, scale, CTB and output choices
|
||||||
|
/// instead of reapplying the layout page setup over them.
|
||||||
|
plot_prev_context: Option<(usize, String)>,
|
||||||
|
/// Printer whose capability reply is allowed to reconcile the current
|
||||||
|
/// sheet. Set only after an explicit printer/page-setup choice; merely
|
||||||
|
/// opening Plot must never replace the drawing's stored paper size.
|
||||||
|
plot_reconcile_printer_media: Option<String>,
|
||||||
/// Full source settings behind the fields currently shown in Plot.
|
/// Full source settings behind the fields currently shown in Plot.
|
||||||
plot_setup_template: Option<codec::objects::PlotSettings>,
|
plot_setup_template: Option<codec::objects::PlotSettings>,
|
||||||
/// Paper layouts shown by Print All, in tab order with their selection.
|
/// Paper layouts shown by Print All, in tab order with their selection.
|
||||||
@@ -2386,6 +2394,9 @@ pub enum Message {
|
|||||||
CommandLineFadeChanged(i32),
|
CommandLineFadeChanged(i32),
|
||||||
/// Toggle reversing the mouse-wheel zoom direction (ZOOMWHEEL).
|
/// Toggle reversing the mouse-wheel zoom direction (ZOOMWHEEL).
|
||||||
ZoomWheelReversedChanged(bool),
|
ZoomWheelReversedChanged(bool),
|
||||||
|
TouchpadPanChanged(bool),
|
||||||
|
/// Interface scale in percent (Options > Display).
|
||||||
|
UiScaleChanged(u16),
|
||||||
/// Change how far one wheel notch zooms, from the Options slider
|
/// Change how far one wheel notch zooms, from the Options slider
|
||||||
/// (ZOOMFACTOR).
|
/// (ZOOMFACTOR).
|
||||||
ZoomFactorChanged(i32),
|
ZoomFactorChanged(i32),
|
||||||
@@ -2596,8 +2607,11 @@ pub enum Message {
|
|||||||
/// Pop the trailing character off the command-line input — backspace
|
/// Pop the trailing character off the command-line input — backspace
|
||||||
/// counterpart to `CommandAppendChar`.
|
/// counterpart to `CommandAppendChar`.
|
||||||
CommandBackspace,
|
CommandBackspace,
|
||||||
/// TAB pressed: move focus to the next dynamic-input field (wraps).
|
/// TAB pressed: move focus to the next dynamic-input field or
|
||||||
|
/// autocomplete suggestion.
|
||||||
DynTabNext,
|
DynTabNext,
|
||||||
|
/// Shift+TAB: move to the previous contextual autocomplete suggestion.
|
||||||
|
DynTabPrev,
|
||||||
/// Split the active Model viewport in two. `true` → horizontal divider
|
/// Split the active Model viewport in two. `true` → horizontal divider
|
||||||
/// (top / bottom); `false` → vertical divider (left / right).
|
/// (top / bottom); `false` → vertical divider (left / right).
|
||||||
SplitModelViewport(bool),
|
SplitModelViewport(bool),
|
||||||
@@ -2668,11 +2682,11 @@ pub enum Message {
|
|||||||
ToggleXrefManager,
|
ToggleXrefManager,
|
||||||
/// Rescan the active drawing's references into the palette.
|
/// Rescan the active drawing's references into the palette.
|
||||||
XrefManagerRefresh,
|
XrefManagerRefresh,
|
||||||
/// Toggle one palette row (entry index) in the multi-selection set.
|
/// Toggle one palette row (stable row id) in the multi-selection set.
|
||||||
XrefManagerSelect(usize),
|
XrefManagerSelect(crate::ui::window::xref_manager::XrefRowId),
|
||||||
/// Right-click on a palette row: single-select it when outside the
|
/// Right-click on a palette row: single-select it when outside the
|
||||||
/// selection (the context menu then acts on the selection).
|
/// selection (the context menu then acts on the selection).
|
||||||
XrefRowRightClick(usize),
|
XrefRowRightClick(crate::ui::window::xref_manager::XrefRowId),
|
||||||
/// Flip the palette's list/tree presentation.
|
/// Flip the palette's list/tree presentation.
|
||||||
XrefManagerToggleTree,
|
XrefManagerToggleTree,
|
||||||
/// Start a reference-table column divider drag (column index).
|
/// Start a reference-table column divider drag (column index).
|
||||||
@@ -2709,16 +2723,19 @@ pub enum Message {
|
|||||||
XrefManagerToggleExpand(u64),
|
XrefManagerToggleExpand(u64),
|
||||||
/// Selection-scoped palette operation (detach/unload/reload/overlay/pathtype).
|
/// Selection-scoped palette operation (detach/unload/reload/overlay/pathtype).
|
||||||
XrefManagerOp(crate::ui::window::xref_manager::XrefPaletteOp),
|
XrefManagerOp(crate::ui::window::xref_manager::XrefPaletteOp),
|
||||||
/// Row-scoped palette operation: selects row `index`, then applies the
|
/// Row-scoped palette operation: selects the row by stable id, then
|
||||||
/// operation to it.
|
/// applies the operation to it.
|
||||||
XrefRowOp(usize, crate::ui::window::xref_manager::XrefPaletteOp),
|
XrefRowOp(
|
||||||
|
crate::ui::window::xref_manager::XrefRowId,
|
||||||
|
crate::ui::window::xref_manager::XrefPaletteOp,
|
||||||
|
),
|
||||||
/// Prefill the command line for Find & Replace across references
|
/// Prefill the command line for Find & Replace across references
|
||||||
/// (`XREF Path Find <old> <new>`); the CLI parses and runs it.
|
/// (`XREF Path Find <old> <new>`); the CLI parses and runs it.
|
||||||
XrefFindReplacePrompt,
|
XrefFindReplacePrompt,
|
||||||
/// Pick a new path for a specific row index.
|
/// Pick a new path for a specific row (stable id).
|
||||||
XrefRowPathPick(usize),
|
XrefRowPathPick(crate::ui::window::xref_manager::XrefRowId),
|
||||||
/// Prefill the command line for Find & Replace for a specific row index.
|
/// Prefill the command line for Find & Replace for a specific row id.
|
||||||
XrefRowFindReplacePrompt(usize),
|
XrefRowFindReplacePrompt(crate::ui::window::xref_manager::XrefRowId),
|
||||||
XrefRowChangePathEnter,
|
XrefRowChangePathEnter,
|
||||||
XrefRowChangePathLeave,
|
XrefRowChangePathLeave,
|
||||||
LayerToggleVisible(String),
|
LayerToggleVisible(String),
|
||||||
@@ -4103,7 +4120,6 @@ impl OpenCADStudio {
|
|||||||
videos_loading: false,
|
videos_loading: false,
|
||||||
discussions: Vec::new(),
|
discussions: Vec::new(),
|
||||||
discussions_loading: false,
|
discussions_loading: false,
|
||||||
props_asym_scale: std::collections::HashSet::new(),
|
|
||||||
collapsed_property_sections: rustc_hash::FxHashSet::default(),
|
collapsed_property_sections: rustc_hash::FxHashSet::default(),
|
||||||
start_section: StartSection::default(),
|
start_section: StartSection::default(),
|
||||||
start_action_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
start_action_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
||||||
@@ -4168,6 +4184,8 @@ impl OpenCADStudio {
|
|||||||
polar_mode: false,
|
polar_mode: false,
|
||||||
polar_increment_deg: 45.0,
|
polar_increment_deg: 45.0,
|
||||||
zoom_wheel_reversed: false,
|
zoom_wheel_reversed: false,
|
||||||
|
touchpad_pan: false,
|
||||||
|
ui_scale_percent: 100,
|
||||||
zoom_factor: 60,
|
zoom_factor: 60,
|
||||||
zoom_factor_input: 60.to_string(),
|
zoom_factor_input: 60.to_string(),
|
||||||
cursor_size: 5,
|
cursor_size: 5,
|
||||||
@@ -4409,6 +4427,8 @@ impl OpenCADStudio {
|
|||||||
plot_orientation: crate::io::paper_catalog::Orientation::Landscape,
|
plot_orientation: crate::io::paper_catalog::Orientation::Landscape,
|
||||||
plot_dialog: crate::ui::window::plot::PlotDialogState::default(),
|
plot_dialog: crate::ui::window::plot::PlotDialogState::default(),
|
||||||
plot_prev: None,
|
plot_prev: None,
|
||||||
|
plot_prev_context: None,
|
||||||
|
plot_reconcile_printer_media: None,
|
||||||
plot_setup_template: None,
|
plot_setup_template: None,
|
||||||
print_all_layouts: Vec::new(),
|
print_all_layouts: Vec::new(),
|
||||||
print_all_options: false,
|
print_all_options: false,
|
||||||
@@ -4918,6 +4938,7 @@ pub fn run() -> iced::Result {
|
|||||||
..iced::Settings::default()
|
..iced::Settings::default()
|
||||||
})
|
})
|
||||||
.subscription(OpenCADStudio::subscription)
|
.subscription(OpenCADStudio::subscription)
|
||||||
|
.scale_factor(|state: &OpenCADStudio, _| f32::from(state.ui_scale_percent) / 100.0)
|
||||||
.title(|state: &OpenCADStudio, window_id: window::Id| {
|
.title(|state: &OpenCADStudio, window_id: window::Id| {
|
||||||
let _ = window_id; // all dialogs are in-canvas modals now
|
let _ = window_id; // all dialogs are in-canvas modals now
|
||||||
if let Some(tab) = state.tabs.get(state.active_tab) {
|
if let Some(tab) = state.tabs.get(state.active_tab) {
|
||||||
|
|||||||
+13
-12
@@ -2570,14 +2570,7 @@ impl<'a> HostSession<'a> {
|
|||||||
}
|
}
|
||||||
self.push_undo("Rename style");
|
self.push_undo("Rename style");
|
||||||
self.app.rename_style_storage(kind_of(kind), &from, &to);
|
self.app.rename_style_storage(kind_of(kind), &from, &to);
|
||||||
if kind == TableStyleKind::Text {
|
// dimtxsty follow lives in rename_style_storage (#42).
|
||||||
// A dimension style names its text style; follow the rename.
|
|
||||||
for dim in self.document_mut().dim_styles.iter_mut() {
|
|
||||||
if dim.dimtxsty.eq_ignore_ascii_case(&from) {
|
|
||||||
dim.dimtxsty = to.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.finish_style_change(kind_of(kind));
|
self.finish_style_change(kind_of(kind));
|
||||||
Ok(handle)
|
Ok(handle)
|
||||||
}
|
}
|
||||||
@@ -13043,7 +13036,11 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0)))
|
|||||||
|
|
||||||
// Verify COGO_LINES does not exist initially
|
// Verify COGO_LINES does not exist initially
|
||||||
assert!(!app.tabs[0].scene.document.layers.contains("COGO_LINES"));
|
assert!(!app.tabs[0].scene.document.layers.contains("COGO_LINES"));
|
||||||
assert!(!app.ribbon.layer_names.contains(&"COGO_LINES".to_string()));
|
assert!(!app
|
||||||
|
.ribbon
|
||||||
|
.layer_infos
|
||||||
|
.iter()
|
||||||
|
.any(|l| l.name == "COGO_LINES"));
|
||||||
|
|
||||||
// 1. Add entity via HostSession (simulating plugin add_entity / add_line)
|
// 1. Add entity via HostSession (simulating plugin add_entity / add_line)
|
||||||
{
|
{
|
||||||
@@ -13057,7 +13054,10 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0)))
|
|||||||
// Verify layer is created in document AND refreshed in ribbon + layer panel
|
// Verify layer is created in document AND refreshed in ribbon + layer panel
|
||||||
assert!(app.tabs[0].scene.document.layers.contains("COGO_LINES"));
|
assert!(app.tabs[0].scene.document.layers.contains("COGO_LINES"));
|
||||||
assert!(
|
assert!(
|
||||||
app.ribbon.layer_names.contains(&"COGO_LINES".to_string()),
|
app.ribbon
|
||||||
|
.layer_infos
|
||||||
|
.iter()
|
||||||
|
.any(|l| l.name == "COGO_LINES"),
|
||||||
"Ribbon layers should contain COGO_LINES after add_entity"
|
"Ribbon layers should contain COGO_LINES after add_entity"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -13082,8 +13082,9 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0)))
|
|||||||
.contains("SURVEY_TRAVERSE"));
|
.contains("SURVEY_TRAVERSE"));
|
||||||
assert!(
|
assert!(
|
||||||
app.ribbon
|
app.ribbon
|
||||||
.layer_names
|
.layer_infos
|
||||||
.contains(&"SURVEY_TRAVERSE".to_string()),
|
.iter()
|
||||||
|
.any(|l| l.name == "SURVEY_TRAVERSE"),
|
||||||
"Ribbon layers should contain SURVEY_TRAVERSE after commit_entity"
|
"Ribbon layers should contain SURVEY_TRAVERSE after commit_entity"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
+39
-1
@@ -439,7 +439,7 @@ impl OpenCADStudio {
|
|||||||
if let codec::EntityType::Insert(ins) = entity {
|
if let codec::EntityType::Insert(ins) = entity {
|
||||||
let eq = (ins.x_scale() - ins.y_scale()).abs() < 1e-12
|
let eq = (ins.x_scale() - ins.y_scale()).abs() < 1e-12
|
||||||
&& (ins.x_scale() - ins.z_scale()).abs() < 1e-12;
|
&& (ins.x_scale() - ins.z_scale()).abs() < 1e-12;
|
||||||
let uniform = eq && !self.props_asym_scale.contains(&handle.value());
|
let uniform = eq && !self.tabs[i].props_asym_scale.contains(&handle.value());
|
||||||
for section in sections.iter_mut() {
|
for section in sections.iter_mut() {
|
||||||
let Some(xi) = section.props.iter().position(|p| p.field == "x_scale") else {
|
let Some(xi) = section.props.iter().position(|p| p.field == "x_scale") else {
|
||||||
continue;
|
continue;
|
||||||
@@ -4223,6 +4223,44 @@ fn apply_insert_unit_scale(ins: &mut codec::entities::Insert, ratio: f64) -> boo
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod asym_scale_tests {
|
||||||
|
// #47: the uniform-scale opt-out must be per-tab. Fresh documents restart
|
||||||
|
// handle numbering (seed 1), so two tabs hand out identical handles — an
|
||||||
|
// app-global set leaks one tab's opt-out into the other.
|
||||||
|
use crate::app::OpenCADStudio;
|
||||||
|
|
||||||
|
fn tab_with_insert(app: &mut OpenCADStudio) -> (usize, codec::Handle) {
|
||||||
|
app.tab_counter += 1;
|
||||||
|
let n = app.tab_counter;
|
||||||
|
app.tabs
|
||||||
|
.push(crate::app::document::DocumentTab::new_drawing(n));
|
||||||
|
let idx = app.tabs.len() - 1;
|
||||||
|
app.active_tab = idx;
|
||||||
|
let ins = codec::entities::Insert::new("B", codec::types::Vector3::ZERO);
|
||||||
|
let h = app
|
||||||
|
.commit_entity_handle(codec::EntityType::Insert(ins))
|
||||||
|
.expect("insert commits");
|
||||||
|
(idx, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn asym_scale_opt_out_does_not_leak_across_tabs() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let (a, ha) = tab_with_insert(&mut app);
|
||||||
|
let (b, hb) = tab_with_insert(&mut app);
|
||||||
|
assert_eq!(
|
||||||
|
ha, hb,
|
||||||
|
"premise: identically built fresh docs reuse handle values"
|
||||||
|
);
|
||||||
|
app.tabs[a].props_asym_scale.insert(ha.value());
|
||||||
|
assert!(
|
||||||
|
!app.tabs[b].props_asym_scale.contains(&hb.value()),
|
||||||
|
"tab B must not see tab A's opt-out"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod insert_unit_scale_tests {
|
mod insert_unit_scale_tests {
|
||||||
use super::{apply_insert_unit_scale, format_unit_factor, insert_unit_scale, insunits_to_mm};
|
use super::{apply_insert_unit_scale, format_unit_factor, insert_unit_scale, insunits_to_mm};
|
||||||
|
|||||||
@@ -178,6 +178,11 @@ pub const ZOOM_FACTOR_MAX: i32 = 500;
|
|||||||
/// Hold a ZOOMFACTOR inside the range the application can store, wherever it
|
/// Hold a ZOOMFACTOR inside the range the application can store, wherever it
|
||||||
/// arrives from — the slider, the Options field, or a configuration file
|
/// arrives from — the slider, the Options field, or a configuration file
|
||||||
/// written by hand or by an older build.
|
/// written by hand or by an older build.
|
||||||
|
/// Interface scale in percent, kept to the range the Options slider offers.
|
||||||
|
pub fn clamp_ui_scale(percent: u16) -> u16 {
|
||||||
|
percent.clamp(75, 200)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clamp_zoom_factor(v: i32) -> i32 {
|
pub fn clamp_zoom_factor(v: i32) -> i32 {
|
||||||
v.clamp(ZOOM_FACTOR_MIN, ZOOM_FACTOR_MAX)
|
v.clamp(ZOOM_FACTOR_MIN, ZOOM_FACTOR_MAX)
|
||||||
}
|
}
|
||||||
@@ -383,6 +388,11 @@ pub struct UserSettings {
|
|||||||
pub polar: bool,
|
pub polar: bool,
|
||||||
pub polar_increment_deg: f32,
|
pub polar_increment_deg: f32,
|
||||||
pub zoom_wheel_reversed: bool,
|
pub zoom_wheel_reversed: bool,
|
||||||
|
/// Scrolling pans and Ctrl+scroll (a touchpad pinch) zooms, for a
|
||||||
|
/// touchpad that reports two-finger drags as wheel scrolls.
|
||||||
|
pub touchpad_pan: bool,
|
||||||
|
/// Interface scale in percent, on top of the display's own scale.
|
||||||
|
pub ui_scale_percent: u16,
|
||||||
pub zoom_factor: i32,
|
pub zoom_factor: i32,
|
||||||
/// CURSORSIZE: normalized crosshair reach; 5 retains the original 60 px arms.
|
/// CURSORSIZE: normalized crosshair reach; 5 retains the original 60 px arms.
|
||||||
pub cursor_size: i32,
|
pub cursor_size: i32,
|
||||||
@@ -407,6 +417,8 @@ pub struct UserSettings {
|
|||||||
pub attdef_on_screen: bool,
|
pub attdef_on_screen: bool,
|
||||||
/// FIELDDISPLAY: fields show on a gray background.
|
/// FIELDDISPLAY: fields show on a gray background.
|
||||||
pub field_display: bool,
|
pub field_display: bool,
|
||||||
|
/// SURFACEASSOCIATIVITY: new surfaces stay associative to their sources.
|
||||||
|
pub surface_associativity: bool,
|
||||||
/// Nested-copy symbol handling: false inserts, true binds.
|
/// Nested-copy symbol handling: false inserts, true binds.
|
||||||
pub ncopy_bind: bool,
|
pub ncopy_bind: bool,
|
||||||
/// Last Options page; unknown saved names fall back without rejecting the config.
|
/// Last Options page; unknown saved names fall back without rejecting the config.
|
||||||
@@ -805,6 +817,8 @@ impl Default for UserSettings {
|
|||||||
polar: false,
|
polar: false,
|
||||||
polar_increment_deg: 45.0,
|
polar_increment_deg: 45.0,
|
||||||
zoom_wheel_reversed: false,
|
zoom_wheel_reversed: false,
|
||||||
|
touchpad_pan: false,
|
||||||
|
ui_scale_percent: 100,
|
||||||
zoom_factor: 60,
|
zoom_factor: 60,
|
||||||
cursor_size: 5,
|
cursor_size: 5,
|
||||||
pick_box: 3,
|
pick_box: 3,
|
||||||
@@ -821,6 +835,7 @@ impl Default for UserSettings {
|
|||||||
image_frame: 1,
|
image_frame: 1,
|
||||||
attdef_on_screen: true,
|
attdef_on_screen: true,
|
||||||
field_display: true,
|
field_display: true,
|
||||||
|
surface_associativity: true,
|
||||||
ncopy_bind: false,
|
ncopy_bind: false,
|
||||||
cursor_type: CursorType::Crosshair,
|
cursor_type: CursorType::Crosshair,
|
||||||
crosshair_color: None,
|
crosshair_color: None,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ pub(super) fn default_bindings() -> BTreeMap<String, String> {
|
|||||||
("DELETE".to_string(), "DELETESELECTED"),
|
("DELETE".to_string(), "DELETESELECTED"),
|
||||||
("BACKSPACE".to_string(), "BACKSPACE"),
|
("BACKSPACE".to_string(), "BACKSPACE"),
|
||||||
("TAB".to_string(), "DYNTAB"),
|
("TAB".to_string(), "DYNTAB"),
|
||||||
|
("SHIFT+TAB".to_string(), "DYNTABPREV"),
|
||||||
("UP".to_string(), "HISTORYPREV"),
|
("UP".to_string(), "HISTORYPREV"),
|
||||||
("DOWN".to_string(), "HISTORYNEXT"),
|
("DOWN".to_string(), "HISTORYNEXT"),
|
||||||
("LEFT".to_string(), "CARETLEFT"),
|
("LEFT".to_string(), "CARETLEFT"),
|
||||||
@@ -154,6 +155,7 @@ pub(super) const INPUT_ACTIONS: &[&str] = &[
|
|||||||
"DELETESELECTED",
|
"DELETESELECTED",
|
||||||
"BACKSPACE",
|
"BACKSPACE",
|
||||||
"DYNTAB",
|
"DYNTAB",
|
||||||
|
"DYNTABPREV",
|
||||||
"HISTORYPREV",
|
"HISTORYPREV",
|
||||||
"HISTORYNEXT",
|
"HISTORYNEXT",
|
||||||
"CARETLEFT",
|
"CARETLEFT",
|
||||||
@@ -310,6 +312,7 @@ impl OpenCADStudio {
|
|||||||
"DELETESELECTED" => Message::DeleteSelected,
|
"DELETESELECTED" => Message::DeleteSelected,
|
||||||
"BACKSPACE" => Message::CommandBackspace,
|
"BACKSPACE" => Message::CommandBackspace,
|
||||||
"DYNTAB" => Message::DynTabNext,
|
"DYNTAB" => Message::DynTabNext,
|
||||||
|
"DYNTABPREV" => Message::DynTabPrev,
|
||||||
"HISTORYPREV" => Message::CommandHistoryPrev,
|
"HISTORYPREV" => Message::CommandHistoryPrev,
|
||||||
"HISTORYNEXT" => Message::CommandHistoryNext,
|
"HISTORYNEXT" => Message::CommandHistoryNext,
|
||||||
"CARETLEFT" => Message::MTextCaretMove(-1),
|
"CARETLEFT" => Message::MTextCaretMove(-1),
|
||||||
|
|||||||
+37
-1
@@ -291,7 +291,8 @@ impl OpenCADStudio {
|
|||||||
StyleKind::Text => self.load_textstyle_bufs(i),
|
StyleKind::Text => self.load_textstyle_bufs(i),
|
||||||
StyleKind::Dim => self.load_dimstyle_bufs(i),
|
StyleKind::Dim => self.load_dimstyle_bufs(i),
|
||||||
StyleKind::MLeader => self.load_mleaderstyle_bufs(i),
|
StyleKind::MLeader => self.load_mleaderstyle_bufs(i),
|
||||||
StyleKind::Table | StyleKind::MLine => {}
|
StyleKind::Table => self.load_tablestyle_bufs(i),
|
||||||
|
StyleKind::MLine => self.load_mlstyle_bufs(i),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,6 +598,13 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Dimension styles name their text style; follow the rename so
|
||||||
|
// Apply keeps resolving the handle instead of collapsing to NULL.
|
||||||
|
for ds in doc.dim_styles.iter_mut() {
|
||||||
|
if ds.dimtxsty.eq_ignore_ascii_case(old) {
|
||||||
|
ds.dimtxsty = new.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
StyleKind::Dim => {
|
StyleKind::Dim => {
|
||||||
let doc = &mut self.tabs[i].scene.document;
|
let doc = &mut self.tabs[i].scene.document;
|
||||||
@@ -1244,3 +1252,31 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod rename_follow_tests {
|
||||||
|
// #42: renaming a text style must follow into dimstyle dimtxsty.
|
||||||
|
use super::{OpenCADStudio, StyleKind};
|
||||||
|
use codec::tables::{DimStyle, TextStyle};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_rename_follows_dimtxsty() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let mut ts = TextStyle::new("OLD");
|
||||||
|
ts.handle = app.tabs[i].scene.document.allocate_handle();
|
||||||
|
app.tabs[i].scene.document.text_styles.add(ts).unwrap();
|
||||||
|
let mut ds = DimStyle::new("D");
|
||||||
|
ds.dimtxsty = "OLD".to_string();
|
||||||
|
app.tabs[i].scene.document.dim_styles.add(ds).unwrap();
|
||||||
|
app.style_rename = Some("OLD".to_string());
|
||||||
|
app.style_rename_buf = "NEW".to_string();
|
||||||
|
app.style_rename_commit(StyleKind::Text);
|
||||||
|
let dim = app.tabs[i].scene.document.dim_styles.get("D").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
dim.dimtxsty, "NEW",
|
||||||
|
"dimstyle must follow the text-style rename"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+361
-34
@@ -304,16 +304,21 @@ impl OpenCADStudio {
|
|||||||
// Live incremental search for INSERT/MINSERT (see CommandInput)
|
// Live incremental search for INSERT/MINSERT (see CommandInput)
|
||||||
let live = self.command_line.input.clone();
|
let live = self.command_line.input.clone();
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let (should_update, opts, prompt) =
|
let (should_update, opts, prompt, suggestions) =
|
||||||
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||||
if cmd.on_live_input(&live) {
|
let changed = cmd.on_live_input(&live);
|
||||||
(true, cmd.options(), cmd.prompt())
|
let suggestions = cmd.text_suggestions(&live);
|
||||||
|
if changed {
|
||||||
|
(true, cmd.options(), cmd.prompt(), suggestions)
|
||||||
} else {
|
} else {
|
||||||
(false, Vec::new(), String::new())
|
(false, Vec::new(), String::new(), suggestions)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(false, Vec::new(), String::new())
|
(false, Vec::new(), String::new(), Vec::new())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.command_line.set_contextual_suggestions(suggestions);
|
||||||
|
|
||||||
if should_update {
|
if should_update {
|
||||||
self.command_line.set_step_options(opts);
|
self.command_line.set_step_options(opts);
|
||||||
if let Some(last) = self.command_line.history.last_mut() {
|
if let Some(last) = self.command_line.history.last_mut() {
|
||||||
@@ -750,6 +755,12 @@ impl OpenCADStudio {
|
|||||||
self.command_line.autocomplete_cursor = None;
|
self.command_line.autocomplete_cursor = None;
|
||||||
return self.dispatch_command(&command);
|
return self.dispatch_command(&command);
|
||||||
}
|
}
|
||||||
|
} else if self.command_line.has_contextual_suggestions() {
|
||||||
|
// During an active command Enter accepts the highlighted
|
||||||
|
// contextual value instead of the user's partial search.
|
||||||
|
if let Some(value) = self.command_line.selected_suggestion() {
|
||||||
|
self.command_line.input = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
// A whole multi-token command line (`UCS Z 90`, `LINE 0,0
|
// A whole multi-token command line (`UCS Z 90`, `LINE 0,0
|
||||||
@@ -1003,7 +1014,8 @@ impl OpenCADStudio {
|
|||||||
// A tracking ray carries its own base, so it needs no anchor: that is
|
// A tracking ray carries its own base, so it needs no anchor: that is
|
||||||
// how OTRACK and Extension accepted a bare distance for the *first*
|
// how OTRACK and Extension accepted a bare distance for the *first*
|
||||||
// point of a command, before any anchor exists.
|
// point of a command, before any anchor exists.
|
||||||
let pt = if let Some((base, dir)) = self.active_distance_ray(i) {
|
let distance_ray = self.active_distance_ray(i);
|
||||||
|
let pt = if let Some((base, dir)) = distance_ray {
|
||||||
base + dir * dist
|
base + dir * dist
|
||||||
} else {
|
} else {
|
||||||
let anchor = self.tabs[i]
|
let anchor = self.tabs[i]
|
||||||
@@ -1036,6 +1048,9 @@ impl OpenCADStudio {
|
|||||||
self.dyn_coord_absolute = false;
|
self.dyn_coord_absolute = false;
|
||||||
self.sync_dyn_fields();
|
self.sync_dyn_fields();
|
||||||
self.reset_tracking_after_point();
|
self.reset_tracking_after_point();
|
||||||
|
if let Some((_, dir)) = distance_ray {
|
||||||
|
self.snapper.remember_distance_reference(dir);
|
||||||
|
}
|
||||||
self.push_ucs_to_cmd(i);
|
self.push_ucs_to_cmd(i);
|
||||||
|
|
||||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt))?;
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt))?;
|
||||||
@@ -1383,7 +1398,7 @@ impl OpenCADStudio {
|
|||||||
if let Some(idx) = new_idx {
|
if let Some(idx) = new_idx {
|
||||||
self.tabs[i].layers.selected = Some(idx);
|
self.tabs[i].layers.selected = Some(idx);
|
||||||
self.tabs[i].layers.selected_multi = vec![idx];
|
self.tabs[i].layers.selected_multi = vec![idx];
|
||||||
self.tabs[i].layers.editing = Some(idx);
|
self.tabs[i].layers.editing = Some(new_name.clone());
|
||||||
self.tabs[i].layers.edit_buf = new_name.clone();
|
self.tabs[i].layers.edit_buf = new_name.clone();
|
||||||
}
|
}
|
||||||
self.sync_ribbon_layers();
|
self.sync_ribbon_layers();
|
||||||
@@ -1569,16 +1584,18 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
pub(super) fn on_layer_rename_commit(&mut self) -> Task<Message> {
|
pub(super) fn on_layer_rename_commit(&mut self) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let editing_idx = self.tabs[i].layers.editing.take();
|
let editing_name = self.tabs[i].layers.editing.take();
|
||||||
if let Some(idx) = editing_idx {
|
if let Some(old_name) = editing_name {
|
||||||
let new_name = self.tabs[i].layers.edit_buf.trim().to_string();
|
let new_name = self.tabs[i].layers.edit_buf.trim().to_string();
|
||||||
let old_name = self.tabs[i]
|
// The name was captured at RenameStart; look it up fresh so
|
||||||
|
// a sort/sync/filter in between cannot retarget the rename.
|
||||||
|
// Deleted meanwhile → clear and no-op.
|
||||||
|
let still_there = self.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.layers
|
.layers
|
||||||
.get(idx)
|
.iter()
|
||||||
.map(|l| l.name.clone())
|
.any(|l| l.name == old_name);
|
||||||
.unwrap_or_default();
|
if !new_name.is_empty() && new_name != old_name && still_there {
|
||||||
if !new_name.is_empty() && new_name != old_name {
|
|
||||||
self.push_undo_snapshot(i, "LAYER RENAME");
|
self.push_undo_snapshot(i, "LAYER RENAME");
|
||||||
if !self.tabs[i].rename_layer(&old_name, &new_name) {
|
if !self.tabs[i].rename_layer(&old_name, &new_name) {
|
||||||
self.discard_last_undo_entry(i);
|
self.discard_last_undo_entry(i);
|
||||||
@@ -2305,7 +2322,15 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
StyleKey::TableStyle => {
|
StyleKey::TableStyle => {
|
||||||
self.ribbon.active_table_style = name;
|
self.ribbon.active_table_style = name.clone();
|
||||||
|
let i = self.active_tab;
|
||||||
|
let found = self.tabs[i].scene.document.objects.values().any(
|
||||||
|
|o| matches!(o, codec::objects::ObjectType::TableStyle(ts) if ts.name == name),
|
||||||
|
);
|
||||||
|
if found {
|
||||||
|
self.tabs[i].scene.document.header.current_table_style_name = name;
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -4356,13 +4381,15 @@ mod layer_rename_tests {
|
|||||||
|
|
||||||
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
|
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let idx = app.tabs[i]
|
assert!(
|
||||||
.layers
|
app.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.iter()
|
.layers
|
||||||
.position(|layer| layer.name == old_name)
|
.iter()
|
||||||
.expect("layer exists in panel");
|
.any(|layer| layer.name == old_name),
|
||||||
app.tabs[i].layers.editing = Some(idx);
|
"layer exists in panel"
|
||||||
|
);
|
||||||
|
app.tabs[i].layers.editing = Some(old_name.to_string());
|
||||||
app.tabs[i].layers.edit_buf = new_name.to_string();
|
app.tabs[i].layers.edit_buf = new_name.to_string();
|
||||||
let _ = app.on_layer_rename_commit();
|
let _ = app.on_layer_rename_commit();
|
||||||
}
|
}
|
||||||
@@ -4407,11 +4434,17 @@ mod layer_rename_tests {
|
|||||||
fn current_layer_rename_updates_name_and_allocated_handle() {
|
fn current_layer_rename_updates_name_and_allocated_handle() {
|
||||||
let mut app = app_with_editing_layer();
|
let mut app = app_with_editing_layer();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let idx = app.tabs[i]
|
let old_name = app.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.editing
|
.editing
|
||||||
|
.clone()
|
||||||
.expect("new layer is being edited");
|
.expect("new layer is being edited");
|
||||||
let old_name = app.tabs[i].layers.layers[idx].name.clone();
|
let idx = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.position(|l| l.name == old_name)
|
||||||
|
.expect("editing layer in panel");
|
||||||
app.tabs[i]
|
app.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
@@ -4443,11 +4476,17 @@ mod layer_rename_tests {
|
|||||||
fn layer_rename_undo_redo_restores_active_layer() {
|
fn layer_rename_undo_redo_restores_active_layer() {
|
||||||
let mut app = app_with_editing_layer();
|
let mut app = app_with_editing_layer();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let idx = app.tabs[i]
|
let old_name = app.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.editing
|
.editing
|
||||||
|
.clone()
|
||||||
.expect("new layer is being edited");
|
.expect("new layer is being edited");
|
||||||
let old_name = app.tabs[i].layers.layers[idx].name.clone();
|
let idx = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.position(|l| l.name == old_name)
|
||||||
|
.expect("editing layer in panel");
|
||||||
app.tabs[i].layers.selected = Some(idx);
|
app.tabs[i].layers.selected = Some(idx);
|
||||||
let _ = app.on_layer_set_current();
|
let _ = app.on_layer_set_current();
|
||||||
|
|
||||||
@@ -4472,10 +4511,17 @@ mod layer_rename_tests {
|
|||||||
let old_name = app.tabs[i].layers.edit_buf.clone();
|
let old_name = app.tabs[i].layers.edit_buf.clone();
|
||||||
rename_layer(&mut app, &old_name, "A");
|
rename_layer(&mut app, &old_name, "A");
|
||||||
let _ = app.on_layer_new();
|
let _ = app.on_layer_new();
|
||||||
let b_idx = app.tabs[i]
|
let b_name = app.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.editing
|
.editing
|
||||||
|
.clone()
|
||||||
.expect("new layer is being edited");
|
.expect("new layer is being edited");
|
||||||
|
let b_idx = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.position(|l| l.name == b_name)
|
||||||
|
.expect("editing layer in panel");
|
||||||
app.tabs[i].layers.selected = Some(b_idx);
|
app.tabs[i].layers.selected = Some(b_idx);
|
||||||
let _ = app.on_layer_set_current();
|
let _ = app.on_layer_set_current();
|
||||||
let header_name = app.tabs[i].scene.document.header.current_layer_name.clone();
|
let header_name = app.tabs[i].scene.document.header.current_layer_name.clone();
|
||||||
@@ -4538,13 +4584,15 @@ mod layer_name_target_tests {
|
|||||||
|
|
||||||
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
|
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let idx = app.tabs[i]
|
assert!(
|
||||||
.layers
|
app.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.iter()
|
.layers
|
||||||
.position(|layer| layer.name == old_name)
|
.iter()
|
||||||
.expect("layer exists in panel");
|
.any(|layer| layer.name == old_name),
|
||||||
app.tabs[i].layers.editing = Some(idx);
|
"layer exists in panel"
|
||||||
|
);
|
||||||
|
app.tabs[i].layers.editing = Some(old_name.to_string());
|
||||||
app.tabs[i].layers.edit_buf = new_name.to_string();
|
app.tabs[i].layers.edit_buf = new_name.to_string();
|
||||||
let _ = app.on_layer_rename_commit();
|
let _ = app.on_layer_rename_commit();
|
||||||
}
|
}
|
||||||
@@ -4596,6 +4644,135 @@ mod layer_name_target_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_rename_commit_follows_name_across_resort() {
|
||||||
|
// #44: rename state keyed by name survives a panel re-sort; a stale
|
||||||
|
// row index would rename the wrong layer.
|
||||||
|
let mut app = app_with_zulu_alpha();
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.tabs[i].layers.editing = Some("ZULU".to_string());
|
||||||
|
app.tabs[i].layers.edit_buf = "ZULU_NEW".to_string();
|
||||||
|
let _ = app.update(Message::LayerSort(LayerSortCol::Name));
|
||||||
|
let _ = app.update(Message::LayerRenameCommit);
|
||||||
|
let names: Vec<String> = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.name.clone())
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
names.contains(&"ZULU_NEW".to_string()),
|
||||||
|
"ZULU must be renamed, got {names:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!names.contains(&"ZULU".to_string()),
|
||||||
|
"old name must be gone, got {names:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].scene.document.layers.contains("ZULU_NEW"),
|
||||||
|
"document must follow the rename"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_rename_commit_deleted_layer_is_noop() {
|
||||||
|
// Renaming a layer deleted mid-edit must clear state without
|
||||||
|
// renaming anything or pushing undo.
|
||||||
|
let mut app = app_with_zulu_alpha();
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.finish_pending_history(i);
|
||||||
|
let undo_before = app.tabs[i].history.undo_stack.len();
|
||||||
|
app.tabs[i].layers.editing = Some("GONE".to_string());
|
||||||
|
app.tabs[i].layers.edit_buf = "NEW".to_string();
|
||||||
|
let _ = app.update(Message::LayerRenameCommit);
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].layers.editing.is_none(),
|
||||||
|
"stale edit must clear"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].layers.edit_buf.is_empty(),
|
||||||
|
"stale edit buffer must clear"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].history.undo_stack.len(),
|
||||||
|
undo_before,
|
||||||
|
"no-op commit must not push undo"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!app.tabs[i].scene.document.layers.contains("NEW"),
|
||||||
|
"nothing must be renamed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_rename_commit_empty_name_is_noop() {
|
||||||
|
let mut app = app_with_zulu_alpha();
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.finish_pending_history(i);
|
||||||
|
let undo_before = app.tabs[i].history.undo_stack.len();
|
||||||
|
app.tabs[i].layers.editing = Some("ZULU".to_string());
|
||||||
|
app.tabs[i].layers.edit_buf = " ".to_string();
|
||||||
|
let _ = app.update(Message::LayerRenameCommit);
|
||||||
|
assert!(app.tabs[i].layers.editing.is_none(), "edit must clear");
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].scene.document.layers.contains("ZULU"),
|
||||||
|
"blank commit must not rename"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].history.undo_stack.len(),
|
||||||
|
undo_before,
|
||||||
|
"blank commit must not push undo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_color_picker_toggle_follows_name_across_resort() {
|
||||||
|
// Same name-keying guarantee as rename, for the swatch row.
|
||||||
|
let mut app = app_with_zulu_alpha();
|
||||||
|
let _ = app.update(Message::LayerColorPickerToggle("ZULU".to_string()));
|
||||||
|
let _ = app.update(Message::LayerSort(LayerSortCol::Name));
|
||||||
|
let i = app.active_tab;
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].layers.color_picker_row,
|
||||||
|
Some("ZULU".to_string()),
|
||||||
|
"picker must stay on ZULU across a resort"
|
||||||
|
);
|
||||||
|
let _ = app.update(Message::LayerColorPickerToggle("ZULU".to_string()));
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].layers.color_picker_row.is_none(),
|
||||||
|
"second toggle must close the picker"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_select_skips_filtered_out_rows() {
|
||||||
|
// #50: rows hidden by the live filter must not join a shift-range.
|
||||||
|
// Sorted panel: A1(0), B9(1), C1(2); filter "1" hides B9.
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
for new in ["C1", "B9", "A1"] {
|
||||||
|
let _ = app.on_layer_new();
|
||||||
|
let fresh = app.tabs[app.active_tab].layers.edit_buf.clone();
|
||||||
|
rename_layer(&mut app, &fresh, new);
|
||||||
|
}
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.tabs[i].layers.filter = "1".to_string();
|
||||||
|
let _ = app.update(Message::LayerSelect("A1".to_string()));
|
||||||
|
app.shift_down = true;
|
||||||
|
let _ = app.update(Message::LayerSelect("C1".to_string()));
|
||||||
|
app.shift_down = false;
|
||||||
|
let multi = app.tabs[i].layers.selected_multi.clone();
|
||||||
|
let names: Vec<String> = multi
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&x| app.tabs[i].layers.layers.get(x).map(|l| l.name.clone()))
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
!names.contains(&"B9".to_string()),
|
||||||
|
"hidden row must not join the range, got {names:?}"
|
||||||
|
);
|
||||||
|
assert!(names.contains(&"A1".to_string()) && names.contains(&"C1".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn layer_toggle_visible_unknown_name_is_noop() {
|
fn layer_toggle_visible_unknown_name_is_noop() {
|
||||||
let mut app = app_with_zulu_alpha();
|
let mut app = app_with_zulu_alpha();
|
||||||
@@ -4615,6 +4792,156 @@ mod layer_name_target_tests {
|
|||||||
.collect();
|
.collect();
|
||||||
assert_eq!(before, after, "unknown name must change nothing");
|
assert_eq!(before, after, "unknown name must change nothing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_transparency_edit_pushes_undo() {
|
||||||
|
// #43: every sibling layer arm pushes an undo entry; transparency must too.
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.finish_pending_history(i);
|
||||||
|
let before = app.tabs[i].history.undo_stack.len();
|
||||||
|
let _ = app.update(Message::LayerTransparencyEdit(
|
||||||
|
"0".to_string(),
|
||||||
|
"50".to_string(),
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].history.undo_stack.len(),
|
||||||
|
before + 1,
|
||||||
|
"transparency edit must push an undo entry like its siblings"
|
||||||
|
);
|
||||||
|
let panel = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.find(|l| l.name == "0")
|
||||||
|
.expect("layer 0 in panel");
|
||||||
|
assert_eq!(panel.transparency, 50);
|
||||||
|
app.undo_active_tab();
|
||||||
|
let panel = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.find(|l| l.name == "0")
|
||||||
|
.expect("layer 0 in panel");
|
||||||
|
assert_eq!(panel.transparency, 0, "undo must restore transparency");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tablestyle_ribbon_tests {
|
||||||
|
// #45: picking a table style in the ribbon must write the document header
|
||||||
|
// (like Text/Dim/MLeader), not just the ribbon mirror.
|
||||||
|
use crate::app::OpenCADStudio;
|
||||||
|
use crate::modules::StyleKey;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tablestyle_ribbon_pick_writes_header_and_dirties() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let mut extra = codec::objects::TableStyle::standard();
|
||||||
|
extra.name = "EXTRA".to_string();
|
||||||
|
extra.handle = app.tabs[i].scene.document.allocate_handle();
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.insert(extra.handle, codec::objects::ObjectType::TableStyle(extra));
|
||||||
|
app.tabs[i].dirty = false;
|
||||||
|
let _ = app.on_ribbon_style_changed(StyleKey::TableStyle, "EXTRA".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].scene.document.header.current_table_style_name, "EXTRA",
|
||||||
|
"ribbon pick must reach the document header"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].dirty,
|
||||||
|
"ribbon pick must dirty the tab like its siblings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tablestyle_pick_flows_into_table_insert() {
|
||||||
|
// End-to-end: ribbon pick → header → insert dialog defaults to the pick.
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let mut extra = codec::objects::TableStyle::standard();
|
||||||
|
extra.name = "EXTRA".to_string();
|
||||||
|
extra.handle = app.tabs[i].scene.document.allocate_handle();
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.insert(extra.handle, codec::objects::ObjectType::TableStyle(extra));
|
||||||
|
let _ = app.on_ribbon_style_changed(StyleKey::TableStyle, "EXTRA".to_string());
|
||||||
|
app.open_table_insert();
|
||||||
|
assert_eq!(
|
||||||
|
app.table_insert.style, "EXTRA",
|
||||||
|
"insert dialog must default to the ribbon-picked style"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tablestyle_buffer_tests {
|
||||||
|
// #53: New/Copy/Delete must reload Table/MLine buffers (no stale edits
|
||||||
|
// into the fresh style), and cell text-style typos must not persist.
|
||||||
|
use crate::app::{OpenCADStudio, StyleKind};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_new_reloads_cell_buffers() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app.ts_cell_textstyle[0] = "STALE_SENTINEL".to_string();
|
||||||
|
app.style_new(StyleKind::Table);
|
||||||
|
assert_ne!(
|
||||||
|
app.ts_cell_textstyle[0], "STALE_SENTINEL",
|
||||||
|
"buffers must reload from the fresh style"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_cell_unknown_text_style_is_rejected() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let style_name = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.values()
|
||||||
|
.find_map(|o| match o {
|
||||||
|
codec::objects::ObjectType::TableStyle(s) => Some(s.name.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("fresh doc ships a table style");
|
||||||
|
app.tablestyle_selected = style_name.clone();
|
||||||
|
app.ts_cell_textstyle[0] = "NOPE".to_string();
|
||||||
|
let before = app.command_line.history.len();
|
||||||
|
let _ = app.on_table_style_cell_apply(0);
|
||||||
|
let cell_name = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.values()
|
||||||
|
.find_map(|o| match o {
|
||||||
|
codec::objects::ObjectType::TableStyle(s) if s.name == style_name => {
|
||||||
|
Some(s.data_row_style.text_style_name.clone())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_ne!(cell_name, "NOPE", "dangling name must not persist");
|
||||||
|
assert_eq!(
|
||||||
|
app.ts_cell_textstyle[0], "NOPE",
|
||||||
|
"rejected text stays in the buffer for correction"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
app.command_line.history.len() > before,
|
||||||
|
"rejection must say something"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Message::DynTabNext => Some(self.on_context_menu_navigate(ContextMenuNav::Down)),
|
Message::DynTabNext => Some(self.on_context_menu_navigate(ContextMenuNav::Down)),
|
||||||
|
Message::DynTabPrev => Some(self.on_context_menu_navigate(ContextMenuNav::Up)),
|
||||||
Message::CommandFinalize | Message::CommandSpace => {
|
Message::CommandFinalize | Message::CommandSpace => {
|
||||||
if self.command_line.input.trim().is_empty() {
|
if self.command_line.input.trim().is_empty() {
|
||||||
Some(self.on_context_menu_navigate(ContextMenuNav::Enter))
|
Some(self.on_context_menu_navigate(ContextMenuNav::Enter))
|
||||||
|
|||||||
+56
-20
@@ -1514,13 +1514,42 @@ mod tests {
|
|||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xref_rowop_unknown_id_is_noop() {
|
||||||
|
// #49 RED: row messages carry (key, saved_path) identity and resolve
|
||||||
|
// at dispatch; a deleted row's id must no-op instead of hitting whatever
|
||||||
|
// row now sits at the old index. Fails to compile until the payload
|
||||||
|
// carries XrefRowId instead of usize.
|
||||||
|
use crate::app::Message;
|
||||||
|
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
|
||||||
|
let mut app = fresh();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let start = app.command_line.history.len();
|
||||||
|
let ghost = XrefRowId {
|
||||||
|
key: u64::MAX,
|
||||||
|
saved_path: "gone.dwg".to_string(),
|
||||||
|
};
|
||||||
|
let records_before = app.tabs[i].scene.document.block_records.iter().count();
|
||||||
|
let _ = app.update(Message::XrefRowOp(ghost, XrefPaletteOp::Detach));
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].scene.document.block_records.iter().count(),
|
||||||
|
records_before,
|
||||||
|
"no-op must not disturb the document"
|
||||||
|
);
|
||||||
|
let out = palette_output(&app, start);
|
||||||
|
assert!(
|
||||||
|
!out.contains("detached"),
|
||||||
|
"unknown id must not detach anything, got: {out:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn palette_rowop_nested_reports_per_entry() {
|
fn palette_rowop_nested_reports_per_entry() {
|
||||||
// Behavioral matrix: every row op on a NESTED row via the GUI
|
// Behavioral matrix: every row op on a NESTED row via the GUI
|
||||||
// message path. Nested rows must report per-entry errors (CLI
|
// message path. Nested rows must report per-entry errors (CLI
|
||||||
// wording), never silently skip.
|
// wording), never silently skip.
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
|
||||||
use codec::tables::BlockRecord;
|
use codec::tables::BlockRecord;
|
||||||
let dir = palette_tmpdir("nestedmatrix");
|
let dir = palette_tmpdir("nestedmatrix");
|
||||||
let mut host_doc = codec::CadDocument::new();
|
let mut host_doc = codec::CadDocument::new();
|
||||||
@@ -1539,13 +1568,14 @@ mod tests {
|
|||||||
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
||||||
app.tabs[i].current_path = Some(dir.join("app.dwg"));
|
app.tabs[i].current_path = Some(dir.join("app.dwg"));
|
||||||
app.refresh_xref_manager();
|
app.refresh_xref_manager();
|
||||||
let idx = app
|
let row = app
|
||||||
.xref_manager
|
.xref_manager
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|e| e.name == "INNER")
|
.find(|e| e.name == "INNER")
|
||||||
.expect("nested INNER listed");
|
.expect("nested INNER listed");
|
||||||
assert!(app.xref_manager.entries[idx].parent_key.is_some());
|
assert!(row.parent_key.is_some());
|
||||||
|
let row_id = XrefRowId::of(row.key, &row.saved_path);
|
||||||
for op in [
|
for op in [
|
||||||
XrefPaletteOp::Detach,
|
XrefPaletteOp::Detach,
|
||||||
XrefPaletteOp::Unload,
|
XrefPaletteOp::Unload,
|
||||||
@@ -1554,7 +1584,7 @@ mod tests {
|
|||||||
XrefPaletteOp::Overlay,
|
XrefPaletteOp::Overlay,
|
||||||
] {
|
] {
|
||||||
let start = app.command_line.history.len();
|
let start = app.command_line.history.len();
|
||||||
let _ = app.update(Message::XrefRowOp(idx, op));
|
let _ = app.update(Message::XrefRowOp(row_id.clone(), op));
|
||||||
let out = palette_output(&app, start);
|
let out = palette_output(&app, start);
|
||||||
assert!(
|
assert!(
|
||||||
out.contains("INNER"),
|
out.contains("INNER"),
|
||||||
@@ -1578,7 +1608,7 @@ mod tests {
|
|||||||
// Reproduction for "Detach/Unload from the palette do nothing":
|
// Reproduction for "Detach/Unload from the palette do nothing":
|
||||||
// drive the exact GUI message path (row right-click menu item).
|
// drive the exact GUI message path (row right-click menu item).
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
|
||||||
use codec::tables::BlockRecord;
|
use codec::tables::BlockRecord;
|
||||||
let mut app = fresh();
|
let mut app = fresh();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
@@ -1591,14 +1621,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_repro_host.dwg"));
|
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_repro_host.dwg"));
|
||||||
app.refresh_xref_manager();
|
app.refresh_xref_manager();
|
||||||
let idx = app
|
let plan = app
|
||||||
.xref_manager
|
.xref_manager
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|e| e.name == "PLAN")
|
.find(|e| e.name == "PLAN")
|
||||||
.expect("PLAN listed");
|
.expect("PLAN listed")
|
||||||
|
.clone();
|
||||||
|
let plan_id = XrefRowId::of(plan.key, &plan.saved_path);
|
||||||
let start = app.command_line.history.len();
|
let start = app.command_line.history.len();
|
||||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Detach));
|
let _ = app.update(Message::XrefRowOp(plan_id, XrefPaletteOp::Detach));
|
||||||
let out = palette_output(&app, start);
|
let out = palette_output(&app, start);
|
||||||
assert!(out.contains("PLAN"), "detach output missing, got: {out:?}");
|
assert!(out.contains("PLAN"), "detach output missing, got: {out:?}");
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1610,14 +1642,16 @@ mod tests {
|
|||||||
.is_none(),
|
.is_none(),
|
||||||
"PLAN definition must be gone"
|
"PLAN definition must be gone"
|
||||||
);
|
);
|
||||||
let idx = app
|
let site = app
|
||||||
.xref_manager
|
.xref_manager
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|e| e.name == "SITE")
|
.find(|e| e.name == "SITE")
|
||||||
.expect("SITE listed");
|
.expect("SITE listed")
|
||||||
|
.clone();
|
||||||
|
let site_id = XrefRowId::of(site.key, &site.saved_path);
|
||||||
let start = app.command_line.history.len();
|
let start = app.command_line.history.len();
|
||||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Unload));
|
let _ = app.update(Message::XrefRowOp(site_id, XrefPaletteOp::Unload));
|
||||||
let out = palette_output(&app, start);
|
let out = palette_output(&app, start);
|
||||||
assert!(out.contains("SITE"), "unload output missing, got: {out:?}");
|
assert!(out.contains("SITE"), "unload output missing, got: {out:?}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1637,7 +1671,7 @@ mod tests {
|
|||||||
// GUI message path: type flag flips, saved path clears.
|
// GUI message path: type flag flips, saved path clears.
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::io::xref_model::{Pathtype, RefType};
|
use crate::io::xref_model::{Pathtype, RefType};
|
||||||
use crate::ui::window::xref_manager::XrefPaletteOp;
|
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
|
||||||
use codec::tables::BlockRecord;
|
use codec::tables::BlockRecord;
|
||||||
let mut app = fresh();
|
let mut app = fresh();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
@@ -1648,14 +1682,16 @@ mod tests {
|
|||||||
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
app.tabs[i].scene.document.block_records.add(br).unwrap();
|
||||||
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_rowop_host.dwg"));
|
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_rowop_host.dwg"));
|
||||||
app.refresh_xref_manager();
|
app.refresh_xref_manager();
|
||||||
let idx = app
|
let plan = app
|
||||||
.xref_manager
|
.xref_manager
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|e| e.name == "PLAN")
|
.find(|e| e.name == "PLAN")
|
||||||
.expect("PLAN listed");
|
.expect("PLAN listed")
|
||||||
|
.clone();
|
||||||
|
let plan_id = XrefRowId::of(plan.key, &plan.saved_path);
|
||||||
let start = app.command_line.history.len();
|
let start = app.command_line.history.len();
|
||||||
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Overlay));
|
let _ = app.update(Message::XrefRowOp(plan_id.clone(), XrefPaletteOp::Overlay));
|
||||||
let out = palette_output(&app, start);
|
let out = palette_output(&app, start);
|
||||||
assert!(out.contains("PLAN"), "got: {out:?}");
|
assert!(out.contains("PLAN"), "got: {out:?}");
|
||||||
let br = app.tabs[i]
|
let br = app.tabs[i]
|
||||||
@@ -1676,7 +1712,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let start = app.command_line.history.len();
|
let start = app.command_line.history.len();
|
||||||
let _ = app.update(Message::XrefRowOp(
|
let _ = app.update(Message::XrefRowOp(
|
||||||
idx,
|
plan_id,
|
||||||
XrefPaletteOp::Pathtype(Pathtype::None),
|
XrefPaletteOp::Pathtype(Pathtype::None),
|
||||||
));
|
));
|
||||||
let out = palette_output(&app, start);
|
let out = palette_output(&app, start);
|
||||||
|
|||||||
@@ -682,6 +682,7 @@ impl OpenCADStudio {
|
|||||||
self.dyn_coord_absolute = false;
|
self.dyn_coord_absolute = false;
|
||||||
self.sync_dyn_fields();
|
self.sync_dyn_fields();
|
||||||
self.reset_tracking_after_point();
|
self.reset_tracking_after_point();
|
||||||
|
self.snapper.remember_distance_reference(dir);
|
||||||
self.push_ucs_to_cmd(i);
|
self.push_ucs_to_cmd(i);
|
||||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
|
||||||
let task = result.map(|r| self.apply_cmd_result(r))?;
|
let task = result.map(|r| self.apply_cmd_result(r))?;
|
||||||
|
|||||||
+607
-54
@@ -432,10 +432,16 @@ fn plot_content_extents(content: &PlotContent) -> Option<(f64, f64, f64, f64)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stamp the owning entity's effective color index onto a plot wire so CTB
|
/// Stamp the owning entity's effective color index onto a plot wire so CTB
|
||||||
/// plot-style lookups (`wire.aci > 0`) apply: an entity's own ACI 1-255 is
|
/// plot-style lookups (`wire.aci > 0`) apply.
|
||||||
/// used as-is, ByLayer resolves through the layer table, and true-color
|
///
|
||||||
/// objects keep `aci = 0` ("0 means true-color" — no CTB mapping). Wires
|
/// Direct entities are resolved here from their authored color/layer. Expanded
|
||||||
/// whose owner cannot be resolved keep the index they carry.
|
/// block wires are different: `BlockCache` has already resolved their effective
|
||||||
|
/// ACI through ByBlock and layer-0/ByLayer inheritance. Their wire name points
|
||||||
|
/// at the host INSERT rather than the child entity that produced the geometry,
|
||||||
|
/// so resolving that INSERT again would destroy the child's effective ACI.
|
||||||
|
///
|
||||||
|
/// In particular, a true-color INSERT on an indexed-color layer would overwrite
|
||||||
|
/// an inherited child ACI with 0 and bypass the CTB entirely.
|
||||||
fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireModel) {
|
fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireModel) {
|
||||||
let Some(handle) = crate::scene::Scene::handle_from_wire_name(&wire.name) else {
|
let Some(handle) = crate::scene::Scene::handle_from_wire_name(&wire.name) else {
|
||||||
return;
|
return;
|
||||||
@@ -443,6 +449,14 @@ fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireMode
|
|||||||
let Some(entity) = scene.document.get_entity(handle) else {
|
let Some(entity) = scene.document.get_entity(handle) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Expanded block geometry already carries the effective ACI calculated by
|
||||||
|
// BlockCache. `wire.name` identifies the host INSERT for picking, not the
|
||||||
|
// block child whose color inheritance produced `wire.aci`.
|
||||||
|
if matches!(entity, codec::EntityType::Insert(_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let common = entity.common();
|
let common = entity.common();
|
||||||
wire.aci = match &common.color {
|
wire.aci = match &common.color {
|
||||||
codec::types::Color::Index(index) => *index,
|
codec::types::Color::Index(index) => *index,
|
||||||
@@ -459,6 +473,74 @@ fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireMode
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod plot_owner_aci_tests {
|
||||||
|
use super::plot_owner_aci;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expanded_block_wire_keeps_its_resolved_effective_aci() {
|
||||||
|
let mut scene = crate::scene::Scene::new();
|
||||||
|
|
||||||
|
// Real-world regression case:
|
||||||
|
// the INSERT itself has explicit true-color white, while its block
|
||||||
|
// children are layer 0 + ByLayer. BlockCache resolves those children
|
||||||
|
// through the INSERT's layer and stores that effective ACI on the wire.
|
||||||
|
let mut insert = codec::entities::Insert::new("*U264", codec::types::Vector3::ZERO);
|
||||||
|
insert.common.color = codec::types::Color::Rgb {
|
||||||
|
r: 255,
|
||||||
|
g: 255,
|
||||||
|
b: 255,
|
||||||
|
};
|
||||||
|
|
||||||
|
let insert_handle = scene
|
||||||
|
.document
|
||||||
|
.add_entity(codec::EntityType::Insert(insert))
|
||||||
|
.expect("insert must be added");
|
||||||
|
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.name = insert_handle.value().to_string();
|
||||||
|
|
||||||
|
// Pretend BlockCache resolved the layer-0 child to indexed color 5.
|
||||||
|
// The plot preparation step must preserve it instead of replacing it
|
||||||
|
// with ACI 0 from the host INSERT's true color.
|
||||||
|
wire.aci = 5;
|
||||||
|
|
||||||
|
plot_owner_aci(&scene, &mut wire);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
wire.aci, 5,
|
||||||
|
"expanded block geometry must keep the effective ACI already resolved by BlockCache"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn direct_entity_still_refreshes_aci_from_its_owner() {
|
||||||
|
let mut scene = crate::scene::Scene::new();
|
||||||
|
|
||||||
|
let mut line = codec::entities::Line::from_points(
|
||||||
|
codec::types::Vector3::ZERO,
|
||||||
|
codec::types::Vector3::new(1.0, 0.0, 0.0),
|
||||||
|
);
|
||||||
|
line.common.color = codec::types::Color::Index(1);
|
||||||
|
|
||||||
|
let line_handle = scene
|
||||||
|
.document
|
||||||
|
.add_entity(codec::EntityType::Line(line))
|
||||||
|
.expect("line must be added");
|
||||||
|
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.name = line_handle.value().to_string();
|
||||||
|
wire.aci = 5;
|
||||||
|
|
||||||
|
plot_owner_aci(&scene, &mut wire);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
wire.aci, 1,
|
||||||
|
"ordinary entity plot wires must still resolve ACI from their owner"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn plot_scene_content(
|
fn plot_scene_content(
|
||||||
scene: &crate::scene::Scene,
|
scene: &crate::scene::Scene,
|
||||||
paper_space_last: bool,
|
paper_space_last: bool,
|
||||||
@@ -693,6 +775,8 @@ impl OpenCADStudio {
|
|||||||
polar: self.polar_mode,
|
polar: self.polar_mode,
|
||||||
polar_increment_deg: self.polar_increment_deg,
|
polar_increment_deg: self.polar_increment_deg,
|
||||||
zoom_wheel_reversed: self.zoom_wheel_reversed,
|
zoom_wheel_reversed: self.zoom_wheel_reversed,
|
||||||
|
touchpad_pan: self.touchpad_pan,
|
||||||
|
ui_scale_percent: self.ui_scale_percent,
|
||||||
zoom_factor: self.zoom_factor,
|
zoom_factor: self.zoom_factor,
|
||||||
cursor_size: self.cursor_size,
|
cursor_size: self.cursor_size,
|
||||||
pick_box: self.pick_box,
|
pick_box: self.pick_box,
|
||||||
@@ -709,6 +793,7 @@ impl OpenCADStudio {
|
|||||||
image_frame: crate::scene::frame::profile_image_mode(),
|
image_frame: crate::scene::frame::profile_image_mode(),
|
||||||
attdef_on_screen: crate::modules::draw::draw::attdef::session().on_screen,
|
attdef_on_screen: crate::modules::draw::draw::attdef::session().on_screen,
|
||||||
field_display: crate::entities::field::display(),
|
field_display: crate::entities::field::display(),
|
||||||
|
surface_associativity: crate::scene::model::sweep_model::surface_associativity(),
|
||||||
ncopy_bind: self.ncopy_bind,
|
ncopy_bind: self.ncopy_bind,
|
||||||
cursor_type: self.cursor_type,
|
cursor_type: self.cursor_type,
|
||||||
crosshair_color: self.crosshair_color,
|
crosshair_color: self.crosshair_color,
|
||||||
@@ -793,6 +878,8 @@ impl OpenCADStudio {
|
|||||||
self.polar_mode = s.polar;
|
self.polar_mode = s.polar;
|
||||||
self.polar_increment_deg = s.polar_increment_deg;
|
self.polar_increment_deg = s.polar_increment_deg;
|
||||||
self.zoom_wheel_reversed = s.zoom_wheel_reversed;
|
self.zoom_wheel_reversed = s.zoom_wheel_reversed;
|
||||||
|
self.touchpad_pan = s.touchpad_pan;
|
||||||
|
self.ui_scale_percent = crate::app::settings::clamp_ui_scale(s.ui_scale_percent);
|
||||||
self.zoom_factor = crate::app::settings::clamp_zoom_factor(s.zoom_factor);
|
self.zoom_factor = crate::app::settings::clamp_zoom_factor(s.zoom_factor);
|
||||||
// Like the drafting-rotation field below, the Options field edits a
|
// Like the drafting-rotation field below, the Options field edits a
|
||||||
// buffer rather than the value, so it is reseeded whenever the value
|
// buffer rather than the value, so it is reseeded whenever the value
|
||||||
@@ -819,6 +906,7 @@ impl OpenCADStudio {
|
|||||||
crate::scene::frame::set_profile_image_mode(s.image_frame);
|
crate::scene::frame::set_profile_image_mode(s.image_frame);
|
||||||
crate::modules::draw::draw::attdef::session().on_screen = s.attdef_on_screen;
|
crate::modules::draw::draw::attdef::session().on_screen = s.attdef_on_screen;
|
||||||
crate::entities::field::set_display(s.field_display);
|
crate::entities::field::set_display(s.field_display);
|
||||||
|
crate::scene::model::sweep_model::set_surface_associativity(s.surface_associativity);
|
||||||
self.ncopy_bind = s.ncopy_bind;
|
self.ncopy_bind = s.ncopy_bind;
|
||||||
self.cursor_type = s.cursor_type;
|
self.cursor_type = s.cursor_type;
|
||||||
self.crosshair_color = s.crosshair_color;
|
self.crosshair_color = s.crosshair_color;
|
||||||
@@ -1299,6 +1387,9 @@ impl OpenCADStudio {
|
|||||||
self.shortcut_bindings
|
self.shortcut_bindings
|
||||||
.entry("F5".to_string())
|
.entry("F5".to_string())
|
||||||
.or_insert_with(|| "ISOPLANE".to_string());
|
.or_insert_with(|| "ISOPLANE".to_string());
|
||||||
|
self.shortcut_bindings
|
||||||
|
.entry("SHIFT+TAB".to_string())
|
||||||
|
.or_insert_with(|| "DYNTABPREV".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the config only when it changed since the last write, so a toggle
|
/// Write the config only when it changed since the last write, so a toggle
|
||||||
@@ -1735,6 +1826,22 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drop Plot-dialog state that belongs to the previously active drawing.
|
||||||
|
///
|
||||||
|
/// Plot settings such as paper size and CTB are document/layout data.
|
||||||
|
/// A newly opened DWG may reuse the same tab index and layout name as the
|
||||||
|
/// startup drawing, so `(tab, layout)` alone is not enough to identify the
|
||||||
|
/// context. Keeping that cache makes the first Plot dialog show the startup
|
||||||
|
/// A4/no-CTB state; the second opening then appears correct only because the
|
||||||
|
/// first one refreshed the runtime state.
|
||||||
|
fn reset_plot_session_for_opened_document(&mut self) {
|
||||||
|
self.plot_prev = None;
|
||||||
|
self.plot_prev_context = None;
|
||||||
|
self.plot_reconcile_printer_media = None;
|
||||||
|
self.plot_setup_template = None;
|
||||||
|
self.plot_window = None;
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn on_file_opened(
|
pub(super) fn on_file_opened(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -1917,6 +2024,11 @@ impl OpenCADStudio {
|
|||||||
// A file saved without the built-in Standard styles (foreign
|
// A file saved without the built-in Standard styles (foreign
|
||||||
// or damaged) gets them re-seeded so nothing dangles (#366).
|
// or damaged) gets them re-seeded so nothing dangles (#366).
|
||||||
crate::app::style_ops::ensure_standard_styles(&mut self.tabs[i].scene.document);
|
crate::app::style_ops::ensure_standard_styles(&mut self.tabs[i].scene.document);
|
||||||
|
// Copies made by earlier releases kept their source's dimension
|
||||||
|
// association; such a file is reported as damaged elsewhere.
|
||||||
|
crate::scene::dimension_assoc::drop_stray_dimension_reactors(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
);
|
||||||
// Follow the file's saved current UCS from the moment it opens.
|
// Follow the file's saved current UCS from the moment it opens.
|
||||||
self.tabs[i].adopt_active_ucs_from_header();
|
self.tabs[i].adopt_active_ucs_from_header();
|
||||||
// Adopt the drawing's own Ortho ($ORTHOMODE) and running OSNAP
|
// Adopt the drawing's own Ortho ($ORTHOMODE) and running OSNAP
|
||||||
@@ -2004,6 +2116,12 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].scene.load_current_layout_state();
|
self.tabs[i].scene.load_current_layout_state();
|
||||||
self.tabs[i].refresh_active_ucs();
|
self.tabs[i].refresh_active_ucs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The tab may have been reused for the newly opened file. Any Plot
|
||||||
|
// dialog cache belongs to the previous/startup drawing and must not
|
||||||
|
// win over this DWG's embedded layout settings on the first Ctrl+P.
|
||||||
|
self.reset_plot_session_for_opened_document();
|
||||||
|
|
||||||
// Object isolation is session-only. A newly opened drawing must
|
// Object isolation is session-only. A newly opened drawing must
|
||||||
// not inherit the previous tab's filter, and persisted entity
|
// not inherit the previous tab's filter, and persisted entity
|
||||||
// visibility remains independent (not an isolation session).
|
// visibility remains independent (not an isolation session).
|
||||||
@@ -3825,22 +3943,27 @@ impl OpenCADStudio {
|
|||||||
let previous_style = self.active_plot_style.clone();
|
let previous_style = self.active_plot_style.clone();
|
||||||
let previous_window = self.plot_window;
|
let previous_window = self.plot_window;
|
||||||
|
|
||||||
// Once Print All has an explicit override, reopening Options must keep
|
// Refresh runtime-only data such as printers, available media,
|
||||||
// those settings instead of replacing them with the active layout's
|
// CTB names, scales and page-setup rows.
|
||||||
// page setup.
|
//
|
||||||
let keep_print_all_override = self.print_all_settings_override;
|
// `on_plot_dialog_open()` also seeds the editor from the active
|
||||||
|
// layout. That is correct for normal PLOT, but Print All Options is a
|
||||||
// Refresh runtime data such as printers, paper sizes, plot styles and
|
// shared batch override editor: its persisted user choices must win.
|
||||||
// named page setups.
|
|
||||||
let task = self.on_plot_dialog_open();
|
let task = self.on_plot_dialog_open();
|
||||||
|
|
||||||
if keep_print_all_override {
|
// Restore only user-editable values. `copy_settings_from` deliberately
|
||||||
// Restore only the user-editable plot settings. Runtime lists populated
|
// leaves the freshly refreshed runtime lists intact.
|
||||||
// above remain intact.
|
//
|
||||||
self.plot_dialog.copy_settings_from(&previous);
|
// Do this even on the first Print All Options opening after restarting
|
||||||
self.active_plot_style = previous_style.clone();
|
// OCS. Previously this happened only when `print_all_settings_override`
|
||||||
self.plot_window = previous_window;
|
// was already true, so the first opening always fell back to the
|
||||||
}
|
// active layout/default A4 and lost the persisted CTB selection.
|
||||||
|
self.plot_dialog.copy_settings_from(&previous);
|
||||||
|
|
||||||
|
// Preserve the parsed CTB when it matches the restored selection.
|
||||||
|
// If it is absent/stale, dialog_plot_style() can resolve it by name.
|
||||||
|
self.active_plot_style = previous_style.clone();
|
||||||
|
self.plot_window = previous_window;
|
||||||
|
|
||||||
self.print_all_options_prev = Some(previous);
|
self.print_all_options_prev = Some(previous);
|
||||||
self.print_all_plot_style_prev = Some(previous_style);
|
self.print_all_plot_style_prev = Some(previous_style);
|
||||||
@@ -4372,7 +4495,15 @@ impl OpenCADStudio {
|
|||||||
/// layout's plot settings and the printers found on the system.
|
/// layout's plot settings and the printers found on the system.
|
||||||
pub(super) fn on_plot_dialog_open(&mut self) -> Task<Message> {
|
pub(super) fn on_plot_dialog_open(&mut self) -> Task<Message> {
|
||||||
use crate::io::paper_catalog::Orientation;
|
use crate::io::paper_catalog::Orientation;
|
||||||
|
|
||||||
|
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
|
||||||
|
let same_context = self
|
||||||
|
.plot_prev_context
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|(tab, layout)| *tab == self.active_tab && layout == &cur);
|
||||||
|
|
||||||
let previous = self.plot_dialog.clone();
|
let previous = self.plot_dialog.clone();
|
||||||
|
|
||||||
let scales: Vec<(String, f64)> = self.tabs[self.active_tab]
|
let scales: Vec<(String, f64)> = self.tabs[self.active_tab]
|
||||||
.scene
|
.scene
|
||||||
.scale_list()
|
.scale_list()
|
||||||
@@ -4457,30 +4588,71 @@ impl OpenCADStudio {
|
|||||||
self.command_line
|
self.command_line
|
||||||
.push_warning(crate::tf!("Could not list printers: {error}").as_ref());
|
.push_warning(crate::tf!("Could not list printers: {error}").as_ref());
|
||||||
}
|
}
|
||||||
self.plot_prev = Some(previous);
|
self.plot_prev = Some(previous.clone());
|
||||||
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
|
|
||||||
let layout_entry = format!("*{cur}*");
|
let layout_entry = format!("*{cur}*");
|
||||||
// The auto-applied page setup must not reset the output device: a
|
|
||||||
// drawing rarely stores one, so honouring it would snap the dialog
|
if same_context {
|
||||||
// back to the system default on every open. The user's last choice
|
// #1654: reopening Plot for the same drawing/layout must keep the
|
||||||
// (persisted across sessions with the rest of the dialog) survives
|
// values the user just edited. The layout page setup is the initial
|
||||||
// unless the setup names an explicit device of its own.
|
// seed, not something that should overwrite paper, scale, CTB,
|
||||||
let kept_printer = self.plot_dialog.printer.clone();
|
// orientation, area and output choices on every Ctrl+P.
|
||||||
let kept_to_file = self.plot_dialog.to_file;
|
//
|
||||||
if self.tabs[self.active_tab]
|
// `copy_settings_from` deliberately leaves refreshed runtime lists
|
||||||
.scene
|
// (printers, media, scales, page setups, etc.) untouched.
|
||||||
.plot_settings_for(&cur)
|
self.plot_dialog.copy_settings_from(&previous);
|
||||||
.is_some()
|
self.plot_dialog.selected_setup = previous.selected_setup.clone();
|
||||||
{
|
|
||||||
self.select_page_setup(&layout_entry);
|
// A custom ratio such as 1:3 may not exist in the drawing's named
|
||||||
|
// scale list. select_page_setup() normally adds it while loading a
|
||||||
|
// PlotSettings, but same-context reopening intentionally skips that
|
||||||
|
// reload. Keep the restored custom scale available to the picker so
|
||||||
|
// the validation below does not collapse it to 1:1.
|
||||||
|
if !self.plot_dialog.fit_to_paper
|
||||||
|
&& !self
|
||||||
|
.plot_dialog
|
||||||
|
.scales
|
||||||
|
.iter()
|
||||||
|
.any(|(name, _)| name == &self.plot_dialog.scale)
|
||||||
|
{
|
||||||
|
let factor = plot_dialog_scale_factor(&self.plot_dialog);
|
||||||
|
|
||||||
|
if factor.is_finite() && factor > 0.0 {
|
||||||
|
let name = self.plot_dialog.scale.clone();
|
||||||
|
self.plot_dialog.scales.push((name, factor));
|
||||||
|
self.plot_dialog
|
||||||
|
.scales
|
||||||
|
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.select_page_setup(crate::ui::window::plot::SETUP_PREV);
|
// First open for this tab/layout: seed the editor from its actual
|
||||||
}
|
// page setup. An explicit setup selection later still uses
|
||||||
let setup_named_a_device = self.plot_dialog.printer.is_some() || self.plot_dialog.to_file;
|
// `select_page_setup` and therefore intentionally replaces fields.
|
||||||
if !setup_named_a_device {
|
//
|
||||||
self.plot_dialog.printer = kept_printer;
|
// Keep the chosen output device when a drawing page setup does not
|
||||||
self.plot_dialog.to_file = kept_to_file;
|
// name one of its own.
|
||||||
|
let kept_printer = self.plot_dialog.printer.clone();
|
||||||
|
let kept_to_file = self.plot_dialog.to_file;
|
||||||
|
|
||||||
|
if self.tabs[self.active_tab]
|
||||||
|
.scene
|
||||||
|
.plot_settings_for(&cur)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
self.select_page_setup(&layout_entry);
|
||||||
|
} else {
|
||||||
|
self.select_page_setup(crate::ui::window::plot::SETUP_PREV);
|
||||||
|
}
|
||||||
|
|
||||||
|
let setup_named_a_device =
|
||||||
|
self.plot_dialog.printer.is_some() || self.plot_dialog.to_file;
|
||||||
|
if !setup_named_a_device {
|
||||||
|
self.plot_dialog.printer = kept_printer;
|
||||||
|
self.plot_dialog.to_file = kept_to_file;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.plot_prev_context = Some((self.active_tab, cur.clone()));
|
||||||
// A persisted plot window (saved with the dialog settings) restores
|
// A persisted plot window (saved with the dialog settings) restores
|
||||||
// across sessions; a freshly loaded one wins so the two stay in sync.
|
// across sessions; a freshly loaded one wins so the two stay in sync.
|
||||||
if self.plot_window.is_none() {
|
if self.plot_window.is_none() {
|
||||||
@@ -4510,25 +4682,43 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||||
self.request_printer_media()
|
// Loading printer capabilities is informational when Plot merely
|
||||||
|
// opens. It must not replace the sheet stored in the drawing.
|
||||||
|
self.request_printer_media(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keep the dialog's view of the selected printer's media current: use the
|
/// Keep the dialog's view of the selected printer's media current: use the
|
||||||
/// session cache when the printer was asked before, otherwise ask it in
|
/// session cache when the printer was asked before, otherwise ask it in
|
||||||
/// the background and clear the stale answer meanwhile. PDF output and
|
/// the background and clear the stale answer meanwhile. PDF output and
|
||||||
/// the default printer list the catalogue instead.
|
/// the default printer list the catalogue instead.
|
||||||
fn request_printer_media(&mut self) -> Task<Message> {
|
fn request_printer_media(&mut self, reconcile: bool) -> Task<Message> {
|
||||||
let d = &mut self.plot_dialog;
|
let Some(printer) = (!self.plot_dialog.to_file)
|
||||||
let Some(printer) = (!d.to_file).then(|| d.printer.clone()).flatten() else {
|
.then(|| self.plot_dialog.printer.clone())
|
||||||
d.printer_media = None;
|
.flatten()
|
||||||
|
else {
|
||||||
|
self.plot_dialog.printer_media = None;
|
||||||
|
self.plot_reconcile_printer_media = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PAPERUPDATE is relevant after the user explicitly changes the
|
||||||
|
// device/page setup. Merely reopening Plot must leave the drawing's
|
||||||
|
// stored paper untouched.
|
||||||
|
self.plot_reconcile_printer_media = reconcile.then(|| printer.clone());
|
||||||
|
|
||||||
if let Some(caps) = crate::io::plot_device::cached_printer_capabilities(&printer) {
|
if let Some(caps) = crate::io::plot_device::cached_printer_capabilities(&printer) {
|
||||||
d.printer_media = Some(caps);
|
self.plot_dialog.printer_media = Some(caps);
|
||||||
|
|
||||||
|
if reconcile {
|
||||||
|
self.reconcile_sheet_with_printer(&printer);
|
||||||
|
}
|
||||||
|
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
d.printer_media = None;
|
|
||||||
|
self.plot_dialog.printer_media = None;
|
||||||
let name = printer.clone();
|
let name = printer.clone();
|
||||||
|
|
||||||
background_task(
|
background_task(
|
||||||
crate::t!("Printer capabilities"),
|
crate::t!("Printer capabilities"),
|
||||||
move || crate::io::plot_device::printer_capabilities(&printer),
|
move || crate::io::plot_device::printer_capabilities(&printer),
|
||||||
@@ -4703,6 +4893,9 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
match msg {
|
match msg {
|
||||||
M::Close => {
|
M::Close => {
|
||||||
|
// Keep the user's last Plot choices available for the next
|
||||||
|
// invocation even when the dialog was simply closed.
|
||||||
|
self.save_config();
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -4720,14 +4913,31 @@ impl OpenCADStudio {
|
|||||||
self.plot_dialog.printer = Some(s);
|
self.plot_dialog.printer = Some(s);
|
||||||
}
|
}
|
||||||
self.save_config();
|
self.save_config();
|
||||||
self.request_printer_media()
|
// This is an explicit device change, so PAPERUPDATE may
|
||||||
|
// reconcile an unsupported sheet when its capabilities arrive.
|
||||||
|
self.request_printer_media(true)
|
||||||
}
|
}
|
||||||
M::PrinterMedia(printer, caps) => {
|
M::PrinterMedia(printer, caps) => {
|
||||||
let d = &mut self.plot_dialog;
|
let still_current = !self.plot_dialog.to_file
|
||||||
if !d.to_file && d.printer.as_deref() == Some(printer.as_str()) {
|
&& self.plot_dialog.printer.as_deref() == Some(printer.as_str());
|
||||||
d.printer_media = caps;
|
|
||||||
self.reconcile_sheet_with_printer(&printer);
|
if still_current {
|
||||||
|
self.plot_dialog.printer_media = caps;
|
||||||
|
|
||||||
|
let reconcile = self
|
||||||
|
.plot_reconcile_printer_media
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|pending| pending.eq_ignore_ascii_case(&printer));
|
||||||
|
|
||||||
|
if reconcile {
|
||||||
|
// Keep the explicit-device context alive while this
|
||||||
|
// printer remains selected. Capability updates for the
|
||||||
|
// same user-selected device must continue to obey
|
||||||
|
// PAPERUPDATE.
|
||||||
|
self.reconcile_sheet_with_printer(&printer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
M::CustomPaper(msg) => {
|
M::CustomPaper(msg) => {
|
||||||
@@ -4991,13 +5201,44 @@ impl OpenCADStudio {
|
|||||||
Task::done(Message::Command("PLOTWINDOW".into()))
|
Task::done(Message::Command("PLOTWINDOW".into()))
|
||||||
}
|
}
|
||||||
M::SelectSetup(name) => {
|
M::SelectSetup(name) => {
|
||||||
|
// Print All Options edits the page setups represented by the
|
||||||
|
// layout rows. With "Save changes to layout" enabled, preserve
|
||||||
|
// the setup we are leaving before another one is loaded.
|
||||||
|
if self.print_all_options
|
||||||
|
&& self.plot_dialog.save_to_layout
|
||||||
|
&& self.plot_dialog.selected_setup != name
|
||||||
|
{
|
||||||
|
let previous = self.plot_dialog.selected_setup.clone();
|
||||||
|
|
||||||
|
if is_layout_entry(&previous) {
|
||||||
|
let layout = layout_entry_name(&previous).to_string();
|
||||||
|
self.apply_dialog_to_named_layout(&layout);
|
||||||
|
} else if previous == crate::ui::window::plot::SETUP_PREV {
|
||||||
|
// `<previous>` is a transient snapshot, not an actual
|
||||||
|
// page setup. `plot_prev_context` records which layout
|
||||||
|
// that snapshot came from.
|
||||||
|
//
|
||||||
|
// If the user returns to that same layout, commit the
|
||||||
|
// restored snapshot to it before reloading the row.
|
||||||
|
if let Some((tab, layout)) = self.plot_prev_context.clone() {
|
||||||
|
let target = format!("*{layout}*");
|
||||||
|
|
||||||
|
if tab == self.active_tab && name == target {
|
||||||
|
self.apply_dialog_to_named_layout(&layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.select_page_setup(&name);
|
self.select_page_setup(&name);
|
||||||
|
|
||||||
if self.print_all_options {
|
if self.print_all_options {
|
||||||
self.plot_dialog.paper_space = true;
|
self.plot_dialog.paper_space = true;
|
||||||
self.plot_dialog.area = "Layout".into();
|
self.plot_dialog.area = "Layout".into();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The setup may name a different printer.
|
// The setup may name a different printer.
|
||||||
self.request_printer_media()
|
self.request_printer_media(true)
|
||||||
}
|
}
|
||||||
M::SetCurrent => {
|
M::SetCurrent => {
|
||||||
self.apply_dialog_to_layout();
|
self.apply_dialog_to_layout();
|
||||||
@@ -5425,6 +5666,38 @@ impl OpenCADStudio {
|
|||||||
self.apply_plot_page_settings();
|
self.apply_plot_page_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Persist the current Plot editor into a specific layout.
|
||||||
|
///
|
||||||
|
/// This is needed by Print All Options because selecting `*Layout1*`,
|
||||||
|
/// `*Layout2*`, etc. does not switch `scene.current_layout`.
|
||||||
|
fn apply_dialog_to_named_layout(&mut self, layout_name: &str) {
|
||||||
|
use codec::objects::{PlotPaperUnits, PlotSettings};
|
||||||
|
|
||||||
|
let i = self.active_tab;
|
||||||
|
|
||||||
|
let base = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.plot_settings_for(layout_name)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let mut settings = PlotSettings::new("");
|
||||||
|
settings.paper_units = PlotPaperUnits::Millimeters;
|
||||||
|
settings
|
||||||
|
});
|
||||||
|
|
||||||
|
let ps = self.plot_settings_from_dialog(base);
|
||||||
|
|
||||||
|
if self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.set_layout_plot_settings(layout_name, &ps)
|
||||||
|
{
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
|
||||||
|
if self.tabs[i].scene.current_layout == layout_name {
|
||||||
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Open a preview PDF, export a PDF, or send the job to the chosen printer.
|
/// Open a preview PDF, export a PDF, or send the job to the chosen printer.
|
||||||
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
|
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
|
||||||
let d = self.plot_dialog.clone();
|
let d = self.plot_dialog.clone();
|
||||||
@@ -5576,10 +5849,21 @@ impl OpenCADStudio {
|
|||||||
if d.style_name.is_empty() || d.style_missing || !d.apply_plot_styles {
|
if d.style_name.is_empty() || d.style_missing || !d.apply_plot_styles {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.active_plot_style
|
|
||||||
|
// Fast path: the selected table is already active.
|
||||||
|
if let Some(table) = self
|
||||||
|
.active_plot_style
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|table| table.name.eq_ignore_ascii_case(&d.style_name))
|
.filter(|table| table.name.eq_ignore_ascii_case(&d.style_name))
|
||||||
.cloned()
|
{
|
||||||
|
return Some(table.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dialog selection is persisted separately from the parsed CTB
|
||||||
|
// object. After reopening a drawing the name may be correct while the
|
||||||
|
// in-memory table is still the previous/default one. Plotting must not
|
||||||
|
// silently ignore the selected CTB in that state.
|
||||||
|
crate::io::plot_style::PlotStyleTable::load_named(&d.style_name).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::app) fn window_plot_job(&self) -> Option<PdfPageInput> {
|
pub(in crate::app) fn window_plot_job(&self) -> Option<PdfPageInput> {
|
||||||
@@ -6931,6 +7215,275 @@ mod plot_device_persistence_tests {
|
|||||||
assert!(!app.plot_dialog.to_file);
|
assert!(!app.plot_dialog.to_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reopening Plot in the same tab/layout keeps the user's most recent
|
||||||
|
/// paper, scale and CTB choices instead of reloading the layout page setup
|
||||||
|
/// over them. Regression test for #1654.
|
||||||
|
#[test]
|
||||||
|
fn reopening_same_plot_context_keeps_recent_settings() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
|
||||||
|
// First open seeds from the drawing/layout as usual.
|
||||||
|
let _ = app.on_plot_dialog_open();
|
||||||
|
|
||||||
|
// User edits the high-frequency Plot settings.
|
||||||
|
app.plot_dialog.paper = "ISO_A3_(297.00_x_420.00_MM)".into();
|
||||||
|
app.plot_dialog.paper_width_mm = 297.0;
|
||||||
|
app.plot_dialog.paper_height_mm = 420.0;
|
||||||
|
app.plot_dialog.scale = "1:100".into();
|
||||||
|
app.plot_dialog.fit_to_paper = false;
|
||||||
|
app.plot_dialog.style_name = "office.ctb".into();
|
||||||
|
app.plot_dialog.apply_plot_styles = true;
|
||||||
|
|
||||||
|
// Reopening Ctrl+P in the same layout must not reapply the layout's
|
||||||
|
// old page setup and erase those choices.
|
||||||
|
let _ = app.on_plot_dialog_open();
|
||||||
|
|
||||||
|
assert_eq!(app.plot_dialog.paper, "ISO_A3_(297.00_x_420.00_MM)");
|
||||||
|
assert_eq!(app.plot_dialog.scale, "1:100");
|
||||||
|
assert!(!app.plot_dialog.fit_to_paper);
|
||||||
|
assert_eq!(app.plot_dialog.style_name, "office.ctb");
|
||||||
|
assert!(app.plot_dialog.apply_plot_styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opening a different drawing in a reused tab must invalidate the Plot
|
||||||
|
/// session cache. The first Plot dialog must read paper and CTB from the
|
||||||
|
/// newly opened document instead of showing the startup drawing's values.
|
||||||
|
#[test]
|
||||||
|
fn newly_opened_document_forces_first_plot_dialog_to_read_layout() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let i = app.active_tab;
|
||||||
|
|
||||||
|
app.tabs[i].scene.current_layout = "Layout1".into();
|
||||||
|
|
||||||
|
let mut ps = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.plot_settings_for("Layout1")
|
||||||
|
.expect("Layout1 must exist");
|
||||||
|
|
||||||
|
ps.paper_size = "ISO_A2_(420.00_x_594.00_MM)".into();
|
||||||
|
ps.paper_width = 420.0;
|
||||||
|
ps.paper_height = 594.0;
|
||||||
|
ps.current_style_sheet = crate::io::plot_style::MONOCHROME_PLOT_STYLE.into();
|
||||||
|
ps.flags.plot_plot_styles = true;
|
||||||
|
|
||||||
|
assert!(app.tabs[i].scene.set_layout_plot_settings("Layout1", &ps));
|
||||||
|
|
||||||
|
// Reproduce stale state inherited from the startup/previous drawing.
|
||||||
|
app.plot_dialog.paper = "ISO_A4_(210.00_x_297.00_MM)".into();
|
||||||
|
app.plot_dialog.style_name.clear();
|
||||||
|
app.plot_dialog.apply_plot_styles = false;
|
||||||
|
|
||||||
|
app.plot_prev = Some(app.plot_dialog.clone());
|
||||||
|
app.plot_prev_context = Some((i, "Layout1".into()));
|
||||||
|
|
||||||
|
// Installing another drawing must make that state unusable.
|
||||||
|
app.reset_plot_session_for_opened_document();
|
||||||
|
|
||||||
|
assert!(app.plot_prev.is_none());
|
||||||
|
assert!(app.plot_prev_context.is_none());
|
||||||
|
|
||||||
|
let _ = app.on_plot_dialog_open();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
app.plot_dialog.paper, "ISO_A2_(420.00_x_594.00_MM)",
|
||||||
|
"the first Plot opening must use the new drawing's paper"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
app.plot_dialog
|
||||||
|
.style_name
|
||||||
|
.eq_ignore_ascii_case(crate::io::plot_style::MONOCHROME_PLOT_STYLE),
|
||||||
|
"the first Plot opening must use the new drawing's CTB"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
app.plot_dialog.apply_plot_styles,
|
||||||
|
"the stored CTB must already be enabled on the first Plot opening"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print All Options is a shared batch-settings editor. On the first
|
||||||
|
/// opening after restarting OCS it must keep the persisted user choices
|
||||||
|
/// instead of replacing them with the active layout's page setup.
|
||||||
|
#[test]
|
||||||
|
fn print_all_options_keeps_persisted_plot_preferences_on_first_open() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
|
||||||
|
// Simulate preferences restored from the previous application session.
|
||||||
|
app.plot_dialog.paper = "ISO_A2_(420.00_x_594.00_MM)".into();
|
||||||
|
app.plot_dialog.paper_width_mm = 420.0;
|
||||||
|
app.plot_dialog.paper_height_mm = 594.0;
|
||||||
|
app.plot_dialog.scale = "1:1000".into();
|
||||||
|
app.plot_dialog.style_name = crate::io::plot_style::MONOCHROME_PLOT_STYLE.into();
|
||||||
|
app.plot_dialog.apply_plot_styles = true;
|
||||||
|
|
||||||
|
// Fresh process: no explicit Print All override exists yet.
|
||||||
|
app.print_all_settings_override = false;
|
||||||
|
|
||||||
|
let _ = app.on_print_all_options();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
app.plot_dialog.paper, "ISO_A2_(420.00_x_594.00_MM)",
|
||||||
|
"first Print All Options opening must keep the persisted paper"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
app.plot_dialog.scale, "1:1000",
|
||||||
|
"first Print All Options opening must keep the persisted scale"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
app.plot_dialog
|
||||||
|
.style_name
|
||||||
|
.eq_ignore_ascii_case(crate::io::plot_style::MONOCHROME_PLOT_STYLE),
|
||||||
|
"first Print All Options opening must keep the persisted CTB"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
app.plot_dialog.apply_plot_styles,
|
||||||
|
"the persisted CTB must remain enabled"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
app.print_all_options,
|
||||||
|
"the dialog must remain in Print All Options mode"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `<previous>` belongs to the layout that produced the snapshot.
|
||||||
|
/// Returning to that layout with "Save changes to layout" enabled must
|
||||||
|
/// preserve the restored paper/CTB instead of reloading the old embedded
|
||||||
|
/// page setup over it.
|
||||||
|
#[test]
|
||||||
|
fn previous_snapshot_is_saved_back_to_its_origin_layout() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let i = app.active_tab;
|
||||||
|
|
||||||
|
app.tabs[i].scene.current_layout = "Layout1".into();
|
||||||
|
|
||||||
|
// Layout1 still contains an old page setup.
|
||||||
|
let mut stored = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.plot_settings_for("Layout1")
|
||||||
|
.expect("Layout1 must exist");
|
||||||
|
|
||||||
|
stored.paper_size = "ISO_A4_(210.00_x_297.00_MM)".into();
|
||||||
|
stored.paper_width = 210.0;
|
||||||
|
stored.paper_height = 297.0;
|
||||||
|
stored.current_style_sheet.clear();
|
||||||
|
stored.flags.plot_plot_styles = false;
|
||||||
|
|
||||||
|
assert!(app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.set_layout_plot_settings("Layout1", &stored));
|
||||||
|
|
||||||
|
// `<previous>` is the desired state that originally belonged to
|
||||||
|
// Layout1.
|
||||||
|
app.print_all_options = true;
|
||||||
|
app.plot_prev_context = Some((i, "Layout1".into()));
|
||||||
|
app.plot_dialog.selected_setup = crate::ui::window::plot::SETUP_PREV.into();
|
||||||
|
|
||||||
|
app.plot_dialog.paper = "ISO_A2_(420.00_x_594.00_MM)".into();
|
||||||
|
app.plot_dialog.paper_width_mm = 420.0;
|
||||||
|
app.plot_dialog.paper_height_mm = 594.0;
|
||||||
|
app.plot_dialog.style_name = crate::io::plot_style::MONOCHROME_PLOT_STYLE.into();
|
||||||
|
app.plot_dialog.apply_plot_styles = true;
|
||||||
|
app.plot_dialog.save_to_layout = true;
|
||||||
|
|
||||||
|
let _ = app.on_plot_dlg(crate::ui::window::plot::PlotDlgMsg::SelectSetup(
|
||||||
|
"*Layout1*".into(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let saved = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.plot_settings_for("Layout1")
|
||||||
|
.expect("Layout1 must still exist");
|
||||||
|
|
||||||
|
assert_eq!(saved.paper_size, "ISO_A2_(420.00_x_594.00_MM)");
|
||||||
|
|
||||||
|
assert!(saved
|
||||||
|
.current_style_sheet
|
||||||
|
.eq_ignore_ascii_case(crate::io::plot_style::MONOCHROME_PLOT_STYLE));
|
||||||
|
|
||||||
|
assert!(saved.flags.plot_plot_styles);
|
||||||
|
|
||||||
|
assert_eq!(app.plot_dialog.paper, "ISO_A2_(420.00_x_594.00_MM)");
|
||||||
|
|
||||||
|
assert!(app
|
||||||
|
.plot_dialog
|
||||||
|
.style_name
|
||||||
|
.eq_ignore_ascii_case(crate::io::plot_style::MONOCHROME_PLOT_STYLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A CTB selected in the dialog must be usable even when the parsed
|
||||||
|
/// active table was not restored with it. This is the state seen after
|
||||||
|
/// reopening a drawing: the dropdown can name the correct CTB while the
|
||||||
|
/// first preview otherwise plots using raw layer colours.
|
||||||
|
#[test]
|
||||||
|
fn selected_ctb_is_resolved_when_active_table_is_stale() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
|
||||||
|
app.plot_dialog.style_name = crate::io::plot_style::MONOCHROME_PLOT_STYLE.into();
|
||||||
|
app.plot_dialog.apply_plot_styles = true;
|
||||||
|
app.plot_dialog.style_missing = false;
|
||||||
|
|
||||||
|
// Reproduce a fresh-session mismatch: the visible selection exists,
|
||||||
|
// but no matching parsed table is active yet.
|
||||||
|
app.active_plot_style = None;
|
||||||
|
|
||||||
|
let table = app
|
||||||
|
.dialog_plot_style(&app.plot_dialog)
|
||||||
|
.expect("selected CTB must be resolved by name");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
table
|
||||||
|
.name
|
||||||
|
.eq_ignore_ascii_case(crate::io::plot_style::MONOCHROME_PLOT_STYLE),
|
||||||
|
"the table used for plotting must match the visible CTB selection"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Printer capability discovery on merely opening Plot must never mutate
|
||||||
|
/// the paper stored in the drawing. PAPERUPDATE is applied only after an
|
||||||
|
/// explicit device/page-setup choice.
|
||||||
|
#[test]
|
||||||
|
fn opening_plot_does_not_replace_sheet_when_printer_media_arrives() {
|
||||||
|
use crate::io::paper_catalog::{resolve, Margins};
|
||||||
|
use crate::io::plot_device::{PrinterCapabilities, PrinterMedia};
|
||||||
|
|
||||||
|
let a2 = resolve("ISO_A2_(420.00_x_594.00_MM)").expect("A2");
|
||||||
|
let a4 = resolve("ISO_A4_(210.00_x_297.00_MM)").expect("A4");
|
||||||
|
|
||||||
|
let caps = std::sync::Arc::new(PrinterCapabilities {
|
||||||
|
media: vec![PrinterMedia {
|
||||||
|
paper: a4.clone(),
|
||||||
|
margins: Margins::uniform(3.0),
|
||||||
|
borderless: false,
|
||||||
|
}],
|
||||||
|
default_paper: Some(a4),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.plot_dialog.to_file = false;
|
||||||
|
app.plot_dialog.printer = Some("OCS Test Printer".into());
|
||||||
|
app.plot_dialog.paper = a2.canonical.to_string();
|
||||||
|
app.plot_dialog.paper_update = 1;
|
||||||
|
|
||||||
|
// No explicit printer change is pending: this models the asynchronous
|
||||||
|
// capability reply caused only by opening the Plot dialog.
|
||||||
|
app.plot_reconcile_printer_media = None;
|
||||||
|
|
||||||
|
let _ = app.on_plot_dlg(crate::ui::window::plot::PlotDlgMsg::PrinterMedia(
|
||||||
|
"OCS Test Printer".into(),
|
||||||
|
Some(caps),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
app.plot_dialog.paper, a2.canonical,
|
||||||
|
"opening Plot must preserve the drawing's stored sheet"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A layout whose plot settings name an explicit device wins over the
|
/// A layout whose plot settings name an explicit device wins over the
|
||||||
/// saved preference: selecting its setup is an explicit device choice.
|
/// saved preference: selecting its setup is an explicit device choice.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+294
-98
@@ -24,6 +24,7 @@ fn is_modal_blocked_key_msg(msg: &Message) -> bool {
|
|||||||
| Message::CommandLineArrowProbe { .. }
|
| Message::CommandLineArrowProbe { .. }
|
||||||
| Message::CommandLineArrowResolved { .. }
|
| Message::CommandLineArrowResolved { .. }
|
||||||
| Message::DynTabNext
|
| Message::DynTabNext
|
||||||
|
| Message::DynTabPrev
|
||||||
| Message::MTextCaretMove(_)
|
| Message::MTextCaretMove(_)
|
||||||
| Message::DeleteSelected
|
| Message::DeleteSelected
|
||||||
| Message::ToggleSnapEnabled
|
| Message::ToggleSnapEnabled
|
||||||
@@ -533,7 +534,9 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(started) = perf_started {
|
if let Some(started) = perf_started {
|
||||||
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
let elapsed_us = started.elapsed().as_micros() as u64;
|
||||||
|
crate::perf::note_update_time(elapsed_us);
|
||||||
|
let elapsed_ms = elapsed_us as f64 / 1000.0;
|
||||||
if elapsed_ms >= 5.0 {
|
if elapsed_ms >= 5.0 {
|
||||||
crate::perf_record!("[perf] update {:>7.1}ms message={perf_label}", elapsed_ms,);
|
crate::perf_record!("[perf] update {:>7.1}ms message={perf_label}", elapsed_ms,);
|
||||||
}
|
}
|
||||||
@@ -2165,16 +2168,21 @@ impl OpenCADStudio {
|
|||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
// Capture prompt/options while holding cmd borrow, then release before
|
// Capture prompt/options while holding cmd borrow, then release before
|
||||||
// borrowing command_line to satisfy borrow checker.
|
// borrowing command_line to satisfy borrow checker.
|
||||||
let (should_update, opts, prompt) =
|
let (should_update, opts, prompt, suggestions) =
|
||||||
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||||
if cmd.on_live_input(&live_input) {
|
let changed = cmd.on_live_input(&live_input);
|
||||||
(true, cmd.options(), cmd.prompt())
|
let suggestions = cmd.text_suggestions(&live_input);
|
||||||
|
if changed {
|
||||||
|
(true, cmd.options(), cmd.prompt(), suggestions)
|
||||||
} else {
|
} else {
|
||||||
(false, Vec::new(), String::new())
|
(false, Vec::new(), String::new(), suggestions)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(false, Vec::new(), String::new())
|
(false, Vec::new(), String::new(), Vec::new())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.command_line.set_contextual_suggestions(suggestions);
|
||||||
|
|
||||||
if should_update {
|
if should_update {
|
||||||
self.command_line.set_step_options(opts);
|
self.command_line.set_step_options(opts);
|
||||||
// Update pinned prompt text live without pushing new history entry
|
// Update pinned prompt text live without pushing new history entry
|
||||||
@@ -2203,6 +2211,12 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::DynTabNext => {
|
Message::DynTabNext => {
|
||||||
|
if self.command_line.has_contextual_suggestions()
|
||||||
|
&& self.command_line.autocomplete_next()
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let n = self.tabs[i].dyn_fields.len();
|
let n = self.tabs[i].dyn_fields.len();
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
@@ -2214,6 +2228,13 @@ impl OpenCADStudio {
|
|||||||
self.focus_cmd_input()
|
self.focus_cmd_input()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::DynTabPrev => {
|
||||||
|
if self.command_line.has_contextual_suggestions() {
|
||||||
|
self.command_line.autocomplete_prev();
|
||||||
|
}
|
||||||
|
self.focus_cmd_input()
|
||||||
|
}
|
||||||
|
|
||||||
Message::SplitModelViewport(horizontal) => {
|
Message::SplitModelViewport(horizontal) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.tabs[i].scene.split_active_pane(horizontal);
|
self.tabs[i].scene.split_active_pane(horizontal);
|
||||||
@@ -2250,7 +2271,8 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if !self.command_line.history_navigation_active()
|
if !self.command_line.history_navigation_active()
|
||||||
&& self.tabs[i].active_cmd.is_none()
|
&& (self.tabs[i].active_cmd.is_none()
|
||||||
|
|| self.command_line.has_contextual_suggestions())
|
||||||
&& self.command_line.autocomplete_prev()
|
&& self.command_line.autocomplete_prev()
|
||||||
{
|
{
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -2276,7 +2298,8 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if !self.command_line.history_navigation_active()
|
if !self.command_line.history_navigation_active()
|
||||||
&& self.tabs[i].active_cmd.is_none()
|
&& (self.tabs[i].active_cmd.is_none()
|
||||||
|
|| self.command_line.has_contextual_suggestions())
|
||||||
&& self.command_line.autocomplete_next()
|
&& self.command_line.autocomplete_next()
|
||||||
{
|
{
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -2477,11 +2500,20 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::CommandSuggestionPick(cmd) => {
|
Message::CommandSuggestionPick(value) => {
|
||||||
|
self.command_line.close_history();
|
||||||
|
|
||||||
|
if self.tabs[self.active_tab].active_cmd.is_some()
|
||||||
|
&& self.command_line.has_contextual_suggestions()
|
||||||
|
{
|
||||||
|
self.command_line.input = value;
|
||||||
|
self.command_line.autocomplete_cursor = None;
|
||||||
|
return self.on_command_submit();
|
||||||
|
}
|
||||||
|
|
||||||
self.command_line.input.clear();
|
self.command_line.input.clear();
|
||||||
self.command_line.autocomplete_cursor = None;
|
self.command_line.autocomplete_cursor = None;
|
||||||
self.command_line.close_history();
|
self.dispatch_command(&value)
|
||||||
self.dispatch_command(&cmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::CommandOptionPick(kw) => {
|
Message::CommandOptionPick(kw) => {
|
||||||
@@ -2584,7 +2616,7 @@ impl OpenCADStudio {
|
|||||||
self.refresh_xref_manager();
|
self.refresh_xref_manager();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::XrefManagerSelect(index) => {
|
Message::XrefManagerSelect(id) => {
|
||||||
use crate::ui::window::xref_manager::SelectExtend;
|
use crate::ui::window::xref_manager::SelectExtend;
|
||||||
// Normal GUI list behavior off the globally tracked
|
// Normal GUI list behavior off the globally tracked
|
||||||
// modifiers (same source as the layer list): plain click
|
// modifiers (same source as the layer list): plain click
|
||||||
@@ -2596,12 +2628,12 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
SelectExtend::Single
|
SelectExtend::Single
|
||||||
};
|
};
|
||||||
self.xref_manager.click_select(index, extend);
|
self.xref_manager.click_select(&id, extend);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::XrefRowRightClick(index) => {
|
Message::XrefRowRightClick(id) => {
|
||||||
self.xref_manager.row_change_path_open = false;
|
self.xref_manager.row_change_path_open = false;
|
||||||
self.xref_manager.right_click_select(index);
|
self.xref_manager.right_click_select(&id);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::XrefManagerToggleTree => {
|
Message::XrefManagerToggleTree => {
|
||||||
@@ -2744,67 +2776,76 @@ impl OpenCADStudio {
|
|||||||
self.xref_manager_op(op);
|
self.xref_manager_op(op);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::XrefRowOp(index, op) => {
|
Message::XrefRowOp(id, op) => {
|
||||||
// Open is not a palette op — it navigates to the file.
|
// Open is not a palette op — it navigates to the file.
|
||||||
if op == crate::ui::window::xref_manager::XrefPaletteOp::Open {
|
if op == crate::ui::window::xref_manager::XrefPaletteOp::Open {
|
||||||
self.xref_manager.right_click_select(index);
|
self.xref_manager.right_click_select(&id);
|
||||||
self.xref_manager.row_change_path_open = false;
|
self.xref_manager.row_change_path_open = false;
|
||||||
if let Some(entry) = self.xref_manager.entries.get(index) {
|
if let Some(index) = self.xref_manager.row_index(&id) {
|
||||||
if let Some(found) = entry.found_at.clone() {
|
if let Some(entry) = self.xref_manager.entries.get(index) {
|
||||||
let is_dwg = found.to_ascii_lowercase().ends_with(".dwg")
|
if let Some(found) = entry.found_at.clone() {
|
||||||
|| found.to_ascii_lowercase().ends_with(".dxf");
|
let is_dwg = found.to_ascii_lowercase().ends_with(".dwg")
|
||||||
self.command_line
|
|| found.to_ascii_lowercase().ends_with(".dxf");
|
||||||
.push_output(crate::tf!("XOPEN: opening \"{}\".", found).as_ref());
|
self.command_line.push_output(
|
||||||
if is_dwg {
|
crate::tf!("XOPEN: opening \"{}\".", found).as_ref(),
|
||||||
return self
|
);
|
||||||
.update(Message::OpenRecent(std::path::PathBuf::from(found)));
|
if is_dwg {
|
||||||
|
return self.update(Message::OpenRecent(
|
||||||
|
std::path::PathBuf::from(found),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let _ = open::that_detached(&found);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
self.command_line.push_error(
|
||||||
let _ = open::that_detached(&found);
|
crate::t!("File not found — check the saved path.").as_ref(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
self.command_line.push_error(
|
|
||||||
crate::t!("File not found — check the saved path.").as_ref(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
// Attach prompts the file dialog to pick a drawing to attach.
|
// Attach prompts the file dialog to pick a drawing to attach.
|
||||||
if matches!(op, crate::ui::window::xref_manager::XrefPaletteOp::Attach) {
|
if matches!(op, crate::ui::window::xref_manager::XrefPaletteOp::Attach) {
|
||||||
self.xref_manager.right_click_select(index);
|
self.xref_manager.right_click_select(&id);
|
||||||
self.xref_manager.row_change_path_open = false;
|
self.xref_manager.row_change_path_open = false;
|
||||||
self.command_line.push_output(crate::t!("XATTACH").as_ref());
|
self.command_line.push_output(crate::t!("XATTACH").as_ref());
|
||||||
return self.update(Message::XAttachPick);
|
return self.update(Message::XAttachPick);
|
||||||
}
|
}
|
||||||
// Row-scoped op: select the row first, then run the op.
|
// Row-scoped op: select the row first, then run the op.
|
||||||
self.xref_manager.right_click_select(index);
|
self.xref_manager.right_click_select(&id);
|
||||||
self.xref_manager.row_change_path_open = false;
|
self.xref_manager.row_change_path_open = false;
|
||||||
self.xref_manager_op(op);
|
self.xref_manager_op(op);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::XrefRowPathPick(index) => {
|
Message::XrefRowPathPick(id) => {
|
||||||
self.xref_manager.right_click_select(index);
|
self.xref_manager.right_click_select(&id);
|
||||||
self.xref_manager.row_change_path_open = false;
|
self.xref_manager.row_change_path_open = false;
|
||||||
self.update(Message::XrefPathPick)
|
self.update(Message::XrefPathPick)
|
||||||
}
|
}
|
||||||
Message::XrefRowFindReplacePrompt(index) => {
|
Message::XrefRowFindReplacePrompt(id) => {
|
||||||
self.xref_manager.right_click_select(index);
|
self.xref_manager.right_click_select(&id);
|
||||||
self.xref_manager.row_change_path_open = false;
|
self.xref_manager.row_change_path_open = false;
|
||||||
let prefill = if let Some(entry) = self.xref_manager.entries.get(index) {
|
let prefill = if let Some(index) = self.xref_manager.row_index(&id) {
|
||||||
let saved = &entry.saved_path;
|
self.xref_manager.entries.get(index).map_or_else(
|
||||||
if let Some(parent) = std::path::Path::new(saved)
|
|| "XREF Path Find ".to_string(),
|
||||||
.parent()
|
|entry| {
|
||||||
.and_then(|p| p.to_str())
|
let saved = &entry.saved_path;
|
||||||
{
|
if let Some(parent) = std::path::Path::new(saved)
|
||||||
if !parent.is_empty() {
|
.parent()
|
||||||
format!("XREF Path Find \"{}\" ", parent)
|
.and_then(|p| p.to_str())
|
||||||
} else {
|
{
|
||||||
"XREF Path Find ".to_string()
|
if !parent.is_empty() {
|
||||||
}
|
format!("XREF Path Find \"{}\" ", parent)
|
||||||
} else {
|
} else {
|
||||||
"XREF Path Find ".to_string()
|
"XREF Path Find ".to_string()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
"XREF Path Find ".to_string()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
"XREF Path Find ".to_string()
|
"XREF Path Find ".to_string()
|
||||||
};
|
};
|
||||||
@@ -3559,10 +3600,14 @@ impl OpenCADStudio {
|
|||||||
panel.selected_multi.push(idx);
|
panel.selected_multi.push(idx);
|
||||||
}
|
}
|
||||||
} else if shift {
|
} else if shift {
|
||||||
// Shift-click selects the range from the anchor to here.
|
// Shift-click selects the range from the anchor to here —
|
||||||
|
// visible rows only, so filtered-out rows between them
|
||||||
|
// cannot join bulk edits/deletes (#50).
|
||||||
let anchor = panel.selected.unwrap_or(idx);
|
let anchor = panel.selected.unwrap_or(idx);
|
||||||
let (lo, hi) = (anchor.min(idx), anchor.max(idx));
|
let (lo, hi) = (anchor.min(idx), anchor.max(idx));
|
||||||
panel.selected_multi = (lo..=hi).collect();
|
let visible: std::collections::HashSet<usize> =
|
||||||
|
panel.visible_indices().into_iter().collect();
|
||||||
|
panel.selected_multi = (lo..=hi).filter(|r| visible.contains(r)).collect();
|
||||||
} else if !panel.selected_multi.contains(&idx) {
|
} else if !panel.selected_multi.contains(&idx) {
|
||||||
// Plain click collapses to this row — but NOT when it is
|
// Plain click collapses to this row — but NOT when it is
|
||||||
// already part of a multi-selection, so clicking a property
|
// already part of a multi-selection, so clicking a property
|
||||||
@@ -3581,10 +3626,8 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
self.tabs[i].layers.selected = Some(idx);
|
self.tabs[i].layers.selected = Some(idx);
|
||||||
self.tabs[i].layers.selected_multi = vec![idx];
|
self.tabs[i].layers.selected_multi = vec![idx];
|
||||||
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
self.tabs[i].layers.edit_buf = name.clone();
|
||||||
self.tabs[i].layers.edit_buf = layer.name.clone();
|
self.tabs[i].layers.editing = Some(name);
|
||||||
}
|
|
||||||
self.tabs[i].layers.editing = Some(idx);
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3602,11 +3645,11 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let panel = &mut self.tabs[i].layers;
|
let panel = &mut self.tabs[i].layers;
|
||||||
if panel.color_picker_row == Some(idx) {
|
if panel.color_picker_row.as_deref() == Some(name.as_str()) {
|
||||||
panel.color_picker_row = None;
|
panel.color_picker_row = None;
|
||||||
panel.color_full_palette = false;
|
panel.color_full_palette = false;
|
||||||
} else {
|
} else {
|
||||||
panel.color_picker_row = Some(idx);
|
panel.color_picker_row = Some(name);
|
||||||
panel.color_full_palette = false;
|
panel.color_full_palette = false;
|
||||||
panel.selected = Some(idx);
|
panel.selected = Some(idx);
|
||||||
// Opening the swatch on a row outside the current
|
// Opening the swatch on a row outside the current
|
||||||
@@ -3719,23 +3762,25 @@ impl OpenCADStudio {
|
|||||||
// Apply the edited transparency to every selected layer (#236).
|
// Apply the edited transparency to every selected layer (#236).
|
||||||
if let Some(v) = val {
|
if let Some(v) = val {
|
||||||
let targets = self.layer_row_action_targets(i, idx);
|
let targets = self.layer_row_action_targets(i, idx);
|
||||||
for name in &targets {
|
|
||||||
if let Some(layer) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
||||||
layer.transparency =
|
|
||||||
codec::types::Transparency::from_percent(v as f64 / 100.0);
|
|
||||||
}
|
|
||||||
if let Some(pl) = self.tabs[i]
|
|
||||||
.layers
|
|
||||||
.layers
|
|
||||||
.iter_mut()
|
|
||||||
.find(|l| &l.name == name)
|
|
||||||
{
|
|
||||||
pl.transparency = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !targets.is_empty() {
|
if !targets.is_empty() {
|
||||||
|
let undo = self.begin_layer_undo(i, "LAYER TRANSPARENCY", &targets);
|
||||||
|
for name in &targets {
|
||||||
|
if let Some(layer) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
|
layer.transparency =
|
||||||
|
codec::types::Transparency::from_percent(v as f64 / 100.0);
|
||||||
|
}
|
||||||
|
if let Some(pl) = self.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter_mut()
|
||||||
|
.find(|l| &l.name == name)
|
||||||
|
{
|
||||||
|
pl.transparency = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
|
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -4558,28 +4603,36 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
Message::AnnoObjectScaleToggle(name) => {
|
Message::AnnoObjectScaleToggle(name) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if let Some(entity) = self.anno_object_scale_target {
|
let Some(entity) = self.anno_object_scale_target else {
|
||||||
if self.tabs[i].scene.is_layer_locked(entity) {
|
self.command_line.push_info(
|
||||||
return Task::none();
|
crate::t!("Select one object first, then run OBJECTSCALE.").as_ref(),
|
||||||
}
|
);
|
||||||
if let Some(sh) = self.tabs[i].scene.scale_handle_ensuring(&name) {
|
return Task::none();
|
||||||
self.push_undo_snapshot(i, "OBJECTSCALE");
|
};
|
||||||
let doc = &mut self.tabs[i].scene.document;
|
if self.tabs[i].scene.is_layer_locked(entity) {
|
||||||
let is_member =
|
self.command_line
|
||||||
crate::scene::annotative::object_scale_memberships(doc, entity)
|
.push_info(crate::t!("The object is on a locked layer.").as_ref());
|
||||||
.iter()
|
return Task::none();
|
||||||
.any(|(_, h)| *h == sh);
|
}
|
||||||
if is_member {
|
if let Some(sh) = self.tabs[i].scene.scale_handle_ensuring(&name) {
|
||||||
crate::scene::annotative::remove_annotation_context_for_scale(
|
self.push_undo_snapshot(i, "OBJECTSCALE");
|
||||||
doc, entity, sh,
|
let doc = &mut self.tabs[i].scene.document;
|
||||||
);
|
let is_member = crate::scene::annotative::object_scale_memberships(doc, entity)
|
||||||
} else {
|
.iter()
|
||||||
crate::scene::annotative::create_annotation_context(doc, entity, sh);
|
.any(|(_, h)| *h == sh);
|
||||||
}
|
if is_member {
|
||||||
self.tabs[i].dirty = true;
|
crate::scene::annotative::remove_annotation_context_for_scale(
|
||||||
self.invalidate_property_targets(i, &[entity]);
|
doc, entity, sh,
|
||||||
self.refresh_properties();
|
);
|
||||||
|
} else {
|
||||||
|
crate::scene::annotative::create_annotation_context(doc, entity, sh);
|
||||||
}
|
}
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.invalidate_property_targets(i, &[entity]);
|
||||||
|
self.refresh_properties();
|
||||||
|
} else {
|
||||||
|
self.command_line
|
||||||
|
.push_info(crate::tf!("Unknown annotation scale '{name}'.").as_ref());
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -6892,11 +6945,12 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
let Some((sx, sy, sz)) = scales else { return };
|
let Some((sx, sy, sz)) = scales else { return };
|
||||||
let eq = (sx - sy).abs() < 1e-12 && (sx - sz).abs() < 1e-12;
|
let eq = (sx - sy).abs() < 1e-12 && (sx - sz).abs() < 1e-12;
|
||||||
let checked = eq && !app.props_asym_scale.contains(&handle.value());
|
let checked =
|
||||||
|
eq && !app.tabs[i].props_asym_scale.contains(&handle.value());
|
||||||
if checked {
|
if checked {
|
||||||
app.props_asym_scale.insert(handle.value());
|
app.tabs[i].props_asym_scale.insert(handle.value());
|
||||||
} else {
|
} else {
|
||||||
app.props_asym_scale.remove(&handle.value());
|
app.tabs[i].props_asym_scale.remove(&handle.value());
|
||||||
if let Some(codec::EntityType::Insert(ins)) =
|
if let Some(codec::EntityType::Insert(ins)) =
|
||||||
app.tabs[i].scene.document.get_entity_mut(handle)
|
app.tabs[i].scene.document.get_entity_mut(handle)
|
||||||
{
|
{
|
||||||
@@ -8411,6 +8465,18 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::TouchpadPanChanged(enabled) => {
|
||||||
|
self.touchpad_pan = enabled;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::UiScaleChanged(percent) => {
|
||||||
|
self.ui_scale_percent = crate::app::settings::clamp_ui_scale(percent);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::ZoomFactorChanged(factor) => {
|
Message::ZoomFactorChanged(factor) => {
|
||||||
self.zoom_factor = crate::app::settings::clamp_zoom_factor(factor);
|
self.zoom_factor = crate::app::settings::clamp_zoom_factor(factor);
|
||||||
// The field beside the slider reads the same value, so the
|
// The field beside the slider reads the same value, so the
|
||||||
@@ -11124,6 +11190,136 @@ mod prop_pointer_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod anno_scale_toggle_tests {
|
||||||
|
// #59: silent no-op toggles must explain themselves on the command line.
|
||||||
|
use super::Message;
|
||||||
|
use crate::app::OpenCADStudio;
|
||||||
|
|
||||||
|
fn app_with_target() -> (OpenCADStudio, usize) {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let mut line = codec::entities::Line::new();
|
||||||
|
line.start = codec::types::Vector3::ZERO;
|
||||||
|
line.end = codec::types::Vector3::new(1.0, 0.0, 0.0);
|
||||||
|
let h = app
|
||||||
|
.commit_entity_handle(codec::EntityType::Line(line))
|
||||||
|
.expect("line commits");
|
||||||
|
app.anno_object_scale_target = Some(h);
|
||||||
|
(app, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_layer_toggle_explains_itself() {
|
||||||
|
let (mut app, i) = app_with_target();
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.layers
|
||||||
|
.get_mut("0")
|
||||||
|
.expect("layer 0 exists")
|
||||||
|
.flags
|
||||||
|
.locked = true;
|
||||||
|
let before = app.command_line.history.len();
|
||||||
|
let _ = app.update(Message::AnnoObjectScaleToggle("1:50".to_string()));
|
||||||
|
let new: Vec<String> = app.command_line.history[before..]
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.text.clone())
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
new.iter().any(|t| t.to_lowercase().contains("locked")),
|
||||||
|
"locked-layer toggle must say so, got {new:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_scale_toggle_explains_itself() {
|
||||||
|
let (mut app, _i) = app_with_target();
|
||||||
|
let before = app.command_line.history.len();
|
||||||
|
let _ = app.update(Message::AnnoObjectScaleToggle("NOPE".to_string()));
|
||||||
|
let new: Vec<String> = app.command_line.history[before..]
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.text.clone())
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
!new.is_empty(),
|
||||||
|
"unknown-scale toggle must say something, got silence"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_target_toggle_explains_itself() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app.anno_object_scale_target = None;
|
||||||
|
let before = app.command_line.history.len();
|
||||||
|
let _ = app.update(Message::AnnoObjectScaleToggle("1:50".to_string()));
|
||||||
|
let new: Vec<String> = app.command_line.history[before..]
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.text.clone())
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
!new.is_empty(),
|
||||||
|
"target-less toggle must say something, got silence"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod dock_visibility_tests {
|
||||||
|
// #57: one visibility predicate for edge rendering and drag math. Pins
|
||||||
|
// the full 8-panel table so a newly added panel cannot silently diverge
|
||||||
|
// (behavior-preserving dedup: passes before and after by construction).
|
||||||
|
use crate::app::OpenCADStudio;
|
||||||
|
use crate::ui::dock::PanelId;
|
||||||
|
|
||||||
|
const ALL: [PanelId; 8] = [
|
||||||
|
PanelId::Properties,
|
||||||
|
PanelId::BlockPalette,
|
||||||
|
PanelId::ExternalReferences,
|
||||||
|
PanelId::Browser,
|
||||||
|
PanelId::NodeGraph,
|
||||||
|
PanelId::PointCloudManager,
|
||||||
|
PanelId::Count,
|
||||||
|
PanelId::SheetSetManager,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_panel_visible_covers_every_panel() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app.show_properties = true;
|
||||||
|
app.show_block_palette = true;
|
||||||
|
app.show_external_references = true;
|
||||||
|
app.show_browser = true;
|
||||||
|
app.show_node_graph = true;
|
||||||
|
app.pc_manager.show = true;
|
||||||
|
app.count_palette.show = true;
|
||||||
|
app.sheet_set.show = true;
|
||||||
|
for id in ALL {
|
||||||
|
assert!(app.dock_panel_visible(id), "{id:?} must be visible");
|
||||||
|
}
|
||||||
|
app.clean_screen = true;
|
||||||
|
for id in ALL {
|
||||||
|
assert!(
|
||||||
|
!app.dock_panel_visible(id),
|
||||||
|
"{id:?} must hide on clean screen"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_panel_hidden_on_start_tab() {
|
||||||
|
// Fresh app, no drawing opened: the welcome tab shows no panels.
|
||||||
|
let app = OpenCADStudio::new_for_test();
|
||||||
|
assert!(app.tabs[app.active_tab].is_start);
|
||||||
|
for id in ALL {
|
||||||
|
assert!(!app.dock_panel_visible(id), "{id:?} must hide on Start");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod free_text_entry_tests {
|
mod free_text_entry_tests {
|
||||||
//! The free-form text prompts (table cell content, TEXT / MTEXT bodies)
|
//! The free-form text prompts (table cell content, TEXT / MTEXT bodies)
|
||||||
|
|||||||
+17
-2
@@ -695,12 +695,23 @@ impl OpenCADStudio {
|
|||||||
self.ts_border_spacing[r][b].trim().parse().ok(),
|
self.ts_border_spacing[r][b].trim().parse().ok(),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
// Reject dangling references: a typo must not persist as a text
|
||||||
|
// style that does not exist (#53). Decided up front so the
|
||||||
|
// cell borrow below does not conflict with command-line I/O.
|
||||||
|
let ts_known = ts.is_empty()
|
||||||
|
|| self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.name.eq_ignore_ascii_case(&ts));
|
||||||
|
let ts_rejected = !ts.is_empty() && !ts_known;
|
||||||
if let Some(c) = self
|
if let Some(c) = self
|
||||||
.tablestyle_mut(i)
|
.tablestyle_mut(i)
|
||||||
.and_then(|s| Self::ts_cell_of(s, row))
|
.and_then(|s| Self::ts_cell_of(s, row))
|
||||||
{
|
{
|
||||||
if !ts.is_empty() {
|
if !ts.is_empty() && ts_known {
|
||||||
c.text_style_name = ts;
|
c.text_style_name = ts.clone();
|
||||||
}
|
}
|
||||||
if let Some(h) = height {
|
if let Some(h) = height {
|
||||||
c.text_height = h;
|
c.text_height = h;
|
||||||
@@ -732,6 +743,10 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ts_rejected {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::tf!("Unknown text style '{ts}'.").as_ref());
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+163
-196
@@ -42,9 +42,6 @@ enum ScrollIntent {
|
|||||||
Zoom { notches: f32 },
|
Zoom { notches: f32 },
|
||||||
/// Trackpad movement to pan by, in screen pixels.
|
/// Trackpad movement to pan by, in screen pixels.
|
||||||
///
|
///
|
||||||
/// Built only where a pixel delta can only mean a trackpad, so it has no
|
|
||||||
/// constructor on the other targets.
|
|
||||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
||||||
Pan { dx: f32, dy: f32 },
|
Pan { dx: f32, dy: f32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +52,23 @@ const ZOOM_DIR_FLIP_PX: f32 = 2.0;
|
|||||||
|
|
||||||
/// A wheel notch zooms; a trackpad's two fingers pan.
|
/// A wheel notch zooms; a trackpad's two fingers pan.
|
||||||
///
|
///
|
||||||
/// A wheel reports notches, a precise-scrolling device reports pixels, and that
|
/// On macOS a wheel reports notches and a trackpad pixels, and that is the
|
||||||
/// is the whole distinction — there is no modifier to check and no setting to
|
/// whole distinction. Elsewhere a touchpad's two-finger drag arrives as wheel
|
||||||
/// read. The pixels only mean "trackpad" on macOS: the browser build reports
|
/// notches (the browser build reports every wheel as pixels), so nothing tells
|
||||||
/// every wheel notch as pixels too, so everywhere else they keep zooming.
|
/// it from a wheel: with `touchpad_pan` on, every scroll pans and Ctrl+scroll —
|
||||||
fn scroll_intent(delta: mouse::ScrollDelta) -> ScrollIntent {
|
/// what a pinch sends there — zooms.
|
||||||
|
fn scroll_intent(delta: mouse::ScrollDelta, touchpad_pan: bool, ctrl: bool) -> ScrollIntent {
|
||||||
|
/// Screen pixels one wheel line pans.
|
||||||
|
const LINE_PX: f32 = 40.0;
|
||||||
|
if touchpad_pan && !ctrl {
|
||||||
|
return match delta {
|
||||||
|
mouse::ScrollDelta::Pixels { x, y } => ScrollIntent::Pan { dx: x, dy: y },
|
||||||
|
mouse::ScrollDelta::Lines { x, y } => ScrollIntent::Pan {
|
||||||
|
dx: x * LINE_PX,
|
||||||
|
dy: y * LINE_PX,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
match delta {
|
match delta {
|
||||||
// Both axes go straight through: the two-finger gesture moves the
|
// Both axes go straight through: the two-finger gesture moves the
|
||||||
// drawing the same way the middle-button drag moves it. If a pan ever
|
// drawing the same way the middle-button drag moves it. If a pan ever
|
||||||
@@ -626,6 +635,107 @@ impl OpenCADStudio {
|
|||||||
/// cursor exactly where the GPU draws it; otherwise the model/paper camera
|
/// cursor exactly where the GPU draws it; otherwise the model/paper camera
|
||||||
/// and the normal hit-test wires. `bounds` is the pane-local rectangle.
|
/// and the normal hit-test wires. `bounds` is the pane-local rectangle.
|
||||||
|
|
||||||
|
/// The entities a selection box from `a` to `p` (tile pixels) catches,
|
||||||
|
/// as a window or crossing, through the wire, hatch and mesh tests — the
|
||||||
|
/// one box test the drag and click-move-click gestures and automation
|
||||||
|
/// share. The selection filter is the caller's.
|
||||||
|
pub(in crate::app) fn box_selection_handles(
|
||||||
|
&self,
|
||||||
|
i: usize,
|
||||||
|
edit_cam: &Option<crate::scene::view::camera::Camera>,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
a: iced::Point,
|
||||||
|
p: iced::Point,
|
||||||
|
crossing: bool,
|
||||||
|
) -> (Vec<Handle>, BoxSelectTiming) {
|
||||||
|
let (view_rot, eye, all_wires) = self.pick_view(i, edit_cam, bounds);
|
||||||
|
let world_aabb = [a, iced::Point::new(a.x, p.y), p, iced::Point::new(p.x, a.y)]
|
||||||
|
.map(|point| self.cursor_model_point(i, edit_cam, point, bounds))
|
||||||
|
.into_iter()
|
||||||
|
.fold(
|
||||||
|
[
|
||||||
|
f64::INFINITY,
|
||||||
|
f64::INFINITY,
|
||||||
|
f64::NEG_INFINITY,
|
||||||
|
f64::NEG_INFINITY,
|
||||||
|
],
|
||||||
|
|mut aabb, world| {
|
||||||
|
aabb[0] = aabb[0].min(world.x);
|
||||||
|
aabb[1] = aabb[1].min(world.y);
|
||||||
|
aabb[2] = aabb[2].max(world.x);
|
||||||
|
aabb[3] = aabb[3].max(world.y);
|
||||||
|
aabb
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// A large box selection was slow and two guesses about which step
|
||||||
|
// owns the time were both wrong, so each step is timed.
|
||||||
|
let elapsed = |start: Option<Instant>| {
|
||||||
|
start.map_or(0.0, |start| start.elapsed().as_secs_f64() * 1000.0)
|
||||||
|
};
|
||||||
|
let mut timing = BoxSelectTiming::default();
|
||||||
|
let t_sel = crate::perf::enabled().then(Instant::now);
|
||||||
|
let area_candidates = self.tabs[i].scene.interaction_candidates_in_aabb(
|
||||||
|
all_wires,
|
||||||
|
world_aabb,
|
||||||
|
[a.x.min(p.x), a.y.min(p.y), a.x.max(p.x), a.y.max(p.y)],
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
);
|
||||||
|
timing.candidates = elapsed(t_sel);
|
||||||
|
let t_hit = crate::perf::enabled().then(Instant::now);
|
||||||
|
let scene = &self.tabs[i].scene;
|
||||||
|
let candidate_handles = scene.interaction_candidate_handles(&area_candidates);
|
||||||
|
timing.handles = elapsed(t_hit);
|
||||||
|
let mut handles: Vec<Handle> =
|
||||||
|
scene::pick::hit_test::box_hit(a, p, crossing, &area_candidates, view_rot, eye, bounds)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| Scene::handle_from_wire_name(s))
|
||||||
|
.collect();
|
||||||
|
timing.wires = elapsed(t_hit);
|
||||||
|
handles.extend(scene::pick::hit_test::box_hit_hatch(
|
||||||
|
a,
|
||||||
|
p,
|
||||||
|
crossing,
|
||||||
|
&scene.visible_hatches_for_click(candidate_handles.as_ref()),
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
candidate_handles.as_ref(),
|
||||||
|
));
|
||||||
|
handles.extend(scene::pick::hit_test::box_hit_insert_hatch(
|
||||||
|
a,
|
||||||
|
p,
|
||||||
|
crossing,
|
||||||
|
scene.insert_hatches_for_click().as_ref(),
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
candidate_handles.as_ref(),
|
||||||
|
));
|
||||||
|
timing.hatch = elapsed(t_hit);
|
||||||
|
handles.extend(scene.mesh_box_hit(
|
||||||
|
a,
|
||||||
|
p,
|
||||||
|
crossing,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
candidate_handles.as_ref(),
|
||||||
|
));
|
||||||
|
handles.extend(scene.block_mesh_box_hit(
|
||||||
|
a,
|
||||||
|
p,
|
||||||
|
crossing,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
candidate_handles.as_ref(),
|
||||||
|
));
|
||||||
|
timing.hit = elapsed(t_hit);
|
||||||
|
(handles, timing)
|
||||||
|
}
|
||||||
|
|
||||||
pub(in crate::app) fn pick_view(
|
pub(in crate::app) fn pick_view(
|
||||||
&self,
|
&self,
|
||||||
i: usize,
|
i: usize,
|
||||||
@@ -1090,6 +1200,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::app) fn on_viewport_move(&mut self, p: Point) -> Task<Message> {
|
pub(in crate::app) fn on_viewport_move(&mut self, p: Point) -> Task<Message> {
|
||||||
|
crate::perf::note_pointer_move();
|
||||||
// A ribbon dropdown is open over the viewport. Its backdrop
|
// A ribbon dropdown is open over the viewport. Its backdrop
|
||||||
// cannot swallow cursor motion — in iced 0.14 mouse_area/opaque
|
// cannot swallow cursor motion — in iced 0.14 mouse_area/opaque
|
||||||
// capture only button presses, never CursorMoved — so the move
|
// capture only button presses, never CursorMoved — so the move
|
||||||
@@ -4861,101 +4972,17 @@ impl OpenCADStudio {
|
|||||||
// it here on release.
|
// it here on release.
|
||||||
if let Some(a) = box_anchor {
|
if let Some(a) = box_anchor {
|
||||||
let crossing = box_crossing;
|
let crossing = box_crossing;
|
||||||
let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
|
let (mut handles, timing) =
|
||||||
let world_aabb =
|
self.box_selection_handles(i, &edit_cam, bounds, a, p, crossing);
|
||||||
[a, iced::Point::new(a.x, p.y), p, iced::Point::new(p.x, a.y)]
|
// The drag gesture applies the selection filter as the click one does.
|
||||||
.map(|point| self.cursor_model_point(i, &edit_cam, point, bounds))
|
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
||||||
.into_iter()
|
let cand_ms = timing.candidates;
|
||||||
.fold(
|
|
||||||
[
|
|
||||||
f64::INFINITY,
|
|
||||||
f64::INFINITY,
|
|
||||||
f64::NEG_INFINITY,
|
|
||||||
f64::NEG_INFINITY,
|
|
||||||
],
|
|
||||||
|mut aabb, world| {
|
|
||||||
aabb[0] = aabb[0].min(world.x);
|
|
||||||
aabb[1] = aabb[1].min(world.y);
|
|
||||||
aabb[2] = aabb[2].max(world.x);
|
|
||||||
aabb[3] = aabb[3].max(world.y);
|
|
||||||
aabb
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// A 186 k-entity box selection sits at ~800 ms in this
|
|
||||||
// handler and two guesses about which step owns it have
|
|
||||||
// both been wrong. Split it.
|
|
||||||
let t_sel = crate::perf::enabled().then(Instant::now);
|
|
||||||
let area_candidates = self.tabs[i].scene.interaction_candidates_in_aabb(
|
|
||||||
all_wires,
|
|
||||||
world_aabb,
|
|
||||||
[a.x.min(p.x), a.y.min(p.y), a.x.max(p.x), a.y.max(p.y)],
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
);
|
|
||||||
let cand_ms = t_sel.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
|
|
||||||
let t_hit = crate::perf::enabled().then(Instant::now);
|
|
||||||
let candidate_handles = self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.interaction_candidate_handles(&area_candidates);
|
|
||||||
let mut handles: Vec<Handle> = scene::pick::hit_test::box_hit(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
&area_candidates,
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|s| Scene::handle_from_wire_name(s))
|
|
||||||
.collect();
|
|
||||||
handles.extend(scene::pick::hit_test::box_hit_hatch(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
&self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.visible_hatches_for_click(candidate_handles.as_ref()),
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
candidate_handles.as_ref(),
|
|
||||||
));
|
|
||||||
handles.extend(scene::pick::hit_test::box_hit_insert_hatch(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
candidate_handles.as_ref(),
|
|
||||||
));
|
|
||||||
handles.extend(self.tabs[i].scene.mesh_box_hit(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
candidate_handles.as_ref(),
|
|
||||||
));
|
|
||||||
handles.extend(self.tabs[i].scene.block_mesh_box_hit(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
candidate_handles.as_ref(),
|
|
||||||
));
|
|
||||||
// Box/lasso accumulates like individual picks
|
// Box/lasso accumulates like individual picks
|
||||||
// (issue #83): a plain box adds to the current
|
// (issue #83): a plain box adds to the current
|
||||||
// selection, Shift+box removes the boxed
|
// selection, Shift+box removes the boxed
|
||||||
// entities. Esc / empty-space click still clears.
|
// entities. Esc / empty-space click still clears.
|
||||||
// PICKADD 0 (#226): a plain box REPLACES.
|
// PICKADD 0 (#226): a plain box REPLACES.
|
||||||
let hit_ms = t_hit.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
|
let hit_ms = timing.hit;
|
||||||
let t_apply = crate::perf::enabled().then(Instant::now);
|
let t_apply = crate::perf::enabled().then(Instant::now);
|
||||||
if self.shift_down || self.select_remove_mode {
|
if self.shift_down || self.select_remove_mode {
|
||||||
self.tabs[i].scene.deselect_entities(&handles);
|
self.tabs[i].scene.deselect_entities(&handles);
|
||||||
@@ -5268,101 +5295,11 @@ was_selected={}",
|
|||||||
}
|
}
|
||||||
|
|
||||||
let crossing = box_crossing;
|
let crossing = box_crossing;
|
||||||
let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
|
let (mut handles, timing) =
|
||||||
let world_aabb = [a, iced::Point::new(a.x, p.y), p, iced::Point::new(p.x, a.y)]
|
self.box_selection_handles(i, &edit_cam, bounds, a, p, crossing);
|
||||||
.map(|point| self.cursor_model_point(i, &edit_cam, point, bounds))
|
let (cand_ms, hit_ms) = (timing.candidates, timing.hit);
|
||||||
.into_iter()
|
let (m_handles, m_wires, m_hatch) =
|
||||||
.fold(
|
(Some(timing.handles), Some(timing.wires), Some(timing.hatch));
|
||||||
[
|
|
||||||
f64::INFINITY,
|
|
||||||
f64::INFINITY,
|
|
||||||
f64::NEG_INFINITY,
|
|
||||||
f64::NEG_INFINITY,
|
|
||||||
],
|
|
||||||
|mut aabb, world| {
|
|
||||||
aabb[0] = aabb[0].min(world.x);
|
|
||||||
aabb[1] = aabb[1].min(world.y);
|
|
||||||
aabb[2] = aabb[2].max(world.x);
|
|
||||||
aabb[3] = aabb[3].max(world.y);
|
|
||||||
aabb
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// This is the path a click-move-click window takes, as
|
|
||||||
// opposed to a press-drag; it is the one a large selection
|
|
||||||
// actually goes through.
|
|
||||||
let t_sel = crate::perf::enabled().then(Instant::now);
|
|
||||||
let area_candidates = self.tabs[i].scene.interaction_candidates_in_aabb(
|
|
||||||
all_wires,
|
|
||||||
world_aabb,
|
|
||||||
[a.x.min(p.x), a.y.min(p.y), a.x.max(p.x), a.y.max(p.y)],
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
);
|
|
||||||
let cand_ms = t_sel.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
|
|
||||||
let t_hit = crate::perf::enabled().then(Instant::now);
|
|
||||||
let candidate_handles = self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.interaction_candidate_handles(&area_candidates);
|
|
||||||
// `hit` covers five things, and which of them owns it has
|
|
||||||
// never been separated: resolving candidate handles, the wire
|
|
||||||
// box test, the two hatch tests and the two mesh tests.
|
|
||||||
let m_handles = t_hit.map(|t| t.elapsed().as_secs_f64() * 1000.0);
|
|
||||||
let mut handles: Vec<Handle> = scene::pick::hit_test::box_hit(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
&area_candidates,
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|s| Scene::handle_from_wire_name(s))
|
|
||||||
.collect();
|
|
||||||
let m_wires = t_hit.map(|t| t.elapsed().as_secs_f64() * 1000.0);
|
|
||||||
handles.extend(scene::pick::hit_test::box_hit_hatch(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
&self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.visible_hatches_for_click(candidate_handles.as_ref()),
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
candidate_handles.as_ref(),
|
|
||||||
));
|
|
||||||
handles.extend(scene::pick::hit_test::box_hit_insert_hatch(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
candidate_handles.as_ref(),
|
|
||||||
));
|
|
||||||
let m_hatch = t_hit.map(|t| t.elapsed().as_secs_f64() * 1000.0);
|
|
||||||
handles.extend(self.tabs[i].scene.mesh_box_hit(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
candidate_handles.as_ref(),
|
|
||||||
));
|
|
||||||
handles.extend(self.tabs[i].scene.block_mesh_box_hit(
|
|
||||||
a,
|
|
||||||
p,
|
|
||||||
crossing,
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
candidate_handles.as_ref(),
|
|
||||||
));
|
|
||||||
let hit_ms = t_hit.map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0);
|
|
||||||
let t_filter = crate::perf::enabled().then(Instant::now);
|
let t_filter = crate::perf::enabled().then(Instant::now);
|
||||||
// Selection filter: keep only allowed types.
|
// Selection filter: keep only allowed types.
|
||||||
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
||||||
@@ -5805,8 +5742,9 @@ properties={:.1}ms picked={}",
|
|||||||
/// A wheel notch zooms; a trackpad's two fingers pan. Which one a delta
|
/// A wheel notch zooms; a trackpad's two fingers pan. Which one a delta
|
||||||
/// means is `scroll_intent`'s call.
|
/// means is `scroll_intent`'s call.
|
||||||
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
|
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
|
||||||
match scroll_intent(delta) {
|
match scroll_intent(delta, self.touchpad_pan, self.ctrl_down) {
|
||||||
ScrollIntent::Zoom { notches } => {
|
ScrollIntent::Zoom { notches } => {
|
||||||
|
crate::perf::note_scroll_zoom();
|
||||||
let mut s = notches * crate::app::settings::zoom_notch_steps(self.zoom_factor);
|
let mut s = notches * crate::app::settings::zoom_notch_steps(self.zoom_factor);
|
||||||
if self.zoom_wheel_reversed {
|
if self.zoom_wheel_reversed {
|
||||||
s = -s;
|
s = -s;
|
||||||
@@ -6838,10 +6776,27 @@ mod scroll_intent_tests {
|
|||||||
use crate::scene::view::camera::Camera;
|
use crate::scene::view::camera::Camera;
|
||||||
use iced::mouse::ScrollDelta;
|
use iced::mouse::ScrollDelta;
|
||||||
|
|
||||||
|
/// With the touchpad option, a scroll pans (wheel lines as pixels) and
|
||||||
|
/// Ctrl+scroll — a pinch — still zooms.
|
||||||
|
#[test]
|
||||||
|
fn touchpad_pan_scrolls_pan_and_ctrl_zooms() {
|
||||||
|
assert_eq!(
|
||||||
|
scroll_intent(ScrollDelta::Lines { x: 0.5, y: -0.25 }, true, false),
|
||||||
|
ScrollIntent::Pan {
|
||||||
|
dx: 20.0,
|
||||||
|
dy: -10.0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
scroll_intent(ScrollDelta::Lines { x: 0.0, y: 1.0 }, true, true),
|
||||||
|
ScrollIntent::Zoom { notches: 1.0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wheel_notches_zoom() {
|
fn wheel_notches_zoom() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scroll_intent(ScrollDelta::Lines { x: 0.0, y: 1.0 }),
|
scroll_intent(ScrollDelta::Lines { x: 0.0, y: 1.0 }, false, false),
|
||||||
ScrollIntent::Zoom { notches: 1.0 }
|
ScrollIntent::Zoom { notches: 1.0 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6852,7 +6807,7 @@ mod scroll_intent_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn two_finger_scroll_pans() {
|
fn two_finger_scroll_pans() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scroll_intent(ScrollDelta::Pixels { x: 4.0, y: 6.0 }),
|
scroll_intent(ScrollDelta::Pixels { x: 4.0, y: 6.0 }, false, false),
|
||||||
ScrollIntent::Pan { dx: 4.0, dy: 6.0 }
|
ScrollIntent::Pan { dx: 4.0, dy: 6.0 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8329,3 +8284,15 @@ mod selection_preview_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where a box selection spent its time, in milliseconds since each step's
|
||||||
|
/// start (perf builds only; zero otherwise). `handles`, `wires` and `hatch`
|
||||||
|
/// are cumulative within `hit`.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(in crate::app) struct BoxSelectTiming {
|
||||||
|
pub candidates: f64,
|
||||||
|
pub handles: f64,
|
||||||
|
pub wires: f64,
|
||||||
|
pub hatch: f64,
|
||||||
|
pub hit: f64,
|
||||||
|
}
|
||||||
+11
-6
@@ -202,6 +202,7 @@ impl OpenCADStudio {
|
|||||||
crate::perf_record!("[perf] view {:>7.1}ms", elapsed_ms);
|
crate::perf_record!("[perf] view {:>7.1}ms", elapsed_ms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
crate::perf::note_view_finish();
|
||||||
element
|
element
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -929,11 +930,14 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
snap_angle_deg: self.snap_angle_deg,
|
snap_angle_deg: self.snap_angle_deg,
|
||||||
pick_pending: self.pending_pick_label().is_some(),
|
pick_pending: self.pending_pick_label().is_some(),
|
||||||
// An infinite-line preview runs along the arms; drop them
|
// An infinite-line preview runs along the arms; drop them
|
||||||
// so the preview stays visible (#1437).
|
// so the preview stays visible (#1437). A command asking
|
||||||
hide_arms: tab
|
// for objects shows the pickbox alone, so picking reads
|
||||||
.active_cmd
|
// apart from both idle and point input (#1661).
|
||||||
.as_ref()
|
hide_arms: tab.active_cmd.as_ref().is_some_and(|c| {
|
||||||
.is_some_and(|c| matches!(c.name(), "XLINE" | "RAY")),
|
matches!(c.name(), "XLINE" | "RAY")
|
||||||
|
|| c.needs_entity_pick()
|
||||||
|
|| c.is_selection_gathering()
|
||||||
|
}),
|
||||||
snap_color: self.snap_marker_color,
|
snap_color: self.snap_marker_color,
|
||||||
},
|
},
|
||||||
crate::ui::overlay::SelectionVisualOptions {
|
crate::ui::overlay::SelectionVisualOptions {
|
||||||
@@ -1898,7 +1902,8 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
// collecting its own input. A pending Lengthen / Radius / Arc Length
|
// collecting its own input. A pending Lengthen / Radius / Arc Length
|
||||||
// value is numeric input, not the prefix of a new command (for example,
|
// value is numeric input, not the prefix of a new command (for example,
|
||||||
// `3` must not open the 3DALIGN / 3DARRAY suggestions).
|
// `3` must not open the 3DALIGN / 3DARRAY suggestions).
|
||||||
let allow_autocomplete = tab.active_cmd.is_none() && self.grip_pending.is_none();
|
let allow_autocomplete = self.grip_pending.is_none()
|
||||||
|
&& (tab.active_cmd.is_none() || self.command_line.has_contextual_suggestions());
|
||||||
// Dynamic input captures keystrokes when its fields are showing,
|
// Dynamic input captures keystrokes when its fields are showing,
|
||||||
// so the command-line field must release focus / its on_input.
|
// so the command-line field must release focus / its on_input.
|
||||||
// The MText preview also captures keystrokes (typing edits it), so the
|
// The MText preview also captures keystrokes (typing edits it), so the
|
||||||
|
|||||||
+18
-8
@@ -293,6 +293,8 @@ impl OpenCADStudio {
|
|||||||
cliprompt_lines: self.cliprompt_lines,
|
cliprompt_lines: self.cliprompt_lines,
|
||||||
commandline_fade_ms: self.commandline_fade_ms,
|
commandline_fade_ms: self.commandline_fade_ms,
|
||||||
zoom_wheel_reversed: self.zoom_wheel_reversed,
|
zoom_wheel_reversed: self.zoom_wheel_reversed,
|
||||||
|
touchpad_pan: self.touchpad_pan,
|
||||||
|
ui_scale_percent: self.ui_scale_percent,
|
||||||
zoom_factor: self.zoom_factor,
|
zoom_factor: self.zoom_factor,
|
||||||
texteditmode: self.texteditmode,
|
texteditmode: self.texteditmode,
|
||||||
dimension_continue_mode: self.dimension_continue_mode,
|
dimension_continue_mode: self.dimension_continue_mode,
|
||||||
@@ -619,14 +621,22 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
linetypes.sort_by_key(|name| name.to_lowercase());
|
linetypes.sort_by_key(|name| name.to_lowercase());
|
||||||
sized_flow(ex, 1180, 560, |flow| {
|
// Shared ownership: the build closure runs twice per frame
|
||||||
crate::ui::window::layer_state_manager::view_editor(
|
// (INTRINSIC + FILL) and each row keeps its own copy, so a
|
||||||
state,
|
// borrow cannot work here — an Arc clone per row is a
|
||||||
&self.layer_state_edit_filter,
|
// refcount bump instead of N string clones (#21, cf. #34).
|
||||||
self.layer_state_edit_color_open,
|
let linetypes = std::sync::Arc::<[String]>::from(linetypes);
|
||||||
linetypes.clone(),
|
sized_flow(ex, 1180, 560, {
|
||||||
flow,
|
let linetypes = std::sync::Arc::clone(&linetypes);
|
||||||
)
|
move |flow| {
|
||||||
|
crate::ui::window::layer_state_manager::view_editor(
|
||||||
|
state,
|
||||||
|
&self.layer_state_edit_filter,
|
||||||
|
self.layer_state_edit_color_open,
|
||||||
|
std::sync::Arc::clone(&linetypes),
|
||||||
|
flow,
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
automatic_flow(ex, |flow| {
|
automatic_flow(ex, |flow| {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ fn fixture() -> (OpenCADStudio, Handle, ViewportFrame) {
|
|||||||
viewport.view_height = 1000.0;
|
viewport.view_height = 1000.0;
|
||||||
viewport.custom_scale = 0.1;
|
viewport.custom_scale = 0.1;
|
||||||
viewport.view_direction = Vector3::UNIT_Z;
|
viewport.view_direction = Vector3::UNIT_Z;
|
||||||
viewport.status.is_on = true;
|
viewport.turn_on();
|
||||||
let viewport = scene.add_entity(EntityType::Viewport(viewport));
|
let viewport = scene.add_entity(EntityType::Viewport(viewport));
|
||||||
let frame = scene.viewport_frame(viewport).unwrap();
|
let frame = scene.viewport_frame(viewport).unwrap();
|
||||||
scene.document.header.dimension_associativity = 2;
|
scene.document.header.dimension_associativity = 2;
|
||||||
@@ -505,7 +505,7 @@ fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
|
|||||||
first.viewport
|
first.viewport
|
||||||
);
|
);
|
||||||
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(first.viewport) {
|
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(first.viewport) {
|
||||||
vp.status.is_on = false;
|
vp.turn_off();
|
||||||
}
|
}
|
||||||
assert!(scene.viewport_frames_at_paper_point(paper).is_empty());
|
assert!(scene.viewport_frames_at_paper_point(paper).is_empty());
|
||||||
assert!(scene.dimension_pick_through_viewport(paper, 0.2).is_none());
|
assert!(scene.dimension_pick_through_viewport(paper, 0.2).is_none());
|
||||||
|
|||||||
@@ -151,6 +151,28 @@ impl Delegate {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn main() {
|
fn main() {
|
||||||
|
// Explicit CLI invocations must reach the real argument parser before
|
||||||
|
// AppKit starts. In particular, --mcp needs this process's stdin/stdout,
|
||||||
|
// and --serve, --export, --help and --version must not launch a bare GUI.
|
||||||
|
// Keep arguments as OsString so file names need not be valid UTF-8.
|
||||||
|
// Finder may supply only its legacy process serial number; that still
|
||||||
|
// belongs to the persistent document-open relay below.
|
||||||
|
let args: Vec<_> = std::env::args_os().skip(1).collect();
|
||||||
|
let finder_launch =
|
||||||
|
args.is_empty() || (args.len() == 1 && args[0].as_encoded_bytes().starts_with(b"-psn_"));
|
||||||
|
if !finder_launch {
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
|
||||||
|
// Replacing the process preserves stdio, PID, signals and exit status.
|
||||||
|
// Do not exec on a normal Finder launch: its AppKit delegate must stay
|
||||||
|
// alive to receive subsequent document-open events.
|
||||||
|
let error = std::process::Command::new(real_binary_path())
|
||||||
|
.args(args)
|
||||||
|
.exec();
|
||||||
|
eprintln!("OpenCADStudio launcher: failed to execute the application: {error}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
let mtm = MainThreadMarker::new().expect("the launcher must run on the main thread");
|
let mtm = MainThreadMarker::new().expect("the launcher must run on the main thread");
|
||||||
let app = NSApplication::sharedApplication(mtm);
|
let app = NSApplication::sharedApplication(mtm);
|
||||||
// No window, no Dock icon, no menu bar — this process only relays.
|
// No window, no Dock icon, no menu bar — this process only relays.
|
||||||
|
|||||||
@@ -2430,6 +2430,32 @@ pub struct PointPickContext {
|
|||||||
pub aperture_px: f32,
|
pub aperture_px: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One context-sensitive command-line autocomplete entry.
|
||||||
|
///
|
||||||
|
/// Normal command-name autocomplete only needs `value`. Interactive commands
|
||||||
|
/// may optionally attach CAD display metadata, allowing the shared suggestion
|
||||||
|
/// popup to render richer rows without knowing which command supplied them.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CommandSuggestion {
|
||||||
|
pub value: String,
|
||||||
|
pub color: Option<codec::types::Color>,
|
||||||
|
pub visible: Option<bool>,
|
||||||
|
pub frozen: Option<bool>,
|
||||||
|
pub locked: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandSuggestion {
|
||||||
|
pub fn plain(value: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
value: value.into(),
|
||||||
|
color: None,
|
||||||
|
visible: None,
|
||||||
|
frozen: None,
|
||||||
|
locked: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait CadCommand: Send {
|
pub trait CadCommand: Send {
|
||||||
/// Preserve source appearance for commands that extract existing entities.
|
/// Preserve source appearance for commands that extract existing entities.
|
||||||
fn preserve_commit_style(&self) -> bool {
|
fn preserve_commit_style(&self) -> bool {
|
||||||
@@ -2471,6 +2497,15 @@ pub trait CadCommand: Send {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Context-sensitive autocomplete entries for the command line while this
|
||||||
|
/// command is active. Unlike normal command-name completion these entries
|
||||||
|
/// may be drawing data such as layer, block or style names.
|
||||||
|
///
|
||||||
|
/// Commands that do not provide contextual completion leave this empty.
|
||||||
|
fn text_suggestions(&self, _input: &str) -> Vec<CommandSuggestion> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
/// Push the active coordinate frame in full precision. Geometry commands
|
/// Push the active coordinate frame in full precision. Geometry commands
|
||||||
/// use it for plane-local construction; inquiry and modify commands use it
|
/// use it for plane-local construction; inquiry and modify commands use it
|
||||||
/// for local deltas, angles and transformation axes.
|
/// for local deltas, angles and transformation axes.
|
||||||
|
|||||||
@@ -663,11 +663,7 @@ fn apply_grip(ml: &mut MultiLeader, grip_id: usize, apply: GripApply) {
|
|||||||
} else {
|
} else {
|
||||||
old_sign
|
old_sign
|
||||||
};
|
};
|
||||||
ml.context.text_attachment_point = if new_sign >= 0.0 {
|
set_mleader_text_side(ml, new_sign >= 0.0);
|
||||||
codec::entities::multileader::TextAttachmentPointType::Left
|
|
||||||
} else {
|
|
||||||
codec::entities::multileader::TextAttachmentPointType::Right
|
|
||||||
};
|
|
||||||
set_mleader_text_location(ml, target + axis * (new_sign * (dogleg + gap)));
|
set_mleader_text_location(ml, target + axis * (new_sign * (dogleg + gap)));
|
||||||
|
|
||||||
if let Some(root) = ml.context.leader_roots.first_mut() {
|
if let Some(root) = ml.context.leader_roots.first_mut() {
|
||||||
@@ -2165,10 +2161,10 @@ impl MultiLeaderTess for MultiLeader {
|
|||||||
use codec::entities::multileader::TextAttachmentDirectionType;
|
use codec::entities::multileader::TextAttachmentDirectionType;
|
||||||
// The context's attachment point exists in every DWG version;
|
// The context's attachment point exists in every DWG version;
|
||||||
// the entity-level copy only exists from R2010 on.
|
// the entity-level copy only exists from R2010 on.
|
||||||
let h_anchor: f32 = match ctx.text_alignment {
|
let h_anchor: f32 = match ctx.text_attachment_point {
|
||||||
TextAlignmentType::Left => 0.0,
|
codec::entities::multileader::TextAttachmentPointType::Left => 0.0,
|
||||||
TextAlignmentType::Center => 0.5,
|
codec::entities::multileader::TextAttachmentPointType::Center => 0.5,
|
||||||
TextAlignmentType::Right => 1.0,
|
codec::entities::multileader::TextAttachmentPointType::Right => 1.0,
|
||||||
};
|
};
|
||||||
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
||||||
// Horizontal — leader attaches left/right; use ml.text_left_attachment
|
// Horizontal — leader attaches left/right; use ml.text_left_attachment
|
||||||
@@ -2645,3 +2641,23 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Put the text on the right (`to_right`) or left of its landing: the text
|
||||||
|
/// block attaches by its near edge, and left- or right-justified text turns
|
||||||
|
/// to read away from the leader. Centred text stays centred.
|
||||||
|
pub(crate) fn set_mleader_text_side(ml: &mut MultiLeader, to_right: bool) {
|
||||||
|
use codec::entities::multileader::TextAttachmentPointType;
|
||||||
|
let (attachment, alignment) = if to_right {
|
||||||
|
(TextAttachmentPointType::Left, TextAlignmentType::Left)
|
||||||
|
} else {
|
||||||
|
(TextAttachmentPointType::Right, TextAlignmentType::Right)
|
||||||
|
};
|
||||||
|
ml.text_attachment_point = attachment;
|
||||||
|
ml.context.text_attachment_point = attachment;
|
||||||
|
if ml.context.text_alignment != TextAlignmentType::Center {
|
||||||
|
ml.context.text_alignment = alignment;
|
||||||
|
}
|
||||||
|
if ml.text_alignment != TextAlignmentType::Center {
|
||||||
|
ml.text_alignment = alignment;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -304,7 +304,7 @@ fn region_area_perimeter(region: &Region) -> (f64, f64) {
|
|||||||
.collect::<Option<Vec<_>>>()?;
|
.collect::<Option<Vec<_>>>()?;
|
||||||
let area = curves
|
let area = curves
|
||||||
.iter()
|
.iter()
|
||||||
.map(|curve| curve.enclosed_area())
|
.map(|curve| curve.chain_area())
|
||||||
.sum::<f64>()
|
.sum::<f64>()
|
||||||
.abs();
|
.abs();
|
||||||
let perimeter = curves.iter().map(|curve| curve.length()).sum::<f64>();
|
let perimeter = curves.iter().map(|curve| curve.length()).sum::<f64>();
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ fn properties(vp: &Viewport) -> Vec<PropSection> {
|
|||||||
field: "vp_on",
|
field: "vp_on",
|
||||||
value: PropValue::BoolToggle {
|
value: PropValue::BoolToggle {
|
||||||
field: "vp_on",
|
field: "vp_on",
|
||||||
value: vp.status.is_on,
|
value: vp.is_on(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ro(
|
ro(
|
||||||
@@ -192,11 +192,18 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
"vp_on" => {
|
"vp_on" => {
|
||||||
vp.status.is_on = if value == "toggle" {
|
// turn_on/turn_off keep the explicit-off bit in step, so a
|
||||||
!vp.status.is_on
|
// viewport the file marked off can be turned back on.
|
||||||
|
let on = if value == "toggle" {
|
||||||
|
!vp.is_on()
|
||||||
} else {
|
} else {
|
||||||
value == "true"
|
value == "true"
|
||||||
};
|
};
|
||||||
|
if on {
|
||||||
|
vp.turn_on();
|
||||||
|
} else {
|
||||||
|
vp.turn_off();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
"vp_perspective" => {
|
"vp_perspective" => {
|
||||||
|
|||||||
+6
-1
@@ -53,6 +53,8 @@ pub enum Language {
|
|||||||
JaJp,
|
JaJp,
|
||||||
#[serde(rename = "ko-KR")]
|
#[serde(rename = "ko-KR")]
|
||||||
KoKr,
|
KoKr,
|
||||||
|
#[serde(rename = "lo-LA")]
|
||||||
|
LoLa,
|
||||||
#[serde(rename = "cs-CZ")]
|
#[serde(rename = "cs-CZ")]
|
||||||
CsCz,
|
CsCz,
|
||||||
#[serde(rename = "it-IT")]
|
#[serde(rename = "it-IT")]
|
||||||
@@ -70,7 +72,7 @@ pub enum Language {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Language {
|
impl Language {
|
||||||
pub const ALL: [Language; 22] = [
|
pub const ALL: [Language; 23] = [
|
||||||
Language::System,
|
Language::System,
|
||||||
Language::EnUs,
|
Language::EnUs,
|
||||||
Language::BgBg,
|
Language::BgBg,
|
||||||
@@ -85,6 +87,7 @@ impl Language {
|
|||||||
Language::ItIt,
|
Language::ItIt,
|
||||||
Language::JaJp,
|
Language::JaJp,
|
||||||
Language::KoKr,
|
Language::KoKr,
|
||||||
|
Language::LoLa,
|
||||||
Language::PlPl,
|
Language::PlPl,
|
||||||
Language::RuRu,
|
Language::RuRu,
|
||||||
Language::ZhCn,
|
Language::ZhCn,
|
||||||
@@ -112,6 +115,7 @@ impl Language {
|
|||||||
Language::ArSa => vec!["ar-SA".parse().expect("valid locale")],
|
Language::ArSa => vec!["ar-SA".parse().expect("valid locale")],
|
||||||
Language::JaJp => vec!["ja-JP".parse().expect("valid locale")],
|
Language::JaJp => vec!["ja-JP".parse().expect("valid locale")],
|
||||||
Language::KoKr => vec!["ko-KR".parse().expect("valid locale")],
|
Language::KoKr => vec!["ko-KR".parse().expect("valid locale")],
|
||||||
|
Language::LoLa => vec!["lo-LA".parse().expect("valid locale")],
|
||||||
Language::CsCz => vec!["cs-CZ".parse().expect("valid locale")],
|
Language::CsCz => vec!["cs-CZ".parse().expect("valid locale")],
|
||||||
Language::ItIt => vec!["it-IT".parse().expect("valid locale")],
|
Language::ItIt => vec!["it-IT".parse().expect("valid locale")],
|
||||||
Language::FiFi => vec!["fi-FI".parse().expect("valid locale")],
|
Language::FiFi => vec!["fi-FI".parse().expect("valid locale")],
|
||||||
@@ -139,6 +143,7 @@ impl Language {
|
|||||||
Language::ArSa => crate::tr!("language", "arabic"),
|
Language::ArSa => crate::tr!("language", "arabic"),
|
||||||
Language::JaJp => crate::tr!("language", "japanese"),
|
Language::JaJp => crate::tr!("language", "japanese"),
|
||||||
Language::KoKr => crate::tr!("language", "korean"),
|
Language::KoKr => crate::tr!("language", "korean"),
|
||||||
|
Language::LoLa => crate::tr!("language", "lao"),
|
||||||
Language::CsCz => crate::tr!("language", "czech"),
|
Language::CsCz => crate::tr!("language", "czech"),
|
||||||
Language::ItIt => crate::tr!("language", "italian"),
|
Language::ItIt => crate::tr!("language", "italian"),
|
||||||
Language::FiFi => crate::tr!("language", "finnish"),
|
Language::FiFi => crate::tr!("language", "finnish"),
|
||||||
|
|||||||
+1
-1
@@ -2511,7 +2511,7 @@ fn fix_viewport_status_flags(doc: &mut CadDocument) {
|
|||||||
// If bit 0 is not set but bit 15 is, this is an external-format viewport:
|
// If bit 0 is not set but bit 15 is, this is an external-format viewport:
|
||||||
// treat bit 15 as "on" and bit 14 as "locked".
|
// treat bit 15 as "on" and bit 14 as "locked".
|
||||||
if (bits & 0x0001) == 0 && (bits & 0x8000) != 0 {
|
if (bits & 0x0001) == 0 && (bits & 0x8000) != 0 {
|
||||||
vp.status.is_on = true;
|
vp.turn_on();
|
||||||
vp.status.locked = (bits & 0x4000) != 0;
|
vp.status.locked = (bits & 0x4000) != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1933,6 +1933,12 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"M density" => Some(("properties", "m-density")),
|
"M density" => Some(("properties", "m-density")),
|
||||||
"M vertex count" => Some(("properties", "m-vertex-count")),
|
"M vertex count" => Some(("properties", "m-vertex-count")),
|
||||||
"MASSPROP: no entities selected. Select entities first." => Some(("inquiry", "massprop-no-entities-selected-select-entities")),
|
"MASSPROP: no entities selected. Select entities first." => Some(("inquiry", "massprop-no-entities-selected-select-entities")),
|
||||||
|
"MASSPROP: {} has no closed body to measure." => Some(("inquiry", "massprop-has-no-closed-body-to-measure")),
|
||||||
|
"{} Volume={:.4} Centroid=({:.4},{:.4},{:.4})" => Some(("inquiry", "volume-4-centroid-4-4-4")),
|
||||||
|
"Moments of inertia X={:.4} Y={:.4} Z={:.4}" => Some(("inquiry", "moments-of-inertia-x-4-y-4-z-4")),
|
||||||
|
"Products of inertia XY={:.4} YZ={:.4} ZX={:.4}" => Some(("inquiry", "products-of-inertia-xy-4-yz-4-zx-4")),
|
||||||
|
"Radii of gyration X={:.4} Y={:.4} Z={:.4}" => Some(("inquiry", "radii-of-gyration-x-4-y-4-z-4")),
|
||||||
|
"Principal moments about centroid I={:.4} J={:.4} K={:.4}" => Some(("inquiry", "principal-moments-about-centroid-i-4-j-4-k-4")),
|
||||||
"MATCHPROP Select destination objects (Enter to finish):" => Some(("properties", "matchprop-select-destination-objects-enter-to")),
|
"MATCHPROP Select destination objects (Enter to finish):" => Some(("properties", "matchprop-select-destination-objects-enter-to")),
|
||||||
"MATCHPROP Select source object:" => Some(("properties", "matchprop-select-source-object")),
|
"MATCHPROP Select source object:" => Some(("properties", "matchprop-select-source-object")),
|
||||||
"MEASURE Select object to measure:" => Some(("inquiry", "measure-select-object-to-measure")),
|
"MEASURE Select object to measure:" => Some(("inquiry", "measure-select-object-to-measure")),
|
||||||
@@ -4797,6 +4803,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"User Preferences" => Some(("common", "user-preferences")),
|
"User Preferences" => Some(("common", "user-preferences")),
|
||||||
"Zoom" => Some(("common", "zoom")),
|
"Zoom" => Some(("common", "zoom")),
|
||||||
"Reverse mouse wheel zoom (ZOOMWHEEL)" => Some(("common", "reverse-mouse-wheel-zoom-zoomwheel")),
|
"Reverse mouse wheel zoom (ZOOMWHEEL)" => Some(("common", "reverse-mouse-wheel-zoom-zoomwheel")),
|
||||||
|
"Touchpad: scroll pans, Ctrl+scroll or pinch zooms" => Some(("common", "touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms")),
|
||||||
|
"Interface scale" => Some(("common", "interface-scale")),
|
||||||
"Zoom factor" => Some(("common", "zoom-factor")),
|
"Zoom factor" => Some(("common", "zoom-factor")),
|
||||||
"How far one wheel notch zooms (ZOOMFACTOR)." => Some(("common", "how-far-one-wheel-notch-zooms-zoomfactor")),
|
"How far one wheel notch zooms (ZOOMFACTOR)." => Some(("common", "how-far-one-wheel-notch-zooms-zoomfactor")),
|
||||||
"Text and Dimensions" => Some(("common", "text-and-dimensions")),
|
"Text and Dimensions" => Some(("common", "text-and-dimensions")),
|
||||||
|
|||||||
+68
-1
@@ -29,7 +29,7 @@ const MODERN_PROTOCOL_VERSION: &str = "2026-07-28";
|
|||||||
/// changes, so schema drift (new params, renamed tools) is always a conscious,
|
/// changes, so schema drift (new params, renamed tools) is always a conscious,
|
||||||
/// reviewed edit — and clients can detect a stale bridge by comparing digests.
|
/// reviewed edit — and clients can detect a stale bridge by comparing digests.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
const TOOL_SCHEMA_DIGEST: &str = "2ec01401ca96bc0574a9be41958d24dbd9c27a497dce80064c8afcca5258256e";
|
const TOOL_SCHEMA_DIGEST: &str = "8dc370c3e4bde86a3c2f6be8488dd1ed318850303326b00e2c28e8c033006472";
|
||||||
const MAX_REQUEST: usize = 1_048_576;
|
const MAX_REQUEST: usize = 1_048_576;
|
||||||
const MAX_RESPONSE: u64 = 16 * 1024 * 1024;
|
const MAX_RESPONSE: u64 = 16 * 1024 * 1024;
|
||||||
const CACHE_TTL_MS: u64 = 3_600_000;
|
const CACHE_TTL_MS: u64 = 3_600_000;
|
||||||
@@ -2330,6 +2330,16 @@ pub(crate) fn tool_definitions() -> Value {
|
|||||||
|
|
||||||
fn tool_result(value: Value) -> Value {
|
fn tool_result(value: Value) -> Value {
|
||||||
if value.get("$resource").is_some() || value.get("$image").is_some() {
|
if value.get("$resource").is_some() || value.get("$image").is_some() {
|
||||||
|
// A baseline also reports changed=false, but the client has not seen
|
||||||
|
// that image yet. Only omit media after an actual unchanged comparison.
|
||||||
|
// Keep the capture metadata (hash, URI, spatial data and request id)
|
||||||
|
// so clients can still fetch the stored image explicitly if needed.
|
||||||
|
let meta = &value["metadata"];
|
||||||
|
if meta["diff"]["is_baseline"].as_bool() == Some(false)
|
||||||
|
&& meta["diff"]["changed"].as_bool() == Some(false)
|
||||||
|
{
|
||||||
|
return tool_result(meta.clone());
|
||||||
|
}
|
||||||
let mut content = Vec::new();
|
let mut content = Vec::new();
|
||||||
if let Some(res) = value.get("$resource") {
|
if let Some(res) = value.get("$resource") {
|
||||||
let uri = res["uri"].as_str().unwrap_or("");
|
let uri = res["uri"].as_str().unwrap_or("");
|
||||||
@@ -3794,6 +3804,63 @@ mod tests {
|
|||||||
assert!(both_content.iter().any(|c| c["type"] == "image"));
|
assert!(both_content.iter().any(|c| c["type"] == "image"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unchanged_capture_omits_media_but_preserves_metadata() {
|
||||||
|
let image = image::RgbaImage::from_pixel(4, 4, image::Rgba([255, 255, 255, 255]));
|
||||||
|
let diff = crate::app::control::vision::compute_visual_diff(
|
||||||
|
&image,
|
||||||
|
&image,
|
||||||
|
&[1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
|
||||||
|
);
|
||||||
|
assert!(!diff.changed);
|
||||||
|
for delivery in ["inline", "resource", "both"] {
|
||||||
|
let metadata = json!({
|
||||||
|
"hash":"unchanged-hash", "uri":"cad://session/s/snapshot/unchanged-hash.png",
|
||||||
|
"request_id":"capture-2", "revision":12,
|
||||||
|
"_spatial":{"crs":"CAD_WCS"},
|
||||||
|
"diff":{"is_baseline":false,"changed":diff.changed}
|
||||||
|
});
|
||||||
|
let mut capture = json!({"metadata":metadata});
|
||||||
|
if delivery != "resource" {
|
||||||
|
capture["$image"] = json!("image-bytes-must-not-be-returned");
|
||||||
|
}
|
||||||
|
if delivery != "inline" {
|
||||||
|
capture["$resource"] = json!({"uri":metadata["uri"],"mimeType":"image/png"});
|
||||||
|
}
|
||||||
|
let result = tool_result(capture);
|
||||||
|
assert_eq!(result["isError"], false);
|
||||||
|
assert_eq!(result["structuredContent"], metadata);
|
||||||
|
let content = result["content"].as_array().unwrap();
|
||||||
|
assert_eq!(content.len(), 1);
|
||||||
|
assert_eq!(content[0]["type"], "text");
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<Value>(content[0]["text"].as_str().unwrap()).unwrap(),
|
||||||
|
metadata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn baseline_changed_and_non_diff_captures_keep_images() {
|
||||||
|
for diff in [
|
||||||
|
json!({"is_baseline":true,"changed":false}),
|
||||||
|
json!({"is_baseline":false,"changed":true}),
|
||||||
|
Value::Null,
|
||||||
|
] {
|
||||||
|
let result = tool_result(json!({
|
||||||
|
"$image":"baseline-or-changed-png",
|
||||||
|
"metadata":{"diff":diff}
|
||||||
|
}));
|
||||||
|
assert!(result["content"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(
|
||||||
|
|block| block["type"] == "image" && block["data"] == "baseline-or-changed-png"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn capture_tool_schema_and_diff_metadata_handling() {
|
fn capture_tool_schema_and_diff_metadata_handling() {
|
||||||
// 1. Verify ocs_capture schema has diff parameters
|
// 1. Verify ocs_capture schema has diff parameters
|
||||||
|
|||||||
+4
-12
@@ -251,18 +251,10 @@ pub static SELECT: OpDef = OpDef {
|
|||||||
opt("handles", Ty::Handles, "Explicit entity handles to select."),
|
opt("handles", Ty::Handles, "Explicit entity handles to select."),
|
||||||
opt("type", Ty::Str, "Entity type filter."),
|
opt("type", Ty::Str, "Entity type filter."),
|
||||||
opt("layer", Ty::Str, "Layer name filter."),
|
opt("layer", Ty::Str, "Layer name filter."),
|
||||||
opt(
|
opt("window", Ty::Raw(r#"{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4}"#), "World XY box [x1, y1, x2, y2]: selects what lies inside it, as a viewport window does."),
|
||||||
"clear",
|
opt("crossing", Ty::Bool, "With window: also select what the box crosses."),
|
||||||
Ty::Bool,
|
opt("clear", Ty::Bool, "Clear existing selection before applying."),
|
||||||
"Clear existing selection before applying.",
|
opt("where", Ty::ArrayOf(&Ty::Raw(r#"{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string"},"value":{}},"required":["path"]}"#)), "Property filter predicates."),
|
||||||
),
|
|
||||||
opt(
|
|
||||||
"where",
|
|
||||||
Ty::ArrayOf(&Ty::Raw(
|
|
||||||
r#"{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string"},"value":{}},"required":["path"]}"#,
|
|
||||||
)),
|
|
||||||
"Property filter predicates.",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
rules: &[],
|
rules: &[],
|
||||||
example: r#"{"op":"select","clear":true}"#,
|
example: r#"{"op":"select","clear":true}"#,
|
||||||
|
|||||||
@@ -705,11 +705,7 @@ fn build_mleader(
|
|||||||
let to_right = (elbow_pt - arrow_pt).dot(ux.as_dvec3()) >= 0.0;
|
let to_right = (elbow_pt - arrow_pt).dot(ux.as_dvec3()) >= 0.0;
|
||||||
let sign = if to_right { 1.0 } else { -1.0 };
|
let sign = if to_right { 1.0 } else { -1.0 };
|
||||||
let landing = ux * (sign as f32);
|
let landing = ux * (sign as f32);
|
||||||
ml.context.text_attachment_point = if to_right {
|
crate::entities::multileader::set_mleader_text_side(&mut ml, to_right);
|
||||||
codec::entities::multileader::TextAttachmentPointType::Left
|
|
||||||
} else {
|
|
||||||
codec::entities::multileader::TextAttachmentPointType::Right
|
|
||||||
};
|
|
||||||
|
|
||||||
ml.context.text_rotation = (ux.y as f64).atan2(ux.x as f64);
|
ml.context.text_rotation = (ux.y as f64).atan2(ux.x as f64);
|
||||||
ml.context.text_direction = Vector3::new(ux.x as f64, ux.y as f64, 0.0);
|
ml.context.text_direction = Vector3::new(ux.x as f64, ux.y as f64, 0.0);
|
||||||
|
|||||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user