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:
Claude committed 2026-10-11 18:44:00 +00:00
commit 2fca3fd18d
136 files changed
+12408 -949

No files matched your search

+42 -8
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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"] }
+3 -2
View File
@@ -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.
+1
View File
@@ -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 ─────────────────────────────────────────────────────────────
+173 -1
View File
@@ -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();
} }
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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"
+8
View File
@@ -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:
+90
View File
@@ -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")
+3 -3
View File
@@ -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 لغة:
> العربية · البرتغالية البرازيلية · البلغارية · التشيكية · الهولندية · الإنجليزية · الفنلندية · الفرنسية · الألمانية · اليونانية · الهندية · المجرية · الإيطالية · اليابانية · الكورية · البولندية · الروسية · الصينية المبسطة · الإسبانية · الصينية التقليدية · التركية > العربية · البرتغالية البرازيلية · البلغارية · التشيكية · الهولندية · الإنجليزية · الفنلندية · الفرنسية · الألمانية · اليونانية · الهندية · المجرية · الإيطالية · اليابانية · الكورية · اللاوية · البولندية · الروسية · الصينية المبسطة · الإسبانية · الصينية التقليدية · التركية
غيّر اللغة من إعدادات التطبيق. عند اختيار **النظام**، يستخدم إصدار المتصفح أيضاً الإعدادات المحلية المفضلة للمتصفح. غيّر اللغة من إعدادات التطبيق. عند اختيار **النظام**، يستخدم إصدار المتصفح أيضاً الإعدادات المحلية المفضلة للمتصفح.
+3 -3
View File
@@ -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 езика на интерфейса:
> Арабски · Бразилски португалски · Български · Чешки · Нидерландски · Английски · Фински · Френски · Немски · Гръцки · Хинди · Унгарски · Италиански · Японски · Корейски · Полски · Руски · Опростен китайски · Испански · Традиционен китайски · Турски > Арабски · Бразилски португалски · Български · Чешки · Нидерландски · Английски · Фински · Френски · Немски · Гръцки · Хинди · Унгарски · Италиански · Японски · Корейски · Лаоски · Полски · Руски · Опростен китайски · Испански · Традиционен китайски · Турски
Променете езика от настройките на приложението. Когато е избрано **Системен език**, браузърната версия също използва предпочитания локал на браузъра. Променете езика от настройките на приложението. Когато е избрано **Системен език**, браузърната версия също използва предпочитания локал на браузъра.
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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 γλώσσες περιβάλλοντος:
> Αραβικά · Πορτογαλικά Βραζιλίας · Βουλγαρικά · Τσεχικά · Ολλανδικά · Αγγλικά · Φινλανδικά · Γαλλικά · Γερμανικά · Ελληνικά · Χίντι · Ουγγρικά · Ιταλικά · Ιαπωνικά · Κορεατικά · Πολωνικά · Ρωσικά · Απλοποιημένα κινεζικά · Ισπανικά · Παραδοσιακά κινεζικά · Τουρκικά > Αραβικά · Πορτογαλικά Βραζιλίας · Βουλγαρικά · Τσεχικά · Ολλανδικά · Αγγλικά · Φινλανδικά · Γαλλικά · Γερμανικά · Ελληνικά · Χίντι · Ουγγρικά · Ιταλικά · Ιαπωνικά · Κορεατικά · Λαοτινά · Πολωνικά · Ρωσικά · Απλοποιημένα κινεζικά · Ισπανικά · Παραδοσιακά κινεζικά · Τουρκικά
Αλλάξτε τη γλώσσα από τις ρυθμίσεις της εφαρμογής. Όταν είναι επιλεγμένη η **Γλώσσα συστήματος**, η διαδικτυακή έκδοση χρησιμοποιεί επίσης την προτιμώμενη γλώσσα του προγράμματος περιήγησης. Αλλάξτε τη γλώσσα από τις ρυθμίσεις της εφαρμογής. Όταν είναι επιλεγμένη η **Γλώσσα συστήματος**, η διαδικτυακή έκδοση χρησιμοποιεί επίσης την προτιμώμενη γλώσσα του προγράμματος περιήγησης.
+3 -3
View File
@@ -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**.
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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é.
+3 -3
View File
@@ -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 इंटरफ़ेस भाषाओं में से किसी एक का उपयोग कर सकता है:
> अरबी · ब्राज़ीलियाई पुर्तगाली · बुल्गारियाई · चेक · डच · अंग्रेज़ी · फ़िनिश · फ़्रेंच · जर्मन · यूनानी · हिन्दी · हंगेरियन · इतालवी · जापानी · कोरियाई · पोलिश · रूसी · सरलीकृत चीनी · स्पैनिश · पारंपरिक चीनी · तुर्की > अरबी · ब्राज़ीलियाई पुर्तगाली · बुल्गारियाई · चेक · डच · अंग्रेज़ी · फ़िनिश · फ़्रेंच · जर्मन · यूनानी · हिन्दी · हंगेरियन · इतालवी · जापानी · कोरियाई · लाओ · पोलिश · रूसी · सरलीकृत चीनी · स्पैनिश · पारंपरिक चीनी · तुर्की
अनुप्रयोग सेटिंग में भाषा बदलें। **सिस्टम** चुने जाने पर ब्राउज़र संस्करण भी ब्राउज़र की पसंदीदा लोकेल का उपयोग करता है। अनुप्रयोग सेटिंग में भाषा बदलें। **सिस्टम** चुने जाने पर ब्राउज़र संस्करण भी ब्राउज़र की पसंदीदा लोकेल का उपयोग करता है।
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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**.
+3 -3
View File
@@ -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 のインターフェース言語を使用できます。
> アラビア語 · ブラジルポルトガル語 · ブルガリア語 · チェコ語 · オランダ語 · 英語 · フィンランド語 · フランス語 · ドイツ語 · ギリシャ語 · ヒンディー語 · ハンガリー語 · イタリア語 · 日本語 · 韓国語 · ポーランド語 · ロシア語 · 簡体字中国語 · スペイン語 · 繁体字中国語 · トルコ語 > アラビア語 · ブラジルポルトガル語 · ブルガリア語 · チェコ語 · オランダ語 · 英語 · フィンランド語 · フランス語 · ドイツ語 · ギリシャ語 · ヒンディー語 · ハンガリー語 · イタリア語 · 日本語 · 韓国語 · ラオ語 · ポーランド語 · ロシア語 · 簡体字中国語 · スペイン語 · 繁体字中国語 · トルコ語
言語はアプリケーション設定から変更できます。**システム** を選ぶと、ブラウザ版もブラウザの優先ロケールを使用します。 言語はアプリケーション設定から変更できます。**システム** を選ぶと、ブラウザ版もブラウザの優先ロケールを使用します。
+3 -3
View File
@@ -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개 인터페이스 언어 중 하나를 사용할 수 있습니다.
> 아랍어 · 브라질 포르투갈어 · 불가리아어 · 체코어 · 네덜란드어 · 영어 · 핀란드어 · 프랑스어 · 독일어 · 그리스어 · 힌디어 · 헝가리어 · 이탈리아어 · 일본어 · 한국어 · 폴란드어 · 러시아어 · 중국어 간체 · 스페인어 · 중국어 번체 · 터키어 > 아랍어 · 브라질 포르투갈어 · 불가리아어 · 체코어 · 네덜란드어 · 영어 · 핀란드어 · 프랑스어 · 독일어 · 그리스어 · 힌디어 · 헝가리어 · 이탈리아어 · 일본어 · 한국어 · 라오어 · 폴란드어 · 러시아어 · 중국어 간체 · 스페인어 · 중국어 번체 · 터키어
애플리케이션 설정에서 언어를 변경할 수 있습니다. **시스템**을 선택하면 브라우저 버전도 브라우저의 기본 로캘을 사용합니다. 애플리케이션 설정에서 언어를 변경할 수 있습니다. **시스템**을 선택하면 브라우저 버전도 브라우저의 기본 로캘을 사용합니다.
+198
View File
@@ -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).
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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):
> Арабский · Бразильский португальский · Болгарский · Чешский · Нидерландский · Английский · Финский · Французский · Немецкий · Греческий · Хинди · Венгерский · Итальянский · Японский · Корейский · Польский · Русский · Упрощённый китайский · Испанский · Традиционный китайский · Турецкий > Арабский · Бразильский португальский · Болгарский · Чешский · Нидерландский · Английский · Финский · Французский · Немецкий · Греческий · Хинди · Венгерский · Итальянский · Японский · Корейский · Лаосский · Польский · Русский · Упрощённый китайский · Испанский · Традиционный китайский · Турецкий
Язык меняется в настройках приложения. При выборе **Система** версия для браузера также использует предпочтительную локаль браузера. Язык меняется в настройках приложения. При выборе **Система** версия для браузера также использует предпочтительную локаль браузера.
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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 种界面语言:
> 阿拉伯语 · 巴西葡萄牙语 · 保加利亚语 · 捷克语 · 荷兰语 · 英语 · 芬兰语 · 法语 · 德语 · 希腊语 · 印地语 · 匈牙利语 · 意大利语 · 日语 · 韩语 · 波兰语 · 俄语 · 简体中文 · 西班牙语 · 繁体中文 · 土耳其语 > 阿拉伯语 · 巴西葡萄牙语 · 保加利亚语 · 捷克语 · 荷兰语 · 英语 · 芬兰语 · 法语 · 德语 · 希腊语 · 印地语 · 匈牙利语 · 意大利语 · 日语 · 韩语 · 老挝语 · 波兰语 · 俄语 · 简体中文 · 西班牙语 · 繁体中文 · 土耳其语
可在应用设置中更改语言。选择**系统**时,浏览器版也会使用浏览器的首选区域设置。 可在应用设置中更改语言。选择**系统**时,浏览器版也会使用浏览器的首选区域设置。
+3 -3
View File
@@ -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 種介面語言:
> 阿拉伯文 · 巴西葡萄牙文 · 保加利亞文 · 捷克文 · 荷蘭文 · 英文 · 芬蘭文 · 法文 · 德文 · 希臘文 · 北印度文 · 匈牙利文 · 義大利文 · 日文 · 韓文 · 波蘭文 · 俄文 · 簡體中文 · 西班牙文 · 繁體中文 · 土耳其文 > 阿拉伯文 · 巴西葡萄牙文 · 保加利亞文 · 捷克文 · 荷蘭文 · 英文 · 芬蘭文 · 法文 · 德文 · 希臘文 · 北印度文 · 匈牙利文 · 義大利文 · 日文 · 韓文 · 寮文 · 波蘭文 · 俄文 · 簡體中文 · 西班牙文 · 繁體中文 · 土耳其文
可在應用程式設定中變更語言。選擇**系統**時,瀏覽器版也會使用瀏覽器的偏好地區設定。 可在應用程式設定中變更語言。選擇**系統**時,瀏覽器版也會使用瀏覽器的偏好地區設定。
+9
View File
@@ -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 أدخل خيارًا [مسافة/نصف قطر/زاوية/مساحة]:
+9
View File
@@ -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 Въведи опция [Разстояние/Радиус/Ъгъл/Площ]:
+9
View File
@@ -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]:
+9
View File
@@ -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]:
+9
View File
@@ -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 Δώστε επιλογή [Απόσταση/Ακτίνα/Γωνία/Εμβαδόν]:
+9
View File
@@ -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]:
+9
View File
@@ -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]:
+9
View File
@@ -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]:
+9
View File
@@ -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] :
+9
View File
@@ -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 विकल्प दर्ज करें [दूरी / त्रिज्या / कोण / क्षेत्रफल]:
+9
View File
@@ -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]:
+9
View File
@@ -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]:
+9
View File
@@ -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 オプションを入力 [距離/半径/角度/面積]:
+9
View File
@@ -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
+9
View File
@@ -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]:
+9
View File
@@ -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]:
+9
View File
@@ -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]:
+9
View File
@@ -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 Введите опцию [Расстояние/Радиус/Англия/Арея]:
+9
View File
@@ -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]:
+9
View File
@@ -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 输入选项 [距离/半径/角度/面积]:
+9
View File
@@ -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 輸入選項 [距離/半徑/角度/面積]:
+2
View File
@@ -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>
+1 -1
View File
@@ -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",
+94
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+124 -11
View File
@@ -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"
);
}
} }
+2 -1
View File
@@ -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;
} }
+25 -2
View File
@@ -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 {
+359 -8
View File
@@ -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
View File
@@ -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 {
+76
View File
@@ -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!(
+96
View File
@@ -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]
+1
View File
@@ -706,6 +706,7 @@ inventory::submit!(crate::command::CommandRegistration {
"PDFOSNAP", "PDFOSNAP",
"UOSNAP", "UOSNAP",
"FIELDDISPLAY", "FIELDDISPLAY",
"SURFACEASSOCIATIVITY",
"FIELDEVAL", "FIELDEVAL",
"PDFIMPORTMODE", "PDFIMPORTMODE",
"PDFIMPORTFILTER", "PDFIMPORTFILTER",
+97 -19
View File
@@ -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(&lt.name)) !standard.iter().any(|s| s.eq_ignore_ascii_case(&lt.name))
&& !used_linetypes.contains(&lt.name) && !used_linetypes.contains(&lt.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 &lt_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" => {
+1 -1
View File
@@ -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 {
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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};
+15
View File
@@ -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,
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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)]
+1
View File
@@ -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
View File
@@ -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);
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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| {
+2 -2
View File
@@ -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());
+22
View File
@@ -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.
+35
View File
@@ -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.
+25 -9
View File
@@ -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;
}
}
+1 -1
View File
@@ -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>();
+10 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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;
} }
} }
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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}"#,
+1 -5
View File
@@ -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