diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 286667b34..337141135 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -20,7 +20,7 @@ on: default: false permissions: - contents: read + contents: write discussions: read pages: write id-token: write @@ -145,6 +145,19 @@ jobs: jq -n --arg version "${RELEASE_TAG#v}" --arg commit "$RELEASE_COMMIT" \ '{version: $version, commit: $commit}' > dist/app/release.json + # Attach the bundle to the release so a version can be pinned and + # self-hosted; it expects to be served from /app/. Taken before the + # site data below is added, and skipped for hotfixes built from main. + - name: Attach web bundle to the release + if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.build_main) }} + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.source.outputs.tag }} + run: | + ASSET="OpenCADStudio-$RELEASE_TAG-web.zip" + (cd dist && zip -qr "../$ASSET" app) + gh release upload "$RELEASE_TAG" "$ASSET" --clobber --repo "$GITHUB_REPOSITORY" + # GitHub Pages runs Jekyll, which ignores files; opt out so the # wasm-bindgen output is served verbatim. - run: touch dist/.nojekyll diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55a752b0c..a9f2f42b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -517,15 +517,21 @@ jobs: timeout-minutes: 2 run: python3 docs/automation/mcp_handshake.py OpenCADStudio.app/Contents/MacOS/OpenCADStudio + # Stage the app beside an /Applications symlink so the mounted image + # offers drag-to-install (#769), matching packaging/build_macos_signed.sh. - name: Create .dmg run: | + rm -rf dmg-staging + mkdir dmg-staging + cp -R OpenCADStudio.app dmg-staging/ + ln -s /Applications dmg-staging/Applications set +e for attempt in 1 2 3 4 5; do hdiutil detach "/Volumes/Open CAD Studio" >/dev/null 2>&1 || true rm -f OpenCADStudio.dmg hdiutil create \ -volname "Open CAD Studio" \ - -srcfolder OpenCADStudio.app \ + -srcfolder dmg-staging \ -ov -format UDZO \ OpenCADStudio.dmg && break echo "hdiutil create failed (attempt $attempt) โ€” retrying in 5s" @@ -533,6 +539,25 @@ jobs: done set -e test -f OpenCADStudio.dmg + rm -rf dmg-staging + + - name: Verify .dmg layout + run: | + MNT=$(mktemp -d) + hdiutil attach -readonly -nobrowse -mountpoint "$MNT" OpenCADStudio.dmg + ls -la "$MNT" + test -d "$MNT/OpenCADStudio.app" + test "$(readlink "$MNT/Applications")" = /Applications + codesign --verify --deep --strict "$MNT/OpenCADStudio.app" + hdiutil detach "$MNT" + + - name: Upload .dmg as workflow artifact + if: env.RELEASE_TAG == '' + uses: actions/upload-artifact@v4 + with: + name: OpenCADStudio-macos-arm64-dmg + path: OpenCADStudio.dmg + if-no-files-found: error - name: Upload .dmg to release if: env.RELEASE_TAG != '' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..546aeb73a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,101 @@ +# OpenCADStudio Changelog + +> **Summary:** OpenCADStudio follows a weekly release cycle (Sunday 12:00 UTC) with versioning formatted as `v.` (e.g. `v2026.41`). This master changelog indexes all releases with an executive summary and links to dedicated per-release changelog documents located in [`changelogs/`](changelogs/). + +--- + +## Index of Releases + +| Release | Date | Highlights | Details | +| :--- | :--- | :--- | :--- | +| **[Unreleased](changelogs/unreleased.md)** | *2026-10-11 to Present* | Docking icon rails, block cycle protection & codec explosion, Web/WASM parity wave (in-place save, offline PWA, workers), MCP dynamic schemas & batch bindings | [changelogs/unreleased.md](changelogs/unreleased.md) | +| **[v2026.41](changelogs/v2026.41.md)** | *2026-10-11* | Sheet Set Manager (`SHEETSET`), `COUNT` command & live tables, associative `SWEEP`, RTL text, DimStyle per-end arrowheads (`DIMBLK1`/`DIMBLK2`), GPU options (Metal/OpenGL) | [changelogs/v2026.41.md](changelogs/v2026.41.md) | +| **[v2026.40 / v2026.40.1](changelogs/v2026.40.md)** | *2026-10-05* | `FIELD` command & dialog, `ATTDEF` parity, Blocks palette, MCP DeepZoom pyramidal tiling & SoM visual ID stamps, DWF/DGN underlays, `DYNMODE`, embedded TrueType PDF, instant layer toggling | [changelogs/v2026.40.md](changelogs/v2026.40.md) | +| **[v2026.39](changelogs/v2026.39.md)** | *2026-09-27* | Block creation & `WBLOCK` dialogs, Enter autocomplete, Python Host Model architecture, parametric constraints parity, `XCLIP` boundary clipping, trackpad pan/pinch, tessellation performance | [changelogs/v2026.39.md](changelogs/v2026.39.md) | +| **[v2026.38](changelogs/v2026.38.md)** | *2026-09-20* | 2D geometric constraints engine (`GEOMCONSTRAINT`), External References (`XREF`) overhaul, 3D OSNAP system (22.7x speedup), `DSETTINGS` & `ALIASEDIT`, associative viewport dimensions, 3D SpaceMouse, Windows printing | [changelogs/v2026.38.md](changelogs/v2026.38.md) | + +--- + +## [Unreleased] - 2026-10-13 + +> **Summary:** Overhaul of the workspace docking and layout system with Photoshop-style vertical icon rails and draggable tabs; deep hardening of block definitions against circular recursion and standard-compliant block explosion (`INSERT`/`MINSERT`); major Web/WASM platform parity wave including in-place saving via File System Access API, offline PWA support, and worker-routed operations; plus custom `.pat` and `.lin` pattern loading. + +### ๐ŸŒŸ Key Highlights +* **Modernized Docking & Workspace System:** Photoshop-style collapsible vertical icon rails, auto-hide flyouts, draggable tab reordering, and independent floating palettes. *(Contributed by [@posnofa](https://github.com/posnofa) and [@MarcelP102](https://github.com/MarcelP102))* +* **Block System Hardening & Safe Explosion:** Prevents circular/recursive block definitions and render loops. Delegates `INSERT`/`MINSERT` explosions to `opencadcodec`, restoring transformed `ATTDEF`s, correcting rotated arrays, and consolidating definition ops. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **MCP Extensibility & Schema Preflight:** Added side-effect-free preflight validation, on-demand entity creation schemas, batch parameter bindings, and specialist workflow guides. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Major Web / WASM Platform Wave:** In-place local saving via File System Access API, offline PWA, crash recovery (`restore-all`), clipboard image pasting, worker-routed `WBLOCK`/`SAVEALL`/`ARCHIVE`, and in-browser PDF plotting. *(Contributed by [@posnofa](https://github.com/posnofa), [@HakanSeven12](https://github.com/HakanSeven12), and Claude Opus 5.5)* +* **Custom Resources & Compatibility:** Loading for user `.pat` hatch and `.lin` linetype files, view backgrounds, and strict DWG reader compliance. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12) and [@ramox81](https://github.com/ramox81))* + +๐Ÿ‘‰ **[View Full Unreleased Release Notes](changelogs/unreleased.md)** + +--- + +## [v2026.41] - 2026-10-11 + +> **Summary:** Major feature milestone introducing the Sheet Set Manager (`SHEETSET`) with `.dst` project support; the automated entity `COUNT` command and palette with dynamic table generation; network-integrated associative swept surfaces (`SWEEP`); complete right-to-left (RTL) text layout in `MTEXT`; independent dimension style per-end arrowheads (`DIMBLK1`/`DIMBLK2`); table cell style overrides; GPU hardware selection options (Metal/OpenGL); MCP differential capture payload optimizations; and extensive drafting, plotting, spatial indexing, and memory fixes. + +### ๐ŸŒŸ Key Highlights +* **Sheet Set Manager (`SHEETSET`):** Complete Sheet Set Manager palette, `.dst` project wizard, cross-drawing sheet fields, drawing lock-file coordination, and background sheet opening. *(Contributed by [@ramox81](https://github.com/ramox81) and [@HakanSeven12](https://github.com/HakanSeven12))* +* **Automated Entity Counting (`COUNT`):** Interactive block counting across model space or designated boundaries, collision diagnostics, and live evaluated count table insertion. *(Contributed by [@ramox81](https://github.com/ramox81) and [@HakanSeven12](https://github.com/HakanSeven12))* +* **Associative Swept Surfaces & 3D Modeling:** Network-tracked associative swept surfaces (`SWEEP`) with path-relative base points, miter corner history, B-rep `MASSPROP`, and curved offset/fillets. *(Contributed by [@ramox81](https://github.com/ramox81) and [@HakanSeven12](https://github.com/HakanSeven12))* +* **Advanced Typography & Table Overrides:** Fully compliant right-to-left (RTL) text rendering in `MTEXT`, exact bounding boxes for shaped/rotated text fields, and in-place table cell style overrides with automated formula updates. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Dimension Style Independent Arrowheads (`DIMBLK1`/`DIMBLK2`):** Supported independent arrowhead configurations for start and end leaders with real-time preview synchronization. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **GPU Platform Routing & Hardware Info:** Hardware-aware backend routing (Metal on macOS, OpenGL fallback for legacy hardware), hardware info in About dialog, and dedicated Graphics options tab. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +๐Ÿ‘‰ **[View Full v2026.41 Release Notes](changelogs/v2026.41.md)** + +--- + +## [v2026.40 / v2026.40.1] - 2026-10-05 *(v2026.40.1 replaced pulled v2026.40)* + +> **Summary:** Major release for release week 2026.40 (replacing the pulled `v2026.40` release). Introduces the interactive `FIELD` evaluation command and dialog, full `ATTDEF` attribute definition workflow parity, a redesigned tabbed Blocks palette with drag-and-drop insertion, multiscale pyramidal DeepZoom tiling & Set-of-Marks visual ID stamping for MCP vision tools, DWF/DGN and point cloud underlay support, dynamic input control (`DYNMODE`), script engine upgrades (`.scr`), TrueType embedded font PDF export, and smooth exponential zoom navigation. Incorporates immediate stability hotfixes for instant layer visibility toggling without CPU re-tessellation, 3D cylinder height input validation, load-time `BLOCK`/`ENDBLK` sentinel recovery, and MCP server request lifecycle & cancellation hardening. + +### ๐ŸŒŸ Key Highlights +* **Instant Layer Visibility Toggling:** Toggle visibility states directly on GPU batches without triggering CPU re-tessellation passes. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **`FIELD` Evaluation System & Dialog:** Real-time text/MText field evaluation, table formulas, block placeholders, and plot-time updates. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Attribute Definition (`ATTDEF`) Parity:** Complete `ATTDEF`/`-ATTDEF` dialogs, tag visibility management, and ribbon integration. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Blocks Palette Overhaul:** Redesigned Blocks palette with categorized tabs (Current Drawing, Recent, Favorites) and drag-and-drop insertion. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **MCP Multiscale Pyramidal Tiling (DeepZoom):** On-demand cached multi-resolution pyramid tiling protocol (`manifest.json` and `tile/{level}/{x}/{y}`) allowing LLM vision agents to inspect dense drawings without exceeding token budgets. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Set-of-Marks Visual Grounding & Vision ID Stamp:** High-contrast numbered visual ID badge stamps on entity centroids in viewport captures with bidirectional grounding manifests mapping tags to handles and world bounds. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Visual Differential Streaming & Spatial CRS:** Standardized WCS spatial metadata under `_spatial` and visual diff engine (`compute_visual_diff`) with dirty region cropping (`diff_mode: crop`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Drawing Load Sentinel Recovery:** Materialized missing `BLOCK`/`ENDBLK` sentinels on load, preventing crashes and corrupt database states. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **PDF TrueType Font Embedding:** Subset TrueType font embedding directly in vector PDF export. *(Contributed by [@Chrisyeh1983](https://github.com/Chrisyeh1983))* + +๐Ÿ‘‰ **[View Full v2026.40 / v2026.40.1 Release Notes](changelogs/v2026.40.md)** + +--- + +## [v2026.39] - 2026-09-27 + +> **Summary:** Milestone release featuring the initial Python Host Model plugin architecture, full dimensional and geometric parametric constraint parity (Linear, Aligned, Angular, Radial, Diameter), interactive Block Definition (`BLOCK`/`BMAKE`) and Write Block (`WBLOCK`) dialogs, command-line autocomplete execution on Enter, the `XCLIP` boundary clipping tool, redesigned PDF/XREF underlay integration panels, interactive plugin command previews, trackpad gesture navigation (pan and pinch-to-zoom), major rendering & tessellation performance optimizations, and robust metric drawing template defaults. + +### ๐ŸŒŸ Key Highlights +* **Block Definition (`BLOCK`/`BMAKE`) & WBLOCK Dialogs:** Interactive block creation dialogs with on-screen sequencing, base point coordinate pickers, unit settings, and external file block export. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Command-Line Autocomplete & Architecture Modularization:** Pressing Enter executes highlighted autocomplete suggestions immediately. Modularized monolithic 11.5k-line command driver into clean domain modules. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Python Host Model & Plugin Architecture:** Foundational embedded Python runtime host with CAD entity data structures and automation APIs. *(Contributed by [@felixriestra](https://github.com/felixriestra))* +* **Parametric Constraints Parity:** Complete geometric and dimensional constraint toolsets (Linear, Aligned, Angular, Radial, Diameter). *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Tessellation & Pipeline Performance:** Optimized polyline arc tessellation sampling, bypassed wide fills on thin polylines, memoized constraint glyphs, and capped selection grips at 4096. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Boundary Clipping (`XCLIP`):** Polygonal and rectangular clipping boundaries for external references and block inserts. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Trackpad Navigation Gestures:** Smooth two-finger panning and pinch-to-zoom gestures. *(Contributed by [@swit33](https://github.com/swit33))* + +๐Ÿ‘‰ **[View Full v2026.39 Release Notes](changelogs/v2026.39.md)** + +--- + +## [v2026.38] - 2026-09-20 + +> **Summary:** Massive foundational release bringing the geometric 2D constraint engine (Horizontal, Vertical, Concentric, Smooth, Symmetric, Equal, Fixed, Autoconstrain), complete External References (XREF) Manager overhaul, independent 3D object-snap engine with 22.7x tracking speedup, modernized `DSETTINGS` dialog, `ALIASEDIT` command alias manager, associative paper space viewport dimensions, 3Dconnexion SpaceMouse navigation on Windows, comprehensive Windows printing & CTB routing, context right-click menus, and 3D format fixes (STEP/OBJ). + +### ๐ŸŒŸ Key Highlights +* **External References (XREF) Manager Overhaul:** Docked palette with tree/list views, resizable columns, status detection (loaded, unloaded, stale, orphaned), detach/reload/bind/path ops, and split toolbar. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Independent 3D Object Snap System & Performance:** 3D OSNAP engine supporting 6 distinct modes, coupled with a 22.7x broadphase and tangent gating speedup in cursor snap tracking. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Drafting Settings (`DSETTINGS`) & `ALIASEDIT` Modernization:** Modern tabbed `DSETTINGS` dialog with editable grid/snap spacing, and full shortcut-editor parity in `ALIASEDIT`. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Geometric Parametric Constraints Engine:** 2D constraint engine with `AUTOCONSTRAIN` and the interactive `CONSTRAINTBAR`. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Polyline Arc & Offset Grip Editing:** Interactive radius grip editing for LWPOLYLINE arcs and parallel segment offset grips preserving connected geometry. *(Contributed by [@MarcelP102](https://github.com/MarcelP102))* +* **Fusion Themes & UCS Face/Object Alignment:** Fusion Black and Fusion White visual themes, interactive sketch mode, and direct UCS alignment to solid 3D faces (`UCS FACE`). *(Contributed by [@dasmatus](https://github.com/dasmatus))* +* **Associative Viewport Dimensions:** Associative dimensioning in layout viewports dynamically tied to 3D model geometry. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12), [@CiberNin](https://github.com/CiberNin), and [@kevin9327](https://github.com/kevin9327))* +* **3Dconnexion SpaceMouse Support:** Native 6-DOF controller navigation support on Windows. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Windows Plotting & Printing Subsystem:** Reliable DEVMODE queries, custom paper routing, and lenient CTB loading. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12) and [@CiberNin](https://github.com/CiberNin))* + +๐Ÿ‘‰ **[View Full v2026.38 Release Notes](changelogs/v2026.38.md)** diff --git a/Cargo.lock b/Cargo.lock index 17afafa58..54283507f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,7 @@ dependencies = [ "image", "inventory", "js-sys", + "jsonschema", "kurbo 0.13.1", "libloading", "log", @@ -129,6 +130,7 @@ dependencies = [ "cfg-if", "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -805,6 +807,12 @@ dependencies = [ "piper", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "brotli" version = "8.0.4" @@ -1363,6 +1371,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "data-url" version = "0.3.2" @@ -1537,6 +1551,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1704,6 +1727,17 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fancy-regex" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301f5bf187b3c295fce6468d3875037a0bccc5f6b151c63cac2f85babf21912" +dependencies = [ + "bit-set 0.8.0", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fast-float2" version = "0.2.4" @@ -1835,6 +1869,17 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -1924,6 +1969,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "funty" version = "2.0.0" @@ -2296,6 +2351,8 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", ] @@ -3135,6 +3192,58 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.49.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ec8a241beed129f06114aa68007e905ca350e7baeb6e17a7631bb7978d91b2" +dependencies = [ + "ahash 0.8.12", + "bytecount", + "data-encoding", + "email_address", + "fancy-regex", + "fraction", + "getrandom 0.3.4", + "idna", + "itoa", + "jsonschema-regex", + "jsonschema-value", + "num-cmp", + "num-traits", + "percent-encoding", + "referencing", + "regex", + "serde", + "serde_json", + "strum", + "unicode-general-category", + "uuid-simd", +] + +[[package]] +name = "jsonschema-regex" +version = "0.49.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91994f45017ed5e66aa8e59b8415f4cb033a6380d7200387b7cf117595fbdf85" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "jsonschema-value" +version = "0.49.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec7637f83e510868ae6ed625f7ebfbbde4554ee8ce49854caa5126a8b9b9ecb" +dependencies = [ + "ahash 0.8.12", + "bytecount", + "fraction", + "num-cmp", + "num-traits", + "serde_json", +] + [[package]] name = "kamadak-exif" version = "0.6.1" @@ -3466,6 +3575,12 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "micromap" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" + [[package]] name = "mime" version = "0.3.17" @@ -3698,6 +3813,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -3708,6 +3837,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + [[package]] name = "num-complex" version = "0.4.6" @@ -3754,6 +3889,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -4228,7 +4373,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.6.0" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=3ec70a2#3ec70a2a0134f60a9f3036f1974fea86ad96e439" +source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=e83247d#e83247df8d0b9c24f6eedb75fb9a4c279f217ec8" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4251,7 +4396,7 @@ dependencies = [ [[package]] name = "opencadgraph" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=338a973#338a973202d56c1df7ddf7ce8d40d385af16d406" +source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=4ab7dcb#4ab7dcb70810d087316bf5bc531d7cef570a3dd6" dependencies = [ "opencadkernel", "serde_json", @@ -4260,7 +4405,7 @@ dependencies = [ [[package]] name = "opencadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=e710727#e71072779377116269f822e01a2df97b8fa3420b" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=01ba0bf#01ba0bfc4627ae9c4accd2f39c7aab64e8b1e090" dependencies = [ "cavalier_contours", "opencadcodec", @@ -4271,7 +4416,7 @@ dependencies = [ [[package]] name = "opencadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=e710727#e71072779377116269f822e01a2df97b8fa3420b" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=01ba0bf#01ba0bfc4627ae9c4accd2f39c7aab64e8b1e090" dependencies = [ "nalgebra", ] @@ -4318,7 +4463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -4345,6 +4490,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -5105,6 +5256,43 @@ dependencies = [ "bitflags 2.13.1", ] +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "referencing" +version = "0.49.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6efa2154ea6f5ce0fdecdd2a8d18f2fa1a39a8fbba91564f555a592e4dce8278" +dependencies = [ + "ahash 0.8.12", + "fluent-uri", + "getrandom 0.3.4", + "hashbrown 0.17.1", + "itoa", + "micromap", + "parking_lot", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" version = "1.13.1" @@ -5910,6 +6098,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5938,7 +6147,7 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0811b01ca2c4e8718760713911feaf4675c24f94e50530a015ec646cfb622f7c" dependencies = [ - "skrifa 0.37.0", + "skrifa 0.42.1", "yazi", "zeno", ] @@ -6521,6 +6730,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "v_frame" version = "0.3.9" @@ -6585,6 +6804,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index d3b542ed8..2fa933327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ winresource = "0.1" [dependencies] serde_json = "1" +# Offline validation of the same contracts published to automation clients. +jsonschema = { version = "0.49", default-features = false } serde = { version = "1", features = ["derive"] } calamine = "0.36.1" base64 = "0.22" @@ -35,10 +37,10 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" log = "0.4" -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "3ec70a2", features = ["serde"] } -kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "e710727", features = ["acis", "offset"] } -kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "e710727" } -graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "338a973" } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "e83247d", features = ["serde"] } +kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "01ba0bf", features = ["acis", "offset"] } +kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "01ba0bf" } +graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "4ab7dcb" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } # Shared SAVE serialization dispatch + web-worker wire format: one # implementation for the inline (fallback/native) path and the worker path. @@ -90,7 +92,7 @@ naga = { version = "27", features = ["wgsl-in"] } [target.'cfg(target_os = "windows")'.dependencies] libloading = "0.8" -windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Security", "Win32_System_Com", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing", "Win32_Graphics_Gdi", "Win32_Storage_Xps", "Win32_Globalization", "Win32_Security_Authentication_Identity"] } +windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Security", "Win32_System_Com", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing", "Win32_Graphics_Gdi", "Win32_Storage_Xps", "Win32_Globalization", "Win32_Security_Authentication_Identity", "Win32_UI_Controls_Dialogs"] } [target.'cfg(target_os = "linux")'.dependencies] ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] } diff --git a/assets/icons/refset_add.svg b/assets/icons/refset_add.svg new file mode 100644 index 000000000..e4d929681 --- /dev/null +++ b/assets/icons/refset_add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/refset_remove.svg b/assets/icons/refset_remove.svg new file mode 100644 index 000000000..4f2536ebe --- /dev/null +++ b/assets/icons/refset_remove.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ui/preview.svg b/assets/icons/ui/preview.svg new file mode 100644 index 000000000..53a5513ba --- /dev/null +++ b/assets/icons/ui/preview.svg @@ -0,0 +1 @@ + diff --git a/assets/ocad.pgp b/assets/ocad.pgp index bde60d84f..6c6a6b92c 100644 --- a/assets/ocad.pgp +++ b/assets/ocad.pgp @@ -71,6 +71,10 @@ BE, *BEDIT IAT, *IMAGEATTACH XA, *XATTACH XR, *XREF +-XR, *-XREF +XC, *XCLIP +ICL, *IMAGECLIP +IAD, *IMAGEADJUST ADC, *ADCENTER W, *WBLOCK @@ -155,6 +159,7 @@ SHA, *SHADEMODE COL, *COLOR DR, *DRAWORDER DS, *DSETTINGS +DV, *DVIEW LT, *LINETYPE LTS, *LTSCALE OS, *OSNAP diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md new file mode 100644 index 000000000..f62c1e54f --- /dev/null +++ b/changelogs/unreleased.md @@ -0,0 +1,114 @@ +# OpenCADStudio - Unreleased (2026-10-11 to Present) + +> **Summary:** Overhaul of the workspace docking and layout system with Photoshop-style vertical icon rails and draggable tabs; deep hardening of block definitions against circular recursion and standard-compliant block explosion (`INSERT`/`MINSERT`); major Web/WASM platform parity wave including in-place saving via File System Access API, offline PWA support, and worker-routed operations; plus custom `.pat` and `.lin` pattern loading. + +--- + +### ๐ŸŒŸ Notable Release Notes + +* **Modernized Docking & Workspace System:** Introduced Photoshop-style collapsible vertical icon rails for docked groups, 150ms auto-hide flyout behavior, draggable tab reordering, and independent floating windows for palettes (including a dedicated `LAYERS` manager). *(Contributed by [@posnofa](https://github.com/posnofa) and [@MarcelP102](https://github.com/MarcelP102))* +* **Block System Hardening & Safe Explosion:** Complete overhaul of block references and definitions. Prevents circular/recursive block nesting crashes at both commit time and render time. Block explosions (`INSERT` and `MINSERT`) now delegate to `opencadcodec`, properly restoring transformed `ATTDEF` templates and handling rotated arrays with non-zero base points. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Major Web / WASM Platform Wave:** In-place local file saving via the File System Access API, offline PWA support, automatic crash recovery (`restore-all`), clipboard image pasting, worker-threaded `WBLOCK` / `SAVEALL` / `ARCHIVE` tasks, and in-browser PDF plotting. *(Contributed by [@posnofa](https://github.com/posnofa), [@HakanSeven12](https://github.com/HakanSeven12), and Claude Opus 5.5)* +* **Custom Resources & Compatibility:** Native support for loading custom user hatch patterns (`.pat`) and linetypes (`.lin`), custom view backgrounds (solid, gradient, and device-pixel scaled images), and strict DWG reader compliance fixes. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12) and [@ramox81](https://github.com/ramox81))* + +--- + +### ๐Ÿš€ New Features + +#### UI & Workspace +* **Docking Panels & Icon Strips:** Added vertical icon rails for docked palette groups, per-edge auto-hide flyouts, customizable slot heights, and double-click float/dock toggles. *(Contributed by [@posnofa](https://github.com/posnofa))* +* **Draggable Tabs & Groups:** Palette tabs can now be reordered by dragging; floating windows support grouped tabs. *(Contributed by [@posnofa](https://github.com/posnofa))* +* **Layer Manager Palette:** Integrated `LAYERS` into the docking system as a native pallet. *(Contributed by [@posnofa](https://github.com/posnofa))* +* **Graphic Attributes Palette:** Added toggles, controls for open boundaries, color swatches with `*VARIES*` indicator, and persistent docking layout. *(Contributed by [@MarcelP102](https://github.com/MarcelP102))* +* **MATCHPROP Paintbrush Cursor:** While `MATCHPROP` is armed the CAD crosshair gives way to a monochrome paintbrush whose tip rides the pick point, echoing the AutoCAD "match properties" cursor. The glyph borrows the crosshair's own colour โ€” including a user-customised one โ€” so only the shape changes, never the ink. *(Contributed by [@Ch4rlos](https://github.com/Ch4rlos))* + +#### Selection & Drafting +* **Advanced Selection Filter (`FILTER`):** Added comprehensive selection filtering with normalized logical operators and attribute filters (v2.2). *(Contributed by [@turbuczn](https://github.com/turbuczn))* +* **Transparent Object Snap Overrides:** Introduced transparent object snap overrides within active drafting commands. *(Contributed by [@yztink](https://github.com/yztink))* +* **Chamfer & Fillet No-Trim Mode:** Added support for preserving source entities during `CHAMFER` and `FILLET` (`TRIMMODE=0`). *(Contributed by [@YsDirector](https://github.com/YsDirector))* +* **Line Tangency in Arcs:** Supported line tangency constraints during arc construction with improved trim contact precision. *(Contributed by [@yztink](https://github.com/yztink))* +* **SVG Export:** Added native SVG export via `EXPORTSVG`. *(Contributed by [@posnofa](https://github.com/posnofa))* +* **Custom Resource Loading:** Added direct loading for user `.pat` hatch pattern and `.lin` linetype files. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* + +#### Views & Layouts +* **`-VIEW` Command Line Interface:** Added command-line view presets, orthographic UCS alignment, and window definitions. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **View Backgrounds:** Added support for solid, two-color linear gradient, and scaled/offset device-pixel raster backgrounds. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Automatic Layout Viewport (`LAYOUTCREATEVIEWPORT`):** Automatically creates and enables an initial viewport upon first opening a layout tab. *(Contributed by [@ramox81](https://github.com/ramox81))* + +#### Web & Platform +* **In-Place File System Access:** Native saving in browser via File System Access API. *(Contributed by [@posnofa](https://github.com/posnofa))* +* **PWA & Offline Recovery:** Full offline PWA capabilities with deep linking (`?file=`) and session recovery (`restore-all`). *(Contributed by [@posnofa](https://github.com/posnofa))* +* **Web Worker File Operations:** Offloaded `WBLOCK`, `SAVEALL`, and `ARCHIVE` transmittal generation to background web workers. *(Contributed by [@posnofa](https://github.com/posnofa))* +* **3D Formats in Web:** Enabled STEP/STL export and OBJ import on web builds. *(Contributed by [@posnofa](https://github.com/posnofa))* +* **In-Page Plot Preview:** Embedded PDF plot preview directly in-page on wasm32 with quality presets. *(Contributed by [@posnofa](https://github.com/posnofa))* + +#### Automation & MCP (Model Context Protocol) +* **Canvas Pointer Event Replay:** Added pointer event replay on the canvas for automated test drivers. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Dynamic Entity Schemas & Preflight:** Added on-demand entity creation schema inspection and side-effect-free preflight validation in MCP server (`006be890`, `97afb573`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **MCP Batch Step Bindings & Summaries:** Added batch parameter bindings to earlier step results and batch summaries with read-only detail retrieval (`50030be8`, `7b68d274`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Specialist Guides & Schema Deduplication:** Served architectural and workflow specialist guides on demand and deduplicated operations with shared schemas (`8943a6fd`, `f4b33d5c`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Resource Links for Captures:** Returned dedicated resource URIs for reference-only image captures (`6ff8d8d7`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Spline Automation API:** Added spline creation and parameter serialization with strict knot/weight validation (`entities_create`). *(Contributed by [@YsDirector](https://github.com/YsDirector))* +* **Selection Event Streaming:** Automation clients now receive real-time selection change notifications. *(Contributed by [@YsDirector](https://github.com/YsDirector))* +* **Host API v8:** Added name-addressed `XRecord` CRUD operations on `HostApi`. *(Contributed by [@schoeller](https://github.com/schoeller))* + +--- + +### ๐Ÿ› Bug Fixes + +#### Blocks & Attributes +* **Circular Block Prevention:** Added cycle detection in `block_service` preventing recursive block definitions (`INSERT` in `BEDIT` or redefinition) and safely truncated render-time cycles with session warnings (`9c08e8a3`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Block Explosion & ATTDEF Restoration:** Restored transformed `ATTDEF` definitions when exploding `INSERT` / `MINSERT` blocks, dropped instance attribute values, and corrected rotated array spacings (`8d287dec`, `59bb6faa`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Constant Attributes in ATTEDIT:** Enforced constant attribute flags in attribute editor by disabling constant fields and ignoring edits (`59bb6faa`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Block Service Consolidation:** Consolidated delete, rename, and purge operations through a single block definition service (`2b50b6c8`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Paste Block Origin:** Fixed `PASTEBLOCK` placement to align accurately with the picked point (`925c91cc`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Wipeout Render Cache & Draw Order:** Fixed wipeout render cache invalidations on insert copy/grip moves and preserved draw order for overlapping block wipeouts and hatches (`fe0edb61`, `1074c81f`, `72e97c6f`). *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* +* **Cross-Document Dictionaries:** Prevented copied entities from incorrectly sharing extension dictionaries across documents (`b5b505ee`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +#### Geometry & Drafting +* **Model Space Grip Dragging:** Constrained grip drags to the drawing plane, preventing grips from collapsing onto themselves (`9cc890c9`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Fillet Polyline Join:** Automatically join filleted lines or arcs into the target polyline (`2f087190`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Boundary Edge Preselection in Extend:** Enabled `EXTEND` to honor preselected boundary entities (`3840630a`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Angular Distance Calculations:** Corrected `DIST` XY angle measurement to measure from `ANGBASE` in the direction defined by `ANGDIR` (`fa00c27e`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Dimension Text & Blocks:** Kept picture block positions consistent during world-space edits (`DIMTMOVE=2`) (`c5b1269c`, `41db8d67`). *(Contributed by [@ramox81](https://github.com/ramox81) and [@YsDirector](https://github.com/YsDirector))* +* **Extrude Taper History:** Restored fallback profiles in place under group 295 and applied taper angles strictly when chosen (`656f0cea`, `f4a0b1d6`). *(Contributed by [@ramox81](https://github.com/ramox81))* + +#### UI & General +* **Input Focus & Rebound Spacebar:** Allowed rebound spacebar execution while the command line retains focus (`7e0866e6`, `cbefecb1`). *(Contributed by [@theluckystrike](https://github.com/theluckystrike))* +* **Field Focus Persistence:** Retained typed layer and text style names when UI input fields lose focus (`f6503ad4`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Start Screen Quick-Access:** Restored functionality of the New button on the initial Start screen (`88803cd6`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Windows Default Apps Fallback:** Gracefully fall back to Windows Settings when the system default-apps dialog fails (`46395464`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **macOS Release DMG:** Restored Applications shortcut link in macOS DMG bundles (`f769b875`). *(Contributed by [@KevinGriffin-new](https://github.com/KevinGriffin-new))* +* **Plugin Grip Refresh:** Refreshed selected grip markers following plugin-driven entity edits (`c6dcee2b`). *(Contributed by [@schoeller](https://github.com/schoeller))* +* **Localizations:** Completed Hungarian CAD translations and updated message catalogs (`49f12e01`, `a5435a6d`). *(Contributed by [@turbuczn](https://github.com/turbuczn), [@hvfrancesco](https://github.com/hvfrancesco), and [@HakanSeven12](https://github.com/HakanSeven12))* + +#### Rendering, Plotting & File I/O +* **Transparent Hatch Layering:** Corrected depth sorting to draw transparent hatches cleanly over background lines (`d1a2e9db`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Wide Polyline PDF Output:** Rendered wide polylines in PDF export with square endcaps and mitered joins (`088dec4a`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Plot Style Precedence:** Prioritized explicitly requested CTB plot styles over stored layout page setup tables during headless plots (`468d1e4f`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12) and Claude Opus 5.5)* +* **Window-Plot Sheet Centering:** Centered layout sheets accurately over the plot window (`710a837a`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12) and Claude Opus 5.5)* +* **Atomic Save Retry:** Added retry handling during atomic file replacements to tolerate brief locks (`9e31af3f`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Image Memory Budgeting:** Enforced streaming decode checks and memory budget limits against malformed or excessive GIF/TIFF images (`f015c3b5`, `ce6bd2c7`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12) and [@posnofa](https://github.com/posnofa))* +* **DWG Strict Compliance:** Pinned upstream `opencadcodec` fixes to ensure saved DWGs pass strict audits without recovery dialogs (`390e1216`). *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12) and Claude Opus 5.5)* + +--- + +### ๐Ÿ‘ฅ Contributors + +Huge thanks to everyone who contributed to this cycle: + +* **Hakan Seven** ([@HakanSeven12](https://github.com/HakanSeven12)) โ€” Upstream kernel/codec syncs, DWG/DXF compatibility, headless plot style precedence, window-plot sheet centering. +* **Afonso Portela** ([@posnofa](https://github.com/posnofa)) โ€” Web/WASM platform wave (in-place save, offline PWA, worker saves, 3D formats), docking system overhaul, SVG export. +* **Karim Jerbi** ([@KarimJerbi](https://github.com/KarimJerbi)) โ€” Circular block cycle prevention, block explosion delegating to codec, constant attribute enforcement, rotated MINSERT arrays, unified block definition service, MCP dynamic entity schemas & preflight validation, batch step bindings, specialist guides, and cross-document extension dictionary isolation. +* **ramox81** ([@ramox81](https://github.com/ramox81)) โ€” View backgrounds, `-VIEW` preset CLI, associative surfaces, initial layout viewport generation. +* **Claude** (Claude Opus 5.5) โ€” Core CAD engine, layout/plot, and web/WASM co-authoring. +* **ysdirector** ([@YsDirector](https://github.com/YsDirector)) โ€” Automation API: spline creation/validation, selection event publishing, task completion guards. +* **gianlucafiore** ([@gianlucafiore](https://github.com/gianlucafiore)) โ€” Block wipeout render cache invalidation, GPU boundary masks, draw-order sorting. +* **Marcel** ([@MarcelP102](https://github.com/MarcelP102)) โ€” Graphic Attributes palette and docking placement. +* **Sebastian** ([@schoeller](https://github.com/schoeller)) โ€” Host API v8 XRecord CRUD operations, plugin grip refresh. +* **turbuczn** ([@turbuczn](https://github.com/turbuczn)) โ€” Hungarian localizations, selection filter list enhancements. +* **yztink** ([@yztink](https://github.com/yztink)) โ€” Transparent object snap overrides, arc line tangency construction. +* **Kevin Griffin** ([@KevinGriffin-new](https://github.com/KevinGriffin-new)) โ€” macOS DMG bundle Applications shortcut link (`#769`). +* **francesco fantoni** ([@hvfrancesco](https://github.com/hvfrancesco)) โ€” Translations and UI improvements. +* **theluckystrike** ([@theluckystrike](https://github.com/theluckystrike)) โ€” Command line rebound spacebar execution (`#1710`). +* **Carlos Quiroga** ([@Ch4rlos](https://github.com/Ch4rlos)) โ€” `MATCHPROP` paintbrush cursor. diff --git a/changelogs/v2026.38.md b/changelogs/v2026.38.md new file mode 100644 index 000000000..e84e3e552 --- /dev/null +++ b/changelogs/v2026.38.md @@ -0,0 +1,122 @@ +# OpenCADStudio - Release v2026.38 (2026-09-20) + +> **Summary:** Massive foundational release bringing the geometric 2D constraint engine (Horizontal, Vertical, Concentric, Smooth, Symmetric, Equal, Fixed, Autoconstrain), complete External References (XREF) Manager overhaul, independent 3D object-snap engine with 22.7x tracking speedup, modernized `DSETTINGS` dialog, `ALIASEDIT` command alias manager, polyline radius & parallel offset grip editing, Fusion Black & White UI themes, UCS Face/Object alignment, associative paper space viewport dimensions, 3Dconnexion SpaceMouse navigation on Windows, comprehensive Windows printing & CTB routing, context right-click menus, and 3D format fixes (STEP/OBJ). + +--- + +### ๐ŸŒŸ Notable Release Notes + +* **Geometric Parametric Constraints Engine:** Introduced the 2D geometric constraint engine with full support for Horizontal, Vertical, Perpendicular, Parallel, Tangent, Concentric, Smooth, Symmetric, Equal, and Fixed constraints, accompanied by `AUTOCONSTRAIN` and the interactive `CONSTRAINTBAR`. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **External References (XREF) Manager Overhaul:** Introduced a completely redesigned docked External References palette featuring lexical path handling, stale and orphaned status detection, detach, unload, reload, bind, and path operations, tree and table views, resizable columns, and unified split-button toolbar. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Independent 3D Object Snap System & Performance:** Introduced an independent 3D OSNAP engine supporting 6 distinct modes, coupled with a 22.7x broadphase and tangent gating speedup in cursor snap tracking. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Drafting Settings (`DSETTINGS`) & `ALIASEDIT` Modernization:** Redesigned the `DSETTINGS` dialog with a modern tabbed layout, editable grid/snap spacing, and major grid line intervals, alongside full shortcut-editor parity in `ALIASEDIT`. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Polyline Arc & Offset Grip Editing:** Added interactive radius grip editing for LWPOLYLINE arcs and parallel segment offset grips while preserving connected tangent geometry. *(Contributed by [@MarcelP102](https://github.com/MarcelP102))* +* **Fusion Themes & UCS Face/Object Alignment:** Added high-contrast Fusion Black and Fusion White visual themes, interactive sketch mode, and UCS alignment to solid faces (`UCS FACE`) and planar entities (`UCS OBJECT`). *(Contributed by [@dasmatus](https://github.com/dasmatus))* +* **Associative Viewport Dimensions:** Enabled associative dimensioning in paper space layout viewports linked dynamically to 3D model space geometry across view rotations and scale adjustments. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12), [@CiberNin](https://github.com/CiberNin), and [@kevin9327](https://github.com/kevin9327))* +* **3Dconnexion SpaceMouse Support:** Added native 6-DOF 3Dconnexion SpaceMouse controller navigation support on Windows for smooth 3D orbiting, panning, and zooming. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Windows Plotting & Printing Subsystem:** Re-architected native Windows printer communication, adding reliable DEVMODE media size queries, custom paper routing, and lenient CTB color table loading. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12) and [@CiberNin](https://github.com/CiberNin))* +* **Context Menus & Interaction:** Added right-click context menus across entity selections, command states, and paper space layouts. *(Contributed by [@mechpanos](https://github.com/mechpanos))* + +--- + +### ๐Ÿš€ New Features + +#### Parametric Modeling +* **Geometric Constraints (`GEOMCONSTRAINT`):** Added complete solver integration for Concentric, Smooth, Symmetric, Fixed, and Equal constraints. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Constraint Bar & Value Display:** Interactive on-screen constraint indicator bars with hover markers and visibility controls. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Auto-Constraint Inference:** Implemented `AUTOCONSTRAIN` to automatically infer geometric relationships across drafted geometry. *(Contributed by [@ramox81](https://github.com/ramox81))* + +#### Palettes & External References +* **Docked External References Palette (`XREF`):** Docked panel with tree and list views, resizable columns, live preview on selection, and status detection for loaded, unloaded, stale, and orphaned references. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **XREF Operations & Path Management:** Full lifecycle support for Attach, Detach, Unload, Reload, Bind, and Overlay operations with relative/absolute/no-path actions. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Reference Manager Help Window:** Added integrated Reference Manager documentation and assistance dialog. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +#### Drafting Settings, Snapping & Grips +* **Independent 3D OSNAP System:** Added dedicated 3D object-snap engine supporting six modes (Endpoint, Midpoint, Nearest, Intersection, Perpendicular, Center). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Polyline Arc Radius Grips:** Added interactive mid-arc radius grip editing on 2D lightweight polylines. *(Contributed by [@MarcelP102](https://github.com/MarcelP102))* +* **Parallel Segment Offset Grips:** Added parallel offset grips to polyline segments while keeping adjacent arc connections intact. *(Contributed by [@MarcelP102](https://github.com/MarcelP102))* +* **Drafting Settings Dialog (`DSETTINGS`):** Modernized tabbed dialog layout with single tab strip, editable grid X/Y spacing, and major grid line frequency. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Snap Spacing & Grid Locks:** Added editable Snap X/Y spacing with Equal lock and grid snap synchronization. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Command Alias Editor (`ALIASEDIT`):** Modern shortcut and alias editor with real-time command alias remapping. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Themed Ribbon Color Dropdown:** Implemented themed 5-section ribbon object color dropdown. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Axis-Aligned Snapping:** Added perpendicular axis alignment snapping and reprompting during precision coordinate inputs. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Context Menus:** Contextual right-click menus tailored to active selection filters and commands. *(Contributed by [@mechpanos](https://github.com/mechpanos))* +* **Hyperlink Control:** Added clickable hyperlink properties on drawing entities with sanitized external URL routing. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Options Dialog Actions:** Standardized OK, Apply, and Cancel button behaviors in the application Options dialog. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* + +#### UI, Workspace & Themes +* **Fusion Themes:** Added Fusion Black and Fusion White visual themes with balanced canvas contrast. *(Contributed by [@dasmatus](https://github.com/dasmatus))* +* **UCS Face & UCS Object:** Added direct UCS alignment to solid 3D planar faces and 2D entity planes. *(Contributed by [@dasmatus](https://github.com/dasmatus))* +* **Interactive Sketch Mode:** Added dedicated sketch mode state on active planes. *(Contributed by [@dasmatus](https://github.com/dasmatus))* +* **Canvas Background Colour Wheel:** Added interactive colour wheel picker for viewport canvas backgrounds. *(Contributed by [@dasmatus](https://github.com/dasmatus))* +* **OLE2FRAME Image Embed:** Added support for embedded OLE images and picture frames. *(Contributed by [@huaninstratech](https://github.com/huaninstratech))* + +#### Performance & Graphics +* **22.7x Snap Tracking Speedup:** Optimized OSNAP cursor tracking broadphase and tangent gating for dramatic real-time performance. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **GPU Backend Probe & Fallback:** Added hardware capability probing with automated fallback to OpenGL for older graphics hardware. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Spatial Interaction Indexing:** Chunked batch accumulation for spatial `InteractionIndex` build. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Direct Quad-Strip Triangulation:** Fast quad-strip triangulation for wide arc tessellation. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **MRU Inline Icon Cache:** Added MRU cache to `themed_handle` for instantaneous icon resolution. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Layer-Usage Table Rebuild Speedup:** Extracted and optimized layer-usage table regeneration. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +--- + +### ๐Ÿ› Bug Fixes + +#### Drafting, Snapping & Modeling +* **Extrude Height Commit on Enter:** Committed typed dynamic input height accurately on Enter during `EXTRUDE`. *(Contributed by [@steveking-gh](https://github.com/steveking-gh))* +* **Diameter Dimension Alignment:** Fixed diameter dimension grip placement and text alignment. *(Contributed by [@codallos](https://github.com/codallos))* +* **Layer Invalidation on Model Space Import:** Properly triggered layer invalidation when importing model space content. *(Contributed by [@guywydouw](https://github.com/guywydouw))* +* **Grid Units & Limits Display:** Honored `GRIDUNIT` X/Y spacing and `LIMITS` display toggle during viewport grid rendering. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Isometric Drafting Isolation:** Kept isometric drafting mode scoped strictly to active sessions. *(Contributed by [@huaninstratech](https://github.com/huaninstratech))* +* **ViewCube Oblique Horizon:** Enforced deterministic horizon calculation for oblique viewpoints and generic opposite view transitions. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Block Palette Theme Contrast:** Enforced contrast coverage and theme-aware label colors in the Block Palette. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Command Line Markers & Formatting:** Standardized history markers and bold error highlighting in the command-line console. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Polyline Intersection Snapping:** Resolved snap failure on intersecting polyline arc-line segments. *(Contributed by [@yztink](https://github.com/yztink))* + +#### 3D Formats & Geometry +* **STEP Export with Large Coordinates:** Fixed coordinate precision overflow when exporting geometry positioned far from the world origin to STEP format. *(Contributed by [@kevin9327](https://github.com/kevin9327))* +* **OBJ Import Missing Normals:** Correctly computed and synthesized smooth vertex normals when importing OBJ files lacking normal data. *(Contributed by [@kevin9327](https://github.com/kevin9327))* +* **Zero-Radius Curves:** Preserved valid zero-radius curve segments without collapsing geometry during entity transformations. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Perspective Clipping:** Fixed depth draw-order clipping artifacts in perspective camera projections. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Hatch Boundary Precision:** Resolved boundary edge gaps in complex associative hatch loops. *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* + +#### Dimensions, Plotting & Resilience +* **Off-World Z Transformations:** Corrected rotate and mirror transformations for linear and aligned dimensions whose plane does not align with the world Z axis. *(Contributed by [@kevin9327](https://github.com/kevin9327))* +* **CTB Lineweight Precision:** Fixed lineweight mapping rounding errors when reading CTB plot style tables. *(Contributed by [@kevin9327](https://github.com/kevin9327))* +* **Dimension Preview Styling:** Aligned interactive dimension placement previews with the active dimension style parameters. *(Contributed by [@CiberNin](https://github.com/CiberNin))* +* **Block Text Wipeout Depth:** Prevented block-nested wipeouts from occluding dimension and annotation text. *(Contributed by [@huaninstratech](https://github.com/huaninstratech))* +* **Cloud & Shrinking File Resilience:** Prevented crashes when reading locked, shrinking, or cloud-placeholder drawing files. *(Contributed by [@huaninstratech](https://github.com/huaninstratech))* +* **Plotstyle Wire-Fill Depth:** Composed PLOTSTYLE wire-fill depth consistently with the wire rendering pipeline. *(Contributed by [@huaninstratech](https://github.com/huaninstratech))* + +#### Platform, CI & Build +* **Editor Crash Hardening:** Fixed edge-case crashes during editor state transitions and selection clearing. *(Contributed by [@CiberNin](https://github.com/CiberNin))* +* **macOS DMG Applications Shortcut:** Restored Applications shortcut link in macOS DMG bundles. *(Contributed by [@felixriestra](https://github.com/felixriestra))* +* **WASM Build Fixes:** Fixed wasm32 compilation errors in web packaging workflows. *(Contributed by [@helenkwok](https://github.com/helenkwok))* +* **Nix Flake Support:** Added reproducible Nix flake dev shell and package build definitions. *(Contributed by [@dasmatus](https://github.com/dasmatus))* +* **CI Signing Waterfall:** Added robust multi-stage signing waterfall with fallback for forks without secrets. *(Contributed by [@huaninstratech](https://github.com/huaninstratech))* +* **Shell Icon Action Removal:** Removed legacy `RefreshShellIcons` `ie4uinit` custom action from Windows WiX installer. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +--- + +### ๐Ÿ‘ฅ Contributors + +Huge thanks to all contributors to `v2026.38`: + +* **Hakan Seven** ([@HakanSeven12](https://github.com/HakanSeven12)) โ€” Viewport dimensions, SpaceMouse support, Windows plotting, CTB leniency, perspective rendering. +* **Christopher Overbeck** ([@CiberNin](https://github.com/CiberNin)) โ€” Viewport dimensions, dimension previews, Windows printer routing, editor crash hardening. +* **ramox81** ([@ramox81](https://github.com/ramox81)) โ€” 2D geometric constraint engine, `AUTOCONSTRAIN`, `CONSTRAINTBAR`, hyperlink properties, axis alignment. +* **Karim Jerbi** ([@KarimJerbi](https://github.com/KarimJerbi)) โ€” Docked External References (XREF) Manager overhaul, independent 3D OSNAP system, DSETTINGS modernization, ALIASEDIT shortcut editor, 22.7x snap tracking performance, GPU backend probe fallback, spatial interaction indexing. +* **Panos** ([@mechpanos](https://github.com/mechpanos)) โ€” Context right-click menus, line endpoint snap lookup. +* **Matus Mastena** ([@dasmatus](https://github.com/dasmatus)) โ€” Fusion Black & White themes, UCS Face and UCS Object alignment, sketch mode, background colour wheel, Nix flake build. +* **Marcel** ([@MarcelP102](https://github.com/MarcelP102)) โ€” LWPOLYLINE radius grip editing, parallel segment offset grips, arc geometry preservation on move, shared unit formatters. +* **kevin9327** ([@kevin9327](https://github.com/kevin9327)) โ€” STEP export with large coordinates, OBJ normal synthesis, dimension rotation/mirror off-world Z, CTB lineweight precision. +* **Huan Le** ([@huaninstratech](https://github.com/huaninstratech)) โ€” OLE2FRAME image embedding, PLOTSTYLE wire-fill depth, locked/cloud file resilience, signing waterfall, block text wipeout depth. +* **Gianluca Fiore** ([@gianlucafiore](https://github.com/gianlucafiore)) โ€” Hatch boundary loop precision. +* **yztink** ([@yztink](https://github.com/yztink)) โ€” Polyline intersection snapping. +* **Felix Riestra** ([@felixriestra](https://github.com/felixriestra)) โ€” macOS DMG packaging shortcuts. +* **Helen Kwok** ([@helenkwok](https://github.com/helenkwok)) โ€” Web open-from-bytes support, WASM build fixes, automation property validation. +* **ysdirector** ([@YsDirector](https://github.com/YsDirector)) โ€” Control client protocol hardening. +* **Steve King** ([@steveking-gh](https://github.com/steveking-gh)) โ€” Extrude typed height commit from dynamic input on Enter. +* **Guy Wydouw** ([@guywydouw](https://github.com/guywydouw)) โ€” Layer invalidation for imported model space. +* **codallos** ([@codallos](https://github.com/codallos)) โ€” Diameter dimension grip and text alignment. diff --git a/changelogs/v2026.39.md b/changelogs/v2026.39.md new file mode 100644 index 000000000..e3d938262 --- /dev/null +++ b/changelogs/v2026.39.md @@ -0,0 +1,97 @@ +# OpenCADStudio - Release v2026.39 (2026-09-27) + +> **Summary:** Milestone release featuring the initial Python Host Model plugin architecture, full dimensional and geometric parametric constraint parity (Linear, Aligned, Angular, Radial, Diameter), interactive Block Definition (`BLOCK`/`BMAKE`) and Write Block (`WBLOCK`) dialogs, command-line autocomplete execution on Enter, individual block attribute grips, the `XCLIP` boundary clipping tool, redesigned PDF/XREF underlay integration panels, interactive plugin command previews, trackpad gesture navigation (pan and pinch-to-zoom), major rendering & tessellation performance optimizations, and robust metric drawing template defaults. + +--- + +### ๐ŸŒŸ Notable Release Notes + +* **Python Host Model & Plugin Architecture:** Introduced the foundational Python plugin execution model, embedding an isolated Python runtime host with CAD entity data structures, automation APIs, and lifecycle bindings. *(Contributed by [@felixriestra](https://github.com/felixriestra))* +* **Block Creation & WBLOCK Dialogs:** Implemented interactive Block Definition dialogs (`BLOCK`/`BMAKE`) with on-screen entity sequencing, base point pickers, unit settings, and the standalone `WBLOCK` dialog for writing blocks to external files. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Command-Line Autocomplete & Architecture Modularization:** Pressing Enter now immediately executes the highlighted autocomplete pre-selection. Completely refactored the 11,500-line monolithic command driver into clean, decoupled domain modules. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Parametric Constraints Parity:** Delivered complete geometric and dimensional constraint toolsets including Linear, Aligned, Angular, Radial, and Diameter constraints, constraint conversion (`DELCONSTRAINT`), and dynamic constraint bar display. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Individual Block Attribute Grips & Tracking:** Added individual grip markers on each block attribute definition and dual-vector intersection locks during object tracking (`OTRACK`). *(Contributed by Claude)* +* **Tessellation & Pipeline Performance:** Optimized polyline arc tessellation sampling, bypassed wide fills on thin polylines, memoized constraint glyphs behind epochs, capped selection grips at 4096, and eliminated redundant wire distance calculations. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Boundary Clipping (`XCLIP`):** Implemented the `XCLIP` command, allowing users to define polygonal and rectangular clipping boundaries for external references and block inserts. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Trackpad Navigation Gestures:** Added native trackpad gesture recognition supporting smooth two-finger panning and pinch-to-zoom. *(Contributed by [@swit33](https://github.com/swit33))* +* **PDF & XREF Attachment Parity:** Redesigned the PDF and XREF attachment dialogs with live scale/rotation previews, layer visibility overrides, and context ribbon tabs. *(Contributed by [@ramox81](https://github.com/ramox81))* + +--- + +### ๐Ÿš€ New Features + +#### Blocks & Definitions +* **Block Definition Dialog (`BLOCK`/`BMAKE`):** Added the standard Block Definition dialog with on-screen sequencing, base point coordinate pickers, retain/convert/delete actions, and unit settings. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Write Block Dialog (`WBLOCK`):** Added the `WBLOCK` dialog allowing users to export objects, entire drawings, or existing blocks to separate files. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Individual Block Attribute Grips:** Each visible block attribute now displays its own grip marker for independent position and orientation adjustments. *(Contributed by Claude)* +* **Shared Dialog Button & Form Radio Builders:** Migrated dialogs across the application to consistent shared UI builders with theme-aware styling. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Block Window Localization:** Added multi-language localized strings for block creation dialogs and measurement units. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +#### Command Line & Drafting +* **Autocomplete Execution on Enter:** Pressing Enter with an active autocomplete pre-selection now executes the highlighted command instantly. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Dual-Vector OTRACK Intersection Locks:** Visual object tracking draws both projection vectors simultaneously at ray intersections. *(Contributed by Claude)* +* **XLINE and RAY Direction Grip Stretch:** Moving the directional pass-through grip on infinite lines and rays now applies stretch semantics rather than displacing the base origin. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Metric Drawing Defaults:** Initialized new drawings using standardized ISO metric template defaults (meters/millimeters) and proper dimension styles. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Page Setup Import (`PSETUPIN`):** Supported importing named page setups across drawings via JSON and drawing database exchanges. *(Contributed by [@lalomalvi](https://github.com/lalomalvi))* +* **Raster Image Pixel Dimensions:** Displayed exact pixel width and height metadata for attached raster image references. *(Contributed by [@ramox81](https://github.com/ramox81))* + +#### Plugin System Extensions +* **Interactive Plugin Previews:** Plugins can now emit transient graphic preview entities during active user interaction steps. *(Contributed by [@yztink](https://github.com/yztink))* +* **Command Execution from Plugins:** Added `run_command` capability enabling plugins to invoke built-in CAD commands programmatically. *(Contributed by [@yztink](https://github.com/yztink))* +* **Plugin Layer Property Access:** Exposed granular layer table read and write APIs to plugin scripts. *(Contributed by [@yztink](https://github.com/yztink))* +* **Repeatable Plugin Commands:** Enabled repeating the last-executed plugin command upon pressing Enter or Space. *(Contributed by [@yztink](https://github.com/yztink))* + +#### Performance & Architecture +* **Polyline Arc Tessellation Performance:** Optimized polyline arc sampling and eliminated expensive wide fill mesh generation for zero-width/thin polylines. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Analytical Instancing & Wire Partitioning:** Streamlined render environment resolution, skipped wire distance calculations for solid lines, and optimized handle parsing. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Selection Grip Cap & Epoch Memoization:** Capped total displayed selection grips at 4096 and memoized constraint-glyph layout positions behind `constraints_epoch`. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Statusbar Epoch Caching:** Cached layout and scale derived properties behind change epochs for smooth frame pacing. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Driver & Kernel Modularization:** Decomposed monolithic 11,500-line command driver into domain modules, extracted reusable clipboard and matchprop kernels, and unified table/block merge copies. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +--- + +### ๐Ÿ› Bug Fixes + +#### Geometry, Snapping & Boundary Detection +* **Plan-View Trim Segment Skipping:** Skipped zero plan-view polyline segments in `TRIM` to prevent clipping failures. *(Contributed by Claude)* +* **Attribute Filter Refactoring:** Streamlined attribute filter logic in insert entity handlers. *(Contributed by [@dasmatus](https://github.com/dasmatus))* +* **Boundary Detection for Infinite Rays/XLINEs:** Clipped unbounded sources, rays, and XLINEs to finite view extents during face detection and associative hatch refreshes. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Units Settings in DIST Output:** Enforced user precision and drawing units settings on `DIST` command readout. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Property Operator Snapshot Leak:** Converted `chprop` choices to standardized property ops, eliminating a CTB memory snapshot leak. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Direct Distance Entry without Dynamic Input:** Ensured typed direct distances commit accurately along polar tracking angles when dynamic input is disabled. *(Contributed by [@yztink](https://github.com/yztink))* +* **3D Shell Command Crash:** Resolved an edge-case crash when selecting complex solid shells in the `SHELL` command. *(Contributed by [@ramox81](https://github.com/ramox81))* + +#### Viewport & Plotting +* **Viewport Wide Polyline Plot Widths:** Corrected stroke width scaling for wide polyline segments plotted inside scaled viewports. *(Contributed by [@Chrisyeh1983](https://github.com/Chrisyeh1983))* +* **Dimension Arrowhead Export:** Fixed missing and clipped arrowheads during vector dimension export. *(Contributed by [@ThaiLe-gt](https://github.com/ThaiLe-gt))* +* **Plot Preview Test Isolation:** Isolated plot preview tests from host environment display server dependencies. *(Contributed by [@lalomalvi](https://github.com/lalomalvi))* +* **Text Fallback Empty Outlines:** Prevented empty or missing font outlines from generating zero-area bounding artifacts. *(Contributed by [@swit33](https://github.com/swit33))* +* **ViewCube Visibility Toggle:** Fixed ViewCube rendering states to honor viewport visual style visibility toggles. *(Contributed by [@yztink](https://github.com/yztink))* + +#### UI, Automation & Platform +* **Command Line Duplicate Paste:** Prevented duplicate text insertion when pasting clipboard text into the command-line interface. *(Contributed by [@yztink](https://github.com/yztink))* +* **Missing Font Prompt Hardening:** Prevented recursive dialog prompts when encountering drawings referencing multiple missing SHX fonts. *(Contributed by [@lalomalvi](https://github.com/lalomalvi))* +* **Automation Task Dispatch:** Fixed task dispatching to prevent orphan task registrations in automation pipelines. *(Contributed by [@helenkwok](https://github.com/helenkwok))* +* **Batch Line Tail Trimming:** Corrected tail trimming issues during batch geometry creation. *(Contributed by [@buyun14](https://github.com/buyun14))* +* **API Documentation & Specs:** Documented native and web automation build targets and API endpoints. *(Contributed by [@helenkwok](https://github.com/helenkwok), [@buyun14](https://github.com/buyun14), and [@huaninstratech](https://github.com/huaninstratech))* + +--- + +### ๐Ÿ‘ฅ Contributors + +Huge thanks to all contributors to `v2026.39`: + +* **Hakan Seven** ([@HakanSeven12](https://github.com/HakanSeven12)) โ€” Upstream geometry kernel and codec synchronization, core application architecture. +* **Felix Riestra** ([@felixriestra](https://github.com/felixriestra)) โ€” Python Host Model plugin architecture, Python C-bindings and runtime lifecycle. +* **ramox81** ([@ramox81](https://github.com/ramox81)) โ€” Parametric constraints (Linear, Aligned, Angular, Radial), `XCLIP`, PDF/XREF underlays, metric defaults. +* **Karim Jerbi** ([@KarimJerbi](https://github.com/KarimJerbi)) โ€” Block definition dialogs (BLOCK/BMAKE/WBLOCK), Enter autocomplete execution, command driver modularization, polyline arc tessellation & rendering pipeline performance, grip stretch for XLINE/RAY, statusbar & glyph caching. +* **Claude** (Claude Opus / Sonnet) โ€” Block attribute individual grips, OTRACK dual-vector intersection locks, plan-view trim segment filtering. +* **yztink** ([@yztink](https://github.com/yztink)) โ€” Plugin interactive previews, plugin command execution, direct distance entry, command line paste fix. +* **Chris Yeh** ([@Chrisyeh1983](https://github.com/Chrisyeh1983)) โ€” Viewport wide polyline plot stroke width fixes. +* **Alexey Bagno** ([@swit33](https://github.com/swit33)) โ€” Trackpad pan and pinch gestures, text font fallback outlines. +* **Luis Martinez** ([@lalomalvi](https://github.com/lalomalvi)) โ€” `PSETUPIN` page setup import, missing font prompt hardening, plot preview test isolation. +* **Matus Mastena** ([@dasmatus](https://github.com/dasmatus)) โ€” Attribute filtering logic refactoring. +* **Thai Le** ([@ThaiLe-gt](https://github.com/ThaiLe-gt)) โ€” Dimension arrowhead vector export fix. +* **Helen Kwok** ([@helenkwok](https://github.com/helenkwok)) โ€” Automation task dispatching and web build documentation. +* **buyun14** ([@buyun14](https://github.com/buyun14)) โ€” Batch line geometry trimming and automation docs. +* **Huan Le** ([@huaninstratech](https://github.com/huaninstratech)) โ€” Automation API specification and build scripts. diff --git a/changelogs/v2026.40.md b/changelogs/v2026.40.md new file mode 100644 index 000000000..5a9cad117 --- /dev/null +++ b/changelogs/v2026.40.md @@ -0,0 +1,93 @@ +# OpenCADStudio - Release v2026.40 / v2026.40.1 (2026-10-05) + +> **Summary:** Unified release notes for release week 2026.40. Release `v2026.40` was pulled shortly after release and replaced by `v2026.40.1`. This document merges the complete feature waveโ€”introducing the interactive `FIELD` evaluation command and dialog, `ATTDEF` attribute definition workflow parity, a redesigned tabbed Blocks palette with drag-and-drop insertion, DWF/DGN and point cloud underlays, dynamic input control (`DYNMODE`), script engine upgrades (`.scr`), TrueType embedded font PDF export, and smooth exponential zoom navigationโ€”along with the hotfixes that triggered the `v2026.40.1` replacement (instant GPU layer visibility toggling without CPU re-tessellation, 3D cylinder height input validation, load-time `BLOCK`/`ENDBLK` sentinel recovery, and MCP server request lifecycle & cancellation hardening). + +--- + +### ๐ŸŒŸ Notable Release Notes + +* **Instant Layer Visibility Toggling:** Optimized layer show/hide operations to toggle visibility states directly on GPU batches without triggering CPU re-tessellation passes. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **`FIELD` Evaluation System & Dialog:** Introduced the full `FIELD` command and UI dialog, supporting real-time text/MText field evaluation, table formula integration, block placeholders, attribute fields on insert, and automatic plot-time updates. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Attribute Definition (`ATTDEF`) Parity:** Added the complete `ATTDEF` and `-ATTDEF` dialogs, tag visibility management, entity property grid synchronization, and ribbon block definition integration. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Blocks Palette Overhaul:** Redesigned the Blocks palette with categorized tabs (Current Drawing, Recent, Favorites), insertion scale/rotation options, drag-and-drop canvas insertion, and `-INSERT` CLI parity. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **MCP Multiscale Pyramidal Tiling Protocol (DeepZoom):** Implemented multiscale pyramidal tiling protocol for MCP vision tools (`src/app/control/vision.rs`, `src/mcp.rs`), exposing DeepZoom manifests (`cad://session/{session_id}/pyramid/manifest.json`), on-demand cached multi-resolution PNG/JSON tiles (`tile/{level}/{x}/{y}`), and `tile`/`pyramid_manifest` parameters in `ocs_capture` to allow vision agents to navigate dense drawings without exceeding token limits. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Set-of-Marks (SoM) Visual Grounding & Vision ID Stamp:** Added high-contrast numbered visual ID badge stamping (`draw_som_badge`) on entity centroids in viewport captures, bidirectional entity grounding manifests mapping stamps to handles, layers, types, and world coordinates, regional camera framing, and 2-point raster calibration. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Visual Differential Streaming & Standardized Spatial Metadata:** Added pixel-level visual diff engine (`compute_visual_diff`) with dirty region cropping (`diff_mode: crop`), WCS CRS alignment (`_spatial`), and forward/inverse affine coordinate matrices to dramatically reduce vision token consumption. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Drawing Load Sentinel Recovery:** Materialized missing `BLOCK` and `ENDBLK` sentinel entities on damaged drawings during document load, preventing crashes and corrupt database states. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Underlays & Raster Improvements:** Native support for attaching DWF and DGN underlays (`UNDERLAY`), point cloud attachments (`POINTCLOUD`), image transparency controls, and image frame toggling (`IMAGEFRAME`). *(Contributed by [@ramox81](https://github.com/ramox81))* +* **PDF TrueType Font Embedding:** Enhanced PDF export to embed subset TrueType fonts directly into PDF output, ensuring crisp vector text rendering without font substitution artifacts. *(Contributed by [@Chrisyeh1983](https://github.com/Chrisyeh1983))* +* **MCP Server Lifecycle Hardening:** Enforced accurate time-to-live policies (60 seconds for GUI resources, 1 hour for tools), request cancellation (`tasks/cancel`), and explicit request/document tracking on GUI capture requests. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +--- + +### ๐Ÿš€ New Features + +#### Drafting & Commands +* **Signed Offset & Live Drag Preview:** `OFFSET` opens with live interactive geometry preview on preselected objects and supports signed typed distance entries. *(Contributed by [@CiberNin](https://github.com/CiberNin))* +* **Ribbon Active Command Indicator:** Active drafting commands highlight their corresponding buttons on the ribbon interface. *(Contributed by [@CiberNin](https://github.com/CiberNin))* +* **Dynamic Input System Variable (`DYNMODE`):** Added the `DYNMODE` system variable and command for granular toggling of dynamic input modes. *(Contributed by [@yztink](https://github.com/yztink))* +* **Script File Engine (`.scr`):** Supported standard `.scr` CAD scripts across single-line, multi-line, and prefixed command syntax. *(Contributed by [@yztink](https://github.com/yztink))* +* **Fluid Tangent Snapping:** Supported continuous tangent snaps across circular arcs and ellipses. *(Contributed by [@yztink](https://github.com/yztink))* +* **Polyline Self-Intersection Trimming:** Added self-intersection snapping and trimming for complex polyline loops. *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* + +#### Navigation & Zoom +* **Exponential Zoom & Tool Cursors:** Added smooth exponential mouse-wheel zoom, contextual navigation cursors, and proper canvas framing in `ZOOM EXTENTS`. *(Contributed by [@mechpanos](https://github.com/mechpanos))* +* **Zoom Factor Range Extension:** Expanded the `ZOOMFACTOR` upper range limit to 500. *(Contributed by [@F4brizi](https://github.com/F4brizi) and [@mechpanos](https://github.com/mechpanos))* + +#### MCP & Vision Automation +* **Multiscale Pyramidal Tiling (DeepZoom):** Implemented multiscale pyramidal tiling protocol (`src/app/control/vision.rs`, `src/mcp.rs`), exposing DeepZoom manifests (`cad://session/{session_id}/pyramid/manifest.json`), on-demand cached multi-resolution PNG/JSON tiles (`tile/{level}/{x}/{y}`), and `tile`/`pyramid_manifest` parameters in `ocs_capture`. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Set-of-Marks (SoM) Grounding & Vision ID Stamps:** Stamped high-contrast numbered visual ID badges (`draw_som_badge`) on entity centroids in viewport captures, paired with bidirectional grounding manifests (`annotations` / `visible_entities`) mapping tags to handles, layers, types, and world coordinates. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Visual Differential Streaming & Spatial Metadata:** Standardized WCS spatial metadata under `_spatial` with affine projection matrices and visual differential engine (`compute_visual_diff`) supporting dirty region cropping (`diff_mode: crop`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **2-Point Image Calibration & Regional Framing:** Supported regional camera framing (`bounds`) and 2-point raster image calibration in `embed_image` to calculate scale, origin, and world dimensions from pixel-to-world point pairs. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **4-Pillar Auto-Sync & Declarative Op Table:** Implemented 4-pillar auto-sync, dynamic introspection, and declarative MCP request dispatching. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Text Audit, Spell-Check & Replacement:** Added `text_audit` spell-checking with native OS spellcheckers and dry-run replacement simulation, alongside multiline `text_search` and `text_replace`. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Lifecycle & Cancellation Management:** Supported `tasks/cancel` cancellation handling, public error codes, and schema-sync opt-out. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Inspector Conformance Probing:** Added Inspector CLI conformance tests and descriptor lifetime tracking for newly created GUI elements. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +#### Diagnostics, Localization & CI +* **Desktop Crash Diagnostics:** Added automated desktop crash logging to capture and persist diagnostic trace logs upon unhandled exceptions. *(Contributed by [@mechpanos](https://github.com/mechpanos))* +* **Italian Translation Updates:** Standardized CAD terminology throughout the Italian localization (Join, Plot, Layer, Hatch). *(Contributed by [@abccorsicad](https://github.com/abccorsicad))* +* **Hotfix Release Workflow:** Added automated CI support for deploying patch releases (`v..`) on top of weekly releases. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* + +--- + +### ๐Ÿ› Bug Fixes + +#### Geometry, Snapping & Modeling +* **3D Cylinder & Primitive Creation:** Respected typed height inputs during cylinder and basic 3D primitive creation. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Point Snap Node Constraint:** Constrained `POINT` entities to snap exclusively as `Node` object snaps. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Hatch Association on Trim:** Preserved hatch boundary associations when `TRIM` splits a boundary curve into multiple segments. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Polyline Joint Mitering:** Mitered joints accurately on wide polyline band corners. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **3D Solid History Graph:** Removed unnecessary history generation on edge fillets/chamfers of plain solids and corrected undo evaluation graphs. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **ACIS Transform Normalization:** Eliminated duplicate transforms on center-framed 3D boxes and wedges following ACIS geometry lifts. *(Contributed by [@ramox81](https://github.com/ramox81))* + +#### UI, Layers & System +* **Novel Layer GUI Refresh:** Refreshed ribbon layers and the Layer panel immediately when newly created entities register new layers. *(Contributed by [@yztink](https://github.com/yztink))* +* **Erased Raster Image Placeholders:** Cleared placeholder bounding text cleanly when a broken raster image reference is deleted. *(Contributed by [@mechpanos](https://github.com/mechpanos))* +* **Windows Printer Properties Freeze:** Resolved UI thread lockups during Windows printer properties and DEVMODE size queries. *(Contributed by [@mechpanos](https://github.com/mechpanos))* +* **R14 Layer Plot Flags:** Corrected layer plot flag parsing on legacy R14 drawings. *(Contributed by [@YsDirector](https://github.com/YsDirector))* +* **Chinese Font Hinting (KaiU):** Fixed glyph rendering and hinting distortions for KaiU fonts. *(Contributed by [@Chrisyeh1983](https://github.com/Chrisyeh1983))* + +#### XREFs, Plotting & Web +* **XREF Dimension Anonymous Blocks:** Imported missing anonymous blocks referenced by dimension entities across XREF boundaries. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **PDF Export Window Clipping:** Correctly clipped and excluded entities falling outside the designated export window during PDF plot. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Web Build Compilation:** Gated native-only desktop code out of the wasm32 build target. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Save Verification:** Verified baked-view consistency in `save_verified` automation checks. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +--- + +### ๐Ÿ‘ฅ Contributors + +Huge thanks to all contributors to `v2026.40 / v2026.40.1`: + +* **Hakan Seven** ([@HakanSeven12](https://github.com/HakanSeven12)) โ€” Upstream geometry kernel/codec syncs, instant layer visibility performance, PDF export clipping, XREF anonymous block import, release workflow fixes. +* **ramox81** ([@ramox81](https://github.com/ramox81)) โ€” `FIELD` evaluation system, `ATTDEF` dialogs & parity, Blocks palette, DWF/DGN underlays, point clouds, solid history graph. +* **Karim Jerbi** ([@KarimJerbi](https://github.com/KarimJerbi)) โ€” Multiscale pyramidal tiling protocol (DeepZoom), Set-of-Marks visual ID badge stamping, visual differential streaming, 4-pillar auto-sync & declarative op table, text audit spell-checking, 3D cylinder height enforcement, load-time BLOCK sentinel recovery, MCP server lifecycle & cancellation hardening, save verification. +* **yztink** ([@yztink](https://github.com/yztink)) โ€” `DYNMODE` system variable, `.scr` script engine, fluid tangent arc/ellipse snaps, layer GUI refresh. +* **Panos** ([@mechpanos](https://github.com/mechpanos)) โ€” Exponential zoom, `ZOOM EXTENTS` framing, desktop crash logging, Windows printer freeze fixes. +* **Christopher Overbeck** ([@CiberNin](https://github.com/CiberNin)) โ€” Signed offset with live preview, ribbon active command highlighting. +* **Chris Yeh** ([@Chrisyeh1983](https://github.com/Chrisyeh1983)) โ€” PDF TrueType font subset embedding, KaiU font hinting fixes. +* **Gianluca Fiore** ([@gianlucafiore](https://github.com/gianlucafiore)) โ€” Polyline self-intersection snapping and trimming. +* **F4brizi** ([@F4brizi](https://github.com/F4brizi)) โ€” `ZOOMFACTOR` limit expansion. +* **abccorsicad** ([@abccorsicad](https://github.com/abccorsicad)) โ€” Italian CAD terminology localizations. +* **ysdirector** ([@YsDirector](https://github.com/YsDirector)) โ€” R14 layer plot flag compatibility. diff --git a/changelogs/v2026.41.md b/changelogs/v2026.41.md new file mode 100644 index 000000000..a9b78eaac --- /dev/null +++ b/changelogs/v2026.41.md @@ -0,0 +1,103 @@ +# OpenCADStudio - Release v2026.41 (2026-10-11) + +> **Summary:** Major feature milestone introducing the Sheet Set Manager (`SHEETSET`) with `.dst` project support; the automated entity `COUNT` command and palette with dynamic table generation; network-integrated associative swept surfaces (`SWEEP`); complete right-to-left (RTL) text layout in `MTEXT`; independent dimension style per-end arrowheads (`DIMBLK1`/`DIMBLK2`); table cell style overrides; GPU hardware selection options (Metal/OpenGL); MCP differential capture payload optimizations; and extensive drafting, plotting, spatial indexing, and memory fixes. + +--- + +### ๐ŸŒŸ Notable Release Notes + +* **Sheet Set Manager (`SHEETSET`):** Introduced a complete Sheet Set Manager palette, `.dst` project creation wizard, cross-drawing sheet fields, drawing lock-file coordination, and background sheet opening. *(Contributed by [@ramox81](https://github.com/ramox81) and [@HakanSeven12](https://github.com/HakanSeven12))* +* **Automated Entity Counting (`COUNT`):** Added the `COUNT` feature and palette, providing visual block counting across model space or designated polygon boundaries, collision/overlap diagnostics, and live evaluated count table insertion. *(Contributed by [@ramox81](https://github.com/ramox81) and [@HakanSeven12](https://github.com/HakanSeven12))* +* **Associative Swept Surfaces & 3D Modeling:** Added network-tracked associative swept surfaces (`SWEEP`) with path-relative base points, miter corner history, B-rep `MASSPROP`, and curved offset/fillets in the geometry kernel. *(Contributed by [@ramox81](https://github.com/ramox81) and [@HakanSeven12](https://github.com/HakanSeven12))* +* **Advanced Typography & Table Overrides:** Fully compliant right-to-left (RTL) text rendering in `MTEXT`, exact evaluation bounding boxes for shaped/rotated text fields, and in-place cell style overrides with automated formula recomputation. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Dimension Style Independent Arrowheads (`DIMBLK1`/`DIMBLK2`):** Supported independent arrowhead styles for start and end leaders with real-time preview rendering and style synchronization. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **GPU Platform Routing & Hardware Info:** Added hardware-aware GPU backend routing (Metal on macOS, automated OpenGL fallback for legacy hardware), hardware information in About dialog, and dedicated Graphics options tab. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +--- + +### ๐Ÿš€ New Features + +#### Management & Palettes +* **Sheet Set Manager Palette & Wizard:** Added Sheet Set Manager palette, `.dst` file reload, import sheet statuses, view categories, and Place-on-Sheet actions. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Count Palette & Table Generator:** Added interactive block counting, invalid area inspection dialog, and live count table generation with field references. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Quick Searchable Layer Palette:** Added fast search and filter command for drawing layers. *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* + +#### 3D Modeling & Surfaces +* **Associative Sweep:** Supported surface sweep associativity within the drawing dependency network, honoring custom path alignment and miter corners. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **B-rep Mass Properties & Kernel Operations:** Added `MASSPROP` mass evaluation from solid B-rep, curved `OFFSET`/`FILLET`, and sphere-cylinder boolean operations. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **UCS Face Alignment:** Aligned UCS Face origin and axes to the nearest vertex and edge with solid face raycasting. *(Contributed by [@yztink](https://github.com/yztink))* + +#### Text, Fields & Tables +* **RTL Text Layout:** Added right-to-left word layout, leading space preservation, and mixed-font character advances. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Table Cell Overrides & Formulas:** Supported per-cell style overrides from either layout and automatic formula recomputation after editing. *(Contributed by [@ramox81](https://github.com/ramox81))* +* **Rotated & Shaped Text Field Boxes:** Accurate evaluation boxes, per-field shading backgrounds, and `UPDATEFIELD` traversal reaching nested blocks. *(Contributed by [@ramox81](https://github.com/ramox81))* + +#### UI & Graphics +* **GPU Platform Backends & Hardware Info:** Refined GPU platform selection (Metal on macOS, OpenGL fallback for legacy hardware), hardware info in About dialog, and dedicated Graphics options tab. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **ViewCube UCS Cache:** Cached UCS picker names behind an epoch to accelerate ViewCube orientation refreshes. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Global Interface Scaling:** Added window-wide interface scale setting in Options. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Pickbox-Only Cursor:** Cursor displays pickbox alone when drafting commands prompt for object selection. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Commandline History Retention:** Set `fade time = 0` to keep command-line history permanently visible without fading. *(Contributed by [@yztink](https://github.com/yztink))* +* **Touchpad Pan:** Optional touchpad panning for wheel-scrolling touchpads. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Expanded Ribbon Layer Display:** Ribbon layer dropdown displays full-length layer names. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* + +#### Automation, MCP & Architecture +* **MCP Unchanged Diff Capture Payloads:** Omitted image base64 payloads on visual diff captures when the canvas has no dirty visual changes. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Spatial Index & Navigation Benchmarks:** Added formal QuadTree spatial index construction and viewport navigation benchmarks. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Selection State Refactor:** Decomposed monolithic 31-field `SelectionState` into five modular sub-structs. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **MCP Window & Crossing Selection:** Added window and crossing box selection tools in MCP server. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Lao Localization:** Added full Lao (`lo-LA`) language translation and web font rendering. *(Contributed by [@bounkirdni-2025](https://github.com/bounkirdni-2025))* + +--- + +### ๐Ÿ› Bug Fixes + +#### Dimensions & Annotations +* **Dimension Style Per-End Arrowheads:** Stopped `dimblk1` pick from clobbering `dimblk2` and ensured preview dialog respects independent per-end arrowheads (`50e20a91`, `29392de2`). *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **DimStyle Text Style Sync:** Propagated text style renames automatically into DimStyle `DIMTXSTY`. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **MLeader Attachment:** Fixed text left of multileader landing to correctly attach via its right edge. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* + +#### Layers, Tables & XREFs +* **Layer Message & State Integrity:** Keyed layer messages, renames, and picker states by immutable layer names instead of row indices. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Layer Transparency Undo:** Added missing undo steps for layer transparency changes. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Shared Linetypes Memory Optimization:** Shared linetype table rows via `Arc` instead of per-row cloning. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Table and Multiline Buffer Reload:** Reloaded Table and MLine rendering buffers upon structural changes and validated cell text styles. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Ribbon TableStyle Pick:** Ensured picking table styles in the ribbon writes headers so subsequent inserts inherit the style. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **XREF Row Identity & Empty State:** Carried stable row identity in XREF messages, themed empty states, and clarified silent annotative scale toggles. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Hidden Layer Interaction:** Prevented accidental cursor selection and interaction on hidden layers. *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* + +#### Rendering, Snapping & Drafting +* **Behind-Eye Axis Guard:** Protected `draw_axes` against divide-by-zero behind-the-eye camera projection crashes. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Spatial Index Normalization:** Normalized inverted axis-aligned bounding boxes (AABB) in the QuadTree spatial index. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Idle Redraw Suppression:** Eliminated unnecessary redraw loops when drawing documents lack underlay geometry. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Viewport Statusbar Synchronization:** Synchronized scale pill number, label, and badge strictly to the active viewport. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Properties Palette Asymmetric Scale:** Scoped asymmetric scale opt-outs per document tab. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Circle & Arc Patterned Linetypes:** Correctly preserved patterned linetypes on circles and arc segments. *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* +* **CTB Layout Block Colors:** Preserved CTB block ACI colors and layout plot settings during plotting. *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* +* **Command Previews Over Wipeouts:** Ensured active command previews draw above background wipeout masks. *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* +* **Perpendicular Tracking Distance:** Preserved perpendicular polar tracking after entering typed direct distance values. *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* +* **Paste into Block Editor:** Fixed pasted entities to land directly inside the active Block Editor context. *(Contributed by [@gianlucafiore](https://github.com/gianlucafiore))* +* **Feet-Inch Dynamic Input & Stretch:** Fixed feet/inch notation parsing in dynamic input and preserved preselected entities in `STRETCH`. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* +* **Script Blank Line Repeat:** Blank lines in script files now repeat the last command when outside an active command prompt. *(Contributed by [@yztink](https://github.com/yztink))* +* **`ZOOMFACTOR` Maximum:** Allowed `SETVAR ZOOMFACTOR` to accept values up to 500. *(Contributed by [@F4brizi](https://github.com/F4brizi))* +* **Purge Hang on Unnamed Styles:** Fixed `PURGE ALL` hanging on unnamed shape styles. *(Contributed by [@HakanSeven12](https://github.com/HakanSeven12))* + +#### Platform & Packaging +* **macOS Signed Build Entitlements:** Fixed XML entitlement verification in `build_macos_signed.sh`. *(Contributed by [@AJ-Gonzalez](https://github.com/AJ-Gonzalez))* +* **macOS CLI Arguments & MCP stdio:** Forwarded launcher CLI arguments and preserved MCP stdio streams on macOS. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* +* **Cross-Platform CI:** Added multi-platform testing on Linux, Windows, and macOS with non-blocking lint checks. *(Contributed by [@KarimJerbi](https://github.com/KarimJerbi))* + +--- + +### ๐Ÿ‘ฅ Contributors + +Huge thanks to all contributors to `v2026.41`: + +* **Hakan Seven** ([@HakanSeven12](https://github.com/HakanSeven12)) โ€” Upstream kernel/codec syncs, B-rep mass properties, interface scaling, cursor & drawing fixes. +* **ramox81** ([@ramox81](https://github.com/ramox81)) โ€” Sheet Set Manager, Count feature & tables, associative sweep, RTL text layout. +* **Karim Jerbi** ([@KarimJerbi](https://github.com/KarimJerbi)) โ€” DimStyle per-end arrowheads (`dimblk1`/`dimblk2`), GPU backends & options, ViewCube UCS cache, MCP diff captures, layer state integrity & memory sharing, spatial index normalization, SelectionState modularization, cross-platform CI. +* **gianlucafiore** ([@gianlucafiore](https://github.com/gianlucafiore)) โ€” Searchable layers, patterned linetypes, CTB plot colors, hidden layer interaction, command previews over wipeouts. +* **yztink** ([@yztink](https://github.com/yztink)) โ€” UCS Face alignment, persistent commandline history, script repetition on blank lines. +* **F4brizi** ([@F4brizi](https://github.com/F4brizi)) โ€” `ZOOMFACTOR` limit extension to 500. +* **AJ-Gonzalez** ([@AJ-Gonzalez](https://github.com/AJ-Gonzalez)) โ€” macOS packaging & signed build entitlement checks. +* **Bone NI** ([@bounkirdni-2025](https://github.com/bounkirdni-2025)) โ€” Lao (`lo-LA`) translations and localization. diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index a3252cf25..39491265d 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true } # Pulled in only by the `host` feature, which adds the `opencadcodec`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "3ec70a2", optional = true, features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "e83247d", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -39,7 +39,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # opencadcodec is scanned at build time to generate the embedded type registry. -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "3ec70a2", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "e83247d", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_save/Cargo.toml b/crates/ocs_save/Cargo.toml index 76a45ddb5..ea8407c55 100644 --- a/crates/ocs_save/Cargo.toml +++ b/crates/ocs_save/Cargo.toml @@ -10,6 +10,6 @@ publish = false # exactly one implementation. No UI/app dependencies โ€” only the codec + serde. [dependencies] -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "3ec70a2", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "e83247d", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 54819e69b..be52f3754 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "3ec70a2", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "e83247d", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/docs/automation/guides/workflow.md b/docs/automation/guides/workflow.md index 333a14c40..0314f0a7e 100644 --- a/docs/automation/guides/workflow.md +++ b/docs/automation/guides/workflow.md @@ -4,6 +4,25 @@ Call ocs_sessions, then pass its session_id as ocs_session_id to ocs_read, ocs_e Use commands with parameters.name for a command manifest. Use batch when several steps are known, and request changed_entities when resulting geometry is needed. +Before creating an unfamiliar entity, use `ocs_read` with `op: entity_schema` and +`parameters.type` (for example, `Circle`) to get its creation fields, required +values, units, defaults and example. Omit type for the catalog. Creation schemas +describe the inputs to `entities_create`; `record_schema` describes stored records. + +Use `ocs_read` with `op: validate` and `parameters.request` containing the proposed +execute request to preflight it without running it. These local operations can +omit `ocs_session_id`; no GUI is launched or contacted. `request_id` may be omitted +for preflight, but is required for actual execution. Static validation checks +operation parameters, nested entity and inline data definitions, and batch +bindings. It uses the real entity constructor for geometry checks. Results +include `valid`, `executed: false`, warnings, unresolved bindings and checks that +still require execution. Live revisions, handles, styles, file access and command +behavior are not certified by static preflight. Batches are not simulated: later +bindings are fully validated when their values become available during execution. +Invalid requests return a tool error with a JSON Pointer identifying the bad +field where available. Unknown top-level operation parameters remain warnings; +unknown entity fields and invalid nested data are rejected before dispatch. + For interactive work, call start and follow state.command.accepts, options and input_example. To have the person at the screen pick entities for you, call user_select and keep polling until it completes; running means they are still picking. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z. After a timeout, query the existing operation and never replay a mutation with a new request_id. waiting_input and running are not completion. Let OCS and its geometry kernel calculate geometry; use query near, contains_point and intersections for exact relationships. diff --git a/docs/automation/mcp_smoke.py b/docs/automation/mcp_smoke.py index af13e6f17..64ff3cf93 100644 --- a/docs/automation/mcp_smoke.py +++ b/docs/automation/mcp_smoke.py @@ -135,6 +135,35 @@ def legacy(server: Path) -> None: assert not guide_tool["result"].get("isError"), guide_tool assert guide_tool["result"]["structuredContent"]["ok"] is True assert "resource_link" in guide_tool["result"]["structuredContent"]["contents"][0]["text"] + entity_schema = request(process, { + "jsonrpc": "2.0", "id": "entity-schema", "method": "tools/call", + "params": {"name": "ocs_read", "arguments": { + "op": "entity_schema", "parameters": {"type": "Circle"}, + }}, + }) + assert not entity_schema["result"].get("isError"), entity_schema + contract = entity_schema["result"]["structuredContent"]["schema"] + assert contract["required"] == ["type", "center", "radius"] + validated = request(process, { + "jsonrpc": "2.0", "id": "preflight", "method": "tools/call", + "params": {"name": "ocs_read", "arguments": { + "op": "validate", "parameters": {"request": { + "op": "entities_create", "entities": contract["examples"], + }}, + }}, + }) + assert validated["result"]["structuredContent"]["valid"] is True + assert validated["result"]["structuredContent"]["executed"] is False + rejected = request(process, { + "jsonrpc": "2.0", "id": "bad-preflight", "method": "tools/call", + "params": {"name": "ocs_read", "arguments": { + "op": "validate", "parameters": {"request": { + "op": "entities_create", "entities": [{"type": "Circle", "center": ["bad", 0], "radius": 5}], + }}, + }}, + }) + assert rejected["result"]["isError"] is True + assert "/entities/0/center/0" in rejected["result"]["content"][0]["text"] close(process) diff --git a/docs/automation/web.md b/docs/automation/web.md index a5944d36e..0bebaf578 100644 --- a/docs/automation/web.md +++ b/docs/automation/web.md @@ -58,6 +58,49 @@ earlier drawing is still opening the request is answered with `busy`; retry it o rather than a disk, so use `data_base64` for a file the page has not seen. `data_base64` is rejected on native builds; use `path` there. +## Getting the drawing back + +`save` serializes the active drawing and returns it in the reply, without a download: + +```json +{"op":"save","name":"plan.dwg","target_format":"dwg","target_version":"AC1032"} +``` + +The reply carries `name`, `target_format`, `target_version`, `dropped_on_save`, `bytes` +and `data_base64`, and the tab is marked clean. `target_format` and `target_version` +default to the drawing's own; a conversion that would drop unsupported records needs +`allow_lossy: true`, as on native. + +## Supplying fonts + +The web build has no font folder. A text style whose TrueType or OpenType file is not one +of the bundled Noto subsets falls back to them. The host page can hand the file over: + +```json +{"op":"font_register","file_name":"Brand-Regular.ttf","data_base64":"โ€ฆ"} +``` + +`file_name` is the name the drawing's text styles use; it is matched case-insensitively and +without a directory, as for installed fonts. The reply carries the `family` inside the file +and the number of `faces` it added. Open drawings lay their text out again. The font stays +registered for the session and is never written into saved files. Bytes without a readable +face fail with `font_invalid`; a missing `file_name` or empty or invalid base64 with +`invalid_request`. + +## Receiving saved files in the host page + +A page that embeds the app can take every saved or exported file itself instead of a +browser download by defining a function on the app's window before saving: + +```js +window.openCadStudioSaveFile = (name, bytes) => { + // name: file name, bytes: Uint8Array +}; +``` + +While it is defined, Save, Save As, the save-before-close prompt and the exports call it +with the file name and the bytes, and no file picker opens. Without it nothing changes. + ## Startup dialogs A dialog can be open when the page is ready. The donation prompt appears once per diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index c601eb213..451abc9f4 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = ุงู„ุฅุถุงูุงุช ribbon-tab = + .block-editor = ู…ุญุฑุฑ ุงู„ูƒุชู„ .draw = ุฑุณู… .parametric = ุจุงุฑุงู…ุชุฑูŠ .annotate = ุชุนู„ูŠู‚ุงุช ุชูˆุถูŠุญูŠุฉ @@ -98,6 +99,9 @@ start = .no-recent-files = ุณุชุธู‡ุฑ ุงู„ู…ู„ูุงุช ุงู„ุชูŠ ุชูุชุญู‡ุง ู‡ู†ุง. .browser-storage = ุชุฎุฒูŠู† ุงู„ู…ุชุตูุญ .keep-recent-files = ุงู„ุงุญุชูุงุธ ุจุงู„ู…ู„ูุงุช ุงู„ุฃุฎูŠุฑุฉ + .view-list = ู‚ุงุฆู…ุฉ + .view-icons = ุฃูŠู‚ูˆู†ุงุช + .view-preview = ู…ุนุงูŠู†ุงุช ูƒุจูŠุฑุฉ donation = .title = ุทู„ุจ ุชุจุฑุน @@ -167,6 +171,14 @@ modal = .that-object = ุฐู„ูƒ ุงู„ูƒุงุฆู† .those-objects = ุชู„ูƒ ุงู„ูƒุงุฆู†ุงุช + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = ุชู… ู„ุตู‚ ุงู„ุตูˆุฑุฉ "{ $name }": { $width }ร—{ $height } px .image-unsupported = ุชุนุฐู‘ุฑ ููƒ ุชุฑู…ูŠุฒ ุงู„ุตูˆุฑุฉ ุงู„ู…ู„ุตู‚ุฉ "{ $name }". @@ -286,11 +298,73 @@ command-copy = .mode = COPY ุฃุฏุฎู„ ุฎูŠุงุฑ ูˆุถุน ุงู„ู†ุณุฎ [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ ู‡ู†ุง ูู‚ุท (ุฃุฎุถุฑ)ุŒ __ocs_fmt_1__ ููŠ ุงู„ุฑุณู… ุงู„ุขุฎุฑ ูู‚ุท (ุฃุญู…ุฑ)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ู…ู‚ุงุฑู†ุฉ ู…ุน ุฑุณู… + .label-associative-array-created = __ocs_fmt_0__: ุชู… ุฅู†ุดุงุก ู…ุตููˆูุฉ ุชุฑุงุจุทูŠุฉ. + .assoc-array-array-path = ู…ุตููˆูุฉ (ู…ุณุงุฑ) + .assoc-array-array-polar = ู…ุตููˆูุฉ (ู‚ุทุจูŠุฉ) + .assoc-array-array-rectangular = ู…ุตููˆูุฉ (ู…ุณุชุทูŠู„ุฉ) + .assoc-array-item-spacing = ุชุจุงุนุฏ ุงู„ุนู†ุงุตุฑ + .assoc-array-fill-angle = ุฒุงูˆูŠุฉ ุงู„ุชุนุจุฆุฉ + .assoc-array-items = ุงู„ุนู†ุงุตุฑ + .assoc-array-level-spacing = ุชุจุงุนุฏ ุงู„ู…ุณุชูˆูŠุงุช + .assoc-array-levels = ุงู„ู…ุณุชูˆูŠุงุช + .assoc-array-row-spacing = ุชุจุงุนุฏ ุงู„ุตููˆู + .assoc-array-column-spacing = ุชุจุงุนุฏ ุงู„ุฃุนู…ุฏุฉ + .fbxexport-exported-to = FBXEXPORT: ุชู… ุงู„ุชุตุฏูŠุฑ ุฅู„ู‰ "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: ู„ุง ุชูˆุฌุฏ ุจูŠุงู†ุงุช ุดุจูƒุฉ ุซู„ุงุซูŠุฉ ุงู„ุฃุจุนุงุฏ ููŠ ู‡ุฐุง ุงู„ุฑุณู…. + .fbxexport-error = FBXEXPORT: โ€__ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = ุงู„ุชุธู„ูŠู„ุงุช ุงู„ุฌุฏูŠุฏุฉ ุชุชุจุน ุญุฏูˆุฏู‡ุง (HPASSOC) + .options-show-ribbon = ุฅุธู‡ุงุฑ ุงู„ุดุฑูŠุท (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = ุฅุธู‡ุงุฑ ุดุฑูŠุท ุงู„ู‚ูˆุงุฆู… (MENUBAR) + .options-window-elements = ุนู†ุงุตุฑ ุงู„ู†ุงูุฐุฉ + .menu-viewports = ู…ู†ุงูุฐ ุงู„ุนุฑุถ + .menu-view-twist = ุชุฏูˆูŠุฑ ุงู„ุนุฑุถ + .menu-tools = ุฃุฏูˆุงุช + .menu-table-style = ู†ู…ุท ุงู„ุฌุฏูˆู„ + .menu-run-script = ุชุดุบูŠู„ ู†ุต ุจุฑู…ุฌูŠ + .menu-revision-cloud = ุณุญุงุจุฉ ุงู„ู…ุฑุงุฌุนุฉ + .menu-regen-all = ุฅุนุงุฏุฉ ุชูˆู„ูŠุฏ ุงู„ูƒู„ + .menu-redraw = ุฅุนุงุฏุฉ ุงู„ุฑุณู… + .menu-rectangle = ู…ุณุชุทูŠู„ + .menu-quick-dimension = ุจูุนุฏ ุณุฑูŠุน + .menu-publish = ู†ุดุฑ + .menu-paste-special = ู„ุตู‚ ุฎุงุต + .menu-parametric = ุจุงุฑุงู…ุชุฑูŠ + .menu-page-setup-manager = ู…ุฏูŠุฑ ุฅุนุฏุงุฏ ุงู„ุตูุญุฉ + .menu-orbit = ู…ุฏุงุฑ + .menu-new-ucs = ู†ุธุงู… ุฅุญุฏุงุซูŠุงุช ู…ุณุชุฎุฏู… ุฌุฏูŠุฏ + .menu-named-views = ุงู„ุนุฑูˆุถ ุงู„ู…ุณู…ุงุฉ + .menu-multiline-text = ู†ุต ู…ุชุนุฏุฏ ุงู„ุฃุณุทุฑ + .menu-multiline-style = ู†ู…ุท ุงู„ุฎุท ุงู„ู…ุชุนุฏุฏ ุงู„ู…ุชูˆุงุฒูŠ + .menu-multiline = ุฎุท ู…ุชุนุฏุฏ ู…ุชูˆุงุฒูŠ + .menu-multileader-style = ู†ู…ุท ุงู„ู…ูˆุฌู‡ ุงู„ู…ุชุนุฏุฏ + .menu-match-properties = ู…ุทุงุจู‚ุฉ ุงู„ุฎุตุงุฆุต + .menu-lengthen = ุฅุทุงู„ุฉ + .menu-jogged = ู…ุชุนุฑุฌ + .menu-id-point = ุชุนุฑูŠู ู†ู‚ุทุฉ + .menu-explode-text = ุชูุฌูŠุฑ ุงู„ู†ุต + .menu-edit-spline = ุชุญุฑูŠุฑ ุงู„ู…ู†ุญู†ู‰ + .menu-edit-polyline = ุชุญุฑูŠุฑ ุงู„ุฎุท ุงู„ู…ุชุนุฏุฏ + .menu-edit-hatch = ุชุญุฑูŠุฑ ุงู„ุชุธู„ูŠู„ + .menu-drawing-limits = ุญุฏูˆุฏ ุงู„ุฑุณู… + .menu-donut = ุญู„ู‚ุฉ + .menu-dimension-style = ู†ู…ุท ุงู„ุฃุจุนุงุฏ + .menu-construction-line = ุฎุท ุฅู†ุดุงุฆูŠ + .menu-arc-length = ุทูˆู„ ุงู„ู‚ูˆุณ + .command-line-opacity = ุดูุงููŠุฉ ุณุทุฑ ุงู„ุฃูˆุงู…ุฑ + .txtexp-no-text-selected = TXTEXP: ู„ู… ูŠุชู… ุชุญุฏูŠุฏ ู†ุต. + .angle-override-shown = ุชุฌุงูˆุฒ ุงู„ุฒุงูˆูŠุฉ: __ocs_fmt_0__ .plot-preview-task = ู…ุนุงูŠู†ุฉ ุงู„ุทุจุงุนุฉ .link-a-csv-data-source = ุฑุจุท ู…ุตุฏุฑ ุจูŠุงู†ุงุช CSV .csv-files = ู…ู„ูุงุช CSV .browser-cannot-open-typed-path = ู„ุง ูŠุณุชุทูŠุน ุงู„ู…ุชุตูุญ ูุชุญ ู…ุณุงุฑ ู…ูƒุชูˆุจุ› ุงุฎุชุฑ ุงู„ู…ู„ู ุจุฏู„ุงู‹ ู…ู† ุฐู„ูƒ. .recover-drawing = ุงุณุชุฑุฏุงุฏ ุงู„ุฑุณู… + .requires-0-or-1-or-keyword = ูŠุชุทู„ุจ 0 ุฃูˆ 1 ูู‚ุทุŒ ุฃูˆ ูƒู„ู…ุฉ ุฎูŠุงุฑ. + .requires-a-number = ูŠุชุทู„ุจ ุฑู‚ู…ู‹ุง. .restore-it-as-a-recovered-tab-or-discard-the-copy = ุงุณุชุนุฏู‡ ูƒุนู„ุงู…ุฉ ุชุจูˆูŠุจ ู…ุณุชุฑุฏุฉ ุฃูˆ ุชุฌุงู‡ู„ ุงู„ู†ุณุฎุฉ. .name-has-an-autosave-copy-newer-than-the-last-save = ู„ุฏู‰ "__ocs_fmt_0__" ู†ุณุฎุฉ ุญูุธ ุชู„ู‚ุงุฆูŠ ุฃุญุฏุซ ู…ู† ุขุฎุฑ ุญูุธ. .autosave-copy-could-not-be-stored = ุชุนุฐุฑ ุชุฎุฒูŠู† ู†ุณุฎุฉ ุงู„ุญูุธ ุงู„ุชู„ู‚ุงุฆูŠ: __ocs_fmt_0__ @@ -834,6 +908,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: ุญุฏุฏ ูƒุงุฆู† ู…ู‚ุทุน ุฃูˆ ุดุบู‘ู„ ุงู„ู…ู‚ุทุน ุงู„ุญูŠ. + .sectioncull-prepare-failed = SECTIONCULL: ุชุนุฐุฑ ุชุฌู‡ูŠุฒ ูƒุงุฆู† ู…ู‚ุทูˆุนุ› ู„ู… ูŠุชุบูŠุฑ ุดูŠุก. + .sectioncull-nothing-outside = SECTIONCULL: ู„ุง ูŠูˆุฌุฏ ุดูŠุก ุฎุงุฑุฌ ุงู„ู…ู‚ุทุน. + .sectioncull-result = SECTIONCULL: ุชู… ุญุฐู __ocs_arg_erased__ ูƒุงุฆู† ูˆู‚ุต __ocs_arg_cut__ ุนู†ุฏ ุญุฏ ุงู„ู…ู‚ุทุน. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: ุชู… ุชู…ุฑูŠุฑ ู…ู†ุญู†ู‰ ุนุจุฑ __ocs_fmt_0__ ู†ู‚ุทุฉ. .splinefit-need-at-least-3-points = SPLINEFIT: ูŠู„ุฒู… 3 ู†ู‚ุงุท ุนู„ู‰ ุงู„ุฃู‚ู„. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: ุญุฏุฏ ุฎุทู‹ุง ู…ุชุนุฏุฏู‹ุง ู„ุชู…ุฑูŠุฑ ู…ู†ุญู†ู‰ ุนุจุฑู‡. @@ -871,9 +949,12 @@ common = .sent-to-printer-printer = ุฃูุฑุณู„ ุฅู„ู‰ ุงู„ุทุงุจุนุฉ: __ocs_fmt_0__ .set-current = ุชุนูŠูŠู† ูƒุญุงู„ูŠ .shade-plot = ุชุธู„ูŠู„ ุงู„ุทุจุงุนุฉ + .shaded-with-edges = ู…ุธู„ู„ ู…ุน ุงู„ุญูˆุงู + .shades-of-gray = ุฏุฑุฌุงุช ุงู„ุฑู…ุงุฏูŠ .shadow-flags = ุนู„ุงู…ุงุช ุงู„ุธู„ุงู„ .single = ู…ูุฑุฏ .size = ุงู„ุญุฌู… + .sketchy = ุชุฎุทูŠุทูŠ .snap-override-label-next-pick-only = ุชุฌุงูˆุฒ ุงู„ุงู„ุชู‚ุงุท: __ocs_fmt_0__ (ู„ู„ุงุฎุชูŠุงุฑ ุงู„ุชุงู„ูŠ ูู‚ุท). .solid-created = ุฃูู†ุดุฆ ุงู„ู…ุฌุณู…. .source-object-not-found = ู„ู… ูŠูุนุซุฑ ุนู„ู‰ ุงู„ูƒุงุฆู† ุงู„ู…ุตุฏุฑ. @@ -1079,7 +1160,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: ุฃุฏุฎู„ Left ุฃูˆ Top ุฃูˆ Right. .isodraft-expected-on-or-off = ISODRAFT: ุฃุฏุฎู„ On ุฃูˆ Off. .xdata-selected-entities-are-on-locked-layers = XDATA: ุงู„ุนู†ุงุตุฑ ุงู„ู…ุญุฏุฏุฉ ุนู„ู‰ ุทุจู‚ุงุช ู…ู‚ูู„ุฉ. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: ู„ู… ูŠุชู… ุชุญุฏูŠุฏ ุตูˆุฑ ู†ู‚ุทูŠุฉ ุฃูˆ ุงู„ุฎุงุตูŠุฉ ุบูŠุฑ ู…ุนุฑูˆูุฉ (ุงุณุชุฎุฏู… BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: ุฃุฏุฎู„ ุนุฏุฏู‹ุง ุตุญูŠุญู‹ุง ู…ู† -4 ุฅู„ู‰ 4. .usage-datalink-path-to-csv = ุงู„ุงุณุชุฎุฏุงู…: DATALINK <ู…ุณุงุฑ-ู…ู„ู-.csv> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: ู„ู… ูŠุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ ุฌุฏุงูˆู„ ู…ุฑุชุจุทุฉ. @@ -2072,7 +2152,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = ู…ู„ู .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2381,6 +2461,7 @@ common = .load-linetypes = ุชุญู…ูŠู„ ุฃู†ูˆุงุน ุงู„ุฎุทูˆุท .requires-two-positive-values = ูŠุชุทู„ุจ ู‚ูŠู…ุชูŠู† ู…ูˆุฌุจุชูŠู† (x,y) ุฃูˆ ู‚ูŠู…ุฉ ูˆุงุญุฏุฉ ู„ูƒู„ูŠู‡ู…ุง. .snap-marker-color = ู„ูˆู† ุนู„ุงู…ุฉ ุงู„ุงู„ุชู‚ุงุท + .snap-marker-size = ุญุฌู… ุนู„ุงู…ุฉ ุงู„ุงู„ุชู‚ุงุท .command-line-text-color = ู„ูˆู† ู†ุต ุณุทุฑ ุงู„ุฃูˆุงู…ุฑ .operand = ุงู„ู…ุนุงู…ู„ .linetypes-loaded = ุชู… ุชุญู…ูŠู„ __ocs_fmt_0__ ู…ู† ุฃู†ูˆุงุน ุงู„ุฎุทูˆุท. @@ -2394,6 +2475,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE ุญุฏุฏ ู†ู‚ุทุฉ ุงู„ุฃุณุงุณ [__ocs_arg_count__ ูƒุงุฆู†ุงุช]: .threedrotate-specify-rotation-axis = 3DROTATE ุญุฏุฏ ู…ุญูˆุฑ ุงู„ุฏูˆุฑุงู† [X/Y/Z] ุฃูˆ ู†ู‚ุทุฉ ุซุงู†ูŠุฉ ุนู„ูŠู‡ : .threedrotate-specify-rotation-angle-a = 3DROTATE ุญุฏุฏ ุฒุงูˆูŠุฉ ุงู„ุฏูˆุฑุงู† <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = ุชุญุฐูŠุฑ ุงู„ุญูุธ .unable-to-save-drawing = ุชุนุฐุฑ ุญูุธ ุงู„ุฑุณู… @@ -3017,6 +3105,7 @@ draw = .arc-specify-radius-of-arc = ARC ุญุฏุฏ ู†ุตู ู‚ุทุฑ ุงู„ู‚ูˆุณ: .chord-length = ุทูˆู„ ุงู„ูˆุชุฑ + modify = .add-vertex = ุฅุถุงูุฉ ุฑุฃุณ .closed-cross = ุชู‚ุงุทุน ู…ุบู„ู‚ @@ -3062,6 +3151,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT ุญุฏุฏ ูƒุชู„ุฉ ุฐุงุช ุณู…ุงุช: .annotation-object-scale = ู…ู‚ูŠุงุณ ูƒุงุฆู† ุงู„ุชุนู„ูŠู‚ ุงู„ุชูˆุถูŠุญูŠ .bedit-select-block-reference-to-edit = BEDIT ุญุฏุฏ ู…ุฑุฌุน ุงู„ูƒุชู„ุฉ ู„ุชุญุฑูŠุฑู‡: + .bvstate-option = BVSTATE ุฃุฏุฎู„ ุฎูŠุงุฑู‹ุง [ุฌุฏูŠุฏ/ุชุนูŠูŠู†/ุฅุนุงุฏุฉ ุชุณู…ูŠุฉ/ุญุฐู] <ุชุนูŠูŠู†>: + .bvstate-new-name = BVSTATE ุฃุฏุฎู„ ุงุณู… ุญุงู„ุฉ ุงู„ุฑุคูŠุฉ ุงู„ุฌุฏูŠุฏุฉ: + .bvstate-set = BVSTATE ุฃุฏุฎู„ ุญุงู„ุฉ ุงู„ุฑุคูŠุฉ ุงู„ู…ุฑุงุฏ ุฌุนู„ู‡ุง ุงู„ุญุงู„ูŠุฉ <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE ุฃุฏุฎู„ ุญุงู„ุฉ ุงู„ุฑุคูŠุฉ ุงู„ู…ุฑุงุฏ ุฅุนุงุฏุฉ ุชุณู…ูŠุชู‡ุง <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE ุฃุฏุฎู„ ุงู„ุงุณู… ุงู„ุฌุฏูŠุฏ: + .bvstate-delete = BVSTATE ุฃุฏุฎู„ ุญุงู„ุฉ ุงู„ุฑุคูŠุฉ ุงู„ู…ุฑุงุฏ ุญุฐูู‡ุง: + .bvstate-not-found = ู„ู… ูŠุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ ุญุงู„ุฉ ุงู„ุฑุคูŠุฉ "__ocs_fmt_0__". + .bvstate-exists = ุญุงู„ุฉ ุงู„ุฑุคูŠุฉ "__ocs_fmt_0__" ู…ูˆุฌูˆุฏุฉ ุจุงู„ูุนู„. + .bvstate-last = ู„ุง ูŠู…ูƒู† ุญุฐู ุญุงู„ุฉ ุงู„ุฑุคูŠุฉ ุงู„ูˆุญูŠุฏุฉ. + .bvstate-needs-bedit = __ocs_fmt_0__: ุงูุชุญ ุงู„ูƒุชู„ุฉ ุฃูˆู„ุงู‹ ููŠ ู…ุญุฑุฑ ุงู„ูƒุชู„ (BEDIT). + .bvstate-current = ุญุงู„ุฉ ุงู„ุฑุคูŠุฉ ุงู„ุญุงู„ูŠุฉ: __ocs_fmt_0__ + .bvhide-select = ุญุฏุฏ ุงู„ูƒุงุฆู†ุงุช ุงู„ู…ุฑุงุฏ ุฅุฎูุงุคู‡ุง: + .bvshow-select = ุญุฏุฏ ุงู„ูƒุงุฆู†ุงุช ุงู„ู…ุฑุงุฏ ุฅุธู‡ุงุฑู‡ุง: + .arrayedit-select-array = ARRAYEDIT ุญุฏุฏ ุงู„ู…ุตููˆูุฉ: + .arrayedit-options = ARRAYEDIT ุฃุฏุฎู„ ุฎูŠุงุฑู‹ุง [ุงู„ู…ุตุฏุฑ/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: ุงู„ูƒุงุฆู† ุงู„ู…ุญุฏุฏ ู„ูŠุณ ู…ุตููˆูุฉ ุชุฑุงุจุทูŠุฉ. .blend-enter-continuity-tangent-curvature-c = BLEND ุฃุฏุฎู„ ุงู„ุงุณุชู…ุฑุงุฑูŠุฉ [ู…ู…ุงุณ/ุงู†ุญู†ุงุก] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND ุญุฏุฏ ุงู„ู…ู†ุญู†ู‰ ุงู„ู…ูุชูˆุญ ุงู„ุฃูˆู„ [ุงู„ุงุณุชู…ุฑุงุฑูŠุฉ=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND ุญุฏุฏ ุงู„ู…ู†ุญู†ู‰ ุงู„ู…ูุชูˆุญ ุงู„ุซุงู†ูŠ [ุงู„ุงุณุชู…ุฑุงุฑูŠุฉ=__ocs_arg_c__]: @@ -3614,6 +3719,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE ุญุฏุฏ ู†ู‚ุทุฉ ุงู„ุฅุฏุฑุงุฌ: insert = + .pastespec-specify-insertion-point = PASTESPEC ุญุฏุฏ ู†ู‚ุทุฉ ุงู„ุฅุฏุฑุงุฌ: + .pastespec-paste-as = PASTESPEC ู„ุตู‚ ูƒู€ [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3730,6 +3837,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3784,6 +3898,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *ุชุฎุชู„ู ู…ูˆุงุถุน ุงู„ุงู„ุชู‚ุงุท ููŠ ุงู„ู…ุฑุงุฌุน ุงู„ุณูู„ูŠุฉ* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3923,6 +4038,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = ุญุฏุฏ ุตูˆุฑุฉ ุฃูˆ ุทุจู‚ุฉ ุณูู„ูŠุฉ: + .adjust-select-pdf-underlay = ุญุฏุฏ ุทุจู‚ุฉ ุณูู„ูŠุฉ PDF: + .adjust-select-dwf-underlay = ุญุฏุฏ ุทุจู‚ุฉ ุณูู„ูŠุฉ DWF: + .adjust-select-dgn-underlay = ุญุฏุฏ ุทุจู‚ุฉ ุณูู„ูŠุฉ DGN: + .adjust-mixed-underlays = *ู…ุฌู…ูˆุนุฉ ุชุญุฏูŠุฏ ุบูŠุฑ ุตุงู„ุญุฉ. ุญุฏุฏ ุทุจู‚ุงุช ุณูู„ูŠุฉ ู…ู† ู†ูˆุน ุงู„ู…ู„ู ู†ูุณู‡ ูู‚ุท.* + .adjust-image-option = ุฃุฏุฎู„ ุฎูŠุงุฑ ุงู„ุตูˆุฑุฉ [ุงู„ุชุจุงูŠู†/ุงู„ุชู„ุงุดูŠ/ุงู„ุณุทูˆุน] <ุงู„ุณุทูˆุน>: + .adjust-underlay-option = ุฃุฏุฎู„ ุฎูŠุงุฑ ุงู„ุทุจู‚ุฉ ุงู„ุณูู„ูŠุฉ __ocs_arg_kind__ [ุงู„ุชู„ุงุดูŠ/ุงู„ุชุจุงูŠู†/ุฃุญุงุฏูŠ ุงู„ู„ูˆู†] <__ocs_arg_option__>: + .adjust-monochrome = ุฃุญุงุฏูŠ ุงู„ู„ูˆู†? [ู†ุนู…/ู„ุง] <__ocs_arg_value__>: + .adjust-brightness-value = ุฃุฏุฎู„ ู‚ูŠู…ุฉ ุงู„ุณุทูˆุน (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = ุฃุฏุฎู„ ู‚ูŠู…ุฉ ุงู„ุชุจุงูŠู† (0-100) <__ocs_arg_value__>: + .adjust-fade-value = ุฃุฏุฎู„ ู‚ูŠู…ุฉ ุงู„ุชู„ุงุดูŠ (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = ูŠุฌุจ ุฃู† ุชูƒูˆู† ุงู„ู‚ูŠู…ุฉ ุนุฏุฏู‹ุง ุตุญูŠุญู‹ุง ุจูŠู† 0 ูˆ 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4427,7 +4554,27 @@ model = .value-2point = ู†ู‚ุทุชุงู† .cube = ู…ูƒุนุจ + layout = + .mvsetup-select-objects-to-delete = MVSETUP ุญุฏุฏ ุงู„ูƒุงุฆู†ุงุช ุงู„ู…ุฑุงุฏ ุญุฐูู‡ุง: + .delete-objects-option = ุญุฐู ุงู„ูƒุงุฆู†ุงุช + .mvsetup-enable-paper-space = MVSETUP ุชู…ูƒูŠู† ูุถุงุก ุงู„ูˆุฑู‚ุŸ [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP ุฃุฏุฎู„ ู…ุนุงู…ู„ ุงู„ู…ู‚ูŠุงุณ <1>: + .mvsetup-enter-paper-width = MVSETUP ุฃุฏุฎู„ ุนุฑุถ ุงู„ูˆุฑู‚ุฉ: + .mvsetup-enter-paper-height = MVSETUP ุฃุฏุฎู„ ุงุฑุชูุงุน ุงู„ูˆุฑู‚ุฉ: + .mvsetup-enter-option = MVSETUP ุฃุฏุฎู„ ุฎูŠุงุฑู‹ุง [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP ุฃุฏุฎู„ ุชุฎุทูŠุท ู…ู†ุงูุฐ ุงู„ุนุฑุถ [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP ุญุฏุฏ ุงู„ุฒุงูˆูŠุฉ ุงู„ุฃูˆู„ู‰ ู„ู…ู†ุทู‚ุฉ ู…ู†ุงูุฐ ุงู„ุนุฑุถ : + .mvsetup-specify-opposite-corner = MVSETUP ุญุฏุฏ ุงู„ุฒุงูˆูŠุฉ ุงู„ู…ู‚ุงุจู„ุฉ: + .mvsetup-viewports-in-x = MVSETUP ุฃุฏุฎู„ ุนุฏุฏ ู…ู†ุงูุฐ ุงู„ุนุฑุถ ููŠ ุงุชุฌุงู‡ X <2>: + .mvsetup-viewports-in-y = MVSETUP ุฃุฏุฎู„ ุนุฏุฏ ู…ู†ุงูุฐ ุงู„ุนุฑุถ ููŠ ุงุชุฌุงู‡ Y <2>: + .mvsetup-distance-in-x = MVSETUP ุญุฏุฏ ุงู„ู…ุณุงูุฉ ุจูŠู† ู…ู†ุงูุฐ ุงู„ุนุฑุถ ููŠ ุงุชุฌุงู‡ X <0>: + .mvsetup-distance-in-y = MVSETUP ุญุฏุฏ ุงู„ู…ุณุงูุฉ ุจูŠู† ู…ู†ุงูุฐ ุงู„ุนุฑุถ ููŠ ุงุชุฌุงู‡ Y <0>: + .mvsetup-select-viewports = MVSETUP ุญุฏุฏ ู…ู†ุงูุฐ ุงู„ุนุฑุถ ุงู„ู…ุฑุงุฏ ุชุบูŠูŠุฑ ู…ู‚ูŠุงุณู‡ุง: + .mvsetup-paper-space-units = MVSETUP ุฃุฏุฎู„ ุนุฏุฏ ูˆุญุฏุงุช ูุถุงุก ุงู„ูˆุฑู‚ <1>: + .mvsetup-model-space-units = MVSETUP ุฃุฏุฎู„ ุนุฏุฏ ูˆุญุฏุงุช ูุถุงุก ุงู„ู†ู…ูˆุฐุฌ <1>: + .mvsetup-viewports-do-not-fit = MVSETUP ู…ู†ุงูุฐ ุงู„ุนุฑุถ ู„ุง ุชุชุณุน ููŠ ุงู„ู…ู†ุทู‚ุฉ ุงู„ู…ุญุฏุฏุฉ. + .scale-viewports-option = ุชุบูŠูŠุฑ ู…ู‚ูŠุงุณ ู…ู†ุงูุฐ ุงู„ุนุฑุถ .active = ู†ุดุท .inactive = ุบูŠุฑ ู†ุดุท .insert-view = ุฅุฏุฑุงุฌ ุนุฑุถ @@ -4453,6 +4600,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = ุชุฌู…ูŠุฏ | T = ุฅู„ุบุงุก ุงู„ุชุฌู…ูŠุฏ | Enter = ุฎุฑูˆุฌ view = + .twist-option = ู„ู + .dview-specify-view-twist-angle = DVIEW ุญุฏุฏ ุฒุงูˆูŠุฉ ู„ู ุงู„ุนุฑุถ <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW ุฃุฏุฎู„ ุฎูŠุงุฑู‹ุง [TWist/Undo]: .color-scheme-set-to-system-currently-name = ุชู… ุชุนูŠูŠู† ู†ุธุงู… ุงู„ุฃู„ูˆุงู† ุนู„ู‰ "ุงู„ู†ุธุงู…" (ุญุงู„ูŠู‹ุง __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = ูŠุชุทู„ุจ ุนุฏุฏู‹ุง ุตุญูŠุญู‹ุง ุจูŠู† __ocs_fmt_0__ ูˆ __ocs_fmt_1__. @@ -4647,13 +4797,6 @@ view = .x-no-xdata = __ocs_fmt_0__: ู„ุง ุชูˆุฌุฏ ุจูŠุงู†ุงุช ู…ูˆุณุนุฉ. .value-3d-orbit = ู…ุฏุงุฑ ุซู„ุงุซูŠ ุงู„ุฃุจุนุงุฏ .value-3d-orbit-drag-with-right-mouse-button = ุงู„ู…ุฏุงุฑ ุซู„ุงุซูŠ ุงู„ุฃุจุนุงุฏ: ุงุณุญุจ ุจุฒุฑ ุงู„ูุฃุฑุฉ ุงู„ุฃูŠู…ู†. - .adjust-brightness-contrast-fade = ADJUST [ุงู„ุณุทูˆุน / ุงู„ุชุจุงูŠู† / ุงู„ุชู„ุงุดูŠ]: - .adjust-brightness-0-100 = ADJUST ุงู„ุณุทูˆุน 0-100: - .adjust-contrast-0-100 = ADJUST ุงู„ุชุจุงูŠู† 0-100: - .adjust-fade-0-100 = ADJUST ุงู„ุชู„ุงุดูŠ 0-100: - .adjust-select-raster-image-s-first = ADJUST: ุญุฏุฏ ุตูˆุฑุฉ ู†ู‚ุทูŠุฉ ุฃูˆู„ู‹ุง. - .adjust-value-must-be-0-100 = ADJUST: ูŠุฌุจ ุฃู† ุชูƒูˆู† ุงู„ู‚ูŠู…ุฉ ุจูŠู† 0 ูˆ100. - .adjust-action-v-on-changed-image-s = ADJUST: โ€__ocs_fmt_0__ = __ocs_fmt_1__ ุนู„ู‰ __ocs_fmt_2__ ุตูˆุฑุฉ. .annoallvisible-new-value-0-1 = ANNOALLVISIBLE ุงู„ู‚ูŠู…ุฉ ุงู„ุฌุฏูŠุฏุฉ [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: ุฃุฏุฎู„ 0 ุฃูˆ 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE ุงู„ู‚ูŠู…ุฉ ุงู„ุฌุฏูŠุฏุฉ [-4..4]: @@ -4779,7 +4922,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = ุงู„ุทุจู‚ุฉ ุงู„ุณูู„ูŠุฉ __ocs_fmt_0__: ุงู„ุชู„ุงุดูŠ=__ocs_fmt_1__ุŒ ุงู„ุชุจุงูŠู†=__ocs_fmt_2__ุŒ ุงู„ุชุดุบูŠู„=__ocs_fmt_3__ุŒ ุงู„ู‚ุต=__ocs_fmt_4__ุŒ ุงู„ุฃุญุงุฏูŠ=__ocs_fmt_5__ .updated-changed-underlay-s = ุชู… ุชุญุฏูŠุซ __ocs_fmt_0__ ุทุจู‚ุฉ ุณูู„ูŠุฉ. - .usage-adjust-brightness-contrast-fade-0-100 = ุงู„ุงุณุชุฎุฏุงู…: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = ุงู„ุงุณุชุฎุฏุงู…: ANNOSCALE ู…ุซู„ู‹ุง 1:50 ุฃูˆ 2:1 ุฃูˆ ู…ุนุงู…ู„ .usage-draworder-f-front-b-back-a-above-handle = ุงู„ุงุณุชุฎุฏุงู…: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = ุงู„ุงุณุชุฎุฏุงู…: HYPERLINK (ุญุฏุฏ ุงู„ูƒุงุฆู†ุงุช ุฃูˆู„ู‹ุง) @@ -6007,6 +6149,46 @@ ui = .osnap-settings = ุฅุนุฏุงุฏุงุช ุงู„ุงู„ุชู‚ุงุท... .snap-override-none-next-pick-only = ุชุฌุงูˆุฒ ุงู„ุงู„ุชู‚ุงุท: ุจุฏูˆู† (ู„ู„ุงุฎุชูŠุงุฑ ุงู„ุชุงู„ูŠ ูู‚ุท). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = ุฅุจู‚ุงุก ุงู„ู†ุต ุฏุงุฆู…ู‹ุง ุจูŠู† ุฎุทูˆุท ุงู„ุงู…ุชุฏุงุฏ .angular-dimensions = ุงู„ุฃุจุนุงุฏ ุงู„ุฒุงูˆูŠุฉ @@ -6274,3 +6456,117 @@ graphic-attributes = .reset-angle = ุฅุนุงุฏุฉ ุชุนูŠูŠู† ุงู„ุฒุงูˆูŠุฉ .one-color = ู„ูˆู† ูˆุงุญุฏ .two-colors = ู„ูˆู†ุงู† + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = ุฅุถุงูุฉ ุฅู„ู‰ ู…ุฌู…ูˆุนุฉ ุงู„ุนู…ู„ + .remove-from-working-set = ุฅุฒุงู„ุฉ ู…ู† ู…ุฌู…ูˆุนุฉ ุงู„ุนู…ู„ + .toolbar-objects = ูƒุงุฆู†ุงุช + .dialog-title = ุชุญุฑูŠุฑ ุงู„ู…ุฑุฌุน + .dialog-reference = ุงู„ู…ุฑุฌุน ุงู„ู…ุฑุงุฏ ุชุญุฑูŠุฑู‡ + .dialog-chain-hint = ุงู„ุณู„ุณู„ุฉ ุนู†ุฏ ุงู„ู†ู‚ุทุฉ ุงู„ู…ุญุฏุฏุฉุ› ุงู†ู‚ุฑ ุนู„ู‰ ุดุงุฑุฉ ู„ุชุบูŠูŠุฑ ุงู„ู…ุณุชูˆู‰. + .dialog-path = ุงู„ู…ุณุงุฑ: + .dialog-working-set = ู…ุฌู…ูˆุนุฉ ุงู„ุนู…ู„ + .dialog-all-objects = ูƒู„ ุงู„ูƒุงุฆู†ุงุช + .dialog-pick-objects = ุงู„ุชุญุฏูŠุฏ ููŠ ุงู„ุฑุณู… + .dialog-attributes = ุชุญุฑูŠุฑ ุชุนุฑูŠูุงุช ุงู„ุณู…ุงุช ุฃูŠุถู‹ุง + .dialog-session = ุงู„ุฌู„ุณุฉ + .dialog-lock = ู‚ูู„ ุงู„ูƒุงุฆู†ุงุช ุฎุงุฑุฌ ู…ุฌู…ูˆุนุฉ ุงู„ุนู…ู„ + .dialog-unique = ุฅุนุทุงุก ุฃุณู…ุงุก ูุฑูŠุฏุฉ ู„ู„ุทุจู‚ุงุช ูˆุงู„ุฃู†ู…ุงุท ูˆุงู„ูƒุชู„ ุงู„ู…ุชุนุงุฑุถุฉ + .dialog-start = ุจุฏุก ุงู„ุชุญุฑูŠุฑ + .dialog-help = ูŠูุชุญ ู…ุฑุฌุน ูƒุชู„ุฉ ุฃูˆ ู…ุฑุฌุนู‹ุง ุฎุงุฑุฌูŠู‹ุง ู„ุชุญุฑูŠุฑู‡ ููŠ ู…ูƒุงู†ู‡. + .select-reference = ุญุฏุฏ ุงู„ู…ุฑุฌุน: + .reference-not-found = ู„ู… ูŠุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ ุงู„ู…ุฑุฌุน. + .not-valid-reference = ุงู„ูƒุงุฆู† ู„ูŠุณ ู…ุฑุฌุนู‹ุง ุตุงู„ุญู‹ุง + .one-entity-added = ุชู…ุช ุฅุถุงูุฉ ูƒูŠุงู† ูˆุงุญุฏ + .nesting-level = ุญุฏุฏ ู…ุณุชูˆู‰ ุงู„ุชุฏุงุฎู„ [ู…ูˆุงูู‚/ุงู„ุชุงู„ูŠ] <ุงู„ุชุงู„ูŠ>: + .selection-method = ุฃุฏุฎู„ ุทุฑูŠู‚ุฉ ุชุญุฏูŠุฏ ุงู„ูƒุงุฆู†ุงุช [ุงู„ูƒู„/ู…ุชุฏุงุฎู„ุฉ] <ุงู„ูƒู„>: + .select-nested = ุญุฏุฏ ุงู„ูƒุงุฆู†ุงุช ุงู„ู…ุชุฏุงุฎู„ุฉ: + .display-attdefs = ุนุฑุถ ุชุนุฑูŠูุงุช ุงู„ุณู…ุงุช [ู†ุนู…/ู„ุง] <ู„ุง>: + .zero-items-selected = ุชู… ุชุญุฏูŠุฏ 0 ุนู†ุตุฑ + .one-item-selected = ุชู… ุชุญุฏูŠุฏ ุนู†ุตุฑ ูˆุงุญุฏ + .items-selected = ุชู… ุชุญุฏูŠุฏ __ocs_fmt_0__ ุนู†ุตุฑ + .refset-option = ุฃุฏุฎู„ ุฎูŠุงุฑู‹ุง [ุฅุถุงูุฉ/ุฅุฒุงู„ุฉ] <ุฅุถุงูุฉ>: + .refclose-option = ุฃุฏุฎู„ ุฎูŠุงุฑู‹ุง [ุญูุธ/ุชุฌุงู‡ู„ ุชุบูŠูŠุฑุงุช ุงู„ู…ุฑุฌุน] <ุญูุธ>: + .not-checked-out = ** ุงู„ุฃู…ุฑ ุบูŠุฑ ู…ุณู…ูˆุญ ู…ุง ู„ู… ูŠุชู… ุชุญุฑูŠุฑ ู…ุฑุฌุน ุจุงุณุชุฎุฏุงู… ุงู„ุฃู…ุฑ REFEDIT ** + .already-checked-out = ** ุงู„ุฃู…ุฑ ุบูŠุฑ ู…ุณู…ูˆุญุŒ __ocs_fmt_0__ ู‚ูŠุฏ ุงู„ุชุญุฑูŠุฑ ุจุงู„ูุนู„ ** + .transfer-objects = ู†ู‚ู„ ุงู„ูƒุงุฆู†ุงุช ุจูŠู† ู…ุฌู…ูˆุนุฉ ุนู…ู„ RefEdit ูˆุงู„ุฑุณู… ุงู„ู…ุถูŠู... + .added-to-working-set = ุชู…ุช ุฅุถุงูุฉ __ocs_fmt_0__ ุฅู„ู‰ ู…ุฌู…ูˆุนุฉ ุงู„ุนู…ู„ + .removed-from-working-set = ุชู…ุช ุฅุฒุงู„ุฉ __ocs_fmt_0__ ู…ู† ู…ุฌู…ูˆุนุฉ ุงู„ุนู…ู„ + .one-object-added = ุชู…ุช ุฅุถุงูุฉ ูƒุงุฆู† ูˆุงุญุฏ ุฅู„ู‰ __ocs_fmt_0__ + .objects-added = ุชู…ุช ุฅุถุงูุฉ __ocs_fmt_0__ ูƒุงุฆู† ุฅู„ู‰ __ocs_fmt_1__ + .one-object-removed = ุชู…ุช ุฅุฒุงู„ุฉ ูƒุงุฆู† ูˆุงุญุฏ ู…ู† __ocs_fmt_0__ + .objects-removed = ุชู…ุช ุฅุฒุงู„ุฉ __ocs_fmt_0__ ูƒุงุฆู† ู…ู† __ocs_fmt_1__ + .one-xref-instance = ุชู… ุชุญุฏูŠุซ ู†ุณุฎุฉ xref ูˆุงุญุฏุฉ + .xref-instances = ุชู… ุชุญุฏูŠุซ __ocs_fmt_0__ ู†ุณุฎุฉ xref + .one-block-instance = ุชู… ุชุญุฏูŠุซ ู†ุณุฎุฉ ูƒุชู„ุฉ ูˆุงุญุฏุฉ + .block-instances = ุชู… ุชุญุฏูŠุซ __ocs_fmt_0__ ู†ุณุฎุฉ ูƒุชู„ุฉ + .redefined-reloaded = ุชู…ุช ุฅุนุงุฏุฉ ุชุนุฑูŠู __ocs_fmt_0__ ูˆุฅุนุงุฏุฉ ุชุญู…ูŠู„ู‡. + .redefined = ุชู…ุช ุฅุนุงุฏุฉ ุชุนุฑูŠู __ocs_fmt_0__. + .ok = ู…ูˆุงูู‚ + .nested = ู…ุชุฏุงุฎู„ุฉ + +image-adjust = + .title = ุถุจุท ุงู„ุตูˆุฑุฉ + .preview = ู…ุนุงูŠู†ุฉ + .before = ู‚ุจู„ + .after = ุจุนุฏ + .no-preview = ุชุนุฐุฑ ุชุญู…ูŠู„ ุงู„ุตูˆุฑุฉ + .adjust = ุถุจุท + .brightness = ุงู„ุณุทูˆุน + .contrast = ุงู„ุชุจุงูŠู† + .fade = ุงู„ุชู„ุงุดูŠ + .hint = ุงู†ู‚ุฑ ุนู„ู‰ ุดุฑูŠุท ุฃูˆ ุงุณุญุจู‡ุ› โˆ’ / + ุฎุทูˆุฉ ูˆุงุญุฏุฉุŒ ูˆูŠู‚ุจู„ ุงู„ู…ุฑุจุน ุนุฏุฏู‹ุง ุตุญูŠุญู‹ุง ู…ู† 0 ุฅู„ู‰ 100. + .reset = ุงุณุชุนุงุฏุฉ ุงู„ุงูุชุฑุงุถูŠ + .apply = ุชุทุจูŠู‚ + .help = ูŠุถุจุท ุณุทูˆุน ุงู„ุตูˆุฑ ุงู„ู†ู‚ุทูŠุฉ ุงู„ู…ุญุฏุฏุฉ ูˆุชุจุงูŠู†ู‡ุง ูˆุชู„ุงุดูŠู‡ุง. + +keytips = + .title = KeyTip + .typed = ุงู„ู…ูƒุชูˆุจ: { $path } + .hint-tabs = ุงูƒุชุจ ุญุฑู ุนู„ุงู…ุฉ ุงู„ุชุจูˆูŠุจ ยท Esc ู„ู„ุฅุบู„ุงู‚ + .hint-tab = ุนู„ุงู…ุฉ ุงู„ุชุจูˆูŠุจ { $tab } ยท Esc ู„ู„ุฑุฌูˆุน ู…ุณุชูˆู‰ ูˆุงุญุฏู‹ุง + .hint-items = ุงูƒุชุจ ุญุฑู ุงู„ุนู†ุตุฑ ยท Esc ู„ู„ุฑุฌูˆุน ู…ุณุชูˆู‰ ูˆุงุญุฏู‹ุง diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index ed8c27b53..c5000d365 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = ะŸั€ะธัั‚ะฐะฒะบะธ ribbon-tab = + .block-editor = ะ ะตะดะฐะบั‚ะพั€ ะฝะฐ ะฑะปะพะบะพะฒะต .draw = ะงะตั€ั‚ะฐะฝะต .parametric = ะŸะฐั€ะฐะผะตั‚ั€ะธั‡ะตะฝ .annotate = ะะฝะพั‚ะธั€ะฐะฝะต @@ -98,6 +99,9 @@ start = .no-recent-files = ะคะฐะนะปะพะฒะตั‚ะต, ะบะพะธั‚ะพ ะพั‚ะฒะพั€ะธัˆ, ั‰ะต ัะต ะฟะพัะฒัั‚ ั‚ัƒะบ. .browser-storage = ะฅั€ะฐะฝะธะปะธั‰ะต ะฝะฐ ะฑั€ะฐัƒะทัŠั€ะฐ .keep-recent-files = ะŸะฐะทะธ ัะบะพั€ะพัˆะฝะธั‚ะต ั„ะฐะนะปะพะฒะต + .view-list = ะกะฟะธััŠะบ + .view-icons = ะ˜ะบะพะฝะธ + .view-preview = ะ“ะพะปะตะผะธ ะฟั€ะตะณะปะตะดะธ donation = .title = ะŸั€ะธะทะธะฒ ะทะฐ ะดะฐั€ะตะฝะธะต @@ -167,6 +171,14 @@ modal = .that-object = ั‚ะพะทะธ ะพะฑะตะบั‚ .those-objects = ั‚ะตะทะธ ะพะฑะตะบั‚ะธ + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = ะ’ัั‚ะฐะฒะตะฝะพ ะธะทะพะฑั€ะฐะถะตะฝะธะต "{ $name }": { $width }ร—{ $height } px .image-unsupported = ะ’ัั‚ะฐะฒะตะฝะพั‚ะพ ะธะทะพะฑั€ะฐะถะตะฝะธะต "{ $name }" ะฝะต ะผะพะถะต ะดะฐ ะฑัŠะดะต ะดะตะบะพะดะธั€ะฐะฝะพ. @@ -273,11 +285,73 @@ command-copy = .displacement = COPY ะ—ะฐะดะฐะนั‚ะต ะฟั€ะตะผะตัั‚ะฒะฐะฝะต: .mode = COPY ะ’ัŠะฒะตะดะตั‚ะต ั€ะตะถะธะผ ะฝะฐ ะบะพะฟะธั€ะฐะฝะต [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ ัะฐะผะพ ั‚ัƒะบ (ะทะตะปะตะฝะพ), __ocs_fmt_1__ ัะฐะผะพ ะฒ ะดั€ัƒะณะธั ั‡ะตั€ั‚ะตะถ (ั‡ะตั€ะฒะตะฝะพ)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ะกั€ะฐะฒะฝัะฒะฐะฝะต ั ั‡ะตั€ั‚ะตะถ + .label-associative-array-created = __ocs_fmt_0__: ััŠะทะดะฐะดะตะฝ ะต ะฐัะพั†ะธะฐั‚ะธะฒะตะฝ ะผะฐัะธะฒ. + .assoc-array-array-path = ะœะฐัะธะฒ (ะฟะพ ะฟัŠั‚) + .assoc-array-array-polar = ะœะฐัะธะฒ (ะฟะพะปัั€ะตะฝ) + .assoc-array-array-rectangular = ะœะฐัะธะฒ (ะฟั€ะฐะฒะพัŠะณัŠะปะตะฝ) + .assoc-array-item-spacing = ะ ะฐะทัั‚ะพัะฝะธะต ะผะตะถะดัƒ ะตะปะตะผะตะฝั‚ะธั‚ะต + .assoc-array-fill-angle = ะชะณัŠะป ะฝะฐ ะทะฐะฟัŠะปะฒะฐะฝะต + .assoc-array-items = ะ•ะปะตะผะตะฝั‚ะธ + .assoc-array-level-spacing = ะ ะฐะทัั‚ะพัะฝะธะต ะผะตะถะดัƒ ะฝะธะฒะฐั‚ะฐ + .assoc-array-levels = ะะธะฒะฐ + .assoc-array-row-spacing = ะ ะฐะทัั‚ะพัะฝะธะต ะผะตะถะดัƒ ั€ะตะดะพะฒะตั‚ะต + .assoc-array-column-spacing = ะ ะฐะทัั‚ะพัะฝะธะต ะผะตะถะดัƒ ะบะพะปะพะฝะธั‚ะต + .fbxexport-exported-to = FBXEXPORT: ะตะบัะฟะพั€ั‚ะธั€ะฐะฝะพ ะฒ "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: ะฝัะผะฐ 3D ะดะฐะฝะฝะธ ะทะฐ ะผั€ะตะถะฐ ะฒ ั‚ะพะทะธ ั‡ะตั€ั‚ะตะถ. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = ะะพะฒะธั‚ะต ั‰ั€ะธั…ะพะฒะบะธ ัะปะตะดะฒะฐั‚ ะณั€ะฐะฝะธั†ะธั‚ะต ัะธ (HPASSOC) + .options-show-ribbon = ะŸะพะบะฐะทะฒะฐะฝะต ะฝะฐ ะปะตะฝั‚ะฐั‚ะฐ (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = ะŸะพะบะฐะทะฒะฐะฝะต ะฝะฐ ะปะตะฝั‚ะฐั‚ะฐ ั ะผะตะฝัŽั‚ะฐ (MENUBAR) + .options-window-elements = ะ•ะปะตะผะตะฝั‚ะธ ะฝะฐ ะฟั€ะพะทะพั€ะตั†ะฐ + .menu-viewports = ะŸั€ะพะทะพั€ั†ะธ + .menu-view-twist = ะ—ะฐะฒัŠั€ั‚ะฐะฝะต ะฝะฐ ะธะทะณะปะตะดะฐ + .menu-tools = ะ˜ะฝัั‚ั€ัƒะผะตะฝั‚ะธ + .menu-table-style = ะกั‚ะธะป ะฝะฐ ั‚ะฐะฑะปะธั†ะฐ + .menu-run-script = ะ˜ะทะฟัŠะปะฝะตะฝะธะต ะฝะฐ ัะบั€ะธะฟั‚ + .menu-revision-cloud = ะ ะตะฒะธะทะธะพะฝะตะฝ ะพะฑะปะฐะบ + .menu-regen-all = ะ ะตะณะตะฝะตั€ะธั€ะฐะฝะต ะฝะฐ ะฒัะธั‡ะบะพ + .menu-redraw = ะŸั€ะตั€ะธััƒะฒะฐะฝะต + .menu-rectangle = ะŸั€ะฐะฒะพัŠะณัŠะปะฝะธะบ + .menu-quick-dimension = ะ‘ัŠั€ะท ั€ะฐะทะผะตั€ + .menu-publish = ะŸัƒะฑะปะธะบัƒะฒะฐะฝะต + .menu-paste-special = ะกะฟะตั†ะธะฐะปะฝะพ ะฟะพัั‚ะฐะฒัะฝะต + .menu-parametric = ะŸะฐั€ะฐะผะตั‚ั€ะธั‡ะฝะพ + .menu-page-setup-manager = ะ”ะธัะฟะตั‡ะตั€ ะฝะฐ ะฝะฐัั‚ั€ะพะนะบะธ ะฝะฐ ัั‚ั€ะฐะฝะธั†ะฐ + .menu-orbit = ะžั€ะฑะธั‚ะฐ + .menu-new-ucs = ะะพะฒะฐ ะŸะšะก + .menu-named-views = ะ˜ะผะตะฝัƒะฒะฐะฝะธ ะธะทะณะปะตะดะธ + .menu-multiline-text = ะœะฝะพะณะพั€ะตะดะพะฒ ั‚ะตะบัั‚ + .menu-multiline-style = ะกั‚ะธะป ะฝะฐ ะผัƒะปั‚ะธะปะธะฝะธั + .menu-multiline = ะœัƒะปั‚ะธะปะธะฝะธั + .menu-multileader-style = ะกั‚ะธะป ะฝะฐ ะผัƒะปั‚ะธะธะทะฝะพัะฝะฐ ะปะธะฝะธั + .menu-match-properties = ะกัŠะฟะพัั‚ะฐะฒัะฝะต ะฝะฐ ัะฒะพะนัั‚ะฒะฐ + .menu-lengthen = ะฃะดัŠะปะถะฐะฒะฐะฝะต + .menu-jogged = ะะฐั‡ัƒะฟะตะฝ + .menu-id-point = ะšะพะพั€ะดะธะฝะฐั‚ะธ ะฝะฐ ั‚ะพั‡ะบะฐ + .menu-explode-text = ะ ะฐะทะดะตะปัะฝะต ะฝะฐ ั‚ะตะบัั‚ + .menu-edit-spline = ะ ะตะดะฐะบั‚ะธั€ะฐะฝะต ะฝะฐ ัะฟะปะฐะนะฝ + .menu-edit-polyline = ะ ะตะดะฐะบั‚ะธั€ะฐะฝะต ะฝะฐ ะฟะพะปะธะปะธะฝะธั + .menu-edit-hatch = ะ ะตะดะฐะบั‚ะธั€ะฐะฝะต ะฝะฐ ั‰ั€ะธั…ะพะฒะบะฐ + .menu-drawing-limits = ะ“ั€ะฐะฝะธั†ะธ ะฝะฐ ั‡ะตั€ั‚ะตะถะฐ + .menu-donut = ะŸั€ัŠัั‚ะตะฝ + .menu-dimension-style = ะกั‚ะธะป ะฝะฐ ั€ะฐะทะผะตั€ะธ + .menu-construction-line = ะšะพะฝัั‚ั€ัƒะบั‚ะธะฒะฝะฐ ะปะธะฝะธั + .menu-arc-length = ะ”ัŠะปะถะธะฝะฐ ะฝะฐ ะดัŠะณะฐ + .command-line-opacity = ะะตะฟั€ะพะทั€ะฐั‡ะฝะพัั‚ ะฝะฐ ะบะพะผะฐะฝะดะฝะธั ั€ะตะด + .txtexp-no-text-selected = TXTEXP: ะฝัะผะฐ ะธะทะฑั€ะฐะฝ ั‚ะตะบัั‚. + .angle-override-shown = ะ—ะฐะผัะฝะฐ ะฝะฐ ัŠะณัŠะปะฐ: __ocs_fmt_0__ .plot-preview-task = ะŸั€ะตะณะปะตะด ะฝะฐ ะฟะตั‡ะฐั‚ะฐ .link-a-csv-data-source = ะกะฒัŠั€ะทะฒะฐะฝะต ะฝะฐ CSV ะธะทั‚ะพั‡ะฝะธะบ ะฝะฐ ะดะฐะฝะฝะธ .csv-files = CSV ั„ะฐะนะปะพะฒะต .browser-cannot-open-typed-path = ะ‘ั€ะฐัƒะทัŠั€ัŠั‚ ะฝะต ะผะพะถะต ะดะฐ ะพั‚ะฒะพั€ะธ ะฒัŠะฒะตะดะตะฝ ะฟัŠั‚; ะธะทะฑะตั€ะตั‚ะต ั„ะฐะนะปะฐ. .recover-drawing = ะ’ัŠะทัั‚ะฐะฝะพะฒัะฒะฐะฝะต ะฝะฐ ั‡ะตั€ั‚ะตะถ + .requires-0-or-1-or-keyword = ะ˜ะทะธัะบะฒะฐ ัะต ัะฐะผะพ 0 ะธะปะธ 1, ะธะปะธ ะบะปัŽั‡ะพะฒะฐ ะดัƒะผะฐ ะฝะฐ ะพะฟั†ะธั. + .requires-a-number = ะ˜ะทะธัะบะฒะฐ ัะต ั‡ะธัะปะพ. .restore-it-as-a-recovered-tab-or-discard-the-copy = ะ’ัŠะทัั‚ะฐะฝะพะฒะตั‚ะต ะณะพ ะบะฐั‚ะพ ะฒัŠะทัั‚ะฐะฝะพะฒะตะฝ ั€ะฐะทะดะตะป ะธะปะธ ะพั‚ั…ะฒัŠั€ะปะตั‚ะต ะบะพะฟะธะตั‚ะพ. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" ะธะผะฐ ะบะพะฟะธะต ะพั‚ ะฐะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ ะทะฐะฟะธัะฒะฐะฝะต, ะฟะพ-ะฝะพะฒะพ ะพั‚ ะฟะพัะปะตะดะฝะธั ะทะฐะฟะธั. .autosave-copy-could-not-be-stored = ะšะพะฟะธะตั‚ะพ ะพั‚ ะฐะฒั‚ะพะผะฐั‚ะธั‡ะฝะพั‚ะพ ะทะฐะฟะธัะฒะฐะฝะต ะฝะต ะผะพะถะฐ ะดะฐ ะฑัŠะดะต ััŠั…ั€ะฐะฝะตะฝะพ: __ocs_fmt_0__ @@ -821,6 +895,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: ะธะทะฑะตั€ะตั‚ะต ะพะฑะตะบั‚ ะทะฐ ัะตั‡ะตะฝะธะต ะธะปะธ ะฒะบะปัŽั‡ะตั‚ะต ะถะธะฒะพ ัะตั‡ะตะฝะธะต. + .sectioncull-prepare-failed = SECTIONCULL: ะธะทั€ัะทะฐะฝ ะพะฑะตะบั‚ ะฝะต ะผะพะถะฐ ะดะฐ ะฑัŠะดะต ะฟะพะดะณะพั‚ะฒะตะฝ; ะฝะธั‰ะพ ะฝะต ะต ะฟั€ะพะผะตะฝะตะฝะพ. + .sectioncull-nothing-outside = SECTIONCULL: ะธะทะฒัŠะฝ ัะตั‡ะตะฝะธะตั‚ะพ ะฝัะผะฐ ะฝะธั‰ะพ. + .sectioncull-result = SECTIONCULL: ะธะทั‚ั€ะธั‚ะธ __ocs_arg_erased__ ะพะฑะตะบั‚ะฐ ะธ ะธะทั€ัะทะฐะฝะธ __ocs_arg_cut__ ะฟะพ ะณั€ะฐะฝะธั†ะฐั‚ะฐ ะฝะฐ ัะตั‡ะตะฝะธะตั‚ะพ. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: ัะฟะปะฐะนะฝ ะฟั€ะตะบะฐั€ะฐะฝ ะฟั€ะตะท __ocs_fmt_0__ ั‚ะพั‡ะบะธ. .splinefit-need-at-least-3-points = SPLINEFIT: ะฝัƒะถะฝะธ ัะฐ ะฟะพะฝะต 3 ั‚ะพั‡ะบะธ. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: ะธะทะฑะตั€ะธ ะฟะพะปะธะปะธะฝะธั, ะฟั€ะตะท ะบะพัั‚ะพ ะดะฐ ัะต ะฟั€ะตะบะฐั€ะฐ ัะฟะปะฐะนะฝ. @@ -858,9 +936,12 @@ common = .sent-to-printer-printer = ะ˜ะทะฟั€ะฐั‚ะตะฝะพ ะดะพ ะฟั€ะธะฝั‚ะตั€: __ocs_fmt_0__ .set-current = ะ—ะฐะดะฐะน ะบะฐั‚ะพ ั‚ะตะบัƒั‰ .shade-plot = ะŸะปะพั‚ะธั€ะฐะฝะต ััŠั ะทะฐัะตะฝั‡ะฒะฐะฝะต + .shaded-with-edges = ะ—ะฐัะตะฝั‡ะตะฝ ั ั€ัŠะฑะพะฒะต + .shades-of-gray = ะัŽะฐะฝัะธ ะฝะฐ ัะธะฒะพั‚ะพ .shadow-flags = ะคะปะฐะณะพะฒะต ะฝะฐ ัะตะฝะบะธ .single = ะ•ะดะธะฝะธั‡ะฝะพ .size = ะ ะฐะทะผะตั€ + .sketchy = ะกะบะธั†ะธั€ะฐะฝ .snap-override-label-next-pick-only = ะŸั€ะตัะบะฐั‡ะฐะฝะต ะฝะฐ ะทะฐะบะฐั‡ะฒะฐะฝะต: __ocs_fmt_0__ (ัะฐะผะพ ะทะฐ ัะปะตะดะฒะฐั‰ะพั‚ะพ ะธะทะฑะธั€ะฐะฝะต). .solid-created = ะขัะปะพั‚ะพ ะต ััŠะทะดะฐะดะตะฝะพ. .source-object-not-found = ะ˜ะทั…ะพะดะฝะธัั‚ ะพะฑะตะบั‚ ะฝะต ะต ะฝะฐะผะตั€ะตะฝ. @@ -1066,7 +1147,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: ะฒัŠะฒะตะดะตั‚ะต Left, Top ะธะปะธ Right. .isodraft-expected-on-or-off = ISODRAFT: ะฒัŠะฒะตะดะตั‚ะต On ะธะปะธ Off. .xdata-selected-entities-are-on-locked-layers = XDATA: ะธะทะฑั€ะฐะฝะธั‚ะต ะพะฑะตะบั‚ะธ ัะฐ ะฒ ะทะฐะบะปัŽั‡ะตะฝะธ ัะปะพะตะฒะต. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: ะฝัะผะฐ ะธะทะฑั€ะฐะฝะธ ั€ะฐัั‚ะตั€ะฝะธ ะธะทะพะฑั€ะฐะถะตะฝะธั ะธะปะธ ัะฒะพะนัั‚ะฒะพั‚ะพ ะต ะฝะตะธะทะฒะตัั‚ะฝะพ (ะธะทะฟะพะปะทะฒะฐะนั‚ะต BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: ะฒัŠะฒะตะดะตั‚ะต ั†ัะปะพ ั‡ะธัะปะพ ะพั‚ -4 ะดะพ 4. .usage-datalink-path-to-csv = ะฃะฟะพั‚ั€ะตะฑะฐ: DATALINK <ะฟัŠั‚-ะดะพ-.csv> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: ะฝัะผะฐ ะฝะฐะผะตั€ะตะฝะธ ัะฒัŠั€ะทะฐะฝะธ ั‚ะฐะฑะปะธั†ะธ. @@ -2059,7 +2139,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = ะคะฐะนะป .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2368,6 +2448,7 @@ common = .load-linetypes = ะ—ะฐั€ะตะถะดะฐะฝะต ะฝะฐ ั‚ะธะฟะพะฒะต ะปะธะฝะธะธ .requires-two-positive-values = ะ˜ะทะธัะบะฒะฐั‚ ัะต ะดะฒะต ะฟะพะปะพะถะธั‚ะตะปะฝะธ ัั‚ะพะนะฝะพัั‚ะธ (x,y) ะธะปะธ ะตะดะฝะฐ ะทะฐ ะดะฒะตั‚ะต. .snap-marker-color = ะฆะฒัั‚ ะฝะฐ ะผะฐั€ะบะตั€ะฐ ะทะฐ ะฟั€ะธั…ะฒะฐั‰ะฐะฝะต + .snap-marker-size = ะ ะฐะทะผะตั€ ะฝะฐ ะผะฐั€ะบะตั€ะฐ ะทะฐ ะฟั€ะธั…ะฒะฐั‰ะฐะฝะต .command-line-text-color = ะฆะฒัั‚ ะฝะฐ ั‚ะตะบัั‚ะฐ ะฒ ะบะพะผะฐะฝะดะฝะธั ั€ะตะด .operand = ะžะฟะตั€ะฐะฝะด .linetypes-loaded = ะ—ะฐั€ะตะดะตะฝะธ ั‚ะธะฟะพะฒะต ะปะธะฝะธะธ: __ocs_fmt_0__. @@ -2381,6 +2462,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE ะŸะพัะพั‡ะธ ะฑะฐะทะพะฒะฐ ั‚ะพั‡ะบะฐ [__ocs_arg_count__ ะพะฑะตะบั‚ะฐ]: .threedrotate-specify-rotation-axis = 3DROTATE ะŸะพัะพั‡ะตั‚ะต ะพั ะฝะฐ ะทะฐะฒัŠั€ั‚ะฐะฝะต [X/Y/Z] ะธะปะธ ะฒั‚ะพั€ะฐ ั‚ะพั‡ะบะฐ ะพั‚ ะฝะตั : .threedrotate-specify-rotation-angle-a = 3DROTATE ะŸะพัะพั‡ะธ ัŠะณัŠะป ะฝะฐ ะทะฐะฒัŠั€ั‚ะฐะฝะต <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = ะŸั€ะตะดัƒะฟั€ะตะถะดะตะฝะธะต ะฟั€ะธ ะทะฐะฟะฐะทะฒะฐะฝะต .unable-to-save-drawing = ะงะตั€ั‚ะตะถัŠั‚ ะฝะต ะผะพะถะต ะดะฐ ะฑัŠะดะต ะทะฐะฟะฐะทะตะฝ @@ -3004,6 +3092,7 @@ draw = .arc-specify-radius-of-arc = ARC ะ—ะฐะดะฐะนั‚ะต ั€ะฐะดะธัƒั ะฝะฐ ะดัŠะณะฐั‚ะฐ: .chord-length = ะ”ัŠะปะถะธะฝะฐ ะฝะฐ ั…ะพั€ะดะฐั‚ะฐ + modify = .add-vertex = ะ”ะพะฑะฐะฒัะฝะต ะฝะฐ ะฒั€ัŠั… .closed-cross = ะ—ะฐั‚ะฒะพั€ะตะฝ ะบั€ัŠัั‚ @@ -3049,6 +3138,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT ะ˜ะทะฑะตั€ะธ ะฑะปะพะบ ั ะฐั‚ั€ะธะฑัƒั‚ะธ: .annotation-object-scale = ะœะฐั‰ะฐะฑ ะฝะฐ ะฐะฝะพั‚ะฐั†ะธะพะฝะตะฝ ะพะฑะตะบั‚ .bedit-select-block-reference-to-edit = BEDIT ะ˜ะทะฑะตั€ะธ ะฟั€ะตะฟั€ะฐั‚ะบะฐ ะบัŠะผ ะฑะปะพะบ ะทะฐ ั€ะตะดะฐะบั‚ะธั€ะฐะฝะต: + .bvstate-option = BVSTATE ะ’ัŠะฒะตะดะตั‚ะต ะพะฟั†ะธั [ะะพะฒะพ/ะ—ะฐะดะฐะน/ะŸั€ะตะธะผะตะฝัƒะฒะฐะน/ะ˜ะทั‚ั€ะธะน] <ะ—ะฐะดะฐะน>: + .bvstate-new-name = BVSTATE ะ’ัŠะฒะตะดะตั‚ะต ะธะผะต ะฝะฐ ะฝะพะฒะพั‚ะพ ััŠัั‚ะพัะฝะธะต ะฝะฐ ะฒะธะดะธะผะพัั‚: + .bvstate-set = BVSTATE ะ’ัŠะฒะตะดะตั‚ะต ััŠัั‚ะพัะฝะธะตั‚ะพ ะฝะฐ ะฒะธะดะธะผะพัั‚, ะบะพะตั‚ะพ ะดะฐ ัั‚ะฐะฝะต ั‚ะตะบัƒั‰ะพ <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE ะ’ัŠะฒะตะดะตั‚ะต ััŠัั‚ะพัะฝะธะตั‚ะพ ะฝะฐ ะฒะธะดะธะผะพัั‚ ะทะฐ ะฟั€ะตะธะผะตะฝัƒะฒะฐะฝะต <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE ะ’ัŠะฒะตะดะตั‚ะต ะฝะพะฒะพั‚ะพ ะธะผะต: + .bvstate-delete = BVSTATE ะ’ัŠะฒะตะดะตั‚ะต ััŠัั‚ะพัะฝะธะตั‚ะพ ะฝะฐ ะฒะธะดะธะผะพัั‚ ะทะฐ ะธะทั‚ั€ะธะฒะฐะฝะต: + .bvstate-not-found = ะกัŠัั‚ะพัะฝะธะตั‚ะพ ะฝะฐ ะฒะธะดะธะผะพัั‚ "__ocs_fmt_0__" ะฝะต ะต ะฝะฐะผะตั€ะตะฝะพ. + .bvstate-exists = ะกัŠัั‚ะพัะฝะธะตั‚ะพ ะฝะฐ ะฒะธะดะธะผะพัั‚ "__ocs_fmt_0__" ะฒะตั‡ะต ััŠั‰ะตัั‚ะฒัƒะฒะฐ. + .bvstate-last = ะ•ะดะธะฝัั‚ะฒะตะฝะพั‚ะพ ััŠัั‚ะพัะฝะธะต ะฝะฐ ะฒะธะดะธะผะพัั‚ ะฝะต ะผะพะถะต ะดะฐ ะฑัŠะดะต ะธะทั‚ั€ะธั‚ะพ. + .bvstate-needs-bedit = __ocs_fmt_0__: ะฟัŠั€ะฒะพ ะพั‚ะฒะพั€ะตั‚ะต ะฑะปะพะบะฐ ะฒ ั€ะตะดะฐะบั‚ะพั€ะฐ ะฝะฐ ะฑะปะพะบะพะฒะต (BEDIT). + .bvstate-current = ะขะตะบัƒั‰ะพ ััŠัั‚ะพัะฝะธะต ะฝะฐ ะฒะธะดะธะผะพัั‚: __ocs_fmt_0__ + .bvhide-select = ะ˜ะทะฑะตั€ะตั‚ะต ะพะฑะตะบั‚ะธ ะทะฐ ัะบั€ะธะฒะฐะฝะต: + .bvshow-select = ะ˜ะทะฑะตั€ะตั‚ะต ะพะฑะตะบั‚ะธ ะทะฐ ะฟะพะบะฐะทะฒะฐะฝะต: + .arrayedit-select-array = ARRAYEDIT ะ˜ะทะฑะตั€ะตั‚ะต ะผะฐัะธะฒ: + .arrayedit-options = ARRAYEDIT ะ’ัŠะฒะตะดะตั‚ะต ะพะฟั†ะธั [ะ˜ะทั‚ะพั‡ะฝะธะบ/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: ะธะทะฑั€ะฐะฝะธัั‚ ะพะฑะตะบั‚ ะฝะต ะต ะฐัะพั†ะธะฐั‚ะธะฒะตะฝ ะผะฐัะธะฒ. .blend-enter-continuity-tangent-curvature-c = BLEND ะ’ัŠะฒะตะดะธ ะฝะตะฟั€ะตะบัŠัะฝะฐั‚ะพัั‚ [ะ”ะพะฟะธั€ะฐั‚ะตะปะฝะพัั‚/ะšั€ะธะฒะธะฝะฐ] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND ะ˜ะทะฑะตั€ะธ ะฟัŠั€ะฒะฐ ะพั‚ะฒะพั€ะตะฝะฐ ะบั€ะธะฒะฐ [ะะตะฟั€ะตะบัŠัะฝะฐั‚ะพัั‚=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND ะ˜ะทะฑะตั€ะธ ะฒั‚ะพั€ะฐ ะพั‚ะฒะพั€ะตะฝะฐ ะบั€ะธะฒะฐ [ะะตะฟั€ะตะบัŠัะฝะฐั‚ะพัั‚=__ocs_arg_c__]: @@ -3601,6 +3706,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE ะŸะพัะพั‡ะธ ั‚ะพั‡ะบะฐ ะฝะฐ ะฒะผัŠะบะฒะฐะฝะต: insert = + .pastespec-specify-insertion-point = PASTESPEC ะ—ะฐะดะฐะนั‚ะต ั‚ะพั‡ะบะฐ ะฝะฐ ะฒะผัŠะบะฒะฐะฝะต: + .pastespec-paste-as = PASTESPEC ะŸะพัั‚ะฐะฒัะฝะต ะบะฐั‚ะพ [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3717,6 +3824,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3771,6 +3885,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *ะŸั€ะธั…ะฒะฐั‰ะฐะฝะธัั‚ะฐ ะฝะฐ ะฟะพะดะปะพะถะบะธั‚ะต ัะต ั€ะฐะทะปะธั‡ะฐะฒะฐั‚* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3910,6 +4025,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = ะ˜ะทะฑะตั€ะตั‚ะต ะธะทะพะฑั€ะฐะถะตะฝะธะต ะธะปะธ ะฟะพะดะปะพะถะบะฐ: + .adjust-select-pdf-underlay = ะ˜ะทะฑะตั€ะตั‚ะต ะฟะพะดะปะพะถะบะฐ PDF: + .adjust-select-dwf-underlay = ะ˜ะทะฑะตั€ะตั‚ะต ะฟะพะดะปะพะถะบะฐ DWF: + .adjust-select-dgn-underlay = ะ˜ะทะฑะตั€ะตั‚ะต ะฟะพะดะปะพะถะบะฐ DGN: + .adjust-mixed-underlays = *ะะตะฒะฐะปะธะดะตะฝ ะฝะฐะฑะพั€ ะทะฐ ะธะทะฑะพั€. ะ˜ะทะฑะตั€ะตั‚ะต ัะฐะผะพ ะฟะพะดะปะพะถะบะธ ะพั‚ ะตะดะธะฝ ะธ ััŠั‰ ั„ะฐะนะปะพะฒ ั‚ะธะฟ.* + .adjust-image-option = ะ’ัŠะฒะตะดะตั‚ะต ะพะฟั†ะธั ะทะฐ ะธะทะพะฑั€ะฐะถะตะฝะธะต [ะšะพะฝั‚ั€ะฐัั‚/ะ—ะฐะณะปัƒัˆะฐะฒะฐะฝะต/ะฏั€ะบะพัั‚] <ะฏั€ะบะพัั‚>: + .adjust-underlay-option = ะ’ัŠะฒะตะดะตั‚ะต ะพะฟั†ะธั ะทะฐ ะฟะพะดะปะพะถะบะฐ __ocs_arg_kind__ [ะ—ะฐะณะปัƒัˆะฐะฒะฐะฝะต/ะšะพะฝั‚ั€ะฐัั‚/ะœะพะฝะพั…ั€ะพะผะฝะพ] <__ocs_arg_option__>: + .adjust-monochrome = ะœะพะฝะพั…ั€ะพะผะฝะพ? [ะ”ะฐ/ะะต] <__ocs_arg_value__>: + .adjust-brightness-value = ะ’ัŠะฒะตะดะตั‚ะต ัั‚ะพะนะฝะพัั‚: ัั€ะบะพัั‚ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = ะ’ัŠะฒะตะดะตั‚ะต ัั‚ะพะนะฝะพัั‚: ะบะพะฝั‚ั€ะฐัั‚ (0-100) <__ocs_arg_value__>: + .adjust-fade-value = ะ’ัŠะฒะตะดะตั‚ะต ัั‚ะพะนะฝะพัั‚: ะทะฐะณะปัƒัˆะฐะฒะฐะฝะต (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = ะกั‚ะพะนะฝะพัั‚ั‚ะฐ ั‚ั€ัะฑะฒะฐ ะดะฐ ะต ั†ัะปะพ ั‡ะธัะปะพ ะผะตะถะดัƒ 0 ะธ 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4414,7 +4541,27 @@ model = .value-2point = 2 ั‚ะพั‡ะบะธ .cube = ะšัƒะฑ + layout = + .mvsetup-select-objects-to-delete = MVSETUP ะ˜ะทะฑะตั€ะตั‚ะต ะพะฑะตะบั‚ะธั‚ะต ะทะฐ ะธะทั‚ั€ะธะฒะฐะฝะต: + .delete-objects-option = ะ˜ะทั‚ั€ะธะฒะฐะฝะต ะฝะฐ ะพะฑะตะบั‚ะธ + .mvsetup-enable-paper-space = MVSETUP ะะบั‚ะธะฒะธั€ะฐะฝะต ะฝะฐ ั…ะฐั€ั‚ะธะตะฝะพั‚ะพ ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะพ? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP ะ’ัŠะฒะตะดะตั‚ะต ะผะฐั‰ะฐะฑะตะฝ ะบะพะตั„ะธั†ะธะตะฝั‚ <1>: + .mvsetup-enter-paper-width = MVSETUP ะ’ัŠะฒะตะดะตั‚ะต ัˆะธั€ะธะฝะฐ ะฝะฐ ะปะธัั‚ะฐ: + .mvsetup-enter-paper-height = MVSETUP ะ’ัŠะฒะตะดะตั‚ะต ะฒะธัะพั‡ะธะฝะฐ ะฝะฐ ะปะธัั‚ะฐ: + .mvsetup-enter-option = MVSETUP ะ’ัŠะฒะตะดะตั‚ะต ะพะฟั†ะธั [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP ะ’ัŠะฒะตะดะตั‚ะต ะฟะพะดั€ะตะดะฑะฐ ะฝะฐ ะฟั€ะพะทะพั€ั†ะธั‚ะต [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP ะ—ะฐะดะฐะนั‚ะต ะฟัŠั€ะฒะธ ัŠะณัŠะป ะฝะฐ ะพะฑะปะฐัั‚ั‚ะฐ ะทะฐ ะฟั€ะพะทะพั€ั†ะธั‚ะต : + .mvsetup-specify-opposite-corner = MVSETUP ะ—ะฐะดะฐะนั‚ะต ะฟั€ะพั‚ะธะฒะพะฟะพะปะพะถะตะฝ ัŠะณัŠะป: + .mvsetup-viewports-in-x = MVSETUP ะ’ัŠะฒะตะดะตั‚ะต ะฑั€ะพะน ะฟั€ะพะทะพั€ั†ะธ ะฟะพ X <2>: + .mvsetup-viewports-in-y = MVSETUP ะ’ัŠะฒะตะดะตั‚ะต ะฑั€ะพะน ะฟั€ะพะทะพั€ั†ะธ ะฟะพ Y <2>: + .mvsetup-distance-in-x = MVSETUP ะ—ะฐะดะฐะนั‚ะต ั€ะฐะทัั‚ะพัะฝะธะต ะผะตะถะดัƒ ะฟั€ะพะทะพั€ั†ะธั‚ะต ะฟะพ X <0>: + .mvsetup-distance-in-y = MVSETUP ะ—ะฐะดะฐะนั‚ะต ั€ะฐะทัั‚ะพัะฝะธะต ะผะตะถะดัƒ ะฟั€ะพะทะพั€ั†ะธั‚ะต ะฟะพ Y <0>: + .mvsetup-select-viewports = MVSETUP ะ˜ะทะฑะตั€ะตั‚ะต ะฟั€ะพะทะพั€ั†ะธั‚ะต ะทะฐ ะผะฐั‰ะฐะฑะธั€ะฐะฝะต: + .mvsetup-paper-space-units = MVSETUP ะ’ัŠะฒะตะดะตั‚ะต ะฑั€ะพั ะตะดะธะฝะธั†ะธ ะฒ ั…ะฐั€ั‚ะธะตะฝะพั‚ะพ ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะพ <1>: + .mvsetup-model-space-units = MVSETUP ะ’ัŠะฒะตะดะตั‚ะต ะฑั€ะพั ะตะดะธะฝะธั†ะธ ะฒ ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะพั‚ะพ ะฝะฐ ะผะพะดะตะปะฐ <1>: + .mvsetup-viewports-do-not-fit = MVSETUP ะŸั€ะพะทะพั€ั†ะธั‚ะต ะฝะต ัะต ะฟะพะฑะธั€ะฐั‚ ะฒ ะพะฑะปะฐัั‚ั‚ะฐ. + .scale-viewports-option = ะœะฐั‰ะฐะฑะธั€ะฐะฝะต ะฝะฐ ะฟั€ะพะทะพั€ั†ะธั‚ะต .active = ะะบั‚ะธะฒะฝะพ .inactive = ะะตะฐะบั‚ะธะฒะฝะพ .insert-view = ะ’ะผัŠะบะฝะธ ะธะทะณะปะตะด @@ -4440,6 +4587,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F <ัะปะพะน> = ะ—ะฐะผั€ะฐะทะธ | T <ัะปะพะน> = ะ ะฐะทะผั€ะฐะทะธ | Enter = ะ˜ะทั…ะพะด view = + .twist-option = ะ—ะฐะฒัŠั€ั‚ะฐะฝะต + .dview-specify-view-twist-angle = DVIEW ะ—ะฐะดะฐะนั‚ะต ัŠะณัŠะป ะฝะฐ ะทะฐะฒัŠั€ั‚ะฐะฝะต ะฝะฐ ะธะทะณะปะตะดะฐ <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW ะ’ัŠะฒะตะดะตั‚ะต ะพะฟั†ะธั [TWist/Undo]: .color-scheme-set-to-system-currently-name = ะฆะฒะตั‚ะพะฒะฐั‚ะฐ ัั…ะตะผะฐ ะต ะทะฐะดะฐะดะตะฝะฐ ะฝะฐ โ€žะกะธัั‚ะตะผะฝะฐโ€œ (ะฒ ะผะพะผะตะฝั‚ะฐ __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = ะ˜ะทะธัะบะฒะฐ ัะต ั†ัะปะพ ั‡ะธัะปะพ ะผะตะถะดัƒ __ocs_fmt_0__ ะธ __ocs_fmt_1__. @@ -4634,13 +4784,6 @@ view = .x-no-xdata = __ocs_fmt_0__: ะฝัะผะฐ xdata. .value-3d-orbit = 3D ะžั€ะฑะธั‚ะฐ .value-3d-orbit-drag-with-right-mouse-button = 3D ะžั€ะฑะธั‚ะฐ: ะฒะปะฐั‡ะธ ั ะดะตัะตะฝ ะฑัƒั‚ะพะฝ ะฝะฐ ะผะธัˆะบะฐั‚ะฐ. - .adjust-brightness-contrast-fade = ADJUST [ะฏั€ะบะพัั‚ / ะšะพะฝั‚ั€ะฐัั‚ / ะ—ะฐะณะปัƒัˆะฐะฒะฐะฝะต]: - .adjust-brightness-0-100 = ADJUST ัั€ะบะพัั‚ 0-100: - .adjust-contrast-0-100 = ADJUST ะบะพะฝั‚ั€ะฐัั‚ 0-100: - .adjust-fade-0-100 = ADJUST ะทะฐะณะปัƒัˆะฐะฒะฐะฝะต 0-100: - .adjust-select-raster-image-s-first = ADJUST: ะฟัŠั€ะฒะพ ะธะทะฑะตั€ะธ ั€ะฐัั‚ะตั€ะฝะพ ะธะทะพะฑั€ะฐะถะตะฝะธะต(ั). - .adjust-value-must-be-0-100 = ADJUST: ัั‚ะพะนะฝะพัั‚ั‚ะฐ ั‚ั€ัะฑะฒะฐ ะดะฐ ะต 0-100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ ะฝะฐ __ocs_fmt_2__ ะธะทะพะฑั€ะฐะถะตะฝะธะต(ั). .annoallvisible-new-value-0-1 = ANNOALLVISIBLE ะฝะพะฒะฐ ัั‚ะพะนะฝะพัั‚ [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: ะฒัŠะฒะตะดะธ 0 ะธะปะธ 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE ะฝะพะฒะฐ ัั‚ะพะนะฝะพัั‚ [-4..4]: @@ -4766,7 +4909,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = ะŸะพะดะปะพะถะบะฐ __ocs_fmt_0__: ะทะฐะณะปัƒัˆะฐะฒะฐะฝะต=__ocs_fmt_1__, ะบะพะฝั‚ั€ะฐัั‚=__ocs_fmt_2__, ะฒะบะป.=__ocs_fmt_3__, ะธะทั€ัะทะฒะฐะฝะต=__ocs_fmt_4__, ะผะพะฝะพ=__ocs_fmt_5__ .updated-changed-underlay-s = ะžะฑะฝะพะฒะตะฝะธ __ocs_fmt_0__ ะฟะพะดะปะพะถะบะฐ(ะธ). - .usage-adjust-brightness-contrast-fade-0-100 = ะฃะฟะพั‚ั€ะตะฑะฐ: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = ะฃะฟะพั‚ั€ะตะฑะฐ: ANNOSCALE <ััŠะพั‚ะฝะพัˆะตะฝะธะต> ะฝะฐะฟั€. 1:50, 2:1, ะธะปะธ ั„ะฐะบั‚ะพั€ .usage-draworder-f-front-b-back-a-above-handle = ะฃะฟะพั‚ั€ะตะฑะฐ: DRAWORDER F|FRONT | B|BACK | A|ABOVE <ะผะฐะฝะธะฟัƒะปะฐั‚ะพั€> | U|UNDER <ะผะฐะฝะธะฟัƒะปะฐั‚ะพั€> .usage-hyperlink-url-select-objects-first = ะฃะฟะพั‚ั€ะตะฑะฐ: HYPERLINK (ะฟัŠั€ะฒะพ ะธะทะฑะตั€ะธ ะพะฑะตะบั‚ะธ) @@ -5995,6 +6137,46 @@ ui = .osnap-settings = ะะฐัั‚ั€ะพะนะบะธ ะฝะฐ ะฟั€ะธั…ะฒะฐั‰ะฐะฝะต... .snap-override-none-next-pick-only = ะ’ั€ะตะผะตะฝะฝะพ ะฟั€ะธั…ะฒะฐั‰ะฐะฝะต: ะัะผะฐ (ัะฐะผะพ ะทะฐ ัะปะตะดะฒะฐั‰ะฐั‚ะฐ ั‚ะพั‡ะบะฐ). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = ะ’ะธะฝะฐะณะธ ะฟะฐะทะธ ั‚ะตะบัั‚ะฐ ะผะตะถะดัƒ ัƒะดัŠะปะถะธั‚ะตะปะฝะธั‚ะต ะปะธะฝะธะธ .angular-dimensions = ะชะณะปะพะฒะธ ะพั€ะฐะทะผะตั€ัะฒะฐะฝะธั @@ -6262,3 +6444,117 @@ graphic-attributes = .reset-angle = ะัƒะปะธั€ะฐะฝะต ะฝะฐ ัŠะณัŠะปะฐ .one-color = ะ•ะดะธะฝ ั†ะฒัั‚ .two-colors = ะ”ะฒะฐ ั†ะฒัั‚ะฐ + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = ะ”ะพะฑะฐะฒะธ ะบัŠะผ ั€ะฐะฑะพั‚ะฝะธั ะฝะฐะฑะพั€ + .remove-from-working-set = ะŸั€ะตะผะฐั…ะฝะธ ะพั‚ ั€ะฐะฑะพั‚ะฝะธั ะฝะฐะฑะพั€ + .toolbar-objects = ะพะฑะตะบั‚ะฐ + .dialog-title = ะ ะตะดะฐะบั‚ะธั€ะฐะฝะต ะฝะฐ ะฟั€ะตะฟั€ะฐั‚ะบะฐ + .dialog-reference = ะŸั€ะตะฟั€ะฐั‚ะบะฐ ะทะฐ ั€ะตะดะฐะบั‚ะธั€ะฐะฝะต + .dialog-chain-hint = ะ’ะตั€ะธะณะฐั‚ะฐ ะฒ ะธะทะฑั€ะฐะฝะฐั‚ะฐ ั‚ะพั‡ะบะฐ; ั‰ั€ะฐะบะฝะตั‚ะต ะฒัŠั€ั…ัƒ ะตั‚ะธะบะตั‚, ะทะฐ ะดะฐ ัะผะตะฝะธั‚ะต ะฝะธะฒะพั‚ะพ. + .dialog-path = ะŸัŠั‚: + .dialog-working-set = ะ ะฐะฑะพั‚ะตะฝ ะฝะฐะฑะพั€ + .dialog-all-objects = ะ’ัะธั‡ะบะธ ะพะฑะตะบั‚ะธ + .dialog-pick-objects = ะ˜ะทะฑะพั€ ะฒ ั‡ะตั€ั‚ะตะถะฐ + .dialog-attributes = ะ ะตะดะฐะบั‚ะธั€ะฐะฝะต ะธ ะฝะฐ ะดะตั„ะธะฝะธั†ะธะธั‚ะต ะฝะฐ ะฐั‚ั€ะธะฑัƒั‚ะธ + .dialog-session = ะกะตัะธั + .dialog-lock = ะ—ะฐะบะปัŽั‡ะฒะฐะฝะต ะฝะฐ ะพะฑะตะบั‚ะธั‚ะต ะธะทะฒัŠะฝ ั€ะฐะฑะพั‚ะฝะธั ะฝะฐะฑะพั€ + .dialog-unique = ะฃะฝะธะบะฐะปะฝะธ ะธะผะตะฝะฐ ะทะฐ ะบะพะฝั„ะปะธะบั‚ะฝะธ ัะปะพะตะฒะต, ัั‚ะธะปะพะฒะต ะธ ะฑะปะพะบะพะฒะต + .dialog-start = ะ—ะฐะฟะพั‡ะฝะธ ั€ะตะดะฐะบั‚ะธั€ะฐะฝะต + .dialog-help = ะžั‚ะฒะฐั€ั ะฟั€ะตะฟั€ะฐั‚ะบะฐ ะบัŠะผ ะฑะปะพะบ ะธะปะธ ะฒัŠะฝัˆะฝะฐ ะฟั€ะตะฟั€ะฐั‚ะบะฐ ะทะฐ ั€ะตะดะฐะบั‚ะธั€ะฐะฝะต ะฝะฐ ะผััั‚ะพ. + .select-reference = ะ˜ะทะฑะตั€ะตั‚ะต ะฟั€ะตะฟั€ะฐั‚ะบะฐ: + .reference-not-found = ะŸั€ะตะฟั€ะฐั‚ะบะฐั‚ะฐ ะฝะต ะต ะฝะฐะผะตั€ะตะฝะฐ. + .not-valid-reference = ะžะฑะตะบั‚ัŠั‚ ะฝะต ะต ะฒะฐะปะธะดะฝะฐ ะฟั€ะตะฟั€ะฐั‚ะบะฐ + .one-entity-added = ะ”ะพะฑะฐะฒะตะฝ ะต 1 ะพะฑะตะบั‚ + .nesting-level = ะ˜ะทะฑะตั€ะตั‚ะต ะฝะธะฒะพ ะฝะฐ ะฒะปะฐะณะฐะฝะต [ะžะบ/ะกะปะตะดะฒะฐั‰ะพ] <ะกะปะตะดะฒะฐั‰ะพ>: + .selection-method = ะ’ัŠะฒะตะดะตั‚ะต ะผะตั‚ะพะด ะทะฐ ะธะทะฑะพั€ ะฝะฐ ะพะฑะตะบั‚ะธ [ะ’ัะธั‡ะบะธ/ะ’ะปะพะถะตะฝะธ] <ะ’ัะธั‡ะบะธ>: + .select-nested = ะ˜ะทะฑะตั€ะตั‚ะต ะฒะปะพะถะตะฝะธ ะพะฑะตะบั‚ะธ: + .display-attdefs = ะŸะพะบะฐะทะฒะฐะฝะต ะฝะฐ ะดะตั„ะธะฝะธั†ะธะธ ะฝะฐ ะฐั‚ั€ะธะฑัƒั‚ะธ [ะ”ะฐ/ะะต] <ะะต>: + .zero-items-selected = ะ˜ะทะฑั€ะฐะฝะธ ัะฐ 0 ะตะปะตะผะตะฝั‚ะฐ + .one-item-selected = ะ˜ะทะฑั€ะฐะฝ ะต 1 ะตะปะตะผะตะฝั‚ + .items-selected = ะ˜ะทะฑั€ะฐะฝะธ ะตะปะตะผะตะฝั‚ะธ: __ocs_fmt_0__ + .refset-option = ะ’ัŠะฒะตะดะตั‚ะต ะพะฟั†ะธั [ะ”ะพะฑะฐะฒะธ/ะŸั€ะตะผะฐั…ะฝะธ] <ะ”ะพะฑะฐะฒะธ>: + .refclose-option = ะ’ัŠะฒะตะดะตั‚ะต ะพะฟั†ะธั [ะ—ะฐะฟะธัˆะธ/ะžั‚ั…ะฒัŠั€ะปะธ ะฟั€ะพะผะตะฝะธั‚ะต ะฒ ะฟั€ะตะฟั€ะฐั‚ะบะฐั‚ะฐ] <ะ—ะฐะฟะธัˆะธ>: + .not-checked-out = ** ะšะพะผะฐะฝะดะฐั‚ะฐ ะฝะต ะต ั€ะฐะทั€ะตัˆะตะฝะฐ, ะพัะฒะตะฝ ะฐะบะพ ะฟั€ะตะฟั€ะฐั‚ะบะฐ ะฝะต ัะต ั€ะตะดะฐะบั‚ะธั€ะฐ ั REFEDIT ** + .already-checked-out = ** ะšะพะผะฐะฝะดะฐั‚ะฐ ะฝะต ะต ั€ะฐะทั€ะตัˆะตะฝะฐ, __ocs_fmt_0__ ะฒะตั‡ะต ัะต ั€ะตะดะฐะบั‚ะธั€ะฐ ** + .transfer-objects = ะŸั€ะตั…ะฒัŠั€ะปัะฝะต ะฝะฐ ะพะฑะตะบั‚ะธ ะผะตะถะดัƒ ั€ะฐะฑะพั‚ะฝะธั ะฝะฐะฑะพั€ ะฝะฐ RefEdit ะธ ะพัะฝะพะฒะฝะธั ั‡ะตั€ั‚ะตะถ... + .added-to-working-set = __ocs_fmt_0__ ะดะพะฑะฐะฒะตะฝะธ ะบัŠะผ ั€ะฐะฑะพั‚ะฝะธั ะฝะฐะฑะพั€ + .removed-from-working-set = __ocs_fmt_0__ ะฟั€ะตะผะฐั…ะฝะฐั‚ะธ ะพั‚ ั€ะฐะฑะพั‚ะฝะธั ะฝะฐะฑะพั€ + .one-object-added = 1 ะพะฑะตะบั‚ ะดะพะฑะฐะฒะตะฝ ะบัŠะผ __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ ะพะฑะตะบั‚ะฐ ะดะพะฑะฐะฒะตะฝะธ ะบัŠะผ __ocs_fmt_1__ + .one-object-removed = 1 ะพะฑะตะบั‚ ะฟั€ะตะผะฐั…ะฝะฐั‚ ะพั‚ __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ ะพะฑะตะบั‚ะฐ ะฟั€ะตะผะฐั…ะฝะฐั‚ะธ ะพั‚ __ocs_fmt_1__ + .one-xref-instance = 1 ะตะบะทะตะผะฟะปัั€ ะฝะฐ xref ะพะฑะฝะพะฒะตะฝ + .xref-instances = __ocs_fmt_0__ ะตะบะทะตะผะฟะปัั€ะฐ ะฝะฐ xref ะพะฑะฝะพะฒะตะฝะธ + .one-block-instance = 1 ะตะบะทะตะผะฟะปัั€ ะฝะฐ ะฑะปะพะบ ะพะฑะฝะพะฒะตะฝ + .block-instances = __ocs_fmt_0__ ะตะบะทะตะผะฟะปัั€ะฐ ะฝะฐ ะฑะปะพะบ ะพะฑะฝะพะฒะตะฝะธ + .redefined-reloaded = __ocs_fmt_0__ ะต ะฟั€ะตะดะตั„ะธะฝะธั€ะฐะฝ ะธ ะฟั€ะตะทะฐั€ะตะดะตะฝ. + .redefined = __ocs_fmt_0__ ะต ะฟั€ะตะดะตั„ะธะฝะธั€ะฐะฝ. + .ok = ะžะบ + .nested = ะ’ะปะพะถะตะฝะธ + +image-adjust = + .title = ะะฐัั‚ั€ะพะนะบะฐ ะฝะฐ ะธะทะพะฑั€ะฐะถะตะฝะธะต + .preview = ะŸั€ะตะณะปะตะด + .before = ะŸั€ะตะดะธ + .after = ะกะปะตะด + .no-preview = ะธะทะพะฑั€ะฐะถะตะฝะธะตั‚ะพ ะฝะต ะผะพะถะต ะดะฐ ะฑัŠะดะต ะทะฐั€ะตะดะตะฝะพ + .adjust = ะะฐัั‚ั€ะพะนะบะฐ + .brightness = ะฏั€ะบะพัั‚ + .contrast = ะšะพะฝั‚ั€ะฐัั‚ + .fade = ะ˜ะทะฑะปะตะดะฝัะฒะฐะฝะต + .hint = ะฉั€ะฐะบะฝะตั‚ะต ะธะปะธ ะฟะปัŠะทะฝะตั‚ะต ะปะตะฝั‚ะฐ; โˆ’ / + ะต ะตะดะฝะฐ ัั‚ัŠะฟะบะฐ, ะฟะพะปะตั‚ะพ ะฟั€ะธะตะผะฐ ั†ัะปะพ ั‡ะธัะปะพ 0โ€“100. + .reset = ะ’ัŠะทัั‚ะฐะฝะพะฒะธ ัั‚ะฐะฝะดะฐั€ั‚ะฝะธั‚ะต + .apply = ะŸั€ะธะปะพะถะธ + .help = ะ—ะฐะดะฐะฒะฐ ัั€ะบะพัั‚ั‚ะฐ, ะบะพะฝั‚ั€ะฐัั‚ะฐ ะธ ะธะทะฑะปะตะดะฝัะฒะฐะฝะตั‚ะพ ะฝะฐ ะธะทะฑั€ะฐะฝะธั‚ะต ั€ะฐัั‚ะตั€ะฝะธ ะธะทะพะฑั€ะฐะถะตะฝะธั. + +keytips = + .title = KeyTip + .typed = ะ’ัŠะฒะตะดะตะฝะพ: { $path } + .hint-tabs = ะ’ัŠะฒะตะดะตั‚ะต ะฑัƒะบะฒะฐ ะฝะฐ ั€ะฐะทะดะตะป ยท Esc ะทะฐั‚ะฒะฐั€ั + .hint-tab = ะ ะฐะทะดะตะป { $tab } ยท Esc ะฒั€ัŠั‰ะฐ ะตะดะฝะพ ะฝะธะฒะพ ะฝะฐะทะฐะด + .hint-items = ะ’ัŠะฒะตะดะตั‚ะต ะฑัƒะบะฒะฐ ะฝะฐ ะตะปะตะผะตะฝั‚ ยท Esc ะฒั€ัŠั‰ะฐ ะตะดะฝะพ ะฝะธะฒะพ ะฝะฐะทะฐะด diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 51e95ebef..94a425dc6 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Zรกsuvnรฉ moduly ribbon-tab = + .block-editor = Editor blokลฏ .draw = Kreslenรญ .parametric = Parametrickรฉ .annotate = Poznรกmky @@ -98,6 +99,9 @@ start = .no-recent-files = Otevล™enรฉ soubory se zobrazรญ zde. .browser-storage = รšloลพiลกtฤ› prohlรญลพeฤe .keep-recent-files = Uchovรกvat nedรกvnรฉ soubory + .view-list = Seznam + .view-icons = Ikony + .view-preview = Velkรฉ nรกhledy donation = .title = ลฝรกdost o dar @@ -167,6 +171,14 @@ modal = .that-object = danรฝ objekt .those-objects = danรฉ objekty + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Vloลพen obrรกzek "{ $name }": { $width }ร—{ $height } px .image-unsupported = Vloลพenรฝ obrรกzek "{ $name }" se nepodaล™ilo dekรณdovat. @@ -273,11 +285,73 @@ command-copy = .mode = COPY Zadejte reลพim kopรญrovรกnรญ [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ jen zde (zelenฤ›), __ocs_fmt_1__ jen v druhรฉm vรฝkresu (ฤervenฤ›)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Porovnat s vรฝkresem + .label-associative-array-created = __ocs_fmt_0__: vytvoล™eno asociativnรญ pole. + .assoc-array-array-path = Pole (podรฉl cesty) + .assoc-array-array-polar = Pole (kruhovรฉ) + .assoc-array-array-rectangular = Pole (obdรฉlnรญkovรฉ) + .assoc-array-item-spacing = Rozteฤ poloลพek + .assoc-array-fill-angle = รšhel vyplnฤ›nรญ + .assoc-array-items = Poloลพky + .assoc-array-level-spacing = Rozteฤ รบrovnรญ + .assoc-array-levels = รšrovnฤ› + .assoc-array-row-spacing = Rozteฤ ล™รกdkลฏ + .assoc-array-column-spacing = Rozteฤ sloupcลฏ + .fbxexport-exported-to = FBXEXPORT: exportovรกno do "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: v tomto vรฝkresu nejsou ลพรกdnรก data 3D sรญtฤ›. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Novรฉ ลกrafy sledujรญ svรฉ hranice (HPASSOC) + .options-show-ribbon = Zobrazit pรกs karet (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Zobrazit panel nabรญdek (MENUBAR) + .options-window-elements = Prvky okna + .menu-viewports = Vรฝล™ezy + .menu-view-twist = Natoฤenรญ pohledu + .menu-tools = Nรกstroje + .menu-table-style = Styl tabulky + .menu-run-script = Spustit skript + .menu-revision-cloud = Reviznรญ oblรกฤek + .menu-regen-all = Regenerovat vลกe + .menu-redraw = Pล™ekreslit + .menu-rectangle = Obdรฉlnรญk + .menu-quick-dimension = Rychlรก kรณta + .menu-publish = Publikovat + .menu-paste-special = Vloลพit jinak + .menu-parametric = Parametrickรฉ + .menu-page-setup-manager = Sprรกvce nastavenรญ strรกnky + .menu-orbit = Orbit + .menu-new-ucs = Novรฝ USS + .menu-named-views = Pojmenovanรฉ pohledy + .menu-multiline-text = Vรญceล™รกdkovรฝ text + .menu-multiline-style = Styl multiฤรกry + .menu-multiline = Multiฤรกra + .menu-multileader-style = Styl vรญcenรกsobnรฉ odkazovรฉ ฤรกry + .menu-match-properties = Pล™izpลฏsobit vlastnosti + .menu-lengthen = Prodlouลพit + .menu-jogged = Zalomenรฝ + .menu-id-point = Souล™adnice bodu + .menu-explode-text = Rozloลพit text + .menu-edit-spline = Upravit splajn + .menu-edit-polyline = Upravit kล™ivku + .menu-edit-hatch = Upravit ลกrafy + .menu-drawing-limits = Meze vรฝkresu + .menu-donut = Mezikruลพรญ + .menu-dimension-style = Kรณtovacรญ styl + .menu-construction-line = Konstrukฤnรญ ฤรกra + .menu-arc-length = Dรฉlka oblouku + .command-line-opacity = Neprลฏhlednost pล™รญkazovรฉho ล™รกdku + .txtexp-no-text-selected = TXTEXP: nenรญ vybrรกn ลพรกdnรฝ text. + .angle-override-shown = Pล™epsรกnรญ รบhlu: __ocs_fmt_0__ .plot-preview-task = Nรกhled tisku .link-a-csv-data-source = Propojit zdroj dat CSV .csv-files = Soubory CSV .browser-cannot-open-typed-path = Prohlรญลพeฤ nemลฏลพe otevล™รญt zadanou cestu; vyberte soubor. .recover-drawing = Obnovit vรฝkres + .requires-0-or-1-or-keyword = Vyลพaduje pouze 0 nebo 1, nebo klรญฤovรฉ slovo volby. + .requires-a-number = Vyลพaduje ฤรญslo. .restore-it-as-a-recovered-tab-or-discard-the-copy = Obnovte jej jako obnovenou kartu nebo kopii zahoฤte. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" mรก kopii automatickรฉho uloลพenรญ novฤ›jลกรญ neลพ poslednรญ uloลพenรญ. .autosave-copy-could-not-be-stored = Kopii automatickรฉho uloลพenรญ nelze uloลพit: __ocs_fmt_0__ @@ -821,6 +895,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: vyberte objekt ล™ezu nebo zapnฤ›te ลพivรฝ ล™ez. + .sectioncull-prepare-failed = SECTIONCULL: oล™รญznutรฝ objekt nelze pล™ipravit; nic nebylo zmฤ›nฤ›no. + .sectioncull-nothing-outside = SECTIONCULL: mimo ล™ez nic neleลพรญ. + .sectioncull-result = SECTIONCULL: vymazรกno __ocs_arg_erased__ objekt(ลฏ) a oล™รญznuto __ocs_arg_cut__ na hranici ล™ezu. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: proloลพenรญ spline pล™es body __ocs_fmt_0__. .splinefit-need-at-least-3-points =SPLINEFIT: potล™ebujete alespoลˆ 3 body. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: vyberte kล™ivku pro proloลพenรญ spline. @@ -858,9 +936,12 @@ common = .sent-to-printer-printer = Odeslรกno do tiskรกrny: __ocs_fmt_0__ .set-current = Nastavit aktuรกlnรญ .shade-plot = Stรญnovรฝ pozemek + .shaded-with-edges = Stรญnovanรฝ s hranami + .shades-of-gray = Odstรญny ลกedi .shadow-flags = Stรญnovรฉ vlajky .single = Svobodnรฝ .size = Velikost + .sketchy = Nรกฤrt .snap-override-label-next-pick-only = Pล™epsรกnรญ uchopenรญ: __ocs_fmt_0__ (pouze dalลกรญ vรฝbฤ›r). .solid-created = Pevnรก lรกtka vytvoล™ena. .source-object-not-found = Zdrojovรฝ objekt nebyl nalezen. @@ -1066,7 +1147,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: zadejte Left, Top nebo Right. .isodraft-expected-on-or-off = ISODRAFT: zadejte On nebo Off. .xdata-selected-entities-are-on-locked-layers = XDATA: vybranรฉ objekty jsou v uzamฤenรฝch hladinรกch. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: nejsou vybrรกny rastrovรฉ obrรกzky nebo je vlastnost neznรกmรก (pouลพijte BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: zadejte celรฉ ฤรญslo od -4 do 4. .usage-datalink-path-to-csv = Pouลพitรญ: DATALINK .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: nebyly nalezeny propojenรฉ tabulky. @@ -2059,7 +2139,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = Soubor .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2368,6 +2448,7 @@ common = .load-linetypes = Naฤรญst typy ฤar .requires-two-positive-values = Vyลพaduje dvฤ› kladnรฉ hodnoty (x,y) nebo jednu pro obฤ›. .snap-marker-color = Barva znaฤky uchopenรญ + .snap-marker-size = Velikost znaฤky uchopenรญ .command-line-text-color = Barva textu pล™รญkazovรฉho ล™รกdku .operand = Operand .linetypes-loaded = Naฤteno typลฏ ฤar: __ocs_fmt_0__. @@ -2381,6 +2462,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Zadejte zรกkladnรญ bod [objekty __ocs_arg_count__]: .threedrotate-specify-rotation-axis = 3DROTATE Zadejte osu otรกฤenรญ [X/Y/Z] nebo druhรฝ bod na nรญ : .threedrotate-specify-rotation-angle-a = 3DROTATE Zadejte รบhel natoฤenรญ <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Uloลพit varovรกnรญ .unable-to-save-drawing = Nรกkres nelze uloลพit @@ -3004,6 +3092,7 @@ draw = .arc-specify-radius-of-arc = ARC Zadejte polomฤ›r oblouku: .chord-length = Dรฉlka tฤ›tivy + modify = .add-vertex = Pล™idat vrchol .closed-cross = Uzavล™enรฝ kล™รญลพ @@ -3049,6 +3138,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Vyberte blok s atributy: .annotation-object-scale = Mฤ›ล™รญtko objektu anotace .bedit-select-block-reference-to-edit = BEDIT Vyberte referenci bloku, kterou chcete upravit: + .bvstate-option = BVSTATE Zadejte moลพnost [Novรฝ/Nastavit/Pล™ejmenovat/Odstranit] : + .bvstate-new-name = BVSTATE Zadejte nรกzev novรฉho stavu viditelnosti: + .bvstate-set = BVSTATE Zadejte stav viditelnosti, kterรฝ se stane aktuรกlnรญm <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Zadejte stav viditelnosti k pล™ejmenovรกnรญ <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Zadejte novรฝ nรกzev: + .bvstate-delete = BVSTATE Zadejte stav viditelnosti k odstranฤ›nรญ: + .bvstate-not-found = Stav viditelnosti "__ocs_fmt_0__" nebyl nalezen. + .bvstate-exists = Stav viditelnosti "__ocs_fmt_0__" jiลพ existuje. + .bvstate-last = Jedinรฝ stav viditelnosti nelze odstranit. + .bvstate-needs-bedit = __ocs_fmt_0__: nejprve otevล™ete blok v editoru blokลฏ (BEDIT). + .bvstate-current = Aktuรกlnรญ stav viditelnosti: __ocs_fmt_0__ + .bvhide-select = Vyberte objekty ke skrytรญ: + .bvshow-select = Vyberte objekty k zobrazenรญ: + .arrayedit-select-array = ARRAYEDIT Vyberte pole: + .arrayedit-options = ARRAYEDIT Zadejte moลพnost [Zdroj/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: vybranรฝ objekt nenรญ asociativnรญ pole. .blend-enter-continuity-tangent-curvature-c = BLEND Enter spojitost [Teฤna/Zakล™ivenรญ] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND Vyberte prvnรญ otevล™enou kล™ivku [Continuity=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Vyberte druhou otevล™enou kล™ivku [Continuity=__ocs_arg_c__]: @@ -3599,6 +3704,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Zadejte bod vloลพenรญ: insert = + .pastespec-specify-insertion-point = PASTESPEC Zadejte bod vloลพenรญ: + .pastespec-paste-as = PASTESPEC Vloลพit jako [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3715,6 +3822,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3769,6 +3883,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Uchopenรญ podkladลฏ se liลกรญ* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3908,6 +4023,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Vyberte obrรกzek nebo podklad: + .adjust-select-pdf-underlay = Vyberte podklad PDF: + .adjust-select-dwf-underlay = Vyberte podklad DWF: + .adjust-select-dgn-underlay = Vyberte podklad DGN: + .adjust-mixed-underlays = *Neplatnรก sada vรฝbฤ›ru. Vyberte pouze podklady stejnรฉho typu souboru.* + .adjust-image-option = Zadejte moลพnost obrรกzku [Kontrast/Vyblednout/Jas] : + .adjust-underlay-option = Zadejte moลพnost podkladu __ocs_arg_kind__ [Vyblednout/Kontrast/Monochromatickรฝ] <__ocs_arg_option__>: + .adjust-monochrome = Monochromatickรฝ? [Ano/ne] <__ocs_arg_value__>: + .adjust-brightness-value = Zadejte hodnotu: jas (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Zadejte hodnotu: kontrast (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Zadejte hodnotu: vyblednout (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = Hodnota musรญ bรฝt celรฉ ฤรญslo mezi 0 a 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4412,7 +4539,27 @@ model = .value-2point = 2-bodovรฝ .cube = Krychle + layout = + .mvsetup-select-objects-to-delete = MVSETUP Vyberte objekty k odstranฤ›nรญ: + .delete-objects-option = Odstranit objekty + .mvsetup-enable-paper-space = MVSETUP Povolit vรฝkresovรฝ prostor? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Zadejte mฤ›ล™รญtko <1>: + .mvsetup-enter-paper-width = MVSETUP Zadejte ลกรญล™ku papรญru: + .mvsetup-enter-paper-height = MVSETUP Zadejte vรฝลกku papรญru: + .mvsetup-enter-option = MVSETUP Zadejte moลพnost [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Zadejte rozvrลพenรญ vรฝล™ezลฏ [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Zadejte prvnรญ roh oblasti pro vรฝล™ezy : + .mvsetup-specify-opposite-corner = MVSETUP Zadejte protฤ›jลกรญ roh: + .mvsetup-viewports-in-x = MVSETUP Zadejte poฤet vรฝล™ezลฏ ve smฤ›ru X <2>: + .mvsetup-viewports-in-y = MVSETUP Zadejte poฤet vรฝล™ezลฏ ve smฤ›ru Y <2>: + .mvsetup-distance-in-x = MVSETUP Zadejte vzdรกlenost mezi vรฝล™ezy ve smฤ›ru X <0>: + .mvsetup-distance-in-y = MVSETUP Zadejte vzdรกlenost mezi vรฝล™ezy ve smฤ›ru Y <0>: + .mvsetup-select-viewports = MVSETUP Vyberte vรฝล™ezy ke zmฤ›nฤ› mฤ›ล™รญtka: + .mvsetup-paper-space-units = MVSETUP Zadejte poฤet jednotek vรฝkresovรฉho prostoru <1>: + .mvsetup-model-space-units = MVSETUP Zadejte poฤet jednotek modelovรฉho prostoru <1>: + .mvsetup-viewports-do-not-fit = MVSETUP Vรฝล™ezy se do oblasti nevejdou. + .scale-viewports-option = Mฤ›ล™รญtko vรฝล™ezลฏ .active = Aktivnรญ .inactive = Neaktivnรญ .insert-view = Vloลพit pohled @@ -4438,6 +4585,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Zmrazit | T = Rozmrazit | Enter = Konec view = + .twist-option = Natoฤenรญ + .dview-specify-view-twist-angle = DVIEW Zadejte รบhel natoฤenรญ pohledu <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Zadejte moลพnost [TWist/Undo]: .color-scheme-set-to-system-currently-name = Barevnรฉ schรฉma nastaveno na โ€žSystรฉmโ€œ (aktuรกlnฤ› __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = Vyลพaduje celรฉ ฤรญslo mezi __ocs_fmt_0__ a __ocs_fmt_1__. @@ -4632,13 +4782,6 @@ view = .x-no-xdata = __ocs_fmt_0__: ลพรกdnรก xdata. .value-3d-orbit = 3D obฤ›h .value-3d-orbit-drag-with-right-mouse-button = 3D Orbit: tรกhnฤ›te pravรฝm tlaฤรญtkem myลกi. - .adjust-brightness-contrast-fade = ADJUST [Jas / Kontrast / Vyblednutรญ]: - .adjust-brightness-0-100 = Jas ADJUST 0-100: - .adjust-contrast-0-100 = ADJUST kontrast 0-100: - .adjust-fade-0-100 = ADJUST prolรญnรกnรญ 0-100: - .adjust-select-raster-image-s-first = ADJUST: nejprve vyberte rastrovรฝ obrรกzek(y). - .adjust-value-must-be-0-100 = ADJUST: hodnota musรญ bรฝt 0-100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ na snรญmcรญch __ocs_fmt_2__. .annoallvisible-new-value-0-1 = Novรก hodnota ANNOALLVISIBLE [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: zadejte 0 nebo 1. .annoautoscale-new-value-4-4 = Novรก hodnota ANNOAUTOSCALE [-4..4]: @@ -4764,7 +4907,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Podloลพka __ocs_fmt_0__: vyblednout=__ocs_fmt_1__, kontrast=__ocs_fmt_2__, na=__ocs_fmt_3__, klip=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = Aktualizovanรฉ podloลพky __ocs_fmt_0__. - .usage-adjust-brightness-contrast-fade-0-100 = Pouลพitรญ: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Pouลพitรญ: ANNOSCALE napล™. 1:50, 2:1 nebo faktor .usage-draworder-f-front-b-back-a-above-handle = Pouลพรญvรกnรญ: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Pouลพitรญ: HYPERLINK (nejprve vyberte objekty) @@ -5992,6 +6134,46 @@ ui = .osnap-settings = Nastavenรญ uchopenรญ... .snap-override-none-next-pick-only = Doฤasnรฉ uchopenรญ: ลฝรกdnรฉ (jen pro dalลกรญ bod). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Vลพdy ponechte text mezi vynรกลกecรญmi ล™รกdky .angular-dimensions = รšhlovรฉ rozmฤ›ry @@ -6259,3 +6441,117 @@ graphic-attributes = .reset-angle = Obnovit รบhel .one-color = Jedna barva .two-colors = Dvฤ› barvy + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Pล™idat do pracovnรญ sady + .remove-from-working-set = Odebrat z pracovnรญ sady + .toolbar-objects = objektลฏ + .dialog-title = Upravit referenci + .dialog-reference = Reference k รบpravฤ› + .dialog-chain-hint = ล˜etฤ›zec ve vybranรฉm bodฤ›; klepnutรญm na ลกtรญtek zmฤ›nรญte รบroveลˆ. + .dialog-path = Cesta: + .dialog-working-set = Pracovnรญ sada + .dialog-all-objects = Vลกechny objekty + .dialog-pick-objects = Vybrat ve vรฝkresu + .dialog-attributes = Upravit takรฉ definice atributลฏ + .dialog-session = Relace + .dialog-lock = Zamknout objekty mimo pracovnรญ sadu + .dialog-unique = Pล™idฤ›lit kolidujรญcรญm hladinรกm, stylลฏm a blokลฏm jedineฤnรฉ nรกzvy + .dialog-start = Zahรกjit รบpravy + .dialog-help = Otevล™e referenci bloku nebo externรญ referenci k รบpravฤ› na mรญstฤ›. + .select-reference = Vyberte referenci: + .reference-not-found = Reference nebyla nalezena. + .not-valid-reference = Objekt nenรญ platnรก reference + .one-entity-added = Pล™idรกna 1 entita + .nesting-level = Vyberte รบroveลˆ vnoล™enรญ [Ok/Dalลกรญ] : + .selection-method = Zadejte metodu vรฝbฤ›ru objektลฏ [Vลกe/Vnoล™enรฉ] : + .select-nested = Vyberte vnoล™enรฉ objekty: + .display-attdefs = Zobrazit definice atributลฏ [Ano/Ne] : + .zero-items-selected = Vybrรกno 0 poloลพek + .one-item-selected = Vybrรกna 1 poloลพka + .items-selected = Vybrรกno poloลพek: __ocs_fmt_0__ + .refset-option = Zadejte moลพnost [Pล™idat/Odebrat] : + .refclose-option = Zadejte moลพnost [Uloลพit/Zahodit zmฤ›ny reference] : + .not-checked-out = ** Pล™รญkaz nenรญ povolen, dokud nenรญ reference upravovรกna pล™รญkazem REFEDIT ** + .already-checked-out = ** Pล™รญkaz nenรญ povolen, __ocs_fmt_0__ je jiลพ upravovรกna ** + .transfer-objects = Pล™enos objektลฏ mezi pracovnรญ sadou RefEdit a hostitelskรฝm vรฝkresem... + .added-to-working-set = Pล™idรกno do pracovnรญ sady: __ocs_fmt_0__ + .removed-from-working-set = Odebrรกno z pracovnรญ sady: __ocs_fmt_0__ + .one-object-added = 1 objekt pล™idรกn do __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ objektลฏ pล™idรกno do __ocs_fmt_1__ + .one-object-removed = 1 objekt odebrรกn z __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ objektลฏ odebrรกno z __ocs_fmt_1__ + .one-xref-instance = Aktualizovรกna 1 instance xref + .xref-instances = Aktualizovรกno instancรญ xref: __ocs_fmt_0__ + .one-block-instance = Aktualizovรกna 1 instance bloku + .block-instances = Aktualizovรกno instancรญ bloku: __ocs_fmt_0__ + .redefined-reloaded = __ocs_fmt_0__ pล™edefinovรกn a znovu naฤten. + .redefined = __ocs_fmt_0__ pล™edefinovรกn. + .ok = Ok + .nested = Vnoล™enรฉ + +image-adjust = + .title = รšprava obrรกzku + .preview = Nรกhled + .before = Pล™ed + .after = Po + .no-preview = obrรกzek nelze naฤรญst + .adjust = รšprava + .brightness = Jas + .contrast = Kontrast + .fade = Vyblednutรญ + .hint = Klepnฤ›te na pruh nebo jej pล™etรกhnฤ›te; โˆ’ / + je jeden krok, pole pล™ijรญmรก celรฉ ฤรญslo 0โ€“100. + .reset = Obnovit vรฝchozรญ + .apply = Pouลพรญt + .help = Nastavรญ jas, kontrast a vyblednutรญ vybranรฝch rastrovรฝch obrรกzkลฏ. + +keytips = + .title = KeyTip + .typed = Zadรกno: { $path } + .hint-tabs = Zadejte pรญsmeno karty ยท Esc zavล™e + .hint-tab = Karta { $tab } ยท Esc o รบroveลˆ zpฤ›t + .hint-items = Zadejte pรญsmeno poloลพky ยท Esc o รบroveลˆ zpฤ›t diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 03da184fe..4939d5334 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Plug-ins ribbon-tab = + .block-editor = Blockeditor .draw = Zeichnen .parametric = Parametrisch .annotate = Beschriften @@ -98,6 +99,9 @@ start = .no-recent-files = Geรถffnete Dateien werden hier angezeigt. .browser-storage = Browserspeicher .keep-recent-files = Zuletzt verwendete Dateien behalten + .view-list = Liste + .view-icons = Symbole + .view-preview = GroรŸe Vorschau donation = .title = Spendenaufruf @@ -167,6 +171,14 @@ modal = .that-object = dieses Objekt .those-objects = diese Objekte + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Bild "{ $name }" eingefรผgt: { $width }ร—{ $height } px .image-unsupported = Das eingefรผgte Bild "{ $name }" konnte nicht dekodiert werden. @@ -274,11 +286,73 @@ command-copy = .mode = COPY Kopiermodus eingeben [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ nur hier (grรผn), __ocs_fmt_1__ nur in der anderen Zeichnung (rot)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Mit Zeichnung vergleichen + .label-associative-array-created = __ocs_fmt_0__: assoziative Reihe erstellt. + .assoc-array-array-path = Reihe (Pfad) + .assoc-array-array-polar = Reihe (polar) + .assoc-array-array-rectangular = Reihe (rechteckig) + .assoc-array-item-spacing = Elementabstand + .assoc-array-fill-angle = Fรผllwinkel + .assoc-array-items = Elemente + .assoc-array-level-spacing = Ebenenabstand + .assoc-array-levels = Ebenen + .assoc-array-row-spacing = Zeilenabstand + .assoc-array-column-spacing = Spaltenabstand + .fbxexport-exported-to = FBXEXPORT: exportiert nach "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: keine 3D-Mesh-Daten in dieser Zeichnung. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Neue Schraffuren folgen ihren Grenzen (HPASSOC) + .options-show-ribbon = Multifunktionsleiste anzeigen (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Menรผleiste anzeigen (MENUBAR) + .options-window-elements = Fensterelemente + .menu-viewports = Ansichtsfenster + .menu-view-twist = Ansicht verdrehen + .menu-tools = Werkzeuge + .menu-table-style = Tabellenstil + .menu-run-script = Skript ausfรผhren + .menu-revision-cloud = Revisionswolke + .menu-regen-all = Alles regenerieren + .menu-redraw = Neuzeichnen + .menu-rectangle = Rechteck + .menu-quick-dimension = SchnellbemaรŸung + .menu-publish = Publizieren + .menu-paste-special = Inhalte einfรผgen + .menu-parametric = Parametrisch + .menu-page-setup-manager = Seiteneinrichtungs-Manager + .menu-orbit = Orbit + .menu-new-ucs = Neues BKS + .menu-named-views = Benannte Ansichten + .menu-multiline-text = Absatztext + .menu-multiline-style = Multilinienstil + .menu-multiline = Multilinie + .menu-multileader-style = Multi-Fรผhrungslinienstil + .menu-match-properties = Eigenschaften anpassen + .menu-lengthen = Lรคnge รคndern + .menu-jogged = Verkรผrzt + .menu-id-point = Punkt ID + .menu-explode-text = Text auflรถsen + .menu-edit-spline = Spline bearbeiten + .menu-edit-polyline = Polylinie bearbeiten + .menu-edit-hatch = Schraffur bearbeiten + .menu-drawing-limits = Limiten + .menu-donut = Ring + .menu-dimension-style = BemaรŸungsstil + .menu-construction-line = Konstruktionslinie + .menu-arc-length = Bogenlรคnge + .command-line-opacity = Deckkraft der Befehlszeile + .txtexp-no-text-selected = TXTEXP: kein Text gewรคhlt. + .angle-override-shown = Winkelvorgabe: __ocs_fmt_0__ .plot-preview-task = Plotvorschau .link-a-csv-data-source = CSV-Datenquelle verknรผpfen .csv-files = CSV-Dateien .browser-cannot-open-typed-path = Der Browser kann einen eingegebenen Pfad nicht รถffnen; wรคhle die Datei. .recover-drawing = Zeichnung wiederherstellen + .requires-0-or-1-or-keyword = Nur 0 oder 1 oder ein Optionsschlรผsselwort zulรคssig. + .requires-a-number = Zahl erforderlich. .restore-it-as-a-recovered-tab-or-discard-the-copy = Als wiederhergestellte Registerkarte รถffnen oder die Kopie verwerfen. .name-has-an-autosave-copy-newer-than-the-last-save = Fรผr โ€ž__ocs_fmt_0__โ€œ existiert eine automatische Sicherungskopie, die neuer ist als die letzte gespeicherte Version. .autosave-copy-could-not-be-stored = Automatische Sicherungskopie konnte nicht gespeichert werden: __ocs_fmt_0__ @@ -822,6 +896,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: Wรคhlen Sie ein Schnittobjekt oder aktivieren Sie einen Live-Schnitt. + .sectioncull-prepare-failed = SECTIONCULL: ein geschnittenes Objekt konnte nicht vorbereitet werden; nichts wurde geรคndert. + .sectioncull-nothing-outside = SECTIONCULL: auรŸerhalb des Schnitts liegt nichts. + .sectioncull-result = SECTIONCULL: __ocs_arg_erased__ Objekt(e) gelรถscht und __ocs_arg_cut__ am Schnitt beschnitten. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: Passen Sie einen Spline durch __ocs_fmt_0__ Punkte. .splinefit-need-at-least-3-points = SPLINEFIT: mindestens 3 Punkte werden benรถtigt. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: Wรคhlen Sie eine Polylinie, um eine Spline einzupassen. @@ -859,9 +937,12 @@ common = .sent-to-printer-printer = Gesendet an Drucker: __ocs_fmt_0__ .set-current = Aktuell setzen .shade-plot = Schattierter Plot + .shaded-with-edges = Schattiert mit Kanten + .shades-of-gray = Graustufen .shadow-flags = Schattenfahnen .single = Einzel .size = GrรถรŸe + .sketchy = Skizzenhaft .snap-override-label-next-pick-only = Snap รœbersteuerung: __ocs_fmt_0__ (nur nรคchste Auswahl). .solid-created = Volumenkรถrper erstellt. .source-object-not-found = Quellobjekt nicht gefunden. @@ -1067,7 +1148,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: Left, Top oder Right eingeben. .isodraft-expected-on-or-off = ISODRAFT: On oder Off eingeben. .xdata-selected-entities-are-on-locked-layers = XDATA: Ausgewรคhlte Objekte liegen auf gesperrten Layern. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: Keine Rasterbilder ausgewรคhlt oder unbekannte Eigenschaft (BRIGHTNESS/CONTRAST/FADE verwenden). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: Eine ganze Zahl von -4 bis 4 eingeben. .usage-datalink-path-to-csv = Aufruf: DATALINK .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: keine verknรผpften Tabellen gefunden. @@ -2060,7 +2140,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = Datei .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2369,6 +2449,7 @@ common = .load-linetypes = Linientypen laden .requires-two-positive-values = Erfordert zwei positive Werte (x,y) oder einen fรผr beide. .snap-marker-color = Farbe der Fangmarkierung + .snap-marker-size = GrรถรŸe der Fangmarkierung .command-line-text-color = Textfarbe der Befehlszeile .operand = Operand .linetypes-loaded = __ocs_fmt_0__ Linientyp(en) geladen. @@ -2382,6 +2463,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Basispunkt angeben [__ocs_arg_count__ Objekte]: .threedrotate-specify-rotation-axis = 3DROTATE Drehachse angeben [X/Y/Z] oder einen zweiten Punkt darauf : .threedrotate-specify-rotation-angle-a = 3DROTATE Drehwinkel angeben <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Warnhinweise speichern .unable-to-save-drawing = Zeichnung konnte nicht gespeichert werden @@ -3005,6 +3093,7 @@ draw = .arc-specify-radius-of-arc = ARC Radius des Bogens angeben: .chord-length = Sehnenlรคnge + modify = .add-vertex = Scheitelpunkt hinzufรผgen .closed-cross = Geschlossenes Kreuz @@ -3050,6 +3139,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Block mit Attributen auswรคhlen: .annotation-object-scale = BeschriftungsobjektmaรŸstab .bedit-select-block-reference-to-edit = BEDIT Blockreferenz zum Bearbeiten auswรคhlen: + .bvstate-option = BVSTATE Option eingeben [Neu/Setzen/Umbenennen/Lรถschen] : + .bvstate-new-name = BVSTATE Namen des neuen Sichtbarkeitsstatus eingeben: + .bvstate-set = BVSTATE Aktuell zu setzenden Sichtbarkeitsstatus eingeben <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Umzubenennenden Sichtbarkeitsstatus eingeben <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Neuen Namen eingeben: + .bvstate-delete = BVSTATE Zu lรถschenden Sichtbarkeitsstatus eingeben: + .bvstate-not-found = Sichtbarkeitsstatus "__ocs_fmt_0__" nicht gefunden. + .bvstate-exists = Sichtbarkeitsstatus "__ocs_fmt_0__" existiert bereits. + .bvstate-last = Der einzige Sichtbarkeitsstatus kann nicht gelรถscht werden. + .bvstate-needs-bedit = __ocs_fmt_0__: ร–ffnen Sie den Block zuerst im Blockeditor (BEDIT). + .bvstate-current = Aktueller Sichtbarkeitsstatus: __ocs_fmt_0__ + .bvhide-select = Auszublendende Objekte wรคhlen: + .bvshow-select = Einzublendende Objekte wรคhlen: + .arrayedit-select-array = ARRAYEDIT Reihe wรคhlen: + .arrayedit-options = ARRAYEDIT Option eingeben [Quelle/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: das gewรคhlte Objekt ist keine assoziative Reihe. .blend-enter-continuity-tangent-curvature-c = BLEND Geben Sie Kontinuitรคt ein [Tangente/Krรผmmung] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND Erste offene Kurve auswรคhlen [Continuity=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND zweite offene Kurve auswรคhlen [Continuity=__ocs_arg_c__]: @@ -3602,6 +3707,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Einfรผhrstelle angeben: insert = + .pastespec-specify-insertion-point = PASTESPEC Einfรผgepunkt angeben: + .pastespec-paste-as = PASTESPEC Einfรผgen als [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3718,6 +3825,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3772,6 +3886,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Unterlagen-OFang variiert* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3909,6 +4024,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Bild oder Unterlage wรคhlen: + .adjust-select-pdf-underlay = PDF-Unterlage wรคhlen: + .adjust-select-dwf-underlay = DWF-Unterlage wรคhlen: + .adjust-select-dgn-underlay = DGN-Unterlage wรคhlen: + .adjust-mixed-underlays = *Ungรผltiger Auswahlsatz. Nur Unterlagenobjekte desselben Dateityps wรคhlen.* + .adjust-image-option = Bildoption eingeben [Kontrast/Abblendung/Helligkeit] : + .adjust-underlay-option = __ocs_arg_kind__-Unterlagenoption eingeben [Abblendung/Kontrast/Monochrom] <__ocs_arg_option__>: + .adjust-monochrome = Monochrom? [Ja/Nein] <__ocs_arg_value__>: + .adjust-brightness-value = Wert fรผr Helligkeit eingeben (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Wert fรผr Kontrast eingeben (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Wert fรผr Abblendung eingeben (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = Der Wert muss eine ganze Zahl zwischen 0 und 100 sein. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4413,7 +4540,27 @@ model = .value-2point = 2-Punkt .cube = Wรผrfel + layout = + .mvsetup-select-objects-to-delete = MVSETUP Zu lรถschende Objekte wรคhlen: + .delete-objects-option = Objekte lรถschen + .mvsetup-enable-paper-space = MVSETUP Papierbereich aktivieren? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Skalierfaktor eingeben <1>: + .mvsetup-enter-paper-width = MVSETUP Papierbreite eingeben: + .mvsetup-enter-paper-height = MVSETUP Papierhรถhe eingeben: + .mvsetup-enter-option = MVSETUP Option eingeben [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Ansichtsfensteranordnung eingeben [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Erste Ecke des Bereichs fรผr Ansichtsfenster angeben : + .mvsetup-specify-opposite-corner = MVSETUP Gegenรผberliegende Ecke angeben: + .mvsetup-viewports-in-x = MVSETUP Anzahl der Ansichtsfenster in X-Richtung eingeben <2>: + .mvsetup-viewports-in-y = MVSETUP Anzahl der Ansichtsfenster in Y-Richtung eingeben <2>: + .mvsetup-distance-in-x = MVSETUP Abstand zwischen Ansichtsfenstern in X-Richtung angeben <0>: + .mvsetup-distance-in-y = MVSETUP Abstand zwischen Ansichtsfenstern in Y-Richtung angeben <0>: + .mvsetup-select-viewports = MVSETUP Zu skalierende Ansichtsfenster wรคhlen: + .mvsetup-paper-space-units = MVSETUP Anzahl der Papierbereichseinheiten eingeben <1>: + .mvsetup-model-space-units = MVSETUP Anzahl der Modellbereichseinheiten eingeben <1>: + .mvsetup-viewports-do-not-fit = MVSETUP Die Ansichtsfenster passen nicht in den Bereich. + .scale-viewports-option = Ansichtsfenster skalieren .active = Aktiv .inactive = Inaktiv .insert-view = Ansicht einfรผgen @@ -4439,6 +4586,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Einfrieren | T = Auftauen | Eingabe = Beenden view = + .twist-option = Verdrehen + .dview-specify-view-twist-angle = DVIEW Ansichtsverdrehwinkel angeben <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Option eingeben [TWist/Undo]: .color-scheme-set-to-system-currently-name = Farbschema auf โ€žSystemโ€œ eingestellt (derzeit __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = Erfordert eine ganze Zahl zwischen __ocs_fmt_0__ und __ocs_fmt_1__. @@ -4633,13 +4783,6 @@ view = .x-no-xdata = __ocs_fmt_0__: keine xdata. .value-3d-orbit = 3D-Bahn .value-3d-orbit-drag-with-right-mouse-button = 3D Orbit: Ziehen Sie mit der rechten Maustaste. - .adjust-brightness-contrast-fade = ADJUST [Helligkeit / Kontrast / Ausblenden]: - .adjust-brightness-0-100 = ADJUST Helligkeit 0-100: - .adjust-contrast-0-100 = ADJUST Kontrast 0-100: - .adjust-fade-0-100 = ADJUST Ausblenden 0-100: - .adjust-select-raster-image-s-first = ADJUST: Wรคhlen Sie zuerst Rasterbild(e). - .adjust-value-must-be-0-100 = ADJUST: Der Wert muss 0-100 sein. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ auf __ocs_fmt_2__ Bild(en). .annoallvisible-new-value-0-1 = ANNOALLVISIBLE neuer Wert [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: Geben Sie 0 oder 1 ein. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE neuer Wert [-4..4]: @@ -4765,7 +4908,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Unterlag __ocs_fmt_0__: fade=__ocs_fmt_1__, contrast=__ocs_fmt_2__, on=__ocs_fmt_3__, clip=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = Aktualisiert __ocs_fmt_0__ underlay(s). - .usage-adjust-brightness-contrast-fade-0-100 = Verwendung: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Verwendung: ANNOSCALE e.g. 1:50, 2:1, or a factor .usage-draworder-f-front-b-back-a-above-handle = Verwendung: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Verwendung: HYPERLINK (select objects first) @@ -5993,6 +6135,46 @@ ui = .osnap-settings = Objektfang-Einstellungen... .snap-override-none-next-pick-only = Fang-รœberschreibung: Keine (nur nรคchster Punkt). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Text immer zwischen Hilfslinien halten .angular-dimensions = WinkelbemaรŸungen @@ -6260,3 +6442,117 @@ graphic-attributes = .reset-angle = Winkel zurรผcksetzen .one-color = Einfarbig .two-colors = Zweifarbig + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Zum Arbeitssatz hinzufรผgen + .remove-from-working-set = Aus dem Arbeitssatz entfernen + .toolbar-objects = Objekte + .dialog-title = Referenz bearbeiten + .dialog-reference = Zu bearbeitende Referenz + .dialog-chain-hint = Die Kette am gewรคhlten Punkt; auf einen Chip klicken, um die Ebene zu รคndern. + .dialog-path = Pfad: + .dialog-working-set = Arbeitssatz + .dialog-all-objects = Alle Objekte + .dialog-pick-objects = In der Zeichnung wรคhlen + .dialog-attributes = Auch Attributdefinitionen bearbeiten + .dialog-session = Sitzung + .dialog-lock = Objekte auรŸerhalb des Arbeitssatzes sperren + .dialog-unique = Kollidierenden Layer-, Stil- und Blocknamen eindeutige Namen geben + .dialog-start = Bearbeitung beginnen + .dialog-help = ร–ffnet eine Blockreferenz oder externe Referenz zur Bearbeitung an Ort und Stelle. + .select-reference = Referenz wรคhlen: + .reference-not-found = Referenz nicht gefunden. + .not-valid-reference = Objekt ist keine gรผltige Referenz + .one-entity-added = 1 Element hinzugefรผgt + .nesting-level = Verschachtelungsebene wรคhlen [Ok/Nรคchste] : + .selection-method = Objektwahlmethode eingeben [Alle/Verschachtelt] : + .select-nested = Verschachtelte Objekte wรคhlen: + .display-attdefs = Attributdefinitionen anzeigen [Ja/Nein] : + .zero-items-selected = 0 Elemente gewรคhlt + .one-item-selected = 1 Element gewรคhlt + .items-selected = __ocs_fmt_0__ Elemente gewรคhlt + .refset-option = Option eingeben [Hinzufรผgen/Entfernen] : + .refclose-option = Option eingeben [Speichern/Referenzรคnderungen verwerfen] : + .not-checked-out = ** Befehl nur zulรคssig, wenn eine Referenz mit REFEDIT bearbeitet wird ** + .already-checked-out = ** Befehl nicht zulรคssig, __ocs_fmt_0__ wird bereits bearbeitet ** + .transfer-objects = Objekte zwischen RefEdit-Arbeitssatz und Hostzeichnung รผbertragen... + .added-to-working-set = __ocs_fmt_0__ zum Arbeitssatz hinzugefรผgt + .removed-from-working-set = __ocs_fmt_0__ aus dem Arbeitssatz entfernt + .one-object-added = 1 Objekt zu __ocs_fmt_0__ hinzugefรผgt + .objects-added = __ocs_fmt_0__ Objekte zu __ocs_fmt_1__ hinzugefรผgt + .one-object-removed = 1 Objekt aus __ocs_fmt_0__ entfernt + .objects-removed = __ocs_fmt_0__ Objekte aus __ocs_fmt_1__ entfernt + .one-xref-instance = 1 XRef-Instanz aktualisiert + .xref-instances = __ocs_fmt_0__ XRef-Instanzen aktualisiert + .one-block-instance = 1 Blockinstanz aktualisiert + .block-instances = __ocs_fmt_0__ Blockinstanzen aktualisiert + .redefined-reloaded = __ocs_fmt_0__ neu definiert und neu geladen. + .redefined = __ocs_fmt_0__ neu definiert. + .ok = Ok + .nested = Verschachtelt + +image-adjust = + .title = Bild anpassen + .preview = Vorschau + .before = Vorher + .after = Nachher + .no-preview = Bild konnte nicht geladen werden + .adjust = Anpassen + .brightness = Helligkeit + .contrast = Kontrast + .fade = Ausblenden + .hint = Auf einen Balken klicken oder ziehen; โˆ’ / + ist ein Schritt, ein Feld nimmt eine ganze Zahl 0โ€“100. + .reset = Standardwerte wiederherstellen + .apply = Anwenden + .help = Legt Helligkeit, Kontrast und Ausblendung der gewรคhlten Rasterbilder fest. + +keytips = + .title = KeyTip + .typed = Eingegeben: { $path } + .hint-tabs = Buchstaben einer Registerkarte eingeben ยท Esc schlieรŸt + .hint-tab = Registerkarte { $tab } ยท Esc geht eine Ebene zurรผck + .hint-items = Buchstaben eines Elements eingeben ยท Esc geht eine Ebene zurรผck diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 26ac610cd..1e4a496ba 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = ฮ ฯฯŒฯƒฮธฮตฯ„ฮฑ ribbon-tab = + .block-editor = ฮ•ฯ€ฮตฮพฮตฯฮณฮฑฯƒฯ„ฮฎฯ‚ ฮผฯ€ฮปฮฟฮบ .draw = ฮฃฯ‡ฮตฮดฮฏฮฑฯƒฮท .parametric = ฮ ฮฑฯฮฑฮผฮตฯ„ฯฮนฮบฯŒ .annotate = ฮ•ฯ€ฮนฯƒฮทฮผฮตฮฏฯ‰ฯƒฮท @@ -98,6 +99,9 @@ start = .no-recent-files = ฮคฮฑ ฮฑฯฯ‡ฮตฮฏฮฑ ฯ€ฮฟฯ… ฮฑฮฝฮฟฮฏฮณฮตฯ„ฮต ฮธฮฑ ฮตฮผฯ†ฮฑฮฝฮฏฮถฮฟฮฝฯ„ฮฑฮน ฮตฮดฯŽ. .browser-storage = ฮ‘ฯ€ฮฟฮธฮทฮบฮตฯ…ฯ„ฮนฮบฯŒฯ‚ ฯ‡ฯŽฯฮฟฯ‚ ฯ€ฯฮฟฮณฯฮฌฮผฮผฮฑฯ„ฮฟฯ‚ ฯ€ฮตฯฮนฮฎฮณฮทฯƒฮทฯ‚ .keep-recent-files = ฮ”ฮนฮฑฯ„ฮฎฯฮทฯƒฮท ฯ€ฯฯŒฯƒฯ†ฮฑฯ„ฯ‰ฮฝ ฮฑฯฯ‡ฮตฮฏฯ‰ฮฝ + .view-list = ฮ›ฮฏฯƒฯ„ฮฑ + .view-icons = ฮ•ฮนฮบฮฟฮฝฮฏฮดฮนฮฑ + .view-preview = ฮœฮตฮณฮฌฮปฮตฯ‚ ฯ€ฯฮฟฮตฯ€ฮนฯƒฮบฮฟฯ€ฮฎฯƒฮตฮนฯ‚ donation = .title = ฮˆฮบฮบฮปฮทฯƒฮท ฮณฮนฮฑ ฮดฯ‰ฯฮตฮฌ @@ -167,6 +171,14 @@ modal = .that-object = ฮฑฯ…ฯ„ฯŒ ฯ„ฮฟ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฟ .those-objects = ฮฑฯ…ฯ„ฮฌ ฯ„ฮฑ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = ฮ•ฯ€ฮนฮบฮฟฮปฮปฮฎฮธฮทฮบฮต ฮท ฮตฮนฮบฯŒฮฝฮฑ "{ $name }": { $width }ร—{ $height } px .image-unsupported = ฮ”ฮตฮฝ ฮฎฯ„ฮฑฮฝ ฮดฯ…ฮฝฮฑฯ„ฮฎ ฮท ฮฑฯ€ฮฟฮบฯ‰ฮดฮนฮบฮฟฯ€ฮฟฮฏฮทฯƒฮท ฯ„ฮทฯ‚ ฮตฯ€ฮนฮบฮฟฮปฮปฮทฮผฮญฮฝฮทฯ‚ ฮตฮนฮบฯŒฮฝฮฑฯ‚ "{ $name }". @@ -273,11 +285,73 @@ command-copy = .mode = COPY ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮปฮตฮนฯ„ฮฟฯ…ฯฮณฮฏฮฑ ฮฑฮฝฯ„ฮนฮณฯฮฑฯ†ฮฎฯ‚ [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ ฮผฯŒฮฝฮฟ ฮตฮดฯŽ (ฯ€ฯฮฌฯƒฮนฮฝฮฟ), __ocs_fmt_1__ ฮผฯŒฮฝฮฟ ฯƒฯ„ฮฟ ฮฌฮปฮปฮฟ ฯƒฯ‡ฮญฮดฮนฮฟ (ฮบฯŒฮบฮบฮนฮฝฮฟ)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ฮฃฯฮณฮบฯฮนฯƒฮท ฮผฮต ฯƒฯ‡ฮญฮดฮนฮฟ + .label-associative-array-created = __ocs_fmt_0__: ฮดฮทฮผฮนฮฟฯ…ฯฮณฮฎฮธฮทฮบฮต ฯƒฯ…ฯƒฯ‡ฮตฯ„ฮนฯƒฯ„ฮนฮบฯŒฯ‚ ฯ€ฮฏฮฝฮฑฮบฮฑฯ‚. + .assoc-array-array-path = ฮ ฮฏฮฝฮฑฮบฮฑฯ‚ (ฮดฮนฮฑฮดฯฮฟฮผฮฎ) + .assoc-array-array-polar = ฮ ฮฏฮฝฮฑฮบฮฑฯ‚ (ฯ€ฮฟฮปฮนฮบฯŒฯ‚) + .assoc-array-array-rectangular = ฮ ฮฏฮฝฮฑฮบฮฑฯ‚ (ฮฟฯฮธฮฟฮณฯŽฮฝฮนฮฟฯ‚) + .assoc-array-item-spacing = ฮ‘ฯ€ฯŒฯƒฯ„ฮฑฯƒฮท ฯƒฯ„ฮฟฮนฯ‡ฮตฮฏฯ‰ฮฝ + .assoc-array-fill-angle = ฮ“ฯ‰ฮฝฮฏฮฑ ฯ€ฮปฮฎฯฯ‰ฯƒฮทฯ‚ + .assoc-array-items = ฮฃฯ„ฮฟฮนฯ‡ฮตฮฏฮฑ + .assoc-array-level-spacing = ฮ‘ฯ€ฯŒฯƒฯ„ฮฑฯƒฮท ฮตฯ€ฮนฯ€ฮญฮดฯ‰ฮฝ + .assoc-array-levels = ฮ•ฯ€ฮฏฯ€ฮตฮดฮฑ + .assoc-array-row-spacing = ฮ‘ฯ€ฯŒฯƒฯ„ฮฑฯƒฮท ฮณฯฮฑฮผฮผฯŽฮฝ + .assoc-array-column-spacing = ฮ‘ฯ€ฯŒฯƒฯ„ฮฑฯƒฮท ฯƒฯ„ฮทฮปฯŽฮฝ + .fbxexport-exported-to = FBXEXPORT: ฮตฮพฮฎฯ‡ฮธฮท ฯƒฯ„ฮฟ "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: ฮดฮตฮฝ ฯ…ฯ€ฮฌฯฯ‡ฮฟฯ…ฮฝ ฮดฮตฮดฮฟฮผฮญฮฝฮฑ ฯ€ฮปฮญฮณฮผฮฑฯ„ฮฟฯ‚ 3D ฯƒฮต ฮฑฯ…ฯ„ฯŒ ฯ„ฮฟ ฯƒฯ‡ฮญฮดฮนฮฟ. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = ฮŸฮน ฮฝฮญฮตฯ‚ ฮดฮนฮฑฮณฯฮฑฮผฮผฮฏฯƒฮตฮนฯ‚ ฮฑฮบฮฟฮปฮฟฯ…ฮธฮฟฯฮฝ ฯ„ฮฑ ฯŒฯฮนฮฌ ฯ„ฮฟฯ…ฯ‚ (HPASSOC) + .options-show-ribbon = ฮ•ฮผฯ†ฮฌฮฝฮนฯƒฮท ฮบฮฟฯฮดฮญฮปฮฑฯ‚ (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = ฮ•ฮผฯ†ฮฌฮฝฮนฯƒฮท ฮณฯฮฑฮผฮผฮฎฯ‚ ฮผฮตฮฝฮฟฯ (MENUBAR) + .options-window-elements = ฮฃฯ„ฮฟฮนฯ‡ฮตฮฏฮฑ ฯ€ฮฑฯฮฑฮธฯฯฮฟฯ… + .menu-viewports = ฮ ฮฑฯฮฌฮธฯ…ฯฮฑ ฯ€ฯฮฟฮฒฮฟฮปฮฎฯ‚ + .menu-view-twist = ฮฃฯ„ฯฮญฯˆฮท ฯŒฯˆฮทฯ‚ + .menu-tools = ฮ•ฯฮณฮฑฮปฮตฮฏฮฑ + .menu-table-style = ฮฃฯ„ฯ…ฮป ฯ€ฮฏฮฝฮฑฮบฮฑ + .menu-run-script = ฮ•ฮบฯ„ฮญฮปฮตฯƒฮท ฮดฮญฯƒฮผฮทฯ‚ ฮตฮฝฮตฯฮณฮตฮนฯŽฮฝ + .menu-revision-cloud = ฮฃฯฮฝฮฝฮตฯ†ฮฟ ฮฑฮฝฮฑฮธฮตฯŽฯฮทฯƒฮทฯ‚ + .menu-regen-all = ฮ‘ฮฝฮฑฮดฮทฮผฮนฮฟฯ…ฯฮณฮฏฮฑ ฯŒฮปฯ‰ฮฝ + .menu-redraw = ฮ•ฯ€ฮฑฮฝฮฑฯƒฯ‡ฮตฮดฮฏฮฑฯƒฮท + .menu-rectangle = ฮŸฯฮธฮฟฮณฯŽฮฝฮนฮฟ + .menu-quick-dimension = ฮ“ฯฮฎฮณฮฟฯฮท ฮดฮนฮฌฯƒฯ„ฮฑฯƒฮท + .menu-publish = ฮ”ฮทฮผฮฟฯƒฮฏฮตฯ…ฯƒฮท + .menu-paste-special = ฮ•ฮนฮดฮนฮบฮฎ ฮตฯ€ฮนฮบฯŒฮปฮปฮทฯƒฮท + .menu-parametric = ฮ ฮฑฯฮฑฮผฮตฯ„ฯฮนฮบฮฌ + .menu-page-setup-manager = ฮ”ฮนฮฑฯ‡ฮตฮฏฯฮนฯƒฮท ฮดฮนฮฑฮผฯŒฯฯ†ฯ‰ฯƒฮทฯ‚ ฯƒฮตฮปฮฏฮดฮฑฯ‚ + .menu-orbit = ฮ ฮตฯฮนฯƒฯ„ฯฮฟฯ†ฮฎ + .menu-new-ucs = ฮฮญฮฟ ฮฃฮฃฮง + .menu-named-views = ฮŸฮฝฮฟฮผฮฑฯƒฮผฮญฮฝฮตฯ‚ ฯŒฯˆฮตฮนฯ‚ + .menu-multiline-text = ฮšฮตฮฏฮผฮตฮฝฮฟ ฯ€ฮฟฮปฮปฯŽฮฝ ฮณฯฮฑฮผฮผฯŽฮฝ + .menu-multiline-style = ฮฃฯ„ฯ…ฮป ฯ€ฮฟฮปฮปฮฑฯ€ฮปฮฎฯ‚ ฮณฯฮฑฮผฮผฮฎฯ‚ + .menu-multiline = ฮ ฮฟฮปฮปฮฑฯ€ฮปฮฎ ฮณฯฮฑฮผฮผฮฎ + .menu-multileader-style = ฮฃฯ„ฯ…ฮป ฯ€ฮฟฮปฮปฮฑฯ€ฮปฮฎฯ‚ ฮฟฮดฮทฮณฮฟฯ + .menu-match-properties = ฮ‘ฮฝฯ„ฮนฯƒฯ„ฮฟฮฏฯ‡ฮนฯƒฮท ฮนฮดฮนฮฟฯ„ฮฎฯ„ฯ‰ฮฝ + .menu-lengthen = ฮ•ฯ€ฮนฮผฮฎฮบฯ…ฮฝฯƒฮท + .menu-jogged = ฮคฮตฮธฮปฮฑฯƒฮผฮญฮฝฮท + .menu-id-point = ฮ‘ฮฝฮฑฮณฮฝฯŽฯฮนฯƒฮท ฯƒฮทฮผฮตฮฏฮฟฯ… + .menu-explode-text = ฮ‘ฮฝฮฌฮปฯ…ฯƒฮท ฮบฮตฮนฮผฮญฮฝฮฟฯ… + .menu-edit-spline = ฮ•ฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ ฮบฮฑฮผฯ€ฯฮปฮทฯ‚ spline + .menu-edit-polyline = ฮ•ฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ ฯ€ฮฟฮปฯ…ฮณฯฮฑฮผฮผฮฎฯ‚ + .menu-edit-hatch = ฮ•ฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ ฮดฮนฮฑฮณฯฮฌฮผฮผฮนฯƒฮทฯ‚ + .menu-drawing-limits = ฮŒฯฮนฮฑ ฯƒฯ‡ฮตฮดฮฏฮฟฯ… + .menu-donut = ฮ”ฮฑฮบฯ„ฯฮปฮนฮฟฯ‚ + .menu-dimension-style = ฮฃฯ„ฯ…ฮป ฮดฮนฮฌฯƒฯ„ฮฑฯƒฮทฯ‚ + .menu-construction-line = ฮ’ฮฟฮทฮธฮทฯ„ฮนฮบฮฎ ฮณฯฮฑฮผฮผฮฎ + .menu-arc-length = ฮœฮฎฮบฮฟฯ‚ ฯ„ฯŒฮพฮฟฯ… + .command-line-opacity = ฮ‘ฮดฮนฮฑฯ†ฮฌฮฝฮตฮนฮฑ ฮณฯฮฑฮผฮผฮฎฯ‚ ฮตฮฝฯ„ฮฟฮปฯŽฮฝ + .txtexp-no-text-selected = TXTEXP: ฮดฮตฮฝ ฮตฯ€ฮนฮปฮญฯ‡ฮธฮทฮบฮต ฮบฮตฮฏฮผฮตฮฝฮฟ. + .angle-override-shown = ฮ ฮฑฯฮฌฮบฮฑฮผฯˆฮท ฮณฯ‰ฮฝฮฏฮฑฯ‚: __ocs_fmt_0__ .plot-preview-task = ฮ ฯฮฟฮตฯ€ฮนฯƒฮบฯŒฯ€ฮทฯƒฮท ฮตฮบฯ„ฯฯ€ฯ‰ฯƒฮทฯ‚ .link-a-csv-data-source = ฮฃฯฮฝฮดฮตฯƒฮท ฯ€ฮทฮณฮฎฯ‚ ฮดฮตฮดฮฟฮผฮญฮฝฯ‰ฮฝ CSV .csv-files = ฮ‘ฯฯ‡ฮตฮฏฮฑ CSV .browser-cannot-open-typed-path = ฮŸ browser ฮดฮตฮฝ ฮผฯ€ฮฟฯฮตฮฏ ฮฝฮฑ ฮฑฮฝฮฟฮฏฮพฮตฮน ฯ€ฮปฮทฮบฯ„ฯฮฟฮปฮฟฮณฮทฮผฮญฮฝฮท ฮดฮนฮฑฮดฯฮฟฮผฮฎยท ฮดฮนฮฌฮปฮตฮพฮต ฯ„ฮฟ ฮฑฯฯ‡ฮตฮฏฮฟ. .recover-drawing = ฮ‘ฮฝฮฌฮบฯ„ฮทฯƒฮท ฯƒฯ‡ฮตฮดฮฏฮฟฯ… + .requires-0-or-1-or-keyword = ฮ‘ฯ€ฮฑฮนฯ„ฮตฮฏฯ„ฮฑฮน ฮผฯŒฮฝฮฟ 0 ฮฎ 1, ฮฎ ฮปฮญฮพฮท-ฮบฮปฮตฮนฮดฮฏ ฮตฯ€ฮนฮปฮฟฮณฮฎฯ‚. + .requires-a-number = ฮ‘ฯ€ฮฑฮนฯ„ฮตฮฏฯ„ฮฑฮน ฮฑฯฮนฮธฮผฯŒฯ‚. .restore-it-as-a-recovered-tab-or-discard-the-copy = ฮ•ฯ€ฮฑฮฝฮฑฯ†ฮญฯฮตฯ„ฮญ ฯ„ฮฟ ฯ‰ฯ‚ ฮฑฮฝฮฑฮบฯ„ฮทฮผฮญฮฝฮท ฮบฮฑฯฯ„ฮญฮปฮฑ ฮฎ ฮฑฯ€ฮฟฯฯฮฏฯˆฯ„ฮต ฯ„ฮฟ ฮฑฮฝฯ„ฮฏฮณฯฮฑฯ†ฮฟ. .name-has-an-autosave-copy-newer-than-the-last-save = ฮคฮฟ "__ocs_fmt_0__" ฮญฯ‡ฮตฮน ฮฑฮฝฯ„ฮฏฮณฯฮฑฯ†ฮฟ ฮฑฯ…ฯ„ฯŒฮผฮฑฯ„ฮทฯ‚ ฮฑฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮทฯ‚ ฮฝฮตฯŒฯ„ฮตฯฮฟ ฯ„ฮทฯ‚ ฯ„ฮตฮปฮตฯ…ฯ„ฮฑฮฏฮฑฯ‚ ฮฑฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮทฯ‚. .autosave-copy-could-not-be-stored = ฮ”ฮตฮฝ ฮฎฯ„ฮฑฮฝ ฮดฯ…ฮฝฮฑฯ„ฮฎ ฮท ฮฑฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮท ฯ„ฮฟฯ… ฮฑฮฝฯ„ฮนฮณฯฮฌฯ†ฮฟฯ… ฮฑฯ…ฯ„ฯŒฮผฮฑฯ„ฮทฯ‚ ฮฑฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮทฯ‚: __ocs_fmt_0__ @@ -823,6 +897,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: ฮตฯ€ฮนฮปฮญฮพฯ„ฮต ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฟ ฯ„ฮฟฮผฮฎฯ‚ ฮฎ ฮตฮฝฮตฯฮณฮฟฯ€ฮฟฮนฮฎฯƒฯ„ฮต ฮถฯ‰ฮฝฯ„ฮฑฮฝฮฎ ฯ„ฮฟฮผฮฎ. + .sectioncull-prepare-failed = SECTIONCULL: ฮญฮฝฮฑ ฮบฮฟฮผฮผฮญฮฝฮฟ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฟ ฮดฮตฮฝ ฯ€ฯฮฟฮตฯ„ฮฟฮนฮผฮฌฯƒฯ„ฮทฮบฮตยท ฯ„ฮฏฯ€ฮฟฯ„ฮฑ ฮดฮตฮฝ ฮฌฮปฮปฮฑฮพฮต. + .sectioncull-nothing-outside = SECTIONCULL: ฯ„ฮฏฯ€ฮฟฯ„ฮฑ ฮดฮตฮฝ ฮฒฯฮฏฯƒฮบฮตฯ„ฮฑฮน ฮตฮบฯ„ฯŒฯ‚ ฯ„ฮทฯ‚ ฯ„ฮฟฮผฮฎฯ‚. + .sectioncull-result = SECTIONCULL: ฮดฮนฮฑฮณฯฮฌฯ†ฮทฮบฮฑฮฝ __ocs_arg_erased__ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ ฮบฮฑฮน ฮบฯŒฯ€ฮทฮบฮฑฮฝ __ocs_arg_cut__ ฯƒฯ„ฮฟ ฯŒฯฮนฮฟ ฯ„ฮทฯ‚ ฯ„ฮฟฮผฮฎฯ‚. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: ฯ€ฯฮฟฯƒฮฑฯฮผฮฟฮณฮฎ spline ฮผฮญฯƒฮฑ ฮฑฯ€ฯŒ __ocs_fmt_0__ ฯƒฮทฮผฮตฮฏฮฑ. .splinefit-need-at-least-3-points = SPLINEFIT: ฮฑฯ€ฮฑฮนฯ„ฮฟฯฮฝฯ„ฮฑฮน ฯ„ฮฟฯ…ฮปฮฌฯ‡ฮนฯƒฯ„ฮฟฮฝ 3 ฯƒฮทฮผฮตฮฏฮฑ. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: ฮตฯ€ฮนฮปฮญฮพฯ„ฮต ฮผฮนฮฑ ฯ€ฮฟฮปฯ…ฮณฯฮฑฮผฮผฮฎ ฮณฮนฮฑ ฯ€ฯฮฟฯƒฮฑฯฮผฮฟฮณฮฎ spline. @@ -860,9 +938,12 @@ common = .sent-to-printer-printer = ฮฃฯ„ฮฌฮปฮธฮทฮบฮต ฯƒฯ„ฮฟฮฝ ฮตฮบฯ„ฯ…ฯ€ฯ‰ฯ„ฮฎ: __ocs_fmt_0__ .set-current = ฮŸฯฮนฯƒฮผฯŒฯ‚ ฯ‰ฯ‚ ฯ„ฯฮญฯ‡ฮฟฮฝ .shade-plot = ฮฃฮบฮฏฮฑฯƒฮท ฮตฮบฯ„ฯฯ€ฯ‰ฯƒฮทฯ‚ + .shaded-with-edges = ฮฃฮบฮนฮฑฯƒฮผฮญฮฝฮฟ ฮผฮต ฮฑฮบฮผฮญฯ‚ + .shades-of-gray = ฮ‘ฯ€ฮฟฯ‡ฯฯŽฯƒฮตฮนฯ‚ ฯ„ฮฟฯ… ฮณฮบฯฮน .shadow-flags = ฮฃฮทฮผฮฑฮฏฮตฯ‚ ฯƒฮบฮนฮฌฯ‚ .single = ฮœฮฟฮฝฯŒ .size = ฮœฮญฮณฮตฮธฮฟฯ‚ + .sketchy = ฮฃฮบฮฏฯ„ฯƒฮฟ .snap-override-label-next-pick-only = ฮ ฮฑฯฮฌฮบฮฑฮผฯˆฮท ฮญฮปฮพฮทฯ‚: __ocs_fmt_0__ (ฮผฯŒฮฝฮฟ ฮณฮนฮฑ ฯ„ฮทฮฝ ฮตฯ€ฯŒฮผฮตฮฝฮท ฮตฯ€ฮนฮปฮฟฮณฮฎ). .solid-created = ฮคฮฟ ฯƒฯ„ฮตฯฮตฯŒ ฮดฮทฮผฮนฮฟฯ…ฯฮณฮฎฮธฮทฮบฮต. .source-object-not-found = ฮคฮฟ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฟ ฯ€ฯฮฟฮญฮปฮตฯ…ฯƒฮทฯ‚ ฮดฮตฮฝ ฮฒฯฮญฮธฮทฮบฮต. @@ -1068,7 +1149,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: ฯ€ฮปฮทฮบฯ„ฯฮฟฮปฮฟฮณฮฎฯƒฯ„ฮต Left, Top ฮฎ Right. .isodraft-expected-on-or-off = ISODRAFT: ฯ€ฮปฮทฮบฯ„ฯฮฟฮปฮฟฮณฮฎฯƒฯ„ฮต On ฮฎ Off. .xdata-selected-entities-are-on-locked-layers = XDATA: ฮฟฮน ฮตฯ€ฮนฮปฮตฮณฮผฮญฮฝฮตฯ‚ ฮฟฮฝฯ„ฯŒฯ„ฮทฯ„ฮตฯ‚ ฮฒฯฮฏฯƒฮบฮฟฮฝฯ„ฮฑฮน ฯƒฮต ฮบฮปฮตฮนฮดฯ‰ฮผฮญฮฝฮฑ ฮตฯ€ฮฏฯ€ฮตฮดฮฑ. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: ฮดฮตฮฝ ฮญฯ‡ฮฟฯ…ฮฝ ฮตฯ€ฮนฮปฮตฮณฮตฮฏ ฮตฮนฮบฯŒฮฝฮตฯ‚ ฯฮฌฯƒฯ„ฮตฯ ฮฎ ฮท ฮนฮดฮนฯŒฯ„ฮทฯ„ฮฑ ฮตฮฏฮฝฮฑฮน ฮฌฮณฮฝฯ‰ฯƒฯ„ฮท (ฯ‡ฯฮทฯƒฮนฮผฮฟฯ€ฮฟฮนฮฎฯƒฯ„ฮต BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: ฮตฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮฑฮบฮญฯฮฑฮนฮฟ ฮฑฯ€ฯŒ -4 ฮญฯ‰ฯ‚ 4. .usage-datalink-path-to-csv = ฮงฯฮฎฯƒฮท: DATALINK <ฮดฮนฮฑฮดฯฮฟฮผฮฎ-ฯ€ฯฮฟฯ‚-.csv> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: ฮดฮตฮฝ ฮฒฯฮญฮธฮทฮบฮฑฮฝ ฯƒฯ…ฮฝฮดฮตฮดฮตฮผฮญฮฝฮฟฮน ฯ€ฮฏฮฝฮฑฮบฮตฯ‚. @@ -2059,7 +2139,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = ฮ‘ฯฯ‡ฮตฮฏฮฟ .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2368,6 +2448,7 @@ common = .load-linetypes = ฮฆฯŒฯฯ„ฯ‰ฯƒฮท ฯ„ฯฯ€ฯ‰ฮฝ ฮณฯฮฑฮผฮผฮฎฯ‚ .requires-two-positive-values = ฮ‘ฯ€ฮฑฮนฯ„ฮฟฯฮฝฯ„ฮฑฮน ฮดฯฮฟ ฮธฮตฯ„ฮนฮบฮญฯ‚ ฯ„ฮนฮผฮญฯ‚ (x,y) ฮฎ ฮผฮฏฮฑ ฮบฮฑฮน ฮณฮนฮฑ ฯ„ฮนฯ‚ ฮดฯฮฟ. .snap-marker-color = ฮงฯฯŽฮผฮฑ ฮดฮตฮฏฮบฯ„ฮท ฮญฮปฮพฮทฯ‚ + .snap-marker-size = ฮœฮญฮณฮตฮธฮฟฯ‚ ฮดฮตฮฏฮบฯ„ฮท ฮญฮปฮพฮทฯ‚ .command-line-text-color = ฮงฯฯŽฮผฮฑ ฮบฮตฮนฮผฮญฮฝฮฟฯ… ฮณฯฮฑฮผฮผฮฎฯ‚ ฮตฮฝฯ„ฮฟฮปฯŽฮฝ .operand = ฮคฮตฮปฮตฯƒฯ„ฮญฮฟฯ‚ .linetypes-loaded = ฮฆฮฟฯฯ„ฯŽฮธฮทฮบฮฑฮฝ __ocs_fmt_0__ ฯ„ฯฯ€ฮฟฮน ฮณฯฮฑฮผฮผฮฎฯ‚. @@ -2381,6 +2462,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE ฮŸฯฮฏฯƒฯ„ฮต ฯƒฮทฮผฮตฮฏฮฟ ฮฒฮฌฯƒฮทฯ‚ [__ocs_arg_count__ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ]: .threedrotate-specify-rotation-axis = 3DROTATE ฮŸฯฮฏฯƒฯ„ฮต ฮฌฮพฮฟฮฝฮฑ ฯ€ฮตฯฮนฯƒฯ„ฯฮฟฯ†ฮฎฯ‚ [X/Y/Z] ฮฎ ฮดฮตฯฯ„ฮตฯฮฟ ฯƒฮทฮผฮตฮฏฮฟ ฯ„ฮฟฯ… : .threedrotate-specify-rotation-angle-a = 3DROTATE ฮŸฯฮฏฯƒฯ„ฮต ฮณฯ‰ฮฝฮฏฮฑ ฯ€ฮตฯฮนฯƒฯ„ฯฮฟฯ†ฮฎฯ‚ <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = ฮ ฯฮฟฮตฮนฮดฮฟฯ€ฮฟฮฏฮทฯƒฮท ฮฑฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮทฯ‚ .unable-to-save-drawing = ฮ‘ฮดฯ…ฮฝฮฑฮผฮฏฮฑ ฮฑฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮทฯ‚ ฯƒฯ‡ฮตฮดฮฏฮฟฯ… @@ -3005,6 +3093,7 @@ draw = .arc-specify-radius-of-arc = ARC ฮŸฯฮฏฯƒฯ„ฮต ฮฑฮบฯ„ฮฏฮฝฮฑ ฯ„ฯŒฮพฮฟฯ…: .chord-length = ฮœฮฎฮบฮฟฯ‚ ฯ‡ฮฟฯฮดฮฎฯ‚ + modify = .count-objects = (__ocs_arg_count__ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ) .edge-extend-indented-title-case = [ฮ‘ฮบฮผฮฎ: ฮ•ฯ€ฮญฮบฯ„ฮฑฯƒฮท] @@ -3050,6 +3139,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮผฯ€ฮปฮฟฮบ ฮผฮต ฯ‡ฮฑฯฮฑฮบฯ„ฮทฯฮนฯƒฯ„ฮนฮบฮฌ: .annotation-object-scale = ฮšฮปฮฏฮผฮฑฮบฮฑ ฮฑฮฝฯ„ฮนฮบฮตฮนฮผฮญฮฝฮฟฯ… ฮตฯ€ฮนฯƒฮทฮผฮตฮฏฯ‰ฯƒฮทฯ‚ .bedit-select-block-reference-to-edit = BEDIT ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮฑฮฝฮฑฯ†ฮฟฯฮฌ ฮผฯ€ฮปฮฟฮบ ฯ€ฯฮฟฯ‚ ฮตฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ: + .bvstate-option = BVSTATE ฮ”ฯŽฯƒฯ„ฮต ฮตฯ€ฮนฮปฮฟฮณฮฎ [ฮฮญฮฑ/ฮŸฯฮนฯƒฮผฯŒฯ‚/ฮœฮตฯ„ฮฟฮฝฮฟฮผฮฑฯƒฮฏฮฑ/ฮ”ฮนฮฑฮณฯฮฑฯ†ฮฎ] <ฮŸฯฮนฯƒฮผฯŒฯ‚>: + .bvstate-new-name = BVSTATE ฮ”ฯŽฯƒฯ„ฮต ฯŒฮฝฮฟฮผฮฑ ฮฝฮญฮฑฯ‚ ฮบฮฑฯ„ฮฌฯƒฯ„ฮฑฯƒฮทฯ‚ ฮฟฯฮฑฯ„ฯŒฯ„ฮทฯ„ฮฑฯ‚: + .bvstate-set = BVSTATE ฮ”ฯŽฯƒฯ„ฮต ฯ„ฮทฮฝ ฮบฮฑฯ„ฮฌฯƒฯ„ฮฑฯƒฮท ฮฟฯฮฑฯ„ฯŒฯ„ฮทฯ„ฮฑฯ‚ ฯ€ฮฟฯ… ฮธฮฑ ฮณฮฏฮฝฮตฮน ฯ„ฯฮญฯ‡ฮฟฯ…ฯƒฮฑ <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE ฮ”ฯŽฯƒฯ„ฮต ฯ„ฮทฮฝ ฮบฮฑฯ„ฮฌฯƒฯ„ฮฑฯƒฮท ฮฟฯฮฑฯ„ฯŒฯ„ฮทฯ„ฮฑฯ‚ ฯ€ฯฮฟฯ‚ ฮผฮตฯ„ฮฟฮฝฮฟฮผฮฑฯƒฮฏฮฑ <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE ฮ”ฯŽฯƒฯ„ฮต ฮฝฮญฮฟ ฯŒฮฝฮฟฮผฮฑ: + .bvstate-delete = BVSTATE ฮ”ฯŽฯƒฯ„ฮต ฯ„ฮทฮฝ ฮบฮฑฯ„ฮฌฯƒฯ„ฮฑฯƒฮท ฮฟฯฮฑฯ„ฯŒฯ„ฮทฯ„ฮฑฯ‚ ฯ€ฯฮฟฯ‚ ฮดฮนฮฑฮณฯฮฑฯ†ฮฎ: + .bvstate-not-found = ฮ— ฮบฮฑฯ„ฮฌฯƒฯ„ฮฑฯƒฮท ฮฟฯฮฑฯ„ฯŒฯ„ฮทฯ„ฮฑฯ‚ "__ocs_fmt_0__" ฮดฮตฮฝ ฮฒฯฮญฮธฮทฮบฮต. + .bvstate-exists = ฮ— ฮบฮฑฯ„ฮฌฯƒฯ„ฮฑฯƒฮท ฮฟฯฮฑฯ„ฯŒฯ„ฮทฯ„ฮฑฯ‚ "__ocs_fmt_0__" ฯ…ฯ€ฮฌฯฯ‡ฮตฮน ฮฎฮดฮท. + .bvstate-last = ฮ— ฮผฮฟฮฝฮฑฮดฮนฮบฮฎ ฮบฮฑฯ„ฮฌฯƒฯ„ฮฑฯƒฮท ฮฟฯฮฑฯ„ฯŒฯ„ฮทฯ„ฮฑฯ‚ ฮดฮตฮฝ ฮผฯ€ฮฟฯฮตฮฏ ฮฝฮฑ ฮดฮนฮฑฮณฯฮฑฯ†ฮตฮฏ. + .bvstate-needs-bedit = __ocs_fmt_0__: ฮฑฮฝฮฟฮฏฮพฯ„ฮต ฯ€ฯฯŽฯ„ฮฑ ฯ„ฮฟ ฮผฯ€ฮปฮฟฮบ ฯƒฯ„ฮฟฮฝ ฮตฯ€ฮตฮพฮตฯฮณฮฑฯƒฯ„ฮฎ ฮผฯ€ฮปฮฟฮบ (BEDIT). + .bvstate-current = ฮคฯฮญฯ‡ฮฟฯ…ฯƒฮฑ ฮบฮฑฯ„ฮฌฯƒฯ„ฮฑฯƒฮท ฮฟฯฮฑฯ„ฯŒฯ„ฮทฯ„ฮฑฯ‚: __ocs_fmt_0__ + .bvhide-select = ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ ฮณฮนฮฑ ฮฑฯ€ฯŒฮบฯฯ…ฯˆฮท: + .bvshow-select = ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ ฮณฮนฮฑ ฮตฮผฯ†ฮฌฮฝฮนฯƒฮท: + .arrayedit-select-array = ARRAYEDIT ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮดฮนฮฌฯ„ฮฑฮพฮท: + .arrayedit-options = ARRAYEDIT ฮ”ฯŽฯƒฯ„ฮต ฮตฯ€ฮนฮปฮฟฮณฮฎ [ฮ ฮทฮณฮฎ/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: ฯ„ฮฟ ฮตฯ€ฮนฮปฮตฮณฮผฮญฮฝฮฟ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฟ ฮดฮตฮฝ ฮตฮฏฮฝฮฑฮน ฯƒฯ…ฯƒฯ‡ฮตฯ„ฮนฯƒฯ„ฮนฮบฮฎ ฮดฮนฮฌฯ„ฮฑฮพฮท. .blend-enter-continuity-tangent-curvature-c = BLEND ฮ”ฯŽฯƒฯ„ฮต ฯƒฯ…ฮฝฮญฯ‡ฮตฮนฮฑ [ฮ•ฯ†ฮฑฯ€ฯ„ฮฟฮผฮตฮฝฮนฮบฮฎ/ฮšฮฑฮผฯ€ฯ…ฮปฯŒฯ„ฮทฯ„ฮฑ] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฯ€ฯฯŽฯ„ฮท ฮฑฮฝฮฟฮนฮบฯ„ฮฎ ฮบฮฑฮผฯ€ฯฮปฮท [ฮฃฯ…ฮฝฮญฯ‡ฮตฮนฮฑ=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮดฮตฯฯ„ฮตฯฮท ฮฑฮฝฮฟฮนฮบฯ„ฮฎ ฮบฮฑฮผฯ€ฯฮปฮท [ฮฃฯ…ฮฝฮญฯ‡ฮตฮนฮฑ=__ocs_arg_c__]: @@ -3602,6 +3707,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE ฮŸฯฮฏฯƒฯ„ฮต ฯƒฮทฮผฮตฮฏฮฟ ฮตฮนฯƒฮฑฮณฯ‰ฮณฮฎฯ‚: insert = + .pastespec-specify-insertion-point = PASTESPEC ฮšฮฑฮธฮฟฯฮฏฯƒฯ„ฮต ฯƒฮทฮผฮตฮฏฮฟ ฮตฮนฯƒฮฑฮณฯ‰ฮณฮฎฯ‚: + .pastespec-paste-as = PASTESPEC ฮ•ฯ€ฮนฮบฯŒฮปฮปฮทฯƒฮท ฯ‰ฯ‚ [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3718,6 +3825,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3779,6 +3893,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *ฮŸฮน ฮญฮปฮพฮตฮนฯ‚ ฯ…ฯ€ฮฟฮฒฮฌฮธฯฯ‰ฮฝ ฮดฮนฮฑฯ†ฮญฯฮฟฯ…ฮฝ* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3921,6 +4036,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮตฮนฮบฯŒฮฝฮฑ ฮฎ ฯ…ฯ€ฯŒฮฒฮฑฮธฯฮฟ: + .adjust-select-pdf-underlay = ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฯ…ฯ€ฯŒฮฒฮฑฮธฯฮฟ PDF: + .adjust-select-dwf-underlay = ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฯ…ฯ€ฯŒฮฒฮฑฮธฯฮฟ DWF: + .adjust-select-dgn-underlay = ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฯ…ฯ€ฯŒฮฒฮฑฮธฯฮฟ DGN: + .adjust-mixed-underlays = *ฮœฮท ฮญฮณฮบฯ…ฯฮฟ ฯƒฯฮฝฮฟฮปฮฟ ฮตฯ€ฮนฮปฮฟฮณฮฎฯ‚. ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮผฯŒฮฝฮฟ ฯ…ฯ€ฯŒฮฒฮฑฮธฯฮฑ ฯ„ฮฟฯ… ฮฏฮดฮนฮฟฯ… ฯ„ฯฯ€ฮฟฯ… ฮฑฯฯ‡ฮตฮฏฮฟฯ….* + .adjust-image-option = ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮตฯ€ฮนฮปฮฟฮณฮฎ ฮตฮนฮบฯŒฮฝฮฑฯ‚ [ฮ‘ฮฝฯ„ฮฏฮธฮตฯƒฮท/ฮžฮตฮธฯŽฯฮนฮฑฯƒฮผฮฑ/ฮฆฯ‰ฯ„ฮตฮนฮฝฯŒฯ„ฮทฯ„ฮฑ] <ฮฆฯ‰ฯ„ฮตฮนฮฝฯŒฯ„ฮทฯ„ฮฑ>: + .adjust-underlay-option = ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮตฯ€ฮนฮปฮฟฮณฮฎ ฯ…ฯ€ฮฟฮฒฮฌฮธฯฮฟฯ… __ocs_arg_kind__ [ฮžฮตฮธฯŽฯฮนฮฑฯƒฮผฮฑ/ฮ‘ฮฝฯ„ฮฏฮธฮตฯƒฮท/ฮœฮฟฮฝฯŒฯ‡ฯฯ‰ฮผฮฟ] <__ocs_arg_option__>: + .adjust-monochrome = ฮœฮฟฮฝฯŒฯ‡ฯฯ‰ฮผฮฟ? [ฮฮฑฮน/ฮŒฯ‡ฮน] <__ocs_arg_value__>: + .adjust-brightness-value = ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฯ„ฮนฮผฮฎ: ฯ†ฯ‰ฯ„ฮตฮนฮฝฯŒฯ„ฮทฯ„ฮฑ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฯ„ฮนฮผฮฎ: ฮฑฮฝฯ„ฮฏฮธฮตฯƒฮท (0-100) <__ocs_arg_value__>: + .adjust-fade-value = ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฯ„ฮนฮผฮฎ: ฮพฮตฮธฯŽฯฮนฮฑฯƒฮผฮฑ (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = ฮ— ฯ„ฮนฮผฮฎ ฯ€ฯฮญฯ€ฮตฮน ฮฝฮฑ ฮตฮฏฮฝฮฑฮน ฮฑฮบฮญฯฮฑฮนฮฟฯ‚ ฮฑฯฮนฮธฮผฯŒฯ‚ ฮฑฯ€ฯŒ 0 ฮญฯ‰ฯ‚ 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4415,7 +4542,27 @@ model = .value-2point = 2 ฯƒฮทฮผฮตฮฏฯ‰ฮฝ .cube = ฮšฯฮฒฮฟฯ‚ + layout = + .mvsetup-select-objects-to-delete = MVSETUP ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฯ„ฮฑ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ ฮณฮนฮฑ ฮดฮนฮฑฮณฯฮฑฯ†ฮฎ: + .delete-objects-option = ฮ”ฮนฮฑฮณฯฮฑฯ†ฮฎ ฮฑฮฝฯ„ฮนฮบฮตฮนฮผฮญฮฝฯ‰ฮฝ + .mvsetup-enable-paper-space = MVSETUP ฮ•ฮฝฮตฯฮณฮฟฯ€ฮฟฮฏฮทฯƒฮท ฯ‡ฯŽฯฮฟฯ… ฯ‡ฮฑฯฯ„ฮนฮฟฯ; [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฯƒฯ…ฮฝฯ„ฮตฮปฮตฯƒฯ„ฮฎ ฮบฮปฮฏฮผฮฑฮบฮฑฯ‚ <1>: + .mvsetup-enter-paper-width = MVSETUP ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฯ€ฮปฮฌฯ„ฮฟฯ‚ ฯ‡ฮฑฯฯ„ฮนฮฟฯ: + .mvsetup-enter-paper-height = MVSETUP ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฯฯˆฮฟฯ‚ ฯ‡ฮฑฯฯ„ฮนฮฟฯ: + .mvsetup-enter-option = MVSETUP ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮตฯ€ฮนฮปฮฟฮณฮฎ [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮดฮนฮฌฯ„ฮฑฮพฮท ฯ€ฮฑฯฮฑฮธฯฯฯ‰ฮฝ [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP ฮšฮฑฮธฮฟฯฮฏฯƒฯ„ฮต ฯ€ฯฯŽฯ„ฮท ฮณฯ‰ฮฝฮฏฮฑ ฯ„ฮทฯ‚ ฯ€ฮตฯฮนฮฟฯ‡ฮฎฯ‚ ฯ€ฮฑฯฮฑฮธฯฯฯ‰ฮฝ : + .mvsetup-specify-opposite-corner = MVSETUP ฮšฮฑฮธฮฟฯฮฏฯƒฯ„ฮต ฯ„ฮทฮฝ ฮฑฯ€ฮญฮฝฮฑฮฝฯ„ฮน ฮณฯ‰ฮฝฮฏฮฑ: + .mvsetup-viewports-in-x = MVSETUP ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮฑฯฮนฮธฮผฯŒ ฯ€ฮฑฯฮฑฮธฯฯฯ‰ฮฝ ฯƒฯ„ฮทฮฝ ฮบฮฑฯ„ฮตฯฮธฯ…ฮฝฯƒฮท X <2>: + .mvsetup-viewports-in-y = MVSETUP ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮฑฯฮนฮธฮผฯŒ ฯ€ฮฑฯฮฑฮธฯฯฯ‰ฮฝ ฯƒฯ„ฮทฮฝ ฮบฮฑฯ„ฮตฯฮธฯ…ฮฝฯƒฮท Y <2>: + .mvsetup-distance-in-x = MVSETUP ฮšฮฑฮธฮฟฯฮฏฯƒฯ„ฮต ฮฑฯ€ฯŒฯƒฯ„ฮฑฯƒฮท ฮผฮตฯ„ฮฑฮพฯ ฯ€ฮฑฯฮฑฮธฯฯฯ‰ฮฝ ฯƒฯ„ฮทฮฝ ฮบฮฑฯ„ฮตฯฮธฯ…ฮฝฯƒฮท X <0>: + .mvsetup-distance-in-y = MVSETUP ฮšฮฑฮธฮฟฯฮฏฯƒฯ„ฮต ฮฑฯ€ฯŒฯƒฯ„ฮฑฯƒฮท ฮผฮตฯ„ฮฑฮพฯ ฯ€ฮฑฯฮฑฮธฯฯฯ‰ฮฝ ฯƒฯ„ฮทฮฝ ฮบฮฑฯ„ฮตฯฮธฯ…ฮฝฯƒฮท Y <0>: + .mvsetup-select-viewports = MVSETUP ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฯ„ฮฑ ฯ€ฮฑฯฮฌฮธฯ…ฯฮฑ ฮณฮนฮฑ ฮบฮปฮนฮผฮฌฮบฯ‰ฯƒฮท: + .mvsetup-paper-space-units = MVSETUP ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮฑฯฮนฮธฮผฯŒ ฮผฮฟฮฝฮฌฮดฯ‰ฮฝ ฯ‡ฯŽฯฮฟฯ… ฯ‡ฮฑฯฯ„ฮนฮฟฯ <1>: + .mvsetup-model-space-units = MVSETUP ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮฑฯฮนฮธฮผฯŒ ฮผฮฟฮฝฮฌฮดฯ‰ฮฝ ฯ‡ฯŽฯฮฟฯ… ฮผฮฟฮฝฯ„ฮญฮปฮฟฯ… <1>: + .mvsetup-viewports-do-not-fit = MVSETUP ฮคฮฑ ฯ€ฮฑฯฮฌฮธฯ…ฯฮฑ ฮดฮตฮฝ ฯ‡ฯ‰ฯฮฟฯฮฝ ฯƒฯ„ฮทฮฝ ฯ€ฮตฯฮนฮฟฯ‡ฮฎ. + .scale-viewports-option = ฮšฮปฮนฮผฮฌฮบฯ‰ฯƒฮท ฯ€ฮฑฯฮฑฮธฯฯฯ‰ฮฝ .active = ฮ•ฮฝฮตฯฮณฮฎ .inactive = ฮ‘ฮฝฮตฮฝฮตฯฮณฮฎ .insert-view = ฮ•ฮนฯƒฮฑฮณฯ‰ฮณฮฎ ฯ€ฯฮฟฮฒฮฟฮปฮฎฯ‚ @@ -4441,6 +4588,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F <ฮตฯ€ฮฏฯ€ฮตฮดฮฟ> = ฮ ฮฌฮณฯ‰ฮผฮฑ | T <ฮตฯ€ฮฏฯ€ฮตฮดฮฟ> = ฮžฮตฯ€ฮฌฮณฯ‰ฮผฮฑ | Enter = ฮˆฮพฮฟฮดฮฟฯ‚ view = + .twist-option = ฮฃฯ„ฯฮญฯˆฮท + .dview-specify-view-twist-angle = DVIEW ฮšฮฑฮธฮฟฯฮฏฯƒฯ„ฮต ฮณฯ‰ฮฝฮฏฮฑ ฯƒฯ„ฯฮญฯˆฮทฯ‚ ฯŒฯˆฮทฯ‚ <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW ฮ•ฮนฯƒฮฑฮณฮฌฮณฮตฯ„ฮต ฮตฯ€ฮนฮปฮฟฮณฮฎ [TWist/Undo]: .color-scheme-set-to-system-currently-name = ฮŸ ฯ‡ฯฯ‰ฮผฮฑฯ„ฮนฮบฯŒฯ‚ ฯƒฯ…ฮฝฮดฯ…ฮฑฯƒฮผฯŒฯ‚ ฯฯ…ฮธฮผฮฏฯƒฯ„ฮทฮบฮต ฯƒฮต โ€žฮฃฯฯƒฯ„ฮทฮผฮฑโ€œ (ฮฑฯ…ฯ„ฮฎ ฯ„ฮท ฯƒฯ„ฮนฮณฮผฮฎ __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = ฮ‘ฯ€ฮฑฮนฯ„ฮตฮฏฯ„ฮฑฮน ฮฑฮบฮญฯฮฑฮนฮฟฯ‚ ฮฑฯฮนฮธฮผฯŒฯ‚ ฮผฮตฯ„ฮฑฮพฯ __ocs_fmt_0__ ฮบฮฑฮน __ocs_fmt_1__. @@ -4628,13 +4778,6 @@ view = .x-no-xdata = __ocs_fmt_0__: ฯ‡ฯ‰ฯฮฏฯ‚ xdata. .value-3d-orbit = ฮคฯฮฟฯ‡ฮนฮฌ 3D .value-3d-orbit-drag-with-right-mouse-button = ฮคฯฮฟฯ‡ฮนฮฌ 3D: ฯƒฯฯฮตฯ„ฮต ฮผฮต ฯ„ฮฟ ฮดฮตฮพฮฏ ฯ€ฮปฮฎฮบฯ„ฯฮฟ ฯ„ฮฟฯ… ฯ€ฮฟฮฝฯ„ฮนฮบฮนฮฟฯ. - .adjust-brightness-contrast-fade = ADJUST [ฮฆฯ‰ฯ„ฮตฮนฮฝฯŒฯ„ฮทฯ„ฮฑ / ฮ‘ฮฝฯ„ฮฏฮธฮตฯƒฮท / ฮžฮตฮธฯŽฯฮนฮฑฯƒฮผฮฑ]: - .adjust-brightness-0-100 = ADJUST ฯ†ฯ‰ฯ„ฮตฮนฮฝฯŒฯ„ฮทฯ„ฮฑ 0-100: - .adjust-contrast-0-100 = ADJUST ฮฑฮฝฯ„ฮฏฮธฮตฯƒฮท 0-100: - .adjust-fade-0-100 = ADJUST ฮพฮตฮธฯŽฯฮนฮฑฯƒฮผฮฑ 0-100: - .adjust-select-raster-image-s-first = ADJUST: ฮตฯ€ฮนฮปฮญฮพฯ„ฮต ฯ€ฯฯŽฯ„ฮฑ ฮตฮนฮบฯŒฮฝฮฑ(ฮตฯ‚) ฯฮฌฯƒฯ„ฮตฯ. - .adjust-value-must-be-0-100 = ADJUST: ฮท ฯ„ฮนฮผฮฎ ฯ€ฯฮญฯ€ฮตฮน ฮฝฮฑ ฮตฮฏฮฝฮฑฮน 0-100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ ฯƒฮต __ocs_fmt_2__ ฮตฮนฮบฯŒฮฝฮฑ(ฮตฯ‚). .annoallvisible-new-value-0-1 = ANNOALLVISIBLE ฮฝฮญฮฑ ฯ„ฮนฮผฮฎ [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: ฮดฯŽฯƒฯ„ฮต 0 ฮฎ 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE ฮฝฮญฮฑ ฯ„ฮนฮผฮฎ [-4..4]: @@ -4766,7 +4909,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = ฮฅฯ€ฯŒฮฒฮฑฮธฯฮฟ __ocs_fmt_0__: ฮพฮตฮธฯŽฯฮนฮฑฯƒฮผฮฑ=__ocs_fmt_1__, ฮฑฮฝฯ„ฮฏฮธฮตฯƒฮท=__ocs_fmt_2__, ฮตฮฝฮตฯฮณฯŒ=__ocs_fmt_3__, ฮฑฯ€ฮฟฮบฮฟฯ€ฮฎ=__ocs_fmt_4__, ฮผฮฟฮฝฯŒฯ‡ฯฯ‰ฮผฮฟ=__ocs_fmt_5__ .updated-changed-underlay-s = ฮ•ฮฝฮทฮผฮตฯฯŽฮธฮทฮบฮฑฮฝ __ocs_fmt_0__ ฯ…ฯ€ฯŒฮฒฮฑฮธฯฮฟ(ฮฑ). - .usage-adjust-brightness-contrast-fade-0-100 = ฮงฯฮฎฯƒฮท: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = ฮงฯฮฎฯƒฮท: ANNOSCALE <ฮปฯŒฮณฮฟฯ‚> ฯ€.ฯ‡. 1:50, 2:1 ฮฎ ฯƒฯ…ฮฝฯ„ฮตฮปฮตฯƒฯ„ฮฎฯ‚ .usage-draworder-f-front-b-back-a-above-handle = ฮงฯฮฎฯƒฮท: DRAWORDER F|FRONT | B|BACK | A|ABOVE <ฮฑฮฝฮฑฮณฮฝฯ‰ฯฮนฯƒฯ„ฮนฮบฯŒ> | U|UNDER <ฮฑฮฝฮฑฮณฮฝฯ‰ฯฮนฯƒฯ„ฮนฮบฯŒ> .usage-hyperlink-url-select-objects-first = ฮงฯฮฎฯƒฮท: HYPERLINK (ฮตฯ€ฮนฮปฮญฮพฯ„ฮต ฯ€ฯฯŽฯ„ฮฑ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ) @@ -5995,6 +6137,46 @@ ui = .osnap-settings = ฮกฯ…ฮธฮผฮฏฯƒฮตฮนฯ‚ ฮญฮปฮพฮทฯ‚... .snap-override-none-next-pick-only = ฮ ฯฮฟฯƒฯ‰ฯฮนฮฝฮฎ ฮญฮปฮพฮท: ฮšฮฑฮผฮฏฮฑ (ฮผฯŒฮฝฮฟ ฮณฮนฮฑ ฯ„ฮฟ ฮตฯ€ฯŒฮผฮตฮฝฮฟ ฯƒฮทฮผฮตฮฏฮฟ). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = ฮคฮฟ ฮบฮตฮฏฮผฮตฮฝฮฟ ฮฝฮฑ ฯ€ฮฑฯฮฑฮผฮญฮฝฮตฮน ฯ€ฮฌฮฝฯ„ฮฑ ฮผฮตฯ„ฮฑฮพฯ ฯ„ฯ‰ฮฝ ฮณฯฮฑฮผฮผฯŽฮฝ ฯ€ฯฮฟฮญฮบฯ„ฮฑฯƒฮทฯ‚ .angular-dimensions = ฮ“ฯ‰ฮฝฮนฮฑฮบฮญฯ‚ ฮดฮนฮฑฯƒฯ„ฮฌฯƒฮตฮนฯ‚ @@ -6262,3 +6444,117 @@ graphic-attributes = .reset-angle = ฮ•ฯ€ฮฑฮฝฮฑฯ†ฮฟฯฮฌ ฮณฯ‰ฮฝฮฏฮฑฯ‚ .one-color = ฮˆฮฝฮฑ ฯ‡ฯฯŽฮผฮฑ .two-colors = ฮ”ฯฮฟ ฯ‡ฯฯŽฮผฮฑฯ„ฮฑ + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = ฮ ฯฮฟฯƒฮธฮฎฮบฮท ฯƒฯ„ฮฟ ฯƒฯฮฝฮฟฮปฮฟ ฮตฯฮณฮฑฯƒฮฏฮฑฯ‚ + .remove-from-working-set = ฮ‘ฯ†ฮฑฮฏฯฮตฯƒฮท ฮฑฯ€ฯŒ ฯ„ฮฟ ฯƒฯฮฝฮฟฮปฮฟ ฮตฯฮณฮฑฯƒฮฏฮฑฯ‚ + .toolbar-objects = ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ + .dialog-title = ฮ•ฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ ฮฑฮฝฮฑฯ†ฮฟฯฮฌฯ‚ + .dialog-reference = ฮ‘ฮฝฮฑฯ†ฮฟฯฮฌ ฯ€ฯฮฟฯ‚ ฮตฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ + .dialog-chain-hint = ฮ— ฮฑฮปฯ…ฯƒฮฏฮดฮฑ ฯƒฯ„ฮฟ ฮตฯ€ฮนฮปฮตฮณฮผฮญฮฝฮฟ ฯƒฮทฮผฮตฮฏฮฟยท ฮบฮฌฮฝฯ„ฮต ฮบฮปฮนฮบ ฯƒฮต ฮผฮนฮฑ ฮตฯ„ฮนฮบฮญฯ„ฮฑ ฮณฮนฮฑ ฮฑฮปฮปฮฑฮณฮฎ ฮตฯ€ฮนฯ€ฮญฮดฮฟฯ…. + .dialog-path = ฮ”ฮนฮฑฮดฯฮฟฮผฮฎ: + .dialog-working-set = ฮฃฯฮฝฮฟฮปฮฟ ฮตฯฮณฮฑฯƒฮฏฮฑฯ‚ + .dialog-all-objects = ฮŒฮปฮฑ ฯ„ฮฑ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ + .dialog-pick-objects = ฮ•ฯ€ฮนฮปฮฟฮณฮฎ ฯƒฯ„ฮฟ ฯƒฯ‡ฮญฮดฮนฮฟ + .dialog-attributes = ฮ•ฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ ฮบฮฑฮน ฯ„ฯ‰ฮฝ ฮฟฯฮนฯƒฮผฯŽฮฝ ฯ‡ฮฑฯฮฑฮบฯ„ฮทฯฮนฯƒฯ„ฮนฮบฯŽฮฝ + .dialog-session = ฮฃฯ…ฮฝฮตฮดฯฮฏฮฑ + .dialog-lock = ฮšฮปฮตฮฏฮดฯ‰ฮผฮฑ ฮฑฮฝฯ„ฮนฮบฮตฮนฮผฮญฮฝฯ‰ฮฝ ฮตฮบฯ„ฯŒฯ‚ ฯƒฯ…ฮฝฯŒฮปฮฟฯ… ฮตฯฮณฮฑฯƒฮฏฮฑฯ‚ + .dialog-unique = ฮœฮฟฮฝฮฑฮดฮนฮบฮฌ ฮฟฮฝฯŒฮผฮฑฯ„ฮฑ ฮณฮนฮฑ ฮตฯ€ฮฏฯ€ฮตฮดฮฑ, ฯƒฯ„ฯ…ฮป ฮบฮฑฮน ฮผฯ€ฮปฮฟฮบ ฯ€ฮฟฯ… ฯƒฯ…ฮณฮบฯฮฟฯฮฟฮฝฯ„ฮฑฮน + .dialog-start = ฮˆฮฝฮฑฯฮพฮท ฮตฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑฯ‚ + .dialog-help = ฮ‘ฮฝฮฟฮฏฮณฮตฮน ฮฑฮฝฮฑฯ†ฮฟฯฮฌ ฮผฯ€ฮปฮฟฮบ ฮฎ ฮตฮพฯ‰ฯ„ฮตฯฮนฮบฮฎ ฮฑฮฝฮฑฯ†ฮฟฯฮฌ ฮณฮนฮฑ ฮตฯ€ฮตฮพฮตฯฮณฮฑฯƒฮฏฮฑ ฮตฯ€ฮฏ ฯ„ฯŒฯ€ฮฟฯ…. + .select-reference = ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮฑฮฝฮฑฯ†ฮฟฯฮฌ: + .reference-not-found = ฮ— ฮฑฮฝฮฑฯ†ฮฟฯฮฌ ฮดฮตฮฝ ฮฒฯฮญฮธฮทฮบฮต. + .not-valid-reference = ฮคฮฟ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฟ ฮดฮตฮฝ ฮตฮฏฮฝฮฑฮน ฮญฮณฮบฯ…ฯฮท ฮฑฮฝฮฑฯ†ฮฟฯฮฌ + .one-entity-added = ฮ ฯฮฟฯƒฯ„ฮญฮธฮทฮบฮต 1 ฮฟฮฝฯ„ฯŒฯ„ฮทฯ„ฮฑ + .nesting-level = ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮตฯ€ฮฏฯ€ฮตฮดฮฟ ฮญฮฝฮธฮตฯƒฮทฯ‚ [Ok/ฮ•ฯ€ฯŒฮผฮตฮฝฮฟ] <ฮ•ฯ€ฯŒฮผฮตฮฝฮฟ>: + .selection-method = ฮ”ฯŽฯƒฯ„ฮต ฮผฮญฮธฮฟฮดฮฟ ฮตฯ€ฮนฮปฮฟฮณฮฎฯ‚ ฮฑฮฝฯ„ฮนฮบฮตฮนฮผฮญฮฝฯ‰ฮฝ [ฮŒฮปฮฑ/ฮˆฮฝฮธฮตฯ„ฮฑ] <ฮŒฮปฮฑ>: + .select-nested = ฮ•ฯ€ฮนฮปฮญฮพฯ„ฮต ฮญฮฝฮธฮตฯ„ฮฑ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ: + .display-attdefs = ฮ•ฮผฯ†ฮฌฮฝฮนฯƒฮท ฮฟฯฮนฯƒฮผฯŽฮฝ ฯ‡ฮฑฯฮฑฮบฯ„ฮทฯฮนฯƒฯ„ฮนฮบฯŽฮฝ [ฮฮฑฮน/ฮŒฯ‡ฮน] <ฮŒฯ‡ฮน>: + .zero-items-selected = ฮ•ฯ€ฮนฮปฮญฯ‡ฮธฮทฮบฮฑฮฝ 0 ฯƒฯ„ฮฟฮนฯ‡ฮตฮฏฮฑ + .one-item-selected = ฮ•ฯ€ฮนฮปฮญฯ‡ฮธฮทฮบฮต 1 ฯƒฯ„ฮฟฮนฯ‡ฮตฮฏฮฟ + .items-selected = ฮ•ฯ€ฮนฮปฮญฯ‡ฮธฮทฮบฮฑฮฝ __ocs_fmt_0__ ฯƒฯ„ฮฟฮนฯ‡ฮตฮฏฮฑ + .refset-option = ฮ”ฯŽฯƒฯ„ฮต ฮตฯ€ฮนฮปฮฟฮณฮฎ [ฮ ฯฮฟฯƒฮธฮฎฮบฮท/ฮ‘ฯ†ฮฑฮฏฯฮตฯƒฮท] <ฮ ฯฮฟฯƒฮธฮฎฮบฮท>: + .refclose-option = ฮ”ฯŽฯƒฯ„ฮต ฮตฯ€ฮนฮปฮฟฮณฮฎ [ฮ‘ฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮท/ฮ‘ฯ€ฯŒฯฯฮนฯˆฮท ฮฑฮปฮปฮฑฮณฯŽฮฝ ฮฑฮฝฮฑฯ†ฮฟฯฮฌฯ‚] <ฮ‘ฯ€ฮฟฮธฮฎฮบฮตฯ…ฯƒฮท>: + .not-checked-out = ** ฮ— ฮตฮฝฯ„ฮฟฮปฮฎ ฮดฮตฮฝ ฮตฯ€ฮนฯ„ฯฮญฯ€ฮตฯ„ฮฑฮน ฮตฮบฯ„ฯŒฯ‚ ฮฑฮฝ ฮผฮนฮฑ ฮฑฮฝฮฑฯ†ฮฟฯฮฌ ฮตฯ€ฮตฮพฮตฯฮณฮฌฮถฮตฯ„ฮฑฮน ฮผฮต ฯ„ฮทฮฝ REFEDIT ** + .already-checked-out = ** ฮ— ฮตฮฝฯ„ฮฟฮปฮฎ ฮดฮตฮฝ ฮตฯ€ฮนฯ„ฯฮญฯ€ฮตฯ„ฮฑฮน, ฯ„ฮฟ __ocs_fmt_0__ ฮตฯ€ฮตฮพฮตฯฮณฮฌฮถฮตฯ„ฮฑฮน ฮฎฮดฮท ** + .transfer-objects = ฮœฮตฯ„ฮฑฯ†ฮฟฯฮฌ ฮฑฮฝฯ„ฮนฮบฮตฮนฮผฮญฮฝฯ‰ฮฝ ฮผฮตฯ„ฮฑฮพฯ ฯ„ฮฟฯ… ฯƒฯ…ฮฝฯŒฮปฮฟฯ… ฮตฯฮณฮฑฯƒฮฏฮฑฯ‚ RefEdit ฮบฮฑฮน ฯ„ฮฟฯ… ฮบฮตฮฝฯ„ฯฮนฮบฮฟฯ ฯƒฯ‡ฮตฮดฮฏฮฟฯ…... + .added-to-working-set = __ocs_fmt_0__ ฯ€ฯฮฟฯƒฯ„ฮญฮธฮทฮบฮฑฮฝ ฯƒฯ„ฮฟ ฯƒฯฮฝฮฟฮปฮฟ ฮตฯฮณฮฑฯƒฮฏฮฑฯ‚ + .removed-from-working-set = __ocs_fmt_0__ ฮฑฯ†ฮฑฮนฯฮญฮธฮทฮบฮฑฮฝ ฮฑฯ€ฯŒ ฯ„ฮฟ ฯƒฯฮฝฮฟฮปฮฟ ฮตฯฮณฮฑฯƒฮฏฮฑฯ‚ + .one-object-added = 1 ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฟ ฯ€ฯฮฟฯƒฯ„ฮญฮธฮทฮบฮต ฯƒฯ„ฮฟ __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ ฯ€ฯฮฟฯƒฯ„ฮญฮธฮทฮบฮฑฮฝ ฯƒฯ„ฮฟ __ocs_fmt_1__ + .one-object-removed = 1 ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฟ ฮฑฯ†ฮฑฮนฯฮญฮธฮทฮบฮต ฮฑฯ€ฯŒ ฯ„ฮฟ __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ ฮฑฮฝฯ„ฮนฮบฮตฮฏฮผฮตฮฝฮฑ ฮฑฯ†ฮฑฮนฯฮญฮธฮทฮบฮฑฮฝ ฮฑฯ€ฯŒ ฯ„ฮฟ __ocs_fmt_1__ + .one-xref-instance = ฮ•ฮฝฮทฮผฮตฯฯŽฮธฮทฮบฮต 1 ฯƒฯ„ฮนฮณฮผฮนฯŒฯ„ฯ…ฯ€ฮฟ xref + .xref-instances = ฮ•ฮฝฮทฮผฮตฯฯŽฮธฮทฮบฮฑฮฝ __ocs_fmt_0__ ฯƒฯ„ฮนฮณฮผฮนฯŒฯ„ฯ…ฯ€ฮฑ xref + .one-block-instance = ฮ•ฮฝฮทฮผฮตฯฯŽฮธฮทฮบฮต 1 ฯƒฯ„ฮนฮณฮผฮนฯŒฯ„ฯ…ฯ€ฮฟ ฮผฯ€ฮปฮฟฮบ + .block-instances = ฮ•ฮฝฮทฮผฮตฯฯŽฮธฮทฮบฮฑฮฝ __ocs_fmt_0__ ฯƒฯ„ฮนฮณฮผฮนฯŒฯ„ฯ…ฯ€ฮฑ ฮผฯ€ฮปฮฟฮบ + .redefined-reloaded = ฮคฮฟ __ocs_fmt_0__ ฮตฯ€ฮฑฮฝฮฑฮฟฯฮฏฯƒฯ„ฮทฮบฮต ฮบฮฑฮน ฮตฯ€ฮฑฮฝฮฑฯ†ฮฟฯฯ„ฯŽฮธฮทฮบฮต. + .redefined = ฮคฮฟ __ocs_fmt_0__ ฮตฯ€ฮฑฮฝฮฑฮฟฯฮฏฯƒฯ„ฮทฮบฮต. + .ok = Ok + .nested = ฮˆฮฝฮธฮตฯ„ฮฑ + +image-adjust = + .title = ฮกฯฮธฮผฮนฯƒฮท ฮตฮนฮบฯŒฮฝฮฑฯ‚ + .preview = ฮ ฯฮฟฮตฯ€ฮนฯƒฮบฯŒฯ€ฮทฯƒฮท + .before = ฮ ฯฮนฮฝ + .after = ฮœฮตฯ„ฮฌ + .no-preview = ฮท ฮตฮนฮบฯŒฮฝฮฑ ฮดฮตฮฝ ฯ†ฮฟฯฯ„ฯŽฮธฮทฮบฮต + .adjust = ฮกฯฮธฮผฮนฯƒฮท + .brightness = ฮฆฯ‰ฯ„ฮตฮนฮฝฯŒฯ„ฮทฯ„ฮฑ + .contrast = ฮ‘ฮฝฯ„ฮฏฮธฮตฯƒฮท + .fade = ฮžฮตฮธฯŽฯฮนฮฑฯƒฮผฮฑ + .hint = ฮšฮฌฮฝฯ„ฮต ฮบฮปฮนฮบ ฮฎ ฯƒฯฯฮตฯ„ฮต ฮผฮนฮฑ ฮผฯ€ฮฌฯฮฑยท โˆ’ / + ฮตฮฏฮฝฮฑฮน ฮญฮฝฮฑ ฮฒฮฎฮผฮฑ, ฮญฮฝฮฑ ฯ€ฮปฮฑฮฏฯƒฮนฮฟ ฮดฮญฯ‡ฮตฯ„ฮฑฮน ฮฑฮบฮญฯฮฑฮนฮฟ 0โ€“100. + .reset = ฮ•ฯ€ฮฑฮฝฮฑฯ†ฮฟฯฮฌ ฯ€ฯฮฟฮตฯ€ฮนฮปฮฟฮณฯŽฮฝ + .apply = ฮ•ฯ†ฮฑฯฮผฮฟฮณฮฎ + .help = ฮŸฯฮฏฮถฮตฮน ฯ„ฮท ฯ†ฯ‰ฯ„ฮตฮนฮฝฯŒฯ„ฮทฯ„ฮฑ, ฯ„ฮทฮฝ ฮฑฮฝฯ„ฮฏฮธฮตฯƒฮท ฮบฮฑฮน ฯ„ฮฟ ฮพฮตฮธฯŽฯฮนฮฑฯƒฮผฮฑ ฯ„ฯ‰ฮฝ ฮตฯ€ฮนฮปฮตฮณฮผฮญฮฝฯ‰ฮฝ ฮตฮนฮบฯŒฮฝฯ‰ฮฝ ฯฮฌฯƒฯ„ฮตฯ. + +keytips = + .title = KeyTip + .typed = ฮ ฮปฮทฮบฯ„ฯฮฟฮปฮฟฮณฮฎฮธฮทฮบฮต: { $path } + .hint-tabs = ฮ ฮปฮทฮบฯ„ฯฮฟฮปฮฟฮณฮฎฯƒฯ„ฮต ฮณฯฮฌฮผฮผฮฑ ฮบฮฑฯฯ„ฮญฮปฮฑฯ‚ ยท ฮคฮฟ Esc ฮบฮปฮตฮฏฮฝฮตฮน + .hint-tab = ฮšฮฑฯฯ„ฮญฮปฮฑ { $tab } ยท ฮคฮฟ Esc ฮตฯ€ฮนฯƒฯ„ฯฮญฯ†ฮตฮน ฮญฮฝฮฑ ฮตฯ€ฮฏฯ€ฮตฮดฮฟ + .hint-items = ฮ ฮปฮทฮบฯ„ฯฮฟฮปฮฟฮณฮฎฯƒฯ„ฮต ฮณฯฮฌฮผฮผฮฑ ฯƒฯ„ฮฟฮนฯ‡ฮตฮฏฮฟฯ… ยท ฮคฮฟ Esc ฮตฯ€ฮนฯƒฯ„ฯฮญฯ†ฮตฮน ฮญฮฝฮฑ ฮตฯ€ฮฏฯ€ฮตฮดฮฟ diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index ee3cb8fc8..15c13a19e 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Plugins ribbon-tab = + .block-editor = Block Editor .draw = Draw .parametric = Parametric .annotate = Annotate @@ -98,6 +99,9 @@ start = .no-recent-files = Files you open will show up here. .browser-storage = Browser storage .keep-recent-files = Keep recent files + .view-list = List + .view-icons = Icons + .view-preview = Large previews donation = .title = Request for Donation @@ -167,6 +171,14 @@ modal = .that-object = that object .those-objects = those objects + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Pasted image "{ $name }": { $width }ร—{ $height } px .image-unsupported = The pasted image "{ $name }" could not be decoded. @@ -273,8 +285,70 @@ command-copy = .mode = COPY Enter a copy mode option [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ only here (green), __ocs_fmt_1__ only in the other drawing (red)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Compare with drawing + .label-associative-array-created = __ocs_fmt_0__: associative array created. + .assoc-array-array-path = Array (Path) + .assoc-array-array-polar = Array (Polar) + .assoc-array-array-rectangular = Array (Rectangular) + .assoc-array-item-spacing = Item spacing + .assoc-array-fill-angle = Fill angle + .assoc-array-items = Items + .assoc-array-level-spacing = Level spacing + .assoc-array-levels = Levels + .assoc-array-row-spacing = Row spacing + .assoc-array-column-spacing = Column spacing + .fbxexport-exported-to = FBXEXPORT: exported to "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: no 3D mesh data in this drawing. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = Export FBX + .fbx-files = STEP Files + .options-hpassoc = New hatches follow their boundaries (HPASSOC) + .options-show-ribbon = Show the ribbon (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Show the menu bar (MENUBAR) + .options-window-elements = Window Elements + .menu-viewports = Viewports + .menu-view-twist = View Twist + .menu-tools = Tools + .menu-table-style = Table Style + .menu-run-script = Run Script + .menu-revision-cloud = Revision Cloud + .menu-regen-all = Regen All + .menu-redraw = Redraw + .menu-rectangle = Rectangle + .menu-quick-dimension = Quick Dimension + .menu-publish = Publish + .menu-paste-special = Paste Special + .menu-parametric = Parametric + .menu-page-setup-manager = Page Setup Manager + .menu-orbit = Orbit + .menu-new-ucs = New UCS + .menu-named-views = Named Views + .menu-multiline-text = Multiline Text + .menu-multiline-style = Multiline Style + .menu-multiline = Multiline + .menu-multileader-style = Multileader Style + .menu-match-properties = Match Properties + .menu-lengthen = Lengthen + .menu-jogged = Jogged + .menu-id-point = ID Point + .menu-explode-text = Explode Text + .menu-edit-spline = Edit Spline + .menu-edit-polyline = Edit Polyline + .menu-edit-hatch = Edit Hatch + .menu-drawing-limits = Drawing Limits + .menu-donut = Donut + .menu-dimension-style = Dimension Style + .menu-construction-line = Construction Line + .menu-arc-length = Arc Length + .command-line-opacity = Command line opacity + .txtexp-no-text-selected = TXTEXP: no text selected. + .angle-override-shown = Angle Override: __ocs_fmt_0__ .browser-cannot-open-typed-path = The browser cannot open a typed path; pick the file instead. .recover-drawing = Recover Drawing + .requires-0-or-1-or-keyword = Requires 0 or 1 only, or option keyword. + .requires-a-number = Requires a number. .restore-it-as-a-recovered-tab-or-discard-the-copy = Restore it as a recovered tab, or discard the copy. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" has an autosave copy newer than the last save. .autosave-copy-could-not-be-stored = Autosave copy could not be stored: __ocs_fmt_0__ @@ -820,6 +894,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: select a section object or turn on a live section. + .sectioncull-prepare-failed = SECTIONCULL: a cut object could not be prepared; nothing was changed. + .sectioncull-nothing-outside = SECTIONCULL: nothing lies outside the section. + .sectioncull-result = SECTIONCULL: erased __ocs_arg_erased__ object(s) and cut __ocs_arg_cut__ across the section. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: fit a spline through __ocs_fmt_0__ points. .splinefit-need-at-least-3-points = SPLINEFIT: need at least 3 points. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: select a polyline to fit a spline through. @@ -857,9 +935,12 @@ common = .sent-to-printer-printer = Sent to printer: __ocs_fmt_0__ .set-current = Set Current .shade-plot = Shade plot + .shaded-with-edges = Shaded with edges + .shades-of-gray = Shades of Gray .shadow-flags = Shadow flags .single = Single .size = Size + .sketchy = Sketchy .snap-override-label-next-pick-only = Snap override: __ocs_fmt_0__ (next pick only). .solid-created = Solid created. .source-object-not-found = Source object not found. @@ -1065,7 +1146,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: expected Left, Top, or Right. .isodraft-expected-on-or-off = ISODRAFT: expected On or Off. .xdata-selected-entities-are-on-locked-layers = XDATA: selected entities are on locked layers. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: enter an integer from -4 through 4. .usage-datalink-path-to-csv = Usage: DATALINK .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: no linked tables found. @@ -2369,6 +2449,7 @@ common = .load-linetypes = Load Linetypes .requires-two-positive-values = Requires two positive values (x,y) or one for both. .snap-marker-color = Snap marker color + .snap-marker-size = Snap marker size .command-line-text-color = Command line text color .operand = Operand .linetypes-loaded = __ocs_fmt_0__ linetype(s) loaded. @@ -2382,6 +2463,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Specify base point [__ocs_arg_count__ objects]: .threedrotate-specify-rotation-axis = 3DROTATE Specify rotation axis [X/Y/Z] or a second point on it : .threedrotate-specify-rotation-angle-a = 3DROTATE Specify rotation angle <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Save Warning .unable-to-save-drawing = Unable to Save Drawing @@ -3051,6 +3139,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Select block with attributes: .annotation-object-scale = Annotation Object Scale .bedit-select-block-reference-to-edit = BEDIT Select block reference to edit: + .bvstate-option = BVSTATE Enter an option [New/Set/Rename/Delete] : + .bvstate-new-name = BVSTATE Enter name of new visibility state: + .bvstate-set = BVSTATE Enter visibility state to make current <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Enter visibility state to rename <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Enter new name: + .bvstate-delete = BVSTATE Enter visibility state to delete: + .bvstate-not-found = Visibility state "__ocs_fmt_0__" not found. + .bvstate-exists = Visibility state "__ocs_fmt_0__" already exists. + .bvstate-last = The only visibility state cannot be deleted. + .bvstate-needs-bedit = __ocs_fmt_0__: open the block in the Block Editor first (BEDIT). + .bvstate-current = Current visibility state: __ocs_fmt_0__ + .bvhide-select = Select objects to hide: + .bvshow-select = Select objects to make visible: + .arrayedit-select-array = ARRAYEDIT Select array: + .arrayedit-options = ARRAYEDIT Enter an option [Source/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: the selected object is not an associative array. .blend-enter-continuity-tangent-curvature-c = BLEND Enter continuity [Tangent/Curvature] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND Select first open curve [Continuity=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Select second open curve [Continuity=__ocs_arg_c__]: @@ -3603,6 +3707,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Specify insertion point: insert = + .pastespec-specify-insertion-point = PASTESPEC Specify insertion point: + .pastespec-paste-as = PASTESPEC Paste as [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3719,6 +3825,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3773,6 +3886,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Underlay OSnap's vary* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3922,6 +4036,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Select image or underlay: + .adjust-select-pdf-underlay = Select PDF underlay: + .adjust-select-dwf-underlay = Select DWF underlay: + .adjust-select-dgn-underlay = Select DGN underlay: + .adjust-mixed-underlays = *Invalid selection set. Select underlay objects of the same file type only.* + .adjust-image-option = Enter image option [Contrast/Fade/Brightness] : + .adjust-underlay-option = Enter __ocs_arg_kind__ underlay option [Fade/Contrast/Monochrome] <__ocs_arg_option__>: + .adjust-monochrome = Monochrome? [Yes/No] <__ocs_arg_value__>: + .adjust-brightness-value = Enter brightness value (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Enter contrast value (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Enter fade value (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = Value must be an integer between 0 and 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4417,6 +4543,25 @@ model = .cube = Cube layout = + .mvsetup-select-objects-to-delete = MVSETUP Select the objects to delete: + .delete-objects-option = Delete objects + .mvsetup-enable-paper-space = MVSETUP Enable paper space? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Enter the scale factor <1>: + .mvsetup-enter-paper-width = MVSETUP Enter the paper width: + .mvsetup-enter-paper-height = MVSETUP Enter the paper height: + .mvsetup-enter-option = MVSETUP Enter an option [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Enter viewport layout [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Specify first corner of bounding area for viewport(s) : + .mvsetup-specify-opposite-corner = MVSETUP Specify opposite corner: + .mvsetup-viewports-in-x = MVSETUP Enter number of viewports in X direction <2>: + .mvsetup-viewports-in-y = MVSETUP Enter number of viewports in Y direction <2>: + .mvsetup-distance-in-x = MVSETUP Specify distance between viewports in X direction <0>: + .mvsetup-distance-in-y = MVSETUP Specify distance between viewports in Y direction <0>: + .mvsetup-select-viewports = MVSETUP Select the viewports to scale: + .mvsetup-paper-space-units = MVSETUP Enter the number of paper space units <1>: + .mvsetup-model-space-units = MVSETUP Enter the number of model space units <1>: + .mvsetup-viewports-do-not-fit = MVSETUP The viewports do not fit in the bounding area. + .scale-viewports-option = Scale viewports .active = Active .inactive = Inactive .insert-view = Insert view @@ -4442,6 +4587,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Freeze | T = Thaw | Enter = Exit view = + .twist-option = TWist + .dview-specify-view-twist-angle = DVIEW Specify view twist angle <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Enter option [TWist/Undo]: .color-scheme-set-to-system-currently-name = Color scheme set to 'System' (currently __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = Requires an integer between __ocs_fmt_0__ and __ocs_fmt_1__. @@ -4629,13 +4777,6 @@ view = .x-no-xdata = __ocs_fmt_0__: no xdata. .value-3d-orbit = 3D Orbit .value-3d-orbit-drag-with-right-mouse-button = 3D Orbit: drag with right mouse button. - .adjust-brightness-contrast-fade = ADJUST [Brightness / Contrast / Fade]: - .adjust-brightness-0-100 = ADJUST brightness 0-100: - .adjust-contrast-0-100 = ADJUST contrast 0-100: - .adjust-fade-0-100 = ADJUST fade 0-100: - .adjust-select-raster-image-s-first = ADJUST: select raster image(s) first. - .adjust-value-must-be-0-100 = ADJUST: value must be 0-100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ on __ocs_fmt_2__ image(s). .annoallvisible-new-value-0-1 = ANNOALLVISIBLE new value [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: enter 0 or 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE new value [-4..4]: @@ -4767,7 +4908,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Underlay __ocs_fmt_0__: fade=__ocs_fmt_1__, contrast=__ocs_fmt_2__, on=__ocs_fmt_3__, clip=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = Updated __ocs_fmt_0__ underlay(s). - .usage-adjust-brightness-contrast-fade-0-100 = Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Usage: ANNOSCALE e.g. 1:50, 2:1, or a factor .usage-draworder-f-front-b-back-a-above-handle = Usage: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Usage: HYPERLINK (select objects first) @@ -5996,6 +6136,46 @@ ui = .osnap-settings = Osnap Settings... .snap-override-none-next-pick-only = Snap override: None (next pick only). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Always keep text between extension lines .angular-dimensions = Angular dimensions @@ -6263,3 +6443,117 @@ graphic-attributes = .reset-angle = Reset angle .one-color = One Color .two-colors = Two Colors + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Add to Working Set + .remove-from-working-set = Remove from Working Set + .toolbar-objects = objects + .dialog-title = Reference Edit + .dialog-reference = Reference to edit + .dialog-chain-hint = The chain at the picked point; click a chip to change the level. + .dialog-path = Path: + .dialog-working-set = Working set + .dialog-all-objects = All objects + .dialog-pick-objects = Pick in the drawing + .dialog-attributes = Also edit attribute definitions + .dialog-session = Session + .dialog-lock = Lock objects not in the working set + .dialog-unique = Give clashing layer, style and block names unique names + .dialog-start = Start Editing + .dialog-help = Checks out a block reference or an external reference for editing in place. + .select-reference = Select reference: + .reference-not-found = Reference not found. + .not-valid-reference = Object is not a valid reference + .one-entity-added = 1 entity added + .nesting-level = Select nesting level [Ok/Next] : + .selection-method = Enter object selection method [All/Nested] : + .select-nested = Select nested objects: + .display-attdefs = Display attribute definitions [Yes/No] : + .zero-items-selected = 0 items selected + .one-item-selected = 1 item selected + .items-selected = __ocs_fmt_0__ items selected + .refset-option = Enter an option [Add/Remove] : + .refclose-option = Enter option [Save/Discard reference changes] : + .not-checked-out = ** Command not allowed unless a reference is checked out with REFEDIT command ** + .already-checked-out = ** Command not allowed, __ocs_fmt_0__ already checked out for editing ** + .transfer-objects = Transfer objects between the RefEdit working set and host drawing... + .added-to-working-set = __ocs_fmt_0__ Added to working set + .removed-from-working-set = __ocs_fmt_0__ Removed from working set + .one-object-added = 1 object added to __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ objects added to __ocs_fmt_1__ + .one-object-removed = 1 object removed from __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ objects removed from __ocs_fmt_1__ + .one-xref-instance = 1 xref instance updated + .xref-instances = __ocs_fmt_0__ xref instances updated + .one-block-instance = 1 block instance updated + .block-instances = __ocs_fmt_0__ block instances updated + .redefined-reloaded = __ocs_fmt_0__ redefined and reloaded. + .redefined = __ocs_fmt_0__ redefined. + .ok = Ok + .nested = Nested + +image-adjust = + .title = Image Adjust + .preview = Preview + .before = Before + .after = After + .no-preview = image could not be loaded + .adjust = Adjust + .brightness = Brightness + .contrast = Contrast + .fade = Fade + .hint = Click or drag a bar; โˆ’ / + is one step, a box takes a whole number 0โ€“100. + .reset = Restore defaults + .apply = Apply + .help = Sets the brightness, contrast and fade of the selected raster images. + +keytips = + .title = KeyTip + .typed = Typed: { $path } + .hint-tabs = Type a tab letter ยท Esc closes + .hint-tab = { $tab } tab ยท Esc goes back one level + .hint-items = Type an item letter ยท Esc goes back one level diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 233b63454..385dc7148 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Complementos ribbon-tab = + .block-editor = Editor de bloques .draw = Dibujo .parametric = Paramรฉtrico .annotate = Anotar @@ -98,6 +99,9 @@ start = .no-recent-files = Los archivos que abra aparecerรกn aquรญ. .browser-storage = Almacenamiento del navegador .keep-recent-files = Conservar archivos recientes + .view-list = Lista + .view-icons = Iconos + .view-preview = Vistas previas grandes donation = .title = Solicitud de donaciรณn @@ -167,6 +171,14 @@ modal = .that-object = ese objeto .those-objects = esos objetos + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Imagen "{ $name }" pegada: { $width }ร—{ $height } px .image-unsupported = No se pudo decodificar la imagen pegada "{ $name }". @@ -274,11 +286,73 @@ command-copy = .mode = COPY Indique modo de copia [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ solo aquรญ (verde), __ocs_fmt_1__ solo en el otro dibujo (rojo)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Comparar con dibujo + .label-associative-array-created = __ocs_fmt_0__: se ha creado una matriz asociativa. + .assoc-array-array-path = Matriz (trayectoria) + .assoc-array-array-polar = Matriz (polar) + .assoc-array-array-rectangular = Matriz (rectangular) + .assoc-array-item-spacing = Espaciado de elementos + .assoc-array-fill-angle = รngulo de relleno + .assoc-array-items = Elementos + .assoc-array-level-spacing = Espaciado de niveles + .assoc-array-levels = Niveles + .assoc-array-row-spacing = Espaciado de filas + .assoc-array-column-spacing = Espaciado de columnas + .fbxexport-exported-to = FBXEXPORT: exportado a ยซ__ocs_fmt_0__ยป + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: no hay datos de malla 3D en este dibujo. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Los sombreados nuevos siguen sus contornos (HPASSOC) + .options-show-ribbon = Mostrar la cinta de opciones (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Mostrar la barra de menรบs (MENUBAR) + .options-window-elements = Elementos de ventana + .menu-viewports = Ventanas grรกficas + .menu-view-twist = Girar vista + .menu-tools = Herramientas + .menu-table-style = Estilo de tabla + .menu-run-script = Ejecutar archivo de comandos + .menu-revision-cloud = Nube de revisiรณn + .menu-regen-all = Regenerar todo + .menu-redraw = Redibujar + .menu-rectangle = Rectรกngulo + .menu-quick-dimension = Acotaciรณn rรกpida + .menu-publish = Publicar + .menu-paste-special = Pegado especial + .menu-parametric = Paramรฉtrico + .menu-page-setup-manager = Administrador de configuraciones de pรกgina + .menu-orbit = ร“rbita + .menu-new-ucs = Nuevo SCP + .menu-named-views = Vistas guardadas + .menu-multiline-text = Texto de lรญneas mรบltiples + .menu-multiline-style = Estilo de lรญnea mรบltiple + .menu-multiline = Lรญnea mรบltiple + .menu-multileader-style = Estilo de directriz mรบltiple + .menu-match-properties = Igualar propiedades + .menu-lengthen = Longitud + .menu-jogged = Con recodo + .menu-id-point = Punto ID + .menu-explode-text = Descomponer texto + .menu-edit-spline = Editar spline + .menu-edit-polyline = Editar polilรญnea + .menu-edit-hatch = Editar sombreado + .menu-drawing-limits = Lรญmites del dibujo + .menu-donut = Arandela + .menu-dimension-style = Estilo de cota + .menu-construction-line = Lรญnea auxiliar + .menu-arc-length = Longitud de arco + .command-line-opacity = Opacidad de la lรญnea de comando + .txtexp-no-text-selected = TXTEXP: no se ha seleccionado ningรบn texto. + .angle-override-shown = Modificaciรณn de รกngulo: __ocs_fmt_0__ .plot-preview-task = Vista previa de trazado .link-a-csv-data-source = Vincular un origen de datos CSV .csv-files = Archivos CSV .browser-cannot-open-typed-path = El navegador no puede abrir una ruta escrita; elige el archivo. .recover-drawing = Recuperar dibujo + .requires-0-or-1-or-keyword = Solo admite 0 o 1, o una palabra clave de opciรณn. + .requires-a-number = Se requiere un nรบmero. .restore-it-as-a-recovered-tab-or-discard-the-copy = Restรกurelo como una pestaรฑa recuperada o descarte la copia. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" tiene una copia de autoguardado mรกs reciente que la รบltima copia guardada. .autosave-copy-could-not-be-stored = No se pudo guardar la copia de autoguardado: __ocs_fmt_0__ @@ -822,6 +896,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: seleccione un objeto de secciรณn o active una secciรณn dinรกmica. + .sectioncull-prepare-failed = SECTIONCULL: no se pudo preparar un objeto cortado; no se cambiรณ nada. + .sectioncull-nothing-outside = SECTIONCULL: no hay nada fuera de la secciรณn. + .sectioncull-result = SECTIONCULL: se borraron __ocs_arg_erased__ objeto(s) y se cortaron __ocs_arg_cut__ en el lรญmite de la secciรณn. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: se ajustรณ una spline por __ocs_fmt_0__ puntos. .splinefit-need-at-least-3-points = SPLINEFIT: se necesitan al menos 3 puntos. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: seleccione una polilรญnea por la que ajustar una spline. @@ -859,9 +937,12 @@ common = .sent-to-printer-printer = Enviado a la impresora: __ocs_fmt_0__ .set-current = Establecer actual .shade-plot = Trazado sombreado + .shaded-with-edges = Sombreado con aristas + .shades-of-gray = Tonos de gris .shadow-flags = Indicadores de sombra .single = รšnico .size = Tamaรฑo + .sketchy = Bosquejo .snap-override-label-next-pick-only = Modificaciรณn de referencia: __ocs_fmt_0__ (solo la siguiente designaciรณn). .solid-created = Se creรณ el sรณlido. .source-object-not-found = No se encontrรณ el objeto de origen. @@ -1067,7 +1148,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: introduzca Left, Top o Right. .isodraft-expected-on-or-off = ISODRAFT: introduzca On u Off. .xdata-selected-entities-are-on-locked-layers = XDATA: las entidades seleccionadas estรกn en capas bloqueadas. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: no hay imรกgenes rรกster seleccionadas o la propiedad es desconocida (use BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: introduzca un entero de -4 a 4. .usage-datalink-path-to-csv = Uso: DATALINK .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: no se encontraron tablas vinculadas. @@ -2060,7 +2140,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = Archivo .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2369,6 +2449,7 @@ common = .load-linetypes = Cargar tipos de lรญnea .requires-two-positive-values = Requiere dos valores positivos (x,y) o uno para ambos. .snap-marker-color = Color del marcador de referencia + .snap-marker-size = Tamaรฑo del marcador de referencia .command-line-text-color = Color del texto de la lรญnea de comandos .operand = Operando .linetypes-loaded = __ocs_fmt_0__ tipo(s) de lรญnea cargado(s). @@ -2382,6 +2463,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Especifique el punto base [__ocs_arg_count__ objetos]: .threedrotate-specify-rotation-axis = 3DROTATE Precise el eje de giro [X/Y/Z] o un segundo punto en รฉl : .threedrotate-specify-rotation-angle-a = 3DROTATE Precise el รกngulo de giro <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Advertencia de guardado .unable-to-save-drawing = No se puede guardar el dibujo @@ -3005,6 +3093,7 @@ draw = .arc-specify-radius-of-arc = ARC Precise radio del arco: .chord-length = Longitud de cuerda + modify = .add-vertex = Aรฑadir vรฉrtice .closed-cross = Cruz cerrada @@ -3050,6 +3139,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Seleccione un bloque con atributos: .annotation-object-scale = Escala del objeto anotativo .bedit-select-block-reference-to-edit = BEDIT Seleccione la referencia de bloque que desea editar: + .bvstate-option = BVSTATE Indique una opciรณn [Nuevo/Establecer/Renombrar/Suprimir] : + .bvstate-new-name = BVSTATE Indique el nombre del nuevo estado de visibilidad: + .bvstate-set = BVSTATE Indique el estado de visibilidad que se harรก actual <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Indique el estado de visibilidad que se renombrarรก <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Indique el nuevo nombre: + .bvstate-delete = BVSTATE Indique el estado de visibilidad que se suprimirรก: + .bvstate-not-found = No se encuentra el estado de visibilidad "__ocs_fmt_0__". + .bvstate-exists = El estado de visibilidad "__ocs_fmt_0__" ya existe. + .bvstate-last = No se puede suprimir el รบnico estado de visibilidad. + .bvstate-needs-bedit = __ocs_fmt_0__: abra primero el bloque en el Editor de bloques (BEDIT). + .bvstate-current = Estado de visibilidad actual: __ocs_fmt_0__ + .bvhide-select = Seleccione los objetos que se ocultarรกn: + .bvshow-select = Seleccione los objetos que se harรกn visibles: + .arrayedit-select-array = ARRAYEDIT Seleccione la matriz: + .arrayedit-options = ARRAYEDIT Indique una opciรณn [Origen/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: el objeto seleccionado no es una matriz asociativa. .blend-enter-continuity-tangent-curvature-c = BLEND Indique la continuidad [Tangente/Curvatura] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND Seleccione la primera curva abierta [Continuidad=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Seleccione la segunda curva abierta [Continuidad=__ocs_arg_c__]: @@ -3602,6 +3707,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Especifique el punto de inserciรณn: insert = + .pastespec-specify-insertion-point = PASTESPEC Precise el punto de inserciรณn: + .pastespec-paste-as = PASTESPEC Pegar como [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3718,6 +3825,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3772,6 +3886,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Las referencias a calcos varรญan* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3911,6 +4026,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Seleccione una imagen o un calco subyacente: + .adjust-select-pdf-underlay = Seleccione un calco subyacente PDF: + .adjust-select-dwf-underlay = Seleccione un calco subyacente DWF: + .adjust-select-dgn-underlay = Seleccione un calco subyacente DGN: + .adjust-mixed-underlays = *Conjunto de selecciรณn no vรกlido. Seleccione solo calcos subyacentes del mismo tipo de archivo.* + .adjust-image-option = Indique una opciรณn de imagen [Contraste/Desvanecimiento/Brillo] : + .adjust-underlay-option = Indique una opciรณn de calco subyacente __ocs_arg_kind__ [Desvanecimiento/Contraste/Monocromo] <__ocs_arg_option__>: + .adjust-monochrome = Monocromo? [Sรญ/No] <__ocs_arg_value__>: + .adjust-brightness-value = Indique el valor de brillo (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Indique el valor de contraste (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Indique el valor de desvanecimiento (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = El valor debe ser un nรบmero entero entre 0 y 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4415,7 +4542,27 @@ model = .value-2point = 2 puntos .cube = Cubo + layout = + .mvsetup-select-objects-to-delete = MVSETUP Seleccione los objetos que desea suprimir: + .delete-objects-option = Suprimir objetos + .mvsetup-enable-paper-space = MVSETUP ยฟActivar espacio papel? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Indique el factor de escala <1>: + .mvsetup-enter-paper-width = MVSETUP Indique la anchura del papel: + .mvsetup-enter-paper-height = MVSETUP Indique la altura del papel: + .mvsetup-enter-option = MVSETUP Indique una opciรณn [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Indique la disposiciรณn de ventanas [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Precise la primera esquina del รกrea de ventanas : + .mvsetup-specify-opposite-corner = MVSETUP Precise la esquina opuesta: + .mvsetup-viewports-in-x = MVSETUP Indique el nรบmero de ventanas en direcciรณn X <2>: + .mvsetup-viewports-in-y = MVSETUP Indique el nรบmero de ventanas en direcciรณn Y <2>: + .mvsetup-distance-in-x = MVSETUP Precise la distancia entre ventanas en direcciรณn X <0>: + .mvsetup-distance-in-y = MVSETUP Precise la distancia entre ventanas en direcciรณn Y <0>: + .mvsetup-select-viewports = MVSETUP Seleccione las ventanas que desea escalar: + .mvsetup-paper-space-units = MVSETUP Indique el nรบmero de unidades de espacio papel <1>: + .mvsetup-model-space-units = MVSETUP Indique el nรบmero de unidades de espacio modelo <1>: + .mvsetup-viewports-do-not-fit = MVSETUP Las ventanas no caben en el รกrea. + .scale-viewports-option = Escalar ventanas .active = Activo .inactive = Inactivo .insert-view = Insertar vista @@ -4441,167 +4588,170 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Inutilizar | T = Reutilizar | Intro = Salir view = + .twist-option = Girar + .dview-specify-view-twist-angle = DVIEW Precise el รกngulo de giro de la vista <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Indique una opciรณn [TWist/Undo]: .color-scheme-set-to-system-currently-name = Esquema de color establecido en ยซSistemaยป (actualmente __ocs_fmt_0__). - .ss-create-sheet-set = Create Sheet Set + .ss-create-sheet-set = Crear conjunto de planos .ss-requires-integer-between = Requiere un nรบmero entero entre __ocs_fmt_0__ y __ocs_fmt_1__. - .ss-new-sheet = New Sheet - .ss-subset-properties = Subset Properties - .ss-rename-renumber-sheet = Rename & Renumber Sheet - .ss-import-layout-as-sheet = Import Layout as Sheet - .ss-sheet-set-manager = Sheet Set Manager - .ss-open = Open... - .ss-open-a-sheet-set-or-create-one-with-newsheetset = Open a sheet set or create one with NEWSHEETSET. - .ss-no-sheet-views = No sheet views. - .ss-no-locations = No locations. - .ss-double-click-opens-a-sheet-right-click-for-more-commands = Double-click opens a sheet. Right-click for more commands. - .ss-an-empty-sheet-set = An empty sheet set - .ss-existing-drawings = Existing drawings - .ss-create-a-folder-hierarchy-based-on-subsets = Create a folder hierarchy based on subsets - .ss-sheet-set-properties = Sheet Set Properties... - .ss-select-folders-containing-drawings-layouts-in-the-drawings-c = Select folders containing drawings. Layouts in the drawings can be added to the sheet set. - .ss-sheet-set = Sheet Set - .ss-sheet-set-data-file = Sheet set data file - .ss-sheets-to-import = Sheets to import - .ss-add-custom-property = Add Custom Property... - .ss-new-sheet-x = New Sheet... - .ss-new-subset = New Subset... - .ss-import-layout-as-sheet-x = Import Layout as Sheet... - .ss-rename-renumber = Rename & Renumber... - .ss-close-sheet-set = Close Sheet Set - .ss-properties = Properties... - .ss-sheet-list = Sheet List - .ss-sheet-views = Sheet Views - .ss-model-views = Model Views - .ss-views-by-sheet = Views by sheet - .ss-locations = Locations - .ss-create-a-sheet-set-using = Create a sheet set using - .ss-name-of-new-sheet-set = Name of new sheet set: - .ss-description-optional = Description (optional): - .ss-store-sheet-set-data-file-dst-here = Store sheet set data file (.dst) here: - .ss-choose-layouts = Choose Layouts - .ss-confirm = Confirm - .ss-default-value = Default value: - .ss-owner = Owner - .ss-custom-properties = Custom Properties - .ss-sheet-title = Sheet title - .ss-folder-path = Folder path - .ss-sheet-template = Sheet template - .ss-subset-name = Subset name - .ss-new-sheet-location = New sheet location - .ss-remove-subset = Remove Subset - .ss-rename-subset = Rename Subset... - .ss-content-model-space = Content cannot be placed into model space. Please switch to paper space and try again. - .ss-cannot-read = cannot be read - .ss-rename-renumber-view = Rename & Renumber View - .ss-list-of-blocks = List of Blocks - .ss-select-block = Select Block - .ss-set-category = Set category - .ss-place-callout-block = Place Callout Block - .ss-place-view-label-block = Place View Label Block - .ss-place-on-sheet = Place on Sheet - .ss-see-model-space-views = See Model Space Views - .ss-view-title = View title - .ss-add-x = Add... - .ss-blocks-associated = The following blocks are associated with this sheet set: - .ss-enter-drawing-file-name = Enter the drawing file name: - .ss-enter-drawing-file-name-msg = Enter the drawing file name. - .ss-select-drawing-as-block = Select the drawing file as a block - .ss-choose-blocks-in-drawing = Choose blocks in the drawing file: - .ss-preview = preview - .ss-open-sheet-to-place = Open a sheet of this sheet set at its layout to place a view on it. - .ss-open-view-sheet = Open the view's sheet at its layout to place a block on it. - .ss-no-label-block = The sheet set has no label block for views. - .ss-no-view-to-place = __ocs_fmt_0__: no view to place. - .ss-cannot-be-read = __ocs_fmt_0__: cannot be read. - .ss-layout-already-part = This layout is already part of a sheet set - not available for import. - .ss-layout-may-belong = Warning: this layout may belong to another sheet set. - .ss-status-missing = missing - .ss-status-open = open - .ss-view-by-category = View by category - .ss-view-by-sheet = View by sheet - .ss-import-layouts-as-sheets = Import Layouts as Sheets - .ss-remove-sheets = Remove Sheets - .ss-view-category = View Category - .ss-new-view-category = New View Category... - .ss-add-new-location = Add New Location... - .ss-remove-location = Remove Location - .ss-publish-by-sheet = Publish by Sheet 'Include for Publish' Setting - .ss-do-not-publish-sheets = Do Not Publish Sheets - .ss-open-in-drawing-editor = Open in drawing editor - .ss-create-folder-hierarchy = Create folder hierarchy - .ss-publish-sheets-in-subset = Publish sheets in subset - .ss-rename-options = Rename options - .ss-rename-layout-to-match = Rename layout to match: - .ss-prefix-with-sheet-number = Prefix with sheet number - .ss-rename-drawing-file-to-match = Rename drawing file to match: - .ss-available-for-import = Available for import - .ss-drawings = Drawings - .ss-select-drawing-files-containing-layouts = Select drawing files containing layouts - .ss-browse-for-drawings = Browse for Drawings... - .ss-a-layout-can-belong-to-only-one-sheet-set = A layout can belong to only one sheet set. If a layout already belongs to a sheet set, you must create a copy of the layout to import it. - .ss-prefix-sheet-titles-with-file-name = Prefix sheet titles with file name - .ss-import-checked = Import Checked - .ss-template = Template - .ss-drawing-template-file-name = Drawing template file name - .ss-select-a-layout-to-create-new-sheets = Select a layout to create new sheets - .ss-category-name = Category name - .ss-select-the-callout-blocks = Select the callout blocks to be used in this category - .ss-selected-blocks-will-be-available = Selected blocks will be available for selection when you insert a callout block from the view tab. - .ss-add-blocks = Add Blocks... - .ss-layout-and-drawing-not-deleted = (The layout and the drawing file will not be deleted.) - .ss-remove-sheet-question = Are you sure that you want to remove __ocs_fmt_0__ Sheet from __ocs_fmt_1__ Sheet Set? - .ss-drawing-open-cannot-rename = The drawing is open; close it to rename its file. - .ss-category-name-empty = The category name cannot be empty. - .ss-associate-hyperlink = Associate hyperlink - .ss-remove-sheet = Remove Sheet - .ss-lets-you-specify-one-or-more-folders-containing-drawings-the = Lets you specify one or more folders containing drawings. The layouts from these drawings can be automatically imported into the sheet set. - .ss-creates-a-sheet-set-with-no-subsets-or-sheets = Creates a sheet set with no subsets or sheets. - .ss-open-sheet-set = Open Sheet Set - .ss-sheet-set-dst = Sheet Set (*.dst) - .ss-browse-for-folder = Browse for Folder - .ss-select-layout-as-sheet-template = Select Layout as Sheet Template - .ss-drawing-template-dwt = Drawing Template (*.dwt) - .ss-drawing-dwg = Drawing (*.dwg) - .ss-the-name-cannot-be-empty = The name cannot be empty. - .ss-a-custom-property-with-this-name-already-exists = A custom property with this name already exists. - .ss-sheet-sets-organize-layouts-from-several-drawings-into-one-n = Sheet sets organize layouts from several drawings into one named set of sheets. - .ss-default-new-drawing = (default new drawing) - .ss-select-drawing = Select Drawing - .ss-the-sheet-set-name-cannot-be-empty = The sheet set name cannot be empty. - .ss-the-folder-for-the-sheet-set-data-file-does-not-exist = The folder for the sheet set data file does not exist. - .ss-the-drawing-has-no-layout-to-use-as-a-sheet-template = The drawing has no layout to use as a sheet template. - .ss-the-subset-name-cannot-be-empty = The subset name cannot be empty. - .ss-the-drawing-has-no-layout-to-import = The drawing has no layout to import. - .ss-the-layout-is-already-a-sheet-of-this-sheet-set = The layout is already a sheet of this sheet set. - .ss-the-sheet-title-cannot-be-empty = The sheet title cannot be empty. - .ss-the-sheet-folder-cannot-be-created = The sheet folder cannot be created. - .ss-sheet-set-properties-x = Sheet Set Properties - .ss-sheet-properties = Sheet Properties - .ss-sheet-drawing-not-found = Sheet drawing not found: __ocs_fmt_0__ - .ss-already-exists = __ocs_fmt_0__ already exists. - .ss-model-view = Model view - .ss-label-block-for-views = Label block for views - .ss-callout-blocks = Callout blocks - .ss-page-setup-overrides-file = Page setup overrides file - .ss-project-control = Project Control - .ss-sheet-creation = Sheet Creation - .ss-sheet-storage-location = Sheet storage location - .ss-sheet-creation-template = Sheet creation template - .ss-prompt-for-template = Prompt for template - .ss-subset = Subset - .ss-layout-name = Layout name - .ss-drawing-file = Drawing file - .ss-project-number = Project number - .ss-project-name = Project name - .ss-project-phase = Project phase - .ss-project-milestone = Project milestone - .ss-revision-number = Revision number - .ss-revision-date = Revision date - .ss-issue-purpose = Issue purpose - .ss-custom-property-name = Custom property name - .ss-placeholder-type = Placeholder type - .ss-sheet-set-x = Sheet set - .ss-sheet-navigation-tree = Sheet navigation tree + .ss-new-sheet = Plano nuevo + .ss-subset-properties = Propiedades del subconjunto + .ss-rename-renumber-sheet = Cambiar nombre y renumerar plano + .ss-import-layout-as-sheet = Importar presentaciรณn como plano + .ss-sheet-set-manager = Administrador de conjuntos de planos + .ss-open = Abrir... + .ss-open-a-sheet-set-or-create-one-with-newsheetset = Abra un conjunto de planos o cree uno con NEWSHEETSET. + .ss-no-sheet-views = No hay vistas de plano. + .ss-no-locations = No hay ubicaciones. + .ss-double-click-opens-a-sheet-right-click-for-more-commands = Haga doble clic para abrir un plano. Haga clic con el botรณn derecho para ver mรกs comandos. + .ss-an-empty-sheet-set = Un conjunto de planos vacรญo + .ss-existing-drawings = Dibujos existentes + .ss-create-a-folder-hierarchy-based-on-subsets = Crear una jerarquรญa de carpetas basada en los subconjuntos + .ss-sheet-set-properties = Propiedades del conjunto de planos... + .ss-select-folders-containing-drawings-layouts-in-the-drawings-c = Seleccione las carpetas que contienen dibujos. Las presentaciones de los dibujos se pueden aรฑadir al conjunto de planos. + .ss-sheet-set = Conjunto de planos + .ss-sheet-set-data-file = Archivo de datos del conjunto de planos + .ss-sheets-to-import = Planos que se importarรกn + .ss-add-custom-property = Aรฑadir propiedad personalizada... + .ss-new-sheet-x = Plano nuevo... + .ss-new-subset = Subconjunto nuevo... + .ss-import-layout-as-sheet-x = Importar presentaciรณn como plano... + .ss-rename-renumber = Cambiar nombre y renumerar... + .ss-close-sheet-set = Cerrar conjunto de planos + .ss-properties = Propiedades... + .ss-sheet-list = Lista de planos + .ss-sheet-views = Vistas de plano + .ss-model-views = Vistas de modelo + .ss-views-by-sheet = Vistas por plano + .ss-locations = Ubicaciones + .ss-create-a-sheet-set-using = Crear un conjunto de planos usando + .ss-name-of-new-sheet-set = Nombre del nuevo conjunto de planos: + .ss-description-optional = Descripciรณn (opcional): + .ss-store-sheet-set-data-file-dst-here = Guardar aquรญ el archivo de datos del conjunto de planos (.dst): + .ss-choose-layouts = Elegir presentaciones + .ss-confirm = Confirmar + .ss-default-value = Valor predeterminado: + .ss-owner = Propietario + .ss-custom-properties = Propiedades personalizadas + .ss-sheet-title = Tรญtulo del plano + .ss-folder-path = Ruta de la carpeta + .ss-sheet-template = Plantilla de plano + .ss-subset-name = Nombre del subconjunto + .ss-new-sheet-location = Ubicaciรณn del nuevo plano + .ss-remove-subset = Quitar subconjunto + .ss-rename-subset = Cambiar nombre del subconjunto... + .ss-content-model-space = El contenido no se puede colocar en el espacio modelo. Cambie al espacio papel e intรฉntelo de nuevo. + .ss-cannot-read = no se puede leer + .ss-rename-renumber-view = Cambiar nombre y renumerar vista + .ss-list-of-blocks = Lista de bloques + .ss-select-block = Seleccionar bloque + .ss-set-category = Establecer categorรญa + .ss-place-callout-block = Colocar bloque de referencia + .ss-place-view-label-block = Colocar bloque de rรณtulo de vista + .ss-place-on-sheet = Colocar en el plano + .ss-see-model-space-views = Ver las vistas del espacio modelo + .ss-view-title = Tรญtulo de la vista + .ss-add-x = Aรฑadir... + .ss-blocks-associated = Los siguientes bloques estรกn asociados a este conjunto de planos: + .ss-enter-drawing-file-name = Introduzca el nombre del archivo de dibujo: + .ss-enter-drawing-file-name-msg = Introduzca el nombre del archivo de dibujo. + .ss-select-drawing-as-block = Seleccione el archivo de dibujo como bloque + .ss-choose-blocks-in-drawing = Elija los bloques del archivo de dibujo: + .ss-preview = vista previa + .ss-open-sheet-to-place = Abra un plano de este conjunto de planos en su presentaciรณn para colocar una vista en รฉl. + .ss-open-view-sheet = Abra el plano de la vista en su presentaciรณn para colocar un bloque en รฉl. + .ss-no-label-block = El conjunto de planos no tiene bloque de rรณtulo para vistas. + .ss-no-view-to-place = __ocs_fmt_0__: no hay ninguna vista que colocar. + .ss-cannot-be-read = __ocs_fmt_0__: no se puede leer. + .ss-layout-already-part = Esta presentaciรณn ya forma parte de un conjunto de planos; no estรก disponible para importar. + .ss-layout-may-belong = Advertencia: esta presentaciรณn puede pertenecer a otro conjunto de planos. + .ss-status-missing = no encontrado + .ss-status-open = abierto + .ss-view-by-category = Ver por categorรญa + .ss-view-by-sheet = Ver por plano + .ss-import-layouts-as-sheets = Importar presentaciones como planos + .ss-remove-sheets = Quitar planos + .ss-view-category = Categorรญa de vista + .ss-new-view-category = Categorรญa de vista nueva... + .ss-add-new-location = Aรฑadir ubicaciรณn nueva... + .ss-remove-location = Quitar ubicaciรณn + .ss-publish-by-sheet = Publicar segรบn el valor ยซIncluir al publicarยป de cada plano + .ss-do-not-publish-sheets = No publicar planos + .ss-open-in-drawing-editor = Abrir en el editor de dibujo + .ss-create-folder-hierarchy = Crear jerarquรญa de carpetas + .ss-publish-sheets-in-subset = Publicar los planos del subconjunto + .ss-rename-options = Opciones de cambio de nombre + .ss-rename-layout-to-match = Cambiar el nombre de la presentaciรณn para que coincida: + .ss-prefix-with-sheet-number = Anteponer el nรบmero de plano + .ss-rename-drawing-file-to-match = Cambiar el nombre del archivo de dibujo para que coincida: + .ss-available-for-import = Disponible para importar + .ss-drawings = Dibujos + .ss-select-drawing-files-containing-layouts = Seleccione los archivos de dibujo que contienen presentaciones + .ss-browse-for-drawings = Examinar dibujos... + .ss-a-layout-can-belong-to-only-one-sheet-set = Una presentaciรณn solo puede pertenecer a un conjunto de planos. Si una presentaciรณn ya pertenece a un conjunto de planos, debe crear una copia de la presentaciรณn para importarla. + .ss-prefix-sheet-titles-with-file-name = Anteponer el nombre del archivo a los tรญtulos de plano + .ss-import-checked = Importar los marcados + .ss-template = Plantilla + .ss-drawing-template-file-name = Nombre del archivo de plantilla de dibujo + .ss-select-a-layout-to-create-new-sheets = Seleccione una presentaciรณn para crear planos nuevos + .ss-category-name = Nombre de la categorรญa + .ss-select-the-callout-blocks = Seleccione los bloques de referencia que se usarรกn en esta categorรญa + .ss-selected-blocks-will-be-available = Los bloques seleccionados estarรกn disponibles al insertar un bloque de referencia desde la pestaรฑa Vista. + .ss-add-blocks = Aรฑadir bloques... + .ss-layout-and-drawing-not-deleted = (La presentaciรณn y el archivo de dibujo no se eliminarรกn.) + .ss-remove-sheet-question = ยฟSeguro que desea quitar el plano __ocs_fmt_0__ del conjunto de planos __ocs_fmt_1__? + .ss-drawing-open-cannot-rename = El dibujo estรก abierto; ciรฉrrelo para cambiar el nombre de su archivo. + .ss-category-name-empty = El nombre de la categorรญa no puede estar vacรญo. + .ss-associate-hyperlink = Asociar hipervรญnculo + .ss-remove-sheet = Quitar plano + .ss-lets-you-specify-one-or-more-folders-containing-drawings-the = Permite especificar una o mรกs carpetas que contienen dibujos. Las presentaciones de estos dibujos se pueden importar automรกticamente al conjunto de planos. + .ss-creates-a-sheet-set-with-no-subsets-or-sheets = Crea un conjunto de planos sin subconjuntos ni planos. + .ss-open-sheet-set = Abrir conjunto de planos + .ss-sheet-set-dst = Conjunto de planos (*.dst) + .ss-browse-for-folder = Examinar carpeta + .ss-select-layout-as-sheet-template = Seleccionar presentaciรณn como plantilla de plano + .ss-drawing-template-dwt = Plantilla de dibujo (*.dwt) + .ss-drawing-dwg = Dibujo (*.dwg) + .ss-the-name-cannot-be-empty = El nombre no puede estar vacรญo. + .ss-a-custom-property-with-this-name-already-exists = Ya existe una propiedad personalizada con este nombre. + .ss-sheet-sets-organize-layouts-from-several-drawings-into-one-n = Los conjuntos de planos organizan las presentaciones de varios dibujos en un รบnico conjunto de planos con nombre. + .ss-default-new-drawing = (dibujo nuevo predeterminado) + .ss-select-drawing = Seleccionar dibujo + .ss-the-sheet-set-name-cannot-be-empty = El nombre del conjunto de planos no puede estar vacรญo. + .ss-the-folder-for-the-sheet-set-data-file-does-not-exist = La carpeta del archivo de datos del conjunto de planos no existe. + .ss-the-drawing-has-no-layout-to-use-as-a-sheet-template = El dibujo no tiene ninguna presentaciรณn que usar como plantilla de plano. + .ss-the-subset-name-cannot-be-empty = El nombre del subconjunto no puede estar vacรญo. + .ss-the-drawing-has-no-layout-to-import = El dibujo no tiene ninguna presentaciรณn que importar. + .ss-the-layout-is-already-a-sheet-of-this-sheet-set = La presentaciรณn ya es un plano de este conjunto de planos. + .ss-the-sheet-title-cannot-be-empty = El tรญtulo del plano no puede estar vacรญo. + .ss-the-sheet-folder-cannot-be-created = No se puede crear la carpeta del plano. + .ss-sheet-set-properties-x = Propiedades del conjunto de planos + .ss-sheet-properties = Propiedades del plano + .ss-sheet-drawing-not-found = No se encontrรณ el dibujo del plano: __ocs_fmt_0__ + .ss-already-exists = __ocs_fmt_0__ ya existe. + .ss-model-view = Vista de modelo + .ss-label-block-for-views = Bloque de rรณtulo para vistas + .ss-callout-blocks = Bloques de referencia + .ss-page-setup-overrides-file = Archivo de reemplazos de configuraciรณn de pรกgina + .ss-project-control = Control de proyecto + .ss-sheet-creation = Creaciรณn de planos + .ss-sheet-storage-location = Ubicaciรณn de almacenamiento de planos + .ss-sheet-creation-template = Plantilla de creaciรณn de planos + .ss-prompt-for-template = Solicitar plantilla + .ss-subset = Subconjunto + .ss-layout-name = Nombre de la presentaciรณn + .ss-drawing-file = Archivo de dibujo + .ss-project-number = Nรบmero de proyecto + .ss-project-name = Nombre del proyecto + .ss-project-phase = Fase del proyecto + .ss-project-milestone = Hito del proyecto + .ss-revision-number = Nรบmero de revisiรณn + .ss-revision-date = Fecha de revisiรณn + .ss-issue-purpose = Propรณsito de la emisiรณn + .ss-custom-property-name = Nombre de la propiedad personalizada + .ss-placeholder-type = Tipo de marcador de posiciรณn + .ss-sheet-set-x = Conjunto de planos + .ss-sheet-navigation-tree = รrbol de navegaciรณn de planos .ss-sheetset = SheetSet .datalink-specify-insertion-point = DATALINK Especifique el punto de inserciรณn: .table-block-definition-not-found = TABLE: no se encontrรณ la definiciรณn de bloque. @@ -4635,13 +4785,6 @@ view = .x-no-xdata = __ocs_fmt_0__: sin datos extendidos. .value-3d-orbit = ร“rbita 3D .value-3d-orbit-drag-with-right-mouse-button = ร“rbita 3D: arrastre con el botรณn derecho del ratรณn. - .adjust-brightness-contrast-fade = ADJUST [Brillo/Contraste/Desvanecimiento]: - .adjust-brightness-0-100 = ADJUST Brillo 0-100: - .adjust-contrast-0-100 = ADJUST Contraste 0-100: - .adjust-fade-0-100 = ADJUST Desvanecimiento 0-100: - .adjust-select-raster-image-s-first = ADJUST: seleccione primero una o mรกs imรกgenes rรกster. - .adjust-value-must-be-0-100 = ADJUST: el valor debe estar entre 0 y 100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ en __ocs_fmt_2__ imagen(es). .annoallvisible-new-value-0-1 = ANNOALLVISIBLE Valor nuevo [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: introduzca 0 o 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE Valor nuevo [-4..4]: @@ -4767,7 +4910,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Calco __ocs_fmt_0__: difuminado=__ocs_fmt_1__, contraste=__ocs_fmt_2__, activado=__ocs_fmt_3__, delimitado=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = Se actualizaron __ocs_fmt_0__ calcos subyacentes. - .usage-adjust-brightness-contrast-fade-0-100 = Uso: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Uso: ANNOSCALE p. ej., 1:50, 2:1 o un factor .usage-draworder-f-front-b-back-a-above-handle = Uso: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Uso: HYPERLINK (seleccione primero los objetos) @@ -5995,6 +6137,46 @@ ui = .osnap-settings = Parรกmetros de referencia... .snap-override-none-next-pick-only = Referencia temporal: Ninguna (solo el siguiente punto). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Mantener siempre el texto entre las lรญneas de referencia .angular-dimensions = Cotas angulares @@ -6262,3 +6444,117 @@ graphic-attributes = .reset-angle = Restablecer รกngulo .one-color = Un color .two-colors = Dos colores + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Aรฑadir al conjunto de trabajo + .remove-from-working-set = Eliminar del conjunto de trabajo + .toolbar-objects = objetos + .dialog-title = Editar referencia + .dialog-reference = Referencia que se editarรก + .dialog-chain-hint = La cadena en el punto designado; haga clic en una etiqueta para cambiar de nivel. + .dialog-path = Ruta: + .dialog-working-set = Conjunto de trabajo + .dialog-all-objects = Todos los objetos + .dialog-pick-objects = Designar en el dibujo + .dialog-attributes = Editar tambiรฉn las definiciones de atributo + .dialog-session = Sesiรณn + .dialog-lock = Bloquear los objetos fuera del conjunto de trabajo + .dialog-unique = Asignar nombres รบnicos a capas, estilos y bloques en conflicto + .dialog-start = Iniciar ediciรณn + .dialog-help = Abre una referencia a bloque o una referencia externa para editarla in situ. + .select-reference = Seleccione la referencia: + .reference-not-found = No se encuentra la referencia. + .not-valid-reference = El objeto no es una referencia vรกlida + .one-entity-added = 1 entidad aรฑadida + .nesting-level = Seleccione el nivel de anidamiento [Aceptar/Siguiente] : + .selection-method = Indique el mรฉtodo de selecciรณn de objetos [Todos/Anidados] : + .select-nested = Seleccione los objetos anidados: + .display-attdefs = ยฟMostrar definiciones de atributos? [Sรญ/No] : + .zero-items-selected = 0 elementos seleccionados + .one-item-selected = 1 elemento seleccionado + .items-selected = __ocs_fmt_0__ elementos seleccionados + .refset-option = Indique una opciรณn [Aรฑadir/Eliminar] : + .refclose-option = Indique una opciรณn [Guardar/Descartar cambios de referencia] : + .not-checked-out = ** Comando no permitido salvo que se edite una referencia con REFEDIT ** + .already-checked-out = ** Comando no permitido, __ocs_fmt_0__ ya se estรก editando ** + .transfer-objects = Transferir objetos entre el conjunto de trabajo de RefEdit y el dibujo anfitriรณn... + .added-to-working-set = __ocs_fmt_0__ aรฑadidos al conjunto de trabajo + .removed-from-working-set = __ocs_fmt_0__ eliminados del conjunto de trabajo + .one-object-added = 1 objeto aรฑadido a __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ objetos aรฑadidos a __ocs_fmt_1__ + .one-object-removed = 1 objeto eliminado de __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ objetos eliminados de __ocs_fmt_1__ + .one-xref-instance = 1 ejemplar de refX actualizado + .xref-instances = __ocs_fmt_0__ ejemplares de refX actualizados + .one-block-instance = 1 ejemplar de bloque actualizado + .block-instances = __ocs_fmt_0__ ejemplares de bloque actualizados + .redefined-reloaded = __ocs_fmt_0__ redefinido y recargado. + .redefined = __ocs_fmt_0__ redefinido. + .ok = Aceptar + .nested = Anidados + +image-adjust = + .title = Ajuste de imagen + .preview = Vista previa + .before = Antes + .after = Despuรฉs + .no-preview = no se pudo cargar la imagen + .adjust = Ajuste + .brightness = Brillo + .contrast = Contraste + .fade = Atenuaciรณn + .hint = Haga clic o arrastre una barra; โˆ’ / + es un paso, una casilla admite un entero de 0 a 100. + .reset = Restablecer valores por defecto + .apply = Aplicar + .help = Define el brillo, el contraste y la atenuaciรณn de las imรกgenes rรกster seleccionadas. + +keytips = + .title = KeyTip + .typed = Escrito: { $path } + .hint-tabs = Escriba la letra de una ficha ยท Esc cierra + .hint-tab = Ficha { $tab } ยท Esc retrocede un nivel + .hint-items = Escriba la letra de un elemento ยท Esc retrocede un nivel diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 0d856e192..e583fe8de 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Liitรคnnรคiset ribbon-tab = + .block-editor = Lohkoeditori .draw = Piirrรค .parametric = Parametrinen .annotate = Merkinnรคt @@ -98,6 +99,9 @@ start = .no-recent-files = Avaamasi tiedostot nรคkyvรคt tรคssรค. .browser-storage = Selaimen tallennustila .keep-recent-files = Sรคilytรค viimeisimmรคt tiedostot + .view-list = Luettelo + .view-icons = Kuvakkeet + .view-preview = Suuret esikatselut donation = .title = Lahjoituspyyntรถ @@ -167,6 +171,14 @@ modal = .that-object = kyseisen objektin .those-objects = kyseiset objektit + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Liitetty kuva "{ $name }": { $width }ร—{ $height } px .image-unsupported = Liitettyรค kuvaa "{ $name }" ei voitu purkaa. @@ -273,11 +285,73 @@ command-copy = .mode = COPY Valitse kopiointitila [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ vain tรครคllรค (vihreรค), __ocs_fmt_1__ vain toisessa piirustuksessa (punainen)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Vertaa piirustukseen + .label-associative-array-created = __ocs_fmt_0__: assosiatiivinen taulukko luotu. + .assoc-array-array-path = Taulukko (polku) + .assoc-array-array-polar = Taulukko (polaarinen) + .assoc-array-array-rectangular = Taulukko (suorakulmainen) + .assoc-array-item-spacing = Kohdevรคli + .assoc-array-fill-angle = Tรคyttรถkulma + .assoc-array-items = Kohteet + .assoc-array-level-spacing = Tasovรคli + .assoc-array-levels = Tasot + .assoc-array-row-spacing = Rivivรคli + .assoc-array-column-spacing = Sarakevรคli + .fbxexport-exported-to = FBXEXPORT: viety tiedostoon "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: tรคssรค piirustuksessa ei ole 3D-verkkotietoja. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Uudet viivoitukset seuraavat rajojaan (HPASSOC) + .options-show-ribbon = Nรคytรค valintanauha (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Nรคytรค valikkorivi (MENUBAR) + .options-window-elements = Ikkunan osat + .menu-viewports = Nรคkymรคikkunat + .menu-view-twist = Nรคkymรคn kierto + .menu-tools = Tyรถkalut + .menu-table-style = Taulukkotyyli + .menu-run-script = Suorita skripti + .menu-revision-cloud = Revisiopilvi + .menu-regen-all = Luo kaikki uudelleen + .menu-redraw = Piirrรค uudelleen + .menu-rectangle = Suorakulmio + .menu-quick-dimension = Pikamitoitus + .menu-publish = Julkaise + .menu-paste-special = Liitรค mรครคrรคten + .menu-parametric = Parametrinen + .menu-page-setup-manager = Sivuasetusten hallinta + .menu-orbit = Kiertorata + .menu-new-ucs = Uusi KKJ + .menu-named-views = Nimetyt nรคkymรคt + .menu-multiline-text = Monirivinen teksti + .menu-multiline-style = Monirinnakkaisviivan tyyli + .menu-multiline = Monirinnakkaisviiva + .menu-multileader-style = Monijohtoviivatyyli + .menu-match-properties = Sovita ominaisuudet + .menu-lengthen = Pidennรค + .menu-jogged = Taitettu + .menu-id-point = Pisteen tunnistus + .menu-explode-text = Hajota teksti + .menu-edit-spline = Muokkaa splinia + .menu-edit-polyline = Muokkaa moniviivaa + .menu-edit-hatch = Muokkaa viivoitusta + .menu-drawing-limits = Piirustuksen rajat + .menu-donut = Rengas + .menu-dimension-style = Mitoitustyyli + .menu-construction-line = Rakenneviiva + .menu-arc-length = Kaaren pituus + .command-line-opacity = Komentorivin peittรคvyys + .txtexp-no-text-selected = TXTEXP: tekstiรค ei ole valittu. + .angle-override-shown = Kulman ohitus: __ocs_fmt_0__ .plot-preview-task = Tulostuksen esikatselu .link-a-csv-data-source = Linkitรค CSV-tietolรคhde .csv-files = CSV-tiedostot .browser-cannot-open-typed-path = Selain ei voi avata kirjoitettua polkua; valitse tiedosto. .recover-drawing = Palauta piirustus + .requires-0-or-1-or-keyword = Vain 0 tai 1 tai valinnan avainsana sallitaan. + .requires-a-number = Vaatii luvun. .restore-it-as-a-recovered-tab-or-discard-the-copy = Palauta se palautettuna vรคlilehtenรค tai hylkรครค kopio. .name-has-an-autosave-copy-newer-than-the-last-save = Kohteella "__ocs_fmt_0__" on automaattisen tallennuksen kopio, joka on uudempi kuin viimeisin tallennus. .autosave-copy-could-not-be-stored = Automaattisen tallennuksen kopiota ei voitu tallentaa: __ocs_fmt_0__ @@ -821,6 +895,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: valitse leikkausobjekti tai ota live-leikkaus kรคyttรถรถn. + .sectioncull-prepare-failed = SECTIONCULL: leikattua objektia ei voitu valmistella; mitรครคn ei muutettu. + .sectioncull-nothing-outside = SECTIONCULL: leikkauksen ulkopuolella ei ole mitรครคn. + .sectioncull-result = SECTIONCULL: __ocs_arg_erased__ objekti(a) poistettu ja __ocs_arg_cut__ leikattu leikkauksen rajalla. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: sovita spline __ocs_fmt_0__-pisteiden lรคpi. .splinefit-need-at-least-3-points =SPLINEFIT: tarvitset vรคhintรครคn 3 pistettรค. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: valitse polyline sovittaaksesi splinin lรคpi. @@ -858,9 +936,12 @@ common = .sent-to-printer-printer = Lรคhetetty tulostimelle: __ocs_fmt_0__ .set-current = Aseta nykyinen .shade-plot = Shade juoni + .shaded-with-edges = Varjostettu reunoilla + .shades-of-gray = Harmaasรคvyt .shadow-flags = Varjoliput .single = Sinkku .size = Koko + .sketchy = Luonnos .snap-override-label-next-pick-only = Snap-ohitus: __ocs_fmt_0__ (vain seuraava valinta). .solid-created = Kiinteรค luotu. .source-object-not-found = Lรคhdeobjektia ei lรถydy. @@ -1066,7 +1147,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: anna Left, Top tai Right. .isodraft-expected-on-or-off = ISODRAFT: anna On tai Off. .xdata-selected-entities-are-on-locked-layers = XDATA: valitut objektit ovat lukituilla tasoilla. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: rasterikuvia ei ole valittu tai ominaisuus on tuntematon (kรคytรค BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: anna kokonaisluku vรคliltรค -4โ€“4. .usage-datalink-path-to-csv = Kรคyttรถ: DATALINK <.csv-tiedoston-polku> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: linkitettyjรค taulukoita ei lรถytynyt. @@ -2059,7 +2139,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = Tiedosto .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2368,6 +2448,7 @@ common = .load-linetypes = Lataa viivatyypit .requires-two-positive-values = Vaatii kaksi positiivista arvoa (x,y) tai yhden molemmille. .snap-marker-color = Kohdistusmerkin vรคri + .snap-marker-size = Kohdistusmerkin koko .command-line-text-color = Komentorivin tekstin vรคri .operand = Operandi .linetypes-loaded = Ladattu __ocs_fmt_0__ viivatyyppiรค. @@ -2381,6 +2462,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Mรครคritรค peruspiste [__ocs_arg_count__ objektit]: .threedrotate-specify-rotation-axis = 3DROTATE Mรครคritรค kiertoakseli [X/Y/Z] tai sen toinen piste : .threedrotate-specify-rotation-angle-a = 3DROTATE Mรครคritรค kiertokulma <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Tallenna varoitus .unable-to-save-drawing = Piirustusta ei voi tallentaa @@ -3004,6 +3092,7 @@ draw = .arc-specify-radius-of-arc = ARC Mรครคritรค kaaren sรคde: .chord-length = Jรคnteen pituus + modify = .add-vertex = Lisรครค kรคrkipiste .closed-cross = Suljettu risteys @@ -3049,6 +3138,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Valitse lohko mรครคritteillรค: .annotation-object-scale = Merkintรคobjektin mittakaava .bedit-select-block-reference-to-edit = BEDIT Valitse muokattava lohkoviite: + .bvstate-option = BVSTATE Anna vaihtoehto [Uusi/Aseta/Nimeรค uudelleen/Poista] : + .bvstate-new-name = BVSTATE Anna uuden nรคkyvyystilan nimi: + .bvstate-set = BVSTATE Anna nykyiseksi asetettava nรคkyvyystila <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Anna uudelleennimettรคvรค nรคkyvyystila <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Anna uusi nimi: + .bvstate-delete = BVSTATE Anna poistettava nรคkyvyystila: + .bvstate-not-found = Nรคkyvyystilaa "__ocs_fmt_0__" ei lรถydy. + .bvstate-exists = Nรคkyvyystila "__ocs_fmt_0__" on jo olemassa. + .bvstate-last = Ainoaa nรคkyvyystilaa ei voi poistaa. + .bvstate-needs-bedit = __ocs_fmt_0__: avaa lohko ensin lohkoeditorissa (BEDIT). + .bvstate-current = Nykyinen nรคkyvyystila: __ocs_fmt_0__ + .bvhide-select = Valitse piilotettavat objektit: + .bvshow-select = Valitse nรคytettรคvรคt objektit: + .arrayedit-select-array = ARRAYEDIT Valitse sarja: + .arrayedit-options = ARRAYEDIT Anna vaihtoehto [Lรคhde/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: valittu objekti ei ole assosiatiivinen sarja. .blend-enter-continuity-tangent-curvature-c = BLEND Enter jatkuvuus [tangentti/kaarevuus] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND Valitse ensimmรคinen avoin kรคyrรค [Continuity=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Valitse toinen avoin kรคyrรค [Continuity=__ocs_arg_c__]: @@ -3601,6 +3706,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Mรครคritรค lisรคyskohta: insert = + .pastespec-specify-insertion-point = PASTESPEC Mรครคritรค lisรคyspiste: + .pastespec-paste-as = PASTESPEC Liitรค muodossa [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3717,6 +3824,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3771,6 +3885,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Pohjien kohdistukset vaihtelevat* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3910,6 +4025,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Valitse kuva tai pohja: + .adjust-select-pdf-underlay = Valitse PDF-pohja: + .adjust-select-dwf-underlay = Valitse DWF-pohja: + .adjust-select-dgn-underlay = Valitse DGN-pohja: + .adjust-mixed-underlays = *Virheellinen valintajoukko. Valitse vain samaa tiedostotyyppiรค olevia pohjia.* + .adjust-image-option = Anna kuvan asetus [Kontrasti/Hรคivyttรครค/Kirkkaus] : + .adjust-underlay-option = Anna __ocs_arg_kind__-pohjan asetus [Hรคivyttรครค/Kontrasti/Yksivรคrinen] <__ocs_arg_option__>: + .adjust-monochrome = Yksivรคrinen? [Kyllรค/Ei] <__ocs_arg_value__>: + .adjust-brightness-value = Anna arvo: kirkkaus (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Anna arvo: kontrasti (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Anna arvo: hรคivyttรครค (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = Arvon on oltava kokonaisluku vรคliltรค 0โ€“100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4414,7 +4541,27 @@ model = .value-2point = 2-piste .cube = Kuutio + layout = + .mvsetup-select-objects-to-delete = MVSETUP Valitse poistettavat objektit: + .delete-objects-option = Poista objektit + .mvsetup-enable-paper-space = MVSETUP Otetaanko paperitila kรคyttรถรถn? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Anna mittakaavakerroin <1>: + .mvsetup-enter-paper-width = MVSETUP Anna paperin leveys: + .mvsetup-enter-paper-height = MVSETUP Anna paperin korkeus: + .mvsetup-enter-option = MVSETUP Anna valinta [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Anna nรคkymรคikkunoiden asettelu [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Mรครคritรค nรคkymรคikkunoiden alueen ensimmรคinen kulma : + .mvsetup-specify-opposite-corner = MVSETUP Mรครคritรค vastakkainen kulma: + .mvsetup-viewports-in-x = MVSETUP Anna nรคkymรคikkunoiden mรครคrรค X-suunnassa <2>: + .mvsetup-viewports-in-y = MVSETUP Anna nรคkymรคikkunoiden mรครคrรค Y-suunnassa <2>: + .mvsetup-distance-in-x = MVSETUP Mรครคritรค nรคkymรคikkunoiden vรคli X-suunnassa <0>: + .mvsetup-distance-in-y = MVSETUP Mรครคritรค nรคkymรคikkunoiden vรคli Y-suunnassa <0>: + .mvsetup-select-viewports = MVSETUP Valitse skaalattavat nรคkymรคikkunat: + .mvsetup-paper-space-units = MVSETUP Anna paperitilan yksikรถiden mรครคrรค <1>: + .mvsetup-model-space-units = MVSETUP Anna mallitilan yksikรถiden mรครคrรค <1>: + .mvsetup-viewports-do-not-fit = MVSETUP Nรคkymรคikkunat eivรคt mahdu alueelle. + .scale-viewports-option = Skaalaa nรคkymรคikkunat .active = Aktiivinen .inactive = Ei-aktiivinen .insert-view = Lisรครค nรคkymรค @@ -4440,6 +4587,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Jรครคdytys | T = Sulatus | Enter = Poistu view = + .twist-option = Kierto + .dview-specify-view-twist-angle = DVIEW Mรครคritรค nรคkymรคn kiertokulma <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Anna valinta [TWist/Undo]: .color-scheme-set-to-system-currently-name = Vรคriteema asetettu arvoon "Jรคrjestelmรค" (tรคllรค hetkellรค __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = Vaatii kokonaisluvun vรคliltรค __ocs_fmt_0__โ€“__ocs_fmt_1__. @@ -4634,13 +4784,6 @@ view = .x-no-xdata = __ocs_fmt_0__: ei xdataa. .value-3d-orbit =3D kiertorata .value-3d-orbit-drag-with-right-mouse-button = 3D-kiertorata: vedรค hiiren oikealla painikkeella. - .adjust-brightness-contrast-fade = ADJUST [Kirkkaus / Kontrasti / Haalistuminen]: - .adjust-brightness-0-100 = ADJUST kirkkaus 0-100: - .adjust-contrast-0-100 = ADJUST kontrasti 0-100: - .adjust-fade-0-100 = ADJUST hรคivytys 0-100: - .adjust-select-raster-image-s-first = ADJUST: valitse ensin rasterikuva(t). - .adjust-value-must-be-0-100 = ADJUST: arvon on oltava 0-100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ __ocs_fmt_2__-kuvissa. .annoallvisible-new-value-0-1 = ANNOALLVISIBLE uusi arvo [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: kirjoita 0 tai 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE uusi arvo [-4..4]: @@ -4766,7 +4909,6 @@ view = .userr-n-val = USER__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Aluskate __ocs_fmt_0__: haalistua=__ocs_fmt_1__, kontrasti=__ocs_fmt_2__, pรครคllรค=__ocs_fmt_3__, klipsi=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = Pรคivitetty __ocs_fmt_0__-alusta(t). - .usage-adjust-brightness-contrast-fade-0-100 = Kรคyttรถ: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Kรคyttรถ: ANNOSCALE esim. 1:50, 2:1 tai kerroin .usage-draworder-f-front-b-back-a-above-handle = Kรคyttรถ: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Kรคyttรถ: HYPERLINK (valitse ensin objektit) @@ -5992,6 +6134,46 @@ ui = .osnap-settings = Tartunta-asetukset... .snap-override-none-next-pick-only = Tilapรคinen tartunta: Ei mitรครคn (vain seuraava piste). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Pidรค teksti aina jatkorivien vรคlissรค .angular-dimensions = Kulmamitat @@ -6259,3 +6441,117 @@ graphic-attributes = .reset-angle = Palauta kulma .one-color = Yksi vรคri .two-colors = Kaksi vรคriรค + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Lisรครค tyรถjoukkoon + .remove-from-working-set = Poista tyรถjoukosta + .toolbar-objects = objektia + .dialog-title = Muokkaa viittausta + .dialog-reference = Muokattava viittaus + .dialog-chain-hint = Ketju valitussa pisteessรค; vaihda tasoa napsauttamalla tunnistetta. + .dialog-path = Polku: + .dialog-working-set = Tyรถjoukko + .dialog-all-objects = Kaikki objektit + .dialog-pick-objects = Valitse piirustuksesta + .dialog-attributes = Muokkaa myรถs attribuuttimรครคrityksiรค + .dialog-session = Istunto + .dialog-lock = Lukitse tyรถjoukon ulkopuoliset objektit + .dialog-unique = Anna ristiriitaisille taso-, tyyli- ja lohkonimille yksilรถlliset nimet + .dialog-start = Aloita muokkaus + .dialog-help = Avaa lohko- tai ulkoisen viittauksen muokattavaksi paikallaan. + .select-reference = Valitse viittaus: + .reference-not-found = Viittausta ei lรถydy. + .not-valid-reference = Objekti ei ole kelvollinen viittaus + .one-entity-added = 1 objekti lisรคtty + .nesting-level = Valitse sisรคkkรคisyystaso [Ok/Seuraava] : + .selection-method = Anna objektien valintatapa [Kaikki/Sisรคkkรคiset] : + .select-nested = Valitse sisรคkkรคiset objektit: + .display-attdefs = Nรคytetรครคnkรถ attribuuttimรครคritykset [Kyllรค/Ei] : + .zero-items-selected = 0 kohdetta valittu + .one-item-selected = 1 kohde valittu + .items-selected = __ocs_fmt_0__ kohdetta valittu + .refset-option = Anna vaihtoehto [Lisรครค/Poista] : + .refclose-option = Anna vaihtoehto [Tallenna/Hylkรครค viittauksen muutokset] : + .not-checked-out = ** Komento ei ole sallittu, ellei viittausta muokata REFEDIT-komennolla ** + .already-checked-out = ** Komento ei ole sallittu, __ocs_fmt_0__ on jo muokattavana ** + .transfer-objects = Siirrรค objekteja RefEdit-tyรถjoukon ja isรคntรคpiirustuksen vรคlillรค... + .added-to-working-set = __ocs_fmt_0__ lisรคtty tyรถjoukkoon + .removed-from-working-set = __ocs_fmt_0__ poistettu tyรถjoukosta + .one-object-added = 1 objekti lisรคtty kohteeseen __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ objektia lisรคtty kohteeseen __ocs_fmt_1__ + .one-object-removed = 1 objekti poistettu kohteesta __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ objektia poistettu kohteesta __ocs_fmt_1__ + .one-xref-instance = 1 xref-esiintymรค pรคivitetty + .xref-instances = __ocs_fmt_0__ xref-esiintymรครค pรคivitetty + .one-block-instance = 1 lohkoesiintymรค pรคivitetty + .block-instances = __ocs_fmt_0__ lohkoesiintymรครค pรคivitetty + .redefined-reloaded = __ocs_fmt_0__ mรครคritetty uudelleen ja ladattu uudelleen. + .redefined = __ocs_fmt_0__ mรครคritetty uudelleen. + .ok = Ok + .nested = Sisรคkkรคiset + +image-adjust = + .title = Kuvan sรครคtรถ + .preview = Esikatselu + .before = Ennen + .after = Jรคlkeen + .no-preview = kuvaa ei voitu ladata + .adjust = Sรครคtรถ + .brightness = Kirkkaus + .contrast = Kontrasti + .fade = Hรคivytys + .hint = Napsauta tai vedรค palkkia; โˆ’ / + on yksi askel, ruutuun kirjoitetaan kokonaisluku 0โ€“100. + .reset = Palauta oletukset + .apply = Kรคytรค + .help = Asettaa valittujen rasterikuvien kirkkauden, kontrastin ja hรคivytyksen. + +keytips = + .title = KeyTip + .typed = Kirjoitettu: { $path } + .hint-tabs = Kirjoita vรคlilehden kirjain ยท Esc sulkee + .hint-tab = Vรคlilehti { $tab } ยท Esc palaa tason taaksepรคin + .hint-items = Kirjoita kohteen kirjain ยท Esc palaa tason taaksepรคin diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index de4e2a87e..6a90f77b9 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Extensions ribbon-tab = + .block-editor = ร‰diteur de blocs .draw = Dessin .parametric = Paramรฉtrique .annotate = Annoter @@ -61,7 +62,7 @@ options = .right-click-customization = Personnalisation du clic droit .right-click-in-drawing-area = Clic droit dans la zone de dessin .hold-duration = Durรฉe de maintien - .right-click-mode-help = Menu contextuel : le clic droit ouvre toujours le menu. Sensible au temps : un clic bref vaut Entrรฉe, un appui prolongรฉ ouvre le menu (SHORTCUTMENUDURATION). + .right-click-mode-help = Menu contextuel : le clic droit ouvre toujours le menu. Un clic bref vaut Entrรฉe, un appui prolongรฉ ouvre le menu (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Menu contextuel .right-click-mode-time-sensitive = Sensible au temps (clic bref = Entrรฉe) .right-click-mode-enter-first = Entrรฉe d'abord, second clic = menu @@ -98,6 +99,9 @@ start = .no-recent-files = Les fichiers que vous ouvrez apparaรฎtront ici. .browser-storage = Stockage du navigateur .keep-recent-files = Conserver les fichiers rรฉcents + .view-list = Liste + .view-icons = Icรดnes + .view-preview = Grands aperรงus donation = .title = Appel aux dons @@ -134,7 +138,7 @@ modal = .unsaved-save-prompt = Voulez-vous enregistrer les modifications ? .point-style = Style de point .attribute-editor = ร‰diteur d'attributs - .save-drawing-as = Enregistrer le dessin sous + .save-drawing-as = Enregistrer sous .recovery-report = Rapport de rรฉcupรฉration du dessin .recovery-prompt = Rรฉcupรฉration du dessin .file-name-could-not-be-saved = Impossible dโ€™enregistrer ยซ __ocs_arg_file_name__ ยป. @@ -153,7 +157,7 @@ modal = .open-dwg-and-dxf-drawings-in-open-cad-studio-b = Ouvrez par dรฉfaut les dessins .dwg et .dxf dans Open CAD Studio. Vous pouvez modifier cela plus tard dans les paramรจtres de votre systรจme. .path-path = Voie : __ocs_arg_path__ .reload-from-disk = Recharger ร  partir du disque - .save-as = Enregistrer comme... + .save-as = Enregistrer sous... .saving-now-could-destroy-those-external-change = L'enregistrement pourrait dรฉtruire ces modifications externes. Recharger la copie du disque, enregistrer votre travail local ailleurs, ou l'รฉcraser explicitement. .the-file-is-open-or-being-used-by-another-appl = Le fichier est ouvert ou utilisรฉ par une autre application. Fermez-le dans cette application et rรฉessayez, ou enregistrez ce dessin sous un autre nom. .the-selected-layer-state-is-no-longer-availabl = L'รฉtat de calque sรฉlectionnรฉ n'est plus disponible. @@ -167,6 +171,14 @@ modal = .that-object = cet objet .those-objects = ces objets + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Ignorer + .missing-fonts-title = Polices manquantes + .missing-fonts-description = Ce dessin contient des polices de caractรจres absentes de cette machine. OpenCADStudio peut tรฉlรฉcharger les polices du rรฉfรฉrentiel choisi; les polices absentes seront remplacรฉes. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Image collรฉe "{ $name }" : { $width }ร—{ $height } px .image-unsupported = L'image collรฉe "{ $name }" n'a pas pu รชtre dรฉcodรฉe. @@ -180,7 +192,7 @@ clipboard = .cut-to-clipboard = Couper sur le presse-papiers .pasteclip-pick-insertion-point = PASTECLIP Point d'insertion de la sรฉlection: .paste-as-block = Coller comme bloc - .paste-from-clipboard = Coller du presse-papiers + .paste-from-clipboard = Coller depuis le presse-papiers .paste-to-original-coordinates = Coller aux coordonnรฉes originales viewport = @@ -233,10 +245,10 @@ area = .option-back = Retour .option-add = Ajouter une aire .option-subtract = Soustraire une aire - .prompt-first = AREA Indiquez le premier sommet ou [Objets/Ajouter une aire/Soustraire une aire] : - .prompt-add = AREA AJOUTER Indiquez le premier sommet ou [Objets/Soustraire une aire] : - .prompt-subtract = AREA SOUSTRAIRE Indiquez le premier sommet ou [Objets/Ajouter une aire] : - .prompt-next = AREA Indiquez le point suivant ({ $count } sรฉlectionnรฉ(s), Entrรฉe pour calculer) : + .prompt-first = AREA Premier sommet ou [Objets/Ajouter une aire/Soustraire une aire] : + .prompt-add = AREA AJOUTER Premier sommet ou [Objets/Soustraire une aire] : + .prompt-subtract = AREA SOUSTRAIRE Premier sommet ou [Objets/Ajouter une aire] : + .prompt-next = AREA Point suivant ({ $count } sรฉlectionnรฉ(s), Entrรฉe pour calculer) : .prompt-objects = AREA Sรฉlectionnez les objets ({ $count } sรฉlectionnรฉ(s), Entrรฉe pour calculer) : .objects-not-measurable = AREA : aucun des objets sรฉlectionnรฉs ne possรจde d'aire mesurable. .result = Aire = { $area }, Pรฉrimรจtre = { $perimeter } @@ -250,23 +262,23 @@ area = command-move = .base = - MOVE Spรฉcifiez le point de base [{ $count -> + MOVE Point d'origine [{ $count -> [one] { $count } objet *[other] { $count } objets }] : - .target = MOVE Spรฉcifiez la destination [base { $x },{ $y }] : + .target = MOVE Point de destination [base { $x },{ $y }] : .displacement = MOVE Spรฉcifiez le dรฉplacement : command-copy = - .array-count = COPY Saisissez le nombre d'รฉlรฉments du rรฉseau : + .array-count = COPY Saisissez le nombre d'รฉlรฉments : .base = - COPY Spรฉcifiez le point de base [{ $count -> + COPY Point d'origine [{ $count -> [one] { $count } objet *[other] { $count } objets }] : - .array-target = COPY Spรฉcifiez le second point du rรฉseau de { $count } รฉlรฉments [base { $x },{ $y }] : + .array-target = COPY Second point du rรฉseau de { $count } รฉlรฉments [base { $x },{ $y }] : .target = - COPY Spรฉcifiez la destination [{ $count -> + COPY Destination [{ $count -> [one] { $count } copie jusqu'ici *[other] { $count } copies jusqu'ici } | Rรฉseau | Entrรฉe=terminer | base { $x },{ $y }] : @@ -274,11 +286,73 @@ command-copy = .mode = COPY Entrez le mode de copie [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ seulement ici (vert), __ocs_fmt_1__ seulement dans lโ€™autre dessin (rouge)__ocs_fmt_2__ [Next/Previous/eXit] : + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Comparer avec un dessin + .label-associative-array-created = __ocs_fmt_0__ : rรฉseau associatif crรฉรฉ. + .assoc-array-array-path = Rรฉseau (trajectoire) + .assoc-array-array-polar = Rรฉseau (polaire) + .assoc-array-array-rectangular = Rรฉseau (rectangulaire) + .assoc-array-item-spacing = Espacement des รฉlรฉments + .assoc-array-fill-angle = Angle de remplissage + .assoc-array-items = ร‰lรฉments + .assoc-array-level-spacing = Espacement des niveaux + .assoc-array-levels = Niveaux + .assoc-array-row-spacing = Espacement des rangรฉes + .assoc-array-column-spacing = Espacement des colonnes + .fbxexport-exported-to = FBXEXPORT: exportรฉ vers "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: aucune donnรฉe de maillage 3D dans ce dessin. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Les nouvelles hachures suivent leurs contours (HPASSOC) + .options-show-ribbon = Afficher le ruban (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Afficher la barre de menus (MENUBAR) + .options-window-elements = ร‰lรฉments de fenรชtre + .menu-viewports = Fenรชtres + .menu-view-twist = Rotation de la vue + .menu-tools = Outils + .menu-table-style = Style de tableau + .menu-run-script = Exรฉcuter un script + .menu-revision-cloud = Nuage de rรฉvision + .menu-regen-all = Tout rรฉgรฉnรฉrer + .menu-redraw = Redessiner + .menu-rectangle = Rectangle + .menu-quick-dimension = Cotation rapide + .menu-publish = Publier + .menu-paste-special = Collage spรฉcial + .menu-parametric = Paramรฉtrique + .menu-page-setup-manager = Gestionnaire des mises en page + .menu-orbit = Orbite + .menu-new-ucs = Nouveau SCU + .menu-named-views = Vues nommรฉes + .menu-multiline-text = Texte multiligne + .menu-multiline-style = Style de multiligne + .menu-multiline = Multiligne + .menu-multileader-style = Style de ligne de repรจre multiple + .menu-match-properties = Copier les propriรฉtรฉs + .menu-lengthen = Modifier la longueur + .menu-jogged = Raccourci + .menu-id-point = Point ID + .menu-explode-text = Dรฉcomposer le texte + .menu-edit-spline = Modifier la spline + .menu-edit-polyline = Modifier la polyligne + .menu-edit-hatch = Modifier les hachures + .menu-drawing-limits = Limites du dessin + .menu-donut = Anneau + .menu-dimension-style = Style de cote + .menu-construction-line = Droite de construction + .menu-arc-length = Longueur dโ€™arc + .command-line-opacity = Opacitรฉ de la ligne de commande + .txtexp-no-text-selected = TXTEXP : aucun texte sรฉlectionnรฉ. + .angle-override-shown = Remplacement dโ€™angle : __ocs_fmt_0__ .plot-preview-task = Aperรงu du tracรฉ .link-a-csv-data-source = Lier une source de donnรฉes CSV .csv-files = Fichiers CSV .browser-cannot-open-typed-path = Le navigateur ne peut pas ouvrir un chemin saisi ; choisissez le fichier. .recover-drawing = Rรฉcupรฉrer le dessin + .requires-0-or-1-or-keyword = Nรฉcessite 0 ou 1 uniquement, ou un mot-clรฉ dโ€™option. + .requires-a-number = Nรฉcessite un nombre. .restore-it-as-a-recovered-tab-or-discard-the-copy = Restaurez-le dans un onglet rรฉcupรฉrรฉ ou supprimez la copie. .name-has-an-autosave-copy-newer-than-the-last-save = ยซ__ocs_fmt_0__ยป dispose dโ€™une copie de sauvegarde automatique plus rรฉcente que le dernier enregistrement. .autosave-copy-could-not-be-stored = Impossible de stocker la copie de sauvegarde automatique : __ocs_fmt_0__ @@ -402,7 +476,7 @@ common = .center-mark-dimcen = Marque centrale (DIMCEN) .centered = Centrรฉ .centimeters = Centimรจtres - .chamfer = Chรขtain + .chamfer = Chamfrein .changelog = Changer de journal .changing-a-base-colour-switches-to-custom-iced = Changer une couleur de base change en Custom. Iced gรฉnรจre chaque teinte de composant de ces six couleurs. .circle = Cercle @@ -467,7 +541,7 @@ common = .dim-style = Style de cote .dimension = Cotation .dimensions = Cotations - .discard = Jetez + .discard = Annuler .discussions = Dรฉbat .discussions-load-from-github = Charge de discussions de GitHub. .display = Affichage @@ -538,7 +612,7 @@ common = .gouraud-shaded = Ombrage Gouraud .gouraud-shaded-edges = Gouraud Shaded + Bords .green = Vert - .group = Groupe + .group = Grouper .group-created = Groupe crรฉรฉ ยซ__ocs_fmt_0__ยป. .groups = Groupes .h-margin = H Marge : @@ -555,7 +629,7 @@ common = .hidden = Cachรฉ .hidden-line = Ligne cachรฉe .high = ร‰levรฉ - .horizontal = Horizontale + .horizontal = Horizontal .horizontal-inside-dimtih = Intรฉrieur horizontal (DIMTIH) .horizontal-just-dimjust = Horizontal juste (DIMJUST) .horizontal-outside-dimtoh = Extรฉrieur horizontal (DIMTOH) @@ -822,6 +896,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: sรฉlectionnez un objet de coupe ou activez une coupe dynamique. + .sectioncull-prepare-failed = SECTIONCULL: un objet coupรฉ n'a pas pu รชtre prรฉparรฉ ; rien n'a รฉtรฉ modifiรฉ. + .sectioncull-nothing-outside = SECTIONCULL: rien ne se trouve hors de la coupe. + .sectioncull-result = SECTIONCULL: __ocs_arg_erased__ objet(s) effacรฉ(s) et __ocs_arg_cut__ coupรฉ(s) ร  la limite de la coupe. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: adaptez une รฉlingue ร  des points __ocs_fmt_0__. .splinefit-need-at-least-3-points = SPLINEFIT: nรฉcessite au moins 3 points. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT : sรฉlectionnez une polyligne pour l'adapter ร  une spline. @@ -859,9 +937,12 @@ common = .sent-to-printer-printer = Envoyรฉ ร  l'imprimante : __ocs_fmt_0__ .set-current = Dรฉfinir comme courant .shade-plot = Terrain ombragรฉ + .shaded-with-edges = Ombrรฉ avec arรชtes + .shades-of-gray = Nuances de gris .shadow-flags = Drapeaux d'ombre .single = Un seul .size = Taille + .sketchy = Croquis .snap-override-label-next-pick-only = Accrochage temporaire : __ocs_fmt_0__ (prochaine sรฉlection uniquement). .solid-created = Solide crรฉรฉ. .source-object-not-found = Objet source introuvable. @@ -974,7 +1055,7 @@ common = .vplayer-thawed-count-layer-s-thawed-in-viewpor = VPLAYER : __ocs_fmt_0__ calque(s) libรฉrรฉ(s) dans la fenรชtre. .value = Valeur .version = Version - .vertical = Verticale + .vertical = Vertical .vertical-pos-dimtad = Position verticale (DIMTAD) .vertical-pos-dimtvp = Position verticale (DIMTVP) .videos = Vidรฉos @@ -1067,7 +1148,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE : saisissez Left, Top ou Right. .isodraft-expected-on-or-off = ISODRAFT : saisissez On ou Off. .xdata-selected-entities-are-on-locked-layers = XDATA : les objets sรฉlectionnรฉs sont sur des calques verrouillรฉs. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST : aucune image matricielle sรฉlectionnรฉe ou propriรฉtรฉ inconnue (utilisez BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE : saisissez un entier entre -4 et 4. .usage-datalink-path-to-csv = Utilisation : DATALINK .datalinkupdate-no-linked-tables-found = DATALINKUPDATE : aucune table liรฉe trouvรฉe. @@ -2042,8 +2122,8 @@ common = .distance-and-angle-tolerances-must-be-non-negative-numbers = Distance and angle tolerances must be non-negative numbers. .distance1-distance2 = Distance1 = __ocs_fmt_0__, Distance2 = __ocs_fmt_1__ .draw-section = Draw section - .drawing-table = Drawing table - .drawings-folders = Drawings / folders + .drawing-table = Table ร  dessin + .drawings-folders = Dossiers ร  dessins .dynamic-prompts = Dynamic Prompts .edit-an-existing-data-extraction = Edit an existing data extraction .editing-weighted-or-tolerance-fitted-interpolation-data-is-not-supported = Editing weighted or tolerance-fitted interpolation data is not supported. @@ -2060,7 +2140,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = Fichier .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2369,6 +2449,7 @@ common = .load-linetypes = Charger des types de ligne .requires-two-positive-values = Nรฉcessite deux valeurs positives (x,y) ou une seule pour les deux. .snap-marker-color = Couleur du marqueur d'accrochage + .snap-marker-size = Taille du marqueur d'accrochage .command-line-text-color = Couleur du texte de la ligne de commande .operand = Opรฉrande .linetypes-loaded = __ocs_fmt_0__ type(s) de ligne chargรฉ(s). @@ -2382,6 +2463,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Spรฉcifier le point de base [__ocs_arg_count__ objets]: .threedrotate-specify-rotation-axis = 3DROTATE Spรฉcifiez l'axe de rotation [X/Y/Z] ou un second point sur celui-ci : .threedrotate-specify-rotation-angle-a = 3DROTATE Spรฉcifier l'angle de rotation <__ocs_arg_a__: + .parametric-coincident = Coincident + .parametric-colinear = Colinรฉaire + .parametric-concentric = Concentrique + .parametric-symmetric = Symmรฉtrique + .parametric-equal = ร‰gal + .parametric-fix = Fix + errors = .save-warning = Enregistrer l' avertissement .unable-to-save-drawing = Impossible de sauvegarder le dessin @@ -2527,7 +2615,7 @@ inquiry = .area-area-perimeter-perimeter = Superficie = __ocs_arg_area__, pรฉrimรจtre = __ocs_arg_perimeter__ .bedit-block-edit-discarded = BEDIT: bloc "__ocs_fmt_0__" รฉdition supprimรฉe. .bedit-block-saved-all-references-updated = BEDIT: Bloc "__ocs_fmt_0__" sauvegardรฉ. Toutes les rรฉfรฉrences sont mises ร  jour. - .bedit-editing-block-use-save-block-or-discard = BEDIT: Modifier le bloc "__ocs_fmt_0__". Utilisez Enregistrer le bloc ou Jetez pour terminer. + .bedit-editing-block-use-save-block-or-discard = BEDIT: Modifier le bloc "__ocs_fmt_0__". Utilisez Enregistrer le bloc ou Annuler pour terminer. .bedit-block-not-found = BEDIT: bloc "__ocs_fmt_0__" non trouvรฉ. .bedit-cannot-edit-an-external-reference-xref = BEDIT: ne peut pas modifier une rรฉfรฉrence externe (xref). .bedit-finish-the-active-refedit-refclose-first = BEDIT: terminer d'abord le REFEDIT actif (REFCLOSE). @@ -2672,7 +2760,7 @@ draw = .attedit-updated-changed-attribute-s-tag-up-new = ATTEDIT: mise ร  jour de l'attribut __ocs_fmt_0__ [__ocs_fmt_1__] = __ocs_fmt_2__. .arc-length-symbol-dimarcsym = Symbole de la longueur de l'arc (DIMARCSYM) .axis = Axe - .axis-end = Axis, fin + .axis-end = Axe, fin .boundary-pick-internal-point-miss = BOUNDARY Choisir le point interne:__ocs_arg_miss__ .boundary = Limite .centerline-select-first-line = CENTERLINE Sรฉlectionner la premiรจre ligne : @@ -2701,7 +2789,7 @@ draw = .center-start-angle = Centre, dรฉpart, angle .center-start-end = Centre, Dรฉbut, Fin .center-start-length = Centre, Dรฉbut, Longueur - .circumscribed = Circonscription + .circumscribed = Circonscrit .current-settings-edit-mode = Paramรจtres actuels : mode dโ€™รฉdition = __ocs_fmt_0__ .ddedit-selected-entity-is-not-text = DDEDIT : l'entitรฉ sรฉlectionnรฉe n'est pas un texte. .dimcenter-select-arc-or-circle = DIMCENTER Sรฉlectionner l'arc ou le cercle: @@ -2769,7 +2857,7 @@ draw = .polysolid-wall-width = POLYSOLID largeur de paroi: .pyramid-base-radius = PYRAMID rayon de base: .pyramid-height-add-sides-by-typing-a-3rd-numbe = PYRAMID hauteur (ajouter les cรดtรฉs en tapant un 3รจme nombre): - .polygon-circumscribed = Polygone - Circonscription + .polygon-circumscribed = Polygone - Circonscrit .polygon-edge = Polygone - Bord .polygon-inscribed = Polygone - Inscrit .ray-specify-start-point = RAY Spรฉcifier le point de dรฉpart : @@ -3005,6 +3093,7 @@ draw = .arc-specify-radius-of-arc = ARC Spรฉcifiez le rayon de l'arc : .chord-length = Longueur de corde + modify = .add-vertex = Ajouter un sommet .closed-cross = Croix fermรฉe @@ -3050,6 +3139,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Sรฉlectionner un bloc avec des attributs : .annotation-object-scale = ร‰chelle d'objets d'annotation .bedit-select-block-reference-to-edit = BEDIT Sรฉlectionnez la rรฉfรฉrence de bloc ร  modifier : + .bvstate-option = BVSTATE Entrez une option [Nouveau/Dรฉfinir/Renommer/Supprimer] : + .bvstate-new-name = BVSTATE Entrez le nom du nouvel รฉtat de visibilitรฉ : + .bvstate-set = BVSTATE Entrez l'รฉtat de visibilitรฉ ร  rendre courant <__ocs_fmt_0__> : + .bvstate-rename-from = BVSTATE Entrez l'รฉtat de visibilitรฉ ร  renommer <__ocs_fmt_0__> : + .bvstate-rename-to = BVSTATE Entrez le nouveau nom : + .bvstate-delete = BVSTATE Entrez l'รฉtat de visibilitรฉ ร  supprimer : + .bvstate-not-found = ร‰tat de visibilitรฉ "__ocs_fmt_0__" introuvable. + .bvstate-exists = L'รฉtat de visibilitรฉ "__ocs_fmt_0__" existe dรฉjร . + .bvstate-last = Le seul รฉtat de visibilitรฉ ne peut pas รชtre supprimรฉ. + .bvstate-needs-bedit = __ocs_fmt_0__ : ouvrez d'abord le bloc dans l'รฉditeur de blocs (BEDIT). + .bvstate-current = ร‰tat de visibilitรฉ courant : __ocs_fmt_0__ + .bvhide-select = Sรฉlectionnez les objets ร  masquer : + .bvshow-select = Sรฉlectionnez les objets ร  rendre visibles : + .arrayedit-select-array = ARRAYEDIT Sรฉlectionnez le rรฉseau : + .arrayedit-options = ARRAYEDIT Entrez une option [Source/eXit] : + .arrayedit-not-an-array = ARRAYEDIT : l'objet sรฉlectionnรฉ n'est pas un rรฉseau associatif. .blend-enter-continuity-tangent-curvature-c = BLEND Entrer la continuitรฉ [Tangent/Curvature] <__ocs_arg_c__: .blend-select-first-open-curve-continuity-c = BLEND Sรฉlectionner la premiรจre courbe ouverte [Continuitรฉ=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Sรฉlectionner la deuxiรจme courbe ouverte [Continuitรฉ=__ocs_arg_c__]: @@ -3084,10 +3189,10 @@ modify = .extend-edge-fence-pick-points-placed-enter-ext = EXTEND__ocs_fmt_0__ Fence: points de sรฉlection [__ocs_fmt_1__ placรฉ, Entrรฉe = extension croisรฉe]: .extrim-click-the-side-to-trim-away = EXTRIM Cliquez sur le cรดtรฉ pour couper : .extrim-select-cutting-boundary = EXTRIM Sรฉlectionnez la limite de coupe : - .edge-extend-title-case = Edge: ร‰largir + .edge-extend-title-case = Edge: ร‰tendre .erase = Effacer .explode = Dรฉcomposer - .extend = ร‰largir + .extend = ร‰tendre .fillet-enter-fillet-radius-4 = FILLET Entrez le rayon du filet <__ocs_fmt_0__>: .fillet-select-first-object-line-arc-lwpolyline = FILLET Sรฉlectionnez le premier objet (Ligne/Arc/Polyligne lรฉgรจre) [R=__ocs_fmt_0__] : .fillet-select-second-object-line-arc-lwpolylin = FILLET Sรฉlectionnez le second objet (Ligne/Arc/Polyligne lรฉgรจre) [R=__ocs_fmt_0__] : @@ -3117,8 +3222,8 @@ modify = .pedit-select-polyline-or-a-line-arc-to-convert = PEDIT Sรฉlectionnez polyligne (ou une ligne/arc ร  convertir): .pedit-specify-new-width = PEDIT Spรฉcifier une nouvelle largeur: .pedit-join-select-objects-to-join-count-picked = PEDIT Joignez les objets ร  joindre (__ocs_arg_count__ sรฉlectionnรฉ), Entrez pour fusionner : - .path-array = Voie - .polar-array = Tableau polaire + .path-array = Rรฉseau trajectoire + .polar-array = Rรฉseau polaire .refclose-save-discard-save = REFCLOSE [Enregistrer/Dรฉsister] : .refedit-select-block-reference-to-edit = REFEDIT Sรฉlectionnez la rรฉfรฉrence de bloc ร  modifier : .reverse-select-line-polyline-or-spline-to-reve = REVERSE Sรฉlectionnez la ligne, la polyligne ou l'รฉpingle pour inverser : @@ -3127,7 +3232,7 @@ modify = .rotate-specify-rotation-angle-a = ROTATE Spรฉcifier l'angle de rotation <__ocs_arg_a__: .rotate-specify-rotation-center-count-objects = ROTATE Spรฉcifier le centre de rotation [__ocs_arg_count__ objets]: .rotate-specify-second-reference-point = ROTATE Prรฉciser le deuxiรจme point de rรฉfรฉrence: - .rectangular-array = Array rectangulaire + .rectangular-array = Rรฉseau rectangulaire .scale-specify-base-point-count-objects = SCALE Spรฉcifier le point de base [__ocs_arg_count__ objets]: .scale-specify-first-reference-point-or-type-re = SCALE Prรฉciser le premier point de rรฉfรฉrence ou la longueur de rรฉfรฉrence du type: .scale-specify-new-length-from-base-or-type-a-l = SCALE Spรฉcifiez une nouvelle longueur ร  partir de la base ou tapez une longueur [ref=__ocs_arg_d__]: @@ -3602,6 +3707,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Spรฉcifier le point d'insertion: insert = + .pastespec-specify-insertion-point = PASTESPEC Spรฉcifiez le point dโ€™insertion : + .pastespec-paste-as = PASTESPEC Coller comme [Objects/Block/Text] : .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3718,6 +3825,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3772,6 +3886,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Les accrochages aux calques sous-jacents varient* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3911,6 +4026,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Sรฉlectionnez une image ou un calque sous-jacent : + .adjust-select-pdf-underlay = Sรฉlectionnez un calque sous-jacent PDF : + .adjust-select-dwf-underlay = Sรฉlectionnez un calque sous-jacent DWF : + .adjust-select-dgn-underlay = Sรฉlectionnez un calque sous-jacent DGN : + .adjust-mixed-underlays = *Jeu de sรฉlection non valide. Sรฉlectionnez uniquement des calques sous-jacents du mรชme type de fichier.* + .adjust-image-option = Entrez une option d'image [Contraste/Estompage/Luminositรฉ] : + .adjust-underlay-option = Entrez une option de calque sous-jacent __ocs_arg_kind__ [Estompage/Contraste/Monochrome] <__ocs_arg_option__>: + .adjust-monochrome = Monochrome? [Oui/Non] <__ocs_arg_value__>: + .adjust-brightness-value = Entrez la valeur de luminositรฉ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Entrez la valeur de contraste (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Entrez la valeur dโ€™estompage (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = La valeur doit รชtre un nombre entier compris entre 0 et 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4130,12 +4257,12 @@ insert = .bp-icon = Icon .bp-insertion-point = Insertion Point .bp-view-large = Large Icons - .bp-tab-libraries = Libraries + .bp-tab-libraries = Bibliothรจques .bp-heading-libraries = Library Blocks .bp-view-medium = Medium Icons - .bp-tab-recent = Recent - .bp-heading-recent = Recent Blocks - .bp-gallery-recent = Recent Blocks... + .bp-tab-recent = Rรฉcent + .bp-heading-recent = Blocs rรฉcents + .bp-gallery-recent = Blocs rรฉcents... .bp-redefine = Redefine .bp-remove-block = Remove Block .bp-remove-favorite = Remove from Favorites @@ -4158,13 +4285,13 @@ insert = .attdef-edit-title = Edit Attribute Definition .attdef-edit-help = Changes the tag, prompt and default value of an attribute definition. .attdef-help = Defines an attribute: a tag, prompt and default value placed as text in a block definition. - .attdef-define-attributes = Define Attributes + .attdef-define-attributes = Dรฉfinir attributs .attdef-manage-attributes = Manage Attributes .attdef-set-base-point = Set Base Point .attdef-attmode-retain = Retain Attribute Display - .attdef-attmode-all = Display All Attributes - .attdef-attmode-none = Hide All Attributes - .attdef-edit-attribute = Edit Attribute + .attdef-attmode-all = Afficher tous les attributs + .attdef-attmode-none = Cacher tous les attributs + .attdef-edit-attribute = Modifier l'attribut .attdef-multiline-editor = Multiline editor .attdef-tag-empty = The tag cannot be empty. .attdef-tag-spaces = The tag may not contain spaces. @@ -4214,6 +4341,7 @@ insert = .field-named-textstyle = Textstyle .field-prop-endpoint = EndPoint .field-prop-startpoint = StartPoint + model = .create = Crรฉer .boolean = Opรฉrations boolรฉennes @@ -4222,7 +4350,7 @@ model = .extrude = Extruder .revolve = Rรฉvolution .loft = Lissage - .sweep = Balayage + .sweep = Balayer .presspull = Tirer-pousser .n-specify-center-point = __ocs_arg_n__ Spรฉcifier le point central: .n-specify-first-corner = __ocs_arg_n__ Spรฉcifiez le premier coin : @@ -4250,7 +4378,7 @@ model = .extra-modeler-data = Donnรฉes supplรฉmentaires du modรฉlisateur .hemispherical = Hรฉmisphรฉrique .history = Historique - .intersect = Intersecte + .intersect = Intersection .isoline-list = Liste isoline .isolines = Isolines .kind = Genre @@ -4268,7 +4396,7 @@ model = .revolve-entity = Objet de rรฉvolution .revolve-flags = Drapeaux renouvelables .spherical = Sphรฉrique - .subtract = Sous-traitance + .subtract = Soustraire .surface-construction = Construction de surface .sweep-path = Balayage / Chemin .sweep-entity = Entitรฉ @@ -4280,7 +4408,7 @@ model = .u-vector-1 = U Vecteur 1 .u-vector-2 = U Vecteur 2 .uid = UID - .union = Union europรฉenne + .union = Union .v-isolines = V. Isolines .v-vector-1 = V Vecteur 1 .v-vector-2 = V Vecteur 2 @@ -4415,7 +4543,27 @@ model = .value-2point = 2 points .cube = Cube + layout = + .mvsetup-select-objects-to-delete = MVSETUP Sรฉlectionnez les objets ร  supprimer : + .delete-objects-option = Supprimer des objets + .mvsetup-enable-paper-space = MVSETUP Activer lโ€™espace papier ? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Entrez le facteur dโ€™รฉchelle <1> : + .mvsetup-enter-paper-width = MVSETUP Entrez la largeur du papier : + .mvsetup-enter-paper-height = MVSETUP Entrez la hauteur du papier : + .mvsetup-enter-option = MVSETUP Entrez une option [Create/Delete objects/Scale viewports] : + .mvsetup-enter-viewport-layout = MVSETUP Entrez la disposition des fenรชtres [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Spรฉcifiez le premier coin de la zone des fenรชtres : + .mvsetup-specify-opposite-corner = MVSETUP Spรฉcifiez le coin opposรฉ : + .mvsetup-viewports-in-x = MVSETUP Entrez le nombre de fenรชtres en X <2> : + .mvsetup-viewports-in-y = MVSETUP Entrez le nombre de fenรชtres en Y <2> : + .mvsetup-distance-in-x = MVSETUP Spรฉcifiez la distance entre les fenรชtres en X <0> : + .mvsetup-distance-in-y = MVSETUP Spรฉcifiez la distance entre les fenรชtres en Y <0> : + .mvsetup-select-viewports = MVSETUP Sรฉlectionnez les fenรชtres ร  mettre ร  lโ€™รฉchelle : + .mvsetup-paper-space-units = MVSETUP Entrez le nombre dโ€™unitรฉs de lโ€™espace papier <1> : + .mvsetup-model-space-units = MVSETUP Entrez le nombre dโ€™unitรฉs de lโ€™espace objet <1> : + .mvsetup-viewports-do-not-fit = MVSETUP Les fenรชtres ne tiennent pas dans la zone. + .scale-viewports-option = ร‰chelle des fenรชtres .active = Actif .inactive = Inactif .insert-view = Insรฉrer une vue @@ -4441,6 +4589,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Freeze , T = Thaw , Entrรฉe = Sortie view = + .twist-option = Rotation + .dview-specify-view-twist-angle = DVIEW Spรฉcifiez lโ€™angle de rotation de la vue <__ocs_fmt_0__> : + .dview-enter-option-twist-undo = DVIEW Entrez une option [TWist/Undo] : .color-scheme-set-to-system-currently-name = Jeu de couleurs dรฉfini sur ยซ Systรจme ยป (actuellement __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = Nรฉcessite un nombre entier compris entre __ocs_fmt_0__ et __ocs_fmt_1__. @@ -4635,13 +4786,6 @@ view = .x-no-xdata = __ocs_fmt_0__: pas de xdata. .value-3d-orbit = Orbite 3D .value-3d-orbit-drag-with-right-mouse-button = Orbite 3D: faites glisser avec le bouton droit de la souris. - .adjust-brightness-contrast-fade = ADJUST [Luminositรฉ / Contraste / Estompage] : - .adjust-brightness-0-100 = ADJUST luminositรฉ 0-100: - .adjust-contrast-0-100 = ADJUST contraste 0-100: - .adjust-fade-0-100 = ADJUST estompage 0-100 : - .adjust-select-raster-image-s-first = ADJUST : sรฉlectionnez d'abord les images master. - .adjust-value-must-be-0-100 = ADJUST: la valeur doit รชtre 0-100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ sur __ocs_fmt_2__ image(s). .annoallvisible-new-value-0-1 = ANNOALLVISIBLE nouvelle valeur [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE : entrez 0 ou 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE nouvelle valeur [-4..4]: @@ -4767,7 +4911,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Sous-couche __ocs_fmt_0__ : estompage=__ocs_fmt_1__, contraste=__ocs_fmt_2__, actif=__ocs_fmt_3__, dรฉlimitation=__ocs_fmt_4__, monochrome=__ocs_fmt_5__ .updated-changed-underlay-s = Mise ร  jour de __ocs_fmt_0__ sous-couche(s). - .usage-adjust-brightness-contrast-fade-0-100 = Utilisation : ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Utilisation : ANNOSCALE e.g. 1:50, 2:1, or a factor .usage-draworder-f-front-b-back-a-above-handle = Utilisation : DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Utilisation : HYPERLINK (select objects first) @@ -5775,34 +5918,34 @@ plot = manage = .about = ร€ propos .application = Demande - .audit = Vรฉrification des comptes + .audit = Auditer .cleanup = Nettoyage .customization = Personnalisation .edit-aliases = Modifier les alias - .export = Exportation + .export = Exporter .find-non-purgeable-items = - Trouver des non- - ร‰lรฉments pugables + Trouver des รฉlรฉments + non purgeables .load-partial-cui = Charge partielle CUI - .overkill = Surcapacitรฉ + .overkill = Nettoyer doublons .purge = Purger .user-interface = - Utilisateur Interface + Utilisateur plugins = .api-incompatible = API incompatible .add-from-github = Ajouter ร  partir de GitHub .add-repository = Ajouter un dรฉpรดt .added-from-github = Ajoutรฉ de GitHub - .available-plugins = Greffons disponibles - .browse-install-and-manage-add-ons-select-one-t = Parcourir, installer et gรฉrer les modules complรฉmentaires. Sรฉlectionnez un pour afficher son README. + .available-plugins = Plugins disponibles + .browse-install-and-manage-add-ons-select-one-t = Gรฉrer les modules complรฉmentaires. Sรฉlectionnez un pour afficher son README. .check-your-internet-or-proxy-connection-then-r = Vรฉrifiez votre connexion Internet ou proxy, puis rรฉessayez. Les dรฉpรดts ajoutรฉs manuellement restent disponibles. .commands-cmds = Commandes : __ocs_arg_cmds__ - .connecting-securely-using-your-system-certific = Connexion sรฉcurisรฉe en utilisant les paramรจtres de votre certificat systรจme. + .connecting-securely-using-your-system-certific = Connexion sรฉcurisรฉe en utilisant votre certificat systรจme. .copy-details = Copier les dรฉtails .disable = Dรฉsactiver - .disabled = Handicapรฉs + .disabled = Dรฉsactivรฉ .download-desktop-app = Tรฉlรฉcharger l'application de bureau .enable = Activer .fetching-the-default-branch-from-github = Rรฉcupรฉration de la branche par dรฉfaut de GitHub. @@ -5810,8 +5953,8 @@ plugins = .hide-details = Masquer les dรฉtails .incompatible = Incompatible .install = Installer - .load-failed = La charge a รฉchouรฉ - .load-failed-error = La charge a รฉchouรฉ : __ocs_arg_error__ + .load-failed = Le chargement a รฉchouรฉ + .load-failed-error = Le chargement a รฉchouรฉ : __ocs_arg_error__ .loading-readme = Chargement README... .loading-plugin-catalog = Chargement du catalogue plugin... .no-additional-plugins-are-available = Aucun plugin supplรฉmentaire n'est disponible. @@ -5994,6 +6137,46 @@ ui = .osnap-settings = Paramรจtres d'accrochage... .snap-override-none-next-pick-only = Remplacement d'accrochage : Aucun (prochain point seulement). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Toujours conserver le texte entre les lignes dโ€™attache .angular-dimensions = Cotes angulaires @@ -6261,3 +6444,117 @@ graphic-attributes = .reset-angle = Rรฉinitialiser l'angle .one-color = Une couleur .two-colors = Deux couleurs + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Ajouter au jeu de travail + .remove-from-working-set = Supprimer du jeu de travail + .toolbar-objects = objets + .dialog-title = Modification de rรฉfรฉrence + .dialog-reference = Rรฉfรฉrence ร  modifier + .dialog-chain-hint = La chaรฎne au point choisi ; cliquez sur une pastille pour changer de niveau. + .dialog-path = Chemin : + .dialog-working-set = Jeu de travail + .dialog-all-objects = Tous les objets + .dialog-pick-objects = Choisir dans le dessin + .dialog-attributes = Modifier aussi les dรฉfinitions d'attributs + .dialog-session = Session + .dialog-lock = Verrouiller les objets hors du jeu de travail + .dialog-unique = Donner des noms uniques aux calques, styles et blocs en conflit + .dialog-start = Commencer la modification + .dialog-help = Ouvre une rรฉfรฉrence de bloc ou une rรฉfรฉrence externe pour la modifier sur place. + .select-reference = Sรฉlectionnez la rรฉfรฉrence : + .reference-not-found = Rรฉfรฉrence introuvable. + .not-valid-reference = L'objet n'est pas une rรฉfรฉrence valide + .one-entity-added = 1 entitรฉ ajoutรฉe + .nesting-level = Sรฉlectionnez le niveau d'imbrication [Ok/Suivant] : + .selection-method = Entrez la mรฉthode de sรฉlection d'objets [Tous/Imbriquรฉs] : + .select-nested = Sรฉlectionnez les objets imbriquรฉs : + .display-attdefs = Afficher les dรฉfinitions d'attributs [Oui/Non] : + .zero-items-selected = 0 รฉlรฉment sรฉlectionnรฉ + .one-item-selected = 1 รฉlรฉment sรฉlectionnรฉ + .items-selected = __ocs_fmt_0__ รฉlรฉments sรฉlectionnรฉs + .refset-option = Entrez une option [Ajouter/Supprimer] : + .refclose-option = Entrez une option [Enregistrer/Ignorer les modifications de la rรฉfรฉrence] : + .not-checked-out = ** Commande interdite si aucune rรฉfรฉrence n'est en cours de modification avec REFEDIT ** + .already-checked-out = ** Commande interdite, __ocs_fmt_0__ est dรฉjร  en cours de modification ** + .transfer-objects = Transfert d'objets entre le jeu de travail RefEdit et le dessin hรดte... + .added-to-working-set = __ocs_fmt_0__ ajoutรฉ(s) au jeu de travail + .removed-from-working-set = __ocs_fmt_0__ supprimรฉ(s) du jeu de travail + .one-object-added = 1 objet ajoutรฉ ร  __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ objets ajoutรฉs ร  __ocs_fmt_1__ + .one-object-removed = 1 objet supprimรฉ de __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ objets supprimรฉs de __ocs_fmt_1__ + .one-xref-instance = 1 occurrence de xrรฉf mise ร  jour + .xref-instances = __ocs_fmt_0__ occurrences de xrรฉf mises ร  jour + .one-block-instance = 1 occurrence de bloc mise ร  jour + .block-instances = __ocs_fmt_0__ occurrences de bloc mises ร  jour + .redefined-reloaded = __ocs_fmt_0__ redรฉfini et rechargรฉ. + .redefined = __ocs_fmt_0__ redรฉfini. + .ok = Ok + .nested = Imbriquรฉs + +image-adjust = + .title = Rรฉglage de l'image + .preview = Aperรงu + .before = Avant + .after = Aprรจs + .no-preview = l'image n'a pas pu รชtre chargรฉe + .adjust = Rรฉglage + .brightness = Luminositรฉ + .contrast = Contraste + .fade = Estompage + .hint = Cliquez ou faites glisser une barre ; โˆ’ / + est un pas, une case accepte un entier de 0 ร  100. + .reset = Rรฉtablir les valeurs par dรฉfaut + .apply = Appliquer + .help = Dรฉfinit la luminositรฉ, le contraste et l'estompage des images raster sรฉlectionnรฉes. + +keytips = + .title = KeyTip + .typed = Saisi : { $path } + .hint-tabs = Tapez la lettre d'un onglet ยท ร‰chap ferme + .hint-tab = Onglet { $tab } ยท ร‰chap remonte d'un niveau + .hint-items = Tapez la lettre d'un รฉlรฉment ยท ร‰chap remonte d'un niveau diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 5745a31fe..7853fb838 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = เคชเฅเคฒเค—-เค‡เคจ ribbon-tab = + .block-editor = เคฌเฅเคฒเฅ‰เค• เคธเค‚เคชเคพเคฆเค• .draw = เคกเฅเคฐเฅ‰ .parametric = เคชเฅˆเคฐเคพเคฎเฅ€เคŸเฅเคฐเคฟเค• .annotate = เคเคจเฅ‹เคŸเฅ‡เคŸ @@ -98,6 +99,9 @@ start = .no-recent-files = เค†เคชเค•เฅ‡ เคฆเฅเคตเคพเคฐเคพ เค–เฅ‹เคฒเฅ€ เค—เคˆ เคซเคผเคพเค‡เคฒเฅ‡เค‚ เคฏเคนเคพเค เคฆเคฟเค–เคพเคˆ เคฆเฅ‡เค‚เค—เฅ€เฅค .browser-storage = เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เคธเค‚เค—เฅเคฐเคนเคฃ .keep-recent-files = เคนเคพเคฒ เค•เฅ€ เคซเคผเคพเค‡เคฒเฅ‡เค‚ เคฐเค–เฅ‡เค‚ + .view-list = เคธเฅ‚เคšเฅ€ + .view-icons = เค†เค‡เค•เคจ + .view-preview = เคฌเคกเคผเฅ‡ เคชเฅ‚เคฐเฅเคตเคพเคตเคฒเฅ‹เค•เคจ donation = .title = เคฆเคพเคจ เค•เคพ เค…เคจเฅเคฐเฅ‹เคง @@ -167,6 +171,14 @@ modal = .that-object = เค‰เคธ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เค•เฅ‹ .those-objects = เค‰เคจ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เค•เฅ‹ + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = เค›เคตเคฟ "{ $name }" เคชเฅ‡เคธเฅเคŸ เค•เฅ€ เค—เคˆ: { $width }ร—{ $height } px .image-unsupported = เคชเฅ‡เคธเฅเคŸ เค•เฅ€ เค—เคˆ เค›เคตเคฟ "{ $name }" เค•เฅ‹ เคกเคฟเค•เฅ‹เคก เคจเคนเฅ€เค‚ เค•เคฟเคฏเคพ เคœเคพ เคธเค•เคพเฅค @@ -262,11 +274,73 @@ command-copy = .mode = COPY เค•เฅ‰เคชเฅ€ เคฎเฅ‹เคก เคตเคฟเค•เคฒเฅเคช เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ เค•เฅ‡เคตเคฒ เคฏเคนเคพเค (เคนเคฐเคพ), __ocs_fmt_1__ เค•เฅ‡เคตเคฒ เคฆเฅ‚เคธเคฐเฅ€ เคกเฅเคฐเฅ‰เค‡เค‚เค— เคฎเฅ‡เค‚ (เคฒเคพเคฒ)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = เคกเฅเคฐเฅ‰เค‡เค‚เค— เคธเฅ‡ เคคเฅเคฒเคจเคพ เค•เคฐเฅ‡เค‚ + .label-associative-array-created = __ocs_fmt_0__: เคธเคนเคฏเฅ‹เค—เฅ€ เคเคฐเฅ‡ เคฌเคจเคพเคฏเคพ เค—เคฏเคพเฅค + .assoc-array-array-path = เคเคฐเฅ‡ (เคชเคฅ) + .assoc-array-array-polar = เคเคฐเฅ‡ (เคงเฅเคฐเฅเคตเฅ€เคฏ) + .assoc-array-array-rectangular = เคเคฐเฅ‡ (เค†เคฏเคคเคพเค•เคพเคฐ) + .assoc-array-item-spacing = เค†เค‡เคŸเคฎ เค…เค‚เคคเคฐเคพเคฒ + .assoc-array-fill-angle = เคญเคฐเคฃ เค•เฅ‹เคฃ + .assoc-array-items = เค†เค‡เคŸเคฎ + .assoc-array-level-spacing = เคธเฅเคคเคฐ เค…เค‚เคคเคฐเคพเคฒ + .assoc-array-levels = เคธเฅเคคเคฐ + .assoc-array-row-spacing = เคชเค‚เค•เฅเคคเคฟ เค…เค‚เคคเคฐเคพเคฒ + .assoc-array-column-spacing = เค•เฅ‰เคฒเคฎ เค…เค‚เคคเคฐเคพเคฒ + .fbxexport-exported-to = FBXEXPORT: "__ocs_fmt_0__" เคฎเฅ‡เค‚ เคจเคฟเคฐเฅเคฏเคพเคค เค•เคฟเคฏเคพ เค—เคฏเคพ + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: เค‡เคธ เคกเฅเคฐเฅ‰เค‡เค‚เค— เคฎเฅ‡เค‚ เค•เฅ‹เคˆ 3D เคฎเฅ‡เคถ เคกเฅ‡เคŸเคพ เคจเคนเฅ€เค‚ เคนเฅˆเฅค + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = เคจเค เคนเฅˆเคš เค…เคชเคจเฅ€ เคธเฅ€เคฎเคพเค“เค‚ เค•เคพ เค…เคจเฅเคธเคฐเคฃ เค•เคฐเคคเฅ‡ เคนเฅˆเค‚ (HPASSOC) + .options-show-ribbon = เคฐเคฟเคฌเคจ เคฆเคฟเค–เคพเคเค (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = เคฎเฅ‡เคจเฅ‚ เคฌเคพเคฐ เคฆเคฟเค–เคพเคเค (MENUBAR) + .options-window-elements = เคตเคฟเค‚เคกเฅ‹ เคคเคคเฅเคต + .menu-viewports = เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ + .menu-view-twist = เคฆเฅƒเคถเฅเคฏ เค˜เฅเคฎเคพเคต + .menu-tools = เค‰เคชเค•เคฐเคฃ + .menu-table-style = เคคเคพเคฒเคฟเค•เคพ เคถเฅˆเคฒเฅ€ + .menu-run-script = เคธเฅเค•เฅเคฐเคฟเคชเฅเคŸ เคšเคฒเคพเคเค + .menu-revision-cloud = เคธเค‚เคถเฅ‹เคงเคจ เค•เฅเคฒเคพเค‰เคก + .menu-regen-all = เคธเคญเฅ€ เคชเฅเคจเคƒ เค‰เคคเฅเคชเคจเฅเคจ เค•เคฐเฅ‡เค‚ + .menu-redraw = เคชเฅเคจเคƒ เค†เคฐเฅ‡เค–เคฟเคค เค•เคฐเฅ‡เค‚ + .menu-rectangle = เค†เคฏเคค + .menu-quick-dimension = เคคเฅเคตเคฐเคฟเคค เค†เคฏเคพเคฎ + .menu-publish = เคชเฅเคฐเค•เคพเคถเคฟเคค เค•เคฐเฅ‡เค‚ + .menu-paste-special = เคตเคฟเคถเฅ‡เคท เคšเคฟเคชเค•เคพเคเค + .menu-parametric = เคชเฅˆเคฐเคพเคฎเฅ€เคŸเฅเคฐเคฟเค• + .menu-page-setup-manager = เคชเฅ‡เคœ เคธเฅ‡เคŸเค…เคช เคชเฅเคฐเคฌเค‚เคงเค• + .menu-orbit = เค‘เคฐเฅเคฌเคฟเคŸ + .menu-new-ucs = เคจเคฏเคพ UCS + .menu-named-views = เคจเคพเคฎเคฟเคค เคฆเฅƒเคถเฅเคฏ + .menu-multiline-text = เคฌเคนเฅ-เคชเค‚เค•เฅเคคเคฟ เคŸเฅ‡เค•เฅเคธเฅเคŸ + .menu-multiline-style = เคฎเคฒเฅเคŸเฅ€เคฒเคพเค‡เคจ เคถเฅˆเคฒเฅ€ + .menu-multiline = เคฎเคฒเฅเคŸเฅ€เคฒเคพเค‡เคจ + .menu-multileader-style = เคฎเคฒเฅเคŸเฅ€เคฒเฅ€เคกเคฐ เคถเฅˆเคฒเฅ€ + .menu-match-properties = เค—เฅเคฃ เคฎเคฟเคฒเคพเคเค + .menu-lengthen = เคฒเค‚เคฌเคพ เค•เคฐเฅ‡เค‚ + .menu-jogged = เคœเฅ‰เค—เฅเคก + .menu-id-point = เคฌเคฟเค‚เคฆเฅ เค†เคˆเคกเฅ€ + .menu-explode-text = เคŸเฅ‡เค•เฅเคธเฅเคŸ เคตเคฟเคธเฅเคซเฅ‹เคŸเคฟเคค เค•เคฐเฅ‡เค‚ + .menu-edit-spline = เคธเฅเคชเฅเคฒเคพเค‡เคจ เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐเฅ‡เค‚ + .menu-edit-polyline = เคชเฅ‰เคฒเฅ€เคฒเคพเค‡เคจ เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐเฅ‡เค‚ + .menu-edit-hatch = เคนเฅˆเคš เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐเฅ‡เค‚ + .menu-drawing-limits = เคกเฅเคฐเฅ‰เค‡เค‚เค— เคธเฅ€เคฎเคพเคเค + .menu-donut = เคกเฅ‹เคจเคŸ + .menu-dimension-style = เค†เคฏเคพเคฎ เคถเฅˆเคฒเฅ€ + .menu-construction-line = เคจเคฟเคฐเฅเคฎเคพเคฃ เคฐเฅ‡เค–เคพ + .menu-arc-length = เคšเคพเคช เคฒเค‚เคฌเคพเคˆ + .command-line-opacity = เค•เคฎเคพเค‚เคก เคฒเคพเค‡เคจ เค…เคชเคพเคฐเคฆเคฐเฅเคถเคฟเคคเคพ + .txtexp-no-text-selected = TXTEXP: เค•เฅ‹เคˆ เคŸเฅ‡เค•เฅเคธเฅเคŸ เคšเคฏเคจเคฟเคค เคจเคนเฅ€เค‚เฅค + .angle-override-shown = เค•เฅ‹เคฃ เค“เคตเคฐเคฐเคพเค‡เคก: __ocs_fmt_0__ .plot-preview-task = เคชเฅเคฒเฅ‰เคŸ เคชเฅ‚เคฐเฅเคตเคพเคตเคฒเฅ‹เค•เคจ .link-a-csv-data-source = CSV เคกเฅ‡เคŸเคพ เคธเฅเคฐเฅ‹เคค เคฒเคฟเค‚เค• เค•เคฐเฅ‡เค‚ .csv-files = CSV เคซเคผเคพเค‡เคฒเฅ‡เค‚ .browser-cannot-open-typed-path = เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เคŸเคพเค‡เคช เค•เคฟเค เค—เค เคชเคฅ เค•เฅ‹ เคจเคนเฅ€เค‚ เค–เฅ‹เคฒ เคธเค•เคคเคพ; เค‡เคธเค•เฅ‡ เคฌเคœเคพเคฏ เคซเคผเคพเค‡เคฒ เคšเฅเคจเฅ‡เค‚เฅค .recover-drawing = เคšเคฟเคคเฅเคฐ เคชเฅเคจเคฐเฅเคชเฅเคฐเคพเคชเฅเคค เค•เคฐเฅ‡เค‚ + .requires-0-or-1-or-keyword = เค•เฅ‡เคตเคฒ 0 เคฏเคพ 1, เคฏเคพ เคตเคฟเค•เคฒเฅเคช เค•เฅ€เคตเคฐเฅเคก เค†เคตเคถเฅเคฏเค• เคนเฅˆเฅค + .requires-a-number = เคเค• เคธเค‚เค–เฅเคฏเคพ เค†เคตเคถเฅเคฏเค• เคนเฅˆเฅค .restore-it-as-a-recovered-tab-or-discard-the-copy = เค‡เคธเฅ‡ เคชเฅเคจเคฐเฅเคชเฅเคฐเคพเคชเฅเคค เคŸเฅˆเคฌ เค•เฅ‡ เคฐเฅ‚เคช เคฎเฅ‡เค‚ เคชเฅเคจเคฐเฅเคธเฅเคฅเคพเคชเคฟเคค เค•เคฐเฅ‡เค‚, เคฏเคพ เคชเฅเคฐเคคเคฟเคฒเคฟเคชเคฟ เคคเฅเคฏเคพเค— เคฆเฅ‡เค‚เฅค .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" เค•เฅ‡ เคชเคพเคธ เค…เค‚เคคเคฟเคฎ เคธเคนเฅ‡เคœเคจเฅ‡ เคธเฅ‡ เคจเคˆ เค‘เคŸเฅ‹เคธเฅ‡เคต เคชเฅเคฐเคคเคฟเคฒเคฟเคชเคฟ เคนเฅˆเฅค .autosave-copy-could-not-be-stored = เค‘เคŸเฅ‹เคธเฅ‡เคต เคชเฅเคฐเคคเคฟเคฒเคฟเคชเคฟ เคธเค‚เค—เฅเคฐเคนเคฟเคค เคจเคนเฅ€เค‚ เค•เฅ€ เคœเคพ เคธเค•เฅ€: __ocs_fmt_0__ @@ -810,6 +884,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: เคเค• เคธเฅ‡เค•เฅเคถเคจ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคšเฅเคจเฅ‡เค‚ เคฏเคพ เคฒเคพเค‡เคต เคธเฅ‡เค•เฅเคถเคจ เคšเคพเคฒเฅ‚ เค•เคฐเฅ‡เค‚เฅค + .sectioncull-prepare-failed = SECTIONCULL: เค•เคŸเคพ เคนเฅเค† เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคคเฅˆเคฏเคพเคฐ เคจเคนเฅ€เค‚ เคนเฅ‹ เคธเค•เคพ; เค•เฅเค› เคจเคนเฅ€เค‚ เคฌเคฆเคฒเคพ เค—เคฏเคพเฅค + .sectioncull-nothing-outside = SECTIONCULL: เคธเฅ‡เค•เฅเคถเคจ เค•เฅ‡ เคฌเคพเคนเคฐ เค•เฅเค› เคจเคนเฅ€เค‚ เคนเฅˆเฅค + .sectioncull-result = SECTIONCULL: __ocs_arg_erased__ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคฎเคฟเคŸเคพเค เค—เค เค”เคฐ เคธเฅ‡เค•เฅเคถเคจ เค•เฅ€ เคธเฅ€เคฎเคพ เคชเคฐ __ocs_arg_cut__ เค•เคพเคŸเฅ‡ เค—เคเฅค .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ เค…เค‚เค• เค•เฅ‡ เคฎเคพเคงเฅเคฏเคฎ เคธเฅ‡ เคเค• เคธเฅเคชเฅˆเคจ เคซเคฟเคŸเฅค .splinefit-need-at-least-3-points = SPLINEFIT: เค•เคฎ เคธเฅ‡ เค•เคฎ 3 เค…เค‚เค• เค•เฅ€ เค†เคตเคถเฅเคฏเค•เคคเคพ เคนเฅˆเฅค .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: เคเค• sline เค•เฅ‡ เคฎเคพเคงเฅเคฏเคฎ เคธเฅ‡ เคซเคฟเคŸ เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคเค• เคชเฅ‰เคฒเฅ€เคฒเคพเค‡เคจ เค•เคพ เคšเคฏเคจ เค•เคฐเฅ‡เค‚เฅค @@ -847,9 +925,12 @@ common = .sent-to-printer-printer = เคชเฅเคฐเคฟเค‚เคŸเคฐ เค•เฅ‡ เคฒเคฟเค เคญเฅ‡เคœเคพ เค—เคฏเคพ: __ocs_fmt_0__ .set-current = เคตเคฐเฅเคคเคฎเคพเคจ เคธเฅ‡เคŸ เค•เคฐเฅ‡เค‚ .shade-plot = เค›เคพเคฏเคพ เค•เคฅเคพ + .shaded-with-edges = เค•เคฟเคจเคพเคฐเฅ‹เค‚ เคธเคนเคฟเคค เค›เคพเคฏเคพเค‚เค•เคฟเคค + .shades-of-gray = เค—เฅเคฐเฅ‡ เค•เฅ‡ เคถเฅ‡เคก .shadow-flags = เค›เคพเคฏเคพ เคเค‚เคกเฅ‡ .single = เคเค•เคฒ .size = เค†เค•เคพเคฐ + .sketchy = เคฐเฅ‡เค–เคพเคšเคฟเคคเฅเคฐ .snap-override-label-next-pick-only = เคธเฅเคจเฅˆเคช เค“เคตเคฐเคฐเคพเค‡เคก: __ocs_fmt_0__ (เค…เค•เฅ‡เคฒเฅ‡ เค•เฅ‡เคตเคฒ เคชเคฟเค•)เฅค .solid-created = เค เฅ‹เคธ เคฌเคจเคพเคฏเคพ เค—เคฏเคพเฅค .source-object-not-found = เคธเฅเคฐเฅ‹เคค เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคจเคนเฅ€เค‚ เคฎเคฟเคฒเคพเฅค @@ -1055,7 +1136,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: Left, Top เคฏเคพ Right เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚เฅค .isodraft-expected-on-or-off = ISODRAFT: On เคฏเคพ Off เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚เฅค .xdata-selected-entities-are-on-locked-layers = XDATA: เคšเคฏเคจเคฟเคค เค‡เค•เคพเค‡เคฏเคพเค เคฒเฅ‰เค• เคฒเฅ‡เคฏเคฐ เคชเคฐ เคนเฅˆเค‚เฅค - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: เค•เฅ‹เคˆ เคฐเคพเคธเฅเคŸเคฐ เค›เคตเคฟ เคšเคฏเคจเคฟเคค เคจเคนเฅ€เค‚ เคนเฅˆ เคฏเคพ เค—เฅเคฃ เค…เคœเฅเคžเคพเคค เคนเฅˆ (BRIGHTNESS/CONTRAST/FADE เค‰เคชเคฏเฅ‹เค— เค•เคฐเฅ‡เค‚)เฅค .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: -4 เคธเฅ‡ 4 เคคเค• เคชเฅ‚เคฐเฅเคฃเคพเค‚เค• เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚เฅค .usage-datalink-path-to-csv = เค‰เคชเคฏเฅ‹เค—: DATALINK <.csv-เค•เคพ-เคชเคฅ> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: เค•เฅ‹เคˆ เคฒเคฟเค‚เค• เค•เฅ€ เค—เคˆ เคคเคพเคฒเคฟเค•เคพ เคจเคนเฅ€เค‚ เคฎเคฟเคฒเฅ€เฅค @@ -2048,7 +2128,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = เคซเคผเคพเค‡เคฒ .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2357,6 +2437,7 @@ common = .load-linetypes = เคฒเคพเค‡เคจเคŸเคพเค‡เคช เคฒเฅ‹เคก เค•เคฐเฅ‡เค‚ .requires-two-positive-values = เคฆเฅ‹ เคงเคจเคพเคคเฅเคฎเค• เคฎเคพเคจ (x,y) เคฏเคพ เคฆเฅ‹เคจเฅ‹เค‚ เค•เฅ‡ เคฒเคฟเค เคเค• เคฎเคพเคจ เค†เคตเคถเฅเคฏเค• เคนเฅˆเฅค .snap-marker-color = เคธเฅเคจเฅˆเคช เคฎเคพเคฐเฅเค•เคฐ เค•เคพ เคฐเค‚เค— + .snap-marker-size = เคธเฅเคจเฅˆเคช เคฎเคพเคฐเฅเค•เคฐ เค•เคพ เค†เค•เคพเคฐ .command-line-text-color = เค•เคฎเคพเค‚เคก เคฒเคพเค‡เคจ เคŸเฅ‡เค•เฅเคธเฅเคŸ เคฐเค‚เค— .operand = เค‘เคชเคฐเฅ‡เค‚เคก .linetypes-loaded = __ocs_fmt_0__ เคฒเคพเค‡เคจเคŸเคพเค‡เคช เคฒเฅ‹เคก เคนเฅเคเฅค @@ -2370,6 +2451,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE เค†เคงเคพเคฐ เคฌเคฟเค‚เคฆเฅ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚ [__ocs_arg_count__ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ]: .threedrotate-specify-rotation-axis = 3DROTATE เค˜เฅ‚เคฐเฅเคฃเคจ เค…เค•เฅเคท [X/Y/Z] เคฏเคพ เค‰เคธ เคชเคฐ เคฆเฅ‚เคธเคฐเคพ เคฌเคฟเค‚เคฆเฅ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚ : .threedrotate-specify-rotation-angle-a = 3DROTATE เค˜เฅ‚เคฐเฅเคฃเคจ เค•เฅ‹เคฃ เค•เฅ‹ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚ <__ocs_arg_a__: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = เคšเฅ‡เคคเคพเคตเคจเฅ€ เคธเคนเฅ‡เคœเฅ‡เค‚ .unable-to-save-drawing = เคกเฅเคฐเฅ‰เค‡เค‚เค— เคธเคนเฅ‡เคœเฅ€ เคจเคนเฅ€เค‚ เคœเคพ เคธเค•เฅ€ @@ -2993,6 +3081,7 @@ draw = .arc-specify-radius-of-arc = ARC เคšเคพเคช เค•เฅ€ เคคเฅเคฐเคฟเคœเฅเคฏเคพ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚: .chord-length = เคœเฅ€เคตเคพ เค•เฅ€ เคฒเค‚เคฌเคพเคˆ + modify = .add-vertex = เคถเฅ€เคฐเฅเคท เคœเฅ‹เคกเคผเฅ‡เค‚ .closed-cross = เคฌเค‚เคฆ เค•เฅเคฐเฅ‰เคธ @@ -3038,6 +3127,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT เคตเคฟเคถเฅ‡เคทเคคเคพเค“เค‚ เค•เฅ‡ เคธเคพเคฅ เคฌเฅเคฒเฅ‰เค• เค•เคพ เคšเคฏเคจ เค•เคฐเฅ‡เค‚: .annotation-object-scale = Annotation เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคธเฅเค•เฅ‡เคฒ .bedit-select-block-reference-to-edit = BEDIT เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคฌเฅเคฒเฅ‰เค• เคธเค‚เคฆเคฐเฅเคญ เค•เคพ เคšเคฏเคจ เค•เคฐเฅ‡เค‚: + .bvstate-option = BVSTATE เคตเคฟเค•เคฒเฅเคช เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [เคจเคฏเคพ/เคธเฅ‡เคŸ/เคจเคพเคฎ เคฌเคฆเคฒเฅ‡เค‚/เคนเคŸเคพเคเค] <เคธเฅ‡เคŸ>: + .bvstate-new-name = BVSTATE เคจเคˆ เคฆเฅƒเคถเฅเคฏเคคเคพ เคธเฅเคฅเคฟเคคเคฟ เค•เคพ เคจเคพเคฎ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚: + .bvstate-set = BVSTATE เคตเคฐเฅเคคเคฎเคพเคจ เคฌเคจเคพเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคฆเฅƒเคถเฅเคฏเคคเคพ เคธเฅเคฅเคฟเคคเคฟ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE เคจเคพเคฎ เคฌเคฆเคฒเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคฆเฅƒเคถเฅเคฏเคคเคพ เคธเฅเคฅเคฟเคคเคฟ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE เคจเคฏเคพ เคจเคพเคฎ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚: + .bvstate-delete = BVSTATE เคนเคŸเคพเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคฆเฅƒเคถเฅเคฏเคคเคพ เคธเฅเคฅเคฟเคคเคฟ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚: + .bvstate-not-found = เคฆเฅƒเคถเฅเคฏเคคเคพ เคธเฅเคฅเคฟเคคเคฟ "__ocs_fmt_0__" เคจเคนเฅ€เค‚ เคฎเคฟเคฒเฅ€เฅค + .bvstate-exists = เคฆเฅƒเคถเฅเคฏเคคเคพ เคธเฅเคฅเคฟเคคเคฟ "__ocs_fmt_0__" เคชเคนเคฒเฅ‡ เคธเฅ‡ เคฎเฅŒเคœเฅ‚เคฆ เคนเฅˆเฅค + .bvstate-last = เคเค•เคฎเคพเคคเฅเคฐ เคฆเฅƒเคถเฅเคฏเคคเคพ เคธเฅเคฅเคฟเคคเคฟ เคนเคŸเคพเคˆ เคจเคนเฅ€เค‚ เคœเคพ เคธเค•เคคเฅ€เฅค + .bvstate-needs-bedit = __ocs_fmt_0__: เคชเคนเคฒเฅ‡ เคฌเฅเคฒเฅ‰เค• เค•เฅ‹ เคฌเฅเคฒเฅ‰เค• เคเคกเคฟเคŸเคฐ เคฎเฅ‡เค‚ เค–เฅ‹เคฒเฅ‡เค‚ (BEDIT)เฅค + .bvstate-current = เคตเคฐเฅเคคเคฎเคพเคจ เคฆเฅƒเคถเฅเคฏเคคเคพ เคธเฅเคฅเคฟเคคเคฟ: __ocs_fmt_0__ + .bvhide-select = เค›เคฟเคชเคพเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคšเฅเคจเฅ‡เค‚: + .bvshow-select = เคฆเคฟเค–เคพเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคšเฅเคจเฅ‡เค‚: + .arrayedit-select-array = ARRAYEDIT เคเคฐเฅ‡ เคšเฅเคจเฅ‡เค‚: + .arrayedit-options = ARRAYEDIT เคตเคฟเค•เคฒเฅเคช เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [เคธเฅเคฐเฅ‹เคค/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: เคšเฅเคจเคพ เค—เคฏเคพ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคธเคนเคฏเฅ‹เค—เฅ€ เคเคฐเฅ‡ เคจเคนเฅ€เค‚ เคนเฅˆเฅค .blend-enter-continuity-tangent-curvature-c = BLEND เคจเคฟเคฐเค‚เคคเคฐเคคเคพ เคšเฅเคจเฅ‡เค‚ [เคธเฅเคชเคฐเฅเคถเคฐเฅ‡เค–เคพ/เคตเค•เฅเคฐเคคเคพ] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND เคชเฅเคฐเคฅเคฎ เค–เฅเคฒเฅ‡ เคตเค•เฅเคฐ เค•เคพ เคšเคฏเคจ เค•เคฐเฅ‡เค‚ [Continuity=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND เคฆเฅ‚เคธเคฐเคพ เค–เฅเคฒเคพ เคตเค•เฅเคฐ เคšเฅเคจเฅ‡เค‚ [Continuity=__ocs_arg_c__]: @@ -3590,6 +3695,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เคธเคฎเฅเคฎเคฟเคฒเคจ เคฌเคฟเค‚เคฆเฅ: insert = + .pastespec-specify-insertion-point = PASTESPEC เคธเคฎเฅเคฎเคฟเคฒเคจ เคฌเคฟเค‚เคฆเฅ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚: + .pastespec-paste-as = PASTESPEC เค‡เคธ เคฐเฅ‚เคช เคฎเฅ‡เค‚ เคšเคฟเคชเค•เคพเคเค [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3706,6 +3813,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3760,6 +3874,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *เค…เค‚เคกเคฐเคฒเฅ‡ เคธเฅเคจเฅˆเคช เคญเคฟเคจเฅเคจ เคนเฅˆเค‚* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3899,6 +4014,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = เค›เคตเคฟ เคฏเคพ เค…เค‚เคกเคฐเคฒเฅ‡ เคšเฅเคจเฅ‡เค‚: + .adjust-select-pdf-underlay = PDF เค…เค‚เคกเคฐเคฒเฅ‡ เคšเฅเคจเฅ‡เค‚: + .adjust-select-dwf-underlay = DWF เค…เค‚เคกเคฐเคฒเฅ‡ เคšเฅเคจเฅ‡เค‚: + .adjust-select-dgn-underlay = DGN เค…เค‚เคกเคฐเคฒเฅ‡ เคšเฅเคจเฅ‡เค‚: + .adjust-mixed-underlays = *เค…เคฎเคพเคจเฅเคฏ เคšเคฏเคจ เคธเฅ‡เคŸเฅค เค•เฅ‡เคตเคฒ เคเค• เคนเฅ€ เคซเคผเคพเค‡เคฒ เคชเฅเคฐเค•เคพเคฐ เค•เฅ‡ เค…เค‚เคกเคฐเคฒเฅ‡ เคšเฅเคจเฅ‡เค‚เฅค* + .adjust-image-option = เค›เคตเคฟ เคตเคฟเค•เคฒเฅเคช เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [เค•เค‚เคŸเฅเคฐเคพเคธเฅเคŸ/เคซเฅ‡เคก/เคšเคฎเค•] <เคšเคฎเค•>: + .adjust-underlay-option = __ocs_arg_kind__ เค…เค‚เคกเคฐเคฒเฅ‡ เคตเคฟเค•เคฒเฅเคช เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [เคซเฅ‡เคก/เค•เค‚เคŸเฅเคฐเคพเคธเฅเคŸ/เคฎเฅ‹เคจเฅ‹เค•เฅเคฐเฅ‹เคฎ] <__ocs_arg_option__>: + .adjust-monochrome = เคฎเฅ‹เคจเฅ‹เค•เฅเคฐเฅ‹เคฎ? [เคนเคพเค/เคจเคนเฅ€เค‚] <__ocs_arg_value__>: + .adjust-brightness-value = เคšเคฎเค• เคฎเคพเคจ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = เค•เค‚เคŸเฅเคฐเคพเคธเฅเคŸ เคฎเคพเคจ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ (0-100) <__ocs_arg_value__>: + .adjust-fade-value = เคซเฅ‡เคก เคฎเคพเคจ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = เคฎเคพเคจ 0 เค”เคฐ 100 เค•เฅ‡ เคฌเฅ€เคš เค•เคพ เคชเฅ‚เคฐเฅเคฃเคพเค‚เค• เคนเฅ‹เคจเคพ เคšเคพเคนเคฟเคเฅค .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4403,7 +4530,27 @@ model = .value-2point = 2-เคชเฅ‰เค‡เค‚เคŸ .cube = เค˜เคจ + layout = + .mvsetup-select-objects-to-delete = MVSETUP เคนเคŸเคพเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคšเฅเคจเฅ‡เค‚: + .delete-objects-option = เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคนเคŸเคพเคเค + .mvsetup-enable-paper-space = MVSETUP เคชเฅ‡เคชเคฐ เคธเฅเคชเฅ‡เคธ เคธเค•เฅเคทเคฎ เค•เคฐเฅ‡เค‚? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP เคธเฅเค•เฅ‡เคฒ เคซเคผเฅˆเค•เฅเคŸเคฐ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ <1>: + .mvsetup-enter-paper-width = MVSETUP เคชเฅ‡เคชเคฐ เค•เฅ€ เคšเฅŒเคกเคผเคพเคˆ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚: + .mvsetup-enter-paper-height = MVSETUP เคชเฅ‡เคชเคฐ เค•เฅ€ เคŠเคเคšเคพเคˆ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚: + .mvsetup-enter-option = MVSETUP เคตเคฟเค•เคฒเฅเคช เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ เคฒเฅ‡เค†เค‰เคŸ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ เค•เฅเคทเฅ‡เคคเฅเคฐ เค•เคพ เคชเคนเคฒเคพ เค•เฅ‹เคจเคพ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚ : + .mvsetup-specify-opposite-corner = MVSETUP เคตเคฟเคชเคฐเฅ€เคค เค•เฅ‹เคจเคพ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚: + .mvsetup-viewports-in-x = MVSETUP X เคฆเคฟเคถเคพ เคฎเฅ‡เค‚ เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ เค•เฅ€ เคธเค‚เค–เฅเคฏเคพ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ <2>: + .mvsetup-viewports-in-y = MVSETUP Y เคฆเคฟเคถเคพ เคฎเฅ‡เค‚ เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ เค•เฅ€ เคธเค‚เค–เฅเคฏเคพ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ <2>: + .mvsetup-distance-in-x = MVSETUP X เคฆเคฟเคถเคพ เคฎเฅ‡เค‚ เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ เค•เฅ‡ เคฌเฅ€เคš เคฆเฅ‚เคฐเฅ€ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚ <0>: + .mvsetup-distance-in-y = MVSETUP Y เคฆเคฟเคถเคพ เคฎเฅ‡เค‚ เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ เค•เฅ‡ เคฌเฅ€เคš เคฆเฅ‚เคฐเฅ€ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚ <0>: + .mvsetup-select-viewports = MVSETUP เคธเฅเค•เฅ‡เคฒ เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ เคšเฅเคจเฅ‡เค‚: + .mvsetup-paper-space-units = MVSETUP เคชเฅ‡เคชเคฐ เคธเฅเคชเฅ‡เคธ เค‡เค•เคพเค‡เคฏเฅ‹เค‚ เค•เฅ€ เคธเค‚เค–เฅเคฏเคพ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ <1>: + .mvsetup-model-space-units = MVSETUP เคฎเฅ‰เคกเคฒ เคธเฅเคชเฅ‡เคธ เค‡เค•เคพเค‡เคฏเฅ‹เค‚ เค•เฅ€ เคธเค‚เค–เฅเคฏเคพ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ <1>: + .mvsetup-viewports-do-not-fit = MVSETUP เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ เค•เฅเคทเฅ‡เคคเฅเคฐ เคฎเฅ‡เค‚ เคซเคฟเคŸ เคจเคนเฅ€เค‚ เคนเฅ‹เคคเฅ‡เฅค + .scale-viewports-option = เคตเฅเคฏเฅ‚เคชเฅ‹เคฐเฅเคŸ เคธเฅเค•เฅ‡เคฒ เค•เคฐเฅ‡เค‚ .active = เคธเค•เฅเคฐเคฟเคฏ .inactive = เคจเคฟเคทเฅเค•เฅเคฐเคฟเคฏ .insert-view = เคฆเฅƒเคถเฅเคฏ เคกเคพเคฒเฅ‡เค‚ @@ -4429,6 +4576,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F <เคฒเฅ‡เคฏเคฐ> = เคซเคผเฅเคฐเฅ€เคœเคผ | T <เคฒเฅ‡เคฏเคฐ> = เคฅเฅ‰ | Enter = เคฌเคพเคนเคฐ เคจเคฟเค•เคฒเฅ‡เค‚ view = + .twist-option = เค˜เฅเคฎเคพเคต + .dview-specify-view-twist-angle = DVIEW เคฆเฅƒเคถเฅเคฏ เค˜เฅเคฎเคพเคต เค•เฅ‹เคฃ เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เค•เคฐเฅ‡เค‚ <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW เคตเคฟเค•เคฒเฅเคช เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [TWist/Undo]: .color-scheme-set-to-system-currently-name = เคฐเค‚เค— เคฏเฅ‹เคœเคจเคพ 'เคธเคฟเคธเฅเคŸเคฎ' เคชเคฐ เคธเฅ‡เคŸ เค•เฅ€ เค—เคˆ (เคตเคฐเฅเคคเคฎเคพเคจ เคฎเฅ‡เค‚ __ocs_fmt_0__)เฅค .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = __ocs_fmt_0__ เค”เคฐ __ocs_fmt_1__ เค•เฅ‡ เคฌเฅ€เคš เคเค• เคชเฅ‚เคฐเฅเคฃเคพเค‚เค• เค†เคตเคถเฅเคฏเค• เคนเฅˆเฅค @@ -4623,13 +4773,6 @@ view = .x-no-xdata = __ocs_fmt_0__: เค•เฅ‹เคˆ Xdata เคจเคนเฅ€เค‚เฅค .value-3d-orbit = 3D เค•เค•เฅเคทเคพ .value-3d-orbit-drag-with-right-mouse-button = 3D Orbit: เคฆเคพเคนเคฟเคจเฅ‡ เคฎเคพเค‰เคธ เคฌเคŸเคจ เค•เฅ‡ เคธเคพเคฅ เค–เฅ€เค‚เคšเฅ‡เค‚เฅค - .adjust-brightness-contrast-fade = ADJUST [เคšเคฎเค• / เค•เฅ‰เคจเฅเคŸเฅเคฐเคพเคธเฅเคŸ / เคซเฅ‡เคก]: - .adjust-brightness-0-100 = ADJUST เคšเคฎเค• 0-100: - .adjust-contrast-0-100 = ADJUST เคตเคฟเคชเคฐเฅ€เคค 0-100: - .adjust-fade-0-100 = ADJUST เคซเฅ€เค•เคพ 0-100: - .adjust-select-raster-image-s-first = ADJUST: เคชเคนเคฒเฅ‡ เคฐเคพเคธเฅเคŸเคฐ เค›เคตเคฟ เค•เคพ เคšเคฏเคจ เค•เคฐเฅ‡เค‚เฅค - .adjust-value-must-be-0-100 = ADJUST: เคฎเคพเคจ 0-100 เคนเฅ‹เคจเคพ เคšเคพเคนเคฟเคเฅค - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ เคชเคฐ __ocs_fmt_2__ เค›เคตเคฟ(s). .annoallvisible-new-value-0-1 = ANNOALLVISIBLE เคจเคฏเคพ เคฎเคพเคจ [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: 0 เคฏเคพ 1 เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚เฅค .annoautoscale-new-value-4-4 = ANNOAUTOSCALE เคจเคฏเคพ เคฎเฅ‚เคฒเฅเคฏ [-4..4]: @@ -4755,7 +4898,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Underlay __ocs_fmt_0__: frequ = __ocs_fmt_1__, เคตเคฟเคชเคฐเฅ€เคค = __ocs_fmt_2__, on=__ocs_fmt_3__, เค•เฅเคฒเคฟเคช =__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = เค…เคฆเฅเคฏเคคเคจ __ocs_fmt_0__ underlay(s)). - .usage-adjust-brightness-contrast-fade-0-100 = เค‰เคชเคฏเฅ‹เค—: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = เค‰เคชเคฏเฅ‹เค—: ANNOSCALE e.g. 1:50, 2:1, or a factor .usage-draworder-f-front-b-back-a-above-handle = เค‰เคชเคฏเฅ‹เค—: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = เค‰เคชเคฏเฅ‹เค—: HYPERLINK (select objects first) @@ -5983,6 +6125,46 @@ ui = .osnap-settings = เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคธเฅเคจเฅˆเคช เคธเฅ‡เคŸเคฟเค‚เค—เฅเคธ... .snap-override-none-next-pick-only = เคธเฅเคจเฅˆเคช เค“เคตเคฐเคฐเคพเค‡เคก: เค•เฅ‹เคˆ เคจเคนเฅ€เค‚ (เค•เฅ‡เคตเคฒ เค…เค—เคฒเฅ‡ เคšเคฏเคจ เค•เฅ‡ เคฒเคฟเค)เฅค + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = เคชเคพเค  เค•เฅ‹ เคนเคฎเฅ‡เคถเคพ เคตเคฟเคธเฅเคคเคพเคฐ เคฐเฅ‡เค–เคพเค“เค‚ เค•เฅ‡ เคฌเฅ€เคš เคฐเค–เฅ‡เค‚ .angular-dimensions = เค•เฅ‹เคฃเฅ€เคฏ เคฎเคพเคช @@ -6250,3 +6432,117 @@ graphic-attributes = .reset-angle = เค•เฅ‹เคฃ เคฐเฅ€เคธเฅ‡เคŸ เค•เคฐเฅ‡เค‚ .one-color = เคเค• เคฐเค‚เค— .two-colors = เคฆเฅ‹ เคฐเค‚เค— + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = เค•เคพเคฐเฅเคฏ เคธเฅ‡เคŸ เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเฅ‡เค‚ + .remove-from-working-set = เค•เคพเคฐเฅเคฏ เคธเฅ‡เคŸ เคธเฅ‡ เคนเคŸเคพเคเค + .toolbar-objects = เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ + .dialog-title = เคธเค‚เคฆเคฐเฅเคญ เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐเฅ‡เค‚ + .dialog-reference = เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคธเค‚เคฆเคฐเฅเคญ + .dialog-chain-hint = เคšเฅเคจเฅ‡ เค—เค เคฌเคฟเค‚เคฆเฅ เคชเคฐ เคถเฅเคฐเฅƒเค‚เค–เคฒเคพ; เคธเฅเคคเคฐ เคฌเคฆเคฒเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคšเคฟเคช เคชเคฐ เค•เฅเคฒเคฟเค• เค•เคฐเฅ‡เค‚เฅค + .dialog-path = เคชเคฅ: + .dialog-working-set = เค•เคพเคฐเฅเคฏ เคธเฅ‡เคŸ + .dialog-all-objects = เคธเคญเฅ€ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ + .dialog-pick-objects = เคกเฅเคฐเฅ‰เค‡เค‚เค— เคฎเฅ‡เค‚ เคšเฅเคจเฅ‡เค‚ + .dialog-attributes = เคเคŸเฅเคฐเคฟเคฌเฅเคฏเฅ‚เคŸ เคชเคฐเคฟเคญเคพเคทเคพเคเค เคญเฅ€ เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐเฅ‡เค‚ + .dialog-session = เคธเคคเฅเคฐ + .dialog-lock = เค•เคพเคฐเฅเคฏ เคธเฅ‡เคŸ เค•เฅ‡ เคฌเคพเคนเคฐ เค•เฅ‡ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคฒเฅ‰เค• เค•เคฐเฅ‡เค‚ + .dialog-unique = เคŸเค•เคฐเคพเคจเฅ‡ เคตเคพเคฒเฅ‡ เคฒเฅ‡เคฏเคฐ, เคถเฅˆเคฒเฅ€ เค”เคฐ เคฌเฅเคฒเฅ‰เค• เคจเคพเคฎเฅ‹เค‚ เค•เฅ‹ เค…เคฆเฅเคตเคฟเคคเฅ€เคฏ เคจเคพเคฎ เคฆเฅ‡เค‚ + .dialog-start = เคธเค‚เคชเคพเคฆเคจ เคถเฅเคฐเฅ‚ เค•เคฐเฅ‡เค‚ + .dialog-help = เคฌเฅเคฒเฅ‰เค• เคธเค‚เคฆเคฐเฅเคญ เคฏเคพ เคฌเคพเคนเคฐเฅ€ เคธเค‚เคฆเคฐเฅเคญ เค•เฅ‹ เค‰เคธเฅ€ เคธเฅเคฅเคพเคจ เคชเคฐ เคธเค‚เคชเคพเคฆเคจ เค•เฅ‡ เคฒเคฟเค เค–เฅ‹เคฒเคคเคพ เคนเฅˆเฅค + .select-reference = เคธเค‚เคฆเคฐเฅเคญ เคšเฅเคจเฅ‡เค‚: + .reference-not-found = เคธเค‚เคฆเคฐเฅเคญ เคจเคนเฅ€เค‚ เคฎเคฟเคฒเคพเฅค + .not-valid-reference = เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคฎเคพเคจเฅเคฏ เคธเค‚เคฆเคฐเฅเคญ เคจเคนเฅ€เค‚ เคนเฅˆ + .one-entity-added = 1 เคเค‚เคŸเคฟเคŸเฅ€ เคœเฅ‹เคกเคผเฅ€ เค—เคˆ + .nesting-level = เคจเฅ‡เคธเฅเคŸเคฟเค‚เค— เคธเฅเคคเคฐ เคšเฅเคจเฅ‡เค‚ [เค เฅ€เค•/เค…เค—เคฒเคพ] <เค…เค—เคฒเคพ>: + .selection-method = เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคšเคฏเคจ เคตเคฟเคงเคฟ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [เคธเคญเฅ€/เคจเฅ‡เคธเฅเคŸเฅ‡เคก] <เคธเคญเฅ€>: + .select-nested = เคจเฅ‡เคธเฅเคŸเฅ‡เคก เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคšเฅเคจเฅ‡เค‚: + .display-attdefs = เคเคŸเฅเคฐเคฟเคฌเฅเคฏเฅ‚เคŸ เคชเคฐเคฟเคญเคพเคทเคพเคเค เคฆเคฟเค–เคพเคเค [เคนเคพเค/เคจเคนเฅ€เค‚] <เคจเคนเฅ€เค‚>: + .zero-items-selected = 0 เค†เค‡เคŸเคฎ เคšเฅเคจเฅ‡ เค—เค + .one-item-selected = 1 เค†เค‡เคŸเคฎ เคšเฅเคจเคพ เค—เคฏเคพ + .items-selected = __ocs_fmt_0__ เค†เค‡เคŸเคฎ เคšเฅเคจเฅ‡ เค—เค + .refset-option = เคตเคฟเค•เคฒเฅเคช เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [เคœเฅ‹เคกเคผเฅ‡เค‚/เคนเคŸเคพเคเค] <เคœเฅ‹เคกเคผเฅ‡เค‚>: + .refclose-option = เคตเคฟเค•เคฒเฅเคช เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚ [เคธเคนเฅ‡เคœเฅ‡เค‚/เคธเค‚เคฆเคฐเฅเคญ เคชเคฐเคฟเคตเคฐเฅเคคเคจ เค›เฅ‹เคกเคผเฅ‡เค‚] <เคธเคนเฅ‡เคœเฅ‡เค‚>: + .not-checked-out = ** เค•เคฎเคพเค‚เคก เค•เฅ€ เค…เคจเฅเคฎเคคเคฟ เคจเคนเฅ€เค‚ เคนเฅˆ เคœเคฌ เคคเค• เค•เฅ‹เคˆ เคธเค‚เคฆเคฐเฅเคญ REFEDIT เคธเฅ‡ เคธเค‚เคชเคพเคฆเคจ เคฎเฅ‡เค‚ เคจ เคนเฅ‹ ** + .already-checked-out = ** เค•เคฎเคพเค‚เคก เค•เฅ€ เค…เคจเฅเคฎเคคเคฟ เคจเคนเฅ€เค‚ เคนเฅˆ, __ocs_fmt_0__ เคชเคนเคฒเฅ‡ เคธเฅ‡ เคธเค‚เคชเคพเคฆเคจ เคฎเฅ‡เค‚ เคนเฅˆ ** + .transfer-objects = RefEdit เค•เคพเคฐเฅเคฏ เคธเฅ‡เคŸ เค”เคฐ เคนเฅ‹เคธเฅเคŸ เคกเฅเคฐเฅ‰เค‡เค‚เค— เค•เฅ‡ เคฌเฅ€เคš เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ เคธเฅเคฅเคพเคจเคพเค‚เคคเคฐเคฟเคค เค•เคฐเฅ‡เค‚... + .added-to-working-set = __ocs_fmt_0__ เค•เคพเคฐเฅเคฏ เคธเฅ‡เคŸ เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเฅ‡ เค—เค + .removed-from-working-set = __ocs_fmt_0__ เค•เคพเคฐเฅเคฏ เคธเฅ‡เคŸ เคธเฅ‡ เคนเคŸเคพเค เค—เค + .one-object-added = 1 เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ __ocs_fmt_0__ เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเคพ เค—เคฏเคพ + .objects-added = __ocs_fmt_0__ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ __ocs_fmt_1__ เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเฅ‡ เค—เค + .one-object-removed = 1 เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ __ocs_fmt_0__ เคธเฅ‡ เคนเคŸเคพเคฏเคพ เค—เคฏเคพ + .objects-removed = __ocs_fmt_0__ เค‘เคฌเฅเคœเฅ‡เค•เฅเคŸ __ocs_fmt_1__ เคธเฅ‡ เคนเคŸเคพเค เค—เค + .one-xref-instance = 1 xref เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เค…เคชเคกเฅ‡เคŸ เค•เคฟเคฏเคพ เค—เคฏเคพ + .xref-instances = __ocs_fmt_0__ xref เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เค…เคชเคกเฅ‡เคŸ เค•เคฟเค เค—เค + .one-block-instance = 1 เคฌเฅเคฒเฅ‰เค• เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เค…เคชเคกเฅ‡เคŸ เค•เคฟเคฏเคพ เค—เคฏเคพ + .block-instances = __ocs_fmt_0__ เคฌเฅเคฒเฅ‰เค• เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เค…เคชเคกเฅ‡เคŸ เค•เคฟเค เค—เค + .redefined-reloaded = __ocs_fmt_0__ เคชเฅเคจเคƒ เคชเคฐเคฟเคญเคพเคทเคฟเคค เค”เคฐ เคชเฅเคจเคƒ เคฒเฅ‹เคก เค•เคฟเคฏเคพ เค—เคฏเคพเฅค + .redefined = __ocs_fmt_0__ เคชเฅเคจเคƒ เคชเคฐเคฟเคญเคพเคทเคฟเคค เค•เคฟเคฏเคพ เค—เคฏเคพเฅค + .ok = เค เฅ€เค• + .nested = เคจเฅ‡เคธเฅเคŸเฅ‡เคก + +image-adjust = + .title = เค›เคตเคฟ เคธเคฎเคพเคฏเฅ‹เคœเคจ + .preview = เคชเฅ‚เคฐเฅเคตเคพเคตเคฒเฅ‹เค•เคจ + .before = เคชเคนเคฒเฅ‡ + .after = เคฌเคพเคฆ + .no-preview = เค›เคตเคฟ เคฒเฅ‹เคก เคจเคนเฅ€เค‚ เคนเฅ‹ เคธเค•เฅ€ + .adjust = เคธเคฎเคพเคฏเฅ‹เคœเคจ + .brightness = เคšเคฎเค• + .contrast = เค•เค‚เคŸเฅเคฐเคพเคธเฅเคŸ + .fade = เคซเคผเฅ‡เคก + .hint = เคฌเคพเคฐ เคชเคฐ เค•เฅเคฒเคฟเค• เค•เคฐเฅ‡เค‚ เคฏเคพ เค–เฅ€เค‚เคšเฅ‡เค‚; โˆ’ / + เคเค• เคšเคฐเคฃ เคนเฅˆ, เคฌเฅ‰เค•เฅเคธ 0โ€“100 เค•เคพ เคชเฅ‚เคฐเฅเคฃเคพเค‚เค• เคฒเฅ‡เคคเคพ เคนเฅˆเฅค + .reset = เคกเคฟเคซเคผเฅ‰เคฒเฅเคŸ เคชเฅเคจเคฐเฅเคธเฅเคฅเคพเคชเคฟเคค เค•เคฐเฅ‡เค‚ + .apply = เคฒเคพเค—เฅ‚ เค•เคฐเฅ‡เค‚ + .help = เคšเคฏเคจเคฟเคค เคฐเคพเคธเฅเคŸเคฐ เค›เคตเคฟเคฏเฅ‹เค‚ เค•เฅ€ เคšเคฎเค•, เค•เค‚เคŸเฅเคฐเคพเคธเฅเคŸ เค”เคฐ เคซเคผเฅ‡เคก เคธเฅ‡เคŸ เค•เคฐเคคเคพ เคนเฅˆเฅค + +keytips = + .title = KeyTip + .typed = เคŸเคพเค‡เคช เค•เคฟเคฏเคพ: { $path } + .hint-tabs = เคŸเฅˆเคฌ เค•เคพ เค…เค•เฅเคทเคฐ เคŸเคพเค‡เคช เค•เคฐเฅ‡เค‚ ยท Esc เคฌเค‚เคฆ เค•เคฐเคคเคพ เคนเฅˆ + .hint-tab = { $tab } เคŸเฅˆเคฌ ยท Esc เคเค• เคธเฅเคคเคฐ เคชเฅ€เค›เฅ‡ เคœเคพเคคเคพ เคนเฅˆ + .hint-items = เค†เค‡เคŸเคฎ เค•เคพ เค…เค•เฅเคทเคฐ เคŸเคพเค‡เคช เค•เคฐเฅ‡เค‚ ยท Esc เคเค• เคธเฅเคคเคฐ เคชเฅ€เค›เฅ‡ เคœเคพเคคเคพ เคนเฅˆ diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index cf22bb3fb..772b27ef8 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Bล‘vรญtmรฉnyek ribbon-tab = + .block-editor = Blokkszerkesztล‘ .draw = Rajz .parametric = Parametrikus .annotate = Feliratozรกs @@ -98,6 +99,9 @@ start = .no-recent-files = A megnyitott fรกjlok itt jelennek meg. .browser-storage = Bรถngรฉszล‘ tรกrhelye .keep-recent-files = Legutรณbbi fรกjlok megล‘rzรฉse + .view-list = Lista + .view-icons = Ikonok + .view-preview = Nagy elล‘nรฉzetek donation = .title = Adomรกnykรฉrรฉs @@ -106,6 +110,13 @@ donation = .decline = Nem, kรถszรถnรถm modal = + .missing-fonts-downloading = Letรถltรฉs... + .missing-fonts-download-available = Elรฉrhetล‘k letรถltรฉse + .missing-fonts-skip = Kihagyรกs + .missing-fonts-title = Hiรกnyzรณ betลฑkรฉszletek + .missing-fonts-description = A rajz olyan betลฑkรฉszleteket hasznรกl, amelyek nincsenek telepรญtve ezen a gรฉpen. Az Open CAD Studio letรถltheti a kivรกlasztott tรกrhelyen elรฉrhetล‘, szabadon terjeszthetล‘ betลฑkรฉszleteket. A nem elรฉrhetล‘ betลฑkรฉszleteket tovรกbbra is helyettesรญti. + .missing-fonts-source = Betลฑkรฉszletek forrรกsa โ€“ hagyja รผresen a kรถzรถssรฉgi gyลฑjtemรฉny hasznรกlatรกhoz, vagy adjon meg egy mappรกt (pl. a cรฉges betลฑkรฉszlet-kiszolgรกlรณn): + .missing-fonts-source-placeholder = https://server/fonts vagy GitHub raw mappa .about = Nรฉvjegy .keyboard-shortcuts = Billentyลฑparancsok .command-aliases = Parancsrรถvidรญtรฉsek @@ -273,10 +284,76 @@ command-copy = .mode = COPY Adja meg a mรกsolรกsi mรณdot [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ csak itt (zรถld), __ocs_fmt_1__ csak a mรกsik rajzban (piros)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ร–sszehasonlรญtรกs rajzzal + .label-associative-array-created = __ocs_fmt_0__: asszociatรญv tรถmb lรฉtrehozva. + .assoc-array-array-path = Tรถmb (รบtvonal) + .assoc-array-array-polar = Tรถmb (polรกris) + .assoc-array-array-rectangular = Tรถmb (tรฉglalap) + .assoc-array-item-spacing = Elemtรกvolsรกg + .assoc-array-fill-angle = Kitรถltรฉsi szรถg + .assoc-array-items = Elemek + .assoc-array-level-spacing = Szinttรกvolsรกg + .assoc-array-levels = Szintek + .assoc-array-row-spacing = Sortรกvolsรกg + .assoc-array-column-spacing = Oszloptรกvolsรกg + .fbxexport-exported-to = FBXEXPORT: exportรกlva a kรถvetkezล‘be: "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: a rajz nem tartalmaz 3D hรกlรณadatot. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Az รบj sraffozรกsok kรถvetik hatรกraikat (HPASSOC) + .options-show-ribbon = Szalag megjelenรญtรฉse (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Menรผsor megjelenรญtรฉse (MENUBAR) + .options-window-elements = Ablakelemek + .menu-viewports = Nรฉzetablakok + .menu-view-twist = Nรฉzet elforgatรกsa + .menu-tools = Eszkรถzรถk + .menu-table-style = Tรกblรกzatstรญlus + .menu-run-script = Parancsfรกjl futtatรกsa + .menu-revision-cloud = Revรญziรณfelhล‘ + .menu-regen-all = ร–sszes รบjragenerรกlรกsa + .menu-redraw = รšjrarajzolรกs + .menu-rectangle = Tรฉglalap + .menu-quick-dimension = Gyors mรฉretezรฉs + .menu-publish = Kรถzzรฉtรฉtel + .menu-paste-special = Irรกnyรญtott beillesztรฉs + .menu-parametric = Parametrikus + .menu-page-setup-manager = Oldalbeรกllรญtรกs-kezelล‘ + .menu-orbit = Kรถrbejรกrรกs + .menu-new-ucs = รšj FKR + .menu-named-views = Elnevezett nรฉzetek + .menu-multiline-text = Tรถbbsoros szรถveg + .menu-multiline-style = Multivonalstรญlus + .menu-multiline = Multivonal + .menu-multileader-style = Tรถbbszรถrรถs mutatรณvonal-stรญlus + .menu-match-properties = Tulajdonsรกgok egyeztetรฉse + .menu-lengthen = Hosszabbรญtรกs + .menu-jogged = Tรถrt + .menu-id-point = Pont azonosรญtรกsa + .menu-explode-text = Szรถveg szรฉtvetรฉse + .menu-edit-spline = Spline szerkesztรฉse + .menu-edit-polyline = Vonallรกnc szerkesztรฉse + .menu-edit-hatch = Sraffozรกs szerkesztรฉse + .menu-drawing-limits = Rajzhatรกrok + .menu-donut = Gyลฑrลฑ + .menu-dimension-style = Mรฉretstรญlus + .menu-construction-line = Szerkesztล‘vonal + .menu-arc-length = รvhossz + .command-line-opacity = Parancssor รกtlรกtszatlansรกga + .txtexp-no-text-selected = TXTEXP: nincs kijelรถlt szรถveg. + .angle-override-shown = Szรถg felรผlbรญrรกlรกsa: __ocs_fmt_0__ + .parametric-coincident = Egybeesล‘ + .parametric-colinear = Egy egyenesbe esล‘ + .parametric-concentric = Koncentrikus + .parametric-symmetric = Szimmetrikus + .parametric-equal = Egyenlล‘ + .parametric-fix = Rรถgzรญtรฉs .plot-preview-task = Nyomtatรกsi kรฉp .link-a-csv-data-source = CSV-adatforrรกs csatolรกsa .csv-files = CSV-fรกjlok - .browser-cannot-open-typed-path = A bรถngรฉszล‘ nem nyithat meg beรญrt รบtvonalat; vรกlaszd a fรกjlt. + .browser-cannot-open-typed-path = A bรถngรฉszล‘ben nem nyithatรณ meg kรถzvetlenรผl a megadott fรกjlรบtvonal; vรกlassza ki a fรกjlt. .jogged-radius = Megtรถrt sugรกrmรฉret .none-use-shortcuts-set-key-command = (nincs โ€“ hasznรกlat: SHORTCUTS SET ) .alignment-indented-label-title-case = Igazรญtรกs: @@ -286,6 +363,8 @@ common = .fill-color-indented-label-sentence-case = Kitรถltรฉs szรญne: .format-string = Formรกtumkarakterlรกnc: .off-color-ltype = Ki Szรญn Vonaltรญpus + .requires-0-or-1-or-keyword = Csak 0, 1 vagy egy opciรณkulcsszรณ adhatรณ meg. + .requires-a-number = Szรกmot kell megadni. .text-color-indented-label-sentence-case = Szรถveg szรญne: .text-height-indented-label-sentence-case = Szรถveg magassรกga: .unit-type = Egysรฉg tรญpusa: @@ -821,6 +900,10 @@ common = .slice-result-commit-failed = SLICE: az egyik eredmรฉny nem vรฉglegesรญthetล‘; az รถsszes forrรกsobjektum megmaradt. .slice-retained-update-failed = SLICE: az egyik megtartott eredmรฉny nem frissรญthetล‘. .slice-created-results = SLICE: __ocs_arg_count__ eredmรฉnyobjektum lรฉtrehozva. + .sectioncull-no-section = SECTIONCULL: vรกlasszon metszetobjektumot, vagy kapcsolja be az รฉlล‘ metszetet. + .sectioncull-prepare-failed = SECTIONCULL: egy vรกgott objektumot nem sikerรผlt elล‘kรฉszรญteni; semmi sem vรกltozott. + .sectioncull-nothing-outside = SECTIONCULL: a metszeten kรญvรผl nincs semmi. + .sectioncull-result = SECTIONCULL: __ocs_arg_erased__ objektum tรถrรถlve, __ocs_arg_cut__ levรกgva a metszet hatรกrรกn. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: illesszen be egy spline-t a __ocs_fmt_0__ pontokon. .splinefit-need-at-least-3-points = SPLINEFIT: legalรกbb 3 pont szรผksรฉges. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: vรกlasszon egy vonallรกncot a spline รกtillesztรฉsรฉhez. @@ -858,9 +941,12 @@ common = .sent-to-printer-printer = Nyomtatรณra kรผldve: __ocs_fmt_0__ .set-current = Aktuรกlissรก tรฉtel .shade-plot = รrnyalt nyomtatรกs + .shaded-with-edges = รrnyalt รฉlekkel + .shades-of-gray = Szรผrkeรกrnyalatok .shadow-flags = รrnyรฉkbeรกllรญtรกsok .single = Egyetlen .size = Mรฉret + .sketchy = Vรกzlatos .snap-override-label-next-pick-only = Ideiglenes tรกrgyraszter: __ocs_fmt_0__ (csak a kรถvetkezล‘ pontnรกl). .solid-created = Test lรฉtrehozva. .source-object-not-found = A forrรกsobjektum nem talรกlhatรณ. @@ -1052,7 +1138,7 @@ common = .loft-could-not-create-a-complete-display-the-source-sections-were-preserved = A LOFT eredmรฉnye nem jelenรญthetล‘ meg teljesen. A forrรกskeresztmetszetek megmaradtak. .loft-surface-created = LOFT: felรผlet lรฉtrehozva. .loft-solid-created = LOFT: test lรฉtrehozva. - .attsync-no-editable-block-references = ATTSYNC: nincsenek szerkeszthetล‘ blokkhivatkozรกsok. + .attsync-no-editable-block-references = ATTSYNC: nincsenek szerkeszthetล‘ blokkreferenciรกk. .xref-reloaded-with-repairs = XREF รšjratรถltve javรญtรกsokkal: "__ocs_fmt_0__" .xref-reload-failed = XREF รšjratรถltรฉs sikertelen: "__ocs_fmt_0__" (__ocs_fmt_1__) .zoom-previous = Elล‘zล‘ nรฉzet @@ -1066,7 +1152,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: รญrja be: Left, Top vagy Right. .isodraft-expected-on-or-off = ISODRAFT: รญrja be: On vagy Off. .xdata-selected-entities-are-on-locked-layers = XDATA: a kijelรถlt objektumok zรกrolt fรณliรกkon vannak. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: nincs kijelรถlt raszterkรฉp, vagy ismeretlen a tulajdonsรกg (hasznรกlja: BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: adjon meg egy egรฉsz szรกmot -4 รฉs 4 kรถzรถtt. .usage-datalink-path-to-csv = Hasznรกlat: DATALINK <.csv-elรฉrรฉsi-รบt> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: nem talรกlhatรณ csatolt tรกblรกzat. @@ -2369,6 +2454,7 @@ common = .load-linetypes = Vonaltรญpusok betรถltรฉse .requires-two-positive-values = Kรฉt pozitรญv รฉrtรฉk (x,y) vagy egy kรถzรถs รฉrtรฉk szรผksรฉges. .snap-marker-color = Tรกrgyraszterjel szรญne + .snap-marker-size = Tรกrgyraszterjel mรฉrete .command-line-text-color = Parancssor szรถvegszรญne .operand = Operandus .linetypes-loaded = __ocs_fmt_0__ vonaltรญpus betรถltve. @@ -2382,6 +2468,7 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Alappont megadรกsa [__ocs_arg_count__ objektumok]: .threedrotate-specify-rotation-axis = 3DROTATE Adja meg a forgรกstengelyt [X/Y/Z] vagy egy mรกsodik pontjรกt : .threedrotate-specify-rotation-angle-a = 3DROTATE Forgatรกsi szรถg megadรกsa <__ocs_arg_a__>: + errors = .save-warning = Mentรฉsi figyelmeztetรฉs .unable-to-save-drawing = Nem sikerรผlt menteni a rajzot @@ -2504,9 +2591,9 @@ file = .error-external-change-checks-remain-active = __ocs_fmt_0__. A kรผlsล‘ vรกltozรกsellenล‘rzรฉsek aktรญvak maradnak. .warning-external-change-checks-remain-active = __ocs_fmt_0__. A kรผlsล‘ vรกltozรกsellenล‘rzรฉsek aktรญvak maradnak. .opening-a-drawing-by-path-is-not-available-in = A rajz รบtvonal szerinti megnyitรกsa nem รฉrhetล‘ el a bรถngรฉszล‘ben. - .pick-the-file-again-to-load-its-contents = Vรกlaszd ki รบjra a fรกjlt a tartalmรกnak bรถngรฉszล‘be tรถltรฉsรฉhez. - .adding-folders-is-not-available-in-the-browser = Mappรกk hozzรกadรกsa nem รฉrhetล‘ el a bรถngรฉszล‘ben. Ehelyett rajzfรกjlokat adj hozzรก. - .drawing-not-loaded-in-the-browser = A raj nincs betรถltve a bรถngรฉszล‘ben: __ocs_fmt_0__. + .pick-the-file-again-to-load-its-contents = Vรกlassza ki รบjra a fรกjlt a tartalmรกnak bรถngรฉszล‘be tรถrtรฉnล‘ betรถltรฉsรฉhez. + .adding-folders-is-not-available-in-the-browser = Mappรกk hozzรกadรกsa nem รฉrhetล‘ el a bรถngรฉszล‘ben. Ehelyett adjon hozzรก rajzfรกjlokat. + .drawing-not-loaded-in-the-browser = A rajz nincs betรถltve a bรถngรฉszล‘ben: __ocs_fmt_0__. .image-over-the-decode-limit-cannot-be-attached = A(z) "__ocs_fmt_0__" __ocs_fmt_1__ร—__ocs_fmt_2__ px felbontรกsรบ, รฉs a dekรณdolรกsa __ocs_fmt_3__ MB-ot igรฉnyelne โ€” ez meghaladja e build __ocs_fmt_4__ MB-os kรฉphatรกrรกt, ezรฉrt nem csatolhatรณ. A hatรกron belรผli kรฉpek szรผksรฉg esetรฉn automatikusan csรถkkentett megjelenรญtรฉsi felbontรกssal csatolรณdnak. selection = @@ -2590,7 +2677,7 @@ inquiry = .refedit-a-session-is-already-active-use-refclo = REFEDIT: egy munkamenet mรกr aktรญv. Elล‘szรถr hasznรกlja a REFCLOSE-t. .refedit-block-not-found = REFEDIT: "__ocs_fmt_0__" blokk nem talรกlhatรณ. .refedit-block-is-empty = REFEDIT: a blokk รผres. - .refedit-selected-object-is-not-an-insert = REFEDIT: a kivรกlasztott objektum nem INSERT. + .refedit-selected-object-is-not-an-insert = REFEDIT: a kivรกlasztott objektum nem blokkreferencia. .radius-radius-diameter-diameter = Sugรกr = __ocs_arg_radius__, รกtmรฉrล‘ = __ocs_arg_diameter__ .selectall-object-s-selected = SELECTALL: __ocs_fmt_0__ objektum(ok) kivรกlasztva. .selhandles-no-entities-selected-select-entitie = SELHANDLES: nincsenek kivรกlasztva entitรกsok. Elล‘szรถr vรกlassza ki az entitรกsokat. @@ -2668,8 +2755,8 @@ draw = .attdef-specify-insertion-point-for-tag = ATTDEF Adja meg a '__ocs_arg_tag__' beszรบrรกsi pontjรกt: .attdisp-attribute-display-on-off-normal = ATTDISP attribรบtum kijelzล‘ [On (Be) / Off (Ki) / Normal (Normรกl)]: .attdisp-sub-count-attribute-definition-s-updat = ATTDISP __ocs_fmt_0__: A __ocs_fmt_1__ attribรบtumdefinรญciรณ(k) frissรญtve. - .attedit-no-insert-entities-in-selection = ATTEDIT: a kijelรถlรฉs nem tartalmaz blokkbeszรบrรกst. - .attedit-select-an-insert-entity-first = ATTEDIT: elล‘szรถr vรกlasszon ki egy beszรบrรกsi entitรกst. + .attedit-no-insert-entities-in-selection = ATTEDIT: a kijelรถlรฉs nem tartalmaz blokkreferenciรกt. + .attedit-select-an-insert-entity-first = ATTEDIT: elล‘szรถr vรกlasszon ki egy blokkreferenciรกt. .attedit-tag-tag-up-not-found-in-selection = ATTEDIT: a โ€ž__ocs_fmt_0__โ€ cรญmke nem talรกlhatรณ a kijelรถlรฉsben. .attedit-updated-changed-attribute-s-tag-up-new = ATTEDIT: frissรญtett __ocs_fmt_0__ attribรบtum(ok) [__ocs_fmt_1__] = __ocs_fmt_2__. .arc-length-symbol-dimarcsym = รvhossz-szimbรณlum (DIMARCSYM) @@ -3008,6 +3095,7 @@ draw = .arc-specify-radius-of-arc = ARC Adja meg az รญv sugarรกt: .chord-length = Hรบrhossz + modify = .add-vertex = Csรบcspont beszรบrรกsa .closed-cross = Zรกrt kereszt @@ -3053,6 +3141,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Blokk kivรกlasztรกsa attribรบtumokkal: .annotation-object-scale = Feliratozรกsi objektum lรฉptรฉke .bedit-select-block-reference-to-edit = BEDIT Vรกlassza ki a szerkeszteni kรญvรกnt blokkreferenciรกt: + .bvstate-option = BVSTATE Adjon meg egy lehetล‘sรฉget [รšj/Beรกllรญtรกs/รtnevezรฉs/Tรถrlรฉs] : + .bvstate-new-name = BVSTATE Adja meg az รบj lรกthatรณsรกgi รกllapot nevรฉt: + .bvstate-set = BVSTATE Adja meg az aktuรกlissรก teendล‘ lรกthatรณsรกgi รกllapotot <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Adja meg az รกtnevezendล‘ lรกthatรณsรกgi รกllapotot <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Adja meg az รบj nevet: + .bvstate-delete = BVSTATE Adja meg a tรถrlendล‘ lรกthatรณsรกgi รกllapotot: + .bvstate-not-found = "__ocs_fmt_0__" lรกthatรณsรกgi รกllapot nem talรกlhatรณ. + .bvstate-exists = "__ocs_fmt_0__" lรกthatรณsรกgi รกllapot mรกr lรฉtezik. + .bvstate-last = Az egyetlen lรกthatรณsรกgi รกllapot nem tรถrรถlhetล‘. + .bvstate-needs-bedit = __ocs_fmt_0__: elล‘bb nyissa meg a blokkot a blokkszerkesztล‘ben (BEDIT). + .bvstate-current = Aktuรกlis lรกthatรณsรกgi รกllapot: __ocs_fmt_0__ + .bvhide-select = Jelรถlje ki az elrejtendล‘ objektumokat: + .bvshow-select = Jelรถlje ki a megjelenรญtendล‘ objektumokat: + .arrayedit-select-array = ARRAYEDIT Vรกlassza ki a tรถmbรถt: + .arrayedit-options = ARRAYEDIT Adjon meg egy lehetล‘sรฉget [Forrรกs/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: a kijelรถlt objektum nem asszociatรญv tรถmb. .blend-enter-continuity-tangent-curvature-c = BLEND Adja meg a folytonossรกgot [Tangent/Curvature] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND Vรกlassza ki az elsล‘ nyitott gรถrbรฉt [Continuity=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Vรกlassza ki a mรกsodik nyitott gรถrbรฉt [Continuity=__ocs_arg_c__]: @@ -3191,7 +3295,7 @@ layers = .dimstyle-already-exists = DIMSTYLE: A โ€ž__ocs_fmt_0__โ€ mรกr lรฉtezik. .dimstyle-created = DIMSTYLE: โ€ž__ocs_fmt_0__โ€ lรฉtrehozva. .dimstyle-not-found = DIMSTYLE: โ€ž__ocs_fmt_0__โ€ nem talรกlhatรณ. - .dimstyle-unknown-property-try-dimtxt-dimasz-di = DIMSTYLE: ismeretlen tulajdonsรกg โ€ž__ocs_fmt_0__โ€. Prรณbรกld ki: dimtxt dimasz dimdli dimexo dimexe dimgap dimscale dimlfac dimdle dimcen dimtsz + .dimstyle-unknown-property-try-dimtxt-dimasz-di = DIMSTYLE: ismeretlen tulajdonsรกg โ€ž__ocs_fmt_0__โ€. Prรณbรกlja meg a kรถvetkezล‘ket: dimtxt dimasz dimdli dimexo dimexe dimgap dimscale dimlfac dimdle dimcen dimtsz .delete-layer = Fรณlia tรถrlรฉse .dimstyles = Mรฉretstรญlusok (__ocs_fmt_0__) .edit = Szerkesztรฉs @@ -3252,7 +3356,7 @@ layers = .mleaderstyle-created = MLEADERSTYLE: โ€ž__ocs_fmt_0__โ€ lรฉtrehozva. .mleaderstyle-not-found = MLEADERSTYLE: โ€ž__ocs_fmt_0__โ€ nem talรกlhatรณ. .mleaderstyle-current-style-set-to-name = MLEADERSTYLE: az aktuรกlis stรญlus beรกllรญtรกsa โ€ž__ocs_fmt_0__โ€. - .mleaderstyle-unknown-property-try-text-height = MLEADERSTYLE: ismeretlen tulajdonsรกg โ€ž__ocs_fmt_0__โ€. Prรณbรกld ki: text_height arrowhead_size landing_distance landing_gap + .mleaderstyle-unknown-property-try-text-height = MLEADERSTYLE: ismeretlen tulajdonsรกg โ€ž__ocs_fmt_0__โ€. Prรณbรกlja meg a kรถvetkezล‘ket: text_height arrowhead_size landing_distance landing_gap .mlstyle-already-exists = MLSTYLE: A โ€ž__ocs_fmt_0__โ€ mรกr lรฉtezik. .mlstyle-created = MLSTYLE: โ€ž__ocs_fmt_0__โ€ lรฉtrehozva. .mlstyle-deleted = MLSTYLE: โ€ž__ocs_fmt_0__โ€ tรถrรถlve. @@ -3601,6 +3705,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Beszรบrรกsi pont megadรกsa: insert = + .pastespec-specify-insertion-point = PASTESPEC Adja meg a beszรบrรกsi pontot: + .pastespec-paste-as = PASTESPEC Beillesztรฉs mint [Objects/Block/Text]: .pdfui-pdf-scale = PDF-lรฉptรฉk: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Adja meg a beszรบrรกsi pontot: .pdfui-no-pdf-underlays = Nem talรกlhatรณ PDF-alรกvetรญtรฉs. @@ -3717,6 +3823,13 @@ insert = .xref-attach-hide-details = Rรฉszletek elrejtรฉse .xref-attach-select-file = Hivatkozรกsi fรกjl kivรกlasztรกsa .xref-attach-all-files = Minden hivatkozรกsi fรกjl + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Kรผlsล‘ fรกjlokra โ€“ pรฉldรกul mรกs rajzokra, raszterkรฉpekre รฉs alรกvetรญtรฉsekre โ€“ mutatรณ hivatkozรกsokat szรบr be. .xref-attach-invalid-input = ร‰rvรฉnytelen bevitel. .xref-insertion-point-x = Beszรบrรกsi pont X @@ -3771,6 +3884,7 @@ insert = .display-and-plot-frames = Keretek megjelenรญtรฉse รฉs nyomtatรกsa .display-not-plot-frames = Keretek megjelenรญtรฉse nyomtatรกs nรฉlkรผl .frames-vary = { "*" }Eltรฉrล‘ keretbeรกllรญtรกsok* + .underlay-osnaps-vary = *Az alรกtรฉtek tรกrgyraszterei eltรฉrnek* .regenerating-model = Modell รบjragenerรกlรกsa. .requires-int-0-2 = 0 รฉs 2 kรถzรถtti egรฉsz szรกm szรผksรฉges. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3908,6 +4022,18 @@ insert = .xref-attach-dwg = DWG csatolรกsa .xref-attach-image = Kรฉp csatolรกsa .select-images = Vรกlasszon kรฉpeket: + .adjust-select-image-or-underlay = Vรกlasszon kรฉpet vagy alรกtรฉtet: + .adjust-select-pdf-underlay = Vรกlasszon PDF alรกtรฉtet: + .adjust-select-dwf-underlay = Vรกlasszon DWF alรกtรฉtet: + .adjust-select-dgn-underlay = Vรกlasszon DGN alรกtรฉtet: + .adjust-mixed-underlays = *ร‰rvรฉnytelen kijelรถlรฉsi kรฉszlet. Csak azonos fรกjltรญpusรบ alรกtรฉteket vรกlasszon.* + .adjust-image-option = Adja meg a kรฉp opciรณjรกt [Kontraszt/Halvรกnyรญtรกs/Fรฉnyerล‘] : + .adjust-underlay-option = Adja meg a(z) __ocs_arg_kind__ alรกtรฉt opciรณjรกt [Halvรกnyรญtรกs/Kontraszt/Monokrรณm] <__ocs_arg_option__>: + .adjust-monochrome = Monokrรณm? [Igen/Nem] <__ocs_arg_value__>: + .adjust-brightness-value = Adja meg az รฉrtรฉket: fรฉnyerล‘ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Adja meg az รฉrtรฉket: kontraszt (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Adja meg az รฉrtรฉket: halvรกnyรญtรกs (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = Az รฉrtรฉknek 0 รฉs 100 kรถzรถtti egรฉsz szรกmnak kell lennie. .transparency-mode-on = Adja meg az รกtlรกtszรณsรกgi mรณdot [ON/OFF] : .transparency-mode-off = Adja meg az รกtlรกtszรณsรกgi mรณdot [ON/OFF] : .xref-attach-menu = Csatolรกsโ€ฆ @@ -4190,7 +4316,7 @@ insert = .field-object-name = Objektum neve .field-block-placeholder = Blokkhelyล‘rzล‘ .field-block-editor-only = Csak a blokkszerkesztล‘ben รฉrhetล‘ el - .field-block-reference-property = Blokkhivatkozรกs tulajdonsรกga + .field-block-reference-property = Blokkreferencia tulajdonsรกga .field-cat-linked = Csatolt .field-current-precision = Aktuรกlis pontossรกg .field-evaluate = Kiรฉrtรฉkelรฉs @@ -4413,7 +4539,27 @@ model = .value-2point = 2-pont .cube = Kocka + layout = + .mvsetup-select-objects-to-delete = MVSETUP Vรกlassza ki a tรถrlendล‘ objektumokat: + .delete-objects-option = Objektumok tรถrlรฉse + .mvsetup-enable-paper-space = MVSETUP Papรญrtรฉr engedรฉlyezรฉse? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Adja meg a lรฉptรฉktรฉnyezล‘t <1>: + .mvsetup-enter-paper-width = MVSETUP Adja meg a papรญr szรฉlessรฉgรฉt: + .mvsetup-enter-paper-height = MVSETUP Adja meg a papรญr magassรกgรกt: + .mvsetup-enter-option = MVSETUP Adja meg a lehetล‘sรฉget [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Adja meg a nรฉzetablak-elrendezรฉst [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Adja meg a nรฉzetablakok terรผletรฉnek elsล‘ sarkรกt : + .mvsetup-specify-opposite-corner = MVSETUP Adja meg a szemkรถzti sarkot: + .mvsetup-viewports-in-x = MVSETUP Adja meg a nรฉzetablakok szรกmรกt X irรกnyban <2>: + .mvsetup-viewports-in-y = MVSETUP Adja meg a nรฉzetablakok szรกmรกt Y irรกnyban <2>: + .mvsetup-distance-in-x = MVSETUP Adja meg a nรฉzetablakok tรกvolsรกgรกt X irรกnyban <0>: + .mvsetup-distance-in-y = MVSETUP Adja meg a nรฉzetablakok tรกvolsรกgรกt Y irรกnyban <0>: + .mvsetup-select-viewports = MVSETUP Vรกlassza ki a mรฉretezendล‘ nรฉzetablakokat: + .mvsetup-paper-space-units = MVSETUP Adja meg a papรญrtรฉr egysรฉgeinek szรกmรกt <1>: + .mvsetup-model-space-units = MVSETUP Adja meg a modelltรฉr egysรฉgeinek szรกmรกt <1>: + .mvsetup-viewports-do-not-fit = MVSETUP A nรฉzetablakok nem fรฉrnek el a terรผleten. + .scale-viewports-option = Nรฉzetablakok mรฉretezรฉse .active = Aktรญv .inactive = Inaktรญv .insert-view = Nรฉzet beszรบrรกsa @@ -4439,6 +4585,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Fagyasztรกs | T = Felengedรฉs | Enter = Kilรฉpรฉs view = + .twist-option = Elforgatรกs + .dview-specify-view-twist-angle = DVIEW Adja meg a nรฉzet elforgatรกsi szรถgรฉt <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Adja meg a lehetล‘sรฉget [TWist/Undo]: .color-scheme-set-to-system-currently-name = Szรญnsรฉma beรกllรญtva: โ€žRendszerโ€ (jelenleg __ocs_fmt_0__). .ss-create-sheet-set = Tervlapkรฉszlet lรฉtrehozรกsa .ss-requires-integer-between = __ocs_fmt_0__ รฉs __ocs_fmt_1__ kรถzรถtti egรฉsz szรกm szรผksรฉges. @@ -4633,13 +4782,6 @@ view = .x-no-xdata = __ocs_fmt_0__: nincs xdata. .value-3d-orbit = 3D keringรฉs .value-3d-orbit-drag-with-right-mouse-button = 3D Orbit: hรบzza a jobb egรฉrgombbal. - .adjust-brightness-contrast-fade = ADJUST [Brightness / Contrast / Fade]: - .adjust-brightness-0-100 = ADJUST fรฉnyerล‘ 0-100: - .adjust-contrast-0-100 = ADJUST kontraszt 0-100: - .adjust-fade-0-100 = ADJUST Halvรกnyรญtรกs 0โ€“100: - .adjust-select-raster-image-s-first = ADJUST: elล‘szรถr vรกlassza ki a raszterkรฉpe(ke)t. - .adjust-value-must-be-0-100 = ADJUST: az รฉrtรฉknek 0 รฉs 100 kรถzรถtt kell lennie. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ __ocs_fmt_2__ kรฉp(ek)en. .annoallvisible-new-value-0-1 = ANNOALLVISIBLE รบj รฉrtรฉk [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: รญrjon be 0 vagy 1 รฉrtรฉket. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE รบj รฉrtรฉk [-4..4]: @@ -4759,7 +4901,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Alรกvetรญtรฉs __ocs_fmt_0__: halvรกnyรญtรกs=__ocs_fmt_1__, kontraszt=__ocs_fmt_2__, be=__ocs_fmt_3__, kรถrรผlvรกgรกs=__ocs_fmt_4__, egyszรญnลฑ=__ocs_fmt_5__ .updated-changed-underlay-s = Frissรญtett __ocs_fmt_0__ alรกvetรญtรฉs(ek). - .usage-adjust-brightness-contrast-fade-0-100 = Hasznรกlat: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Hasznรกlat: ANNOSCALE pl. 1:50, 2:1 vagy egy tรฉnyezล‘ .usage-draworder-f-front-b-back-a-above-handle = Hasznรกlat: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Hasznรกlat: HYPERLINK (elล‘szรถr vรกlassza ki az objektumokat) @@ -4906,7 +5047,7 @@ properties = .big-font = Nagy betลฑtรญpus .blend-creases = ร‰lek simรญtรกsa .block-end = Blokk vรฉge - .block-reference = Blokkhivatkozรกs + .block-reference = Blokkreferencia .block-contents = Blokk tartalma .block-scale = Blokklรฉptรฉk .blockend = Blokkvรฉg @@ -5828,6 +5969,45 @@ plugins = .no-releases = nincs kiadรกs ui = + .window-gpu-backend = GPU (hรกttรฉrrendszer) + .window-click-add-alias-type-the-alias-and-command-accep = Kattintson a โ€ž+ รlnรฉv hozzรกadรกsaโ€ gombra, adja meg az รกlnevet รฉs a parancsot, majd fogadja el a sort รฉs kattintson az Alkalmaz gombra. Az Esc megszakรญtja a hozzรกadรกst. + .window-number-of-aliases = รlnevek szรกma + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Biztosan visszaรกllรญtja az alapรฉrtรฉkeket? Minden jelenlegi parancsรกlnรฉv elvรฉsz! + .window-alias-already-used-for-command = A parancsรกlnรฉv mรกr hasznรกlatban van: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Bรถngรฉszล‘ + .window-origin-planes = Alapsรญkok + .window-no-sketch-open = Nincs megnyitott vรกzlat + .window-bodies = Testek (__ocs_fmt_0__) + .window-no-solids-yet = Mรฉg nincsenek testek + .window-saved-immediately-changes-take-effect-the-next-t = A mรณdosรญtรกsokat a program azonnal menti, de csak az Open CAD Studio kรถvetkezล‘ indรญtรกsakor lรฉpnek รฉletbe. + .window-this-session = Jelenlegi munkamenet + .window-driver = Illesztล‘program + .window-backend = Hรกttรฉrrendszer + .window-selected-by = Kivรกlasztotta + .window-next-launch = Kรถvetkezล‘ indรญtรกs + .window-graphics-backend = Grafikus hรกttรฉrrendszer + .window-use-the-compatibility-renderer-for-gpus-without = Kompatibilis megjelenรญtล‘ hasznรกlata (tรกrolรณpuffer nรฉlkรผli GPU-khoz) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Rรฉgebbi GPU-kon az OpenGL elล‘nyben rรฉszesรญtรฉse (csak automatikus hรกttรฉrrendszernรฉl) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = Ha a rรถgzรญtett hรกttรฉrrendszerrel nem indul el a program, a kรถvetkezล‘ indรญtรกskor automatikus kivรกlasztรกsra vรกlt. A --backend kapcsolรณ รฉs a WGPU_BACKEND vรกltozรณ felรผlรญrja ezeket a beรกllรญtรกsokat. + .window-paused = Szรผneteltetve + .window-enable-spacemouse = SpaceMouse engedรฉlyezรฉse + .window-navigation = Navigรกciรณ + .window-reverse-pan-direction = Eltolรกs irรกnyรกnak megfordรญtรกsa + .window-pan-speed = Eltolรกs sebessรฉge + .window-resume-spacemouse = SpaceMouse folytatรกsa + .window-configure-3d-navigation-buttons-and-radial-menus = A 3D navigรกciรณt, a gombokat รฉs a radiรกlis menรผket a 3Dconnexion beรกllรญtรกsaiban adhatja meg. A โ€žCsak eltolรกsโ€ รฉs az โ€žEltolรกs รฉs zoomโ€ ugyanazokat a mozgatรกsi รฉs dรถntรฉsi beรกllรญtรกsokat hasznรกlja. + .window-connection-details = Kapcsolat rรฉszletei + .window-1-a-notch = __ocs_fmt_0__% lรฉpรฉsenkรฉnt + .window-the-slider-covers-the-system-variable-s-to-the-f = A csรบszka a rendszervรกltozรณ __ocs_fmt_0__ รฉs __ocs_fmt_1__ kรถzรถtti tartomรกnyรกt kezeli; a mezล‘ben legfeljebb __ocs_fmt_2__ รกllรญthatรณ be a gyorsabb gรถrgetรฉshez. + .window-bundled-with-open-cad-studio = Az Open CAD Studio rรฉsze + .window-how-the-reference-manager-works-on-web = A Referenciakezelล‘ mลฑkรถdรฉse bรถngรฉszล‘ben + .window-this-panel-lists-the-drawing-s-references-read-o = Ez a panel csak olvashatรณ mรณdon sorolja fel a rajz hivatkozรกsait: รกllapot, tรญpus, mรฉret, dรกtum รฉs mentett elรฉrรฉsi รบt. A kijelรถlt hivatkozรกs adatai a Rรฉszletek panelen lรกthatรณk. A Lista รฉs a Fa a megjelenรญtรฉsi mรณdot vรกltja. + .window-attach-refresh-and-change-path-stay-disabled-her = A Csatolรกs, Frissรญtรฉs รฉs รštvonal mรณdosรญtรกsa itt nem hasznรกlhatรณ: a bรถngรฉszล‘s vรกltozat nem fรฉr hozzรก a fรกjlrendszerhez, ezรฉrt a hivatkozรกsok nem adhatรณk hozzรก, nem tรถlthetล‘k รบjra รฉs nem irรกnyรญthatรณk รกt. Elล‘nรฉzet sincs. A beรกgyazott hivatkozรกsok minden platformon รญrรกsvรฉdettek; relatรญv รบtvonal hasznรกlatรกhoz elล‘bb menteni kell a rajzot. + .window-hit-something-broken-or-missing-report-it-on-the = Hibรกt vagy hiรกnyzรณ funkciรณt talรกlt? Jelezze a GitHub hibajegyek kรถzรถtt, hogy javรญthatรณ legyen. + .window-report-an-issue = Hiba bejelentรฉse + .window-reference-manager-web = Referenciakezelล‘ โ€“ web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES a bรถngรฉszล‘s vรกltozatban + .window-embed-image-in-drawing = Kรฉp beรกgyazรกsa (rajzba) .build = Buildazonosรญtรณ .commit-date = Commit dรกtuma .count-steps-available = __ocs_arg_count__ lรฉpรฉs รฉrhetล‘ el @@ -6223,8 +6403,8 @@ graphic-attributes = .fills-on-current-layer = Kitรถltรฉsek lรฉtrehozรกsa az aktuรกlis fรณliรกn .edit-hatch = Sraffozรกs szerkesztรฉse .edit-gradient = Szรญnรกtmenet szerkesztรฉse - .close-open-object = A kijelรถlt objektum nyitott. Bezรกrja a kitรถltรฉs lรฉtrehozรกsa elล‘tt? [Yes / No] - .close-open-objects = __ocs_fmt_0__ kijelรถlt objektum nyitott. Bezรกrja ล‘ket a kitรถltรฉs lรฉtrehozรกsa elล‘tt? [Yes / No] + .close-open-object = A kijelรถlt objektum nyitott. Bezรกrja a kitรถltรฉs lรฉtrehozรกsa elล‘tt? [Yes (Igen) / No (Nem)] (Igen) + .close-open-objects = __ocs_fmt_0__ kijelรถlt objektum nyitott. Bezรกrja ล‘ket a kitรถltรฉs lรฉtrehozรกsa elล‘tt? [Yes (Igen) / No (Nem)] (Igen) .enter-new-layer-name = Adja meg az รบj fรณlia nevรฉt: .invalid-layer-name = Grafikus jellemzล‘k: adjon meg รฉrvรฉnyes fรณlianevet <>/\":;?*|,=` nรฉlkรผl. .layer-exists = Grafikus jellemzล‘k: a(z) "__ocs_fmt_0__" fรณlia mรกr lรฉtezik. @@ -6248,3 +6428,117 @@ graphic-attributes = .reset-angle = Szรถg visszaรกllรญtรกsa .one-color = Egy szรญn .two-colors = Kรฉt szรญn + +property-patch = + .patch-01 = A 3DxWare nem รฉrhetล‘ el + .patch-02 = Kรถrnyezetfรผggล‘ + .patch-03 = Eltolรกs รฉs nagyรญtรกs a papรญrtรฉrben; teljes navigรกciรณ a modellnรฉzetekben. + .patch-04 = 3Dconnexion-beรกllรญtรกsok megnyitรกsaโ€ฆ + .patch-05 = 3Dconnexion-beรกllรญtรกsok megnyitรกsaโ€ฆ + .patch-06 = Tizedesjel szerint + .patch-07 = Mลฑveleti jelek szerint + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Hozzรกadรกs a munkakรฉszlethez + .remove-from-working-set = Eltรกvolรญtรกs a munkakรฉszletbล‘l + .toolbar-objects = objektum + .dialog-title = Hivatkozรกs szerkesztรฉse + .dialog-reference = Szerkesztendล‘ hivatkozรกs + .dialog-chain-hint = A lรกnc a kijelรถlt pontnรกl; kattintson egy cรญmkรฉre a szint vรกltรกsรกhoz. + .dialog-path = รštvonal: + .dialog-working-set = Munkakรฉszlet + .dialog-all-objects = Minden objektum + .dialog-pick-objects = Kijelรถlรฉs a rajzon + .dialog-attributes = Attribรบtumdefinรญciรณk szerkesztรฉse is + .dialog-session = Munkamenet + .dialog-lock = Munkakรฉszleten kรญvรผli objektumok zรกrolรกsa + .dialog-unique = รœtkรถzล‘ fรณlia-, stรญlus- รฉs blokknevek egyedi neveket kapnak + .dialog-start = Szerkesztรฉs indรญtรกsa + .dialog-help = Blokkhivatkozรกst vagy kรผlsล‘ hivatkozรกst nyit meg helyben tรถrtรฉnล‘ szerkesztรฉsre. + .select-reference = Vรกlassza ki a hivatkozรกst: + .reference-not-found = A hivatkozรกs nem talรกlhatรณ. + .not-valid-reference = Az objektum nem รฉrvรฉnyes hivatkozรกs + .one-entity-added = 1 elem hozzรกadva + .nesting-level = Vรกlassza ki a beรกgyazรกsi szintet [Ok/Kรถvetkezล‘] : + .selection-method = Adja meg az objektumkijelรถlรฉs mรณdjรกt [Mind/Beรกgyazott] : + .select-nested = Jelรถlje ki a beรกgyazott objektumokat: + .display-attdefs = Megjelenjenek az attribรบtumdefinรญciรณk [Igen/Nem] : + .zero-items-selected = 0 elem kijelรถlve + .one-item-selected = 1 elem kijelรถlve + .items-selected = __ocs_fmt_0__ elem kijelรถlve + .refset-option = Adjon meg egy lehetล‘sรฉget [Hozzรกadรกs/Eltรกvolรญtรกs] : + .refclose-option = Adja meg a lehetล‘sรฉget [Mentรฉs/Hivatkozรกsmรณdosรญtรกsok elvetรฉse] : + .not-checked-out = ** A parancs csak akkor engedรฉlyezett, ha egy hivatkozรกs REFEDIT paranccsal szerkesztรฉs alatt รกll ** + .already-checked-out = ** A parancs nem engedรฉlyezett, __ocs_fmt_0__ mรกr szerkesztรฉs alatt รกll ** + .transfer-objects = Objektumok รกthelyezรฉse a RefEdit munkakรฉszlet รฉs a gazdarajz kรถzรถtt... + .added-to-working-set = __ocs_fmt_0__ hozzรกadva a munkakรฉszlethez + .removed-from-working-set = __ocs_fmt_0__ eltรกvolรญtva a munkakรฉszletbล‘l + .one-object-added = 1 objektum hozzรกadva ehhez: __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ objektum hozzรกadva ehhez: __ocs_fmt_1__ + .one-object-removed = 1 objektum eltรกvolรญtva ebbล‘l: __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ objektum eltรกvolรญtva ebbล‘l: __ocs_fmt_1__ + .one-xref-instance = 1 xref-pรฉldรกny frissรญtve + .xref-instances = __ocs_fmt_0__ xref-pรฉldรกny frissรญtve + .one-block-instance = 1 blokkpรฉldรกny frissรญtve + .block-instances = __ocs_fmt_0__ blokkpรฉldรกny frissรญtve + .redefined-reloaded = __ocs_fmt_0__ รบjradefiniรกlva รฉs รบjratรถltve. + .redefined = __ocs_fmt_0__ รบjradefiniรกlva. + .ok = Ok + .nested = Beรกgyazott + +image-adjust = + .title = Kรฉp beรกllรญtรกsa + .preview = Elล‘nรฉzet + .before = Elล‘tte + .after = Utรกna + .no-preview = a kรฉp nem tรถlthetล‘ be + .adjust = Beรกllรญtรกs + .brightness = Fรฉnyerล‘ + .contrast = Kontraszt + .fade = Halvรกnyรญtรกs + .hint = Kattintson egy sรกvra vagy hรบzza; โˆ’ / + egy lรฉpรฉs, a mezล‘be 0โ€“100 kรถzรถtti egรฉsz szรกm รญrhatรณ. + .reset = Alapรฉrtรฉkek visszaรกllรญtรกsa + .apply = Alkalmaz + .help = Beรกllรญtja a kijelรถlt raszterkรฉpek fรฉnyerejรฉt, kontrasztjรกt รฉs halvรกnyรญtรกsรกt. + +keytips = + .title = KeyTip + .typed = Begรฉpelve: { $path } + .hint-tabs = รrja be a lap betลฑjรฉt ยท Az Esc bezรกr + .hint-tab = { $tab } lap ยท Az Esc egy szinttel visszalรฉp + .hint-items = รrja be az elem betลฑjรฉt ยท Az Esc egy szinttel visszalรฉp diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 77bc17df5..c79257d90 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Plugin ribbon-tab = + .block-editor = Editor blocchi .draw = Disegno .parametric = Parametrico .annotate = Annotazione @@ -98,6 +99,9 @@ start = .no-recent-files = I file aperti verranno visualizzati qui. .browser-storage = Memoria del browser .keep-recent-files = Conserva i file recenti + .view-list = Elenco + .view-icons = Icone + .view-preview = Anteprime grandi donation = .title = Richiesta di donazione @@ -167,6 +171,14 @@ modal = .that-object = quell'oggetto .those-objects = quegli oggetti + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Immagine "{ $name }" incollata: { $width }ร—{ $height } px .image-unsupported = Impossibile decodificare l'immagine incollata "{ $name }". @@ -273,11 +285,73 @@ command-copy = .mode = COPY Specificare modalitร  di copia [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ solo qui (verde), __ocs_fmt_1__ solo nell'altro disegno (rosso)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Confronta con disegno + .label-associative-array-created = __ocs_fmt_0__: serie associativa creata. + .assoc-array-array-path = Serie (percorso) + .assoc-array-array-polar = Serie (polare) + .assoc-array-array-rectangular = Serie (rettangolare) + .assoc-array-item-spacing = Spaziatura elementi + .assoc-array-fill-angle = Angolo di riempimento + .assoc-array-items = Elementi + .assoc-array-level-spacing = Spaziatura livelli + .assoc-array-levels = Livelli + .assoc-array-row-spacing = Spaziatura righe + .assoc-array-column-spacing = Spaziatura colonne + .fbxexport-exported-to = FBXEXPORT: esportato in "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: nessun dato mesh 3D in questo disegno. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = I nuovi tratteggi seguono i contorni (HPASSOC) + .options-show-ribbon = Mostra la barra multifunzione (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Mostra la barra dei menu (MENUBAR) + .options-window-elements = Elementi della finestra + .menu-viewports = Finestre + .menu-view-twist = Rotazione vista + .menu-tools = Strumenti + .menu-table-style = Stile tabella + .menu-run-script = Esegui script + .menu-revision-cloud = Nuvola di revisione + .menu-regen-all = Rigenera tutto + .menu-redraw = Ridisegna + .menu-rectangle = Rettangolo + .menu-quick-dimension = Quota rapida + .menu-publish = Pubblica + .menu-paste-special = Incolla speciale + .menu-parametric = Parametrico + .menu-page-setup-manager = Gestione impostazioni pagina + .menu-orbit = Orbita + .menu-new-ucs = Nuovo UCS + .menu-named-views = Viste con nome + .menu-multiline-text = Testo multilinea + .menu-multiline-style = Stile multilinea + .menu-multiline = Multilinea + .menu-multileader-style = Stile multidirettrice + .menu-match-properties = Corrispondenza proprietร  + .menu-lengthen = Allunga + .menu-jogged = Con raccordo + .menu-id-point = Punto ID + .menu-explode-text = Esplodi testo + .menu-edit-spline = Modifica spline + .menu-edit-polyline = Modifica polilinea + .menu-edit-hatch = Modifica tratteggio + .menu-drawing-limits = Limiti disegno + .menu-donut = Ciambella + .menu-dimension-style = Stile di quota + .menu-construction-line = Linea di costruzione + .menu-arc-length = Lunghezza arco + .command-line-opacity = Opacitร  della riga di comando + .txtexp-no-text-selected = TXTEXP: nessun testo selezionato. + .angle-override-shown = Sostituzione angolo: __ocs_fmt_0__ .plot-preview-task = Anteprima di stampa .link-a-csv-data-source = Collega un'origine dati CSV .csv-files = File CSV .browser-cannot-open-typed-path = Il browser non puรฒ aprire un percorso digitato; scegli il file. .recover-drawing = Recupera disegno + .requires-0-or-1-or-keyword = Richiede solo 0 o 1, o una parola chiave di opzione. + .requires-a-number = Richiede un numero. .restore-it-as-a-recovered-tab-or-discard-the-copy = Ripristinalo in una scheda recuperata oppure elimina la copia. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" ha una copia di salvataggio automatico piรน recente dellโ€™ultimo salvataggio. .autosave-copy-could-not-be-stored = Impossibile salvare la copia di salvataggio automatico: __ocs_fmt_0__ @@ -821,6 +895,10 @@ common = .slice-result-commit-failed = SLICE: impossibile ottenere il risultato; gli oggetti originali sono rimasti come prima. .slice-retained-update-failed = SLICE: non riesco ad aggiornare il risultato ottenuto. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: selezionare un oggetto sezione o attivare una sezione dinamica. + .sectioncull-prepare-failed = SECTIONCULL: non รจ stato possibile preparare un oggetto tagliato; nulla รจ stato modificato. + .sectioncull-nothing-outside = SECTIONCULL: nulla si trova fuori dalla sezione. + .sectioncull-result = SECTIONCULL: cancellati __ocs_arg_erased__ oggetti e tagliati __ocs_arg_cut__ sul limite della sezione. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: adatta una spline attraverso i punti __ocs_fmt_0__. .splinefit-need-at-least-3-points =SPLINEFIT: servono almeno 3 punti. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: seleziona una polilinea attraverso la quale inserire una spline. @@ -858,9 +936,12 @@ common = .sent-to-printer-printer = Inviato alla stampante: __ocs_fmt_0__ .set-current = Imposta corrente .shade-plot = Stampa ombreggiatura + .shaded-with-edges = Ombreggiato con spigoli + .shades-of-gray = Sfumature di grigio .shadow-flags = Opzioni di ombreggiatura .single = Singolo .size = Dimensioni + .sketchy = Schizzo .snap-override-label-next-pick-only = Sostituzione dello snap: __ocs_fmt_0__ (solo scelta successiva). .solid-created = Solido creato. .source-object-not-found = Oggetto di origine non trovato. @@ -1066,7 +1147,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: immetti Left, Top o Right. .isodraft-expected-on-or-off = ISODRAFT: immetti On o Off. .xdata-selected-entities-are-on-locked-layers = XDATA: le entitร  selezionate sono su layer bloccati. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: nessuna immagine raster selezionata o proprietร  sconosciuta (usa BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: immetti un intero da -4 a 4. .usage-datalink-path-to-csv = Uso: DATALINK .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: nessuna tabella collegata trovata. @@ -2368,6 +2448,7 @@ common = .load-linetypes = Carica tipi di linea .requires-two-positive-values = Richiede due valori positivi (x,y) o uno per entrambi. .snap-marker-color = Colore del marcatore di snap + .snap-marker-size = Dimensione del marcatore di snap .command-line-text-color = Colore del testo della riga di comando .operand = Operando .linetypes-loaded = __ocs_fmt_0__ tipo/i di linea caricato/i. @@ -2381,6 +2462,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Specificare il punto base [oggetti __ocs_arg_count__]: .threedrotate-specify-rotation-axis = 3DROTATE Specificare l'asse di rotazione [X/Y/Z] o un secondo punto su di esso : .threedrotate-specify-rotation-angle-a = 3DROTATE Specificare l'angolo di rotazione <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Salva avviso .unable-to-save-drawing = Impossibile salvare il disegno @@ -3004,6 +3092,7 @@ draw = .arc-specify-radius-of-arc = ARC Specificare raggio dell'arco: .chord-length = Lunghezza corda + modify = .add-vertex = Aggiungi vertice .closed-cross = Croce chiusa @@ -3049,6 +3138,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Seleziona blocco con attributi: .annotation-object-scale = Scala oggetto annotazione .bedit-select-block-reference-to-edit = BEDIT Selezionare il riferimento di blocco da modificare: + .bvstate-option = BVSTATE Immettere un'opzione [Nuovo/Imposta/Rinomina/Elimina] : + .bvstate-new-name = BVSTATE Immettere il nome del nuovo stato di visibilitร : + .bvstate-set = BVSTATE Immettere lo stato di visibilitร  da rendere corrente <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Immettere lo stato di visibilitร  da rinominare <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Immettere il nuovo nome: + .bvstate-delete = BVSTATE Immettere lo stato di visibilitร  da eliminare: + .bvstate-not-found = Stato di visibilitร  "__ocs_fmt_0__" non trovato. + .bvstate-exists = Lo stato di visibilitร  "__ocs_fmt_0__" esiste giร . + .bvstate-last = L'unico stato di visibilitร  non puรฒ essere eliminato. + .bvstate-needs-bedit = __ocs_fmt_0__: aprire prima il blocco nell'Editor blocchi (BEDIT). + .bvstate-current = Stato di visibilitร  corrente: __ocs_fmt_0__ + .bvhide-select = Selezionare gli oggetti da nascondere: + .bvshow-select = Selezionare gli oggetti da rendere visibili: + .arrayedit-select-array = ARRAYEDIT Selezionare la serie: + .arrayedit-options = ARRAYEDIT Immettere un'opzione [Origine/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: l'oggetto selezionato non รจ una serie associativa. .blend-enter-continuity-tangent-curvature-c = BLEND Enter continuitร  [Tangente/Curvatura] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND Selezionare la prima curva aperta [Continuitร =__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Selezionare la seconda curva aperta [Continuitร =__ocs_arg_c__]: @@ -3601,6 +3706,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Specificare il punto di inserimento: insert = + .pastespec-specify-insertion-point = PASTESPEC Specificare il punto di inserimento: + .pastespec-paste-as = PASTESPEC Incolla come [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3717,6 +3824,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3771,6 +3885,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Snap ad oggetto sottoposti variabili* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3910,6 +4025,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Selezionare un'immagine o un sottoposto: + .adjust-select-pdf-underlay = Selezionare un sottoposto PDF: + .adjust-select-dwf-underlay = Selezionare un sottoposto DWF: + .adjust-select-dgn-underlay = Selezionare un sottoposto DGN: + .adjust-mixed-underlays = *Gruppo di selezione non valido. Selezionare solo sottoposti dello stesso tipo di file.* + .adjust-image-option = Specificare un'opzione immagine [Contrasto/Svanire/Luminositร ] : + .adjust-underlay-option = Specificare un'opzione sottoposto __ocs_arg_kind__ [Svanire/Contrasto/Monocromatico] <__ocs_arg_option__>: + .adjust-monochrome = Monocromatico? [Sรฌ/No] <__ocs_arg_value__>: + .adjust-brightness-value = Specificare il valore di luminositร  (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Specificare il valore di contrasto (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Specificare il valore di svanire (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = Il valore deve essere un numero intero compreso tra 0 e 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4414,7 +4541,27 @@ model = .value-2point = 2 punti .cube = Cubo + layout = + .mvsetup-select-objects-to-delete = MVSETUP Selezionare gli oggetti da eliminare: + .delete-objects-option = Elimina oggetti + .mvsetup-enable-paper-space = MVSETUP Attivare lo spazio carta? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Immettere il fattore di scala <1>: + .mvsetup-enter-paper-width = MVSETUP Immettere la larghezza del foglio: + .mvsetup-enter-paper-height = MVSETUP Immettere l'altezza del foglio: + .mvsetup-enter-option = MVSETUP Immettere un'opzione [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Immettere la disposizione delle finestre [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Specificare il primo angolo dell'area delle finestre : + .mvsetup-specify-opposite-corner = MVSETUP Specificare l'angolo opposto: + .mvsetup-viewports-in-x = MVSETUP Immettere il numero di finestre in direzione X <2>: + .mvsetup-viewports-in-y = MVSETUP Immettere il numero di finestre in direzione Y <2>: + .mvsetup-distance-in-x = MVSETUP Specificare la distanza tra le finestre in direzione X <0>: + .mvsetup-distance-in-y = MVSETUP Specificare la distanza tra le finestre in direzione Y <0>: + .mvsetup-select-viewports = MVSETUP Selezionare le finestre da scalare: + .mvsetup-paper-space-units = MVSETUP Immettere il numero di unitร  dello spazio carta <1>: + .mvsetup-model-space-units = MVSETUP Immettere il numero di unitร  dello spazio modello <1>: + .mvsetup-viewports-do-not-fit = MVSETUP Le finestre non entrano nell'area. + .scale-viewports-option = Scala finestre .active = Attivo .inactive = Inattivo .insert-view = Inserisci vista @@ -4440,6 +4587,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Congela | T = Scongela | Enter = Esci view = + .twist-option = Rotazione + .dview-specify-view-twist-angle = DVIEW Specificare l'angolo di rotazione della vista <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Immettere un'opzione [TWist/Undo]: .color-scheme-set-to-system-currently-name = Combinazione colori impostata su "Sistema" (attualmente __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = Richiede un numero intero compreso tra __ocs_fmt_0__ e __ocs_fmt_1__. @@ -4634,13 +4784,6 @@ view = .x-no-xdata = __ocs_fmt_0__: nessun dato x. .value-3d-orbit =Orbita 3D .value-3d-orbit-drag-with-right-mouse-button = Orbita 3D: trascina con il tasto destro del mouse. - .adjust-brightness-contrast-fade = ADJUST [Luminositร /Contrasto/Dissolvenza]: - .adjust-brightness-0-100 = Luminositร  ADJUST 0-100: - .adjust-contrast-0-100 = ADJUST contrasto 0-100: - .adjust-fade-0-100 = ADJUST dissolvenza 0-100: - .adjust-select-raster-image-s-first = ADJUST: seleziona prima le immagini raster. - .adjust-value-must-be-0-100 = ADJUST: il valore deve essere compreso tra 0 e 100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ sulle immagini __ocs_fmt_2__. .annoallvisible-new-value-0-1 = ANNOALLVISIBLE nuovo valore [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: inserisci 0 o 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE nuovo valore [-4..4]: @@ -4766,7 +4909,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Sottofondo __ocs_fmt_0__: dissolvenza=__ocs_fmt_1__, contrasto=__ocs_fmt_2__, on=__ocs_fmt_3__, clip=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = Sottofondi __ocs_fmt_0__ aggiornati. - .usage-adjust-brightness-contrast-fade-0-100 = Utilizzo: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Utilizzo: ANNOSCALE ad es. 1:50, 2:1 o un fattore .usage-draworder-f-front-b-back-a-above-handle = Utilizzo: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Utilizzo: HYPERLINK (selezionare prima gli oggetti) @@ -5994,6 +6136,46 @@ ui = .osnap-settings = Impostazioni snap... .snap-override-none-next-pick-only = Snap temporaneo: Nessuno (solo il prossimo punto). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Mantieni sempre il testo tra le linee di estensione .angular-dimensions = Dimensioni angolari @@ -6261,3 +6443,117 @@ graphic-attributes = .reset-angle = Reimposta angolo .one-color = Un colore .two-colors = Due colori + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Aggiungi al gruppo di lavoro + .remove-from-working-set = Rimuovi dal gruppo di lavoro + .toolbar-objects = oggetti + .dialog-title = Modifica riferimento + .dialog-reference = Riferimento da modificare + .dialog-chain-hint = La catena nel punto scelto; fare clic su un'etichetta per cambiare livello. + .dialog-path = Percorso: + .dialog-working-set = Gruppo di lavoro + .dialog-all-objects = Tutti gli oggetti + .dialog-pick-objects = Seleziona nel disegno + .dialog-attributes = Modifica anche le definizioni di attributo + .dialog-session = Sessione + .dialog-lock = Blocca gli oggetti fuori dal gruppo di lavoro + .dialog-unique = Assegna nomi univoci a layer, stili e blocchi in conflitto + .dialog-start = Inizia modifica + .dialog-help = Apre un riferimento a blocco o un riferimento esterno per modificarlo sul posto. + .select-reference = Selezionare il riferimento: + .reference-not-found = Riferimento non trovato. + .not-valid-reference = L'oggetto non รจ un riferimento valido + .one-entity-added = 1 entitร  aggiunta + .nesting-level = Selezionare il livello di nidificazione [Ok/Successivo] : + .selection-method = Immettere il metodo di selezione oggetti [Tutti/Nidificati] : + .select-nested = Selezionare gli oggetti nidificati: + .display-attdefs = Visualizzare le definizioni di attributo [Sรฌ/No] : + .zero-items-selected = 0 elementi selezionati + .one-item-selected = 1 elemento selezionato + .items-selected = __ocs_fmt_0__ elementi selezionati + .refset-option = Immettere un'opzione [Aggiungi/Rimuovi] : + .refclose-option = Immettere un'opzione [Salva/Annulla modifiche al riferimento] : + .not-checked-out = ** Comando non consentito se non รจ in modifica un riferimento con REFEDIT ** + .already-checked-out = ** Comando non consentito, __ocs_fmt_0__ รจ giร  in modifica ** + .transfer-objects = Trasferisci oggetti tra il gruppo di lavoro RefEdit e il disegno host... + .added-to-working-set = __ocs_fmt_0__ aggiunti al gruppo di lavoro + .removed-from-working-set = __ocs_fmt_0__ rimossi dal gruppo di lavoro + .one-object-added = 1 oggetto aggiunto a __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ oggetti aggiunti a __ocs_fmt_1__ + .one-object-removed = 1 oggetto rimosso da __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ oggetti rimossi da __ocs_fmt_1__ + .one-xref-instance = 1 istanza di xrif aggiornata + .xref-instances = __ocs_fmt_0__ istanze di xrif aggiornate + .one-block-instance = 1 istanza di blocco aggiornata + .block-instances = __ocs_fmt_0__ istanze di blocco aggiornate + .redefined-reloaded = __ocs_fmt_0__ ridefinito e ricaricato. + .redefined = __ocs_fmt_0__ ridefinito. + .ok = Ok + .nested = Nidificati + +image-adjust = + .title = Regola immagine + .preview = Anteprima + .before = Prima + .after = Dopo + .no-preview = impossibile caricare l'immagine + .adjust = Regola + .brightness = Luminositร  + .contrast = Contrasto + .fade = Dissolvenza + .hint = Fare clic o trascinare una barra; โˆ’ / + รจ un passo, una casella accetta un intero 0โ€“100. + .reset = Ripristina valori predefiniti + .apply = Applica + .help = Imposta luminositร , contrasto e dissolvenza delle immagini raster selezionate. + +keytips = + .title = KeyTip + .typed = Digitato: { $path } + .hint-tabs = Digitare la lettera di una scheda ยท Esc chiude + .hint-tab = Scheda { $tab } ยท Esc torna indietro di un livello + .hint-items = Digitare la lettera di un elemento ยท Esc torna indietro di un livello diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index c16fa3b08..dd7a1f238 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = ใƒ—ใƒฉใ‚ฐใ‚คใƒณ ribbon-tab = + .block-editor = ใƒ–ใƒญใƒƒใ‚ฏ ใ‚จใƒ‡ใ‚ฃใ‚ฟ .draw = ไฝœๅ›ณ .parametric = ใƒ‘ใƒฉใƒกใƒˆใƒชใƒƒใ‚ฏ .annotate = ๆณจ้‡ˆ @@ -98,6 +99,9 @@ start = .no-recent-files = ้–‹ใ„ใŸใƒ•ใ‚กใ‚คใƒซใŒใ“ใ“ใซ่กจ็คบใ•ใ‚Œใพใ™ใ€‚ .browser-storage = ใƒ–ใƒฉใ‚ฆใ‚ถใƒผใ‚นใƒˆใƒฌใƒผใ‚ธ .keep-recent-files = ๆœ€่ฟ‘ไฝฟใฃใŸใƒ•ใ‚กใ‚คใƒซใ‚’ไฟๆŒ + .view-list = ไธ€่ฆง + .view-icons = ใ‚ขใ‚คใ‚ณใƒณ + .view-preview = ๅคงใใ„ใƒ—ใƒฌใƒ“ใƒฅใƒผ donation = .title = ๅฏ„ไป˜ใฎใŠ้ก˜ใ„ @@ -167,6 +171,14 @@ modal = .that-object = ใใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆ .those-objects = ใใ‚Œใ‚‰ใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆ + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = ็”ปๅƒ "{ $name }" ใ‚’่ฒผใ‚Šไป˜ใ‘ใพใ—ใŸ: { $width }ร—{ $height } px .image-unsupported = ่ฒผใ‚Šไป˜ใ‘ใŸ็”ปๅƒ "{ $name }" ใ‚’ใƒ‡ใ‚ณใƒผใƒ‰ใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ @@ -264,11 +276,73 @@ command-copy = .mode = COPY ใ‚ณใƒ”ใƒผใƒขใƒผใƒ‰ใ‚’ๅ…ฅๅŠ› [Single/Multiple] : common = + .compare-prompt = COMPARE ใ“ใ“ใฎใฟ __ocs_fmt_0__ (็ท‘)ใ€ใ‚‚ใ†ไธ€ๆ–นใฎๅ›ณ้ขใฎใฟ __ocs_fmt_1__ (่ตค)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ๅ›ณ้ขใจๆฏ”่ผƒ + .label-associative-array-created = __ocs_fmt_0__: ่‡ชๅ‹•่ชฟๆ•ด้…ๅˆ—่ค‡ๅ†™ใ‚’ไฝœๆˆใ—ใพใ—ใŸใ€‚ + .assoc-array-array-path = ้…ๅˆ—่ค‡ๅ†™ (ใƒ‘ใ‚น) + .assoc-array-array-polar = ้…ๅˆ—่ค‡ๅ†™ (ๅ††ๅฝข็Šถ) + .assoc-array-array-rectangular = ้…ๅˆ—่ค‡ๅ†™ (็Ÿฉๅฝข็Šถ) + .assoc-array-item-spacing = ้ …็›ฎใฎ้–“้š” + .assoc-array-fill-angle = ๅ……ๅกซ่ง’ๅบฆ + .assoc-array-items = ้ …็›ฎ + .assoc-array-level-spacing = ใƒฌใƒ™ใƒซใฎ้–“้š” + .assoc-array-levels = ใƒฌใƒ™ใƒซ + .assoc-array-row-spacing = ่กŒใฎ้–“้š” + .assoc-array-column-spacing = ๅˆ—ใฎ้–“้š” + .fbxexport-exported-to = FBXEXPORT: "__ocs_fmt_0__" ใซๆ›ธใๅ‡บใ—ใพใ—ใŸ + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: ใ“ใฎๅ›ณ้ขใซ 3D ใƒกใƒƒใ‚ทใƒฅใƒ‡ใƒผใ‚ฟใŒใ‚ใ‚Šใพใ›ใ‚“ใ€‚ + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = ๆ–ฐใ—ใ„ใƒใƒƒใƒใƒณใ‚ฐใฏๅขƒ็•Œใซๅพ“ใ† (HPASSOC) + .options-show-ribbon = ใƒชใƒœใƒณใ‚’่กจ็คบ (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = ใƒกใƒ‹ใƒฅใƒผ ใƒใƒผใ‚’่กจ็คบ (MENUBAR) + .options-window-elements = ใ‚ฆใ‚ฃใƒณใƒ‰ใ‚ฆใฎ่ฆ็ด  + .menu-viewports = ใƒ“ใƒฅใƒผใƒใƒผใƒˆ + .menu-view-twist = ใƒ“ใƒฅใƒผใฎใญใ˜ใ‚Œ + .menu-tools = ใƒ„ใƒผใƒซ + .menu-table-style = ่กจใ‚นใ‚ฟใ‚คใƒซ + .menu-run-script = ใ‚นใ‚ฏใƒชใƒ—ใƒˆๅฎŸ่กŒ + .menu-revision-cloud = ้›ฒใƒžใƒผใ‚ฏ + .menu-regen-all = ๅ…จๅ†ไฝœๅ›ณ + .menu-redraw = ๅ†ๆ็”ป + .menu-rectangle = ้•ทๆ–นๅฝข + .menu-quick-dimension = ใ‚ฏใ‚คใƒƒใ‚ฏๅฏธๆณ• + .menu-publish = ใƒ‘ใƒ–ใƒชใƒƒใ‚ทใƒฅ + .menu-paste-special = ๅฝขๅผใ‚’้ธๆŠžใ—ใฆ่ฒผใ‚Šไป˜ใ‘ + .menu-parametric = ใƒ‘ใƒฉใƒกใƒˆใƒชใƒƒใ‚ฏ + .menu-page-setup-manager = ใƒšใƒผใ‚ธ่จญๅฎš็ฎก็† + .menu-orbit = ใ‚ชใƒผใƒ“ใƒƒใƒˆ + .menu-new-ucs = ๆ–ฐ่ฆ UCS + .menu-named-views = ใƒ“ใƒฅใƒผ็ฎก็† + .menu-multiline-text = ใƒžใƒซใƒใƒ†ใ‚ญใ‚นใƒˆ + .menu-multiline-style = ใƒžใƒซใƒใƒฉใ‚คใƒณ ใ‚นใ‚ฟใ‚คใƒซ + .menu-multiline = ใƒžใƒซใƒใƒฉใ‚คใƒณ + .menu-multileader-style = ใƒžใƒซใƒๅผ•ๅ‡บ็ทšใ‚นใ‚ฟใ‚คใƒซ + .menu-match-properties = ใƒ—ใƒญใƒ‘ใƒ†ใ‚ฃใ‚ณใƒ”ใƒผ + .menu-lengthen = ้•ทใ•ๅค‰ๆ›ด + .menu-jogged = ๆŠ˜ใ‚Šๆ›ฒใ’ + .menu-id-point = ็‚นใฎๅบงๆจ™ + .menu-explode-text = ๆ–‡ๅญ—ใ‚’ๅˆ†่งฃ + .menu-edit-spline = ใ‚นใƒ—ใƒฉใ‚คใƒณ็ทจ้›† + .menu-edit-polyline = ใƒใƒชใƒฉใ‚คใƒณ็ทจ้›† + .menu-edit-hatch = ใƒใƒƒใƒใƒณใ‚ฐ็ทจ้›† + .menu-drawing-limits = ๅ›ณ้ข็ฏ„ๅ›ฒ + .menu-donut = ใƒ‰ใƒผใƒŠใƒ„ + .menu-dimension-style = ๅฏธๆณ•ใ‚นใ‚ฟใ‚คใƒซ + .menu-construction-line = ๆง‹็ฏ‰็ทš + .menu-arc-length = ๅผง้•ท + .command-line-opacity = ใ‚ณใƒžใƒณใƒ‰ใƒฉใ‚คใƒณใฎไธ้€ๆ˜Žๅบฆ + .txtexp-no-text-selected = TXTEXP: ๆ–‡ๅญ—ใŒ้ธๆŠžใ•ใ‚Œใฆใ„ใพใ›ใ‚“ใ€‚ + .angle-override-shown = ่ง’ๅบฆใฎๅ„ชๅ…ˆ: __ocs_fmt_0__ .plot-preview-task = ๅฐๅˆทใƒ—ใƒฌใƒ“ใƒฅใƒผ .link-a-csv-data-source = CSV ใƒ‡ใƒผใ‚ฟ ใ‚ฝใƒผใ‚นใ‚’ใƒชใƒณใ‚ฏ .csv-files = CSV ใƒ•ใ‚กใ‚คใƒซ .browser-cannot-open-typed-path = ใƒ–ใƒฉใ‚ฆใ‚ถใฏๅ…ฅๅŠ›ใ•ใ‚ŒใŸใƒ‘ใ‚นใ‚’้–‹ใ‘ใพใ›ใ‚“ใ€‚ใƒ•ใ‚กใ‚คใƒซใ‚’้ธๆŠžใ—ใฆใใ ใ•ใ„ใ€‚ .recover-drawing = ๅ›ณ้ขใฎๅพฉๅ…ƒ + .requires-0-or-1-or-keyword = 0 ใพใŸใฏ 1ใ€ใ‚ใ‚‹ใ„ใฏใ‚ชใƒ—ใ‚ทใƒงใƒณใฎใ‚ญใƒผใƒฏใƒผใƒ‰ใฎใฟๆŒ‡ๅฎšใงใใพใ™ใ€‚ + .requires-a-number = ๆ•ฐๅ€คใ‚’ๆŒ‡ๅฎšใ—ใฆใใ ใ•ใ„ใ€‚ .restore-it-as-a-recovered-tab-or-discard-the-copy = ๅพฉๅ…ƒใ‚ฟใƒ–ใจใ—ใฆ้–‹ใใ‹ใ€ใ‚ณใƒ”ใƒผใ‚’็ ดๆฃ„ใ—ใฆใใ ใ•ใ„ใ€‚ .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" ใซใฏๆœ€็ต‚ไฟๅญ˜ใ‚ˆใ‚Šๆ–ฐใ—ใ„่‡ชๅ‹•ไฟๅญ˜ใ‚ณใƒ”ใƒผใŒใ‚ใ‚Šใพใ™ใ€‚ .autosave-copy-could-not-be-stored = ่‡ชๅ‹•ไฟๅญ˜ใ‚ณใƒ”ใƒผใ‚’ไฟๅญ˜ใงใใพใ›ใ‚“ใงใ—ใŸ: __ocs_fmt_0__ @@ -812,6 +886,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: ๆ–ญ้ขใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’้ธๆŠžใ™ใ‚‹ใ‹ใ€ใƒฉใ‚คใƒ–ๆ–ญ้ขใ‚’ใ‚ชใƒณใซใ—ใฆใใ ใ•ใ„ใ€‚ + .sectioncull-prepare-failed = SECTIONCULL: ๅˆ‡ๆ–ญใ—ใŸใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’ๆบ–ๅ‚™ใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ไฝ•ใ‚‚ๅค‰ๆ›ดใ•ใ‚Œใฆใ„ใพใ›ใ‚“ใ€‚ + .sectioncull-nothing-outside = SECTIONCULL: ๆ–ญ้ขใฎๅค–ๅดใซใฏไฝ•ใ‚‚ใ‚ใ‚Šใพใ›ใ‚“ใ€‚ + .sectioncull-result = SECTIONCULL: __ocs_arg_erased__ ๅ€‹ใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’ๅ‰Š้™คใ—ใ€ๆ–ญ้ขใฎๅขƒ็•Œใง __ocs_arg_cut__ ๅ€‹ใ‚’ๅˆ‡ๆ–ญใ—ใพใ—ใŸใ€‚ .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ ็‚นใ‚’้€šใ‚‹ใ‚นใƒ—ใƒฉใ‚คใƒณใ‚’ไฝœๆˆใ—ใพใ—ใŸใ€‚ .splinefit-need-at-least-3-points = SPLINEFIT: 3 ็‚นไปฅไธŠๅฟ…่ฆใงใ™ใ€‚ .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: ใ‚นใƒ—ใƒฉใ‚คใƒณใ‚’้€šใ™ใƒใƒชใƒฉใ‚คใƒณใ‚’้ธๆŠžใ—ใฆใใ ใ•ใ„ใ€‚ @@ -849,9 +927,12 @@ common = .sent-to-printer-printer = ใƒ—ใƒชใƒณใ‚ฟใƒผใธ้€ไฟกใ—ใพใ—ใŸ: __ocs_fmt_0__ .set-current = ็พๅœจใซ่จญๅฎš .shade-plot = ใ‚ทใ‚งใƒผใƒ‡ใ‚ฃใƒณใ‚ฐๅฐๅˆท + .shaded-with-edges = ้™ฐๅฝฑไป˜ใ(ใ‚จใƒƒใ‚ธ่กจ็คบ) + .shades-of-gray = ใ‚ฐใƒฌใƒผใ‚นใ‚ฑใƒผใƒซ .shadow-flags = ๅฝฑใƒ•ใƒฉใ‚ฐ .single = ๅ˜ไธ€ .size = ใ‚ตใ‚คใ‚บ + .sketchy = ใ‚นใ‚ฑใƒƒใƒ .snap-override-label-next-pick-only = ใ‚นใƒŠใƒƒใƒ—ๅ„ชๅ…ˆ: __ocs_fmt_0__๏ผˆๆฌกใฎ้ธๆŠžใฎใฟ๏ผ‰ใ€‚ .solid-created = ใ‚ฝใƒชใƒƒใƒ‰ใ‚’ไฝœๆˆใ—ใพใ—ใŸใ€‚ .source-object-not-found = ๅ…ƒใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใ€‚ @@ -1057,7 +1138,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: Leftใ€Topใ€Right ใฎใ„ใšใ‚Œใ‹ใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚ .isodraft-expected-on-or-off = ISODRAFT: On ใพใŸใฏ Off ใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚ .xdata-selected-entities-are-on-locked-layers = XDATA: ้ธๆŠžใ—ใŸๅ›ณๅฝขใฏใƒญใƒƒใ‚ฏใ•ใ‚ŒใŸ็”ปๅฑคไธŠใซใ‚ใ‚Šใพใ™ใ€‚ - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: ใƒฉใ‚นใ‚ฟใƒผ็”ปๅƒใŒ้ธๆŠžใ•ใ‚Œใฆใ„ใชใ„ใ‹ใ€ใƒ—ใƒญใƒ‘ใƒ†ใ‚ฃใŒไธๆ˜Žใงใ™๏ผˆBRIGHTNESS/CONTRAST/FADE ใ‚’ไฝฟ็”จ๏ผ‰ใ€‚ .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: -4 ใ‹ใ‚‰ 4 ใพใงใฎๆ•ดๆ•ฐใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚ .usage-datalink-path-to-csv = ไฝฟ็”จๆณ•: DATALINK <.csvใฎใƒ‘ใ‚น> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: ใƒชใƒณใ‚ฏใ•ใ‚ŒใŸ่กจใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใ€‚ @@ -2050,7 +2130,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = ใƒ•ใ‚กใ‚คใƒซ .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2359,6 +2439,7 @@ common = .load-linetypes = ็ทš็จฎใ‚’ใƒญใƒผใƒ‰ .requires-two-positive-values = 2ใคใฎๆญฃใฎๅ€ค (x,y)ใ€ใพใŸใฏไธกๆ–นใซๅ…ฑ้€šใฎ1ใคใฎๅ€คใŒๅฟ…่ฆใงใ™ใ€‚ .snap-marker-color = ใ‚นใƒŠใƒƒใƒ— ใƒžใƒผใ‚ซใƒผใฎ่‰ฒ + .snap-marker-size = ใ‚นใƒŠใƒƒใƒ— ใƒžใƒผใ‚ซใƒผใฎใ‚ตใ‚คใ‚บ .command-line-text-color = ใ‚ณใƒžใƒณใƒ‰ใƒฉใ‚คใƒณใฎๆ–‡ๅญ—่‰ฒ .operand = ใ‚ชใƒšใƒฉใƒณใƒ‰ .linetypes-loaded = __ocs_fmt_0__ ๅ€‹ใฎ็ทš็จฎใ‚’ใƒญใƒผใƒ‰ใ—ใพใ—ใŸใ€‚ @@ -2372,6 +2453,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE ๅŸบ็‚นใ‚’ๆŒ‡ๅฎš [__ocs_arg_count__ ๅ€‹]: .threedrotate-specify-rotation-axis = 3DROTATE ๅ›ž่ปข่ปธ [X/Y/Z] ใพใŸใฏ่ปธไธŠใฎ 2 ็‚น็›ฎใ‚’ๆŒ‡ๅฎš : .threedrotate-specify-rotation-angle-a = 3DROTATE ๅ›ž่ปข่ง’ๅบฆใ‚’ๆŒ‡ๅฎš <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = ไฟๅญ˜ใฎ่ญฆๅ‘Š .unable-to-save-drawing = ๅ›ณ้ขใ‚’ไฟๅญ˜ใงใใพใ›ใ‚“ @@ -2995,6 +3083,7 @@ draw = .arc-specify-radius-of-arc = ARC ๅ††ๅผงใฎๅŠๅพ„ใ‚’ๆŒ‡ๅฎš: .chord-length = ๅผฆใฎ้•ทใ• + modify = .add-vertex = ้ ‚็‚นใ‚’่ฟฝๅŠ  .closed-cross = ้–‰ใ˜ใŸไบคๅทฎ @@ -3040,6 +3129,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT ๅฑžๆ€งไป˜ใใƒ–ใƒญใƒƒใ‚ฏใ‚’้ธๆŠž: .annotation-object-scale = ๆณจ้‡ˆใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆๅฐบๅบฆ .bedit-select-block-reference-to-edit = BEDIT ็ทจ้›†ใ™ใ‚‹ใƒ–ใƒญใƒƒใ‚ฏๅ‚็…งใ‚’้ธๆŠž: + .bvstate-option = BVSTATE ใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’ๅ…ฅๅŠ› [ๆ–ฐ่ฆ/่จญๅฎš/ๅๅ‰ๅค‰ๆ›ด/ๅ‰Š้™ค] <่จญๅฎš>: + .bvstate-new-name = BVSTATE ๆ–ฐใ—ใ„ๅฏ่ฆ–ๆ€ง็Šถๆ…‹ใฎๅๅ‰ใ‚’ๅ…ฅๅŠ›: + .bvstate-set = BVSTATE ็พๅœจใซใ™ใ‚‹ๅฏ่ฆ–ๆ€ง็Šถๆ…‹ใ‚’ๅ…ฅๅŠ› <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE ๅๅ‰ใ‚’ๅค‰ๆ›ดใ™ใ‚‹ๅฏ่ฆ–ๆ€ง็Šถๆ…‹ใ‚’ๅ…ฅๅŠ› <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE ๆ–ฐใ—ใ„ๅๅ‰ใ‚’ๅ…ฅๅŠ›: + .bvstate-delete = BVSTATE ๅ‰Š้™คใ™ใ‚‹ๅฏ่ฆ–ๆ€ง็Šถๆ…‹ใ‚’ๅ…ฅๅŠ›: + .bvstate-not-found = ๅฏ่ฆ–ๆ€ง็Šถๆ…‹ใ€Œ__ocs_fmt_0__ใ€ใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใ€‚ + .bvstate-exists = ๅฏ่ฆ–ๆ€ง็Šถๆ…‹ใ€Œ__ocs_fmt_0__ใ€ใฏๆ—ขใซๅญ˜ๅœจใ—ใพใ™ใ€‚ + .bvstate-last = ๅ”ฏไธ€ใฎๅฏ่ฆ–ๆ€ง็Šถๆ…‹ใฏๅ‰Š้™คใงใใพใ›ใ‚“ใ€‚ + .bvstate-needs-bedit = __ocs_fmt_0__: ๅ…ˆใซใƒ–ใƒญใƒƒใ‚ฏ ใ‚จใƒ‡ใ‚ฃใ‚ฟใงใƒ–ใƒญใƒƒใ‚ฏใ‚’้–‹ใ„ใฆใใ ใ•ใ„ (BEDIT)ใ€‚ + .bvstate-current = ็พๅœจใฎๅฏ่ฆ–ๆ€ง็Šถๆ…‹: __ocs_fmt_0__ + .bvhide-select = ้ž่กจ็คบใซใ™ใ‚‹ใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’้ธๆŠž: + .bvshow-select = ่กจ็คบใ™ใ‚‹ใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’้ธๆŠž: + .arrayedit-select-array = ARRAYEDIT ้…ๅˆ—่ค‡ๅ†™ใ‚’้ธๆŠž: + .arrayedit-options = ARRAYEDIT ใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’ๅ…ฅๅŠ› [ๅ…ƒ/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: ้ธๆŠžใ—ใŸใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใฏ่‡ชๅ‹•่ชฟๆ•ด้…ๅˆ—่ค‡ๅ†™ใงใฏใ‚ใ‚Šใพใ›ใ‚“ใ€‚ .blend-enter-continuity-tangent-curvature-c = BLEND ้€ฃ็ถšๆ€งใ‚’ๅ…ฅๅŠ› [ๆŽฅ็ทš/ๆ›ฒ็އ] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND 1 ๆœฌ็›ฎใฎ้–‹ใ„ใŸๆ›ฒ็ทšใ‚’้ธๆŠž [้€ฃ็ถšๆ€ง=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND 2 ๆœฌ็›ฎใฎ้–‹ใ„ใŸๆ›ฒ็ทšใ‚’้ธๆŠž [้€ฃ็ถšๆ€ง=__ocs_arg_c__]: @@ -3592,6 +3697,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE ๆŒฟๅ…ฅ็‚นใ‚’ๆŒ‡ๅฎš: insert = + .pastespec-specify-insertion-point = PASTESPEC ๆŒฟๅ…ฅ็‚นใ‚’ๆŒ‡ๅฎš: + .pastespec-paste-as = PASTESPEC ่ฒผใ‚Šไป˜ใ‘ๅฝขๅผ [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3708,6 +3815,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3760,6 +3874,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *ใ‚ขใƒณใƒ€ใƒผใƒฌใ‚ค O ใ‚นใƒŠใƒƒใƒ—ใฏไธ€ๅฎšใงใชใ„* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3899,6 +4014,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = ใ‚คใƒกใƒผใ‚ธใพใŸใฏใ‚ขใƒณใƒ€ใƒผใƒฌใ‚คใ‚’้ธๆŠž: + .adjust-select-pdf-underlay = PDF ใ‚ขใƒณใƒ€ใƒผใƒฌใ‚คใ‚’้ธๆŠž: + .adjust-select-dwf-underlay = DWF ใ‚ขใƒณใƒ€ใƒผใƒฌใ‚คใ‚’้ธๆŠž: + .adjust-select-dgn-underlay = DGN ใ‚ขใƒณใƒ€ใƒผใƒฌใ‚คใ‚’้ธๆŠž: + .adjust-mixed-underlays = *้ธๆŠžใ‚ปใƒƒใƒˆใŒ็„กๅŠนใงใ™ใ€‚ๅŒใ˜ใƒ•ใ‚กใ‚คใƒซ ใ‚ฟใ‚คใƒ—ใฎใ‚ขใƒณใƒ€ใƒผใƒฌใ‚คใฎใฟใ‚’้ธๆŠžใ—ใฆใใ ใ•ใ„ใ€‚* + .adjust-image-option = ใ‚คใƒกใƒผใ‚ธ ใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’ๅ…ฅๅŠ› [ใ‚ณใƒณใƒˆใƒฉใ‚นใƒˆ/ใƒ•ใ‚งใƒผใƒ‰/ๆ˜Žใ‚‹ใ•] <ๆ˜Žใ‚‹ใ•>: + .adjust-underlay-option = __ocs_arg_kind__ ใ‚ขใƒณใƒ€ใƒผใƒฌใ‚ค ใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’ๅ…ฅๅŠ› [ใƒ•ใ‚งใƒผใƒ‰/ใ‚ณใƒณใƒˆใƒฉใ‚นใƒˆ/ใƒขใƒŽใ‚ฏใƒญ] <__ocs_arg_option__>: + .adjust-monochrome = ใƒขใƒŽใ‚ฏใƒญ? [ใฏใ„/ใ„ใ„ใˆ] <__ocs_arg_value__>: + .adjust-brightness-value = ๆ˜Žใ‚‹ใ•ใฎๅ€คใ‚’ๅ…ฅๅŠ› (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = ใ‚ณใƒณใƒˆใƒฉใ‚นใƒˆใฎๅ€คใ‚’ๅ…ฅๅŠ› (0-100) <__ocs_arg_value__>: + .adjust-fade-value = ใƒ•ใ‚งใƒผใƒ‰ใฎๅ€คใ‚’ๅ…ฅๅŠ› (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = ๅ€คใฏ 0 ใ‹ใ‚‰ 100 ใพใงใฎๆ•ดๆ•ฐใงใชใ‘ใ‚Œใฐใชใ‚Šใพใ›ใ‚“ใ€‚ .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4403,7 +4530,27 @@ model = .value-2point = 2 ็‚น .cube = ็ซ‹ๆ–นไฝ“ + layout = + .mvsetup-select-objects-to-delete = MVSETUP ๅ‰Š้™คใ™ใ‚‹ใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’้ธๆŠž: + .delete-objects-option = ใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’ๅ‰Š้™ค + .mvsetup-enable-paper-space = MVSETUP ใƒšใƒผใƒ‘ใƒผ็ฉบ้–“ใ‚’ๆœ‰ๅŠนใซใ—ใพใ™ใ‹? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP ๅฐบๅบฆใ‚’ๅ…ฅๅŠ› <1>: + .mvsetup-enter-paper-width = MVSETUP ็”จ็ด™ใฎๅน…ใ‚’ๅ…ฅๅŠ›: + .mvsetup-enter-paper-height = MVSETUP ็”จ็ด™ใฎ้ซ˜ใ•ใ‚’ๅ…ฅๅŠ›: + .mvsetup-enter-option = MVSETUP ใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’ๅ…ฅๅŠ› [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP ใƒ“ใƒฅใƒผใƒใƒผใƒˆใฎ้…็ฝฎใ‚’ๅ…ฅๅŠ› [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP ใƒ“ใƒฅใƒผใƒใƒผใƒˆ้ ˜ๅŸŸใฎๆœ€ๅˆใฎใ‚ณใƒผใƒŠใƒผใ‚’ๆŒ‡ๅฎš : + .mvsetup-specify-opposite-corner = MVSETUP ใ‚‚ใ†ไธ€ๆ–นใฎใ‚ณใƒผใƒŠใƒผใ‚’ๆŒ‡ๅฎš: + .mvsetup-viewports-in-x = MVSETUP X ๆ–นๅ‘ใฎใƒ“ใƒฅใƒผใƒใƒผใƒˆๆ•ฐใ‚’ๅ…ฅๅŠ› <2>: + .mvsetup-viewports-in-y = MVSETUP Y ๆ–นๅ‘ใฎใƒ“ใƒฅใƒผใƒใƒผใƒˆๆ•ฐใ‚’ๅ…ฅๅŠ› <2>: + .mvsetup-distance-in-x = MVSETUP X ๆ–นๅ‘ใฎใƒ“ใƒฅใƒผใƒใƒผใƒˆ้–“้š”ใ‚’ๆŒ‡ๅฎš <0>: + .mvsetup-distance-in-y = MVSETUP Y ๆ–นๅ‘ใฎใƒ“ใƒฅใƒผใƒใƒผใƒˆ้–“้š”ใ‚’ๆŒ‡ๅฎš <0>: + .mvsetup-select-viewports = MVSETUP ๅฐบๅบฆใ‚’่จญๅฎšใ™ใ‚‹ใƒ“ใƒฅใƒผใƒใƒผใƒˆใ‚’้ธๆŠž: + .mvsetup-paper-space-units = MVSETUP ใƒšใƒผใƒ‘ใƒผ็ฉบ้–“ใฎๅ˜ไฝๆ•ฐใ‚’ๅ…ฅๅŠ› <1>: + .mvsetup-model-space-units = MVSETUP ใƒขใƒ‡ใƒซ็ฉบ้–“ใฎๅ˜ไฝๆ•ฐใ‚’ๅ…ฅๅŠ› <1>: + .mvsetup-viewports-do-not-fit = MVSETUP ใƒ“ใƒฅใƒผใƒใƒผใƒˆใŒ้ ˜ๅŸŸใซๅŽใพใ‚Šใพใ›ใ‚“ใ€‚ + .scale-viewports-option = ใƒ“ใƒฅใƒผใƒใƒผใƒˆใฎๅฐบๅบฆ .active = ใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ– .inactive = ้žใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ– .insert-view = ใƒ“ใƒฅใƒผใ‚’ๆŒฟๅ…ฅ @@ -4429,6 +4576,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = ใƒ•ใƒชใƒผใ‚บ | T = ใƒ•ใƒชใƒผใ‚บ่งฃ้™ค | Enter = ็ต‚ไบ† view = + .twist-option = ใญใ˜ใ‚Œ + .dview-specify-view-twist-angle = DVIEW ใƒ“ใƒฅใƒผใฎใญใ˜ใ‚Œ่ง’ๅบฆใ‚’ๆŒ‡ๅฎš <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW ใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’ๅ…ฅๅŠ› [TWist/Undo]: .color-scheme-set-to-system-currently-name = ใ‚ซใƒฉใƒผใ‚นใ‚ญใƒผใƒ ใ‚’ใ€Œใ‚ทใ‚นใƒ†ใƒ ใ€ใซ่จญๅฎšใ—ใพใ—ใŸ๏ผˆ็พๅœจ: __ocs_fmt_0__๏ผ‰ใ€‚ .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = __ocs_fmt_0__ ใ‹ใ‚‰ __ocs_fmt_1__ ใพใงใฎๆ•ดๆ•ฐใŒๅฟ…่ฆใงใ™ใ€‚ @@ -4623,13 +4773,6 @@ view = .x-no-xdata = __ocs_fmt_0__: ๆ‹กๅผตใƒ‡ใƒผใ‚ฟใชใ—ใ€‚ .value-3d-orbit = 3D ใ‚ชใƒผใƒ“ใƒƒใƒˆ .value-3d-orbit-drag-with-right-mouse-button = 3D ใ‚ชใƒผใƒ“ใƒƒใƒˆ: ใƒžใ‚ฆใ‚นๅณใƒœใ‚ฟใƒณใงใƒ‰ใƒฉใƒƒใ‚ฐใ—ใพใ™ใ€‚ - .adjust-brightness-contrast-fade = ADJUST [ๆ˜Žใ‚‹ใ• / ใ‚ณใƒณใƒˆใƒฉใ‚นใƒˆ / ใƒ•ใ‚งใƒผใƒ‰]: - .adjust-brightness-0-100 = ADJUST ๆ˜Žใ‚‹ใ• 0-100: - .adjust-contrast-0-100 = ADJUST ใ‚ณใƒณใƒˆใƒฉใ‚นใƒˆ 0-100: - .adjust-fade-0-100 = ADJUST ใƒ•ใ‚งใƒผใƒ‰ 0-100: - .adjust-select-raster-image-s-first = ADJUST: ๅ…ˆใซใƒฉใ‚นใ‚ฟใƒผ็”ปๅƒใ‚’้ธๆŠžใ—ใฆใใ ใ•ใ„ใ€‚ - .adjust-value-must-be-0-100 = ADJUST: ๅ€คใฏ 0๏ฝž100 ใซใ—ใฆใใ ใ•ใ„ใ€‚ - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_2__ ๅ€‹ใฎ็”ปๅƒใง __ocs_fmt_0__ = __ocs_fmt_1__ ใซๅค‰ๆ›ดใ—ใพใ—ใŸใ€‚ .annoallvisible-new-value-0-1 = ANNOALLVISIBLE ๆ–ฐใ—ใ„ๅ€ค [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: 0 ใพใŸใฏ 1 ใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚ .annoautoscale-new-value-4-4 = ANNOAUTOSCALE ๆ–ฐใ—ใ„ๅ€ค [-4..4]: @@ -4755,7 +4898,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = ใ‚ขใƒณใƒ€ใƒผใƒฌใ‚ค __ocs_fmt_0__: ใƒ•ใ‚งใƒผใƒ‰=__ocs_fmt_1__ใ€ใ‚ณใƒณใƒˆใƒฉใ‚นใƒˆ=__ocs_fmt_2__ใ€่กจ็คบ=__ocs_fmt_3__ใ€ใ‚ฏใƒชใƒƒใƒ—=__ocs_fmt_4__ใ€ใƒขใƒŽใ‚ฏใƒญ=__ocs_fmt_5__ .updated-changed-underlay-s = __ocs_fmt_0__ ๅ€‹ใฎใ‚ขใƒณใƒ€ใƒผใƒฌใ‚คใ‚’ๆ›ดๆ–ฐใ—ใพใ—ใŸใ€‚ - .usage-adjust-brightness-contrast-fade-0-100 = ไฝฟ็”จๆณ•: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = ไฝฟ็”จๆณ•: ANNOSCALE ไพ‹: 1:50ใ€2:1ใ€ใพใŸใฏไฟ‚ๆ•ฐ .usage-draworder-f-front-b-back-a-above-handle = ไฝฟ็”จๆณ•: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = ไฝฟ็”จๆณ•: HYPERLINK ๏ผˆๅ…ˆใซใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’้ธๆŠž๏ผ‰ @@ -5983,6 +6125,46 @@ ui = .osnap-settings = ใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚นใƒŠใƒƒใƒ—่จญๅฎš... .snap-override-none-next-pick-only = ๅ„ชๅ…ˆใ‚นใƒŠใƒƒใƒ—: ใชใ— (ๆฌกใฎ็‚นใฎใฟ)ใ€‚ + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = ๅฏธๆณ•ๅ€คใ‚’ๅธธใซๅฏธๆณ•่ฃœๅŠฉ็ทšใฎ้–“ใซ้…็ฝฎ .angular-dimensions = ่ง’ๅบฆๅฏธๆณ• @@ -6250,3 +6432,117 @@ graphic-attributes = .reset-angle = ่ง’ๅบฆใ‚’ใƒชใ‚ปใƒƒใƒˆ .one-color = 1 ่‰ฒ .two-colors = 2 ่‰ฒ + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = ไฝœๆฅญใ‚ปใƒƒใƒˆใซ่ฟฝๅŠ  + .remove-from-working-set = ไฝœๆฅญใ‚ปใƒƒใƒˆใ‹ใ‚‰้™คๅŽป + .toolbar-objects = ๅ€‹ใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆ + .dialog-title = ๅ‚็…ง็ทจ้›† + .dialog-reference = ็ทจ้›†ใ™ใ‚‹ๅ‚็…ง + .dialog-chain-hint = ้ธๆŠžใ—ใŸ็‚นใฎใƒใ‚นใƒˆ; ใƒใƒƒใƒ—ใ‚’ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆใƒฌใƒ™ใƒซใ‚’ๅค‰ๆ›ดใ—ใพใ™ใ€‚ + .dialog-path = ใƒ‘ใ‚น: + .dialog-working-set = ไฝœๆฅญใ‚ปใƒƒใƒˆ + .dialog-all-objects = ใ™ในใฆใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆ + .dialog-pick-objects = ๅ›ณ้ขใง้ธๆŠž + .dialog-attributes = ๅฑžๆ€งๅฎš็พฉใ‚‚็ทจ้›† + .dialog-session = ใ‚ปใƒƒใ‚ทใƒงใƒณ + .dialog-lock = ไฝœๆฅญใ‚ปใƒƒใƒˆๅค–ใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’ใƒญใƒƒใ‚ฏ + .dialog-unique = ้‡่ค‡ใ™ใ‚‹็”ปๅฑคใ€ใ‚นใ‚ฟใ‚คใƒซใ€ใƒ–ใƒญใƒƒใ‚ฏๅใซๅ›บๆœ‰ใฎๅๅ‰ใ‚’ไป˜ใ‘ใ‚‹ + .dialog-start = ็ทจ้›†ใ‚’้–‹ๅง‹ + .dialog-help = ใƒ–ใƒญใƒƒใ‚ฏๅ‚็…งใพใŸใฏๅค–้ƒจๅ‚็…งใ‚’ใใฎๅ ดใง็ทจ้›†ใงใใ‚‹ใ‚ˆใ†ใซ้–‹ใใพใ™ใ€‚ + .select-reference = ๅ‚็…งใ‚’้ธๆŠž: + .reference-not-found = ๅ‚็…งใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใ€‚ + .not-valid-reference = ใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใฏๆœ‰ๅŠนใชๅ‚็…งใงใฏใ‚ใ‚Šใพใ›ใ‚“ + .one-entity-added = 1 ๅ€‹ใฎๅ›ณๅฝขใ‚’่ฟฝๅŠ ใ—ใพใ—ใŸ + .nesting-level = ใƒใ‚นใƒˆใฎใƒฌใƒ™ใƒซใ‚’้ธๆŠž [OK/ๆฌก] <ๆฌก>: + .selection-method = ใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆ้ธๆŠžๆ–นๆณ•ใ‚’ๅ…ฅๅŠ› [ใ™ในใฆ/ใƒใ‚นใƒˆ] <ใ™ในใฆ>: + .select-nested = ใƒใ‚นใƒˆใ•ใ‚ŒใŸใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’้ธๆŠž: + .display-attdefs = ๅฑžๆ€งๅฎš็พฉใ‚’่กจ็คบ [ใฏใ„/ใ„ใ„ใˆ] <ใ„ใ„ใˆ>: + .zero-items-selected = 0 ๅ€‹ใฎ้ …็›ฎใ‚’้ธๆŠž + .one-item-selected = 1 ๅ€‹ใฎ้ …็›ฎใ‚’้ธๆŠž + .items-selected = __ocs_fmt_0__ ๅ€‹ใฎ้ …็›ฎใ‚’้ธๆŠž + .refset-option = ใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’ๅ…ฅๅŠ› [่ฟฝๅŠ /้™คๅŽป] <่ฟฝๅŠ >: + .refclose-option = ใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’ๅ…ฅๅŠ› [ไฟๅญ˜/ๅ‚็…งใฎๅค‰ๆ›ดใ‚’็ ดๆฃ„] <ไฟๅญ˜>: + .not-checked-out = ** REFEDIT ใ‚ณใƒžใƒณใƒ‰ใงๅ‚็…งใ‚’็ทจ้›†ไธญใงใชใ„้™ใ‚Šใ€ใ“ใฎใ‚ณใƒžใƒณใƒ‰ใฏไฝฟ็”จใงใใพใ›ใ‚“ ** + .already-checked-out = ** ใ‚ณใƒžใƒณใƒ‰ใฏไฝฟ็”จใงใใพใ›ใ‚“ใ€‚__ocs_fmt_0__ ใฏๆ—ขใซ็ทจ้›†ไธญใงใ™ ** + .transfer-objects = RefEdit ไฝœๆฅญใ‚ปใƒƒใƒˆใจใƒ›ใ‚นใƒˆๅ›ณ้ขใฎ้–“ใงใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’็งปๅ‹•... + .added-to-working-set = __ocs_fmt_0__ ๅ€‹ใ‚’ไฝœๆฅญใ‚ปใƒƒใƒˆใซ่ฟฝๅŠ ใ—ใพใ—ใŸ + .removed-from-working-set = __ocs_fmt_0__ ๅ€‹ใ‚’ไฝœๆฅญใ‚ปใƒƒใƒˆใ‹ใ‚‰้™คๅŽปใ—ใพใ—ใŸ + .one-object-added = 1 ๅ€‹ใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’ __ocs_fmt_0__ ใซ่ฟฝๅŠ ใ—ใพใ—ใŸ + .objects-added = __ocs_fmt_0__ ๅ€‹ใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’ __ocs_fmt_1__ ใซ่ฟฝๅŠ ใ—ใพใ—ใŸ + .one-object-removed = 1 ๅ€‹ใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’ __ocs_fmt_0__ ใ‹ใ‚‰้™คๅŽปใ—ใพใ—ใŸ + .objects-removed = __ocs_fmt_0__ ๅ€‹ใฎใ‚ชใƒ–ใ‚ธใ‚งใ‚ฏใƒˆใ‚’ __ocs_fmt_1__ ใ‹ใ‚‰้™คๅŽปใ—ใพใ—ใŸ + .one-xref-instance = 1 ๅ€‹ใฎๅค–้ƒจๅ‚็…งใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใ‚’ๆ›ดๆ–ฐใ—ใพใ—ใŸ + .xref-instances = __ocs_fmt_0__ ๅ€‹ใฎๅค–้ƒจๅ‚็…งใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใ‚’ๆ›ดๆ–ฐใ—ใพใ—ใŸ + .one-block-instance = 1 ๅ€‹ใฎใƒ–ใƒญใƒƒใ‚ฏ ใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใ‚’ๆ›ดๆ–ฐใ—ใพใ—ใŸ + .block-instances = __ocs_fmt_0__ ๅ€‹ใฎใƒ–ใƒญใƒƒใ‚ฏ ใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใ‚’ๆ›ดๆ–ฐใ—ใพใ—ใŸ + .redefined-reloaded = __ocs_fmt_0__ ใ‚’ๅ†ๅฎš็พฉใ—ใ€ๅ†ใƒญใƒผใƒ‰ใ—ใพใ—ใŸใ€‚ + .redefined = __ocs_fmt_0__ ใ‚’ๅ†ๅฎš็พฉใ—ใพใ—ใŸใ€‚ + .ok = OK + .nested = ใƒใ‚นใƒˆ + +image-adjust = + .title = ใ‚คใƒกใƒผใ‚ธ่ชฟๆ•ด + .preview = ใƒ—ใƒฌใƒ“ใƒฅใƒผ + .before = ๅ‰ + .after = ๅพŒ + .no-preview = ใ‚คใƒกใƒผใ‚ธใ‚’่ชญใฟ่พผใ‚ใพใ›ใ‚“ใงใ—ใŸ + .adjust = ่ชฟๆ•ด + .brightness = ๆ˜Žใ‚‹ใ• + .contrast = ใ‚ณใƒณใƒˆใƒฉใ‚นใƒˆ + .fade = ใƒ•ใ‚งใƒผใƒ‰ + .hint = ใƒใƒผใ‚’ใ‚ฏใƒชใƒƒใ‚ฏใพใŸใฏใƒ‰ใƒฉใƒƒใ‚ฐใ€‚โˆ’ / + ใง 1 ใ‚นใƒ†ใƒƒใƒ—ใ€ใƒœใƒƒใ‚ฏใ‚นใซใฏ 0๏ฝž100 ใฎๆ•ดๆ•ฐใ‚’ๅ…ฅๅŠ›ใ€‚ + .reset = ๆ—ขๅฎšๅ€คใซๆˆปใ™ + .apply = ้ฉ็”จ + .help = ้ธๆŠžใ—ใŸใƒฉใ‚นใ‚ฟใƒผ ใ‚คใƒกใƒผใ‚ธใฎๆ˜Žใ‚‹ใ•ใ€ใ‚ณใƒณใƒˆใƒฉใ‚นใƒˆใ€ใƒ•ใ‚งใƒผใƒ‰ใ‚’่จญๅฎšใ—ใพใ™ใ€‚ + +keytips = + .title = KeyTip + .typed = ๅ…ฅๅŠ›: { $path } + .hint-tabs = ใ‚ฟใƒ–ใฎๆ–‡ๅญ—ใ‚’ๅ…ฅๅŠ› ยท Esc ใง้–‰ใ˜ใ‚‹ + .hint-tab = { $tab } ใ‚ฟใƒ– ยท Esc ใง 1 ใคไธŠใซๆˆปใ‚‹ + .hint-items = ้ …็›ฎใฎๆ–‡ๅญ—ใ‚’ๅ…ฅๅŠ› ยท Esc ใง 1 ใคไธŠใซๆˆปใ‚‹ diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 1bbaea5c3..8bfb9fc12 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = ํ”Œ๋Ÿฌ๊ทธ์ธ ribbon-tab = + .block-editor = ๋ธ”๋ก ํŽธ์ง‘๊ธฐ .draw = ๊ทธ๋ฆฌ๊ธฐ .parametric = ํŒŒ๋ผ๋ฉ”ํŠธ๋ฆญ .annotate = ์ฃผ์„ @@ -98,6 +99,9 @@ start = .no-recent-files = ์—ฐ ํŒŒ์ผ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. .browser-storage = ๋ธŒ๋ผ์šฐ์ € ์ €์žฅ์†Œ .keep-recent-files = ์ตœ๊ทผ ํŒŒ์ผ ์œ ์ง€ + .view-list = ๋ชฉ๋ก + .view-icons = ์•„์ด์ฝ˜ + .view-preview = ํฐ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ donation = .title = ๊ธฐ๋ถ€ ์š”์ฒญ @@ -167,6 +171,14 @@ modal = .that-object = ํ•ด๋‹น ๊ฐ์ฒด .those-objects = ํ•ด๋‹น ๊ฐ์ฒด๋“ค + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = ์ด๋ฏธ์ง€ "{ $name }" ๋ถ™์—ฌ๋„ฃ์Œ: { $width }ร—{ $height } px .image-unsupported = ๋ถ™์—ฌ๋„ฃ์€ ์ด๋ฏธ์ง€ "{ $name }"์„(๋ฅผ) ๋””์ฝ”๋”ฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. @@ -273,11 +285,73 @@ command-copy = .mode = COPY ๋ณต์‚ฌ ๋ชจ๋“œ ์˜ต์…˜ ์ž…๋ ฅ [Single/Multiple] : common = + .compare-prompt = COMPARE ์—ฌ๊ธฐ์—๋งŒ __ocs_fmt_0__ (๋…น์ƒ‰), ๋‹ค๋ฅธ ๋„๋ฉด์—๋งŒ __ocs_fmt_1__ (๋นจ๊ฐ„์ƒ‰)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ๋„๋ฉด๊ณผ ๋น„๊ต + .label-associative-array-created = __ocs_fmt_0__: ์—ฐ๊ด€ ๋ฐฐ์—ด์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. + .assoc-array-array-path = ๋ฐฐ์—ด (๊ฒฝ๋กœ) + .assoc-array-array-polar = ๋ฐฐ์—ด (์›ํ˜•) + .assoc-array-array-rectangular = ๋ฐฐ์—ด (์ง์‚ฌ๊ฐํ˜•) + .assoc-array-item-spacing = ํ•ญ๋ชฉ ๊ฐ„๊ฒฉ + .assoc-array-fill-angle = ์ฑ„์šฐ๊ธฐ ๊ฐ๋„ + .assoc-array-items = ํ•ญ๋ชฉ + .assoc-array-level-spacing = ๋ ˆ๋ฒจ ๊ฐ„๊ฒฉ + .assoc-array-levels = ๋ ˆ๋ฒจ + .assoc-array-row-spacing = ํ–‰ ๊ฐ„๊ฒฉ + .assoc-array-column-spacing = ์—ด ๊ฐ„๊ฒฉ + .fbxexport-exported-to = FBXEXPORT: "__ocs_fmt_0__"๋กœ ๋‚ด๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค. + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: ์ด ๋„๋ฉด์—๋Š” 3D ๋ฉ”์‰ฌ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = ์ƒˆ ํ•ด์น˜๊ฐ€ ๊ฒฝ๊ณ„๋ฅผ ๋”ฐ๋ฆ„ (HPASSOC) + .options-show-ribbon = ๋ฆฌ๋ณธ ํ‘œ์‹œ (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = ๋ฉ”๋‰ด ๋ง‰๋Œ€ ํ‘œ์‹œ (MENUBAR) + .options-window-elements = ์ฐฝ ์š”์†Œ + .menu-viewports = ๋ทฐํฌํŠธ + .menu-view-twist = ๋ทฐ ๋น„ํ‹€๊ธฐ + .menu-tools = ๋„๊ตฌ + .menu-table-style = ํ…Œ์ด๋ธ” ์Šคํƒ€์ผ + .menu-run-script = ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ + .menu-revision-cloud = ์ˆ˜์ • ๊ธฐํ˜ธ + .menu-regen-all = ๋ชจ๋‘ ์žฌ์ƒ์„ฑ + .menu-redraw = ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ + .menu-rectangle = ์ง์‚ฌ๊ฐํ˜• + .menu-quick-dimension = ๋น ๋ฅธ ์น˜์ˆ˜ + .menu-publish = ๊ฒŒ์‹œ + .menu-paste-special = ์„ ํƒํ•˜์—ฌ ๋ถ™์—ฌ๋„ฃ๊ธฐ + .menu-parametric = ํŒŒ๋ผ๋ฉ”ํŠธ๋ฆญ + .menu-page-setup-manager = ํŽ˜์ด์ง€ ์„ค์ • ๊ด€๋ฆฌ์ž + .menu-orbit = ๊ถค๋„ + .menu-new-ucs = ์ƒˆ UCS + .menu-named-views = ๋ช…๋ช…๋œ ๋ทฐ + .menu-multiline-text = ์—ฌ๋Ÿฌ ์ค„ ๋ฌธ์ž + .menu-multiline-style = ๋‹ค์ค‘์„  ์Šคํƒ€์ผ + .menu-multiline = ๋‹ค์ค‘์„  + .menu-multileader-style = ๋‹ค์ค‘ ์ง€์‹œ์„  ์Šคํƒ€์ผ + .menu-match-properties = ํŠน์„ฑ ์ผ์น˜ + .menu-lengthen = ๊ธธ์ด ์กฐ์ • + .menu-jogged = ๊บพ๊ธฐ + .menu-id-point = ID ์  + .menu-explode-text = ๋ฌธ์ž ๋ถ„ํ•ด + .menu-edit-spline = ์Šคํ”Œ๋ผ์ธ ํŽธ์ง‘ + .menu-edit-polyline = ํด๋ฆฌ์„  ํŽธ์ง‘ + .menu-edit-hatch = ํ•ด์น˜ ํŽธ์ง‘ + .menu-drawing-limits = ๋„๋ฉด ํ•œ๊ณ„ + .menu-donut = ๋„๋„› + .menu-dimension-style = ์น˜์ˆ˜ ์Šคํƒ€์ผ + .menu-construction-line = ๊ตฌ์„ฑ์„  + .menu-arc-length = ํ˜ธ ๊ธธ์ด + .command-line-opacity = ๋ช…๋ นํ–‰ ๋ถˆํˆฌ๋ช…๋„ + .txtexp-no-text-selected = TXTEXP: ์„ ํƒ๋œ ๋ฌธ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. + .angle-override-shown = ๊ฐ๋„ ์žฌ์ง€์ •: __ocs_fmt_0__ .plot-preview-task = ํ”Œ๋กฏ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ .link-a-csv-data-source = CSV ๋ฐ์ดํ„ฐ ์›๋ณธ ์—ฐ๊ฒฐ .csv-files = CSV ํŒŒ์ผ .browser-cannot-open-typed-path = ๋ธŒ๋ผ์šฐ์ €๋Š” ์ž…๋ ฅํ•œ ๊ฒฝ๋กœ๋ฅผ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํŒŒ์ผ์„ ์„ ํƒํ•˜์„ธ์š”. .recover-drawing = ๋„๋ฉด ๋ณต๊ตฌ + .requires-0-or-1-or-keyword = 0 ๋˜๋Š” 1, ๋˜๋Š” ์˜ต์…˜ ํ‚ค์›Œ๋“œ๋งŒ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + .requires-a-number = ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. .restore-it-as-a-recovered-tab-or-discard-the-copy = ๋ณต๊ตฌ๋œ ํƒญ์œผ๋กœ ์—ด๊ฑฐ๋‚˜ ์‚ฌ๋ณธ์„ ์‚ญ์ œํ•˜์„ธ์š”. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__"์— ๋งˆ์ง€๋ง‰ ์ €์žฅ๋ณด๋‹ค ์ƒˆ๋กœ์šด ์ž๋™ ์ €์žฅ ์‚ฌ๋ณธ์ด ์žˆ์Šต๋‹ˆ๋‹ค. .autosave-copy-could-not-be-stored = ์ž๋™ ์ €์žฅ ์‚ฌ๋ณธ์„ ์ €์žฅํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: __ocs_fmt_0__ @@ -821,6 +895,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: ๋‹จ๋ฉด ๊ฐ์ฒด๋ฅผ ์„ ํƒํ•˜๊ฑฐ๋‚˜ ๋ผ์ด๋ธŒ ๋‹จ๋ฉด์„ ์ผœ์‹ญ์‹œ์˜ค. + .sectioncull-prepare-failed = SECTIONCULL: ์ž˜๋ฆฐ ๊ฐ์ฒด๋ฅผ ์ค€๋น„ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์•„๋ฌด๊ฒƒ๋„ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. + .sectioncull-nothing-outside = SECTIONCULL: ๋‹จ๋ฉด ๋ฐ–์— ์žˆ๋Š” ๊ฐ์ฒด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. + .sectioncull-result = SECTIONCULL: ๊ฐ์ฒด __ocs_arg_erased__๊ฐœ๋ฅผ ์ง€์šฐ๊ณ  ๋‹จ๋ฉด ๊ฒฝ๊ณ„์—์„œ __ocs_arg_cut__๊ฐœ๋ฅผ ์ž˜๋ž์Šต๋‹ˆ๋‹ค. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ ์ ์„ ํ†ตํ•ด ์Šคํ”Œ๋ผ์ธ์„ ๋งž์ถฅ๋‹ˆ๋‹ค. .splinefit-need-at-least-3-points =SPLINEFIT: ์ตœ์†Œ 3๊ฐœ์˜ ํฌ์ธํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: ์Šคํ”Œ๋ผ์ธ์— ๋งž๊ฒŒ ํด๋ฆฌ์„ ์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. @@ -858,9 +936,12 @@ common = .sent-to-printer-printer = ํ”„๋ฆฐํ„ฐ๋กœ ์ „์†ก๋จ: __ocs_fmt_0__ .set-current = ํ˜„์žฌ ์„ค์ • .shade-plot = ์Œ์˜ ํ”Œ๋กฏ + .shaded-with-edges = ๋ชจ์„œ๋ฆฌ๋กœ ์Œ์˜์ฒ˜๋ฆฌ๋จ + .shades-of-gray = ํšŒ์ƒ‰ ์Œ์˜ .shadow-flags = ๊ทธ๋ฆผ์ž ๊นƒ๋ฐœ .single = ์‹ฑ๊ธ€ .size = ํฌ๊ธฐ + .sketchy = ์Šค์ผ€์น˜ .snap-override-label-next-pick-only = ์Šค๋ƒ… ์žฌ์ •์˜: __ocs_fmt_0__(๋‹ค์Œ ์„ ํƒ์—๋งŒ ํ•ด๋‹น) .solid-created = ์†”๋ฆฌ๋“œ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. .source-object-not-found = ์†Œ์Šค ๊ฐœ์ฒด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. @@ -1066,7 +1147,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: Left, Top ๋˜๋Š” Right๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. .isodraft-expected-on-or-off = ISODRAFT: On ๋˜๋Š” Off๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. .xdata-selected-entities-are-on-locked-layers = XDATA: ์„ ํƒํ•œ ๊ฐ์ฒด๊ฐ€ ์ž ๊ธด ๋„๋ฉด์ธต์— ์žˆ์Šต๋‹ˆ๋‹ค. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: ๋ž˜์Šคํ„ฐ ์ด๋ฏธ์ง€๋ฅผ ์„ ํƒํ•˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ์•Œ ์ˆ˜ ์—†๋Š” ์†์„ฑ์ž…๋‹ˆ๋‹ค(BRIGHTNESS/CONTRAST/FADE ์‚ฌ์šฉ). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: -4๋ถ€ํ„ฐ 4๊นŒ์ง€ ์ •์ˆ˜๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. .usage-datalink-path-to-csv = ์‚ฌ์šฉ๋ฒ•: DATALINK <.csv-๊ฒฝ๋กœ> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. @@ -2059,7 +2139,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = ํŒŒ์ผ .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2368,6 +2448,7 @@ common = .load-linetypes = ์„ ์ข…๋ฅ˜ ๋กœ๋“œ .requires-two-positive-values = ์–‘์ˆ˜ ๊ฐ’ ๋‘ ๊ฐœ(x,y) ๋˜๋Š” ๋‘˜ ๋‹ค์— ์“ธ ๊ฐ’ ํ•˜๋‚˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. .snap-marker-color = ์Šค๋ƒ… ๋งˆ์ปค ์ƒ‰์ƒ + .snap-marker-size = ์Šค๋ƒ… ๋งˆ์ปค ํฌ๊ธฐ .command-line-text-color = ๋ช…๋ น์ค„ ํ…์ŠคํŠธ ์ƒ‰์ƒ .operand = ํ”ผ์—ฐ์‚ฐ์ž .linetypes-loaded = ์„ ์ข…๋ฅ˜ __ocs_fmt_0__๊ฐœ๋ฅผ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค. @@ -2381,6 +2462,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE ๊ธฐ์ค€์  ์ง€์ • [__ocs_arg_count__ ๊ฐ์ฒด]: .threedrotate-specify-rotation-axis = 3DROTATE ํšŒ์ „ ์ถ• [X/Y/Z] ๋˜๋Š” ์ถ• ์œ„์˜ ๋‘ ๋ฒˆ์งธ ์  ์ง€์ • : .threedrotate-specify-rotation-angle-a = 3DROTATE ํšŒ์ „ ๊ฐ๋„ <__ocs_arg_a__> ์ง€์ •: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = ๊ฒฝ๊ณ  ์ €์žฅ .unable-to-save-drawing = ๋„๋ฉด์„ ์ €์žฅํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค @@ -3004,6 +3092,7 @@ draw = .arc-specify-radius-of-arc = ARC ํ˜ธ์˜ ๋ฐ˜์ง€๋ฆ„ ์ง€์ •: .chord-length = ํ˜„ ๊ธธ์ด + modify = .add-vertex = ์ •์  ์ถ”๊ฐ€ .closed-cross = ๋‹ซํžŒ ์‹ญ์ž @@ -3049,6 +3138,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT ์†์„ฑ์ด ์žˆ๋Š” ๋ธ”๋ก ์„ ํƒ: .annotation-object-scale = ์ฃผ์„ ๊ฐ์ฒด ์ถ•์ฒ™ .bedit-select-block-reference-to-edit = BEDIT ํŽธ์ง‘ํ•  ๋ธ”๋ก ์ฐธ์กฐ ์„ ํƒ: + .bvstate-option = BVSTATE ์˜ต์…˜ ์ž…๋ ฅ [์ƒˆ๋กœ ๋งŒ๋“ค๊ธฐ/์„ค์ •/์ด๋ฆ„ ๋ฐ”๊พธ๊ธฐ/์‚ญ์ œ] <์„ค์ •>: + .bvstate-new-name = BVSTATE ์ƒˆ ๊ฐ€์‹œ์„ฑ ์ƒํƒœ์˜ ์ด๋ฆ„ ์ž…๋ ฅ: + .bvstate-set = BVSTATE ํ˜„์žฌ๋กœ ์„ค์ •ํ•  ๊ฐ€์‹œ์„ฑ ์ƒํƒœ ์ž…๋ ฅ <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE ์ด๋ฆ„์„ ๋ฐ”๊ฟ€ ๊ฐ€์‹œ์„ฑ ์ƒํƒœ ์ž…๋ ฅ <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE ์ƒˆ ์ด๋ฆ„ ์ž…๋ ฅ: + .bvstate-delete = BVSTATE ์‚ญ์ œํ•  ๊ฐ€์‹œ์„ฑ ์ƒํƒœ ์ž…๋ ฅ: + .bvstate-not-found = ๊ฐ€์‹œ์„ฑ ์ƒํƒœ "__ocs_fmt_0__"์„(๋ฅผ) ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + .bvstate-exists = ๊ฐ€์‹œ์„ฑ ์ƒํƒœ "__ocs_fmt_0__"์ด(๊ฐ€) ์ด๋ฏธ ์žˆ์Šต๋‹ˆ๋‹ค. + .bvstate-last = ์œ ์ผํ•œ ๊ฐ€์‹œ์„ฑ ์ƒํƒœ๋Š” ์‚ญ์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + .bvstate-needs-bedit = __ocs_fmt_0__: ๋จผ์ € ๋ธ”๋ก ํŽธ์ง‘๊ธฐ์—์„œ ๋ธ”๋ก์„ ์—ฌ์‹ญ์‹œ์˜ค(BEDIT). + .bvstate-current = ํ˜„์žฌ ๊ฐ€์‹œ์„ฑ ์ƒํƒœ: __ocs_fmt_0__ + .bvhide-select = ์ˆจ๊ธธ ๊ฐ์ฒด ์„ ํƒ: + .bvshow-select = ํ‘œ์‹œํ•  ๊ฐ์ฒด ์„ ํƒ: + .arrayedit-select-array = ARRAYEDIT ๋ฐฐ์—ด ์„ ํƒ: + .arrayedit-options = ARRAYEDIT ์˜ต์…˜ ์ž…๋ ฅ [์›๋ณธ/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: ์„ ํƒํ•œ ๊ฐ์ฒด๋Š” ์—ฐ๊ด€ ๋ฐฐ์—ด์ด ์•„๋‹™๋‹ˆ๋‹ค. .blend-enter-continuity-tangent-curvature-c = BLEND Enter ์—ฐ์†์„ฑ [์ ‘์„ /๊ณก๋ฅ ] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND ์ฒซ ๋ฒˆ์งธ ์—ด๋ฆฐ ๊ณก์„  ์„ ํƒ [์—ฐ์†์„ฑ=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND ๋‘ ๋ฒˆ์งธ ์—ด๋ฆฐ ๊ณก์„  ์„ ํƒ [์—ฐ์†์„ฑ=__ocs_arg_c__]: @@ -3601,6 +3706,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE ์‚ฝ์ž…์  ์ง€์ •: insert = + .pastespec-specify-insertion-point = PASTESPEC ์‚ฝ์ž…์  ์ง€์ •: + .pastespec-paste-as = PASTESPEC ๋ถ™์—ฌ๋„ฃ๊ธฐ ํ˜•์‹ [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3717,6 +3824,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3771,6 +3885,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *์–ธ๋”๋ ˆ์ด ๊ฐ์ฒด ์Šค๋ƒ…์ด ์„œ๋กœ ๋‹ค๋ฆ„* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3910,6 +4025,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = ์ด๋ฏธ์ง€ ๋˜๋Š” ์–ธ๋”๋ ˆ์ด ์„ ํƒ: + .adjust-select-pdf-underlay = PDF ์–ธ๋”๋ ˆ์ด ์„ ํƒ: + .adjust-select-dwf-underlay = DWF ์–ธ๋”๋ ˆ์ด ์„ ํƒ: + .adjust-select-dgn-underlay = DGN ์–ธ๋”๋ ˆ์ด ์„ ํƒ: + .adjust-mixed-underlays = *์„ ํƒ ์„ธํŠธ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ™์€ ํŒŒ์ผ ์œ ํ˜•์˜ ์–ธ๋”๋ ˆ์ด๋งŒ ์„ ํƒํ•˜์‹ญ์‹œ์˜ค.* + .adjust-image-option = ์ด๋ฏธ์ง€ ์˜ต์…˜ ์ž…๋ ฅ [๋Œ€๋น„/ํŽ˜์ด๋“œ/๋ฐ๊ธฐ] <๋ฐ๊ธฐ>: + .adjust-underlay-option = __ocs_arg_kind__ ์–ธ๋”๋ ˆ์ด ์˜ต์…˜ ์ž…๋ ฅ [ํŽ˜์ด๋“œ/๋Œ€๋น„/ํ‘๋ฐฑ] <__ocs_arg_option__>: + .adjust-monochrome = ํ‘๋ฐฑ? [์˜ˆ/์•„๋‹ˆ์š”] <__ocs_arg_value__>: + .adjust-brightness-value = ๋ฐ๊ธฐ ๊ฐ’ ์ž…๋ ฅ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = ๋Œ€๋น„ ๊ฐ’ ์ž…๋ ฅ (0-100) <__ocs_arg_value__>: + .adjust-fade-value = ํŽ˜์ด๋“œ ๊ฐ’ ์ž…๋ ฅ (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = ๊ฐ’์€ 0์—์„œ 100 ์‚ฌ์ด์˜ ์ •์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4414,7 +4541,27 @@ model = .value-2point = 2์  .cube = ์ •์œก๋ฉด์ฒด + layout = + .mvsetup-select-objects-to-delete = MVSETUP ์‚ญ์ œํ•  ๊ฐ์ฒด ์„ ํƒ: + .delete-objects-option = ๊ฐ์ฒด ์‚ญ์ œ + .mvsetup-enable-paper-space = MVSETUP ๋„๋ฉด ๊ณต๊ฐ„์„ ์‚ฌ์šฉํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP ์ถ•์ฒ™ ๋น„์œจ ์ž…๋ ฅ <1>: + .mvsetup-enter-paper-width = MVSETUP ์šฉ์ง€ ํญ ์ž…๋ ฅ: + .mvsetup-enter-paper-height = MVSETUP ์šฉ์ง€ ๋†’์ด ์ž…๋ ฅ: + .mvsetup-enter-option = MVSETUP ์˜ต์…˜ ์ž…๋ ฅ [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP ๋ทฐํฌํŠธ ๋ฐฐ์น˜ ์ž…๋ ฅ [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP ๋ทฐํฌํŠธ ์˜์—ญ์˜ ์ฒซ ๋ฒˆ์งธ ๊ตฌ์„ ์ง€์ • : + .mvsetup-specify-opposite-corner = MVSETUP ๋ฐ˜๋Œ€ ๊ตฌ์„ ์ง€์ •: + .mvsetup-viewports-in-x = MVSETUP X ๋ฐฉํ–ฅ ๋ทฐํฌํŠธ ์ˆ˜ ์ž…๋ ฅ <2>: + .mvsetup-viewports-in-y = MVSETUP Y ๋ฐฉํ–ฅ ๋ทฐํฌํŠธ ์ˆ˜ ์ž…๋ ฅ <2>: + .mvsetup-distance-in-x = MVSETUP X ๋ฐฉํ–ฅ ๋ทฐํฌํŠธ ๊ฐ„ ๊ฑฐ๋ฆฌ ์ง€์ • <0>: + .mvsetup-distance-in-y = MVSETUP Y ๋ฐฉํ–ฅ ๋ทฐํฌํŠธ ๊ฐ„ ๊ฑฐ๋ฆฌ ์ง€์ • <0>: + .mvsetup-select-viewports = MVSETUP ์ถ•์ฒ™ํ•  ๋ทฐํฌํŠธ ์„ ํƒ: + .mvsetup-paper-space-units = MVSETUP ๋„๋ฉด ๊ณต๊ฐ„ ๋‹จ์œ„ ์ˆ˜ ์ž…๋ ฅ <1>: + .mvsetup-model-space-units = MVSETUP ๋ชจํ˜• ๊ณต๊ฐ„ ๋‹จ์œ„ ์ˆ˜ ์ž…๋ ฅ <1>: + .mvsetup-viewports-do-not-fit = MVSETUP ๋ทฐํฌํŠธ๊ฐ€ ์˜์—ญ์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + .scale-viewports-option = ๋ทฐํฌํŠธ ์ถ•์ฒ™ .active = ํ™œ์„ฑ .inactive = ๋น„ํ™œ์„ฑ .insert-view = ์‚ฝ์ž… ๋ทฐ @@ -4440,6 +4587,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = ๋™๊ฒฐ | T = ํ•ด๋™ | Enter = ์ข…๋ฃŒ view = + .twist-option = ๋น„ํ‹€๊ธฐ + .dview-specify-view-twist-angle = DVIEW ๋ทฐ ๋น„ํ‹€๊ธฐ ๊ฐ๋„ ์ง€์ • <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW ์˜ต์…˜ ์ž…๋ ฅ [TWist/Undo]: .color-scheme-set-to-system-currently-name = ์ƒ‰ ๊ตฌ์„ฑํ‘œ๊ฐ€ '์‹œ์Šคํ…œ'์œผ๋กœ ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค(ํ˜„์žฌ __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = __ocs_fmt_0__์—์„œ __ocs_fmt_1__ ์‚ฌ์ด์˜ ์ •์ˆ˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. @@ -4634,13 +4784,6 @@ view = .x-no-xdata = __ocs_fmt_0__: ํ™•์žฅ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. .value-3d-orbit =3D ๊ถค๋„ .value-3d-orbit-drag-with-right-mouse-button = 3D ๊ถค๋„: ๋งˆ์šฐ์Šค ์˜ค๋ฅธ์ชฝ ๋ฒ„ํŠผ์œผ๋กœ ๋“œ๋ž˜๊ทธํ•ฉ๋‹ˆ๋‹ค. - .adjust-brightness-contrast-fade = ADJUST [๋ฐ๊ธฐ / ๋Œ€๋น„ / ํŽ˜์ด๋“œ]: - .adjust-brightness-0-100 = ADJUST ๋ฐ๊ธฐ 0-100: - .adjust-contrast-0-100 = ADJUST ๋Œ€๋น„ 0-100: - .adjust-fade-0-100 = ADJUST ํŽ˜์ด๋“œ 0-100: - .adjust-select-raster-image-s-first = ADJUST: ๋จผ์ € ๋ž˜์Šคํ„ฐ ์ด๋ฏธ์ง€๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. - .adjust-value-must-be-0-100 = ADJUST: ๊ฐ’์€ 0-100์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_2__ ์ด๋ฏธ์ง€์˜ __ocs_fmt_0__ = __ocs_fmt_1__. .annoallvisible-new-value-0-1 = ANNOALLVISIBLE ์ƒˆ ๊ฐ’ [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: 0 ๋˜๋Š” 1์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE ์ƒˆ ๊ฐ’ [-4..4]: @@ -4766,7 +4909,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = ์–ธ๋”๋ ˆ์ด __ocs_fmt_0__: fade=__ocs_fmt_1__, ๋Œ€๋น„=__ocs_fmt_2__, on=__ocs_fmt_3__, ํด๋ฆฝ=__ocs_fmt_4__, ๋ชจ๋…ธ=__ocs_fmt_5__ .updated-changed-underlay-s = __ocs_fmt_0__ ์–ธ๋”๋ ˆ์ด๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. - .usage-adjust-brightness-contrast-fade-0-100 = ์‚ฌ์šฉ๋ฒ•: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = ์‚ฌ์šฉ๋ฒ•: ANNOSCALE ์˜ˆ: 1:50, 2:1 ๋˜๋Š” ์š”์†Œ .usage-draworder-f-front-b-back-a-above-handle = ์‚ฌ์šฉ๋ฒ•: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = ์‚ฌ์šฉ๋ฒ•: HYPERLINK (๊ฐ์ฒด๋ฅผ ๋จผ์ € ์„ ํƒ) @@ -5994,6 +6136,46 @@ ui = .osnap-settings = ๊ฐ์ฒด ์Šค๋ƒ… ์„ค์ •... .snap-override-none-next-pick-only = ์Šค๋ƒ… ์žฌ์ง€์ •: ์—†์Œ (๋‹ค์Œ ์„ ํƒ๋งŒ). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = ๋ณด์กฐ์„  ์‚ฌ์ด์— ํ•ญ์ƒ ํ…์ŠคํŠธ ์œ ์ง€ .angular-dimensions = ๊ฐ๋„ ์น˜์ˆ˜ @@ -6261,3 +6443,117 @@ graphic-attributes = .reset-angle = ๊ฐ๋„ ์žฌ์„ค์ • .one-color = ๋‹จ์ƒ‰ .two-colors = ๋‘ ์ƒ‰ + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = ์ž‘์—… ์„ธํŠธ์— ์ถ”๊ฐ€ + .remove-from-working-set = ์ž‘์—… ์„ธํŠธ์—์„œ ์ œ๊ฑฐ + .toolbar-objects = ๊ฐœ ๊ฐ์ฒด + .dialog-title = ์ฐธ์กฐ ํŽธ์ง‘ + .dialog-reference = ํŽธ์ง‘ํ•  ์ฐธ์กฐ + .dialog-chain-hint = ์„ ํƒํ•œ ์ ์˜ ์ฒด์ธ์ž…๋‹ˆ๋‹ค. ์นฉ์„ ํด๋ฆญํ•˜์—ฌ ์ˆ˜์ค€์„ ๋ณ€๊ฒฝํ•˜์‹ญ์‹œ์˜ค. + .dialog-path = ๊ฒฝ๋กœ: + .dialog-working-set = ์ž‘์—… ์„ธํŠธ + .dialog-all-objects = ๋ชจ๋“  ๊ฐ์ฒด + .dialog-pick-objects = ๋„๋ฉด์—์„œ ์„ ํƒ + .dialog-attributes = ์†์„ฑ ์ •์˜๋„ ํŽธ์ง‘ + .dialog-session = ์„ธ์…˜ + .dialog-lock = ์ž‘์—… ์„ธํŠธ ๋ฐ–์˜ ๊ฐ์ฒด ์ž ๊ธˆ + .dialog-unique = ์ถฉ๋Œํ•˜๋Š” ๋„๋ฉด์ธต, ์Šคํƒ€์ผ ๋ฐ ๋ธ”๋ก ์ด๋ฆ„์— ๊ณ ์œ  ์ด๋ฆ„ ์ง€์ • + .dialog-start = ํŽธ์ง‘ ์‹œ์ž‘ + .dialog-help = ๋ธ”๋ก ์ฐธ์กฐ ๋˜๋Š” ์™ธ๋ถ€ ์ฐธ์กฐ๋ฅผ ์ œ์ž๋ฆฌ ํŽธ์ง‘์šฉ์œผ๋กœ ์—ฝ๋‹ˆ๋‹ค. + .select-reference = ์ฐธ์กฐ ์„ ํƒ: + .reference-not-found = ์ฐธ์กฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + .not-valid-reference = ๊ฐ์ฒด๊ฐ€ ์œ ํšจํ•œ ์ฐธ์กฐ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค + .one-entity-added = 1๊ฐœ ๊ฐœ์ฒด ์ถ”๊ฐ€๋จ + .nesting-level = ๋‚ดํฌ ์ˆ˜์ค€ ์„ ํƒ [ํ™•์ธ/๋‹ค์Œ] <๋‹ค์Œ>: + .selection-method = ๊ฐ์ฒด ์„ ํƒ ๋ฐฉ๋ฒ• ์ž…๋ ฅ [๋ชจ๋‘/๋‚ดํฌ] <๋ชจ๋‘>: + .select-nested = ๋‚ดํฌ๋œ ๊ฐ์ฒด ์„ ํƒ: + .display-attdefs = ์†์„ฑ ์ •์˜ ํ‘œ์‹œ [์˜ˆ/์•„๋‹ˆ์˜ค] <์•„๋‹ˆ์˜ค>: + .zero-items-selected = 0๊ฐœ ํ•ญ๋ชฉ ์„ ํƒ๋จ + .one-item-selected = 1๊ฐœ ํ•ญ๋ชฉ ์„ ํƒ๋จ + .items-selected = __ocs_fmt_0__๊ฐœ ํ•ญ๋ชฉ ์„ ํƒ๋จ + .refset-option = ์˜ต์…˜ ์ž…๋ ฅ [์ถ”๊ฐ€/์ œ๊ฑฐ] <์ถ”๊ฐ€>: + .refclose-option = ์˜ต์…˜ ์ž…๋ ฅ [์ €์žฅ/์ฐธ์กฐ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ทจ์†Œ] <์ €์žฅ>: + .not-checked-out = ** REFEDIT ๋ช…๋ น์œผ๋กœ ์ฐธ์กฐ๋ฅผ ํŽธ์ง‘ ์ค‘์ด ์•„๋‹ˆ๋ฉด ๋ช…๋ น์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค ** + .already-checked-out = ** ๋ช…๋ น์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. __ocs_fmt_0__์€(๋Š”) ์ด๋ฏธ ํŽธ์ง‘ ์ค‘์ž…๋‹ˆ๋‹ค ** + .transfer-objects = RefEdit ์ž‘์—… ์„ธํŠธ์™€ ํ˜ธ์ŠคํŠธ ๋„๋ฉด ๊ฐ„ ๊ฐ์ฒด ์ด๋™... + .added-to-working-set = ์ž‘์—… ์„ธํŠธ์— __ocs_fmt_0__๊ฐœ ์ถ”๊ฐ€๋จ + .removed-from-working-set = ์ž‘์—… ์„ธํŠธ์—์„œ __ocs_fmt_0__๊ฐœ ์ œ๊ฑฐ๋จ + .one-object-added = 1๊ฐœ ๊ฐ์ฒด๊ฐ€ __ocs_fmt_0__์— ์ถ”๊ฐ€๋จ + .objects-added = __ocs_fmt_0__๊ฐœ ๊ฐ์ฒด๊ฐ€ __ocs_fmt_1__์— ์ถ”๊ฐ€๋จ + .one-object-removed = 1๊ฐœ ๊ฐ์ฒด๊ฐ€ __ocs_fmt_0__์—์„œ ์ œ๊ฑฐ๋จ + .objects-removed = __ocs_fmt_0__๊ฐœ ๊ฐ์ฒด๊ฐ€ __ocs_fmt_1__์—์„œ ์ œ๊ฑฐ๋จ + .one-xref-instance = ์™ธ๋ถ€ ์ฐธ์กฐ ์ธ์Šคํ„ด์Šค 1๊ฐœ ์—…๋ฐ์ดํŠธ๋จ + .xref-instances = ์™ธ๋ถ€ ์ฐธ์กฐ ์ธ์Šคํ„ด์Šค __ocs_fmt_0__๊ฐœ ์—…๋ฐ์ดํŠธ๋จ + .one-block-instance = ๋ธ”๋ก ์ธ์Šคํ„ด์Šค 1๊ฐœ ์—…๋ฐ์ดํŠธ๋จ + .block-instances = ๋ธ”๋ก ์ธ์Šคํ„ด์Šค __ocs_fmt_0__๊ฐœ ์—…๋ฐ์ดํŠธ๋จ + .redefined-reloaded = __ocs_fmt_0__์ด(๊ฐ€) ์žฌ์ •์˜๋˜๊ณ  ๋‹ค์‹œ ๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + .redefined = __ocs_fmt_0__์ด(๊ฐ€) ์žฌ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + .ok = ํ™•์ธ + .nested = ๋‚ดํฌ + +image-adjust = + .title = ์ด๋ฏธ์ง€ ์กฐ์ • + .preview = ๋ฏธ๋ฆฌ ๋ณด๊ธฐ + .before = ์ด์ „ + .after = ์ดํ›„ + .no-preview = ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Œ + .adjust = ์กฐ์ • + .brightness = ๋ฐ๊ธฐ + .contrast = ๋Œ€๋น„ + .fade = ํŽ˜์ด๋“œ + .hint = ๋ง‰๋Œ€๋ฅผ ํด๋ฆญํ•˜๊ฑฐ๋‚˜ ๋Œ์–ด ๋†“์œผ์‹ญ์‹œ์˜ค. โˆ’ / +๋Š” ํ•œ ๋‹จ๊ณ„์ด๋ฉฐ ์ƒ์ž์—๋Š” 0โ€“100์˜ ์ •์ˆ˜๋ฅผ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. + .reset = ๊ธฐ๋ณธ๊ฐ’ ๋ณต์› + .apply = ์ ์šฉ + .help = ์„ ํƒํ•œ ๋ž˜์Šคํ„ฐ ์ด๋ฏธ์ง€์˜ ๋ฐ๊ธฐ, ๋Œ€๋น„ ๋ฐ ํŽ˜์ด๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + +keytips = + .title = KeyTip + .typed = ์ž…๋ ฅ: { $path } + .hint-tabs = ํƒญ ๋ฌธ์ž๋ฅผ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค ยท Esc๋กœ ๋‹ซ๊ธฐ + .hint-tab = { $tab } ํƒญ ยท Esc๋กœ ํ•œ ๋‹จ๊ณ„ ๋’ค๋กœ + .hint-items = ํ•ญ๋ชฉ ๋ฌธ์ž๋ฅผ ์ž…๋ ฅํ•˜์‹ญ์‹œ์˜ค ยท Esc๋กœ ํ•œ ๋‹จ๊ณ„ ๋’ค๋กœ diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index f8527bb9f..b7746219a 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = เบ›เบฅเบฑเบเบญเบดเบ™ ribbon-tab = + .block-editor = เบ•เบปเบงเปเบเป‰เป„เบ‚เบšเบฅเบฑเบญเบ .draw = เปเบ•เป‰เบก .parametric = เบžเบฒเบฃเบฒเบกเบตเบ•เบฃเบดเบ .annotate = เบ„เบณเบญเบฐเบ—เบดเบšเบฒเบ @@ -98,6 +99,9 @@ start = .no-recent-files = เป„เบŸเบฅเปŒเบ—เบตเปˆเบ—เปˆเบฒเบ™เป€เบ›เบตเบ”เบˆเบฐเบชเบฐเปเบ”เบ‡เบขเบนเปˆเบ™เบตเป‰. .browser-storage = เบšเปˆเบญเบ™เป€เบเบฑเบšเบ‚เปเป‰เบกเบนเบ™เบšเบฃเบฒเบงเป€เบŠเบต .keep-recent-files = เบฎเบฑเบเบชเบฒเป„เบŸเบฅเปŒเบซเบผเป‰เบฒเบชเบธเบ”เป„เบงเป‰ + .view-list = เบฅเบฒเบเบเบฒเบ™ + .view-icons = เป„เบญเบ„เบญเบ™ + .view-preview = เบ•เบปเบงเบขเปˆเบฒเบ‡เบ‚เบฐเปœเบฒเบ”เปƒเบซเบเปˆ donation = .title = เบ„เบณเบฎเป‰เบญเบ‡เบ‚เปเบเบฒเบ™เบšเปเบฅเบดเบˆเบฒเบ @@ -167,6 +171,14 @@ modal = .that-object = เบงเบฑเบ”เบ–เบธเบ™เบฑเป‰เบ™ .those-objects = เบงเบฑเบ”เบ–เบธเป€เบซเบผเบปเปˆเบฒเบ™เบฑเป‰เบ™ + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .image-unsupported = เบšเปเปˆเบชเบฒเบกเบฒเบ”เบ–เบญเบ”เบฅเบฐเบซเบฑเบ”เบฎเบนเบšเบžเบฒเบšเบ—เบตเปˆเบงเบฒเบ‡ "{ $name }" เป„เบ”เป‰. .pasted-image = เบงเบฒเบ‡เบฎเบนเบšเบžเบฒเบš "{ $name }": { $width }ร—{ $height } px @@ -273,6 +285,66 @@ command-copy = .mode = COPY เบ›เป‰เบญเบ™เบ•เบปเบงเป€เบฅเบทเบญเบเป‚เปเบ”เบเบฒเบ™เบ„เบฑเบ”เบฅเบญเบ [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ เบขเบนเปˆเบ™เบตเป‰เป€เบ—เบปเปˆเบฒเบ™เบฑเป‰เบ™ (เบ‚เบฝเบง), __ocs_fmt_1__ เบขเบนเปˆเปƒเบ™เปเบšเบšเบญเบทเปˆเบ™เป€เบ—เบปเปˆเบฒเบ™เบฑเป‰เบ™ (เปเบ”เบ‡)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = เบ›เบฝเบšเบ—เบฝเบšเบเบฑเบšเปเบšเบš + .label-associative-array-created = __ocs_fmt_0__: เบชเป‰เบฒเบ‡เบญเบฒเป€เบฃเปเบšเบšเป€เบŠเบทเปˆเบญเบกเป‚เบเบ‡เปเบฅเป‰เบง. + .assoc-array-array-path = เบญเบฒเป€เบฃ (เป€เบชเบฑเป‰เบ™เบ—เบฒเบ‡) + .assoc-array-array-polar = เบญเบฒเป€เบฃ (เบงเบปเบ‡เบกเบปเบ™) + .assoc-array-array-rectangular = เบญเบฒเป€เบฃ (เบชเบตเปˆเบซเบผเปˆเบฝเบก) + .assoc-array-item-spacing = เป„เบฅเบเบฐเบซเปˆเบฒเบ‡เบฅเบฒเบเบเบฒเบ™ + .assoc-array-fill-angle = เบกเบธเบกเป€เบ•เบตเบก + .assoc-array-items = เบฅเบฒเบเบเบฒเบ™ + .assoc-array-level-spacing = เป„เบฅเบเบฐเบซเปˆเบฒเบ‡เบฅเบฐเบ”เบฑเบš + .assoc-array-levels = เบฅเบฐเบ”เบฑเบš + .assoc-array-row-spacing = เป„เบฅเบเบฐเบซเปˆเบฒเบ‡เปเบ–เบง + .assoc-array-column-spacing = เป„เบฅเบเบฐเบซเปˆเบฒเบ‡เบ–เบฑเบ™ + .fbxexport-exported-to = FBXEXPORT: เบชเบปเปˆเบ‡เบญเบญเบเป„เบ›เบ—เบตเปˆ "__ocs_fmt_0__" เปเบฅเป‰เบง + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: เบšเปเปˆเบกเบตเบ‚เปเป‰เบกเบนเบ™เป€เบกเบŠ 3D เปƒเบ™เบฎเบนเบšเปเบ•เป‰เบกเบ™เบตเป‰. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = เบชเบปเปˆเบ‡เบญเบญเบ FBX + .fbx-files = เป„เบŸเบฅเปŒ STEP + .options-hpassoc = เบเบฒเบ™เบ‚เบตเบ”เบฅเบฒเบเปƒเปเปˆเบ•เบดเบ”เบ•เบฒเบกเบ‚เบญเบšเป€เบ‚เบ” (HPASSOC) + .options-show-ribbon = เบชเบฐเปเบ”เบ‡เปเบ–เบšเป‚เบš (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = เบชเบฐเปเบ”เบ‡เปเบ–เบšเป€เบกเบ™เบน (MENUBAR) + .options-window-elements = เบญเบปเบ‡เบ›เบฐเบเบญเบšเปœเป‰เบฒเบ•เปˆเบฒเบ‡ + .menu-viewports = เบŠเปˆเบญเบ‡เบกเบญเบ‡ + .menu-view-twist = เบšเบดเบ”เบกเบธเบกเบกเบญเบ‡ + .menu-tools = เป€เบ„เบทเปˆเบญเบ‡เบกเบท + .menu-table-style = เบฎเบนเบšเปเบšเบšเบ•เบฒเบ•เบฐเบฅเบฒเบ‡ + .menu-run-script = เปเบฅเปˆเบ™เบชเบฐเบ„เบฃเบดเบš + .menu-revision-cloud = เป€เบกเบเปเบเป‰เป„เบ‚ + .menu-regen-all = เบชเป‰เบฒเบ‡เบ„เบทเบ™เบ—เบฑเบ‡เปเบปเบ” + .menu-redraw = เปเบ•เป‰เบกเบ„เบทเบ™ + .menu-rectangle = เบชเบตเปˆเบซเบผเปˆเบฝเบก + .menu-quick-dimension = เบกเบดเบ•เบดเบ”เปˆเบงเบ™ + .menu-publish = เป€เบœเบตเบเปเบœเปˆ + .menu-paste-special = เบงเบฒเบ‡เปเบšเบšเบžเบดเป€เบชเบ” + .menu-parametric = เบžเบฒเบฃเบฒเบกเบดเป€เบ•เบต + .menu-page-setup-manager = เบ•เบปเบงเบˆเบฑเบ”เบเบฒเบ™เบ•เบฑเป‰เบ‡เบ„เปˆเบฒเปœเป‰เบฒ + .menu-orbit = เปเบธเบ™เบงเบปเบ‡ + .menu-new-ucs = UCS เปƒเปเปˆ + .menu-named-views = เบกเบธเบกเบกเบญเบ‡เบ—เบตเปˆเบ•เบฑเป‰เบ‡เบŠเบทเปˆ + .menu-multiline-text = เบ‚เปเป‰เบ„เบงเบฒเบกเบซเบผเบฒเบเปเบ–เบง + .menu-multiline-style = เบฎเบนเบšเปเบšเบšเป€เบชเบฑเป‰เบ™เบ‚เบฐเปœเบฒเบ™ + .menu-multiline = เป€เบชเบฑเป‰เบ™เบ‚เบฐเปœเบฒเบ™ + .menu-multileader-style = เบฎเบนเบšเปเบšเบšเป€เบชเบฑเป‰เบ™เบŠเบตเป‰เบซเบผเบฒเบ + .menu-match-properties = เบˆเบฑเบšเบ„เบนเปˆเบ„เบธเบ™เบชเบปเบกเบšเบฑเบ” + .menu-lengthen = เบเบทเบ” + .menu-jogged = เบซเบฑเบ + .menu-id-point = เบˆเบธเบ” ID + .menu-explode-text = เปเบเบเบ‚เปเป‰เบ„เบงเบฒเบก + .menu-edit-spline = เปเบเป‰เป„เบ‚ Spline + .menu-edit-polyline = เปเบเป‰เป„เบ‚เป€เบชเบฑเป‰เบ™เบซเบผเบฒเบเบ—เปˆเบญเบ™ + .menu-edit-hatch = เปเบเป‰เป„เบ‚เบเบฒเบ™เบ‚เบตเบ”เบฅเบฒเบ + .menu-drawing-limits = เบ‚เบญเบšเป€เบ‚เบ”เปเบšเบš + .menu-donut = เบงเบปเบ‡เปเบซเบงเบ™ + .menu-dimension-style = เบฎเบนเบšเปเบšเบšเบกเบดเบ•เบด + .menu-construction-line = เป€เบชเบฑเป‰เบ™เบเปเปˆเบชเป‰เบฒเบ‡ + .menu-arc-length = เบ„เบงเบฒเบกเบเบฒเบงเป‚เบ„เป‰เบ‡ + .command-line-opacity = เบ„เบงเบฒเบกเบ—เบถเบšเบ‚เบญเบ‡เปเบ–เบงเบ„เบณเบชเบฑเปˆเบ‡ + .txtexp-no-text-selected = TXTEXP: เบšเปเปˆเป„เบ”เป‰เป€เบฅเบทเบญเบเบ‚เปเป‰เบ„เบงเบฒเบก. + .angle-override-shown = เบเบณเบ™เบปเบ”เบกเบธเบกเปเบ—เบ™: __ocs_fmt_0__ .remote-image-url-could-not-be-fetched-the-request-timed-out = เบšเปเปˆเบชเบฒเบกเบฒเบ”เบ”เบถเบ‡เบฎเบนเบšเบžเบฒเบšเบ—เบฒเบ‡เป„เบ "__ocs_fmt_0__" เป„เบ”เป‰: เบเบฒเบ™เบฎเป‰เบญเบ‡เบ‚เปเปเบปเบ”เป€เบงเบฅเบฒ. .remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = เบšเปเปˆเป„เบ”เป‰เป‚เบซเบผเบ”เบฎเบนเบšเบžเบฒเบšเบ—เบฒเบ‡เป„เบ "__ocs_fmt_0__": เบกเบฑเบ™เป€เบเบตเบ™เบ‚เบตเบ”เบˆเบณเบเบฑเบ” 64 MB เบ‚เบญเบ‡เบฎเบนเบšเบžเบฒเบšเบ—เบฒเบ‡เป„เบ. .remote-image-url-could-not-be-fetched-the-server-returned-http-status = เบšเปเปˆเบชเบฒเบกเบฒเบ”เบ”เบถเบ‡เบฎเบนเบšเบžเบฒเบšเบ—เบฒเบ‡เป„เบ "__ocs_fmt_0__" เป„เบ”เป‰: เป€เบŠเบตเบšเป€เบงเบตเบ•เบญเบšเบเบฑเบš HTTP __ocs_fmt_1__. @@ -280,6 +352,8 @@ common = .autosave-copy-could-not-be-queued-too-many-pending-opens = เบšเปเปˆเบชเบฒเบกเบฒเบ”เป€เบ‚เบปเป‰เบฒเบ„เบดเบงเบชเบณเป€เบ™เบปเบฒเบšเบฑเบ™เบ—เบถเบเบญเบฑเบ”เบ•เบฐเป‚เบ™เบกเบฑเบ”เป„เบ”เป‰: เบกเบตเบเบฒเบ™เป€เบ›เบตเบ”เบ—เบตเปˆเบฅเปเบ–เป‰เบฒเบซเบผเบฒเบเป€เบเบตเบ™เป„เบ› .an-open-is-already-in-progress = เบเบณเบฅเบฑเบ‡เป€เบ›เบตเบ”เป„เบŸเบฅเปŒเบขเบนเปˆเปเบฅเป‰เบง. .autosave-copy-could-not-be-read = เบšเปเปˆเบชเบฒเบกเบฒเบ”เบญเปˆเบฒเบ™เบชเบณเป€เบ™เบปเบฒเบšเบฑเบ™เบ—เบถเบเบญเบฑเบ”เบ•เบฐเป‚เบ™เบกเบฑเบ”เป„เบ”เป‰: __ocs_fmt_0__ + .requires-0-or-1-or-keyword = เบ•เป‰เบญเบ‡เบเบฒเบ™เบชเบฐเป€เบžเบฒเบฐ 0 เบซเบผเบท 1, เบซเบผเบท เบ„เบณเบชเบฑเบšเบ•เบปเบงเป€เบฅเบทเบญเบ. + .requires-a-number = เบ•เป‰เบญเบ‡เบเบฒเบ™เบ•เบปเบงเป€เบฅเบ. .restored-from-the-autosave-copy = เบเบนเป‰เบ„เบทเบ™ "__ocs_fmt_0__" เบˆเบฒเบเบชเบณเป€เบ™เบปเบฒเบšเบฑเบ™เบ—เบถเบเบญเบฑเบ”เบ•เบฐเป‚เบ™เบกเบฑเบ”เปเบฅเป‰เบง .autosave-copy-could-not-be-stored = เบšเปเปˆเบชเบฒเบกเบฒเบ”เป€เบเบฑเบšเบชเบณเป€เบ™เบปเบฒเบšเบฑเบ™เบ—เบถเบเบญเบฑเบ”เบ•เบฐเป‚เบ™เบกเบฑเบ”เป„เบ”เป‰: __ocs_fmt_0__ .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" เบกเบตเบชเบณเป€เบ™เบปเบฒเบšเบฑเบ™เบ—เบถเบเบญเบฑเบ”เบ•เบฐเป‚เบ™เบกเบฑเบ”เบ—เบตเปˆเปƒเปเปˆเบเบงเปˆเบฒเบเบฒเบ™เบšเบฑเบ™เบ—เบถเบเบ„เบฑเป‰เบ‡เบชเบธเบ”เบ—เป‰เบฒเบ. @@ -827,6 +901,10 @@ common = .slice-result-commit-failed = SLICE: เบšเปเปˆเบชเบฒเบกเบฒเบ”เบšเบฑเบ™เบ—เบถเบเบœเบปเบ™เบฎเบฑเบšเป„เบ”เป‰; เบงเบฑเบ”เบ–เบธเบ•เบปเป‰เบ™เบชเบฐเบšเบฑเบšเบ—เบฑเบ‡เปเบปเบ”เบเบฑเบ‡เบ–เบทเบเบฎเบฑเบเบชเบฒเป„เบงเป‰. .slice-retained-update-failed = SLICE: เบšเปเปˆเบชเบฒเบกเบฒเบ”เบญเบฑเบšเป€เบ”เบ”เบœเบปเบ™เบฎเบฑเบšเบ—เบตเปˆเบฎเบฑเบเบชเบฒเป„เบงเป‰เป„เบ”เป‰. .slice-created-results = SLICE: เบชเป‰เบฒเบ‡ __ocs_arg_count__ เบงเบฑเบ”เบ–เบธเบœเบปเบ™เบฎเบฑเบšเปเบฅเป‰เบง. + .sectioncull-no-section = SECTIONCULL: เป€เบฅเบทเบญเบเบงเบฑเบ”เบ–เบธเบžเบฒเบเบ•เบฑเบ” เบซเบผเบท เป€เบ›เบตเบ”เบžเบฒเบเบ•เบฑเบ”เบชเบปเบ”. + .sectioncull-prepare-failed = SECTIONCULL: เบšเปเปˆเบชเบฒเบกเบฒเบ”เบเบฐเบเบฝเบกเบงเบฑเบ”เบ–เบธเบ—เบตเปˆเบ–เบทเบเบ•เบฑเบ”เป„เบ”เป‰; เบšเปเปˆเบกเบตเบซเบเบฑเบ‡เบ›เปˆเบฝเบ™เปเบ›เบ‡. + .sectioncull-nothing-outside = SECTIONCULL: เบšเปเปˆเบกเบตเบซเบเบฑเบ‡เบขเบนเปˆเบ™เบญเบเบžเบฒเบเบ•เบฑเบ”. + .sectioncull-result = SECTIONCULL: เบฅเบถเบš __ocs_arg_erased__ เบงเบฑเบ”เบ–เบธ เปเบฅเบฐ เบ•เบฑเบ” __ocs_arg_cut__ เบขเบนเปˆเบ‚เบญเบšเบžเบฒเบเบ•เบฑเบ”. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: เบ›เบฑเบšเป€เบชเบฑเป‰เบ™เบชเบฐเบžเบฅเบฒเบเบœเปˆเบฒเบ™ __ocs_fmt_0__ เบˆเบธเบ”. .splinefit-need-at-least-3-points = SPLINEFIT: เบ•เป‰เบญเบ‡เบเบฒเบ™เบขเปˆเบฒเบ‡เปœเป‰เบญเบ 3 เบˆเบธเบ”. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: เป€เบฅเบทเบญเบเป‚เบžเบฅเบตเบฅเบฒเบเป€เบžเบทเปˆเบญเบ›เบฑเบšเป€เบชเบฑเป‰เบ™เบชเบฐเบžเบฅเบฒเบเบœเปˆเบฒเบ™. @@ -864,9 +942,12 @@ common = .sent-to-printer-printer = เบชเบปเปˆเบ‡เป„เบ›เป€เบ„เบทเปˆเบญเบ‡เบžเบดเบกเปเบฅเป‰เบง: __ocs_fmt_0__ .set-current = เบ•เบฑเป‰เบ‡เป€เบ›เบฑเบ™เบ›เบฑเบ”เบˆเบธเบšเบฑเบ™ .shade-plot = เบเบฒเบ™เบžเบฅเบฑเบญเบ”เปเบšเบšเปเบฎเป€เบ‡เบปเบฒ + .shaded-with-edges = เบกเบตเป€เบ‡เบปเบฒเบžเป‰เบญเบกเบ‚เบญเบš + .shades-of-gray = เป‚เบ—เบ™เบชเบตเป€เบ—เบปเบฒ .shadow-flags = เปเบŸเบฅเบเป€เบ‡เบปเบฒ .single = เบ”เปˆเบฝเบง .size = เบ‚เบฐเปœเบฒเบ” + .sketchy = เปเบšเบšเบฎเปˆเบฒเบ‡ .snap-override-label-next-pick-only = เบ›เปˆเบฝเบ™เบ„เปˆเบฒเบชเบฐเปเบ™เบฑเบšเบŠเบปเปˆเบงเบ„เบฒเบง: __ocs_fmt_0__ (เบชเบณเบฅเบฑเบšเบเบฒเบ™เป€เบฅเบทเบญเบเบ„เบฑเป‰เบ‡เบ•เปเปˆเป„เบ›เป€เบ—เบปเปˆเบฒเบ™เบฑเป‰เบ™). .solid-created = เบชเป‰เบฒเบ‡เบฎเบนเบšเบŠเบปเบ‡เบ•เบฑเบ™เปเบฅเป‰เบง. .source-object-not-found = เบšเปเปˆเบžเบปเบšเบงเบฑเบ”เบ–เบธเบ•เบปเป‰เบ™เบชเบฐเบšเบฑเบš. @@ -1072,7 +1153,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: เบ•เป‰เบญเบ‡เบเบฒเบ™ Left, Top, เบซเบผเบท Right. .isodraft-expected-on-or-off = ISODRAFT: เบ•เป‰เบญเบ‡เบเบฒเบ™ On เบซเบผเบท Off. .xdata-selected-entities-are-on-locked-layers = XDATA: เบญเบปเบ‡เบ›เบฐเบเบญเบšเบ—เบตเปˆเป€เบฅเบทเบญเบเบขเบนเปˆเปƒเบ™เป€เบฅเป€เบขเบตเบ—เบตเปˆเบฅเบฑเบญเบเป„เบงเป‰. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: เบšเปเปˆเบกเบตเบฎเบนเบšเบžเบฒเบšเบฃเบฒเบชเป€เบ•เบตเบ–เบทเบเป€เบฅเบทเบญเบ, เบซเบผเบท เบšเปเปˆเบฎเบนเป‰เบˆเบฑเบเบ„เบธเบ™เบชเบปเบกเบšเบฑเบ” (เปƒเบŠเป‰ BRIGHTNESS|CONTRAST|FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: เบ›เป‰เบญเบ™เบˆเบณเบ™เบงเบ™เบ–เป‰เบงเบ™เปเบ•เปˆ -4 เบซเบฒ 4. .usage-datalink-path-to-csv = เบงเบดเบ—เบตเปƒเบŠเป‰: DATALINK <เป€เบชเบฑเป‰เบ™เบ—เบฒเบ‡เป„เบŸเบฅเปŒ-.csv> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: เบšเปเปˆเบžเบปเบšเบ•เบฒเบ•เบฐเบฅเบฒเบ‡เบ—เบตเปˆเป€เบŠเบทเปˆเบญเบกเป‚เบเบ‡. @@ -2369,6 +2449,7 @@ common = .load-linetypes = เป‚เบซเบผเบ”เบฎเบนเบšเปเบšเบšเป€เบชเบฑเป‰เบ™ .requires-two-positive-values = เบ•เป‰เบญเบ‡เบเบฒเบ™เบ„เปˆเบฒเบšเบงเบเบชเบญเบ‡เบ„เปˆเบฒ (x,y) เบซเบผเบท เบ„เปˆเบฒเบ”เบฝเบงเบชเบณเบฅเบฑเบšเบ—เบฑเบ‡เบชเบญเบ‡. .snap-marker-color = เบชเบตเป€เบ„เบทเปˆเบญเบ‡เปเบฒเบเบˆเบธเบ”เบ”เบนเบ” + .snap-marker-size = เบ‚เบฐเปœเบฒเบ”เป€เบ„เบทเปˆเบญเบ‡เปเบฒเบเบˆเบธเบ”เบ”เบนเบ” .command-line-text-color = เบชเบตเบ‚เปเป‰เบ„เบงเบฒเบกเปเบ–เบงเบ„เบณเบชเบฑเปˆเบ‡ .operand = เบ•เบปเบงเบ”เบณเป€เบ™เบตเบ™เบเบฒเบ™ .linetypes-loaded = เป‚เบซเบผเบ”เบฎเบนเบšเปเบšเบšเป€เบชเบฑเป‰เบ™เปเบฅเป‰เบง __ocs_fmt_0__ เบฎเบนเบšเปเบšเบš. @@ -2382,6 +2463,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE เบเบณเบ™เบปเบ”เบˆเบธเบ”เบซเบผเบฑเบ [__ocs_arg_count__ เบงเบฑเบ”เบ–เบธ]: .threedrotate-specify-rotation-axis = 3DROTATE เบเบณเบ™เบปเบ”เปเบเบ™เปเบธเบ™ [X/Y/Z] เบซเบผเบท เบˆเบธเบ”เบ—เบตเบชเบญเบ‡เป€เบ—เบดเบ‡เปเบเบ™เปเบธเบ™ : .threedrotate-specify-rotation-angle-a = 3DROTATE เบเบณเบ™เบปเบ”เบกเบธเบกเปเบธเบ™ <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = เปเบˆเป‰เบ‡เป€เบ•เบทเบญเบ™เบเบฒเบ™เบšเบฑเบ™เบ—เบถเบ .unable-to-save-drawing = เบšเปเปˆเบชเบฒเบกเบฒเบ”เบšเบฑเบ™เบ—เบถเบเปเบšเบšเปเบ•เป‰เบกเป„เบ”เป‰ @@ -3051,6 +3139,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT เป€เบฅเบทเบญเบเบšเบฅเบฑเบญเบเบ—เบตเปˆเบกเบตเบ„เบธเบ™เบฅเบฑเบเบชเบฐเบ™เบฐ: .annotation-object-scale = เบกเบฒเบ”เบ•เบฒเบชเปˆเบงเบ™เบงเบฑเบ”เบ–เบธเบ„เบณเบญเบฐเบ—เบดเบšเบฒเบ .bedit-select-block-reference-to-edit = BEDIT เป€เบฅเบทเบญเบเบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡เบšเบฅเบฑเบญเบเบ—เบตเปˆเบˆเบฐเปเบเป‰เป„เบ‚: + .bvstate-option = BVSTATE เบ›เป‰เบญเบ™เบ—เบฒเบ‡เป€เบฅเบทเบญเบ [เปƒเปเปˆ/เบ•เบฑเป‰เบ‡/เบ›เปˆเบฝเบ™เบŠเบทเปˆ/เบฅเบถเบš] <เบ•เบฑเป‰เบ‡>: + .bvstate-new-name = BVSTATE เบ›เป‰เบญเบ™เบŠเบทเปˆเบชเบฐเบ–เบฒเบ™เบฐเบเบฒเบ™เป€เบšเบดเปˆเบ‡เป€เบซเบฑเบ™เปƒเปเปˆ: + .bvstate-set = BVSTATE เบ›เป‰เบญเบ™เบชเบฐเบ–เบฒเบ™เบฐเบเบฒเบ™เป€เบšเบดเปˆเบ‡เป€เบซเบฑเบ™เบ—เบตเปˆเบˆเบฐเป€เบ›เบฑเบ™เบ›เบฑเบ”เบˆเบธเบšเบฑเบ™ <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE เบ›เป‰เบญเบ™เบชเบฐเบ–เบฒเบ™เบฐเบเบฒเบ™เป€เบšเบดเปˆเบ‡เป€เบซเบฑเบ™เบ—เบตเปˆเบˆเบฐเบ›เปˆเบฝเบ™เบŠเบทเปˆ <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE เบ›เป‰เบญเบ™เบŠเบทเปˆเปƒเปเปˆ: + .bvstate-delete = BVSTATE เบ›เป‰เบญเบ™เบชเบฐเบ–เบฒเบ™เบฐเบเบฒเบ™เป€เบšเบดเปˆเบ‡เป€เบซเบฑเบ™เบ—เบตเปˆเบˆเบฐเบฅเบถเบš: + .bvstate-not-found = เบšเปเปˆเบžเบปเบšเบชเบฐเบ–เบฒเบ™เบฐเบเบฒเบ™เป€เบšเบดเปˆเบ‡เป€เบซเบฑเบ™ "__ocs_fmt_0__". + .bvstate-exists = เบชเบฐเบ–เบฒเบ™เบฐเบเบฒเบ™เป€เบšเบดเปˆเบ‡เป€เบซเบฑเบ™ "__ocs_fmt_0__" เบกเบตเบขเบนเปˆเปเบฅเป‰เบง. + .bvstate-last = เบšเปเปˆเบชเบฒเบกเบฒเบ”เบฅเบถเบšเบชเบฐเบ–เบฒเบ™เบฐเบเบฒเบ™เป€เบšเบดเปˆเบ‡เป€เบซเบฑเบ™เบ”เบฝเบงเป„เบ”เป‰. + .bvstate-needs-bedit = __ocs_fmt_0__: เป€เบ›เบตเบ”เบšเบฅเบฑเบญเบเปƒเบ™เบ•เบปเบงเปเบเป‰เป„เบ‚เบšเบฅเบฑเบญเบเบเปˆเบญเบ™ (BEDIT). + .bvstate-current = เบชเบฐเบ–เบฒเบ™เบฐเบเบฒเบ™เป€เบšเบดเปˆเบ‡เป€เบซเบฑเบ™เบ›เบฑเบ”เบˆเบธเบšเบฑเบ™: __ocs_fmt_0__ + .bvhide-select = เป€เบฅเบทเบญเบเบงเบฑเบ”เบ–เบธเบ—เบตเปˆเบˆเบฐเป€เบŠเบทเปˆเบญเบ‡: + .bvshow-select = เป€เบฅเบทเบญเบเบงเบฑเบ”เบ–เบธเบ—เบตเปˆเบˆเบฐเบชเบฐเปเบ”เบ‡: + .arrayedit-select-array = ARRAYEDIT เป€เบฅเบทเบญเบเบญเบฒเป€เบฃ: + .arrayedit-options = ARRAYEDIT เบ›เป‰เบญเบ™เบ—เบฒเบ‡เป€เบฅเบทเบญเบ [เปเบซเบผเปˆเบ‡/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: เบงเบฑเบ”เบ–เบธเบ—เบตเปˆเป€เบฅเบทเบญเบเบšเปเปˆเปเบกเปˆเบ™เบญเบฒเป€เบฃเบ—เบตเปˆเป€เบŠเบทเปˆเบญเบกเป‚เบเบ‡. .blend-enter-continuity-tangent-curvature-c = BLEND เบ›เป‰เบญเบ™เบ„เบงเบฒเบกเบ•เปเปˆเป€เบ™เบทเปˆเบญเบ‡ [Tangent/Curvature] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND เป€เบฅเบทเบญเบเป€เบชเบฑเป‰เบ™เป‚เบ„เป‰เบ‡เบ›เบฒเบเป€เบ›เบตเบ”เบ—เบณเบญเบดเบ” [Continuity=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND เป€เบฅเบทเบญเบเป€เบชเบฑเป‰เบ™เป‚เบ„เป‰เบ‡เบ›เบฒเบเป€เบ›เบตเบ”เบ—เบตเบชเบญเบ‡ [Continuity=__ocs_arg_c__]: @@ -3562,7 +3666,7 @@ annotate = .mleader-options-leader-type-landing-content-type-max-points-first-angle-second-angle = MLEADER เบ•เบปเบงเป€เบฅเบทเบญเบ [Leader type/Landing/Content type/Max points/First angle/Second angle/Layer/Exit]: .mleader-leader-type-straight-spline-none = MLEADER เบ›เบฐเป€เบžเบ”เป€เบชเบฑเป‰เบ™เบŠเบตเป‰ [Straight/Spline/None]: .mleader-leader-landing-yes-no = MLEADER เบˆเบธเบ”เบžเบฑเบเป€เบชเบฑเป‰เบ™เบŠเบตเป‰ [Yes/No]: -.mleader-specify-landing-distance = MLEADER เบเปเบฒเบ™เบปเบ”เป„เบฅเบเบฐเบžเบฑเบเป€เบชเบฑเป‰เบ™เบŠเบตเป‰เบ™เปเบฒ: + .mleader-specify-landing-distance = MLEADER เบเปเบฒเบ™เบปเบ”เป„เบฅเบเบฐเบžเบฑเบเป€เบชเบฑเป‰เบ™เบŠเบตเป‰เบ™เปเบฒ: .mleader-content-type-mtext-block-none = MLEADER เบ›เบฐเป€เบžเบ”เป€เบ™เบทเป‰เบญเปƒเบ™ [MText/เบšเบฅเบฑเบญเบ/เบšเปเปˆเบกเบต]: .mleader-enter-maximum-leader-points = MLEADER เบ›เป‰เบญเบ™เบˆเปเบฒเบ™เบงเบ™เบˆเบธเบ”เป€เบชเบฑเป‰เบ™เบŠเบตเป‰เบ™เปเบฒเบชเบนเบ‡เบชเบธเบ”: .mleader-first-segment-angle-any-15-30-45-60-90 = MLEADER เบกเบธเบกเบชเปˆเบงเบ™เป€เบชเบฑเป‰เบ™เบ—เปเบฒเบญเบดเบ” [เปƒเบ”เป†/15/30/45/60/90]: @@ -3602,7 +3706,10 @@ annotate = .text-specify-second-endpoint = TEXT เบเปเบฒเบ™เบปเบ”เบˆเบธเบ”เบ›เบฒเบเบ—เบตเบชเบญเบ‡: .tolerance-specify-insertion-point = TOLERANCE เบเปเบฒเบ™เบปเบ”เบˆเบธเบ”เปเบŠเบ: + insert = + .pastespec-specify-insertion-point = PASTESPEC เบเบณเบ™เบปเบ”เบˆเบธเบ”เปเบŠเบ: + .pastespec-paste-as = PASTESPEC เบงเบฒเบ‡เป€เบ›เบฑเบ™ [Objects/Block/Text]: .xref-unresolved-external-references = XREF เบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡เบžเบฒเบเบ™เบญเบเบ—เบตเปˆเบšเปเปˆเบžเบปเบš: __ocs_fmt_0__ โ€” เบฅเบฒเบเป„เบŸเบฅเปŒ DWG/DXF เบ—เบตเปˆเบญเป‰เบฒเบ‡เบญเบตเบ‡เบกเบฒเบงเบฒเบ‡เปƒเบชเปˆเปœเป‰เบฒเป€เบžเบทเปˆเบญเบฅเบปเบ‡เบ—เบฐเบšเบฝเบ™, เบˆเบฒเบเบ™เบฑเป‰เบ™เป€เบ›เบตเบ”เปเบšเบšเปเบ•เป‰เบกเบ™เบตเป‰เบ„เบทเบ™ เบซเบผเบท เป€เบญเบตเป‰เบ™เปƒเบŠเป‰ XRELOAD. .pdfui-pdf-scale = เบกเบฒเบ”เบ•เบฒเบชเปˆเบงเบ™ PDF: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = เบเปเบฒเบ™เบปเบ”เบˆเบธเบ”เปเบŠเบ: @@ -3720,6 +3827,13 @@ insert = .xref-attach-hide-details = เป€เบŠเบทเปˆเบญเบ‡เบฅเบฒเบเบฅเบฐเบญเบฝเบ” .xref-attach-select-file = เป€เบฅเบทเบญเบเป„เบŸเบฅเปŒเบญเป‰เบฒเบ‡เบญเบตเบ‡ .xref-attach-all-files = เป„เบŸเบฅเปŒเบญเป‰เบฒเบ‡เบญเบตเบ‡เบ—เบฑเบ‡เปเบปเบ” + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = เปเบŠเบเบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡เปƒเบชเปˆเป„เบŸเบฅเปŒเบžเบฒเบเบ™เบญเบ เป€เบŠเบฑเปˆเบ™: เบฎเบนเบšเปเบ•เป‰เบกเบญเบทเปˆเบ™, เบฎเบนเบšเบžเบฒเบšเปเบฃเบฑเบชเป€เบ•เบต เปเบฅเบฐ เบŠเบฑเป‰เบ™เบฎเบญเบ‡เบžเบทเป‰เบ™. .xref-attach-invalid-input = เบ‚เปเป‰เบกเบนเบ™เบ›เป‰เบญเบ™เป€เบ‚เบปเป‰เบฒเบšเปเปˆเบ–เบทเบเบ•เป‰เบญเบ‡. .xref-insertion-point-x = เบˆเบธเบ”เปเบŠเบ X @@ -3774,6 +3888,7 @@ insert = .display-and-plot-frames = เบชเบฐเปเบ”เบ‡ เปเบฅเบฐ เบžเบดเบกเบ‚เบญเบš .display-not-plot-frames = เบชเบฐเปเบ”เบ‡เปเบ•เปˆเบšเปเปˆเบžเบดเบกเบ‚เบญเบš .frames-vary = *เบ‚เบญเบšเปเบ•เบเบ•เปˆเบฒเบ‡เบเบฑเบ™* + .underlay-osnaps-vary = *เบเบฒเบ™เบˆเบฑเบšเบˆเบธเบ”เบžเบทเป‰เบ™เบซเบผเบฑเบ‡เปเบ•เบเบ•เปˆเบฒเบ‡เบเบฑเบ™* .regenerating-model = เบเปเบฒเบฅเบฑเบ‡เบชเป‰เบฒเบ‡เป‚เบกเป€เบ”เบงเปƒเปเปˆเบ„เบทเบ™. .requires-int-0-2 = เบ•เป‰เบญเบ‡เบเบฒเบ™เบˆเปเบฒเบ™เบงเบ™เบ–เป‰เบงเบ™เบฅเบฐเบซเบงเปˆเบฒเบ‡ 0 เบซเบฒ 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3923,6 +4038,18 @@ insert = .xref-attach-dwg = เปเบ™เบš DWG .xref-attach-image = เปเบ™เบšเบฎเบนเบšเบžเบฒเบš .select-images = เป€เบฅเบทเบญเบเบฎเบนเบšเบžเบฒเบš: + .adjust-select-image-or-underlay = เป€เบฅเบทเบญเบเบฎเบนเบšเบžเบฒเบš เบซเบผเบท เบŠเบฑเป‰เบ™เบฎเบญเบ‡: + .adjust-select-pdf-underlay = เป€เบฅเบทเบญเบเบŠเบฑเป‰เบ™เบฎเบญเบ‡ PDF: + .adjust-select-dwf-underlay = เป€เบฅเบทเบญเบเบŠเบฑเป‰เบ™เบฎเบญเบ‡ DWF: + .adjust-select-dgn-underlay = เป€เบฅเบทเบญเบเบŠเบฑเป‰เบ™เบฎเบญเบ‡ DGN: + .adjust-mixed-underlays = *เบŠเบธเบ”เบเบฒเบ™เป€เบฅเบทเบญเบเบšเปเปˆเบ–เบทเบเบ•เป‰เบญเบ‡. เป€เบฅเบทเบญเบเบชเบฐเป€เบžเบฒเบฐเบŠเบฑเป‰เบ™เบฎเบญเบ‡เบ—เบตเปˆเบกเบตเบ›เบฐเป€เบžเบ”เป„เบŸเบฅเปŒเบ”เบฝเบงเบเบฑเบ™.* + .adjust-image-option = เบ›เป‰เบญเบ™เบ•เบปเบงเป€เบฅเบทเบญเบเบฎเบนเบšเบžเบฒเบš [เบ„เบงเบฒเบกเบ„เบปเบกเบŠเบฑเบ”/เบˆเบฒเบ‡/เบ„เบงเบฒเบกเบชเบฐเบซเบงเปˆเบฒเบ‡] <เบ„เบงเบฒเบกเบชเบฐเบซเบงเปˆเบฒเบ‡>: + .adjust-underlay-option = เบ›เป‰เบญเบ™เบ•เบปเบงเป€เบฅเบทเบญเบเบŠเบฑเป‰เบ™เบฎเบญเบ‡ __ocs_arg_kind__ [เบˆเบฒเบ‡/เบ„เบงเบฒเบกเบ„เบปเบกเบŠเบฑเบ”/เบชเบตเบ‚เบฒเบงเบ”เบณ] <__ocs_arg_option__>: + .adjust-monochrome = เบชเบตเบ‚เบฒเบงเบ”เบณ? [เปเบกเปˆเบ™/เบšเปเปˆ] <__ocs_arg_value__>: + .adjust-brightness-value = เบ›เป‰เบญเบ™เบ„เปˆเบฒ เบ„เบงเบฒเบกเบชเบฐเบซเบงเปˆเบฒเบ‡ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = เบ›เป‰เบญเบ™เบ„เปˆเบฒ เบ„เบงเบฒเบกเบ„เบปเบกเบŠเบฑเบ” (0-100) <__ocs_arg_value__>: + .adjust-fade-value = เบ›เป‰เบญเบ™เบ„เปˆเบฒ เบˆเบฒเบ‡ (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = เบ„เปˆเบฒเบ•เป‰เบญเบ‡เป€เบ›เบฑเบ™เบˆเปเบฒเบ™เบงเบ™เบ–เป‰เบงเบ™เบฅเบฐเบซเบงเปˆเบฒเบ‡ 0 เบซเบฒ 100. .transparency-mode-on = เบ›เป‰เบญเบ™เป‚เปเบ”เบ„เบงเบฒเบกเป‚เบ›เปˆเบ‡เปƒเบช [ON/OFF] : .transparency-mode-off = เบ›เป‰เบญเบ™เป‚เปเบ”เบ„เบงเบฒเบกเป‚เบ›เปˆเบ‡เปƒเบช [ON/OFF] : .xref-attach-menu = เปเบ™เบš... @@ -4417,6 +4544,25 @@ model = .cube = เบฅเบนเบเบšเบฒเบ”เบเปŒ layout = + .mvsetup-select-objects-to-delete = MVSETUP เป€เบฅเบทเบญเบเบงเบฑเบ”เบ–เบธเบ—เบตเปˆเบˆเบฐเบฅเบถเบš: + .delete-objects-option = เบฅเบถเบšเบงเบฑเบ”เบ–เบธ + .mvsetup-enable-paper-space = MVSETUP เป€เบ›เบตเบ”เปƒเบŠเป‰เบžเบทเป‰เบ™เบ—เบตเปˆเป€เบˆเป‰เบเบšเป? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP เปƒเบชเปˆเบ•เบปเบงเบ„เบนเบ™เบกเบฒเบ”เบ•เบฒเบชเปˆเบงเบ™ <1>: + .mvsetup-enter-paper-width = MVSETUP เปƒเบชเปˆเบ„เบงเบฒเบกเบเบงเป‰เบฒเบ‡เป€เบˆเป‰เบ: + .mvsetup-enter-paper-height = MVSETUP เปƒเบชเปˆเบ„เบงเบฒเบกเบชเบนเบ‡เป€เบˆเป‰เบ: + .mvsetup-enter-option = MVSETUP เปƒเบชเปˆเบ•เบปเบงเป€เบฅเบทเบญเบ [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP เปƒเบชเปˆเบเบฒเบ™เบˆเบฑเบ”เบงเบฒเบ‡เบŠเปˆเบญเบ‡เบกเบญเบ‡ [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP เบเบณเบ™เบปเบ”เบกเบธเบกเบ—เบณเบญเบดเบ”เบ‚เบญเบ‡เบžเบทเป‰เบ™เบ—เบตเปˆเบŠเปˆเบญเบ‡เบกเบญเบ‡ : + .mvsetup-specify-opposite-corner = MVSETUP เบเบณเบ™เบปเบ”เบกเบธเบกเบเบปเบ‡เบเบฑเบ™เบ‚เป‰เบฒเบก: + .mvsetup-viewports-in-x = MVSETUP เปƒเบชเปˆเบˆเบณเบ™เบงเบ™เบŠเปˆเบญเบ‡เบกเบญเบ‡เปƒเบ™เบ—เบดเบ” X <2>: + .mvsetup-viewports-in-y = MVSETUP เปƒเบชเปˆเบˆเบณเบ™เบงเบ™เบŠเปˆเบญเบ‡เบกเบญเบ‡เปƒเบ™เบ—เบดเบ” Y <2>: + .mvsetup-distance-in-x = MVSETUP เบเบณเบ™เบปเบ”เป„เบฅเบเบฐเบซเปˆเบฒเบ‡เบฅเบฐเบซเบงเปˆเบฒเบ‡เบŠเปˆเบญเบ‡เบกเบญเบ‡เปƒเบ™เบ—เบดเบ” X <0>: + .mvsetup-distance-in-y = MVSETUP เบเบณเบ™เบปเบ”เป„เบฅเบเบฐเบซเปˆเบฒเบ‡เบฅเบฐเบซเบงเปˆเบฒเบ‡เบŠเปˆเบญเบ‡เบกเบญเบ‡เปƒเบ™เบ—เบดเบ” Y <0>: + .mvsetup-select-viewports = MVSETUP เป€เบฅเบทเบญเบเบŠเปˆเบญเบ‡เบกเบญเบ‡เบ—เบตเปˆเบˆเบฐเบ›เบฑเบšเบกเบฒเบ”เบ•เบฒเบชเปˆเบงเบ™: + .mvsetup-paper-space-units = MVSETUP เปƒเบชเปˆเบˆเบณเบ™เบงเบ™เบซเบปเบงเปœเปˆเบงเบเบžเบทเป‰เบ™เบ—เบตเปˆเป€เบˆเป‰เบ <1>: + .mvsetup-model-space-units = MVSETUP เปƒเบชเปˆเบˆเบณเบ™เบงเบ™เบซเบปเบงเปœเปˆเบงเบเบžเบทเป‰เบ™เบ—เบตเปˆเปเบšเบšเบˆเบณเบฅเบญเบ‡ <1>: + .mvsetup-viewports-do-not-fit = MVSETUP เบŠเปˆเบญเบ‡เบกเบญเบ‡เบšเปเปˆเบžเปเบ”เบตเบเบฑเบšเบžเบทเป‰เบ™เบ—เบตเปˆ. + .scale-viewports-option = เบ›เบฑเบšเบกเบฒเบ”เบ•เบฒเบชเปˆเบงเบ™เบŠเปˆเบญเบ‡เบกเบญเบ‡ .active = เปƒเบŠเป‰เบ‡เบฒเบ™เบขเบนเปˆ .inactive = เบšเปเปˆเป„เบ”เป‰เปƒเบŠเป‰เบ‡เบฒเบ™ .insert-view = เปเบŠเบเบกเบธเบกเบกเบญเบ‡ @@ -4442,6 +4588,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F <เป€เบฅเป€เบขเบต> = เปเบŠเปˆเปเบ‚เบ‡ | T <เป€เบฅเป€เบขเบต> = เบฅเบฐเบฅเบฒเบ | Enter = เบญเบญเบ view = + .twist-option = เบšเบดเบ” + .dview-specify-view-twist-angle = DVIEW เบเบณเบ™เบปเบ”เบกเบธเบกเบšเบดเบ”เบกเบธเบกเบกเบญเบ‡ <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW เปƒเบชเปˆเบ•เบปเบงเป€เบฅเบทเบญเบ [TWist/Undo]: .color-scheme-set-to-system-currently-name = เบ•เบฑเป‰เบ‡เบฎเบนเบšเปเบšเบšเบชเบตเป€เบ›เบฑเบ™ 'เบฅเบฐเบšเบปเบš' เปเบฅเป‰เบง (เบ›เบฑเบ”เบˆเบธเบšเบฑเบ™ __ocs_fmt_0__). .ss-create-sheet-set = เบชเป‰เบฒเบ‡เบŠเบธเบ”เปเบœเปˆเบ™เบ‡เบฒเบ™ .ss-requires-integer-between = เบ•เป‰เบญเบ‡เบเบฒเบ™เบˆเปเบฒเบ™เบงเบ™เบ–เป‰เบงเบ™เบฅเบฐเบซเบงเปˆเบฒเบ‡ __ocs_fmt_0__ เบซเบฒ __ocs_fmt_1__. @@ -4629,13 +4778,6 @@ view = .x-no-xdata = __ocs_fmt_0__: เบšเปเปˆเบกเบต xdata. .value-3d-orbit = เปเบนเบ™เบกเบธเบกเบกเบญเบ‡ 3D (3D Orbit) .value-3d-orbit-drag-with-right-mouse-button = 3D Orbit: เบฅเบฒเบเบ”เป‰เบงเบเบ›เบธเปˆเบกเป€เบกเบปเบฒเบชเปŒเบ‚เบงเบฒ. - .adjust-brightness-contrast-fade = ADJUST [เบ„เบงเบฒเบกเบชเบฐเบซเบงเปˆเบฒเบ‡ / เบ„เบงเบฒเบกเบ„เบปเบกเบŠเบฑเบ” / เบ„เบงเบฒเบกเบˆเบฒเบ‡]: - .adjust-brightness-0-100 = ADJUST เบ„เบงเบฒเบกเบชเบฐเบซเบงเปˆเบฒเบ‡ 0-100: - .adjust-contrast-0-100 = ADJUST เบ„เบงเบฒเบกเบ„เบปเบกเบŠเบฑเบ” 0-100: - .adjust-fade-0-100 = ADJUST เบ„เบงเบฒเบกเบˆเบฒเบ‡ 0-100: - .adjust-select-raster-image-s-first = ADJUST: เป€เบฅเบทเบญเบเบฎเบนเบšเบžเบฒเบšเปเบฃเบฑเบชเป€เบ•เบตเบเปˆเบญเบ™. - .adjust-value-must-be-0-100 = ADJUST: เบ„เปˆเบฒเบ•เป‰เบญเบ‡เบขเบนเปˆเบฅเบฐเบซเบงเปˆเบฒเบ‡ 0-100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ เปƒเบ™ __ocs_fmt_2__ เบฎเบนเบšเบžเบฒเบš. .annoallvisible-new-value-0-1 = ANNOALLVISIBLE เบ„เปˆเบฒเปƒเปเปˆ [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: เบ›เป‰เบญเบ™ 0 เบซเบผเบท 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE เบ„เปˆเบฒเปƒเปเปˆ [-4..4]: @@ -4767,7 +4909,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = เบŠเบฑเป‰เบ™เบฎเบญเบ‡เบžเบทเป‰เบ™ __ocs_fmt_0__: เบ„เบงเบฒเบกเบˆเบฒเบ‡=__ocs_fmt_1__, เบ„เบงเบฒเบกเบ„เบปเบกเบŠเบฑเบ”=__ocs_fmt_2__, เป€เบ›เบตเบ”=__ocs_fmt_3__, เบ•เบฑเบ”เบ‚เบญเบš=__ocs_fmt_4__, เบ‚เบฒเบงเบ”เปเบฒ=__ocs_fmt_5__ .updated-changed-underlay-s = เบญเบฑเบšเป€เบ”เบ”เปเบฅเป‰เบง __ocs_fmt_0__ เบŠเบฑเป‰เบ™เบฎเบญเบ‡เบžเบทเป‰เบ™. - .usage-adjust-brightness-contrast-fade-0-100 = เบงเบดเบ—เบตเปƒเบŠเป‰: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = เบงเบดเบ—เบตเปƒเบŠเป‰: ANNOSCALE <เบญเบฑเบ”เบ•เบฒเบชเปˆเบงเบ™> เป€เบŠเบฑเปˆเบ™ 1:50, 2:1, เบซเบผเบท เบ•เบปเบงเบ„เบนเบ™ .usage-draworder-f-front-b-back-a-above-handle = เบงเบดเบ—เบตเปƒเบŠเป‰: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = เบงเบดเบ—เบตเปƒเบŠเป‰: HYPERLINK (เป€เบฅเบทเบญเบเบงเบฑเบ”เบ–เบธเบเปˆเบญเบ™) @@ -5996,6 +6137,46 @@ ui = .osnap-settings = เบเบฒเบ™เบ•เบฑเป‰เบ‡เบ„เปˆเบฒเบเบฒเบ™เบˆเบฑเบšเบˆเบธเบ”เบงเบฑเบ”เบ–เบธ... .snap-override-none-next-pick-only = เบšเบฑเบ‡เบ„เบฑเบšเบˆเบฑเบšเบˆเบธเบ”: เบšเปเปˆเบกเบต (เบชเบฐเป€เบžเบฒเบฐเบเบฒเบ™เบ„เบฅเบดเบเบ„เบฑเป‰เบ‡เบ•เปเปˆเป„เบ›). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = เบฎเบฑเบเบชเบฒเบ‚เปเป‰เบ„เบงเบฒเบกเป„เบงเป‰เบฅเบฐเบซเบงเปˆเบฒเบ‡เป€เบชเบฑเป‰เบ™เบŠเปˆเบงเบเบšเบญเบเบ‚เบฐเปœเบฒเบ”เบชเบฐเป€เปเบต .angular-dimensions = เบ‚เบฐเปœเบฒเบ”เบกเบธเบก @@ -6263,3 +6444,117 @@ graphic-attributes = .reset-angle = เบฃเบตเป€เบŠเบฑเบ”เบกเบธเบก .one-color = เปœเบถเปˆเบ‡เบชเบต .two-colors = เบชเบญเบ‡เบชเบต + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = เป€เบžเบตเปˆเบกเปƒเบชเปˆเบŠเบธเบ”เป€เบฎเบฑเบ”เบงเบฝเบ + .remove-from-working-set = เบฅเบถเบšเบญเบญเบเบˆเบฒเบเบŠเบธเบ”เป€เบฎเบฑเบ”เบงเบฝเบ + .toolbar-objects = เบงเบฑเบ”เบ–เบธ + .dialog-title = เปเบเป‰เป„เบ‚เบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡ + .dialog-reference = เบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡เบ—เบตเปˆเบˆเบฐเปเบเป‰เป„เบ‚ + .dialog-chain-hint = เบฅเบฐเบšเบปเบšเบ•เปˆเบญเบ‡เป‚เบชเป‰เบขเบนเปˆเบˆเบธเบ”เบ—เบตเปˆเป€เบฅเบทเบญเบ; เบ„เบฅเบดเบเบ›เป‰เบฒเบเป€เบžเบทเปˆเบญเบ›เปˆเบฝเบ™เบฅเบฐเบ”เบฑเบš. + .dialog-path = เป€เบชเบฑเป‰เบ™เบ—เบฒเบ‡: + .dialog-working-set = เบŠเบธเบ”เป€เบฎเบฑเบ”เบงเบฝเบ + .dialog-all-objects = เบงเบฑเบ”เบ–เบธเบ—เบฑเบ‡เปเบปเบ” + .dialog-pick-objects = เป€เบฅเบทเบญเบเปƒเบ™เปเบšเบšเปเบ•เป‰เบก + .dialog-attributes = เปเบเป‰เป„เบ‚เบ„เบณเบ™เบดเบเบฒเบกเบ„เบธเบ™เบฅเบฑเบเบชเบฐเบ™เบฐเบ™เบณ + .dialog-session = เป€เบŠเบ”เบŠเบฑเบ™ + .dialog-lock = เบฅเบฑเบญเบเบงเบฑเบ”เบ–เบธเบ™เบญเบเบŠเบธเบ”เป€เบฎเบฑเบ”เบงเบฝเบ + .dialog-unique = เปƒเบซเป‰เบŠเบทเปˆเบ—เบตเปˆเบšเปเปˆเบŠเป‰เบณเบเบฑเบ™เปเบเปˆเบŠเบฑเป‰เบ™, เบฎเบนเบšเปเบšเบš เปเบฅเบฐ เบšเบฅเบฑเบญเบเบ—เบตเปˆเบ‚เบฑเบ”เบเบฑเบ™ + .dialog-start = เป€เบฅเบตเปˆเบกเปเบเป‰เป„เบ‚ + .dialog-help = เป€เบ›เบตเบ”เบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡เบšเบฅเบฑเบญเบ เบซเบผเบท เบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡เบžเบฒเบเบ™เบญเบเป€เบžเบทเปˆเบญเปเบเป‰เป„เบ‚เปƒเบ™เบšเปˆเบญเบ™. + .select-reference = เป€เบฅเบทเบญเบเบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡: + .reference-not-found = เบšเปเปˆเบžเบปเบšเบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡. + .not-valid-reference = เบงเบฑเบ”เบ–เบธเบšเปเปˆเปเบกเปˆเบ™เบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡เบ—เบตเปˆเบ–เบทเบเบ•เป‰เบญเบ‡ + .one-entity-added = เป€เบžเบตเปˆเบก 1 เบงเบฑเบ”เบ–เบธเปเบฅเป‰เบง + .nesting-level = เป€เบฅเบทเบญเบเบฅเบฐเบ”เบฑเบšเบŠเป‰เบญเบ™ [เบ•เบปเบเบฅเบปเบ‡/เบ–เบฑเบ”เป„เบ›] <เบ–เบฑเบ”เป„เบ›>: + .selection-method = เบ›เป‰เบญเบ™เบงเบดเบ—เบตเป€เบฅเบทเบญเบเบงเบฑเบ”เบ–เบธ [เบ—เบฑเบ‡เปเบปเบ”/เบŠเป‰เบญเบ™] <เบ—เบฑเบ‡เปเบปเบ”>: + .select-nested = เป€เบฅเบทเบญเบเบงเบฑเบ”เบ–เบธเบ—เบตเปˆเบŠเป‰เบญเบ™: + .display-attdefs = เบชเบฐเปเบ”เบ‡เบ„เบณเบ™เบดเบเบฒเบกเบ„เบธเบ™เบฅเบฑเบเบชเบฐเบ™เบฐ [เปเบกเปˆเบ™/เบšเปเปˆ] <เบšเปเปˆ>: + .zero-items-selected = เป€เบฅเบทเบญเบ 0 เบฅเบฒเบเบเบฒเบ™ + .one-item-selected = เป€เบฅเบทเบญเบ 1 เบฅเบฒเบเบเบฒเบ™ + .items-selected = เป€เบฅเบทเบญเบ __ocs_fmt_0__ เบฅเบฒเบเบเบฒเบ™ + .refset-option = เบ›เป‰เบญเบ™เบ—เบฒเบ‡เป€เบฅเบทเบญเบ [เป€เบžเบตเปˆเบก/เบฅเบถเบš] <เป€เบžเบตเปˆเบก>: + .refclose-option = เบ›เป‰เบญเบ™เบ—เบฒเบ‡เป€เบฅเบทเบญเบ [เบšเบฑเบ™เบ—เบถเบ/เบเบปเบเป€เบฅเบตเบเบเบฒเบ™เบ›เปˆเบฝเบ™เปเบ›เบ‡เบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡] <เบšเบฑเบ™เบ—เบถเบ>: + .not-checked-out = ** เบšเปเปˆเบญเบฐเบ™เบธเบเบฒเบ”เบ„เบณเบชเบฑเปˆเบ‡ เป€เบงเบฑเป‰เบ™เป€เบชเบเปเบ•เปˆเบเบฒเบ™เบญเป‰เบฒเบ‡เบญเบตเบ‡เบเบณเบฅเบฑเบ‡เบ–เบทเบเปเบเป‰เป„เบ‚เบ”เป‰เบงเบ REFEDIT ** + .already-checked-out = ** เบšเปเปˆเบญเบฐเบ™เบธเบเบฒเบ”เบ„เบณเบชเบฑเปˆเบ‡, __ocs_fmt_0__ เบเบณเบฅเบฑเบ‡เบ–เบทเบเปเบเป‰เป„เบ‚เบขเบนเปˆเปเบฅเป‰เบง ** + .transfer-objects = เบเป‰เบฒเบเบงเบฑเบ”เบ–เบธเบฅเบฐเบซเบงเปˆเบฒเบ‡เบŠเบธเบ”เป€เบฎเบฑเบ”เบงเบฝเบ RefEdit เปเบฅเบฐเปเบšเบšเปเบ•เป‰เบกเบซเบผเบฑเบ... + .added-to-working-set = เป€เบžเบตเปˆเบก __ocs_fmt_0__ เป€เบ‚เบปเป‰เบฒเบŠเบธเบ”เป€เบฎเบฑเบ”เบงเบฝเบเปเบฅเป‰เบง + .removed-from-working-set = เบฅเบถเบš __ocs_fmt_0__ เบญเบญเบเบˆเบฒเบเบŠเบธเบ”เป€เบฎเบฑเบ”เบงเบฝเบเปเบฅเป‰เบง + .one-object-added = เป€เบžเบตเปˆเบก 1 เบงเบฑเบ”เบ–เบธเป€เบ‚เบปเป‰เบฒ __ocs_fmt_0__ เปเบฅเป‰เบง + .objects-added = เป€เบžเบตเปˆเบก __ocs_fmt_0__ เบงเบฑเบ”เบ–เบธเป€เบ‚เบปเป‰เบฒ __ocs_fmt_1__ เปเบฅเป‰เบง + .one-object-removed = เบฅเบถเบš 1 เบงเบฑเบ”เบ–เบธเบญเบญเบเบˆเบฒเบ __ocs_fmt_0__ เปเบฅเป‰เบง + .objects-removed = เบฅเบถเบš __ocs_fmt_0__ เบงเบฑเบ”เบ–เบธเบญเบญเบเบˆเบฒเบ __ocs_fmt_1__ เปเบฅเป‰เบง + .one-xref-instance = เบญเบฑเบšเป€เบ”เบ” 1 เบญเบดเบ™เบชเบฐเปเบ•เบ™ xref เปเบฅเป‰เบง + .xref-instances = เบญเบฑเบšเป€เบ”เบ” __ocs_fmt_0__ เบญเบดเบ™เบชเบฐเปเบ•เบ™ xref เปเบฅเป‰เบง + .one-block-instance = เบญเบฑเบšเป€เบ”เบ” 1 เบญเบดเบ™เบชเบฐเปเบ•เบ™เบšเบฅเบฑเบญเบเปเบฅเป‰เบง + .block-instances = เบญเบฑเบšเป€เบ”เบ” __ocs_fmt_0__ เบญเบดเบ™เบชเบฐเปเบ•เบ™เบšเบฅเบฑเบญเบเปเบฅเป‰เบง + .redefined-reloaded = เบ™เบดเบเบฒเบก __ocs_fmt_0__ เปƒเปเปˆ เปเบฅเบฐเป‚เบซเบผเบ”เปƒเปเปˆเปเบฅเป‰เบง. + .redefined = เบ™เบดเบเบฒเบก __ocs_fmt_0__ เปƒเปเปˆเปเบฅเป‰เบง. + .ok = เบ•เบปเบเบฅเบปเบ‡ + .nested = เบŠเป‰เบญเบ™ + +image-adjust = + .title = เบ›เบฑเบšเบฎเบนเบšเบžเบฒเบš + .preview = เบ•เบปเบงเบขเปˆเบฒเบ‡ + .before = เบเปˆเบญเบ™ + .after = เบซเบผเบฑเบ‡ + .no-preview = เบšเปเปˆเบชเบฒเบกเบฒเบ”เป‚เบซเบผเบ”เบฎเบนเบšเบžเบฒเบšเป„เบ”เป‰ + .adjust = เบ›เบฑเบš + .brightness = เบ„เบงเบฒเบกเบชเบฐเบซเบงเปˆเบฒเบ‡ + .contrast = เบ„เบงเบฒเบกเบ„เบปเบกเบŠเบฑเบ” + .fade = เบ„เบงเบฒเบกเบˆเบฒเบ‡ + .hint = เบ„เบฅเบดเบ เบซเบผเบท เบฅเบฒเบเปเบ–เบš; โˆ’ / + เปเบกเปˆเบ™เปœเบถเปˆเบ‡เบ‚เบฑเป‰เบ™, เบเปˆเบญเบ‡เบฎเบฑเบšเบˆเบณเบ™เบงเบ™เป€เบ•เบฑเบก 0โ€“100. + .reset = เบเบนเป‰เบ„เบทเบ™เบ„เปˆเบฒเป€เบฅเบตเปˆเบกเบ•เบปเป‰เบ™ + .apply = เบ™เบณเปƒเบŠเป‰ + .help = เบ•เบฑเป‰เบ‡เบ„เบงเบฒเบกเบชเบฐเบซเบงเปˆเบฒเบ‡, เบ„เบงเบฒเบกเบ„เบปเบกเบŠเบฑเบ” เปเบฅเบฐ เบ„เบงเบฒเบกเบˆเบฒเบ‡เบ‚เบญเบ‡เบฎเบนเบšเบžเบฒเบš raster เบ—เบตเปˆเป€เบฅเบทเบญเบ. + +keytips = + .title = KeyTip + .typed = เบžเบดเบกเปเบฅเป‰เบง: { $path } + .hint-tabs = เบžเบดเบกเบ•เบปเบงเบญเบฑเบเบชเบญเบ™เปเบ–เบš ยท Esc เบ›เบดเบ” + .hint-tab = เปเบ–เบš { $tab } ยท Esc เบเบฑเบšเบ„เบทเบ™เปœเบถเปˆเบ‡เบฅเบฐเบ”เบฑเบš + .hint-items = เบžเบดเบกเบ•เบปเบงเบญเบฑเบเบชเบญเบ™เบฅเบฒเบเบเบฒเบ™ ยท Esc เบเบฑเบšเบ„เบทเบ™เปœเบถเปˆเบ‡เบฅเบฐเบ”เบฑเบš diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 730ea01da..7b8c9aa3b 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Plug-ins ribbon-tab = + .block-editor = Blokeditor .draw = Tekenen .parametric = Parametrisch .annotate = Annoteren @@ -98,6 +99,9 @@ start = .no-recent-files = Bestanden die u opent, verschijnen hier. .browser-storage = Browseropslag .keep-recent-files = Recente bestanden bewaren + .view-list = Lijst + .view-icons = Pictogrammen + .view-preview = Grote voorbeelden donation = .title = Verzoek om een donatie @@ -167,6 +171,14 @@ modal = .that-object = dat object .those-objects = die objecten + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Afbeelding "{ $name }" geplakt: { $width }ร—{ $height } px .image-unsupported = De geplakte afbeelding "{ $name }" kon niet worden gedecodeerd. @@ -274,11 +286,73 @@ command-copy = .mode = COPY Geef kopieermodus op [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ alleen hier (groen), __ocs_fmt_1__ alleen in de andere tekening (rood)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Vergelijken met tekening + .label-associative-array-created = __ocs_fmt_0__: associatieve reeks gemaakt. + .assoc-array-array-path = Reeks (pad) + .assoc-array-array-polar = Reeks (polair) + .assoc-array-array-rectangular = Reeks (rechthoekig) + .assoc-array-item-spacing = Itemafstand + .assoc-array-fill-angle = Vulhoek + .assoc-array-items = Items + .assoc-array-level-spacing = Niveau-afstand + .assoc-array-levels = Niveaus + .assoc-array-row-spacing = Rijafstand + .assoc-array-column-spacing = Kolomafstand + .fbxexport-exported-to = FBXEXPORT: geรซxporteerd naar "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: geen 3D-maasgegevens in deze tekening. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Nieuwe arceringen volgen hun grenzen (HPASSOC) + .options-show-ribbon = Lint tonen (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Menubalk tonen (MENUBAR) + .options-window-elements = Vensterelementen + .menu-viewports = Viewports + .menu-view-twist = Weergave draaien + .menu-tools = Extra + .menu-table-style = Tabelstijl + .menu-run-script = Script uitvoeren + .menu-revision-cloud = Revisiewolk + .menu-regen-all = Alles regenereren + .menu-redraw = Opnieuw tekenen + .menu-rectangle = Rechthoek + .menu-quick-dimension = Snelle maatvoering + .menu-publish = Publiceren + .menu-paste-special = Plakken speciaal + .menu-parametric = Parametrisch + .menu-page-setup-manager = Paginainstellingenbeheer + .menu-orbit = Baan + .menu-new-ucs = Nieuw GCS + .menu-named-views = Benoemde aanzichten + .menu-multiline-text = Multiline tekst + .menu-multiline-style = Multilijnstijl + .menu-multiline = Multilijn + .menu-multileader-style = Multidirectiestijl + .menu-match-properties = Eigenschappen overnemen + .menu-lengthen = Verlengen + .menu-jogged = Geknikt + .menu-id-point = Punt-ID + .menu-explode-text = Tekst exploderen + .menu-edit-spline = Spline bewerken + .menu-edit-polyline = Polylijn bewerken + .menu-edit-hatch = Arcering bewerken + .menu-drawing-limits = Tekeninglimieten + .menu-donut = Ring + .menu-dimension-style = Maatstijl + .menu-construction-line = Constructielijn + .menu-arc-length = Booglengte + .command-line-opacity = Dekking van de opdrachtregel + .txtexp-no-text-selected = TXTEXP: geen tekst geselecteerd. + .angle-override-shown = Hoekoverschrijving: __ocs_fmt_0__ .plot-preview-task = Plotvoorbeeld .link-a-csv-data-source = CSV-gegevensbron koppelen .csv-files = CSV-bestanden .browser-cannot-open-typed-path = De browser kan een getypt pad niet openen; kies het bestand. .recover-drawing = Tekening herstellen + .requires-0-or-1-or-keyword = Alleen 0 of 1, of een optietrefwoord toegestaan. + .requires-a-number = Vereist een getal. .restore-it-as-a-recovered-tab-or-discard-the-copy = Herstel het als een hersteld tabblad of verwijder de kopie. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" heeft een automatisch opgeslagen kopie die nieuwer is dan de laatste opslag. .autosave-copy-could-not-be-stored = Automatische opslagkopie kon niet worden bewaard: __ocs_fmt_0__ @@ -820,6 +894,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: selecteer een doorsnedeobject of zet een live doorsnede aan. + .sectioncull-prepare-failed = SECTIONCULL: een gesneden object kon niet worden voorbereid; er is niets gewijzigd. + .sectioncull-nothing-outside = SECTIONCULL: er ligt niets buiten de doorsnede. + .sectioncull-result = SECTIONCULL: __ocs_arg_erased__ object(en) gewist en __ocs_arg_cut__ afgesneden op de doorsnede. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: pas een spline door __ocs_fmt_0__ punten. .splinefit-need-at-least-3-points = SPLINEFIT: minimaal 3 punten nodig. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: selecteer een polylijn om een spline door te passen. @@ -857,9 +935,12 @@ common = .sent-to-printer-printer = Verzonden aan printer: __ocs_fmt_0__ .set-current = Actief maken .shade-plot = Schaduwplot + .shaded-with-edges = Gearceerd met randen + .shades-of-gray = Grijstinten .shadow-flags = Schaduwvlaggen .single = Enkel .size = Grootte + .sketchy = Schetsmatig .snap-override-label-next-pick-only = Objectmagneet negeren: __ocs_fmt_0__ (alleen volgende selectie). .solid-created = Vast gemaakt. .source-object-not-found = Bronobject niet gevonden. @@ -1065,7 +1146,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: voer Left, Top of Right in. .isodraft-expected-on-or-off = ISODRAFT: voer On of Off in. .xdata-selected-entities-are-on-locked-layers = XDATA: geselecteerde objecten staan op vergrendelde lagen. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: geen rasterafbeeldingen geselecteerd of onbekende eigenschap (gebruik BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: voer een geheel getal van -4 tot en met 4 in. .usage-datalink-path-to-csv = Gebruik: DATALINK .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: geen gekoppelde tabellen gevonden. @@ -2060,7 +2140,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = Bestand .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2369,6 +2449,7 @@ common = .load-linetypes = Lijntypen laden .requires-two-positive-values = Vereist twee positieve waarden (x,y) of รฉรฉn voor beide. .snap-marker-color = Kleur van snapmarkering + .snap-marker-size = Grootte van snapmarkering .command-line-text-color = Tekstkleur opdrachtregel .operand = Operand .linetypes-loaded = __ocs_fmt_0__ lijntype(n) geladen. @@ -2382,6 +2463,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Basispunt [__ocs_arg_count__ objecten] opgeven: .threedrotate-specify-rotation-axis = 3DROTATE Geef de rotatieas op [X/Y/Z] of een tweede punt erop : .threedrotate-specify-rotation-angle-a = 3DROTATE Rotatiehoek <__ocs_arg_a__> opgeven: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Waarschuwing opslaan .unable-to-save-drawing = Kan tekening niet opslaan @@ -3005,6 +3093,7 @@ draw = .arc-specify-radius-of-arc = ARC Geef straal van boog op: .chord-length = Koordelengte + modify = .add-vertex = Hoekpunt toevoegen .closed-cross = Gesloten kruis @@ -3050,6 +3139,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Selecteer symbool met attributen: .annotation-object-scale = Annotatie objectschaal .bedit-select-block-reference-to-edit = BEDIT Selecteer symboolverwijzing naar bewerken: + .bvstate-option = BVSTATE Geef een optie [Nieuw/Instellen/Hernoemen/Verwijderen] : + .bvstate-new-name = BVSTATE Geef de naam van de nieuwe zichtbaarheidsstatus: + .bvstate-set = BVSTATE Geef de zichtbaarheidsstatus die huidig wordt <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Geef de te hernoemen zichtbaarheidsstatus <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Geef de nieuwe naam: + .bvstate-delete = BVSTATE Geef de te verwijderen zichtbaarheidsstatus: + .bvstate-not-found = Zichtbaarheidsstatus "__ocs_fmt_0__" niet gevonden. + .bvstate-exists = Zichtbaarheidsstatus "__ocs_fmt_0__" bestaat al. + .bvstate-last = De enige zichtbaarheidsstatus kan niet worden verwijderd. + .bvstate-needs-bedit = __ocs_fmt_0__: open het blok eerst in de blokeditor (BEDIT). + .bvstate-current = Huidige zichtbaarheidsstatus: __ocs_fmt_0__ + .bvhide-select = Selecteer te verbergen objecten: + .bvshow-select = Selecteer zichtbaar te maken objecten: + .arrayedit-select-array = ARRAYEDIT Selecteer de reeks: + .arrayedit-options = ARRAYEDIT Geef een optie [Bron/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: het geselecteerde object is geen associatieve reeks. .blend-enter-continuity-tangent-curvature-c = BLEND Vul continuรฏteit [tangent/curvature] <__ocs_arg_c__> in: .blend-select-first-open-curve-continuity-c = BLEND Selecteer eerste open curve [Continuรฏteit=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Selecteer tweede open curve [Continuรฏteit=__ocs_arg_c__]: @@ -3600,6 +3705,8 @@ annotate = .dimradius-select-arc-circle-or-polyline-arc = DIMRADIUS Selecteer een boog, cirkel of polylijnboog: insert = + .pastespec-specify-insertion-point = PASTESPEC Geef invoegpunt op: + .pastespec-paste-as = PASTESPEC Plakken als [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3716,6 +3823,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3770,6 +3884,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Onderleggers-OSnap verschilt* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3909,6 +4024,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Selecteer afbeelding of onderlegger: + .adjust-select-pdf-underlay = Selecteer PDF-onderlegger: + .adjust-select-dwf-underlay = Selecteer DWF-onderlegger: + .adjust-select-dgn-underlay = Selecteer DGN-onderlegger: + .adjust-mixed-underlays = *Ongeldige selectieset. Selecteer alleen onderleggers van hetzelfde bestandstype.* + .adjust-image-option = Voer afbeeldingsoptie in [Contrast/Transparantie/Helderheid] : + .adjust-underlay-option = Voer __ocs_arg_kind__-onderleggeroptie in [Transparantie/Contrast/Monochroom] <__ocs_arg_option__>: + .adjust-monochrome = Monochroom? [Ja/Nee] <__ocs_arg_value__>: + .adjust-brightness-value = Voer waarde voor helderheid in (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Voer waarde voor contrast in (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Voer waarde voor transparantie in (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = De waarde moet een geheel getal tussen 0 en 100 zijn. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4413,7 +4540,27 @@ model = .value-2point = 2-punt .cube = Kubus + layout = + .mvsetup-select-objects-to-delete = MVSETUP Selecteer de te verwijderen objecten: + .delete-objects-option = Objecten verwijderen + .mvsetup-enable-paper-space = MVSETUP Papierruimte inschakelen? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Geef schaalfactor op <1>: + .mvsetup-enter-paper-width = MVSETUP Geef papierbreedte op: + .mvsetup-enter-paper-height = MVSETUP Geef papierhoogte op: + .mvsetup-enter-option = MVSETUP Geef optie op [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Geef viewportindeling op [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Geef eerste hoek van het viewportgebied op : + .mvsetup-specify-opposite-corner = MVSETUP Geef tegenoverliggende hoek op: + .mvsetup-viewports-in-x = MVSETUP Geef aantal viewports in X-richting op <2>: + .mvsetup-viewports-in-y = MVSETUP Geef aantal viewports in Y-richting op <2>: + .mvsetup-distance-in-x = MVSETUP Geef afstand tussen viewports in X-richting op <0>: + .mvsetup-distance-in-y = MVSETUP Geef afstand tussen viewports in Y-richting op <0>: + .mvsetup-select-viewports = MVSETUP Selecteer de te schalen viewports: + .mvsetup-paper-space-units = MVSETUP Geef aantal papierruimte-eenheden op <1>: + .mvsetup-model-space-units = MVSETUP Geef aantal modelruimte-eenheden op <1>: + .mvsetup-viewports-do-not-fit = MVSETUP De viewports passen niet in het gebied. + .scale-viewports-option = Viewports schalen .active = Actief .inactive = Inactief .insert-view = Beeld invoegen @@ -4439,6 +4586,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Bevriezen | T = Ontdooien | Enter = Afsluiten view = + .twist-option = Draaien + .dview-specify-view-twist-angle = DVIEW Geef weergavedraaihoek op <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Geef optie op [TWist/Undo]: .color-scheme-set-to-system-currently-name = Kleurenschema ingesteld op 'Systeem' (momenteel __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = Vereist een geheel getal tussen __ocs_fmt_0__ en __ocs_fmt_1__. @@ -4633,13 +4783,6 @@ view = .x-no-xdata = __ocs_fmt_0__: geen xdata. .value-3d-orbit = 3D-baan .value-3d-orbit-drag-with-right-mouse-button = 3D Orbit: sleep met de rechter muisknop. - .adjust-brightness-contrast-fade = ADJUST [Helderheid / Contrast / Vervagen]: - .adjust-brightness-0-100 = ADJUST helderheid 0-100: - .adjust-contrast-0-100 = ADJUST contrast 0-100: - .adjust-fade-0-100 = ADJUST vervaging 0-100: - .adjust-select-raster-image-s-first = ADJUST: selecteer eerst rasterafbeelding(s). - .adjust-value-must-be-0-100 = ADJUST: waarde moet 0-100 zijn. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ op __ocs_fmt_2__ image(s). .annoallvisible-new-value-0-1 = ANNOALLVISIBLE nieuwe waarde [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: vul 0 of 1 in. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE nieuwe waarde [-4..4]: @@ -4765,7 +4908,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Onderlaag __ocs_fmt_0__: fade=__ocs_fmt_1__, contrast=__ocs_fmt_2__, on=__ocs_fmt_3__, clip=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = Bijgewerkt __ocs_fmt_0__ underlay(s). - .usage-adjust-brightness-contrast-fade-0-100 = Gebruik: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Gebruik: ANNOSCALE e.g. 1:50, 2:1, or a factor .usage-draworder-f-front-b-back-a-above-handle = Gebruik: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Gebruik: HYPERLINK (select objects first) @@ -5993,6 +6135,46 @@ ui = .osnap-settings = Objectsnap-instellingen... .snap-override-none-next-pick-only = Tijdelijke snap: Geen (alleen volgend punt). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Tekst altijd tussen hulplijnen houden .angular-dimensions = Hoekmaten @@ -6260,3 +6442,117 @@ graphic-attributes = .reset-angle = Hoek herstellen .one-color = Eรฉn kleur .two-colors = Twee kleuren + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Toevoegen aan werkset + .remove-from-working-set = Verwijderen uit werkset + .toolbar-objects = objecten + .dialog-title = Referentie bewerken + .dialog-reference = Te bewerken referentie + .dialog-chain-hint = De keten bij het gekozen punt; klik op een label om het niveau te wijzigen. + .dialog-path = Pad: + .dialog-working-set = Werkset + .dialog-all-objects = Alle objecten + .dialog-pick-objects = In de tekening kiezen + .dialog-attributes = Ook attribuutdefinities bewerken + .dialog-session = Sessie + .dialog-lock = Objecten buiten de werkset vergrendelen + .dialog-unique = Conflicterende laag-, stijl- en bloknamen unieke namen geven + .dialog-start = Bewerken starten + .dialog-help = Opent een blokreferentie of externe referentie om ter plaatse te bewerken. + .select-reference = Selecteer referentie: + .reference-not-found = Referentie niet gevonden. + .not-valid-reference = Object is geen geldige referentie + .one-entity-added = 1 entiteit toegevoegd + .nesting-level = Selecteer nestingsniveau [Ok/Volgende] : + .selection-method = Geef objectselectiemethode [Alle/Genest] : + .select-nested = Selecteer geneste objecten: + .display-attdefs = Attribuutdefinities weergeven [Ja/Nee] : + .zero-items-selected = 0 items geselecteerd + .one-item-selected = 1 item geselecteerd + .items-selected = __ocs_fmt_0__ items geselecteerd + .refset-option = Geef een optie [Toevoegen/Verwijderen] : + .refclose-option = Geef optie [Opslaan/Referentiewijzigingen negeren] : + .not-checked-out = ** Opdracht niet toegestaan tenzij een referentie met REFEDIT wordt bewerkt ** + .already-checked-out = ** Opdracht niet toegestaan, __ocs_fmt_0__ wordt al bewerkt ** + .transfer-objects = Objecten overbrengen tussen de RefEdit-werkset en de hosttekening... + .added-to-working-set = __ocs_fmt_0__ toegevoegd aan werkset + .removed-from-working-set = __ocs_fmt_0__ verwijderd uit werkset + .one-object-added = 1 object toegevoegd aan __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ objecten toegevoegd aan __ocs_fmt_1__ + .one-object-removed = 1 object verwijderd uit __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ objecten verwijderd uit __ocs_fmt_1__ + .one-xref-instance = 1 xref-exemplaar bijgewerkt + .xref-instances = __ocs_fmt_0__ xref-exemplaren bijgewerkt + .one-block-instance = 1 blokexemplaar bijgewerkt + .block-instances = __ocs_fmt_0__ blokexemplaren bijgewerkt + .redefined-reloaded = __ocs_fmt_0__ opnieuw gedefinieerd en opnieuw geladen. + .redefined = __ocs_fmt_0__ opnieuw gedefinieerd. + .ok = Ok + .nested = Genest + +image-adjust = + .title = Afbeelding aanpassen + .preview = Voorbeeld + .before = Voor + .after = Na + .no-preview = afbeelding kon niet worden geladen + .adjust = Aanpassen + .brightness = Helderheid + .contrast = Contrast + .fade = Vervagen + .hint = Klik of sleep een balk; โˆ’ / + is รฉรฉn stap, een vak neemt een geheel getal 0โ€“100. + .reset = Standaardwaarden herstellen + .apply = Toepassen + .help = Stelt helderheid, contrast en vervaging van de geselecteerde rasterafbeeldingen in. + +keytips = + .title = KeyTip + .typed = Getypt: { $path } + .hint-tabs = Typ de letter van een tabblad ยท Esc sluit + .hint-tab = Tabblad { $tab } ยท Esc gaat รฉรฉn niveau terug + .hint-items = Typ de letter van een item ยท Esc gaat รฉรฉn niveau terug diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 92554bcf4..732b758ec 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Wtyczki ribbon-tab = + .block-editor = Edytor blokรณw .draw = Rysowanie .parametric = Parametryczne .annotate = Opisy @@ -98,6 +99,9 @@ start = .no-recent-files = Otwarte pliki pojawiฤ… siฤ™ tutaj. .browser-storage = Pamiฤ™ฤ‡ przeglฤ…darki .keep-recent-files = Zachowuj ostatnie pliki + .view-list = Lista + .view-icons = Ikony + .view-preview = Duลผe podglฤ…dy donation = .title = Proล›ba o darowiznฤ™ @@ -167,6 +171,14 @@ modal = .that-object = ten obiekt .those-objects = te obiekty + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Wklejono obraz "{ $name }": { $width }ร—{ $height } px .image-unsupported = Nie udaล‚o siฤ™ zdekodowaฤ‡ wklejonego obrazu "{ $name }". @@ -273,11 +285,73 @@ command-copy = .mode = COPY Podaj tryb kopiowania [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ tylko tutaj (zielone), __ocs_fmt_1__ tylko w drugim rysunku (czerwone)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Porรณwnaj z rysunkiem + .label-associative-array-created = __ocs_fmt_0__: utworzono szyk skojarzony. + .assoc-array-array-path = Szyk (ล›cieลผka) + .assoc-array-array-polar = Szyk (biegunowy) + .assoc-array-array-rectangular = Szyk (prostokฤ…tny) + .assoc-array-item-spacing = Odstฤ™p elementรณw + .assoc-array-fill-angle = Kฤ…t wypeล‚nienia + .assoc-array-items = Elementy + .assoc-array-level-spacing = Odstฤ™p poziomรณw + .assoc-array-levels = Poziomy + .assoc-array-row-spacing = Odstฤ™p wierszy + .assoc-array-column-spacing = Odstฤ™p kolumn + .fbxexport-exported-to = FBXEXPORT: wyeksportowano do โ€ž__ocs_fmt_0__โ€ + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: brak danych siatki 3D na tym rysunku. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Nowe kreskowania podฤ…ลผajฤ… za obwiedniami (HPASSOC) + .options-show-ribbon = Pokaลผ wstฤ…ลผkฤ™ (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Pokaลผ pasek menu (MENUBAR) + .options-window-elements = Elementy okna + .menu-viewports = Rzutnie + .menu-view-twist = Skrฤ™cenie widoku + .menu-tools = Narzฤ™dzia + .menu-table-style = Styl tabeli + .menu-run-script = Uruchom skrypt + .menu-revision-cloud = Chmurka rewizji + .menu-regen-all = Regeneruj wszystko + .menu-redraw = Odล›wieลผ + .menu-rectangle = Prostokฤ…t + .menu-quick-dimension = Szybki wymiar + .menu-publish = Publikuj + .menu-paste-special = Wklej specjalnie + .menu-parametric = Parametryczne + .menu-page-setup-manager = Menedลผer ustawieล„ strony + .menu-orbit = Orbita + .menu-new-ucs = Nowy LUW + .menu-named-views = Widoki nazwane + .menu-multiline-text = Tekst wielowierszowy + .menu-multiline-style = Styl multilinii + .menu-multiline = Multilinia + .menu-multileader-style = Styl wielolinii odniesienia + .menu-match-properties = Dopasuj wล‚aล›ciwoล›ci + .menu-lengthen = Wydล‚uลผ + .menu-jogged = ลamany + .menu-id-point = Wspรณล‚rzฤ™dne punktu + .menu-explode-text = Rozbij tekst + .menu-edit-spline = Edytuj splajn + .menu-edit-polyline = Edytuj poliliniฤ™ + .menu-edit-hatch = Edytuj kreskowanie + .menu-drawing-limits = Granice rysunku + .menu-donut = Pierล›cieล„ + .menu-dimension-style = Styl wymiarowania + .menu-construction-line = Linia konstrukcyjna + .menu-arc-length = Dล‚ugoล›ฤ‡ ล‚uku + .command-line-opacity = Krycie wiersza poleceล„ + .txtexp-no-text-selected = TXTEXP: nie wybrano tekstu. + .angle-override-shown = Zastฤ…pienie kฤ…ta: __ocs_fmt_0__ .plot-preview-task = Podglฤ…d wydruku .link-a-csv-data-source = Poล‚ฤ…cz ลบrรณdล‚o danych CSV .csv-files = Pliki CSV .browser-cannot-open-typed-path = Przeglฤ…darka nie otworzy wpisanej ล›cieลผki; wybierz plik. .recover-drawing = Odzyskaj rysunek + .requires-0-or-1-or-keyword = Wymaga wyล‚ฤ…cznie 0 lub 1 albo sล‚owa kluczowego opcji. + .requires-a-number = Wymaga liczby. .restore-it-as-a-recovered-tab-or-discard-the-copy = Przywrรณฤ‡ go jako odzyskanฤ… kartฤ™ lub odrzuฤ‡ kopiฤ™. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" ma kopiฤ™ autozapisu nowszฤ… niลผ ostatni zapis. .autosave-copy-could-not-be-stored = Nie moลผna zapisaฤ‡ kopii autozapisu: __ocs_fmt_0__ @@ -821,6 +895,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: wybierz obiekt przekroju lub wล‚ฤ…cz przekrรณj dynamiczny. + .sectioncull-prepare-failed = SECTIONCULL: nie udaล‚o siฤ™ przygotowaฤ‡ przeciฤ™tego obiektu; nic nie zmieniono. + .sectioncull-nothing-outside = SECTIONCULL: nic nie leลผy poza przekrojem. + .sectioncull-result = SECTIONCULL: usuniฤ™to __ocs_arg_erased__ obiekt(รณw) i przyciฤ™to __ocs_arg_cut__ na granicy przekroju. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: dopasuj splajn przez punkty __ocs_fmt_0__. .splinefit-need-at-least-3-points =SPLINEFIT: potrzebne sฤ… co najmniej 3 punkty. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: wybierz poliliniฤ™, przez ktรณrฤ… ma zostaฤ‡ dopasowany splajn. @@ -858,9 +936,12 @@ common = .sent-to-printer-printer = Wysล‚ano do drukarki: __ocs_fmt_0__ .set-current = Ustaw prฤ…d .shade-plot = Fabuล‚a cienia + .shaded-with-edges = Cieniowany z krawฤ™dziami + .shades-of-gray = Odcienie szaroล›ci .shadow-flags = Flagi cienia .single = Pojedynczy .size = Rozmiar + .sketchy = Szkicowy .snap-override-label-next-pick-only = Nadpisanie przyciฤ…gania: __ocs_fmt_0__ (tylko nastฤ™pny wybรณr). .solid-created = Utworzono bryล‚ฤ™. .source-object-not-found = Nie znaleziono obiektu ลบrรณdล‚owego. @@ -1066,7 +1147,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: wpisz Left, Top lub Right. .isodraft-expected-on-or-off = ISODRAFT: wpisz On lub Off. .xdata-selected-entities-are-on-locked-layers = XDATA: zaznaczone obiekty znajdujฤ… siฤ™ na zablokowanych warstwach. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: brak zaznaczonych obrazรณw rastrowych lub nieznana wล‚aล›ciwoล›ฤ‡ (uลผyj BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: wpisz liczbฤ™ caล‚kowitฤ… od -4 do 4. .usage-datalink-path-to-csv = Uลผycie: DATALINK <ล›cieลผka-do-.csv> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: nie znaleziono poล‚ฤ…czonych tabel. @@ -2059,7 +2139,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = Plik .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2368,6 +2448,7 @@ common = .load-linetypes = Wczytaj rodzaje linii .requires-two-positive-values = Wymaga dwรณch wartoล›ci dodatnich (x,y) lub jednej dla obu. .snap-marker-color = Kolor znacznika przyciฤ…gania + .snap-marker-size = Rozmiar znacznika przyciฤ…gania .command-line-text-color = Kolor tekstu wiersza poleceล„ .operand = Operand .linetypes-loaded = Wczytano rodzaje linii: __ocs_fmt_0__. @@ -2381,6 +2462,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Okreล›l punkt bazowy [obiekty __ocs_arg_count__]: .threedrotate-specify-rotation-axis = 3DROTATE Okreล›l oล› obrotu [X/Y/Z] lub drugi punkt na niej : .threedrotate-specify-rotation-angle-a = 3DROTATE Okreล›l kฤ…t obrotu <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Zapisz ostrzeลผenie .unable-to-save-drawing = Nie moลผna zapisaฤ‡ rysunku @@ -3004,6 +3092,7 @@ draw = .arc-specify-radius-of-arc = ARC Okreล›l promieล„ ล‚uku: .chord-length = Dล‚ugoล›ฤ‡ ciฤ™ciwy + modify = .add-vertex = Dodaj wierzchoล‚ek .closed-cross = Zamkniฤ™te skrzyลผowanie @@ -3049,6 +3138,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Wybierz blok z atrybutami: .annotation-object-scale = Skala obiektu adnotacji .bedit-select-block-reference-to-edit = BEDIT Wybierz odniesienie do bloku do edycji: + .bvstate-option = BVSTATE Podaj opcjฤ™ [Nowy/Ustaw/Zmieล„ nazwฤ™/Usuล„] : + .bvstate-new-name = BVSTATE Podaj nazwฤ™ nowego stanu widocznoล›ci: + .bvstate-set = BVSTATE Podaj stan widocznoล›ci, ktรณry ma byฤ‡ bieลผฤ…cy <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Podaj stan widocznoล›ci do zmiany nazwy <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Podaj nowฤ… nazwฤ™: + .bvstate-delete = BVSTATE Podaj stan widocznoล›ci do usuniฤ™cia: + .bvstate-not-found = Nie znaleziono stanu widocznoล›ci "__ocs_fmt_0__". + .bvstate-exists = Stan widocznoล›ci "__ocs_fmt_0__" juลผ istnieje. + .bvstate-last = Nie moลผna usunฤ…ฤ‡ jedynego stanu widocznoล›ci. + .bvstate-needs-bedit = __ocs_fmt_0__: najpierw otwรณrz blok w edytorze blokรณw (BEDIT). + .bvstate-current = Bieลผฤ…cy stan widocznoล›ci: __ocs_fmt_0__ + .bvhide-select = Wybierz obiekty do ukrycia: + .bvshow-select = Wybierz obiekty do pokazania: + .arrayedit-select-array = ARRAYEDIT Wybierz szyk: + .arrayedit-options = ARRAYEDIT Podaj opcjฤ™ [ลนrรณdล‚o/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: wybrany obiekt nie jest szykiem skojarzonym. .blend-enter-continuity-tangent-curvature-c = BLEND Enter ciฤ…gล‚oล›ฤ‡ [styczna/krzywizna] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND Wybierz pierwszฤ… otwartฤ… krzywฤ… [Continuity=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Wybierz drugฤ… otwartฤ… krzywฤ… [Continuity=__ocs_arg_c__]: @@ -3601,6 +3706,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Okreล›l punkt wstawienia: insert = + .pastespec-specify-insertion-point = PASTESPEC Okreล›l punkt wstawienia: + .pastespec-paste-as = PASTESPEC Wklej jako [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3717,6 +3824,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3771,6 +3885,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Lokalizacje podkล‚adรณw sฤ… rรณลผne* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3910,6 +4025,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Wybierz obraz lub podkล‚ad: + .adjust-select-pdf-underlay = Wybierz podkล‚ad PDF: + .adjust-select-dwf-underlay = Wybierz podkล‚ad DWF: + .adjust-select-dgn-underlay = Wybierz podkล‚ad DGN: + .adjust-mixed-underlays = *Nieprawidล‚owy zbiรณr wskazaล„. Wybierz tylko podkล‚ady tego samego typu pliku.* + .adjust-image-option = Podaj opcjฤ™ obrazu [Kontrast/Zniknฤ…ฤ‡/Jasnoล›ฤ‡] : + .adjust-underlay-option = Podaj opcjฤ™ podkล‚adu __ocs_arg_kind__ [Zniknฤ…ฤ‡/Kontrast/Monochromatyczny] <__ocs_arg_option__>: + .adjust-monochrome = Monochromatyczny? [Tak/Nie] <__ocs_arg_value__>: + .adjust-brightness-value = Podaj wartoล›ฤ‡: jasnoล›ฤ‡ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Podaj wartoล›ฤ‡: kontrast (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Podaj wartoล›ฤ‡: zniknฤ…ฤ‡ (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = Wartoล›ฤ‡ musi byฤ‡ liczbฤ… caล‚kowitฤ… z zakresu od 0 do 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4414,7 +4541,27 @@ model = .value-2point = 2-punktowy .cube = Szeล›cian + layout = + .mvsetup-select-objects-to-delete = MVSETUP Wybierz obiekty do usuniฤ™cia: + .delete-objects-option = Usuล„ obiekty + .mvsetup-enable-paper-space = MVSETUP Wล‚ฤ…czyฤ‡ obszar papieru? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Podaj wspรณล‚czynnik skali <1>: + .mvsetup-enter-paper-width = MVSETUP Podaj szerokoล›ฤ‡ papieru: + .mvsetup-enter-paper-height = MVSETUP Podaj wysokoล›ฤ‡ papieru: + .mvsetup-enter-option = MVSETUP Podaj opcjฤ™ [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Podaj ukล‚ad rzutni [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Okreล›l pierwszy naroลผnik obszaru rzutni : + .mvsetup-specify-opposite-corner = MVSETUP Okreล›l przeciwlegล‚y naroลผnik: + .mvsetup-viewports-in-x = MVSETUP Podaj liczbฤ™ rzutni w kierunku X <2>: + .mvsetup-viewports-in-y = MVSETUP Podaj liczbฤ™ rzutni w kierunku Y <2>: + .mvsetup-distance-in-x = MVSETUP Okreล›l odstฤ™p miฤ™dzy rzutniami w kierunku X <0>: + .mvsetup-distance-in-y = MVSETUP Okreล›l odstฤ™p miฤ™dzy rzutniami w kierunku Y <0>: + .mvsetup-select-viewports = MVSETUP Wybierz rzutnie do skalowania: + .mvsetup-paper-space-units = MVSETUP Podaj liczbฤ™ jednostek obszaru papieru <1>: + .mvsetup-model-space-units = MVSETUP Podaj liczbฤ™ jednostek obszaru modelu <1>: + .mvsetup-viewports-do-not-fit = MVSETUP Rzutnie nie mieszczฤ… siฤ™ w obszarze. + .scale-viewports-option = Skaluj rzutnie .active = Aktywny .inactive = Nieaktywny .insert-view = Wstaw widok @@ -4440,6 +4587,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Zamroลผenie | T = Odwilลผ | Enter = Wyjล›cie view = + .twist-option = Skrฤ™cenie + .dview-specify-view-twist-angle = DVIEW Okreล›l kฤ…t skrฤ™cenia widoku <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Podaj opcjฤ™ [TWist/Undo]: .color-scheme-set-to-system-currently-name = Schemat kolorรณw ustawiono na โ€žSystemโ€ (obecnie __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = Wymagana liczba caล‚kowita z zakresu od __ocs_fmt_0__ do __ocs_fmt_1__. @@ -4634,13 +4784,6 @@ view = .x-no-xdata = __ocs_fmt_0__: brak xdata. .value-3d-orbit =Orbita 3D .value-3d-orbit-drag-with-right-mouse-button = Orbita 3D: przeciฤ…gnij prawym przyciskiem myszy. - .adjust-brightness-contrast-fade = ADJUST [Jasnoล›ฤ‡ / Kontrast / Zanik]: - .adjust-brightness-0-100 = ADJUST jasnoล›ฤ‡ 0-100: - .adjust-contrast-0-100 = ADJUST kontrast 0-100: - .adjust-fade-0-100 = ADJUST zanikanie 0-100: - .adjust-select-raster-image-s-first = ADJUST: najpierw wybierz obrazy rastrowe. - .adjust-value-must-be-0-100 = ADJUST: wartoล›ฤ‡ musi wynosiฤ‡ 0-100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ na obrazach __ocs_fmt_2__. .annoallvisible-new-value-0-1 = ANNOALLVISIBLE nowa wartoล›ฤ‡ [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: wprowadลบ 0 lub 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE nowa wartoล›ฤ‡ [-4..4]: @@ -4766,7 +4909,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Podkล‚ad __ocs_fmt_0__: fade=__ocs_fmt_1__, kontrast=__ocs_fmt_2__, on=__ocs_fmt_3__, klip=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = Zaktualizowano podkล‚adki __ocs_fmt_0__. - .usage-adjust-brightness-contrast-fade-0-100 = Zastosowanie: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Zastosowanie: ANNOSCALE np. 1:50, 2:1 lub wspรณล‚czynnik .usage-draworder-f-front-b-back-a-above-handle = Zastosowanie: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Sposรณb uลผycia: HYPERLINK (najpierw wybierz obiekty) @@ -5994,6 +6136,46 @@ ui = .osnap-settings = Ustawienia lokalizacji... .snap-override-none-next-pick-only = Tymczasowa lokalizacja: Brak (tylko nastฤ™pny punkt). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Zawsze przechowuj tekst pomiฤ™dzy liniami pomocniczymi .angular-dimensions = Wymiary kฤ…towe @@ -6261,3 +6443,117 @@ graphic-attributes = .reset-angle = Resetuj kฤ…t .one-color = Jeden kolor .two-colors = Dwa kolory + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Dodaj do zbioru roboczego + .remove-from-working-set = Usuล„ ze zbioru roboczego + .toolbar-objects = obiekty + .dialog-title = Edycja odnoล›nika + .dialog-reference = Odnoล›nik do edycji + .dialog-chain-hint = ลaล„cuch w wybranym punkcie; kliknij etykietฤ™, aby zmieniฤ‡ poziom. + .dialog-path = ลšcieลผka: + .dialog-working-set = Zbiรณr roboczy + .dialog-all-objects = Wszystkie obiekty + .dialog-pick-objects = Wskaลผ na rysunku + .dialog-attributes = Edytuj teลผ definicje atrybutรณw + .dialog-session = Sesja + .dialog-lock = Zablokuj obiekty spoza zbioru roboczego + .dialog-unique = Nadaj unikatowe nazwy kolidujฤ…cym warstwom, stylom i blokom + .dialog-start = Rozpocznij edycjฤ™ + .dialog-help = Otwiera odniesienie bloku lub odnoล›nik zewnฤ™trzny do edycji w miejscu. + .select-reference = Wybierz odnoล›nik: + .reference-not-found = Nie znaleziono odnoล›nika. + .not-valid-reference = Obiekt nie jest prawidล‚owym odnoล›nikiem + .one-entity-added = Dodano 1 obiekt + .nesting-level = Wybierz poziom zagnieลผdลผenia [Ok/Nastฤ™pny] : + .selection-method = Podaj metodฤ™ wyboru obiektรณw [Wszystkie/Zagnieลผdลผone] : + .select-nested = Wybierz zagnieลผdลผone obiekty: + .display-attdefs = Wyล›wietliฤ‡ definicje atrybutรณw [Tak/Nie] : + .zero-items-selected = Wybrano 0 elementรณw + .one-item-selected = Wybrano 1 element + .items-selected = Wybrano elementรณw: __ocs_fmt_0__ + .refset-option = Podaj opcjฤ™ [Dodaj/Usuล„] : + .refclose-option = Podaj opcjฤ™ [Zapisz/Odrzuฤ‡ zmiany odnoล›nika] : + .not-checked-out = ** Polecenie niedozwolone, dopรณki odnoล›nik nie jest edytowany poleceniem REFEDIT ** + .already-checked-out = ** Polecenie niedozwolone, __ocs_fmt_0__ jest juลผ edytowany ** + .transfer-objects = Przenoszenie obiektรณw miฤ™dzy zbiorem roboczym RefEdit a rysunkiem gล‚รณwnym... + .added-to-working-set = Dodano do zbioru roboczego: __ocs_fmt_0__ + .removed-from-working-set = Usuniฤ™to ze zbioru roboczego: __ocs_fmt_0__ + .one-object-added = Dodano 1 obiekt do __ocs_fmt_0__ + .objects-added = Dodano __ocs_fmt_0__ obiektรณw do __ocs_fmt_1__ + .one-object-removed = Usuniฤ™to 1 obiekt z __ocs_fmt_0__ + .objects-removed = Usuniฤ™to __ocs_fmt_0__ obiektรณw z __ocs_fmt_1__ + .one-xref-instance = Zaktualizowano 1 wystฤ…pienie odnoล›nika + .xref-instances = Zaktualizowano wystฤ…pieล„ odnoล›nika: __ocs_fmt_0__ + .one-block-instance = Zaktualizowano 1 wystฤ…pienie bloku + .block-instances = Zaktualizowano wystฤ…pieล„ bloku: __ocs_fmt_0__ + .redefined-reloaded = Przedefiniowano i ponownie wczytano __ocs_fmt_0__. + .redefined = Przedefiniowano __ocs_fmt_0__. + .ok = Ok + .nested = Zagnieลผdลผone + +image-adjust = + .title = Dopasowanie obrazu + .preview = Podglฤ…d + .before = Przed + .after = Po + .no-preview = nie moลผna wczytaฤ‡ obrazu + .adjust = Dopasowanie + .brightness = Jasnoล›ฤ‡ + .contrast = Kontrast + .fade = Zanikanie + .hint = Kliknij lub przeciฤ…gnij pasek; โˆ’ / + to jeden krok, pole przyjmuje liczbฤ™ caล‚kowitฤ… 0โ€“100. + .reset = Przywrรณฤ‡ domyล›lne + .apply = Zastosuj + .help = Ustawia jasnoล›ฤ‡, kontrast i zanikanie wybranych obrazรณw rastrowych. + +keytips = + .title = KeyTip + .typed = Wpisano: { $path } + .hint-tabs = Wpisz literฤ™ karty ยท Esc zamyka + .hint-tab = Karta { $tab } ยท Esc cofa o jeden poziom + .hint-items = Wpisz literฤ™ elementu ยท Esc cofa o jeden poziom diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index e31741d24..9c28f9dc1 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Plugins ribbon-tab = + .block-editor = Editor de blocos .draw = Desenhar .parametric = Paramรฉtrico .annotate = Anotar @@ -98,6 +99,9 @@ start = .no-recent-files = Os arquivos que vocรช abrir aparecerรฃo aqui. .browser-storage = Armazenamento do navegador .keep-recent-files = Manter arquivos recentes + .view-list = Lista + .view-icons = รcones + .view-preview = Visualizaรงรตes grandes donation = .title = Pedido de doaรงรฃo @@ -167,6 +171,14 @@ modal = .that-object = esse objeto .those-objects = esses objetos + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = Imagem "{ $name }" colada: { $width }ร—{ $height } px .image-unsupported = Nรฃo foi possรญvel decodificar a imagem colada "{ $name }". @@ -274,11 +286,73 @@ command-copy = .mode = COPY Informe o modo de cรณpia [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ sรณ aqui (verde), __ocs_fmt_1__ sรณ no outro desenho (vermelho)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Comparar com desenho + .label-associative-array-created = __ocs_fmt_0__: matriz associativa criada. + .assoc-array-array-path = Matriz (caminho) + .assoc-array-array-polar = Matriz (polar) + .assoc-array-array-rectangular = Matriz (retangular) + .assoc-array-item-spacing = Espaรงamento dos itens + .assoc-array-fill-angle = ร‚ngulo de preenchimento + .assoc-array-items = Itens + .assoc-array-level-spacing = Espaรงamento dos nรญveis + .assoc-array-levels = Nรญveis + .assoc-array-row-spacing = Espaรงamento das linhas + .assoc-array-column-spacing = Espaรงamento das colunas + .fbxexport-exported-to = FBXEXPORT: exportado para โ€œ__ocs_fmt_0__โ€ + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: nรฃo hรก dados de malha 3D neste desenho. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Novas hachuras acompanham seus contornos (HPASSOC) + .options-show-ribbon = Mostrar a faixa de opรงรตes (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Mostrar a barra de menus (MENUBAR) + .options-window-elements = Elementos da janela + .menu-viewports = Janelas de visualizaรงรฃo + .menu-view-twist = Girar vista + .menu-tools = Ferramentas + .menu-table-style = Estilo de tabela + .menu-run-script = Executar script + .menu-revision-cloud = Nuvem de revisรฃo + .menu-regen-all = Regenerar tudo + .menu-redraw = Redesenhar + .menu-rectangle = Retรขngulo + .menu-quick-dimension = Cota rรกpida + .menu-publish = Publicar + .menu-paste-special = Colar especial + .menu-parametric = Paramรฉtrico + .menu-page-setup-manager = Gerenciador de configuraรงรฃo de pรกgina + .menu-orbit = ร“rbita + .menu-new-ucs = Novo UCS + .menu-named-views = Vistas nomeadas + .menu-multiline-text = Texto multilinha + .menu-multiline-style = Estilo de multilinha + .menu-multiline = Multilinha + .menu-multileader-style = Estilo de multidirecionador + .menu-match-properties = Corresponder propriedades + .menu-lengthen = Alongar + .menu-jogged = Com quebra + .menu-id-point = Ponto ID + .menu-explode-text = Explodir texto + .menu-edit-spline = Editar spline + .menu-edit-polyline = Editar polilinha + .menu-edit-hatch = Editar hachura + .menu-drawing-limits = Limites do desenho + .menu-donut = Rosca + .menu-dimension-style = Estilo de cota + .menu-construction-line = Linha de construรงรฃo + .menu-arc-length = Comprimento do arco + .command-line-opacity = Opacidade da linha de comando + .txtexp-no-text-selected = TXTEXP: nenhum texto selecionado. + .angle-override-shown = Substituiรงรฃo de รขngulo: __ocs_fmt_0__ .plot-preview-task = Visualizaรงรฃo da plotagem .link-a-csv-data-source = Vincular uma fonte de dados CSV .csv-files = Arquivos CSV .browser-cannot-open-typed-path = O navegador nรฃo pode abrir um caminho digitado; escolha o ficheiro. .recover-drawing = Recuperar desenho + .requires-0-or-1-or-keyword = Requer apenas 0 ou 1, ou uma palavra-chave de opรงรฃo. + .requires-a-number = Requer um nรบmero. .restore-it-as-a-recovered-tab-or-discard-the-copy = Restaure-o como uma guia recuperada ou descarte a cรณpia. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" tem uma cรณpia de salvamento automรกtico mais recente que o รบltimo salvamento. .autosave-copy-could-not-be-stored = Nรฃo foi possรญvel armazenar a cรณpia de salvamento automรกtico: __ocs_fmt_0__ @@ -822,6 +896,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: selecione um objeto de seรงรฃo ou ative uma seรงรฃo dinรขmica. + .sectioncull-prepare-failed = SECTIONCULL: nรฃo foi possรญvel preparar um objeto cortado; nada foi alterado. + .sectioncull-nothing-outside = SECTIONCULL: nada estรก fora da seรงรฃo. + .sectioncull-result = SECTIONCULL: __ocs_arg_erased__ objeto(s) apagado(s) e __ocs_arg_cut__ cortado(s) no limite da seรงรฃo. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: spline ajustada atravรฉs de __ocs_fmt_0__ pontos. .splinefit-need-at-least-3-points = SPLINEFIT: sรฃo necessรกrios pelo menos 3 pontos. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: selecione uma polilinha atravรฉs da qual ajustar uma spline. @@ -859,9 +937,12 @@ common = .sent-to-printer-printer = Enviado ร  impressora: __ocs_fmt_0__ .set-current = Definir como atual .shade-plot = Plotagem sombreada + .shaded-with-edges = Sombreado com arestas + .shades-of-gray = Tons de cinza .shadow-flags = Indicadores de sombra .single = รšnico .size = Tamanho + .sketchy = Esboรงo .snap-override-label-next-pick-only = Substituiรงรฃo de snap: __ocs_fmt_0__ (somente a prรณxima escolha). .solid-created = Sรณlido criado. .source-object-not-found = Objeto de origem nรฃo encontrado. @@ -1067,7 +1148,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: digite Left, Top ou Right. .isodraft-expected-on-or-off = ISODRAFT: digite On ou Off. .xdata-selected-entities-are-on-locked-layers = XDATA: as entidades selecionadas estรฃo em camadas bloqueadas. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: nenhuma imagem raster selecionada ou propriedade desconhecida (use BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: digite um inteiro de -4 a 4. .usage-datalink-path-to-csv = Uso: DATALINK .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: nenhuma tabela vinculada encontrada. @@ -2060,7 +2140,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = Arquivo .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2369,6 +2449,7 @@ common = .load-linetypes = Carregar tipos de linha .requires-two-positive-values = Requer dois valores positivos (x,y) ou um para ambos. .snap-marker-color = Cor do marcador de snap + .snap-marker-size = Tamanho do marcador de snap .command-line-text-color = Cor do texto da linha de comando .operand = Operando .linetypes-loaded = __ocs_fmt_0__ tipo(s) de linha carregado(s). @@ -2382,6 +2463,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Especifique o ponto base [__ocs_arg_count__ objetos]: .threedrotate-specify-rotation-axis = 3DROTATE Especifique o eixo de rotaรงรฃo [X/Y/Z] ou um segundo ponto nele : .threedrotate-specify-rotation-angle-a = 3DROTATE Especifique o รขngulo de rotaรงรฃo <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Aviso de salvamento .unable-to-save-drawing = Nรฃo foi possรญvel salvar o desenho @@ -3005,6 +3093,7 @@ draw = .arc-specify-radius-of-arc = ARC Especifique o raio do arco: .chord-length = Comprimento da corda + modify = .add-vertex = Adicionar vรฉrtice .closed-cross = Cruz fechada @@ -3050,6 +3139,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT Selecione um bloco com atributos: .annotation-object-scale = Escala de objeto de anotaรงรฃo .bedit-select-block-reference-to-edit = BEDIT Selecione a referรชncia de bloco a editar: + .bvstate-option = BVSTATE Digite uma opรงรฃo [Novo/Definir/Renomear/Excluir] : + .bvstate-new-name = BVSTATE Digite o nome do novo estado de visibilidade: + .bvstate-set = BVSTATE Digite o estado de visibilidade a tornar atual <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Digite o estado de visibilidade a renomear <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Digite o novo nome: + .bvstate-delete = BVSTATE Digite o estado de visibilidade a excluir: + .bvstate-not-found = Estado de visibilidade "__ocs_fmt_0__" nรฃo encontrado. + .bvstate-exists = O estado de visibilidade "__ocs_fmt_0__" jรก existe. + .bvstate-last = O รบnico estado de visibilidade nรฃo pode ser excluรญdo. + .bvstate-needs-bedit = __ocs_fmt_0__: abra primeiro o bloco no Editor de blocos (BEDIT). + .bvstate-current = Estado de visibilidade atual: __ocs_fmt_0__ + .bvhide-select = Selecione os objetos a ocultar: + .bvshow-select = Selecione os objetos a tornar visรญveis: + .arrayedit-select-array = ARRAYEDIT Selecione o arranjo: + .arrayedit-options = ARRAYEDIT Digite uma opรงรฃo [Origem/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: o objeto selecionado nรฃo รฉ um arranjo associativo. .blend-enter-continuity-tangent-curvature-c = BLEND Insira a continuidade [Tangente/Curvatura] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND Selecione a primeira curva aberta [Continuidade=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND Selecione a segunda curva aberta [Continuidade=__ocs_arg_c__]: @@ -3602,6 +3707,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Especifique o ponto de inserรงรฃo: insert = + .pastespec-specify-insertion-point = PASTESPEC Especifique o ponto de inserรงรฃo: + .pastespec-paste-as = PASTESPEC Colar como [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3718,6 +3825,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3772,6 +3886,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *Os snaps de subjacentes variam* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3912,6 +4027,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = Selecione uma imagem ou referรชncia subjacente: + .adjust-select-pdf-underlay = Selecione uma referรชncia subjacente PDF: + .adjust-select-dwf-underlay = Selecione uma referรชncia subjacente DWF: + .adjust-select-dgn-underlay = Selecione uma referรชncia subjacente DGN: + .adjust-mixed-underlays = *Conjunto de seleรงรฃo invรกlido. Selecione apenas referรชncias subjacentes do mesmo tipo de arquivo.* + .adjust-image-option = Insira a opรงรฃo de imagem [Contraste/Esmaecimento/Brilho] : + .adjust-underlay-option = Insira a opรงรฃo de referรชncia subjacente __ocs_arg_kind__ [Esmaecimento/Contraste/Monocromรกtico] <__ocs_arg_option__>: + .adjust-monochrome = Monocromรกtico? [Sim/Nรฃo] <__ocs_arg_value__>: + .adjust-brightness-value = Insira o valor de brilho (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = Insira o valor de contraste (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Insira o valor de esmaecimento (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = O valor deve ser um nรบmero inteiro entre 0 e 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4415,7 +4542,27 @@ model = .value-2point = 2 pontos .cube = Cubo + layout = + .mvsetup-select-objects-to-delete = MVSETUP Selecione os objetos a excluir: + .delete-objects-option = Excluir objetos + .mvsetup-enable-paper-space = MVSETUP Ativar espaรงo do papel? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP Informe o fator de escala <1>: + .mvsetup-enter-paper-width = MVSETUP Informe a largura do papel: + .mvsetup-enter-paper-height = MVSETUP Informe a altura do papel: + .mvsetup-enter-option = MVSETUP Informe uma opรงรฃo [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Informe o arranjo das janelas [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Especifique o primeiro canto da รกrea das janelas : + .mvsetup-specify-opposite-corner = MVSETUP Especifique o canto oposto: + .mvsetup-viewports-in-x = MVSETUP Informe o nรบmero de janelas na direรงรฃo X <2>: + .mvsetup-viewports-in-y = MVSETUP Informe o nรบmero de janelas na direรงรฃo Y <2>: + .mvsetup-distance-in-x = MVSETUP Especifique a distรขncia entre janelas na direรงรฃo X <0>: + .mvsetup-distance-in-y = MVSETUP Especifique a distรขncia entre janelas na direรงรฃo Y <0>: + .mvsetup-select-viewports = MVSETUP Selecione as janelas a escalar: + .mvsetup-paper-space-units = MVSETUP Informe o nรบmero de unidades do espaรงo do papel <1>: + .mvsetup-model-space-units = MVSETUP Informe o nรบmero de unidades do espaรงo do modelo <1>: + .mvsetup-viewports-do-not-fit = MVSETUP As janelas nรฃo cabem na รกrea. + .scale-viewports-option = Escalar janelas .active = Ativo .inactive = Inativo .insert-view = Inserir vista @@ -4441,6 +4588,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Congelar | T = Descongelar | Enter = Sair view = + .twist-option = Girar + .dview-specify-view-twist-angle = DVIEW Especifique o รขngulo de giro da vista <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Informe uma opรงรฃo [TWist/Undo]: .color-scheme-set-to-system-currently-name = Esquema de cores definido como "Sistema" (atualmente __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = Requer um nรบmero inteiro entre __ocs_fmt_0__ e __ocs_fmt_1__. @@ -4635,13 +4785,6 @@ view = .x-no-xdata = __ocs_fmt_0__: sem dados estendidos. .value-3d-orbit = ร“rbita 3D .value-3d-orbit-drag-with-right-mouse-button = ร“rbita 3D: arraste com o botรฃo direito do mouse. - .adjust-brightness-contrast-fade = ADJUST [Brilho / Contraste / Esmaecimento]: - .adjust-brightness-0-100 = ADJUST brilho 0-100: - .adjust-contrast-0-100 = ADJUST contraste 0-100: - .adjust-fade-0-100 = ADJUST esmaecimento 0-100: - .adjust-select-raster-image-s-first = ADJUST: selecione primeiro as imagens raster. - .adjust-value-must-be-0-100 = ADJUST: o valor deve estar entre 0 e 100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ em __ocs_fmt_2__ imagens. .annoallvisible-new-value-0-1 = ANNOALLVISIBLE novo valor [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: insira 0 ou 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE novo valor [-4..4]: @@ -4767,7 +4910,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Subjacรชncia __ocs_fmt_0__: esmaecimento=__ocs_fmt_1__, contraste=__ocs_fmt_2__, ligada=__ocs_fmt_3__, delimitada=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = __ocs_fmt_0__ subjacรชncias atualizadas. - .usage-adjust-brightness-contrast-fade-0-100 = Uso: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Uso: ANNOSCALE por exemplo, 1:50, 2:1 ou um fator .usage-draworder-f-front-b-back-a-above-handle = Uso: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Uso: HYPERLINK (selecione primeiro os objetos) @@ -5995,6 +6137,46 @@ ui = .osnap-settings = Configuraรงรตes de snap... .snap-override-none-next-pick-only = Substituiรงรฃo de snap: Nenhum (somente o prรณximo ponto). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Sempre manter o texto entre as linhas de extensรฃo .angular-dimensions = Cotas angulares @@ -6262,3 +6444,117 @@ graphic-attributes = .reset-angle = Redefinir รขngulo .one-color = Uma cor .two-colors = Duas cores + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = Adicionar ao conjunto de trabalho + .remove-from-working-set = Remover do conjunto de trabalho + .toolbar-objects = objetos + .dialog-title = Editar referรชncia + .dialog-reference = Referรชncia a editar + .dialog-chain-hint = A cadeia no ponto escolhido; clique em um rรณtulo para mudar o nรญvel. + .dialog-path = Caminho: + .dialog-working-set = Conjunto de trabalho + .dialog-all-objects = Todos os objetos + .dialog-pick-objects = Selecionar no desenho + .dialog-attributes = Editar tambรฉm as definiรงรตes de atributo + .dialog-session = Sessรฃo + .dialog-lock = Bloquear objetos fora do conjunto de trabalho + .dialog-unique = Dar nomes รบnicos a camadas, estilos e blocos em conflito + .dialog-start = Iniciar ediรงรฃo + .dialog-help = Abre uma referรชncia de bloco ou referรชncia externa para ediรงรฃo no local. + .select-reference = Selecione a referรชncia: + .reference-not-found = Referรชncia nรฃo encontrada. + .not-valid-reference = O objeto nรฃo รฉ uma referรชncia vรกlida + .one-entity-added = 1 entidade adicionada + .nesting-level = Selecione o nรญvel de aninhamento [Ok/Prรณximo] : + .selection-method = Digite o mรฉtodo de seleรงรฃo de objetos [Todos/Aninhados] : + .select-nested = Selecione os objetos aninhados: + .display-attdefs = Exibir definiรงรตes de atributo [Sim/Nรฃo] : + .zero-items-selected = 0 itens selecionados + .one-item-selected = 1 item selecionado + .items-selected = __ocs_fmt_0__ itens selecionados + .refset-option = Digite uma opรงรฃo [Adicionar/Remover] : + .refclose-option = Digite uma opรงรฃo [Salvar/Descartar alteraรงรตes da referรชncia] : + .not-checked-out = ** Comando nรฃo permitido, a menos que uma referรชncia esteja em ediรงรฃo com REFEDIT ** + .already-checked-out = ** Comando nรฃo permitido, __ocs_fmt_0__ jรก estรก em ediรงรฃo ** + .transfer-objects = Transferir objetos entre o conjunto de trabalho do RefEdit e o desenho hospedeiro... + .added-to-working-set = __ocs_fmt_0__ adicionados ao conjunto de trabalho + .removed-from-working-set = __ocs_fmt_0__ removidos do conjunto de trabalho + .one-object-added = 1 objeto adicionado a __ocs_fmt_0__ + .objects-added = __ocs_fmt_0__ objetos adicionados a __ocs_fmt_1__ + .one-object-removed = 1 objeto removido de __ocs_fmt_0__ + .objects-removed = __ocs_fmt_0__ objetos removidos de __ocs_fmt_1__ + .one-xref-instance = 1 instรขncia de xref atualizada + .xref-instances = __ocs_fmt_0__ instรขncias de xref atualizadas + .one-block-instance = 1 instรขncia de bloco atualizada + .block-instances = __ocs_fmt_0__ instรขncias de bloco atualizadas + .redefined-reloaded = __ocs_fmt_0__ redefinido e recarregado. + .redefined = __ocs_fmt_0__ redefinido. + .ok = Ok + .nested = Aninhados + +image-adjust = + .title = Ajuste de imagem + .preview = Visualizaรงรฃo + .before = Antes + .after = Depois + .no-preview = nรฃo foi possรญvel carregar a imagem + .adjust = Ajuste + .brightness = Brilho + .contrast = Contraste + .fade = Esmaecimento + .hint = Clique ou arraste uma barra; โˆ’ / + รฉ um passo, uma caixa aceita um inteiro de 0 a 100. + .reset = Restaurar padrรตes + .apply = Aplicar + .help = Define o brilho, o contraste e o esmaecimento das imagens raster selecionadas. + +keytips = + .title = KeyTip + .typed = Digitado: { $path } + .hint-tabs = Digite a letra de uma guia ยท Esc fecha + .hint-tab = Guia { $tab } ยท Esc volta um nรญvel + .hint-items = Digite a letra de um item ยท Esc volta um nรญvel diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index e00da7b3c..d60caaeee 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = ะœะพะดัƒะปะธ ribbon-tab = + .block-editor = ะ ะตะดะฐะบั‚ะพั€ ะฑะปะพะบะพะฒ .draw = ะะฐั‡ะตั€ั‚ะธั‚ัŒ .parametric = ะŸะฐั€ะฐะผะตั‚ั€ะธั‡ะตัะบะธะน .annotate = ะะฝะฝะพั‚ะฐั†ะธะธ @@ -98,6 +99,9 @@ start = .no-recent-files = ะžั‚ะบั€ั‹ั‚ั‹ะต ะฒะฐะผะธ ั„ะฐะนะปั‹ ะฟะพัะฒัั‚ัั ะทะดะตััŒ. .browser-storage = ะฅั€ะฐะฝะธะปะธั‰ะต ะฑั€ะฐัƒะทะตั€ะฐ .keep-recent-files = ะฅั€ะฐะฝะธั‚ัŒ ะฝะตะดะฐะฒะฝะธะต ั„ะฐะนะปั‹ + .view-list = ะกะฟะธัะพะบ + .view-icons = ะ—ะฝะฐั‡ะบะธ + .view-preview = ะšั€ัƒะฟะฝั‹ะต ััะบะธะทั‹ donation = .title = ะŸั€ะพััŒะฑะฐ ะพ ะฟะพะถะตั€ั‚ะฒะพะฒะฐะฝะธะธ @@ -167,6 +171,14 @@ modal = .that-object = ัั‚ะพั‚ ะพะฑัŠะตะบั‚ .those-objects = ัั‚ะธ ะพะฑัŠะตะบั‚ั‹ + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = ะ’ัั‚ะฐะฒะปะตะฝะพ ะธะทะพะฑั€ะฐะถะตะฝะธะต "{ $name }": { $width }ร—{ $height } px .image-unsupported = ะะต ัƒะดะฐะปะพััŒ ะดะตะบะพะดะธั€ะพะฒะฐั‚ัŒ ะฒัั‚ะฐะฒะปะตะฝะฝะพะต ะธะทะพะฑั€ะฐะถะตะฝะธะต "{ $name }". @@ -277,11 +289,73 @@ command-copy = .mode = COPY ะ—ะฐะดะฐะนั‚ะต ั€ะตะถะธะผ ะบะพะฟะธั€ะพะฒะฐะฝะธั [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ ั‚ะพะปัŒะบะพ ะทะดะตััŒ (ะทะตะปั‘ะฝั‹ะต), __ocs_fmt_1__ ั‚ะพะปัŒะบะพ ะฒ ะดั€ัƒะณะพะผ ั‡ะตั€ั‚ะตะถะต (ะบั€ะฐัะฝั‹ะต)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ะกั€ะฐะฒะฝะธั‚ัŒ ั ั‡ะตั€ั‚ะตะถะพะผ + .label-associative-array-created = __ocs_fmt_0__: ัะพะทะดะฐะฝ ะฐััะพั†ะธะฐั‚ะธะฒะฝั‹ะน ะผะฐััะธะฒ. + .assoc-array-array-path = ะœะฐััะธะฒ (ะฟะพ ั‚ั€ะฐะตะบั‚ะพั€ะธะธ) + .assoc-array-array-polar = ะœะฐััะธะฒ (ะบั€ัƒะณะพะฒะพะน) + .assoc-array-array-rectangular = ะœะฐััะธะฒ (ะฟั€ัะผะพัƒะณะพะปัŒะฝั‹ะน) + .assoc-array-item-spacing = ะ ะฐััั‚ะพัะฝะธะต ะผะตะถะดัƒ ัะปะตะผะตะฝั‚ะฐะผะธ + .assoc-array-fill-angle = ะฃะณะพะป ะทะฐะฟะพะปะฝะตะฝะธั + .assoc-array-items = ะญะปะตะผะตะฝั‚ั‹ + .assoc-array-level-spacing = ะ ะฐััั‚ะพัะฝะธะต ะผะตะถะดัƒ ัƒั€ะพะฒะฝัะผะธ + .assoc-array-levels = ะฃั€ะพะฒะฝะธ + .assoc-array-row-spacing = ะ ะฐััั‚ะพัะฝะธะต ะผะตะถะดัƒ ั€ัะดะฐะผะธ + .assoc-array-column-spacing = ะ ะฐััั‚ะพัะฝะธะต ะผะตะถะดัƒ ัั‚ะพะปะฑั†ะฐะผะธ + .fbxexport-exported-to = FBXEXPORT: ัะบัะฟะพั€ั‚ะธั€ัƒะตั‚ัั ะฒ "__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: ะฒ ัั‚ะพะผ ั‡ะตั€ั‚ะตะถะต ะฝะตั‚ ะดะฐะฝะฝั‹ั… 3D-ัะตั‚ะธ. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = ะะพะฒั‹ะต ัˆั‚ั€ะธั…ะพะฒะบะธ ัะปะตะดัƒัŽั‚ ะทะฐ ะบะพะฝั‚ัƒั€ะฐะผะธ (HPASSOC) + .options-show-ribbon = ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ะปะตะฝั‚ัƒ (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ัั‚ั€ะพะบัƒ ะผะตะฝัŽ (MENUBAR) + .options-window-elements = ะญะปะตะผะตะฝั‚ั‹ ะพะบะฝะฐ + .menu-viewports = ะ’ะธะดะพะฒั‹ะต ัะบั€ะฐะฝั‹ + .menu-view-twist = ะŸะพะฒะพั€ะพั‚ ะฒะธะดะฐ + .menu-tools = ะกะตั€ะฒะธั + .menu-table-style = ะกั‚ะธะปัŒ ั‚ะฐะฑะปะธั† + .menu-run-script = ะ—ะฐะฟัƒัั‚ะธั‚ัŒ ัั†ะตะฝะฐั€ะธะน + .menu-revision-cloud = ะžะฑะปะฐะบะพ + .menu-regen-all = ะ ะตะณะตะฝะตั€ะธั€ะพะฒะฐั‚ัŒ ะฒัะต + .menu-redraw = ะžัะฒะตะถะธั‚ัŒ + .menu-rectangle = ะŸั€ัะผะพัƒะณะพะปัŒะฝะธะบ + .menu-quick-dimension = ะ‘ั‹ัั‚ั€ั‹ะน ั€ะฐะทะผะตั€ + .menu-publish = ะŸัƒะฑะปะธะบะฐั†ะธั + .menu-paste-special = ะกะฟะตั†ะธะฐะปัŒะฝะฐั ะฒัั‚ะฐะฒะบะฐ + .menu-parametric = ะŸะฐั€ะฐะผะตั‚ั€ะธะทะฐั†ะธั + .menu-page-setup-manager = ะ”ะธัะฟะตั‚ั‡ะตั€ ะฟะฐั€ะฐะผะตั‚ั€ะพะฒ ะปะธัั‚ะพะฒ + .menu-orbit = ะžั€ะฑะธั‚ะฐ + .menu-new-ucs = ะะพะฒะฐั ะŸะกะš + .menu-named-views = ะ˜ะผะตะฝะพะฒะฐะฝะฝั‹ะต ะฒะธะดั‹ + .menu-multiline-text = ะœะฝะพะณะพัั‚ั€ะพั‡ะฝั‹ะน ั‚ะตะบัั‚ + .menu-multiline-style = ะกั‚ะธะปัŒ ะผัƒะปัŒั‚ะธะปะธะฝะธะธ + .menu-multiline = ะœัƒะปัŒั‚ะธะปะธะฝะธั + .menu-multileader-style = ะกั‚ะธะปัŒ ะผัƒะปัŒั‚ะธะฒั‹ะฝะพัะบะธ + .menu-match-properties = ะšะพะฟะธั€ะพะฒะฐั‚ัŒ ัะฒะพะนัั‚ะฒะฐ + .menu-lengthen = ะฃะฒะตะปะธั‡ะธั‚ัŒ + .menu-jogged = ะก ะธะทะปะพะผะพะผ + .menu-id-point = ะšะพะพั€ะดะธะฝะฐั‚ั‹ ั‚ะพั‡ะบะธ + .menu-explode-text = ะ ะฐัั‡ะปะตะฝะธั‚ัŒ ั‚ะตะบัั‚ + .menu-edit-spline = ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ัะฟะปะฐะนะฝ + .menu-edit-polyline = ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะฟะพะปะธะปะธะฝะธัŽ + .menu-edit-hatch = ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ัˆั‚ั€ะธั…ะพะฒะบัƒ + .menu-drawing-limits = ะ›ะธะผะธั‚ั‹ ั‡ะตั€ั‚ะตะถะฐ + .menu-donut = ะšะพะปัŒั†ะพ + .menu-dimension-style = ะ ะฐะทะผะตั€ะฝั‹ะน ัั‚ะธะปัŒ + .menu-construction-line = ะ’ัะฟะพะผะพะณะฐั‚ะตะปัŒะฝะฐั ะฟั€ัะผะฐั + .menu-arc-length = ะ”ะปะธะฝะฐ ะดัƒะณะธ + .command-line-opacity = ะะตะฟั€ะพะทั€ะฐั‡ะฝะพัั‚ัŒ ะบะพะผะฐะฝะดะฝะพะน ัั‚ั€ะพะบะธ + .txtexp-no-text-selected = TXTEXP: ั‚ะตะบัั‚ ะฝะต ะฒั‹ะฑั€ะฐะฝ. + .angle-override-shown = ะŸะตั€ะตะพะฟั€ะตะดะตะปะตะฝะธะต ัƒะณะปะฐ: __ocs_fmt_0__ .plot-preview-task = ะŸั€ะตะดะฒะฐั€ะธั‚ะตะปัŒะฝั‹ะน ะฟั€ะพัะผะพั‚ั€ ะฟะตั‡ะฐั‚ะธ .link-a-csv-data-source = ะกะฒัะทะฐั‚ัŒ ะธัั‚ะพั‡ะฝะธะบ ะดะฐะฝะฝั‹ั… CSV .csv-files = ะคะฐะนะปั‹ CSV .browser-cannot-open-typed-path = ะ‘ั€ะฐัƒะทะตั€ ะฝะต ะผะพะถะตั‚ ะพั‚ะบั€ั‹ั‚ัŒ ะฒะฒะตะดั‘ะฝะฝั‹ะน ะฟัƒั‚ัŒ; ะฒั‹ะฑะตั€ะธั‚ะต ั„ะฐะนะป. .recover-drawing = ะ’ะพััั‚ะฐะฝะพะฒะปะตะฝะธะต ั‡ะตั€ั‚ะตะถะฐ + .requires-0-or-1-or-keyword = ะ”ะพะฟัƒัะบะฐะตั‚ัั ั‚ะพะปัŒะบะพ 0 ะธะปะธ 1 ะปะธะฑะพ ะบะปัŽั‡ะตะฒะพะต ัะปะพะฒะพ ะฟะฐั€ะฐะผะตั‚ั€ะฐ. + .requires-a-number = ะขั€ะตะฑัƒะตั‚ัั ั‡ะธัะปะพ. .restore-it-as-a-recovered-tab-or-discard-the-copy = ะ’ะพััั‚ะฐะฝะพะฒะธั‚ะต ะตะณะพ ะฝะฐ ะพั‚ะดะตะปัŒะฝะพะน ะฒะบะปะฐะดะบะต ะธะปะธ ัƒะดะฐะปะธั‚ะต ะบะพะฟะธัŽ. .name-has-an-autosave-copy-newer-than-the-last-save = ะฃ ยซ__ocs_fmt_0__ยป ะตัั‚ัŒ ะบะพะฟะธั ะฐะฒั‚ะพัะพั…ั€ะฐะฝะตะฝะธั ะฝะพะฒะตะต ะฟะพัะปะตะดะฝะตะณะพ ัะพั…ั€ะฐะฝะตะฝะธั. .autosave-copy-could-not-be-stored = ะะต ัƒะดะฐะปะพััŒ ัะพั…ั€ะฐะฝะธั‚ัŒ ะบะพะฟะธัŽ ะฐะฒั‚ะพัะพั…ั€ะฐะฝะตะฝะธั: __ocs_fmt_0__ @@ -825,6 +899,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: ะฒั‹ะฑะตั€ะธั‚ะต ะพะฑัŠะตะบั‚ ัะตั‡ะตะฝะธั ะธะปะธ ะฒะบะปัŽั‡ะธั‚ะต ะดะธะฝะฐะผะธั‡ะตัะบะพะต ัะตั‡ะตะฝะธะต. + .sectioncull-prepare-failed = SECTIONCULL: ะฝะต ัƒะดะฐะปะพััŒ ะฟะพะดะณะพั‚ะพะฒะธั‚ัŒ ะพะฑั€ะตะทะฐะฝะฝั‹ะน ะพะฑัŠะตะบั‚; ะฝะธั‡ะตะณะพ ะฝะต ะธะทะผะตะฝะตะฝะพ. + .sectioncull-nothing-outside = SECTIONCULL: ะทะฐ ะฟั€ะตะดะตะปะฐะผะธ ัะตั‡ะตะฝะธั ะฝะธั‡ะตะณะพ ะฝะตั‚. + .sectioncull-result = SECTIONCULL: ัƒะดะฐะปะตะฝะพ ะพะฑัŠะตะบั‚ะพะฒ: __ocs_arg_erased__, ะพะฑั€ะตะทะฐะฝะพ ะฟะพ ะณั€ะฐะฝะธั†ะต ัะตั‡ะตะฝะธั: __ocs_arg_cut__. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: ะฟั€ะพะฒะตะดะธั‚ะต ัะฟะปะธะฝ ั‡ะตั€ะตะท ั‚ะพั‡ะบะธ __ocs_fmt_0__. .splinefit-need-at-least-3-points = SPLINEFIT: ะฝะตะพะฑั…ะพะดะธะผะพ ะฝะต ะผะตะฝะตะต 3 ะฑะฐะปะปะพะฒ. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: ะฒั‹ะฑะตั€ะธั‚ะต ะฟะพะปะธะปะธะฝ, ั‡ั‚ะพะฑั‹ ัะพะพั‚ะฒะตั‚ัั‚ะฒะพะฒะฐั‚ัŒ ัะฟะปะธะฝัƒ. @@ -862,9 +940,12 @@ common = .sent-to-printer-printer = ะžั‚ะฟั€ะฐะฒะปะตะฝะพ ะฝะฐ ะฟั€ะธะฝั‚ะตั€: __ocs_fmt_0__ .set-current = ะกะดะตะปะฐั‚ัŒ ั‚ะตะบัƒั‰ะธะผ .shade-plot = ะกะบั€ะธะฝัˆะพั‚ั‹ ะธะณั€ั‹ Shade + .shaded-with-edges = ะขะพะฝะธั€ะพะฒะฐะฝะฝั‹ะน ั ะบั€ะพะผะบะฐะผะธ + .shades-of-gray = ะžั‚ั‚ะตะฝะบะธ ัะตั€ะพะณะพ .shadow-flags = ะคะปะฐะณะธ ั‚ะตะฝะตะน .single = ะžะดะธะฝะฐั€ะฝั‹ะน .size = ะ ะฐะทะผะตั€ + .sketchy = ะญัะบะธะทะฝั‹ะน .snap-override-label-next-pick-only = ะŸะตั€ะตะพะฟั€ะตะดะตะปะตะฝะธะต ะฟะพ ั‰ะตะปั‡ะบัƒ: __ocs_fmt_0__ (ั‚ะพะปัŒะบะพ ัะปะตะดัƒัŽั‰ะธะน ะฒั‹ะฑะพั€). .solid-created = ะขะฒะตั€ะดะพะต ัะพะทะดะฐะฝะธะต. .source-object-not-found = ะ˜ัั‚ะพั‡ะฝะธะบ ะพะฑัŠะตะบั‚ะฐ ะฝะต ะฝะฐะนะดะตะฝ. @@ -1070,7 +1151,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: ะฒะฒะตะดะธั‚ะต Left, Top ะธะปะธ Right. .isodraft-expected-on-or-off = ISODRAFT: ะฒะฒะตะดะธั‚ะต On ะธะปะธ Off. .xdata-selected-entities-are-on-locked-layers = XDATA: ะฒั‹ะฑั€ะฐะฝะฝั‹ะต ะพะฑัŠะตะบั‚ั‹ ะฝะฐั…ะพะดัั‚ัั ะฝะฐ ะทะฐะฑะปะพะบะธั€ะพะฒะฐะฝะฝั‹ั… ัะปะพัั…. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: ั€ะฐัั‚ั€ะพะฒั‹ะต ะธะทะพะฑั€ะฐะถะตะฝะธั ะฝะต ะฒั‹ะฑั€ะฐะฝั‹ ะธะปะธ ัะฒะพะนัั‚ะฒะพ ะฝะตะธะทะฒะตัั‚ะฝะพ (ะธัะฟะพะปัŒะทัƒะนั‚ะต BRIGHTNESS/CONTRAST/FADE). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: ะฒะฒะตะดะธั‚ะต ั†ะตะปะพะต ั‡ะธัะปะพ ะพั‚ -4 ะดะพ 4. .usage-datalink-path-to-csv = ะ˜ัะฟะพะปัŒะทะพะฒะฐะฝะธะต: DATALINK <ะฟัƒั‚ัŒ-ะบ-.csv> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: ัะฒัะทะฐะฝะฝั‹ะต ั‚ะฐะฑะปะธั†ั‹ ะฝะต ะฝะฐะนะดะตะฝั‹. @@ -2063,7 +2143,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = ะคะฐะนะป .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2372,6 +2452,7 @@ common = .load-linetypes = ะ—ะฐะณั€ัƒะทะธั‚ัŒ ั‚ะธะฟั‹ ะปะธะฝะธะน .requires-two-positive-values = ะขั€ะตะฑัƒัŽั‚ัั ะดะฒะฐ ะฟะพะปะพะถะธั‚ะตะปัŒะฝั‹ั… ะทะฝะฐั‡ะตะฝะธั (x,y) ะธะปะธ ะพะดะฝะพ ะดะปั ะพะฑะพะธั…. .snap-marker-color = ะฆะฒะตั‚ ะผะฐั€ะบะตั€ะฐ ะฟั€ะธะฒัะทะบะธ + .snap-marker-size = ะ ะฐะทะผะตั€ ะผะฐั€ะบะตั€ะฐ ะฟั€ะธะฒัะทะบะธ .command-line-text-color = ะฆะฒะตั‚ ั‚ะตะบัั‚ะฐ ะบะพะผะฐะฝะดะฝะพะน ัั‚ั€ะพะบะธ .operand = ะžะฟะตั€ะฐะฝะด .linetypes-loaded = ะ—ะฐะณั€ัƒะถะตะฝะพ ั‚ะธะฟะพะฒ ะปะธะฝะธะน: __ocs_fmt_0__. @@ -2385,6 +2466,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE ะฃะบะฐะถะธั‚ะต ะฑะฐะทะพะฒัƒัŽ ั‚ะพั‡ะบัƒ [__ocs_arg_count__ ะพะฑัŠะตะบั‚ั‹]: .threedrotate-specify-rotation-axis = 3DROTATE ะฃะบะฐะถะธั‚ะต ะพััŒ ะฟะพะฒะพั€ะพั‚ะฐ [X/Y/Z] ะธะปะธ ะฒั‚ะพั€ัƒัŽ ั‚ะพั‡ะบัƒ ะฝะฐ ะฝะตะน : .threedrotate-specify-rotation-angle-a = 3DROTATE ะฃะบะฐะถะธั‚ะต ัƒะณะพะป ะฟะพะฒะพั€ะพั‚ะฐ <__ocs_arg_a__> + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = ะกะฟะฐัั‚ะธ ะฟั€ะตะดะพัั‚ะตั€ะตะถะตะฝะธะต .unable-to-save-drawing = ะะต ัƒะดะฐะปะพััŒ ัะพั…ั€ะฐะฝะธั‚ัŒ ั‡ะตั€ั‚ั‘ะถ @@ -3008,6 +3096,7 @@ draw = .arc-specify-radius-of-arc = ARC ะฃะบะฐะถะธั‚ะต ั€ะฐะดะธัƒั ะดัƒะณะธ: .chord-length = ะ”ะปะธะฝะฐ ั…ะพั€ะดั‹ + modify = .add-vertex = ะ”ะพะฑะฐะฒะธั‚ัŒ ะฒะตั€ัˆะธะฝัƒ .closed-cross = ะ—ะฐะบั€ั‹ั‚ะพะต ะฟะตั€ะตัะตั‡ะตะฝะธะต @@ -3053,6 +3142,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT ะ’ั‹ะฑะตั€ะธั‚ะต ะฑะปะพะบ ั ะฐั‚ั€ะธะฑัƒั‚ะฐะผะธ: .annotation-object-scale = ะœะฐััˆั‚ะฐะฑ ะฐะฝะฝะพั‚ะฐั‚ะธะฒะฝะพะณะพ ะพะฑัŠะตะบั‚ะฐ .bedit-select-block-reference-to-edit = BEDIT ะ’ั‹ะฑะตั€ะธั‚ะต ััั‹ะปะบัƒ ะฝะฐ ะฑะปะพะบ ะดะปั ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธั: + .bvstate-option = BVSTATE ะ’ะฒะตะดะธั‚ะต ะฟะฐั€ะฐะผะตั‚ั€ [ะะพะฒะพะต/ะฃัั‚ะฐะฝะพะฒะธั‚ัŒ/ะŸะตั€ะตะธะผะตะฝะพะฒะฐั‚ัŒ/ะฃะดะฐะปะธั‚ัŒ] <ะฃัั‚ะฐะฝะพะฒะธั‚ัŒ>: + .bvstate-new-name = BVSTATE ะ’ะฒะตะดะธั‚ะต ะธะผั ะฝะพะฒะพะณะพ ัะพัั‚ะพัะฝะธั ะฒะธะดะธะผะพัั‚ะธ: + .bvstate-set = BVSTATE ะ’ะฒะตะดะธั‚ะต ัะพัั‚ะพัะฝะธะต ะฒะธะดะธะผะพัั‚ะธ, ะบะพั‚ะพั€ะพะต ัั‚ะฐะฝะตั‚ ั‚ะตะบัƒั‰ะธะผ <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE ะ’ะฒะตะดะธั‚ะต ัะพัั‚ะพัะฝะธะต ะฒะธะดะธะผะพัั‚ะธ ะดะปั ะฟะตั€ะตะธะผะตะฝะพะฒะฐะฝะธั <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE ะ’ะฒะตะดะธั‚ะต ะฝะพะฒะพะต ะธะผั: + .bvstate-delete = BVSTATE ะ’ะฒะตะดะธั‚ะต ัะพัั‚ะพัะฝะธะต ะฒะธะดะธะผะพัั‚ะธ ะดะปั ัƒะดะฐะปะตะฝะธั: + .bvstate-not-found = ะกะพัั‚ะพัะฝะธะต ะฒะธะดะธะผะพัั‚ะธ "__ocs_fmt_0__" ะฝะต ะฝะฐะนะดะตะฝะพ. + .bvstate-exists = ะกะพัั‚ะพัะฝะธะต ะฒะธะดะธะผะพัั‚ะธ "__ocs_fmt_0__" ัƒะถะต ััƒั‰ะตัั‚ะฒัƒะตั‚. + .bvstate-last = ะ•ะดะธะฝัั‚ะฒะตะฝะฝะพะต ัะพัั‚ะพัะฝะธะต ะฒะธะดะธะผะพัั‚ะธ ะฝะตะปัŒะทั ัƒะดะฐะปะธั‚ัŒ. + .bvstate-needs-bedit = __ocs_fmt_0__: ัะฝะฐั‡ะฐะปะฐ ะพั‚ะบั€ะพะนั‚ะต ะฑะปะพะบ ะฒ ั€ะตะดะฐะบั‚ะพั€ะต ะฑะปะพะบะพะฒ (BEDIT). + .bvstate-current = ะขะตะบัƒั‰ะตะต ัะพัั‚ะพัะฝะธะต ะฒะธะดะธะผะพัั‚ะธ: __ocs_fmt_0__ + .bvhide-select = ะ’ั‹ะฑะตั€ะธั‚ะต ะพะฑัŠะตะบั‚ั‹, ะบะพั‚ะพั€ั‹ะต ะฝัƒะถะฝะพ ัะบั€ั‹ั‚ัŒ: + .bvshow-select = ะ’ั‹ะฑะตั€ะธั‚ะต ะพะฑัŠะตะบั‚ั‹, ะบะพั‚ะพั€ั‹ะต ะฝัƒะถะฝะพ ะฟะพะบะฐะทะฐั‚ัŒ: + .arrayedit-select-array = ARRAYEDIT ะ’ั‹ะฑะตั€ะธั‚ะต ะผะฐััะธะฒ: + .arrayedit-options = ARRAYEDIT ะ’ะฒะตะดะธั‚ะต ะฟะฐั€ะฐะผะตั‚ั€ [ะ˜ัั…ะพะดะฝั‹ะต/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: ะฒั‹ะฑั€ะฐะฝะฝั‹ะน ะพะฑัŠะตะบั‚ ะฝะต ัะฒะปัะตั‚ัั ะฐััะพั†ะธะฐั‚ะธะฒะฝั‹ะผ ะผะฐััะธะฒะพะผ. .blend-enter-continuity-tangent-curvature-c = BLEND ะ’ะฒะตะดะธั‚ะต ะฝะตะฟั€ะตั€ั‹ะฒะฝะพัั‚ัŒ [ะขะฐะฝะณะตะฝั‚/ะšัƒั€ะฒะฐั‚ัƒั€ะฐ] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND ะ’ั‹ะฑะตั€ะธั‚ะต ะฟะตั€ะฒัƒัŽ ะพั‚ะบั€ั‹ั‚ัƒัŽ ะบั€ะธะฒัƒัŽ [ะŸั€ะพะดะพะปะถะตะฝะธะต=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND ะ’ั‹ะฑะตั€ะธั‚ะต ะฒั‚ะพั€ัƒัŽ ะพั‚ะบั€ั‹ั‚ัƒัŽ ะบั€ะธะฒัƒัŽ [ะŸั€ะพะดะพะปะถะตะฝะธะต=__ocs_arg_c__]: @@ -3603,6 +3708,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE ะฃะบะฐะถะธั‚ะต ั‚ะพั‡ะบัƒ ะฒัั‚ะฐะฒะบะธ: insert = + .pastespec-specify-insertion-point = PASTESPEC ะฃะบะฐะถะธั‚ะต ั‚ะพั‡ะบัƒ ะฒัั‚ะฐะฒะบะธ: + .pastespec-paste-as = PASTESPEC ะ’ัั‚ะฐะฒะธั‚ัŒ ะบะฐะบ [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3719,6 +3826,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3773,6 +3887,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *ะŸั€ะธะฒัะทะบะธ ะบ ะฟะพะดะปะพะถะบะฐะผ ั€ะฐะทะปะธั‡ะฐัŽั‚ัั* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3912,6 +4027,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = ะ’ั‹ะฑะตั€ะธั‚ะต ะธะทะพะฑั€ะฐะถะตะฝะธะต ะธะปะธ ะฟะพะดะปะพะถะบัƒ: + .adjust-select-pdf-underlay = ะ’ั‹ะฑะตั€ะธั‚ะต ะฟะพะดะปะพะถะบัƒ PDF: + .adjust-select-dwf-underlay = ะ’ั‹ะฑะตั€ะธั‚ะต ะฟะพะดะปะพะถะบัƒ DWF: + .adjust-select-dgn-underlay = ะ’ั‹ะฑะตั€ะธั‚ะต ะฟะพะดะปะพะถะบัƒ DGN: + .adjust-mixed-underlays = *ะะตะดะพะฟัƒัั‚ะธะผั‹ะน ะฝะฐะฑะพั€. ะ’ั‹ะฑะตั€ะธั‚ะต ั‚ะพะปัŒะบะพ ะฟะพะดะปะพะถะบะธ ะพะดะฝะพะณะพ ั‚ะธะฟะฐ ั„ะฐะนะปะพะฒ.* + .adjust-image-option = ะ’ะฒะตะดะธั‚ะต ะฟะฐั€ะฐะผะตั‚ั€ ะธะทะพะฑั€ะฐะถะตะฝะธั [ะบะพะฝั‚ั€ะฐัั‚/ะฃะณะฐัะฐะฝะธะต/ะฏั€ะบะพัั‚ัŒ] <ะฏั€ะบะพัั‚ัŒ>: + .adjust-underlay-option = ะ’ะฒะตะดะธั‚ะต ะฟะฐั€ะฐะผะตั‚ั€ ะฟะพะดะปะพะถะบะธ __ocs_arg_kind__ [ะฃะณะฐัะฐะฝะธะต/ะบะพะฝั‚ั€ะฐัั‚/ะœะพะฝะพั…ั€ะพะผะฝั‹ะน] <__ocs_arg_option__>: + .adjust-monochrome = ะœะพะฝะพั…ั€ะพะผะฝั‹ะน? [ะ”ะฐ/ะะตั‚] <__ocs_arg_value__>: + .adjust-brightness-value = ะ’ะฒะตะดะธั‚ะต ะทะฝะฐั‡ะตะฝะธะต: ัั€ะบะพัั‚ัŒ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = ะ’ะฒะตะดะธั‚ะต ะทะฝะฐั‡ะตะฝะธะต: ะบะพะฝั‚ั€ะฐัั‚ (0-100) <__ocs_arg_value__>: + .adjust-fade-value = ะ’ะฒะตะดะธั‚ะต ะทะฝะฐั‡ะตะฝะธะต: ัƒะณะฐัะฐะฝะธะต (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = ะ—ะฝะฐั‡ะตะฝะธะต ะดะพะปะถะฝะพ ะฑั‹ั‚ัŒ ั†ะตะปั‹ะผ ั‡ะธัะปะพะผ ะพั‚ 0 ะดะพ 100. .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4416,7 +4543,27 @@ model = .value-2point = 2-ั‚ะพั‡ะบะฐ .cube = ะšัƒะฑ + layout = + .mvsetup-select-objects-to-delete = MVSETUP ะ’ั‹ะฑะตั€ะธั‚ะต ะพะฑัŠะตะบั‚ั‹ ะดะปั ัƒะดะฐะปะตะฝะธั: + .delete-objects-option = ะฃะดะฐะปะธั‚ัŒ ะพะฑัŠะตะบั‚ั‹ + .mvsetup-enable-paper-space = MVSETUP ะ’ะบะปัŽั‡ะธั‚ัŒ ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะพ ะปะธัั‚ะฐ? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP ะ’ะฒะตะดะธั‚ะต ะผะฐััˆั‚ะฐะฑะฝั‹ะน ะบะพัั„ั„ะธั†ะธะตะฝั‚ <1>: + .mvsetup-enter-paper-width = MVSETUP ะ’ะฒะตะดะธั‚ะต ัˆะธั€ะธะฝัƒ ะปะธัั‚ะฐ: + .mvsetup-enter-paper-height = MVSETUP ะ’ะฒะตะดะธั‚ะต ะฒั‹ัะพั‚ัƒ ะปะธัั‚ะฐ: + .mvsetup-enter-option = MVSETUP ะ—ะฐะดะฐะนั‚ะต ะฟะฐั€ะฐะผะตั‚ั€ [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP ะ—ะฐะดะฐะนั‚ะต ะบะพะผะฟะพะฝะพะฒะบัƒ ะฒะธะดะพะฒั‹ั… ัะบั€ะฐะฝะพะฒ [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP ะฃะบะฐะถะธั‚ะต ะฟะตั€ะฒั‹ะน ัƒะณะพะป ะพะฑะปะฐัั‚ะธ ะฒะธะดะพะฒั‹ั… ัะบั€ะฐะฝะพะฒ : + .mvsetup-specify-opposite-corner = MVSETUP ะฃะบะฐะถะธั‚ะต ะฟั€ะพั‚ะธะฒะพะฟะพะปะพะถะฝั‹ะน ัƒะณะพะป: + .mvsetup-viewports-in-x = MVSETUP ะ’ะฒะตะดะธั‚ะต ั‡ะธัะปะพ ะฒะธะดะพะฒั‹ั… ัะบั€ะฐะฝะพะฒ ะฟะพ X <2>: + .mvsetup-viewports-in-y = MVSETUP ะ’ะฒะตะดะธั‚ะต ั‡ะธัะปะพ ะฒะธะดะพะฒั‹ั… ัะบั€ะฐะฝะพะฒ ะฟะพ Y <2>: + .mvsetup-distance-in-x = MVSETUP ะฃะบะฐะถะธั‚ะต ั€ะฐััั‚ะพัะฝะธะต ะผะตะถะดัƒ ะฒะธะดะพะฒั‹ะผะธ ัะบั€ะฐะฝะฐะผะธ ะฟะพ X <0>: + .mvsetup-distance-in-y = MVSETUP ะฃะบะฐะถะธั‚ะต ั€ะฐััั‚ะพัะฝะธะต ะผะตะถะดัƒ ะฒะธะดะพะฒั‹ะผะธ ัะบั€ะฐะฝะฐะผะธ ะฟะพ Y <0>: + .mvsetup-select-viewports = MVSETUP ะ’ั‹ะฑะตั€ะธั‚ะต ะฒะธะดะพะฒั‹ะต ัะบั€ะฐะฝั‹ ะดะปั ะผะฐััˆั‚ะฐะฑะธั€ะพะฒะฐะฝะธั: + .mvsetup-paper-space-units = MVSETUP ะ’ะฒะตะดะธั‚ะต ั‡ะธัะปะพ ะตะดะธะฝะธั† ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐ ะปะธัั‚ะฐ <1>: + .mvsetup-model-space-units = MVSETUP ะ’ะฒะตะดะธั‚ะต ั‡ะธัะปะพ ะตะดะธะฝะธั† ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐ ะผะพะดะตะปะธ <1>: + .mvsetup-viewports-do-not-fit = MVSETUP ะ’ะธะดะพะฒั‹ะต ัะบั€ะฐะฝั‹ ะฝะต ะฟะพะผะตั‰ะฐัŽั‚ัั ะฒ ะพะฑะปะฐัั‚ะธ. + .scale-viewports-option = ะœะฐััˆั‚ะฐะฑ ะฒะธะดะพะฒั‹ั… ัะบั€ะฐะฝะพะฒ .active = ะ’ะบะปัŽั‡ั‘ะฝ .inactive = ะฝะตะฐะบั‚ะธะฒะฝั‹ะน .insert-view = ะ’ะบะปัŽั‡ะธั‚ัŒ ะฒะธะด @@ -4442,6 +4589,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F <ัะปะพะน> = ะทะฐะผะพั€ะพะทะธั‚ัŒ | T <ัะปะพะน> = ั€ะฐะทะผะพั€ะพะทะธั‚ัŒ | Enter = ะฒั‹ั…ะพะด view = + .twist-option = ะŸะพะฒะพั€ะพั‚ + .dview-specify-view-twist-angle = DVIEW ะฃะบะฐะถะธั‚ะต ัƒะณะพะป ะฟะพะฒะพั€ะพั‚ะฐ ะฒะธะดะฐ <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW ะ—ะฐะดะฐะนั‚ะต ะฟะฐั€ะฐะผะตั‚ั€ [TWist/Undo]: .color-scheme-set-to-system-currently-name = ะฆะฒะตั‚ะพะฒะฐั ัั…ะตะผะฐ ัƒัั‚ะฐะฝะพะฒะปะตะฝะฐ ะฝะฐ ยซะกะธัั‚ะตะผะฝัƒัŽยป (ัะตะนั‡ะฐั __ocs_fmt_0__). .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = ะขั€ะตะฑัƒะตั‚ัั ั†ะตะปะพะต ั‡ะธัะปะพ ะพั‚ __ocs_fmt_0__ ะดะพ __ocs_fmt_1__. @@ -4636,13 +4786,6 @@ view = .x-no-xdata = __ocs_fmt_0__: ะฝะตั‚ ะดะฐะฝะฝั‹ั…. .value-3d-orbit = 3D ะพั€ะฑะธั‚ะฐะปัŒะฝะฐั .value-3d-orbit-drag-with-right-mouse-button = 3D Orbit: ะฟะตั€ะตั‚ะฐั‰ะธั‚ะต ะฟั€ะฐะฒะพะน ะบะฝะพะฟะบะพะน ะผั‹ัˆะธ. - .adjust-brightness-contrast-fade = ADJUST [ะฏั€ะบะพัั‚ัŒ / ะšะพะฝั‚ั€ะฐัั‚ / ะคะฐะดะต]: - .adjust-brightness-0-100 = ADJUST ัั€ะบะพัั‚ัŒ 0-100: - .adjust-contrast-0-100 = ADJUST ะšะพะฝั‚ั€ะฐัั‚ 0-100: - .adjust-fade-0-100 = ADJUST ะฒั‹ั†ะฒะตั‚ะฐะฝะธะต 0-100: - .adjust-select-raster-image-s-first = ADJUST: ัะฝะฐั‡ะฐะปะฐ ะฒั‹ะฑะตั€ะธั‚ะต ั€ะฐัั‚ั€ะพะฒะพะต ะธะทะพะฑั€ะฐะถะตะฝะธะต(ะธ). - .adjust-value-must-be-0-100 = ADJUST: ะทะฝะฐั‡ะตะฝะธะต ะดะพะปะถะฝะพ ะฑั‹ั‚ัŒ 0-100. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__ = __ocs_fmt_1__ ะฝะฐ __ocs_fmt_2__ ะธะทะพะฑั€ะฐะถะตะฝะธะธ(ะฐั…). .annoallvisible-new-value-0-1 = ANNOALLVISIBLE ะฝะพะฒะพะต ะทะฝะฐั‡ะตะฝะธะต [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: ะฒะฒะตะดะธั‚ะต 0 ะธะปะธ 1. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE ะฝะพะฒะพะต ะทะฝะฐั‡ะตะฝะธะต [-4..4]: @@ -4768,7 +4911,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = ะŸะพะดะปะพะถะบะฐ __ocs_fmt_0__: fade=__ocs_fmt_1__, contrast=__ocs_fmt_2__, on=__ocs_fmt_3__, clip=__ocs_fmt_4__, mono=__ocs_fmt_5__ .updated-changed-underlay-s = ะžะฑะฝะพะฒะปะตะฝะพ __ocs_fmt_0__ underlay(s). - .usage-adjust-brightness-contrast-fade-0-100 = ะ˜ัะฟะพะปัŒะทะพะฒะฐะฝะธะต: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = ะ˜ัะฟะพะปัŒะทะพะฒะฐะฝะธะต: ANNOSCALE e.g. 1:50, 2:1, or a factor .usage-draworder-f-front-b-back-a-above-handle = ะ˜ัะฟะพะปัŒะทะพะฒะฐะฝะธะต: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = ะ˜ัะฟะพะปัŒะทะพะฒะฐะฝะธะต: HYPERLINK (select objects first) @@ -5996,6 +6138,46 @@ ui = .osnap-settings = ะะฐัั‚ั€ะพะนะบะธ ะฟั€ะธะฒัะทะบะธ... .snap-override-none-next-pick-only = ะ ะฐะทะพะฒะฐั ะฟั€ะธะฒัะทะบะฐ: ะะตั‚ (ั‚ะพะปัŒะบะพ ัะปะตะดัƒัŽั‰ะฐั ั‚ะพั‡ะบะฐ). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = ะ’ัะตะณะดะฐ ั€ะฐะทะผะตั‰ะฐั‚ัŒ ั‚ะตะบัั‚ ะผะตะถะดัƒ ะฒั‹ะฝะพัะฝั‹ะผะธ ะปะธะฝะธัะผะธ .angular-dimensions = ะฃะณะปะพะฒั‹ะต ั€ะฐะทะผะตั€ั‹ @@ -6263,3 +6445,117 @@ graphic-attributes = .reset-angle = ะกะฑั€ะพัะธั‚ัŒ ัƒะณะพะป .one-color = ะžะดะธะฝ ั†ะฒะตั‚ .two-colors = ะ”ะฒะฐ ั†ะฒะตั‚ะฐ + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = ะ”ะพะฑะฐะฒะธั‚ัŒ ะฒ ั€ะฐะฑะพั‡ะธะน ะฝะฐะฑะพั€ + .remove-from-working-set = ะฃะดะฐะปะธั‚ัŒ ะธะท ั€ะฐะฑะพั‡ะตะณะพ ะฝะฐะฑะพั€ะฐ + .toolbar-objects = ะพะฑัŠะตะบั‚ะพะฒ + .dialog-title = ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธะต ััั‹ะปะบะธ + .dialog-reference = ะกัั‹ะปะบะฐ ะดะปั ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธั + .dialog-chain-hint = ะฆะตะฟะพั‡ะบะฐ ะฒ ัƒะบะฐะทะฐะฝะฝะพะน ั‚ะพั‡ะบะต; ั‰ะตะปะบะฝะธั‚ะต ะผะตั‚ะบัƒ, ั‡ั‚ะพะฑั‹ ัะผะตะฝะธั‚ัŒ ัƒั€ะพะฒะตะฝัŒ. + .dialog-path = ะŸัƒั‚ัŒ: + .dialog-working-set = ะ ะฐะฑะพั‡ะธะน ะฝะฐะฑะพั€ + .dialog-all-objects = ะ’ัะต ะพะฑัŠะตะบั‚ั‹ + .dialog-pick-objects = ะ’ั‹ะฑั€ะฐั‚ัŒ ะฝะฐ ั‡ะตั€ั‚ะตะถะต + .dialog-attributes = ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ั‚ะฐะบะถะต ะพะฟั€ะตะดะตะปะตะฝะธั ะฐั‚ั€ะธะฑัƒั‚ะพะฒ + .dialog-session = ะกะตะฐะฝั + .dialog-lock = ะ‘ะปะพะบะธั€ะพะฒะฐั‚ัŒ ะพะฑัŠะตะบั‚ั‹ ะฒะฝะต ั€ะฐะฑะพั‡ะตะณะพ ะฝะฐะฑะพั€ะฐ + .dialog-unique = ะ”ะฐะฒะฐั‚ัŒ ัƒะฝะธะบะฐะปัŒะฝั‹ะต ะธะผะตะฝะฐ ะบะพะฝั„ะปะธะบั‚ัƒัŽั‰ะธะผ ัะปะพัะผ, ัั‚ะธะปัะผ ะธ ะฑะปะพะบะฐะผ + .dialog-start = ะะฐั‡ะฐั‚ัŒ ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธะต + .dialog-help = ะžั‚ะบั€ั‹ะฒะฐะตั‚ ะฒั…ะพะถะดะตะฝะธะต ะฑะปะพะบะฐ ะธะปะธ ะฒะฝะตัˆะฝัŽัŽ ััั‹ะปะบัƒ ะดะปั ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธั ะฝะฐ ะผะตัั‚ะต. + .select-reference = ะ’ั‹ะฑะตั€ะธั‚ะต ััั‹ะปะบัƒ: + .reference-not-found = ะกัั‹ะปะบะฐ ะฝะต ะฝะฐะนะดะตะฝะฐ. + .not-valid-reference = ะžะฑัŠะตะบั‚ ะฝะต ัะฒะปัะตั‚ัั ะดะพะฟัƒัั‚ะธะผะพะน ััั‹ะปะบะพะน + .one-entity-added = ะ”ะพะฑะฐะฒะปะตะฝ 1 ะพะฑัŠะตะบั‚ + .nesting-level = ะ’ั‹ะฑะตั€ะธั‚ะต ัƒั€ะพะฒะตะฝัŒ ะฒะปะพะถะตะฝะฝะพัั‚ะธ [ะžะบ/ะกะปะตะดัƒัŽั‰ะธะน] <ะกะปะตะดัƒัŽั‰ะธะน>: + .selection-method = ะ’ะฒะตะดะธั‚ะต ัะฟะพัะพะฑ ะฒั‹ะฑะพั€ะฐ ะพะฑัŠะตะบั‚ะพะฒ [ะ’ัะต/ะ’ะปะพะถะตะฝะฝั‹ะต] <ะ’ัะต>: + .select-nested = ะ’ั‹ะฑะตั€ะธั‚ะต ะฒะปะพะถะตะฝะฝั‹ะต ะพะฑัŠะตะบั‚ั‹: + .display-attdefs = ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ะพะฟั€ะตะดะตะปะตะฝะธั ะฐั‚ั€ะธะฑัƒั‚ะพะฒ [ะ”ะฐ/ะะตั‚] <ะะตั‚>: + .zero-items-selected = ะ’ั‹ะฑั€ะฐะฝะพ ัะปะตะผะตะฝั‚ะพะฒ: 0 + .one-item-selected = ะ’ั‹ะฑั€ะฐะฝ 1 ัะปะตะผะตะฝั‚ + .items-selected = ะ’ั‹ะฑั€ะฐะฝะพ ัะปะตะผะตะฝั‚ะพะฒ: __ocs_fmt_0__ + .refset-option = ะ’ะฒะตะดะธั‚ะต ะฟะฐั€ะฐะผะตั‚ั€ [ะ”ะพะฑะฐะฒะธั‚ัŒ/ะฃะดะฐะปะธั‚ัŒ] <ะ”ะพะฑะฐะฒะธั‚ัŒ>: + .refclose-option = ะ’ะฒะตะดะธั‚ะต ะฟะฐั€ะฐะผะตั‚ั€ [ะกะพั…ั€ะฐะฝะธั‚ัŒ/ะžั‚ะผะตะฝะธั‚ัŒ ะธะทะผะตะฝะตะฝะธั ััั‹ะปะบะธ] <ะกะพั…ั€ะฐะฝะธั‚ัŒ>: + .not-checked-out = ** ะšะพะผะฐะฝะดะฐ ะฝะตะดะพัั‚ัƒะฟะฝะฐ, ะฟะพะบะฐ ััั‹ะปะบะฐ ะฝะต ะพั‚ะบั€ั‹ั‚ะฐ ะดะปั ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธั ะบะพะผะฐะฝะดะพะน REFEDIT ** + .already-checked-out = ** ะšะพะผะฐะฝะดะฐ ะฝะตะดะพัั‚ัƒะฟะฝะฐ, __ocs_fmt_0__ ัƒะถะต ะพั‚ะบั€ั‹ั‚ ะดะปั ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐะฝะธั ** + .transfer-objects = ะŸะตั€ะตะฝะพั ะพะฑัŠะตะบั‚ะพะฒ ะผะตะถะดัƒ ั€ะฐะฑะพั‡ะธะผ ะฝะฐะฑะพั€ะพะผ RefEdit ะธ ะพัะฝะพะฒะฝั‹ะผ ั‡ะตั€ั‚ะตะถะพะผ... + .added-to-working-set = ะ”ะพะฑะฐะฒะปะตะฝะพ ะฒ ั€ะฐะฑะพั‡ะธะน ะฝะฐะฑะพั€: __ocs_fmt_0__ + .removed-from-working-set = ะฃะดะฐะปะตะฝะพ ะธะท ั€ะฐะฑะพั‡ะตะณะพ ะฝะฐะฑะพั€ะฐ: __ocs_fmt_0__ + .one-object-added = 1 ะพะฑัŠะตะบั‚ ะดะพะฑะฐะฒะปะตะฝ ะฒ __ocs_fmt_0__ + .objects-added = ะžะฑัŠะตะบั‚ะพะฒ ะดะพะฑะฐะฒะปะตะฝะพ ะฒ __ocs_fmt_1__: __ocs_fmt_0__ + .one-object-removed = 1 ะพะฑัŠะตะบั‚ ัƒะดะฐะปะตะฝ ะธะท __ocs_fmt_0__ + .objects-removed = ะžะฑัŠะตะบั‚ะพะฒ ัƒะดะฐะปะตะฝะพ ะธะท __ocs_fmt_1__: __ocs_fmt_0__ + .one-xref-instance = ะžะฑะฝะพะฒะปะตะฝะพ ะฒั…ะพะถะดะตะฝะธะต ะฒะฝะตัˆะฝะตะน ััั‹ะปะบะธ: 1 + .xref-instances = ะžะฑะฝะพะฒะปะตะฝะพ ะฒั…ะพะถะดะตะฝะธะน ะฒะฝะตัˆะฝะตะน ััั‹ะปะบะธ: __ocs_fmt_0__ + .one-block-instance = ะžะฑะฝะพะฒะปะตะฝะพ ะฒั…ะพะถะดะตะฝะธะต ะฑะปะพะบะฐ: 1 + .block-instances = ะžะฑะฝะพะฒะปะตะฝะพ ะฒั…ะพะถะดะตะฝะธะน ะฑะปะพะบะฐ: __ocs_fmt_0__ + .redefined-reloaded = __ocs_fmt_0__ ะฟะตั€ะตะพะฟั€ะตะดะตะปะตะฝ ะธ ะฟะตั€ะตะทะฐะณั€ัƒะถะตะฝ. + .redefined = __ocs_fmt_0__ ะฟะตั€ะตะพะฟั€ะตะดะตะปะตะฝ. + .ok = ะžะบ + .nested = ะ’ะปะพะถะตะฝะฝั‹ะต + +image-adjust = + .title = ะะฐัั‚ั€ะพะนะบะฐ ะธะทะพะฑั€ะฐะถะตะฝะธั + .preview = ะŸั€ะตะดะฒะฐั€ะธั‚ะตะปัŒะฝั‹ะน ะฟั€ะพัะผะพั‚ั€ + .before = ะ”ะพ + .after = ะŸะพัะปะต + .no-preview = ะฝะต ัƒะดะฐะปะพััŒ ะทะฐะณั€ัƒะทะธั‚ัŒ ะธะทะพะฑั€ะฐะถะตะฝะธะต + .adjust = ะะฐัั‚ั€ะพะนะบะฐ + .brightness = ะฏั€ะบะพัั‚ัŒ + .contrast = ะšะพะฝั‚ั€ะฐัั‚ + .fade = ะ—ะฐั‚ะตะฝะตะฝะธะต + .hint = ะฉะตะปะบะฝะธั‚ะต ะธะปะธ ะฟะตั€ะตั‚ะฐั‰ะธั‚ะต ะฟะพะปะพััƒ; โˆ’ / + โ€” ะพะดะธะฝ ัˆะฐะณ, ะฒ ะฟะพะปะต ะฒะฒะพะดะธั‚ัั ั†ะตะปะพะต ั‡ะธัะปะพ 0โ€“100. + .reset = ะ’ะพััั‚ะฐะฝะพะฒะธั‚ัŒ ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ + .apply = ะŸั€ะธะผะตะฝะธั‚ัŒ + .help = ะ—ะฐะดะฐะตั‚ ัั€ะบะพัั‚ัŒ, ะบะพะฝั‚ั€ะฐัั‚ ะธ ะทะฐั‚ะตะฝะตะฝะธะต ะฒั‹ะฑั€ะฐะฝะฝั‹ั… ั€ะฐัั‚ั€ะพะฒั‹ั… ะธะทะพะฑั€ะฐะถะตะฝะธะน. + +keytips = + .title = KeyTip + .typed = ะ’ะฒะตะดะตะฝะพ: { $path } + .hint-tabs = ะ’ะฒะตะดะธั‚ะต ะฑัƒะบะฒัƒ ะฒะบะปะฐะดะบะธ ยท Esc ะทะฐะบั€ั‹ะฒะฐะตั‚ + .hint-tab = ะ’ะบะปะฐะดะบะฐ { $tab } ยท Esc โ€” ะฝะฐ ัƒั€ะพะฒะตะฝัŒ ะฝะฐะทะฐะด + .hint-items = ะ’ะฒะตะดะธั‚ะต ะฑัƒะบะฒัƒ ัะปะตะผะตะฝั‚ะฐ ยท Esc โ€” ะฝะฐ ัƒั€ะพะฒะตะฝัŒ ะฝะฐะทะฐะด diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 37fbeb6ca..6d45a347c 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = Eklentiler ribbon-tab = + .block-editor = Blok Dรผzenleyici .draw = ร‡izim .parametric = Parametrik .annotate = Aรงฤฑklama @@ -98,6 +99,9 @@ start = .no-recent-files = AรงtฤฑฤŸฤฑnฤฑz dosyalar burada gรถrรผnรผr. .browser-storage = Tarayฤฑcฤฑ depolama alanฤฑ .keep-recent-files = Son dosyalarฤฑ sakla + .view-list = Liste + .view-icons = Simgeler + .view-preview = Bรผyรผk รถnizlemeler donation = .title = BaฤŸฤฑลŸ Talebi @@ -165,6 +169,14 @@ modal = .that-object = bu nesneyi .those-objects = bu nesneleri + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = "{ $name }" gรถrseli yapฤฑลŸtฤฑrฤฑldฤฑ: { $width }ร—{ $height } px .image-unsupported = YapฤฑลŸtฤฑrฤฑlan "{ $name }" gรถrseli รงรถzรผmlenemedi. @@ -259,11 +271,73 @@ command-copy = .mode = COPY Kopyalama modu seรงin [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ yalnฤฑzca burada (yeลŸil), __ocs_fmt_1__ yalnฤฑzca diฤŸer รงizimde (kฤฑrmฤฑzฤฑ)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ร‡izimle karลŸฤฑlaลŸtฤฑr + .label-associative-array-created = __ocs_fmt_0__: iliลŸkisel dizi oluลŸturuldu. + .assoc-array-array-path = Dizi (Yol) + .assoc-array-array-polar = Dizi (Kutupsal) + .assoc-array-array-rectangular = Dizi (Dikdรถrtgen) + .assoc-array-item-spacing = ร–ฤŸe aralฤฑฤŸฤฑ + .assoc-array-fill-angle = Doldurma aรงฤฑsฤฑ + .assoc-array-items = ร–ฤŸeler + .assoc-array-level-spacing = Seviye aralฤฑฤŸฤฑ + .assoc-array-levels = Seviyeler + .assoc-array-row-spacing = Satฤฑr aralฤฑฤŸฤฑ + .assoc-array-column-spacing = Sรผtun aralฤฑฤŸฤฑ + .fbxexport-exported-to = FBXEXPORT: "__ocs_fmt_0__" konumuna dฤฑลŸa aktarฤฑldฤฑ + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: Bu รงizimde 3B รถrgรผ verisi yok. + .fbxexport-error = FBXEXPORT: __ocs_fmt_0__ + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = Yeni taramalar sฤฑnฤฑrlarฤฑnฤฑ izler (HPASSOC) + .options-show-ribbon = ลžeridi gรถster (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = Menรผ รงubuฤŸunu gรถster (MENUBAR) + .options-window-elements = Pencere ร–ฤŸeleri + .menu-viewports = Gรถrรผnรผm Pencereleri + .menu-view-twist = Gรถrรผnรผmรผ Dรถndรผr + .menu-tools = Araรงlar + .menu-table-style = Tablo Stili + .menu-run-script = Komut Dosyasฤฑ ร‡alฤฑลŸtฤฑr + .menu-revision-cloud = Revizyon Bulutu + .menu-regen-all = Tรผmรผnรผ Yenile + .menu-redraw = Yeniden ร‡iz + .menu-rectangle = Dikdรถrtgen + .menu-quick-dimension = Hฤฑzlฤฑ ร–lรงรผ + .menu-publish = Yayฤฑmla + .menu-paste-special = ร–zel YapฤฑลŸtฤฑr + .menu-parametric = Parametrik + .menu-page-setup-manager = Sayfa Dรผzeni Yรถneticisi + .menu-orbit = Yรถrรผnge + .menu-new-ucs = Yeni KKS + .menu-named-views = AdlandฤฑrฤฑlmฤฑลŸ Gรถrรผnรผmler + .menu-multiline-text = ร‡ok Satฤฑrlฤฑ Metin + .menu-multiline-style = ร‡oklu ร‡izgi Stili + .menu-multiline = ร‡oklu ร‡izgi + .menu-multileader-style = ร‡oklu Ok Stili + .menu-match-properties = ร–zellikleri EลŸle + .menu-lengthen = Uzat + .menu-jogged = Kฤฑrฤฑk + .menu-id-point = Nokta KimliฤŸi + .menu-explode-text = Metni Patlat + .menu-edit-spline = EฤŸriyi Dรผzenle + .menu-edit-polyline = ร‡oklu ร‡izgiyi Dรผzenle + .menu-edit-hatch = Taramayฤฑ Dรผzenle + .menu-drawing-limits = ร‡izim Sฤฑnฤฑrlarฤฑ + .menu-donut = Halka + .menu-dimension-style = ร–lรงรผ Stili + .menu-construction-line = Yapฤฑ ร‡izgisi + .menu-arc-length = Yay UzunluฤŸu + .command-line-opacity = Komut satฤฑrฤฑ opaklฤฑฤŸฤฑ + .txtexp-no-text-selected = TXTEXP: metin seรงilmedi. + .angle-override-shown = Aรงฤฑ geรงersiz kฤฑlma: __ocs_fmt_0__ .plot-preview-task = ร‡izim รถnizleme .link-a-csv-data-source = CSV veri kaynaฤŸฤฑ baฤŸla .csv-files = CSV Dosyalarฤฑ .browser-cannot-open-typed-path = Tarayฤฑcฤฑ yazฤฑlan bir yolu aรงamaz; dosyayฤฑ seรงin. .recover-drawing = ร‡izimi Kurtar + .requires-0-or-1-or-keyword = Yalnฤฑzca 0 veya 1 ya da bir seรงenek anahtar sรถzcรผฤŸรผ girilebilir. + .requires-a-number = Bir sayฤฑ gerekli. .restore-it-as-a-recovered-tab-or-discard-the-copy = Onu kurtarฤฑlan bir sekme olarak geri yรผkleyin veya kopyayฤฑ atฤฑn. .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" son kayฤฑttan daha yeni bir otomatik kayฤฑt kopyasฤฑna sahip. .autosave-copy-could-not-be-stored = Otomatik kayฤฑt kopyasฤฑ depolanamadฤฑ: __ocs_fmt_0__ @@ -810,6 +884,10 @@ common = .slice-result-commit-failed = SLICE: Bir sonuรง iลŸlenemedi; tรผm kaynak nesneler korundu. .slice-retained-update-failed = SLICE: Korunan sonuรง gรผncellenemedi. .slice-created-results = SLICE: __ocs_arg_count__ sonuรง nesnesi oluลŸturuldu. + .sectioncull-no-section = SECTIONCULL: bir kesit nesnesi seรงin veya canlฤฑ kesiti aรงฤฑn. + .sectioncull-prepare-failed = SECTIONCULL: kesilen bir nesne hazฤฑrlanamadฤฑ; hiรงbir ลŸey deฤŸiลŸtirilmedi. + .sectioncull-nothing-outside = SECTIONCULL: kesitin dฤฑลŸฤฑnda kalan bir ลŸey yok. + .sectioncull-result = SECTIONCULL: __ocs_arg_erased__ nesne silindi, kesit sฤฑnฤฑrฤฑndaki __ocs_arg_cut__ nesne kesildi. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ noktadan geรงen bir spline uyduruldu. .splinefit-need-at-least-3-points = SPLINEFIT: En az 3 nokta gerekir. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: ฤฐรงinden spline geรงirilecek bir รงoklu รงizgi seรงin. @@ -847,9 +925,12 @@ common = .sent-to-printer-printer = Yazฤฑcฤฑya gรถnderildi: __ocs_fmt_0__ .set-current = Geรงerli Yap .shade-plot = GรถlgelendirilmiลŸ yazdฤฑrma + .shaded-with-edges = Kenarlarla gรถlgeli + .shades-of-gray = Gri tonlarฤฑ .shadow-flags = Gรถlge bayraklarฤฑ .single = Tek .size = Boyut + .sketchy = Eskiz .snap-override-label-next-pick-only = Yakalama geรงersiz kฤฑlma: __ocs_fmt_0__ (yalnฤฑzca sonraki seรงim). .solid-created = Katฤฑ oluลŸturuldu. .source-object-not-found = Kaynak nesne bulunamadฤฑ. @@ -1053,7 +1134,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE: Left, Top veya Right girin. .isodraft-expected-on-or-off = ISODRAFT: On veya Off girin. .xdata-selected-entities-are-on-locked-layers = XDATA: seรงilen nesneler kilitli katmanlarda. - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST: raster gรถrรผntรผ seรงilmedi veya รถzellik bilinmiyor (BRIGHTNESS/CONTRAST/FADE kullanฤฑn). .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE: -4 ile 4 arasฤฑnda bir tamsayฤฑ girin. .usage-datalink-path-to-csv = Kullanฤฑm: DATALINK <.csv-dosya-yolu> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE: baฤŸlantฤฑlฤฑ tablo bulunamadฤฑ. @@ -2044,7 +2124,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = Dosya .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2353,6 +2433,7 @@ common = .load-linetypes = ร‡izgi tiplerini yรผkle .requires-two-positive-values = ฤฐki pozitif deฤŸer (x,y) ya da ikisi iรงin tek deฤŸer gerekir. .snap-marker-color = Yakalama iลŸaretรงisi rengi + .snap-marker-size = Yakalama iลŸaretรงisi boyutu .command-line-text-color = Komut satฤฑrฤฑ metin rengi .operand = ฤฐลŸlenen .linetypes-loaded = __ocs_fmt_0__ รงizgi tipi yรผklendi. @@ -2366,6 +2447,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE Taban noktasฤฑnฤฑ belirtin [__ocs_arg_count__ nesne]: .threedrotate-specify-rotation-axis = 3DROTATE Dรถndรผrme eksenini [X/Y/Z] ya da eksen รผzerinde ikinci bir noktayฤฑ belirtin : .threedrotate-specify-rotation-angle-a = 3DROTATE Dรถndรผrme aรงฤฑsฤฑnฤฑ belirtin <__ocs_arg_a__>: + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = Kaydetme Uyarฤฑsฤฑ .unable-to-save-drawing = ร‡izim Kaydedilemiyor @@ -3032,6 +3120,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT ร–znitelikleri olan bir blok seรงin: .annotation-object-scale = Aรงฤฑklama Nesnesi ร–lรงeฤŸi .bedit-select-block-reference-to-edit = BEDIT Dรผzenlenecek blok referansฤฑnฤฑ seรงin: + .bvstate-option = BVSTATE Bir seรงenek girin [Yeni/Ayarla/Yeniden adlandฤฑr/Sil] : + .bvstate-new-name = BVSTATE Yeni gรถrรผnรผrlรผk durumunun adฤฑnฤฑ girin: + .bvstate-set = BVSTATE Geรงerli yapฤฑlacak gรถrรผnรผrlรผk durumunu girin <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE Yeniden adlandฤฑrฤฑlacak gรถrรผnรผrlรผk durumunu girin <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE Yeni adฤฑ girin: + .bvstate-delete = BVSTATE Silinecek gรถrรผnรผrlรผk durumunu girin: + .bvstate-not-found = "__ocs_fmt_0__" gรถrรผnรผrlรผk durumu bulunamadฤฑ. + .bvstate-exists = "__ocs_fmt_0__" gรถrรผnรผrlรผk durumu zaten var. + .bvstate-last = Tek gรถrรผnรผrlรผk durumu silinemez. + .bvstate-needs-bedit = __ocs_fmt_0__: รถnce bloฤŸu Blok Dรผzenleyici'de aรงฤฑn (BEDIT). + .bvstate-current = Geรงerli gรถrรผnรผrlรผk durumu: __ocs_fmt_0__ + .bvhide-select = Gizlenecek nesneleri seรงin: + .bvshow-select = Gรถrรผnรผr yapฤฑlacak nesneleri seรงin: + .arrayedit-select-array = ARRAYEDIT Diziyi seรงin: + .arrayedit-options = ARRAYEDIT Bir seรงenek girin [Kaynak/รงฤฑKฤฑลŸ] <รงฤฑKฤฑลŸ>: + .arrayedit-not-an-array = ARRAYEDIT: seรงilen nesne iliลŸkili bir dizi deฤŸil. .blend-enter-continuity-tangent-curvature-c = BLEND SรผrekliliฤŸi girin [TeฤŸet/EฤŸrilik] <__ocs_arg_c__>: .blend-select-first-open-curve-continuity-c = BLEND ฤฐlk aรงฤฑk eฤŸriyi seรงin [Sรผreklilik=__ocs_arg_c__]: .blend-select-second-open-curve-continuity-c = BLEND ฤฐkinci aรงฤฑk eฤŸriyi seรงin [Sรผreklilik=__ocs_arg_c__]: @@ -3576,6 +3680,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Ekleme noktasฤฑnฤฑ belirtin: insert = + .pastespec-specify-insertion-point = PASTESPEC Ekleme noktasฤฑnฤฑ belirtin: + .pastespec-paste-as = PASTESPEC ลžu biรงimde yapฤฑลŸtฤฑr [Objects/Block/Text]: .pdfui-pdf-scale = PDF รถlรงeฤŸi: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Ekleme noktasฤฑnฤฑ belirtin: .pdfui-no-pdf-underlays = PDF altlฤฑฤŸฤฑ bulunamadฤฑ. @@ -3692,6 +3798,13 @@ insert = .xref-attach-hide-details = Ayrฤฑntฤฑlarฤฑ Gizle .xref-attach-select-file = Referans Dosyasฤฑ Seรง .xref-attach-all-files = Tรผm Referans Dosyalarฤฑ + .xref-attach-filter-images = Tรผm gรถrรผntรผ dosyalarฤฑ + .xref-attach-filter-dwf = DWF dosyalarฤฑ (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = Tรผm DGN Dosyalarฤฑ (*.*) + .xref-attach-filter-pdf = PDF dosyalarฤฑ (*.pdf) + .xref-attach-filter-point-cloud = Nokta Bulutu (*.rcp; *.rcs) + .xref-attach-filter-all = Tรผm dosyalar (*.*) .xref-attach-help = DiฤŸer รงizimler, raster gรถrรผntรผler ve altlฤฑklar gibi harici dosyalara referans ekler. .xref-attach-invalid-input = Geรงersiz giriลŸ. .xref-insertion-point-x = Ekleme noktasฤฑ X @@ -3742,6 +3855,7 @@ insert = .display-and-plot-frames = ร‡erรงeveleri gรถster ve รงizdir .display-not-plot-frames = ร‡erรงeveleri gรถster, รงizdirme .frames-vary = *ร‡erรงeveler farklฤฑ* + .underlay-osnaps-vary = *Altlฤฑk nesne yakalamalarฤฑ farklฤฑ* .regenerating-model = Model yeniden oluลŸturuluyor. .requires-int-0-2 = 0 ile 2 arasฤฑnda bir tamsayฤฑ gerekli. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3878,6 +3992,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Gรถrรผntรผleri seรงin: + .adjust-select-image-or-underlay = Gรถrรผntรผ veya altlฤฑk seรงin: + .adjust-select-pdf-underlay = PDF altlฤฑฤŸฤฑ seรงin: + .adjust-select-dwf-underlay = DWF altlฤฑฤŸฤฑ seรงin: + .adjust-select-dgn-underlay = DGN altlฤฑฤŸฤฑ seรงin: + .adjust-mixed-underlays = *Geรงersiz seรงim kรผmesi. Yalnฤฑz aynฤฑ dosya tรผrรผndeki altlฤฑk nesnelerini seรงin.* + .adjust-image-option = Gรถrรผntรผ seรงeneฤŸini girin [KarลŸฤฑtlฤฑk/Solukluk/Parlaklฤฑk] : + .adjust-underlay-option = __ocs_arg_kind__ altlฤฑk seรงeneฤŸini girin [Solukluk/KarลŸฤฑtlฤฑk/Tek Renkli] <__ocs_arg_option__>: + .adjust-monochrome = Tek Renkli? [Evet/Hayฤฑr] <__ocs_arg_value__>: + .adjust-brightness-value = Parlaklฤฑk deฤŸerini girin (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = KarลŸฤฑtlฤฑk deฤŸerini girin (0-100) <__ocs_arg_value__>: + .adjust-fade-value = Solukluk deฤŸerini girin (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = DeฤŸer 0 ile 100 arasฤฑnda bir tam sayฤฑ olmalฤฑdฤฑr. .transparency-mode-on = Saydamlฤฑk modunu girin [ON/OFF] : .transparency-mode-off = Saydamlฤฑk modunu girin [ON/OFF] : .xref-attach-menu = Attach... @@ -4376,6 +4502,25 @@ model = .cube = Kรผp layout = + .mvsetup-select-objects-to-delete = MVSETUP Silinecek nesneleri seรงin: + .delete-objects-option = Nesneleri sil + .mvsetup-enable-paper-space = MVSETUP KรขฤŸฤฑt alanฤฑ etkinleลŸtirilsin mi? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP ร–lรงek faktรถrรผnรผ girin <1>: + .mvsetup-enter-paper-width = MVSETUP KรขฤŸฤฑt geniลŸliฤŸini girin: + .mvsetup-enter-paper-height = MVSETUP KรขฤŸฤฑt yรผksekliฤŸini girin: + .mvsetup-enter-option = MVSETUP Seรงenek girin [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP Gรถrรผnรผm penceresi dรผzenini girin [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP Gรถrรผnรผm pencereleri alanฤฑnฤฑn ilk kรถลŸesini belirtin : + .mvsetup-specify-opposite-corner = MVSETUP KarลŸฤฑ kรถลŸeyi belirtin: + .mvsetup-viewports-in-x = MVSETUP X yรถnรผndeki gรถrรผnรผm penceresi sayฤฑsฤฑnฤฑ girin <2>: + .mvsetup-viewports-in-y = MVSETUP Y yรถnรผndeki gรถrรผnรผm penceresi sayฤฑsฤฑnฤฑ girin <2>: + .mvsetup-distance-in-x = MVSETUP X yรถnรผnde gรถrรผnรผm pencereleri arasฤฑndaki mesafeyi belirtin <0>: + .mvsetup-distance-in-y = MVSETUP Y yรถnรผnde gรถrรผnรผm pencereleri arasฤฑndaki mesafeyi belirtin <0>: + .mvsetup-select-viewports = MVSETUP ร–lรงeklenecek gรถrรผnรผm pencerelerini seรงin: + .mvsetup-paper-space-units = MVSETUP KรขฤŸฤฑt alanฤฑ birim sayฤฑsฤฑnฤฑ girin <1>: + .mvsetup-model-space-units = MVSETUP Model alanฤฑ birim sayฤฑsฤฑnฤฑ girin <1>: + .mvsetup-viewports-do-not-fit = MVSETUP Gรถrรผnรผm pencereleri alana sฤฑฤŸmฤฑyor. + .scale-viewports-option = Gรถrรผnรผm pencerelerini รถlรงekle .active = Etkin .inactive = Etkin DeฤŸil .insert-view = Gรถrรผnรผm Ekle @@ -4401,6 +4546,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F = Dondur | T = ร‡รถz | Enter = ร‡ฤฑk view = + .twist-option = Dรถndรผr + .dview-specify-view-twist-angle = DVIEW Gรถrรผnรผm dรถndรผrme aรงฤฑsฤฑnฤฑ belirtin <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW Seรงenek girin [TWist/Undo]: .color-scheme-set-to-system-currently-name = Renk dรผzeni 'Sistem' olarak ayarlandฤฑ (ลŸu anda __ocs_fmt_0__). .ss-create-sheet-set = Sayfa Kรผmesi OluลŸtur .ss-requires-integer-between = __ocs_fmt_0__ ile __ocs_fmt_1__ arasฤฑnda bir tam sayฤฑ gerekir. @@ -4588,13 +4736,6 @@ view = .x-no-xdata = __ocs_fmt_0__: xdata yok. .value-3d-orbit = 3B Yรถrรผnge .value-3d-orbit-drag-with-right-mouse-button = 3B Yรถrรผnge: saฤŸ fare dรผฤŸmesiyle sรผrรผkleyin. - .adjust-brightness-contrast-fade = ADJUST [Parlaklฤฑk / KarลŸฤฑtlฤฑk / Solma]: - .adjust-brightness-0-100 = ADJUST Parlaklฤฑk 0-100: - .adjust-contrast-0-100 = ADJUST KarลŸฤฑtlฤฑk 0-100: - .adjust-fade-0-100 = ADJUST Solma 0-100: - .adjust-select-raster-image-s-first = ADJUST: รถnce raster gรถrรผntรผleri seรงin. - .adjust-value-must-be-0-100 = ADJUST: deฤŸer 0 ile 100 arasฤฑnda olmalฤฑdฤฑr. - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_2__ gรถrรผntรผde __ocs_fmt_0__ = __ocs_fmt_1__ olarak deฤŸiลŸtirildi. .annoallvisible-new-value-0-1 = ANNOALLVISIBLE Yeni deฤŸer [0/1]: .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: 0 veya 1 girin. .annoautoscale-new-value-4-4 = ANNOAUTOSCALE Yeni deฤŸer [-4..4]: @@ -4714,7 +4855,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__ = __ocs_fmt_1__ .underlay-x-fade-contrast-on-clip-mono = Altlฤฑk __ocs_fmt_0__: solukluk=__ocs_fmt_1__, karลŸฤฑtlฤฑk=__ocs_fmt_2__, aรงฤฑk=__ocs_fmt_3__, kฤฑrp=__ocs_fmt_4__, tek renk=__ocs_fmt_5__ .updated-changed-underlay-s = __ocs_fmt_0__ altlฤฑk gรผncellendi. - .usage-adjust-brightness-contrast-fade-0-100 = Kullanฤฑm: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = Kullanฤฑm: ANNOSCALE รถr. 1:50, 2:1 veya bir katsayฤฑ .usage-draworder-f-front-b-back-a-above-handle = Kullanฤฑm: DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = Kullanฤฑm: HYPERLINK (รถnce nesneleri seรงin) @@ -5918,6 +6058,46 @@ ui = .osnap-settings = Nesne kenetleme ayarlarฤฑ... .snap-override-none-next-pick-only = Geรงici kenetleme: Yok (yalnฤฑzca sonraki nokta). + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = Metni her zaman uzatma รงizgileri arasฤฑnda tut .angular-dimensions = Aรงฤฑsal รถlรงรผler @@ -6185,3 +6365,117 @@ graphic-attributes = .reset-angle = Aรงฤฑyฤฑ sฤฑfฤฑrla .one-color = Tek renk .two-colors = ฤฐki renk + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Arka Plan + .none = Yok + .solid = Dรผz + .gradient = Gradyan + .image = Gรถrรผntรผ + .preview = ร–nizleme + .solid-color = Dรผz renk + .color = Renk + .select = Seรงโ€ฆ + .three-colors = รœรง renk + .rotation = Dรถndรผrme + .top = รœst renk + .middle = Orta renk + .bottom = Alt renk + .browse = Gรถzatโ€ฆ + .position = YerleลŸim + .center = Ortala + .stretch = Uzat + .tile = DรถลŸe + .scale = ร–lรงek + .aspect = En-boy oranฤฑnฤฑ koru + .adjust = Ayarla + .reset = Sฤฑfฤฑrla + .offset = ร–teleme + .pixels = px + .no-image = gรถrรผntรผ seรงilmedi + .canvas = รงizim alanฤฑ rengi + .info = Bilgi + .none-info = Gรถrรผnรผm geri yรผklendiฤŸinde arka plan deฤŸiลŸmez; รงizim alanฤฑ rengi kullanฤฑlฤฑr. + .apply = Uygula + .help = AdlandฤฑrฤฑlmฤฑลŸ gรถrรผnรผm geri yรผklendiฤŸinde gรถsterilecek arka planฤฑ ayarlar. + .image-files = Gรถrรผntรผ dosyalarฤฑ + .select-image = Arka Plan Gรถrรผntรผsรผ Seรง + +ref-edit = + .add-to-working-set = ร‡alฤฑลŸma Kรผmesine Ekle + .remove-from-working-set = ร‡alฤฑลŸma Kรผmesinden ร‡ฤฑkar + .toolbar-objects = nesne + .dialog-title = Referansฤฑ Dรผzenle + .dialog-reference = Dรผzenlenecek referans + .dialog-chain-hint = Seรงilen noktadaki zincir; bir รงipe tฤฑklayarak dรผzeyi deฤŸiลŸtirin. + .dialog-path = Yol: + .dialog-working-set = ร‡alฤฑลŸma kรผmesi + .dialog-all-objects = Tรผm nesneler + .dialog-pick-objects = ร‡izimde seรงeceฤŸim + .dialog-attributes = ร–znitelik tanฤฑmlarฤฑnฤฑ da dรผzenle + .dialog-session = Oturum + .dialog-lock = Kรผmede olmayan nesneleri kilitle + .dialog-unique = ร‡akฤฑลŸan katman, stil ve blok adlarฤฑnฤฑ benzersiz yap + .dialog-start = Dรผzenlemeye BaลŸla + .dialog-help = Bir blok referansฤฑnฤฑ veya dฤฑลŸ referansฤฑ yerinde dรผzenlemek iรงin aรงar. + .select-reference = Referans seรงin: + .reference-not-found = Referans bulunamadฤฑ. + .not-valid-reference = Nesne geรงerli bir referans deฤŸil + .one-entity-added = 1 nesne eklendi + .nesting-level = ฤฐรง iรงe dรผzeyi seรงin [Tamam/Sonraki] : + .selection-method = Nesne seรงim yรถntemini girin [Tรผmรผ/ฤฐรง iรงe] : + .select-nested = ฤฐรง iรงe nesneleri seรงin: + .display-attdefs = ร–znitelik tanฤฑmlarฤฑ gรถsterilsin mi [Evet/Hayฤฑr] : + .zero-items-selected = 0 รถฤŸe seรงildi + .one-item-selected = 1 รถฤŸe seรงildi + .items-selected = __ocs_fmt_0__ รถฤŸe seรงildi + .refset-option = Bir seรงenek girin [Ekle/Kaldฤฑr] : + .refclose-option = SeรงeneฤŸi girin [Kaydet/Referans deฤŸiลŸikliklerinden vazgeรง] : + .not-checked-out = ** Bir referans REFEDIT komutuyla dรผzenlemeye alฤฑnmadฤฑkรงa komuta izin verilmez ** + .already-checked-out = ** Komuta izin verilmez, __ocs_fmt_0__ zaten dรผzenlemeye alฤฑnmฤฑลŸ ** + .transfer-objects = Nesneleri RefEdit รงalฤฑลŸma kรผmesi ile ana รงizim arasฤฑnda aktarฤฑn... + .added-to-working-set = __ocs_fmt_0__ nesne รงalฤฑลŸma kรผmesine eklendi + .removed-from-working-set = __ocs_fmt_0__ nesne รงalฤฑลŸma kรผmesinden รงฤฑkarฤฑldฤฑ + .one-object-added = 1 nesne __ocs_fmt_0__ tanฤฑmฤฑna eklendi + .objects-added = __ocs_fmt_0__ nesne __ocs_fmt_1__ tanฤฑmฤฑna eklendi + .one-object-removed = 1 nesne __ocs_fmt_0__ tanฤฑmฤฑndan รงฤฑkarฤฑldฤฑ + .objects-removed = __ocs_fmt_0__ nesne __ocs_fmt_1__ tanฤฑmฤฑndan รงฤฑkarฤฑldฤฑ + .one-xref-instance = 1 xref รถrneฤŸi gรผncellendi + .xref-instances = __ocs_fmt_0__ xref รถrneฤŸi gรผncellendi + .one-block-instance = 1 blok รถrneฤŸi gรผncellendi + .block-instances = __ocs_fmt_0__ blok รถrneฤŸi gรผncellendi + .redefined-reloaded = __ocs_fmt_0__ yeniden tanฤฑmlandฤฑ ve yeniden yรผklendi. + .redefined = __ocs_fmt_0__ yeniden tanฤฑmlandฤฑ. + .ok = Tamam + .nested = ฤฐรง iรงe + +image-adjust = + .title = Gรถrรผntรผ Ayarฤฑ + .preview = ร–nizleme + .before = ร–nce + .after = Sonra + .no-preview = gรถrรผntรผ yรผklenemedi + .adjust = Ayar + .brightness = Parlaklฤฑk + .contrast = KarลŸฤฑtlฤฑk + .fade = Solukluk + .hint = ร‡ubuฤŸa tฤฑklayฤฑn veya sรผrรผkleyin; โˆ’ / + bir adฤฑm, kutuya yazฤฑlan 0โ€“100 tamsayฤฑ. + .reset = Varsayฤฑlana dรถn + .apply = Uygula + .help = Seรงilen raster gรถrรผntรผlerin parlaklฤฑฤŸฤฑnฤฑ, karลŸฤฑtlฤฑฤŸฤฑnฤฑ ve soluklฤฑฤŸฤฑnฤฑ ayarlar. + +keytips = + .title = KeyTip + .typed = Yazฤฑlan: { $path } + .hint-tabs = Sekme harfini yazฤฑn ยท Esc kapatฤฑr + .hint-tab = { $tab } sekmesi ยท Esc bir dรผzey geri + .hint-items = ร–ฤŸe harfini yazฤฑn ยท Esc bir dรผzey geri diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 85e50033a..925cc8f1c 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = ๆ’ไปถ ribbon-tab = + .block-editor = ๅ—็ผ–่พ‘ๅ™จ .draw = ็ป˜ๅˆถ .parametric = ๅ‚ๆ•ฐๅŒ– .annotate = ๆณจ้‡Š @@ -98,6 +99,9 @@ start = .no-recent-files = ๆ‚จๆ‰“ๅผ€็š„ๆ–‡ไปถๅฐ†ๆ˜พ็คบๅœจๆญคๅค„ใ€‚ .browser-storage = ๆต่งˆๅ™จๅญ˜ๅ‚จ .keep-recent-files = ไฟ็•™ๆœ€่ฟ‘ๆ–‡ไปถ + .view-list = ๅˆ—่กจ + .view-icons = ๅ›พๆ ‡ + .view-preview = ๅคง้ข„่งˆ donation = .title = ๆๆฌพ่ฏทๆฑ‚ @@ -166,6 +170,14 @@ modal = .that-object = ่ฏฅๅฏน่ฑก .those-objects = ่ฟ™ไบ›ๅฏน่ฑก + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = ๅทฒ็ฒ˜่ดดๅ›พๅƒ "{ $name }"๏ผš{ $width }ร—{ $height } px .image-unsupported = ๆ— ๆณ•่งฃ็ ็ฒ˜่ดด็š„ๅ›พๅƒ "{ $name }"ใ€‚ @@ -261,11 +273,73 @@ command-copy = .mode = COPY ่พ“ๅ…ฅๅคๅˆถๆจกๅผ้€‰้กน [Single/Multiple] : common = + .compare-prompt = COMPARE ไป…ๆญคๅค„ __ocs_fmt_0__ (็ปฟ่‰ฒ)๏ผŒไป…ๅฆไธ€ๅ›พๅฝข __ocs_fmt_1__ (็บข่‰ฒ)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ไธŽๅ›พๅฝขๆฏ”่พƒ + .label-associative-array-created = __ocs_fmt_0__: ๅทฒๅˆ›ๅปบๅ…ณ่”้˜ตๅˆ—ใ€‚ + .assoc-array-array-path = ้˜ตๅˆ— (่ทฏๅพ„) + .assoc-array-array-polar = ้˜ตๅˆ— (็Žฏๅฝข) + .assoc-array-array-rectangular = ้˜ตๅˆ— (็Ÿฉๅฝข) + .assoc-array-item-spacing = ้กน็›ฎ้—ด่ท + .assoc-array-fill-angle = ๅกซๅ……่ง’ๅบฆ + .assoc-array-items = ้กน็›ฎ + .assoc-array-level-spacing = ๅฑ‚้—ด่ท + .assoc-array-levels = ๅฑ‚ๆ•ฐ + .assoc-array-row-spacing = ่กŒ้—ด่ท + .assoc-array-column-spacing = ๅˆ—้—ด่ท + .fbxexport-exported-to = FBXEXPORT:ๅฏผๅ‡บไธบ"__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT:ๆญค็ป˜ๅ›พไธญๆฒกๆœ‰3D็ฝ‘ๆ ผๆ•ฐๆฎใ€‚ + .fbxexport-error = FBXEXPORT:__ocs_fmt_0__ ๆ—ถ้—ด่ฝด: + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = ๆ–ฐๅ›พๆกˆๅกซๅ……่ทŸ้šๅ…ถ่พน็•Œ (HPASSOC) + .options-show-ribbon = ๆ˜พ็คบๅŠŸ่ƒฝๅŒบ (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = ๆ˜พ็คบ่œๅ•ๆ  (MENUBAR) + .options-window-elements = ็ช—ๅฃๅ…ƒ็ด  + .menu-viewports = ่ง†ๅฃ + .menu-view-twist = ่ง†ๅ›พๆ‰ญๆ›ฒ + .menu-tools = ๅทฅๅ…ท + .menu-table-style = ่กจๆ ผๆ ทๅผ + .menu-run-script = ่ฟ่กŒ่„šๆœฌ + .menu-revision-cloud = ไฟฎ่ฎขไบ‘็บฟ + .menu-regen-all = ๅ…จ้ƒจ้‡็”Ÿๆˆ + .menu-redraw = ้‡็”ป + .menu-rectangle = ็Ÿฉๅฝข + .menu-quick-dimension = ๅฟซ้€Ÿๆ ‡ๆณจ + .menu-publish = ๅ‘ๅธƒ + .menu-paste-special = ้€‰ๆ‹ฉๆ€ง็ฒ˜่ดด + .menu-parametric = ๅ‚ๆ•ฐๅŒ– + .menu-page-setup-manager = ้กต้ข่ฎพ็ฝฎ็ฎก็†ๅ™จ + .menu-orbit = ๅŠจๆ€่ง‚ๅฏŸ + .menu-new-ucs = ๆ–ฐๅปบ UCS + .menu-named-views = ๅ‘ฝๅ่ง†ๅ›พ + .menu-multiline-text = ๅคš่กŒๆ–‡ๅญ— + .menu-multiline-style = ๅคš็บฟๆ ทๅผ + .menu-multiline = ๅคš็บฟ + .menu-multileader-style = ๅคš้‡ๅผ•็บฟๆ ทๅผ + .menu-match-properties = ็‰นๆ€งๅŒน้… + .menu-lengthen = ๆ‹‰้•ฟ + .menu-jogged = ๆŠ˜ๅผฏ + .menu-id-point = ็‚นๅๆ ‡ + .menu-explode-text = ๅˆ†่งฃๆ–‡ๅญ— + .menu-edit-spline = ็ผ–่พ‘ๆ ทๆกๆ›ฒ็บฟ + .menu-edit-polyline = ็ผ–่พ‘ๅคšๆฎต็บฟ + .menu-edit-hatch = ็ผ–่พ‘ๅ›พๆกˆๅกซๅ…… + .menu-drawing-limits = ๅ›พๅฝข็•Œ้™ + .menu-donut = ๅœ†็Žฏ + .menu-dimension-style = ๆ ‡ๆณจๆ ทๅผ + .menu-construction-line = ๆž„้€ ็บฟ + .menu-arc-length = ๅผง้•ฟ + .command-line-opacity = ๅ‘ฝไปค่กŒไธ้€ๆ˜Žๅบฆ + .txtexp-no-text-selected = TXTEXP: ๆœช้€‰ๆ‹ฉๆ–‡ๅญ—ใ€‚ + .angle-override-shown = ่ง’ๅบฆๆ›ฟไปฃ๏ผš__ocs_fmt_0__ .plot-preview-task = ๆ‰“ๅฐ้ข„่งˆ .link-a-csv-data-source = ้“พๆŽฅ CSV ๆ•ฐๆฎๆบ .csv-files = CSV ๆ–‡ไปถ .browser-cannot-open-typed-path = ๆต่งˆๅ™จๆ— ๆณ•ๆ‰“ๅผ€้”ฎๅ…ฅ็š„่ทฏๅพ„๏ผ›่ฏทๆ”นไธบ้€‰ๆ‹ฉๆ–‡ไปถใ€‚ .recover-drawing = ๆขๅคๅ›พๅฝข + .requires-0-or-1-or-keyword = ๅช่ƒฝไธบ 0 ๆˆ– 1๏ผŒๆˆ–้€‰้กนๅ…ณ้”ฎๅญ—ใ€‚ + .requires-a-number = ้œ€่ฆ่พ“ๅ…ฅๆ•ฐๅญ—ใ€‚ .restore-it-as-a-recovered-tab-or-discard-the-copy = ๅฐ†ๅ…ถๆขๅคไธบๅทฒๆขๅค็š„ๆ ‡็ญพ้กต๏ผŒๆˆ–ๆ”พๅผƒ่ฏฅๅ‰ฏๆœฌใ€‚ .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" ๆœ‰ไธ€ไปฝๆฏ”ไธŠๆฌกไฟๅญ˜ๆ›ดๆ–ฐ็š„่‡ชๅŠจไฟๅญ˜ๅ‰ฏๆœฌใ€‚ .autosave-copy-could-not-be-stored = ๆ— ๆณ•ๅญ˜ๅ‚จ่‡ชๅŠจไฟๅญ˜ๅ‰ฏๆœฌ: __ocs_fmt_0__ @@ -809,6 +883,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: ่ฏท้€‰ๆ‹ฉๆˆช้ขๅฏน่ฑกๆˆ–ๆ‰“ๅผ€ๆดปๅŠจๆˆช้ขใ€‚ + .sectioncull-prepare-failed = SECTIONCULL: ๆ— ๆณ•ๅ‡†ๅค‡ๅ‰ชๅˆ‡ๅŽ็š„ๅฏน่ฑก๏ผ›ๆœชๅšไปปไฝ•ๆ›ดๆ”นใ€‚ + .sectioncull-nothing-outside = SECTIONCULL: ๆˆช้ขไน‹ๅค–ๆฒกๆœ‰ไปปไฝ•ๅฏน่ฑกใ€‚ + .sectioncull-result = SECTIONCULL: ๅทฒๅˆ ้™ค __ocs_arg_erased__ ไธชๅฏน่ฑก๏ผŒๅนถๅœจๆˆช้ข่พน็•Œๅค„ๅ‰ชๅˆ‡ __ocs_arg_cut__ ไธชใ€‚ .splinefit-fit-a-spline-through-n-points = SPLINEFIT: ๅŒน้…ไธ€ๆก้€š่ฟ‡__ocs_fmt_0__็‚น็š„็›ธ็‰‡ใ€‚ .splinefit-need-at-least-3-points = SPLINEFIT:่‡ณๅฐ‘้œ€่ฆ3ไธช็‚นใ€‚ .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT:้€‰ๆ‹ฉไธ€ๆกๅคš็บฟไปฅ้€‚ๅบ”้€š่ฟ‡็š„ๅˆ†็บฟใ€‚ @@ -846,9 +924,12 @@ common = .sent-to-printer-printer = ๅ‘้€ๅˆฐๆ‰“ๅฐๆœบ : __ocs_fmt_0__ .set-current = ็ฝฎไธบๅฝ“ๅ‰ .shade-plot = ็€่‰ฒๆ‰“ๅฐ + .shaded-with-edges = ๅธฆ่พน็ผ˜็€่‰ฒ + .shades-of-gray = ็ฐๅบฆ .shadow-flags = ้˜ดๅฝฑๆ ‡ๅฟ— .single = ๅ•ไธ€ .size = ๅคงๅฐ + .sketchy = ๅ‹พ็”ป .snap-override-label-next-pick-only = ๆŠ“ๅ–่ฆ†็›– : __ocs_fmt_0__ (ไป…้€‰ๆ‹ฉไธ‹ไธ€ๆญฅ) ใ€‚ .solid-created = ๅฎž็›ธๆ‰€้€ . .source-object-not-found = ๆ‰พไธๅˆฐๆบๅฏน่ฑก ใ€‚ @@ -1054,7 +1135,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE๏ผš่ฏท่พ“ๅ…ฅ Leftใ€Top ๆˆ– Rightใ€‚ .isodraft-expected-on-or-off = ISODRAFT๏ผš่ฏท่พ“ๅ…ฅ On ๆˆ– Offใ€‚ .xdata-selected-entities-are-on-locked-layers = XDATA๏ผš้€‰ๅฎšๅฎžไฝ“ไฝไบŽ้”ๅฎš็š„ๅ›พๅฑ‚ไธŠใ€‚ - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST๏ผšๆœช้€‰ๆ‹ฉๅ…‰ๆ …ๅ›พๅƒๆˆ–ๅฑžๆ€งๆœช็Ÿฅ๏ผˆไฝฟ็”จ BRIGHTNESS/CONTRAST/FADE๏ผ‰ใ€‚ .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE๏ผš่ฏท่พ“ๅ…ฅ -4 ๅˆฐ 4 ไน‹้—ด็š„ๆ•ดๆ•ฐใ€‚ .usage-datalink-path-to-csv = ็”จๆณ•๏ผšDATALINK <.csvๆ–‡ไปถ่ทฏๅพ„> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE๏ผšๆœชๆ‰พๅˆฐ้“พๆŽฅ่กจๆ ผใ€‚ @@ -2047,7 +2127,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = ๆ–‡ไปถ .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2356,6 +2436,7 @@ common = .load-linetypes = ๅŠ ่ฝฝ็บฟๅž‹ .requires-two-positive-values = ้œ€่ฆไธคไธชๆญฃๅ€ผ (x,y)๏ผŒๆˆ–ไธ€ไธชๅŒๆ—ถ็”จไบŽไธค่€…็š„ๅ€ผใ€‚ .snap-marker-color = ๆ•ๆ‰ๆ ‡่ฎฐ้ขœ่‰ฒ + .snap-marker-size = ๆ•ๆ‰ๆ ‡่ฎฐๅคงๅฐ .command-line-text-color = ๅ‘ฝไปค่กŒๆ–‡ๅญ—้ขœ่‰ฒ .operand = ๆ“ไฝœๅฏน่ฑก .linetypes-loaded = ๅทฒๅŠ ่ฝฝ __ocs_fmt_0__ ไธช็บฟๅž‹ใ€‚ @@ -2369,6 +2450,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE ๆŒ‡ๅฎšๅŸบ็‚น [__ocs_arg_count__ ไธชๅฏน่ฑก]๏ผš .threedrotate-specify-rotation-axis = 3DROTATE ๆŒ‡ๅฎšๆ—‹่ฝฌ่ฝด [X/Y/Z] ๆˆ–่ฝดไธŠ็š„็ฌฌไบŒ็‚น ๏ผš .threedrotate-specify-rotation-angle-a = 3DROTATE ๆŒ‡ๅฎšๆ—‹่ฝฌ่ง’ๅบฆ <__ocs_arg_a__>๏ผš + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = ไฟๅญ˜่ญฆๅ‘Š .unable-to-save-drawing = ๆ— ๆณ•ไฟๅญ˜ๅ›พๅฝข @@ -2992,6 +3080,7 @@ draw = .arc-specify-radius-of-arc = ARC ๆŒ‡ๅฎšๅœ†ๅผง็š„ๅŠๅพ„: .chord-length = ๅผฆ้•ฟ + modify = .add-vertex = ๆทปๅŠ ้กถ็‚น .closed-cross = ้—ญๅˆๅๅญ— @@ -3037,6 +3126,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT ้€‰ๆ‹ฉๅธฆๅฑžๆ€ง็š„ๅ—๏ผš .annotation-object-scale = ๆณจ้‡Šๅฏน่ฑกๆฏ”ไพ‹ .bedit-select-block-reference-to-edit = BEDIT ้€‰ๆ‹ฉ่ฆ็ผ–่พ‘็š„ๅ—ๅผ•็”จ : + .bvstate-option = BVSTATE ่พ“ๅ…ฅ้€‰้กน [ๆ–ฐๅปบ/่ฎพ็ฝฎ/้‡ๅ‘ฝๅ/ๅˆ ้™ค] <่ฎพ็ฝฎ>: + .bvstate-new-name = BVSTATE ่พ“ๅ…ฅๆ–ฐๅฏ่งๆ€ง็Šถๆ€็š„ๅ็งฐ: + .bvstate-set = BVSTATE ่พ“ๅ…ฅ่ฆ็ฝฎไธบๅฝ“ๅ‰็š„ๅฏ่งๆ€ง็Šถๆ€ <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE ่พ“ๅ…ฅ่ฆ้‡ๅ‘ฝๅ็š„ๅฏ่งๆ€ง็Šถๆ€ <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE ่พ“ๅ…ฅๆ–ฐๅ็งฐ: + .bvstate-delete = BVSTATE ่พ“ๅ…ฅ่ฆๅˆ ้™ค็š„ๅฏ่งๆ€ง็Šถๆ€: + .bvstate-not-found = ๆ‰พไธๅˆฐๅฏ่งๆ€ง็Šถๆ€โ€œ__ocs_fmt_0__โ€ใ€‚ + .bvstate-exists = ๅฏ่งๆ€ง็Šถๆ€โ€œ__ocs_fmt_0__โ€ๅทฒๅญ˜ๅœจใ€‚ + .bvstate-last = ไธ่ƒฝๅˆ ้™คๅ”ฏไธ€็š„ๅฏ่งๆ€ง็Šถๆ€ใ€‚ + .bvstate-needs-bedit = __ocs_fmt_0__: ่ฏทๅ…ˆๅœจๅ—็ผ–่พ‘ๅ™จไธญๆ‰“ๅผ€่ฏฅๅ— (BEDIT)ใ€‚ + .bvstate-current = ๅฝ“ๅ‰ๅฏ่งๆ€ง็Šถๆ€: __ocs_fmt_0__ + .bvhide-select = ้€‰ๆ‹ฉ่ฆ้š่—็š„ๅฏน่ฑก: + .bvshow-select = ้€‰ๆ‹ฉ่ฆๆ˜พ็คบ็š„ๅฏน่ฑก: + .arrayedit-select-array = ARRAYEDIT ้€‰ๆ‹ฉ้˜ตๅˆ—: + .arrayedit-options = ARRAYEDIT ่พ“ๅ…ฅ้€‰้กน [ๆบ/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: ๆ‰€้€‰ๅฏน่ฑกไธๆ˜ฏๅ…ณ่”้˜ตๅˆ—ใ€‚ .blend-enter-continuity-tangent-curvature-c = BLEND ่พ“ๅ…ฅ่ฟž็ปญๆ€ง [็›ธๅˆ‡/ๆ›ฒ็އ] <__ocs_arg_c__>๏ผš .blend-select-first-open-curve-continuity-c = BLEND ้€‰ๆ‹ฉ็ฌฌไธ€ๆกๅผ€ๆ”พๆ›ฒ็บฟ [Continuity=__ocs_arg_c__]๏ผš .blend-select-second-open-curve-continuity-c = BLEND ้€‰ๆ‹ฉ็ฌฌไบŒๆกๅผ€ๆ”พๆ›ฒ็บฟ [Continuity=__ocs_arg_c__]๏ผš @@ -3589,6 +3694,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE ๆŒ‡ๅฎšๆ’ๅ…ฅ็‚น๏ผš insert = + .pastespec-specify-insertion-point = PASTESPEC ๆŒ‡ๅฎšๆ’ๅ…ฅ็‚น: + .pastespec-paste-as = PASTESPEC ็ฒ˜่ดดไธบ [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3705,6 +3812,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3757,6 +3871,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *ๅ‚่€ƒๅบ•ๅ›พๅฏน่ฑกๆ•ๆ‰ไธไธ€่‡ด* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3894,6 +4009,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = ้€‰ๆ‹ฉๅ›พๅƒๆˆ–ๅ‚่€ƒๅบ•ๅ›พ: + .adjust-select-pdf-underlay = ้€‰ๆ‹ฉ PDF ๅ‚่€ƒๅบ•ๅ›พ: + .adjust-select-dwf-underlay = ้€‰ๆ‹ฉ DWF ๅ‚่€ƒๅบ•ๅ›พ: + .adjust-select-dgn-underlay = ้€‰ๆ‹ฉ DGN ๅ‚่€ƒๅบ•ๅ›พ: + .adjust-mixed-underlays = *้€‰ๆ‹ฉ้›†ๆ— ๆ•ˆใ€‚่ฏทไป…้€‰ๆ‹ฉ็›ธๅŒๆ–‡ไปถ็ฑปๅž‹็š„ๅ‚่€ƒๅบ•ๅ›พใ€‚* + .adjust-image-option = ่พ“ๅ…ฅๅ›พๅƒ้€‰้กน [ๅฏนๆฏ”ๅบฆ/ๅ˜ๆš—/ไบฎๅบฆ] <ไบฎๅบฆ>: + .adjust-underlay-option = ่พ“ๅ…ฅ __ocs_arg_kind__ ๅ‚่€ƒๅบ•ๅ›พ้€‰้กน [ๅ˜ๆš—/ๅฏนๆฏ”ๅบฆ/ๅ•่‰ฒ] <__ocs_arg_option__>: + .adjust-monochrome = ๅ•่‰ฒ? [ๆ˜ฏ/ๅฆ] <__ocs_arg_value__>: + .adjust-brightness-value = ่พ“ๅ…ฅไบฎๅบฆๅ€ผ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = ่พ“ๅ…ฅๅฏนๆฏ”ๅบฆๅ€ผ (0-100) <__ocs_arg_value__>: + .adjust-fade-value = ่พ“ๅ…ฅๅ˜ๆš—ๅ€ผ (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = ๅ€ผๅฟ…้กปๆ˜ฏไป‹ไบŽ 0 ๅ’Œ 100 ไน‹้—ด็š„ๆ•ดๆ•ฐใ€‚ .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4398,7 +4525,27 @@ model = .value-2point = ไธค็‚น .cube = ็ซ‹ๆ–นไฝ“ + layout = + .mvsetup-select-objects-to-delete = MVSETUP ้€‰ๆ‹ฉ่ฆๅˆ ้™ค็š„ๅฏน่ฑก: + .delete-objects-option = ๅˆ ้™คๅฏน่ฑก + .mvsetup-enable-paper-space = MVSETUP ๅฏ็”จๅ›พ็บธ็ฉบ้—ด? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP ่พ“ๅ…ฅๆฏ”ไพ‹ๅ› ๅญ <1>: + .mvsetup-enter-paper-width = MVSETUP ่พ“ๅ…ฅๅ›พ็บธๅฎฝๅบฆ: + .mvsetup-enter-paper-height = MVSETUP ่พ“ๅ…ฅๅ›พ็บธ้ซ˜ๅบฆ: + .mvsetup-enter-option = MVSETUP ่พ“ๅ…ฅ้€‰้กน [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP ่พ“ๅ…ฅ่ง†ๅฃๅธƒๅฑ€ [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP ๆŒ‡ๅฎš่ง†ๅฃๅŒบๅŸŸ็š„็ฌฌไธ€ไธช่ง’็‚น : + .mvsetup-specify-opposite-corner = MVSETUP ๆŒ‡ๅฎšๅฏน่ง’็‚น: + .mvsetup-viewports-in-x = MVSETUP ่พ“ๅ…ฅ X ๆ–นๅ‘็š„่ง†ๅฃๆ•ฐ <2>: + .mvsetup-viewports-in-y = MVSETUP ่พ“ๅ…ฅ Y ๆ–นๅ‘็š„่ง†ๅฃๆ•ฐ <2>: + .mvsetup-distance-in-x = MVSETUP ๆŒ‡ๅฎš X ๆ–นๅ‘็š„่ง†ๅฃ้—ด่ท <0>: + .mvsetup-distance-in-y = MVSETUP ๆŒ‡ๅฎš Y ๆ–นๅ‘็š„่ง†ๅฃ้—ด่ท <0>: + .mvsetup-select-viewports = MVSETUP ้€‰ๆ‹ฉ่ฆ็ผฉๆ”พ็š„่ง†ๅฃ: + .mvsetup-paper-space-units = MVSETUP ่พ“ๅ…ฅๅ›พ็บธ็ฉบ้—ดๅ•ไฝๆ•ฐ <1>: + .mvsetup-model-space-units = MVSETUP ่พ“ๅ…ฅๆจกๅž‹็ฉบ้—ดๅ•ไฝๆ•ฐ <1>: + .mvsetup-viewports-do-not-fit = MVSETUP ่ง†ๅฃๆ— ๆณ•ๆ”พๅ…ฅ่ฏฅๅŒบๅŸŸใ€‚ + .scale-viewports-option = ็ผฉๆ”พ่ง†ๅฃ .active = ๆฟ€ๆดป .inactive = ไธๆดปๅŠจ .insert-view = ๆ’ๅ…ฅ่ง†ๅ›พ @@ -4424,6 +4571,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F <ๅ›พๅฑ‚> = ๅ†ป็ป“ | T <ๅ›พๅฑ‚> = ่งฃๅ†ป | Enter = ้€€ๅ‡บ view = + .twist-option = ๆ‰ญๆ›ฒ + .dview-specify-view-twist-angle = DVIEW ๆŒ‡ๅฎš่ง†ๅ›พๆ‰ญๆ›ฒ่ง’ๅบฆ <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW ่พ“ๅ…ฅ้€‰้กน [TWist/Undo]: .color-scheme-set-to-system-currently-name = ้ขœ่‰ฒๆ–นๆกˆๅทฒ่ฎพไธบโ€œ็ณป็ปŸโ€๏ผˆๅฝ“ๅ‰๏ผš__ocs_fmt_0__๏ผ‰ใ€‚ .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = ้œ€่ฆไป‹ไบŽ __ocs_fmt_0__ ๅ’Œ __ocs_fmt_1__ ไน‹้—ด็š„ๆ•ดๆ•ฐใ€‚ @@ -4618,13 +4768,6 @@ view = .x-no-xdata = __ocs_fmt_0__:ๆ— xdata. .value-3d-orbit = ไธ‰็ปดๅŠจๆ€่ง‚ๅฏŸ .value-3d-orbit-drag-with-right-mouse-button = ไธ‰็ปด่ฝจ้“:็”จ้ผ ๆ ‡ๅณ้”ฎๆ‹–ๅŠจ. - .adjust-brightness-contrast-fade = ADJUST [ๅ…‰ไบฎ/ๅฏนๆฏ”/ๆทก: - .adjust-brightness-0-100 = ADJUST ไบฎๅบฆ 0-100 : - .adjust-contrast-0-100 = ADJUST ๅฏนๆฏ” 0-100 : - .adjust-fade-0-100 = ADJUST ๆทกๅ‡บ 0- 100 : - .adjust-select-raster-image-s-first = ADJUST:ๅ…ˆ้€‰ๆ‹ฉๅ…‰ๆ …ๅ›พๅƒใ€‚ - .adjust-value-must-be-0-100 = ADJUST:ๅ€ผๅฟ…้กปๆ˜ฏ0-100ใ€‚ - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__= __ocs_fmt_1__ๅœจ__ocs_fmt_2__ๅ›พๅƒไธŠใ€‚ .annoallvisible-new-value-0-1 = ANNOALLVISIBLE ๆ–ฐๅ€ผ [0/1]๏ผš .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: ่พ“ๅ…ฅ 0 ๆˆ– 1 ใ€‚ .annoautoscale-new-value-4-4 = ANNOAUTOSCALE ๆ–ฐๅ€ผ [-4..4]๏ผš @@ -4750,7 +4893,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__=__ocs_fmt_1__(็ฎ€ไฝ“ไธญๆ–‡) .underlay-x-fade-contrast-on-clip-mono = ๅ†…้ฅฐ __ocs_fmt_0__: ๆทกๅ‡บ=( ๆทกๅ‡บ=)__ocs_fmt_1__,ๅฏนๆฏ”=__ocs_fmt_2__ๆ‰“ๅผ€=__ocs_fmt_3__,ๅคน=__ocs_fmt_4__ๅ•ๆ•ฐ=__ocs_fmt_5__ .updated-changed-underlay-s = ๅทฒๆ›ดๆ–ฐ __ocs_fmt_0__ ๅ†…็ฝฎ ใ€‚ - .usage-adjust-brightness-contrast-fade-0-100 = ็”จๆณ•๏ผš ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = ็”จๆณ•๏ผš ANNOSCALE e.g. 1:50, 2:1, or a factor .usage-draworder-f-front-b-back-a-above-handle = ็”จๆณ•๏ผš DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = ็”จๆณ•๏ผš HYPERLINK (select objects first) @@ -5978,6 +6120,46 @@ ui = .osnap-settings = ๅฏน่ฑกๆ•ๆ‰่ฎพ็ฝฎ... .snap-override-none-next-pick-only = ๆ•ๆ‰ๆ›ฟไปฃ๏ผšๆ— ๏ผˆไป…ไธ‹ไธ€ๆฌกๆ‹พๅ–๏ผ‰ใ€‚ + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = ๅง‹็ปˆๅฐ†ๆ–‡ๅญ—็ฝฎไบŽๅฐบๅฏธ็•Œ็บฟไน‹้—ด .angular-dimensions = ่ง’ๅบฆๆ ‡ๆณจ @@ -6245,3 +6427,117 @@ graphic-attributes = .reset-angle = ้‡็ฝฎ่ง’ๅบฆ .one-color = ๅ•่‰ฒ .two-colors = ๅŒ่‰ฒ + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = ๆทปๅŠ ๅˆฐๅทฅไฝœ้›† + .remove-from-working-set = ไปŽๅทฅไฝœ้›†ๅˆ ้™ค + .toolbar-objects = ไธชๅฏน่ฑก + .dialog-title = ๅ‚็…ง็ผ–่พ‘ + .dialog-reference = ่ฆ็ผ–่พ‘็š„ๅ‚็…ง + .dialog-chain-hint = ้€‰ๆ‹ฉ็‚นๅค„็š„ๅตŒๅฅ—้“พ๏ผ›ๅ•ๅ‡ปๆ ‡็ญพๅฏๆ›ดๆ”น็บงๅˆซใ€‚ + .dialog-path = ่ทฏๅพ„: + .dialog-working-set = ๅทฅไฝœ้›† + .dialog-all-objects = ๆ‰€ๆœ‰ๅฏน่ฑก + .dialog-pick-objects = ๅœจๅ›พๅฝขไธญ้€‰ๆ‹ฉ + .dialog-attributes = ๅŒๆ—ถ็ผ–่พ‘ๅฑžๆ€งๅฎšไน‰ + .dialog-session = ไผš่ฏ + .dialog-lock = ้”ๅฎšๅทฅไฝœ้›†ไน‹ๅค–็š„ๅฏน่ฑก + .dialog-unique = ไธบๅ†ฒ็ช็š„ๅ›พๅฑ‚ใ€ๆ ทๅผๅ’Œๅ—ๅ็งฐๆŒ‡ๅฎšๅ”ฏไธ€ๅ็งฐ + .dialog-start = ๅผ€ๅง‹็ผ–่พ‘ + .dialog-help = ๆ‰“ๅผ€ๅ—ๅ‚็…งๆˆ–ๅค–้ƒจๅ‚็…งไปฅ่ฟ›่กŒๅœจไฝ็ผ–่พ‘ใ€‚ + .select-reference = ้€‰ๆ‹ฉๅ‚็…ง: + .reference-not-found = ๆ‰พไธๅˆฐๅ‚็…งใ€‚ + .not-valid-reference = ๅฏน่ฑกไธๆ˜ฏๆœ‰ๆ•ˆ็š„ๅ‚็…ง + .one-entity-added = ๅทฒๆทปๅŠ  1 ไธชๅ›พๅ…ƒ + .nesting-level = ้€‰ๆ‹ฉๅตŒๅฅ—็บงๅˆซ [็กฎๅฎš/ไธ‹ไธ€ไธช] <ไธ‹ไธ€ไธช>: + .selection-method = ่พ“ๅ…ฅๅฏน่ฑก้€‰ๆ‹ฉๆ–นๆณ• [ๅ…จ้ƒจ/ๅตŒๅฅ—] <ๅ…จ้ƒจ>: + .select-nested = ้€‰ๆ‹ฉๅตŒๅฅ—ๅฏน่ฑก: + .display-attdefs = ๆ˜พ็คบๅฑžๆ€งๅฎšไน‰ [ๆ˜ฏ/ๅฆ] <ๅฆ>: + .zero-items-selected = ๅทฒ้€‰ๆ‹ฉ 0 ไธช้กน็›ฎ + .one-item-selected = ๅทฒ้€‰ๆ‹ฉ 1 ไธช้กน็›ฎ + .items-selected = ๅทฒ้€‰ๆ‹ฉ __ocs_fmt_0__ ไธช้กน็›ฎ + .refset-option = ่พ“ๅ…ฅ้€‰้กน [ๆทปๅŠ /ๅˆ ้™ค] <ๆทปๅŠ >: + .refclose-option = ่พ“ๅ…ฅ้€‰้กน [ไฟๅญ˜/ๆ”พๅผƒๅ‚็…งไฟฎๆ”น] <ไฟๅญ˜>: + .not-checked-out = ** ้™ค้žไฝฟ็”จ REFEDIT ๅ‘ฝไปค็ผ–่พ‘ๅ‚็…ง๏ผŒๅฆๅˆ™ไธๅ…่ฎธไฝฟ็”จๆญคๅ‘ฝไปค ** + .already-checked-out = ** ไธๅ…่ฎธไฝฟ็”จๆญคๅ‘ฝไปค๏ผŒ__ocs_fmt_0__ ๅทฒๅœจ็ผ–่พ‘ไธญ ** + .transfer-objects = ๅœจ RefEdit ๅทฅไฝœ้›†ๅ’Œๅฎฟไธปๅ›พๅฝขไน‹้—ดไผ ่พ“ๅฏน่ฑก... + .added-to-working-set = ๅทฒๅฐ† __ocs_fmt_0__ ไธชๆทปๅŠ ๅˆฐๅทฅไฝœ้›† + .removed-from-working-set = ๅทฒไปŽๅทฅไฝœ้›†ๅˆ ้™ค __ocs_fmt_0__ ไธช + .one-object-added = ๅทฒๅฐ† 1 ไธชๅฏน่ฑกๆทปๅŠ ๅˆฐ __ocs_fmt_0__ + .objects-added = ๅทฒๅฐ† __ocs_fmt_0__ ไธชๅฏน่ฑกๆทปๅŠ ๅˆฐ __ocs_fmt_1__ + .one-object-removed = ๅทฒไปŽ __ocs_fmt_0__ ๅˆ ้™ค 1 ไธชๅฏน่ฑก + .objects-removed = ๅทฒไปŽ __ocs_fmt_1__ ๅˆ ้™ค __ocs_fmt_0__ ไธชๅฏน่ฑก + .one-xref-instance = ๅทฒๆ›ดๆ–ฐ 1 ไธชๅค–้ƒจๅ‚็…งๅฎžไพ‹ + .xref-instances = ๅทฒๆ›ดๆ–ฐ __ocs_fmt_0__ ไธชๅค–้ƒจๅ‚็…งๅฎžไพ‹ + .one-block-instance = ๅทฒๆ›ดๆ–ฐ 1 ไธชๅ—ๅฎžไพ‹ + .block-instances = ๅทฒๆ›ดๆ–ฐ __ocs_fmt_0__ ไธชๅ—ๅฎžไพ‹ + .redefined-reloaded = ๅทฒ้‡ๅฎšไน‰ๅนถ้‡่ฝฝ __ocs_fmt_0__ใ€‚ + .redefined = ๅทฒ้‡ๅฎšไน‰ __ocs_fmt_0__ใ€‚ + .ok = ็กฎๅฎš + .nested = ๅตŒๅฅ— + +image-adjust = + .title = ๅ›พๅƒ่ฐƒๆ•ด + .preview = ้ข„่งˆ + .before = ่ฐƒๆ•ดๅ‰ + .after = ่ฐƒๆ•ดๅŽ + .no-preview = ๆ— ๆณ•ๅŠ ่ฝฝๅ›พๅƒ + .adjust = ่ฐƒๆ•ด + .brightness = ไบฎๅบฆ + .contrast = ๅฏนๆฏ”ๅบฆ + .fade = ๆทกๅ…ฅๅบฆ + .hint = ๅ•ๅ‡ปๆˆ–ๆ‹–ๅŠจๆป‘ๆก๏ผ›โˆ’ / + ไธบไธ€ๆญฅ๏ผŒ่พ“ๅ…ฅๆก†ๆŽฅๅ— 0โ€“100 ็š„ๆ•ดๆ•ฐใ€‚ + .reset = ๆขๅค้ป˜่ฎคๅ€ผ + .apply = ๅบ”็”จ + .help = ่ฎพ็ฝฎๆ‰€้€‰ๅ…‰ๆ …ๅ›พๅƒ็š„ไบฎๅบฆใ€ๅฏนๆฏ”ๅบฆๅ’Œๆทกๅ…ฅๅบฆใ€‚ + +keytips = + .title = KeyTip + .typed = ๅทฒ้”ฎๅ…ฅ: { $path } + .hint-tabs = ้”ฎๅ…ฅ้€‰้กนๅกๅญ—ๆฏ ยท Esc ๅ…ณ้—ญ + .hint-tab = { $tab } ้€‰้กนๅก ยท Esc ่ฟ”ๅ›žไธŠไธ€็บง + .hint-items = ้”ฎๅ…ฅ้กน็›ฎๅญ—ๆฏ ยท Esc ่ฟ”ๅ›žไธŠไธ€็บง diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 2d89f57eb..bedcb7edc 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -29,6 +29,7 @@ action = .plugins = ๅค–ๆŽ›็จ‹ๅผ ribbon-tab = + .block-editor = ๅœ–ๅกŠ็ทจ่ผฏๅ™จ .draw = ็นช่ฃฝ .parametric = ๅƒๆ•ธๅŒ– .annotate = ่จป่งฃ @@ -98,6 +99,9 @@ start = .no-recent-files = ๆ‚จ้–‹ๅ•Ÿ็š„ๆช”ๆกˆๅฐ‡้กฏ็คบๅœจๆญค่™•ใ€‚ .browser-storage = ็€่ฆฝๅ™จๅ„ฒๅญ˜็ฉบ้–“ .keep-recent-files = ไฟ็•™ๆœ€่ฟ‘ๆช”ๆกˆ + .view-list = ๆธ…ๅ–ฎ + .view-icons = ๅœ–็คบ + .view-preview = ๅคงๅž‹้ ่ฆฝ donation = .title = ๆๆฌพ่ซ‹ๆฑ‚ @@ -166,6 +170,14 @@ modal = .that-object = ่ฉฒ็‰ฉไปถ .those-objects = ้€™ไบ›็‰ฉไปถ + .missing-fonts-downloading = Downloading... + .missing-fonts-download-available = Download available + .missing-fonts-skip = Skip + .missing-fonts-title = Missing fonts + .missing-fonts-description = This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute. + .missing-fonts-source = Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server): + .missing-fonts-source-placeholder = https://server/fonts or a GitHub raw folder + clipboard = .pasted-image = ๅทฒ่ฒผไธŠๅฝฑๅƒ "{ $name }"๏ผš{ $width }ร—{ $height } px .image-unsupported = ็„กๆณ•่งฃ็ขผ่ฒผไธŠ็š„ๅฝฑๅƒ "{ $name }"ใ€‚ @@ -273,11 +285,73 @@ command-copy = .mode = COPY ่ผธๅ…ฅ่ค‡่ฃฝๆจกๅผ้ธ้ … [Single/Multiple] : common = + .compare-prompt = COMPARE ๅƒ…ๆญค่™• __ocs_fmt_0__ (็ถ ่‰ฒ)๏ผŒๅƒ…ๅฆไธ€ๅœ–้ข __ocs_fmt_1__ (็ด…่‰ฒ)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ่ˆ‡ๅœ–้ขๆฏ”่ผƒ + .label-associative-array-created = __ocs_fmt_0__: ๅทฒๅปบ็ซ‹้—œ่ฏ้™ฃๅˆ—ใ€‚ + .assoc-array-array-path = ้™ฃๅˆ— (่ทฏๅพ‘) + .assoc-array-array-polar = ้™ฃๅˆ— (็’ฐๅฝข) + .assoc-array-array-rectangular = ้™ฃๅˆ— (็Ÿฉๅฝข) + .assoc-array-item-spacing = ้ …็›ฎ้–“่ท + .assoc-array-fill-angle = ๅกซๆปฟ่ง’ๅบฆ + .assoc-array-items = ้ …็›ฎ + .assoc-array-level-spacing = ๅฑค้–“่ท + .assoc-array-levels = ๅฑคๆ•ธ + .assoc-array-row-spacing = ๅˆ—้–“่ท + .assoc-array-column-spacing = ๆฌ„้–“่ท + .fbxexport-exported-to = FBXEXPORT:ๅŒฏๅ‡บ็‚บ"__ocs_fmt_0__" + .fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT:ๆญค็นชๅœ–ไธญๆฒ’ๆœ‰3D็ถฒๆ ผ่ณ‡ๆ–™ใ€‚ + .fbxexport-error = FBXEXPORT:__ocs_fmt_0__ ๆ™‚้–“่ปธ: + .export-fbx = { manage.export } FBX + .fbx-files = { common.files } (FBX) + .options-hpassoc = ๆ–ฐๅกซๅ……็ทš่ทŸ้šจๅ…ถ้‚Š็•Œ (HPASSOC) + .options-show-ribbon = ้กฏ็คบๅŠŸ่ƒฝๅ€ (RIBBON / RIBBONCLOSE) + .options-show-menu-bar = ้กฏ็คบๅŠŸ่ƒฝ่กจๅˆ— (MENUBAR) + .options-window-elements = ่ฆ–็ช—ๅ…ƒ็ด  + .menu-viewports = ่ฆ–ๅŸ  + .menu-view-twist = ่ฆ–ๅœ–ๆ‰ญ่ฝ‰ + .menu-tools = ๅทฅๅ…ท + .menu-table-style = ่กจๆ ผๅž‹ๅผ + .menu-run-script = ๅŸท่กŒ่…ณๆœฌ + .menu-revision-cloud = ไฟฎ่จ‚้›ฒๅฝข + .menu-regen-all = ๅ…จ้ƒจ้‡็”Ÿ + .menu-redraw = ้‡็•ซ + .menu-rectangle = ็Ÿฉๅฝข + .menu-quick-dimension = ๅฟซ้€Ÿๆจ™่จป + .menu-publish = ็™ผไฝˆ + .menu-paste-special = ้ธๆ“‡ๆ€ง่ฒผไธŠ + .menu-parametric = ๅƒๆ•ธๅผ + .menu-page-setup-manager = ้ ้ข่จญ็ฝฎ็ฎก็†ๅ“ก + .menu-orbit = ็’ฐ่ฝ‰ + .menu-new-ucs = ๆ–ฐ UCS + .menu-named-views = ๅ…ทๅ่ฆ–ๅœ– + .menu-multiline-text = ๅคš่กŒๆ–‡ๅญ— + .menu-multiline-style = ่ค‡็ทšๅž‹ๅผ + .menu-multiline = ่ค‡็ทš + .menu-multileader-style = ๅคš้‡ๅผ•็ทšๅž‹ๅผ + .menu-match-properties = ่ค‡่ฃฝๆ€ง่ณช + .menu-lengthen = ่ชฟๆ•ด้•ทๅบฆ + .menu-jogged = ่ฝ‰ๆŠ˜ + .menu-id-point = ้ปžๅบงๆจ™ + .menu-explode-text = ๅˆ†่งฃๆ–‡ๅญ— + .menu-edit-spline = ็ทจ่ผฏ้›ฒๅฝข็ทš + .menu-edit-polyline = ็ทจ่ผฏ่šๅˆ็ทš + .menu-edit-hatch = ็ทจ่ผฏๅกซๅ……็ทš + .menu-drawing-limits = ๅœ–้ข็ฏ„ๅœ + .menu-donut = ็’ฐ + .menu-dimension-style = ๆจ™่จปๅž‹ๅผ + .menu-construction-line = ๅปบๆง‹็ทš + .menu-arc-length = ๅผง้•ท + .command-line-opacity = ๅ‘ฝไปคๅˆ—ไธ้€ๆ˜Žๅบฆ + .txtexp-no-text-selected = TXTEXP: ๆœช้ธๆ“‡ๆ–‡ๅญ—ใ€‚ + .angle-override-shown = ่ง’ๅบฆๅ–ไปฃ๏ผš__ocs_fmt_0__ .plot-preview-task = ๅ‡บๅœ–้ ่ฆฝ .link-a-csv-data-source = ้€ฃ็ต CSV ่ณ‡ๆ–™ไพ†ๆบ .csv-files = CSV ๆช”ๆกˆ .browser-cannot-open-typed-path = ็€่ฆฝๅ™จ็„กๆณ•้–‹ๅ•Ÿ่ผธๅ…ฅ็š„่ทฏๅพ‘๏ผ›่ซ‹ๆ”น็‚บ้ธๆ“‡ๆช”ๆกˆใ€‚ .recover-drawing = ๆขๅพฉๅœ–้ข + .requires-0-or-1-or-keyword = ๅช่ƒฝ็‚บ 0 ๆˆ– 1๏ผŒๆˆ–้ธ้ …้—œ้ตๅญ—ใ€‚ + .requires-a-number = ้œ€่ฆ่ผธๅ…ฅๆ•ธๅญ—ใ€‚ .restore-it-as-a-recovered-tab-or-discard-the-copy = ๅฐ‡ๅ…ถๆขๅพฉ็‚บๅทฒๆขๅพฉ็š„ๅˆ†้ ๏ผŒๆˆ–ๆจๆฃ„่ฉฒๅ‰ฏๆœฌใ€‚ .name-has-an-autosave-copy-newer-than-the-last-save = "__ocs_fmt_0__" ๆœ‰ไธ€ไปฝๆฏ”ไธŠๆฌกๅ„ฒๅญ˜ๆ›ดๆ–ฐ็š„่‡ชๅ‹•ๅ„ฒๅญ˜ๅ‰ฏๆœฌใ€‚ .autosave-copy-could-not-be-stored = ็„กๆณ•ๅ„ฒๅญ˜่‡ชๅ‹•ๅ„ฒๅญ˜ๅ‰ฏๆœฌ: __ocs_fmt_0__ @@ -821,6 +895,10 @@ common = .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. .slice-retained-update-failed = SLICE: a retained result could not be updated. .slice-created-results = SLICE: created __ocs_arg_count__ result object(s). + .sectioncull-no-section = SECTIONCULL: ่ซ‹้ธๅ–ๅ‰–้ข็‰ฉไปถๆˆ–้–‹ๅ•Ÿๅณๆ™‚ๅ‰–้ขใ€‚ + .sectioncull-prepare-failed = SECTIONCULL: ็„กๆณ•ๆบ–ๅ‚™ๅ‰ชๅˆ‡ๅพŒ็š„็‰ฉไปถ๏ผ›ๆœชๅšไปปไฝ•่ฎŠๆ›ดใ€‚ + .sectioncull-nothing-outside = SECTIONCULL: ๅ‰–้ขไน‹ๅค–ๆฒ’ๆœ‰ไปปไฝ•็‰ฉไปถใ€‚ + .sectioncull-result = SECTIONCULL: ๅทฒๅˆช้™ค __ocs_arg_erased__ ๅ€‹็‰ฉไปถ๏ผŒไธฆๅœจๅ‰–้ข้‚Š็•Œๅ‰ชๅˆ‡ __ocs_arg_cut__ ๅ€‹ใ€‚ .splinefit-fit-a-spline-through-n-points = SPLINEFIT: ๅŒน้…ไธ€ๆข้€š้Ž__ocs_fmt_0__้ปž็š„็›ธ็‰‡ใ€‚ .splinefit-need-at-least-3-points = SPLINEFIT:่‡ณๅฐ‘้œ€่ฆ3ๅ€‹้ปžใ€‚ .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT:้ธๆ“‡ไธ€ๆขๅคš็ทšไปฅ้ฉๆ‡‰้€š้Ž็š„ๅˆ†็ทšใ€‚ @@ -858,9 +936,12 @@ common = .sent-to-printer-printer = ็™ผ้€ๅˆฐๅฐ่กจๆฉŸ : __ocs_fmt_0__ .set-current = ็ฝฎ็‚บ็›ฎๅ‰ .shade-plot = ่‘—่‰ฒๅˆ—ๅฐ + .shaded-with-edges = ๅธถ้‚Š็ทฃ่‘—่‰ฒ + .shades-of-gray = ็ฐ้šŽ .shadow-flags = ้™ฐๅฝฑๆจ™่ชŒ .single = ๅ–ฎไธ€ .size = ๅคงๅฐ + .sketchy = ่‰ๅœ– .snap-override-label-next-pick-only = ๆŠ“ๅ–่ฆ†่“‹ : __ocs_fmt_0__ (ๅƒ…้ธๆ“‡ไธ‹ไธ€ๆญฅ) ใ€‚ .solid-created = ๅฏฆ็›ธๆ‰€้€ . .source-object-not-found = ๆ‰พไธๅˆฐๆบ็‰ฉไปถ ใ€‚ @@ -1066,7 +1147,6 @@ common = .isoplane-expected-left-top-or-right = ISOPLANE๏ผš่ซ‹่ผธๅ…ฅ Leftใ€Top ๆˆ– Rightใ€‚ .isodraft-expected-on-or-off = ISODRAFT๏ผš่ซ‹่ผธๅ…ฅ On ๆˆ– Offใ€‚ .xdata-selected-entities-are-on-locked-layers = XDATA๏ผš้ธๅ–็š„ๅฏฆ้ซ”ไฝๆ–ผ้Ž–ๅฎš็š„ๅœ–ๅฑคไธŠใ€‚ - .adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade = ADJUST๏ผšๆœช้ธๅ–้ปž้™ฃๅฝฑๅƒๆˆ–ๅฑฌๆ€งไธๆ˜Ž๏ผˆไฝฟ็”จ BRIGHTNESS/CONTRAST/FADE๏ผ‰ใ€‚ .annoautoscale-enter-an-integer-from-4-through-4 = ANNOAUTOSCALE๏ผš่ซ‹่ผธๅ…ฅ -4 ๅˆฐ 4 ไน‹้–“็š„ๆ•ดๆ•ธใ€‚ .usage-datalink-path-to-csv = ็”จๆณ•๏ผšDATALINK <.csvๆช”ๆกˆ่ทฏๅพ‘> .datalinkupdate-no-linked-tables-found = DATALINKUPDATE๏ผšๆ‰พไธๅˆฐ้€ฃ็ต็š„่กจๆ ผใ€‚ @@ -2059,7 +2139,7 @@ common = .fillet-select-first-object-or-polyline-radius-r = FILLET Select first object or [Polyline/Radius] [R=__ocs_fmt_0__]: .fillet-specify-fillet-radius-or-first-point = FILLET Specify fillet radius or first point <__ocs_fmt_0__>: .fillet-specify-second-point-for-radius = FILLET Specify second point for radius: - .file = File + .file = ๆช”ๆกˆ .file-not-found-check-the-saved-path = File not found โ€” check the saved path. .first-row = First row .fit-data-cannot-be-purged-without-a-stored-control-curve = Fit data cannot be purged without a stored control curve. @@ -2368,6 +2448,7 @@ common = .load-linetypes = ่ผ‰ๅ…ฅ็ทšๅž‹ .requires-two-positive-values = ้œ€่ฆๅ…ฉๅ€‹ๆญฃๅ€ผ (x,y)๏ผŒๆˆ–ไธ€ๅ€‹ๅŒๆ™‚็”จๆ–ผๅ…ฉ่€…็š„ๅ€ผใ€‚ .snap-marker-color = ้Ž–้ปžๆจ™่จ˜้ก่‰ฒ + .snap-marker-size = ้Ž–้ปžๆจ™่จ˜ๅคงๅฐ .command-line-text-color = ๅ‘ฝไปคๅˆ—ๆ–‡ๅญ—้ก่‰ฒ .operand = ้‹็ฎ—ๅ…ƒ .linetypes-loaded = ๅทฒ่ผ‰ๅ…ฅ __ocs_fmt_0__ ๅ€‹็ทšๅž‹ใ€‚ @@ -2381,6 +2462,13 @@ common = .threedrotate-specify-base-point-count-objects = 3DROTATE ๆŒ‡ๅฎšๅŸบ้ปž [__ocs_arg_count__ ๅ€‹็‰ฉไปถ]๏ผš .threedrotate-specify-rotation-axis = 3DROTATE ๆŒ‡ๅฎšๆ—‹่ฝ‰่ปธ [X/Y/Z] ๆˆ–่ปธไธŠ็š„็ฌฌไบŒ้ปž ๏ผš .threedrotate-specify-rotation-angle-a = 3DROTATE ๆŒ‡ๅฎšๆ—‹่ฝ‰่ง’ๅบฆ <__ocs_arg_a__>๏ผš + .parametric-coincident = Coincident + .parametric-colinear = Colinear + .parametric-concentric = Concentric + .parametric-symmetric = Symmetric + .parametric-equal = Equal + .parametric-fix = Fix + errors = .save-warning = ๅ„ฒๅญ˜่ญฆๅ‘Š .unable-to-save-drawing = ็„กๆณ•ๅ„ฒๅญ˜ๅœ–ๅฝข @@ -3004,6 +3092,7 @@ draw = .arc-specify-radius-of-arc = ARC ๆŒ‡ๅฎšๅผง็š„ๅŠๅพ‘: .chord-length = ๅผฆ้•ท + modify = .add-vertex = ๆ–ฐๅขž้ ‚้ปž .closed-cross = ้–‰ๅˆๅๅญ— @@ -3049,6 +3138,22 @@ modify = .attedit-select-block-with-attributes = ATTEDIT ้ธๆ“‡ๅธถๅฑฌๆ€ง็š„ๅœ–ๅกŠ๏ผš .annotation-object-scale = ่จป่งฃ็‰ฉไปถๆฏ”ไพ‹ .bedit-select-block-reference-to-edit = BEDIT ้ธๆ“‡่ฆ็ทจ่ผฏ็š„ๅœ–ๅกŠๅผ•็”จ : + .bvstate-option = BVSTATE ่ผธๅ…ฅ้ธ้ … [ๆ–ฐๅปบ/่จญๅฎš/ๆ›ดๅ/ๅˆช้™ค] <่จญๅฎš>: + .bvstate-new-name = BVSTATE ่ผธๅ…ฅๆ–ฐๅฏ่ฆ‹ๆ€ง็‹€ๆ…‹็š„ๅ็จฑ: + .bvstate-set = BVSTATE ่ผธๅ…ฅ่ฆ่จญ็‚บ็›ฎๅ‰็š„ๅฏ่ฆ‹ๆ€ง็‹€ๆ…‹ <__ocs_fmt_0__>: + .bvstate-rename-from = BVSTATE ่ผธๅ…ฅ่ฆๆ›ดๅ็š„ๅฏ่ฆ‹ๆ€ง็‹€ๆ…‹ <__ocs_fmt_0__>: + .bvstate-rename-to = BVSTATE ่ผธๅ…ฅๆ–ฐๅ็จฑ: + .bvstate-delete = BVSTATE ่ผธๅ…ฅ่ฆๅˆช้™ค็š„ๅฏ่ฆ‹ๆ€ง็‹€ๆ…‹: + .bvstate-not-found = ๆ‰พไธๅˆฐๅฏ่ฆ‹ๆ€ง็‹€ๆ…‹ใ€Œ__ocs_fmt_0__ใ€ใ€‚ + .bvstate-exists = ๅฏ่ฆ‹ๆ€ง็‹€ๆ…‹ใ€Œ__ocs_fmt_0__ใ€ๅทฒๅญ˜ๅœจใ€‚ + .bvstate-last = ็„กๆณ•ๅˆช้™คๅ”ฏไธ€็š„ๅฏ่ฆ‹ๆ€ง็‹€ๆ…‹ใ€‚ + .bvstate-needs-bedit = __ocs_fmt_0__: ่ซ‹ๅ…ˆๅœจๅœ–ๅกŠ็ทจ่ผฏๅ™จไธญ้–‹ๅ•Ÿๅœ–ๅกŠ (BEDIT)ใ€‚ + .bvstate-current = ็›ฎๅ‰ๅฏ่ฆ‹ๆ€ง็‹€ๆ…‹: __ocs_fmt_0__ + .bvhide-select = ้ธๅ–่ฆ้šฑ่—็š„็‰ฉไปถ: + .bvshow-select = ้ธๅ–่ฆ้กฏ็คบ็š„็‰ฉไปถ: + .arrayedit-select-array = ARRAYEDIT ้ธๅ–้™ฃๅˆ—: + .arrayedit-options = ARRAYEDIT ่ผธๅ…ฅ้ธ้ … [ไพ†ๆบ/eXit] : + .arrayedit-not-an-array = ARRAYEDIT: ้ธๅ–็š„็‰ฉไปถไธๆ˜ฏ้—œ่ฏๅผ้™ฃๅˆ—ใ€‚ .blend-enter-continuity-tangent-curvature-c = BLEND ่ผธๅ…ฅ้€ฃ็บŒๆ€ง [็›ธๅˆ‡/ๆ›ฒ็އ] <__ocs_arg_c__>๏ผš .blend-select-first-open-curve-continuity-c = BLEND ้ธๆ“‡็ฌฌไธ€ๆข้–‹ๆ”พๆ›ฒ็ทš [Continuity=__ocs_arg_c__]๏ผš .blend-select-second-open-curve-continuity-c = BLEND ้ธๆ“‡็ฌฌไบŒๆข้–‹ๆ”พๆ›ฒ็ทš [Continuity=__ocs_arg_c__]๏ผš @@ -3601,6 +3706,8 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE ๆŒ‡ๅฎšๆ’ๅ…ฅ้ปž๏ผš insert = + .pastespec-specify-insertion-point = PASTESPEC ๆŒ‡ๅฎšๆ’ๅ…ฅ้ปž: + .pastespec-paste-as = PASTESPEC ่ฒผไธŠ็‚บ [Objects/Block/Text]: .pdfui-pdf-scale = PDF scale: __ocs_fmt_0__:1 .pdfui-specify-insertion-point = Specify insertion point: .pdfui-no-pdf-underlays = No PDF underlays found. @@ -3717,6 +3824,13 @@ insert = .xref-attach-hide-details = Hide Details .xref-attach-select-file = Select Reference File .xref-attach-all-files = All Reference Files + .xref-attach-filter-images = All image files + .xref-attach-filter-dwf = DWF files (*.dwfx; *.dwf) + .xref-attach-filter-dgn = MicroStation DGN (*.dgn) + .xref-attach-filter-all-dgn = All DGN Files (*.*) + .xref-attach-filter-pdf = PDF files (*.pdf) + .xref-attach-filter-point-cloud = Point Cloud (*.rcp; *.rcs) + .xref-attach-filter-all = All files (*.*) .xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays. .xref-attach-invalid-input = Invalid input. .xref-insertion-point-x = Insertion point X @@ -3769,6 +3883,7 @@ insert = .display-and-plot-frames = Display and plot frames .display-not-plot-frames = Display but don't plot frames .frames-vary = *Frames vary* + .underlay-osnaps-vary = *ๅƒ่€ƒๅบ•ๅœ–็‰ฉไปถ้Ž–้ปžไธไธ€่‡ด* .regenerating-model = Regenerating model. .requires-int-0-2 = Requires an integer between 0 and 2. .insert-prompt-hint = INSERT __ocs_arg_prompt____ocs_arg_hint__: @@ -3906,6 +4021,18 @@ insert = .xref-attach-dwg = Attach DWG .xref-attach-image = Attach Image .select-images = Select image(s): + .adjust-select-image-or-underlay = ้ธๅ–ๅฝฑๅƒๆˆ–ๅƒ่€ƒๅบ•ๅœ–: + .adjust-select-pdf-underlay = ้ธๅ– PDF ๅƒ่€ƒๅบ•ๅœ–: + .adjust-select-dwf-underlay = ้ธๅ– DWF ๅƒ่€ƒๅบ•ๅœ–: + .adjust-select-dgn-underlay = ้ธๅ– DGN ๅƒ่€ƒๅบ•ๅœ–: + .adjust-mixed-underlays = *้ธๅ–้›†็„กๆ•ˆใ€‚่ซ‹ๅช้ธๅ–็›ธๅŒๆช”ๆกˆ้กžๅž‹็š„ๅƒ่€ƒๅบ•ๅœ–ใ€‚* + .adjust-image-option = ่ผธๅ…ฅๅฝฑๅƒ้ธ้ … [ๅฐๆฏ”ๅบฆ/่ฎŠๆš—/ไบฎๅบฆ] <ไบฎๅบฆ>: + .adjust-underlay-option = ่ผธๅ…ฅ __ocs_arg_kind__ ๅƒ่€ƒๅบ•ๅœ–้ธ้ … [่ฎŠๆš—/ๅฐๆฏ”ๅบฆ/ๅ–ฎ่‰ฒ] <__ocs_arg_option__>: + .adjust-monochrome = ๅ–ฎ่‰ฒ? [ๆ˜ฏ/ๅฆ] <__ocs_arg_value__>: + .adjust-brightness-value = ่ผธๅ…ฅไบฎๅบฆๅ€ผ (0-100) <__ocs_arg_value__>: + .adjust-contrast-value = ่ผธๅ…ฅๅฐๆฏ”ๅบฆๅ€ผ (0-100) <__ocs_arg_value__>: + .adjust-fade-value = ่ผธๅ…ฅ่ฎŠๆš—ๅ€ผ (0-100) <__ocs_arg_value__>: + .adjust-value-integer-0-100 = ๅ€ผๅฟ…้ ˆๆ˜ฏไป‹ๆ–ผ 0 ๅ’Œ 100 ไน‹้–“็š„ๆ•ดๆ•ธใ€‚ .transparency-mode-on = Enter transparency mode [ON/OFF] : .transparency-mode-off = Enter transparency mode [ON/OFF] : .xref-attach-menu = Attach... @@ -4410,7 +4537,27 @@ model = .value-2point = ๅ…ฉ้ปž .cube = ็ซ‹ๆ–น้ซ” + layout = + .mvsetup-select-objects-to-delete = MVSETUP ้ธๆ“‡่ฆๅˆช้™ค็š„็‰ฉไปถ: + .delete-objects-option = ๅˆช้™ค็‰ฉไปถ + .mvsetup-enable-paper-space = MVSETUP ๅ•Ÿ็”จๅœ–็ด™็ฉบ้–“? [No/Yes] : + .mvsetup-enter-scale-factor = MVSETUP ่ผธๅ…ฅๆฏ”ไพ‹ไฟ‚ๆ•ธ <1>: + .mvsetup-enter-paper-width = MVSETUP ่ผธๅ…ฅๅœ–็ด™ๅฏฌๅบฆ: + .mvsetup-enter-paper-height = MVSETUP ่ผธๅ…ฅๅœ–็ด™้ซ˜ๅบฆ: + .mvsetup-enter-option = MVSETUP ่ผธๅ…ฅ้ธ้ … [Create/Delete objects/Scale viewports]: + .mvsetup-enter-viewport-layout = MVSETUP ่ผธๅ…ฅ่ฆ–ๅŸ ้…็ฝฎ [Single/Engineering/Array] : + .mvsetup-specify-first-corner = MVSETUP ๆŒ‡ๅฎš่ฆ–ๅŸ ๅ€ๅŸŸ็š„็ฌฌไธ€ๅ€‹่ง’้ปž : + .mvsetup-specify-opposite-corner = MVSETUP ๆŒ‡ๅฎšๅฐ่ง’้ปž: + .mvsetup-viewports-in-x = MVSETUP ่ผธๅ…ฅ X ๆ–นๅ‘็š„่ฆ–ๅŸ ๆ•ธ <2>: + .mvsetup-viewports-in-y = MVSETUP ่ผธๅ…ฅ Y ๆ–นๅ‘็š„่ฆ–ๅŸ ๆ•ธ <2>: + .mvsetup-distance-in-x = MVSETUP ๆŒ‡ๅฎš X ๆ–นๅ‘็š„่ฆ–ๅŸ ้–“่ท <0>: + .mvsetup-distance-in-y = MVSETUP ๆŒ‡ๅฎš Y ๆ–นๅ‘็š„่ฆ–ๅŸ ้–“่ท <0>: + .mvsetup-select-viewports = MVSETUP ้ธๆ“‡่ฆ็ธฎๆ”พ็š„่ฆ–ๅŸ : + .mvsetup-paper-space-units = MVSETUP ่ผธๅ…ฅๅœ–็ด™็ฉบ้–“ๅ–ฎไฝๆ•ธ <1>: + .mvsetup-model-space-units = MVSETUP ่ผธๅ…ฅๆจกๅž‹็ฉบ้–“ๅ–ฎไฝๆ•ธ <1>: + .mvsetup-viewports-do-not-fit = MVSETUP ่ฆ–ๅŸ ็„กๆณ•ๆ”พๅ…ฅ่ฉฒๅ€ๅŸŸใ€‚ + .scale-viewports-option = ็ธฎๆ”พ่ฆ–ๅŸ  .active = ๆฟ€ๆดป .inactive = ไธๆดปๅ‹• .insert-view = ๆ’ๅ…ฅ่ฆ–ๅœ– @@ -4436,6 +4583,9 @@ layout = .vplayer-f-layer-freeze-t-layer-thaw-enter-exit = VPLAYER F <ๅœ–ๅฑค> = ๅ‡็ต | T <ๅœ–ๅฑค> = ่งฃๅ‡ | Enter = ้€€ๅ‡บ view = + .twist-option = ๆ‰ญ่ฝ‰ + .dview-specify-view-twist-angle = DVIEW ๆŒ‡ๅฎš่ฆ–ๅœ–ๆ‰ญ่ฝ‰่ง’ๅบฆ <__ocs_fmt_0__>: + .dview-enter-option-twist-undo = DVIEW ่ผธๅ…ฅ้ธ้ … [TWist/Undo]: .color-scheme-set-to-system-currently-name = ้…่‰ฒๆ–นๆกˆๅทฒ่จญ็‚บใ€Œ็ณป็ตฑใ€๏ผˆ็›ฎๅ‰๏ผš__ocs_fmt_0__๏ผ‰ใ€‚ .ss-create-sheet-set = Create Sheet Set .ss-requires-integer-between = ้œ€่ฆไป‹ๆ–ผ __ocs_fmt_0__ ๅ’Œ __ocs_fmt_1__ ไน‹้–“็š„ๆ•ดๆ•ธใ€‚ @@ -4630,13 +4780,6 @@ view = .x-no-xdata = __ocs_fmt_0__:็„กxdata. .value-3d-orbit = ไธ‰็ถญๅ‹•ๆ…‹่ง€ๅฏŸ .value-3d-orbit-drag-with-right-mouse-button = ไธ‰็ถญ่ปŒ้“:็”จๆป‘้ผ ๅณ้ตๆ‹–ๅ‹•. - .adjust-brightness-contrast-fade = ADJUST [ๅ…‰ไบฎ/ๅฐๆฏ”/ๆทก: - .adjust-brightness-0-100 = ADJUST ไบฎๅบฆ 0-100 : - .adjust-contrast-0-100 = ADJUST ๅฐๆฏ” 0-100 : - .adjust-fade-0-100 = ADJUST ๆทกๅ‡บ 0- 100 : - .adjust-select-raster-image-s-first = ADJUST:ๅ…ˆ้ธๆ“‡ๅ…‰ๆŸตๅœ–ๅƒใ€‚ - .adjust-value-must-be-0-100 = ADJUST:ๅ€ผๅฟ…้ ˆๆ˜ฏ0-100ใ€‚ - .adjust-action-v-on-changed-image-s = ADJUST: __ocs_fmt_0__= __ocs_fmt_1__ๅœจ__ocs_fmt_2__ๅœ–ๅƒไธŠใ€‚ .annoallvisible-new-value-0-1 = ANNOALLVISIBLE ๆ–ฐๅ€ผ [0/1]๏ผš .annoallvisible-enter-0-or-1 = ANNOALLVISIBLE: ่ผธๅ…ฅ 0 ๆˆ– 1 ใ€‚ .annoautoscale-new-value-4-4 = ANNOAUTOSCALE ๆ–ฐๅ€ผ [-4..4]๏ผš @@ -4762,7 +4905,6 @@ view = .userr-n-val = USERR__ocs_fmt_0__=__ocs_fmt_1__(็ฐก้ซ”ไธญๆ–‡) .underlay-x-fade-contrast-on-clip-mono = ๅ…ง้ฃพ __ocs_fmt_0__: ๆทกๅ‡บ=( ๆทกๅ‡บ=)__ocs_fmt_1__,ๅฐๆฏ”=__ocs_fmt_2__้–‹ๅ•Ÿ=__ocs_fmt_3__,ๅคพ=__ocs_fmt_4__ๅ–ฎๆ•ธ=__ocs_fmt_5__ .updated-changed-underlay-s = ๅทฒๆ›ดๆ–ฐ __ocs_fmt_0__ ๅ…ง็ฝฎ ใ€‚ - .usage-adjust-brightness-contrast-fade-0-100 = ็”จๆณ•๏ผš ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> .usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor = ็”จๆณ•๏ผš ANNOSCALE e.g. 1:50, 2:1, or a factor .usage-draworder-f-front-b-back-a-above-handle = ็”จๆณ•๏ผš DRAWORDER F|FRONT | B|BACK | A|ABOVE | U|UNDER .usage-hyperlink-url-select-objects-first = ็”จๆณ•๏ผš HYPERLINK (select objects first) @@ -5990,6 +6132,46 @@ ui = .osnap-settings = ็‰ฉไปถ้Ž–้ปž่จญๅฎš... .snap-override-none-next-pick-only = ้Ž–้ปžๅ–ไปฃ๏ผš็„ก๏ผˆๅƒ…ไธ‹ไธ€ๆฌก้ปž้ธ๏ผ‰ใ€‚ + .window-gpu-backend = GPU (backend) + .window-click-add-alias-type-the-alias-and-command-accep = Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row. + .window-number-of-aliases = Number of aliases + .window-are-you-sure-you-want-to-reset-you-will-lose-all = Are you sure you want to reset? You will lose all your current aliases! + .window-alias-already-used-for-command = Alias already used for command: __ocs_fmt_0__ โ†’ __ocs_fmt_1__ + .window-browser = Browser + .window-origin-planes = Origin planes + .window-no-sketch-open = No sketch open + .window-bodies = Bodies (__ocs_fmt_0__) + .window-no-solids-yet = No solids yet + .window-saved-immediately-changes-take-effect-the-next-t = Saved immediately. Changes take effect the next time Open CAD Studio starts. + .window-this-session = This session + .window-driver = Driver + .window-backend = Backend + .window-selected-by = Selected by + .window-next-launch = Next launch + .window-graphics-backend = Graphics backend + .window-use-the-compatibility-renderer-for-gpus-without = Use the compatibility renderer (for GPUs without shader storage buffers) + .window-prefer-opengl-on-older-gpus-automatic-backend-on = Prefer OpenGL on older GPUs (automatic backend only) + .window-if-a-pinned-backend-fails-to-start-the-next-laun = If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page. + .window-paused = Paused + .window-enable-spacemouse = Enable SpaceMouse + .window-navigation = Navigation + .window-reverse-pan-direction = Reverse pan direction + .window-pan-speed = Pan speed + .window-resume-spacemouse = Resume SpaceMouse + .window-configure-3d-navigation-buttons-and-radial-menus = Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here. + .window-connection-details = Connection details + .window-1-a-notch = __ocs_fmt_0__% a notch + .window-the-slider-covers-the-system-variable-s-to-the-f = The slider covers the system variable's __ocs_fmt_0__ to __ocs_fmt_1__; the field takes up to __ocs_fmt_2__ for a faster wheel. + .window-bundled-with-open-cad-studio = Bundled with Open CAD Studio + .window-how-the-reference-manager-works-on-web = How the Reference Manager works on web + .window-this-panel-lists-the-drawing-s-references-read-o = This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation. + .window-attach-refresh-and-change-path-stay-disabled-her = Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first. + .window-hit-something-broken-or-missing-report-it-on-the = Hit something broken or missing? Report it on the GitHub issues page so it can be fixed. + .window-report-an-issue = Report an issue + .window-reference-manager-web = Reference Manager โ€” Web + .window-externalreferences-on-the-web-build = EXTERNALREFERENCES on the web build + .window-embed-image-in-drawing = Embed Image (in drawing) + dimstyle = .keep-text-between-extension-lines = ๅง‹็ต‚ๅฐ‡ๆ–‡ๅญ—็ฝฎๆ–ผๅฐบๅฏธ็•Œ็ทšไน‹้–“ .angular-dimensions = ่ง’ๅบฆๆจ™่จป @@ -6257,3 +6439,117 @@ graphic-attributes = .reset-angle = ้‡่จญ่ง’ๅบฆ .one-color = ๅ–ฎ่‰ฒ .two-colors = ้›™่‰ฒ + +property-patch = + .patch-01 = 3DxWare unavailable + .patch-02 = Follow context + .patch-03 = Pan and zoom on paper sheets; full navigation in model views. + .patch-04 = Open 3Dconnexion settingsโ€ฆ + .patch-05 = Open 3Dconnexion settings... + .patch-06 = Decimal Separator + .patch-07 = Operational Symbols + +view-background = + .title = Background + .none = None + .solid = Solid + .gradient = Gradient + .image = Image + .preview = Preview + .solid-color = Solid color + .color = Color + .select = Selectโ€ฆ + .three-colors = Three colors + .rotation = Rotation + .top = Top color + .middle = Middle color + .bottom = Bottom color + .browse = Browseโ€ฆ + .position = Position + .center = Center + .stretch = Stretch + .tile = Tile + .scale = Scale + .aspect = Keep aspect ratio + .adjust = Adjust + .reset = Reset + .offset = Offset + .pixels = px + .no-image = No image selected + .canvas = Drawing area color + .info = Info + .none-info = The background does not change when the view is restored; the drawing area color is used. + .apply = Apply + .help = Sets the background a named view shows when it is restored. + .image-files = Image files + .select-image = Select Background Image + +ref-edit = + .add-to-working-set = ๅŠ ๅ…ฅๅทฅไฝœ้›† + .remove-from-working-set = ๅพžๅทฅไฝœ้›†็งป้™ค + .toolbar-objects = ๅ€‹็‰ฉไปถ + .dialog-title = ๅƒ่€ƒ็ทจ่ผฏ + .dialog-reference = ่ฆ็ทจ่ผฏ็š„ๅƒ่€ƒ + .dialog-chain-hint = ้ธๅ–้ปž่™•็š„ๅทข็‹€้ˆ๏ผ›ๆŒ‰ไธ€ไธ‹ๆจ™็ฑคๅฏ่ฎŠๆ›ดๅฑค็ดšใ€‚ + .dialog-path = ่ทฏๅพ‘: + .dialog-working-set = ๅทฅไฝœ้›† + .dialog-all-objects = ๆ‰€ๆœ‰็‰ฉไปถ + .dialog-pick-objects = ๅœจๅœ–้ขไธญ้ธๅ– + .dialog-attributes = ๅŒๆ™‚็ทจ่ผฏๅฑฌๆ€งๅฎš็พฉ + .dialog-session = ๅทฅไฝœ้šŽๆฎต + .dialog-lock = ้Ž–ๅฎšๅทฅไฝœ้›†ไปฅๅค–็š„็‰ฉไปถ + .dialog-unique = ็‚บ่ก็ช็š„ๅœ–ๅฑคใ€ๅž‹ๅผๅ’Œๅœ–ๅกŠๅ็จฑๆŒ‡ๅฎšๅ”ฏไธ€ๅ็จฑ + .dialog-start = ้–‹ๅง‹็ทจ่ผฏ + .dialog-help = ้–‹ๅ•Ÿๅœ–ๅกŠๅƒ่€ƒๆˆ–ๅค–้ƒจๅƒ่€ƒไปฅ้€ฒ่กŒๅฐฑๅœฐ็ทจ่ผฏใ€‚ + .select-reference = ้ธๅ–ๅƒ่€ƒ: + .reference-not-found = ๆ‰พไธๅˆฐๅƒ่€ƒใ€‚ + .not-valid-reference = ็‰ฉไปถไธๆ˜ฏๆœ‰ๆ•ˆ็š„ๅƒ่€ƒ + .one-entity-added = ๅทฒๅŠ ๅ…ฅ 1 ๅ€‹็‰ฉไปถ + .nesting-level = ้ธๅ–ๅทข็‹€ๅฑค็ดš [็ขบๅฎš/ไธ‹ไธ€ๅ€‹] <ไธ‹ไธ€ๅ€‹>: + .selection-method = ่ผธๅ…ฅ็‰ฉไปถ้ธๅ–ๆ–นๆณ• [ๅ…จ้ƒจ/ๅทข็‹€] <ๅ…จ้ƒจ>: + .select-nested = ้ธๅ–ๅทข็‹€็‰ฉไปถ: + .display-attdefs = ้กฏ็คบๅฑฌๆ€งๅฎš็พฉ [ๆ˜ฏ/ๅฆ] <ๅฆ>: + .zero-items-selected = ๅทฒ้ธๅ– 0 ๅ€‹้ …็›ฎ + .one-item-selected = ๅทฒ้ธๅ– 1 ๅ€‹้ …็›ฎ + .items-selected = ๅทฒ้ธๅ– __ocs_fmt_0__ ๅ€‹้ …็›ฎ + .refset-option = ่ผธๅ…ฅ้ธ้ … [ๅŠ ๅ…ฅ/็งป้™ค] <ๅŠ ๅ…ฅ>: + .refclose-option = ่ผธๅ…ฅ้ธ้ … [ๅ„ฒๅญ˜/ๆ”พๆฃ„ๅƒ่€ƒ่ฎŠๆ›ด] <ๅ„ฒๅญ˜>: + .not-checked-out = ** ้™ค้žไปฅ REFEDIT ๆŒ‡ไปค็ทจ่ผฏๅƒ่€ƒ๏ผŒๅฆๅ‰‡ไธๅ…่จฑไฝฟ็”จๆญคๆŒ‡ไปค ** + .already-checked-out = ** ไธๅ…่จฑไฝฟ็”จๆญคๆŒ‡ไปค๏ผŒ__ocs_fmt_0__ ๅทฒๅœจ็ทจ่ผฏไธญ ** + .transfer-objects = ๅœจ RefEdit ๅทฅไฝœ้›†่ˆ‡ไธปๅœ–้ขไน‹้–“ๅ‚ณ่ผธ็‰ฉไปถ... + .added-to-working-set = ๅทฒๅฐ‡ __ocs_fmt_0__ ๅ€‹ๅŠ ๅ…ฅๅทฅไฝœ้›† + .removed-from-working-set = ๅทฒๅพžๅทฅไฝœ้›†็งป้™ค __ocs_fmt_0__ ๅ€‹ + .one-object-added = ๅทฒๅฐ‡ 1 ๅ€‹็‰ฉไปถๅŠ ๅ…ฅ __ocs_fmt_0__ + .objects-added = ๅทฒๅฐ‡ __ocs_fmt_0__ ๅ€‹็‰ฉไปถๅŠ ๅ…ฅ __ocs_fmt_1__ + .one-object-removed = ๅทฒๅพž __ocs_fmt_0__ ็งป้™ค 1 ๅ€‹็‰ฉไปถ + .objects-removed = ๅทฒๅพž __ocs_fmt_1__ ็งป้™ค __ocs_fmt_0__ ๅ€‹็‰ฉไปถ + .one-xref-instance = ๅทฒๆ›ดๆ–ฐ 1 ๅ€‹ๅค–้ƒจๅƒ่€ƒๅฏฆ้ซ” + .xref-instances = ๅทฒๆ›ดๆ–ฐ __ocs_fmt_0__ ๅ€‹ๅค–้ƒจๅƒ่€ƒๅฏฆ้ซ” + .one-block-instance = ๅทฒๆ›ดๆ–ฐ 1 ๅ€‹ๅœ–ๅกŠๅฏฆ้ซ” + .block-instances = ๅทฒๆ›ดๆ–ฐ __ocs_fmt_0__ ๅ€‹ๅœ–ๅกŠๅฏฆ้ซ” + .redefined-reloaded = ๅทฒ้‡ๆ–ฐๅฎš็พฉไธฆ้‡ๆ–ฐ่ผ‰ๅ…ฅ __ocs_fmt_0__ใ€‚ + .redefined = ๅทฒ้‡ๆ–ฐๅฎš็พฉ __ocs_fmt_0__ใ€‚ + .ok = ็ขบๅฎš + .nested = ๅทข็‹€ + +image-adjust = + .title = ๅฝฑๅƒ่ชฟๆ•ด + .preview = ้ ่ฆฝ + .before = ่ชฟๆ•ดๅ‰ + .after = ่ชฟๆ•ดๅพŒ + .no-preview = ็„กๆณ•่ผ‰ๅ…ฅๅฝฑๅƒ + .adjust = ่ชฟๆ•ด + .brightness = ไบฎๅบฆ + .contrast = ๅฐๆฏ” + .fade = ๆทกๅ…ฅๅบฆ + .hint = ๆŒ‰ไธ€ไธ‹ๆˆ–ๆ‹–ๆ›ณๆป‘ๆกฟ๏ผ›โˆ’ / + ็‚บไธ€ๆญฅ๏ผŒๆ–นๅกŠๆŽฅๅ— 0โ€“100 ็š„ๆ•ดๆ•ธใ€‚ + .reset = ้‚„ๅŽŸ้ ่จญๅ€ผ + .apply = ๅฅ—็”จ + .help = ่จญๅฎš้ธๅ–ไน‹้ปž้™ฃๅฝฑๅƒ็š„ไบฎๅบฆใ€ๅฐๆฏ”่ˆ‡ๆทกๅ…ฅๅบฆใ€‚ + +keytips = + .title = KeyTip + .typed = ๅทฒ่ผธๅ…ฅ: { $path } + .hint-tabs = ่ผธๅ…ฅ็ดขๅผ•ๆจ™็ฑคๅญ—ๆฏ ยท Esc ้—œ้–‰ + .hint-tab = { $tab } ็ดขๅผ•ๆจ™็ฑค ยท Esc ่ฟ”ๅ›žไธŠไธ€ๅฑค + .hint-items = ่ผธๅ…ฅ้ …็›ฎๅญ—ๆฏ ยท Esc ่ฟ”ๅ›žไธŠไธ€ๅฑค diff --git a/plugins/opencad-python/Cargo.lock b/plugins/opencad-python/Cargo.lock index d3e57e7f6..fb7176299 100644 --- a/plugins/opencad-python/Cargo.lock +++ b/plugins/opencad-python/Cargo.lock @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.6.0" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=3ec70a2#3ec70a2a0134f60a9f3036f1974fea86ad96e439" +source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=e83247d#e83247df8d0b9c24f6eedb75fb9a4c279f217ec8" dependencies = [ "anyhow", "bitflags", diff --git a/src/app/alias.rs b/src/app/alias.rs index 7d11ba72e..acc50ee5e 100644 --- a/src/app/alias.rs +++ b/src/app/alias.rs @@ -80,7 +80,7 @@ const WEB_ALIAS_KEY: &str = "opencadstudio.aliases"; /// migrated forward (see `introduced_at` / `migrate_aliases`). Version 1 shipped /// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases; version 3 /// introduced HB (HATCHTOBACK); version 4 introduced ER (EXTERNALREFERENCES). -const DEFAULT_ALIASES_VERSION: u32 = 6; +const DEFAULT_ALIASES_VERSION: u32 = 9; #[cfg(target_arch = "wasm32")] const WEB_ALIAS_VERSION_KEY: &str = "opencadstudio.aliases.version"; @@ -101,6 +101,9 @@ fn introduced_at(version: u32) -> &'static [(&'static str, &'static str)] { 4 => &[("ER", "EXTERNALREFERENCES")], 5 => &[("-ATT", "-ATTDEF")], 6 => &[("VV", "QLAYER")], + 7 => &[("DV", "DVIEW")], + 8 => &[("-XR", "-XREF"), ("XC", "XCLIP"), ("ICL", "IMAGECLIP")], + 9 => &[("IAD", "IMAGEADJUST")], _ => &[], } } diff --git a/src/app/automation.rs b/src/app/automation.rs index 45d8bac5c..a6e10ddba 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -700,6 +700,12 @@ impl OpenCADStudio { }; match req["op"].as_str().unwrap_or("") { "new" => { + if self.tabs[self.active_tab].block_test.is_some() { + return err("Close Test Block before replacing the drawing."); + } + if self.tabs[self.active_tab].block_edit_history.is_some() { + return err("Close the block editor before replacing the drawing."); + } if let Some(template) = req["template"].as_str() { let purged = match self.apply_template(template) { Ok(purged) => purged, @@ -723,6 +729,12 @@ impl OpenCADStudio { } #[cfg(not(target_arch = "wasm32"))] "open" => { + if self.tabs[self.active_tab].block_test.is_some() { + return err("Close Test Block before replacing the drawing."); + } + if self.tabs[self.active_tab].block_edit_history.is_some() { + return err("Close the block editor before replacing the drawing."); + } let Some(path) = req["path"].as_str() else { return err("open: missing \"path\""); }; @@ -786,6 +798,7 @@ impl OpenCADStudio { if self.command_line.error_revision != error_revision { return err(self.command_line.last_error.clone().unwrap_or_default()); } + let i = self.active_tab; let after = self.tabs[i].scene.document.entities().count(); // A command can finish with an interactive surface still open: // the in-place text editor (the `TEXT` content step), the MTEXT @@ -990,6 +1003,9 @@ impl OpenCADStudio { json!({ "ok": true, "selected": self.tabs[i].scene.selected_entities().len() }) } "save" => { + if self.tabs[self.active_tab].block_test.is_some() { + return err("Test Block is temporary and cannot be saved."); + } let i = self.active_tab; let path = req["path"] .as_str() @@ -1527,6 +1543,11 @@ impl OpenCADStudio { #[cfg(not(target_arch = "wasm32"))] pub(super) fn save_verified_request(&mut self, req: &Value) -> Result { let i = self.active_tab; + if self.tabs[i].block_test.is_some() { + return Err( + json!({"ok":false,"code":"temporary_document","error":"Test Block is temporary and cannot be saved."}), + ); + } let Some(raw_path) = req["path"].as_str() else { return Err( json!({"ok":false,"status":"failed","code":"path_required","error":"save_verified requires an explicit absolute path"}), diff --git a/src/app/block_editor.rs b/src/app/block_editor.rs new file mode 100644 index 000000000..065f18555 --- /dev/null +++ b/src/app/block_editor.rs @@ -0,0 +1,703 @@ +use super::document::{ + DeltaSnapshot, HistorySnapshot, HistoryState, ParametricConstraintsEntryDelta, + StructureSnapshot, +}; +use super::{Message, ModalKind, OpenCADStudio}; +use codec::{CadDocument, EntityType}; +use iced::Task; +use rustc_hash::{FxHashMap, FxHashSet}; + +/// A single save checkpoint. Entity payloads remain shared through the +/// document's Arc storage; only modified entities detach during editing. +pub(super) struct BlockEditCheckpoint { + document: CadDocument, + constraints: Vec, + parameters: crate::scene::named_parameters::ParameterTable, + dirty: bool, + active_layer: String, +} + +pub(super) struct BlockEditHistory { + pub main: HistoryState, + checkpoint: BlockEditCheckpoint, + return_isolation: crate::scene::ObjectIsolationState, +} + +#[derive(Clone, Debug)] +pub(super) enum BlockEditContinuation { + Close, + Switch(String), + Create(String), + Layout(String), + Tab(u64), + Quit, +} + +fn same_constraints( + before: &crate::scene::parametric_constraints::ParametricConstraintSet, + after: &crate::scene::parametric_constraints::ParametricConstraintSet, +) -> bool { + before.constraints == after.constraints + && before.local_parameters == after.local_parameters + && before.retained_standard_groups == after.retained_standard_groups + && before.dimensions == after.dimensions +} + +fn normalize_allocation(document: &mut CadDocument, next: u64) { + if document.next_handle() < next { + document.header.handle_seed = next - 1; + document.allocate_handle(); + } + document.header.handle_seed = next; +} + +impl OpenCADStudio { + pub(super) fn reconcile_block_edit_membership(&mut self, i: usize) { + let Some(session) = self.tabs[i].active_block_edit_session() else { + return; + }; + let name = session.block_name.clone(); + let document = &mut self.tabs[i].scene.document; + let Some(record) = document.block_records.get(&name) else { + return; + }; + let members: Vec<_> = record + .entity_handles + .iter() + .copied() + .filter(|handle| document.get_entity(*handle).is_some()) + .collect(); + if members != record.entity_handles { + document + .block_records + .get_mut(&name) + .unwrap() + .entity_handles = members; + } + } + + fn block_edit_checkpoint(&self, i: usize) -> BlockEditCheckpoint { + let tab = &self.tabs[i]; + BlockEditCheckpoint { + document: tab.scene.document.clone(), + constraints: tab.scene.parametric_constraints.clone(), + parameters: tab.scene.named_parameters.clone(), + dirty: tab.dirty, + active_layer: tab.active_layer.clone(), + } + } + + pub(super) fn begin_block_edit_history(&mut self, i: usize) { + self.finish_pending_history(i); + let checkpoint = self.block_edit_checkpoint(i); + let main = std::mem::take(&mut self.tabs[i].history); + let return_isolation = self.tabs[i].scene.object_isolation.clone(); + self.tabs[i].block_edit_history = Some(BlockEditHistory { + main, + checkpoint, + return_isolation, + }); + } + + fn block_edit_delta(&mut self, i: usize, for_save: bool) -> Option { + self.reconcile_block_edit_membership(i); + self.finish_pending_history(i); + let current_structure = self.tabs[i].scene.document.snapshot_structure(); + let tab = &self.tabs[i]; + let history = tab.block_edit_history.as_ref()?; + let checkpoint = &history.checkpoint; + let session = tab.active_block_edit_session()?; + let before_doc = if for_save { + session + .unpersisted_pre_document + .as_ref() + .unwrap_or(&checkpoint.document) + } else { + &checkpoint.document + }; + let before: FxHashMap<_, _> = before_doc + .entities() + .map(|e| { + let h = e.common().handle; + (h, before_doc.get_entity_arc(h).unwrap()) + }) + .collect(); + let after: FxHashMap<_, _> = tab + .scene + .document + .entities() + .map(|e| { + let h = e.common().handle; + (h, tab.scene.document.get_entity_arc(h).unwrap()) + }) + .collect(); + let mut handles: FxHashSet<_> = before.keys().copied().collect(); + handles.extend(after.keys().copied()); + let mut entities: Vec<_> = handles + .into_iter() + .filter_map(|h| { + let a = before.get(&h); + let b = after.get(&h); + (a != b).then(|| (h, a.cloned(), b.cloned())) + }) + .collect(); + entities.sort_by_key(|(handle, _, _)| handle.value()); + let mut before_structure = before_doc.clone(); + before_structure = before_structure.snapshot_structure(); + let mut after_structure = current_structure; + // The editor temporarily uses Model rendering. Drawing history must + // retain the layout to which the editor returns. + let model = session.return_layout == "Model"; + before_structure.header.show_model_space = model; + after_structure.header.show_model_space = model; + if !model { + crate::io::set_saved_active_layout(&mut before_structure, &session.return_layout); + crate::io::set_saved_active_layout(&mut after_structure, &session.return_layout); + } + let next = before_structure + .next_handle() + .max(after_structure.next_handle()) + .max(before_structure.header.handle_seed) + .max(after_structure.header.handle_seed); + // Reserving handles is not a content edit. Undoing every edit should + // return a session to clean without reusing allocated handles. + normalize_allocation(&mut before_structure, next); + normalize_allocation(&mut after_structure, next); + let structure = (before_structure != after_structure) + .then_some(StructureSnapshot::Full(before_structure)); + let mut scopes: Vec<_> = checkpoint.constraints.iter().map(|c| c.scope).collect(); + for set in &tab.scene.parametric_constraints { + if !scopes.contains(&set.scope) { + scopes.push(set.scope); + } + } + let parametric_constraints: Vec<_> = scopes + .into_iter() + .filter_map(|scope| { + let empty = + || crate::scene::parametric_constraints::ParametricConstraintSet::new(scope); + let a = checkpoint + .constraints + .iter() + .find(|c| c.scope == scope) + .cloned() + .unwrap_or_else(empty); + let b = tab + .scene + .parametric_constraints + .iter() + .find(|c| c.scope == scope) + .cloned() + .unwrap_or_else(empty); + (!same_constraints(&a, &b)).then_some(ParametricConstraintsEntryDelta { + scope, + before: a, + after: b, + }) + }) + .collect(); + let named_parameters = (checkpoint.parameters != tab.scene.named_parameters).then(|| { + ( + checkpoint.parameters.clone(), + tab.scene.named_parameters.clone(), + ) + }); + let active_layer = (checkpoint.active_layer != tab.active_layer) + .then(|| (checkpoint.active_layer.clone(), tab.active_layer.clone())); + if entities.is_empty() + && structure.is_none() + && parametric_constraints.is_empty() + && named_parameters.is_none() + && active_layer.is_none() + { + return None; + } + Some(DeltaSnapshot { + entities, + current_layout_before: session.return_layout.clone(), + current_layout_after: session.return_layout.clone(), + selected_before: Vec::new(), + selected_after: Vec::new(), + dirty_before: checkpoint.dirty, + dirty_after: true, + active_layer, + structure, + parametric_constraints, + named_parameters, + label: format!("Edit block {}", session.block_name), + }) + } + + pub(super) fn block_edit_dirty(&mut self, i: usize) -> bool { + self.block_edit_delta(i, false).is_some() + } + + pub(super) fn validate_block_save_as_name( + &self, + i: usize, + raw: &str, + ) -> Result { + let name = + crate::scene::block_service::validate_block_name(raw).map_err(|e| e.to_string())?; + if self.tabs[i] + .scene + .document + .block_records + .get(&name) + .is_some() + { + return Err( + crate::scene::block_service::BlockServiceError::NameExists(name).to_string(), + ); + } + Ok(name) + } + + pub(super) fn open_bedit_dialog(&mut self, i: usize) { + if self.tabs[i].refedit_session.is_some() { + self.command_line.push_error( + crate::t!("BEDIT: finish the active REFEDIT (REFCLOSE) first.").as_ref(), + ); + return; + } + + let existing_names = + crate::scene::block_service::bedit_candidate_block_names(&self.tabs[i].scene.document); + + let initial_name = if let Some(session) = self.tabs[i].active_block_edit_session() { + session.block_name.clone() + } else if let Some(first) = existing_names.first() { + first.clone() + } else { + String::new() + }; + + let preview = if !initial_name.is_empty() { + self.tabs[i].scene.block_preview_wires(&initial_name) + } else { + Vec::new() + }; + + let description = if !initial_name.is_empty() { + self.tabs[i] + .scene + .document + .block_records + .get(&initial_name) + .map(|b| b.description.clone()) + .unwrap_or_default() + } else { + String::new() + }; + + let error = if initial_name.is_empty() { + None + } else { + crate::scene::block_service::validate_block_name(&initial_name) + .err() + .map(|e| e.to_string()) + }; + + self.block_edit_picker = Some(crate::ui::window::block_edit_picker::State { + document_id: self.tabs[i].id, + name: initial_name, + existing_names, + preview, + description, + error, + }); + self.active_modal = Some(ModalKind::BlockEditPicker); + self.reset_modal_geometry(); + } + + pub(super) fn apply_bedit_dialog(&mut self) -> Task { + let Some(state) = self.block_edit_picker.as_ref() else { + return Task::none(); + }; + let id = state.document_id; + let name = state.name.trim().to_string(); + if name.is_empty() || state.error.is_some() { + return Task::none(); + } + let Some(i) = self.tabs.iter().position(|tab| tab.id == id) else { + return Task::none(); + }; + self.active_tab = i; + self.block_edit_picker = None; + self.active_modal = None; + self.reset_modal_geometry(); + + if let Some(record) = self.tabs[i].scene.document.block_records.get(&name) { + let real_name = record.name.clone(); + self.dispatch_command(&format!("BEDIT_OPEN:{real_name}")) + } else { + self.dispatch_command(&format!("BEDIT_NEW:{name}")) + } + } + + pub(super) fn open_block_save_as_dialog(&mut self, i: usize) { + let Some(session) = self.tabs[i].active_block_edit_session() else { + self.command_line + .push_error(crate::t!("No block editor is open.").as_ref()); + return; + }; + let source = session.br_handle; + let name = session.block_name.clone(); + let mut existing_names = self.tabs[i].scene.custom_block_names(); + existing_names.sort_by_key(|name| name.to_ascii_lowercase()); + self.block_save_as = Some(crate::ui::window::block_save_as::State { + document_id: self.tabs[i].id, + source, + name: String::new(), + existing_names, + preview: self.tabs[i].scene.block_preview_wires(&name), + error: self.validate_block_save_as_name(i, "").err(), + }); + self.active_modal = Some(ModalKind::BlockSaveAs); + self.reset_modal_geometry(); + } + + pub(super) fn apply_block_save_as_dialog(&mut self) -> Task { + let Some(state) = self.block_save_as.as_ref() else { + return Task::none(); + }; + let id = state.document_id; + let source = state.source; + let name = state.name.clone(); + let Some(i) = self.tabs.iter().position(|tab| tab.id == id) else { + return Task::none(); + }; + if self.tabs[i] + .active_block_edit_session() + .is_none_or(|session| session.br_handle != source) + { + if let Some(state) = self.block_save_as.as_mut() { + state.error = Some( + crate::t!("The block editor has changed; reopen Save Block As.").into_owned(), + ); + } + return Task::none(); + } + self.active_tab = i; + if self.save_block_edit_as(i, &name) { + self.block_save_as = None; + self.active_modal = None; + self.reset_modal_geometry(); + } else if let Some(state) = self.block_save_as.as_mut() { + state.error = self.command_line.last_error.clone(); + } + Task::none() + } + + pub(super) fn save_block_edit_as(&mut self, i: usize, raw: &str) -> bool { + let Some(session) = self.tabs[i].active_block_edit_session() else { + self.command_line + .push_error(crate::t!("No block editor is open.").as_ref()); + return false; + }; + let source_name = session.block_name.clone(); + let source_handle = session.br_handle; + let name = match self.validate_block_save_as_name(i, raw) { + Ok(name) => name, + Err(error) => { + self.command_line.push_error(&error); + return false; + } + }; + self.finish_pending_history(i); + self.reconcile_block_edit_membership(i); + let Some(history) = self.tabs[i].block_edit_history.as_ref() else { + return false; + }; + let copied = match super::block_save_as::copy_definition( + &self.tabs[i].scene.document, + history.checkpoint.document.clone(), + &source_name, + &name, + ) { + Ok(copied) => copied, + Err(error) => { + self.command_line.push_error(&error); + return false; + } + }; + let mut constraints = history.checkpoint.constraints.clone(); + for mut set in self.tabs[i].scene.parametric_constraints.iter().filter(|set| { + matches!(set.scope, crate::scene::parametric_constraints::ParametricScope::Block(handle) if copied.handles.contains_key(&handle)) + }).cloned() { + if !set.retained_standard_groups.is_empty() || set.constraints.iter().any(|c| c.native_origin.is_some()) { + self.command_line.push_error(crate::t!("Save Block As cannot copy retained native constraints yet.").as_ref()); + return false; + } + let crate::scene::parametric_constraints::ParametricScope::Block(old) = set.scope else { unreachable!() }; + set.scope = crate::scene::parametric_constraints::ParametricScope::Block(copied.handles[&old]); + for constraint in &mut set.constraints { + for reference in constraint.refs.iter_mut().chain(constraint.rigid_points.iter_mut().map(|(r, _)| r)) { + let Some(new) = copied.handles.get(&reference.entity) else { + self.command_line.push_error(crate::t!("A block constraint refers to geometry outside the definition.").as_ref()); + return false; + }; + reference.entity = *new; + } + } + for handle in set.dimensions.values_mut() { + let Some(new) = copied.handles.get(handle) else { + self.command_line.push_error(crate::t!("A block constraint dimension is missing.").as_ref()); + return false; + }; + *handle = *new; + } + set.dof = None; + set.conflicts.clear(); + constraints.push(set); + } + for entity in copied.document.entities_in_block(&name) { + if let EntityType::Insert(insert) = entity { + if crate::scene::block_service::would_create_cycle( + &copied.document, + &name, + &insert.block_name, + ) { + self.command_line.push_error( + &crate::scene::block_service::circular_dependency_error( + &name, + &insert.block_name, + ), + ); + return false; + } + } + } + let parameters = self.tabs[i].scene.named_parameters.clone(); + let active_layer = self.tabs[i].active_layer.clone(); + self.tabs[i].scene.document = copied.document; + self.tabs[i].scene.parametric_constraints = constraints; + self.tabs[i].scene.named_parameters = parameters; + self.tabs[i].active_layer = active_layer; + let mut delta = self + .block_edit_delta(i, true) + .expect("a new definition changes the document"); + delta.label = format!("Save block as {name}"); + let mut history = self.tabs[i].block_edit_history.take().unwrap(); + std::mem::swap(&mut self.tabs[i].history, &mut history.main); + self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta))); + std::mem::swap(&mut self.tabs[i].history, &mut history.main); + self.tabs[i].history = HistoryState::default(); + self.tabs[i].dirty = true; + let session = self.tabs[i].active_block_edit_session_mut().unwrap(); + debug_assert_eq!(session.br_handle, source_handle); + session.br_handle = copied.record; + session.block_name = name.clone(); + session.unpersisted_pre_document = None; + self.tabs[i].scene.block_edit_block = Some(copied.record); + self.tabs[i].scene.deselect_all(); + self.tabs[i].active_grip = None; + self.grip_hover = None; + self.grip_popup = None; + self.visibility_popup = None; + history.checkpoint = self.block_edit_checkpoint(i); + self.tabs[i].block_edit_history = Some(history); + self.tabs[i].scene.rebuild_derived_caches(); + self.tabs[i].refresh_active_ucs(); + self.refresh_block_palette(); + self.refresh_properties(); + self.sync_ribbon_layers(); + self.command_line + .push_output(crate::tf!("Block {} saved.", name).as_ref()); + true + } + + pub(super) fn save_block_edit(&mut self, i: usize) -> bool { + let Some(session) = self.tabs[i].active_block_edit_session() else { + self.command_line + .push_error(crate::t!("No block editor is open.").as_ref()); + return false; + }; + let name = session.block_name.clone(); + if let Err(error) = crate::scene::block_service::validate_block_name(&name) { + // Existing anonymous definitions are editable; name validation + // applies to named definitions rather than generated variants. + if !name.starts_with('*') { + self.command_line.push_error(&error.to_string()); + return false; + } + } + let nested: Vec<_> = self.tabs[i] + .scene + .document + .entities_in_block(&name) + .filter_map(|entity| match entity { + EntityType::Insert(ins) => Some(ins.block_name.clone()), + _ => None, + }) + .collect(); + for child in nested { + if crate::scene::block_service::would_create_cycle( + &self.tabs[i].scene.document, + &name, + &child, + ) { + self.command_line.push_error( + &crate::scene::block_service::circular_dependency_error(&name, &child), + ); + return false; + } + } + // A dynamic block leaves the editor showing its first visibility + // state, with erased objects dropped from its states. + let definition = session.br_handle; + crate::scene::block_visibility::finish_editing( + &mut self.tabs[i].scene.document, + definition, + ); + self.tabs[i].scene.visibility_states.remove(&definition); + if let Some(delta) = self.block_edit_delta(i, true) { + let mut history = self.tabs[i].block_edit_history.take().unwrap(); + std::mem::swap(&mut self.tabs[i].history, &mut history.main); + self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta))); + std::mem::swap(&mut self.tabs[i].history, &mut history.main); + self.tabs[i].dirty = true; + history.checkpoint = self.block_edit_checkpoint(i); + self.tabs[i].block_edit_history = Some(history); + self.tabs[i].scene.bump_geometry(); + } + if let Some(session) = self.tabs[i].active_block_edit_session_mut() { + session.unpersisted_pre_document = None; + } + self.command_line + .push_output(crate::tf!("Block {} saved.", name).as_ref()); + true + } + + pub(super) fn close_block_edit(&mut self, i: usize, discard: bool) { + self.finish_pending_history(i); + let Some(session) = self.tabs[i].block_edits.pop() else { + return; + }; + if let Some(history) = self.tabs[i].block_edit_history.take() { + if discard { + if let Some(pre_doc) = session.unpersisted_pre_document { + self.tabs[i].scene.document = pre_doc; + } else { + self.tabs[i].scene.document = history.checkpoint.document; + } + self.tabs[i].scene.parametric_constraints = history.checkpoint.constraints; + self.tabs[i].scene.named_parameters = history.checkpoint.parameters; + self.tabs[i].active_layer = history.checkpoint.active_layer; + self.tabs[i].dirty = history.checkpoint.dirty; + } + self.tabs[i].history = history.main; + self.tabs[i].scene.object_isolation = history.return_isolation; + } + self.tabs[i].active_block_edit = None; + self.restore_after_block_edit_close( + i, + None, + &session.return_layout, + &session.return_camera, + ); + self.cancel_attr_editor(); + self.discard_block_tests(self.tabs[i].id); + } + + pub(super) fn request_block_edit_close( + &mut self, + i: usize, + continuation: BlockEditContinuation, + ) -> Task { + let id = self.tabs[i].id; + if self.tabs[i].active_block_edit_session().is_some() { + if self.block_edit_dirty(i) { + self.active_tab = i; + self.pending_block_close = Some((self.tabs[i].id, continuation)); + self.active_modal = Some(ModalKind::BlockEditChanges); + return Task::none(); + } + self.close_block_edit(i, true); + } + let i = self.tabs.iter().position(|tab| tab.id == id).unwrap(); + self.continue_after_block_edit(continuation, i) + } + + fn continue_after_block_edit( + &mut self, + continuation: BlockEditContinuation, + i: usize, + ) -> Task { + match continuation { + BlockEditContinuation::Close => Task::none(), + BlockEditContinuation::Switch(name) => { + self.active_tab = i; + self.dispatch_command(&format!("BEDIT_OPEN:{name}")) + } + BlockEditContinuation::Create(name) => { + self.active_tab = i; + self.dispatch_command(&format!("BEDIT_NEW:{name}")) + } + BlockEditContinuation::Layout(name) => { + self.active_tab = i; + self.on_layout_switch(name) + } + BlockEditContinuation::Tab(id) => self + .tabs + .iter() + .position(|tab| tab.id == id) + .map(|index| { + let close = self.on_tab_close(index); + if self.pending_close.is_none() && self.pending_block_close.is_none() { + Task::batch([close, self.continue_tab_close_queue()]) + } else { + close + } + }) + .unwrap_or_else(Task::none), + BlockEditContinuation::Quit => self.request_quit_with_block_edits(), + } + } + + pub(super) fn resolve_block_edit_close(&mut self, save: bool) -> Task { + let Some((id, continuation)) = self.pending_block_close.clone() else { + return Task::none(); + }; + let Some(i) = self.tabs.iter().position(|tab| tab.id == id) else { + self.cancel_block_edit_close(); + return Task::none(); + }; + if save && !self.save_block_edit(i) { + return Task::none(); + } + self.pending_block_close = None; + self.active_modal = None; + self.close_block_edit(i, !save); + let i = self.tabs.iter().position(|tab| tab.id == id).unwrap(); + self.continue_after_block_edit(continuation, i) + } + + pub(super) fn cancel_block_edit_close(&mut self) { + self.pending_block_close = None; + self.pending_tab_closes.clear(); + if self.active_modal == Some(ModalKind::BlockEditChanges) { + self.active_modal = None; + } + } + + pub(super) fn request_quit_with_block_edits(&mut self) -> Task { + if let Some(i) = self + .tabs + .iter() + .position(|tab| tab.active_block_edit_session().is_some()) + { + return self.request_block_edit_close(i, BlockEditContinuation::Quit); + } + if self + .tabs + .iter() + .any(|tab| tab.dirty && tab.block_test.is_none()) + { + self.pending_close = Some(super::PendingClose::Quit); + self.open_unsaved_dialog_window() + } else { + self.exit_app() + } + } +} diff --git a/src/app/block_editor_tests.rs b/src/app/block_editor_tests.rs new file mode 100644 index 000000000..e3c955bb8 --- /dev/null +++ b/src/app/block_editor_tests.rs @@ -0,0 +1,1154 @@ +use super::{Message, ModalKind, OpenCADStudio}; +use codec::entities::{Insert, Line}; +use codec::types::Vector3; +use codec::{EntityType, Handle}; + +fn fixture() -> (OpenCADStudio, Handle) { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let i = app.active_tab; + app.tabs[i].scene.define_block_raw( + "EDIT_ME", + Vector3::ZERO, + vec![EntityType::Line(Line::from_points( + Vector3::ZERO, + Vector3::UNIT_X, + ))], + ); + let reference = app.tabs[i] + .scene + .document + .add_entity(EntityType::Insert(Insert::new("EDIT_ME", Vector3::ZERO))) + .unwrap(); + app.tabs[i].dirty = false; + (app, reference) +} + +fn begin(app: &mut OpenCADStudio, reference: Handle) { + let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value())); + assert!(app.tabs[app.active_tab].active_block_edit.is_some()); +} + +fn circle(app: &mut OpenCADStudio, x: f64) { + let result = app.automation_op(&format!(r#"{{"op":"run","cmd":"CIRCLE {x},0 2"}}"#)); + assert_eq!(result["ok"], true, "{result}"); + app.finish_pending_history(app.active_tab); +} + +fn members(app: &OpenCADStudio) -> Vec { + app.tabs[app.active_tab] + .scene + .document + .block_records + .get("EDIT_ME") + .unwrap() + .entity_handles + .clone() +} + +fn main_depth(app: &OpenCADStudio) -> usize { + let tab = &app.tabs[app.active_tab]; + tab.block_edit_history + .as_ref() + .map(|h| h.main.undo_stack.len()) + .unwrap_or(tab.history.undo_stack.len()) +} + +#[test] +fn local_undo_preserves_imported_native_networks_when_constraints_are_unchanged() { + use codec::objects::{ + AssocNetwork, AssociativeData, AssociativeObject, Dictionary, ObjectType, + }; + let (mut app, reference) = fixture(); + let i = app.active_tab; + let doc = &mut app.tabs[i].scene.document; + let owner = doc.block_records.get("EDIT_ME").unwrap().handle; + let dictionary_handle = doc.allocate_handle(); + let network_handle = doc.allocate_handle(); + let mut dictionary = Dictionary::new(); + dictionary.handle = dictionary_handle; + dictionary.owner = owner; + dictionary.hard_owner = true; + dictionary.add_entry("ACAD_ASSOCNETWORK", network_handle); + doc.objects + .insert(dictionary_handle, ObjectType::Dictionary(dictionary)); + let network = AssociativeObject { + handle: network_handle, + owner: dictionary_handle, + reactors: vec![dictionary_handle], + dxf_name: "ASSOCNETWORK".into(), + cpp_class_name: "AcDbAssocNetwork".into(), + data: AssociativeData::Network(AssocNetwork::default()), + ..Default::default() + }; + doc.objects + .insert(network_handle, ObjectType::Associative(network)); + begin(&mut app, reference); + let original_objects = app.tabs[i].scene.document.objects.clone(); + circle(&mut app, 1.0); + assert!(!app.save_block_edit_as(i, "COPY")); + assert_eq!(app.tabs[i].scene.document.objects, original_objects); + app.undo_steps(1); + assert_eq!(members(&app).len(), 1); + assert_eq!(app.tabs[i].scene.document.objects, original_objects); + app.redo_steps(1); + assert_eq!(members(&app).len(), 2); + assert_eq!(app.tabs[i].scene.document.objects, original_objects); +} + +#[test] +fn save_as_forks_unsaved_edits_and_undoes_as_one_drawing_step() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + circle(&mut app, 1.0); + app.save_block_edit(app.active_tab); + let saved = members(&app); + circle(&mut app, 2.0); + let edited = members(&app).len(); + assert!(app.save_block_edit_as(app.active_tab, " COPY ")); + let i = app.active_tab; + assert_eq!(members(&app), saved); + assert_eq!( + app.tabs[i].active_block_edit_session().unwrap().block_name, + "COPY" + ); + assert_eq!( + app.tabs[i] + .scene + .document + .block_records + .get("COPY") + .unwrap() + .entity_handles + .len(), + edited + ); + assert_eq!(main_depth(&app), 2); + assert!(app.tabs[i].history.undo_stack.is_empty()); + assert!(!app.block_edit_dirty(i)); + app.undo_steps(1); + assert!(app.tabs[i] + .scene + .document + .block_records + .get("COPY") + .is_some()); + circle(&mut app, 3.0); + app.close_block_edit(i, true); + assert_eq!( + app.tabs[i] + .scene + .document + .block_records + .get("COPY") + .unwrap() + .entity_handles + .len(), + edited + ); + app.undo_steps(1); + assert!(app.tabs[i] + .scene + .document + .block_records + .get("COPY") + .is_none()); + assert_eq!(members(&app), saved); + app.redo_steps(1); + assert_eq!( + app.tabs[i] + .scene + .document + .block_records + .get("COPY") + .unwrap() + .entity_handles + .len(), + edited + ); +} + +#[test] +fn save_as_name_refusals_do_not_change_the_session() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + circle(&mut app, 1.0); + let i = app.active_tab; + let document = app.tabs[i].scene.document.clone(); + let depth = app.tabs[i].history.undo_stack.len(); + for name in ["", "bad/name", "edit_me", "EDIT_ME"] { + assert!(!app.save_block_edit_as(i, name)); + assert_eq!(app.tabs[i].scene.document, document); + assert_eq!(app.tabs[i].history.undo_stack.len(), depth); + assert_eq!(main_depth(&app), 0); + assert_eq!( + app.tabs[i].active_block_edit_session().unwrap().block_name, + "EDIT_ME" + ); + } + assert_eq!( + app.command_line.last_error.as_deref(), + Some("block \"EDIT_ME\" already exists") + ); +} + +#[test] +fn save_as_dialog_cancel_and_enter_follow_the_current_session() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + circle(&mut app, 1.0); + let i = app.active_tab; + let document = app.tabs[i].scene.document.clone(); + let _ = app.update(Message::DropdownSelectItem { + dropdown_id: "BLOCK_SAVE", + cmd: "BSAVEAS", + }); + assert_eq!(app.active_modal, Some(ModalKind::BlockSaveAs)); + let _ = app.update(Message::BlockSaveAsName("edit_me".into())); + assert!(app.block_save_as.as_ref().unwrap().error.is_some()); + let _ = app.update(Message::CommandEscape); + assert!(app.block_save_as.is_none()); + assert_eq!(app.tabs[i].scene.document, document); + let _ = app.dispatch_command("BSAVEAS"); + let _ = app.update(Message::BlockSaveAsName("COPY".into())); + assert!(app.block_save_as.as_ref().unwrap().error.is_none()); + let _ = app.update(Message::CommandFinalize); + assert!(app.active_modal.is_none()); + assert_eq!( + app.tabs[i].active_block_edit_session().unwrap().block_name, + "COPY" + ); +} + +#[test] +fn save_as_preserves_base_point_description_units_and_nested_attributes() { + use codec::entities::AttributeEntity; + let (mut app, reference) = fixture(); + let i = app.active_tab; + app.tabs[i] + .scene + .define_block_raw("CHILD", Vector3::ZERO, vec![]); + let owner = app.tabs[i] + .scene + .document + .block_records + .get("EDIT_ME") + .unwrap() + .handle; + let mut nested = Insert::new("CHILD", Vector3::new(4.0, 5.0, 0.0)); + let mut attribute = AttributeEntity::new("TAG".into(), "value".into()); + attribute.common.handle = app.tabs[i].scene.document.allocate_handle(); + nested.attributes.push(attribute); + nested.common.owner_handle = owner; + nested.seqend_handle = Some(app.tabs[i].scene.document.allocate_handle()); + let nested = app.tabs[i] + .scene + .document + .add_entity(EntityType::Insert(nested)) + .unwrap(); + let base = Vector3::new(12.0, 34.0, 0.0); + let record = app.tabs[i] + .scene + .document + .block_records + .get_mut("EDIT_ME") + .unwrap(); + record.base_point = base; + record.description = "A reusable definition".into(); + record.units = 4; + let source_marker = record.block_entity_handle; + if let Some(EntityType::Block(marker)) = + app.tabs[i].scene.document.get_entity_mut(source_marker) + { + marker.base_point = base; + } + let source_insert = app.tabs[i] + .scene + .document + .get_entity(nested) + .unwrap() + .clone(); + begin(&mut app, reference); + assert!(app.save_block_edit_as(i, "COPY")); + let doc = &app.tabs[i].scene.document; + let record = doc.block_records.get("COPY").unwrap(); + assert_eq!(record.base_point, base); + assert_eq!(record.units, 4); + assert_eq!(record.description, "A reusable definition"); + assert_eq!(doc.get_entity(nested).unwrap(), &source_insert); + let copied = doc + .entities_in_block("COPY") + .find_map(|e| match e { + EntityType::Insert(insert) => Some(insert), + _ => None, + }) + .unwrap(); + let EntityType::Insert(original) = &source_insert else { + unreachable!() + }; + assert_eq!(copied.block_name, "CHILD"); + assert_eq!(copied.insert_point, original.insert_point); + assert_ne!( + copied.attributes[0].common.handle, + original.attributes[0].common.handle + ); + assert_eq!( + copied.attributes[0].common.owner_handle, + copied.common.handle + ); + assert_ne!(copied.seqend_handle, original.seqend_handle); + assert_eq!(copied.attributes[0].value, original.attributes[0].value); +} + +#[test] +fn save_as_clones_extension_dictionaries_and_draw_order_independently() { + use codec::objects::{ + Dictionary, ObjectType, SortEntitiesTable, XRecord, XRecordEntry, XRecordValue, + }; + let (mut app, reference) = fixture(); + let i = app.active_tab; + let owner = app.tabs[i] + .scene + .document + .block_records + .get("EDIT_ME") + .unwrap() + .handle; + let first = members(&app)[0]; + let mut second = Line::from_points(Vector3::ZERO, Vector3::UNIT_Y); + second.common.owner_handle = owner; + let second = app.tabs[i] + .scene + .document + .add_entity(EntityType::Line(second)) + .unwrap(); + let root = app.tabs[i] + .scene + .document + .ensure_extension_dictionary(owner); + let mut record = XRecord::new(); + record.handle = app.tabs[i].scene.document.allocate_handle(); + record.owner = root; + record + .entries + .push(XRecordEntry::new(340, XRecordValue::Handle(first))); + let leaf = record.handle; + app.tabs[i] + .scene + .document + .objects + .insert(leaf, ObjectType::XRecord(record)); + let mut order = SortEntitiesTable::for_block(owner); + order.handle = app.tabs[i].scene.document.allocate_handle(); + order.owner_handle = root; + order.add_entry(first, Handle::new(0x200)); + order.send_to_back(second); + let order_handle = order.handle; + app.tabs[i] + .scene + .document + .objects + .insert(order_handle, ObjectType::SortEntitiesTable(order)); + let Some(ObjectType::Dictionary(dictionary)) = + app.tabs[i].scene.document.objects.get_mut(&root) + else { + unreachable!() + }; + dictionary.add_entry("DATA", leaf); + dictionary.add_entry("ACAD_SORTENTS", order_handle); + let original: Dictionary = dictionary.clone(); + begin(&mut app, reference); + assert!(app.save_block_edit_as(i, "COPY")); + let doc = &app.tabs[i].scene.document; + let owner = doc.block_records.get("COPY").unwrap().handle; + let copy_members = doc + .block_records + .get("COPY") + .unwrap() + .entity_handles + .clone(); + let copied_root = doc.extension_dictionary_handle(owner).unwrap(); + assert_ne!(root, copied_root); + assert_eq!( + doc.objects.get(&root), + Some(&ObjectType::Dictionary(original)) + ); + let Some(ObjectType::Dictionary(dictionary)) = doc.objects.get(&copied_root) else { + unreachable!() + }; + let Some(ObjectType::XRecord(record)) = doc.objects.get(&dictionary.get("DATA").unwrap()) + else { + unreachable!() + }; + assert_eq!(record.owner, copied_root); + assert_eq!( + record.entries[0].value, + XRecordValue::Handle(copy_members[0]) + ); + let Some(ObjectType::SortEntitiesTable(order)) = + doc.objects.get(&dictionary.get("ACAD_SORTENTS").unwrap()) + else { + unreachable!() + }; + assert_eq!(order.block_owner_handle, owner); + assert!( + order.effective_sort_handle(copy_members[1]) < order.effective_sort_handle(copy_members[0]) + ); + app.close_block_edit(i, true); + app.undo_steps(1); + assert!(!app.tabs[i] + .scene + .document + .objects + .contains_key(&copied_root)); + app.redo_steps(1); + assert!(app.tabs[i] + .scene + .document + .objects + .contains_key(&copied_root)); +} + +#[test] +fn save_as_preserves_block_parameters_and_remaps_constraint_geometry() { + use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef, ParametricScope}; + let (mut app, reference) = fixture(); + let i = app.active_tab; + let owner = app.tabs[i] + .scene + .document + .block_records + .get("EDIT_ME") + .unwrap() + .handle; + let member = members(&app)[0]; + let set = app.tabs[i] + .scene + .parametric_constraint_set_mut(ParametricScope::Block(owner)); + set.local_parameters.set("width", "7").unwrap(); + set.add( + ConstraintKind::Horizontal, + vec![ParametricRef::whole(member)], + None, + ); + begin(&mut app, reference); + app.tabs[i] + .scene + .parametric_constraint_set_mut(ParametricScope::Block(owner)) + .local_parameters + .set("width", "9") + .unwrap(); + assert!(app.save_block_edit_as(i, "COPY")); + let new_owner = app.tabs[i] + .scene + .document + .block_records + .get("COPY") + .unwrap() + .handle; + let copied = app.tabs[i] + .scene + .parametric_constraint_set(ParametricScope::Block(new_owner)) + .unwrap(); + assert_eq!(copied.local_parameters.resolve("width").unwrap(), 9.0); + assert_ne!(copied.constraints[0].refs[0].entity, member); + assert_eq!( + app.tabs[i] + .scene + .parametric_constraint_set(ParametricScope::Block(owner)) + .unwrap() + .local_parameters + .resolve("width") + .unwrap(), + 7.0 + ); + app.close_block_edit(i, true); + app.undo_steps(1); + assert!(app.tabs[i] + .scene + .parametric_constraint_set(ParametricScope::Block(new_owner)) + .is_none()); + app.redo_steps(1); + assert_eq!( + app.tabs[i] + .scene + .parametric_constraint_set(ParametricScope::Block(new_owner)) + .unwrap() + .local_parameters + .resolve("width") + .unwrap(), + 9.0 + ); +} + +#[test] +fn save_as_refuses_unsupported_owned_graphs_without_losing_edits() { + use codec::objects::{DynamicBlockObject, ObjectType}; + let (mut app, reference) = fixture(); + let i = app.active_tab; + let mut object = DynamicBlockObject::new("BLOCKLINEARPARAMETER", "LinearParameter"); + object.handle = app.tabs[i].scene.document.allocate_handle(); + object.owner = app.tabs[i] + .scene + .document + .block_records + .get("EDIT_ME") + .unwrap() + .handle; + app.tabs[i] + .scene + .document + .objects + .insert(object.handle, ObjectType::DynamicBlock(object)); + begin(&mut app, reference); + circle(&mut app, 1.0); + let document = app.tabs[i].scene.document.clone(); + assert!(!app.save_block_edit_as(i, "COPY")); + assert_eq!(app.tabs[i].scene.document, document); + assert_eq!(members(&app).len(), 2); + assert_eq!(main_depth(&app), 0); + assert!(app + .command_line + .last_error + .as_ref() + .unwrap() + .contains("native object relationships")); +} + +#[test] +fn save_as_retains_new_shared_layers_and_nested_definitions() { + use codec::entities::{AttributeDefinition, AttributeEntity, MText}; + let (mut app, reference) = fixture(); + begin(&mut app, reference); + let i = app.active_tab; + let _ = app.dispatch_command("LAYER NEW COPY_LAYER"); + let _ = app.dispatch_command("CLAYER COPY_LAYER"); + let mut definition = AttributeDefinition::new("TAG".into(), "Prompt".into(), "first".into()); + definition.is_multiline = true; + definition.mtext_flag = codec::entities::attribute_definition::MTextFlag::MultiLine; + let mut text = MText::new(); + text.value = "first\\Psecond".into(); + definition.embedded_mtext = Some(Box::new(text)); + app.tabs[i].scene.define_block_raw( + "NEW_CHILD", + Vector3::ZERO, + vec![EntityType::AttributeDefinition(definition)], + ); + let source_definition = app.tabs[i] + .scene + .document + .block_records + .get("NEW_CHILD") + .unwrap() + .entity_handles[0]; + let mut child = Insert::new("NEW_CHILD", Vector3::ZERO); + let mut attribute = AttributeEntity::new("TAG".into(), "first".into()); + attribute.attdef_handle = source_definition; + child.attributes.push(attribute); + child.common.layer = "COPY_LAYER".into(); + child.common.owner_handle = app.tabs[i].active_block_edit_session().unwrap().br_handle; + app.tabs[i] + .scene + .document + .add_entity(EntityType::Insert(child)) + .unwrap(); + assert!(app.save_block_edit_as(i, "COPY")); + assert_eq!(app.tabs[i].active_layer, "COPY_LAYER"); + assert!(app.tabs[i] + .scene + .document + .layers + .get("COPY_LAYER") + .is_some()); + assert!(app.tabs[i] + .scene + .document + .block_records + .get("NEW_CHILD") + .is_some()); + assert_eq!(members(&app).len(), 1); + let doc = &app.tabs[i].scene.document; + let copied_definition = doc.block_records.get("NEW_CHILD").unwrap().entity_handles[0]; + assert_ne!(copied_definition, source_definition); + let EntityType::AttributeDefinition(definition) = doc.get_entity(copied_definition).unwrap() + else { + unreachable!() + }; + let text = definition.embedded_mtext.as_ref().unwrap(); + assert_eq!(text.value, "first\\Psecond"); + assert!(!text.common.handle.is_null()); + let copied_attribute = doc + .entities_in_block("COPY") + .find_map(|entity| match entity { + EntityType::Insert(insert) => Some(&insert.attributes[0]), + _ => None, + }) + .unwrap(); + assert_eq!(copied_attribute.attdef_handle, copied_definition); + assert!(app.tabs[i].scene.document.entities_in_block("COPY").any(|entity| matches!(entity, EntityType::Insert(insert) if insert.block_name=="NEW_CHILD" && insert.common.layer=="COPY_LAYER"))); +} + +#[test] +fn save_as_can_create_an_empty_definition() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + let i = app.active_tab; + let original = members(&app); + app.push_undo_snapshot(i, "Erase"); + app.tabs[i].scene.erase_entities(&original); + app.finish_pending_history(i); + assert!(app.save_block_edit_as(i, "EMPTY")); + assert_eq!(members(&app), original); + assert!(app.tabs[i] + .scene + .document + .block_records + .get("EMPTY") + .unwrap() + .entity_handles + .is_empty()); + assert!(!app.block_edit_dirty(i)); +} + +#[test] +fn session_undo_preserves_membership_and_cannot_reach_drawing_history() { + let (mut app, reference) = fixture(); + circle(&mut app, 50.0); + let depth = main_depth(&app); + begin(&mut app, reference); + assert_eq!(main_depth(&app), depth); + circle(&mut app, 1.0); + assert_eq!(members(&app).len(), 2); + app.undo_steps(1); + assert_eq!(members(&app).len(), 1); + assert!(!app.block_edit_dirty(app.active_tab)); + app.undo_steps(100); + assert_eq!(main_depth(&app), depth); + assert!(app.tabs[app.active_tab].active_block_edit.is_some()); + app.redo_steps(1); + assert_eq!(members(&app).len(), 2); + assert!(members(&app).iter().all(|h| app.tabs[app.active_tab] + .scene + .document + .get_entity(*h) + .is_some())); +} + +#[test] +fn repeated_saves_stay_open_and_commit_one_drawing_step_each() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + circle(&mut app, 1.0); + circle(&mut app, 2.0); + let _ = app.dispatch_command("BSAVE"); + assert!(app.tabs[app.active_tab].active_block_edit.is_some()); + assert_eq!(main_depth(&app), 1); + assert!(!app.block_edit_dirty(app.active_tab)); + let _ = app.dispatch_command("BSAVE"); + assert_eq!(main_depth(&app), 1, "a clean save is not a new edit"); + circle(&mut app, 3.0); + let _ = app.dispatch_command("BSAVE"); + assert_eq!(main_depth(&app), 2); + circle(&mut app, 4.0); + let _ = app.dispatch_command("BEDIT_DISCARD"); + assert_eq!(members(&app).len(), 4, "discard restores the last save"); + assert_eq!(main_depth(&app), 2); + assert_eq!( + app.tabs[app.active_tab] + .history + .undo_stack + .last() + .unwrap() + .label(), + "Edit block EDIT_ME" + ); + app.undo_steps(1); + assert_eq!(members(&app).len(), 3); + app.undo_steps(1); + assert_eq!(members(&app).len(), 1); + app.redo_steps(2); + assert_eq!(members(&app).len(), 4); +} + +#[test] +fn discard_preserves_drawing_redo_and_never_leaks_local_edits() { + let (mut app, reference) = fixture(); + circle(&mut app, 50.0); + app.undo_steps(1); + let main_redo = app.tabs[app.active_tab].history.redo_stack.len(); + begin(&mut app, reference); + circle(&mut app, 1.0); + let _ = app.dispatch_command("BEDIT_DISCARD"); + assert_eq!(members(&app).len(), 1); + assert_eq!(app.tabs[app.active_tab].history.redo_stack.len(), main_redo); + app.redo_steps(1); + assert_eq!(members(&app).len(), 1); +} + +#[test] +fn dirty_close_cancel_discard_and_save_follow_the_checkpoint() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + circle(&mut app, 1.0); + let _ = app.dispatch_command("BCLOSE"); + assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges)); + let _ = app.update(Message::BlockEditChangesCancel); + assert!(app.tabs[app.active_tab].active_block_edit.is_some()); + assert_eq!(members(&app).len(), 2); + let _ = app.dispatch_command("BCLOSE"); + let _ = app.update(Message::BlockEditChangesDiscard); + assert!(app.tabs[app.active_tab].active_block_edit.is_none()); + assert_eq!(members(&app).len(), 1); + assert_eq!(main_depth(&app), 0); + begin(&mut app, reference); + circle(&mut app, 2.0); + let _ = app.dispatch_command("BCLOSE"); + let _ = app.update(Message::BlockEditChangesSave); + assert!(app.tabs[app.active_tab].active_block_edit.is_none()); + assert_eq!(members(&app).len(), 2); + assert_eq!(main_depth(&app), 1); +} + +#[test] +fn clean_close_does_not_prompt_or_dirty_the_drawing() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + assert!(!app.tabs[app.active_tab].dirty); + let _ = app.dispatch_command("BCLOSE"); + assert!(app.tabs[app.active_tab].active_block_edit.is_none()); + assert!(app.active_modal.is_none()); + assert!(!app.tabs[app.active_tab].dirty); + assert_eq!(main_depth(&app), 0); +} + +#[test] +fn dirty_switch_can_cancel_and_never_nests_sessions() { + let (mut app, reference) = fixture(); + let i = app.active_tab; + app.tabs[i] + .scene + .define_block_raw("OTHER", Vector3::ZERO, vec![]); + let other = app.tabs[i] + .scene + .document + .add_entity(EntityType::Insert(Insert::new("OTHER", Vector3::ZERO))) + .unwrap(); + begin(&mut app, reference); + circle(&mut app, 1.0); + let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", other.value())); + assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges)); + let _ = app.update(Message::BlockEditChangesCancel); + assert_eq!( + app.tabs[i].active_block_edit_session().unwrap().block_name, + "EDIT_ME" + ); + let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", other.value())); + let _ = app.update(Message::BlockEditChangesSave); + assert_eq!(app.tabs[i].block_edits.len(), 1); + assert_eq!( + app.tabs[i].active_block_edit_session().unwrap().block_name, + "OTHER" + ); + assert_eq!(main_depth(&app), 1); +} + +#[test] +fn tab_close_prompts_for_block_edits_before_drawing_changes() { + let (mut app, reference) = fixture(); + let id = app.tabs[app.active_tab].id; + begin(&mut app, reference); + circle(&mut app, 1.0); + let _ = app.update(Message::TabClose(id)); + assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges)); + let _ = app.update(Message::BlockEditChangesCancel); + assert!(app.tabs.iter().any(|t| t.id == id)); + let _ = app.update(Message::TabClose(id)); + let _ = app.update(Message::BlockEditChangesDiscard); + assert!(!app.tabs.iter().any(|t| t.id == id)); +} + +#[test] +fn save_refuses_a_cycle_introduced_outside_the_insert_command() { + let (mut app, reference) = fixture(); + let i = app.active_tab; + app.tabs[i].scene.define_block_raw( + "OTHER", + Vector3::ZERO, + vec![EntityType::Insert(Insert::new("EDIT_ME", Vector3::ZERO))], + ); + begin(&mut app, reference); + app.tabs[i] + .scene + .add_entity(EntityType::Insert(Insert::new("OTHER", Vector3::ZERO))); + assert!(!app.save_block_edit(i)); + assert_eq!(main_depth(&app), 0); + assert!(app.tabs[i].active_block_edit.is_some()); +} + +#[test] +fn drawing_history_retains_the_paper_layout_after_save_and_undo() { + let (mut app, reference) = fixture(); + let i = app.active_tab; + app.tabs[i].scene.add_layout("Sheet").unwrap(); + app.tabs[i].scene.set_current_layout("Sheet".into()); + begin(&mut app, reference); + circle(&mut app, 1.0); + let _ = app.dispatch_command("BEDIT_SAVE"); + assert_eq!(app.tabs[i].scene.current_layout, "Sheet"); + assert!(!app.tabs[i].scene.document.header.show_model_space); + app.undo_steps(1); + assert_eq!(app.tabs[i].scene.current_layout, "Sheet"); + assert!(!app.tabs[i].scene.document.header.show_model_space); + assert_eq!( + crate::io::saved_active_layout(&app.tabs[i].scene.document).as_deref(), + Some("Sheet") + ); + assert_eq!(members(&app).len(), 1); + app.redo_steps(1); + assert!(!app.tabs[i].scene.document.header.show_model_space); + assert_eq!(members(&app).len(), 2); +} + +#[test] +fn local_parameters_are_saved_and_discarded_with_the_definition() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + let i = app.active_tab; + let scope = app.tabs[i].current_parametric_scope(); + app.tabs[i] + .scene + .parametric_constraint_set_mut(scope) + .local_parameters + .set("Width", "7") + .unwrap(); + assert!(app.save_block_edit(i)); + app.tabs[i] + .scene + .parametric_constraint_set_mut(scope) + .local_parameters + .set("Width", "9") + .unwrap(); + let _ = app.dispatch_command("BEDIT_DISCARD"); + assert_eq!( + app.tabs[i] + .scene + .parametric_constraint_set(scope) + .unwrap() + .local_parameters + .resolve("Width") + .unwrap(), + 7.0 + ); + app.undo_steps(1); + assert!(!app.tabs[i] + .scene + .parametric_constraint_set(scope) + .unwrap() + .local_parameters + .contains("Width")); + app.redo_steps(1); + assert_eq!( + app.tabs[i] + .scene + .parametric_constraint_set(scope) + .unwrap() + .local_parameters + .resolve("Width") + .unwrap(), + 7.0 + ); +} + +#[test] +fn quit_prompts_for_block_edits_before_unsaved_drawing_changes() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + circle(&mut app, 1.0); + let _ = app.request_quit_with_block_edits(); + assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges)); + let _ = app.update(Message::BlockEditChangesCancel); + assert!(app.pending_close.is_none()); + assert!(app.tabs[app.active_tab].active_block_edit.is_some()); + let _ = app.request_quit_with_block_edits(); + let _ = app.update(Message::BlockEditChangesSave); + assert!(app.tabs[app.active_tab].active_block_edit.is_none()); + assert_eq!(app.active_modal, Some(ModalKind::Unsaved)); + assert!(matches!(app.pending_close, Some(super::PendingClose::Quit))); +} + +#[test] +fn document_replacement_is_refused_until_the_editor_is_closed() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], false); + assert!(app.tabs[app.active_tab].active_block_edit.is_some()); +} + +#[test] +fn erasing_the_last_member_saves_an_empty_definition_and_undo_restores_it() { + let (mut app, reference) = fixture(); + begin(&mut app, reference); + let i = app.active_tab; + let line = members(&app)[0]; + app.push_undo_snapshot(i, "Erase block member"); + app.tabs[i].scene.erase_entities(&[line]); + app.finish_pending_history(i); + assert!(members(&app).is_empty()); + app.undo_steps(1); + assert_eq!(members(&app), vec![line]); + app.redo_steps(1); + assert!(members(&app).is_empty()); + let _ = app.dispatch_command("BEDIT_SAVE"); + assert!(members(&app).is_empty()); + app.undo_steps(1); + assert_eq!(members(&app), vec![line]); +} + +#[test] +fn close_all_resumes_after_discarding_a_dirty_block() { + let (mut app, reference) = fixture(); + let first = app.tabs[app.active_tab].id; + app.tabs.push(super::document::DocumentTab::new_drawing(2)); + let second = app.tabs.last().unwrap().id; + begin(&mut app, reference); + circle(&mut app, 1.0); + let _ = app.update(Message::DocTabCloseAll); + assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges)); + let _ = app.update(Message::BlockEditChangesDiscard); + assert!(!app + .tabs + .iter() + .any(|tab| tab.id == first || tab.id == second)); + assert!(app.pending_tab_closes.is_empty()); +} + +#[test] +fn locked_bedit_no_selection_opens_edit_block_definition_dialog() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let tab = app.active_tab; + app.tabs[tab].scene.define_block_raw( + "DOOR", + Vector3::ZERO, + vec![EntityType::Line(Line::from_points( + Vector3::ZERO, + Vector3::UNIT_X, + ))], + ); + app.tabs[tab] + .scene + .document + .block_records + .get_mut("DOOR") + .unwrap() + .description = "Wooden door".to_string(); + app.tabs[tab] + .scene + .define_block_raw("WINDOW", Vector3::ZERO, vec![]); + + app.tabs[tab].scene.deselect_all(); + let _ = app.dispatch_command("BEDIT"); + + assert_eq!(app.active_modal, Some(ModalKind::BlockEditPicker)); + let picker = app.block_edit_picker.as_ref().expect("picker state set"); + assert_eq!(picker.existing_names, vec!["DOOR", "WINDOW"]); + assert_eq!(picker.name, "DOOR"); + assert_eq!(picker.description, "Wooden door"); + assert!(picker.error.is_none()); +} + +#[test] +fn locked_bedit_dialog_select_existing_block() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let tab = app.active_tab; + app.tabs[tab] + .scene + .define_block_raw("DOOR", Vector3::ZERO, vec![]); + app.tabs[tab] + .scene + .define_block_raw("WINDOW", Vector3::ZERO, vec![]); + + app.tabs[tab].scene.deselect_all(); + let _ = app.dispatch_command("BEDIT"); + + let _ = app.update(Message::BlockEditPickerSelect("WINDOW".to_string())); + let picker = app.block_edit_picker.as_ref().unwrap(); + assert_eq!(picker.name, "WINDOW"); + + let _ = app.update(Message::BlockEditPickerApply); + assert_eq!(app.active_modal, None); + assert!(app.tabs[tab].active_block_edit.is_some()); + assert_eq!( + app.tabs[tab] + .active_block_edit_session() + .unwrap() + .block_name, + "WINDOW" + ); +} + +#[test] +fn locked_bedit_dialog_name_validation() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let tab = app.active_tab; + app.tabs[tab].scene.deselect_all(); + let _ = app.dispatch_command("BEDIT"); + + let _ = app.update(Message::BlockEditPickerName("INVALID/NAME".to_string())); + assert!(app.block_edit_picker.as_ref().unwrap().error.is_some()); + + let _ = app.update(Message::BlockEditPickerApply); + assert_eq!(app.active_modal, Some(ModalKind::BlockEditPicker)); + assert!(app.tabs[tab].active_block_edit.is_none()); + + let _ = app.update(Message::BlockEditPickerName("".to_string())); + let _ = app.update(Message::BlockEditPickerApply); + assert_eq!(app.active_modal, Some(ModalKind::BlockEditPicker)); + assert!(app.tabs[tab].active_block_edit.is_none()); + + let _ = app.update(Message::CloseModal); + assert_eq!(app.active_modal, None); +} + +#[test] +fn locked_bedit_dialog_create_new_block_closes_without_saving() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let tab = app.active_tab; + app.tabs[tab].scene.deselect_all(); + let _ = app.dispatch_command("BEDIT"); + + let _ = app.update(Message::BlockEditPickerName("BRAND_NEW".to_string())); + assert!(app.block_edit_picker.as_ref().unwrap().error.is_none()); + + let _ = app.update(Message::BlockEditPickerApply); + assert_eq!(app.active_modal, None); + assert!(app.tabs[tab].active_block_edit.is_some()); + assert_eq!( + app.tabs[tab] + .active_block_edit_session() + .unwrap() + .block_name, + "BRAND_NEW" + ); + assert!(!app.block_edit_dirty(tab)); + + let _ = app.dispatch_command("BCLOSE"); + assert_eq!(app.active_modal, None); + assert!(app.tabs[tab].active_block_edit.is_none()); + assert!( + app.tabs[tab] + .scene + .document + .block_records + .get("BRAND_NEW") + .is_none(), + "unpersisted new block closed without saving must not remain in drawing" + ); +} + +#[test] +fn locked_bedit_dialog_create_new_block_discard_dirty_leaves_no_block() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let tab = app.active_tab; + app.tabs[tab].scene.deselect_all(); + let _ = app.dispatch_command("BEDIT"); + + let _ = app.update(Message::BlockEditPickerName("BRAND_NEW2".to_string())); + let _ = app.update(Message::BlockEditPickerApply); + assert!(app.tabs[tab].active_block_edit.is_some()); + + circle(&mut app, 1.0); + assert!(app.block_edit_dirty(tab)); + + let _ = app.dispatch_command("BCLOSE"); + assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges)); + + let _ = app.update(Message::BlockEditChangesDiscard); + assert_eq!(app.active_modal, None); + assert!(app.tabs[tab].active_block_edit.is_none()); + assert!( + app.tabs[tab] + .scene + .document + .block_records + .get("BRAND_NEW2") + .is_none(), + "discarded new block must not remain in drawing" + ); +} + +#[test] +fn locked_bedit_dialog_create_new_block_save_persists_and_undoes() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let tab = app.active_tab; + app.tabs[tab].scene.deselect_all(); + let _ = app.dispatch_command("BEDIT"); + + let _ = app.update(Message::BlockEditPickerName("SAVED_BLOCK".to_string())); + let _ = app.update(Message::BlockEditPickerApply); + assert!(app.tabs[tab].active_block_edit.is_some()); + + circle(&mut app, 1.0); + let _ = app.dispatch_command("BSAVE"); + assert!(app.tabs[tab] + .scene + .document + .block_records + .get("SAVED_BLOCK") + .is_some()); + + let _ = app.dispatch_command("BCLOSE"); + assert!(app.tabs[tab].active_block_edit.is_none()); + assert!(app.tabs[tab] + .scene + .document + .block_records + .get("SAVED_BLOCK") + .is_some()); + + app.undo_steps(1); + assert!( + app.tabs[tab] + .scene + .document + .block_records + .get("SAVED_BLOCK") + .is_none(), + "undo must revert block creation" + ); + + app.redo_steps(1); + assert!( + app.tabs[tab] + .scene + .document + .block_records + .get("SAVED_BLOCK") + .is_some(), + "redo must restore block creation" + ); +} + +#[test] +fn locked_bedit_with_preselected_insert_opens_directly() { + let (mut app, reference) = fixture(); + let tab = app.active_tab; + app.tabs[tab].scene.deselect_all(); + app.tabs[tab].scene.select_entity(reference, true); + + let _ = app.dispatch_command("BEDIT"); + assert_eq!(app.active_modal, None); + assert!(app.tabs[tab].active_block_edit.is_some()); + assert_eq!( + app.tabs[tab] + .active_block_edit_session() + .unwrap() + .block_name, + "EDIT_ME" + ); +} diff --git a/src/app/block_save_as.rs b/src/app/block_save_as.rs new file mode 100644 index 000000000..e6abcb4bd --- /dev/null +++ b/src/app/block_save_as.rs @@ -0,0 +1,517 @@ +use codec::entities::{Block, BlockEnd, EntityCommon}; +use codec::objects::ObjectType; +use codec::tables::BlockRecord; +use codec::{CadDocument, EntityType, Handle}; +use rustc_hash::{FxHashMap, FxHashSet}; +use std::collections::HashMap; + +/// The copy is built in the last saved document, so the source definition and +/// its references remain at their checkpoint even when the editor is dirty. +pub(super) struct DefinitionCopy { + pub document: CadDocument, + pub handles: FxHashMap, + pub record: Handle, +} + +fn remap_common(common: &mut EntityCommon, handles: &FxHashMap) { + let map = |h: Handle| handles.get(&h).copied().unwrap_or(h); + common.handle = map(common.handle); + common.owner_handle = map(common.owner_handle); + common.xdictionary_handle = common.xdictionary_handle.map(map); + common.reactors = common + .reactors + .iter() + .filter_map(|h| handles.get(h).copied()) + .collect(); + let mut data = codec::xdata::ExtendedData::new(); + for mut record in common.extended_data.records().iter().cloned() { + for value in &mut record.values { + if let codec::xdata::XDataValue::Handle(handle) = value { + *handle = map(*handle); + } + } + data.add_record(record); + } + common.extended_data = data; + common.raw_record = None; +} + +fn cloneable_object(object: &ObjectType) -> bool { + matches!( + object, + ObjectType::Dictionary(_) + | ObjectType::DictionaryWithDefault(_) + | ObjectType::DictionaryVariable(_) + | ObjectType::SpatialFilter(_) + | ObjectType::ObjectContextData(_) + | ObjectType::SortEntitiesTable(_) + | ObjectType::Field(_) + | ObjectType::FieldList(_) + | ObjectType::PlaceHolder(_) + | ObjectType::ImageDefinitionReactor(_) + | ObjectType::XRecord(codec::objects::XRecord { + entries_complete: true, + .. + }) + ) +} + +pub(super) fn copy_definition( + source: &CadDocument, + mut target: CadDocument, + source_name: &str, + name: &str, +) -> Result { + let root = source + .block_records + .get(source_name) + .ok_or_else(|| format!("block \"{source_name}\" does not exist"))?; + // Shared symbol records can be created while editing. Retain them when + // restoring the source definition to its checkpoint. + target.header = source.header.clone(); + target.layers = source.layers.clone(); + target.line_types = source.line_types.clone(); + target.text_styles = source.text_styles.clone(); + target.dim_styles = source.dim_styles.clone(); + target.app_ids = source.app_ids.clone(); + let mut records = vec![(root.clone(), name.to_string())]; + let mut names = FxHashSet::default(); + names.insert(source_name.to_ascii_uppercase()); + let mut index = 0; + let mut anonymous = 0; + while index < records.len() { + for entity in source.entities_in_block(&records[index].0.name) { + if let EntityType::Insert(insert) = entity { + if insert.block_name.eq_ignore_ascii_case(name) { + return Err(crate::scene::block_service::circular_dependency_error( + name, + &insert.block_name, + )); + } + if target.block_records.get(&insert.block_name).is_none() + && names.insert(insert.block_name.to_ascii_uppercase()) + { + let record = source + .block_records + .get(&insert.block_name) + .ok_or_else(|| format!("block \"{}\" does not exist", insert.block_name))?; + records.push((record.clone(), record.name.clone())); + } + } + if let EntityType::Dimension(dimension) = entity { + let picture = &dimension.base().block_name; + if picture.is_empty() || !names.insert(picture.to_ascii_uppercase()) { + continue; + } + let Some(record) = source.block_records.get(picture) else { + return Err(format!("Dimension picture block \"{picture}\" is missing.")); + }; + let new_name = loop { + let candidate = format!("*D{anonymous}"); + anonymous += 1; + if target.block_records.get(&candidate).is_none() + && !records + .iter() + .any(|(_, name)| name.eq_ignore_ascii_case(&candidate)) + { + break candidate; + } + }; + records.push((record.clone(), new_name)); + } + } + index += 1; + } + + let mut entities = Vec::new(); + let mut identities = FxHashSet::default(); + for (record, _) in &records { + identities.insert(record.handle); + for marker in [record.block_entity_handle, record.block_end_handle] { + if let Some(entity) = source.get_entity(marker) { + entities.push(entity.clone()); + } + } + for entity in source.entities_in_block(&record.name) { + if matches!(entity, EntityType::Block(_) | EntityType::BlockEnd(_)) { + continue; + } + if matches!(entity, EntityType::Unknown(_)) { + return Err("Save Block As cannot copy opaque entities yet.".to_string()); + } + entities.push(entity.clone()); + } + } + for entity in &entities { + identities.insert(entity.common().handle); + match entity { + EntityType::Insert(insert) => { + identities.extend(insert.attributes.iter().map(|a| a.common.handle)); + identities.extend( + insert + .attributes + .iter() + .filter_map(|a| a.embedded_mtext.as_ref().map(|text| text.common.handle)), + ); + identities.extend(insert.seqend_handle); + } + EntityType::AttributeDefinition(attribute) => { + identities.extend( + attribute + .embedded_mtext + .as_ref() + .map(|text| text.common.handle), + ); + } + EntityType::AttributeEntity(attribute) => { + identities.extend( + attribute + .embedded_mtext + .as_ref() + .map(|text| text.common.handle), + ); + } + EntityType::Polyline3D(polyline) => { + identities.extend(polyline.vertices.iter().map(|v| v.handle)) + } + _ => {} + } + } + identities.remove(&Handle::NULL); + // Ownership, rather than arbitrary references, determines what must be + // independent. Nested INSERT definitions and shared styles stay shared. + let mut objects = Vec::new(); + loop { + let added: Vec<_> = source + .objects + .iter() + .filter(|(handle, _)| { + !identities.contains(handle) + && source + .object_owner(**handle) + .is_some_and(|owner| identities.contains(&owner)) + }) + .map(|(handle, object)| (*handle, object.clone())) + .collect(); + if added.is_empty() { + break; + } + for (handle, object) in added { + if !cloneable_object(&object) { + return Err( + "Save Block As cannot copy this block's native object relationships yet." + .to_string(), + ); + } + identities.insert(handle); + objects.push((handle, object)); + } + } + for owner in &identities { + if let Some(dictionary) = source.extension_dictionary_handle(*owner) { + if !identities.contains(&dictionary) { + return Err( + "Save Block As requires an independently owned extension dictionary." + .to_string(), + ); + } + } + } + for (_, object) in &objects { + if let ObjectType::Dictionary(dictionary) = object { + if dictionary.entries.iter().any(|(key, handle)| { + !handle.is_null() + && (dictionary.hard_owner || dictionary.is_entry_hard_owner(key)) + && !identities.contains(handle) + }) { + return Err( + "Save Block As cannot duplicate a dictionary's foreign owned entries." + .to_string(), + ); + } + } + } + identities.remove(&Handle::NULL); + let floor = source + .next_handle() + .max(source.header.handle_seed) + .max(target.next_handle()); + if target.next_handle() < floor { + target.header.handle_seed = floor - 1; + target.allocate_handle(); + } + let mut identities: Vec<_> = identities.into_iter().collect(); + identities.sort_by_key(|h| h.value()); + let handles: FxHashMap<_, _> = identities + .into_iter() + .map(|h| (h, target.allocate_handle())) + .collect(); + let remap: HashMap<_, _> = handles.iter().map(|(a, b)| (*a, *b)).collect(); + let block_names: HashMap<_, _> = records + .iter() + .map(|(r, n)| (r.name.to_ascii_uppercase(), n.clone())) + .collect(); + for (record, new_name) in &records { + let mut copied: BlockRecord = record.clone(); + copied.name = new_name.clone(); + copied.handle = handles[&record.handle]; + copied.block_entity_handle = handles + .get(&record.block_entity_handle) + .copied() + .unwrap_or_else(|| target.allocate_handle()); + copied.block_end_handle = handles + .get(&record.block_end_handle) + .copied() + .unwrap_or_else(|| target.allocate_handle()); + copied.entity_handles.clear(); + copied.insert_handles.clear(); + copied.insert_count_bytes.clear(); + copied.preview_data.clear(); + copied.flags.anonymous = new_name.starts_with('*'); + copied.layout = Handle::NULL; + target + .block_records + .add(copied.clone()) + .map_err(|e| e.to_string())?; + if source.get_entity(record.block_entity_handle).is_none() { + let mut marker = Block::new(new_name, record.base_point); + marker.common.handle = copied.block_entity_handle; + marker.common.owner_handle = copied.handle; + target + .add_entity(EntityType::Block(marker)) + .map_err(|e| e.to_string())?; + } + if source.get_entity(record.block_end_handle).is_none() { + let mut marker = BlockEnd::new(); + marker.common.handle = copied.block_end_handle; + marker.common.owner_handle = copied.handle; + target + .add_entity(EntityType::BlockEnd(marker)) + .map_err(|e| e.to_string())?; + } + } + for mut entity in entities { + remap_common(entity.common_mut(), &handles); + match &mut entity { + EntityType::Block(block) => { + block.name = block_names[&block.name.to_ascii_uppercase()].clone(); + } + EntityType::Insert(insert) => { + insert.seqend_handle = insert.seqend_handle.map(|h| handles[&h]); + let copied_definition = block_names + .get(&insert.block_name.to_ascii_uppercase()) + .is_some_and(|name| name.eq_ignore_ascii_case(&insert.block_name)); + for attribute in &mut insert.attributes { + remap_common(&mut attribute.common, &handles); + if attribute.common.handle.is_null() { + attribute.common.handle = target.allocate_handle(); + } + attribute.common.owner_handle = insert.common.handle; + if copied_definition { + attribute.attdef_handle = handles + .get(&attribute.attdef_handle) + .copied() + .unwrap_or(attribute.attdef_handle); + } + if let Some(text) = &mut attribute.embedded_mtext { + remap_common(&mut text.common, &handles); + if text.common.handle.is_null() { + text.common.handle = target.allocate_handle(); + } + } + } + } + EntityType::AttributeDefinition(attribute) => { + if let Some(text) = &mut attribute.embedded_mtext { + remap_common(&mut text.common, &handles); + if text.common.handle.is_null() { + text.common.handle = target.allocate_handle(); + } + } + } + EntityType::AttributeEntity(attribute) => { + attribute.attdef_handle = handles + .get(&attribute.attdef_handle) + .copied() + .unwrap_or(attribute.attdef_handle); + if let Some(text) = &mut attribute.embedded_mtext { + remap_common(&mut text.common, &handles); + if text.common.handle.is_null() { + text.common.handle = target.allocate_handle(); + } + } + } + EntityType::Polyline3D(polyline) => { + for vertex in &mut polyline.vertices { + vertex.handle = handles + .get(&vertex.handle) + .copied() + .unwrap_or_else(|| target.allocate_handle()); + } + } + EntityType::Dimension(dimension) => { + if let Some(name) = + block_names.get(&dimension.base().block_name.to_ascii_uppercase()) + { + dimension.base_mut().block_name = name.clone(); + } + } + EntityType::Leader(leader) => { + if let Some(handle) = handles.get(&leader.annotation_handle) { + leader.annotation_handle = *handle; + } + } + EntityType::RasterImage(image) => { + image.definition_reactor_handle = image + .definition_reactor_handle + .map(|h| handles.get(&h).copied().unwrap_or(h)); + } + EntityType::Wipeout(image) => { + image.definition_reactor_handle = image + .definition_reactor_handle + .map(|h| handles.get(&h).copied().unwrap_or(h)); + } + _ => {} + } + target.add_entity(entity).map_err(|e| e.to_string())?; + } + crate::scene::remap_hatch_associations(&mut target, &handles); + for (old, mut object) in objects { + let new = handles[&old]; + match &mut object { + ObjectType::SortEntitiesTable(table) => { + let source_block = table.block_owner_handle; + let mut ordered: Vec<_> = source + .entities() + .filter(|e| { + e.common().owner_handle == source_block + && !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) + }) + .map(|e| { + ( + table.effective_sort_handle(e.common().handle), + e.common().handle, + ) + }) + .collect(); + ordered.sort_by_key(|(sort, entity)| (sort.value(), entity.value())); + table.handle = new; + table.owner_handle = handles[&table.owner_handle]; + table.block_owner_handle = handles[&source_block]; + table.clear(); + table.raw_dxf_codes = None; + table.raw_dxf_version = None; + for (_, entity) in ordered { + table.add_entry(handles[&entity], target.allocate_handle()); + } + } + ObjectType::Field(field) => { + field.handle = new; + field.owner = handles[&field.owner]; + for h in field + .child_fields + .iter_mut() + .chain(field.referenced_objects.iter_mut()) + { + if let Some(new) = handles.get(h) { + *h = *new; + } + } + for value in std::iter::once(&mut field.value) + .chain(field.child_values.iter_mut().map(|c| &mut c.value)) + { + if let Some(handle) = value.handle_value.as_mut() { + if let Some(new) = handles.get(handle) { + *handle = *new; + } + } + } + } + ObjectType::FieldList(fields) => { + fields.handle = new; + fields.owner = handles[&fields.owner]; + for h in &mut fields.fields { + if let Some(new) = handles.get(h) { + *h = *new; + } + } + } + ObjectType::PlaceHolder(placeholder) => { + placeholder.handle = new; + placeholder.owner = handles[&placeholder.owner]; + } + ObjectType::ImageDefinitionReactor(reactor) => { + reactor.handle = new; + reactor.owner = handles[&reactor.owner]; + if let Some(new) = handles.get(&reactor.image_handle) { + reactor.image_handle = *new; + } + } + _ => super::command_driver::remap_object(&mut object, new, &remap), + } + if let ObjectType::XRecord(record) = &mut object { + for reference in &mut record.object_references { + if let Some(new) = handles.get(&reference.handle) { + reference.handle = *new; + } + } + record.reactors = record + .reactors + .iter() + .filter_map(|h| handles.get(h).copied()) + .collect(); + record.xdictionary_handle = record + .xdictionary_handle + .map(|h| handles.get(&h).copied().unwrap_or(h)); + record.raw_dwg_data = None; + record.raw_dwg_version = None; + } + target.objects.insert(new, object); + } + for (_, name) in &records { + let has_attributes = target + .entities_in_block(name) + .any(|entity| matches!(entity, EntityType::AttributeDefinition(_))); + target + .block_records + .get_mut(name) + .unwrap() + .flags + .has_attributes = has_attributes; + for entity in target.entities_in_block(name) { + if let EntityType::Insert(insert) = entity { + if crate::scene::block_service::would_create_cycle( + &target, + name, + &insert.block_name, + ) { + return Err(crate::scene::block_service::circular_dependency_error( + name, + &insert.block_name, + )); + } + } + } + } + for (old, new) in &handles { + if let Some(field) = source.fields.get(old) { + let mut field = field.clone(); + field.handle = *new; + field.owner = handles.get(&field.owner).copied().unwrap_or(field.owner); + for object in &mut field.objects { + if let Some(new) = handles.get(object) { + *object = *new; + } + } + target.fields.insert(*new, field); + } + if let Some(scale) = source.context_scales.get(old) { + target.context_scales.insert(*new, *scale); + } + } + Ok(DefinitionCopy { + record: handles[&root.handle], + document: target, + handles, + }) +} diff --git a/src/app/block_test.rs b/src/app/block_test.rs new file mode 100644 index 000000000..729c4d829 --- /dev/null +++ b/src/app/block_test.rs @@ -0,0 +1,163 @@ +use super::{document::DocumentTab, Message, OpenCADStudio}; +use codec::entities::{AttributeEntity, Entity, Insert}; +use codec::EntityType; +use iced::Task; + +pub(super) struct BlockTestSession { + pub source_id: u64, +} + +impl OpenCADStudio { + pub(super) fn refuse_block_test_save(&mut self, i: usize) -> bool { + if self.tabs[i].block_test.is_none() { + return false; + } + self.command_line.push_error( + crate::t!("Test Block is temporary. Close it and save from the Block Editor.").as_ref(), + ); + true + } + + pub(super) fn open_block_test(&mut self, i: usize) -> Task { + let source_id = self.tabs[i] + .block_test + .as_ref() + .map_or(self.tabs[i].id, |test| test.source_id); + let Some(i) = self.tabs.iter().position(|tab| tab.id == source_id) else { + return Task::none(); + }; + let Some(session) = self.tabs[i].active_block_edit_session() else { + self.command_line + .push_error(crate::t!("No block editor is open.").as_ref()); + return Task::none(); + }; + let name = session.block_name.clone(); + let definition = session.br_handle; + self.finish_pending_history(i); + let mut document = self.tabs[i].scene.document.clone(); + // Keep definitions and their native relationships in the clone. Only + // drawing-space contents are replaced by the temporary reference. + let spaces: Vec<_> = document + .block_records + .iter() + .filter(|record| record.is_model_space() || record.is_paper_space()) + .map(|record| record.handle) + .collect(); + let removed: Vec<_> = document + .entities() + .filter(|entity| { + spaces.contains(&entity.common().owner_handle) + && !matches!(entity, EntityType::Block(_) | EntityType::BlockEnd(_)) + }) + .map(|entity| entity.common().handle) + .collect(); + for handle in removed { + document.remove_entity(handle); + } + for record in document + .block_records + .iter_mut() + .filter(|record| spaces.contains(&record.handle)) + { + record.entity_handles.retain(|handle| { + *handle == record.block_entity_handle || *handle == record.block_end_handle + }); + record.insert_handles.clear(); + record.insert_count_bytes.clear(); + } + document.header.show_model_space = true; + crate::scene::block_visibility::finish_editing(&mut document, definition); + let record = document.block_records.get(&name).unwrap(); + let base = record.base_point; + let mut insert = Insert::new(&name, codec::types::Vector3::ZERO); + insert.common.layer = self.tabs[i].active_layer.clone(); + insert.common.handle = document.allocate_handle(); + insert.common.owner_handle = document.header.model_space_block_handle; + let transform = crate::scene::render_graph::insert_transform_with_base(&insert, base); + for entity in document.entities_in_block(&name) { + if let EntityType::AttributeDefinition(definition) = entity { + if !definition.flags.constant { + let mut attribute = AttributeEntity::from_definition(definition, None); + attribute.attdef_handle = definition.common.handle; + attribute.apply_transform(&transform); + insert.attributes.push(attribute); + } + } + } + for attribute in &mut insert.attributes { + attribute.common.handle = document.allocate_handle(); + attribute.common.owner_handle = insert.common.handle; + if let Some(text) = &mut attribute.embedded_mtext { + text.common.handle = document.allocate_handle(); + text.common.owner_handle = attribute.common.handle; + text.common.raw_record = None; + text.common.xdictionary_handle = None; + text.common.reactors.clear(); + } + } + if !insert.attributes.is_empty() { + insert.seqend_handle = Some(document.allocate_handle()); + } + if let Err(error) = document.add_entity(EntityType::Insert(insert)) { + self.command_line.push_error(&error.to_string()); + return Task::none(); + } + let mut tab = DocumentTab::new_drawing(0); + tab.tab_title = crate::tf!("Test Block Window - {}", name).into_owned(); + tab.block_test = Some(BlockTestSession { source_id }); + tab.active_layer = self.tabs[i].active_layer.clone(); + tab.scene.document = document; + tab.scene.named_parameters = self.tabs[i].scene.named_parameters.clone(); + tab.scene.parametric_constraints = self.tabs[i] + .scene + .parametric_constraints + .iter() + .filter(|set| { + matches!( + set.scope, + crate::scene::parametric_constraints::ParametricScope::Block(_) + ) + }) + .cloned() + .collect(); + tab.scene.material_base_dir = self.tabs[i].scene.material_base_dir.clone(); + tab.scene.sync_annotation_scale_from_header(); + tab.scene.rebuild_derived_caches(); + tab.scene.set_current_layout("Model".into()); + self.discard_block_tests(source_id); + self.visibility_popup = None; + self.grip_popup = None; + self.grip_hover = None; + self.tabs.push(tab); + self.active_tab = self.tabs.len() - 1; + self.sync_ribbon_layers(); + self.sync_ribbon_styles(); + self.sync_ribbon_from_selection(); + self.refresh_block_palette(); + self.refresh_properties(); + self.ribbon.set_block_editing(false); + self.ribbon.set_block_testing(true); + self.dispatch_command("ZOOM EXTENTS") + } + + pub(super) fn close_block_test(&mut self, i: usize) -> Task { + if self.tabs[i].block_test.is_none() { + self.command_line + .push_error(crate::t!("No Test Block is open.").as_ref()); + return Task::none(); + } + self.on_tab_close(i) + } + + pub(super) fn discard_block_tests(&mut self, source_id: u64) { + for index in (0..self.tabs.len()).rev() { + if self.tabs[index] + .block_test + .as_ref() + .is_some_and(|test| test.source_id == source_id) + { + let _ = self.on_tab_close(index); + } + } + } +} diff --git a/src/app/block_test_tests.rs b/src/app/block_test_tests.rs new file mode 100644 index 000000000..8fa9a2647 --- /dev/null +++ b/src/app/block_test_tests.rs @@ -0,0 +1,346 @@ +use super::{Message, ModalKind, OpenCADStudio}; +use codec::entities::{Insert, Line}; +use codec::types::Vector3; +use codec::{EntityType, Handle}; + +fn fixture() -> (OpenCADStudio, usize, Handle) { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let i = app.active_tab; + app.tabs[i].scene.define_block_raw( + "SAMPLE", + Vector3::new(10.0, 20.0, 0.0), + vec![EntityType::Line(Line::from_points( + Vector3::new(10.0, 20.0, 0.0), + Vector3::new(15.0, 20.0, 0.0), + ))], + ); + app.tabs[i] + .scene + .document + .block_records + .get_mut("SAMPLE") + .unwrap() + .base_point = Vector3::new(10.0, 20.0, 0.0); + let reference = app.tabs[i] + .scene + .document + .add_entity(EntityType::Insert(Insert::new( + "SAMPLE", + Vector3::new(100.0, 200.0, 0.0), + ))) + .unwrap(); + let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value())); + (app, i, reference) +} + +#[test] +fn block_test_uses_unsaved_definition_and_preserves_editor_history_and_view() { + let (mut app, source, _) = fixture(); + assert_eq!( + app.automation_op(r#"{"op":"run","cmd":"CIRCLE 12,20 2"}"#)["ok"], + true + ); + app.finish_pending_history(source); + let document = app.tabs[source].scene.document.clone(); + let depth = app.tabs[source].history.undo_stack.len(); + let camera = app.tabs[source].scene.camera.borrow().clone(); + let source_id = app.tabs[source].id; + assert_eq!( + app.automation_op(r#"{"op":"run","cmd":"BTESTBLOCK"}"#)["ok"], + true + ); + let test = app.active_tab; + assert_ne!(app.tabs[test].id, source_id); + assert!(app.tabs[test].active_block_edit.is_none()); + assert_eq!( + app.tabs[test].scene.document.block_records.get("SAMPLE"), + document.block_records.get("SAMPLE") + ); + let model = app.tabs[test] + .scene + .document + .header + .model_space_block_handle; + let top: Vec<_> = app.tabs[test] + .scene + .document + .entities() + .filter(|e| { + e.common().owner_handle == model + && !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) + }) + .collect(); + assert_eq!(top.len(), 1); + let EntityType::Insert(reference) = top[0] else { + panic!("missing test reference") + }; + assert_eq!(reference.insert_point, Vector3::ZERO); + assert_eq!(reference.block_name, "SAMPLE"); + assert!(app.tabs[test].history.undo_stack.is_empty()); + assert_eq!( + app.automation_op(r#"{"op":"run","cmd":"CIRCLE 1000,1000 8"}"#)["ok"], + true + ); + assert_eq!( + app.automation_op(r#"{"op":"run","cmd":"BCLOSETESTBLOCK"}"#)["ok"], + true + ); + assert_eq!(app.active_tab, source); + assert_eq!(app.tabs[source].scene.document, document); + assert_eq!(app.tabs[source].history.undo_stack.len(), depth); + assert_eq!(app.tabs[source].scene.camera.borrow().target, camera.target); + assert!(app.active_modal.is_none()); + app.undo_steps(1); + assert_eq!( + app.tabs[source] + .scene + .document + .entities_in_block("SAMPLE") + .count(), + 1 + ); +} + +#[test] +fn block_test_keytips_follow_context_and_offer_close() { + let (mut app, source, _) = fixture(); + let tab_tip = |app: &OpenCADStudio, target: &str| { + app.ribbon.keytips_json(false)["tips"] + .as_array() + .unwrap() + .iter() + .any(|tip| tip["target"] == target) + }; + app.ribbon.show_keytips(false); + assert!(!tab_tip(&app, "block_test")); + let _ = app.ribbon.keytip_type(Some("H")); + assert!(tab_tip(&app, "LINE")); + assert!(!tab_tip(&app, "BCLOSETESTBLOCK")); + let _ = app.ribbon.hide_keytips(); + let _ = app.open_block_test(source); + app.ribbon.show_keytips(false); + assert_eq!(app.ribbon.keytips_json(false)["tab"], "draw"); + assert!(!tab_tip(&app, "block_test")); + assert!(!tab_tip(&app, "block_editor")); + let _ = app.ribbon.keytip_type(Some("H")); + assert!(tab_tip(&app, "LINE")); + assert!(tab_tip(&app, "Close")); + assert!(tab_tip(&app, "BCLOSETESTBLOCK")); + let _ = app.ribbon.keytip_type(Some("C")); + let crate::ui::ribbon::KeyTipStep::Run(close) = app.ribbon.keytip_type(Some("T")) else { + panic!("close key tip did not dispatch a ribbon action"); + }; + assert!(matches!( + &close, + Message::RibbonToolClick { event: crate::modules::ModuleEvent::Command(command), .. } + if command == "BCLOSETESTBLOCK" + )); + let _ = app.ribbon.hide_keytips(); + let _ = app.update(close); + app.ribbon.show_keytips(false); + assert!(!tab_tip(&app, "block_test")); + assert!(tab_tip(&app, "block_editor")); + assert_eq!(app.ribbon.keytips_json(false)["tab"], "block_editor"); + let _ = app.ribbon.keytip_type(Some("H")); + assert!(!tab_tip(&app, "Close")); + assert!(!tab_tip(&app, "BCLOSETESTBLOCK")); + let _ = app.ribbon.hide_keytips(); +} + +#[test] +fn block_test_close_x_discards_dirty_edits_and_repeated_test_refreshes() { + let (mut app, source, _) = fixture(); + let _ = app.open_block_test(source); + let first = app.tabs[app.active_tab].id; + let _ = app.open_block_test(app.active_tab); + assert!(!app.tabs.iter().any(|tab| tab.id == first)); + assert_eq!( + app.tabs + .iter() + .filter(|tab| tab.block_test.is_some()) + .count(), + 1 + ); + let i = app.active_tab; + app.tabs[i].dirty = true; + let _ = app.on_tab_close(i); + assert_eq!(app.active_tab, source); + assert!(app.pending_close.is_none()); + assert!(app.active_modal.is_none()); +} + +#[test] +fn block_test_refuses_persistence_and_replacing_the_scratch_drawing() { + let (mut app, source, _) = fixture(); + let _ = app.open_block_test(source); + let test = app.active_tab; + for command in [ + "SAVE", + "QSAVE", + "SAVEAS", + "SAVEALL", + "BSAVE", + "BSAVEAS:OTHER", + "BEDIT_OPEN:SAMPLE", + "REFEDIT", + ] { + let request = serde_json::json!({"op":"run", "cmd":command}); + assert_eq!( + app.automation_op(&request.to_string())["ok"], + false, + "{command}" + ); + } + assert_eq!( + app.automation_op(r#"{"op":"save","path":"blocked-test.dwg"}"#)["ok"], + false + ); + assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], false); + let _ = app.update(Message::SaveFile); + let _ = app.update(Message::SaveAs); + assert!(app.active_modal.is_none()); + assert_eq!(app.active_tab, test); + assert!(app.tabs[test].current_path.is_none()); + assert!(app.active_save_jobs.is_empty()); + app.tabs[test].dirty = true; + let _ = app.update(Message::AutoSave); + assert!(app.active_save_jobs.is_empty()); +} + +#[test] +fn block_test_parent_close_cancel_keeps_test_and_discard_removes_it() { + let (mut app, source, _) = fixture(); + app.automation_op(r#"{"op":"run","cmd":"CIRCLE 12,20 2"}"#); + let _ = app.open_block_test(source); + let test_id = app.tabs[app.active_tab].id; + let _ = app.on_tab_close(source); + assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges)); + let _ = app.update(Message::BlockEditChangesCancel); + assert!(app.tabs.iter().any(|tab| tab.id == test_id)); + let _ = app.on_tab_close(source); + let _ = app.update(Message::BlockEditChangesDiscard); + assert!(!app.tabs.iter().any(|tab| tab.id == test_id)); + assert!(!app.tabs.iter().any(|tab| tab.block_test.is_some())); +} + +#[test] +fn block_test_no_editor_is_refused() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let count = app.tabs.len(); + let _ = app.open_block_test(app.active_tab); + assert_eq!(app.tabs.len(), count); + assert!(app.command_line.last_error.is_some()); +} + +#[test] +fn block_test_reordering_before_its_source_keeps_close_targets_stable() { + let (mut app, source, _) = fixture(); + let source_id = app.tabs[source].id; + let _ = app.open_block_test(source); + let test = app.active_tab; + app.tabs.swap(source, test); + app.active_tab = test; + let _ = app.dispatch_command("BEDIT_DISCARD"); + assert!(!app.tabs.iter().any(|tab| tab.block_test.is_some())); + assert_eq!(app.tabs[app.active_tab].id, source_id); + assert!(app.tabs[app.active_tab].active_block_edit.is_none()); + assert_eq!(app.tabs.len(), 1); +} + +#[test] +fn block_test_unsaved_visibility_states_work_without_changing_the_editor() { + let (mut app, source, _) = fixture(); + let doc = &mut app.tabs[source].scene.document; + let owner = doc.block_records.get("SAMPLE").unwrap().handle; + let first = doc.block_records.get("SAMPLE").unwrap().entity_handles[0]; + let mut line = Line::from_points(Vector3::new(10.0, 23.0, 0.0), Vector3::new(15.0, 23.0, 0.0)); + line.common.owner_handle = owner; + let second = doc.add_entity(EntityType::Line(line)).unwrap(); + let parameter = crate::scene::block_visibility::create(doc, owner, Vector3::ZERO).unwrap(); + crate::scene::block_visibility::add_state(doc, parameter, "Alternative", 0).unwrap(); + crate::scene::block_visibility::set_visible(doc, parameter, 0, &[second], false); + crate::scene::block_visibility::set_visible(doc, parameter, 1, &[first], false); + let original = app.tabs[source].scene.document.clone(); + let _ = app.open_block_test(source); + let test = app.active_tab; + let doc = &app.tabs[test].scene.document; + let model = doc.header.model_space_block_handle; + let reference = doc + .entities() + .find(|e| e.common().owner_handle == model && matches!(e, EntityType::Insert(_))) + .unwrap() + .common() + .handle; + assert!(doc.dynamic_visibility_for_insert(reference).is_some()); + app.apply_visibility_state(reference, 1); + app.finish_pending_history(test); + let doc = &app.tabs[test].scene.document; + let EntityType::Insert(insert) = doc.get_entity(reference).unwrap() else { + unreachable!() + }; + assert_ne!(insert.block_name, "SAMPLE"); + let visible: Vec<_> = doc + .entities_in_block(&insert.block_name) + .filter(|e| !e.common().invisible) + .collect(); + assert_eq!(visible.len(), 1); + let EntityType::Line(line) = visible[0] else { + panic!("expected the alternative line") + }; + assert_eq!(line.start.y, 23.0); + assert_eq!(app.tabs[source].scene.document, original); + app.visibility_popup = Some(super::visibility::VisibilityPopup { + insert_handle: reference, + anchor: iced::Point::ORIGIN, + items: vec!["Alternative".into()], + current: Some(1), + }); + let _ = app.close_block_test(test); + assert!(app.visibility_popup.is_none()); + assert_eq!(app.tabs[source].scene.document, original); +} + +#[test] +fn block_test_attributes_use_defaults_and_the_stored_base_point() { + use codec::entities::AttributeDefinition; + let (mut app, source, _) = fixture(); + let doc = &mut app.tabs[source].scene.document; + let owner = doc.block_records.get("SAMPLE").unwrap().handle; + let mut attribute = AttributeDefinition::new("TAG".into(), "Value".into(), "default".into()); + attribute.insertion_point = Vector3::new(12.0, 23.0, 0.0); + attribute.common.owner_handle = owner; + let definition = doc + .add_entity(EntityType::AttributeDefinition(attribute)) + .unwrap(); + let _ = app.open_block_test(source); + let doc = &app.tabs[app.active_tab].scene.document; + let model = doc.header.model_space_block_handle; + let insert = doc + .entities() + .find_map(|entity| match entity { + EntityType::Insert(insert) if insert.common.owner_handle == model => Some(insert), + _ => None, + }) + .unwrap(); + assert_eq!(insert.attributes.len(), 1); + let attribute = &insert.attributes[0]; + assert_eq!(attribute.value, "default"); + assert_eq!(attribute.insertion_point, Vector3::new(2.0, 3.0, 0.0)); + assert_eq!(attribute.attdef_handle, definition); + assert!(!attribute.common.handle.is_null()); + assert_eq!(attribute.common.owner_handle, insert.common.handle); +} + +#[test] +fn block_test_close_others_discards_the_test_without_prompting() { + let (mut app, source, _) = fixture(); + let _ = app.open_block_test(source); + app.tabs[app.active_tab].dirty = true; + let test = app.tabs[app.active_tab].id; + let _ = app.update(Message::DocTabCloseOthers(source)); + assert!(!app.tabs.iter().any(|tab| tab.id == test)); + assert!(app.active_modal.is_none()); + assert!(app.tabs[source].active_block_edit.is_some()); +} diff --git a/src/app/command_driver/clipboard.rs b/src/app/command_driver/clipboard.rs index 9c1ffc150..1bfcfce95 100644 --- a/src/app/command_driver/clipboard.rs +++ b/src/app/command_driver/clipboard.rs @@ -66,6 +66,46 @@ impl OpenCADStudio { None } + /// ARRAYRECT / ARRAYPOLAR with Associative on: the objects become the + /// source of an associative array, in one undo step. + pub(super) fn handle_assoc_array( + &mut self, + mut handles: Vec, + spec: crate::scene::assoc_array::AssocArraySpec, + ) { + let i = self.active_tab; + handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.restore_pre_cmd_tangent(); + if handles.is_empty() { + return; + } + let label = self.history_label_from_active_cmd(i, "ARRAY"); + let source_point = crate::app::helpers::entities_lower_left_by_bbox( + &self.tabs[i].scene.document, + &handles, + ); + self.push_undo_snapshot(i, label.clone()); + match crate::scene::assoc_array::create_from_spec( + &mut self.tabs[i].scene, + &handles, + source_point, + &spec, + ) { + Ok(array) => { + self.tabs[i].scene.deselect_all(); + self.tabs[i].scene.select_entity(array, false); + self.command_line + .push_output(crate::tf!("{label}: associative array created.").as_ref()); + } + Err(error) => self.command_line.push_error(&format!("{label}: {error}")), + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + pub(super) fn handle_replace_many( &mut self, replacements: Vec<(Handle, Vec)>, diff --git a/src/app/command_driver/entity_commit.rs b/src/app/command_driver/entity_commit.rs index 3416e9015..043a85ac7 100644 --- a/src/app/command_driver/entity_commit.rs +++ b/src/app/command_driver/entity_commit.rs @@ -990,6 +990,35 @@ impl OpenCADStudio { pub(super) fn handle_attreq_needed(&mut self, block_name: String) -> Option> { let i = self.active_tab; + // When active space is a block editor session (BEDIT), refuse inserting + // any block that would create a circular nesting dependency. + if let Some(edit_handle) = self.tabs[i].scene.block_edit_block { + let container_name = self.tabs[i] + .scene + .document + .block_records + .iter() + .find(|br| br.handle == edit_handle) + .map(|br| br.name.clone()); + if let Some(container) = container_name { + if crate::scene::block_service::would_create_cycle( + &self.tabs[i].scene.document, + &container, + &block_name, + ) { + self.command_line.push_error( + &crate::scene::block_service::circular_dependency_error( + &container, + &block_name, + ), + ); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + return None; + } + } + } // Collect the full AttributeDefinitions owned by this block // record so each created attribute keeps its geometry (#255). let mut attdefs: Vec = { diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index b34b45038..62dc55ec2 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -80,7 +80,7 @@ use crate::command::{CmdResult, SelectionEntity, StepInput}; use codec::Handle; use iced::Task; -mod clipboard; +pub(super) mod clipboard; mod constraint; mod dim_edit; mod entity_commit; @@ -95,7 +95,8 @@ mod utilities; mod viewport; pub(crate) use clipboard::{ - copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, PasteSpace, + copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, remap_object, + PasteSpace, }; impl OpenCADStudio { @@ -169,6 +170,7 @@ impl OpenCADStudio { &result, CmdResult::Relaunch(..) | CmdResult::Dispatch(..) + | CmdResult::QSelectSample { .. } | CmdResult::SolidEdgeBlend { .. } | CmdResult::SolidSubtract { .. } | CmdResult::SliceEntities { .. } @@ -436,6 +438,13 @@ impl OpenCADStudio { // runtime or messages such as a chosen render mode are dropped. let mut dispatched = Task::none(); match result { + CmdResult::QSelectSample { + state, + tab_id, + handle, + } => { + self.finish_qselect_sample(*state, tab_id, handle); + } CmdResult::OpenAutoConstrainSettings => { self.handle_open_auto_constrain_settings(); } @@ -598,6 +607,9 @@ impl OpenCADStudio { return task; } } + CmdResult::AssocArray { handles, spec } => { + self.handle_assoc_array(handles, spec); + } CmdResult::ReplaceMany(replacements, additions) => { if let Some(task) = self.handle_replace_many(replacements, additions) { return task; @@ -874,6 +886,21 @@ impl OpenCADStudio { CmdResult::ZoomToWindow { p1, p2 } => { self.handle_zoom_to_window(p1, p2); } + CmdResult::Show { p1, p2 } => { + let i = self.active_tab; + self.tabs[i].scene.remember_current_view(); + self.tabs[i].scene.zoom_to_window(p1.as_vec3(), p2.as_vec3()); + } + CmdResult::CommitAndZoom { entity, p1, p2 } => { + self.handle_commit_entities_and_exit(vec![entity], false, false); + self.handle_zoom_to_window(p1, p2); + } + CmdResult::ScaleViewports { handles, factor } => { + self.handle_scale_viewports(handles, factor); + } + CmdResult::TwistView(twist) => { + self.handle_twist_view(twist); + } CmdResult::Measurement(msg) => { self.handle_measurement(msg); } @@ -1284,7 +1311,9 @@ impl OpenCADStudio { self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); self.restore_pre_cmd_tangent(); - self.command_line.push_output(&msg); + if !msg.is_empty() { + self.command_line.push_output(&msg); + } } fn handle_report_measurement(&mut self, msg: String) { diff --git a/src/app/command_driver/tests.rs b/src/app/command_driver/tests.rs index da8faf9f5..502b646ad 100644 --- a/src/app/command_driver/tests.rs +++ b/src/app/command_driver/tests.rs @@ -1284,12 +1284,9 @@ mod bedit_characterization_tests { assert!(app.tabs[tab].block_edits.is_empty()); } - /// Documenting: the REFEDIT guard lives only in the `BEDIT` picker arm. - /// `BEDIT_BEGIN:` (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. + /// Every entry point must refuse while REFEDIT is active. #[test] - fn documenting_bedit_begin_bypasses_refedit_guard() { + fn locked_bedit_begin_refuses_active_refedit() { let mut app = OpenCADStudio::new_for_test(); let (insert, _) = setup_block_and_insert(&mut app); let tab = app.active_tab; @@ -1297,8 +1294,8 @@ mod bedit_characterization_tests { 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" + app.tabs[tab].active_block_edit.is_none(), + "begin must refuse while REFEDIT is active" ); } @@ -1323,12 +1320,9 @@ mod bedit_characterization_tests { // ---- 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. + /// Discard removes local before-images before drawing undo resumes. #[test] - fn documenting_modify_discard_then_undo_probes_stale_before_image() { + fn locked_modify_discard_then_undo_does_not_restore_local_edits() { let mut app = OpenCADStudio::new_for_test(); let (insert, members) = setup_block_and_insert(&mut app); let line = members[0]; @@ -1367,10 +1361,9 @@ mod bedit_characterization_tests { ); } - /// Documenting: undo during a session crosses the entry boundary. A - /// single undo after an in-session edit may revert the BEDIT_BEGIN step. + /// Local undo restores both the entity store and block membership. #[test] - fn documenting_undo_inside_session_crosses_entry_boundary() { + fn locked_session_undo_restores_membership_without_crossing_entry() { let mut app = OpenCADStudio::new_for_test(); let (insert, _) = setup_block_and_insert(&mut app); begin_bedit(&mut app, insert); @@ -1381,38 +1374,33 @@ mod bedit_characterization_tests { 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" + 1, + "block membership must match the undone entity store" ); 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_eq!(dangling, 0, "every member must have an 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. + /// Opening starts an empty local history; undo cannot cross entry. #[test] - fn documenting_undo_across_entry_leaves_session_open() { + fn locked_session_entry_has_no_undo_step() { 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"); + assert_eq!(depth_after_begin, 0, "opening must not record an undo step"); app.undo_steps(1); assert!( @@ -1421,10 +1409,9 @@ mod bedit_characterization_tests { ); } - /// Documenting: BEDIT on a block while another block is open nests a - /// second session. Records the resulting stack. + /// Switching a clean block replaces the single session. #[test] - fn documenting_nested_bedit_opens_second_session() { + fn locked_clean_switch_replaces_the_session() { let mut app = OpenCADStudio::new_for_test(); let (insert, _) = setup_block_and_insert(&mut app); let tab = app.active_tab; @@ -1448,15 +1435,14 @@ mod bedit_characterization_tests { begin_bedit(&mut app, other_insert); assert_eq!( app.tabs[tab].block_edits.len(), - 2, - "documenting: a second BEDIT pushes a nested session" + 1, + "a drawing has only one block-editor session" ); } - /// Documenting: BEDIT_BEGIN pushes an undo entry, and BEDIT_SAVE and - /// BEDIT_DISCARD push none. Phase 1 may change this. + /// No-change Save and Discard leave the drawing history untouched. #[test] - fn documenting_save_and_discard_push_no_undo_entry() { + fn locked_clean_save_and_discard_leave_history_unchanged() { let mut app = OpenCADStudio::new_for_test(); let (insert, _) = setup_block_and_insert(&mut app); begin_bedit(&mut app, insert); diff --git a/src/app/command_driver/transform.rs b/src/app/command_driver/transform.rs index 2a158a622..421cdc274 100644 --- a/src/app/command_driver/transform.rs +++ b/src/app/command_driver/transform.rs @@ -24,6 +24,9 @@ impl OpenCADStudio { // component so each member keeps its own dimensions. Other // transforms retain their selected-object behavior. let pending = self.begin_undo(i, label, handles.len(), true); + let handles = self.tabs[i] + .scene + .transform_picked_segments(&handles, &transform); self.tabs[i].scene.transform_entities(&handles, &transform); self.apply_inferred_constraints(i, &handles); self.tabs[i].dirty = true; diff --git a/src/app/command_driver/utilities.rs b/src/app/command_driver/utilities.rs index 2f995013f..38e35314d 100644 --- a/src/app/command_driver/utilities.rs +++ b/src/app/command_driver/utilities.rs @@ -98,6 +98,7 @@ impl OpenCADStudio { self.otrack_cross = None; self.otrack_kind = None; self.axis_lock_dir = None; + self.angle_override = None; self.dyn_user_reshaped = false; self.dyn_coord_absolute = false; self.grip_hover = None; @@ -506,6 +507,32 @@ impl OpenCADStudio { StepInput::SelectionComplete(kept) } + /// Keeps only the kinds of object the active command takes, dropping + /// the rest of a completed selection silently. + fn filter_command_entity_selection(&mut self, input: StepInput) -> StepInput { + let StepInput::SelectionComplete(handles) = input else { + return input; + }; + let i = self.active_tab; + let Some(keep) = self.tabs[i] + .active_cmd + .as_ref() + .and_then(|command| command.selection_entity_filter()) + else { + return StepInput::SelectionComplete(handles); + }; + let (kept, dropped): (Vec, Vec) = handles + .into_iter() + .partition(|h| self.tabs[i].scene.document.get_entity(*h).is_some_and(keep)); + for handle in &dropped { + self.tabs[i].scene.deselect_entity(*handle); + } + if !dropped.is_empty() { + self.refresh_properties(); + } + StepInput::SelectionComplete(kept) + } + /// Drops locked point clouds from a completed selection when the active /// command would move them, saying which were locked and how many went. fn filter_locked_point_cloud_selection(&mut self, input: StepInput) -> StepInput { @@ -561,6 +588,7 @@ impl OpenCADStudio { let input = self.filter_associative_dimension_selection(input); let input = self.filter_block_reference_selection(input); let input = self.filter_locked_point_cloud_selection(input); + let input = self.filter_command_entity_selection(input); // XCLIP / CLIP learn which chosen references carry a clip already. { let i = self.active_tab; @@ -595,6 +623,7 @@ impl OpenCADStudio { if !self.command_point_allowed(i, *point) { return (Task::none(), true); } + self.angle_override = None; // Typed / dynamic-input / headless points carry no snap, but the // accepted-snap list must stay index-parallel with the points the // command collects. Interactive picks record themselves in @@ -1130,6 +1159,9 @@ impl OpenCADStudio { self.apply_snap_override(kind); return Task::none(); } + if self.try_angle_override(token) { + return Task::none(); + } if let Some((coord, kind)) = crate::app::helpers::parse_coord(token) { // Match the GUI command line: typed coordinates are in the active // UCS (relative offsets are rotated by the UCS axes), so a multi- @@ -1161,7 +1193,13 @@ impl OpenCADStudio { // bare number is the step's own value, not a distance โ€” ROTATE's // angle, SCALE's factor, CIRCLE's radius. Only a token no step // claims becomes direct distance entry, measured along the cursor - // direction from the command's anchor. + // direction from the command's anchor. Under a typed `, factor: f64) { + let i = self.active_tab; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + let handles: Vec = handles + .into_iter() + .filter(|handle| { + matches!( + self.tabs[i].scene.document.get_entity(*handle), + Some(codec::EntityType::Viewport(viewport)) if viewport.id > 1 + ) + }) + .collect(); + if handles.is_empty() { + return; + } + self.push_undo_snapshot(i, "MVSETUP"); + for handle in handles { + self.tabs[i] + .scene + .set_viewport_scale_factor_for(handle, factor); + } + self.tabs[i].dirty = true; + } + + /// Turn the active view about its line of sight so the drawing shows + /// `twist` radians counter-clockwise from its untwisted orientation. + pub(in crate::app) fn handle_twist_view(&mut self, twist: f64) { + let i = self.active_tab; + self.tabs[i].scene.remember_current_view(); + let set = |camera: &mut crate::scene::view::camera::Camera| { + // The drawing turns one way when the camera rolls the other. + camera.roll_by(-twist as f32 - camera.roll()); + }; + if self.tabs[i].scene.active_viewport.is_some() { + self.tabs[i].scene.mutate_active_viewport_camera(set); + } else { + set(&mut self.tabs[i].scene.camera.borrow_mut()); + } + self.tabs[i].scene.camera_generation += 1; + if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) { + self.command_line.push_info(&prompt); + } + } + pub(super) fn handle_set_plot_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) { let i = self.active_tab; let layout_name = self.tabs[i].scene.current_layout.clone(); diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index e4e869ed0..b6f985351 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -95,6 +95,23 @@ impl OpenCADStudio { } } + /// PASTESPEC with the formats on hand: the copied objects, and `text` + /// from the system clipboard once it has been read. + pub(in crate::app) fn paste_special_command( + &self, + i: usize, + text: Option<&str>, + ) -> Box { + let document = &self.tabs[i].scene.document; + let defaults = crate::scene::creation_style::current_text_defaults(document); + Box::new(crate::modules::draw::clipboard::pastespec::PasteSpecCommand::new( + !self.clipboard.is_empty(), + text, + defaults.style_name, + defaults.height, + )) + } + pub(super) fn dispatch_blocks(&mut self, cmd: &str, i: usize) -> Option> { match cmd { // โ”€โ”€ BASE โ€” drawing insertion base point โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -284,6 +301,58 @@ impl OpenCADStudio { } } + // ARRAYEDIT โ€” edit an associative array; a preselected one skips + // the pick. + "ARRAYEDIT" => { + use crate::modules::draw::modify::arrayedit::ArrayEditCommand; + let selected = self.tabs[i].scene.selected_handles_in_order(); + let array = match selected.as_slice() { + [handle] => Some(*handle), + _ => None, + }; + self.tabs[i].scene.deselect_all(); + let command = ArrayEditCommand::new(array); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + + // ARRAYEDIT Source: edit the objects the array repeats in the + // Block Editor; every item follows when it is saved. + cmd if cmd.starts_with("ARRAYEDIT_SOURCE:") => { + let handle = codec::Handle::new( + cmd["ARRAYEDIT_SOURCE:".len()..].parse().unwrap_or(0), + ); + let document = &self.tabs[i].scene.document; + let source = crate::scene::assoc_array::array_of_insert(document, handle) + .and_then(|array| { + document + .block_records + .iter() + .find(|record| record.handle == array.source_block) + .map(|record| record.name.clone()) + }); + match source { + Some(name) => return Some(self.dispatch_command(&format!("BEDIT_OPEN:{name}"))), + None => self.command_line.push_error( + crate::t!("ARRAYEDIT: the selected object is not an associative array.") + .as_ref(), + ), + } + } + + // PASTESPEC โ€” offer the copied objects now; the system clipboard's + // text joins the formats once read (Message::PasteSpecial). + "PASTESPEC" => { + let command = self.paste_special_command(i, None); + let started = self.start_count_command(i, command); + #[cfg(target_arch = "wasm32")] + let read = Task::perform(crate::sys::read_clipboard_text(), Message::PasteSpecial); + #[cfg(not(target_arch = "wasm32"))] + let read = iced::clipboard::read_text() + .map(|result| Message::PasteSpecial(result.ok().map(|text| (*text).clone()))); + return Some(Task::batch([started, read])); + } + // PASTEORIG โ€” paste at the entities' original coordinates (no pick). "PASTEORIG" => { if self.clipboard.is_empty() { @@ -371,7 +440,7 @@ impl OpenCADStudio { let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard); use crate::modules::insert::insert_block::InsertBlockCommand; let cmd = - InsertBlockCommand::new_for_block(name, wires, base.as_vec3()); + InsertBlockCommand::new_for_block(name, wires, base); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } @@ -839,7 +908,7 @@ impl OpenCADStudio { .split_once(char::is_whitespace) .map(|(verb, rest)| (verb.to_ascii_uppercase(), rest.trim())) .unwrap_or_else(|| (cmd.to_ascii_uppercase(), "")); - if verb == "PDFATTACH" && rest.is_empty() { + if verb == "PDFATTACH" && rest.is_empty() && crate::sys::file_dia() { return Some(Task::done(Message::PdfAttachPick)); } let insunits = self.tabs[i].scene.document.header.insertion_units; @@ -921,17 +990,30 @@ impl OpenCADStudio { } } - // EXTERNALREFERENCES โ€” toggle the docked External References panel. + // EXTERNALREFERENCES opens the docked External References panel + // (an open one stays open); EXTERNALREFERENCESCLOSE closes it. // (XREFMAN was a provisional name and is not kept, not even as an // alias.) The XREF command below is untouched. "EXTERNALREFERENCES" => { - self.show_external_references ^= true; - if self.show_external_references { - // Always open expanded so the panel is immediately usable; - // dock it on the right if the user closed it from the layout. - self.dock_open_at_default(crate::ui::dock::PanelId::ExternalReferences); - self.refresh_xref_manager(); - } + self.show_external_references = true; + // Always open expanded so the panel is immediately usable; + // dock it on the right if the user closed it from the layout. + self.dock_open_at_default(crate::ui::dock::PanelId::ExternalReferences); + self.refresh_xref_manager(); + } + "EXTERNALREFERENCESCLOSE" => self.show_external_references = false, + // ERSTATE (read only): whether the panel is open. + cmd if cmd + .strip_prefix("SETVAR ") + .unwrap_or(cmd) + .split_whitespace() + .next() + .is_some_and(|name| name.eq_ignore_ascii_case("ERSTATE")) => + { + self.command_line.push_output(&format!( + "ERSTATE = {} (read only)", + u8::from(self.show_external_references) + )); } cmd if cmd.eq_ignore_ascii_case("XREF") diff --git a/src/app/commands/blockvars.rs b/src/app/commands/blockvars.rs index 8a19cde58..89046fd74 100644 --- a/src/app/commands/blockvars.rs +++ b/src/app/commands/blockvars.rs @@ -16,6 +16,9 @@ pub(super) const BLOCK_SYSVARS: &[&str] = &[ "BLOCKSTATE", "EXPLMODE", "INSNAME", + "REFEDITNAME", + "XEDIT", + "XFADECTL", ]; /// The `SETVAR ?` line for this family. @@ -45,6 +48,15 @@ impl OpenCADStudio { self.command_line .push_output(&format!("BLOCKSTATE = {state} (read only)")); } + "REFEDITNAME" => { + let name = self.tabs[i] + .refedit_session + .as_ref() + .map(|s| s.block_name.clone()) + .unwrap_or_default(); + self.command_line + .push_output(&format!("REFEDITNAME = \"{name}\" (read only)")); + } "INSNAME" | "BLOCKNAVIGATE" => { let current = if name == "INSNAME" { self.insname.clone() @@ -77,6 +89,8 @@ impl OpenCADStudio { let (current, max) = match name.as_str() { "BLOCKMRULIST" => (self.block_mru_list as i32, 100), "BLOCKREDEFINEMODE" => (self.block_redefine_mode as i32, 2), + "XEDIT" => (self.tabs[i].scene.document.header.xedit as i32, 1), + "XFADECTL" => (crate::scene::cache::block_cache::refedit_fade_ctl(), 90), _ => (explmode(&self.tabs[i].scene.document) as i32, 1), }; let ask = |app: &mut Self, name: String| { @@ -109,6 +123,23 @@ impl OpenCADStudio { self.block_redefine_mode = n as u8; self.persist_settings_if_changed(); } + "XEDIT" => { + if n != current { + self.push_undo_snapshot(i, "XEDIT"); + self.tabs[i].scene.document.header.xedit = n == 1; + self.tabs[i].dirty = true; + } + } + "XFADECTL" => { + crate::scene::cache::block_cache::set_refedit_fade_ctl(n); + self.persist_settings_if_changed(); + for tab in &mut self.tabs { + if !tab.is_start { + tab.scene.recolor_meshes(); + tab.scene.bump_geometry(); + } + } + } _ => { if n != current { self.push_undo_snapshot(i, "EXPLMODE"); diff --git a/src/app/commands/bvisibility.rs b/src/app/commands/bvisibility.rs new file mode 100644 index 000000000..c70a66626 --- /dev/null +++ b/src/app/commands/bvisibility.rs @@ -0,0 +1,188 @@ +//! BVSTATE, BVHIDE and BVSHOW: the visibility states of the block open in +//! the Block Editor (see `scene::block_visibility`). + +use crate::app::{Message, OpenCADStudio}; +use crate::scene::block_visibility as visibility; +use codec::{CadDocument, EntityType, Handle}; +use iced::Task; + +impl OpenCADStudio { + /// The block open in the Block Editor, or an error naming `command`. + fn visibility_block(&mut self, i: usize, command: &str) -> Option { + let block = self.tabs[i] + .active_block_edit_session() + .map(|session| session.br_handle); + if block.is_none() { + self.command_line.push_error( + crate::tf!("{}: open the block in the Block Editor first (BEDIT).", command) + .as_ref(), + ); + } + block + } + + /// The block's visibility parameter, made when it has none: its grip + /// sits on the block's base point. + fn visibility_parameter(&mut self, i: usize, block: Handle) -> Option { + let document = &mut self.tabs[i].scene.document; + let base = document + .block_records + .iter() + .find(|record| record.handle == block) + .and_then(|record| match document.get_entity(record.block_entity_handle) { + Some(EntityType::Block(begin)) => Some(begin.base_point), + _ => None, + }) + .unwrap_or(codec::types::Vector3::ZERO); + visibility::create(document, block, base) + } + + /// The state the block shows in the editor. + fn visibility_current(&self, i: usize, block: Handle) -> usize { + let scene = &self.tabs[i].scene; + scene.visibility_states.get(&block).copied().unwrap_or_else(|| { + visibility::parameter(&scene.document, block) + .map_or(0, |parameter| visibility::shown_state(&scene.document, parameter.handle)) + }) + } + + fn show_visibility_state(&mut self, i: usize, block: Handle, parameter: Handle, state: usize) { + visibility::apply_state(&mut self.tabs[i].scene.document, parameter, state); + self.tabs[i].scene.visibility_states.insert(block, state); + self.tabs[i].scene.bump_geometry(); + self.tabs[i].dirty = true; + if let Some(name) = visibility::state_names(&self.tabs[i].scene.document, parameter) + .get(state) + { + self.command_line + .push_output(crate::tf!("Current visibility state: {}", name).as_ref()); + } + } + + pub(super) fn dispatch_bvisibility(&mut self, cmd: &str, i: usize) -> Option> { + use crate::command::CadCommand; + use crate::modules::draw::modify::bvstate::{BvStateCommand, SEP}; + match cmd { + "BVSTATE" => { + let block = self.visibility_block(i, "BVSTATE")?; + let document = &self.tabs[i].scene.document; + let names = visibility::parameter(document, block) + .map(|parameter| visibility::state_names(document, parameter.handle)) + .unwrap_or_else(|| vec!["VisibilityState0".to_string()]); + let current = self.visibility_current(i, block); + if let Some(name) = names.get(current) { + self.command_line + .push_output(crate::tf!("Current visibility state: {}", name).as_ref()); + } + let command = BvStateCommand::new(names, current); + self.command_line.push_info(&command.prompt()); + self.command_line.set_step_options(command.options()); + self.tabs[i].active_cmd = Some(Box::new(command)); + Some(Task::none()) + } + cmd if cmd.starts_with("_BVSTATE") => { + let block = self.visibility_block(i, "BVSTATE")?; + let fields: Vec<&str> = cmd.split(SEP).skip(1).collect(); + self.push_undo_snapshot(i, "BVSTATE"); + let parameter = self.visibility_parameter(i, block)?; + let current = self.visibility_current(i, block); + let document = &mut self.tabs[i].scene.document; + let index = |document: &CadDocument, name: &str| { + visibility::state_names(document, parameter) + .iter() + .position(|candidate| candidate.eq_ignore_ascii_case(name)) + }; + let shown = match fields.as_slice() { + ["NEW", name] => visibility::add_state(document, parameter, name, current), + ["SET", name] => index(document, name), + ["RENAME", from, to] => { + if let Some(state) = index(document, from) { + visibility::rename_state(document, parameter, state, to); + } + Some(current) + } + ["DELETE", name] => index(document, name).map(|state| { + visibility::delete_state(document, parameter, state); + let count = visibility::state_names(document, parameter).len(); + if current == state || current >= count { + 0 + } else if current > state { + current - 1 + } else { + current + } + }), + _ => None, + }; + self.show_visibility_state(i, block, parameter, shown.unwrap_or(current)); + self.tabs[i].active_cmd = None; + Some(Task::none()) + } + "BVHIDE" | "BVSHOW" => { + let block = self.visibility_block(i, cmd)?; + let hide = cmd == "BVHIDE"; + if !hide { + // Every object of the block shows while they are picked; + // the state is put back once the pick ends. + let document = &mut self.tabs[i].scene.document; + let members: Vec = document + .block_records + .iter() + .find(|record| record.handle == block) + .map(|record| record.entity_handles.clone()) + .unwrap_or_default(); + for handle in members { + if let Some(entity) = document.get_entity_mut(handle) { + entity.common_mut().invisible = false; + } + } + self.tabs[i].scene.bump_geometry(); + } + let pending = if hide { "_BVHIDE" } else { "_BVSHOW" }; + if !self.tabs[i].scene.selected.is_empty() { + return Some(self.dispatch_command(pending)); + } + let prompt = if hide { + "Select objects to hide:" + } else { + "Select objects to make visible:" + }; + use crate::modules::draw::select::SelectObjectsCommand; + let command = SelectObjectsCommand::with_prompt(cmd, pending, prompt); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + Some(Task::none()) + } + "_BVHIDE" | "_BVSHOW" => { + let block = self.visibility_block(i, &cmd[1..])?; + let handles: Vec = self.tabs[i] + .scene + .selected_handles_in_order() + .into_iter() + .filter(|handle| { + self.tabs[i] + .scene + .document + .get_entity(*handle) + .is_some_and(|entity| entity.common().owner_handle == block) + }) + .collect(); + self.push_undo_snapshot(i, &cmd[1..]); + let parameter = self.visibility_parameter(i, block)?; + let current = self.visibility_current(i, block); + visibility::set_visible( + &mut self.tabs[i].scene.document, + parameter, + current, + &handles, + cmd == "_BVSHOW", + ); + self.tabs[i].scene.deselect_all(); + self.show_visibility_state(i, block, parameter, current); + self.tabs[i].active_cmd = None; + Some(Task::none()) + } + _ => None, + } + } +} diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index 88674128c..52ba30aff 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -188,7 +188,7 @@ impl OpenCADStudio { } } - fn start_count_command(&mut self, i: usize, command: Box) -> Task { + pub(in crate::app) fn start_count_command(&mut self, i: usize, command: Box) -> Task { self.reset_command_start_state(i); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(command); diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 112981216..ba4a89d96 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -935,6 +935,21 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } + "DVIEW" => { + use crate::modules::view::dview::DviewCommand; + let twist = { + let scene = &self.tabs[i].scene; + let roll = match scene.active_viewport { + Some(_) => scene.navigation_camera().roll(), + None => scene.camera.borrow().roll(), + }; + -(roll as f64) + }; + let cmd = DviewCommand::new(twist.rem_euclid(std::f64::consts::TAU)); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + "ZOOM" | "ZOOM WINDOW" | "ZOOM W" | "ZW" => { use crate::modules::view::zoom_window::ZoomWindowCommand; let new_cmd = ZoomWindowCommand::new(); @@ -1288,6 +1303,38 @@ impl OpenCADStudio { } } + // TXTEXP โ€” text becomes the polylines of its character outlines. + "TXTEXP" => { + let selected: Vec<_> = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .filter(|(handle, _)| !self.tabs[i].scene.is_layer_locked(*handle)) + .collect(); + if selected.is_empty() { + use crate::modules::draw::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("TXTEXP"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::draw::modify::explode::apply_explode_replacements; + use crate::modules::draw::modify::txtexp::plan_text_explode; + let replacements = plan_text_explode(&selected, &self.tabs[i].scene.document); + let exploded = replacements.len(); + if exploded > 0 { + self.push_undo_snapshot(i, "TXTEXP"); + apply_explode_replacements(&mut self.tabs[i].scene, replacements); + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line + .push_output(crate::tf!("{exploded} object(s) exploded.").as_ref()); + } else { + self.command_line + .push_info(crate::t!("TXTEXP: no text selected.").as_ref()); + } + } + } + "OFFSET" => { use crate::modules::draw::modify::offset::{is_offsettable, OffsetCommand}; let all_entities: Vec<_> = self.tabs[i] @@ -1386,8 +1433,17 @@ impl OpenCADStudio { .map(|e| (h, e)) }) .collect(); + let initial_edges: Vec = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .filter_map(|(handle, entity)| { + crate::modules::draw::modify::trim::is_trim_boundary_entity(&entity) + .then_some(handle) + }) + .collect(); let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = ExtendCommand::new(all_entities); + let new_cmd = ExtendCommand::with_boundary_edges(all_entities, initial_edges); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index 43191aa5b..fb37ba4ac 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -618,6 +618,11 @@ impl OpenCADStudio { "CLEANSCREEN" => { return Some(Task::done(Message::ToggleCleanScreen)); } + // โ”€โ”€ RIBBON / RIBBONCLOSE โ€” show or hide the ribbon โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + "RIBBON" | "RIBBONCLOSE" => { + self.ribbon_closed = cmd == "RIBBONCLOSE"; + self.persist_settings_if_changed(); + } // โ”€โ”€ QUICKPROPERTIES โ€” toggle the floating quick-properties readout โ”€โ”€โ”€ "QUICKPROPERTIES" => { return Some(Task::done(Message::ToggleQuickProperties)); @@ -935,6 +940,11 @@ impl OpenCADStudio { return Some(Task::done(Message::StlExport)); } + // FBXEXPORT โ€” export 3D meshes to FBX for visualisation tools + "FBXEXPORT" | "EXPORTFBX" => { + return Some(Task::done(Message::FbxExport)); + } + // STEPOUT โ€” export 3D meshes to STEP AP203 format "STEPOUT" | "EXPORTSTEP" | "STPOUT" => { return Some(Task::done(Message::StepExport)); @@ -951,7 +961,9 @@ impl OpenCADStudio { "PLOT" | "PRINT" => { return Some(Task::done(Message::PlotDialogOpen)); } - "PRINTALL" => { + // PUBLISH: the layouts of the drawing, each with its own page + // setup, into one multi-page PDF or to the printer. + "PRINTALL" | "PUBLISH" => { return Some(Task::done(Message::PrintAllOpen)); } // PRINTERS โ€” what the system reports about printing, on the @@ -1379,102 +1391,155 @@ impl OpenCADStudio { return Some(Task::none()); } - // ADJUST โ€” set brightness / contrast / fade on selected raster images - // ADJUST BRIGHTNESS|CONTRAST|FADE <0-100> - "ADJUST" => { - use crate::command::SelectThenKeywordCommand; - let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); - let c = SelectThenKeywordCommand::new( - "ADJUST", - "ADJUST [Brightness / Contrast / Fade]:", - vec![ - ( - "Brightness", - "BRIGHTNESS", - Some("ADJUST brightness 0-100:"), - ), - ("Contrast", "CONTRAST", Some("ADJUST contrast 0-100:")), - ("Fade", "FADE", Some("ADJUST fade 0-100:")), - ], - has_sel, - ); - self.command_line.push_info(&c.prompt()); - self.tabs[i].active_cmd = Some(Box::new(c)); - } - cmd if cmd.starts_with("ADJUST ") => { - let rest = cmd.trim_start_matches("ADJUST").trim(); - let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect(); - let action = parts.first().map(|s| s.to_uppercase()).unwrap_or_default(); - let arg = parts.get(1).copied().unwrap_or("").trim(); - let handles: Vec = self.tabs[i] + // IMAGEADJUST: chosen images (pickfirst first), then the dialog. + "IMAGEADJUST" => { + let keep = crate::modules::insert::xadjust::keeps("I"); + let chosen: Vec = self.tabs[i] .scene .selected_entities() - .iter() - .map(|(h, _)| *h) - .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .into_iter() + .filter(|(_, e)| keep(e)) + .map(|(h, _)| h) .collect(); - if handles.is_empty() { - self.command_line - .push_error(crate::t!("ADJUST: select raster image(s) first.").as_ref()); - } else if action.is_empty() { - self.command_line.push_info( - crate::t!("Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>").as_ref(), + if chosen.is_empty() { + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); + let command = crate::modules::draw::select::SelectObjectsCommand::filtered( + "IMAGEADJUST", + "_IMAGEADJUSTSEL", + crate::modules::insert::xadjust::select_prompt("I"), + keep, ); - } else if let Ok(v) = arg.parse::() { - let v = v.min(100); - self.push_undo_snapshot(i, "ADJUST"); - let mut changed = 0usize; - let mut changed_handles = Vec::new(); - for h in &handles { - if let Some(codec::EntityType::RasterImage(img)) = self.tabs[i] - .scene - .document - .entities_mut() - .find(|e| e.common().handle == *h) - { - match action.as_str() { - "BRIGHTNESS" => { - img.brightness = v; - changed += 1; - changed_handles.push(*h); - } - "CONTRAST" => { - img.contrast = v; - changed += 1; - changed_handles.push(*h); - } - "FADE" => { - img.fade = v; - changed += 1; - changed_handles.push(*h); - } - _ => {} - } - } - } - if changed > 0 { - self.tabs[i].dirty = true; - for &handle in &changed_handles { - self.tabs[i].scene.reseed_derived_caches(handle); - } - let changes: Vec<_> = changed_handles - .into_iter() - .map(|handle| (handle, crate::scene::ChangeKind::Modified)) - .collect(); - self.tabs[i].scene.bump_entities(&changes); - self.command_line.push_output( - crate::tf!("ADJUST: {action} = {v} on {changed} image(s).").as_ref(), - ); - } else { - self.command_line.push_error( - crate::t!("ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE).").as_ref(), - ); - } + use crate::command::CadCommand; + self.command_line.push_info(&command.prompt()); + self.command_line.set_step_options(command.options()); + self.tabs[i].active_cmd = Some(Box::new(command)); } else { - self.command_line - .push_error(crate::t!("ADJUST: value must be 0-100.").as_ref()); + self.tabs[i] + .scene + .replace_selection(chosen.into_iter().collect()); + return Some(self.dispatch_command("_IMAGEADJUSTSEL")); } } + "_IMAGEADJUSTSEL" => { + self.open_image_adjust(i); + return Some(self.finish_dispatch("IMAGEADJUST")); + } + // ADJUST / -IMAGEADJUST / PDFADJUST / DWFADJUST / DGNADJUST: + // chosen images or underlays (pickfirst first), then the option + // and the value. + "ADJUST" | "-IMAGEADJUST" | "PDFADJUST" | "DWFADJUST" | "DGNADJUST" => { + let mode = match cmd { + "-IMAGEADJUST" => "I", + "PDFADJUST" => "P", + "DWFADJUST" => "W", + "DGNADJUST" => "G", + _ => "A", + }; + let keep = crate::modules::insert::xadjust::keeps(mode); + let chosen: Vec = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .filter(|(_, e)| keep(e)) + .map(|(h, _)| h) + .collect(); + if !chosen.is_empty() { + self.tabs[i] + .scene + .replace_selection(chosen.into_iter().collect()); + return Some(self.dispatch_command(&format!("_ADJUSTSEL {mode}"))); + } + self.start_adjust_selection(i, cmd, mode); + } + // The gathered objects: one kind at a time, then the option. + c if c.starts_with("_ADJUSTSEL ") => { + use crate::command::CadCommand; + use crate::modules::insert::xadjust::{self, AdjustCommand}; + let mode = c["_ADJUSTSEL ".len()..].trim().to_string(); + let keep = xadjust::keeps(&mode); + let name = xadjust::command_name(&mode); + let objects: Vec<(codec::Handle, codec::EntityType)> = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .filter(|(h, e)| keep(e) && !self.tabs[i].scene.is_layer_locked(*h)) + .map(|(h, e)| (h, e.clone())) + .collect(); + match AdjustCommand::for_objects(&objects) { + None => {} + Some(Err(message)) => { + self.command_line.push_error(crate::t!(message).as_ref()); + self.start_adjust_selection(i, name, &mode); + } + Some(Ok(command)) => { + self.command_line.push_info(&command.prompt()); + self.command_line.set_step_options(command.options()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + } + // Repeating runs the command, not this step. + return Some(self.finish_dispatch(name)); + } + // `_ADJUSTAPPLY ` + // writes the value to every listed image or underlay, one undo step. + c if c.starts_with("_ADJUSTAPPLY ") => { + let mut parts = c.split_whitespace().skip(1); + let prop = parts.next().unwrap_or("").to_string(); + let value: u8 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(0); + let handles: Vec = parts + .filter_map(|h| u64::from_str_radix(h, 16).ok()) + .map(codec::Handle::new) + .collect(); + self.push_undo_snapshot(i, "ADJUST"); + let mut changed = Vec::new(); + for h in &handles { + let done = match self.tabs[i].scene.document.get_entity_mut(*h) { + Some(codec::EntityType::RasterImage(img)) => match prop.as_str() { + "BRIGHTNESS" => { + img.brightness = value; + true + } + "CONTRAST" => { + img.contrast = value; + true + } + "FADE" => { + img.fade = value; + true + } + _ => false, + }, + Some(codec::EntityType::Underlay(u)) => match prop.as_str() { + "CONTRAST" => { + u.set_contrast(value); + true + } + "FADE" => { + u.set_fade(value); + true + } + "MONOCHROME" => { + u.set_monochrome(value != 0); + true + } + _ => false, + }, + _ => false, + }; + if done { + self.tabs[i].scene.reseed_derived_caches(*h); + changed.push((*h, crate::scene::ChangeKind::Modified)); + } + } + if !changed.is_empty() { + self.tabs[i].scene.bump_entities(&changed); + self.tabs[i].dirty = true; + } + // The command used the set up; nothing stays chosen. + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); + } // ANNOSCALE / CANNOSCALE โ€” set the current annotation scale // (e.g. 1:50, 2:1, or a plain factor). Drives annotative-object size @@ -2049,7 +2114,7 @@ impl OpenCADStudio { } } - "UNDERLAYLAYERS" | "ULAYERS" => self.open_underlay_layers_dialog(i), + "ULAYERS" => self.open_underlay_layers_dialog(i), "RECAP" | "SYNCPVIEWPORTS" => { self.command_line .push_info(crate::tf!("{cmd}: not yet implemented.").as_ref()); @@ -2204,6 +2269,113 @@ pub(crate) fn table_to_csv(table: &codec::entities::Table) -> String { .join("\r\n") } +impl OpenCADStudio { + /// The ADJUST family's object prompt, keeping only the kinds `mode` + /// takes (see `xadjust::keeps`). + fn start_adjust_selection(&mut self, i: usize, name: &str, mode: &str) { + use crate::command::CadCommand; + use crate::modules::insert::xadjust; + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); + let command = crate::modules::draw::select::SelectObjectsCommand::filtered( + name, + &format!("_ADJUSTSEL {mode}"), + xadjust::select_prompt(mode), + xadjust::keeps(mode), + ); + self.command_line.push_info(&command.prompt()); + self.command_line.set_step_options(command.options()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + + /// The Image Adjust dialog for the chosen images, starting from the + /// first one's values; nothing opens without an image. + fn open_image_adjust(&mut self, i: usize) { + let keep = crate::modules::insert::xadjust::keeps("I"); + let scene = &self.tabs[i].scene; + let images: Vec<(codec::Handle, codec::EntityType)> = scene + .selected_entities() + .into_iter() + .filter(|(h, e)| keep(e) && !scene.is_layer_locked(*h)) + .map(|(h, e)| (h, e.clone())) + .collect(); + let Some((_, codec::EntityType::RasterImage(first))) = images.first() else { + return; + }; + let canvas = scene.bg_color; + let state = crate::ui::window::pdf_dialogs::ImageAdjustState::new( + images.iter().map(|(h, _)| *h).collect(), + &first.file_path, + [first.brightness, first.contrast, first.fade], + [0, 1, 2].map(|k| (canvas[k].clamp(0.0, 1.0) * 255.0).round() as u8), + ); + self.image_adjust = Some(state); + self.active_modal = Some(crate::app::ModalKind::ImageAdjust); + } + + pub(in crate::app) fn update_image_adjust( + &mut self, + message: crate::ui::window::pdf_dialogs::PdfDialogMsg, + ) -> Task { + use crate::ui::window::pdf_dialogs::{PdfDialogMsg as M, IMAGE_ADJUST_DEFAULTS}; + let i = self.active_tab; + let Some(state) = self.image_adjust.as_mut() else { + return Task::none(); + }; + match message { + M::IaValue(row, value) => state.set(row as usize, value), + M::IaStep(row, dir) => { + let k = row as usize; + state.set(k, state.values[k].saturating_add_signed(dir.signum())); + } + // A box takes whole numbers 0โ€“100 only; anything else is not + // typed in. An emptied box keeps the value until a number comes. + M::IaText(row, typed) => { + let k = row as usize; + let typed = typed.trim().to_string(); + if typed.is_empty() { + state.texts[k] = typed; + } else if let Some(value) = typed + .parse::() + .ok() + .filter(|v| *v <= 100 && typed.chars().all(|c| c.is_ascii_digit())) + { + state.set(k, value); + } + } + M::IaReset => state.set_all(IMAGE_ADJUST_DEFAULTS), + M::IaApply => { + let Some(state) = self.image_adjust.take() else { + return Task::none(); + }; + self.close_active_modal(); + let [brightness, contrast, fade] = state.values; + self.push_undo_snapshot(i, "IMAGEADJUST"); + let mut changed = Vec::new(); + for h in &state.handles { + if let Some(codec::EntityType::RasterImage(img)) = + self.tabs[i].scene.document.get_entity_mut(*h) + { + img.brightness = brightness; + img.contrast = contrast; + img.fade = fade; + self.tabs[i].scene.reseed_derived_caches(*h); + changed.push((*h, crate::scene::ChangeKind::Modified)); + } + } + if !changed.is_empty() { + self.tabs[i].scene.bump_entities(&changed); + self.tabs[i].dirty = true; + } + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); + } + _ => {} + } + Task::none() + } +} + #[cfg(test)] mod tests { use crate::app::OpenCADStudio; diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 0dc6605db..ccdb7ba5c 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -764,7 +764,8 @@ impl OpenCADStudio { }); let new_cmd = HatchCommand::new(outlines, boundary_sources, selected, inherited, plane) - .with_origin(self.tabs[i].scene.document.hatch_origin()); + .with_origin(self.tabs[i].scene.document.hatch_origin()) + .with_associative(self.hatch_associative); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); self.refresh_area_preview(i); @@ -2147,6 +2148,11 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(subtract)); } + // SECTIONCULL โ€” erase what lies outside a section object. + "SECTIONCULL" => { + return Some(self.section_cull()); + } + // INTERFERE โ€” non-destructive intersect: solid from the overlap. "INTERFERE" => { return Some(self.solid_interfere()); @@ -2307,6 +2313,9 @@ impl OpenCADStudio { self.tabs[i].scene.erase_entities(&sources); self.refresh_properties(); } + // The command consumes the selection, kept sources included. + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); self.tabs[i].dirty = true; self.command_line .push_output(crate::tf!("REGION: created {count} region(s).").as_ref()); @@ -2540,16 +2549,37 @@ impl OpenCADStudio { .filter(|handle| { matches!( scene.document.get_entity(*handle), - Some(codec::EntityType::Solid3D(_) | codec::EntityType::Surface(_)) + Some( + codec::EntityType::Solid3D(_) + | codec::EntityType::Surface(_) + | codec::EntityType::Mesh(_) + | codec::EntityType::PolygonMesh(_) + | codec::EntityType::PolyfaceMesh(_) + ) ) }) .collect::>(); scene.restore_solid_models(&targets); - targets.retain(|handle| scene.solid_models.contains_key(handle)); + let mesh_extent = |handle: &codec::Handle| { + let entity = scene.document.get_entity(*handle)?; + let (vertices, _) = crate::entities::mesh::mesh_polygons(entity)?; + vertices.iter().fold(None, |bounds, point| { + let (mut min, mut max) = bounds.unwrap_or((*point, *point)); + for axis in 0..3 { + min[axis] = f64::min(min[axis], point[axis]); + max[axis] = f64::max(max[axis], point[axis]); + } + Some((min, max)) + }) + }; + targets.retain(|handle| { + scene.solid_models.contains_key(handle) || mesh_extent(handle).is_some() + }); let bounds = targets .iter() - .filter_map(|handle| { - crate::scene::model::solid_model::extent(&scene.solid_models[handle]) + .filter_map(|handle| match scene.solid_models.get(handle) { + Some(body) => crate::scene::model::solid_model::extent(body), + None => mesh_extent(handle), }) .fold(None::<([f64; 3], [f64; 3])>, |bounds, (low, high)| { Some(match bounds { diff --git a/src/app/commands/fileops.rs b/src/app/commands/fileops.rs index b9e6d75a9..825763781 100644 --- a/src/app/commands/fileops.rs +++ b/src/app/commands/fileops.rs @@ -38,7 +38,7 @@ impl OpenCADStudio { let mut saved = 0usize; let mut skipped = 0usize; for t in 0..self.tabs.len() { - if self.tabs[t].is_start { + if self.tabs[t].is_start || self.tabs[t].block_test.is_some() { continue; } if let Some(path) = self.tabs[t].current_path.clone() { diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index 2d1ed0a4e..3cc1305f7 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -457,37 +457,7 @@ impl OpenCADStudio { self.open_attedit_dialog(); } - // โ”€โ”€ REFEDIT โ€” in-place block editing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - "REFEDIT" => { - use crate::modules::draw::modify::refedit::RefEditPickCommand; - // If a session is already active, tell the user. - if self.tabs[i].refedit_session.is_some() { - self.command_line.push_error( - crate::t!("REFEDIT: a session is already active. Use REFCLOSE first.") - .as_ref(), - ); - } else { - // Check if a single INSERT is already selected. - let selected: Vec<_> = - self.tabs[i].scene.selected_entities().into_iter().collect(); - if selected.len() == 1 { - if let Some(codec::EntityType::Insert(_)) = selected.first().map(|(_, e)| e) - { - let handle = selected[0].0; - // Skip pick phase โ€” jump straight to begin. - let _ = - self.dispatch_command(&format!("REFEDIT_BEGIN:{}", handle.value())); - return Some(Task::none()); - } - } - let cmd_obj = RefEditPickCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); - } - } - "BEDIT" => { - use crate::modules::draw::modify::block_edit::BlockEditPickCommand; if self.tabs[i].refedit_session.is_some() { self.command_line.push_error( crate::t!("BEDIT: finish the active REFEDIT (REFCLOSE) first.").as_ref(), @@ -505,36 +475,79 @@ impl OpenCADStudio { return Some(Task::none()); } } - let cmd_obj = BlockEditPickCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); + self.open_bedit_dialog(i); } } - cmd if cmd.starts_with("BEDIT_BEGIN:") => { - use crate::modules::draw::modify::block_edit::BlockEditSession; - use codec::Handle; - - let handle_u64: u64 = cmd["BEDIT_BEGIN:".len()..].parse().unwrap_or(0); - let insert_handle = Handle::new(handle_u64); - if self.reject_locked_edit(i, insert_handle) { + cmd if cmd.starts_with("BEDIT_NEW:") => { + if self.tabs[i].refedit_session.is_some() { + self.command_line.push_error( + crate::t!("BEDIT: finish the active REFEDIT (REFCLOSE) first.").as_ref(), + ); return Some(Task::none()); } - - let block_name = match self.tabs[i].scene.document.get_entity(insert_handle) { - Some(codec::EntityType::Insert(ins)) => ins.block_name.clone(), - _ => { - self.command_line.push_error( - crate::t!("BEDIT: selected object is not a block reference.").as_ref(), - ); + let raw_name = &cmd["BEDIT_NEW:".len()..]; + let block_name = match crate::scene::block_service::validate_block_name(raw_name) { + Ok(n) => n, + Err(e) => { + self.command_line.push_error(&e.to_string()); return Some(Task::none()); } }; + if let Some(br) = self.tabs[i].scene.document.block_records.get(&block_name) { + let real_name = br.name.clone(); + return Some(self.dispatch_command(&format!("BEDIT_OPEN:{real_name}"))); + } + if self.tabs[i].active_block_edit_session().is_some() { + return Some(self.request_block_edit_close( + i, + crate::app::block_editor::BlockEditContinuation::Create(block_name), + )); + } + let pre_doc = self.tabs[i].scene.document.clone(); + self.tabs[i] + .scene + .define_block_raw(&block_name, codec::types::Vector3::ZERO, vec![]); + let Some(record) = self.tabs[i].scene.document.block_records.get(&block_name) else { + return Some(Task::none()); + }; + let br_handle = record.handle; + self.start_block_edit_session(i, block_name, br_handle, Some(pre_doc)); + } + + cmd if cmd.starts_with("BEDIT_BEGIN:") || cmd.starts_with("BEDIT_OPEN:") => { + if self.tabs[i].refedit_session.is_some() { + self.command_line.push_error( + crate::t!("BEDIT: finish the active REFEDIT (REFCLOSE) first.").as_ref(), + ); + return Some(Task::none()); + } + use codec::Handle; + + let block_name = if let Some(name) = cmd.strip_prefix("BEDIT_OPEN:") { + name.to_string() + } else { + let handle_u64: u64 = cmd["BEDIT_BEGIN:".len()..].parse().unwrap_or(0); + let insert_handle = Handle::new(handle_u64); + if self.reject_locked_edit(i, insert_handle) { + return Some(Task::none()); + } + match self.tabs[i].scene.document.get_entity(insert_handle) { + Some(codec::EntityType::Insert(ins)) => ins.block_name.clone(), + _ => { + self.command_line.push_error( + crate::t!("BEDIT: selected object is not a block reference.") + .as_ref(), + ); + return Some(Task::none()); + } + } + }; // Resolve the block record; reject external references (xrefs). let (br_handle, is_xref) = match self.tabs[i].scene.document.block_records.get(&block_name) { - Some(br) => (br.handle, br.flags.is_xref), + Some(br) => (br.handle, br.flags.is_xref || br.flags.is_xref_overlay), None => { self.command_line.push_error( crate::tf!("BEDIT: block \"{}\" not found.", block_name).as_ref(), @@ -548,540 +561,55 @@ impl OpenCADStudio { ); return Some(Task::none()); } - if self.tabs[i] - .block_edits - .iter() - .any(|session| session.br_handle == br_handle) - { - self.tabs[i].active_cmd = None; - return Some(Task::done(Message::BlockEditSwitch(block_name.clone()))); + if let Some(session) = self.tabs[i].active_block_edit_session() { + if session.br_handle == br_handle { + return Some(Task::none()); + } + return Some(self.request_block_edit_close( + i, + crate::app::block_editor::BlockEditContinuation::Switch(block_name), + )); } - // Snapshot the complete block definition so Discard can restore - // structural markers, ATTDEFs and geometry with exact handles. - let snapshot: Vec<_> = { - let br = self.tabs[i] - .scene - .document - .block_records - .get(&block_name) - .unwrap(); - br.entity_handles - .iter() - .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) - .collect() - }; - let dependent_snapshot: Vec<_> = self.tabs[i] - .scene - .block_definition_dependent_handles(br_handle) - .into_iter() - .filter_map(|handle| self.tabs[i].scene.document.get_entity_arc(handle)) - .collect(); - let reference_attributes: Vec<_> = self.tabs[i] - .scene - .document - .entities() - .filter_map(|entity| match entity { - codec::EntityType::Insert(reference) - if reference.block_name.eq_ignore_ascii_case(&block_name) - && !reference.attributes.is_empty() => - { - Some((reference.common.handle, reference.attributes.clone())) - } - _ => None, - }) - .collect(); - - self.push_undo_snapshot(i, "BEDIT"); - - // Capture the camera before the editor reframes it, so leaving - // the block editor returns the view exactly where it was (#425). - let current_camera = self.tabs[i].scene.camera.borrow().clone(); - let current_ucs = self.tabs[i].active_ucs.clone(); - let (return_layout, return_block, return_camera) = - if let Some(parent_index) = self.tabs[i].active_block_edit { - let parent = &mut self.tabs[i].block_edits[parent_index]; - parent.editor_camera = current_camera.clone(); - parent.editor_ucs = current_ucs; - ( - parent.return_layout.clone(), - Some(parent.block_name.clone()), - parent.return_camera.clone(), - ) - } else { - self.tabs[i].scene.sync_camera_to_document(); - ( - self.tabs[i].scene.current_layout.clone(), - None, - current_camera.clone(), - ) - }; - // A block editor renders in model style; switch to Model first so - // the paper-space code paths stay off, then scope to the block. - if self.tabs[i].scene.current_layout != "Model" { - self.tabs[i].scene.set_current_layout("Model".to_string()); - } - self.tabs[i].scene.block_edit_block = Some(br_handle); - self.tabs[i].block_edits.push(BlockEditSession { - block_name: block_name.clone(), - br_handle, - return_layout, - return_block, - snapshot, - dependent_snapshot, - reference_attributes, - return_camera, - editor_camera: current_camera, - editor_ucs: None, - }); - self.tabs[i].active_block_edit = Some(self.tabs[i].block_edits.len() - 1); - // Every BEDIT tab owns an isolated local UCS. Never inherit or - // overwrite the drawing's model-space UCS. - self.tabs[i].refresh_active_ucs(); - - self.tabs[i].scene.deselect_all(); - // The first click of a double-click selects the model-space - // INSERT and populates the cached grip overlay. Clearing only - // Scene::selected leaves that INSERT's grips active inside the - // block editor, where dragging one moves the outer reference - // instead of block-local geometry (#517). - self.tabs[i].active_grip = None; - self.grip_hover = None; - self.grip_popup = None; - self.visibility_popup = None; - // Entering BEDIT changes which block is assembled, not the - // cached geometry of that block's entities. - self.tabs[i].scene.bump_geometry_no_blocks(); - // Frame the camera on the block's own geometry (block-local, near - // origin) โ€” fit_all() goes through current_layout_block_handle so - // it already scopes to the edited block. Without this the view - // stays wherever model/paper space was. (#261) - self.tabs[i].scene.fit_all(); - let editor_camera = self.tabs[i].scene.camera.borrow().clone(); - if let Some(session) = self.tabs[i].active_block_edit_session_mut() { - session.editor_camera = editor_camera; - } - self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; - self.refresh_properties(); - self.tabs[i].active_cmd = None; - self.tabs[i].dirty = true; - self.command_line.push_info( - crate::tf!( - "BEDIT: Editing block \"{}\". Use Save Block or Discard to finish.", - block_name - ) - .as_ref(), - ); + self.start_block_edit_session(i, block_name, br_handle, None); } - "BEDIT_SAVE" => { - let session = match self.tabs[i].active_block_edit.take() { - Some(index) if index < self.tabs[i].block_edits.len() => { - self.tabs[i].block_edits.remove(index) - } - None => { - self.command_line - .push_error(crate::t!("BEDIT_SAVE: no block editor is open.").as_ref()); - return Some(Task::none()); - } - Some(_) => { - self.command_line.push_error( - crate::t!("BEDIT_SAVE: invalid block editor state.").as_ref(), - ); - return Some(Task::none()); - } - }; - // Edits are live on the block record โ€” just leave the block space. - self.restore_after_block_edit_close( - i, - session.return_block.as_deref(), - &session.return_layout, - &session.return_camera, - ); - self.tabs[i].dirty = true; - self.command_line.push_output( - crate::tf!( - "BEDIT: Block \"{}\" saved. All references updated.", - session.block_name - ) - .as_ref(), - ); + "BSAVE" => { + self.save_block_edit(i); } - - "BEDIT_DISCARD" => { - let session = match self.tabs[i].active_block_edit.take() { - Some(index) if index < self.tabs[i].block_edits.len() => { - self.tabs[i].block_edits.remove(index) - } - None => { - self.command_line.push_error( - crate::t!("BEDIT_DISCARD: no block editor is open.").as_ref(), - ); - return Some(Task::none()); - } - Some(_) => { - self.command_line.push_error( - crate::t!("BEDIT_DISCARD: invalid block editor state.").as_ref(), - ); - return Some(Task::none()); - } - }; - // Restore the block definition to its on-entry snapshot: remove the - // block's current entities, then re-add the snapshot ones (mirrors - // the REFCLOSE_SAVE write-back). - let old_handles: Vec<_> = match self.tabs[i] - .scene - .document - .block_records - .get(&session.block_name) - { - Some(br) => br.entity_handles.clone(), - None => vec![], - }; - for h in &old_handles { - self.tabs[i].scene.document.remove_entity(*h); - } - if let Some(br) = self.tabs[i] - .scene - .document - .block_records - .get_mut(&session.block_name) - { - br.entity_handles.clear(); - } - let block_name = session.block_name.clone(); - let return_block = session.return_block.clone(); - let return_layout = session.return_layout.clone(); - let return_camera = session.return_camera.clone(); - for mut entity in session.snapshot { - // Preserve the original handle so ATTDEF references and - // other handle-based relationships survive Discard. - entity.common_mut().owner_handle = session.br_handle; - let _ = self.tabs[i].scene.document.add_entity(entity); - } - for (handle, attributes) in session.reference_attributes { - if let Some(codec::EntityType::Insert(reference)) = - self.tabs[i].scene.document.get_entity_mut(handle) - { - reference.attributes = attributes; - } - } - for entity in session.dependent_snapshot { - let handle = entity.common().handle; - let _ = self.tabs[i] - .scene - .document - .replace_entity_arc(handle, entity); - } - self.restore_after_block_edit_close( - i, - return_block.as_deref(), - &return_layout, - &return_camera, - ); - self.tabs[i].dirty = true; - self.command_line.push_output( - crate::tf!("BEDIT: Block \"{}\" edit discarded.", block_name).as_ref(), - ); + "BTESTBLOCK" => return Some(self.open_block_test(i)), + "BCLOSETESTBLOCK" => return Some(self.close_block_test(i)), + "BSAVEAS" => { + self.open_block_save_as_dialog(i); } - - cmd if cmd.starts_with("REFEDIT_BEGIN:") => { - use crate::modules::draw::modify::refedit::{ - apply_insert_transform, RefEditSession, - }; - use codec::Handle; - - let handle_u64: u64 = cmd["REFEDIT_BEGIN:".len()..].parse().unwrap_or(0); - let insert_handle = Handle::new(handle_u64); - if self.reject_locked_edit(i, insert_handle) { - return Some(Task::none()); - } - - // Get INSERT entity. - let insert = match self.tabs[i].scene.document.get_entity(insert_handle) { - Some(codec::EntityType::Insert(ins)) => ins.clone(), - _ => { - self.command_line.push_error( - crate::t!("REFEDIT: selected object is not an INSERT.").as_ref(), - ); - return Some(Task::none()); - } - }; - - // Build the INSERT's full placement transform (OCS + rotation + - // scale, including non-uniform / mirrored, minus the block base - // point) and its inverse, so edits round-trip back to - // block-local coordinates on SAVE. The base point is part of - // the placement: without it the working copy sits Rot*Scale*base - // away from the reference the user picked. - let sx = insert.x_scale(); - let sy = insert.y_scale(); - let sz = insert.z_scale(); - let base = crate::scene::render_graph::block_base_point( - &self.tabs[i].scene.document, - &insert.block_name, - ); - let forward = crate::scene::render_graph::insert_transform_with_base(&insert, base); - let inverse = { - use codec::types::{Matrix3, Matrix4, Transform}; - let ocs_t = - Matrix4::from_matrix3(Matrix3::arbitrary_axis(insert.normal).transpose()); - let t_inv = Matrix4::translation( - -insert.insert_point.x, - -insert.insert_point.y, - -insert.insert_point.z, - ); - let r_inv = Matrix4::rotation_z(-insert.rotation); - let s_inv = Matrix4::scaling(1.0 / sx, 1.0 / sy, 1.0 / sz); - // inverse(T(-base)ยทOCSยทTยทRยทS) = T(+base)ยทSโปยนยทRโปยนยทTโปยนยทOCSแต€ - Transform::from_matrix( - Matrix4::translation(base.x, base.y, base.z) - * (s_inv * r_inv * t_inv * ocs_t), - ) - }; - - // Find the block record. - let br_handle = match self.tabs[i] - .scene - .document - .block_records - .get(&insert.block_name) - { - Some(br) => br.handle, - None => { - self.command_line.push_error( - crate::tf!("REFEDIT: block \"{}\" not found.", insert.block_name) - .as_ref(), - ); - return Some(Task::none()); - } - }; - - // Collect block-local entities (skip structural Block/BlockEnd/AttDef). - let block_entities: Vec<_> = { - let br = self.tabs[i] - .scene - .document - .block_records - .get(&insert.block_name) - .unwrap(); - br.entity_handles - .iter() - .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) - .filter(|e| { - !matches!( - e, - codec::EntityType::Block(_) - | codec::EntityType::BlockEnd(_) - | codec::EntityType::AttributeDefinition(_) - ) - }) - .collect() - }; - - if block_entities.is_empty() { + cmd if cmd.starts_with("BSAVEAS:") => { + self.save_block_edit_as(i, &cmd["BSAVEAS:".len()..]); + } + "BCLOSE" => { + if self.tabs[i].active_block_edit.is_none() { self.command_line - .push_error(crate::t!("REFEDIT: block is empty.").as_ref()); - return Some(Task::none()); - } - - let session = RefEditSession { - block_name: insert.block_name.clone(), - br_handle, - temp_handles: vec![], - forward, - inverse, - // Everything allocated from here on is part of the session. - handle_watermark: self.tabs[i] - .scene - .document - .next_handle() - .max(self.tabs[i].scene.document.header.handle_seed), - }; - - self.push_undo_snapshot(i, "REFEDIT"); - self.tabs[i].refedit_session = Some(session.clone()); - - // Add block entities to model space with INSERT transform applied. - let mut temp_handles = Vec::new(); - for mut entity in block_entities { - apply_insert_transform(&mut entity, &session); - entity.common_mut().handle = codec::Handle::NULL; - entity.common_mut().owner_handle = codec::Handle::NULL; - let h = self.tabs[i].scene.add_entity(entity); - temp_handles.push(h); - } - self.tabs[i].refedit_session.as_mut().unwrap().temp_handles = temp_handles.clone(); - - // Fade everything except the entities being edited, so the - // surrounding drawing stays visible for context but the block's - // geometry stands out. (#136) - self.tabs[i] - .scene - .set_refedit_keep(Some(temp_handles.iter().copied().collect())); - - // Select the temp entities so user can see what they're editing. - self.tabs[i].scene.deselect_all(); - for h in &temp_handles { - self.tabs[i].scene.select_entity(*h, false); - } - self.tabs[i].dirty = true; - - // No active command โ€” the user edits the block's geometry freely - // (move, grips, draw, eraseโ€ฆ) and runs REFCLOSE when done. (#136) - self.tabs[i].active_cmd = None; - self.command_line.push_info(crate::tf!( - "REFEDIT: Editing block \"{}\". Run REFCLOSE to save, REFCLOSE_DISCARD to cancel.", - insert.block_name - ).as_ref()); - } - - "REFCLOSE" => { - if self.tabs[i].refedit_session.is_some() { - use crate::modules::draw::modify::refedit::RefCloseCommand; - let cmd_obj = RefCloseCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); + .push_error(crate::t!("No block editor is open.").as_ref()); } else { + return Some(self.request_block_edit_close( + i, + crate::app::block_editor::BlockEditContinuation::Close, + )); + } + } + "BEDIT_SAVE" => { + if self.save_block_edit(i) { + self.close_block_edit(i, false); + } + } + "BEDIT_DISCARD" => { + if self.tabs[i].active_block_edit.is_none() { self.command_line - .push_error(crate::t!("REFCLOSE: no REFEDIT session active.").as_ref()); + .push_error(crate::t!("No block editor is open.").as_ref()); + } else { + self.close_block_edit(i, true); } } - "REFCLOSE_SAVE" => { - use crate::modules::draw::modify::explode::normalize_entity_for_block; - use crate::modules::draw::modify::refedit::apply_insert_inverse_transform; - - let session = match self.tabs[i].refedit_session.take() { - Some(s) => s, - None => { - self.command_line - .push_error(crate::t!("REFCLOSE: no REFEDIT session active.").as_ref()); - return Some(Task::none()); - } - }; - - self.push_undo_snapshot(i, "REFCLOSE"); - - // The working set: the temp copies PLUS everything the user - // created in this space during the session (drawn / pasted / - // offset entities carry handles at or above the watermark) โ€” - // those must fold into the block too, not stay behind in - // model space (#423). - let working: Vec = { - let space = self.tabs[i].scene.current_layout_block_handle_pub(); - let drawn = self.tabs[i].scene.document.entities().filter(|e| { - let c = e.common(); - c.owner_handle == space - && c.handle.value() >= session.handle_watermark - && !session.temp_handles.contains(&c.handle) - }); - session - .temp_handles - .iter() - .copied() - .chain(drawn.map(|e| e.common().handle)) - .collect() - }; - - // Collect the edited temp entities. - let new_entities: Vec = working - .iter() - .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) - .collect(); - - // Remove temp entities from model space. - self.tabs[i].scene.erase_entities(&working); - - // Apply inverse INSERT transform โ†’ block-local coordinates. - let new_entities: Vec<_> = new_entities - .into_iter() - .map(|mut entity| { - apply_insert_inverse_transform(&mut entity, &session); - let mut entity = normalize_entity_for_block(entity); - entity.common_mut().handle = codec::Handle::NULL; - entity.common_mut().owner_handle = session.br_handle; - entity - }) - .collect(); - - // Remove old block entities from the document. - let old_handles: Vec<_> = match self.tabs[i] - .scene - .document - .block_records - .get(&session.block_name) - { - Some(br) => br.entity_handles.clone(), - None => vec![], - }; - for h in &old_handles { - self.tabs[i].scene.document.remove_entity(*h); - } - // Flush the entity_handles list from the block record. - if let Some(br) = self.tabs[i] - .scene - .document - .block_records - .get_mut(&session.block_name) - { - br.entity_handles.clear(); - } - - // Add the new block entities. - for entity in new_entities { - let _ = self.tabs[i].scene.document.add_entity(entity); - } - - self.tabs[i].dirty = true; - self.command_line.push_output( - crate::tf!( - "REFCLOSE: Block \"{}\" saved. All references updated.", - session.block_name - ) - .as_ref(), - ); - // End the edit fade before rebuilding, so the restored geometry - // recolours bright. (#136) - self.tabs[i].scene.set_refedit_keep(None); - // Rebuild hatch/image/mesh caches since block content changed. - self.tabs[i].scene.rebuild_derived_caches(); - } - - "REFCLOSE_DISCARD" => { - let session = match self.tabs[i].refedit_session.take() { - Some(s) => s, - None => { - self.command_line - .push_error(crate::t!("REFCLOSE: no REFEDIT session active.").as_ref()); - return Some(Task::none()); - } - }; - // Remove the working set without modifying the block โ€” the - // temp copies plus anything created during the session (#423). - let working: Vec = { - let space = self.tabs[i].scene.current_layout_block_handle_pub(); - let drawn = self.tabs[i].scene.document.entities().filter(|e| { - let c = e.common(); - c.owner_handle == space - && c.handle.value() >= session.handle_watermark - && !session.temp_handles.contains(&c.handle) - }); - session - .temp_handles - .iter() - .copied() - .chain(drawn.map(|e| e.common().handle)) - .collect() - }; - self.tabs[i].scene.erase_entities(&working); - self.tabs[i].scene.deselect_all(); - // End the edit fade โ€” restore the drawing to full brightness. - self.tabs[i].scene.set_refedit_keep(None); - self.command_line - .push_output(crate::t!("REFCLOSE: Changes discarded.").as_ref()); - } - "ALIGN" => { use crate::modules::draw::modify::align::AlignCommand; @@ -1462,7 +990,88 @@ impl OpenCADStudio { Some(self.finish_dispatch(cmd)) } - fn restore_after_block_edit_close( + pub(in crate::app) fn start_block_edit_session( + &mut self, + i: usize, + block_name: String, + br_handle: codec::Handle, + unpersisted_pre_document: Option, + ) { + use crate::modules::draw::modify::block_edit::BlockEditSession; + // Capture the camera before the editor reframes it, so leaving + // the block editor returns the view exactly where it was (#425). + let current_camera = self.tabs[i].scene.camera.borrow().clone(); + let current_ucs = self.tabs[i].active_ucs.clone(); + let (return_layout, return_block, return_camera) = + if let Some(parent_index) = self.tabs[i].active_block_edit { + let parent = &mut self.tabs[i].block_edits[parent_index]; + parent.editor_camera = current_camera.clone(); + parent.editor_ucs = current_ucs; + ( + parent.return_layout.clone(), + Some(parent.block_name.clone()), + parent.return_camera.clone(), + ) + } else { + self.tabs[i].scene.sync_camera_to_document(); + ( + self.tabs[i].scene.current_layout.clone(), + None, + current_camera.clone(), + ) + }; + // A block editor renders in model style; switch to Model first so + // the paper-space code paths stay off, then scope to the block. + if self.tabs[i].scene.current_layout != "Model" { + self.tabs[i].scene.set_current_layout("Model".to_string()); + } + self.tabs[i].scene.block_edit_block = Some(br_handle); + self.tabs[i].block_edits.push(BlockEditSession { + block_name: block_name.clone(), + br_handle, + return_layout, + return_block, + return_camera, + editor_camera: current_camera, + editor_ucs: None, + unpersisted_pre_document, + }); + self.tabs[i].active_block_edit = Some(self.tabs[i].block_edits.len() - 1); + // Every BEDIT tab owns an isolated local UCS. Never inherit or + // overwrite the drawing's model-space UCS. + self.tabs[i].refresh_active_ucs(); + + self.tabs[i].scene.deselect_all(); + // The first click of a double-click selects the model-space + // INSERT and populates the cached grip overlay. Clearing only + // Scene::selected leaves that INSERT's grips active inside the + // block editor, where dragging one moves the outer reference + // instead of block-local geometry (#517). + self.tabs[i].active_grip = None; + self.grip_hover = None; + self.grip_popup = None; + self.visibility_popup = None; + // Entering BEDIT changes which block is assembled, not the + // cached geometry of that block's entities. + self.tabs[i].scene.bump_geometry_no_blocks(); + // Frame the camera on the block's own geometry (block-local, near + // origin) โ€” fit_all() goes through current_layout_block_handle so + // it already scopes to the edited block. Without this the view + // stays wherever model/paper space was. (#261) + self.tabs[i].scene.fit_all(); + let editor_camera = self.tabs[i].scene.camera.borrow().clone(); + if let Some(session) = self.tabs[i].active_block_edit_session_mut() { + session.editor_camera = editor_camera; + } + self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; + self.refresh_properties(); + self.tabs[i].active_cmd = None; + self.begin_block_edit_history(i); + self.command_line + .push_info(crate::tf!("Block Editor: editing {}", block_name).as_ref()); + } + + pub(in crate::app) fn restore_after_block_edit_close( &mut self, i: usize, return_block: Option<&str>, @@ -1510,6 +1119,10 @@ impl OpenCADStudio { self.tabs[i].scene.camera_generation += 1; self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; self.tabs[i].scene.rebuild_derived_caches(); + let layers = self.tabs[i].scene.document.layers.clone(); + let viewports = self.tabs[i].scene.viewport_list(); + self.tabs[i].layers.sync_with_viewports(&layers, viewports); + self.sync_ribbon_layers(); self.tabs[i].refresh_active_ucs(); self.refresh_properties(); self.adopt_view_display(i); diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index 03b810729..a120507c1 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -891,7 +891,11 @@ impl OpenCADStudio { self.command_line .push_error(crate::t!("Usage: VIEW DELETE ").as_ref()); } else { - if self.tabs[i].scene.document.views.remove(&name).is_some() { + if let Some(view) = self.tabs[i].scene.document.views.remove(&name) { + crate::scene::erase_background( + &mut self.tabs[i].scene.document, + view.background_handle, + ); self.command_line .push_output(crate::tf!("View '{}' deleted.", name).as_ref()); } else { @@ -909,6 +913,7 @@ impl OpenCADStudio { let found = self.tabs[i].scene.document.views.get(&name).cloned(); if let Some(v) = found { self.tabs[i].scene.restore_named_view(&v); + self.apply_view_render_mode(i, &v); self.command_line.push_output( crate::tf!("View '{}' restored.", v.name).as_ref(), ); @@ -932,6 +937,7 @@ impl OpenCADStudio { let found = self.tabs[i].scene.document.views.get(&name).cloned(); if let Some(v) = found { self.tabs[i].scene.restore_named_view(&v); + self.apply_view_render_mode(i, &v); self.command_line .push_output(crate::tf!("View '{}' restored.", v.name).as_ref()); } else { diff --git a/src/app/commands/layers.rs b/src/app/commands/layers.rs index 7560af332..d444b05c2 100644 --- a/src/app/commands/layers.rs +++ b/src/app/commands/layers.rs @@ -893,6 +893,7 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "DWGUNITS"); self.tabs[i].scene.document.header.insertion_units = to; + self.tabs[i].scene.sync_annotation_scale_from_header(); let factor = units::conversion_factor(from, to).filter(|f| (f - 1.0).abs() > 1e-12); let (from_label, to_label) = (units::label(from), units::label(to)); diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 367b7262a..467319a6d 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; mod attdef; mod blocks; mod blockvars; +mod bvisibility; mod count; mod dim; pub(crate) mod display; @@ -21,6 +22,7 @@ mod pc_extract; mod pdf_dialogs; pub(crate) mod pdf_import; mod pdf_underlay; +mod refedit; mod plotvars; mod point_cloud; pub(crate) mod sheet_set; @@ -147,6 +149,33 @@ impl OpenCADStudio { }; let resolved = self.resolve_alias(cmd); let cmd = resolved.as_deref().unwrap_or(cmd); + if self.tabs[i].block_test.is_some() { + let name = cmd.split([' ', ':']).next().unwrap_or(cmd); + if matches!( + name, + "BEDIT" + | "BEDIT_BEGIN" + | "BEDIT_OPEN" + | "REFEDIT" + | "REFEDIT_BEGIN" + | "BSAVE" + | "BSAVEAS" + | "SAVE" + | "QSAVE" + | "SAVEAS" + | "SAVEALL" + | "WBLOCK" + | "EXPORT" + | "ETRANSMIT" + | "ARCHIVE" + ) { + self.command_line.push_error( + crate::t!("Close Test Block before saving or opening another block editor.") + .as_ref(), + ); + return Task::none(); + } + } if is_spacemouse_command(cmd) { return self.run_action(cmd); } @@ -301,6 +330,9 @@ impl OpenCADStudio { if let Some(t) = self.dispatch_xref_attach(cmd, i) { return Some(t); } + if let Some(t) = self.dispatch_bvisibility(cmd, i) { + return Some(t); + } if let Some(t) = self.dispatch_blocks(cmd, i) { return Some(t); } @@ -331,6 +363,9 @@ impl OpenCADStudio { if let Some(t) = self.dispatch_dim(cmd, i) { return Some(t); } + if let Some(t) = self.dispatch_refedit(cmd, i) { + return Some(t); + } if let Some(t) = self.dispatch_inquiry(cmd, i) { return Some(t); } @@ -428,6 +463,7 @@ pub fn start_allowed(cmd: &str) -> bool { || matches!( cmd, "NEW" + | "QNEW" | "OPEN" | "EXIT" | "QUIT" @@ -489,6 +525,7 @@ inventory::submit!(crate::command::CommandRegistration { "EXPORTPDF", "EXPORTSVG", "PRINTALL", + "PUBLISH", "DDIM", // Inquiry: list the whole drawing database. "DBLIST", @@ -619,6 +656,7 @@ inventory::submit!(crate::command::CommandRegistration { "SL", "INTERFERE", "INF", + "SECTIONCULL", "PRESSPULL", "THICKEN", "3DROTATE", @@ -710,7 +748,9 @@ inventory::submit!(crate::command::CommandRegistration { "PDFOSNAP", "UOSNAP", "FIELDDISPLAY", + "FILEDIA", "SURFACEASSOCIATIVITY", + "LAYOUTCREATEVIEWPORT", "FIELDEVAL", "PDFIMPORTMODE", "PDFIMPORTFILTER", @@ -751,6 +791,10 @@ inventory::submit!(crate::command::CommandRegistration { "SKTOLERANCE", "CLIPROMPTLINES", "COMMANDLINEFADETIME", + "COMMANDLINEOPACITY", + "MENUBAR", + "RIBBON", + "RIBBONCLOSE", "COLORTHEME", "SELECTIONAREA", "SELECTIONAREAOPACITY", @@ -816,6 +860,8 @@ inventory::submit!(crate::command::CommandRegistration { "EXPORTSTEP", "EXPORTSTL", "EXTERNALREFERENCES", + "EXTERNALREFERENCESCLOSE", + "ERSTATE", "POINTCLOUDMANAGER", "POINTCLOUDMANAGERCLOSE", "EXTRIM", @@ -911,6 +957,7 @@ inventory::submit!(crate::command::CommandRegistration { "SHORTCUTS", "SSM", "STEPOUT", + "FBXEXPORT", "STLOUT", "STPOUT", "STYLE", @@ -941,6 +988,7 @@ inventory::submit!(crate::command::CommandRegistration { "XR", "XREF", "-XREF", + "-ATTACH", "XRELOAD", "ZOOM", "ZS", diff --git a/src/app/commands/pdf_dialogs.rs b/src/app/commands/pdf_dialogs.rs index ec69e6e12..ebdcf0aa1 100644 --- a/src/app/commands/pdf_dialogs.rs +++ b/src/app/commands/pdf_dialogs.rs @@ -289,7 +289,8 @@ impl OpenCADStudio { } } - /// ULAYERS: every underlay of the drawing, the selected one first shown. + /// ULAYERS: every underlay of the drawing, the selected one first shown + /// (none: an empty dialog). pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) { let document = &self.tabs[i].scene.document; let selected: Vec = self.tabs[i] @@ -329,10 +330,6 @@ impl OpenCADStudio { hidden: crate::scene::model::pdf_layers::hidden_layers(u), }); } - if targets.is_empty() { - self.command_line.push_error("No underlays found."); - return; - } let current = targets .iter() .position(|t| selected.contains(&t.handle)) @@ -439,6 +436,8 @@ impl OpenCADStudio { "attach" => crate::t!("Attaches a PDF file as an underlay."), "pointcloud" => crate::t!("Attaches a point cloud scan or project."), "pcsection" => crate::t!("Traces the points on the kept side of a live section as lines."), + "background" => std::borrow::Cow::Owned(crate::tr!("view-background", "help")), + "imageadjust" => std::borrow::Cow::Owned(crate::tr!("image-adjust", "help")), "pointcloudcolormap" => { crate::t!("Sets the color schemes and ranges a point cloud is colored by intensity or elevation.") } @@ -572,6 +571,25 @@ impl OpenCADStudio { | PdfDialogMsg::MapCurrent(_) | PdfDialogMsg::MapApply | PdfDialogMsg::MapOk) => return self.update_point_cloud_color_map(message), + message @ (PdfDialogMsg::BgKind(_) + | PdfDialogMsg::BgPickColor(_) + | PdfDialogMsg::BgColor(..) + | PdfDialogMsg::BgThree(_) + | PdfDialogMsg::BgRotation(_) + | PdfDialogMsg::BgImage(_) + | PdfDialogMsg::BgBrowse + | PdfDialogMsg::BgPosition(_) + | PdfDialogMsg::BgAspect(_) + | PdfDialogMsg::BgOffset(_) + | PdfDialogMsg::BgScale(_) + | PdfDialogMsg::BgStep(..) + | PdfDialogMsg::BgReset + | PdfDialogMsg::BgApply) => return self.update_view_background(message), + message @ (PdfDialogMsg::IaValue(..) + | PdfDialogMsg::IaText(..) + | PdfDialogMsg::IaStep(..) + | PdfDialogMsg::IaReset + | PdfDialogMsg::IaApply) => return self.update_image_adjust(message), message @ (PdfDialogMsg::SecPerimeter(_) | PdfDialogMsg::SecMaxPoints(_) | PdfDialogMsg::SecLayer(_) diff --git a/src/app/commands/refedit.rs b/src/app/commands/refedit.rs new file mode 100644 index 000000000..3c924187b --- /dev/null +++ b/src/app/commands/refedit.rs @@ -0,0 +1,928 @@ +//! REFEDIT, -REFEDIT, REFSET and REFCLOSE: in-place editing of a block +//! reference or an external reference, with the reference's prompts and +//! messages. The session itself lives in `DocumentTab::refedit_session`. + +use crate::app::{Message, OpenCADStudio}; +use crate::modules::draw::modify::refedit::{ + apply_insert_inverse_transform, apply_insert_transform, level_hits, nesting_chain, + rename_symbols, ChainLevel, RefCloseCommand, RefEditNames, RefEditOptionsCommand, + RefEditPickCommand, RefEditSession, RefSetCommand, SymbolKind, NOT_CHECKED_OUT, +}; +use crate::ui::window::refedit_dialog::{RefEditDialogMsg, RefEditDialogState, RefEditLevel}; +use codec::{EntityType, Handle}; +use iced::Task; +use std::collections::HashSet; + +/// `** Command not allowed, NAME already checked out for editing **` +fn already_checked_out(name: &str) -> String { + crate::tf!("** Command not allowed, {} already checked out for editing **", name).into_owned() +} + +impl OpenCADStudio { + pub(super) fn dispatch_refedit(&mut self, cmd: &str, i: usize) -> Option> { + match cmd { + "REFEDIT" | "-REFEDIT" => Some(self.start_refedit(i, cmd == "-REFEDIT")), + "REFSET" | "REFSET_ADD" | "REFSET_REMOVE" => { + if self.tabs[i].refedit_session.is_none() { + self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref()); + return Some(Task::none()); + } + self.command_line.push_info( + crate::t!("Transfer objects between the RefEdit working set and host drawing...") + .as_ref(), + ); + // The side toolbar's Add / Remove go straight to the selection. + let command = match cmd { + "REFSET_ADD" => RefSetCommand::selecting(true), + "REFSET_REMOVE" => RefSetCommand::selecting(false), + _ => RefSetCommand::new(), + }; + use crate::command::CadCommand; + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + Some(Task::none()) + } + "REFCLOSE" => { + if self.tabs[i].refedit_session.is_none() { + self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref()); + return Some(Task::none()); + } + let command = RefCloseCommand::new(); + use crate::command::CadCommand; + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + Some(Task::none()) + } + "REFCLOSE_SAVE" => { + self.close_refedit(i, true); + Some(Task::none()) + } + "REFCLOSE_DISCARD" => { + self.close_refedit(i, false); + Some(Task::none()) + } + _ if cmd.starts_with("REFEDIT_PICKED:") => { + self.refedit_picked(i, &cmd["REFEDIT_PICKED:".len()..]); + Some(Task::none()) + } + _ if cmd.starts_with("REFEDIT_BEGIN:") => { + self.begin_refedit(i, &cmd["REFEDIT_BEGIN:".len()..]); + Some(Task::none()) + } + _ if cmd.starts_with("REFSET_APPLY:") => { + self.apply_refset(i, cmd.ends_with("ADD")); + Some(Task::none()) + } + _ => None, + } + } + + /// Block references and xrefs of the current space. + fn refedit_references(&self, i: usize) -> HashSet { + let space = self.tabs[i].scene.current_layout_block_handle_pub(); + self.tabs[i] + .scene + .document + .entities() + .filter(|e| matches!(e, EntityType::Insert(_)) && e.common().owner_handle == space) + .map(|e| e.common().handle) + .collect() + } + + fn start_refedit(&mut self, i: usize, command_line: bool) -> Task { + if let Some(session) = &self.tabs[i].refedit_session { + let message = already_checked_out(&session.block_name); + self.command_line.push_error(&message); + return Task::none(); + } + let references = self.refedit_references(i); + // A single preselected reference is taken as the pick. + let selected: Vec = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .map(|(h, _)| h) + .collect(); + if let [handle] = selected[..] { + if references.contains(&handle) { + self.tabs[i].scene.deselect_all(); + self.refedit_picked( + i, + &format!("{}:{}", command_line as u8, handle.value()), + ); + return Task::none(); + } + } + use crate::command::CadCommand; + let command = RefEditPickCommand::new(command_line, references); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + Task::none() + } + + /// `cli:handle[:x:y:z]` โ€” the reference was picked (at the point). + fn refedit_picked(&mut self, i: usize, args: &str) { + let parts: Vec<&str> = args.split(':').collect(); + let command_line = parts.first() == Some(&"1"); + let Some(top) = parts.get(1).and_then(|v| v.parse::().ok()).map(Handle::new) else { + return; + }; + let point = (parts.len() >= 5) + .then(|| { + Some(glam::DVec3::new( + parts[2].parse().ok()?, + parts[3].parse().ok()?, + parts[4].parse().ok()?, + )) + }) + .flatten(); + self.tabs[i].scene.deselect_all(); + let chain = nesting_chain(&self.tabs[i].scene.document, top, point); + if chain.is_empty() { + self.command_line + .push_error(crate::t!("Object is not a valid reference").as_ref()); + return; + } + if command_line { + let hits = chain + .iter() + .map(|level| level_hits(&self.tabs[i].scene.document, level)) + .collect(); + let command = RefEditOptionsCommand::new( + top, + chain, + hits, + self.refedit_options.lock, + self.refedit_options.unique_names, + ); + use crate::command::CadCommand; + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } else { + self.open_refedit_dialog(i, top, chain); + } + } + + /// The Edit Reference dialog over the picked chain: the innermost level + /// chosen, the remembered settings. + fn open_refedit_dialog(&mut self, i: usize, top: Handle, chain: Vec) { + let scene = &self.tabs[i].scene; + let document = &scene.document; + let levels = chain + .iter() + .map(|level| { + let record = document.block_records.get(&level.block_name); + RefEditLevel { + insert: level.insert, + name: level.block_name.clone(), + wires: scene.block_preview_wires(&level.block_name), + has_attdefs: record.is_some_and(|br| { + br.entity_handles.iter().any(|h| { + matches!(document.get_entity(*h), Some(EntityType::AttributeDefinition(_))) + }) + }), + xref_path: record + .filter(|br| br.flags.is_xref || br.flags.is_xref_overlay) + .map(|br| br.xref_path.clone()) + .filter(|p| !p.is_empty()), + } + }) + .collect::>(); + let options = &self.refedit_options; + self.refedit_dialog = Some(RefEditDialogState { + top, + level: levels.len().saturating_sub(1), + levels, + chain, + nested: options.nested, + attributes: options.attributes, + lock: options.lock, + unique: options.unique_names, + }); + self.active_modal = Some(crate::app::ModalKind::RefEdit); + } + + pub(in crate::app) fn update_refedit_dialog(&mut self, message: RefEditDialogMsg) -> Task { + let Some(state) = self.refedit_dialog.as_mut() else { + return Task::none(); + }; + match message { + RefEditDialogMsg::Level(level) => { + state.level = level.min(state.levels.len().saturating_sub(1)) + } + RefEditDialogMsg::Nested(on) => state.nested = on, + RefEditDialogMsg::Attributes(on) => state.attributes = on, + RefEditDialogMsg::Lock(on) => state.lock = on, + RefEditDialogMsg::Unique(on) => state.unique = on, + RefEditDialogMsg::Help => { + self.command_line + .push_info(&crate::tr!("ref-edit", "dialog-help")); + } + RefEditDialogMsg::Ok => { + let Some(state) = self.refedit_dialog.take() else { + return Task::none(); + }; + self.close_active_modal(); + let attributes = state.attributes && state.attributes_enabled(); + self.refedit_options = crate::app::settings::RefEditOptions { + unique_names: state.unique, + attributes: state.attributes, + lock: state.lock, + nested: state.nested, + }; + let i = self.active_tab; + if state.nested { + let hits = state + .chain + .iter() + .map(|level| level_hits(&self.tabs[i].scene.document, level)) + .collect(); + let command = RefEditOptionsCommand::nested_pick( + state.top, + state.chain, + hits, + state.level, + attributes, + state.lock, + state.unique, + ); + use crate::command::CadCommand; + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } else { + let last = state.level.min(state.chain.len().saturating_sub(1)); + let path: Vec = state.chain[1..=last] + .iter() + .map(|l| l.insert.value().to_string()) + .collect(); + return self.dispatch_command(&format!( + "REFEDIT_BEGIN:{}:{}:{}::{}:{}", + state.top.value(), + path.join(","), + attributes as u8, + state.lock as u8, + state.unique as u8 + )); + } + } + } + Task::none() + } + + /// `top[:path[:attributes[:nested]]]` โ€” open the session. + fn begin_refedit(&mut self, i: usize, args: &str) { + let mut parts = args.split(':'); + let Some(top) = parts + .next() + .and_then(|v| v.parse::().ok()) + .map(Handle::new) + else { + return; + }; + let handles = |s: Option<&str>| -> Vec { + s.unwrap_or("") + .split(',') + .filter_map(|v| v.parse::().ok()) + .map(Handle::new) + .collect() + }; + let path = handles(parts.next()); + let attributes = parts.next() == Some("1"); + let nested = handles(parts.next()); + let lock = parts.next().map_or(self.refedit_options.lock, |v| v == "1"); + let unique = parts + .next() + .map_or(self.refedit_options.unique_names, |v| v == "1"); + if self.tabs[i].refedit_session.is_some() || self.reject_locked_edit(i, top) { + return; + } + let document = &self.tabs[i].scene.document; + let mut chain = nesting_chain(document, top, None); + if chain.is_empty() { + self.command_line + .push_error(crate::t!("Object is not a valid reference").as_ref()); + return; + } + // Follow the stored path of definition-owned references. + for handle in path { + let Some(EntityType::Insert(inner)) = document.get_entity(handle) else { + break; + }; + let base = crate::scene::render_graph::block_base_point(document, &inner.block_name); + let local = crate::scene::render_graph::insert_transform_with_base(inner, base); + let outer = chain.last().expect("non-empty chain").forward.clone(); + chain.push(crate::modules::draw::modify::refedit::ChainLevel { + insert: handle, + block_name: inner.block_name.clone(), + forward: local.then(&outer), + }); + } + let level = chain.last().expect("non-empty chain").clone(); + // An external reference's dependent names (`XREF|NAME`) of the + // outermost xref in the chain. + let xref_prefix = chain.iter().find_map(|l| { + document + .block_records + .get(&l.block_name) + .filter(|br| br.flags.is_xref || br.flags.is_xref_overlay) + .map(|br| format!("{}|", br.name)) + }); + let Some(br) = document.block_records.get(&level.block_name) else { + return; + }; + let br_handle = br.handle; + let xref_path = (br.flags.is_xref || br.flags.is_xref_overlay) + .then(|| br.xref_path.clone()) + .filter(|p| !p.is_empty()); + let extracted: Vec = br + .entity_handles + .iter() + .copied() + .filter(|h| match document.get_entity(*h) { + Some(EntityType::Block(_) | EntityType::BlockEnd(_)) | None => false, + Some(EntityType::AttributeDefinition(_)) => attributes, + Some(_) => nested.is_empty() || nested.contains(h), + }) + .collect(); + let Some(inverse) = + crate::modules::draw::modify::refedit::inverse_placement(&level.forward) + else { + return; + }; + let session = RefEditSession { + block_name: level.block_name.clone(), + br_handle, + temp_handles: Vec::new(), + extracted: extracted.clone(), + added: HashSet::new(), + removed: HashSet::new(), + forward: level.forward.clone(), + inverse, + // Everything allocated from here on is part of the session. + handle_watermark: document.next_handle().max(document.header.handle_seed), + xref_path, + lock, + names: RefEditNames::default(), + }; + let copies: Vec = extracted + .iter() + .filter_map(|h| document.get_entity(*h).cloned()) + .collect(); + + self.push_undo_snapshot(i, "REFEDIT"); + let mut session = session; + let copies = match xref_prefix { + Some(prefix) => { + let (copies, names) = self.refedit_host_names(i, copies, &prefix, unique); + session.names = names; + copies + } + None => copies, + }; + let mut temp_handles = Vec::new(); + for mut entity in copies { + apply_insert_transform(&mut entity, &session); + entity.common_mut().handle = Handle::NULL; + entity.common_mut().owner_handle = Handle::NULL; + temp_handles.push(self.tabs[i].scene.add_entity(entity)); + } + session.temp_handles = temp_handles.clone(); + self.tabs[i].refedit_session = Some(session); + self.tabs[i].refedit_closed.clear(); + // Fade everything outside the working set by XFADECTL. (#136) + self.tabs[i] + .scene + .set_refedit_keep(Some(temp_handles.iter().copied().collect::>())); + self.tabs[i].scene.refedit_lock = lock; + self.tabs[i].scene.deselect_all(); + self.tabs[i].dirty = true; + // No active command โ€” the definition's objects are edited freely + // (move, grips, draw, eraseโ€ฆ) until REFCLOSE. + self.tabs[i].active_cmd = None; + } + + /// REFSET Add / Remove on the current selection. + fn apply_refset(&mut self, i: usize, add: bool) { + let Some(mut session) = self.tabs[i].refedit_session.take() else { + self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref()); + return; + }; + let picked: Vec = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .map(|(h, _)| h) + .collect(); + let mut moved = 0usize; + for h in picked { + let inside = session.in_working_set(h); + if add && !inside { + session.removed.remove(&h); + if !session.in_working_set(h) { + session.added.insert(h); + } + moved += 1; + } else if !add && inside { + session.added.remove(&h); + if session.in_working_set(h) { + session.removed.insert(h); + } + moved += 1; + } + } + let keep: rustc_hash::FxHashSet = self.tabs[i] + .scene + .document + .entities() + .map(|e| e.common().handle) + .filter(|h| session.in_working_set(*h)) + .collect(); + self.tabs[i].refedit_session = Some(session); + self.tabs[i].scene.set_refedit_keep(Some(keep)); + self.tabs[i].scene.deselect_all(); + self.command_line.push_output(&if add { + crate::tf!("{} Added to working set", moved).into_owned() + } else { + crate::tf!("{} Removed from working set", moved).into_owned() + }); + } + + /// REFCLOSE Save (`save`) or Discard. + fn close_refedit(&mut self, i: usize, save: bool) { + let Some(session) = self.tabs[i].refedit_session.take() else { + self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref()); + return; + }; + self.push_undo_snapshot(i, "REFCLOSE"); + self.command_line + .push_output(crate::t!("Regenerating model.").as_ref()); + let space = self.tabs[i].scene.current_layout_block_handle_pub(); + // The working set: the copies still in it, everything created in this + // space during the session (#423) and the host objects added with + // REFSET โ€” less what REFSET removed. + let created: Vec = self.tabs[i] + .scene + .document + .entities() + .filter(|e| { + let c = e.common(); + c.owner_handle == space && c.handle.value() >= session.handle_watermark + }) + .map(|e| e.common().handle) + .collect(); + let working: Vec = created + .iter() + .chain(session.added.iter()) + .copied() + .filter(|h| session.in_working_set(*h)) + .collect::>() + .into_iter() + .collect(); + if !save { + // The copies and new objects go; host objects added with REFSET + // stay where they are, and copies moved out with REFSET Remove + // stay in the host. + let drop: Vec = working + .iter() + .copied() + .filter(|h| !session.added.contains(h)) + .collect(); + self.tabs[i].scene.erase_entities(&drop); + self.finish_refedit_close(i, session); + return; + } + + use crate::modules::draw::modify::explode::normalize_entity_for_block; + let originals: HashSet = session.temp_handles.iter().copied().collect(); + let added_count = working.iter().filter(|h| !originals.contains(h)).count(); + let removed_count = session.extracted.len() + - working.iter().filter(|h| originals.contains(h)).count(); + let edited: Vec = working + .iter() + .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) + .map(|mut entity| { + // Back to the reference's own dependent names. + rename_symbols(&mut entity, &|kind, name| { + session.names.original(kind, name).map(str::to_string) + }); + apply_insert_inverse_transform(&mut entity, &session); + let mut entity = normalize_entity_for_block(entity); + entity.common_mut().handle = Handle::NULL; + entity.common_mut().owner_handle = session.br_handle; + entity + }) + .collect(); + self.tabs[i].scene.erase_entities(&working); + // The extracted definition objects are replaced; the rest stay. + for h in &session.extracted { + self.tabs[i].scene.document.remove_entity(*h); + } + if let Some(br) = self.tabs[i] + .scene + .document + .block_records + .get_mut(&session.block_name) + { + let extracted: HashSet = session.extracted.iter().copied().collect(); + br.entity_handles.retain(|h| !extracted.contains(h)); + } + for entity in edited { + let _ = self.tabs[i].scene.document.add_entity(entity); + } + let instances = self.tabs[i] + .scene + .document + .entities() + .filter(|e| { + matches!(e, EntityType::Insert(ins) + if ins.block_name.eq_ignore_ascii_case(&session.block_name)) + }) + .count(); + let name = session.block_name.clone(); + match added_count { + 0 => {} + 1 => self + .command_line + .push_output(crate::tf!("1 object added to {}", name).as_ref()), + n => self + .command_line + .push_output(crate::tf!("{} objects added to {}", n, name).as_ref()), + } + match removed_count { + 0 => {} + 1 => self + .command_line + .push_output(crate::tf!("1 object removed from {}", name).as_ref()), + n => self + .command_line + .push_output(crate::tf!("{} objects removed from {}", n, name).as_ref()), + } + let reloaded = match &session.xref_path { + // The file gets the whole new definition: the objects that were + // not checked out stay in it as well as the edited ones. + Some(raw) => match self.write_back_xref(i, &session, raw) { + Ok(()) => true, + Err(error) => { + self.command_line.push_error(&error); + false + } + }, + None => false, + }; + let updated = match (session.xref_path.is_some(), instances) { + (true, 1) => crate::t!("1 xref instance updated").into_owned(), + (true, n) => crate::tf!("{} xref instances updated", n).into_owned(), + (false, 1) => crate::t!("1 block instance updated").into_owned(), + (false, n) => crate::tf!("{} block instances updated", n).into_owned(), + }; + self.command_line.push_output(&updated); + self.command_line.push_output(&if reloaded { + crate::tf!("{} redefined and reloaded.", name).into_owned() + } else { + crate::tf!("{} redefined.", name).into_owned() + }); + self.finish_refedit_close(i, session); + // Rebuild hatch/image/mesh caches since block content changed. + self.tabs[i].scene.rebuild_derived_caches(); + } + + fn finish_refedit_close(&mut self, i: usize, session: RefEditSession) { + self.drop_refedit_names(i, &session.names); + self.tabs[i].refedit_closed.push(session); + self.tabs[i].scene.refedit_lock = false; + self.tabs[i].scene.set_refedit_keep(None); + self.tabs[i].scene.deselect_all(); + self.tabs[i].dirty = true; + } + + /// Write the edited definition into the referenced drawing and reload it. + fn write_back_xref( + &mut self, + i: usize, + session: &RefEditSession, + raw: &str, + ) -> Result<(), String> { + let edited: Vec = self.tabs[i] + .scene + .document + .entities() + .filter(|entity| entity.common().owner_handle == session.br_handle) + .cloned() + .map(|mut entity| { + entity.common_mut().handle = Handle::NULL; + entity + }) + .collect(); + let base_dir = self.tabs[i] + .current_path + .as_ref() + .and_then(|p| p.parent().map(std::path::Path::to_path_buf)) + .unwrap_or_default(); + let path = crate::io::xref::resolve_path(raw, &base_dir) + .ok_or_else(|| format!("\"{raw}\" cannot be found."))?; + let mut source = crate::io::load_file(&path)?; + let prefix = format!("{}|", session.block_name); + let strip = |name: &str| -> String { + if name.len() > prefix.len() + && name.is_char_boundary(prefix.len()) + && name[..prefix.len()].eq_ignore_ascii_case(&prefix) + { + name[prefix.len()..].to_string() + } else { + name.to_string() + } + }; + // Dependencies come from the host (where they carry the xref prefix) + // and go back under their own names. + let mut deps = crate::app::ClipboardDeps::capture(&self.tabs[i].scene.document, &edited); + { + use codec::TableEntry; + for layer in &mut deps.layers { + let name = strip(layer.name()); + layer.name = name; + } + for linetype in &mut deps.linetypes { + let name = strip(linetype.name()); + linetype.name = name; + } + for style in &mut deps.text_styles { + let name = strip(style.name()); + style.name = name; + } + } + for block in &mut deps.blocks { + block.name = strip(&block.name); + } + let entities: Vec = edited + .into_iter() + .map(|mut entity| { + let common = entity.common_mut(); + common.layer = strip(&common.layer); + common.linetype = strip(&common.linetype); + common.layer_handle = None; + common.linetype_handle = None; + common.owner_handle = Handle::NULL; + match &mut entity { + EntityType::Insert(insert) => insert.block_name = strip(&insert.block_name), + EntityType::Text(text) => text.style = strip(&text.style), + EntityType::MText(text) => text.style = strip(&text.style), + _ => {} + } + entity + }) + .collect(); + let model: Vec = source + .model_space_entities() + .map(|e| e.common().handle) + .collect(); + for h in model { + source.remove_entity(h); + } + crate::app::command_driver::clipboard::paste_entities_kernel( + &mut source, + &entities, + &deps, + None, + crate::app::command_driver::clipboard::PasteSpace::Model, + ); + if self.backup_on_save { + crate::io::write_backup(&path); + } + crate::io::save(&source, &path)?; + let keys: rustc_hash::FxHashSet = [session.br_handle].into_iter().collect(); + self.tabs[i].xref_stat_cache.0.remove(&session.br_handle.value()); + let _ = crate::io::xref::resolve_xrefs_for_keys( + &mut self.tabs[i].scene.document, + &base_dir, + &keys, + ); + Ok(()) + } + + /// Undo of REFCLOSE checks the reference out again; undo of REFEDIT ends + /// the session (their redo does the reverse). + pub(in crate::app) fn refedit_after_history_step(&mut self, i: usize, label: &str, undo: bool) { + match (label, undo) { + ("REFCLOSE", true) | ("REFEDIT", false) => { + let reopened = if undo { + self.tabs[i].refedit_closed.pop() + } else { + self.tabs[i].refedit_reopen.pop() + }; + if let Some(session) = reopened { + let keep: rustc_hash::FxHashSet = self.tabs[i] + .scene + .document + .entities() + .map(|e| e.common().handle) + .filter(|h| session.in_working_set(*h)) + .collect(); + self.tabs[i].scene.refedit_lock = session.lock; + self.tabs[i].refedit_session = Some(session); + self.tabs[i].scene.set_refedit_keep(Some(keep)); + } + } + ("REFCLOSE", false) | ("REFEDIT", true) => { + if let Some(session) = self.tabs[i].refedit_session.take() { + if undo { + self.tabs[i].refedit_reopen.push(session); + } else { + self.tabs[i].refedit_closed.push(session); + } + self.tabs[i].scene.refedit_lock = false; + self.tabs[i].scene.set_refedit_keep(None); + } + } + _ => {} + } + } + + /// The working set's objects of an external reference under host names: + /// `XREF|NAME` becomes `$N$NAME` (the first free N) with records of its + /// own when `unique`, else `NAME`, sharing a host record of that name when + /// one exists. Missing records are copied from the reference's. + fn refedit_host_names( + &mut self, + i: usize, + mut copies: Vec, + prefix: &str, + unique: bool, + ) -> (Vec, RefEditNames) { + let document = &mut self.tabs[i].scene.document; + let deps = crate::app::ClipboardDeps::capture(document, &copies); + let strip = |name: &str| -> Option { + (name.len() > prefix.len() + && name.is_char_boundary(prefix.len()) + && name[..prefix.len()].eq_ignore_ascii_case(prefix)) + .then(|| name[prefix.len()..].to_string()) + }; + let exists = |document: &codec::CadDocument, kind: SymbolKind, name: &str| match kind { + SymbolKind::Layer => document.layers.contains(name), + SymbolKind::Linetype => document.line_types.contains(name), + SymbolKind::TextStyle => document.text_styles.contains(name), + SymbolKind::Block => document.block_records.get(name).is_some(), + }; + let wanted: Vec<(SymbolKind, String)> = deps + .layers + .iter() + .map(|r| (SymbolKind::Layer, r.name.clone())) + .chain(deps.linetypes.iter().map(|r| (SymbolKind::Linetype, r.name.clone()))) + .chain(deps.text_styles.iter().map(|r| (SymbolKind::TextStyle, r.name.clone()))) + .chain(deps.blocks.iter().map(|b| (SymbolKind::Block, b.name.clone()))) + .collect(); + let mut names = RefEditNames::default(); + for (kind, original) in wanted { + let Some(base) = strip(&original) else { + continue; + }; + let taken = |names: &RefEditNames, candidate: &str| { + exists(document, kind, candidate) + || names + .renamed + .iter() + .any(|(k, used, _)| *k == kind && used.eq_ignore_ascii_case(candidate)) + }; + // Layer 0 is the host's own in every drawing. + let used = if kind == SymbolKind::Layer && base == "0" { + base + } else if unique { + (0u32..) + .map(|n| format!("${n}${base}")) + .find(|candidate| !taken(&names, candidate)) + .expect("a free name") + } else { + base + }; + if !exists(document, kind, &used) + && !names + .created + .iter() + .any(|(k, n)| *k == kind && n.eq_ignore_ascii_case(&used)) + { + names.created.push((kind, used.clone())); + } + names.renamed.push((kind, used, original)); + } + let used_for = |kind: SymbolKind, name: &str| { + names + .renamed + .iter() + .find(|(k, _, original)| *k == kind && original.eq_ignore_ascii_case(name)) + .map(|(_, used, _)| used.clone()) + }; + let created = |kind: SymbolKind, name: &str| -> Option { + used_for(kind, name).filter(|used| { + names + .created + .iter() + .any(|(k, n)| *k == kind && n.eq_ignore_ascii_case(used)) + }) + }; + for record in &deps.layers { + if let Some(used) = created(SymbolKind::Layer, &record.name) { + let mut copy = record.clone(); + copy.name = used; + copy.flags.xref_dependent = false; + copy.xref_block_record_handle = Handle::NULL; + if let Some(linetype) = used_for(SymbolKind::Linetype, ©.line_type) { + copy.line_type = linetype; + } + copy.handle = document.allocate_handle(); + let _ = document.layers.add(copy); + } + } + for record in &deps.linetypes { + if let Some(used) = created(SymbolKind::Linetype, &record.name) { + let mut copy = record.clone(); + copy.name = used; + copy.xref_dependent = false; + copy.xref_block_record_handle = Handle::NULL; + copy.handle = document.allocate_handle(); + let _ = document.line_types.add(copy); + } + } + for record in &deps.text_styles { + if let Some(used) = created(SymbolKind::TextStyle, &record.name) { + let mut copy = record.clone(); + copy.name = used; + copy.xref_dependent = false; + copy.xref_block_record_handle = Handle::NULL; + copy.handle = document.allocate_handle(); + let _ = document.text_styles.add(copy); + } + } + let rename = |kind: SymbolKind, name: &str| used_for(kind, name); + let blocks: Vec = deps + .blocks + .iter() + .filter_map(|def| { + let used = created(SymbolKind::Block, &def.name)?; + let mut entities = def.entities.clone(); + for entity in &mut entities { + rename_symbols(entity, &rename); + } + Some(crate::app::BlockDef { + name: used, + base_point: def.base_point, + entities, + }) + }) + .collect(); + crate::app::command_driver::clipboard::merge_block_defs(document, &blocks); + for entity in &mut copies { + rename_symbols(entity, &rename); + } + (copies, names) + } + + /// Removes the host records made for a session that nothing uses any more. + fn drop_refedit_names(&mut self, i: usize, names: &RefEditNames) { + for (kind, name) in names.created.iter().rev() { + let scene = &mut self.tabs[i].scene; + let in_use = scene.document.entities().any(|entity| { + let common = entity.common(); + match kind { + SymbolKind::Layer => common.layer.eq_ignore_ascii_case(name), + SymbolKind::Linetype => common.linetype.eq_ignore_ascii_case(name), + SymbolKind::TextStyle => match entity { + EntityType::Text(t) => t.style.eq_ignore_ascii_case(name), + EntityType::MText(t) => t.style.eq_ignore_ascii_case(name), + EntityType::AttributeDefinition(a) => { + a.text_style.eq_ignore_ascii_case(name) + } + _ => false, + }, + SymbolKind::Block => matches!( + entity, + EntityType::Insert(insert) if insert.block_name.eq_ignore_ascii_case(name) + ), + } + }); + // A linetype made for the session may also be a kept layer's. + let in_use = in_use + || (*kind == SymbolKind::Linetype + && scene + .document + .layers + .iter() + .any(|layer| layer.line_type.eq_ignore_ascii_case(name))); + if in_use { + continue; + } + match kind { + SymbolKind::Layer => { + scene.document.layers.remove(name); + } + SymbolKind::Linetype => { + scene.document.line_types.remove(name); + } + SymbolKind::TextStyle => { + scene.document.text_styles.remove(name); + } + SymbolKind::Block => { + let _ = crate::scene::block_service::erase(scene, name); + } + } + } + } +} diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 0599bdb89..8063e12f5 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1148,6 +1148,9 @@ impl OpenCADStudio { | "SHORTCUTMENUDURATION" | "CURSORSIZE" | "PICKBOX" + | "HPASSOC" + | "COMMANDLINEOPACITY" + | "MENUBAR" | "CURSORTYPE" | "SNAPANG" | "TEXTFILL" @@ -1210,9 +1213,11 @@ impl OpenCADStudio { | "DGNOSNAP" | "UOSNAP" | "FIELDDISPLAY" + | "FILEDIA" | "SURFACEASSOCIATIVITY" | "UCSORTHO" | "UCSVIEW" + | "LAYOUTCREATEVIEWPORT" | "PDFIMPORTMODE" | "PDFIMPORTFILTER" | "PDFIMPORTLAYERS" @@ -1346,9 +1351,15 @@ impl OpenCADStudio { if let Some(value) = &value { match value.parse::().ok().filter(|v| (-90..=90).contains(v)) { Some(fade) => self.set_xref_fade(fade), - None => self - .command_line - .push_error("Requires an integer between -90 and 90."), + // Refused: say why and ask again. + None => { + self.command_line + .push_error("Requires an integer between -90 and 90."); + self.command_line.push_output(&format!( + "Enter new value for XDWGFADECTL <{current}>:" + )); + self.pending_setvar = Some(name.clone()); + } } } else { self.command_line.push_output(&format!( @@ -1451,6 +1462,39 @@ impl OpenCADStudio { } return Some(self.finish_dispatch(cmd)); } + if name == "LAYOUTCREATEVIEWPORT" { + // A profile switch shown and taken as ON / OFF or 1 / 0. + let current = if crate::scene::layout_create_viewport() { "ON" } else { "OFF" }; + let parsed = value.as_deref().map(|v| { + match v.trim().trim_start_matches('_').to_ascii_uppercase().as_str() { + "1" | "ON" => Some(true), + "0" | "OFF" => Some(false), + _ => None, + } + }); + match parsed { + Some(Some(on)) => { + if crate::scene::layout_create_viewport() != on { + crate::scene::set_layout_create_viewport(on); + self.save_config(); + } + } + Some(None) => { + self.command_line.push_error( + crate::t!("Requires 0 or 1 only, or option keyword.").as_ref(), + ); + self.command_line + .push_output(&format!("Enter new value for {name} <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + None => { + self.command_line + .push_output(&format!("Enter new value for {name} <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + } + return Some(self.finish_dispatch(cmd)); + } if matches!(name.as_str(), "UCSORTHO" | "UCSVIEW") { use crate::modules::view::view_cmd; let current = i16::from(if name == "UCSORTHO" { view_cmd::ucs_ortho() } else { view_cmd::ucs_view() }); @@ -1559,6 +1603,35 @@ impl OpenCADStudio { } return Some(self.finish_dispatch(cmd)); } + if name == "FILEDIA" { + let current = i16::from(crate::sys::file_dia()); + match value + .as_deref() + .map(|v| v.trim().parse::().ok().filter(|v| (0..=1).contains(v))) + { + Some(Some(mode)) => { + if current != mode { + crate::sys::set_file_dia(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 FILEDIA <{current}>:" + )); + self.pending_setvar = Some(name.clone()); + } + None => { + self.command_line.push_output(&format!( + "Enter new value for FILEDIA <{current}>:" + )); + self.pending_setvar = Some(name.clone()); + } + } + return Some(self.finish_dispatch(cmd)); + } if matches!( name.as_str(), "PDFOSNAP" | "DWFOSNAP" | "DGNOSNAP" | "UOSNAP" @@ -2154,6 +2227,28 @@ impl OpenCADStudio { }, None => Ok((format!("PICKBOX = {}", self.pick_box), false)), }, + "MENUBAR" => match value.as_deref() { + Some(v @ ("0" | "1")) => { + self.show_menu_bar = v == "1"; + Ok((format!("MENUBAR = {v}"), true)) + } + Some(_) => Err("SETVAR: 0 or 1 required.".into()), + None => Ok(( + format!("MENUBAR = {}", u8::from(self.show_menu_bar)), + false, + )), + }, + "HPASSOC" => match value.as_deref() { + Some(v @ ("0" | "1")) => { + self.hatch_associative = v == "1"; + Ok((format!("HPASSOC = {v}"), true)) + } + Some(_) => Err("SETVAR: 0 or 1 required.".into()), + None => Ok(( + format!("HPASSOC = {}", u8::from(self.hatch_associative)), + false, + )), + }, "CURSORTYPE" => match &value { Some(v) => match v.as_str() { "0" => { @@ -2491,6 +2586,22 @@ impl OpenCADStudio { false, )), }, + "COMMANDLINEOPACITY" => match &value { + Some(v) => match v.parse::() { + Ok(n) if (20..=100).contains(&n) => { + self.command_line.set_opacity_percent(n); + Ok((format!("COMMANDLINEOPACITY = {n}"), true)) + } + _ => Err("SETVAR: integer from 20 to 100 required.".into()), + }, + None => Ok(( + format!( + "COMMANDLINEOPACITY = {}", + self.command_line.opacity_percent() + ), + false, + )), + }, "COMMANDLINEFADETIME" => match &value { Some(v) => match v.parse::() { Ok(n) if (0..=60000).contains(&n) => { @@ -3088,6 +3199,10 @@ impl OpenCADStudio { self.delete_objects = value; } if changed { + if name == "INSUNITS" { + self.tabs[i].scene.sync_annotation_scale_from_header(); + self.tabs[i].scene.invalidate_annotation_dependencies(); + } if matches!( name.as_str(), "ZOOMWHEEL" @@ -3096,6 +3211,9 @@ impl OpenCADStudio { | "SHORTCUTMENUDURATION" | "CURSORSIZE" | "PICKBOX" + | "HPASSOC" + | "COMMANDLINEOPACITY" + | "MENUBAR" | "CURSORTYPE" | "SNAPANG" | "CLIPROMPTLINES" @@ -3253,6 +3371,42 @@ impl OpenCADStudio { // Read-only scan: flags entities on undefined layers and block // references to undefined block definitions. Reports only; it does // not auto-repair (so it can never make the drawing worse). + // COMPARE โ€” overlay what differs from another drawing. Bare picks + // the drawing; with a path it compares at once. + "COMPARE" => { + return Some(Task::perform( + async { + crate::sys::file_dialog() + .set_title(crate::t!("Compare with drawing").as_ref()) + .add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"]) + .pick_file() + .await + .map(|handle| crate::sys::handle_path(&handle)) + }, + |path| match path { + Some(path) => Message::Command(format!("COMPARE {}", path.display())), + None => Message::Noop, + }, + )); + } + cmd if cmd.starts_with("COMPARE ") => { + let path = std::path::PathBuf::from(cmd.trim_start_matches("COMPARE").trim()); + match crate::io::load_file(&path) { + Ok(other) => { + let comparison = + crate::scene::compare::compare(&self.tabs[i].scene, other); + // The overlay shows at once, before the cursor moves. + self.tabs[i].scene.set_preview_wires(comparison.overlay.clone()); + let command = + crate::modules::manage::compare::CompareCommand::new(comparison); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + Err(error) => self + .command_line + .push_error(crate::tf!("COMPARE: {error}").as_ref()), + } + } "AUDIT" => { use std::collections::BTreeSet; let mut undefined_layers: BTreeSet = BTreeSet::new(); @@ -4403,11 +4557,12 @@ mod scale_validation_tests { } impl OpenCADStudio { - /// XDWGFADECTL: store it, show it on the ribbon and redraw every tab's - /// referenced drawings. + /// XDWGFADECTL: store it in the profile, show it on the ribbon and redraw + /// every tab's referenced drawings. pub(in crate::app) fn set_xref_fade(&mut self, fade: i32) { crate::scene::cache::block_cache::set_xref_fade_ctl(fade); self.ribbon.xref_fade = crate::scene::cache::block_cache::xref_fade_ctl(); + self.save_config(); for tab in &mut self.tabs { if !tab.is_start { tab.scene.recolor_meshes(); diff --git a/src/app/commands/view.rs b/src/app/commands/view.rs index 97ba45e33..42740de6e 100644 --- a/src/app/commands/view.rs +++ b/src/app/commands/view.rs @@ -647,6 +647,34 @@ impl OpenCADStudio { } } + // MVSETUP โ€” sheet border in model space, viewports on a layout. + "MVSETUP" => { + let scene = &mut self.tabs[i].scene; + let target = (scene.current_layout == "Model").then(|| { + scene + .layout_names() + .into_iter() + .find(|name| name != "Model") + .unwrap_or_else(|| "Layout1".to_string()) + }); + // The sheet MVSETUP lays viewports out on: the layout it + // switches to from model space, or the current one. + let current = target + .clone() + .map(|name| std::mem::replace(&mut scene.current_layout, name)); + let paper_bounds = scene + .printable_area_limits() + .or_else(|| scene.paper_limits()) + .unwrap_or(((0.0, 0.0), (297.0, 210.0))); + if let Some(current) = current { + scene.current_layout = current; + } + use crate::modules::layout::mvsetup::MvsetupCommand; + let command = MvsetupCommand::new(target, paper_bounds); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + // โ”€โ”€ MSPACE / PSPACE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ "MSPACE" => { return Some(Task::done(Message::MspaceCommand)); diff --git a/src/app/commands/view_presets.rs b/src/app/commands/view_presets.rs index 66dc10bb9..fc17eddad 100644 --- a/src/app/commands/view_presets.rs +++ b/src/app/commands/view_presets.rs @@ -53,12 +53,39 @@ impl OpenCADStudio { .to_string(), visual_style: name_of(&visual_styles, view.visual_style_handle), live_section: name_of(&live_sections, view.live_section_handle), + paper_space: view.paper_space, }) .collect(); + let scene = &self.tabs[i].scene; + let sheet_viewports = (scene.current_layout != "Model" && scene.active_viewport.is_none()).then(|| { + scene + .layout_content_viewports() + .iter() + .filter_map(|handle| match document.get_entity(*handle) { + Some(codec::EntityType::Viewport(value)) => Some(( + handle.value(), + [ + value.center.x - value.width / 2.0, + value.center.y - value.height / 2.0, + value.center.x + value.width / 2.0, + value.center.y + value.height / 2.0, + ], + )), + _ => None, + }) + .collect() + }); ViewNames { views, visual_styles: visual_styles.into_iter().map(|(name, _)| name).collect(), live_sections: live_sections.into_iter().map(|(name, _)| name).collect(), + sheet_viewports, + // PICKBOX pixels in sheet units, at the sheet camera's zoom. + sheet_pick_tolerance: { + let height = scene.camera.borrow().ortho_size() as f64 * 2.0; + let (_, pixels) = scene.selection.borrow().view.vp_size; + f64::from(self.pick_box) * height / f64::from(pixels).max(1.0) + }, } } @@ -111,7 +138,7 @@ impl OpenCADStudio { } /// Carry out what -VIEW gathered (`-VIEW >VERB args`). - pub(super) fn run_dash_view(&mut self, rest: &str, i: usize) -> Task { + pub(crate) fn run_dash_view(&mut self, rest: &str, i: usize) -> Task { let (verb, arg) = rest.split_once(' ').unwrap_or((rest, "")); let mut settings_again = false; match verb { @@ -132,18 +159,46 @@ impl OpenCADStudio { if !doomed.is_empty() { self.push_undo_snapshot(i, "-VIEW"); for name in &doomed { - self.tabs[i].scene.document.views.remove(name); + let removed = self.tabs[i].scene.document.views.remove(name); + if let Some(view) = removed { + crate::scene::erase_background( + &mut self.tabs[i].scene.document, + view.background_handle, + ); + } self.tabs[i].scene.document.delete_layer_state(&snapshot_name(name)); } self.tabs[i].dirty = true; } } + "RESTOREVP" => { + let (name, handle) = arg.split_once('\u{1f}').unwrap_or((arg, "")); + let Some(view) = self.tabs[i].scene.document.views.get(name).cloned() else { + return Task::none(); + }; + let Some(handle) = u64::from_str_radix(handle, 16).ok().map(Handle::new) else { + return Task::none(); + }; + // The reference enters the selected viewport and restores the + // view in it. + let task = self.update(Message::EnterViewport(handle)); + self.restore_view_in_active_viewport(i, &view); + return task; + } "RESTORE" => { let Some(view) = self.tabs[i].scene.document.views.get(arg).cloned() else { return Task::none(); }; + if self.tabs[i].scene.current_layout != "Model" + && self.tabs[i].scene.active_viewport.is_some() + && !view.paper_space + { + self.restore_view_in_active_viewport(i, &view); + return Task::none(); + } let before = self.tabs[i].scene.active_gaze_dir(); self.tabs[i].scene.restore_named_view(&view); + self.apply_view_render_mode(i, &view); if view.ucs_associated && self.tabs[i].active_block_edit.is_none() { let world = view.ucs_x_axis == Vector3::new(1.0, 0.0, 0.0) && view.ucs_y_axis == Vector3::new(0.0, 1.0, 0.0) @@ -197,6 +252,14 @@ impl OpenCADStudio { } } "SETTINGS" => settings_again = true, + // Background type chosen: its dialog takes over until Apply or Cancel. + "BACKGROUND" => { + if let Some((view, kind)) = arg.split_once('\u{1f}') { + self.open_view_background(i, view, kind); + return self.finish_dispatch("-VIEW"); + } + settings_again = true; + } "UCSVIEW" => { view_cmd::set_ucs_view(arg == "1"); self.save_config(); @@ -241,18 +304,42 @@ impl OpenCADStudio { } } + /// A model-space view restored in the entered paper-space viewport, with + /// its background and visual style. + fn restore_view_in_active_viewport(&mut self, i: usize, view: &codec::tables::View) { + let Some(viewport) = self.tabs[i].scene.active_viewport else { return }; + self.tabs[i].scene.restore_named_view_in_viewport(view, viewport); + if let Some(ObjectType::VisualStyle(style)) = + self.tabs[i].scene.document.objects.get(&view.visual_style_handle) + { + if let Some((mode, _)) = crate::modules::view::visual_style::mode_for_style_name(&style.description) { + self.tabs[i].scene.set_active_viewport_render_mode(mode); + self.tabs[i].scene.bump_geometry_no_blocks(); + } + } + self.tabs[i].dirty = true; + } + /// Add or replace a named view, keeping the record (and so its settings) /// of a view saved again under the same name. fn save_named_view(&mut self, i: usize, mut view: codec::tables::View) { self.push_undo_snapshot(i, "-VIEW"); + let background = self.tabs[i].scene.active_background(); let document = &mut self.tabs[i].scene.document; + view.background_handle = crate::scene::copy_background(document, background); + let mut replaced = Handle::NULL; match document.views.get(&view.name).cloned() { Some(old) => { view.name = old.name; view.handle = old.handle; view.visual_style_handle = old.visual_style_handle; view.live_section_handle = old.live_section_handle; - view.background_handle = old.background_handle; + // The view keeps its background unless the viewport has + // one, which the view then takes a copy of. + if view.background_handle.is_null() { + view.background_handle = old.background_handle; + } + replaced = old.background_handle; } None => { view.handle = document.allocate_handle(); @@ -265,6 +352,7 @@ impl OpenCADStudio { } } document.views.add_or_replace(view); + crate::scene::erase_background(document, replaced); self.tabs[i].dirty = true; } @@ -275,6 +363,7 @@ impl OpenCADStudio { }; self.push_undo_snapshot(i, "-VIEW"); let document = &mut self.tabs[i].scene.document; + let mut replaced = Handle::NULL; if view.handle.is_null() { view.handle = document.allocate_handle(); } @@ -282,7 +371,7 @@ impl OpenCADStudio { list.into_iter().find(|(entry, _)| entry.eq_ignore_ascii_case(value)).map_or(Handle::NULL, |(_, h)| h) }; match setting { - "BACKGROUND" => view.background_handle = Handle::NULL, + "BACKGROUND" => replaced = std::mem::replace(&mut view.background_handle, Handle::NULL), "VISUALSTYLE" => view.visual_style_handle = find(crate::io::visual_styles::offered(document)), "SECTION" => { let sections: Vec<(String, Handle)> = document @@ -336,6 +425,287 @@ impl OpenCADStudio { _ => {} } document.views.add_or_replace(view); + crate::scene::erase_background(document, replaced); self.tabs[i].dirty = true; } } + +impl OpenCADStudio { + /// The Background dialog for a named view, on the type typed at the + /// prompt and filled from the view's background where it has one. + fn open_view_background(&mut self, i: usize, view: &str, kind: &str) { + use crate::ui::window::pdf_dialogs::{BgKind, BgPosition, ViewBackgroundState}; + use codec::objects::ClassObjectData as D; + let unpack = |c: u32| [(c >> 16) as u8, (c >> 8) as u8, c as u8]; + let canvas = self.tabs[i].scene.bg_color; + let mut state = ViewBackgroundState { + view: view.to_string(), + kind: match kind { + "SOLID" => BgKind::Solid, + "GRADIENT" => BgKind::Gradient, + "IMAGE" => BgKind::Image, + _ => BgKind::None, + }, + // The reference's defaults. + solid: [128, 128, 255], + top: [204, 204, 204], + middle: [128, 128, 128], + bottom: [51, 51, 51], + three: false, + rotation: "0".into(), + image: String::new(), + position: BgPosition::Center, + aspect: false, + offset: ["0".into(), "0".into()], + scale: ["1".into(), "1".into()], + scale_base: [1.0, 1.0], + preview: None, + viewport: { + let (width, height) = self.tabs[i].scene.selection.borrow().view.vp_size; + [width as f32, height as f32] + }, + canvas: [0, 1, 2].map(|k| (canvas[k].clamp(0.0, 1.0) * 255.0).round() as u8), + }; + let document = &self.tabs[i].scene.document; + let current = document.views.get(view).map_or(Handle::NULL, |v| v.background_handle); + if let Some(ObjectType::ClassObject(object)) = document.objects.get(¤t) { + match &object.data { + D::SolidBackground(value) => state.solid = unpack(value.color), + D::GradientBackground(value) => { + (state.top, state.middle, state.bottom) = ( + unpack(value.color_top), + unpack(value.color_middle), + unpack(value.color_bottom), + ); + state.three = value.height > 0.0; + state.rotation = crate::ui::window::pdf_dialogs::short(value.rotation); + } + D::ImageBackground(value) => { + state.image = value.filename.clone(); + state.position = if value.use_tiling { + BgPosition::Tile + } else if value.fit_to_screen { + BgPosition::Stretch + } else { + BgPosition::Center + }; + state.aspect = value.maintain_aspect_ratio; + state.offset = [value.offset.x, value.offset.y].map(crate::ui::window::pdf_dialogs::short); + state.scale = [value.scale.x, value.scale.y].map(crate::ui::window::pdf_dialogs::short); + state.scale_base = [value.scale.x, value.scale.y]; + state.preview = crate::ui::window::pdf_dialogs::bg_preview(&value.filename); + } + _ => {} + } + } + self.view_background = Some(state); + self.active_modal = Some(crate::app::ModalKind::ViewBackground); + } + + pub(in crate::app) fn update_view_background( + &mut self, + message: crate::ui::window::pdf_dialogs::PdfDialogMsg, + ) -> Task { + use crate::ui::window::pdf_dialogs::{BgKind, BgPosition, PdfDialogMsg as M}; + use codec::objects::{ + ClassObjectData as D, GradientBackground, ImageBackground, SolidBackground, + }; + let i = self.active_tab; + let Some(state) = self.view_background.as_mut() else { + return Task::none(); + }; + match message { + M::BgKind(kind) => state.kind = kind, + M::BgThree(on) => state.three = on, + M::BgRotation(value) => state.rotation = value, + M::BgImage(value) => { + state.preview = crate::ui::window::pdf_dialogs::bg_preview(&value); + state.image = value; + } + M::BgPosition(value) => state.position = value, + M::BgAspect(on) => state.aspect = on, + M::BgOffset(value) => state.offset = value, + M::BgScale(value) => { + // Keeping the aspect ratio, both scales follow the one changed. + state.scale = if state.aspect { + let changed = if value[0] != state.scale[0] { 0 } else { 1 }; + [value[changed].clone(), value[changed].clone()] + } else { + value + }; + } + M::BgStep(field, dir) => { + use crate::ui::window::pdf_dialogs::{bg_step, short}; + let current = |text: &String, default: f64| text.trim().parse::().ok().filter(|v| v.is_finite()).unwrap_or(default); + if field < 2 { + let k = field as usize; + state.offset[k] = short(bg_step(field, current(&state.offset[k], 0.0), dir)); + } else { + let k = field as usize - 2; + let next = short(bg_step(field, current(&state.scale[k], state.scale_base[k]), dir)); + if state.aspect { + state.scale = [next.clone(), next]; + } else { + state.scale[k] = next; + } + } + } + M::BgReset => { + state.offset = ["0".into(), "0".into()]; + state.scale = ["1".into(), "1".into()]; + } + M::BgColor(slot, rgb) => match slot { + 0 => state.solid = rgb, + 1 => state.top = rgb, + 2 => state.middle = rgb, + _ => state.bottom = rgb, + }, + M::BgPickColor(slot) => { + let [r, g, b] = match slot { + 0 => state.solid, + 1 => state.top, + 2 => state.middle, + _ => state.bottom, + }; + return Task::done(Message::OpenColorWindow( + crate::app::ColorPickTarget::ViewBackground(slot), + codec::types::Color::Rgb { r, g, b }, + )); + } + M::BgBrowse => { + return Task::perform( + async { + crate::sys::file_dialog() + .set_title(crate::tr!("view-background", "select-image")) + .add_filter( + crate::tr!("view-background", "image-files"), + &["png", "jpg", "jpeg", "bmp", "tif", "tiff", "gif", "tga"], + ) + .pick_file() + .await + .map(|handle| crate::sys::handle_path(&handle)) + }, + |path| match path { + Some(path) => Message::PdfDialog(M::BgImage(path.display().to_string())), + None => Message::Noop, + }, + ); + } + M::BgApply => { + // Colours are stored as true colours (flag byte 0xC2), as the + // reference stores them. + let pack = |c: [u8; 3]| { + 0xC200_0000 | (c[0] as u32) << 16 | (c[1] as u32) << 8 | c[2] as u32 + }; + let data = match state.kind { + BgKind::None => None, + BgKind::Solid => Some(D::SolidBackground(SolidBackground { + class_version: 1, + color: pack(state.solid), + })), + BgKind::Gradient => { + let Some(rotation) = state + .rotation + .trim() + .parse::() + .ok() + .filter(|v| v.is_finite()) + else { + self.command_line + .push_error(crate::t!("Requires a number.").as_ref()); + return Task::none(); + }; + Some(D::GradientBackground(GradientBackground { + class_version: 1, + color_top: pack(state.top), + color_middle: pack(state.middle), + color_bottom: pack(state.bottom), + horizon: 0.5, + // The reference marks a three-colour gradient by its + // middle band's height. + height: if state.three { 0.33 } else { 0.0 }, + rotation, + })) + } + BgKind::Image => { + if !std::path::Path::new(state.image.trim()).is_file() { + self.command_line.push_error("Invalid Background Image File!"); + return Task::none(); + } + use crate::ui::window::pdf_dialogs::{BG_OFFSET_LIMIT, BG_SCALE_LIMIT}; + let number = |text: &String| text.trim().parse::().ok().filter(|v| v.is_finite()); + let (Some(ox), Some(oy), Some(sx), Some(sy)) = ( + number(&state.offset[0]), + number(&state.offset[1]), + number(&state.scale[0]), + number(&state.scale[1]), + ) else { + self.command_line.push_error(crate::t!("Requires a number.").as_ref()); + return Task::none(); + }; + // Within the reference's slider ranges; a zero scale + // is ignored there, so the image keeps its scale. + let scale = |value: f64, base: f64| { + if value.abs() < 1e-9 { base } else { value.clamp(-BG_SCALE_LIMIT, BG_SCALE_LIMIT) } + }; + Some(D::ImageBackground(ImageBackground { + class_version: 1, + filename: state.image.trim().to_string(), + fit_to_screen: state.position == BgPosition::Stretch, + maintain_aspect_ratio: state.aspect, + use_tiling: state.position == BgPosition::Tile, + offset: codec::types::Vector2::new( + ox.clamp(-BG_OFFSET_LIMIT, BG_OFFSET_LIMIT), + oy.clamp(-BG_OFFSET_LIMIT, BG_OFFSET_LIMIT), + ), + scale: codec::types::Vector2::new( + scale(sx, state.scale_base[0]), + scale(sy, state.scale_base[1]), + ), + })) + } + }; + let name = state.view.clone(); + self.view_background = None; + self.close_active_modal(); + if self.tabs[i].scene.document.views.get(&name).is_some() { + self.push_undo_snapshot(i, "-VIEW"); + let document = &mut self.tabs[i].scene.document; + let handle = data.map_or(Handle::NULL, |data| { + crate::scene::add_background(document, data) + }); + if let Some(view) = document.views.get_mut(&name) { + let old = std::mem::replace(&mut view.background_handle, handle); + crate::scene::erase_background(document, old); + } + self.tabs[i].dirty = true; + } + return self.run_dash_view("SETTINGS", i); + } + _ => {} + } + Task::none() + } +} + +impl OpenCADStudio { + /// A restored view's visual style becomes the model tab's render mode + /// (the closest of OCS's modes); a view without one leaves it. + pub(crate) fn apply_view_render_mode(&mut self, i: usize, view: &codec::tables::View) { + if self.tabs[i].scene.current_layout != "Model" { + return; + } + let Some(ObjectType::VisualStyle(style)) = + self.tabs[i].scene.document.objects.get(&view.visual_style_handle) + else { + return; + }; + use crate::modules::view::visual_style; + let Some((mode, named)) = visual_style::mode_for_style_name(&style.description) else { + return; + }; + // A style of its own keeps its name; the render modes keep theirs. + let label = if named { style.description.clone() } else { visual_style::label_for(mode).to_string() }; + self.apply_render_mode(i, mode, &label); + } +} diff --git a/src/app/commands/xref_attach.rs b/src/app/commands/xref_attach.rs index 5e4827744..363bdfd36 100644 --- a/src/app/commands/xref_attach.rs +++ b/src/app/commands/xref_attach.rs @@ -8,9 +8,23 @@ use crate::modules::insert::xattach::{ existing_reference, path_to_block_name, prepare_xref_definition, stored_path, XAttachCommand, XrefAttachRequest, XrefPlacement, }; +use crate::modules::insert::xattach::IMAGE_EXTENSIONS; use crate::modules::insert::xref_cmd::display_path; use crate::ui::window::xref_attach::{XrefAttachMsg, XrefAttachState}; +/// ATTACH's file types, in the reference order (the picker counts from 1). +const ATTACH_FILTERS: [(&str, &[&str]); 8] = [ + ("All image files", IMAGE_EXTENSIONS), + ("DWF files (*.dwfx; *.dwf)", &["dwfx", "dwf"]), + ("MicroStation DGN (*.dgn)", &["dgn"]), + ("All DGN Files (*.*)", &["*"]), + ("PDF files (*.pdf)", &["pdf"]), + ("Point Cloud (*.rcp; *.rcs)", &["rcp", "rcs"]), + ("Drawing (*.dwg)", &["dwg"]), + ("All files (*.*)", &["*"]), +]; +const ALL_DGN_FILTER: u32 = 4; + impl OpenCADStudio { /// DWFATTACH / DGNATTACH on the command line: with a file, from its /// sheet / model prompt; without one, from the path prompt. @@ -42,12 +56,26 @@ impl OpenCADStudio { Some((verb, rest)) => (verb.to_ascii_uppercase(), rest.trim()), None => (upper.clone(), ""), }; + // FILEDIA 0 sends the picking commands to their command-line forms. + let dialog = crate::sys::file_dia(); match (verb.as_str(), rest.is_empty()) { - ("ATTACH", true) => Some(Task::done(Message::AttachPick)), - ("DWFATTACH", true) => Some(Task::done(Message::UnderlayAttachPick( + ("ATTACH", true) if dialog => Some(Task::done(Message::AttachPick)), + ("ATTACH", true) | ("-ATTACH", _) => { + use crate::command::CadCommand; + let command = crate::modules::insert::xref_cmd::AttachCommand::new(); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + if !rest.is_empty() { + return Some( + self.feed_command(crate::command::StepInput::Text(rest.to_string())), + ); + } + Some(Task::none()) + } + ("DWFATTACH", true) if dialog => Some(Task::done(Message::UnderlayAttachPick( codec::entities::UnderlayType::Dwf, ))), - ("DGNATTACH", true) => Some(Task::done(Message::UnderlayAttachPick( + ("DGNATTACH", true) if dialog => Some(Task::done(Message::UnderlayAttachPick( codec::entities::UnderlayType::Dgn, ))), // A file after DWFATTACH / DGNATTACH, or the - forms, go on the @@ -62,7 +90,9 @@ impl OpenCADStudio { } // POINTCLOUDATTACH picks the file; with a file, or as // -POINTCLOUDATTACH, it asks on the command line. - ("POINTCLOUDATTACH", true) => Some(Task::done(Message::PointCloudAttachPick)), + ("POINTCLOUDATTACH", true) if dialog => { + Some(Task::done(Message::PointCloudAttachPick)) + } ("POINTCLOUDATTACH" | "-POINTCLOUDATTACH", _) => { use crate::command::CadCommand; let command = crate::modules::insert::pc_attach::PointCloudAttachCommand::new(); @@ -75,7 +105,14 @@ impl OpenCADStudio { } Some(Task::none()) } - ("XATTACH", true) => Some(Task::done(Message::XAttachPick)), + ("XATTACH", true) if dialog => Some(Task::done(Message::XAttachPick)), + ("XATTACH", true) => { + let command = + crate::modules::insert::xref_cmd::XrefCommand::attach(self.saved_xref_paths(i)); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + Some(Task::none()) + } // A file named on the command line skips the picker. // Handled at once, so the placement it starts belongs to this // command line (automation included). @@ -96,15 +133,8 @@ impl OpenCADStudio { Some(self.finish_dispatch(cmd)) } ("-XREF", true) => { - let saved: Vec<(String, String)> = self.tabs[i] - .scene - .document - .block_records - .iter() - .filter(|br| br.flags.is_xref || br.flags.is_xref_overlay) - .map(|br| (br.name.clone(), br.xref_path.clone())) - .collect(); - let command = crate::modules::insert::xref_cmd::XrefCommand::new(saved); + let command = + crate::modules::insert::xref_cmd::XrefCommand::new(self.saved_xref_paths(i)); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); Some(Task::none()) @@ -128,6 +158,18 @@ impl OpenCADStudio { } } + /// Saved paths by reference name, for -XREF. + fn saved_xref_paths(&self, i: usize) -> Vec<(String, String)> { + self.tabs[i] + .scene + .document + .block_records + .iter() + .filter(|br| br.flags.is_xref || br.flags.is_xref_overlay) + .map(|br| (br.name.clone(), br.xref_path.clone())) + .collect() + } + /// `-XREF ?` โ€” the reference table as the reference application prints it. fn list_xrefs(&mut self, i: usize, pattern: &str) { let rows: Vec<(String, &'static str, String)> = self.tabs[i] @@ -392,40 +434,41 @@ impl OpenCADStudio { }, Message::AttachPickResult, ), - Message::AttachPick => Task::perform( - async { - let handle = crate::sys::file_dialog() - .set_title(crate::t!("Select Reference File").as_ref()) - .add_filter( - crate::t!("All Reference Files").as_ref(), - &[ - "dwg", "dxf", "pdf", "dwf", "dwfx", "dgn", "png", "jpg", "jpeg", - "bmp", "tif", "tiff", "rcp", "rcs", - ], - ) - .add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"]) - .add_filter(crate::t!("PDF Files").as_ref(), &["pdf"]) - .add_filter(crate::t!("DWF Files").as_ref(), &["dwf", "dwfx"]) - .add_filter(crate::t!("DGN Files").as_ref(), &["dgn"]) - .add_filter( - crate::t!("Images").as_ref(), - &["png", "jpg", "jpeg", "bmp", "tif", "tiff"], - ) - .add_filter(crate::t!("Point Clouds").as_ref(), &["rcp", "rcs"]) - .pick_file() - .await; - match handle { - Some(h) => Ok(crate::sys::handle_path(&h)), - None => Err("Cancelled".to_string()), - } - }, - Message::AttachPickResult, - ), + Message::AttachPick => { + let filters = ATTACH_FILTERS + .iter() + .map(|(label, extensions)| (crate::t!(label).to_string(), *extensions)) + .collect(); + let title = crate::t!("Select Reference File").to_string(); + let index = crate::sys::attach_filter(); + Task::perform( + async move { + match crate::sys::pick_file_with_filter(title, filters, index).await { + Some((path, index)) => { + crate::sys::set_attach_filter(index); + Ok(path) + } + None => Err("Cancelled".to_string()), + } + }, + Message::AttachPickResult, + ) + } Message::AttachPickResult(Ok(path)) => { - let ext = path + // The file type the picker was left on is a profile setting. + self.save_config(); + let mut ext = path .extension() .map(|e| e.to_string_lossy().to_ascii_lowercase()) .unwrap_or_default(); + // Any file picked under "All DGN Files" is a DGN. + let known = ["dwg", "dxf", "pdf", "dwf", "dwfx", "dgn", "rcp", "rcs"]; + if crate::sys::attach_filter() == ALL_DGN_FILTER + && !known.contains(&ext.as_str()) + && !IMAGE_EXTENSIONS.contains(&ext.as_str()) + { + ext = "dgn".to_string(); + } match ext.as_str() { "pdf" => match std::fs::read(&path) { Ok(bytes) => self.update(Message::PdfAttachPickResult(Ok(( @@ -477,7 +520,7 @@ impl OpenCADStudio { self.open_point_cloud_attach_dialog(&path); Task::none() } - "png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff" => { + e if IMAGE_EXTENSIONS.contains(&e) => { let result = image::image_dimensions(&path) .map(|(w, h)| (path.clone(), w, h)) .map_err(|e| e.to_string()); diff --git a/src/app/config.rs b/src/app/config.rs index c86c27920..408fa6679 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -369,6 +369,8 @@ pub struct ModelSpaceThemeConfig { pub grip_hot: u8, /// GRIPHOVER: hovered/warm grip ACI color (0 = Theme Primary Strong, 1..=255 = ACI). pub grip_hover: u8, + /// SNAPMARKERSIZE: object-snap marker half-size in pixels (1โ€“20, default 5). + pub snap_marker_size: u8, } impl Default for ModelSpaceThemeConfig { @@ -390,6 +392,7 @@ impl Default for ModelSpaceThemeConfig { grip_color: 0, grip_hot: 0, grip_hover: 0, + snap_marker_size: 5, } } } @@ -518,6 +521,8 @@ pub struct RecentConfig { pub files: Vec, /// How many recent files to keep. pub limit: usize, + /// List, icons or large previews. + pub view: super::RecentView, } impl Default for RecentConfig { @@ -525,6 +530,7 @@ impl Default for RecentConfig { Self { files: Vec::new(), limit: super::recent::RECENT_DEFAULT, + view: super::RecentView::default(), } } } diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index cbe3fdcb0..923f1d775 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -103,6 +103,9 @@ pub(super) const NAMES: &[&str] = &[ #[cfg(not(target_arch = "wasm32"))] "external_change_cancel", "pdf_dialog_ok", + "view_background", + "image_adjust", + "refedit_dialog", "pdf_layer_toggle", "pc_manager_toggle", "pc_manager", @@ -113,6 +116,7 @@ pub(super) const NAMES: &[&str] = &[ "pc_section", "ribbon_tab", "ribbon_dropdown", + "keytips", "dialog_ok", "attdef_dialog", "field_dialog", @@ -123,6 +127,7 @@ pub(super) const NAMES: &[&str] = &[ "toggle_grid", "toggle_snap", "toggle_ortho", + "grip_menu_pick", "mtext_insert", "mtext_commit", "mtext_cancel", @@ -405,6 +410,12 @@ impl OpenCADStudio { /// this needs no undo snapshot. Format follows the path extension. #[cfg(not(target_arch = "wasm32"))] pub(super) fn control_wblock(&mut self, req: &Value) -> Result, Value> { + if self.tabs[self.active_tab].block_test.is_some() { + return Err(failure( + "temporary_document", + "Test Block is temporary and cannot be saved", + )); + } let path = string(req, "path")?; let document = &self.tabs[self.active_tab].scene.document; // A template base keeps its tables/styles in the exported file @@ -698,6 +709,15 @@ impl OpenCADStudio { if req.get("upside_down").is_some() { page_dialog.upside_down = request.upside_down; } + // The requested table wins over the layout's own, which + // the stored page setup just loaded (or failed to find). + if let Some(table) = &loaded_style { + app.active_plot_style = Some(table.clone()); + page_dialog.style_name = table.name.clone(); + page_dialog.apply_plot_styles = true; + page_dialog.style_missing = false; + page_dialog.style_error = None; + } } app.plot_dialog = page_dialog; if app.plot_dialog.area == "Display" { @@ -831,6 +851,7 @@ impl OpenCADStudio { "point": [grip.world.x, grip.world.y, grip.world.z], "shape": format!("{:?}", grip.shape), "moves_entity": grip.is_midpoint, + "axis": grip.axis.map(|a| [a.x, a.y, a.z]), }) }) .collect::>(); @@ -901,27 +922,33 @@ impl OpenCADStudio { /// Click the viewport where `point` (world) shows, as the left button /// would, holding Ctrl and/or Shift when `modifiers` names them. - pub(super) fn control_click(&mut self, req: &Value) -> Result, Value> { - let i = self.active_tab; - let at = point(req)?; - let modifiers = req["modifiers"].as_str().unwrap_or("").to_ascii_lowercase(); - let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size; - let tile = self.tabs[i].scene.active_model_tile_bounds(vw, vh); + /// Where a world point shows on the drawing canvas, through the camera + /// the cursor edits with: an entered viewport's, else the active tile's. + fn control_world_to_canvas(&self, at: glam::DVec3) -> Result { + let scene = &self.tabs[self.active_tab].scene; + let (vw, vh) = scene.selection.borrow().view.vp_size; + let (camera, bounds) = scene.viewport_edit_frame((vw, vh)).unwrap_or_else(|| { + (scene.camera.borrow().clone(), scene.active_model_tile_bounds(vw, vh)) + }); let local = iced::Rectangle { - width: tile.width, - height: tile.height, + width: bounds.width, + height: bounds.height, ..iced::Rectangle::default() }; - let screen = self.tabs[i] - .scene - .camera - .borrow() + camera .project(at, local) - .ok_or_else(|| failure("invalid_point", "The point is not in view"))?; + .filter(|p| p.x.is_finite() && p.y.is_finite()) + .map(|p| iced::Point::new(p.x + bounds.x, p.y + bounds.y)) + .ok_or_else(|| failure("invalid_point", "The point is not in view")) + } + + pub(super) fn control_click(&mut self, req: &Value) -> Result, Value> { + let at = point(req)?; + let modifiers = req["modifiers"].as_str().unwrap_or("").to_ascii_lowercase(); + let cursor = self.control_world_to_canvas(at)?; let saved = (self.ctrl_down, self.shift_down); self.ctrl_down = modifiers.contains("ctrl"); self.shift_down = modifiers.contains("shift"); - let cursor = iced::Point::new(screen.x + tile.x, screen.y + tile.y); let tasks = [ self.update(Message::ViewportMove(cursor)), self.update(Message::ViewportLeftPress), @@ -931,6 +958,86 @@ impl OpenCADStudio { Ok(Task::batch(tasks)) } + /// Replay pointer events on the drawing canvas in order, as the mouse + /// sends them: moves (canvas pixels, or a world point projected through + /// the editing camera), button presses and releases, and wheel steps. + /// Each step reports where the cursor landed in the world, so a drag + /// shows the points the interaction actually received. + pub(super) fn control_pointer(&mut self, req: &Value) -> Result, Value> { + let i = self.active_tab; + let events = req["events"] + .as_array() + .filter(|events| !events.is_empty()) + .ok_or_else(|| failure("invalid_request", "Supply a non-empty events array"))?; + let modifiers = req["modifiers"].as_str().unwrap_or("").to_ascii_lowercase(); + let coords = |value: &Value, n: usize| -> Option> { + let values: Vec = value.as_array()?.iter().filter_map(Value::as_f64).collect(); + (values.len() == n && values.iter().all(|v| v.is_finite())).then_some(values) + }; + let saved = (self.ctrl_down, self.shift_down); + self.ctrl_down = modifiers.contains("ctrl"); + self.shift_down = modifiers.contains("shift"); + let mut tasks = Vec::new(); + let mut steps = Vec::new(); + let mut outcome = Ok(()); + for event in events { + let message = if let Some(xy) = event.get("move").and_then(|v| coords(v, 2)) { + Message::ViewportMove(iced::Point::new(xy[0] as f32, xy[1] as f32)) + } else if let Some(at) = event.get("move_world").and_then(|v| coords(v, 3)) { + match self.control_world_to_canvas(glam::DVec3::new(at[0], at[1], at[2])) { + Ok(cursor) => Message::ViewportMove(cursor), + Err(error) => { + outcome = Err(error); + break; + } + } + } else if let Some(button) = event.get("press").and_then(Value::as_str) { + match button { + "left" => Message::ViewportLeftPress, + "right" => Message::ViewportRightPress, + "middle" => Message::ViewportMiddlePress, + _ => { + outcome = Err(failure("invalid_request", "press is left, right or middle")); + break; + } + } + } else if let Some(button) = event.get("release").and_then(Value::as_str) { + match button { + "left" => Message::ViewportLeftRelease, + "right" => Message::ViewportRightRelease, + "middle" => Message::ViewportMiddleRelease, + _ => { + outcome = Err(failure("invalid_request", "release is left, right or middle")); + break; + } + } + } else if let Some(lines) = event.get("wheel").and_then(Value::as_f64) { + Message::ViewportScroll(iced::mouse::ScrollDelta::Lines { + x: 0.0, + y: lines as f32, + }) + } else { + outcome = Err(failure( + "invalid_request", + "Each event is move, move_world, press, release or wheel", + )); + break; + }; + tasks.push(self.update(message)); + let tab = &self.tabs[i]; + let world = tab.last_cursor_world; + steps.push(json!({ + "screen":[tab.last_cursor_screen.x, tab.last_cursor_screen.y], + "world":[world.x, world.y, world.z], + "dragging_grip":tab.active_grip.is_some(), + })); + } + (self.ctrl_down, self.shift_down) = saved; + outcome?; + self.set_control_result(json!({"steps":steps})); + Ok(Task::batch(tasks)) + } + pub(super) fn control_set_property(&mut self, req: &Value) -> Result, Value> { self.refresh_properties(); let field = string(req, "field")?; @@ -1786,6 +1893,169 @@ impl OpenCADStudio { }) } "ribbon_dropdown" => Message::ToggleRibbonDropdown(string(req, "value")?.into()), + // The Edit Reference dialog, one field per call: "level=" (0 = + // the picked reference), "nested=0|1" (pick the working set in the + // drawing), "attributes=0|1", "lock=0|1", "unique=0|1", "ok" or + // "cancel". + "refedit_dialog" => { + use crate::ui::window::refedit_dialog::RefEditDialogMsg as R; + if self.active_modal != Some(crate::app::ModalKind::RefEdit) { + return Err(failure("no_dialog", "The Reference Edit dialog is not open")); + } + let spec = string(req, "value")?; + let (key, value) = spec.split_once('=').unwrap_or((spec, "")); + let flag = || match value { + "1" => Ok(true), + "0" => Ok(false), + _ => Err(failure("invalid_value", spec)), + }; + if key == "cancel" { + Message::CloseModal + } else { + Message::RefEditDialog(match key { + "ok" => R::Ok, + "level" => R::Level(value.parse().map_err(|_| failure("invalid_value", spec))?), + "nested" => R::Nested(flag()?), + "attributes" => R::Attributes(flag()?), + "lock" => R::Lock(flag()?), + "unique" => R::Unique(flag()?), + _ => return Err(failure("invalid_value", "Unknown Reference Edit field")), + }) + } + } + // Ribbon key tips: `show`, `alt` (a lone Alt), `type:`, `enter`, + // `esc` or `hide`. Read them back with the `keytips` read operation. + "keytips" => { + use crate::ui::ribbon::KeyTipInput as K; + let value = string(req, "value")?; + let inputs: Vec = match value { + "show" => vec![K::Open { refocus: false }], + "alt" => vec![K::AltTap], + "esc" => vec![K::Escape], + "hide" => vec![K::Hide], + "enter" => vec![K::Enter], + _ => match value.strip_prefix("type:") { + Some(codes) if !codes.is_empty() => codes + .chars() + .map(|ch| K::Char(ch.to_uppercase().collect())) + .collect(), + _ => return Err(failure("invalid_value", "Use show, alt, type:, enter, esc or hide")), + }, + }; + let tasks: Vec<_> = inputs.into_iter().map(|input| self.on_keytip(input)).collect(); + return Ok(Task::batch(tasks)); + } + // The -VIEW Background dialog, one field per call: "type=gradient", + // "color|top|middle|bottom=r,g,b", "three=1", "rotation=30", + // "image=", "position=center|stretch|tile", "aspect=1", "offset=x,y" (pixels), "scale=x,y", + // "step=offset_x|offset_y|scale_x|scale_y,+1|-1", "reset", + // "apply" or "cancel". + "view_background" => { + use crate::ui::window::pdf_dialogs::{BgKind, BgPosition, PdfDialogMsg as P}; + if self.active_modal != Some(crate::app::ModalKind::ViewBackground) { + return Err(failure("no_dialog", "The Background dialog is not open")); + } + let spec = string(req, "value")?; + let (key, value) = spec.split_once('=').unwrap_or((spec, "")); + let bad = || failure("invalid_value", spec); + let rgb = || -> Result<[u8; 3], Value> { + let parts: Vec = value + .split(',') + .map(|part| part.trim().parse::()) + .collect::>() + .map_err(|_| bad())?; + <[u8; 3]>::try_from(parts).map_err(|_| bad()) + }; + match key { + "cancel" => Message::CloseModal, + "apply" => Message::PdfDialog(P::BgApply), + "type" => Message::PdfDialog(P::BgKind(match value { + "none" => BgKind::None, + "solid" => BgKind::Solid, + "gradient" => BgKind::Gradient, + "image" => BgKind::Image, + _ => return Err(bad()), + })), + "color" => Message::PdfDialog(P::BgColor(0, rgb()?)), + "top" => Message::PdfDialog(P::BgColor(1, rgb()?)), + "middle" => Message::PdfDialog(P::BgColor(2, rgb()?)), + "bottom" => Message::PdfDialog(P::BgColor(3, rgb()?)), + "three" => Message::PdfDialog(P::BgThree(value == "1")), + "rotation" => Message::PdfDialog(P::BgRotation(value.to_string())), + "image" => Message::PdfDialog(P::BgImage(value.to_string())), + "position" => Message::PdfDialog(P::BgPosition(match value { + "center" => BgPosition::Center, + "stretch" => BgPosition::Stretch, + "tile" => BgPosition::Tile, + _ => return Err(bad()), + })), + "aspect" => Message::PdfDialog(P::BgAspect(value == "1")), + "offset" | "scale" => { + let pair: Vec = value.split(',').map(|part| part.trim().to_string()).collect(); + let pair = <[String; 2]>::try_from(pair).map_err(|_| bad())?; + if pair.iter().any(|v| v.parse::().map_or(true, |v| !v.is_finite())) { + return Err(bad()); + } + if key == "offset" { + Message::PdfDialog(P::BgOffset(pair)) + } else { + Message::PdfDialog(P::BgScale(pair)) + } + } + "step" => { + // step=offset_x|offset_y|scale_x|scale_y,+1|-1 + let (field, dir) = value.split_once(',').ok_or_else(bad)?; + let field = match field { + "offset_x" => 0, + "offset_y" => 1, + "scale_x" => 2, + "scale_y" => 3, + _ => return Err(bad()), + }; + let dir: i8 = dir.trim().parse().map_err(|_| bad())?; + Message::PdfDialog(P::BgStep(field, dir)) + } + "reset" => Message::PdfDialog(P::BgReset), + _ => return Err(bad()), + } + } + // The Image Adjust dialog, one field per call: "brightness=70", + // "contrast=40", "fade=20" (the bar), "type=fade,abc" (typed into + // the box), "step=brightness,+1|-1", "reset", "apply" or "cancel". + "image_adjust" => { + use crate::ui::window::pdf_dialogs::PdfDialogMsg as P; + if self.active_modal != Some(crate::app::ModalKind::ImageAdjust) { + return Err(failure("no_dialog", "The Image Adjust dialog is not open")); + } + let spec = string(req, "value")?; + let (key, value) = spec.split_once('=').unwrap_or((spec, "")); + let bad = || failure("invalid_value", spec); + let row = |name: &str| match name.trim() { + "brightness" => Ok(0u8), + "contrast" => Ok(1), + "fade" => Ok(2), + _ => Err(bad()), + }; + match key { + "cancel" => Message::CloseModal, + "apply" => Message::PdfDialog(P::IaApply), + "reset" => Message::PdfDialog(P::IaReset), + "brightness" | "contrast" | "fade" => { + let v: u8 = value.trim().parse().ok().filter(|v| *v <= 100).ok_or_else(bad)?; + Message::PdfDialog(P::IaValue(row(key)?, v)) + } + "type" => { + let (name, typed) = value.split_once(',').ok_or_else(bad)?; + Message::PdfDialog(P::IaText(row(name)?, typed.to_string())) + } + "step" => { + let (name, dir) = value.split_once(',').ok_or_else(bad)?; + let dir: i8 = dir.trim().parse().map_err(|_| bad())?; + Message::PdfDialog(P::IaStep(row(name)?, dir)) + } + _ => return Err(bad()), + } + } "pdf_dialog_ok" => { use crate::ui::window::pdf_dialogs::PdfDialogMsg; Message::PdfDialog(match self.active_modal { @@ -1825,6 +2095,20 @@ impl OpenCADStudio { "toggle_grid" => Message::ToggleGrid, "toggle_snap" => Message::ToggleSnapEnabled, "toggle_ortho" => Message::ToggleOrtho, + // Choose an entry of the open multi-functional grip menu by its + // position (0 = first), as a click on it does. + "grip_menu_pick" => { + let entries = self.grip_popup.as_ref().map_or(0, |popup| popup.items.len()); + if entries == 0 { + return Err(failure("menu_closed", "No grip menu is open")); + } + let index = string(req, "value")? + .parse::() + .ok() + .filter(|index| *index < entries) + .ok_or_else(|| failure("invalid_value", "value is a menu entry index"))?; + Message::GripMenuPick(index) + } "mtext_insert" => { if self.mtext_editor.is_none() { return Err(failure("editor_closed", "MText editor is closed")); diff --git a/src/app/control/entities.rs b/src/app/control/entities.rs index e26214e1b..2e9f9131b 100644 --- a/src/app/control/entities.rs +++ b/src/app/control/entities.rs @@ -5,161 +5,10 @@ use super::*; use crate::command::EntityTransform; +use crate::entity_creation::{build_entity, deg_field, point_field}; type Parsed = Result; -/// Parse `[x,y]` or `[x,y,z]` without silently turning a malformed -/// coordinate into zero. -fn point_value(value: Option<&Value>, label: &str, code: &str) -> Parsed { - let values = value - .and_then(Value::as_array) - .filter(|v| (2..=3).contains(&v.len())) - .ok_or_else(|| failure(code, format!("{label} must be [x,y] or [x,y,z]")))?; - let coord = |i: usize| { - values[i] - .as_f64() - .filter(|v| v.is_finite()) - .ok_or_else(|| failure(code, format!("{label} needs finite coordinates"))) - }; - let z = values - .get(2) - .map(|value| { - value - .as_f64() - .filter(|v| v.is_finite()) - .ok_or_else(|| failure(code, format!("{label} needs finite coordinates"))) - }) - .transpose()? - .unwrap_or(0.0); - Ok(codec::types::Vector3::new(coord(0)?, coord(1)?, z)) -} - -/// `[x,y]` or `[x,y,z]` (missing z = 0) from `req[key]`. -fn point_field(req: &Value, key: &str) -> Parsed { - point_value(req.get(key), key, "invalid_point") -} - -fn point_list_field( - spec: &Value, - key: &str, - minimum: usize, -) -> Parsed> { - let points = spec[key] - .as_array() - .filter(|points| points.len() >= minimum) - .ok_or_else(|| { - failure( - "invalid_spline_points", - format!("{key} needs at least {minimum} points"), - ) - })?; - points - .iter() - .enumerate() - .map(|(index, point)| { - point_value( - Some(point), - &format!("{key}[{index}]"), - "invalid_spline_points", - ) - }) - .collect() -} - -fn finite_number_list_field(spec: &Value, key: &str) -> Parsed> { - let values = spec[key] - .as_array() - .filter(|values| !values.is_empty()) - .ok_or_else(|| failure("invalid_spline_values", format!("{key} needs a non-empty array")))?; - values - .iter() - .enumerate() - .map(|(index, value)| { - value - .as_f64() - .filter(|value| value.is_finite()) - .ok_or_else(|| { - failure( - "invalid_spline_values", - format!("{key}[{index}] must be finite"), - ) - }) - }) - .collect() -} - -fn deg_field(req: &Value, key: &str) -> f64 { - req[key].as_f64().unwrap_or(0.0).to_radians() -} - -/// Reject fields that the typed builder would otherwise silently ignore. -fn validate_entity_fields(spec: &Value, kind: &str) -> Parsed<()> { - let Some(object) = spec.as_object() else { - return Ok(()); - }; - for field in object.keys() { - let allowed = match kind { - "LINE" => matches!( - field.as_str(), - "type" | "start" | "end" | "thickness" | "layer" | "color" - ), - "CIRCLE" => matches!(field.as_str(), "type" | "center" | "radius" | "layer" | "color"), - "ARC" => matches!( - field.as_str(), - "type" | "center" | "radius" | "start_angle_deg" | "end_angle_deg" | "layer" | "color" - ), - "SPLINE" => matches!( - field.as_str(), - "type" | "degree" | "knots" | "control_points" | "fit_points" | "weights" - | "closed" | "periodic" | "rational" | "planar" | "linear" | "layer" | "color" - ), - "LWPOLYLINE" | "POLYLINE" => matches!( - field.as_str(), - "type" | "vertices" | "closed" | "constant_width" | "layer" | "color" - ), - "POINT" => matches!(field.as_str(), "type" | "location" | "layer" | "color"), - "TEXT" => matches!( - field.as_str(), - "type" | "value" | "position" | "height" | "rotation_deg" | "style" | "layer" | "color" - ), - "MTEXT" => matches!( - field.as_str(), - "type" | "value" | "position" | "height" | "width" | "rotation_deg" | "style" | "layer" | "color" - ), - "INSERT" => matches!( - field.as_str(), - "type" | "block" | "position" | "scale" | "rotation_deg" | "layer" | "color" - ), - "SOLID" => matches!(field.as_str(), "type" | "corners" | "layer" | "color"), - "HATCH" => matches!( - field.as_str(), - "type" | "boundary" | "solid" | "pattern" | "pattern_scale" | "pattern_angle_deg" | "layer" | "color" - ), - _ => true, - }; - if !allowed { - return Err(failure( - "unknown_entity_field", - format!("Unknown field '{field}' for {kind} entity"), - )); - } - } - Ok(()) -} - -/// Required finite angle in degrees, converted to radians without a default. -fn finite_angle_field(spec: &Value, key: &str) -> Parsed { - let degrees = spec[key] - .as_f64() - .filter(|value| value.is_finite()) - .ok_or_else(|| failure("invalid_angle", format!("arc needs a finite {key}")))?; - let radians = degrees.to_radians(); - radians - .is_finite() - .then_some(radians) - .ok_or_else(|| failure("invalid_angle", format!("arc needs a finite {key}"))) -} - pub(super) fn hex_handles(req: &Value, key: &str) -> Parsed> { let list = req[key] .as_array() @@ -211,330 +60,6 @@ pub(super) fn require_existing( } } -fn apply_common_properties(spec: &Value, entity: &mut codec::EntityType) -> Parsed<()> { - if let Some(layer) = spec["layer"].as_str() { - if !layer.is_empty() - && !matches!( - entity, - codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_) - ) - { - entity.common_mut().layer = layer.to_owned(); - } - } - if let Some(index) = spec["color"].as_i64().and_then(|v| i16::try_from(v).ok()) { - entity.common_mut().color = codec::types::Color::from_index(index); - } - Ok(()) -} - -/// An explicit text style must exist in the current document; otherwise a -/// typo would silently fall back to the default style. -fn text_style_field(spec: &Value, document: &codec::CadDocument) -> Parsed> { - let Some(style) = spec["style"].as_str().filter(|s| !s.is_empty()) else { - return Ok(None); - }; - if !document.text_styles.contains(style) { - return Err(failure( - "invalid_text_style", - format!("Text style '{style}' does not exist in the document"), - )); - } - Ok(Some(style.to_owned())) -} - -/// One `entities` array entry โ†’ a real entity. Geometry validation happens -/// here so a bad definition aborts the whole batch before anything commits. -fn build_entity(spec: &Value, document: &codec::CadDocument) -> Parsed { - use codec::entities::*; - use codec::types::Vector3; - let kind = spec["type"].as_str().unwrap_or("").to_ascii_uppercase(); - validate_entity_fields(spec, &kind)?; - let entity = match kind.as_str() { - "LINE" => { - let mut line = Line::from_points(point_field(spec, "start")?, point_field(spec, "end")?); - if let Some(t) = spec["thickness"].as_f64() { - line.thickness = t; - } - EntityType::Line(line) - } - "CIRCLE" => { - let radius = spec["radius"] - .as_f64() - .filter(|r| r.is_finite() && *r >= 0.0) - .ok_or_else(|| failure("invalid_radius", "circle needs a finite radius >= 0"))?; - EntityType::Circle(Circle::from_center_radius(point_field(spec, "center")?, radius)) - } - "ARC" => { - let radius = spec["radius"] - .as_f64() - .filter(|r| r.is_finite() && *r > 0.0) - .ok_or_else(|| failure("invalid_radius", "arc needs a finite radius > 0"))?; - let arc = Arc::from_center_radius_angles( - point_field(spec, "center")?, - radius, - finite_angle_field(spec, "start_angle_deg")?, - finite_angle_field(spec, "end_angle_deg")?, - ); - if !arc.sweep_angle().is_finite() || arc.sweep_angle() <= 0.0 { - return Err(failure( - "invalid_sweep", - "arc start_angle_deg and end_angle_deg must define a finite non-zero sweep", - )); - } - EntityType::Arc(arc) - } - "SPLINE" => { - let control_points = if spec.get("control_points").is_some() { - point_list_field(spec, "control_points", 2)? - } else { - Vec::new() - }; - let fit_points = if spec.get("fit_points").is_some() { - point_list_field(spec, "fit_points", 2)? - } else { - Vec::new() - }; - if control_points.is_empty() && fit_points.is_empty() { - return Err(failure( - "invalid_spline_points", - "spline needs control_points or fit_points (at least 2 points)", - )); - } - let degree = match spec.get("degree") { - None | Some(Value::Null) => 3, - Some(value) => value - .as_i64() - .filter(|degree| (1..=10).contains(degree)) - .ok_or_else(|| { - failure("invalid_spline_values", "degree must be an integer 1..10") - })? as i32, - }; - let count = control_points.len(); - if count > 0 && count <= degree as usize { - return Err(failure( - "invalid_spline_values", - format!("a degree {degree} spline needs more than {degree} control_points"), - )); - } - let mut spline = if control_points.is_empty() { - let mut spline = Spline::from_fit_points(fit_points.clone()); - spline.degree = degree; - spline - } else { - Spline::from_control_points(degree, control_points) - }; - if !fit_points.is_empty() { - spline.fit_points = fit_points; - } - if spec.get("knots").is_some() { - if spline.control_points.is_empty() { - return Err(failure( - "invalid_spline_values", - "spline knots require control_points", - )); - } - let knots = finite_number_list_field(spec, "knots")?; - if knots.len() != count + degree as usize + 1 - || knots.windows(2).any(|pair| pair[1] < pair[0]) - { - return Err(failure( - "invalid_spline_values", - format!( - "knots must be {} non-decreasing values (control_points + degree + 1)", - count + degree as usize + 1 - ), - )); - } - spline.knots = knots; - } - if spec.get("weights").is_some() { - if spline.control_points.is_empty() { - return Err(failure( - "invalid_spline_values", - "spline weights require control_points", - )); - } - let weights = finite_number_list_field(spec, "weights")?; - if weights.len() != count || weights.iter().any(|weight| *weight <= 0.0) { - return Err(failure( - "invalid_spline_values", - format!("weights must be {count} positive values, one per control point"), - )); - } - spline.weights = weights; - spline.flags.rational = true; - } - if let Some(value) = spec["closed"].as_bool() { - spline.flags.closed = value; - } - if let Some(value) = spec["periodic"].as_bool() { - spline.flags.periodic = value; - } - if let Some(value) = spec["rational"].as_bool() { - spline.flags.rational = value; - } - if let Some(value) = spec["planar"].as_bool() { - spline.flags.planar = value; - } - if let Some(value) = spec["linear"].as_bool() { - spline.flags.linear = value; - } - EntityType::Spline(spline) - } - "LWPOLYLINE" | "POLYLINE" => { - let vertices = spec["vertices"] - .as_array() - .filter(|v| v.len() >= 2) - .ok_or_else(|| failure("invalid_vertices", "polyline needs vertices:[[x,y],โ€ฆ]"))?; - let mut pline = LwPolyline::new(); - for point in vertices { - let x = point[0].as_f64().filter(|v| v.is_finite()).ok_or_else(|| { - failure("invalid_vertices", "polyline vertices must be finite [x,y]") - })?; - let y = point[1].as_f64().filter(|v| v.is_finite()).ok_or_else(|| { - failure("invalid_vertices", "polyline vertices must be finite [x,y]") - })?; - pline.add_point(codec::types::Vector2::new(x, y)); - } - if spec["closed"].as_bool().unwrap_or(false) { - pline.close(); - } - if let Some(width) = spec["constant_width"].as_f64() { - pline.constant_width = width; - } - EntityType::LwPolyline(pline) - } - "POINT" => EntityType::Point(Point::at(point_field(spec, "location")?)), - "TEXT" => { - let value = spec["value"].as_str().unwrap_or(""); - if value.is_empty() { - return Err(failure("invalid_text", "text needs a non-empty value")); - } - let mut text = Text::with_value(value, point_field(spec, "position")?) - .with_height(spec["height"].as_f64().unwrap_or(2.5).abs().max(1e-6)) - .with_rotation(deg_field(spec, "rotation_deg")); - if let Some(style) = text_style_field(spec, document)? { - text.style = style; - } - EntityType::Text(text) - } - "MTEXT" => { - let value = spec["value"].as_str().unwrap_or(""); - if value.is_empty() { - return Err(failure("invalid_text", "mtext needs a non-empty value")); - } - let mut mtext = MText::with_value(value, point_field(spec, "position")?) - .with_height(spec["height"].as_f64().unwrap_or(2.5).abs().max(1e-6)); - if let Some(width) = spec["width"].as_f64().filter(|w| *w > 0.0) { - mtext.rectangle_width = width; - } - mtext.rotation = deg_field(spec, "rotation_deg"); - if let Some(style) = text_style_field(spec, document)? { - mtext.style = style; - } - EntityType::MText(mtext) - } - "INSERT" => { - let block = spec["block"].as_str().unwrap_or(""); - if block.is_empty() { - return Err(failure("block_required", "insert needs a block name")); - } - let mut insert = Insert::new(block, point_field(spec, "position")?) - .with_rotation(deg_field(spec, "rotation_deg")); - match &spec["scale"] { - Value::Number(factor) => { - let factor = factor.as_f64().filter(|f| *f != 0.0).ok_or_else(|| { - failure("invalid_scale", "insert scale must be nonzero") - })?; - insert = insert.with_uniform_scale(factor); - } - Value::Array(factors) if factors.len() == 3 => { - let axis = |i: usize| { - factors[i].as_f64().filter(|f| *f != 0.0).ok_or_else(|| { - failure("invalid_scale", "insert scale components must be nonzero") - }) - }; - insert = insert.with_scale(axis(0)?, axis(1)?, axis(2)?); - } - _ => {} - } - EntityType::Insert(insert) - } - "SOLID" => { - let raw_corners = spec["corners"] - .as_array() - .filter(|v| (3..=4).contains(&v.len())) - .ok_or_else(|| failure("invalid_corners", "solid needs 3 or 4 corners"))?; - let corners: Vec = raw_corners - .iter() - .enumerate() - .map(|(index, value)| { - if value.as_array().is_none_or(|values| values.len() != 3) { - return Err(failure( - "invalid_corners", - format!("corners[{index}] must be [x,y,z]"), - )); - } - point_value( - Some(value), - &format!("corners[{index}]"), - "invalid_corners", - ) - }) - .collect::>()?; - let solid = match corners.as_slice() { - [first, second, third] => Solid::triangle(*first, *second, *third), - [first, second, third, fourth] => Solid::new(*first, *second, *third, *fourth), - _ => unreachable!("corner count was validated above"), - }; - EntityType::Solid(solid) - } - "HATCH" => { - let boundary = spec["boundary"] - .as_array() - .filter(|v| v.len() >= 3) - .ok_or_else(|| failure("invalid_boundary", "hatch needs boundary:[[x,y],โ€ฆ] (>= 3 points)"))?; - let mut vertices = Vec::with_capacity(boundary.len()); - for point in boundary { - let x = point[0].as_f64().filter(|v| v.is_finite()).ok_or_else(|| { - failure("invalid_boundary", "hatch boundary must be finite [x,y]") - })?; - let y = point[1].as_f64().filter(|v| v.is_finite()).ok_or_else(|| { - failure("invalid_boundary", "hatch boundary must be finite [x,y]") - })?; - vertices.push(codec::types::Vector2::new(x, y)); - } - let mut hatch = Hatch::new(); - if spec["solid"].as_bool().unwrap_or(false) { - hatch.is_solid = true; - } else { - hatch.is_solid = false; - hatch.pattern = HatchPattern::new( - spec["pattern"].as_str().unwrap_or("ANSI31"), - ); - hatch.pattern_scale = spec["pattern_scale"].as_f64().unwrap_or(1.0).abs().max(1e-6); - hatch.pattern_angle = deg_field(spec, "pattern_angle_deg"); - } - let mut path = BoundaryPath::external(); - path.add_edge(BoundaryEdge::Polyline(PolylineEdge::new(vertices, true))); - hatch.paths.push(path); - EntityType::Hatch(hatch) - } - other => { - return Err(failure( - "unknown_entity_type", - format!( - "Unknown entity type '{other}'. Supported: Line, Circle, Arc, Spline, LwPolyline, Point, Text, MText, Insert, Solid, Hatch" - ), - )) - } - }; - let mut entity = entity; - apply_common_properties(spec, &mut entity)?; - Ok(entity) -} - impl OpenCADStudio { /// `entities_create` โ€” build a batch of typed entities, auto-create the /// layers they name, and commit everything as one undoable step. All definitions are validated before the first @@ -547,7 +72,7 @@ impl OpenCADStudio { let built: Vec = { let document = &self.tabs[self.active_tab].scene.document; list.iter() - .map(|spec| build_entity(spec, document)) + .map(|spec| build_entity(spec, Some(document))) .collect::>()? }; diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index a5524e652..caaff5e31 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -204,6 +204,26 @@ fn open_bytes_request(req: &Value) -> Result<(String, Vec), Value> { } Ok((name.to_string(), bytes)) } +/// `{"op":"font_register","file_name":"Brand-Regular.ttf","data_base64":"โ€ฆ"}`: +/// a host hands over a TrueType/OpenType file that text styles reference by +/// name. The web build has no font folder, so this is how such styles get +/// their real face instead of a fallback. +fn font_register_request(req: &Value) -> Result<(String, Vec), Value> { + use base64::Engine as _; + let name = string(req, "file_name")?; + let name = name.rsplit(['/', '\\']).next().unwrap_or(name).trim(); + if name.is_empty() { + return Err(failure("invalid_request", "Missing file_name")); + } + let bytes = base64::engine::general_purpose::STANDARD + .decode(req["data_base64"].as_str().unwrap_or_default()) + .map_err(|e| failure("invalid_request", format!("data_base64: {e}")))?; + if bytes.is_empty() { + return Err(failure("invalid_request", "data_base64 is empty")); + } + Ok((name.to_string(), bytes)) +} + fn string<'a>(req: &'a Value, key: &str) -> Result<&'a str, Value> { req[key] .as_str() @@ -277,6 +297,7 @@ fn command_category(name: &str) -> &'static str { "BREAK", "JOIN", "EXPLODE", + "TXTEXP", "STRETCH", "ARRAY", "PEDIT", @@ -349,6 +370,7 @@ fn command_selection_policy(name: &str) -> &'static str { "CUTCLIP", "ERASE", "EXPLODE", + "TXTEXP", "GROUP", "LAYFRZ", "LAYLCK", @@ -537,6 +559,7 @@ impl OpenCADStudio { | "audit" | "text_search" | "text_audit" + | "keytips" ); if !query && !self.control.enabled { return ( @@ -773,7 +796,12 @@ impl OpenCADStudio { } // Interactive requests operate on the active tab by definition, so // a client may omit document_id (the GUI-hosted REST channel does). - } else if !query && !matches!(op, "new" | "open" | "stop" | "user_select" | "getpoint") { + } else if !query + && !matches!( + op, + "new" | "open" | "font_register" | "stop" | "user_select" | "getpoint" + ) + { return ( failure("document_required", "Read state and supply document_id"), Task::none(), @@ -823,6 +851,7 @@ impl OpenCADStudio { } } "grips" => self.control_grips(), + "keytips" => self.ribbon.keytips_json(req["detail"].as_str() == Some("full")), "xdata_get" => self.xdata_read(&req), "get_selection" => self.control_get_selection(), "text_search" => self.control_text_search(&req).unwrap_or_else(|e| e), @@ -977,6 +1006,24 @@ impl OpenCADStudio { "open" => self.update(Message::OpenExternal(std::path::PathBuf::from(string( req, "path", )?))), + "font_register" => { + let (name, bytes) = font_register_request(req)?; + let (family, faces) = + crate::scene::text::sysfont::register_font_file(&name, &bytes) + .ok_or_else(|| failure("font_invalid", "data_base64 holds no font face"))?; + // Same invalidation as a fetched web font: styles that fell + // back while the file was missing lay out again. + crate::scene::text::ttf_glyph::clear_fallback_cache(); + for tab in self.tabs.iter_mut() { + tab.scene.invalidate_text_geometry_dependencies(); + } + self.set_control_result(json!({ + "file_name": name, + "family": family, + "faces": faces, + })); + Task::none() + } "activate" | "switch_document" => { let i = self .tabs @@ -1070,6 +1117,7 @@ impl OpenCADStudio { "property" => self.control_set_property(req)?, "grip_drag" => self.control_grip_drag(req)?, "click" => self.control_click(req)?, + "pointer" => self.control_pointer(req)?, "set_properties" => self.control_set_record_properties(req)?, "action" => self.control_ui_action(req)?, #[cfg(not(target_arch = "wasm32"))] @@ -1100,6 +1148,12 @@ impl OpenCADStudio { "view_focus" => self.control_view_focus(req)?, #[cfg(not(target_arch = "wasm32"))] "save" => { + if self.tabs[i].block_test.is_some() { + return Err(failure( + "temporary_document", + "Test Block is temporary and cannot be saved", + )); + } let path = req["path"] .as_str() .map(std::path::PathBuf::from) @@ -1147,6 +1201,55 @@ impl OpenCADStudio { ) } } + // Web: no filesystem path; the drawing comes back in the reply. + #[cfg(target_arch = "wasm32")] + "save" => { + if self.tabs[i].block_test.is_some() { + return Err(failure( + "temporary_document", + "Test Block is temporary and cannot be saved", + )); + } + use base64::Engine as _; + let default_is_dxf = crate::io::source_is_dxf(None, &self.tabs[i].scene.document); + let (version, is_dxf) = super::automation::requested_save_target( + req, + self.tabs[i].scene.document.version, + default_is_dxf, + None, + ) + .map_err(|error| failure("invalid_target", error))?; + let dropped = + crate::io::dropped_on_save_count(&self.tabs[i].scene.document, version, is_dxf); + if dropped > 0 && req["allow_lossy"].as_bool() != Some(true) { + return Err(failure( + "lossy_conversion_not_acknowledged", + format!( + "Conversion would drop {dropped} unsupported record(s); set allow_lossy=true to acknowledge" + ), + )); + } + let ext = if is_dxf { "dxf" } else { "dwg" }; + self.sync_view_state_for_save(i); + super::update::util::sync_annotation_scale_header(&mut self.tabs[i].scene); + self.stamp_header_sysvars(i); + self.sync_solid_models_for_save(i); + self.tabs[i].scene.sync_native_parametric_graph(); + let bytes = crate::io::save_to_bytes(&self.tabs[i].scene.document, ext, version) + .map_err(|error| failure("save_failed", error))?; + self.tabs[i].dirty = false; + let name = req["name"].as_str().filter(|name| !name.is_empty()); + let name = name.map_or_else(|| format!("drawing.{ext}"), str::to_string); + self.set_control_result(json!({ + "name": name, + "target_format": ext, + "target_version": format!("{version:?}"), + "dropped_on_save": dropped, + "bytes": bytes.len(), + "data_base64": base64::engine::general_purpose::STANDARD.encode(&bytes), + })); + Task::none() + } #[cfg(not(target_arch = "wasm32"))] "save_verified" => { let result = self.save_verified_request(req)?; @@ -2260,6 +2363,59 @@ mod tests { let bad = open_bytes_request(&json!({"name":"a.dwg","data_base64":"not base64!"})); assert_eq!(bad.unwrap_err()["code"], "invalid_request"); } + #[test] + fn font_register_validates_the_request() { + let ok = font_register_request( + &json!({"file_name":"C:\\Fonts\\Brand.ttf","data_base64":"AAEC"}), + ); + assert_eq!(ok, Ok(("Brand.ttf".to_string(), vec![0, 1, 2]))); + for req in [ + json!({"data_base64":"AAEC"}), + json!({"file_name":"fonts/","data_base64":"AAEC"}), + json!({"file_name":"a.ttf","data_base64":""}), + json!({"file_name":"a.ttf","data_base64":"not base64!"}), + ] { + assert_eq!( + font_register_request(&req).unwrap_err()["code"], + "invalid_request", + "{req}" + ); + } + } + + #[test] + fn font_register_makes_a_style_file_resolve() { + use base64::Engine as _; + let bytes = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/web/fonts/greek.ttf")) + .expect("bundled web font"); + let mut app = OpenCADStudio::new_for_test(); + request(&mut app, json!({"op":"new"})); + let reply = request( + &mut app, + json!({ + "op":"font_register", + "file_name":"Control-Registered.ttf", + "data_base64": base64::engine::general_purpose::STANDARD.encode(&bytes), + }), + ); + assert_eq!(reply["ok"], true, "{reply}"); + let family = reply["result"]["family"] + .as_str() + .expect("family") + .to_string(); + assert!(reply["result"]["faces"].as_u64().unwrap() >= 1, "{reply}"); + assert_eq!( + crate::scene::text::sysfont::family_for_reference("control-registered.ttf"), + Some(family) + ); + + let refused = request( + &mut app, + json!({"op":"font_register","file_name":"x.ttf","data_base64":"AAEC"}), + ); + assert_eq!(refused["code"], "font_invalid", "{refused}"); + } + #[test] fn open_from_bytes_is_refused_natively_without_touching_documents() { let mut app = OpenCADStudio::new_for_test(); @@ -2763,8 +2919,8 @@ mod tests { assert_eq!(response["status"], "completed", "{response}"); // The plotted PDF carries two distinct pen widths โ€” the per-layer - // 0.13 mm / 0.50 mm hierarchy survives the plot style (thin clamps to - // the 1 px floor, so the ratio is the clamped-pixel ratio 1.889). + // 0.13 mm / 0.50 mm hierarchy survives the plot style at its true + // ratio; the screen's 1 px floor does not reach the plot (#1749). let text = crate::io::pdf_export::pdf_stream_text(&std::fs::read(&pdf).expect("pdf written")); let tokens: Vec<&str> = text.split_whitespace().collect(); @@ -2787,7 +2943,7 @@ mod tests { "two layer weights in the plotted PDF: {widths:?}" ); assert!( - (widths[1] / widths[0] - 1.889).abs() < 0.15, + (widths[1] / widths[0] - 50.0 / 13.0).abs() < 0.15, "0.50 mm vs 0.13 mm hierarchy: {widths:?}" ); let _ = std::fs::remove_file(&pdf); diff --git a/src/app/control/sheets.rs b/src/app/control/sheets.rs index a9c32b788..aafd39cd8 100644 --- a/src/app/control/sheets.rs +++ b/src/app/control/sheets.rs @@ -20,7 +20,10 @@ impl OpenCADStudio { self.set_control_result(json!({ "closed": false, "reason": "start_page" })); return Ok(Task::none()); } - if self.tabs[index].dirty && !req["discard"].as_bool().unwrap_or(false) { + if self.tabs[index].dirty + && self.tabs[index].block_test.is_none() + && !req["discard"].as_bool().unwrap_or(false) + { return Err(failure( "document_dirty", "Save the document or pass discard:true", @@ -136,7 +139,6 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "LAYOUTCREATE"); self.tabs[i] .scene - .document .add_layout(&name) .map_err(|e| failure("layout_failed", e))?; let plot_style = self diff --git a/src/app/document.rs b/src/app/document.rs index 8c3a33df8..f1f33e4ec 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -205,6 +205,9 @@ pub(super) struct DocumentTab { /// Index of the field that TAB has focused (the one keystrokes edit). pub(super) dyn_active: usize, pub(super) history: HistoryState, + /// Drawing history is suspended while `history` belongs to BEDIT. + pub(super) block_edit_history: Option, + pub(super) block_test: Option, 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 @@ -227,6 +230,10 @@ pub(super) struct DocumentTab { pub(super) paper_bg_color: Option<[f32; 4]>, /// Active REFEDIT session, if any. pub(super) refedit_session: Option, + /// Sessions closed by REFCLOSE, reopened by undoing it. + pub(super) refedit_closed: Vec, + /// Sessions ended by undoing REFEDIT, reopened by redoing it. + pub(super) refedit_reopen: Vec, /// COUNT mode, while it is on. pub(crate) count: Option, /// Open BEDIT block tabs. Definitions are edited live; each tab owns its @@ -655,6 +662,8 @@ impl DocumentTab { dyn_ref_screen: None, dyn_active: 0, history: HistoryState::default(), + block_edit_history: None, + block_test: None, active_layer: "0".to_string(), props_asym_scale: std::collections::HashSet::new(), graph: Default::default(), @@ -664,6 +673,8 @@ impl DocumentTab { bg_color: None, paper_bg_color: None, refedit_session: None, + refedit_closed: Vec::new(), + refedit_reopen: Vec::new(), count: None, block_edits: Vec::new(), active_block_edit: None, diff --git a/src/app/drafting_settings.rs b/src/app/drafting_settings.rs index 058fff8ab..b95cd229f 100644 --- a/src/app/drafting_settings.rs +++ b/src/app/drafting_settings.rs @@ -32,6 +32,7 @@ impl DraftingSettingsState { ortho_on: app.ortho_mode, polar_increment_deg: app.polar_increment_deg, osnap_on: app.snapper.snap_enabled, + osnap_aperture_input: format!("{}", app.snapper.osnap_radius_px), otrack_on: app.snapper.otrack_enabled, snap_modes: app.snapper.enabled.clone(), osnap3d_on: app.snapper.snap3d_enabled, @@ -107,6 +108,13 @@ impl OpenCADStudio { self.polar_mode = state.polar_on; self.ortho_mode = state.ortho_on; self.polar_increment_deg = state.polar_increment_deg; + let aperture = state.osnap_aperture_input.trim().parse::().ok() + .filter(|v| v.is_finite() && (4.0..=40.0).contains(v)); + let Some(aperture) = aperture else { + self.command_line.push_error("OSNAP capture radius must be between 4 and 40 pixels."); + return false; + }; + self.snapper.osnap_radius_px = aperture; self.snapper.snap_enabled = state.osnap_on; self.snapper.otrack_enabled = state.otrack_on; self.snapper.enabled = state.snap_modes.clone(); @@ -145,6 +153,7 @@ mod tests { ortho_on: false, polar_increment_deg: 15.0, osnap_on: true, + osnap_aperture_input: "15".to_string(), otrack_on: false, snap_modes: rustc_hash::FxHashSet::default(), osnap3d_on: false, @@ -186,6 +195,7 @@ mod tests { ortho_on: false, polar_increment_deg: 15.0, osnap_on: true, + osnap_aperture_input: "15".to_string(), otrack_on: false, snap_modes: rustc_hash::FxHashSet::default(), osnap3d_on: false, diff --git a/src/app/history.rs b/src/app/history.rs index 01b919f14..649135591 100644 --- a/src/app/history.rs +++ b/src/app/history.rs @@ -152,7 +152,7 @@ impl OpenCADStudio { defer_history_drop(discarded); } - fn push_undo_entry(&mut self, i: usize, snapshot: HistorySnapshot) { + pub(super) fn push_undo_entry(&mut self, i: usize, snapshot: HistorySnapshot) { self.tabs[i].history.undo_stack.push(snapshot); self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1); self.clear_redo_history(i); @@ -283,6 +283,7 @@ impl OpenCADStudio { return; }; self.tabs[i].scene.document.end_entity_change_recording(); + self.reconcile_block_edit_membership(i); let structure_after = self.tabs[i].scene.document.snapshot_structure(); let before_images = pending.recorder.take_before_images(); let added_handles: Vec = before_images @@ -292,11 +293,25 @@ impl OpenCADStudio { .then_some(*handle) }) .collect(); + let block_members = self.tabs[i] + .active_block_edit_session() + .and_then(|session| { + pending + .structure_before + .block_records + .get(&session.block_name) + .map(|record| (session.block_name.clone(), record.entity_handles.clone())) + }); codec::CadDocument::align_added_entity_structure( &mut pending.structure_before, &structure_after, &added_handles, ); + if let Some((name, members)) = block_members { + if let Some(record) = pending.structure_before.block_records.get_mut(&name) { + record.entity_handles = members; + } + } let structure_changed = pending.structure_before != structure_after; let entities: Vec<_> = before_images .into_iter() @@ -402,8 +417,8 @@ impl OpenCADStudio { ) -> Option { self.finish_pending_history(i); let label = label.into(); - let structure_before = - (!delta_safe).then(|| self.tabs[i].scene.document.snapshot_structure()); + let structure_before = (!delta_safe || self.tabs[i].active_block_edit.is_some()) + .then(|| self.tabs[i].scene.document.snapshot_structure()); self.tabs[i].scene.begin_undo_recording(); Some(PendingDelta { label, @@ -740,6 +755,7 @@ impl OpenCADStudio { let Some(rec) = self.tabs[i].scene.take_undo_recording() else { return; }; + self.reconcile_block_edit_membership(i); // The predicate should have kept the command entity-only; if a primitive // still poisoned the recording we've already mutated and the pre-command // full state is gone, so the entity delta is the best we can do (its @@ -813,6 +829,16 @@ impl OpenCADStudio { let mut structure = pending.structure_before.map(StructureSnapshot::Full); if let Some(before_structure) = structure.as_mut() { if let StructureSnapshot::Full(before_structure) = before_structure { + let block_members = self.tabs[i] + .active_block_edit_session() + .and_then(|session| { + before_structure + .block_records + .get(&session.block_name) + .map(|record| { + (session.block_name.clone(), record.entity_handles.clone()) + }) + }); let after_structure = self.tabs[i].scene.document.snapshot_structure(); let added_handles: Vec = entities .iter() @@ -825,6 +851,11 @@ impl OpenCADStudio { &after_structure, &added_handles, ); + if let Some((name, members)) = block_members { + if let Some(record) = before_structure.block_records.get_mut(&name) { + record.entity_handles = members; + } + } if *before_structure == after_structure { structure = None; } @@ -1004,6 +1035,20 @@ impl OpenCADStudio { scene.bump_constraints_epoch(); } for entry in &d.parametric_constraints { + if let crate::scene::parametric_constraints::ParametricScope::Block(owner) = entry.scope + { + if !scene + .document + .block_records + .iter() + .any(|record| record.handle == owner) + { + scene + .parametric_constraints + .retain(|set| set.scope != entry.scope); + continue; + } + } let value = if undo { &entry.before } else { &entry.after }; *scene.parametric_constraint_set_mut(entry.scope) = value.clone(); } @@ -1081,6 +1126,7 @@ impl OpenCADStudio { i: usize, had_full: bool, layer_panel_changed: bool, + native_graph_changed: bool, changes: &[(Handle, crate::scene::ChangeKind)], ) { self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1); @@ -1128,7 +1174,11 @@ impl OpenCADStudio { } } } - scene.sync_native_parametric_graph(); + // Geometry or visibility replay does not change the constraint + // model. Rebuilding its graph would replace imported networks. + if native_graph_changed { + scene.sync_native_parametric_graph(); + } scene.clear_preview_wire(); } // UCS state is cached separately from its persisted header/viewport @@ -1171,14 +1221,18 @@ impl OpenCADStudio { let mut last_label = String::new(); let mut had_full = false; let mut layer_panel_changed = false; + let mut native_graph_changed = false; let mut changes = Vec::new(); for _ in 0..steps { let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else { break; }; last_label = snapshot.label().to_string(); + let step_label = last_label.clone(); match snapshot { HistorySnapshot::Delta(mut d) => { + native_graph_changed |= + !d.parametric_constraints.is_empty() || d.named_parameters.is_some(); // Symmetric: undo applies the before side, then the same // delta rides to the redo stack (it still holds the after // side) โ€” no current-state capture needed. @@ -1211,8 +1265,15 @@ impl OpenCADStudio { .push(HistorySnapshot::ObjectVisibility(v)); } } + self.refedit_after_history_step(i, &step_label, true); } - self.finish_history_apply(i, had_full, layer_panel_changed, &changes); + self.finish_history_apply( + i, + had_full, + layer_panel_changed, + native_graph_changed, + &changes, + ); self.command_line .push_output(crate::tf!("Undo: {last_label}").as_ref()); } @@ -1231,14 +1292,18 @@ impl OpenCADStudio { let mut last_label = String::new(); let mut had_full = false; let mut layer_panel_changed = false; + let mut native_graph_changed = false; let mut changes = Vec::new(); for _ in 0..steps { let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else { break; }; last_label = snapshot.label().to_string(); + let step_label = last_label.clone(); match snapshot { HistorySnapshot::Delta(mut d) => { + native_graph_changed |= + !d.parametric_constraints.is_empty() || d.named_parameters.is_some(); layer_panel_changed |= d.structure.as_ref().is_some_and(|structure| { matches!( structure, @@ -1268,8 +1333,15 @@ impl OpenCADStudio { .push(HistorySnapshot::ObjectVisibility(v)); } } + self.refedit_after_history_step(i, &step_label, false); } - self.finish_history_apply(i, had_full, layer_panel_changed, &changes); + self.finish_history_apply( + i, + had_full, + layer_panel_changed, + native_graph_changed, + &changes, + ); self.command_line .push_output(crate::tf!("Redo: {last_label}").as_ref()); } diff --git a/src/app/mod.rs b/src/app/mod.rs index e8435b03d..47188882f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -8,6 +8,13 @@ pub(crate) mod config; #[cfg(not(target_arch = "wasm32"))] pub use automation::{export_headless, serve}; mod annotation_data; +mod block_editor; +mod block_test; +#[cfg(test)] +mod block_test_tests; +mod block_save_as; +#[cfg(test)] +mod block_editor_tests; mod command_driver; pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel, PasteSpace}; pub(crate) mod commands; @@ -223,10 +230,51 @@ impl std::fmt::Display for QSelectPropertyChoice { } } -/// Open Quick Select panel state. The filter is one -/// `(scope, type, property, op, value)` row plus result behavior. +/// One committed condition in the Quick Select filter list. +#[derive(Clone, Debug)] +pub struct QSelectCondition { + pub type_filter: Option, + pub property: Option, + pub operator: QSelectOp, + pub value: String, +} + +impl QSelectCondition { + pub fn validation_error(&self) -> Option { + if self.type_filter.is_none() && self.property.is_none() { + return Some(crate::t!("Choose an object type or property.").into_owned()); + } + let Some(property) = &self.property else { + return None; + }; + if matches!(self.operator, QSelectOp::Any) { + return None; + } + if matches!(self.operator, QSelectOp::Gt | QSelectOp::Lt) + && !matches!(property.editor, QSelectValueEditor::Number) + { + return Some(crate::t!("This operator requires a numeric property.").into_owned()); + } + match &property.editor { + QSelectValueEditor::Number + if crate::entities::common::parse_f64(&self.value) + .is_none_or(|value| !value.is_finite()) => + { + Some(crate::t!("Enter a valid number.").into_owned()) + } + QSelectValueEditor::Choice(_) if self.value.is_empty() => { + Some(crate::t!("Choose a value.").into_owned()) + } + _ => None, + } + } +} + +/// Quick Select editor and committed conditions (combined with AND). #[derive(Clone, Debug)] pub struct QSelectState { + pub conditions: Vec, + pub editing_condition: Option, pub scope: QSelectScope, pub available_types: Vec, pub available_properties: Vec, @@ -242,8 +290,31 @@ pub struct QSelectState { pub error: Option, } +impl QSelectState { + pub fn normalize_operator(&mut self) { + if matches!(self.operator, QSelectOp::Gt | QSelectOp::Lt) + && !self + .property + .as_ref() + .is_some_and(|property| matches!(property.editor, QSelectValueEditor::Number)) + { + self.operator = QSelectOp::Eq; + } + } + + pub fn draft_condition(&self) -> QSelectCondition { + QSelectCondition { + type_filter: self.type_filter.clone(), + property: self.property.clone(), + operator: self.operator, + value: self.value.clone(), + } + } +} + #[derive(Clone, Debug)] struct QSelectSettings { + conditions: Vec, scope: QSelectScope, type_filter: Option, property_field: Option, @@ -256,6 +327,7 @@ struct QSelectSettings { impl From<&QSelectState> for QSelectSettings { fn from(state: &QSelectState) -> Self { Self { + conditions: state.conditions.clone(), scope: state.scope, type_filter: state.type_filter.clone(), property_field: state @@ -365,6 +437,15 @@ pub enum StartSection { Supporters, } +/// How the Start page lists recent files. +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, serde::Serialize, serde::Deserialize)] +pub enum RecentView { + #[default] + List, + Icons, + Preview, +} + /// What the Space / Enter keys currently mean at the command line. One /// decision point (`OpenCADStudio::text_entry_mode`) for every keyboard /// route that used to re-derive the answer from editor state. @@ -404,6 +485,7 @@ pub(super) struct OpenCADStudio { /// Live text of the recent-limit input box (may differ from `recent_limit` /// mid-edit; applied on Enter). Kept in sync when the +/- buttons change it. recent_limit_input: String, + recent_view: RecentView, command_line: CommandLine, /// Recent Patreon supporters shown on the Start page (name, USD cents), /// fetched once at boot, highest payment first. @@ -492,6 +574,8 @@ pub(super) struct OpenCADStudio { drawing_units: Option, /// Working copy of the Block Definition dialog; `None` while it is closed. block_definition: Option, + block_edit_picker: Option, + block_save_as: Option, /// PDF dialogs' working copies; `None` while closed. pdf_attach: Option, point_cloud_attach: Option, @@ -502,6 +586,12 @@ pub(super) struct OpenCADStudio { field_dialog: Option, point_cloud_color_map: Option, pc_section: Option, + /// -VIEW sEttings Background dialog. + view_background: Option, + /// IMAGEADJUST dialog. + image_adjust: Option, + /// The Edit Reference dialog (REFEDIT). + refedit_dialog: Option, underlay_layers: Option, pdf_import_settings: Option, pdf_import_file: Option, @@ -558,6 +648,10 @@ pub(super) struct OpenCADStudio { zoom_wheel_reversed: bool, /// Scroll pans and Ctrl+scroll zooms (touchpads); see `scroll_intent`. touchpad_pan: bool, + /// Multi-finger gestures on a touch screen. + touch: crate::input::touch::TouchGestures, + /// Where the drawing area sits in the window, learned from a lone finger. + touch_offset: Option, /// Interface scale in percent, applied through the window scale factor. ui_scale_percent: u16, /// Mouse-wheel zoom sensitivity (ZOOMFACTOR), held inside @@ -573,6 +667,12 @@ pub(super) struct OpenCADStudio { cursor_size: i32, /// Selection-box size setting (PICKBOX, 0..=50). pick_box: i32, + /// HPASSOC: new hatches follow their boundary objects. + hatch_associative: bool, + /// MENUBAR: the classic pull-down menus show above the ribbon. + show_menu_bar: bool, + /// RIBBONCLOSE hid the ribbon; RIBBON brings it back. + ribbon_closed: bool, /// Use REFEDIT rather than BEDIT when double-clicking an attribute-free block. double_click_block_refedit: bool, /// Open ATTEDIT when double-clicking a block with attributes. @@ -700,6 +800,8 @@ pub(super) struct OpenCADStudio { block_freq: std::collections::HashMap, /// BLOCKMRULIST: how many recent blocks the palette keeps (0โ€“100). pub(crate) block_mru_list: u8, + /// The Edit Reference dialog's remembered settings. + pub(crate) refedit_options: crate::app::settings::RefEditOptions, /// BLOCKREDEFINEMODE (0โ€“2). pub(crate) block_redefine_mode: u8, /// BLOCKNAVIGATE: the Libraries tab's start folder ("." for none). @@ -1150,6 +1252,13 @@ pub(super) struct OpenCADStudio { /// osnap hits included โ€” projects onto this ray from `last_point` (or the /// grip origin). Cleared when Shift releases. axis_lock_dir: Option, + /// A direction typed as `, + /// A left press on a selected object: the point it grabbed, and whether + /// the drag has turned into a MOVE (or, with Ctrl, a COPY) of the + /// selection. + drag_move: Option<(glam::DVec3, bool)>, /// Inline rename state: (original_name, current_edit_value). layout_rename_state: Option<(String, String)>, /// Timestamp of the previous viewport left-click release (for double-click detection). @@ -1269,6 +1378,8 @@ pub(super) struct OpenCADStudio { style_rename: Option, /// Edit buffer for the inline rename text input. style_rename_buf: String, + /// The style manager the open rename belongs to. + style_rename_kind: Option, /// Active style-manager transaction. Edits mutate the document live for an /// in-dialog preview but only persist on Apply; closing without Apply /// restores this snapshot. `None` when no style manager is staging. @@ -1437,6 +1548,7 @@ pub(super) struct OpenCADStudio { // โ”€โ”€ Unsaved-changes dialog โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /// Set when the user tries to close a tab or quit while there are unsaved changes. pending_close: Option, + pending_block_close: Option<(u64, block_editor::BlockEditContinuation)>, /// Stable document ids waiting to be closed by "Close All" or /// "Close All Other Drawings". Dirty drawings pause this queue at the /// existing unsaved-changes dialog and resume after Save or Discard. @@ -1709,6 +1821,8 @@ pub enum ColorPickTarget { MText, /// An Options interface colour (true colour only). Interface(UiColor), + /// A colour of the view Background dialog: 0 solid, 1 top, 2 middle, 3 bottom. + ViewBackground(u8), } /// Table records the clipboard entities depend on, snapshotted from the source @@ -2049,6 +2163,9 @@ pub enum ModalKind { PointCloudAttach, PointCloudColorMap, PcSection, + ViewBackground, + ImageAdjust, + RefEdit, UnderlayLayers, PdfImportSettings, PdfImportFile, @@ -2072,6 +2189,9 @@ pub enum ModalKind { MLeaderStyle, DimStyle, Unsaved, + BlockEditChanges, + BlockEditPicker, + BlockSaveAs, SaveDialog, Recovery, /// Fonts referenced by the drawing are missing on this machine. @@ -2246,6 +2366,8 @@ pub enum Message { SpaceMousePreferences, SpaceMouseDriverSettings, SpaceMouseDetails, + /// A touch screen finger event, for multi-finger gestures. + Touch(iced::touch::Event), /// One magnification delta from a trackpad pinch, positive zooming in. TrackpadPinch(f32), ControlRequest(control::Envelope), @@ -2256,7 +2378,9 @@ pub enum Message { /// A frame has been drawn (or the anti-hang timer expired) for a parked /// `capture` request: take the screenshot now, so it shows the latest /// state instead of the previous frame (C11). - ControlCaptureFrame { redrew: bool }, + ControlCaptureFrame { + redrew: bool, + }, ControlToggle, /// Node graph overlay interaction. Graph(crate::ui::node_graph::GraphMsg), @@ -2282,13 +2406,13 @@ pub enum Message { PasteShortcut, /// Which text input held focus when `PasteShortcut` arrived. PasteShortcutResolved(PasteFocus), + /// Completion of the system text clipboard read PASTESPEC starts. + PasteSpecial(Option), /// Completion of a system text clipboard read requested by PASTECLIP. SystemClipboardPaste(SystemClipboardText), - /// Web: completion of the clipboard-image read the paste fallback starts - /// when no text was found โ€” `Some((name, bytes))` attaches the image, - /// `None` reports the usual unsupported-content error. Native never - /// constructs it. - #[cfg(target_arch = "wasm32")] + /// Completion of the clipboard-image read the paste fallback starts when + /// no text was found โ€” `Some((name, bytes))` attaches the image, `None` + /// reports the usual unsupported-content error. SystemClipboardPasteImage(Option<(String, Vec)>), /// Ctrl/Cmd+A โ€” select all layer rows when the Layer Manager is open, or all /// drawing objects otherwise (#236). @@ -2354,6 +2478,7 @@ pub enum Message { RecentRemove(PathBuf), /// Set how many recent documents to keep (persisted). SetRecentLimit(usize), + SetRecentView(RecentView), /// Live edit of the recent-limit input box (not applied until Enter). RecentLimitInput(String), /// User clicked Cancel on the loading overlay. The parser thread keeps @@ -2482,6 +2607,8 @@ pub enum Message { SelectionHighlightColorChanged(u8), /// Change Grip size in pixels (1โ€“25, GRIPSIZE). GripSizeChanged(u8), + /// Change the object-snap marker size in pixels (1โ€“20, SNAPMARKERSIZE). + SnapMarkerSizeChanged(u8), /// Change Unselected Grip color ACI index (0 = Theme Primary, 1..=255 = ACI, GRIPCOLOR). GripColorChanged(u8), /// Change Selected/Hot Grip color ACI index (0 = Theme Danger, 1..=255 = ACI, GRIPHOT). @@ -2515,9 +2642,17 @@ pub enum Message { ClipromptLinesChanged(i32), /// Change how long command-line history lines stay visible (COMMANDLINEFADETIME). CommandLineFadeChanged(i32), + /// Change the command line's panel opacity (COMMANDLINEOPACITY). + CommandLineOpacityChanged(u8), /// Toggle reversing the mouse-wheel zoom direction (ZOOMWHEEL). ZoomWheelReversedChanged(bool), TouchpadPanChanged(bool), + /// Options: show the pull-down menus (MENUBAR). + MenuBarChanged(bool), + /// Options: show the ribbon (RIBBON / RIBBONCLOSE). + RibbonVisibleChanged(bool), + /// Options: new hatches are associative (HPASSOC). + HatchAssociativeChanged(bool), /// Interface scale in percent (Options > Display). UiScaleChanged(u16), /// Change how far one wheel notch zooms, from the Options slider @@ -2630,6 +2765,8 @@ pub enum Message { SetProjection(bool), /// Select a ribbon module tab by index. RibbonSelectTab(usize), + /// Ribbon key tips: Alt, typed codes and Esc. + KeyTip(crate::ui::ribbon::KeyTipInput), /// Change the ribbon tool-panel density (persisted). SetRibbonCollapseMode(crate::ui::ribbon::CollapseMode), /// A ribbon tool button was clicked โ€” highlights the tool and dispatches its event. @@ -2680,6 +2817,14 @@ pub enum Message { UnsavedDialogDiscard, /// User clicked "Cancel" in the unsaved-changes dialog. UnsavedDialogCancel, + BlockEditChangesSave, + BlockEditChangesDiscard, + BlockEditChangesCancel, + BlockSaveAsName(String), + BlockSaveAsApply, + BlockEditPickerName(String), + BlockEditPickerSelect(String), + BlockEditPickerApply, // โ”€โ”€ AEC / unsupported-object drop warning (lossy Save-As) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /// Save as the source DWG version so the unsupported objects survive. AecDropSameVersion, @@ -2915,6 +3060,8 @@ pub enum Message { /// User clicked a command-line option button โ€” feed its keyword to the /// active command as if typed (empty keyword = Enter). (#304) CommandOptionPick(String), + /// The pointer entered (true) or left the command line's overlay history. + CommandHistoryHover(bool), ToggleLayers, /// Open/focus (or close) the Reference Manager palette. ToggleXrefManager, @@ -2999,6 +3146,10 @@ pub enum Message { LayerLineweightSet(LineWeight), LayerTransparencyEdit(String, String), LayerRenameCommit, + /// The widget focused after a click while a layer rename is open. + LayerRenameFocus(Option), + /// The widget focused after a click while a style rename is open. + StyleRenameFocus(Option), // โ”€โ”€ Layer State Manager โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ LayerStateManagerOpen, LayerStateManagerSelect(String), @@ -3031,6 +3182,8 @@ pub enum Message { /// never fall through and rotate a different camera. ViewportClick(Option), ViewportMove(Point), + /// Position recorded by the pointer event itself, not the last hover. + ViewportLeftPressAt(Point), ViewportLeftPress, ViewportLeftRelease, ViewportRightPress, @@ -3332,6 +3485,7 @@ pub enum Message { DraftingSettingsTogglePolar, DraftingSettingsToggleOrtho, DraftingSettingsToggleOsnap, + DraftingSettingsOsnapApertureChanged(String), DraftingSettingsToggleOtrack, DraftingSettingsToggleSnapMode(crate::snap::SnapType), DraftingSettingsToggleSnapMode3d(crate::snap::SnapType), @@ -3477,6 +3631,8 @@ pub enum Message { PropPointerPressed, /// Toggle the inline color picker dropdown open/closed. PropColorPickerToggle, + /// A Properties dropdown toggle and whether Quick Properties sent it. + PropPickerSurface(bool, Box), /// Toggle the MTEXT background-colour picker dropdown open/closed. PropBgColorPickerToggle, /// User picked an MTEXT background fill colour โ€” sets `background_color` @@ -3884,6 +4040,13 @@ pub enum Message { /// Append-to-current-selection toggle. QSelectSetAppend(bool), /// Apply the current filter and close the panel. + QSelectAddCondition, + /// Hide the filter and sample an object's filterable properties. + QSelectPickObject, + QSelectEditCondition(usize), + QSelectRemoveCondition(usize), + QSelectClearConditions, + QSelectCancelEdit, QSelectApply, /// The user clicked the title-bar โœ• (fires before the window closes). WindowCloseRequested(window::Id), @@ -4216,6 +4379,8 @@ pub enum Message { PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg), AttdefDialog(crate::ui::window::attdef_dialog::AttdefDialogMsg), FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg), + /// An edit in the Edit Reference dialog. + RefEditDialog(crate::ui::window::refedit_dialog::RefEditDialogMsg), /// A click or search in the Point Cloud Manager. PcManager(crate::ui::window::pc_manager::PcManagerMsg), Count(crate::ui::window::count_palette::CountMsg), @@ -4302,6 +4467,12 @@ pub enum Message { /// Callback after the user picks (or cancels) the STEP save path. StepExportPath(Option), StepExportFinished(std::path::PathBuf, Result<(), String>), + // โ”€โ”€ FBX export โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + /// Trigger FBX export: show save dialog. + FbxExport, + /// Callback after the user picks (or cancels) the FBX save path. + FbxExportPath(Option), + FbxExportFinished(std::path::PathBuf, Result<(), String>), // โ”€โ”€ OBJ import โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /// Trigger OBJ import: show open-file dialog. ObjImport, @@ -4365,6 +4536,7 @@ impl OpenCADStudio { recent_thumbs: std::collections::HashMap::new(), recent_limit: recent::RECENT_DEFAULT, recent_limit_input: recent::RECENT_DEFAULT.to_string(), + recent_view: RecentView::default(), command_line: CommandLine::new(), patrons: Vec::new(), videos: Vec::new(), @@ -4412,6 +4584,8 @@ impl OpenCADStudio { layer_translator: None, drawing_units: None, block_definition: None, + block_edit_picker: None, + block_save_as: None, pdf_attach: None, point_cloud_attach: None, attdef_dialog: None, @@ -4419,6 +4593,9 @@ impl OpenCADStudio { field_dialog: None, point_cloud_color_map: None, pc_section: None, + view_background: None, + image_adjust: None, + refedit_dialog: None, underlay_layers: None, pdf_import_settings: None, pdf_import_file: None, @@ -4437,11 +4614,16 @@ impl OpenCADStudio { polar_increment_deg: 45.0, zoom_wheel_reversed: false, touchpad_pan: false, + touch: Default::default(), + touch_offset: None, ui_scale_percent: 100, zoom_factor: 60, zoom_factor_input: 60.to_string(), cursor_size: 5, pick_box: 3, + hatch_associative: true, + show_menu_bar: false, + ribbon_closed: false, double_click_block_refedit: false, double_click_block_attedit: true, right_click_mode: settings::RightClickMode::ShortcutMenu, @@ -4513,6 +4695,7 @@ impl OpenCADStudio { block_mru: Vec::new(), block_freq: std::collections::HashMap::new(), block_mru_list: 50, + refedit_options: Default::default(), block_redefine_mode: 1, block_navigate: ".".to_string(), insname: String::new(), @@ -4688,6 +4871,8 @@ impl OpenCADStudio { text_inline: None, snap_override_popup: None, axis_lock_dir: None, + angle_override: None, + drag_move: None, layout_rename_state: None, last_vp_click_time: None, last_vp_click_pos: None, @@ -4723,6 +4908,7 @@ impl OpenCADStudio { queued_interaction_indices: std::collections::VecDeque::new(), pending_close: None, pending_tab_closes: std::collections::VecDeque::new(), + pending_block_close: None, #[cfg(not(target_arch = "wasm32"))] active_save_jobs: std::collections::HashMap::new(), #[cfg(not(target_arch = "wasm32"))] @@ -4799,6 +4985,7 @@ impl OpenCADStudio { // TextStyle font browser style_rename: None, style_rename_buf: String::new(), + style_rename_kind: None, style_stage: None, textstyle_selected: "Standard".to_string(), textstyle_tab: 0, @@ -5174,9 +5361,10 @@ impl OpenCADStudio { // control request by setting `opencadstudio.automation` to "1" in // localStorage (the same pre-load mechanism the settings use): the // startup donation prompt then never queues (#1349). - if web_sys::window().and_then(|window| window.local_storage().ok().flatten()).and_then( - |storage| storage.get_item("opencadstudio.automation").ok().flatten(), - ) == Some("1".to_string()) + if web_sys::window() + .and_then(|window| window.local_storage().ok().flatten()) + .and_then(|storage| storage.get_item("opencadstudio.automation").ok().flatten()) + == Some("1".to_string()) { s.control.automation_seen = true; } diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 757d0e5c2..486d45c40 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -225,6 +225,8 @@ fn subtract_bodies( Ok(result) } +inventory::submit!(crate::command::CommandRegistration { names: &["SECTIONCULL"] }); + fn entity_with_boolean_body(mut source: EntityType, body: &Body) -> Option { let document = crate::scene::convert::acis_export::solid_to_sat(body)?; let wires = solid_model::edge_wires(body); @@ -1741,6 +1743,80 @@ impl super::OpenCADStudio { self.solid_slice_with_tool(requested, ModelSliceTool::Surface(cutter), keep_point) } + /// SLICE on meshes that do not close a volume: the kept side of each face + /// stays (or, with no side chosen, both sides become separate meshes), + /// open along the cut. One undo step; `None` when nothing was changed. + fn slice_open_meshes( + &mut self, + handles: &[Handle], + tool: &ModelSliceTool, + keep_point: Option, + ) -> Option> { + let i = self.active_tab; + let keep_positive = match keep_point { + Some(point) => match tool.side(point.to_array()) { + Some(distance) if distance.abs() > 1.0e-9 => Some(distance > 0.0), + _ => { + self.command_line.push_error( + crate::t!("SLICE: choose a point away from the cutting plane.").as_ref(), + ); + return None; + } + }, + None => None, + }; + let side = |point: [f64; 3]| tool.side(point).unwrap_or(0.0); + let mut pieces = Vec::with_capacity(handles.len()); + for handle in handles { + let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) else { + continue; + }; + let Some((vertices, faces)) = crate::entities::mesh::mesh_polygons(entity) else { + continue; + }; + let common = entity.common(); + let clip = |positive| { + crate::entities::mesh::clipped_mesh(&vertices, &faces, side, positive, common) + }; + let (kept, other) = match keep_positive { + Some(positive) => (clip(positive), None), + None => (clip(false), Some(clip(true))), + }; + // Both sides must have faces, or the plane missed this mesh. + let crosses = vertices.iter().any(|&point| side(point) > 0.0) + && vertices.iter().any(|&point| side(point) < 0.0); + let (Some(kept), true) = (kept, crosses) else { + self.command_line.push_error( + crate::t!("SLICE: the cutting plane does not cross every selected object; no object was changed.") + .as_ref(), + ); + return None; + }; + pieces.push((*handle, kept, other.flatten())); + } + self.push_undo_snapshot(i, "SLICE"); + let mut results = Vec::with_capacity(pieces.len() * 2); + for (handle, mut kept, other) in pieces { + kept.common.handle = handle; + self.tabs[i].scene.update_entity(EntityType::Mesh(kept)); + results.push(handle); + if let Some(mut other) = other { + other.common.handle = Handle::NULL; + results.push(self.tabs[i].scene.add_entity(EntityType::Mesh(other))); + } + } + self.tabs[i].scene.deselect_all(); + for handle in &results { + self.tabs[i].scene.select_entity(*handle, false); + } + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line.push_output( + crate::t!("SLICE: created %{count} result object(s).", count = results.len()).as_ref(), + ); + Some(results) + } + fn solid_slice_with_tool( &mut self, requested: &[Handle], @@ -1755,14 +1831,50 @@ impl super::OpenCADStudio { .filter(|handle| { matches!( self.tabs[i].scene.document.get_entity(*handle), - Some(EntityType::Solid3D(_) | EntityType::Surface(_)) + Some( + EntityType::Solid3D(_) + | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) + ) ) }) .collect::>(); handles.sort_unstable_by_key(|handle| handle.value()); handles.dedup(); + // A mesh that closes a volume is sliced as a solid and capped; an open + // one is clipped face by face and stays a mesh. + let mut bodies = rustc_hash::FxHashMap::default(); + let mut open_meshes = Vec::new(); + for handle in &handles { + let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) else { + continue; + }; + if crate::entities::mesh::mesh_polygons(entity).is_none() { + continue; + } + match crate::entities::mesh::closed_mesh_body(entity) { + Some(body) => { + bodies.insert(*handle, body); + } + None => open_meshes.push(*handle), + } + } + handles.retain(|handle| !open_meshes.contains(handle)); self.tabs[i].scene.restore_solid_models(&handles); - handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle)); + for handle in &handles { + if let Some(body) = self.tabs[i].scene.solid_models.get(handle) { + bodies.entry(*handle).or_insert_with(|| body.clone()); + } + } + handles.retain(|handle| bodies.contains_key(handle)); + if !open_meshes.is_empty() { + let sliced = self.slice_open_meshes(&open_meshes, &tool, keep_point); + if handles.is_empty() || sliced.is_none() { + return Task::none(); + } + } if handles.is_empty() { self.command_line .push_error(crate::t!("SLICE: select at least one solid or surface.").as_ref()); @@ -1771,10 +1883,7 @@ impl super::OpenCADStudio { let keep_positive = if let Some(point) = keep_point { let tolerance = kernel::brep::operation_tolerance( - &handles - .iter() - .map(|handle| &self.tabs[i].scene.solid_models[handle]) - .collect::>(), + &handles.iter().map(|handle| &bodies[handle]).collect::>(), ); let Some(distance) = tool.side(point.to_array()) else { self.command_line @@ -1794,8 +1903,7 @@ impl super::OpenCADStudio { let mut prepared = Vec::with_capacity(handles.len()); for handle in &handles { - let body = &self.tabs[i].scene.solid_models[handle]; - let result = match tool.split(body) { + let result = match tool.split(&bodies[handle]) { Ok(Some(result)) => result, Ok(None) => { self.command_line.push_error( @@ -1967,6 +2075,74 @@ impl super::OpenCADStudio { self.solid_slice(&handles, plane, keep_point) } + /// SECTIONCULL โ€” keep only what lies inside the selected section object + /// (or the live section): bodies are cut to it, open meshes clipped, and + /// everything wholly outside it erased. One undo step. + pub(super) fn section_cull(&mut self) -> Task { + let i = self.active_tab; + let Some(section) = self.tabs[i].scene.cull_section() else { + self.command_line.push_error( + crate::t!("SECTIONCULL: select a section object or turn on a live section.") + .as_ref(), + ); + return Task::none(); + }; + let Some(cull) = self.tabs[i].scene.section_cull(section) else { + return Task::none(); + }; + let mut cut = Vec::with_capacity(cull.cut.len()); + for (handle, body) in cull.cut { + let scene = &self.tabs[i].scene; + let entity = scene + .document + .get_entity(handle) + .cloned() + .and_then(|source| entity_with_boolean_body(source, &body)); + let display = scene + .prepare_solid_model_display(handle, &body) + .filter(|display| display.0.complete); + let (Some(entity), Some(display)) = (entity, display) else { + self.command_line.push_error( + crate::t!( + "SECTIONCULL: a cut object could not be prepared; nothing was changed." + ) + .as_ref(), + ); + return Task::none(); + }; + cut.push((handle, entity, body, display)); + } + let changed = cut.len() + cull.clipped.len() + cull.outside.len(); + if changed == 0 { + self.command_line + .push_output(crate::t!("SECTIONCULL: nothing lies outside the section.").as_ref()); + return Task::none(); + } + self.push_undo_snapshot(i, "SECTIONCULL"); + let scene = &mut self.tabs[i].scene; + for (handle, entity, body, display) in cut { + scene.delete_solid_history(handle); + scene.update_entity(entity); + scene.register_prepared_solid_model(handle, body, display); + } + for mesh in cull.clipped { + scene.update_entity(mesh); + } + scene.erase_entities(&cull.outside); + scene.deselect_all(); + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line.push_output( + crate::t!( + "SECTIONCULL: erased %{erased} object(s) and cut %{cut} across the section.", + erased = cull.outside.len(), + cut = changed - cull.outside.len() + ) + .as_ref(), + ); + Task::none() + } + /// INTERFERE โ€” create a solid from the overlap of the two selected solids, /// leaving the originals in place (a non-destructive boolean intersect). pub(super) fn solid_interfere(&mut self) -> Task { diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index 25e1d7aa9..1bfc64064 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -149,6 +149,18 @@ impl<'a> HostSession<'a> { } } + /// Plugin-driven edits refresh the app-side selection caches exactly + /// like built-in edit paths: a selected entity's grip markers are cached + /// and would otherwise rest at their pre-edit positions (found by the + /// civil plugin's manual QA, 2026-10-12). + fn refresh_grips_of_active_tab(&mut self) { + // The grip cache mirrors the active tab's scene; a session may target + // a background tab, whose edits must not disturb it. + if self.tab == self.app.active_tab { + self.app.refresh_selected_grips(); + } + } + /// Recompute host-derived state for entities a script created or edited. fn normalize_scripted_entity( &self, @@ -408,6 +420,7 @@ impl<'a> HostSession<'a> { self.app.sync_registered_layers(self.tab); if ok { self.publish_document_view(); + self.refresh_grips_of_active_tab(); } ok } @@ -467,6 +480,7 @@ impl<'a> HostSession<'a> { self.app.sync_registered_layers(self.tab); self.set_dirty(); self.publish_document_view(); + self.refresh_grips_of_active_tab(); Ok(()) } @@ -526,6 +540,7 @@ impl<'a> HostSession<'a> { .update_entity(EntityType::Insert(insert)); if removed { self.publish_document_view(); + self.refresh_grips_of_active_tab(); } return removed; } @@ -550,6 +565,7 @@ impl<'a> HostSession<'a> { } } self.publish_document_view(); + self.refresh_grips_of_active_tab(); } removed } @@ -13423,6 +13439,194 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0))) assert!(!host.remove_entity(h)); } + #[test] + fn update_entity_refreshes_selected_grips() { + let mut app = OpenCADStudio::new_for_test(); + app.tabs[0].is_start = false; + let handle = { + let mut host = HostSession::new(&mut app, 0); + let mut line = codec::entities::Line::new(); + line.start = codec::types::Vector3::new(1.0, 1.0, 0.0); + line.end = codec::types::Vector3::new(3.0, 1.0, 0.0); + let handle = host.add_entity(EntityType::Line(line)); + host.set_selection(&[handle]).unwrap(); + handle + }; + // A real selection event rebuilt the grip cache the way the app's + // own paths do. + app.refresh_selected_grips(); + assert_eq!(app.tabs[0].selected_grips.len(), 3); + assert_eq!( + app.tabs[0] + .selected_grips + .iter() + .map(|grip| grip.world) + .collect::>(), + vec![ + glam::DVec3::new(1.0, 1.0, 0.0), + glam::DVec3::new(3.0, 1.0, 0.0), + glam::DVec3::new(2.0, 1.0, 0.0), + ] + ); + { + let mut host = HostSession::new(&mut app, 0); + let mut edited = host.document().get_entity(handle).cloned().unwrap(); + if let EntityType::Line(line) = &mut edited { + line.start = codec::types::Vector3::new(101.0, 1.0, 0.0); + line.end = codec::types::Vector3::new(103.0, 1.0, 0.0); + } + assert!(host.update_entity(edited)); + } + // The cached grip markers must sit on the refreshed geometry, not at + // their pre-edit positions. + assert_eq!( + app.tabs[0] + .selected_grips + .iter() + .map(|grip| grip.world) + .collect::>(), + vec![ + glam::DVec3::new(101.0, 1.0, 0.0), + glam::DVec3::new(103.0, 1.0, 0.0), + glam::DVec3::new(102.0, 1.0, 0.0), + ] + ); + } + + #[test] + fn update_entities_transaction_refreshes_selected_grips() { + let mut app = OpenCADStudio::new_for_test(); + app.tabs[0].is_start = false; + let handle = { + let mut host = HostSession::new(&mut app, 0); + let mut line = codec::entities::Line::new(); + line.start = codec::types::Vector3::new(0.0, 0.0, 0.0); + line.end = codec::types::Vector3::new(2.0, 0.0, 0.0); + let handle = host.add_entity(EntityType::Line(line)); + host.set_selection(&[handle]).unwrap(); + handle + }; + app.refresh_selected_grips(); + assert_eq!(app.tabs[0].selected_grips.len(), 3); + { + let mut host = HostSession::new(&mut app, 0); + let mut edited = host.document().get_entity(handle).cloned().unwrap(); + if let EntityType::Line(line) = &mut edited { + line.start = codec::types::Vector3::new(50.0, 0.0, 0.0); + line.end = codec::types::Vector3::new(52.0, 0.0, 0.0); + } + host.update_entities_transaction("Move line", vec![edited]) + .unwrap(); + } + app.finish_pending_history(0); + assert_eq!( + app.tabs[0] + .selected_grips + .iter() + .map(|grip| grip.world) + .collect::>(), + vec![ + glam::DVec3::new(50.0, 0.0, 0.0), + glam::DVec3::new(52.0, 0.0, 0.0), + glam::DVec3::new(51.0, 0.0, 0.0), + ] + ); + } + + #[test] + fn remove_entity_clears_selected_grips_of_removed_entity() { + let mut app = OpenCADStudio::new_for_test(); + app.tabs[0].is_start = false; + let handle = { + let mut host = HostSession::new(&mut app, 0); + let mut line = codec::entities::Line::new(); + line.start = codec::types::Vector3::new(1.0, 1.0, 0.0); + line.end = codec::types::Vector3::new(3.0, 1.0, 0.0); + let handle = host.add_entity(EntityType::Line(line)); + host.set_selection(&[handle]).unwrap(); + handle + }; + app.refresh_selected_grips(); + assert_eq!(app.tabs[0].selected_grips.len(), 3); + { + let mut host = HostSession::new(&mut app, 0); + assert!(host.remove_entity(handle)); + } + // Erase deselects the handle; the grip cache must drop the markers + // of the removed entity instead of keeping them until the next + // selection event. + assert!(app.tabs[0].selected_grips.is_empty()); + } + + #[test] + fn update_entity_without_selection_keeps_grip_cache_empty() { + let mut app = OpenCADStudio::new_for_test(); + app.tabs[0].is_start = false; + let handle = { + let mut host = HostSession::new(&mut app, 0); + let mut line = codec::entities::Line::new(); + line.start = codec::types::Vector3::new(1.0, 1.0, 0.0); + line.end = codec::types::Vector3::new(3.0, 1.0, 0.0); + host.add_entity(EntityType::Line(line)) + }; + app.refresh_selected_grips(); + assert!(app.tabs[0].selected_grips.is_empty()); + { + let mut host = HostSession::new(&mut app, 0); + let mut edited = host.document().get_entity(handle).cloned().unwrap(); + if let EntityType::Line(line) = &mut edited { + line.start = codec::types::Vector3::new(10.0, 1.0, 0.0); + } + // Still succeeds; nothing was selected, so no panic, no phantom + // grips. + assert!(host.update_entity(edited)); + } + assert!(app.tabs[0].selected_grips.is_empty()); + } + + #[test] + fn background_tab_update_leaves_active_grips_untouched() { + let mut app = OpenCADStudio::new_for_test(); + app.tabs[0].is_start = false; + let _handle = { + let mut host = HostSession::new(&mut app, 0); + let mut line = codec::entities::Line::new(); + line.start = codec::types::Vector3::new(1.0, 1.0, 0.0); + line.end = codec::types::Vector3::new(3.0, 1.0, 0.0); + let handle = host.add_entity(EntityType::Line(line)); + host.set_selection(&[handle]).unwrap(); + handle + }; + app.refresh_selected_grips(); + assert_eq!(app.tabs[0].selected_grips.len(), 3); + // Mark one cached grip so an accidental refresh (which would rebuild + // it from the active tab's unchanged scene) is observable. + app.tabs[0].selected_grips[0].world = glam::DVec3::new(999.0, 999.0, 0.0); + // A second document tab is background while tab 0 stays active. + app.push_test_document(); + assert_eq!(app.active_tab, 0); + { + let mut host = HostSession::new(&mut app, 1); + let mut line = codec::entities::Line::new(); + line.start = codec::types::Vector3::new(100.0, 100.0, 0.0); + line.end = codec::types::Vector3::new(102.0, 100.0, 0.0); + let background = host.add_entity(EntityType::Line(line)); + host.set_selection(&[background]).unwrap(); + let mut edited = host.document().get_entity(background).cloned().unwrap(); + if let EntityType::Line(line) = &mut edited { + line.start = codec::types::Vector3::new(200.0, 100.0, 0.0); + } + assert!(host.update_entity(edited)); + } + // The background-tab session neither refreshed nor disturbed the + // active tab's cached grips. + assert_eq!( + app.tabs[0].selected_grips[0].world, + glam::DVec3::new(999.0, 999.0, 0.0) + ); + assert_eq!(app.tabs[0].selected_grips.len(), 3); + } + /// A plugin command: second point commits a Point and ends. struct PlacePoint { got_first: bool, diff --git a/src/app/properties.rs b/src/app/properties.rs index 4d82fcc7b..1566146cf 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -747,7 +747,7 @@ impl OpenCADStudio { label: t!("Associative").into_owned(), field: "associative", value: crate::scene::model::object::PropValue::ReadOnly( - if associative { "Yes" } else { "No" }.to_string(), + if associative { t!("Yes") } else { t!("No") }.into_owned(), ), }); } @@ -793,7 +793,7 @@ impl OpenCADStudio { label: t!("Associative").into_owned(), field: "associative", value: crate::scene::model::object::PropValue::ReadOnly( - if associative { "Yes" } else { "No" }.to_string(), + if associative { t!("Yes") } else { t!("No") }.into_owned(), ), }); } @@ -924,6 +924,10 @@ impl OpenCADStudio { crate::io::xref::layer_overrides(doc, &br.name, &br.xref_path, &base_dir); xref_rows(&mut sections, ins, &br.xref_path, factor, overrides); } + // An associative array's counts and spacings. + if let Some(array) = crate::scene::assoc_array::array_of_insert(doc, handle) { + sections.push(crate::scene::assoc_array::property_section(doc, &array)); + } } // Underlay: name, page, path and size from the definition. codec::EntityType::Underlay(ul) => { @@ -1250,7 +1254,7 @@ impl OpenCADStudio { update_row_text( &mut sections, "tbl_flow_direction", - if flow_up { "Up" } else { "Down" }.to_string(), + if flow_up { t!("Up") } else { t!("Down") }.into_owned(), ); update_row_text( &mut sections, @@ -1540,7 +1544,7 @@ impl OpenCADStudio { set_row( &mut sections, "annotative", - if is_anno { "Yes" } else { "No" }.to_string(), + if is_anno { t!("Yes") } else { t!("No") }.into_owned(), ) } codec::EntityType::Leader(_) @@ -1548,7 +1552,7 @@ impl OpenCADStudio { doc, entity, ) => { - set_row(&mut sections, "annotative", "Yes".to_string()) + set_row(&mut sections, "annotative", t!("Yes").into_owned()) } codec::EntityType::Text(_) | codec::EntityType::Insert(_) @@ -1560,7 +1564,7 @@ impl OpenCADStudio { _ => set_row( &mut sections, "annotative", - if is_anno { "Yes" } else { "No" }.to_string(), + if is_anno { t!("Yes") } else { t!("No") }.into_owned(), ), } } @@ -3404,7 +3408,15 @@ pub(super) fn build_selection_groups( for (kind, handles) in by_type { groups.push(ui::properties::SelectionGroup { - label: format!("{}({})", t!(title_case_word(&kind)), handles.len()), + label: format!( + "{}({})", + if kind == "insert" { + t!("Block Reference").into_owned() + } else { + t!(title_case_word(&kind)).into_owned() + }, + handles.len() + ), handles, }); } diff --git a/src/app/settings.rs b/src/app/settings.rs index a70db4532..5a2d5b350 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -398,6 +398,12 @@ pub struct UserSettings { pub cursor_size: i32, /// PICKBOX: normalized visible-box and click-aperture size. pub pick_box: i32, + /// HPASSOC: new hatches follow their boundary objects. + pub hatch_associative: bool, + /// MENUBAR: the classic pull-down menus show above the ribbon. + pub show_menu_bar: bool, + /// RIBBONCLOSE hid the ribbon; RIBBON brings it back. + pub ribbon_closed: bool, /// When true, double-clicking a block reference starts REFEDIT instead of BEDIT. pub double_click_block_refedit: bool, /// When true, double-clicking a block with attributes opens ATTEDIT. @@ -417,12 +423,21 @@ pub struct UserSettings { pub attdef_on_screen: bool, /// FIELDDISPLAY: fields show on a gray background. pub field_display: bool, + /// XDWGFADECTL: how much referenced drawings fade (-90..=90; negative is off). + pub xref_fade: i32, + /// FILEDIA: file dialogs of commands that have a command-line form. + pub file_dia: bool, + /// The file type ATTACH's picker was last left on (1-based). + pub attach_filter: u32, /// SURFACEASSOCIATIVITY: new surfaces stay associative to their sources. pub surface_associativity: bool, /// UCSORTHO: an orthographic view preset also sets its orthographic UCS. pub ucs_ortho: bool, /// UCSVIEW: a saved named view keeps the current UCS. pub ucs_view: bool, + /// LAYOUTCREATEVIEWPORT: a layout opened for the first time gets a + /// viewport. + pub layout_create_viewport: bool, /// Nested-copy symbol handling: false inserts, true binds. pub ncopy_bind: bool, /// Last Options page; unknown saved names fall back without rejecting the config. @@ -582,6 +597,8 @@ pub struct UserSettings { deserialize_with = "deserialize_commandline_fade_ms" )] pub commandline_fade_ms: i32, + /// COMMANDLINEOPACITY: command-line panel opacity in percent (20โ€“100). + pub command_line_opacity: u8, /// SNAPUNIT X/Y spacing used by grid snap. 10 matches the Drafting /// Settings dialog defaults; older configs without these keys fall /// back via `default_snap_spacing`. @@ -639,6 +656,12 @@ pub struct UserSettings { /// BLOCKNAVIGATE (default "."). #[serde(default = "default_block_navigate")] pub block_navigate: String, + /// The Edit Reference dialog's settings. + #[serde(default)] + pub refedit: RefEditOptions, + /// XFADECTL: how far objects outside the REFEDIT working set fade (0โ€“90, default 70). + #[serde(default = "default_xfadectl")] + pub xfadectl: u8, /// COUNTCOLOR (default 3). #[serde(default = "default_count_color")] pub count_color: i16, @@ -671,6 +694,30 @@ pub struct PaletteBlockRef { pub drawing: bool, } +/// The Edit Reference dialog's settings, remembered between sessions: give +/// an external reference's clashing named objects unique names, check out +/// attribute definitions, lock what is outside the working set, pick the +/// working set's objects in the drawing. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct RefEditOptions { + pub unique_names: bool, + pub attributes: bool, + pub lock: bool, + pub nested: bool, +} + +impl Default for RefEditOptions { + fn default() -> Self { + Self { + unique_names: true, + attributes: false, + lock: true, + nested: false, + } + } +} + /// The Blocks palette's insertion options. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct BlockInsertOptions { @@ -709,6 +756,10 @@ fn default_block_view() -> u8 { 1 } +fn default_xfadectl() -> u8 { + 70 +} + fn default_block_mru_list() -> u8 { 50 } @@ -839,6 +890,9 @@ impl Default for UserSettings { zoom_factor: 60, cursor_size: 5, pick_box: 3, + hatch_associative: true, + show_menu_bar: false, + ribbon_closed: false, options_tab: crate::ui::window::options::OptionsTab::General, show_viewcube: true, show_ucs_icon: true, @@ -852,9 +906,13 @@ impl Default for UserSettings { image_frame: 1, attdef_on_screen: true, field_display: true, + xref_fade: 50, + file_dia: true, + attach_filter: 7, surface_associativity: true, ucs_ortho: true, ucs_view: true, + layout_create_viewport: true, ncopy_bind: false, cursor_type: CursorType::Crosshair, crosshair_color: None, @@ -902,6 +960,7 @@ impl Default for UserSettings { language: crate::i18n::Language::default(), cliprompt_lines: 3, commandline_fade_ms: 3000, + command_line_opacity: 80, snap_spacing_x: 10.0, snap_spacing_y: 10.0, grid_spacing_x: 10.0, @@ -920,6 +979,8 @@ impl Default for UserSettings { block_mru_list: 50, block_redefine_mode: 1, block_navigate: ".".to_string(), + refedit: RefEditOptions::default(), + xfadectl: 70, count_color: 3, count_error_color: 1, count_service: true, diff --git a/src/app/style_ops.rs b/src/app/style_ops.rs index c20ef77fb..1cea49a71 100644 --- a/src/app/style_ops.rs +++ b/src/app/style_ops.rs @@ -710,7 +710,7 @@ impl OpenCADStudio { // closing the window without Apply calls `style_stage_discard`, which // restores the snapshot taken when the manager opened. - pub(super) fn style_new(&mut self, kind: StyleKind) { + pub(super) fn style_new(&mut self, kind: StyleKind) -> iced::Task { let i = self.active_tab; let name = self.unique_new_name(kind); let h = self.tabs[i].scene.document.allocate_handle(); @@ -720,21 +720,37 @@ impl OpenCADStudio { self.after_style_change(kind); self.command_line .push_output(crate::tf!("Style '{name}' created.").as_ref()); + self.rename_new_style(kind, name) } - pub(super) fn style_copy(&mut self, kind: StyleKind) { + pub(super) fn style_copy(&mut self, kind: StyleKind) -> iced::Task { let i = self.active_tab; let src = self.style_selected(kind); let name = self.unique_suffixed_name(kind, &src); let h = self.tabs[i].scene.document.allocate_handle(); if !self.clone_style_as(kind, &src, &name, h) { - return; + return iced::Task::none(); } self.set_style_selected(kind, name.clone()); self.load_style_bufs(kind); self.after_style_change(kind); self.command_line .push_output(crate::tf!("Style '{name}' created.").as_ref()); + self.rename_new_style(kind, name) + } + + /// A new style opens with its generated name ready to be typed over. + fn rename_new_style( + &mut self, + kind: StyleKind, + name: String, + ) -> iced::Task { + self.style_rename_start(kind, name); + let id = crate::ui::style::style_list::rename_input_id(); + iced::Task::batch([ + iced::widget::operation::focus(id.clone()), + iced::widget::operation::select_all(id), + ]) } pub(super) fn style_delete(&mut self, kind: StyleKind) { @@ -770,6 +786,7 @@ impl OpenCADStudio { self.load_style_bufs(kind); self.style_rename_buf = name.clone(); self.style_rename = Some(name); + self.style_rename_kind = Some(kind); } /// Commit the inline rename. No-op (with feedback) on empty / unchanged / diff --git a/src/app/update/block_ops_tests.rs b/src/app/update/block_ops_tests.rs index 2dc81cacb..05a3c25a7 100644 --- a/src/app/update/block_ops_tests.rs +++ b/src/app/update/block_ops_tests.rs @@ -887,11 +887,9 @@ fn locked_palette_remove_refuses_a_block_with_an_open_bedit_session() { ); } -/// Nested BEDIT sessions all count: the outer and the inner record are both -/// open, so both are refused โ€” the whole session stack is handed to the -/// service, not just the active tab. +/// Switching replaces the open session; the active definition stays protected. #[test] -fn locked_palette_remove_refuses_every_block_of_a_nested_bedit_session() { +fn locked_palette_remove_refuses_the_open_block_after_switching() { let mut app = app(); let inner = "BO_NEST_INNER"; define_block(&mut app, inner); @@ -915,10 +913,10 @@ fn locked_palette_remove_refuses_every_block_of_a_nested_bedit_session() { begin_bedit(&mut app, inner_reference); assert_eq!( app.tabs[tab(&app)].block_edits.len(), - 2, - "the nested session did not open" + 1, + "switching must keep one session" ); - for name in [outer, inner] { + for name in [inner] { palette_remove(&mut app, name); assert!(record_exists(&app, name), "{name} went while open"); let expected = command_line_error(&format!( @@ -1347,3 +1345,219 @@ fn locked_purge_skips_a_block_with_an_open_bedit_session() { assert!(record_exists(&app, &name)); let _ = app.dispatch_command("BEDIT_DISCARD"); } + +#[test] +fn locked_attedit_attribute_flags_cases() { + use crate::ui::window::attribute_editor::AttrRow; + use codec::entities::{AttributeDefinition, AttributeEntity}; + use codec::tables::BlockRecord; + + // Verify flag behavior in ATTEDIT conversion and apply: + // 1. Normal attribute: editable, modifies value + let mut normal_entity = + AttributeEntity::new("NORM_TAG".to_string(), "original_norm".to_string()); + let normal_row = AttrRow { + tag: "NORM_TAG".to_string(), + prompt: "Enter norm:".to_string(), + value: "edited_norm".to_string(), + text_style: "STANDARD".to_string(), + height: "2.5".to_string(), + rotation: "0".to_string(), + width_factor: "1".to_string(), + oblique: "0".to_string(), + h_align: codec::entities::HorizontalAlignment::Left, + v_align: codec::entities::VerticalAlignment::Baseline, + backwards: false, + upside_down: false, + layer: "0".to_string(), + color: codec::types::Color::ByLayer, + linetype: "ByLayer".to_string(), + line_weight: codec::types::LineWeight::ByLayer, + is_constant: false, + }; + assert!(!normal_row.is_constant); + + // 2. Constant attribute: is_constant is true, apply ignores value edit + let mut const_entity = + AttributeEntity::new("CONST_TAG".to_string(), "original_const".to_string()); + const_entity.flags.constant = true; + let mut const_row = normal_row.clone(); + const_row.tag = "CONST_TAG".to_string(); + const_row.value = "attempted_edit".to_string(); + const_row.is_constant = true; + + // 3. Invisible attribute: editable in ATTEDIT (flags.invisible is true, but is_constant is false) + let mut invis_entity = + AttributeEntity::new("INVIS_TAG".to_string(), "original_invis".to_string()); + invis_entity.flags.invisible = true; + let mut invis_row = normal_row.clone(); + invis_row.tag = "INVIS_TAG".to_string(); + invis_row.value = "edited_invis".to_string(); + invis_row.is_constant = false; + + // 4. Preset attribute: editable in ATTEDIT (preset only affects initial insertion prompt) + let mut preset_entity = + AttributeEntity::new("PRESET_TAG".to_string(), "original_preset".to_string()); + preset_entity.flags.preset = true; + let mut preset_row = normal_row.clone(); + preset_row.tag = "PRESET_TAG".to_string(); + preset_row.value = "edited_preset".to_string(); + preset_row.is_constant = false; + + // Verify metadata mapping from CadDocument + let mut doc = codec::CadDocument::new(); + let next = doc.next_handle(); + let br_h = Handle::new(next); + let mut br = BlockRecord::new("FLAG_BLOCK"); + br.handle = br_h; + br.block_entity_handle = Handle::new(next + 1); + br.block_end_handle = Handle::new(next + 2); + + let mut ad_norm = AttributeDefinition::new( + "NORM_TAG".to_string(), + "Enter norm:".to_string(), + "def".to_string(), + ); + let mut ad_const = AttributeDefinition::new( + "CONST_TAG".to_string(), + "Enter const:".to_string(), + "def".to_string(), + ); + ad_const.flags.constant = true; + let mut ad_invis = AttributeDefinition::new( + "INVIS_TAG".to_string(), + "Enter invis:".to_string(), + "def".to_string(), + ); + ad_invis.flags.invisible = true; + let mut ad_preset = AttributeDefinition::new( + "PRESET_TAG".to_string(), + "Enter preset:".to_string(), + "def".to_string(), + ); + ad_preset.flags.preset = true; + + let h1 = Handle::new(next + 3); + let h2 = Handle::new(next + 4); + let h3 = Handle::new(next + 5); + let h4 = Handle::new(next + 6); + ad_norm.common.handle = h1; + ad_const.common.handle = h2; + ad_invis.common.handle = h3; + ad_preset.common.handle = h4; + br.entity_handles = vec![h1, h2, h3, h4]; + + doc.block_records.add(br).unwrap(); + doc.add_entity(EntityType::AttributeDefinition(ad_norm)) + .unwrap(); + doc.add_entity(EntityType::AttributeDefinition(ad_const)) + .unwrap(); + doc.add_entity(EntityType::AttributeDefinition(ad_invis)) + .unwrap(); + doc.add_entity(EntityType::AttributeDefinition(ad_preset)) + .unwrap(); + + let meta = crate::app::update::command::block_attr_meta(&doc, "FLAG_BLOCK"); + assert!(!meta.get("NORM_TAG").unwrap().constant); + assert!(meta.get("CONST_TAG").unwrap().constant); + assert!(!meta.get("INVIS_TAG").unwrap().constant); + assert!(!meta.get("PRESET_TAG").unwrap().constant); + + // Test conversion from AttributeEntity to AttrRow + let r_norm = crate::app::update::command::attr_row_from_entity(&normal_entity, &meta); + assert!(!r_norm.is_constant); + assert_eq!(r_norm.prompt, "Enter norm:"); + + let r_const = crate::app::update::command::attr_row_from_entity(&const_entity, &meta); + assert!(r_const.is_constant); + assert_eq!(r_const.prompt, "Enter const:"); + + let r_invis = crate::app::update::command::attr_row_from_entity(&invis_entity, &meta); + assert!(!r_invis.is_constant); + assert_eq!(r_invis.prompt, "Enter invis:"); + + let r_preset = crate::app::update::command::attr_row_from_entity(&preset_entity, &meta); + assert!(!r_preset.is_constant); + assert_eq!(r_preset.prompt, "Enter preset:"); + + // Test apply_attr_row behavior on constant vs non-constant + // Normal: applies + let mut edited_norm_row = r_norm.clone(); + edited_norm_row.value = "edited_norm".to_string(); + assert!(crate::app::update::command::apply_attr_row( + &mut normal_entity, + &edited_norm_row + )); + assert_eq!(normal_entity.get_value(), "edited_norm"); + + // Constant: ignores value change! + let mut edited_const_row = r_const.clone(); + edited_const_row.value = "attempted_edit".to_string(); + assert!(!crate::app::update::command::apply_attr_row( + &mut const_entity, + &edited_const_row + )); + assert_eq!(const_entity.get_value(), "original_const"); + + // Invisible: applies + let mut edited_invis_row = r_invis.clone(); + edited_invis_row.value = "edited_invis".to_string(); + assert!(crate::app::update::command::apply_attr_row( + &mut invis_entity, + &edited_invis_row + )); + assert_eq!(invis_entity.get_value(), "edited_invis"); + + // Preset: applies + let mut edited_preset_row = r_preset.clone(); + edited_preset_row.value = "edited_preset".to_string(); + assert!(crate::app::update::command::apply_attr_row( + &mut preset_entity, + &edited_preset_row + )); + assert_eq!(preset_entity.get_value(), "edited_preset"); +} + +#[test] +fn locked_bedit_insert_refuses_cycle() { + let mut app = app(); + let name_a = "BEDIT_CYC_A"; + let name_b = "BEDIT_CYC_B"; + + define_block(&mut app, name_a); + + // B contains an insert of A + let ins_a = Insert::new(name_a.to_string(), Vector3::ZERO); + let i = tab(&app); + app.tabs[i] + .scene + .define_block_raw(name_b, Vector3::ZERO, vec![EntityType::Insert(ins_a)]); + + // Now open BEDIT for block A + let insert_a = add_insert(&mut app, name_a); + begin_bedit(&mut app, insert_a); + assert!(bedit_open(&app)); + + // Try committing an INSERT of B into A inside BEDIT space: + // Inserting B into A would create A -> B -> A cycle + let _ = app.dispatch_command("-INSERT"); + let mut feed = |text: &str| { + let _ = app.update(crate::app::Message::CommandInput(text.to_string())); + let _ = app.update(crate::app::Message::CommandSubmit); + }; + feed(name_b); + feed("0,0"); + feed("1"); + feed("1"); + feed("0"); + + // Verify refusal error on command line + let errs = errors(&app); + assert!( + errs.iter() + .any(|e| e.contains("circular dependency detected")), + "expected cycle error in errors: {errs:?}" + ); + + let _ = app.dispatch_command("BEDIT_DISCARD"); +} diff --git a/src/app/update/blocks_palette.rs b/src/app/update/blocks_palette.rs index d128c6707..bbea5ab38 100644 --- a/src/app/update/blocks_palette.rs +++ b/src/app/update/blocks_palette.rs @@ -427,7 +427,11 @@ impl OpenCADStudio { for def in deps.blocks { let scene = &mut self.tabs[i].scene; if redefine && def.name.eq_ignore_ascii_case(name) { - scene.redefine_block_raw(&def.name, def.base_point, def.entities); + if let Err(err) = scene.redefine_block_raw(&def.name, def.base_point, def.entities) + { + self.command_line.push_error(&err); + return Err(err); + } } else if scene.document.block_records.get(&def.name).is_none() { scene.define_block_raw(&def.name, def.base_point, def.entities); } diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 19eda0861..7ca474523 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -55,6 +55,16 @@ fn polyline_add_vertex_target(entity: &AcadEntityType, grip_id: usize) -> Option AcadEntityType::Polyline3D(polyline) if grip_id < polyline.vertices.len() => { Some((grip_id + 1, polyline.vertices.len())) } + // A hatch boundary vertex: the new vertex is the next grip. + AcadEntityType::Hatch(hatch) => { + use crate::entities::traits::Grippable; + use crate::scene::model::object::GripMenuAction; + hatch + .grip_menu(grip_id) + .iter() + .any(|item| matches!(item.action, GripMenuAction::AddVertex)) + .then(|| (grip_id + 1, hatch.grips().len())) + } _ => None, } } @@ -65,6 +75,9 @@ fn polyline_vertex_count(entity: &AcadEntityType) -> Option { AcadEntityType::Polyline(polyline) => Some(polyline.vertices.len()), AcadEntityType::Polyline2D(polyline) => Some(polyline.vertices.len()), AcadEntityType::Polyline3D(polyline) => Some(polyline.vertices.len()), + AcadEntityType::Hatch(hatch) => { + Some(crate::entities::traits::Grippable::grips(hatch).len()) + } _ => None, } } @@ -138,7 +151,7 @@ impl OpenCADStudio { /// Close queued drawings until a dirty tab requires confirmation. Queue /// entries use stable document ids because removing an earlier tab changes /// every later vector index. - pub(super) fn continue_tab_close_queue(&mut self) -> Task { + pub(in crate::app) fn continue_tab_close_queue(&mut self) -> Task { let mut tasks = Vec::new(); while let Some(tab_id) = self.pending_tab_closes.pop_front() { let Some(idx) = self.tabs.iter().position(|tab| tab.id == tab_id) else { @@ -147,6 +160,13 @@ impl OpenCADStudio { if self.tabs[idx].is_start { continue; } + if self.tabs[idx].active_block_edit.is_some() || self.tabs[idx].block_test.is_some() { + tasks.push(self.on_tab_close(idx)); + if self.pending_block_close.is_some() || self.pending_close.is_some() { + break; + } + continue; + } if self.tabs[idx].dirty { self.pending_close = Some(crate::app::PendingClose::Tab(tab_id)); tasks.push(self.open_unsaved_dialog_window()); @@ -157,11 +177,42 @@ impl OpenCADStudio { Task::batch(tasks) } - pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { + pub(in crate::app) fn on_tab_close(&mut self, idx: usize) -> Task { + let test_return_id = self + .tabs + .get(idx) + .and_then(|tab| tab.block_test.as_ref()) + .map(|test| { + if idx == self.active_tab { + test.source_id + } else { + self.tabs[self.active_tab].id + } + }); + if test_return_id.is_some() { + if idx == self.active_tab { + self.cancel_active_grip_edit(); + self.grip_pending = None; + self.visibility_popup = None; + self.grip_popup = None; + self.grip_hover = None; + } + self.tabs[idx].dirty = false; + } // Start tab is fixed โ€” close requests on it are no-ops. if self.tabs.get(idx).map_or(false, |t| t.is_start) { return Task::none(); } + if self + .tabs + .get(idx) + .is_some_and(|tab| tab.active_block_edit.is_some()) + { + return self.request_block_edit_close( + idx, + crate::app::block_editor::BlockEditContinuation::Tab(self.tabs[idx].id), + ); + } // Closing a tab shifts indices / the active tab. The attribute // editor holds a document-local handle into one tab, so drop it // now rather than risk it applying to a different tab's document. @@ -219,6 +270,16 @@ impl OpenCADStudio { // The active tab is now either a brand-new blank or a // different existing tab; in both cases the ribbon needs // to track that doc's defaults / selection. #21. + if let Some(index) = + test_return_id.and_then(|id| self.tabs.iter().position(|tab| tab.id == id)) + { + self.active_tab = index; + self.adopt_header_sysvars(index); + } + self.ribbon + .set_block_testing(self.tabs[self.active_tab].block_test.is_some()); + self.ribbon + .set_block_editing(self.tabs[self.active_tab].active_block_edit.is_some()); self.sync_ribbon_layers(); self.sync_ribbon_styles(); self.sync_ribbon_from_selection(); @@ -868,6 +929,13 @@ impl OpenCADStudio { if let Some(task) = self.try_selection_keyword(&text) { return task; } + // Under a typed ` bool { + let (Some(angle), Some(_)) = (text.trim().strip_prefix('<'), self.last_point) else { + return false; + }; + let Some(angle) = crate::entities::common::parse_typed_direction(angle) else { + return false; + }; + let local = glam::DVec3::new(angle.cos(), angle.sin(), 0.0); + self.angle_override = Some(match &self.tabs[self.active_tab].active_ucs { + Some(ucs) => ucs_rotate_vec(local, ucs), + None => local, + }); + let shown = crate::entities::common::format_direction(angle); + self.command_line.push_output(crate::tf!("Angle Override: {shown}").as_ref()); + true + } + pub(in crate::app) fn try_direct_distance_entry( &mut self, text: &str, @@ -2102,18 +2193,64 @@ impl OpenCADStudio { } } - /// Attach a clipboard image read on the web (a pasted screenshot) the way - /// the IMAGE command's pick does: register the bytes name-keyed, decode - /// for the file's true pixel size, then commit the entity โ€” but placed - /// directly at the cursor (0,0 before any pointer move) with the command's + /// Read an image off the system clipboard for a paste that found no + /// text. The web gets the encoded file from the Clipboard API; native + /// gets pixels, which are written as a PNG beside the drawing (or, for a + /// drawing never saved, in the config folder) so the image entity keeps + /// a file to point at. + pub(in crate::app) fn read_system_clipboard_image(&self) -> Task { + #[cfg(target_arch = "wasm32")] + { + Task::perform( + crate::sys::read_clipboard_image(), + Message::SystemClipboardPasteImage, + ) + } + #[cfg(not(target_arch = "wasm32"))] + { + let folder = self.tabs[self.active_tab] + .current_path + .as_ref() + .and_then(|path| path.parent().map(std::path::Path::to_path_buf)) + .filter(|folder| !folder.as_os_str().is_empty()) + .or_else(|| crate::config::config_dir().map(|dir| dir.join("pasted"))); + iced::clipboard::read_image().map(move |image| { + let saved = image.ok().zip(folder.clone()).and_then(|(image, folder)| { + let pixels = image::RgbaImage::from_raw( + image.size.width, + image.size.height, + image.rgba.to_vec(), + )?; + let mut png = std::io::Cursor::new(Vec::new()); + pixels.write_to(&mut png, image::ImageFormat::Png).ok()?; + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |elapsed| elapsed.as_millis()); + let name = crate::sys::unique_pasted_image_name( + &format!("pasted-{stamp}"), + "png", + |name| folder.join(name).exists(), + ); + let path = folder.join(name); + std::fs::create_dir_all(&folder).ok()?; + std::fs::write(&path, png.get_ref()).ok()?; + Some((path.to_string_lossy().into_owned(), png.into_inner())) + }); + Message::SystemClipboardPasteImage(saved) + }) + } + } + + /// Attach a clipboard image (a pasted screenshot) the way the IMAGE + /// command's pick does: register the bytes name-keyed, decode for the + /// file's true pixel size, then commit the entity โ€” but placed directly + /// at the cursor (0,0 before any pointer move) with the command's /// default width, skipping the pick prompts a paste should not run. - #[cfg(target_arch = "wasm32")] pub(super) fn paste_clipboard_image(&mut self, name: String, bytes: Vec) -> Task { let i = self.active_tab; if self.tabs[i].is_start { - self.command_line.push_error( - crate::tr!("common", "no-drawing-open-use-new-or-open-first").as_ref(), - ); + self.command_line + .push_error(crate::tr!("common", "no-drawing-open-use-new-or-open-first").as_ref()); return Task::none(); } crate::scene::model::image_model::register_source(&name, std::sync::Arc::new(bytes)); @@ -2227,6 +2364,10 @@ impl OpenCADStudio { let append = matches!(scope, crate::app::QSelectScope::CurrentSpace) && remembered.as_ref().is_some_and(|settings| settings.append); self.qselect = Some(crate::app::QSelectState { + conditions: remembered + .as_ref() + .map_or_else(Vec::new, |settings| settings.conditions.clone()), + editing_condition: None, scope, available_types, available_properties, @@ -3685,6 +3826,40 @@ impl OpenCADStudio { Task::none() } + /// An associative array row: the typed value changes the array's + /// parameter and its items are placed again, in one undo step. + fn on_assoc_array_field_commit(&mut self, i: usize, field: &'static str, handles: &[Handle]) { + let Some(text) = self.tabs[i] + .properties + .edit_buf + .remove(&crate::ui::properties::FieldKey::Geom(field)) + else { + return; + }; + let arrays: Vec<_> = handles + .iter() + .filter_map(|handle| { + crate::scene::assoc_array::array_of_insert(&self.tabs[i].scene.document, *handle) + }) + .collect(); + if arrays.is_empty() { + return; + } + self.push_undo_snapshot(i, "ARRAYEDIT"); + for array in &arrays { + if let Err(error) = crate::scene::assoc_array::apply_field( + &mut self.tabs[i].scene, + array, + field, + &crate::app::expr_eval::eval_to_string(&text), + ) { + self.command_line.push_error(&format!("ARRAYEDIT: {error}")); + } + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + pub(super) fn on_prop_geom_commit(&mut self, field: &'static str) -> Task { use crate::ui::window::named_parameters::ParamField; // A dynamic dimension's Name/Expression rows edit its parameter. @@ -3702,6 +3877,10 @@ impl OpenCADStudio { let i = self.active_tab; self.tabs[i].properties.active_field = None; let handles = self.property_target_handles(i); + if field.starts_with("assoc_array_") { + self.on_assoc_array_field_commit(i, field, &handles); + return Task::none(); + } if !handles.is_empty() { let driven_reference = match field { "start_x" | "start_y" | "start_z" => Some(Some(0)), @@ -3763,7 +3942,9 @@ impl OpenCADStudio { return Task::none(); } } - if field.starts_with("ul_") { + if field.starts_with("ul_") + || matches!(field, "ri_bright" | "ri_contrast" | "ri_fade") + { if let Err(message) = crate::entities::underlay::validate_property(field, &val) { self.command_line.push_error(message); @@ -4262,11 +4443,11 @@ impl OpenCADStudio { Some(codec::EntityType::Insert(ins)) if !ins.attributes.is_empty() => { // The prompt text lives on the block's ATTDEFs, not on the // attribute instances โ€” map tag โ†’ prompt from the definition. - let prompts = block_attr_prompts(doc, &ins.block_name); + let meta = block_attr_meta(doc, &ins.block_name); let rows = ins .attributes .iter() - .map(|a| attr_row_from_entity(a, &prompts)) + .map(|a| attr_row_from_entity(a, &meta)) .collect::>(); Ok((ins.block_name.clone(), rows)) } @@ -4298,7 +4479,7 @@ impl OpenCADStudio { /// Close the attribute editor without applying, if it is open. Used when /// the tab it belongs to closes or the active tab switches โ€” its handle is /// document-local and must not outlive its tab. - pub(super) fn cancel_attr_editor(&mut self) { + pub(in crate::app) fn cancel_attr_editor(&mut self) { if self.active_modal == Some(crate::app::ModalKind::AttributeEditor) { self.active_modal = None; self.reset_modal_geometry(); @@ -4360,16 +4541,47 @@ impl OpenCADStudio { } } +pub(crate) struct BlockAttrMeta { + pub(crate) prompt: String, + pub(crate) constant: bool, +} + +/// Map each attribute tag to the metadata (prompt text, constant flag) declared on +/// the block's matching ATTDEF. Tags with no definition are absent. +pub(crate) fn block_attr_meta( + doc: &codec::CadDocument, + block_name: &str, +) -> rustc_hash::FxHashMap { + let mut map = rustc_hash::FxHashMap::default(); + if let Some(br) = doc.block_records.get(block_name) { + for &eh in &br.entity_handles { + if let Some(codec::EntityType::AttributeDefinition(ad)) = doc.get_entity(eh) { + map.insert( + ad.tag.clone(), + BlockAttrMeta { + prompt: ad.prompt.clone(), + constant: ad.flags.constant, + }, + ); + } + } + } + map +} + /// Build the editor's working copy of one attribute. Angles are shown in /// degrees; numeric fields become strings so the user can type freely. -fn attr_row_from_entity( +pub(crate) fn attr_row_from_entity( a: &codec::entities::AttributeEntity, - prompts: &rustc_hash::FxHashMap, + meta: &rustc_hash::FxHashMap, ) -> AttrRow { let fmt = |v: f64| format!("{v}"); + let ad_meta = meta.get(&a.tag); + let prompt = ad_meta.map(|m| m.prompt.clone()).unwrap_or_default(); + let is_constant = a.flags.constant || ad_meta.map_or(false, |m| m.constant); AttrRow { tag: a.tag.clone(), - prompt: prompts.get(&a.tag).cloned().unwrap_or_default(), + prompt, value: a.get_value().to_string(), text_style: a.text_style.clone(), height: fmt(a.height), @@ -4384,15 +4596,16 @@ fn attr_row_from_entity( color: a.common.color, linetype: a.common.linetype.clone(), line_weight: a.common.line_weight, + is_constant, } } /// Write one edited row back onto its attribute; returns whether anything /// changed. Numeric fields are parsed (angles from degrees); an unparseable -/// field is left as-is. -fn apply_attr_row(a: &mut codec::entities::AttributeEntity, row: &AttrRow) -> bool { +/// field is left as-is. Constant attributes ignore edits to their value. +pub(crate) fn apply_attr_row(a: &mut codec::entities::AttributeEntity, row: &AttrRow) -> bool { let mut ch = false; - if a.get_value() != row.value { + if !row.is_constant && a.get_value() != row.value { a.set_value(row.value.clone()); ch = true; } @@ -4463,24 +4676,6 @@ fn apply_attr_row(a: &mut codec::entities::AttributeEntity, row: &AttrRow) -> bo ch } -/// Map each attribute tag to the prompt text declared on the block's matching -/// ATTDEF. Attribute instances (ATTRIB) carry only tag + value; the prompt is -/// defined once on the block definition. Tags with no definition are absent. -fn block_attr_prompts( - doc: &codec::CadDocument, - block_name: &str, -) -> rustc_hash::FxHashMap { - let mut map = rustc_hash::FxHashMap::default(); - if let Some(br) = doc.block_records.get(block_name) { - for &eh in &br.entity_handles { - if let Some(codec::EntityType::AttributeDefinition(ad)) = doc.get_entity(eh) { - map.insert(ad.tag.clone(), ad.prompt.clone()); - } - } - } - map -} - #[cfg(test)] mod layer_rename_tests { use crate::app::OpenCADStudio; diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 5d04c72fd..7dea3f90b 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -22,6 +22,9 @@ use iced::{mouse, Point, Task}; impl OpenCADStudio { pub(in crate::app) fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task { + if self.refuse_block_test_save(tab_idx) { + return Task::none(); + } // Every entry here is Save-As semantics (the SAVEAS command, the // recovery save-as, the native failure redirects): the user is // choosing a destination, so on the web the FS Access picker must @@ -580,11 +583,14 @@ impl OpenCADStudio { .define_block_raw(&def.name, def.base_point, def.entities); } if redefine { - self.tabs[i].scene.redefine_block_raw( + if let Err(err) = self.tabs[i].scene.redefine_block_raw( &name, codec::types::Vector3::new(base.x, base.y, base.z), entities, - ); + ) { + self.command_line.push_error(&err); + return Err(err); + } } else { self.tabs[i] .scene diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index a27a851c3..421b8c94c 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -29,6 +29,10 @@ impl OpenCADStudio { &self, i: usize, ) -> Option<(glam::DVec3, glam::DVec3)> { + // A typed ` Result<(), crate::io::SaveFailure> { + if self.tabs[i].block_test.is_some() { + return Err(crate::io::SaveFailure::other( + "Test Block is temporary and cannot be saved", + )); + } let previous_autosave = self.autosave_target(i); if self.tabs[i].recovery_save_as_required && self.tabs[i] @@ -2120,22 +2148,7 @@ impl OpenCADStudio { ) { crate::scene::text::ttf_glyph::clear_fallback_cache(); } - // Current model-space annotation scale comes from the drawing's - // CANNOSCALEVALUE (paper/drawing factor). Convert its inverse into - // drawing units as well: metric annotation sizes are paper millimetres - // and imperial annotation sizes are paper inches. - // Current model-space annotation scale comes from the drawing's - // CANNOSCALEVALUE (paper/drawing factor). Convert its paper unit into - // the drawing's INSUNITS as well, so e.g. a metre drawing uses - // 0.001 model units for 1 mm of paper at 1:1. - let cannoscale_value = self.tabs[i].scene.document.header.annotation_scale_value; - let unit_factor = self.tabs[i].scene.annotation_scale_unit_factor(); - - self.tabs[i].scene.annotation_scale = if cannoscale_value > 1e-9 { - ((1.0 / cannoscale_value) / unit_factor) as f32 - } else { - (1.0 / unit_factor) as f32 - }; + self.tabs[i].scene.sync_annotation_scale_from_header(); // Open-time breakdown so regressions are visible immediately. // `total` is wall time from the Open click to here (post-xref, @@ -2193,6 +2206,12 @@ impl OpenCADStudio { self.tabs[i].scene.load_current_layout_state(); self.tabs[i].refresh_active_ucs(); } + // A layout saved with a model viewport entered reopens inside it, + // with that viewport's UCS (#1670). + let enter_viewport_task = self.tabs[i] + .scene + .saved_entered_viewport() + .map_or_else(Task::none, |handle| Task::done(Message::EnterViewport(handle))); // The tab may have been reused for the newly opened file. Any Plot // dialog cache belongs to the previous/startup drawing and must not @@ -2314,6 +2333,7 @@ impl OpenCADStudio { interaction_task, startup_script_task, sheet_set_task, + enter_viewport_task, ]) } @@ -2413,7 +2433,7 @@ impl OpenCADStudio { let mut skipped = 0usize; let mut pending: Vec<(crate::app::WebSaveallEntry, codec::CadDocument)> = Vec::new(); for t in 0..self.tabs.len() { - if self.tabs[t].is_start { + if self.tabs[t].is_start || self.tabs[t].block_test.is_some() { continue; } let Some(path) = self.tabs[t].current_path.clone() else { @@ -2583,6 +2603,41 @@ impl OpenCADStudio { ) } + pub(super) fn on_fbx_export_path_some(&mut self, path: std::path::PathBuf) -> Task { + let i = self.active_tab; + // Export uses LOD 0 (full resolution); see StlExportPath above. + let meshes: Vec = self.tabs[i] + .scene + .meshes + .values() + .filter_map(|s| s.lods.first().cloned()) + .collect(); + let header = &self.tabs[i].scene.document.header; + // Centimetres per drawing unit (INSUNITS); unitless metric drawings + // are taken as millimetres, imperial ones as inches. + let unit_cm = match header.insertion_units { + 1 => 2.54, + 2 => 30.48, + 4 => 0.1, + 5 => 1.0, + 6 => 100.0, + 14 => 10.0, + _ if header.measurement == 1 => 0.1, + _ => 2.54, + }; + let worker_path = path.clone(); + background_task( + crate::t!("FBX export"), + move || { + let mesh_refs: Vec<_> = meshes.iter().collect(); + let bytes = crate::io::fbx::build_fbx(&mesh_refs, unit_cm) + .ok_or_else(|| "no mesh data to export".to_string())?; + write_export(&worker_path, &bytes) + }, + move |result| Message::FbxExportFinished(path, result), + ) + } + pub(super) fn on_obj_import_path_some(&mut self, path: std::path::PathBuf) -> Task { let tab_id = self.tabs[self.active_tab].id; let worker_path = path.clone(); @@ -2602,6 +2657,7 @@ impl OpenCADStudio { if self.tabs[i].active_block_edit.is_none() { self.tabs[i].scene.sync_camera_to_document(); self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; + self.tabs[i].scene.sync_active_viewport_to_document(); } } @@ -3037,6 +3093,9 @@ impl OpenCADStudio { set_current_path: bool, check_external_change: bool, ) -> Task { + if self.refuse_block_test_save(i) { + return Task::none(); + } let tab_id = self.tabs[i].id; if self .pending_native_thumbnail_save @@ -3705,6 +3764,9 @@ impl OpenCADStudio { } pub(super) fn on_save_file(&mut self) -> Task { + if self.refuse_block_test_save(self.active_tab) { + return Task::none(); + } if self.read_only { self.command_line.push_error( crate::t!("Read-only session (--read-only): saving is disabled.").as_ref(), @@ -3761,6 +3823,9 @@ impl OpenCADStudio { /// Used by plain Save (QSAVE) on an as-yet-unsaved drawing and by the /// save-before-close flow โ€” the version picker is reserved for Save As. pub(in crate::app) fn save_with_default_format(&mut self, tab_idx: usize) -> Task { + if self.refuse_block_test_save(tab_idx) { + return Task::none(); + } self.active_tab = tab_idx; // Every caller of this entry (Save/QSAVE, the save-before-close // flow) is plain-save semantics: on the web, the tab's retained @@ -3887,8 +3952,10 @@ impl OpenCADStudio { // handle is secured at this gesture moment (or reused from a // previous pick โ€” retained handles need no activation to write) // and only the write happens later. + // A registered host save hook takes the bytes through the + // download path, so no file picker opens. let route = crate::io::fs_access::save_route( - crate::io::fs_access::supported(), + crate::io::fs_access::supported() && crate::sys::save_hook().is_none(), self.save_plain_reuse_fs_handle, crate::io::fs_access::handle_ext(tab_id).as_deref(), ext, @@ -4151,7 +4218,10 @@ impl OpenCADStudio { pub(super) fn on_autosave(&mut self) -> Task { let mut tasks = Vec::new(); for i in 0..self.tabs.len() { - if !self.tabs[i].dirty || self.active_save_jobs.contains_key(&self.tabs[i].id) { + if self.tabs[i].block_test.is_some() + || !self.tabs[i].dirty + || self.active_save_jobs.contains_key(&self.tabs[i].id) + { continue; } self.prepare_snapshot_save(i); @@ -4186,7 +4256,7 @@ impl OpenCADStudio { } let mut pending = Vec::new(); for i in 0..self.tabs.len() { - if !self.tabs[i].dirty { + if self.tabs[i].block_test.is_some() || !self.tabs[i].dirty { continue; } // Same pre-serialize state sync the web Save flow performs @@ -4513,7 +4583,7 @@ impl OpenCADStudio { let mut names: Vec = Vec::new(); let mut active = 0; for (index, tab) in self.tabs.iter().enumerate() { - if tab.is_start { + if tab.is_start || tab.block_test.is_some() { continue; } if index == self.active_tab { diff --git a/src/app/update/keytips.rs b/src/app/update/keytips.rs new file mode 100644 index 000000000..c38e812d5 --- /dev/null +++ b/src/app/update/keytips.rs @@ -0,0 +1,80 @@ +//! Ribbon key tips: a lone Alt opens them, typed codes walk the ribbon and +//! Esc goes back one level. + +use iced::Task; + +use crate::app::{Message, OpenCADStudio}; +use crate::ui::ribbon::{KeyTipInput, KeyTipStep}; + +impl OpenCADStudio { + pub(in crate::app) fn on_keytip(&mut self, input: KeyTipInput) -> Task { + match input { + KeyTipInput::AltTap if self.ribbon.keytips_active() => self.hide_keytips(), + KeyTipInput::AltTap => { + if self.active_modal.is_some() || self.clean_screen || self.ribbon_closed { + return Task::none(); + } + // A focused field other than the command line keeps Alt. + iced::advanced::widget::operate( + iced::advanced::widget::operation::focusable::count(), + ) + .then(|count| { + if count.focused.is_none() { + return Task::done(Message::KeyTip(KeyTipInput::Open { refocus: false })); + } + iced::widget::operation::is_focused(iced::widget::Id::new( + crate::ui::command_line::CMD_INPUT_ID, + )) + .map(|command_line| { + if command_line { + Message::KeyTip(KeyTipInput::Open { refocus: true }) + } else { + Message::Noop + } + }) + }) + } + KeyTipInput::Open { refocus } => { + self.ribbon.show_keytips(refocus); + // Typed codes must not reach the command line. + if refocus { + self.unfocus_widgets() + } else { + Task::none() + } + } + KeyTipInput::Hide => self.hide_keytips(), + KeyTipInput::Escape => { + if self.ribbon.keytip_escape() { + Task::none() + } else { + self.hide_keytips() + } + } + KeyTipInput::Char(_) | KeyTipInput::Enter => { + let ch = match &input { + KeyTipInput::Char(ch) => Some(ch.as_str()), + _ => None, + }; + match self.ribbon.keytip_type(ch) { + KeyTipStep::Ignore | KeyTipStep::Stay => Task::none(), + KeyTipStep::Open(message) => self.update(message), + KeyTipStep::Run(message) => { + let refocus = self.hide_keytips(); + Task::batch([refocus, self.update(message)]) + } + } + } + } + } + + /// Close the key tips, returning the focus to the command line if it had + /// it. + pub(in crate::app) fn hide_keytips(&mut self) -> Task { + if self.ribbon.hide_keytips() { + self.focus_cmd_input() + } else { + Task::none() + } + } +} diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 8e8f1fda0..27f2237b7 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -50,7 +50,7 @@ fn is_modal_blocked_key_msg(msg: &Message) -> bool { fn perf_message_label(msg: &Message) -> &'static str { match msg { - Message::ViewportLeftPress | Message::PanePress(_) => "pointer-down", + Message::ViewportLeftPressAt(_) | Message::ViewportLeftPress | Message::PanePress(_) => "pointer-down", Message::ViewportLeftRelease | Message::PaneRelease(_) => "pointer-up", Message::ViewportMove(_) | Message::PaneMove(_, _) => "pointer-move", Message::CommandFinalize => "command-finalize", @@ -102,10 +102,12 @@ mod dialog; mod dynamic; mod file; mod graphic_attributes; +mod keytips; pub(in crate::app) use file::background_task; #[cfg(not(target_arch = "wasm32"))] pub(in crate::app) use file::native_paths_match; mod page_setup_import; +mod qselect; mod style; pub(in crate::app) mod util; mod viewport; @@ -214,6 +216,7 @@ impl OpenCADStudio { match self.active_modal { // Dismissing these via โœ• is the cancel/decline path. Some(Unsaved) => self.pending_close = None, + Some(BlockEditChanges) => self.cancel_block_edit_close(), Some(AssocPrompt) => self.mark_assoc_prompted(), // Cancel: leave the layer (and its objects) untouched. Some(LayerDeleteWarning) => self.layer_delete_pending = None, @@ -227,6 +230,8 @@ impl OpenCADStudio { } Some(GeometricTolerance) => self.geometric_tolerance = None, Some(BlockDefinition) => self.block_definition = None, + Some(BlockEditPicker) => self.block_edit_picker = None, + Some(BlockSaveAs) => self.block_save_as = None, Some(PdfAttach) => self.pdf_attach = None, Some(UnderlayLayers) => self.underlay_layers = None, Some(PdfImportSettings) => self.pdf_import_settings = None, @@ -338,6 +343,21 @@ impl OpenCADStudio { self.sync_underlay_tab(); } self.sync_frame_dropdown(); + let block_editing = self + .tabs + .get(self.active_tab) + .is_some_and(|tab| tab.active_block_edit.is_some()); + let testing = self + .tabs + .get(self.active_tab) + .is_some_and(|tab| tab.block_test.is_some()); + if testing { + self.ribbon.set_block_editing(false); + self.ribbon.set_block_testing(true); + } else { + self.ribbon.set_block_testing(false); + self.ribbon.set_block_editing(block_editing); + } task } @@ -355,6 +375,12 @@ impl OpenCADStudio { _ => "FRAMES3", }; self.ribbon.set_dropdown_current("FRAMES_DROPDOWN", current); + let uosnap = match crate::scene::model::pdf_vector::uosnap() { + 0 => "UOSNAP0", + 1 => "UOSNAP1", + _ => "UOSNAP2", + }; + self.ribbon.set_dropdown_current("UOSNAP_DROPDOWN", uosnap); let attmode = match tab.scene.document.header.attribute_visibility { 0 => "ATTMODE0", 2 => "ATTMODE2", @@ -465,6 +491,18 @@ impl OpenCADStudio { } } } + if self.active_modal == Some(super::ModalKind::BlockEditPicker) + && (matches!(msg, Message::CommandFinalize) + || matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN"))) + { + return self.apply_bedit_dialog(); + } + if self.active_modal == Some(super::ModalKind::BlockSaveAs) + && (matches!(msg, Message::CommandFinalize) + || matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN"))) + { + return self.apply_block_save_as_dialog(); + } if self.active_modal == Some(super::ModalKind::WriteBlock) { if matches!(msg, Message::CommandFinalize) || matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN")) @@ -545,12 +583,15 @@ impl OpenCADStudio { .as_ref() .map(|c| c.options()) .or_else(|| { - self.graphic_attributes.pending_fill_close.as_ref().map(|_| { - vec![ - crate::command::CmdOption::new("Yes", "Y"), - crate::command::CmdOption::new("No", "N"), - ] - }) + self.graphic_attributes + .pending_fill_close + .as_ref() + .map(|_| { + vec![ + crate::command::CmdOption::new("Yes", "Y"), + crate::command::CmdOption::new("No", "N"), + ] + }) }) .unwrap_or_default(); self.command_line.set_step_options(opts); @@ -606,7 +647,11 @@ impl OpenCADStudio { } } #[cfg(target_arch = "wasm32")] - crate::sys::set_unsaved_changes_warning(self.tabs.iter().any(|tab| tab.dirty)); + crate::sys::set_unsaved_changes_warning( + self.tabs + .iter() + .any(|tab| tab.dirty && tab.block_test.is_none()), + ); if self.tabs[i].active_cmd.is_none() { self.block_palette.placing = None; } @@ -630,6 +675,7 @@ impl OpenCADStudio { match msg { Message::SpaceMouseWake => self.on_spacemouse_wake(), Message::TrackpadPinch(magnification) => self.on_pinch_zoom(magnification), + Message::Touch(event) => self.on_touch(event), Message::SpaceMouseFrame(time) => { self.spacemouse .frame(time.saturating_duration_since(self.start).as_secs_f64() * 1000.); @@ -639,6 +685,14 @@ impl OpenCADStudio { if Some(id) == self.main_window { self.spacemouse_focused = focused; } + // Key tips close when the window loses the focus, and an Alt + // held while it left (Alt+Tab) no longer counts as a lone tap. + if !focused { + crate::ui::ribbon::keytip_pointer_pressed(); + } + if !focused && self.ribbon.keytips_active() { + return self.hide_keytips(); + } Task::none() } Message::SpaceMouseEnabled(enabled) => { @@ -781,9 +835,7 @@ impl OpenCADStudio { // A settled fetch's rebuild trigger was dropped at the // outcome cap; do one unconditional catch-up pass over // every registered name. - registered.extend( - crate::scene::model::image_model::registered_image_names(), - ); + registered.extend(crate::scene::model::image_model::registered_image_names()); } if !registered.is_empty() { for tab in self.tabs.iter_mut() { @@ -854,15 +906,23 @@ impl OpenCADStudio { // Web: fetch every script queued by startup language selection or // drawing text discovery. Each script has one shared store entry. Message::PollWebFonts => { + #[cfg(target_arch = "wasm32")] + let autosave = if web_autosave_due(self.savetime_min) { + self.on_autosave() + } else { + Task::none() + }; + #[cfg(not(target_arch = "wasm32"))] + let autosave = Task::none(); let pending = crate::scene::text::web_font::take_pending(); if pending.is_empty() { - return Task::none(); + return autosave; } - Task::batch(pending.into_iter().map(|script| { + Task::batch(std::iter::once(autosave).chain(pending.into_iter().map(|script| { Task::perform(crate::scene::text::web_font::fetch(script), move |res| { Message::WebFontLoaded(script, res) }) - })) + }))) } // Web: a per-script font arrived. The same bytes feed drawing text, @@ -959,9 +1019,8 @@ impl OpenCADStudio { // (its WebFileOpened then fails the id check and the // bytes are lost) โ€” refuse like on_open_file does. if self.opening.is_some() { - self.command_line.push_info( - crate::t!("An open is already in progress.").as_ref(), - ); + self.command_line + .push_info(crate::t!("An open is already in progress.").as_ref()); return Task::none(); } let state = std::sync::Arc::new(crate::io::OpenProgressState::new( @@ -1181,6 +1240,12 @@ impl OpenCADStudio { Task::none() } + Message::SetRecentView(view) => { + self.recent_view = view; + self.save_config(); + Task::none() + } + Message::RecentLimitInput(s) => { // Keep only digits while typing; applied on Enter (SetRecentLimit). self.recent_limit_input = s.chars().filter(|c| c.is_ascii_digit()).collect(); @@ -1456,7 +1521,7 @@ impl OpenCADStudio { LinkOutcome::Error(text) => self.command_line.push_error(&text), } Task::none() - }, + } #[cfg(target_arch = "wasm32")] Message::WebAutosavesScanned(result) => self.on_web_autosaves_scanned(result), @@ -1731,10 +1796,7 @@ impl OpenCADStudio { ) .add_filter(crate::t!("DWG Files").as_ref(), &["dwg", "DWG"]) .add_filter(crate::t!("DXF Files").as_ref(), &["dxf", "DXF"]) - .add_filter( - crate::t!("Backup Files").as_ref(), - &["bak", "BAK"], - ) + .add_filter(crate::t!("Backup Files").as_ref(), &["bak", "BAK"]) .pick_file() .await; match handle { @@ -2026,6 +2088,62 @@ impl OpenCADStudio { } } + // โ”€โ”€ FBX export โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + Message::FbxExport => { + let i = self.active_tab; + if self.tabs[i].scene.meshes.is_empty() { + self.command_line.push_error( + crate::t!("FBXEXPORT: no 3D mesh data in this drawing.").as_ref(), + ); + return Task::none(); + } + #[cfg(not(target_arch = "wasm32"))] + { + Task::perform( + async { + crate::sys::file_dialog() + .set_title(crate::t!("Export FBX").as_ref()) + .set_file_name("export.fbx") + .add_filter(crate::t!("FBX Files").as_ref(), &["fbx"]) + .add_filter(crate::t!("All Files").as_ref(), &["*"]) + .save_file() + .await + .map(|h| crate::sys::handle_path(&h)) + }, + Message::FbxExportPath, + ) + } + #[cfg(target_arch = "wasm32")] + { + let stem = self.tabs[i] + .current_path + .as_deref() + .and_then(|p: &std::path::Path| p.file_stem()) + .map(|s: &std::ffi::OsStr| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "drawing".into()); + Task::perform( + crate::io::fbx::pick_fbx_path_owned(stem), + Message::FbxExportPath, + ) + } + } + + Message::FbxExportPath(Some(path)) => self.on_fbx_export_path_some(path), + + Message::FbxExportPath(None) => Task::none(), + + Message::FbxExportFinished(path, result) => { + match result { + Ok(()) => self.command_line.push_output( + crate::tf!("FBXEXPORT: exported to \"{}\"", path.display()).as_ref(), + ), + Err(error) => self + .command_line + .push_error(crate::tf!("FBXEXPORT: {error}").as_ref()), + } + Task::none() + } + Message::StepExportPath(Some(path)) => self.on_step_export_path_some(path), Message::StepExportPath(None) => Task::none(), @@ -2236,9 +2354,11 @@ impl OpenCADStudio { Message::PdfDialog(message) => self.update_pdf_dialog(message), Message::AttdefDialog(message) => self.on_attdef_dialog(message), Message::FieldDialog(message) => self.on_field_dialog(message), + Message::RefEditDialog(message) => self.update_refedit_dialog(message), Message::PcManager(message) => self.update_pc_manager(message), Message::Count(message) => self.on_count(message), Message::SheetSet(message) => self.on_sheet_set(message), + Message::KeyTip(input) => self.on_keytip(input), Message::RibbonSelectTab(idx) => { self.ribbon.select(idx); Task::none() @@ -2793,6 +2913,11 @@ impl OpenCADStudio { self.focus_cmd_input() } + Message::CommandHistoryHover(hovered) => { + self.command_line.set_history_hover(hovered); + Task::none() + } + Message::CommandHistoryToggle => { self.command_line.toggle_history(); if !self.command_line.history_open { @@ -3871,11 +3996,7 @@ impl OpenCADStudio { Message::WindowCloseRequested(id) => { if self.main_window == Some(id) { - if self.tabs.iter().any(|t| t.dirty) { - self.pending_close = Some(super::PendingClose::Quit); - return self.open_unsaved_dialog_window(); - } - return self.exit_app(); + return self.request_quit_with_block_edits(); } Task::none() } @@ -4151,6 +4272,21 @@ impl OpenCADStudio { Message::LayerRenameCommit => self.on_layer_rename_commit(), + // A click moved focus off the name field: keep what was typed, as + // Enter would. An untouched name stays open (the field may not have + // taken focus yet on the click that opened it). + Message::LayerRenameFocus(focused) => { + let layers = &self.tabs[self.active_tab].layers; + let typed = layers + .editing + .as_ref() + .is_some_and(|name| *name != layers.edit_buf); + if typed && focused != Some(crate::ui::window::layers::rename_input_id()) { + return self.on_layer_rename_commit(); + } + Task::none() + } + Message::LayerColorPickerToggle(name) => { let i = self.active_tab; let Some(idx) = self.layer_panel_index(i, &name) else { @@ -4270,6 +4406,19 @@ impl OpenCADStudio { // โ”€โ”€ Cursor / viewport messages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Message::CursorMoved(p, viewport) => self.on_cursor_moved(p, viewport), + // A multi-finger gesture owns the pointer while it runs. + Message::ViewportMove(_) + | Message::ViewportLeftPressAt(_) + | Message::ViewportLeftPress + | Message::ViewportLeftRelease + | Message::PaneMove(..) + | Message::PanePress(_) + | Message::PaneRelease(_) + if self.touch.active() => + { + Task::none() + } + Message::ViewportMove(p) => self.on_viewport_move(p), Message::ViewportExit => self.on_viewport_exit(), @@ -4344,6 +4493,17 @@ impl OpenCADStudio { self.update(Message::ViewportScroll(d)) } + Message::ViewportLeftPressAt(position) => { + // Use the pointer location captured by the actual button + // event. The last hover can lag behind fast cursor motion, + // particularly while expensive OSNAP/OTRACK previews run. + // Refresh the snap and world point before accepting the pick. + let moved = self.on_viewport_move(position); + self.graphic_attributes.open_menu = None; + let sweep = self.sync_active_field_if_any(); + Task::batch(vec![moved, sweep, self.on_viewport_left_press()]) + } + Message::ViewportLeftPress => { self.graphic_attributes.open_menu = None; let sweep = self.sync_active_field_if_any(); @@ -5448,6 +5608,8 @@ impl OpenCADStudio { header.angle_base = degrees.to_radians(); } header.insertion_units = state.insertion_units; + self.tabs[i].scene.sync_annotation_scale_from_header(); + self.tabs[i].scene.invalidate_annotation_dependencies(); self.tabs[i].dirty = true; self.tabs[i].scene.bump_geometry(); self.refresh_properties(); @@ -6026,6 +6188,8 @@ impl OpenCADStudio { // menu having done nothing. (#668) self.push_undo_snapshot(i, "UNITS"); self.tabs[i].scene.document.header.insertion_units = code; + self.tabs[i].scene.sync_annotation_scale_from_header(); + self.tabs[i].scene.invalidate_annotation_dependencies(); self.tabs[i].dirty = true; let label = crate::modules::draw::units::label(code); self.command_line.push_output( @@ -6191,6 +6355,12 @@ impl OpenCADStudio { } Task::none() } + Message::DraftingSettingsOsnapApertureChanged(value) => { + if let Some(state) = &mut self.drafting_settings_state { + state.osnap_aperture_input = value; + } + Task::none() + } Message::DraftingSettingsToggleOsnap => { if let Some(state) = &mut self.drafting_settings_state { state.osnap_on = !state.osnap_on; @@ -6443,7 +6613,13 @@ impl OpenCADStudio { self.ribbon.close_dropdown(); return Task::none(); } - self.ribbon.select_dropdown_item(dropdown_id, cmd); + // Save As is a secondary operation; the save face keeps BSAVE. + let face = if dropdown_id == "BLOCK_SAVE" { + "BSAVE" + } else { + cmd + }; + self.ribbon.select_dropdown_item(dropdown_id, face); self.ribbon.activate_tool(cmd); self.dispatch_command(cmd) } @@ -6802,6 +6978,31 @@ impl OpenCADStudio { } } + Message::PasteSpecial(text) => { + let i = self.active_tab; + let waiting = self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|command| command.name() == "PASTESPEC"); + if !waiting { + return Task::none(); + } + match text.filter(|text| !text.trim().is_empty()) { + Some(text) => { + // Same prompt, one more option: swap without echoing it. + self.tabs[i].active_cmd = Some(self.paste_special_command(i, Some(&text))); + self.push_ucs_to_cmd(i); + Task::none() + } + // Nothing but maybe an image: paste it as one. + None if self.clipboard.is_empty() => { + self.tabs[i].active_cmd = None; + self.read_system_clipboard_image() + } + None => Task::none(), + } + } + Message::SystemClipboardPaste(result) => { use super::SystemClipboardText as Text; @@ -6814,24 +7015,11 @@ impl OpenCADStudio { self.focus_cmd_input() } Text::EmptyOrUnsupported => { - // Web: no text โ€” the clipboard may still hold an IMAGE - // (a pasted screenshot). Read it through the async - // Clipboard API and attach it the way the IMAGE - // command's pick does; only when that also finds + // No text โ€” the clipboard may still hold an IMAGE (a + // pasted screenshot). Read it and attach it the way the + // IMAGE command's pick does; only when that also finds // nothing does the unsupported-content error surface. - // Native has no image route off the sync clipboard. - #[cfg(target_arch = "wasm32")] - return Task::perform( - crate::sys::read_clipboard_image(), - Message::SystemClipboardPasteImage, - ); - #[cfg(not(target_arch = "wasm32"))] - { - self.command_line.push_error( - crate::tr!("clipboard", "no-supported-content").as_ref(), - ); - Task::none() - } + self.read_system_clipboard_image() } Text::Unavailable => { self.command_line @@ -6851,10 +7039,9 @@ impl OpenCADStudio { } } - // Web: the clipboard-image fallback of a text-less paste either + // The clipboard-image fallback of a text-less paste either // attaches the image or falls back to the same error the text // path reports when nothing readable was on the clipboard. - #[cfg(target_arch = "wasm32")] Message::SystemClipboardPasteImage(image) => match image { Some((name, bytes)) => self.paste_clipboard_image(name, bytes), None => { @@ -7015,14 +7202,7 @@ impl OpenCADStudio { if matches!(scope, crate::app::QSelectScope::CurrentSelection) { state.append = false; } - if matches!( - state.operator, - crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt - ) && !state.property.as_ref().is_some_and(|property| { - matches!(property.editor, crate::app::QSelectValueEditor::Number) - }) { - state.operator = crate::app::QSelectOp::Eq; - } + state.normalize_operator(); } Task::none() } @@ -7046,14 +7226,7 @@ impl OpenCADStudio { state.property = kept_property; state.value.clear(); state.error = None; - if matches!( - state.operator, - crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt - ) && !state.property.as_ref().is_some_and(|property| { - matches!(property.editor, crate::app::QSelectValueEditor::Number) - }) { - state.operator = crate::app::QSelectOp::Eq; - } + state.normalize_operator(); } Task::none() } @@ -7063,14 +7236,7 @@ impl OpenCADStudio { state.property = p; state.value.clear(); state.error = None; - if matches!( - state.operator, - crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt - ) && !state.property.as_ref().is_some_and(|property| { - matches!(property.editor, crate::app::QSelectValueEditor::Number) - }) { - state.operator = crate::app::QSelectOp::Eq; - } + state.normalize_operator(); } Task::none() } @@ -7109,47 +7275,96 @@ impl OpenCADStudio { Task::none() } - Message::QSelectApply => { - let validation_error = self.qselect.as_ref().and_then(|state| { - let candidate_count = self.tabs[self.active_tab] - .scene - .qselect_candidate_count(state.scope); - if candidate_count == 0 { - Some(crate::t!("No objects are available in this scope.").into_owned()) - } else if let Some(property) = state.property.as_ref() { - if matches!(state.operator, crate::app::QSelectOp::Any) { - None - } else if matches!( - state.operator, - crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt - ) && !matches!( - property.editor, - crate::app::QSelectValueEditor::Number - ) { - Some( - crate::t!("This operator requires a numeric property.") - .into_owned(), - ) - } else { - match &property.editor { - crate::app::QSelectValueEditor::Number - if crate::entities::common::parse_f64(&state.value) - .is_none() => - { - Some(crate::t!("Enter a valid number.").into_owned()) - } - crate::app::QSelectValueEditor::Choice(_) - if state.value.is_empty() => - { - Some(crate::t!("Choose a value.").into_owned()) - } - crate::app::QSelectValueEditor::Text - | crate::app::QSelectValueEditor::Number - | crate::app::QSelectValueEditor::Choice(_) => None, - } + Message::QSelectPickObject => self.on_qselect_pick_object(), + Message::QSelectAddCondition => { + if let Some(state) = self.qselect.as_mut() { + let condition = state.draft_condition(); + if let Some(error) = condition.validation_error() { + state.error = Some(error); + return Task::none(); + } + if let Some(index) = state.editing_condition.take() { + if let Some(existing) = state.conditions.get_mut(index) { + *existing = condition; } } else { - None + state.conditions.push(condition); + } + state.error = None; + } + Task::none() + } + Message::QSelectEditCondition(index) => { + let condition = self + .qselect + .as_ref() + .and_then(|state| state.conditions.get(index).cloned()); + if let Some(condition) = condition { + let scope = self.qselect.as_ref().unwrap().scope; + let properties = self.tabs[self.active_tab] + .scene + .qselect_properties(condition.type_filter.as_deref(), scope); + if let Some(state) = self.qselect.as_mut() { + state.available_properties = properties; + state.type_filter = condition.type_filter; + state.property = condition.property; + state.operator = condition.operator; + state.value = condition.value; + state.editing_condition = Some(index); + state.error = None; + } + } + Task::none() + } + Message::QSelectRemoveCondition(index) => { + if let Some(state) = self.qselect.as_mut() { + if index < state.conditions.len() { + state.conditions.remove(index); + state.editing_condition = match state.editing_condition { + Some(editing) if editing == index => None, + Some(editing) if editing > index => Some(editing - 1), + other => other, + }; + } + state.error = None; + } + Task::none() + } + Message::QSelectClearConditions => { + if let Some(state) = self.qselect.as_mut() { + state.conditions.clear(); + state.editing_condition = None; + state.error = None; + } + Task::none() + } + Message::QSelectCancelEdit => { + if let Some(state) = self.qselect.as_mut() { + state.editing_condition = None; + state.error = None; + } + Task::none() + } + Message::QSelectApply => { + let validation_error = self.qselect.as_ref().and_then(|state| { + if self.tabs[self.active_tab] + .scene + .qselect_candidate_count(state.scope) + == 0 + { + Some(crate::t!("No objects are available in this scope.").into_owned()) + } else if state.editing_condition.is_some() { + Some( + crate::t!("Update or cancel the condition edit before applying.") + .into_owned(), + ) + } else if state.conditions.is_empty() { + state.draft_condition().validation_error() + } else { + state + .conditions + .iter() + .find_map(|condition| condition.validation_error()) } }); if let Some(error) = validation_error { @@ -7164,12 +7379,14 @@ impl OpenCADStudio { self.qselect_settings = Some((&state).into()); self.reset_modal_geometry(); let i = self.active_tab; - let matched = self.tabs[i].scene.qselect( + let conditions = if state.conditions.is_empty() { + vec![state.draft_condition()] + } else { + state.conditions.clone() + }; + let matched = self.tabs[i].scene.qselect_conditions( state.scope, - state.type_filter.as_deref(), - state.property.as_ref().map(|p| p.field.as_str()), - state.operator, - &state.value, + &conditions, state.mode, state.append && matches!(state.scope, crate::app::QSelectScope::CurrentSpace), ); @@ -7194,6 +7411,11 @@ impl OpenCADStudio { .collect(); wblock.error_message = None; self.active_modal = Some(super::ModalKind::WriteBlock); + } else { + // FILTER may be opened from a still-focused command input. + // Release text focus so Delete and clipboard shortcuts act + // on the resulting pick-first selection, not on that input. + return self.unfocus_widgets(); } Task::none() } @@ -7609,12 +7831,23 @@ impl OpenCADStudio { Message::PropAttrCommit(tag) => self.on_prop_attr_commit(tag), Message::PropPointerPressed => { + let mut rename_blur = if self.tabs[self.active_tab].layers.editing.is_some() { + crate::ui::properties::focused_widget_task(Message::LayerRenameFocus) + } else { + Task::none() + }; + if self.style_rename.is_some() { + rename_blur = Task::batch([ + rename_blur, + crate::ui::properties::focused_widget_task(Message::StyleRenameFocus), + ]); + } if !self.dock_panel_visible(crate::ui::dock::PanelId::Properties) && !self.dock_panel_visible(crate::ui::dock::PanelId::GraphicAttributes) { - return Task::none(); + return rename_blur; } - crate::ui::properties::sync_active_field_task() + Task::batch([crate::ui::properties::sync_active_field_task(), rename_blur]) } Message::PropSyncActive(focused) => { @@ -7624,7 +7857,8 @@ impl OpenCADStudio { crate::ui::window::graphic_attributes::LINETYPE_SCALE_FIELD, ); if focused.as_ref() == Some(&scale_field) { - if !std::mem::replace(&mut self.graphic_attributes.linetype_scale_focused, true) { + if !std::mem::replace(&mut self.graphic_attributes.linetype_scale_focused, true) + { return iced::widget::operation::select_all(scale_field); } return Task::none(); @@ -7655,6 +7889,13 @@ impl OpenCADStudio { Task::none() } + Message::PropPickerSurface(quick, message) => { + let task = self.update(*message); + let i = self.active_tab; + self.tabs[i].properties.picker_in_quick = quick; + task + } + Message::PropColorPickerToggle => { let i = self.active_tab; self.tabs[i].properties.color_picker_open = @@ -8818,6 +9059,12 @@ impl OpenCADStudio { Task::none() } + Message::SnapMarkerSizeChanged(size) => { + self.model_space.snap_marker_size = size.clamp(1, 20); + self.persist_settings_if_changed(); + Task::none() + } + Message::GripColorChanged(color) => { self.model_space.grip_color = color; self.sync_model_space_theme(false); @@ -8923,6 +9170,12 @@ impl OpenCADStudio { Task::none() } + Message::CommandLineOpacityChanged(percent) => { + self.command_line.set_opacity_percent(percent); + self.persist_settings_if_changed(); + Task::none() + } + Message::CommandLineFadeChanged(ms) => { let ms = crate::app::settings::clamp_commandline_fade_ms(ms); self.commandline_fade_ms = ms; @@ -8937,6 +9190,24 @@ impl OpenCADStudio { Task::none() } + Message::MenuBarChanged(shown) => { + self.show_menu_bar = shown; + self.persist_settings_if_changed(); + Task::none() + } + + Message::RibbonVisibleChanged(shown) => { + self.ribbon_closed = !shown; + self.persist_settings_if_changed(); + Task::none() + } + + Message::HatchAssociativeChanged(associative) => { + self.hatch_associative = associative; + self.persist_settings_if_changed(); + Task::none() + } + Message::TouchpadPanChanged(enabled) => { self.touchpad_pan = enabled; self.persist_settings_if_changed(); @@ -9237,6 +9508,16 @@ impl OpenCADStudio { self.close_sheet_set_dialog(); return Task::none(); } + // The Background dialog returns to -VIEW's settings prompt. + if self.active_modal == Some(super::ModalKind::ViewBackground) { + self.view_background = None; + self.close_active_modal(); + let i = self.active_tab; + return self.run_dash_view("SETTINGS", i); + } + if self.active_modal == Some(super::ModalKind::ImageAdjust) { + self.image_adjust = None; + } if self.active_modal == Some(super::ModalKind::Field) { self.close_field_dialog(); return Task::none(); @@ -9254,6 +9535,9 @@ impl OpenCADStudio { Task::none() }; } + if self.active_modal == Some(super::ModalKind::RefEdit) { + self.refedit_dialog = None; + } // Cancelling the section settings drops the waiting extraction. if self.active_modal == Some(super::ModalKind::PcSection) { crate::modules::insert::pc_extract::drop_job(); @@ -10208,6 +10492,78 @@ impl OpenCADStudio { Message::UnsavedDialogDiscard => self.on_unsaved_dialog_discard(), Message::UnsavedDialogSave => self.on_unsaved_dialog_save(), + Message::BlockEditChangesSave => self.resolve_block_edit_close(true), + Message::BlockSaveAsName(name) => { + let i = self.active_tab; + let error = self.validate_block_save_as_name(i, &name).err(); + if let Some(state) = self.block_save_as.as_mut() { + state.name = name; + state.error = error; + } + Task::none() + } + Message::BlockSaveAsApply => self.apply_block_save_as_dialog(), + Message::BlockEditPickerName(name) => { + if let Some(state) = self.block_edit_picker.as_mut() { + state.name = name.clone(); + let trimmed = name.trim(); + state.error = if trimmed.is_empty() { + None + } else { + crate::scene::block_service::validate_block_name(trimmed) + .err() + .map(|e| e.to_string()) + }; + if let Some(tab_idx) = + self.tabs.iter().position(|t| t.id == state.document_id) + { + if let Some(matched) = state + .existing_names + .iter() + .find(|n| n.eq_ignore_ascii_case(trimmed)) + { + state.preview = + self.tabs[tab_idx].scene.block_preview_wires(matched); + state.description = self.tabs[tab_idx] + .scene + .document + .block_records + .get(matched) + .map(|b| b.description.clone()) + .unwrap_or_default(); + } else { + state.preview.clear(); + state.description.clear(); + } + } + } + Task::none() + } + Message::BlockEditPickerSelect(name) => { + if let Some(state) = self.block_edit_picker.as_mut() { + state.name = name.clone(); + state.error = None; + if let Some(tab_idx) = + self.tabs.iter().position(|t| t.id == state.document_id) + { + state.preview = self.tabs[tab_idx].scene.block_preview_wires(&name); + state.description = self.tabs[tab_idx] + .scene + .document + .block_records + .get(&name) + .map(|b| b.description.clone()) + .unwrap_or_default(); + } + } + Task::none() + } + Message::BlockEditPickerApply => self.apply_bedit_dialog(), + Message::BlockEditChangesDiscard => self.resolve_block_edit_close(false), + Message::BlockEditChangesCancel => { + self.cancel_block_edit_close(); + Task::none() + } Message::AecDropSameVersion => self.on_aec_drop_same_version(), Message::AecDropProceed => self.on_aec_drop_proceed(), @@ -10883,14 +11239,8 @@ impl OpenCADStudio { } Task::none() } - Message::TextStyleDialogNew => { - self.style_new(crate::app::StyleKind::Text); - Task::none() - } - Message::TextStyleDialogCopy => { - self.style_copy(crate::app::StyleKind::Text); - Task::none() - } + Message::TextStyleDialogNew => self.style_new(crate::app::StyleKind::Text), + Message::TextStyleDialogCopy => self.style_copy(crate::app::StyleKind::Text), Message::TextStyleDialogDelete => { self.style_delete(crate::app::StyleKind::Text); Task::none() @@ -10910,6 +11260,18 @@ impl OpenCADStudio { self.style_rename_commit(kind); Task::none() } + Message::StyleRenameFocus(focused) => { + let typed = self + .style_rename + .as_ref() + .is_some_and(|name| *name != self.style_rename_buf); + if typed && focused != Some(crate::ui::style::style_list::rename_input_id()) { + if let Some(kind) = self.style_rename_kind { + self.style_rename_commit(kind); + } + } + Task::none() + } Message::StyleRenameCancel => { self.style_rename_cancel(); Task::none() @@ -11182,14 +11544,8 @@ impl OpenCADStudio { Task::none() } - Message::TableStyleDialogNew => { - self.style_new(crate::app::StyleKind::Table); - Task::none() - } - Message::TableStyleDialogCopy => { - self.style_copy(crate::app::StyleKind::Table); - Task::none() - } + Message::TableStyleDialogNew => self.style_new(crate::app::StyleKind::Table), + Message::TableStyleDialogCopy => self.style_copy(crate::app::StyleKind::Table), Message::TableStyleDialogDelete => { self.style_delete(crate::app::StyleKind::Table); Task::none() @@ -11255,14 +11611,8 @@ impl OpenCADStudio { self.style_stage_commit(); Task::none() } - Message::MlStyleDialogNew => { - self.style_new(crate::app::StyleKind::MLine); - Task::none() - } - Message::MlStyleDialogCopy => { - self.style_copy(crate::app::StyleKind::MLine); - Task::none() - } + Message::MlStyleDialogNew => self.style_new(crate::app::StyleKind::MLine), + Message::MlStyleDialogCopy => self.style_copy(crate::app::StyleKind::MLine), Message::MlStyleDialogDelete => { self.style_delete(crate::app::StyleKind::MLine); Task::none() @@ -11360,14 +11710,8 @@ impl OpenCADStudio { Task::none() } Message::MLeaderStyleDialogSetCurrent => self.on_mleader_style_dialog_set_current(), - Message::MLeaderStyleDialogNew => { - self.style_new(crate::app::StyleKind::MLeader); - Task::none() - } - Message::MLeaderStyleDialogCopy => { - self.style_copy(crate::app::StyleKind::MLeader); - Task::none() - } + Message::MLeaderStyleDialogNew => self.style_new(crate::app::StyleKind::MLeader), + Message::MLeaderStyleDialogCopy => self.style_copy(crate::app::StyleKind::MLeader), Message::MLeaderStyleDialogDelete => { self.style_delete(crate::app::StyleKind::MLeader); Task::none() @@ -11442,14 +11786,8 @@ impl OpenCADStudio { self.dimstyle_compare = name; Task::none() } - Message::DimStyleDialogNew => { - self.style_new(crate::app::StyleKind::Dim); - Task::none() - } - Message::DimStyleDialogCopy => { - self.style_copy(crate::app::StyleKind::Dim); - Task::none() - } + Message::DimStyleDialogNew => self.style_new(crate::app::StyleKind::Dim), + Message::DimStyleDialogCopy => self.style_copy(crate::app::StyleKind::Dim), Message::DimStyleDialogSetCurrent => { // Staged: persists on Apply. let i = self.active_tab; @@ -12390,3 +12728,24 @@ fn set_preview_bit(current: u8, bit: u8, enabled: bool) -> u8 { current & !bit } } + +/// Web autosave deadline (SAVETIME minutes), polled from a short tick: the +/// first call arms it, a later one past it re-arms it and answers true. +#[cfg(target_arch = "wasm32")] +fn web_autosave_due(savetime_min: i32) -> bool { + use std::cell::Cell; + thread_local!(static DUE_MS: Cell = const { Cell::new(0.0) }); + let now = js_sys::Date::now(); + let period = savetime_min.max(0) as f64 * 60_000.0; + DUE_MS.with(|due| { + if period == 0.0 || due.get() == 0.0 { + due.set(if period == 0.0 { 0.0 } else { now + period }); + return false; + } + let fire = now >= due.get(); + if fire { + due.set(now + period); + } + fire + }) +} diff --git a/src/app/update/qselect.rs b/src/app/update/qselect.rs new file mode 100644 index 000000000..f2805f55c --- /dev/null +++ b/src/app/update/qselect.rs @@ -0,0 +1,253 @@ +//! Read-only object sampling for the selection-filter dialog. + +use crate::app::{OpenCADStudio, QSelectCondition, QSelectState}; +use crate::command::{CadCommand, CmdResult}; +use codec::Handle; +use glam::DVec3; +use iced::Task; + +/// The command owns the suspended dialog, so Escape can restore the exact +/// draft, committed conditions, scope and result mode without global pick state. +struct QSelectSampleCommand { + state: Option>, + tab_id: u64, +} + +impl QSelectSampleCommand { + fn finish(&mut self, handle: Option) -> CmdResult { + match self.state.take() { + Some(state) => CmdResult::QSelectSample { + state, + tab_id: self.tab_id, + handle, + }, + None => CmdResult::Cancel, + } + } +} + +impl CadCommand for QSelectSampleCommand { + fn name(&self) -> &'static str { + "FILTER" + } + + fn prompt(&self) -> String { + format!("FILTER {}", crate::t!("Select object:")) + } + + fn needs_entity_pick(&self) -> bool { + true + } + fn entity_pick_includes_fills(&self) -> bool { + true + } + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult { + if handle.is_null() { + CmdResult::NeedPoint + } else { + self.finish(Some(handle)) + } + } + + fn on_point(&mut self, _point: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + fn on_enter(&mut self) -> CmdResult { + self.finish(None) + } + fn on_escape(&mut self) -> CmdResult { + self.finish(None) + } +} + +fn append_unique_conditions(target: &mut Vec, incoming: Vec) { + for condition in incoming { + let duplicate = target.iter().any(|existing| { + existing.type_filter == condition.type_filter + && existing.property == condition.property + && existing.operator == condition.operator + && existing.value.eq_ignore_ascii_case(&condition.value) + }); + if !duplicate { + target.push(condition); + } + } +} + +impl OpenCADStudio { + pub(super) fn on_qselect_pick_object(&mut self) -> Task { + // Finish or cancel a condition edit first. Sampling only appends rows. + if self + .qselect + .as_ref() + .is_none_or(|state| state.editing_condition.is_some()) + { + return Task::none(); + } + let state = self.qselect.take().expect("checked open filter"); + // Keep a recoverable copy if another command or a document switch + // interrupts picking instead of ending it through Enter/Escape. + self.qselect_settings = Some((&state).into()); + let i = self.active_tab; + let command = QSelectSampleCommand { + state: Some(Box::new(state)), + tab_id: self.tabs[i].id, + }; + self.active_modal = None; + self.command_line.input.clear(); + self.command_line.set_step_options(Vec::new()); + self.command_line.push_info(&command.prompt()); + self.tabs[i].scene.selection.borrow_mut().menu.open_at = None; + self.tabs[i].active_cmd = Some(Box::new(command)); + Task::none() + } + + pub(in crate::app) fn finish_qselect_sample( + &mut self, + mut state: QSelectState, + tab_id: u64, + handle: Option, + ) { + let i = self.active_tab; + // A handle is only meaningful in its original document. + if self.tabs[i].id != tab_id { + return; + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + if let Some(handle) = handle { + let conditions = self.tabs[i].scene.qselect_conditions_from_entity(handle); + append_unique_conditions(&mut state.conditions, conditions); + state.error = None; + } + self.qselect = Some(state); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app::{QSelectMode, QSelectOp, QSelectScope}; + + fn state() -> QSelectState { + QSelectState { + conditions: Vec::new(), + editing_condition: None, + scope: QSelectScope::CurrentSelection, + available_types: Vec::new(), + available_properties: Vec::new(), + candidate_count: 2, + type_filter: Some("Line".into()), + property: None, + operator: QSelectOp::Eq, + value: "unfinished draft".into(), + mode: QSelectMode::Exclude, + append: false, + error: None, + } + } + + #[test] + fn escape_restores_draft_and_scope_without_a_sample() { + let mut command = QSelectSampleCommand { + state: Some(Box::new(state())), + tab_id: 17, + }; + let CmdResult::QSelectSample { + state, + tab_id, + handle, + } = command.on_escape() + else { + panic!("Escape must restore the filter"); + }; + assert_eq!(tab_id, 17); + assert!(handle.is_none()); + assert_eq!(state.value, "unfinished draft"); + assert!(matches!(state.scope, QSelectScope::CurrentSelection)); + assert!(matches!(state.mode, QSelectMode::Exclude)); + } + + #[test] + fn empty_click_keeps_picker_active_and_enter_restores_dialog() { + let mut command = QSelectSampleCommand { + state: Some(Box::new(state())), + tab_id: 17, + }; + assert!(matches!( + command.on_entity_pick(Handle::NULL, DVec3::ZERO), + CmdResult::NeedPoint + )); + assert!(matches!( + command.on_enter(), + CmdResult::QSelectSample { handle: None, .. } + )); + } + + #[test] + fn repeated_sampling_preserves_existing_conditions_without_duplicates() { + let condition = state().draft_condition(); + let mut conditions = vec![condition.clone()]; + append_unique_conditions(&mut conditions, vec![condition.clone(), condition]); + assert_eq!(conditions.len(), 1); + assert_eq!(conditions[0].value, "unfinished draft"); + } + + #[test] + fn sample_round_trip_keeps_selection_and_dialog_geometry() { + use codec::entities::Line; + use codec::types::Vector3; + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + let i = app.active_tab; + let first = app.tabs[i] + .scene + .add_entity(codec::EntityType::Line(Line::from_points( + Vector3::ZERO, + Vector3::new(10.0, 0.0, 0.0), + ))); + let sample = app.tabs[i] + .scene + .add_entity(codec::EntityType::Line(Line::from_points( + Vector3::ZERO, + Vector3::new(20.0, 0.0, 0.0), + ))); + app.tabs[i].scene.select_entity(first, false); + let selection = app.tabs[i].scene.selected.clone(); + let dirty = app.tabs[i].dirty; + app.qselect = Some(state()); + app.modal_offset = iced::Vector::new(10.0, 20.0); + app.modal_resize = iced::Vector::new(30.0, 40.0); + + let _ = app.on_qselect_pick_object(); + assert!(app.qselect.is_none()); + let result = app.tabs[i] + .active_cmd + .as_mut() + .unwrap() + .on_entity_pick(sample, DVec3::ZERO); + let _ = app.apply_cmd_result(result); + let restored = app.qselect.as_ref().unwrap(); + assert!(!restored.conditions.is_empty()); + assert_eq!(restored.value, "unfinished draft"); + assert!(matches!(restored.scope, QSelectScope::CurrentSelection)); + assert_eq!(app.tabs[i].scene.selected, selection); + assert_eq!(app.tabs[i].dirty, dirty); + assert_eq!(app.modal_offset, iced::Vector::new(10.0, 20.0)); + assert_eq!(app.modal_resize, iced::Vector::new(30.0, 40.0)); + assert!(app.tabs[i].active_cmd.is_none()); + + let count = restored.conditions.len(); + let _ = app.on_qselect_pick_object(); + let result = app.tabs[i].active_cmd.as_mut().unwrap().on_escape(); + let _ = app.apply_cmd_result(result); + assert_eq!(app.qselect.as_ref().unwrap().conditions.len(), count); + assert_eq!(app.tabs[i].scene.selected, selection); + } +} diff --git a/src/app/update/style.rs b/src/app/update/style.rs index dfd15aff9..b1cd0da91 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -1190,6 +1190,18 @@ impl OpenCADStudio { crate::ui::window::gradient_editor::GradientMsg::Color(index, color), ))) } + Some(crate::app::ColorPickTarget::ViewBackground(slot)) => { + let rgb = match color { + codec::types::Color::Rgb { r, g, b } => Some([r, g, b]), + codec::types::Color::Index(index) => { + codec::types::aci_table::aci_to_rgb(index).map(|(r, g, b)| [r, g, b]) + } + _ => None, + }; + rgb.map(|rgb| { + Message::PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg::BgColor(slot, rgb)) + }) + } Some(crate::app::ColorPickTarget::GraphicAttributesLine) => Some( Message::GraphicAttributes(GraphicAttributesMsg::LineColor(color)), ), diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 343e3b37a..89cf11ae3 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -340,6 +340,10 @@ impl OpenCADStudio { base: glam::DVec3, allowed: bool, ) -> Option { + if let Some(direction) = self.angle_override.filter(|_| allowed) { + self.axis_lock_dir = Some(direction); + return self.axis_lock_dir; + } if self.shift_down && allowed { if self.axis_lock_dir.is_none() { let ucs = self.tabs[tab].ucs_xform(); @@ -1084,12 +1088,37 @@ impl OpenCADStudio { // viewport, the picker drives that viewport entity's own // render mode; the model-layout tab style is untouched. if self.tabs[i].scene.set_active_viewport_render_mode(mode) { + self.tabs[i] + .scene + .set_active_visual_style_named(crate::modules::view::visual_style::style_name_for(mode)); self.tabs[i].scene.bump_geometry_no_blocks(); self.tabs[i].dirty = true; self.command_line .push_output(crate::tf!("Viewport visual style: {label}").as_ref()); return Task::none(); } + self.apply_render_mode(i, mode, label); + // The viewport record carries the style the mode is saved as. + self.tabs[i] + .scene + .set_active_visual_style_named(crate::modules::view::visual_style::style_name_for(mode)); + self.command_line + .push_output(crate::tf!("Visual style: {label}").as_ref()); + // The point cloud move gizmo follows the visual style. + if i == self.active_tab { + self.refresh_selected_grips(); + } + Task::none() + } + + /// The model tab's visual style, without a message. + pub(crate) fn apply_render_mode( + &mut self, + i: usize, + mode: codec::entities::ViewportRenderMode, + label: &str, + ) { + use codec::entities::ViewportRenderMode as M; self.tabs[i].render_mode = mode; // Write the style onto the active Model tile alone so it // sticks when that tile loses focus and the other tiles keep @@ -1105,13 +1134,6 @@ impl OpenCADStudio { // pipeline keys its upload cache off `geometry_epoch`. self.tabs[i].scene.bump_geometry_no_blocks(); self.tabs[i].dirty = true; - self.command_line - .push_output(crate::tf!("Visual style: {label}").as_ref()); - // The point cloud move gizmo follows the visual style. - if i == self.active_tab { - self.refresh_selected_grips(); - } - Task::none() } pub(super) fn on_cursor_moved( @@ -1211,6 +1233,9 @@ impl OpenCADStudio { return Task::none(); } let i = self.active_tab; + if let Some(task) = self.start_drag_move(i, p) { + return Task::batch([task, self.on_viewport_move(p)]); + } self.constraint_glyph_tooltip = None; let constraint_hover = self .constraint_glyph_under(i, p) @@ -1507,14 +1532,36 @@ impl OpenCADStudio { // In a viewport, pick the cursor's model point with the // viewport camera directly; otherwise project on the paper // sheet and map to model. Either way `raw` is model space. + // + // A model-space grip moves in the drawing plane through where it + // started (the UCS XY plane at the grip's own elevation). The + // plane facing the screen through the orbit target put the point + // somewhere else in depth in any tilted view, so a circle jumped + // to another radius and a vertex left its plane (#1033). Seen + // edge-on that plane cannot be hit; the screen plane takes over. + let grip_plane = self.tabs[i].editing_model_space().then(|| { + let normal = self.tabs[i] + .active_ucs + .as_ref() + .map_or(glam::DVec3::Z, ucs_z_axis); + (normal, grip.origin_world) + }); + let pick = |cam: &crate::scene::view::camera::Camera| match grip_plane { + Some((normal, origin)) + if (cam.rotation * glam::Vec3::NEG_Z) + .as_dvec3() + .dot(normal) + .abs() + > 0.05 => + { + cam.pick_on_plane(p, bounds, normal.as_vec3(), origin) + } + _ => cam.pick_on_target_plane(p, bounds), + }; let raw = match &edit_cam { - Some(cam) => cam.pick_on_target_plane(p, bounds), + Some(cam) => pick(cam), None => { - let paper = self.tabs[i] - .scene - .camera - .borrow() - .pick_on_target_plane(p, bounds); + let paper = pick(&self.tabs[i].scene.camera.borrow()); self.tabs[i].scene.paper_to_model(paper) } }; @@ -1629,7 +1676,7 @@ impl OpenCADStudio { } else { self.tabs[i].scene.hit_test_wires() }; - let snap_candidates = self.tabs[i].scene.interaction_candidates_near( + let mut snap_candidates = self.tabs[i].scene.interaction_candidates_near( all_wires, raw, view_rot, @@ -1637,6 +1684,16 @@ impl OpenCADStudio { bounds, self.snapper.osnap_radius_px, ); + // A solid stays drawn while its grip moves, so its own edges are + // still in the set: the grip would snap back onto the shape it is + // reshaping (a cone's base radius landed on its own side, #1033). + let edited = &self.grip_preview_handles; + if !edited.is_empty() { + snap_candidates.exclude_wires(|wire| { + crate::scene::Scene::handle_from_wire_name(&wire.name) + .is_some_and(|handle| edited.contains(&handle)) + }); + } // The engaged grip is the rubber-band origin. Perpendicular // snapping must drop its foot from this point, including when a // hot-grip set is moved by the same drag vector. @@ -2606,7 +2663,15 @@ impl OpenCADStudio { .map(|snap| snap.world) .unwrap_or(cursor_world); axis_lock_apply(point, base, dir) - } else if let Some(h) = otrack_hit { + } else if let Some(h) = otrack_hit.filter(|_| { + // A real object snap must win over tracking alignment. + // Otherwise the endpoint glyph can appear at the + // corner while the preview/accepted point follows the + // tracking line instead. + !self.tabs[i].snap_result.is_some_and(|s| { + s.snap_type != crate::snap::SnapType::Grid + }) + }) { h.aligned } else { // Snap runs in model space (viewport camera or the @@ -3526,6 +3591,68 @@ impl OpenCADStudio { Some((constraint.kind, constraint.refs.clone())) } + /// Whether a selected object lies under `p` (tile-local). + fn selected_object_under( + &self, + i: usize, + p: Point, + bounds: iced::Rectangle, + edit_cam: &Option, + ) -> bool { + let (view_rot, eye, wires) = self.pick_view(i, edit_cam, bounds); + let aperture = crate::ui::overlay::pick_box_aperture_px(self.pick_box); + let world = self.cursor_model_point(i, edit_cam, p, bounds); + let scene = &self.tabs[i].scene; + let candidates = scene + .interaction_hover_candidates_near(wires, world, view_rot, eye, bounds, aperture * 2.0); + scene::pick::hit_test::click_hit( + p, + &candidates, + view_rot, + eye, + bounds, + scene.document.header.lineweight_display, + aperture, + &scene.draw_depth_map(), + ) + .and_then(Scene::handle_from_wire_name) + .is_some_and(|handle| scene.selected.contains(&handle)) + } + + /// Once a press on a selected object has moved far enough, MOVE (COPY + /// with Ctrl) runs on the selection from the grabbed point, so the drop + /// gets the command's preview, snaps and ortho. + fn start_drag_move(&mut self, i: usize, p: Point) -> Option> { + let (base, false) = self.drag_move? else { + return None; + }; + { + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + let press = sel.input.left_press_pos?; + let held_ms = sel + .input + .left_press_time + .map_or(u128::MAX, |time| time.elapsed().as_millis()); + let (dx, dy) = (p.x - press.x, p.y - press.y); + if !sel.input.left_down || held_ms < POLY_START_DELAY_MS || dx * dx + dy * dy <= 9.0 { + return None; + } + sel.input.left_dragging = true; + sel.gesture.clear_left(); + } + self.drag_move = Some((base, true)); + let command = if self.ctrl_down { "COPY" } else { "MOVE" }; + let started = self.update(Message::Command(command.to_string())); + if self.tabs[i].active_cmd.is_none() { + self.drag_move = None; + return Some(started); + } + Some(Task::batch([ + started, + self.feed_command(crate::command::StepInput::Point(base)), + ])) + } + pub(super) fn on_viewport_left_press(&mut self) -> Task { let i = self.active_tab; self.block_palette.pressed = None; @@ -3895,6 +4022,39 @@ impl OpenCADStudio { } } + // Ctrl+click on a polyline picks the segment under the cursor; the + // edit commands then move just that part. + if self.ctrl_down && self.tabs[i].active_cmd.is_none() { + let (view_rot, eye) = { + let cam = self.tabs[i].scene.camera.borrow(); + (cam.view_proj_rte(bounds), cam.eye()) + }; + let wires = self.tabs[i].scene.hit_test_wires(); + let hit = scene::pick::hit_test::click_hit( + p, + &*wires, + view_rot, + eye, + bounds, + self.tabs[i].scene.document.header.lineweight_display, + crate::ui::overlay::pick_box_aperture_px(self.pick_box), + &self.tabs[i].scene.draw_depth_map(), + ) + .and_then(Scene::handle_from_wire_name) + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); + drop(wires); + let point = self.tabs[i].last_cursor_world; + if hit.is_some_and(|handle| self.tabs[i].scene.toggle_sub_segment(handle, point)) { + self.tabs[i] + .scene + .selection + .borrow_mut() + .clear_left_selection_gesture(); + self.refresh_properties(); + return Task::none(); + } + } + // Ctrl+click on a composite solid picks the solid of it under the // cursor โ€” the one the Properties palette and grips then edit. if self.ctrl_down @@ -3939,6 +4099,12 @@ impl OpenCADStudio { .gesture .box_anchor .is_none(); + // A press on a selected object may become a drag of the selection. + self.drag_move = (self.tabs[i].active_cmd.is_none() + && !self.shift_down + && !self.tabs[i].scene.selected.is_empty() + && self.selected_object_under(i, p, bounds, &edit_cam)) + .then_some((self.tabs[i].last_cursor_world, false)); let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.input.left_down = true; // Stored in full-canvas space (like ViewportMove's cursor and @@ -3962,6 +4128,26 @@ impl OpenCADStudio { if let Some(item) = self.take_block_drop() { return self.drop_block(item); } + // A dragged selection drops where the button comes up: the release + // is the MOVE / COPY destination click, and a COPY places one copy. + if let Some((_, true)) = self.drag_move.take() { + { + // Starting the command reset the press; the drop reads as a + // click at the cursor. + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.input.left_down = true; + sel.input.left_dragging = false; + } + let placed = self.on_viewport_left_release(); + let copying = self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|command| command.name() == "COPY"); + if !copying { + return placed; + } + return Task::batch([placed, self.feed_command(crate::command::StepInput::Enter)]); + } if let Some(command) = self.tabs[i].active_cmd.as_mut() { command.set_ctrl(self.ctrl_down); command.set_shift(self.shift_down); @@ -4320,7 +4506,11 @@ impl OpenCADStudio { } else { None }; - if let Some(h) = otrack { + if let Some(h) = otrack.filter(|_| { + // Match the hover path: never override a genuine OSNAP + // endpoint/intersection with a tracking projection. + !snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid) + }) { pt = h.aligned; if clamp_world_xy { pt.z = 0.0; @@ -5765,6 +5955,80 @@ properties={:.1}ms picked={}", /// Pinch the active view to zoom, as reported by the trackpad monitor /// (`src/input/trackpad.rs`). The pivot is the cursor, like the wheel, and /// the direction is the fingers' own, so ZOOMWHEEL does not apply to it. + /// A touch screen finger event: multi-finger gestures pan, zoom and + /// orbit the view (see `input::touch`). + pub(super) fn on_touch(&mut self, event: iced::touch::Event) -> Task { + use crate::input::touch::TouchAction; + let i = self.active_tab; + if self.tabs[i].is_start { + return Task::none(); + } + // A lone finger drives the cursor, so where it is in the window and + // in the drawing area tells the drawing area's offset. + if let iced::touch::Event::FingerMoved { position, .. } = event { + let cursor = self.tabs[i].scene.selection.borrow().input.last_move_pos; + if let Some(cursor) = cursor.filter(|_| self.touch.fingers() == 1) { + self.touch_offset = Some(position - cursor); + } + } + let mut tasks = Vec::new(); + for action in self.touch.event(event) { + match action { + TouchAction::Cancel => { + self.tabs[i] + .scene + .selection + .borrow_mut() + .clear_left_selection_gesture(); + self.drag_move = None; + self.tabs[i].scene.remember_current_view(); + self.clear_navigation_hover(i); + } + TouchAction::Pan(delta) => { + self.pan_active_view(i, delta.x, delta.y, Instant::now()); + } + TouchAction::Zoom { factor, centre } => { + // Zoom about the fingers once the drawing area's place in + // the window is known; until then about the first finger. + if let Some(offset) = self.touch_offset { + self.tabs[i].scene.selection.borrow_mut().input.last_move_pos = + Some(centre - offset); + } + tasks.push(self.zoom_view_at_cursor(pinch_zoom_steps(factor - 1.0))); + } + TouchAction::Orbit(delta) => { + if self.tabs[i].scene.active_viewport.is_some() { + self.tabs[i].scene.orbit_active_viewport(delta.x, delta.y); + } else if self.tabs[i].scene.current_layout == "Model" { + let scene = &self.tabs[i].scene; + let (vw, vh) = scene.selection.borrow().view.vp_size; + let bounds = scene.active_model_tile_bounds(vw, vh); + let pivot = scene + .orbit_pivot() + .or_else(|| scene.view_center_surface_pivot(bounds)) + .unwrap_or_else(|| scene.camera.borrow().target); + self.tabs[i].scene.refresh_projection_bounds(); + self.tabs[i] + .scene + .camera + .borrow_mut() + .orbit(delta.x, delta.y, Some(pivot)); + } + self.tabs[i].scene.camera_generation += 1; + } + TouchAction::Tap(2) => { + tasks.push(self.update(Message::ViewportRightPress)); + tasks.push(self.update(Message::ViewportRightRelease)); + } + TouchAction::Tap(_) => { + tasks.push(Task::done(Message::Command("ZOOM E".to_string()))); + } + TouchAction::SwipeUp => tasks.push(self.apply_view_preset("SEISO")), + } + } + Task::batch(tasks) + } + pub(super) fn on_pinch_zoom(&mut self, magnification: f32) -> Task { if magnification == 0.0 { return Task::none(); @@ -6301,6 +6565,12 @@ properties={:.1}ms picked={}", } pub(crate) fn on_layout_switch(&mut self, name: String) -> Task { + if self.tabs[self.active_tab].active_block_edit.is_some() { + return self.request_block_edit_close( + self.active_tab, + crate::app::block_editor::BlockEditContinuation::Layout(name), + ); + } self.on_layout_switch_inner(name, false) } @@ -6363,6 +6633,12 @@ properties={:.1}ms picked={}", /// define a view and then returns to its paper layout. This is the only /// layout transition allowed to preserve an active command. pub(crate) fn on_layout_switch_preserving_command(&mut self, name: String) -> Task { + if self.tabs[self.active_tab].active_block_edit.is_some() { + return self.request_block_edit_close( + self.active_tab, + crate::app::block_editor::BlockEditContinuation::Layout(name), + ); + } self.on_layout_switch_inner(name, true) } @@ -6391,10 +6667,10 @@ properties={:.1}ms picked={}", && self.tabs[i] .active_cmd .as_ref() - .is_some_and(|command| command.name() == "MVIEW"); + .is_some_and(|command| matches!(command.name(), "MVIEW" | "MVSETUP")); if context_changed { if preserve_active_command { - // MVIEW keeps its command-owned step data, but all host-owned + // MVIEW and MVSETUP keep their step data, but all host-owned // cursor/snap/dynamic-input state belongs to the old space. self.reset_space_interaction_state(); } @@ -6520,6 +6796,7 @@ properties={:.1}ms picked={}", .as_ref() .map(|style| style.name.clone()) .unwrap_or_default(); + self.tabs[i].scene.new_layout_plot_style = plot_style.clone(); for obj in self.tabs[i].scene.document.objects.values_mut() { if let codec::objects::ObjectType::Layout(l) = obj { if l.name == new_name { @@ -6534,10 +6811,17 @@ properties={:.1}ms picked={}", self.tabs[i].scene.ensure_sheet_viewport(&new_name); let switch_task = self.on_layout_switch(new_name.clone()); self.tabs[i].scene.fit_all(); - self.command_line.push_output( - crate::tf!("Layout \"{new_name}\" created โ€” use MVIEW to add a viewport") - .as_ref(), - ); + // Opened, it already has its first viewport unless + // LAYOUTCREATEVIEWPORT is off. + if crate::scene::layout_create_viewport() { + let name = new_name.clone(); + self.command_line.push_output(crate::tf!("Layout '{name}' created.").as_ref()); + } else { + self.command_line.push_output( + crate::tf!("Layout \"{new_name}\" created โ€” use MVIEW to add a viewport") + .as_ref(), + ); + } // The option that greets every new layout with its page setup. let switch_task = if self.plot_dialog.page_setup_on_new_layout { Task::batch([switch_task, self.on_plot_dialog_open()]) diff --git a/src/app/view/controls.rs b/src/app/view/controls.rs index 200a1ffc3..31343993f 100644 --- a/src/app/view/controls.rs +++ b/src/app/view/controls.rs @@ -30,6 +30,7 @@ pub(super) fn viewport_controls<'a>( tile_count: usize, render_mode_menu_open: bool, render_mode_preview: Option, + style_name: Option, ) -> Element<'a, Message> { let render_modes: Vec = crate::modules::view::visual_style::VISUAL_STYLES .iter() @@ -98,7 +99,7 @@ pub(super) fn viewport_controls<'a>( // flyout keeps the current style unchanged while hovering; a click commits. let picker_head = button( row![ - text(RenderModeChoice(render_mode).to_string()).size(11), + text(style_name.unwrap_or_else(|| RenderModeChoice(render_mode).to_string())).size(11), crate::ui::icons::themed_arrow_toggle(render_mode_menu_open, 9.0), ] .spacing(6) @@ -379,7 +380,7 @@ mod tests { #[test] fn test_viewport_controls_construction() { for style in crate::modules::view::visual_style::VISUAL_STYLES { - let _el = viewport_controls(style.mode, true, true, true, 1, false, None); + let _el = viewport_controls(style.mode, true, true, true, 1, false, None, None); let _el_open = viewport_controls( style.mode, false, @@ -388,6 +389,7 @@ mod tests { 2, true, Some(M::Wireframe3D), + None, ); } } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 8b41da05c..6473c1c65 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -15,6 +15,46 @@ use iced::widget::{ use iced::window; use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme}; use iced_aw::ContextMenu; + +/// Invisible event surface for accurate point picking. MouseArea emits a +/// positionless press message; capturing the cursor in the actual button +/// event avoids using the previous hover location when clicking quickly. +#[derive(Debug, Clone, Copy)] +struct ViewportPickSurface; + +impl iced::widget::canvas::Program for ViewportPickSurface { + type State = (); + + fn update( + &self, + _state: &mut Self::State, + event: &iced::Event, + bounds: iced::Rectangle, + cursor: iced::mouse::Cursor, + ) -> Option> { + if let iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)) = + event + { + return cursor.position_in(bounds).map(|position| { + iced::widget::canvas::Action::publish(Message::ViewportLeftPressAt(position)) + .and_capture() + }); + } + None + } + + fn draw( + &self, + _state: &Self::State, + _renderer: &iced::Renderer, + _theme: &iced::Theme, + _bounds: iced::Rectangle, + _cursor: iced::mouse::Cursor, + ) -> Vec { + Vec::new() + } +} + use std::sync::Arc; mod controls; @@ -182,6 +222,24 @@ impl std::fmt::Display for RenderModeChoice { } } +/// A named style (Realistic, โ€ฆ) the active viewport draws as, by name. +fn named_visual_style( + scene: &crate::scene::Scene, + mode: codec::entities::ViewportRenderMode, +) -> Option { + match scene.document.objects.get(&scene.active_visual_style()) { + Some(codec::objects::ObjectType::VisualStyle(style)) => { + match crate::modules::view::visual_style::mode_for_style_name(&style.description) { + Some((named_mode, true)) if named_mode == mode => { + Some(crate::t!(style.description.as_str()).into_owned()) + } + _ => None, + } + } + _ => None, + } +} + impl OpenCADStudio { #[cfg(not(target_arch = "wasm32"))] pub fn view(&self, window_id: window::Id) -> Element<'_, Message> { @@ -321,6 +379,7 @@ impl OpenCADStudio { &self.recent_thumbs, self.recent_limit, &self.recent_limit_input, + self.recent_view, self.start_action_w.clone(), self.start_section, ) @@ -857,10 +916,16 @@ bg={bg_ms:.1}ms n={view_count}" .hover_highlight .map(|h| tab.scene.is_layer_locked(h)) .unwrap_or(false); - let point_cursor = tab - .active_cmd - .as_ref() - .is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering()); + // Keep the hollow pickbox at rest. Use the standard LINE/RECTANG + // dot crosshair only after the first window/crossing corner. + // No OSNAP or selection behavior changes. + let selecting_window = tab.active_cmd.is_none() + && tab.scene.selection.borrow().gesture.box_anchor.is_some(); + let point_cursor = selecting_window + || tab + .active_cmd + .as_ref() + .is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering()); let constraint_cursor_badge = if is_paper { None } else { @@ -939,7 +1004,13 @@ bg={bg_ms:.1}ms n={view_count}" || c.is_selection_gathering() }), snap_color: self.snap_marker_color, + snap_size: self.model_space.snap_marker_size as f32, }, + // MATCHPROP paints properties onto destinations; it gets the + // paintbrush cursor instead of the crosshair. + tab.active_cmd + .as_ref() + .is_some_and(|c| c.name() == "MATCHPROP"), crate::ui::overlay::SelectionVisualOptions { area: self.model_space.selection_area, opacity: self.model_space.selection_opacity, @@ -961,9 +1032,15 @@ bg={bg_ms:.1}ms n={view_count}" mark("viewport_mouse"); let viewport_mouse = mouse_area(container( - iced::widget::Space::new().width(Fill).height(Fill), + iced::widget::canvas(ViewportPickSurface) + .width(Fill) + .height(Fill), )) .on_move(Message::ViewportMove) + // Left mouse presses are captured by the canvas with their exact + // event-time position. MouseArea::on_press has no coordinate, so + // queued cursor moves could otherwise make quick clicks use a stale + // snap position. Touch presses still use this fallback. .on_press(Message::ViewportLeftPress) .on_release(Message::ViewportLeftRelease) .on_right_press(Message::ViewportRightPress) @@ -1011,6 +1088,20 @@ bg={bg_ms:.1}ms n={view_count}" { let w = tab.last_cursor_world; let base = tab.dyn_anchor.or(self.last_point); + // A single distance field on an acquired tracking ray is measured + // from the acquired OSNAP/OTRACK point, not from the command's + // previous point (or from WCS zero before the first pick). + // Keep typed fields and ordinary polar distance inputs unchanged. + let tracking_distance = if tab.dyn_fields.len() == 1 + && tab.dyn_fields[0].component == DynComponent::Distance + { + self.active_distance_ray(self.active_tab).and_then(|(origin, direction)| { + let axis = direction.try_normalize()?; + Some((w - origin).dot(axis).abs()) + }) + } else { + None + }; let label_screen = tab .active_cmd .as_ref() @@ -1160,6 +1251,16 @@ bg={bg_ms:.1}ms n={view_count}" format!("{lv:.4}") } } + // The displayed live distance must use the same + // acquired tracking origin as the guide and typed + // distance placement, not the last picked point. + (None, _) if f.component == DynComponent::Distance + && tracking_distance.is_some() => + { + crate::entities::common::format_length( + tracking_distance.unwrap() * f.role.value_scale() as f64, + ) + } _ => dyn_component_value( f, w, @@ -1297,6 +1398,7 @@ bg={bg_ms:.1}ms n={view_count}" let rect = tab.scene.active_model_tile_bounds(vw, vh); // Unified control chip: split buttons + render-mode picker + // grid / grid-snap toggles, for the active Model tile. + let style_name = named_visual_style(&tab.scene, tab.render_mode); let bar = viewport_controls( tab.render_mode, self.show_grid, @@ -1305,6 +1407,7 @@ bg={bg_ms:.1}ms n={view_count}" tab.scene.model_tiles.borrow().len(), self.render_mode_menu_open, self.render_mode_preview, + style_name, ); // Adaptive: DensitySwap measures the bar's real width every frame // (reported into `render_bar_w`, which the ViewCube reads to decide @@ -1397,6 +1500,7 @@ bg={bg_ms:.1}ms n={view_count}" 0, self.render_mode_menu_open, self.render_mode_preview, + named_visual_style(&tab.scene, vp_mode), ); let adaptive: Element<'_, Message> = DensitySwap::new(vec![ iced::widget::opaque(bar), @@ -1660,9 +1764,18 @@ bg={bg_ms:.1}ms n={view_count}" // In-place block edit (REFEDIT): right-edge toolbar with Save / Discard // so the edit can be finished by clicking. (#136) - if tab.refedit_session.is_some() && !tab.is_start { - if let Some(tb) = crate::ui::side_toolbar::view( - &crate::modules::draw::modify::refedit::refedit_tools(), + if let (Some(session), false) = (tab.refedit_session.as_ref(), tab.is_start) { + use crate::modules::draw::modify::refedit::{refedit_set_tools, refedit_tools}; + let set_tools = refedit_set_tools(); + let close_tools = refedit_tools(); + let header = crate::ui::side_toolbar::refedit_header( + &session.block_name, + refedit_working_set_count(session, &tab.scene), + ); + if let Some(tb) = crate::ui::side_toolbar::view_sections( + Some(header), + &[&set_tools, &close_tools], + &|_| false, ) { viewport_stack = viewport_stack.push(tb); } @@ -1999,7 +2112,10 @@ bg={bg_ms:.1}ms n={view_count}" // Clean-screen mode drops the ribbon for a full-canvas view; the // status bar stays so the mode can be toggled back off. let mut col = column![]; - if !self.clean_screen { + if !self.clean_screen && self.show_menu_bar { + col = col.push(crate::ui::menu_bar::view()); + } + if !self.clean_screen && !self.ribbon_closed { col = col.push(self.ribbon.view( is_paper, self.tabs[self.active_tab].is_start, @@ -2222,12 +2338,23 @@ bg={bg_ms:.1}ms n={view_count}" iced::widget::Space::new().width(0).height(0).into() }; + // Key tips: the hint bar under the dropdowns, the codes above all. + let (keytip_bar, keytip_layer): (Element<'_, Message>, Element<'_, Message>) = + self.ribbon.keytip_layers().unwrap_or_else(|| { + ( + iced::widget::Space::new().width(0).height(0).into(), + iced::widget::Space::new().width(0).height(0).into(), + ) + }); + let composed = stack![ main_ui, + keytip_bar, dropdown_layer, qselect_layer, snap_override_layer, open_progress_layer, + keytip_layer, ]; // If the Plot Style editor was launched from PLOT, keep the Plot dialog @@ -2440,8 +2567,10 @@ impl OpenCADStudio { let web_fonts = Subscription::none(); // Periodic autosave (SAVETIME): a `.sv$` recovery copy beside the file // on native, an OPFS snapshot on web. SAVETIME is the interval in - // minutes; 0 disables it. - let autosave = if self.savetime_min > 0 { + // minutes; 0 disables it. The web build checks the deadline from the + // `PollWebFonts` tick instead: a long `time::every` there stalls every + // other timer for its whole period (#1775). + let autosave = if self.savetime_min > 0 && cfg!(not(target_arch = "wasm32")) { iced::time::every(std::time::Duration::from_secs( self.savetime_min as u64 * 60, )) @@ -2495,9 +2624,18 @@ impl OpenCADStudio { event::listen_with(|ev, status, win_id| { use iced::event::Status; match ev { + iced::Event::Mouse(iced::mouse::Event::ButtonPressed(_)) + if crate::ui::ribbon::keytips_active() => + { + crate::ui::ribbon::keytip_pointer_pressed(); + Some(Message::KeyTip(crate::ui::ribbon::KeyTipInput::Hide)) + } iced::Event::Mouse(iced::mouse::Event::ButtonPressed( iced::mouse::Button::Left, - )) => Some(Message::PropPointerPressed), + )) => { + crate::ui::ribbon::keytip_pointer_pressed(); + Some(Message::PropPointerPressed) + } iced::Event::Window(window::Event::CloseRequested) => { Some(Message::WindowCloseRequested(win_id)) } @@ -2510,6 +2648,9 @@ impl OpenCADStudio { iced::Event::Window(window::Event::FileDropped(path)) => { Some(Message::FileDropped(path)) } + iced::Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { + crate::ui::ribbon::keytip_key_released(&key) + } iced::Event::Keyboard(keyboard::Event::ModifiersChanged(m)) => { // `command()` is Cmd on macOS, Ctrl elsewhere โ€” the // platform multi-select modifier for the layer list. @@ -2523,8 +2664,14 @@ impl OpenCADStudio { physical_key, modifiers, text, + repeat, .. }) => { + if let Some(keytip) = + crate::ui::ribbon::keytip_key_pressed(&key, modifiers, repeat) + { + return keytip; + } #[cfg(target_arch = "wasm32")] let accel = modifiers.command(); // AltGr reaches Windows apps as Ctrl+Alt, and a macOS @@ -2670,6 +2817,7 @@ impl OpenCADStudio { .map(|_| Message::SpaceMouseWake), crate::input::trackpad::subscription().map(Message::TrackpadPinch), event::listen_with(|event, _, id| match event { + iced::Event::Touch(touch) => Some(Message::Touch(touch)), iced::Event::Window(window::Event::Focused) => { Some(Message::SpaceMouseFocus(id, true)) } @@ -3052,6 +3200,7 @@ pub(super) fn start_page_view<'a>( thumbs: &'a std::collections::HashMap>, recent_limit: usize, recent_limit_input: &'a str, + recent_view: super::RecentView, action_width_out: std::sync::Arc, active: super::StartSection, ) -> Element<'a, Message> { @@ -3067,6 +3216,7 @@ pub(super) fn start_page_view<'a>( thumbs, recent_limit, recent_limit_input, + recent_view, size.width, action_width_out.clone(), active, @@ -3086,6 +3236,7 @@ fn start_page_content<'a>( thumbs: &'a std::collections::HashMap>, recent_limit: usize, recent_limit_input: &'a str, + recent_view: super::RecentView, avail_w: f32, action_width_out: std::sync::Arc, active: super::StartSection, @@ -3309,6 +3460,7 @@ fn start_page_content<'a>( thumbs, recent_limit, recent_limit_input, + recent_view, match start_layout { StartLayout::AllPanels | StartLayout::WithoutVideos @@ -3746,16 +3898,149 @@ fn start_page_content<'a>( // Properties panel normally occupies, but only when the active tab is the // Start tab. The list is restored from disk at boot and re-saved on every // open โ€” entries persist across sessions. +/// Recent files as tiles: small thumbnails wrapping across the panel, or one +/// large preview per row. A tile opens its file; the list view keeps the +/// remove buttons. +fn recent_tiles<'a>( + recents: &'a [std::path::PathBuf], + thumbs: &'a std::collections::HashMap>, + large: bool, +) -> iced::widget::Column<'a, Message> { + let (thumb_w, thumb_h, name_chars) = if large { + (Fill, iced::Length::Fixed(180.0), 40) + } else { + (iced::Length::Fixed(72.0), iced::Length::Fixed(54.0), 11) + }; + let tile = |path: &'a std::path::PathBuf| -> Element<'a, Message> { + let name = path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.to_string_lossy().into_owned()); + let picture: Element<'a, Message> = match thumbs.get(path).and_then(|o| o.as_ref()) { + Some(h) => iced::widget::image(h.clone()) + .content_fit(iced::ContentFit::Contain) + .width(Fill) + .height(Fill) + .into(), + None => crate::ui::icons::themed_secondary(crate::ui::icons::DOC, 28.0), + }; + let picture = container(picture) + .width(thumb_w) + .height(thumb_h) + .center_x(thumb_w) + .center_y(thumb_h) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.base.color)), + border: Border { + color: theme.palette().background.neutral.color, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }); + let mut label = column![text(crate::ui::text_util::elide(&name, name_chars)).size(12)]; + if large { + let dir = path + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default(); + label = label.push( + text(crate::ui::text_util::elide(&dir, 52)) + .size(10) + .style(start_muted_style), + ); + } + button(column![picture, label.spacing(2)].spacing(4)) + .on_press(Message::OpenRecent(path.clone())) + .padding(6) + .width(if large { Fill } else { iced::Length::Shrink }) + .style(|theme: &Theme, status| { + let palette = theme.palette(); + button::Style { + background: matches!(status, button::Status::Hovered) + .then_some(Background::Color(palette.background.strong.color)), + text_color: palette.background.base.text, + border: Border { + radius: 4.0.into(), + ..Default::default() + }, + ..Default::default() + } + }) + .into() + }; + if large { + column(recents.iter().map(tile)).spacing(6) + } else { + column![iced::widget::Row::with_children(recents.iter().map(tile)) + .spacing(4) + .wrap()] + .width(Fill) + } +} + pub(super) fn recent_files_panel<'a>( recents: &'a [std::path::PathBuf], thumbs: &'a std::collections::HashMap>, limit: usize, limit_input: &'a str, + view: super::RecentView, width: iced::Length, ) -> Element<'a, Message> { + use super::RecentView; // Title mirrors the Supporters rail: size 15 in the bright text colour, - // followed by a 12px gap before the content. - let title = text(crate::tr!("start", "recent-documents")).size(15); + // followed by a 12px gap before the content. The view-mode toggles sit + // on the title's right. + let mode_button = |icon: &'static [u8], mode: RecentView, tip: String| { + let active = view == mode; + iced::widget::tooltip( + button(crate::ui::icons::themed(icon, 13.0)) + .on_press(Message::SetRecentView(mode)) + .padding([3, 5]) + .style(move |theme: &Theme, status| { + let palette = theme.palette(); + let background = if active { + Some(palette.primary.weak.color) + } else { + matches!(status, button::Status::Hovered) + .then_some(palette.background.strong.color) + }; + button::Style { + background: background.map(Background::Color), + text_color: palette.background.base.text, + border: Border { + radius: 3.0.into(), + ..Default::default() + }, + ..Default::default() + } + }), + container(text(tip).size(11)).padding([2, 6]).style(container::rounded_box), + iced::widget::tooltip::Position::Bottom, + ) + }; + let title = row![ + text(crate::tr!("start", "recent-documents")) + .size(15) + .width(Fill), + mode_button( + crate::ui::icons::MENU, + RecentView::List, + crate::tr!("start", "view-list"), + ), + mode_button( + crate::ui::icons::GRID, + RecentView::Icons, + crate::tr!("start", "view-icons"), + ), + mode_button( + crate::ui::icons::PREVIEW, + RecentView::Preview, + crate::tr!("start", "view-preview"), + ), + ] + .spacing(2) + .align_y(iced::Center); let body: Element<'a, Message> = if recents.is_empty() { container( @@ -3765,6 +4050,18 @@ pub(super) fn recent_files_panel<'a>( ) .height(Fill) .into() + } else if view != RecentView::List { + iced::widget::scrollable( + recent_tiles(recents, thumbs, view == RecentView::Preview).padding( + iced::Padding { + right: 12.0, + ..iced::Padding::ZERO + }, + ), + ) + .width(Fill) + .height(Fill) + .into() } else { // Right padding reserves a gutter for the scrollbar so it doesn't sit on // top of the row's โœ• remove button. @@ -3974,9 +4271,29 @@ mod tests { &std::collections::HashMap::new(), 50, "18446744073709551615", + super::super::RecentView::default(), 800.0, std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)), super::super::StartSection::default(), ); } } + +/// The objects in a REFEDIT session's working set: the checked-out copies and +/// objects added with REFSET still in it, and everything created in the +/// space since. +// ponytail: walks the document per frame while a session is open; keep a +// running count on the session if large drawings feel it. +fn refedit_working_set_count( + session: &crate::modules::draw::modify::refedit::RefEditSession, + scene: &crate::scene::Scene, +) -> usize { + let space = scene.current_layout_block_handle_pub(); + scene + .document + .entities() + .filter(|entity| { + entity.common().owner_handle == space && session.in_working_set(entity.common().handle) + }) + .count() +} diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 57a88e393..12671c570 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -35,6 +35,13 @@ impl OpenCADStudio { Some(K::PointCloudAttach) => crate::t!("Attach Point Cloud").into_owned(), Some(K::PointCloudColorMap) => crate::t!("Point Cloud Color Map").into_owned(), Some(K::PcSection) => crate::t!("Extract Section Lines from Point Cloud").into_owned(), + Some(K::ViewBackground) => format!( + "{} โ€” {}", + crate::tr!("view-background", "title"), + self.view_background.as_ref().map_or("", |s| s.view.as_str()) + ), + Some(K::ImageAdjust) => crate::tr!("image-adjust", "title"), + Some(K::RefEdit) => crate::tr!("ref-edit", "dialog-title"), Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(), Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(), Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(), @@ -71,6 +78,9 @@ impl OpenCADStudio { Some(K::ExternalChange) => crate::tr!("modal", "drawing-changed"), Some(K::LayerDeleteWarning) => crate::tr!("modal", "delete-layer"), Some(K::Unsaved) => crate::tr!("modal", "unsaved-changes"), + Some(K::BlockEditChanges) => crate::t!("Block Editor").into_owned(), + Some(K::BlockEditPicker) => crate::t!("Edit Block Definition").into_owned(), + Some(K::BlockSaveAs) => crate::t!("Save Block As").into_owned(), Some(K::PointStyle) => crate::tr!("modal", "point-style"), Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"), Some(K::SaveDialog) => crate::tr!("modal", "save-drawing-as"), @@ -294,6 +304,10 @@ impl OpenCADStudio { textfill: crate::scene::text::sdf_atlas::textfill(), cliprompt_lines: self.cliprompt_lines, commandline_fade_ms: self.commandline_fade_ms, + command_line_opacity: self.command_line.opacity_percent(), + show_menu_bar: self.show_menu_bar, + show_ribbon: !self.ribbon_closed, + hatch_associative: self.hatch_associative, zoom_wheel_reversed: self.zoom_wheel_reversed, touchpad_pan: self.touchpad_pan, ui_scale_percent: self.ui_scale_percent, @@ -488,6 +502,18 @@ impl OpenCADStudio { crate::ui::window::block_definition::view_window(state, flow) }) } + super::super::ModalKind::BlockEditPicker => { + let state = self.block_edit_picker.as_ref()?; + sized_flow(ex, 540, 380, |flow| { + crate::ui::window::block_edit_picker::view(state, flow) + }) + } + super::super::ModalKind::BlockSaveAs => { + let state = self.block_save_as.as_ref()?; + sized_flow(ex, 430, 370, |flow| { + crate::ui::window::block_save_as::view(state, flow) + }) + } super::super::ModalKind::PdfAttach => { let state = self.pdf_attach.as_ref()?; sized_flow(ex, 880, 540, |flow| { @@ -512,6 +538,24 @@ impl OpenCADStudio { crate::ui::window::pdf_dialogs::view_pc_section(state, flow) }) } + super::super::ModalKind::ViewBackground => { + let state = self.view_background.as_ref()?; + sized_flow(ex, 420, 560, |flow| { + crate::ui::window::pdf_dialogs::view_background(state, flow) + }) + } + super::super::ModalKind::ImageAdjust => { + let state = self.image_adjust.as_ref()?; + sized_flow(ex, 520, 560, |flow| { + crate::ui::window::pdf_dialogs::image_adjust(state, flow) + }) + } + super::super::ModalKind::RefEdit => { + let state = self.refedit_dialog.as_ref()?; + sized_flow(ex, 560, 470, |flow| { + crate::ui::window::refedit_dialog::view(state, flow) + }) + } super::super::ModalKind::UnderlayLayers => { let state = self.underlay_layers.as_ref()?; sized_flow(ex, 460, 520, |flow| { @@ -1727,7 +1771,19 @@ impl OpenCADStudio { .unwrap_or_default(), None => String::new(), }; - automatic_flow(ex, |flow| unsaved_changes_dialog_window(&tab_name, flow)) + automatic_flow(ex, |flow| { + unsaved_changes_dialog_window(&tab_name, flow, false) + }) + } + super::super::ModalKind::BlockEditChanges => { + let name = self + .pending_block_close + .as_ref() + .and_then(|(id, _)| self.tabs.iter().find(|tab| tab.id == *id)) + .and_then(|tab| tab.active_block_edit_session()) + .map(|session| session.block_name.clone()) + .unwrap_or_default(); + automatic_flow(ex, |flow| unsaved_changes_dialog_window(&name, flow, true)) } super::super::ModalKind::PointStyle => sized_flow(ex, 360, 470, |flow| { crate::ui::style::point_style::view_window( @@ -2045,23 +2101,53 @@ fn save_as_dialog_window<'a>( fn unsaved_changes_dialog_window( name: &str, sizing: crate::ui::modal::ModalSizing, + block_edit: bool, ) -> Element<'static, Message> { container( column![ - text(name.to_owned()).size(14), + text(if block_edit { + crate::tf!("Save changes to {}?", name).into_owned() + } else { + name.to_owned() + }) + .size(14), iced::widget::Space::new().height(4), - text(crate::tr!("modal", "unsaved-save-prompt")) - .size(12) - .style(dialog_muted_text_style), + text(if block_edit { + String::new() + } else { + crate::tr!("modal", "unsaved-save-prompt") + }) + .size(12) + .style(dialog_muted_text_style), iced::widget::Space::new().height(20), row![ - dialog_button(t!("Save"), Message::UnsavedDialogSave, button::primary), + dialog_button( + t!("Save"), + if block_edit { + Message::BlockEditChangesSave + } else { + Message::UnsavedDialogSave + }, + button::primary + ), iced::widget::Space::new().width(8), - dialog_button(t!("Discard"), Message::UnsavedDialogDiscard, button::danger), + dialog_button( + t!("Discard"), + if block_edit { + Message::BlockEditChangesDiscard + } else { + Message::UnsavedDialogDiscard + }, + button::danger + ), iced::widget::Space::new().width(8), dialog_button( t!("Cancel"), - Message::UnsavedDialogCancel, + if block_edit { + Message::BlockEditChangesCancel + } else { + Message::UnsavedDialogCancel + }, button::secondary ), ], diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index b2b896fdf..de085c127 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -1488,8 +1488,13 @@ pub(super) fn qselect_overlay<'a>( candidate_count, crate::ui::modal::ModalSizing::FILL, ); - let content = - crate::ui::modal::intrinsic(measurement, content, iced::Size::INFINITE, modal_resize); + // Long imported values must not make the natural dialog span the viewport. + let content = crate::ui::modal::intrinsic( + measurement, + content, + iced::Size::new(720.0, f32::INFINITY), + modal_resize, + ); crate::ui::modal::modal( Space::new().width(Fill).height(Fill), @@ -1501,6 +1506,15 @@ pub(super) fn qselect_overlay<'a>( ) } +/// Object type label for the selection filter; INSERT reads as a block reference. +fn qselect_type_label(name: &str) -> String { + if name == "Insert" { + t!("Block Reference").into_owned() + } else { + t!(name).into_owned() + } +} + fn qselect_content<'a>( state: &'a crate::app::QSelectState, types: &[String], @@ -1616,7 +1630,7 @@ fn qselect_content<'a>( } else { None }; - let error = state.error.clone().or(derived_error); + let error = state.error.clone().or(derived_error.clone()); let scope_options = vec![ crate::app::QSelectScope::CurrentSpace, @@ -1635,10 +1649,72 @@ fn qselect_content<'a>( let cancel = dialog_button(t!("Cancel"), Message::QSelectClose, false); let apply = dialog_button_styled_opt( t!("Apply"), - error.is_none().then_some(Message::QSelectApply), + (candidate_count > 0 + && state.editing_condition.is_none() + && (state.conditions.is_empty() && error.is_none() || !state.conditions.is_empty())) + .then_some(Message::QSelectApply), button::primary, ); + let mut condition_rows = column![].spacing(5); + if state.conditions.is_empty() { + condition_rows = condition_rows.push(text(t!("No conditions added.")).size(12)); + } + for (index, condition) in state.conditions.iter().enumerate() { + let type_label = condition + .type_filter + .as_deref() + .map(qselect_type_label) + .unwrap_or_else(|| t!(QSELECT_ANY_TYPE).into_owned()); + let property_label = condition + .property + .as_ref() + .map(|property| property.to_string()) + .unwrap_or_else(|| t!(QSELECT_ANY_PROP).into_owned()); + let summary = format!( + "{}. {} | {} | {} | {}", + index + 1, + type_label, + property_label, + condition.operator, + condition.value + ); + condition_rows = condition_rows.push( + row![ + // The measuring copy must not fill the viewport's available width. + text(summary).size(12).width(flex_width), + dialog_button(t!("Edit"), Message::QSelectEditCondition(index), false), + dialog_button(t!("Delete"), Message::QSelectRemoveCondition(index), false), + ] + .spacing(6) + .align_y(iced::Alignment::Center), + ); + } + let add_condition = dialog_button_styled_opt( + if state.editing_condition.is_some() { + t!("Update condition") + } else { + t!("Add condition") + }, + (derived_error.is_none() && (state.type_filter.is_some() || state.property.is_some())) + .then_some(Message::QSelectAddCondition), + button::secondary, + ); + let condition_actions = row![ + add_condition, + dialog_button_styled_opt( + t!("Cancel edit"), + state.editing_condition.map(|_| Message::QSelectCancelEdit), + button::secondary + ), + dialog_button_styled_opt( + t!("Clear conditions"), + (!state.conditions.is_empty()).then_some(Message::QSelectClearConditions), + button::secondary + ), + ] + .spacing(6); + let panel_body = column![ section_label(t!("Scope")), Space::new().height(5), @@ -1659,7 +1735,9 @@ fn qselect_content<'a>( Space::new().height(5), row![ label(t!("Object type:")), - iced::widget::pick_list(Some(type_sel), type_options, |value| t!(value).into_owned(),) + iced::widget::pick_list(Some(type_sel), type_options, |value| { + qselect_type_label(value) + }) .on_select(|s: String| { if s == QSELECT_ANY_TYPE { Message::QSelectSetType(None) @@ -1706,6 +1784,28 @@ fn qselect_content<'a>( Space::new().height(10), container(rule::horizontal(1)).width(flex_width), Space::new().height(10), + condition_actions, + Space::new().height(8), + dialog_button_styled_opt( + t!("Select object"), + state + .editing_condition + .is_none() + .then_some(Message::QSelectPickObject), + button::secondary, + ), + Space::new().height(8), + section_label(t!("Conditions (all must match)")), + Space::new().height(5), + iced::widget::scrollable(condition_rows) + .height(120) + .width(sizing.width), + Space::new().height(5), + text(t!( + "When the list is nonempty, only listed conditions are applied." + )) + .size(11), + Space::new().height(10), section_label(t!("Result")), Space::new().height(5), column![ diff --git a/src/app/visibility.rs b/src/app/visibility.rs index f3d8504ca..049dd55ea 100644 --- a/src/app/visibility.rs +++ b/src/app/visibility.rs @@ -186,6 +186,20 @@ impl OpenCADStudio { return; } + // A reference to the definition itself gets an evaluated copy first, + // so the state picked for it leaves the definition alone. + let direct = { + let doc = &self.tabs[i].scene.document; + matches!(doc.get_entity(insert_handle), Some(EntityType::Insert(ins)) + if doc.block_records.get(&ins.block_name).is_some_and(|record| { + crate::scene::block_visibility::parameter(doc, record.handle).is_some() + })) + }; + if direct { + self.push_undo_snapshot(i, "Visibility State"); + self.tabs[i].scene.visibility_representation(insert_handle); + } + // Resolve everything against an immutable borrow first. let mapping = { let doc = &self.tabs[i].scene.document; @@ -202,7 +216,9 @@ impl OpenCADStudio { return; }; - self.push_undo_snapshot(i, "Visibility State"); + if !direct { + self.push_undo_snapshot(i, "Visibility State"); + } // Apply invisibility flags to the anonymous block's members. { diff --git a/src/command.rs b/src/command.rs index 54efd1035..38ae3219e 100644 --- a/src/command.rs +++ b/src/command.rs @@ -382,7 +382,11 @@ impl CadCommand for FreeTextValuePromptCommand { } fn on_text_input(&mut self, text: &str) -> Option { - Some(CmdResult::Dispatch(format!("{} {}", self.name, text.trim()))) + Some(CmdResult::Dispatch(format!( + "{} {}", + self.name, + text.trim() + ))) } fn on_point(&mut self, _pt: DVec3) -> CmdResult { @@ -1510,6 +1514,12 @@ impl DimensionPreview { /// Returned by every `CadCommand` method to tell main.rs what to do. #[allow(dead_code)] pub enum CmdResult { + /// Return the suspended filter after a read-only sample pick or cancellation. + QSelectSample { + state: Box, + tab_id: u64, + handle: Option, + }, /// Command is still waiting for the next point; show updated prompt. NeedPoint, /// Update the committed-segment wire (normal colour) and keep collecting points. @@ -1611,6 +1621,11 @@ pub enum CmdResult { }, /// Copy selected entities with multiple transforms (e.g. rectangular array); end command. BatchCopy(Vec, Vec), + /// Turn the objects into an associative array; end command. + AssocArray { + handles: Vec, + spec: crate::scene::assoc_array::AssocArraySpec, + }, /// Erase `handle` and replace with new entities; command stays active. ReplaceEntity(Handle, Vec), /// XCLIP on block references; ends the command. @@ -1925,12 +1940,33 @@ pub enum CmdResult { PasteClipboard { base_pt: DVec3, }, + /// Set the view's twist about its line of sight (radians, + /// counter-clockwise); the command stays active. + TwistView(f64), /// Zoom the model-space camera to fit the given corner points; end command. ZoomToWindow { p1: DVec3, p2: DVec3, }, - /// Print a measurement result to the command line and end the command. + /// Zoom to the given corner points; the command stays active. + Show { + p1: DVec3, + p2: DVec3, + }, + /// Commit `entity`, then zoom to the given corner points; end command. + CommitAndZoom { + entity: EntityType, + p1: DVec3, + p2: DVec3, + }, + /// Scale the given layout viewports to `factor` paper units per model + /// unit; end command. + ScaleViewports { + handles: Vec, + factor: f64, + }, + /// Print a measurement result to the command line and end the command + /// (an empty one ends it silently). Measurement(String), /// Print a measurement result and keep the command active. ReportMeasurement(String), @@ -2947,6 +2983,12 @@ pub trait CadCommand: Send { false } + /// The kinds of object the command takes (ADJUST): the host drops the + /// rest of each completed selection without a word. + fn selection_entity_filter(&self) -> Option bool> { + None + } + /// The command takes block references only (XCLIP): the host drops the /// rest and says how many were ineligible. fn selection_keeps_block_references(&self) -> bool { diff --git a/src/entities/extended.rs b/src/entities/extended.rs index 3a815d8d0..dc9b536ab 100644 --- a/src/entities/extended.rs +++ b/src/entities/extended.rs @@ -1971,6 +1971,12 @@ fn grips(entity: &ExtendedEntity) -> Vec { }) .collect() }; + // A section volume is also pushed or pulled at its top and bottom. + if data.state == 4 { + grips.extend(section_plane_edge_grips(data).into_iter().filter(|grip| { + matches!(grip.id, SECTION_GRIP_TOP | SECTION_GRIP_BOTTOM) + })); + } if let Some(right) = data.vertices.last() { let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z); let edge_center_offset = (data.top_height - data.bottom_height) * 0.5; diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index ef7ea5cc7..cdb3a23eb 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -758,7 +758,8 @@ impl Transformable for Hatch { /// Assign sequential grip IDs across all boundary paths and edges. /// Exposed control points per edge type: /// Polyline โ†’ each vertex (x, y) -/// Line โ†’ start, end +/// Line โ†’ each corner once: where one line ends and the next +/// starts is a single grip moving both ends /// CircularArc โ†’ center /// EllipticArc โ†’ center /// Spline โ†’ fit points if present, else control points (x, y) @@ -784,51 +785,9 @@ impl Grippable for Hatch { if self.is_associative { return out; } - for path in &self.paths { - for edge in &path.edges { - match edge { - BoundaryEdge::Polyline(p) => { - for v in &p.vertices { - out.push(center_grip(id, glam::DVec3::new(v.x, v.y, elev))); - id += 1; - } - } - BoundaryEdge::Line(l) => { - out.push(center_grip( - id, - glam::DVec3::new(l.start.x, l.start.y, elev), - )); - id += 1; - out.push(center_grip(id, glam::DVec3::new(l.end.x, l.end.y, elev))); - id += 1; - } - BoundaryEdge::CircularArc(a) => { - out.push(center_grip( - id, - glam::DVec3::new(a.center.x, a.center.y, elev), - )); - id += 1; - } - BoundaryEdge::EllipticArc(e) => { - out.push(center_grip( - id, - glam::DVec3::new(e.center.x, e.center.y, elev), - )); - id += 1; - } - BoundaryEdge::Spline(s) => { - let pts: Vec<[f64; 2]> = if !s.fit_points.is_empty() { - s.fit_points.iter().map(|p| [p.x, p.y]).collect() - } else { - s.control_points.iter().map(|p| [p.x, p.y]).collect() - }; - for [x, y] in pts { - out.push(center_grip(id, glam::DVec3::new(x, y, elev))); - id += 1; - } - } - } - } + for (_, [x, y]) in boundary_points(self) { + out.push(center_grip(id, glam::DVec3::new(x, y, elev))); + id += 1; } out } @@ -866,97 +825,40 @@ impl Grippable for Hatch { id += 1; } - 'outer: for path in &mut self.paths { - for edge in &mut path.edges { - match edge { - BoundaryEdge::Polyline(p) => { - for v in &mut p.vertices { - if id == grip_id { - let (nx, ny) = - resolve(&apply, Vec3::new(v.x as f32, v.y as f32, elev)); - v.x = nx; - v.y = ny; - break 'outer; - } - id += 1; - } - } - BoundaryEdge::Line(l) => { - if id == grip_id { - let (nx, ny) = resolve( - &apply, - Vec3::new(l.start.x as f32, l.start.y as f32, elev), - ); - l.start.x = nx; - l.start.y = ny; - break 'outer; - } - id += 1; - if id == grip_id { - let (nx, ny) = - resolve(&apply, Vec3::new(l.end.x as f32, l.end.y as f32, elev)); - l.end.x = nx; - l.end.y = ny; - break 'outer; - } - id += 1; - } - BoundaryEdge::CircularArc(a) => { - if id == grip_id { - let (nx, ny) = resolve( - &apply, - Vec3::new(a.center.x as f32, a.center.y as f32, elev), - ); - a.center.x = nx; - a.center.y = ny; - break 'outer; - } - id += 1; - } - BoundaryEdge::EllipticArc(e) => { - if id == grip_id { - let (nx, ny) = resolve( - &apply, - Vec3::new(e.center.x as f32, e.center.y as f32, elev), - ); - e.center.x = nx; - e.center.y = ny; - break 'outer; - } - id += 1; - } - BoundaryEdge::Spline(s) => { - if !s.fit_points.is_empty() { - for fp in &mut s.fit_points { - if id == grip_id { - let (nx, ny) = - resolve(&apply, Vec3::new(fp.x as f32, fp.y as f32, elev)); - fp.x = nx; - fp.y = ny; - break 'outer; - } - id += 1; - } - } else { - for cp in &mut s.control_points { - if id == grip_id { - let (nx, ny) = - resolve(&apply, Vec3::new(cp.x as f32, cp.y as f32, elev)); - cp.x = nx; - cp.y = ny; - break 'outer; - } - id += 1; - } - } - } - } - } - } + let Some(&(point, [x, y])) = boundary_points(self).get(grip_id.wrapping_sub(id)) else { + return; + }; + let (nx, ny) = resolve(&apply, Vec3::new(x as f32, y as f32, elev)); + set_boundary_point(self, point, nx, ny); } fn grip_menu(&self, grip_id: usize) -> Vec { use crate::scene::model::object::{GripMenuAction, GripMenuItem}; + if let Some(point) = vertex_point_at(self, grip_id) { + let removable = match point { + BoundaryPoint::LineVertex { ending, starting, .. } => { + ending.is_some() && starting.is_some() + } + _ => true, + }; + let mut items = vec![ + GripMenuItem { + label: "Stretch", + action: GripMenuAction::Stretch, + }, + GripMenuItem { + label: "Add Vertex", + action: GripMenuAction::AddVertex, + }, + ]; + if removable { + items.push(GripMenuItem { + label: "Remove Vertex", + action: GripMenuAction::RemoveVertex, + }); + } + return items; + } if self.pattern.lines.is_empty() || grip_id != 0 { return vec![GripMenuItem { label: "Stretch", @@ -989,6 +891,10 @@ impl Grippable for Hatch { action: crate::scene::model::object::GripMenuAction, ) { use crate::scene::model::object::GripMenuAction as A; + if let Some(point) = vertex_point_at(self, grip_id) { + edit_boundary_vertex(self, point, action); + return; + } if grip_id == 0 && matches!(action, A::OriginPoint) { if let (Some((gx, gy)), Some((_ox, _oy))) = ( boundary_centroid(self), @@ -1099,3 +1005,303 @@ impl FallbackTess for Hatch { (pts, snap_pts, vec![], key_verts) } } + +/// Where one boundary grip's point lives. +#[derive(Clone, Copy, Debug, PartialEq)] +enum BoundaryPoint { + PolylineVertex { path: usize, edge: usize, vertex: usize }, + /// A corner of a chain of line edges: the end of `ending` and the start + /// of `starting`, kept together so moving it never opens the boundary. + LineVertex { path: usize, ending: Option, starting: Option }, + ArcCenter { path: usize, edge: usize }, + EllipseCenter { path: usize, edge: usize }, + SplinePoint { path: usize, edge: usize, index: usize, fit: bool }, +} + +fn same_point(a: &codec::types::Vector2, b: &codec::types::Vector2) -> bool { + let scale = a.x.abs().max(a.y.abs()).max(1.0); + (a.x - b.x).abs() <= 1e-9 * scale && (a.y - b.y).abs() <= 1e-9 * scale +} + +/// The non-associative boundary's grip points, in grip order (after the +/// pattern-origin grip), each joined corner of a line chain once. +fn boundary_points(hatch: &Hatch) -> Vec<(BoundaryPoint, [f64; 2])> { + let mut out = Vec::new(); + for (path_index, path) in hatch.paths.iter().enumerate() { + let mut pending_end: Option = None; + let mut first_start: Option = None; + let line = |edge: usize| match &path.edges[edge] { + BoundaryEdge::Line(line) => Some(line), + _ => None, + }; + for (edge_index, edge) in path.edges.iter().enumerate() { + if let BoundaryEdge::Line(current) = edge { + match pending_end { + Some(previous) if same_point(&line(previous).unwrap().end, ¤t.start) => { + out.push(( + BoundaryPoint::LineVertex { + path: path_index, + ending: Some(previous), + starting: Some(edge_index), + }, + [current.start.x, current.start.y], + )); + } + pending => { + if let Some(previous) = pending { + let end = &line(previous).unwrap().end; + out.push(( + BoundaryPoint::LineVertex { + path: path_index, + ending: Some(previous), + starting: None, + }, + [end.x, end.y], + )); + } + if edge_index == 0 { + first_start = Some(out.len()); + } + out.push(( + BoundaryPoint::LineVertex { + path: path_index, + ending: None, + starting: Some(edge_index), + }, + [current.start.x, current.start.y], + )); + } + } + pending_end = Some(edge_index); + continue; + } + if let Some(previous) = pending_end.take() { + let end = &line(previous).unwrap().end; + out.push(( + BoundaryPoint::LineVertex { + path: path_index, + ending: Some(previous), + starting: None, + }, + [end.x, end.y], + )); + } + match edge { + BoundaryEdge::Polyline(polyline) => { + for (vertex, v) in polyline.vertices.iter().enumerate() { + out.push(( + BoundaryPoint::PolylineVertex { + path: path_index, + edge: edge_index, + vertex, + }, + [v.x, v.y], + )); + } + } + BoundaryEdge::CircularArc(arc) => out.push(( + BoundaryPoint::ArcCenter { + path: path_index, + edge: edge_index, + }, + [arc.center.x, arc.center.y], + )), + BoundaryEdge::EllipticArc(ellipse) => out.push(( + BoundaryPoint::EllipseCenter { + path: path_index, + edge: edge_index, + }, + [ellipse.center.x, ellipse.center.y], + )), + BoundaryEdge::Spline(spline) => { + let fit = !spline.fit_points.is_empty(); + let points: Vec<[f64; 2]> = if fit { + spline.fit_points.iter().map(|p| [p.x, p.y]).collect() + } else { + spline.control_points.iter().map(|p| [p.x, p.y]).collect() + }; + for (index, point) in points.into_iter().enumerate() { + out.push(( + BoundaryPoint::SplinePoint { + path: path_index, + edge: edge_index, + index, + fit, + }, + point, + )); + } + } + BoundaryEdge::Line(_) => {} + } + } + // A closed chain: the last line ends where the first one starts. + if let Some(last) = pending_end { + let end = line(last).unwrap().end.clone(); + match first_start.map(|slot| (slot, &mut out[slot])) { + Some((_, (BoundaryPoint::LineVertex { ending, starting: Some(0), .. }, _))) + if ending.is_none() && same_point(&line(0).unwrap().start, &end) => + { + *ending = Some(last); + } + _ => out.push(( + BoundaryPoint::LineVertex { + path: path_index, + ending: Some(last), + starting: None, + }, + [end.x, end.y], + )), + } + } + } + out +} + +/// Move one boundary grip point, every edge end it stands for included. +fn set_boundary_point(hatch: &mut Hatch, point: BoundaryPoint, x: f64, y: f64) { + match point { + BoundaryPoint::PolylineVertex { path, edge, vertex } => { + if let BoundaryEdge::Polyline(polyline) = &mut hatch.paths[path].edges[edge] { + polyline.vertices[vertex].x = x; + polyline.vertices[vertex].y = y; + } + } + BoundaryPoint::LineVertex { path, ending, starting } => { + if let Some(edge) = ending { + if let BoundaryEdge::Line(line) = &mut hatch.paths[path].edges[edge] { + line.end = codec::types::Vector2::new(x, y); + } + } + if let Some(edge) = starting { + if let BoundaryEdge::Line(line) = &mut hatch.paths[path].edges[edge] { + line.start = codec::types::Vector2::new(x, y); + } + } + } + BoundaryPoint::ArcCenter { path, edge } => { + if let BoundaryEdge::CircularArc(arc) = &mut hatch.paths[path].edges[edge] { + arc.center.x = x; + arc.center.y = y; + } + } + BoundaryPoint::EllipseCenter { path, edge } => { + if let BoundaryEdge::EllipticArc(ellipse) = &mut hatch.paths[path].edges[edge] { + ellipse.center.x = x; + ellipse.center.y = y; + } + } + BoundaryPoint::SplinePoint { path, edge, index, fit } => { + if let BoundaryEdge::Spline(spline) = &mut hatch.paths[path].edges[edge] { + if fit { + if let Some(point) = spline.fit_points.get_mut(index) { + point.x = x; + point.y = y; + } + } else if let Some(point) = spline.control_points.get_mut(index) { + point.x = x; + point.y = y; + } + } + } + } +} + +/// The boundary vertex a grip stands for, when it is one that vertices can +/// be added after or removed at. +fn vertex_point_at(hatch: &Hatch, grip_id: usize) -> Option { + if hatch.is_associative { + return None; + } + // Grip 0 is the pattern origin when there is a pattern. + let first = usize::from(!hatch.pattern.lines.is_empty()); + let (point, _) = *boundary_points(hatch).get(grip_id.checked_sub(first)?)?; + matches!( + point, + BoundaryPoint::PolylineVertex { .. } | BoundaryPoint::LineVertex { .. } + ) + .then_some(point) +} + +/// Add a vertex after `point` (halfway along the following segment, on +/// the arc for a bulged one) or remove `point`, keeping the loop closed. +fn edit_boundary_vertex( + hatch: &mut Hatch, + point: BoundaryPoint, + action: crate::scene::model::object::GripMenuAction, +) { + use crate::scene::model::object::GripMenuAction as A; + match point { + BoundaryPoint::PolylineVertex { path, edge, vertex } => { + let BoundaryEdge::Polyline(polyline) = &mut hatch.paths[path].edges[edge] else { + return; + }; + let n = polyline.vertices.len(); + match action { + A::AddVertex => { + if vertex + 1 == n && !polyline.is_closed { + let last = polyline.vertices[vertex].clone(); + polyline.vertices.push(codec::types::Vector3::new(last.x, last.y, 0.0)); + return; + } + let a = polyline.vertices[vertex].clone(); + let b = polyline.vertices[(vertex + 1) % n].clone(); + let (cx, cy) = (b.x - a.x, b.y - a.y); + // The arc's midpoint sits off the chord by the sagitta, + // to the right of a positive (counter-clockwise) bulge. + let sagitta = a.z * 0.5; + let half = (a.z.atan() * 0.5).tan(); + let mid = codec::types::Vector3::new( + (a.x + b.x) * 0.5 + cy * sagitta, + (a.y + b.y) * 0.5 - cx * sagitta, + half, + ); + polyline.vertices[vertex].z = half; + polyline.vertices.insert(vertex + 1, mid); + } + A::RemoveVertex if n > if polyline.is_closed { 3 } else { 2 } => { + polyline.vertices.remove(vertex); + } + _ => {} + } + } + BoundaryPoint::LineVertex { path, ending, starting } => { + let edges = &mut hatch.paths[path].edges; + match (action, ending, starting) { + (A::AddVertex, _, Some(edge)) => { + let BoundaryEdge::Line(line) = &mut edges[edge] else { + return; + }; + let mid = codec::types::Vector2::new( + (line.start.x + line.end.x) * 0.5, + (line.start.y + line.end.y) * 0.5, + ); + let tail = codec::entities::LineEdge { + start: mid.clone(), + end: line.end.clone(), + }; + line.end = mid; + edges.insert(edge + 1, BoundaryEdge::Line(tail)); + } + (A::RemoveVertex, Some(previous), Some(edge)) if previous != edge => { + let lines = edges + .iter() + .filter(|edge| matches!(edge, BoundaryEdge::Line(_))) + .count(); + if lines <= 3 { + return; + } + let BoundaryEdge::Line(removed) = edges[edge].clone() else { + return; + }; + if let BoundaryEdge::Line(line) = &mut edges[previous] { + line.end = removed.end; + } + edges.remove(edge); + } + _ => {} + } + } + _ => {} + } +} diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index ae173b81a..d134643b2 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -1054,15 +1054,26 @@ fn display_mesh(mesh: &Mesh) -> RefinedMesh { /// Open, non-manifold, non-planar, and degenerate meshes return `None` so a /// modelling command can leave the source untouched. pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option { + if let codec::EntityType::PolygonMesh(mesh) = entity { + if !mesh.is_closed_m() || !mesh.is_closed_n() { + return None; + } + } + let (vertices, faces) = mesh_polygons(entity)?; + kernel::brep::make::faceted_solid(&vertices, &faces) +} + +/// The vertices and face loops (vertex indices) of a MESH, polygon mesh or +/// polyface mesh, as drawn without smoothing. +pub(crate) fn mesh_polygons( + entity: &codec::EntityType, +) -> Option<(Vec<[f64; 3]>, Vec>)> { match entity { codec::EntityType::Mesh(mesh) => { let base = base_refined_mesh(mesh); - kernel::brep::make::faceted_solid(&base.vertices, &base.faces) + Some((base.vertices, base.faces)) } codec::EntityType::PolygonMesh(mesh) => { - if !mesh.is_closed_m() || !mesh.is_closed_n() { - return None; - } let m = mesh.m_vertex_count.max(0) as usize; let n = mesh.n_vertex_count.max(0) as usize; let vertices = mesh @@ -1074,9 +1085,11 @@ pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option Option { let vertices = mesh @@ -1105,12 +1118,62 @@ pub(crate) fn closed_mesh_body(entity: &codec::EntityType) -> Option= 3).then_some(indices) }) .collect::>(); - kernel::brep::make::faceted_solid(&vertices, &faces) + Some((vertices, faces)) } _ => None, } } +/// The part of a mesh on one side of a cut: every face loop clipped to where +/// `side` is positive (`keep_positive`) or negative, rebuilt as a MESH with +/// `common`. The mesh stays open where the cut crossed it. `None` when no face +/// reaches that side. +pub(crate) fn clipped_mesh( + vertices: &[[f64; 3]], + faces: &[Vec], + side: impl Fn([f64; 3]) -> f64, + keep_positive: bool, + common: &codec::entities::EntityCommon, +) -> Option { + let sign = if keep_positive { 1.0 } else { -1.0 }; + let distances: Vec = vertices.iter().map(|&point| sign * side(point)).collect(); + let mut mesh = Mesh::new(); + mesh.common = common.clone(); + let mut welded: std::collections::HashMap<[u64; 3], usize> = Default::default(); + let mut index_of = |mesh: &mut Mesh, point: [f64; 3]| { + *welded.entry(point.map(f64::to_bits)).or_insert_with(|| { + mesh.add_vertex(codec::types::Vector3::new(point[0], point[1], point[2])) + }) + }; + for face in faces { + if face.iter().any(|&index| index >= vertices.len()) { + continue; + } + let mut loop_points = Vec::with_capacity(face.len() + 2); + for (k, &a) in face.iter().enumerate() { + let b = face[(k + 1) % face.len()]; + let (da, db) = (distances[a], distances[b]); + if da >= 0.0 { + loop_points.push(vertices[a]); + } + if (da > 0.0 && db < 0.0) || (da < 0.0 && db > 0.0) { + let t = da / (da - db); + let (pa, pb) = (vertices[a], vertices[b]); + loop_points.push(std::array::from_fn(|axis| pa[axis] + (pb[axis] - pa[axis]) * t)); + } + } + if loop_points.len() < 3 { + continue; + } + let indices = loop_points + .into_iter() + .map(|point| index_of(&mut mesh, point)) + .collect::>(); + mesh.add_face(codec::entities::MeshFace::new(indices)); + } + (!mesh.faces.is_empty()).then_some(mesh) +} + fn face_triangle_indices(vertices: &[[f64; 3]], faces: &[Vec]) -> (Vec, Vec) { let mut indices = Vec::new(); let mut triangle_faces = Vec::new(); diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index cf474df48..ad4ccf5b4 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -2348,6 +2348,7 @@ impl MultiLeaderTess for MultiLeader { fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, + stub: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs index 23b90ddd3..e560a841f 100644 --- a/src/entities/raster_image.rs +++ b/src/entities/raster_image.rs @@ -333,7 +333,20 @@ impl PropertyEditable for RasterImage { .contains(codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY); let clip_inverted = self.clip_boundary.clip_mode == codec::entities::ClipMode::Inside; let transparency = self.common.transparency.to_string(); + let whole = |label: &str, field: &'static str, value: u8| Property { + label: t!(label).into_owned(), + field, + value: PropValue::EditText(value.to_string()), + }; vec![ + PropSection { + title: t!("Image Adjust").into_owned(), + props: vec![ + whole("Brightness", "ri_bright", self.brightness), + whole("Contrast", "ri_contrast", self.contrast), + whole("Fade", "ri_fade", self.fade), + ], + }, PropSection { title: t!("Geometry").into_owned(), props: vec![ @@ -362,13 +375,6 @@ impl PropertyEditable for RasterImage { title: t!("Misc").into_owned(), props: vec![ ro(t!("Name").as_ref(), "ri_name", self.file_name().to_string()), - edit( - t!("Brightness").as_ref(), - "ri_bright", - self.brightness as f64, - ), - edit(t!("Contrast").as_ref(), "ri_contrast", self.contrast as f64), - edit(t!("Fade").as_ref(), "ri_fade", self.fade as f64), ro(t!("Transparency").as_ref(), "ri_transparency", transparency), Property { label: t!("Show image").into_owned(), @@ -449,9 +455,10 @@ impl PropertyEditable for RasterImage { "ri_ox" => self.insertion_point.x = v, "ri_oy" => self.insertion_point.y = v, "ri_oz" => self.insertion_point.z = v, - "ri_bright" => self.brightness = v.clamp(0.0, 100.0) as u8, - "ri_contrast" => self.contrast = v.clamp(0.0, 100.0) as u8, - "ri_fade" => self.fade = v.clamp(0.0, 100.0) as u8, + // Out-of-range values are refused before they get here. + "ri_bright" if (0.0..=100.0).contains(&v) => self.brightness = v as u8, + "ri_contrast" if (0.0..=100.0).contains(&v) => self.contrast = v as u8, + "ri_fade" if (0.0..=100.0).contains(&v) => self.fade = v as u8, _ => {} } } diff --git a/src/entities/underlay.rs b/src/entities/underlay.rs index 9aa7cc3fb..ba40632f6 100644 --- a/src/entities/underlay.rs +++ b/src/entities/underlay.rs @@ -227,7 +227,8 @@ fn parse_flag(value: &str, current: bool) -> bool { pub(crate) fn validate_property(field: &str, value: &str) -> Result<(), &'static str> { let number = || value.trim().parse::().ok(); match field { - "ul_contrast" | "ul_fade" => match number() { + // A raster image's Image Adjust rows take the same range. + "ul_contrast" | "ul_fade" | "ri_bright" | "ri_contrast" | "ri_fade" => match number() { Some(v) if (0.0..=100.0).contains(&v) && v.fract() == 0.0 => Ok(()), _ => Err("Value must be an integer between 0 and 100"), }, diff --git a/src/entity_creation.rs b/src/entity_creation.rs new file mode 100644 index 000000000..f07bfa678 --- /dev/null +++ b/src/entity_creation.rs @@ -0,0 +1,744 @@ +//! Discoverable creation contracts, shared by MCP preflight and the entity builder. +use serde_json::{json, Value}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn published_examples_build_with_the_real_factory() { + let document = codec::CadDocument::new(); + for kind in TYPES { + let contract = schema(kind).unwrap(); + for example in contract["examples"].as_array().unwrap() { + validate_entity(example, "/entities/0").unwrap(); + let built = build_entity(example, Some(&document)); + assert!(built.is_ok(), "{kind}: {built:?}"); + } + } + for name in ["line", "LINE", "polyline", "Polyline", "LWPOLYLINE"] { + let mut example = schema(name).unwrap()["examples"][0].clone(); + example["type"] = json!(name); + validate_entity(&example, "").unwrap(); + assert!(build_entity(&example, Some(&document)).is_ok()); + } + // JSON Schema integers include numerically integral float notation. + let circle = json!({"type":"Circle","center":[0,0],"radius":2,"color":2.0}); + validate_entity(&circle, "").unwrap(); + assert_eq!( + build_entity(&circle, Some(&document)) + .unwrap() + .common() + .color, + codec::types::Color::from_index(2) + ); + let spline = json!({"type":"Spline","degree":2.0,"control_points":[[0,0],[5,10],[10,0]]}); + validate_entity(&spline, "").unwrap(); + let codec::EntityType::Spline(built) = build_entity(&spline, Some(&document)).unwrap() + else { + panic!("expected spline") + }; + assert_eq!(built.degree, 2); + } + + #[test] + fn rejects_bad_nested_fields_and_matches_geometry_constraints() { + let cases = [ + ( + json!({"type":"Line","start":[0,0],"end":[1,"bad"]}), + "/end/1", + ), + (json!({"type":"Circle","center":[0,0]}), "/radius"), + ( + json!({"type":"Circle","center":[0,0],"radius":-1}), + "/radius", + ), + ( + json!({"type":"Solid","corners":[[0,0],[1,0],[0,1]]}), + "/corners/0", + ), + ( + json!({"type":"Point","location":[0,0],"color":32768}), + "/color", + ), + ( + json!({"type":"Line","start":[0,0],"end":[1,1],"unsupported":true}), + "/unsupported", + ), + ]; + for (spec, path) in cases { + let error = validate_entity(&spec, "/entities/0").unwrap_err(); + assert!(error.contains(&format!("/entities/0{path}")), "{error}"); + assert!(build_entity(&spec, None).is_err()); + } + for spec in [ + json!({"type":"Arc","center":[0,0],"radius":5,"start_angle_deg":45,"end_angle_deg":45}), + json!({"type":"Spline","degree":3,"control_points":[[0,0],[1,1]]}), + json!({"type":"Spline","degree":1,"control_points":[[0,0],[1,1]],"weights":[1]}), + json!({"type":"Insert","block":"Example","position":[0,0],"scale":0}), + ] { + assert!(validate_entity(&spec, "").is_err()); + assert!(build_entity(&spec, None).is_err()); + } + // The static path deliberately omits document-dependent style lookup. + let spec = json!({"type":"Text","value":"Label","position":[0,0],"style":"missing_style"}); + assert!(validate_entity(&spec, "").is_ok()); + assert_eq!( + build_entity(&spec, Some(&codec::CadDocument::new())).unwrap_err()["code"], + "invalid_text_style" + ); + } +} + +pub const TYPES: &[&str] = &[ + "Line", + "Circle", + "Arc", + "Spline", + "LwPolyline", + "Point", + "Text", + "MText", + "Insert", + "Solid", + "Hatch", +]; + +fn point(min: usize, max: usize) -> Value { + json!({"type":"array","items":{"type":"number"},"minItems":min,"maxItems":max}) +} + +/// Contracts are built once and shared by discovery, preflight and execution. +pub fn schema(name: &str) -> Result { + use std::sync::OnceLock; + static CONTRACTS: OnceLock> = OnceLock::new(); + let name = if name.eq_ignore_ascii_case("Polyline") { + "LwPolyline" + } else { + name + }; + let index = TYPES.iter().position(|kind| kind.eq_ignore_ascii_case(name)) + .ok_or_else(|| format!("/type: unknown entity type '{name}'; request entity_schema without type to list supported types"))?; + let contracts = CONTRACTS.get_or_init(|| { + TYPES + .iter() + .map(|kind| build_schema(kind).expect("known type")) + .collect() + }); + Ok(contracts[index].clone()) +} + +fn build_schema(name: &str) -> Result { + let name = if name.eq_ignore_ascii_case("Polyline") { + "LwPolyline" + } else { + name + }; + let canonical = TYPES.iter().find(|t| t.eq_ignore_ascii_case(name)) + .ok_or_else(|| format!("/type: unknown entity type '{name}'; request entity_schema without type to list supported types"))?; + let case_pattern = |name: &str| { + name.chars() + .map(|c| format!("[{}{}]", c.to_ascii_uppercase(), c.to_ascii_lowercase())) + .collect::() + }; + let pattern = if *canonical == "LwPolyline" { + format!( + "^({}|{})$", + case_pattern(canonical), + case_pattern("Polyline") + ) + } else { + format!("^{}$", case_pattern(canonical)) + }; + let number = json!({"type":"number"}); + let string = json!({"type":"string"}); + let boolean = json!({"type":"boolean"}); + let mut properties = json!({"type":{"type":"string","pattern":pattern},"layer":{"type":"string","description":"Missing layers are created; empty uses the default layer."},"color":{"type":"integer","minimum":-32768,"maximum":32767,"description":"DXF indexed color, including ByLayer/ByBlock sentinels."}}); + let (required, example) = match *canonical { + "Line" => { + properties["start"] = point(2, 3); + properties["end"] = point(2, 3); + properties["thickness"] = number.clone(); + ( + vec!["start", "end"], + json!({"type":"Line","start":[0,0],"end":[10,10]}), + ) + } + "Circle" | "Arc" => { + properties["center"] = point(2, 3); + properties["radius"] = if *canonical == "Arc" { + json!({"type":"number","exclusiveMinimum":0}) + } else { + json!({"type":"number","minimum":0}) + }; + if *canonical == "Arc" { + properties["start_angle_deg"] = number.clone(); + properties["end_angle_deg"] = number.clone(); + properties["end_angle_deg"]["description"] = json!("Together with start_angle_deg, must define a finite, nonzero sweep using the CAD arc constructor."); + ( + vec!["center", "radius", "start_angle_deg", "end_angle_deg"], + json!({"type":"Arc","center":[0,0],"radius":5,"start_angle_deg":0,"end_angle_deg":90}), + ) + } else { + ( + vec!["center", "radius"], + json!({"type":"Circle","center":[0,0],"radius":5}), + ) + } + } + "Point" => { + properties["location"] = point(2, 3); + (vec!["location"], json!({"type":"Point","location":[0,0]})) + } + "LwPolyline" => { + properties["vertices"] = json!({"type":"array","minItems":2,"items":point(2,3),"description":"XY coordinates; optional Z is ignored."}); + properties["closed"] = boolean.clone(); + properties["constant_width"] = number.clone(); + ( + vec!["vertices"], + json!({"type":"LwPolyline","vertices":[[0,0],[10,0]],"closed":false}), + ) + } + "Text" | "MText" => { + properties["value"] = json!({"type":"string","minLength":1}); + properties["position"] = point(2, 3); + properties["height"] = json!({"type":"number","default":2.5,"description":"Absolute value, clamped to at least 0.000001."}); + properties["rotation_deg"] = number.clone(); + properties["style"] = json!({"type":"string","description":"Nonempty style names must exist in the target document."}); + if *canonical == "MText" { + properties["width"] = json!({"type":"number","description":"Positive values set rectangle width; otherwise default."}); + } + ( + vec!["value", "position"], + json!({"type":canonical,"value":"Label","position":[0,0]}), + ) + } + "Insert" => { + properties["block"] = json!({"type":"string","minLength":1}); + properties["position"] = point(2, 3); + properties["rotation_deg"] = number.clone(); + properties["scale"] = json!({"anyOf":[{"type":"number","not":{"const":0}},{"type":"array","minItems":3,"maxItems":3,"items":{"type":"number","not":{"const":0}}}],"default":1,"description":"Nonzero uniform factor or three nonzero axis factors."}); + ( + vec!["block", "position"], + json!({"type":"Insert","block":"DOOR_90","position":[0,0],"scale":1}), + ) + } + "Solid" => { + properties["corners"] = + json!({"type":"array","minItems":3,"maxItems":4,"items":point(3,3)}); + ( + vec!["corners"], + json!({"type":"Solid","corners":[[0,0,0],[10,0,0],[0,10,0]]}), + ) + } + "Hatch" => { + properties["boundary"] = json!({"type":"array","minItems":3,"items":point(2,3),"description":"Closed XY boundary; optional Z is ignored."}); + properties["solid"] = boolean.clone(); + properties["pattern"] = string; + properties["pattern_scale"] = json!({"type":"number","default":1,"description":"Absolute value, clamped to at least 0.000001."}); + properties["pattern_angle_deg"] = number.clone(); + ( + vec!["boundary"], + json!({"type":"Hatch","boundary":[[0,0],[10,0],[0,10]],"solid":true}), + ) + } + "Spline" => { + properties["degree"] = json!({"type":"integer","minimum":1,"maximum":10,"default":3}); + for key in ["control_points", "fit_points"] { + properties[key] = json!({"type":"array","minItems":2,"items":point(2,3)}); + } + for key in ["knots", "weights"] { + properties[key] = json!({"type":"array","minItems":1,"items":{"type":"number"}}); + } + properties["control_points"]["description"] = + json!("Requires more points than degree (default degree 3)."); + properties["knots"]["description"] = json!("Requires control_points; exactly control_points count + degree + 1 non-decreasing values."); + properties["weights"]["description"] = json!("Requires control_points; one positive weight per control point. Sets rational=true unless overridden explicitly."); + properties["weights"]["items"]["exclusiveMinimum"] = json!(0); + for key in ["closed", "periodic", "rational", "planar", "linear"] { + properties[key] = boolean.clone(); + } + ( + vec![], + json!({"type":"Spline","degree":2,"control_points":[[0,0],[5,10],[10,0]]}), + ) + } + _ => unreachable!(), + }; + let mut required = required; + required.insert(0, "type"); + let mut result = json!({"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":properties,"required":required,"additionalProperties":false,"examples":[example],"description":"Creation input, not serialized record properties. Type names are case-insensitive; Polyline aliases LwPolyline. Angles are degrees; coordinates and lengths use drawing units. Additional geometric constraints are checked by preflight and the builder."}); + if *canonical == "Spline" { + result["anyOf"] = json!([{"required":["control_points"]},{"required":["fit_points"]}]); + } + Ok(result) +} + +pub fn validate_entity(spec: &Value, path: &str) -> Result<(), String> { + let kind = spec["type"] + .as_str() + .ok_or_else(|| format!("{path}/type: expected entity type string"))?; + let contract = schema(kind).map_err(|e| format!("{path}{e}"))?; + crate::schema_validation::validate_shape(&contract, spec, path)?; + build_entity(spec, None).map(|_| ()).map_err(|error| { + format!( + "{path}{}: {}", + error["path"].as_str().unwrap_or(""), + error["error"].as_str().unwrap_or("invalid entity") + ) + }) +} + +type Parsed = Result; + +fn failure_at(code: &str, path: &str, error: impl ToString) -> Value { + let mut result = failure(code, error); + result["path"] = json!(path); + result +} + +fn failure(code: &str, error: impl ToString) -> Value { + json!({"ok":false,"status":"failed","code":code,"error":error.to_string()}) +} + +/// Parse `[x,y]` or `[x,y,z]` without silently turning a malformed +/// coordinate into zero. +fn point_value(value: Option<&Value>, label: &str, code: &str) -> Parsed { + let values = value + .and_then(Value::as_array) + .filter(|v| (2..=3).contains(&v.len())) + .ok_or_else(|| failure(code, format!("{label} must be [x,y] or [x,y,z]")))?; + let coord = |i: usize| { + values[i] + .as_f64() + .filter(|v| v.is_finite()) + .ok_or_else(|| failure(code, format!("{label} needs finite coordinates"))) + }; + let z = values + .get(2) + .map(|value| { + value + .as_f64() + .filter(|v| v.is_finite()) + .ok_or_else(|| failure(code, format!("{label} needs finite coordinates"))) + }) + .transpose()? + .unwrap_or(0.0); + Ok(codec::types::Vector3::new(coord(0)?, coord(1)?, z)) +} + +/// `[x,y]` or `[x,y,z]` (missing z = 0) from `req[key]`. +pub(crate) fn point_field(req: &Value, key: &str) -> Parsed { + point_value(req.get(key), key, "invalid_point") +} + +fn point_list_field(spec: &Value, key: &str, minimum: usize) -> Parsed> { + let points = spec[key] + .as_array() + .filter(|points| points.len() >= minimum) + .ok_or_else(|| { + failure( + "invalid_spline_points", + format!("{key} needs at least {minimum} points"), + ) + })?; + points + .iter() + .enumerate() + .map(|(index, point)| { + point_value( + Some(point), + &format!("{key}[{index}]"), + "invalid_spline_points", + ) + }) + .collect() +} + +fn finite_number_list_field(spec: &Value, key: &str) -> Parsed> { + let values = spec[key] + .as_array() + .filter(|values| !values.is_empty()) + .ok_or_else(|| { + failure( + "invalid_spline_values", + format!("{key} needs a non-empty array"), + ) + })?; + values + .iter() + .enumerate() + .map(|(index, value)| { + value + .as_f64() + .filter(|value| value.is_finite()) + .ok_or_else(|| { + failure( + "invalid_spline_values", + format!("{key}[{index}] must be finite"), + ) + }) + }) + .collect() +} + +pub(crate) fn deg_field(req: &Value, key: &str) -> f64 { + req[key].as_f64().unwrap_or(0.0).to_radians() +} + +/// Required finite angle in degrees, converted to radians without a default. +fn finite_angle_field(spec: &Value, key: &str) -> Parsed { + let degrees = spec[key] + .as_f64() + .filter(|value| value.is_finite()) + .ok_or_else(|| failure("invalid_angle", format!("arc needs a finite {key}")))?; + let radians = degrees.to_radians(); + radians + .is_finite() + .then_some(radians) + .ok_or_else(|| failure("invalid_angle", format!("arc needs a finite {key}"))) +} + +fn apply_common_properties(spec: &Value, entity: &mut codec::EntityType) -> Parsed<()> { + if let Some(layer) = spec["layer"].as_str() { + if !layer.is_empty() + && !matches!( + entity, + codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_) + ) + { + entity.common_mut().layer = layer.to_owned(); + } + } + if let Some(index) = spec["color"] + .as_f64() + .filter(|n| n.fract() == 0.0 && (-32768.0..=32767.0).contains(n)) + { + entity.common_mut().color = codec::types::Color::from_index(index as i16); + } + Ok(()) +} + +/// An explicit text style must exist in the current document; otherwise a +/// typo would silently fall back to the default style. +fn text_style_field(spec: &Value, document: Option<&codec::CadDocument>) -> Parsed> { + let Some(style) = spec["style"].as_str().filter(|s| !s.is_empty()) else { + return Ok(None); + }; + if document.is_some_and(|document| !document.text_styles.contains(style)) { + return Err(failure( + "invalid_text_style", + format!("Text style '{style}' does not exist in the document"), + )); + } + Ok(Some(style.to_owned())) +} + +/// One `entities` array entry โ†’ a real entity. Geometry validation happens +/// here so a bad definition aborts the whole batch before anything commits. +pub(crate) fn build_entity( + spec: &Value, + document: Option<&codec::CadDocument>, +) -> Parsed { + use codec::entities::*; + use codec::types::Vector3; + let kind = spec["type"].as_str().unwrap_or("").to_ascii_uppercase(); + // Preserve the public unknown-field error while deriving allowed keys from + // the published contract, rather than maintaining another field list. + if let (Some(object), Ok(schema)) = (spec.as_object(), schema(&kind)) { + for field in object.keys() { + if schema["properties"].get(field).is_none() { + return Err(failure( + "unknown_entity_field", + format!("Unknown field '{field}' for {kind} entity"), + )); + } + } + } + let entity = match kind.as_str() { + "LINE" => { + let mut line = Line::from_points(point_field(spec, "start")?, point_field(spec, "end")?); + if let Some(t) = spec["thickness"].as_f64() { + line.thickness = t; + } + EntityType::Line(line) + } + "CIRCLE" => { + let radius = spec["radius"] + .as_f64() + .filter(|r| r.is_finite() && *r >= 0.0) + .ok_or_else(|| failure("invalid_radius", "circle needs a finite radius >= 0"))?; + EntityType::Circle(Circle::from_center_radius(point_field(spec, "center")?, radius)) + } + "ARC" => { + let radius = spec["radius"] + .as_f64() + .filter(|r| r.is_finite() && *r > 0.0) + .ok_or_else(|| failure("invalid_radius", "arc needs a finite radius > 0"))?; + let arc = Arc::from_center_radius_angles( + point_field(spec, "center")?, + radius, + finite_angle_field(spec, "start_angle_deg")?, + finite_angle_field(spec, "end_angle_deg")?, + ); + if !arc.sweep_angle().is_finite() || arc.sweep_angle() <= 0.0 { + return Err(failure_at( + "invalid_sweep", "/end_angle_deg", + "arc start_angle_deg and end_angle_deg must define a finite non-zero sweep", + )); + } + EntityType::Arc(arc) + } + "SPLINE" => { + let control_points = if spec.get("control_points").is_some() { + point_list_field(spec, "control_points", 2)? + } else { + Vec::new() + }; + let fit_points = if spec.get("fit_points").is_some() { + point_list_field(spec, "fit_points", 2)? + } else { + Vec::new() + }; + if control_points.is_empty() && fit_points.is_empty() { + return Err(failure( + "invalid_spline_points", + "spline needs control_points or fit_points (at least 2 points)", + )); + } + let degree = match spec.get("degree") { + None | Some(Value::Null) => 3, + Some(value) => value + .as_f64() + .filter(|degree| degree.fract() == 0.0 && (1.0..=10.0).contains(degree)) + .ok_or_else(|| { + failure("invalid_spline_values", "degree must be an integer 1..10") + })? as i32, + }; + let count = control_points.len(); + if count > 0 && count <= degree as usize { + return Err(failure_at( + "invalid_spline_values", "/control_points", + format!("a degree {degree} spline needs more than {degree} control_points"), + )); + } + let mut spline = if control_points.is_empty() { + let mut spline = Spline::from_fit_points(fit_points.clone()); + spline.degree = degree; + spline + } else { + Spline::from_control_points(degree, control_points) + }; + if !fit_points.is_empty() { + spline.fit_points = fit_points; + } + if spec.get("knots").is_some() { + if spline.control_points.is_empty() { + return Err(failure( + "invalid_spline_values", + "spline knots require control_points", + )); + } + let knots = finite_number_list_field(spec, "knots")?; + if knots.len() != count + degree as usize + 1 + || knots.windows(2).any(|pair| pair[1] < pair[0]) + { + return Err(failure_at( + "invalid_spline_values", "/knots", + format!( + "knots must be {} non-decreasing values (control_points + degree + 1)", + count + degree as usize + 1 + ), + )); + } + spline.knots = knots; + } + if spec.get("weights").is_some() { + if spline.control_points.is_empty() { + return Err(failure( + "invalid_spline_values", + "spline weights require control_points", + )); + } + let weights = finite_number_list_field(spec, "weights")?; + if weights.len() != count || weights.iter().any(|weight| *weight <= 0.0) { + return Err(failure_at( + "invalid_spline_values", "/weights", + format!("weights must be {count} positive values, one per control point"), + )); + } + spline.weights = weights; + spline.flags.rational = true; + } + if let Some(value) = spec["closed"].as_bool() { + spline.flags.closed = value; + } + if let Some(value) = spec["periodic"].as_bool() { + spline.flags.periodic = value; + } + if let Some(value) = spec["rational"].as_bool() { + spline.flags.rational = value; + } + if let Some(value) = spec["planar"].as_bool() { + spline.flags.planar = value; + } + if let Some(value) = spec["linear"].as_bool() { + spline.flags.linear = value; + } + EntityType::Spline(spline) + } + "LWPOLYLINE" | "POLYLINE" => { + let vertices = spec["vertices"] + .as_array() + .filter(|v| v.len() >= 2) + .ok_or_else(|| failure("invalid_vertices", "polyline needs vertices:[[x,y],โ€ฆ]"))?; + let mut pline = LwPolyline::new(); + for point in vertices { + let x = point[0].as_f64().filter(|v| v.is_finite()).ok_or_else(|| { + failure("invalid_vertices", "polyline vertices must be finite [x,y]") + })?; + let y = point[1].as_f64().filter(|v| v.is_finite()).ok_or_else(|| { + failure("invalid_vertices", "polyline vertices must be finite [x,y]") + })?; + pline.add_point(codec::types::Vector2::new(x, y)); + } + if spec["closed"].as_bool().unwrap_or(false) { + pline.close(); + } + if let Some(width) = spec["constant_width"].as_f64() { + pline.constant_width = width; + } + EntityType::LwPolyline(pline) + } + "POINT" => EntityType::Point(Point::at(point_field(spec, "location")?)), + "TEXT" => { + let value = spec["value"].as_str().unwrap_or(""); + if value.is_empty() { + return Err(failure("invalid_text", "text needs a non-empty value")); + } + let mut text = Text::with_value(value, point_field(spec, "position")?) + .with_height(spec["height"].as_f64().unwrap_or(2.5).abs().max(1e-6)) + .with_rotation(deg_field(spec, "rotation_deg")); + if let Some(style) = text_style_field(spec, document)? { + text.style = style; + } + EntityType::Text(text) + } + "MTEXT" => { + let value = spec["value"].as_str().unwrap_or(""); + if value.is_empty() { + return Err(failure("invalid_text", "mtext needs a non-empty value")); + } + let mut mtext = MText::with_value(value, point_field(spec, "position")?) + .with_height(spec["height"].as_f64().unwrap_or(2.5).abs().max(1e-6)); + if let Some(width) = spec["width"].as_f64().filter(|w| *w > 0.0) { + mtext.rectangle_width = width; + } + mtext.rotation = deg_field(spec, "rotation_deg"); + if let Some(style) = text_style_field(spec, document)? { + mtext.style = style; + } + EntityType::MText(mtext) + } + "INSERT" => { + let block = spec["block"].as_str().unwrap_or(""); + if block.is_empty() { + return Err(failure("block_required", "insert needs a block name")); + } + let mut insert = Insert::new(block, point_field(spec, "position")?) + .with_rotation(deg_field(spec, "rotation_deg")); + match &spec["scale"] { + Value::Number(factor) => { + let factor = factor.as_f64().filter(|f| *f != 0.0).ok_or_else(|| { + failure("invalid_scale", "insert scale must be nonzero") + })?; + insert = insert.with_uniform_scale(factor); + } + Value::Array(factors) if factors.len() == 3 => { + let axis = |i: usize| { + factors[i].as_f64().filter(|f| *f != 0.0).ok_or_else(|| { + failure("invalid_scale", "insert scale components must be nonzero") + }) + }; + insert = insert.with_scale(axis(0)?, axis(1)?, axis(2)?); + } + _ => {} + } + EntityType::Insert(insert) + } + "SOLID" => { + let raw_corners = spec["corners"] + .as_array() + .filter(|v| (3..=4).contains(&v.len())) + .ok_or_else(|| failure("invalid_corners", "solid needs 3 or 4 corners"))?; + let corners: Vec = raw_corners + .iter() + .enumerate() + .map(|(index, value)| { + if value.as_array().is_none_or(|values| values.len() != 3) { + return Err(failure( + "invalid_corners", + format!("corners[{index}] must be [x,y,z]"), + )); + } + point_value( + Some(value), + &format!("corners[{index}]"), + "invalid_corners", + ) + }) + .collect::>()?; + let solid = match corners.as_slice() { + [first, second, third] => Solid::triangle(*first, *second, *third), + [first, second, third, fourth] => Solid::new(*first, *second, *third, *fourth), + _ => unreachable!("corner count was validated above"), + }; + EntityType::Solid(solid) + } + "HATCH" => { + let boundary = spec["boundary"] + .as_array() + .filter(|v| v.len() >= 3) + .ok_or_else(|| failure("invalid_boundary", "hatch needs boundary:[[x,y],โ€ฆ] (>= 3 points)"))?; + let mut vertices = Vec::with_capacity(boundary.len()); + for point in boundary { + let x = point[0].as_f64().filter(|v| v.is_finite()).ok_or_else(|| { + failure("invalid_boundary", "hatch boundary must be finite [x,y]") + })?; + let y = point[1].as_f64().filter(|v| v.is_finite()).ok_or_else(|| { + failure("invalid_boundary", "hatch boundary must be finite [x,y]") + })?; + vertices.push(codec::types::Vector2::new(x, y)); + } + let mut hatch = Hatch::new(); + if spec["solid"].as_bool().unwrap_or(false) { + hatch.is_solid = true; + } else { + hatch.is_solid = false; + hatch.pattern = HatchPattern::new( + spec["pattern"].as_str().unwrap_or("ANSI31"), + ); + hatch.pattern_scale = spec["pattern_scale"].as_f64().unwrap_or(1.0).abs().max(1e-6); + hatch.pattern_angle = deg_field(spec, "pattern_angle_deg"); + } + let mut path = BoundaryPath::external(); + path.add_edge(BoundaryEdge::Polyline(PolylineEdge::new(vertices, true))); + hatch.paths.push(path); + EntityType::Hatch(hatch) + } + other => { + return Err(failure( + "unknown_entity_type", + format!( + "Unknown entity type '{other}'. Supported: Line, Circle, Arc, Spline, LwPolyline, Point, Text, MText, Insert, Solid, Hatch" + ), + )) + } + }; + let mut entity = entity; + apply_common_properties(spec, &mut entity)?; + let contract = schema(&kind).expect("supported factory type"); + crate::schema_validation::validate_shape(&contract, spec, "") + .map_err(|error| failure("invalid_entity_definition", error))?; + Ok(entity) +} diff --git a/src/i18n.rs b/src/i18n.rs index d2ac1e73b..38bea8676 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -365,6 +365,7 @@ pub fn ribbon_module_title(id: &str, fallback: &str) -> String { "layout" => crate::tr!("ribbon-tab", "layout"), "manage" => crate::tr!("ribbon-tab", "manage"), "view" => crate::tr!("ribbon-tab", "view"), + "block_editor" => crate::tr!("ribbon-tab", "block-editor"), _ => fallback.to_string(), } } diff --git a/src/input/mod.rs b/src/input/mod.rs index a9634889b..a079716f2 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1,3 +1,4 @@ //! Device integrations. Camera ownership and CAD commands stay in the app. pub(crate) mod spacemouse; +pub(crate) mod touch; pub(crate) mod trackpad; diff --git a/src/input/touch.rs b/src/input/touch.rs new file mode 100644 index 000000000..325e61d1b --- /dev/null +++ b/src/input/touch.rs @@ -0,0 +1,242 @@ +//! Touch screen gestures on the drawing. One finger needs nothing here: the +//! window moves the cursor with it and the drawing area takes its press and +//! lift as the left button. Two or more fingers become a gesture instead โ€” +//! two drag to pan and pinch to zoom, three drag to orbit; a quick tap with +//! two opens the context menu, with three zooms to the extents, and three +//! swiped up show the isometric view. + +use std::collections::BTreeMap; +use std::time::{Duration, Instant}; + +use iced::touch; +use iced::{Point, Vector}; + +/// What a touch event asks the view to do. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum TouchAction { + /// A second finger came down: drop the press the first one started. + Cancel, + Pan(Vector), + /// Scale the view by `factor` (above 1 zooms in) about `centre`, in + /// window coordinates. + Zoom { factor: f32, centre: Point }, + Orbit(Vector), + Tap(usize), + SwipeUp, +} + +struct Gesture { + started: Instant, + most_fingers: usize, + travel: f32, + /// How far the fingers moved together, without the jumps of fingers + /// landing or lifting. + swept: Vector, + centre: Point, + spread: f32, +} + +/// A tap is quicker and stiller than this. +const TAP_TIME: Duration = Duration::from_millis(300); +const TAP_TRAVEL: f32 = 12.0; +/// A swipe is a quick long sweep; a slower one is just an orbit. +const SWIPE_TRAVEL: f32 = 80.0; +const SWIPE_TIME: Duration = Duration::from_millis(500); +/// The lift ending a gesture reaches the drawing area as a left release +/// just before or after the gesture ends; this long after, it is let be. +const SETTLE: Duration = Duration::from_millis(150); + +#[derive(Default)] +pub(crate) struct TouchGestures { + fingers: BTreeMap, + gesture: Option, + ended: Option, +} + +impl TouchGestures { + /// A gesture is running (or just ended), so the drawing area's own + /// pointer handling should keep out. + pub(crate) fn active(&self) -> bool { + self.gesture.is_some() || self.ended.is_some_and(|ended| ended.elapsed() < SETTLE) + } + + /// Fingers on the screen. + pub(crate) fn fingers(&self) -> usize { + self.fingers.len() + } + + fn centre_and_spread(&self) -> (Point, f32) { + let count = self.fingers.len().max(1) as f32; + let (x, y) = self + .fingers + .values() + .fold((0.0, 0.0), |(x, y), point| (x + point.x, y + point.y)); + let centre = Point::new(x / count, y / count); + let spread = self + .fingers + .values() + .map(|point| point.distance(centre)) + .sum::() + / count; + (centre, spread) + } + + pub(crate) fn event(&mut self, event: touch::Event) -> Vec { + match event { + touch::Event::FingerPressed { id, position } => { + self.fingers.insert(id.0, position); + if self.fingers.len() < 2 { + return Vec::new(); + } + let (centre, spread) = self.centre_and_spread(); + match &mut self.gesture { + Some(gesture) => { + gesture.most_fingers = gesture.most_fingers.max(self.fingers.len()); + gesture.centre = centre; + gesture.spread = spread; + Vec::new() + } + None => { + self.gesture = Some(Gesture { + started: Instant::now(), + most_fingers: self.fingers.len(), + travel: 0.0, + swept: Vector::new(0.0, 0.0), + centre, + spread, + }); + vec![TouchAction::Cancel] + } + } + } + touch::Event::FingerMoved { id, position } => { + if let Some(finger) = self.fingers.get_mut(&id.0) { + *finger = position; + } + let count = self.fingers.len(); + let (centre, spread) = self.centre_and_spread(); + let Some(gesture) = self.gesture.as_mut() else { + return Vec::new(); + }; + let delta = centre - gesture.centre; + gesture.travel += (delta.x * delta.x + delta.y * delta.y).sqrt(); + gesture.swept = gesture.swept + delta; + let mut actions = Vec::new(); + if count == 2 { + actions.push(TouchAction::Pan(delta)); + if gesture.spread > 1.0 && spread > 1.0 { + gesture.travel += (spread - gesture.spread).abs(); + actions.push(TouchAction::Zoom { + factor: spread / gesture.spread, + centre, + }); + } + } else if count >= 3 { + actions.push(TouchAction::Orbit(delta)); + } + gesture.centre = centre; + gesture.spread = spread; + actions + } + touch::Event::FingerLifted { id, .. } | touch::Event::FingerLost { id, .. } => { + self.fingers.remove(&id.0); + if !self.fingers.is_empty() { + // The rest carry on from where they are, without a jump. + let (centre, spread) = self.centre_and_spread(); + if let Some(gesture) = self.gesture.as_mut() { + gesture.centre = centre; + gesture.spread = spread; + } + return Vec::new(); + } + let Some(gesture) = self.gesture.take() else { + return Vec::new(); + }; + self.ended = Some(Instant::now()); + let swept = gesture.swept; + if gesture.started.elapsed() < TAP_TIME && gesture.travel < TAP_TRAVEL { + vec![TouchAction::Tap(gesture.most_fingers)] + } else if gesture.most_fingers >= 3 + && gesture.started.elapsed() < SWIPE_TIME + && swept.y < -SWIPE_TRAVEL + && swept.y.abs() > 2.0 * swept.x.abs() + { + vec![TouchAction::SwipeUp] + } else { + Vec::new() + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn press(id: u64, x: f32, y: f32) -> touch::Event { + touch::Event::FingerPressed { + id: touch::Finger(id), + position: Point::new(x, y), + } + } + fn moved(id: u64, x: f32, y: f32) -> touch::Event { + touch::Event::FingerMoved { + id: touch::Finger(id), + position: Point::new(x, y), + } + } + fn lift(id: u64) -> touch::Event { + touch::Event::FingerLifted { + id: touch::Finger(id), + position: Point::ORIGIN, + } + } + + #[test] + fn two_fingers_pan_pinch_and_tap() { + let mut touch = TouchGestures::default(); + assert!(touch.event(press(1, 100.0, 100.0)).is_empty()); + assert_eq!(touch.event(press(2, 200.0, 100.0)), vec![TouchAction::Cancel]); + assert!(touch.active()); + // Both fingers slide right: a pan, no zoom. + touch.event(moved(1, 110.0, 100.0)); + let actions = touch.event(moved(2, 210.0, 100.0)); + assert!(matches!(actions[0], TouchAction::Pan(v) if (v.x - 5.0).abs() < 1e-3)); + // They spread apart: zoom in. + let actions = touch.event(moved(2, 260.0, 100.0)); + let zooms_in = + |a: &TouchAction| matches!(a, TouchAction::Zoom { factor, .. } if *factor > 1.0); + assert!(actions.iter().any(zooms_in)); + touch.event(lift(1)); + assert!(touch.event(lift(2)).is_empty()); + + // A quick still touch with two fingers is a tap. + touch.event(press(3, 100.0, 100.0)); + touch.event(press(4, 150.0, 100.0)); + touch.event(lift(3)); + assert_eq!(touch.event(lift(4)), vec![TouchAction::Tap(2)]); + } + + #[test] + fn three_fingers_orbit_and_swipe_up() { + let mut touch = TouchGestures::default(); + touch.event(press(1, 100.0, 300.0)); + touch.event(press(2, 150.0, 300.0)); + touch.event(press(3, 200.0, 300.0)); + let mut orbited = false; + for step in 1..=10 { + let y = 300.0 - 15.0 * step as f32; + for (id, x) in [(1, 100.0), (2, 150.0), (3, 200.0)] { + orbited |= touch + .event(moved(id, x, y)) + .iter() + .any(|a| matches!(a, TouchAction::Orbit(_))); + } + } + assert!(orbited); + touch.event(lift(1)); + touch.event(lift(2)); + assert_eq!(touch.event(lift(3)), vec![TouchAction::SwipeUp]); + } +} diff --git a/src/io/fbx.rs b/src/io/fbx.rs new file mode 100644 index 000000000..9b199de77 --- /dev/null +++ b/src/io/fbx.rs @@ -0,0 +1,459 @@ +// FBX export โ€” the drawing's 3D meshes as a binary FBX 7.4 file for +// visualisation tools. Each mesh becomes one model with its own geometry and a +// material in its display colour; coordinates keep the drawing's Z-up axes and +// the unit scale says how many centimetres one drawing unit is. +// +// Binary layout (version 7400): a 27-byte header, then node records. A record +// is its end offset, property count and property byte length (u32 each), its +// name (u8 length + bytes), the properties, its child records, and a 13-byte +// null record closing the child list. The top level ends with a null record +// and the fixed footer readers check. + +use crate::scene::model::mesh_model::MeshModel; + +const VERSION: u32 = 7400; +const HEADER: &[u8] = b"Kaydara FBX Binary \x00\x1a\x00"; +const FILE_ID: [u8; 16] = [ + 0x28, 0xb3, 0x2a, 0xeb, 0xb6, 0x24, 0xcc, 0xc2, 0xbf, 0xc8, 0xb0, 0x2a, 0xa9, 0x2b, 0xfc, 0xf1, +]; +const CREATION_TIME: &str = "1970-01-01 10:00:00:000"; +const FOOTER_ID: [u8; 16] = [ + 0xfa, 0xbc, 0xab, 0x09, 0xd0, 0xc8, 0xd4, 0x66, 0xb1, 0x76, 0xfb, 0x83, 0x1c, 0xf7, 0x26, 0x7e, +]; +const FOOTER_MAGIC: [u8; 16] = [ + 0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0xe3, 0x75, 0x8f, 0x29, 0x0b, +]; + +enum Prop { + Bool(bool), + I32(i32), + I64(i64), + F64(f64), + Str(String), + Raw(Vec), + I32s(Vec), + F64s(Vec), +} + +struct Node { + name: &'static str, + props: Vec, + children: Vec, +} + +fn node(name: &'static str, props: Vec, children: Vec) -> Node { + Node { + name, + props, + children, + } +} + +fn s(value: &str) -> Prop { + Prop::Str(value.to_string()) +} + +/// `P: name, type, label, flags, valuesโ€ฆ` inside a Properties70 block. +fn p(name: &str, kind: &str, label: &str, values: Vec) -> Node { + let mut props = vec![s(name), s(kind), s(label), s("")]; + props.extend(values); + node("P", props, Vec::new()) +} + +fn write_prop(out: &mut Vec, prop: &Prop) { + let array = |out: &mut Vec, code: u8, count: usize, bytes: Vec| { + out.push(code); + out.extend((count as u32).to_le_bytes()); + out.extend(0u32.to_le_bytes()); + out.extend((bytes.len() as u32).to_le_bytes()); + out.extend(bytes); + }; + match prop { + Prop::Bool(value) => out.extend([b'C', u8::from(*value)]), + Prop::I32(value) => { + out.push(b'I'); + out.extend(value.to_le_bytes()); + } + Prop::I64(value) => { + out.push(b'L'); + out.extend(value.to_le_bytes()); + } + Prop::F64(value) => { + out.push(b'D'); + out.extend(value.to_le_bytes()); + } + Prop::Str(value) => { + out.push(b'S'); + out.extend((value.len() as u32).to_le_bytes()); + out.extend(value.as_bytes()); + } + Prop::Raw(value) => { + out.push(b'R'); + out.extend((value.len() as u32).to_le_bytes()); + out.extend(value); + } + Prop::I32s(values) => array( + out, + b'i', + values.len(), + values.iter().flat_map(|v| v.to_le_bytes()).collect(), + ), + Prop::F64s(values) => array( + out, + b'd', + values.len(), + values.iter().flat_map(|v| v.to_le_bytes()).collect(), + ), + } +} + +fn write_node(out: &mut Vec, node: &Node) { + let start = out.len(); + out.extend([0u8; 12]); + out.push(node.name.len() as u8); + out.extend(node.name.as_bytes()); + let props_start = out.len(); + for prop in &node.props { + write_prop(out, prop); + } + let props_len = (out.len() - props_start) as u32; + if !node.children.is_empty() || node.props.is_empty() { + for child in &node.children { + write_node(out, child); + } + out.extend([0u8; 13]); + } + let end = out.len() as u32; + out[start..start + 4].copy_from_slice(&end.to_le_bytes()); + out[start + 4..start + 8].copy_from_slice(&(node.props.len() as u32).to_le_bytes()); + out[start + 8..start + 12].copy_from_slice(&props_len.to_le_bytes()); +} + +/// Geometry, model and material of one mesh, with the connections that place +/// them in the scene. `None` for a mesh without triangles. +fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])> { + let points: Vec<[f64; 3]> = mesh + .verts + .iter() + .enumerate() + .map(|(k, high)| { + let low = mesh.verts_low.get(k).copied().unwrap_or([0.0; 3]); + std::array::from_fn(|axis| high[axis] as f64 + low[axis] as f64) + }) + .collect(); + let mut polygon_indices = Vec::with_capacity(mesh.indices.len()); + let mut normals = Vec::with_capacity(mesh.indices.len() * 3); + for triangle in mesh.indices.chunks_exact(3) { + if triangle.iter().any(|&k| k as usize >= points.len()) { + continue; + } + let [a, b, c] = [triangle[0], triangle[1], triangle[2]].map(|k| points[k as usize]); + let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; + let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]]; + let n = [ + ab[1] * ac[2] - ab[2] * ac[1], + ab[2] * ac[0] - ab[0] * ac[2], + ab[0] * ac[1] - ab[1] * ac[0], + ]; + let length = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt(); + if length <= 0.0 { + continue; + } + // The last corner of a polygon is stored as its bitwise complement. + polygon_indices.extend([triangle[0] as i32, triangle[1] as i32, !(triangle[2] as i32)]); + for _ in 0..3 { + normals.extend(n.map(|value| value / length)); + } + } + if polygon_indices.is_empty() { + return None; + } + let base = 1_000_000 + index as i64 * 3; + let (geometry_id, model_id, material_id) = (base, base + 1, base + 2); + let name = format!("Mesh{}", index + 1); + let geometry = node( + "Geometry", + vec![Prop::I64(geometry_id), s(&format!("{name}\x00\x01Geometry")), s("Mesh")], + vec![ + node("Vertices", vec![Prop::F64s(points.concat())], Vec::new()), + node("PolygonVertexIndex", vec![Prop::I32s(polygon_indices)], Vec::new()), + node("GeometryVersion", vec![Prop::I32(124)], Vec::new()), + node( + "LayerElementNormal", + vec![Prop::I32(0)], + vec![ + node("Version", vec![Prop::I32(101)], Vec::new()), + node("Name", vec![s("")], Vec::new()), + node("MappingInformationType", vec![s("ByPolygonVertex")], Vec::new()), + node("ReferenceInformationType", vec![s("Direct")], Vec::new()), + node("Normals", vec![Prop::F64s(normals)], Vec::new()), + ], + ), + node( + "LayerElementMaterial", + vec![Prop::I32(0)], + vec![ + node("Version", vec![Prop::I32(101)], Vec::new()), + node("Name", vec![s("")], Vec::new()), + node("MappingInformationType", vec![s("AllSame")], Vec::new()), + node("ReferenceInformationType", vec![s("IndexToDirect")], Vec::new()), + node("Materials", vec![Prop::I32s(vec![0])], Vec::new()), + ], + ), + node( + "Layer", + vec![Prop::I32(0)], + vec![ + node("Version", vec![Prop::I32(100)], Vec::new()), + node( + "LayerElement", + Vec::new(), + vec![ + node("Type", vec![s("LayerElementNormal")], Vec::new()), + node("TypedIndex", vec![Prop::I32(0)], Vec::new()), + ], + ), + node( + "LayerElement", + Vec::new(), + vec![ + node("Type", vec![s("LayerElementMaterial")], Vec::new()), + node("TypedIndex", vec![Prop::I32(0)], Vec::new()), + ], + ), + ], + ), + ], + ); + let model = node( + "Model", + vec![Prop::I64(model_id), s(&format!("{name}\x00\x01Model")), s("Mesh")], + vec![ + node("Version", vec![Prop::I32(232)], Vec::new()), + node("Properties70", Vec::new(), Vec::new()), + node("Shading", vec![Prop::Bool(true)], Vec::new()), + node("Culling", vec![s("CullingOff")], Vec::new()), + ], + ); + let [r, g, b, _] = mesh.color.map(f64::from); + let material = node( + "Material", + vec![ + Prop::I64(material_id), + s(&format!("{name}\x00\x01Material")), + s(""), + ], + vec![ + node("Version", vec![Prop::I32(102)], Vec::new()), + node("ShadingModel", vec![s("phong")], Vec::new()), + node("MultiLayer", vec![Prop::I32(0)], Vec::new()), + node( + "Properties70", + Vec::new(), + vec![p( + "DiffuseColor", + "Color", + "", + vec![Prop::F64(r), Prop::F64(g), Prop::F64(b)], + )], + ), + ], + ); + let link = |child: i64, parent: i64| { + node( + "C", + vec![s("OO"), Prop::I64(child), Prop::I64(parent)], + Vec::new(), + ) + }; + Some(( + [geometry, model, material], + [ + link(model_id, 0), + link(geometry_id, model_id), + link(material_id, model_id), + ], + )) +} + +/// A binary FBX file holding `meshes`; `unit_cm` is the length of one drawing +/// unit in centimetres. `None` when no mesh has a triangle. +pub fn build_fbx(meshes: &[&MeshModel], unit_cm: f64) -> Option> { + let mut objects = Vec::new(); + let mut connections = Vec::new(); + for (index, mesh) in meshes.iter().enumerate() { + if let Some((nodes, links)) = mesh_nodes(mesh, index) { + objects.extend(nodes); + connections.extend(links); + } + } + if objects.is_empty() { + return None; + } + let count = |kind: &str| objects.iter().filter(|o| o.name == kind).count() as i32; + let object_type = |kind: &'static str, n: i32| { + node( + "ObjectType", + vec![s(kind)], + vec![node("Count", vec![Prop::I32(n)], Vec::new())], + ) + }; + let (models, geometries, materials) = (count("Model"), count("Geometry"), count("Material")); + let axis = |name: &str, value: i32| p(name, "int", "Integer", vec![Prop::I32(value)]); + let root = [ + node( + "FBXHeaderExtension", + Vec::new(), + vec![ + node("FBXHeaderVersion", vec![Prop::I32(1003)], Vec::new()), + node("FBXVersion", vec![Prop::I32(VERSION as i32)], Vec::new()), + node("Creator", vec![s("OpenCADStudio")], Vec::new()), + ], + ), + node("FileId", vec![Prop::Raw(FILE_ID.to_vec())], Vec::new()), + node("CreationTime", vec![s(CREATION_TIME)], Vec::new()), + node("Creator", vec![s("OpenCADStudio")], Vec::new()), + node( + "GlobalSettings", + Vec::new(), + vec![ + node("Version", vec![Prop::I32(1000)], Vec::new()), + node( + "Properties70", + Vec::new(), + vec![ + axis("UpAxis", 2), + axis("UpAxisSign", 1), + axis("FrontAxis", 1), + axis("FrontAxisSign", -1), + axis("CoordAxis", 0), + axis("CoordAxisSign", 1), + p("UnitScaleFactor", "double", "Number", vec![Prop::F64(unit_cm)]), + ], + ), + ], + ), + node( + "Definitions", + Vec::new(), + vec![ + node("Version", vec![Prop::I32(100)], Vec::new()), + node( + "Count", + vec![Prop::I32(1 + models + geometries + materials)], + Vec::new(), + ), + object_type("GlobalSettings", 1), + object_type("Model", models), + object_type("Geometry", geometries), + object_type("Material", materials), + ], + ), + node("Objects", Vec::new(), objects), + node("Connections", Vec::new(), connections), + ]; + + let mut out = Vec::new(); + out.extend(HEADER); + out.extend(VERSION.to_le_bytes()); + for node in &root { + write_node(&mut out, node); + } + out.extend([0u8; 13]); + out.extend(FOOTER_ID); + out.extend([0u8; 4]); + let pad = match (16 - out.len() % 16) % 16 { + 0 => 16, + pad => pad, + }; + out.extend(std::iter::repeat_n(0u8, pad)); + out.extend(VERSION.to_le_bytes()); + out.extend([0u8; 120]); + out.extend(FOOTER_MAGIC); + Some(out) +} + +/// Web: no save dialog โ€” the export continues with a virtual filename and +/// the generated bytes are delivered as a browser download. +#[cfg(target_arch = "wasm32")] +pub async fn pick_fbx_path_owned(stem: String) -> Option { + Some(std::path::PathBuf::from(format!("{stem}.fbx"))) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Walk the records the way a reader does and return the top-level + /// names, checking every end offset lands inside its parent. + fn top_level_names(bytes: &[u8]) -> Vec { + fn u32_at(bytes: &[u8], at: usize) -> usize { + u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) as usize + } + fn walk( + bytes: &[u8], + mut at: usize, + end: usize, + mut names: Option<&mut Vec>, + ) -> usize { + loop { + let record_end = u32_at(bytes, at); + if record_end == 0 { + return at + 13; + } + assert!(record_end <= end, "record overruns its parent"); + let name_len = bytes[at + 12] as usize; + if let Some(names) = names.as_deref_mut() { + let name = &bytes[at + 13..at + 13 + name_len]; + names.push(String::from_utf8(name.to_vec()).unwrap()); + } + let children = at + 13 + name_len + u32_at(bytes, at + 8); + if children < record_end { + assert_eq!(walk(bytes, children, record_end, None), record_end); + } + at = record_end; + } + } + let mut names = Vec::new(); + let end = walk(bytes, 27, bytes.len(), Some(&mut names)); + assert_eq!(&bytes[end..end + 16], &FOOTER_ID); + assert_eq!(&bytes[bytes.len() - 16..], &FOOTER_MAGIC); + names + } + + fn mesh(verts: Vec<[f32; 3]>, indices: Vec) -> MeshModel { + MeshModel { + name: String::new(), + verts, + verts_low: Vec::new(), + normals: Vec::new(), + indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), + color: [1.0; 4], + selected: false, + } + } + + #[test] + fn a_triangle_writes_a_well_formed_file() { + let triangle = mesh(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], vec![0, 1, 2]); + let bytes = build_fbx(&[&triangle], 0.1).expect("fbx"); + assert!(bytes.starts_with(HEADER)); + assert_eq!( + top_level_names(&bytes), + [ + "FBXHeaderExtension", + "FileId", + "CreationTime", + "Creator", + "GlobalSettings", + "Definitions", + "Objects", + "Connections", + ] + ); + let empty = mesh(Vec::new(), Vec::new()); + assert!(build_fbx(&[&empty], 1.0).is_none()); + } +} diff --git a/src/io/file_association.rs b/src/io/file_association.rs index 50b34e1cb..e3170292e 100644 --- a/src/io/file_association.rs +++ b/src/io/file_association.rs @@ -230,10 +230,26 @@ mod windows_impl { if owns_com { CoUninitialize(); } - result + // Windows 10/11 retired the advanced-association dialog (it fails + // with 0x80070490, #725); its Settings page takes over. + result.or_else(|error| open_default_apps_settings().ok_or(error)) } } + /// Open Settings โ†’ Default apps on this app's page (Windows 11; Windows + /// 10 shows the general page). + fn open_default_apps_settings() -> Option { + let uri = format!( + "ms-settings:defaultapps?registeredAppUser={}", + APP_REGISTRY_NAME.replace(' ', "%20") + ); + std::process::Command::new("explorer.exe").arg(uri).spawn().ok()?; + Some( + "Opened Settings โ†’ Default apps โ€” choose Open CAD Studio for .dwg / .dxf there." + .to_string(), + ) + } + use windows_sys::Win32::System::Registry::{ RegCloseKey, RegCreateKeyExW, RegDeleteTreeW, RegSetValueExW, HKEY, HKEY_CURRENT_USER, KEY_WRITE, REG_OPTION_NON_VOLATILE, REG_SZ, diff --git a/src/io/linetypes.rs b/src/io/linetypes.rs index 527f200ff..92dc46749 100644 --- a/src/io/linetypes.rs +++ b/src/io/linetypes.rs @@ -69,7 +69,7 @@ static COMPLEX_CATALOG: OnceLock> = OnceLock::new(); /// Returns `None` for simple (dash-only) linetypes or unknown names. pub fn complex_lt(name: &str) -> Option<&'static ComplexLt> { COMPLEX_CATALOG - .get_or_init(|| parse_complex(LIN_SOURCE)) + .get_or_init(|| parse_complex(lin_source())) .get(&name.to_ascii_uppercase()) } @@ -232,6 +232,14 @@ pub fn resolve_complex_lt(document: &CadDocument, name: &str) -> Option &'static str { + static SOURCE: OnceLock = OnceLock::new(); + SOURCE.get_or_init(|| { + format!("{LIN_SOURCE}\n{}", crate::io::patterns::user_definitions("linetypes", "lin")) + }) +} + // โ”€โ”€ Pattern art extraction โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /// Extract the ASCII-art portion of a LIN description. @@ -257,7 +265,7 @@ pub fn extract_pattern(desc: &str) -> String { /// Add all standard OpenCADStudio linetypes to `doc`, skipping existing ones. pub fn populate_document(doc: &mut CadDocument) { - populate_document_from_source(doc, LIN_SOURCE); + populate_document_from_source(doc, lin_source()); } /// Add the linetypes of a caller-supplied `.lin` source, embedded shapes and diff --git a/src/io/mod.rs b/src/io/mod.rs index 2cbd5a5ed..60cf317c1 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -25,6 +25,7 @@ pub mod print_to_printer; pub mod recovery; #[cfg(not(target_arch = "wasm32"))] pub mod single_instance; +pub mod fbx; pub mod step; pub mod stl; pub mod svg_export; @@ -2254,7 +2255,17 @@ fn replace_save_file(temp_path: &Path, path: &Path) -> std::io::Result<()> { // drawing that was hidden before the save stays hidden. (#1414) let was_hidden = std::fs::metadata(path) .is_ok_and(|meta| meta.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0); - replace_save_file_inner(temp_path, path)?; + // Sync clients (Dropbox, OneDrive), indexers and virus scanners open a + // freshly written drawing for a moment without delete sharing, which + // refuses the replace. Retry briefly before reporting the file in use. + let mut attempt = 0; + while let Err(error) = replace_save_file_inner(temp_path, path) { + attempt += 1; + if attempt >= 10 || !replace_error_is_file_in_use(&error) { + return Err(error); + } + std::thread::sleep(std::time::Duration::from_millis(200)); + } if !was_hidden { if let Ok(meta) = std::fs::metadata(path) { let attributes = meta.file_attributes(); diff --git a/src/io/patterns.rs b/src/io/patterns.rs index 47f6f1c38..413b840e9 100644 --- a/src/io/patterns.rs +++ b/src/io/patterns.rs @@ -39,9 +39,41 @@ pub struct PatternDef { static CATALOG: OnceLock> = OnceLock::new(); -/// All parsed patterns, in file order. +/// All parsed patterns, in file order: the bundled catalog, then the user's +/// own `.pat` files. pub fn catalog() -> &'static [PatternDef] { - CATALOG.get_or_init(|| parse(PAT_SRC)) + CATALOG.get_or_init(|| parse(&format!("{PAT_SRC}\n{}", user_definitions("patterns", "pat")))) +} + +/// The text of every `*.` file in `/`, where users drop +/// their own hatch patterns and linetypes. Bundled names come first, so a +/// user file adds definitions rather than replacing the built-in ones. +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn user_definitions(folder: &str, ext: &str) -> String { + let Some(dir) = crate::config::config_dir().map(|dir| dir.join(folder)) else { + return String::new(); + }; + let Ok(entries) = std::fs::read_dir(dir) else { + return String::new(); + }; + let mut paths: Vec<_> = entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().is_some_and(|e| e.eq_ignore_ascii_case(ext))) + .collect(); + paths.sort(); + paths + .iter() + .filter_map(|path| std::fs::read(path).ok()) + .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()) + .collect::>() + .join("\n") +} + +/// The web build has no user folder. +#[cfg(target_arch = "wasm32")] +pub(crate) fn user_definitions(_folder: &str, _ext: &str) -> String { + String::new() } // โ”€โ”€ Geometry helper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 52a230f05..2a8f650c7 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -827,6 +827,13 @@ fn append_pdf_page( } [r, g, b] = plotted_color([r, g, b], a, screening, options); + // A wide polyline's band is geometry: square ends at its vertices + // and mitred corners, whatever pen the plot style gives lines. + let (cap, join) = if wire.world_width > 0.0 { + (Some(LineCapStyle::Butt), Some(LineJoinStyle::Miter)) + } else { + (cap, join) + }; let cap = cap.unwrap_or(LineCapStyle::Round); if last_cap != Some(cap) { ops.push(Op::SetLineCapStyle { cap }); @@ -890,7 +897,20 @@ fn append_pdf_page( // Linetype dash pattern. Without this every wire exported as a solid // line regardless of its linetype (dashed / centre / dash-dot). (#155) let dash_arr = dash_array_from_pattern(wire.pattern_length, &wire.pattern, MM_TO_PT); - let stationed = !dash_arr.is_empty() && wire.pattern_stations.len() > wire.points.len(); + // Dash entries are whole points in the plot's user space, so a + // pattern only a fraction of a drawing unit long (an inch sheet) + // rounds away; such a wire is cut into its dashes here (#1739). + let page_h = paper_h as f64 / scale.max(1e-6) as f64; + let synthesized; + let stations: &[f32] = if wire.pattern_stations.len() > wire.points.len() { + &wire.pattern_stations + } else if !dash_arr.is_empty() && dash_rounds_off(&wire.pattern) { + synthesized = length_stations(wire, page_h); + &synthesized + } else { + &[] + }; + let stationed = !dash_arr.is_empty() && !stations.is_empty(); if stationed { if last_dash.as_ref().is_none_or(|dash| !dash.is_empty()) { ops.push(Op::SetLineDashPattern { @@ -903,11 +923,11 @@ fn append_pdf_page( { continue; } - let start = wire.point_world(index, paper_h as f64 / scale.max(1e-6) as f64); - let end = wire.point_world(index + 1, paper_h as f64 / scale.max(1e-6) as f64); + let start = wire.point_world(index, page_h); + let end = wire.point_world(index + 1, page_h); for [from, to] in visible_station_ranges( - wire.pattern_stations[index], - wire.pattern_stations[index + 1], + stations[index], + stations[index + 1], wire.pattern_length, &wire.pattern, ) { @@ -943,18 +963,95 @@ fn append_pdf_page( let mut segment: Vec = Vec::new(); let dot_radius = (wire.name == "viewport_hatch_pattern") .then_some(Pt(SCREEN_DOT_MM * MM_TO_PT / (2.0 * scale.max(1e-6)))); + // A tapered band varies along each segment, which one stroke width + // cannot draw: each segment fills its own quad, and a segment + // without width is a plain line at the pen weight. + if wire.world_width > 0.0 && !wire.taper_widths.is_empty() { + let page_h = paper_h as f64 / scale.max(1e-6) as f64; + let at = |i: usize| { + let p = wire.point_world(i, page_h); + [p.x + ox, p.y + oy] + }; + let mm = |[x, y]: [f64; 2]| LinePoint { + p: Point::new(Mm(x as f32), Mm(y as f32)), + bezier: false, + }; + let thin = if options.object_lineweights { + lw_override.unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1)) + } else { + 0.1 + } / pen_divisor; + for i in 0..wire.points.len().saturating_sub(1) { + if !wire.points[i][0].is_finite() || !wire.points[i + 1][0].is_finite() { + continue; + } + let (a, b) = (at(i), at(i + 1)); + let half = |k: usize| { + f64::from(wire.taper_widths.get(k).copied().unwrap_or(0.0)) * 0.5 + }; + let (wa, wb) = (half(i), half(i + 1)); + let (dx, dy) = (b[0] - a[0], b[1] - a[1]); + let len = dx.hypot(dy); + if len <= 1e-12 { + continue; + } + if wa <= 0.0 && wb <= 0.0 { + if last_lw.is_none_or(|l| (l - thin).abs() > 0.01) { + ops.push(Op::SetOutlineThickness { pt: Pt(thin) }); + last_lw = Some(thin); + } + flush_line(&mut ops, &[mm(a), mm(b)], None); + continue; + } + let n = [-dy / len, dx / len]; + let side = + |p: [f64; 2], w: f64, s: f64| [p[0] + n[0] * w * s, p[1] + n[1] * w * s]; + ops.push(Op::DrawPolygon { + polygon: Polygon { + rings: vec![PolygonRing { + points: vec![ + mm(side(a, wa, 1.0)), + mm(side(b, wb, 1.0)), + mm(side(b, wb, -1.0)), + mm(side(a, wa, -1.0)), + ], + }], + mode: PaintMode::Fill, + winding_order: WindingOrder::NonZero, + }, + }); + } + continue; + } + + // A band's segments arrive pen-up separated; where the next one + // starts on the last one's end it stays in the same path, so the + // corner gets the mitre join instead of two overlapping ends. + let band = wire.world_width > 0.0; + let mut pen_up = false; for (pi, &[x, y, _z]) in wire.points.iter().enumerate() { if x.is_nan() || y.is_nan() { - flush_line(&mut ops, &segment, dot_radius); - segment.clear(); + if band { + pen_up = true; + } else { + flush_line(&mut ops, &segment, dot_radius); + segment.clear(); + } } else { let point = wire.point_world(pi, paper_h as f64 / scale.max(1e-6) as f64); - let wx = (point.x + ox) as f32; - let wy = (point.y + oy) as f32; - segment.push(LinePoint { - p: Point::new(Mm(wx), Mm(wy)), - bezier: false, - }); + let p = Point::new(Mm((point.x + ox) as f32), Mm((point.y + oy) as f32)); + if pen_up { + pen_up = false; + let joined = segment.last().is_some_and(|last| { + (last.p.x.0 - p.x.0).abs() <= 1e-4 && (last.p.y.0 - p.y.0).abs() <= 1e-4 + }); + if joined { + continue; + } + flush_line(&mut ops, &segment, dot_radius); + segment.clear(); + } + segment.push(LinePoint { p, bezier: false }); } } flush_line(&mut ops, &segment, dot_radius); @@ -1059,6 +1156,38 @@ fn dash_array_from_pattern(pattern_length: f32, pattern: &[f32; 8], mm_to_pt: f3 .collect() } +/// Whether a pattern element is too short for a whole-point dash entry to +/// carry it within 5 %. +fn dash_rounds_off(pattern: &[f32; 8]) -> bool { + pattern + .iter() + .any(|value| *value != 0.0 && value.abs() * MM_TO_PT < 10.0) +} + +/// Distance along the wire at each point (restarting after a pen-up), plus +/// the total, in the layout `visible_station_ranges` reads. +fn length_stations(wire: &crate::scene::WireModel, page_h: f64) -> Vec { + let mut stations = Vec::with_capacity(wire.points.len() + 1); + let mut along = 0.0_f64; + let mut previous: Option = None; + for index in 0..wire.points.len() { + if !wire.points[index][0].is_finite() { + previous = None; + along = 0.0; + stations.push(0.0); + continue; + } + let point = wire.point_world(index, page_h); + if let Some(previous) = previous { + along += point.distance(previous); + } + previous = Some(point); + stations.push(along as f32); + } + stations.push(along as f32); + stations +} + fn visible_station_ranges( start: f32, end: f32, diff --git a/src/io/xref.rs b/src/io/xref.rs index e464a3b12..d051e12c8 100644 --- a/src/io/xref.rs +++ b/src/io/xref.rs @@ -654,14 +654,14 @@ pub fn layer_overrides(doc: &CadDocument, name: &str, raw_path: &str, base_dir: /// to the name-keyed bytes registry instead (trailing-name match); the /// returned path is the registry key. #[cfg(target_arch = "wasm32")] -fn resolve_path(raw: &str, _base_dir: &Path) -> Option { +pub(crate) fn resolve_path(raw: &str, _base_dir: &Path) -> Option { find_registered_source(raw).map(PathBuf::from) } /// Try to build an absolute path from a raw xref path string. /// Handles absolute paths, relative paths, and Windows-style separators. #[cfg(not(target_arch = "wasm32"))] -fn resolve_path(raw: &str, base_dir: &Path) -> Option { +pub(crate) fn resolve_path(raw: &str, base_dir: &Path) -> Option { let normalised = raw.replace('\\', "/"); let p = PathBuf::from(&normalised); diff --git a/src/lib.rs b/src/lib.rs index 210df2614..8c097b908 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod command; pub mod config; pub mod discussions; pub mod entities; +pub(crate) mod entity_creation; #[cfg(not(target_arch = "wasm32"))] pub mod gpu_backend; pub mod i18n; @@ -27,6 +28,7 @@ pub mod plugin; #[cfg(not(target_arch = "wasm32"))] pub mod rest; pub mod scene; +pub(crate) mod schema_validation; pub mod snap; pub mod sys; pub mod sysinfo; diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 9ce668e84..92111d789 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -3,6 +3,97 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> { match source { + "Add to Working Set" => Some(("ref-edit", "add-to-working-set")), + "Remove from Working Set" => Some(("ref-edit", "remove-from-working-set")), + "Select reference:" => Some(("ref-edit", "select-reference")), + "Reference not found." => Some(("ref-edit", "reference-not-found")), + "Object is not a valid reference" => Some(("ref-edit", "not-valid-reference")), + "1 entity added" => Some(("ref-edit", "one-entity-added")), + "Select nesting level [Ok/Next] :" => Some(("ref-edit", "nesting-level")), + "Enter object selection method [All/Nested] :" => Some(("ref-edit", "selection-method")), + "Select nested objects:" => Some(("ref-edit", "select-nested")), + "Display attribute definitions [Yes/No] :" => Some(("ref-edit", "display-attdefs")), + "0 items selected" => Some(("ref-edit", "zero-items-selected")), + "1 item selected" => Some(("ref-edit", "one-item-selected")), + "{} items selected" => Some(("ref-edit", "items-selected")), + "Enter an option [Add/Remove] :" => Some(("ref-edit", "refset-option")), + "Enter option [Save/Discard reference changes] :" => Some(("ref-edit", "refclose-option")), + "** Command not allowed unless a reference is checked out with REFEDIT command **" => Some(("ref-edit", "not-checked-out")), + "** Command not allowed, {} already checked out for editing **" => Some(("ref-edit", "already-checked-out")), + "Transfer objects between the RefEdit working set and host drawing..." => Some(("ref-edit", "transfer-objects")), + "{} Added to working set" => Some(("ref-edit", "added-to-working-set")), + "{} Removed from working set" => Some(("ref-edit", "removed-from-working-set")), + "1 object added to {}" => Some(("ref-edit", "one-object-added")), + "{} objects added to {}" => Some(("ref-edit", "objects-added")), + "1 object removed from {}" => Some(("ref-edit", "one-object-removed")), + "{} objects removed from {}" => Some(("ref-edit", "objects-removed")), + "1 xref instance updated" => Some(("ref-edit", "one-xref-instance")), + "{} xref instances updated" => Some(("ref-edit", "xref-instances")), + "1 block instance updated" => Some(("ref-edit", "one-block-instance")), + "{} block instances updated" => Some(("ref-edit", "block-instances")), + "{} redefined and reloaded." => Some(("ref-edit", "redefined-reloaded")), + "{} redefined." => Some(("ref-edit", "redefined")), + "Ok" => Some(("ref-edit", "ok")), + "Nested" => Some(("ref-edit", "nested")), + "Operational Symbols" => Some(("property-patch", "patch-07")), + "Decimal Separator" => Some(("property-patch", "patch-06")), + "Open 3Dconnexion settings..." => Some(("property-patch", "patch-05")), + "Open 3Dconnexion settingsโ€ฆ" => Some(("property-patch", "patch-04")), + "Pan and zoom on paper sheets; full navigation in model views." => Some(("property-patch", "patch-03")), + "Follow context" => Some(("property-patch", "patch-02")), + "3DxWare unavailable" => Some(("property-patch", "patch-01")), + "Downloading..." => Some(("modal", "missing-fonts-downloading")), + "Download available" => Some(("modal", "missing-fonts-download-available")), + "Skip" => Some(("modal", "missing-fonts-skip")), + "Missing fonts" => Some(("modal", "missing-fonts-title")), + "This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute." => Some(("modal", "missing-fonts-description")), + "Font source โ€” leave empty for the community repository, or point at any folder (e.g. your company's font server):" => Some(("modal", "missing-fonts-source")), + "https://server/fonts or a GitHub raw folder" => Some(("modal", "missing-fonts-source-placeholder")), + "Coincident" => Some(("common", "parametric-coincident")), + "Colinear" => Some(("common", "parametric-colinear")), + "Concentric" => Some(("common", "parametric-concentric")), + "Symmetric" => Some(("common", "parametric-symmetric")), + "Equal" => Some(("common", "parametric-equal")), + "Fix" => Some(("common", "parametric-fix")), + "GPU (backend)" => Some(("ui", "window-gpu-backend")), + "Click + Add alias, type the alias and command, accept the row, then Apply. Esc cancels the pending row." => Some(("ui", "window-click-add-alias-type-the-alias-and-command-accep")), + "Number of aliases" => Some(("ui", "window-number-of-aliases")), + "Are you sure you want to reset? You will lose all your current aliases!" => Some(("ui", "window-are-you-sure-you-want-to-reset-you-will-lose-all")), + "Alias already used for command: {} โ†’ {}" => Some(("ui", "window-alias-already-used-for-command")), + "Origin planes" => Some(("ui", "window-origin-planes")), + "No sketch open" => Some(("ui", "window-no-sketch-open")), + "Bodies ({})" => Some(("ui", "window-bodies")), + "No solids yet" => Some(("ui", "window-no-solids-yet")), + "Saved immediately. Changes take effect the next time Open CAD Studio starts." => Some(("ui", "window-saved-immediately-changes-take-effect-the-next-t")), + "This session" => Some(("ui", "window-this-session")), + "Driver" => Some(("ui", "window-driver")), + "Backend" => Some(("ui", "window-backend")), + "Selected by" => Some(("ui", "window-selected-by")), + "Next launch" => Some(("ui", "window-next-launch")), + "Graphics backend" => Some(("ui", "window-graphics-backend")), + "Use the compatibility renderer (for GPUs without shader storage buffers)" => Some(("ui", "window-use-the-compatibility-renderer-for-gpus-without")), + "Prefer OpenGL on older GPUs (automatic backend only)" => Some(("ui", "window-prefer-opengl-on-older-gpus-automatic-backend-on")), + "If a pinned backend fails to start, the next launch falls back to automatic selection. The --backend option and the WGPU_BACKEND variable override this page." => Some(("ui", "window-if-a-pinned-backend-fails-to-start-the-next-laun")), + "Paused" => Some(("ui", "window-paused")), + "Enable SpaceMouse" => Some(("ui", "window-enable-spacemouse")), + "Navigation" => Some(("ui", "window-navigation")), + "Reverse pan direction" => Some(("ui", "window-reverse-pan-direction")), + "Pan speed" => Some(("ui", "window-pan-speed")), + "Resume SpaceMouse" => Some(("ui", "window-resume-spacemouse")), + "Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here." => Some(("ui", "window-configure-3d-navigation-buttons-and-radial-menus")), + "Connection details" => Some(("ui", "window-connection-details")), + "{:.1}% a notch" => Some(("ui", "window-1-a-notch")), + "The slider covers the system variable's {} to {}; the field takes up to {} for a faster wheel." => Some(("ui", "window-the-slider-covers-the-system-variable-s-to-the-f")), + "Bundled with Open CAD Studio" => Some(("ui", "window-bundled-with-open-cad-studio")), + "How the Reference Manager works on web" => Some(("ui", "window-how-the-reference-manager-works-on-web")), + "This panel lists the drawing's references read-only: status, type, size, date and saved path, plus the Details pane. Select rows to inspect them; List and Tree switch the presentation." => Some(("ui", "window-this-panel-lists-the-drawing-s-references-read-o")), + "Attach, Refresh and Change Path stay disabled here: the web build has no filesystem access and no native file pickers, so references can be neither added, reloaded nor repathed โ€” and previews stay unavailable. Nested references are read-only on every platform, and relative paths need the drawing saved first." => Some(("ui", "window-attach-refresh-and-change-path-stay-disabled-her")), + "Hit something broken or missing? Report it on the GitHub issues page so it can be fixed." => Some(("ui", "window-hit-something-broken-or-missing-report-it-on-the")), + "Report an issue" => Some(("ui", "window-report-an-issue")), + "Reference Manager โ€” Web" => Some(("ui", "window-reference-manager-web")), + "EXTERNALREFERENCES on the web build" => Some(("ui", "window-externalreferences-on-the-web-build")), + "Embed Image (in drawing)" => Some(("ui", "window-embed-image-in-drawing")), + "Create Sheet Set" => Some(("view", "ss-create-sheet-set")), "Requires an integer between {min} and {max}." => Some(("view", "ss-requires-integer-between")), "New Sheet" => Some(("view", "ss-new-sheet")), @@ -498,13 +589,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "ACI:" => Some(("styles", "aci")), "ADDSELECTED: creating a new {kind} is not supported." => Some(("inquiry", "addselected-creating-a-new-kind-is-not-support")), "ADDSELECTED: selected object not found." => Some(("inquiry", "addselected-selected-object-not-found")), - "ADJUST [Brightness / Contrast / Fade]:" => Some(("view", "adjust-brightness-contrast-fade")), - "ADJUST brightness 0-100:" => Some(("view", "adjust-brightness-0-100")), - "ADJUST contrast 0-100:" => Some(("view", "adjust-contrast-0-100")), - "ADJUST fade 0-100:" => Some(("view", "adjust-fade-0-100")), - "ADJUST: select raster image(s) first." => Some(("view", "adjust-select-raster-image-s-first")), - "ADJUST: value must be 0-100." => Some(("view", "adjust-value-must-be-0-100")), - "ADJUST: {action} = {v} on {changed} image(s)." => Some(("view", "adjust-action-v-on-changed-image-s")), "ALIGN Scale objects based on alignment points? [Y/N]:" => Some(("modify", "align-scale-objects-based-on-alignment-points")), "ALIGN Select objects (%{count} selected, Enter when done):" => Some(("modify", "align-select-objects-count-selected-enter-when")), "ALIGN Specify 1st destination point:" => Some(("modify", "align-specify-1st-destination-point")), @@ -627,6 +711,13 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Hide Details" => Some(("insert", "xref-attach-hide-details")), "Select Reference File" => Some(("insert", "xref-attach-select-file")), "All Reference Files" => Some(("insert", "xref-attach-all-files")), + "All image files" => Some(("insert", "xref-attach-filter-images")), + "DWF files (*.dwfx; *.dwf)" => Some(("insert", "xref-attach-filter-dwf")), + "MicroStation DGN (*.dgn)" => Some(("insert", "xref-attach-filter-dgn")), + "All DGN Files (*.*)" => Some(("insert", "xref-attach-filter-all-dgn")), + "PDF files (*.pdf)" => Some(("insert", "xref-attach-filter-pdf")), + "Point Cloud (*.rcp; *.rcs)" => Some(("insert", "xref-attach-filter-point-cloud")), + "All files (*.*)" => Some(("insert", "xref-attach-filter-all")), "Inserts references to external files such as other drawings, raster images, and underlays." => Some(("insert", "xref-attach-help")), "Invalid input." => Some(("insert", "xref-attach-invalid-input")), "Insertion point X" => Some(("insert", "xref-insertion-point-x")), @@ -743,6 +834,22 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "BACKGROUND colour [Default/Black/DarkGray/Gray/LightGray/White] or R G B (0โ€“255):" => Some(("file", "background-colour-default-black-darkgray-gray")), "BASE Specify base point:" => Some(("insert", "base-specify-base-point")), "BEDIT Select block reference to edit:" => Some(("modify", "bedit-select-block-reference-to-edit")), + "BVSTATE Enter an option [New/Set/Rename/Delete] :" => Some(("modify", "bvstate-option")), + "BVSTATE Enter name of new visibility state:" => Some(("modify", "bvstate-new-name")), + "BVSTATE Enter visibility state to make current <{}>:" => Some(("modify", "bvstate-set")), + "BVSTATE Enter visibility state to rename <{}>:" => Some(("modify", "bvstate-rename-from")), + "BVSTATE Enter new name:" => Some(("modify", "bvstate-rename-to")), + "BVSTATE Enter visibility state to delete:" => Some(("modify", "bvstate-delete")), + "Visibility state \"{}\" not found." => Some(("modify", "bvstate-not-found")), + "Visibility state \"{}\" already exists." => Some(("modify", "bvstate-exists")), + "The only visibility state cannot be deleted." => Some(("modify", "bvstate-last")), + "{}: open the block in the Block Editor first (BEDIT)." => Some(("modify", "bvstate-needs-bedit")), + "Current visibility state: {}" => Some(("modify", "bvstate-current")), + "Select objects to hide:" => Some(("modify", "bvhide-select")), + "Select objects to make visible:" => Some(("modify", "bvshow-select")), + "ARRAYEDIT Select array:" => Some(("modify", "arrayedit-select-array")), + "ARRAYEDIT Enter an option [Source/eXit] :" => Some(("modify", "arrayedit-options")), + "ARRAYEDIT: the selected object is not an associative array." => Some(("modify", "arrayedit-not-an-array")), "BEDIT: Block \"{}\" edit discarded." => Some(("inquiry", "bedit-block-edit-discarded")), "BEDIT: Block \"{}\" saved. All references updated." => Some(("inquiry", "bedit-block-saved-all-references-updated")), "BEDIT: Editing block \"{}\". Use Save Block or Discard to finish." => Some(("inquiry", "bedit-editing-block-use-save-block-or-discard")), @@ -1539,6 +1646,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Display and plot frames" => Some(("insert", "display-and-plot-frames")), "Display but don't plot frames" => Some(("insert", "display-not-plot-frames")), "*Frames vary*" => Some(("insert", "frames-vary")), + "*Underlay OSnap's vary*" => Some(("insert", "underlay-osnaps-vary")), "Regenerating model." => Some(("insert", "regenerating-model")), "Requires an integer between 0 and 2." => Some(("insert", "requires-int-0-2")), "Frames On, Not Plotted" => Some(("draw", "frames-on-not-plotted")), @@ -2030,6 +2138,25 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "MText" => Some(("common", "mtext")), "MVIEW Specify corner of viewport or [Polygonal/Object/Fit/Insert view]:" => Some(("layout", "mview-specify-corner-of-viewport-or-polygonal")), "MVIEW Specify opposite corner:" => Some(("layout", "mview-specify-opposite-corner")), + "MVSETUP Enable paper space? [No/Yes] :" => Some(("layout", "mvsetup-enable-paper-space")), + "MVSETUP Enter the scale factor <1>:" => Some(("layout", "mvsetup-enter-scale-factor")), + "MVSETUP Enter the paper width:" => Some(("layout", "mvsetup-enter-paper-width")), + "MVSETUP Enter the paper height:" => Some(("layout", "mvsetup-enter-paper-height")), + "MVSETUP Enter an option [Create/Delete objects/Scale viewports]:" => Some(("layout", "mvsetup-enter-option")), + "MVSETUP Select the objects to delete:" => Some(("layout", "mvsetup-select-objects-to-delete")), + "Delete objects" => Some(("layout", "delete-objects-option")), + "MVSETUP Enter viewport layout [Single/Engineering/Array] :" => Some(("layout", "mvsetup-enter-viewport-layout")), + "MVSETUP Specify first corner of bounding area for viewport(s) :" => Some(("layout", "mvsetup-specify-first-corner")), + "MVSETUP Specify opposite corner:" => Some(("layout", "mvsetup-specify-opposite-corner")), + "MVSETUP Enter number of viewports in X direction <2>:" => Some(("layout", "mvsetup-viewports-in-x")), + "MVSETUP Enter number of viewports in Y direction <2>:" => Some(("layout", "mvsetup-viewports-in-y")), + "MVSETUP Specify distance between viewports in X direction <0>:" => Some(("layout", "mvsetup-distance-in-x")), + "MVSETUP Specify distance between viewports in Y direction <0>:" => Some(("layout", "mvsetup-distance-in-y")), + "MVSETUP Select the viewports to scale:" => Some(("layout", "mvsetup-select-viewports")), + "MVSETUP Enter the number of paper space units <1>:" => Some(("layout", "mvsetup-paper-space-units")), + "MVSETUP Enter the number of model space units <1>:" => Some(("layout", "mvsetup-model-space-units")), + "MVSETUP The viewports do not fit in the bounding area." => Some(("layout", "mvsetup-viewports-do-not-fit")), + "Scale viewports" => Some(("layout", "scale-viewports-option")), "MVIEW Insert view Choose a named view or [New]:" => Some(("layout", "mview-insert-view-choose-a-named-view-or-new")), "MVIEW Insert view No named views; choose [New]:" => Some(("layout", "mview-insert-view-no-named-views-choose-new")), "MVIEW Insert view Specify placement point:" => Some(("layout", "mview-insert-view-specify-placement-point")), @@ -2321,6 +2448,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "PAPER" => Some(("ui", "paper")), "PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space" => Some(("ui", "paper-double-click-viewport-to-enter-mspace-mo")), "PASTEBLOCK: clipboard is empty." => Some(("insert", "pasteblock-clipboard-is-empty")), + "PASTESPEC Paste as [Objects/Block/Text]:" => Some(("insert", "pastespec-paste-as")), + "PASTESPEC Specify insertion point:" => Some(("insert", "pastespec-specify-insertion-point")), "PASTEBLOCK: {e}" => Some(("insert", "pasteblock-e")), "PASTECLIP Pick insertion point:" => Some(("clipboard", "pasteclip-pick-insertion-point")), "PASTEORIG: clipboard is empty." => Some(("insert", "pasteorig-clipboard-is-empty")), @@ -2805,6 +2934,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "SLICE: a result could not be committed; all source objects were retained." => Some(("common", "slice-result-commit-failed")), "SLICE: a retained result could not be updated." => Some(("common", "slice-retained-update-failed")), "SLICE: created %{count} result object(s)." => Some(("common", "slice-created-results")), + "SECTIONCULL: a cut object could not be prepared; nothing was changed." => Some(("common", "sectioncull-prepare-failed")), + "SECTIONCULL: erased %{erased} object(s) and cut %{cut} across the section." => Some(("common", "sectioncull-result")), + "SECTIONCULL: nothing lies outside the section." => Some(("common", "sectioncull-nothing-outside")), + "SECTIONCULL: select a section object or turn on a live section." => Some(("common", "sectioncull-no-section")), "SOLID Specify first point:" => Some(("draw", "solid-specify-first-point")), "SOLID Specify fourth point:" => Some(("draw", "solid-specify-fourth-point")), "SOLID Specify second point:" => Some(("draw", "solid-specify-second-point")), @@ -2820,6 +2953,11 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "STEPOUT: exported to \"{}\"" => Some(("common", "stepout-exported-to")), "STEPOUT: no 3D mesh data in this drawing." => Some(("common", "stepout-no-3d-mesh-data-in-this-drawing")), "STEPOUT: {error}" => Some(("common", "stepout-error")), + "FBXEXPORT: exported to \"{}\"" => Some(("common", "fbxexport-exported-to")), + "FBXEXPORT: no 3D mesh data in this drawing." => Some(("common", "fbxexport-no-3d-mesh-data-in-this-drawing")), + "FBXEXPORT: {error}" => Some(("common", "fbxexport-error")), + "Export FBX" => Some(("common", "export-fbx")), + "FBX Files" => Some(("common", "fbx-files")), "STLOUT: exported to \"{}\"" => Some(("common", "stlout-exported-to")), "STLOUT: no 3D mesh data in this drawing." => Some(("common", "stlout-no-3d-mesh-data-in-this-drawing")), "STLOUT: {error}" => Some(("common", "stlout-error")), @@ -2935,6 +3073,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Shade plot" => Some(("common", "shade-plot")), "Shaded" => Some(("view", "shaded")), "Shaded viewport options" => Some(("plot", "shaded-viewport-options")), + "Shaded with edges" => Some(("common", "shaded-with-edges")), + "Shades of Gray" => Some(("common", "shades-of-gray")), "Shadow flags" => Some(("common", "shadow-flags")), "Shadows" => Some(("properties", "shadows")), "Shape" => Some(("properties", "shape")), @@ -2961,6 +3101,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "SingleLine" => Some(("properties", "singleline")), "Size" => Some(("common", "size")), "Sketch" => Some(("draw", "sketch")), + "Sketchy" => Some(("common", "sketchy")), "Snap override: {label} (next pick only)." => Some(("common", "snap-override-label-next-pick-only")), "Snap to\nUnderlays" => Some(("insert", "snap-to-underlays")), "Softness" => Some(("properties", "softness")), @@ -3423,7 +3564,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Updated {changed} underlay(s)." => Some(("view", "updated-changed-underlay-s")), "Upper" => Some(("annotate", "upper")), "Upside down" => Some(("common", "upside-down")), - "Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>" => Some(("view", "usage-adjust-brightness-contrast-fade-0-100")), "Usage: ANNOSCALE e.g. 1:50, 2:1, or a factor" => Some(("view", "usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor")), "Usage: ATTDISP ON | OFF | NORMAL" => Some(("draw", "usage-attdisp-on-off-normal")), "Usage: ATTSYNC . Blocks: {}" => Some(("insert", "usage-attsync-block-name-blocks")), @@ -3614,6 +3754,56 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Width factor" => Some(("common", "width-factor-sentence-case")), "Width: %{value}" => Some(("ui", "width-value")), "Window" => Some(("common", "window")), + "Column spacing" => Some(("common", "assoc-array-column-spacing")), + "Row spacing" => Some(("common", "assoc-array-row-spacing")), + "Levels" => Some(("common", "assoc-array-levels")), + "Level spacing" => Some(("common", "assoc-array-level-spacing")), + "Items" => Some(("common", "assoc-array-items")), + "Fill angle" => Some(("common", "assoc-array-fill-angle")), + "Item spacing" => Some(("common", "assoc-array-item-spacing")), + "Array (Rectangular)" => Some(("common", "assoc-array-array-rectangular")), + "Array (Polar)" => Some(("common", "assoc-array-array-polar")), + "Array (Path)" => Some(("common", "assoc-array-array-path")), + "Window Elements" => Some(("common", "options-window-elements")), + "Show the menu bar (MENUBAR)" => Some(("common", "options-show-menu-bar")), + "Show the ribbon (RIBBON / RIBBONCLOSE)" => Some(("common", "options-show-ribbon")), + "New hatches follow their boundaries (HPASSOC)" => Some(("common", "options-hpassoc")), + "Arc Length" => Some(("common", "menu-arc-length")), + "Construction Line" => Some(("common", "menu-construction-line")), + "Dimension Style" => Some(("common", "menu-dimension-style")), + "Donut" => Some(("common", "menu-donut")), + "Drawing Limits" => Some(("common", "menu-drawing-limits")), + "Edit Hatch" => Some(("common", "menu-edit-hatch")), + "Edit Polyline" => Some(("common", "menu-edit-polyline")), + "Edit Spline" => Some(("common", "menu-edit-spline")), + "Explode Text" => Some(("common", "menu-explode-text")), + "ID Point" => Some(("common", "menu-id-point")), + "Jogged" => Some(("common", "menu-jogged")), + "Lengthen" => Some(("common", "menu-lengthen")), + "Match Properties" => Some(("common", "menu-match-properties")), + "Multileader Style" => Some(("common", "menu-multileader-style")), + "Multiline" => Some(("common", "menu-multiline")), + "Multiline Style" => Some(("common", "menu-multiline-style")), + "Multiline Text" => Some(("common", "menu-multiline-text")), + "Named Views" => Some(("common", "menu-named-views")), + "New UCS" => Some(("common", "menu-new-ucs")), + "Orbit" => Some(("common", "menu-orbit")), + "Page Setup Manager" => Some(("common", "menu-page-setup-manager")), + "Parametric" => Some(("common", "menu-parametric")), + "Paste Special" => Some(("common", "menu-paste-special")), + "Publish" => Some(("common", "menu-publish")), + "Quick Dimension" => Some(("common", "menu-quick-dimension")), + "Rectangle" => Some(("common", "menu-rectangle")), + "Redraw" => Some(("common", "menu-redraw")), + "Regen All" => Some(("common", "menu-regen-all")), + "Revision Cloud" => Some(("common", "menu-revision-cloud")), + "Run Script" => Some(("common", "menu-run-script")), + "Table Style" => Some(("common", "menu-table-style")), + "Tools" => Some(("common", "menu-tools")), + "View Twist" => Some(("common", "menu-view-twist")), + "Viewports" => Some(("common", "menu-viewports")), + "Command line opacity" => Some(("common", "command-line-opacity")), + "TXTEXP: no text selected." => Some(("common", "txtexp-no-text-selected")), "Windows desktop" => Some(("styles", "windows-desktop")), "Wipeout" => Some(("common", "wipeout")), "Wireframe" => Some(("common", "wireframe")), @@ -3684,6 +3874,18 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Attach DWG" => Some(("insert", "xref-attach-dwg")), "Attach Image" => Some(("insert", "xref-attach-image")), "Select image(s):" => Some(("insert", "select-images")), + "Select image or underlay:" => Some(("insert", "adjust-select-image-or-underlay")), + "Select PDF underlay:" => Some(("insert", "adjust-select-pdf-underlay")), + "Select DWF underlay:" => Some(("insert", "adjust-select-dwf-underlay")), + "Select DGN underlay:" => Some(("insert", "adjust-select-dgn-underlay")), + "*Invalid selection set. Select underlay objects of the same file type only.*" => Some(("insert", "adjust-mixed-underlays")), + "Enter image option [Contrast/Fade/Brightness] :" => Some(("insert", "adjust-image-option")), + "Enter %{kind} underlay option [Fade/Contrast/Monochrome] <%{option}>:" => Some(("insert", "adjust-underlay-option")), + "Monochrome? [Yes/No] <%{value}>:" => Some(("insert", "adjust-monochrome")), + "Enter brightness value (0-100) <%{value}>:" => Some(("insert", "adjust-brightness-value")), + "Enter contrast value (0-100) <%{value}>:" => Some(("insert", "adjust-contrast-value")), + "Enter fade value (0-100) <%{value}>:" => Some(("insert", "adjust-fade-value")), + "Value must be an integer between 0 and 100." => Some(("insert", "adjust-value-integer-0-100")), "Enter transparency mode [ON/OFF] :" => Some(("insert", "transparency-mode-on")), "Enter transparency mode [ON/OFF] :" => Some(("insert", "transparency-mode-off")), "Attach..." => Some(("insert", "xref-attach-menu")), @@ -3776,6 +3978,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "ZOOM [Window / Extents / Extents All / All / In / Out / Scale]:" => Some(("view", "zoom-window-extents-extents-all-all-in-out-sca")), "ZOOM scale factor (e.g. 2 or 0.5):" => Some(("annotate", "zoom-scale-factor-e-g-2-or-0-5")), "ZOOM WINDOW Specify first corner:" => Some(("view", "zoom-window-specify-first-corner")), + "DVIEW Enter option [TWist/Undo]:" => Some(("view", "dview-enter-option-twist-undo")), + "DVIEW Specify view twist angle <{current}>:" => Some(("view", "dview-specify-view-twist-angle")), + "TWist" => Some(("view", "twist-option")), "ZOOM WINDOW Specify opposite corner:" => Some(("view", "zoom-window-specify-opposite-corner")), "Zero" => Some(("properties", "zero")), "Zero suppress (DIMALTZ)" => Some(("common", "zero-suppress-dimaltz")), @@ -3819,6 +4024,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "{exploded} object(s) exploded." => Some(("annotate", "exploded-object-s-exploded")), "{label}:" => Some(("command", "label")), "{label}: {count} {noun} created." => Some(("common", "label-count-noun-created")), + "{label}: associative array created." => Some(("common", "label-associative-array-created")), "{n} object(s) erased." => Some(("draw", "n-object-s-erased")), "{prefix}: '{style_name}' font set to '{font}'." => Some(("layers", "prefix-style-name-font-set-to-font")), "{prefix}: '{style_name}' oblique angle set to {deg:.1}ยฐ." => Some(("layers", "prefix-style-name-oblique-angle-set-to-deg-1")), @@ -4060,7 +4266,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "ISOPLANE: expected Left, Top, or Right." => Some(("common", "isoplane-expected-left-top-or-right")), "ISODRAFT: expected On or Off." => Some(("common", "isodraft-expected-on-or-off")), "XDATA: selected entities are on locked layers." => Some(("common", "xdata-selected-entities-are-on-locked-layers")), - "ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE)." => Some(("common", "adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade")), "ANNOAUTOSCALE: enter an integer from -4 through 4." => Some(("common", "annoautoscale-enter-an-integer-from-4-through-4")), "Usage: DATALINK " => Some(("common", "usage-datalink-path-to-csv")), "DATALINKUPDATE: no linked tables found." => Some(("common", "datalinkupdate-no-linked-tables-found")), @@ -4085,6 +4290,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "UCS requires origin, X-axis point and XY-plane point." => Some(("common", "ucs-requires-origin-x-axis-point-and-xy-plane-point")), "DWGUNITS current unit: {current}" => Some(("common", "dwgunits-current-unit")), "LAYTRANS: {why}." => Some(("common", "laytrans")), + "Compare with drawing" => Some(("common", "compare-with-drawing")), + "COMPARE: {error}" => Some(("common", "compare-error")), + "COMPARE {added} only here (green), {removed} only in the other drawing (red){position} [Next/Previous/eXit]:" => Some(("common", "compare-prompt")), "LAYTRANS: no layer names in common โ€” use LAYTRANS with no file to map them." => Some(("common", "laytrans-no-layer-names-in-common-use-laytrans-with-no-file-to-map-them")), "DWGUNITS: not a unit this drawing can use." => Some(("common", "dwgunits-not-a-unit-this-drawing-can-use")), "DWGUNITS: already in {label}." => Some(("common", "dwgunits-already-in")), @@ -4950,6 +5158,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Load Linetypes" => Some(("common", "load-linetypes")), "Requires two positive values (x,y) or one for both." => Some(("common", "requires-two-positive-values")), "Snap marker color" => Some(("common", "snap-marker-color")), + "Snap marker size" => Some(("common", "snap-marker-size")), "Command line text color" => Some(("common", "command-line-text-color")), "Operand" => Some(("common", "operand")), "{added} linetype(s) loaded." => Some(("common", "linetypes-loaded")), @@ -5797,6 +6006,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "count area is active." => Some(("count-feature", "area-active")), "{name} = {v} (read only)" => Some(("count-feature", "read-only-value")), "Requires 0 or 1 only." => Some(("count-feature", "requires-0-or-1")), + "Requires 0 or 1 only, or option keyword." => Some(("common", "requires-0-or-1-or-keyword")), + "Requires a number." => Some(("common", "requires-a-number")), + "Angle Override: {shown}" => Some(("common", "angle-override-shown")), "{name} ...... {n}" => Some(("count-feature", "target-count")), "MTEXT Current text style: \"{}\" Text height: {:.4}" => Some(("count-feature", "mtext-current-style")), "{found} field(s) found." => Some(("count-feature", "fields-found")), diff --git a/src/mcp.rs b/src/mcp.rs index c37fbd0b7..bd718dccc 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -3,6 +3,7 @@ //! `OpenCADStudio --mcp` speaks MCP over stdio. All drawing work is forwarded //! to the authenticated GUI control bridge; this module contains no geometry. +use crate::mcp_ops::{batch_bindings, validate_execute_request}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use serde::Deserialize; use serde_json::{json, Map, Value}; @@ -31,7 +32,7 @@ const MODERN_PROTOCOL_VERSION: &str = "2026-07-28"; /// /// `block_delete` gained its optional `force` parameter. #[cfg(test)] -const TOOL_SCHEMA_DIGEST: &str = "1cd356f6b9352458e8318c1424722ec3d8d2e4730dc89ac339dfdecafc5dee9f"; +const TOOL_SCHEMA_DIGEST: &str = "9a7aeb5e1fd76b94fabece3a7ba213f622f449076fac9c25c7e69592989094fd"; const MAX_REQUEST: usize = 1_048_576; const MAX_RESPONSE: u64 = 16 * 1024 * 1024; const CACHE_TTL_MS: u64 = 3_600_000; @@ -39,7 +40,7 @@ const CACHE_TTL_MS: u64 = 3_600_000; /// land. Tools and discovery are static by comparison, so they keep the hour. const RESOURCE_TTL_MS: u64 = 60_000; const TASK_TTL_MS: u64 = 3_600_000; -pub(crate) const INSTRUCTIONS: &str = "Call ocs_sessions and pass session_id as ocs_session_id. Read capabilities and record_schema before unfamiliar operations. Preserve document_id, revision and request_id. After a timeout query the existing operation; never replay a mutation with a new request_id. running and waiting_input are not completion: follow state.command.accepts, options and input_example. Let the geometry kernel calculate geometry. Before delivery audit the target format/version and use save_verified with an absolute path. Announce the bridge build on first connection and when it changes. Use batch with response_detail: summary for known steps; bindings can reuse earlier results. Read guides on demand via cad://guides/{name} or ocs_read op: guide with parameters.name: workflow, architecture, capture, batch-bindings. Read architecture before floorplan vectorization and capture before advanced visual verification. Omit parameters.name to list guides."; +pub(crate) const INSTRUCTIONS: &str = "Call ocs_sessions and pass session_id as ocs_session_id. Read capabilities and record_schema before unfamiliar operations. Use entity_schema for creation fields and validate for preflight. Preserve document_id, revision and request_id. After a timeout query the existing operation; never replay a mutation with a new request_id. running and waiting_input are not completion: follow state.command.accepts, options and input_example. Let the geometry kernel calculate geometry. Before delivery audit the target format/version and use save_verified with an absolute path. Announce the bridge build on first connection and when it changes. Use batch with response_detail: summary for known steps; bindings can reuse earlier results. Read guides on demand via cad://guides/{name} or ocs_read op: guide with parameters.name: workflow, architecture, capture, batch-bindings. Read architecture before floorplan vectorization and capture before advanced visual verification. Omit parameters.name to list guides."; const READ_OPS: &[&str] = &[ "state", "hello", @@ -61,10 +62,13 @@ const READ_OPS: &[&str] = &[ "operation", "batch_result", "guide", + "entity_schema", + "validate", "xdata_get", "audit", "text_search", "text_audit", + "keytips", "capture", ]; const EXECUTE_OPS: &[&str] = &[ @@ -82,6 +86,7 @@ const EXECUTE_OPS: &[&str] = &[ "property", "grip_drag", "click", + "pointer", "set_properties", "action", "embed_image", @@ -111,48 +116,6 @@ const EXECUTE_OPS: &[&str] = &[ "stop", "batch", ]; -const BATCH_STEP_OPS: &[&str] = &[ - "new", - "open", - "activate", - "switch_document", - "run", - "start", - "input", - "cancel", - "undo", - "redo", - "select", - "property", - "grip_drag", - "click", - "set_properties", - "action", - "embed_image", - "wblock", - "plot", - "entities_create", - "entities_delete", - "entities_transform", - "text_replace", - "block_define", - "block_delete", - "xdata_set", - "view_focus", - "entities_copy_to", - "group_create", - "selection_set_save", - "selection_set_load", - "user_select", - "getpoint", - "close", - "sysvar", - "layout_create", - "page_setup_set", - "file_identity", - "save", - "stop", -]; const MAX_BATCH_STEPS: usize = 64; #[derive(Clone, Deserialize)] @@ -1539,54 +1502,6 @@ fn required_string<'a>(arguments: &'a Value, key: &str) -> Result<&'a str, Strin .ok_or_else(|| format!("Missing {key}")) } -/// Validate bindings without dispatching or substituting untyped placeholder values. -fn batch_bindings(step: &Value, index: usize) -> Result, String> { - let Some(bindings) = step.get("bindings") else { - return Ok(Vec::new()); - }; - let bindings = bindings - .as_object() - .filter(|v| !v.is_empty()) - .ok_or("bindings must be a nonempty object")?; - let op = crate::mcp_ops::find_op(step["op"].as_str().unwrap_or_default()) - .ok_or("unknown bound operation")?; - for (target, source) in bindings { - if !op.params.iter().any(|p| p.name == target) { - return Err(format!("cannot bind parameter '{target}'")); - } - if step.get(target).is_some() { - return Err(format!("'{target}' has both a literal and a binding")); - } - let source = source - .as_object() - .ok_or("binding must contain step and path")?; - if source.len() != 2 - || !source - .get("step") - .and_then(Value::as_u64) - .is_some_and(|s| s < index as u64) - { - return Err( - "binding must refer to an earlier step and contain only step and path".into(), - ); - } - let path = source - .get("path") - .and_then(Value::as_str) - .ok_or("binding path must be a JSON Pointer")?; - if !path.is_empty() && !path.starts_with('/') { - return Err("binding path must be a JSON Pointer".into()); - } - let mut chars = path.chars(); - while let Some(c) = chars.next() { - if c == '~' && !matches!(chars.next(), Some('0' | '1')) { - return Err("invalid JSON Pointer escape".into()); - } - } - } - Ok(bindings.keys().map(String::as_str).collect()) -} - fn resolve_batch_step(step: &Value, index: usize, results: &[Value]) -> Result { batch_bindings(step, index)?; let mut resolved = step.clone(); @@ -1613,49 +1528,29 @@ fn resolve_batch_step(step: &Value, index: usize, results: &[Value]) -> Result Result { - if op == "batch" { - let steps = request["steps"].as_array().ok_or_else(|| { - r#"Missing steps for batch. Example request: {"op":"batch","request_id":"draw-1","steps":[{"op":"run","cmd":"LINE 0,0 10,0"}]}"#.to_string() - })?; - if steps.is_empty() || steps.len() > MAX_BATCH_STEPS { - return Err(format!( - "batch steps must contain 1 to {MAX_BATCH_STEPS} operations" - )); +fn preflight_request(request: &Value) -> Value { + let result = + required_string(request, "op").and_then(|op| validate_execute_request(request, op)); + match result { + Ok(validated) => { + let deferred: Vec = request["steps"] + .as_array() + .into_iter() + .flatten() + .enumerate() + .filter_map(|(index, step)| { + step.get("bindings") + .map(|bindings| json!({"step":index,"bindings":bindings})) + }) + .collect(); + json!({"ok":true,"valid":true,"scope":"static","warnings":validated.warnings,"deferred_bindings":deferred, + "not_checked":["live document revision, handles, blocks, styles and record property write rules","file access, command execution and non-creation geometry operations","values resolved from earlier batch results"], + "executed":false}) } - let mut warnings = Vec::new(); - for (index, step) in steps.iter().enumerate() { - let step_obj = step - .as_object() - .ok_or_else(|| format!("batch step {index} must be an object"))?; - if step_obj.contains_key("request_id") { - return Err(format!( - "batch step {index} must omit request_id; the batch assigns idempotency keys" - )); - } - let step_op = step_obj - .get("op") - .and_then(Value::as_str) - .ok_or_else(|| format!("batch step {index} is missing op"))?; - if !BATCH_STEP_OPS.contains(&step_op) { - return Err(format!("Unknown operation {step_op} in batch step {index}")); - } - let deferred = batch_bindings(step, index) - .map_err(|error| format!("batch step {index}: {error}"))?; - let mut literal = step.clone(); - literal.as_object_mut().unwrap().remove("bindings"); - let step_val = crate::mcp_ops::validate_request_deferred(&literal, true, &deferred) - .map_err(|error| format!("batch step {index}: {error}"))?; - for w in step_val.warnings { - warnings.push(format!("batch step {index}: {w}")); - } + Err(error) => { + json!({"ok":false,"valid":false,"scope":"static","executed":false,"code":"validation_failed","error":error}) } - return Ok(crate::mcp_ops::ValidationResult { warnings }); } - crate::mcp_ops::validate_request(request, false) } fn compact_state(state: &Value) -> Value { @@ -2066,11 +1961,23 @@ fn call_tool( Ok(Value::Array(sessions(launch).map_err(CallError::Infra)?)) } "ocs_read" => { - let session_id = required_string(arguments, "ocs_session_id")?; let op = arguments["op"].as_str().unwrap_or("state"); if !READ_OPS.contains(&op) { return Err("Use ocs_execute for mutations".to_string().into()); } + if op == "entity_schema" { + return match arguments["parameters"]["type"].as_str() { + Some(kind) => Ok( + json!({"ok":true,"schema":crate::entity_creation::schema(kind)?,"validation":"Use op validate with parameters.request to preflight; live document checks still run on execution."}), + ), + None => Ok( + json!({"ok":true,"types":crate::entity_creation::TYPES,"aliases":{"Polyline":"LwPolyline"},"next":"Set parameters.type for one creation schema and example."}), + ), + }; + } + if op == "validate" { + return Ok(preflight_request(&arguments["parameters"]["request"])); + } if op == "guide" { let mut result = match arguments["parameters"]["name"].as_str() { Some(name) => guide_resource(name).map_err(CallError::Tool)?, @@ -2080,6 +1987,7 @@ fn call_tool( result["status"] = json!("completed"); return Ok(result); } + let session_id = required_string(arguments, "ocs_session_id")?; if op == "batch_result" { let request_id = required_string(&arguments["parameters"], "request_id")?; return cached_batch_result(clients, session_id, request_id); @@ -2536,8 +2444,8 @@ pub(crate) fn tool_definitions() -> Value { }, { "name":"ocs_read", - "description":"Use guide with parameters.name to read workflow, architecture, capture or batch-bindings guidance (omit name to list). Discover capabilities and record schemas, or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session. Use batch_result with parameters.request_id to retrieve retained batch details without executing steps; available only in the originating MCP connection, for up to 64 retained batches per session.", - "inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command or record-type search."},"find":{"type":"string","description":"Text query string for text_search."},"match_case":{"type":"boolean","default":false,"description":"Case-sensitive text search."},"whole_word":{"type":"boolean","default":false,"description":"Match only whole words."},"ignore_accents":{"type":"boolean","description":"Ignore accents/diacritics in text (defaults to true when match_case is false)."},"scope":{"type":"string","enum":["all","active_space","blocks"],"default":"all","description":"Text search scope."},"system_spellcheck":{"type":"boolean","default":false,"description":"Use native OS spell-checker (Windows, macOS, Linux)."},"language":{"type":"string","description":"Language tag for spell-checker (e.g. 'fr-FR', 'en-US', 'de-DE', 'es-ES')."},"suggest":{"type":"boolean","default":true,"description":"Include suggested corrections for misspelled words."},"check_terms":{"type":"array","items":{"type":"string"},"description":"List of terms or suspect misspellings to flag in text_audit."},"dictionary":{"type":"array","items":{"type":"string"},"description":"Known valid words for text_audit dictionary check."},"pairs":{"type":"array","description":"Find/replace pairs for dry-run simulation in text_audit.","items":{"type":"object","properties":{"find":{"type":"string"},"replace":{"type":"string"}},"required":["find","replace"]}},"dry_run_pairs":{"type":"array","description":"Alias for pairs in text_audit.","items":{"type":"object","properties":{"find":{"type":"string"},"replace":{"type":"string"}},"required":["find","replace"]}},"document_id":{"type":"integer","minimum":0},"path":{"type":"string","description":"Optional intended output path for audit; extension determines target format."},"target_format":{"type":"string","enum":["dwg","dxf"],"description":"Intended output format for audit."},"target_version":{"type":"string","enum":["R14","2000","2004","2007","2010","2013","2018","AC1014","AC1015","AC1018","AC1021","AC1024","AC1027","AC1032"],"description":"Intended CAD output version for audit."},"collection":{"type":"string","description":"Record collection, all for records, or omit to discover collections and schema types."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter; for record_schema, returns its complete type graph and writable field paths."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"World point whose object snap the snap op reports."},"from":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Base point for perpendicular and tangent snaps (snap op)."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false}, + "description":"Use entity_schema with parameters.type for creation fields/examples; omit type to list. Use validate with parameters.request to check an execute request without running it (no live document checks). Use guide with parameters.name to read workflow, architecture, capture or batch-bindings guidance (omit name to list). Discover capabilities and record schemas, or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session. Use batch_result with parameters.request_id to retrieve retained batch details without executing steps; available only in the originating MCP connection, for up to 64 retained batches per session.", + "inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"request":{"type":"object","description":"Execute request to preflight without dispatch. request_id may be omitted. Batch bindings are checked structurally; result-dependent values require execution."},"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command or record-type search."},"find":{"type":"string","description":"Text query string for text_search."},"match_case":{"type":"boolean","default":false,"description":"Case-sensitive text search."},"whole_word":{"type":"boolean","default":false,"description":"Match only whole words."},"ignore_accents":{"type":"boolean","description":"Ignore accents/diacritics in text (defaults to true when match_case is false)."},"scope":{"type":"string","enum":["all","active_space","blocks"],"default":"all","description":"Text search scope."},"system_spellcheck":{"type":"boolean","default":false,"description":"Use native OS spell-checker (Windows, macOS, Linux)."},"language":{"type":"string","description":"Language tag for spell-checker (e.g. 'fr-FR', 'en-US', 'de-DE', 'es-ES')."},"suggest":{"type":"boolean","default":true,"description":"Include suggested corrections for misspelled words."},"check_terms":{"type":"array","items":{"type":"string"},"description":"List of terms or suspect misspellings to flag in text_audit."},"dictionary":{"type":"array","items":{"type":"string"},"description":"Known valid words for text_audit dictionary check."},"pairs":{"type":"array","description":"Find/replace pairs for dry-run simulation in text_audit.","items":{"type":"object","properties":{"find":{"type":"string"},"replace":{"type":"string"}},"required":["find","replace"]}},"dry_run_pairs":{"type":"array","description":"Alias for pairs in text_audit.","items":{"type":"object","properties":{"find":{"type":"string"},"replace":{"type":"string"}},"required":["find","replace"]}},"document_id":{"type":"integer","minimum":0},"path":{"type":"string","description":"Optional intended output path for audit; extension determines target format."},"target_format":{"type":"string","enum":["dwg","dxf"],"description":"Intended output format for audit."},"target_version":{"type":"string","enum":["R14","2000","2004","2007","2010","2013","2018","AC1014","AC1015","AC1018","AC1021","AC1024","AC1027","AC1032"],"description":"Intended CAD output version for audit."},"collection":{"type":"string","description":"Record collection, all for records, or omit to discover collections and schema types."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter; for record_schema, returns its complete type graph and writable field paths."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"World point whose object snap the snap op reports."},"from":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Base point for perpendicular and tangent snaps (snap op)."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"anyOf":[{"required":["ocs_session_id"]},{"required":["op"],"properties":{"op":{"enum":["guide","entity_schema","validate"]}}}],"additionalProperties":false}, "outputSchema":read_output_schema(), "title":"Read OCS state","annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false} }, @@ -3245,11 +3153,21 @@ mod tests { assert!(tool["inputSchema"]["properties"] .get("session_id") .is_none()); + } + for tool in [&tools[2], &tools[3]] { assert!(tool["inputSchema"]["required"] .as_array() .unwrap() .contains(&json!("ocs_session_id"))); } + let read_schema = &tools[1]["inputSchema"]; + for op in ["guide", "entity_schema", "validate"] { + crate::schema_validation::validate_shape(read_schema, &json!({"op":op}), "").unwrap(); + } + assert!( + crate::schema_validation::validate_shape(read_schema, &json!({"op":"state"}), "") + .is_err() + ); assert_eq!( tools[2]["inputSchema"]["properties"]["request"]["required"], json!(["op", "request_id"]) @@ -3297,7 +3215,7 @@ mod tests { assert!(EXECUTE_OPS.contains(&"set_properties")); assert!(EXECUTE_OPS.contains(&"save_verified")); assert!(EXECUTE_OPS.contains(&"text_replace")); - assert!(BATCH_STEP_OPS.contains(&"text_replace")); + assert!(crate::mcp_ops::find_op("text_replace").unwrap().batchable); let save_var = find_variant("save_verified"); assert_eq!(save_var["properties"]["target_version"]["enum"][0], "R14"); assert_eq!( @@ -4577,6 +4495,82 @@ mod tests { assert!(after * 100 < before * 70, "expected at least 30% reduction"); } + #[test] + fn entity_schemas_and_preflight_are_local_and_never_dispatch() { + let mut clients = HashMap::new(); + let mut resources = ResourceStore::default(); + let mut call = |op: &str, parameters: Value| { + call_tool( + "ocs_read", + &json!({"op":op,"parameters":parameters}), + &mut clients, + &mut resources, + &mut detached_pump(), + None, + ) + .unwrap() + }; + let catalog = call("entity_schema", json!({})); + assert_eq!(catalog["types"].as_array().unwrap().len(), 11); + assert!(catalog.get("schema").is_none()); + let circle = call("entity_schema", json!({"type":"Circle"})); + assert_eq!( + circle["schema"]["required"], + json!(["type", "center", "radius"]) + ); + let request = json!({"op":"entities_create","entities":[{"type":"Circle","center":[0,0],"radius":2}]}); + let validated = call("validate", json!({"request":request})); + assert_eq!(validated["valid"], true); + assert_eq!(validated["executed"], false); + assert_eq!(validated["scope"], "static"); + crate::schema_validation::validate_shape(&read_output_schema(), &validated, "").unwrap(); + let invalid = call( + "validate", + json!({"request":{"op":"entities_create","entities":[{"type":"Circle","center":["bad",0],"radius":2}]}}), + ); + assert_eq!(invalid["valid"], false); + assert!(invalid["error"] + .as_str() + .unwrap() + .contains("/entities/0/center/0")); + assert!(clients.is_empty()); + assert!(resources.latest.is_empty()); + let rejected = call_tool( + "ocs_execute", + &json!({"ocs_session_id":"offline","request":{"op":"entities_create","request_id":"invalid","entities":[{"type":"Circle","center":[0,0],"radius":-1}]}}), + &mut clients, + &mut resources, + &mut detached_pump(), + None, + ); + assert!(rejected.is_err()); + assert!(clients.is_empty()); + } + + #[test] + fn preflight_checks_nested_data_envelopes_and_deferred_bindings() { + let malformed = json!({"op":"set_properties","collection":"entities","handle":"A","updates":[{"path":123,"value":"Walls"}]}); + let error = preflight_request(&malformed); + assert_eq!(error["valid"], false); + assert!(error["error"].as_str().unwrap().contains("/updates/0/path")); + for request in [ + json!({"op":"run","cmd":"LINE","document_id":-1}), + json!({"op":"run","cmd":"LINE","request_id":""}), + json!({"op":"run","cmd":"LINE","selection":[42]}), + json!({"op":"not_an_operation"}), + ] { + assert_eq!(preflight_request(&request)["valid"], false); + } + let batch = json!({"op":"batch","steps":[{"op":"run","cmd":"LINE"},{"op":"group_create","name":"Walls","bindings":{"handles":{"step":0,"path":"/result/handles"}}}]}); + let validated = preflight_request(&batch); + assert_eq!(validated["valid"], true); + assert_eq!(validated["deferred_bindings"][0]["step"], 1); + assert!(!validated["not_checked"].as_array().unwrap().is_empty()); + let mut invalid = batch; + invalid["steps"][1]["bindings"]["handles"]["step"] = json!(1); + assert_eq!(preflight_request(&invalid)["valid"], false); + } + #[test] fn guides_are_discoverable_without_a_gui_session() { assert!(INSTRUCTIONS.len() < 1100); diff --git a/src/mcp_ops.rs b/src/mcp_ops.rs index 87c86edbc..cc10ab329 100644 --- a/src/mcp_ops.rs +++ b/src/mcp_ops.rs @@ -39,7 +39,10 @@ pub enum Ty { EnumCi(&'static [&'static str]), ActionName, ArrayOf(&'static Ty), - /// Escape hatch: a JSON Schema fragment as text. + /// Entity definitions with on-demand creation schemas. + Entities, + BatchSteps, + /// Inline JSON Schema fragment, validated before dispatch. Raw(&'static str), } @@ -301,6 +304,22 @@ pub static CLICK: OpDef = OpDef { example: r#"{"op":"click","point":[10,0,0],"modifiers":"ctrl"}"#, }; +pub static POINTER: OpDef = OpDef { + name: "pointer", + doc: "Replay mouse events on the drawing canvas in order, as the mouse sends them: move to canvas pixels or to a world point (projected through the camera the cursor edits with), press and release a button, turn the wheel. A grip press-move-release goes through the same cursor path as a hand drag. Each step reports the cursor's world point.", + batchable: true, + params: &[ + req( + "events", + Ty::ArrayOf(&Ty::Raw(r#"{"type":"object","properties":{"move":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":2},"move_world":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3},"press":{"type":"string","enum":["left","right","middle"]},"release":{"type":"string","enum":["left","right","middle"]},"wheel":{"type":"number"}}}"#)), + "Events in order; each holds one of move, move_world, press, release or wheel (lines, positive zooms in).", + ), + opt("modifiers", Ty::Str, "Keys held throughout: \"ctrl\", \"shift\" or \"ctrl+shift\"."), + ], + rules: &[], + example: r#"{"op":"pointer","events":[{"move_world":[10,0,0]},{"press":"left"},{"release":"left"},{"move_world":[12,3,0]},{"press":"left"},{"release":"left"}]}"#, +}; + pub static SET_PROPERTIES: OpDef = OpDef { name: "set_properties", doc: "Atomically update record properties by RFC 6901 JSON pointer paths.", @@ -409,10 +428,8 @@ pub static ENTITIES_CREATE: OpDef = OpDef { batchable: true, params: &[req( "entities", - Ty::ArrayOf(&Ty::Raw( - r#"{"type":"object","properties":{"type":{"type":"string"}},"required":["type"]}"#, - )), - "List of entity definitions to create.", + Ty::Entities, + "Entity definitions. Read ocs_read op entity_schema with parameters.type for fields and examples before creating an unfamiliar type.", )], rules: &[], example: r#"{"op":"entities_create","entities":[{"type":"Line","start":[0,0],"end":[10,10]}]}"#, @@ -855,7 +872,7 @@ pub static BATCH: OpDef = OpDef { doc: "Execute sequential operations with fresh state and step idempotency keys. Stops on first error.", batchable: false, params: &[ - req("steps", Ty::Raw(r##"{"type":"array","minItems":1,"maxItems":64,"description":"Sequential operations list","items":{"$ref":"#/$defs/batch_step"}}"##), "List of batch steps to execute."), + req("steps", Ty::BatchSteps, "List of batch steps to execute."), ], rules: &[], example: r#"{"op":"batch","steps":[{"op":"run","cmd":"LINE 0,0 10,0"}]}"#, @@ -876,6 +893,7 @@ pub static OPS: &[&OpDef] = &[ &PROPERTY, &GRIP_DRAG, &CLICK, + &POINTER, &SET_PROPERTIES, &ACTION, &EMBED_IMAGE, @@ -930,17 +948,27 @@ fn ty_schema(ty: &Ty) -> Value { json!({"type": "string", "enum": names}) } Ty::ArrayOf(inner) => json!({"type": "array", "items": ty_schema(inner)}), + Ty::BatchSteps => { + json!({"type":"array","minItems":1,"maxItems":64,"items":{"$ref":"#/$defs/batch_step"}}) + } + Ty::Entities => { + json!({"type":"array","minItems":1,"items":{"type":"object","required":["type"],"properties":{"type":{"type":"string"}}}}) + } Ty::Raw(s) => serde_json::from_str(s).expect("Raw fragment must be valid JSON"), } } +fn primitive_defs() -> Value { + json!({ + "point": {"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3}, + "window": {"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4}, + "handle": {"type":"string","pattern":"^(0[xX])?[0-9A-Fa-f]+$"} + }) +} + pub fn shared_defs() -> Value { - let mut defs = json!({ - "point": {"type": "array", "items": {"type": "number"}, "minItems": 2, "maxItems": 3}, - "window": {"type": "array", "items": {"type": "number"}, "minItems": 4, "maxItems": 4}, - "handle": {"type": "string", "pattern": "^(0[xX])?[0-9A-Fa-f]+$"}, - "batch_step": batch_step_schema() - }); + let mut defs = primitive_defs(); + defs["batch_step"] = batch_step_schema(); for op in OPS { defs[format!("op_{}", op.name)] = op_schema(op); } @@ -989,28 +1017,34 @@ pub fn op_schema(op: &OpDef) -> Value { }) } -pub fn execute_request_schema() -> Value { - let variants: Vec = OPS.iter().map(|op| op_ref(op)).collect(); - let op_names: Vec<&str> = OPS.iter().map(|op| op.name).collect(); +/// The common envelope is optional in static preflight; dispatch requires an id. +/// Shared with the advertised execute schema so constraints cannot drift. +fn request_envelope_schema() -> Value { json!({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", + "type":"object", "properties": { - "op": {"type": "string", "enum": op_names, "description": "Semantic editor operation."}, - "request_id": {"type": "string", "minLength": 1, "maxLength": 128, "description": "Caller-generated idempotency key."}, - "document_id": {"type": "integer", "minimum": 0, "description": "Target document from current state."}, - "revision": {"type": "integer", "minimum": 0, "description": "Expected edit revision from current state."}, - "geometry_revision": {"type": "integer", "minimum": 0, "description": "Expected geometry revision."}, - "camera_revision": {"type": "integer", "minimum": 0, "description": "Expected camera revision."}, - "selection": {"type": "array", "items": {"$ref": "#/$defs/handle"}, "description": "Expected selected handles."}, - "steps": {"type": "array", "minItems": 1, "maxItems": 64, "description": "Sequential batch operations.", "items": {"$ref": "#/$defs/batch_step"}} + "op": {"type":"string","enum": OPS.iter().map(|op| op.name).collect::>(),"description":"Semantic editor operation."}, + "request_id": {"type":"string","minLength":1,"maxLength":128,"description":"Caller-generated idempotency key (at most 128 UTF-8 bytes)."}, + "document_id": {"type":"integer","minimum":0,"description":"Target document from current state."}, + "revision": {"type":"integer","minimum":0,"description":"Expected edit revision from current state."}, + "geometry_revision": {"type":"integer","minimum":0,"description":"Expected geometry revision."}, + "camera_revision": {"type":"integer","minimum":0,"description":"Expected camera revision."}, + "selection": {"type":"array","items":{"$ref":"#/$defs/handle"},"description":"Expected selected handles."} }, - "required": ["op", "request_id"], - "anyOf": variants, - "$defs": shared_defs() + "$defs":primitive_defs() }) } +pub fn execute_request_schema() -> Value { + let mut schema = request_envelope_schema(); + schema["$schema"] = json!("https://json-schema.org/draft/2020-12/schema"); + schema["properties"]["steps"] = json!({"type":"array","minItems":1,"maxItems":64,"description":"Sequential batch operations.","items":{"$ref":"#/$defs/batch_step"}}); + schema["required"] = json!(["op", "request_id"]); + schema["anyOf"] = Value::Array(OPS.iter().map(|op| op_ref(op)).collect()); + schema["$defs"] = shared_defs(); + schema +} + pub fn batch_step_schema() -> Value { let mut batch_variants: Vec = OPS .iter() @@ -1147,11 +1181,46 @@ fn validate_type(ty: &Ty, val: &Value) -> Result<(), String> { validate_type(inner, item).map_err(|e| format!("item {idx}: {e}"))?; } } - Ty::Raw(_) => {} + Ty::Entities => { + let entities = val + .as_array() + .filter(|a| !a.is_empty()) + .ok_or("expected nonempty entities array")?; + for (index, entity) in entities.iter().enumerate() { + crate::entity_creation::validate_entity(entity, &format!("/entities/{index}"))?; + } + } + Ty::BatchSteps => { + validate_execute_request(&json!({"op":"batch","steps":val}), "batch")?; + } + Ty::Raw(fragment) => { + let schema: Value = serde_json::from_str(fragment).expect("valid inline schema"); + crate::schema_validation::validate_shape(&schema, val, "")?; + } } Ok(()) } +fn validate_parameter(ty: &Ty, val: &Value, path: &str) -> Result<(), String> { + match ty { + Ty::ArrayOf(inner) => { + let items = val + .as_array() + .ok_or_else(|| format!("{path}: expected array"))?; + for (i, item) in items.iter().enumerate() { + validate_parameter(inner, item, &format!("{path}/{i}"))?; + } + Ok(()) + } + Ty::Raw(fragment) => { + let schema: Value = serde_json::from_str(fragment).expect("valid inline schema"); + crate::schema_validation::validate_shape(&schema, val, path) + } + Ty::Entities => validate_type(ty, val), + _ => validate_type(ty, val).map_err(|error| format!("{path}: {error}")), + } +} + pub fn validate_request(request: &Value, is_batch_step: bool) -> Result { validate_request_deferred(request, is_batch_step, &[]) } @@ -1233,7 +1302,7 @@ pub fn validate_request_deferred( for p in op_def.params { if let Some(val) = obj.get(p.name) { if !val.is_null() { - validate_type(&p.ty, val) + validate_parameter(&p.ty, val, &format!("/{}", p.name)) .map_err(|e| format!("parameter '{}' for op '{}': {e}", p.name, op_name))?; } } @@ -1265,3 +1334,106 @@ pub fn validate_request_deferred( Ok(ValidationResult { warnings }) } + +const MAX_BATCH_STEPS: usize = 64; + +/// Validate bindings without dispatching or substituting untyped placeholder values. +pub fn batch_bindings(step: &Value, index: usize) -> Result, String> { + let Some(bindings) = step.get("bindings") else { + return Ok(Vec::new()); + }; + let bindings = bindings + .as_object() + .filter(|v| !v.is_empty()) + .ok_or("bindings must be a nonempty object")?; + let op = find_op(step["op"].as_str().unwrap_or_default()).ok_or("unknown bound operation")?; + for (target, source) in bindings { + if !op.params.iter().any(|p| p.name == target) { + return Err(format!("cannot bind parameter '{target}'")); + } + if step.get(target).is_some() { + return Err(format!("'{target}' has both a literal and a binding")); + } + let source = source + .as_object() + .ok_or("binding must contain step and path")?; + if source.len() != 2 + || !source + .get("step") + .and_then(Value::as_u64) + .is_some_and(|s| s < index as u64) + { + return Err( + "binding must refer to an earlier step and contain only step and path".into(), + ); + } + let path = source + .get("path") + .and_then(Value::as_str) + .ok_or("binding path must be a JSON Pointer")?; + if !path.is_empty() && !path.starts_with('/') { + return Err("binding path must be a JSON Pointer".into()); + } + let mut chars = path.chars(); + while let Some(c) = chars.next() { + if c == '~' && !matches!(chars.next(), Some('0' | '1')) { + return Err("invalid JSON Pointer escape".into()); + } + } + } + Ok(bindings.keys().map(String::as_str).collect()) +} + +pub fn validate_execute_request(request: &Value, op: &str) -> Result { + crate::schema_validation::validate_shape(&request_envelope_schema(), request, "")?; + if request["request_id"] + .as_str() + .is_some_and(|id| id.len() > 128) + { + return Err("/request_id: must not exceed 128 UTF-8 bytes".into()); + } + if op == "batch" { + let steps = request["steps"].as_array().ok_or_else(|| { + r#"Missing steps for batch. Example request: {"op":"batch","request_id":"draw-1","steps":[{"op":"run","cmd":"LINE 0,0 10,0"}]}"#.to_string() + })?; + if steps.is_empty() || steps.len() > MAX_BATCH_STEPS { + return Err(format!( + "batch steps must contain 1 to {MAX_BATCH_STEPS} operations" + )); + } + let mut warnings = Vec::new(); + for (index, step) in steps.iter().enumerate() { + let step_obj = step + .as_object() + .ok_or_else(|| format!("batch step {index} must be an object"))?; + if step_obj.contains_key("request_id") { + return Err(format!( + "batch step {index} must omit request_id; the batch assigns idempotency keys" + )); + } + let step_op = step_obj + .get("op") + .and_then(Value::as_str) + .ok_or_else(|| format!("batch step {index} is missing op"))?; + if !find_op(step_op).is_some_and(|op| op.batchable) { + return Err(format!("Unknown operation {step_op} in batch step {index}")); + } + let deferred = batch_bindings(step, index) + .map_err(|error| format!("batch step {index}: {error}"))?; + let mut literal = step.clone(); + literal.as_object_mut().unwrap().remove("bindings"); + crate::schema_validation::validate_shape( + &request_envelope_schema(), + &literal, + &format!("/steps/{index}"), + )?; + let step_val = validate_request_deferred(&literal, true, &deferred) + .map_err(|error| format!("batch step {index}: {error}"))?; + for w in step_val.warnings { + warnings.push(format!("batch step {index}: {w}")); + } + } + return Ok(ValidationResult { warnings }); + } + validate_request(request, false) +} diff --git a/src/modules/block_editor/mod.rs b/src/modules/block_editor/mod.rs new file mode 100644 index 000000000..09b61c71e --- /dev/null +++ b/src/modules/block_editor/mod.rs @@ -0,0 +1,88 @@ +//! Contextual "Block Editor" ribbon tab, shown only while a BEDIT session is +//! open: saving or discarding the block, attribute definitions, and the +//! constraint tools that drive parametric blocks. + +use crate::modules::parametric::ParametricModule; +use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef}; + +pub struct BlockEditorModule; + +impl CadModule for BlockEditorModule { + fn id(&self) -> &'static str { + "block_editor" + } + + fn title(&self) -> &'static str { + "Block Editor" + } + + fn ribbon_groups(&self) -> &[RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + GROUPS.get_or_init(|| { + let command = |id: &'static str, label: &'static str, icon: &'static [u8]| { + RibbonItem::LargeTool(ToolDef { + id, + label, + icon: IconKind::Svg(icon), + event: ModuleEvent::Command(id.to_string()), + }) + }; + let mut groups = vec![ + RibbonGroup { + title: "Open and Save", + tools: vec![ + command( + "BEDIT", + "Edit Block", + include_bytes!("../../../assets/icons/edit_block.svg"), + ), + RibbonItem::LargeDropdown { + id: "BLOCK_SAVE", + label: "Save Block", + icon: IconKind::Svg(include_bytes!("../../../assets/icons/mt_ok.svg")), + items: vec![ + ( + "BSAVE", + "Save Block", + IconKind::Svg(include_bytes!( + "../../../assets/icons/mt_ok.svg" + )), + ), + ( + "BSAVEAS", + "Save Block As", + IconKind::Svg(include_bytes!( + "../../../assets/icons/ui/save.svg" + )), + ), + ], + default: "BSAVE", + }, + command( + "BCLOSE", + "Close Block Editor", + include_bytes!("../../../assets/icons/mt_cancel.svg"), + ), + command( + "BTESTBLOCK", + "Test Block", + include_bytes!("../../../assets/icons/edit_block.svg"), + ), + ], + }, + RibbonGroup { + title: "Attributes", + tools: vec![command( + "ATTDEF", + "Attribute Definition", + include_bytes!("../../../assets/icons/attdef.svg"), + )], + }, + ]; + // Parametric blocks are driven by the same constraints and + // parameters as the Parametric tab. + groups.extend(ParametricModule.ribbon_groups().iter().cloned()); + groups + }) + } +} diff --git a/src/modules/draw/clipboard/mod.rs b/src/modules/draw/clipboard/mod.rs index 0b72b361d..5122d99c2 100644 --- a/src/modules/draw/clipboard/mod.rs +++ b/src/modules/draw/clipboard/mod.rs @@ -2,3 +2,4 @@ pub mod copy_base; pub mod copy_clip; pub mod cut; pub mod paste; +pub mod pastespec; diff --git a/src/modules/draw/clipboard/pastespec.rs b/src/modules/draw/clipboard/pastespec.rs new file mode 100644 index 000000000..c23cbd38b --- /dev/null +++ b/src/modules/draw/clipboard/pastespec.rs @@ -0,0 +1,136 @@ +// PASTESPEC โ€” paste the clipboard in a chosen format: the copied objects, the +// copied objects as a block, or the system clipboard's text as multi-line text. + +use crate::t; +use codec::entities::mtext::AttachmentPoint; +use codec::entities::MText; +use codec::types::Vector3; +use codec::EntityType; +use glam::DVec3; + +use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane}; + +pub struct PasteSpecCommand { + /// Objects copied inside the application are waiting on the clipboard. + objects: bool, + /// The system clipboard's text, already escaped for MTEXT. + text: Option, + style: String, + height: f64, + placing_text: bool, + plane: WorkingPlane, +} + +impl PasteSpecCommand { + pub fn new(objects: bool, text: Option<&str>, style: String, height: f64) -> Self { + Self { + objects, + text: text.map(mtext_value), + style, + height, + placing_text: false, + plane: WorkingPlane::default(), + } + } +} + +/// Plain text as an MTEXT value: control characters escaped, lines as +/// paragraphs. +fn mtext_value(text: &str) -> String { + text.trim_end_matches(['\r', '\n']) + .replace('\\', "\\\\") + .replace('{', "\\{") + .replace('}', "\\}") + .replace("\r\n", "\\P") + .replace(['\r', '\n'], "\\P") +} + +impl CadCommand for PasteSpecCommand { + fn name(&self) -> &'static str { + "PASTESPEC" + } + + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + + fn prompt(&self) -> String { + if self.placing_text { + t!("PASTESPEC Specify insertion point:").into_owned() + } else { + t!("PASTESPEC Paste as [Objects/Block/Text]:").into_owned() + } + } + + fn options(&self) -> Vec { + if self.placing_text { + return Vec::new(); + } + let mut options = Vec::new(); + if self.objects { + options.push(CmdOption::new("Objects", "O")); + options.push(CmdOption::new("Block", "B")); + } + if self.text.is_some() { + options.push(CmdOption::new("Text", "T")); + } + options + } + + fn wants_text_input(&self) -> bool { + !self.placing_text + } + + fn on_text_input(&mut self, text: &str) -> Option { + if self.placing_text { + return None; + } + match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() { + "O" | "OBJECTS" if self.objects => Some(CmdResult::Dispatch("PASTECLIP".into())), + "B" | "BLOCK" if self.objects => Some(CmdResult::Dispatch("PASTEBLOCK".into())), + "T" | "TEXT" if self.text.is_some() => { + self.placing_text = true; + Some(CmdResult::NeedPoint) + } + _ => None, + } + } + + fn on_point(&mut self, pt: DVec3) -> CmdResult { + let Some(value) = self.text.clone().filter(|_| self.placing_text) else { + return CmdResult::NeedPoint; + }; + let p = self.plane.to_local(pt); + let mut mtext = MText::new(); + mtext.value = value; + mtext.insertion_point = Vector3::new(p.x, p.y, p.z); + mtext.height = self.height; + mtext.style = self.style.clone(); + mtext.attachment_point = AttachmentPoint::TopLeft; + mtext.rectangle_width = 0.0; + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::MText(mtext))) + } + + fn on_enter(&mut self) -> CmdResult { + if self.placing_text { + return CmdResult::NeedPoint; + } + // Enter takes the first format offered. + let first = self.options().first().map(|o| o.keyword.clone()); + first + .and_then(|keyword| self.on_text_input(&keyword)) + .unwrap_or(CmdResult::Cancel) + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["PASTESPEC"] }); + +#[cfg(test)] +mod tests { + use super::mtext_value; + + #[test] + fn plain_text_becomes_escaped_paragraphs() { + assert_eq!(mtext_value("a{b}\\c\r\nline 2\n"), "a\\{b\\}\\\\c\\Pline 2"); + } +} diff --git a/src/modules/draw/defaults.rs b/src/modules/draw/defaults.rs index ac0ce079e..0da88ee54 100644 --- a/src/modules/draw/defaults.rs +++ b/src/modules/draw/defaults.rs @@ -17,6 +17,7 @@ thread_local! { static ARRAY_P_COUNT: Cell = Cell::new(6.0); static ARRAY_P_ANGLE: Cell = Cell::new(360.0); // degrees static ARRAY_PATH_COUNT: Cell = Cell::new(6.0); + static ARRAY_ASSOC: Cell = Cell::new(1.0); // 1 = associative static POLYGON_SIDES: Cell = Cell::new(6.0); static RECT_ELEVATION: Cell = Cell::new(0.0); static RECT_THICKNESS: Cell = Cell::new(0.0); @@ -61,6 +62,7 @@ accessors!(get_fillet_radius, set_fillet_radius, FILLET_RADIUS); accessors!(get_chamfer_dist1, set_chamfer_dist1, CHAMFER_DIST1); accessors!(get_chamfer_dist2, set_chamfer_dist2, CHAMFER_DIST2); accessors!(get_array_rows, set_array_rows, ARRAY_ROWS); +accessors!(get_array_assoc, set_array_assoc, ARRAY_ASSOC); accessors!(get_array_cols, set_array_cols, ARRAY_COLS); accessors!(get_array_row_sp, set_array_row_sp, ARRAY_ROW_SP); accessors!(get_array_col_sp, set_array_col_sp, ARRAY_COL_SP); diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index 587073aba..1811c5e60 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -248,6 +248,12 @@ impl HatchCommand { command } + /// HPASSOC: whether the hatch starts out associative. + pub fn with_associative(mut self, associative: bool) -> Self { + self.associative = associative; + self + } + pub fn with_origin(mut self, origin: [f64; 2]) -> Self { self.default_origin = origin; self diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index baa38796f..dd8386cc3 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -49,8 +49,12 @@ impl CadCommand for DistCommand { let dy = delta.y; let dz = delta.z; - // Angle in XY plane โ€” degrees from +X - let angle_xy = dy.atan2(dx).to_degrees(); + // Angle in XY plane โ€” a direction, so counted from ANGBASE the + // way ANGDIR runs, like every other direction the drawing shows. + let ctx = unit_context(); + let sign = if ctx.angdir_cw { -1.0 } else { 1.0 }; + let turn = std::f64::consts::TAU; + let angle_xy = (sign * (dy.atan2(dx) - ctx.angbase)).rem_euclid(turn).to_degrees(); // Angle from XY plane toward Z (elevation angle) let dist_xy = dx.hypot(dy); let angle_z = dz.atan2(dist_xy).to_degrees(); diff --git a/src/modules/draw/mod.rs b/src/modules/draw/mod.rs index 4acb13587..7fb2cc5a7 100644 --- a/src/modules/draw/mod.rs +++ b/src/modules/draw/mod.rs @@ -15,7 +15,7 @@ mod report; pub mod select; pub mod units; -use crate::modules::{CadModule, RibbonGroup, RibbonItem}; +use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef}; pub struct DrawModule; @@ -249,6 +249,15 @@ impl CadModule for DrawModule { default: "DIST", }], }, + RibbonGroup { + title: "Close", + tools: vec![RibbonItem::LargeTool(ToolDef { + id: "BCLOSETESTBLOCK", + label: "Close Test Block", + icon: IconKind::Svg(include_bytes!("../../../assets/icons/mt_ok.svg")), + event: ModuleEvent::Command("BCLOSETESTBLOCK".into()), + })], + }, // Support group lives on the Start tab now (see view.rs: // start_page_view). Removed from the Draw ribbon to declutter. ] diff --git a/src/modules/draw/modify/array.rs b/src/modules/draw/modify/array.rs index fdf7ed41b..ff9dbb9a7 100644 --- a/src/modules/draw/modify/array.rs +++ b/src/modules/draw/modify/array.rs @@ -19,6 +19,29 @@ use glam::DVec3; use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane}; use crate::modules::draw::defaults; + +/// The Associative option button, labelled with its current state. +fn associative_option(on: bool) -> crate::command::CmdOption { + crate::command::CmdOption { + label: format!( + "{}: {}", + crate::t!("Associative"), + crate::t!(if on { "Yes" } else { "No" }) + ), + keyword: "AS".to_string(), + } +} + +/// Typed `AS` / `ASSOCIATIVE` flips the Associative option. +fn is_associative_keyword(text: &str) -> bool { + matches!(text.trim().to_ascii_uppercase().as_str(), "AS" | "ASSOCIATIVE" | "_AS") +} + +/// Associative arrays are built in world axes; a rotated working plane +/// falls back to plain copies. +fn world_plane(plane: WorkingPlane) -> bool { + plane.x.abs_diff_eq(DVec3::X, 1e-12) && plane.z.abs_diff_eq(DVec3::Z, 1e-12) +} use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; @@ -84,6 +107,7 @@ pub struct ArrayRectCommand { default_cols: u32, default_row_sp: f64, default_col_sp: f64, + associative: bool, plane: WorkingPlane, } @@ -97,6 +121,7 @@ impl ArrayRectCommand { default_cols: (defaults::get_array_cols() as u32).clamp(1, MAX_ARRAY_ITEMS as u32), default_row_sp: defaults::get_array_row_sp(), default_col_sp: defaults::get_array_col_sp(), + associative: defaults::get_array_assoc() != 0.0, plane: WorkingPlane::default(), } } @@ -129,6 +154,10 @@ impl CadCommand for ArrayRectCommand { "ARRAYRECT" } + fn options(&self) -> Vec { + vec![associative_option(self.associative)] + } + fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; } @@ -165,6 +194,11 @@ impl CadCommand for ArrayRectCommand { } fn on_text_input(&mut self, text: &str) -> Option { + if is_associative_keyword(text) { + self.associative = !self.associative; + defaults::set_array_assoc(f64::from(u8::from(self.associative))); + return None; + } let t = text.trim().replace(',', "."); let t = t.as_str(); match self.step { @@ -221,6 +255,17 @@ impl CadCommand for ArrayRectCommand { defaults::set_array_col_sp(v); v }; + if self.associative && world_plane(self.plane) { + return Some(CmdResult::AssocArray { + handles: self.handles.clone(), + spec: crate::scene::assoc_array::AssocArraySpec::Rectangular { + columns: cols, + rows, + column_spacing: col_sp, + row_spacing: row_sp, + }, + }); + } Some(CmdResult::BatchCopy( self.handles.clone(), Self::build_transforms(rows, cols, row_sp, col_sp, self.plane), @@ -295,6 +340,7 @@ pub struct ArrayPolarCommand { step: PolarStep, default_count: u32, default_angle: f64, + associative: bool, plane: WorkingPlane, } @@ -306,6 +352,7 @@ impl ArrayPolarCommand { step: PolarStep::Center, default_count: (defaults::get_array_p_count() as u32).clamp(2, MAX_ARRAY_ITEMS as u32), default_angle: defaults::get_array_p_angle(), + associative: defaults::get_array_assoc() != 0.0, plane: WorkingPlane::default(), } } @@ -320,6 +367,10 @@ impl CadCommand for ArrayPolarCommand { "ARRAYPOLAR" } + fn options(&self) -> Vec { + vec![associative_option(self.associative)] + } + fn prompt(&self) -> String { match &self.step { PolarStep::Center => crate::tf!( @@ -351,6 +402,11 @@ impl CadCommand for ArrayPolarCommand { } fn on_text_input(&mut self, text: &str) -> Option { + if is_associative_keyword(text) { + self.associative = !self.associative; + defaults::set_array_assoc(f64::from(u8::from(self.associative))); + return None; + } let t = text.trim().replace(',', "."); let t = t.as_str(); match &self.step { @@ -383,6 +439,16 @@ impl CadCommand for ArrayPolarCommand { defaults::set_array_p_angle(v); v }; + if self.associative && world_plane(self.plane) { + return Some(CmdResult::AssocArray { + handles: self.handles.clone(), + spec: crate::scene::assoc_array::AssocArraySpec::Polar { + center, + items: count, + fill_angle: total_deg.to_radians(), + }, + }); + } let step_rad = total_deg.to_radians() / count as f64; let transforms = (1..count) .map(|n| EntityTransform::Rotate { diff --git a/src/modules/draw/modify/arrayedit.rs b/src/modules/draw/modify/arrayedit.rs new file mode 100644 index 000000000..d1d48311e --- /dev/null +++ b/src/modules/draw/modify/arrayedit.rs @@ -0,0 +1,78 @@ +// ARRAYEDIT โ€” edit an associative array. Counts and spacings are edited in +// Properties; Source opens the objects every item repeats in the Block +// Editor, and closing it updates all items at once. + +use crate::command::{CadCommand, CmdOption, CmdResult}; +use codec::Handle; +use glam::DVec3; + +pub struct ArrayEditCommand { + array: Option, +} + +impl ArrayEditCommand { + pub fn new(array: Option) -> Self { + Self { array } + } +} + +impl CadCommand for ArrayEditCommand { + fn name(&self) -> &'static str { + "ARRAYEDIT" + } + + fn prompt(&self) -> String { + match self.array { + None => crate::t!("ARRAYEDIT Select array:").into_owned(), + Some(_) => crate::t!("ARRAYEDIT Enter an option [Source/eXit] :").into_owned(), + } + } + + fn options(&self) -> Vec { + match self.array { + None => Vec::new(), + Some(_) => vec![ + CmdOption::new(crate::t!("Source").as_ref(), "S"), + CmdOption::new(crate::t!("eXit").as_ref(), "X"), + ], + } + } + + fn needs_entity_pick(&self) -> bool { + self.array.is_none() + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { + if handle.is_null() { + return CmdResult::NeedPoint; + } + self.array = Some(handle); + CmdResult::NeedPoint + } + + fn wants_text_input(&self) -> bool { + self.array.is_some() + } + + fn on_text_input(&mut self, text: &str) -> Option { + let array = self.array?; + match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() { + "S" | "SOURCE" => Some(CmdResult::Dispatch(format!( + "ARRAYEDIT_SOURCE:{}", + array.value() + ))), + "X" | "EXIT" => Some(CmdResult::Cancel), + _ => None, + } + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["ARRAYEDIT"] }); diff --git a/src/modules/draw/modify/block_edit.rs b/src/modules/draw/modify/block_edit.rs index c2a7ed109..e8038eada 100644 --- a/src/modules/draw/modify/block_edit.rs +++ b/src/modules/draw/modify/block_edit.rs @@ -15,7 +15,7 @@ // block record in place, so there is no copy-back and no transform โ€” the space // render filter (`current_layout_block_handle`) already draws only the block. -use codec::{EntityType, Handle}; +use codec::Handle; use glam::DVec3; use crate::command::{CadCommand, CmdResult}; @@ -26,16 +26,22 @@ use crate::modules::{IconKind, ModuleEvent, ToolDef}; pub fn block_edit_tools() -> Vec { vec![ ToolDef { - id: "BEDIT_SAVE", + id: "BSAVE", label: "Save Block", icon: IconKind::Svg(include_bytes!("../../../../assets/icons/mt_ok.svg")), - event: ModuleEvent::Command("BEDIT_SAVE".to_string()), + event: ModuleEvent::Command("BSAVE".to_string()), }, ToolDef { - id: "BEDIT_DISCARD", - label: "Discard Block Edit", + id: "BSAVEAS", + label: "Save Block As", + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/ui/save.svg")), + event: ModuleEvent::Command("BSAVEAS".to_string()), + }, + ToolDef { + id: "BCLOSE", + label: "Close Block Editor", icon: IconKind::Svg(include_bytes!("../../../../assets/icons/mt_cancel.svg")), - event: ModuleEvent::Command("BEDIT_DISCARD".to_string()), + event: ModuleEvent::Command("BCLOSE".to_string()), }, ] } @@ -55,17 +61,6 @@ pub struct BlockEditSession { /// Parent block tab to return to for a nested BEDIT session. `None` returns /// to `return_layout`. pub return_block: Option, - /// The complete block definition captured on entry, so Discard restores - /// structural markers and content with their original handles. - pub snapshot: Vec, - /// Baked anonymous dimension-block entities transformed alongside - /// dimensions in this definition. They remain in place, so Discard can - /// restore their shared before-images directly. - pub dependent_snapshot: Vec>, - /// Inline ATTRIB values/positions on every existing INSERT of this block. - /// Rebasing the origin moves them with the definition; Discard restores - /// their exact on-entry state. - pub reference_attributes: Vec<(Handle, Vec)>, /// Camera state of the space that was active when BEDIT began, restored /// on Save/Discard so the view returns exactly where it was (#425). pub return_camera: crate::scene::view::camera::Camera, @@ -75,6 +70,10 @@ pub struct BlockEditSession { /// Transient UCS being manipulated in this block tab. On commit its full /// frame is baked into the definition and this returns to `None`. pub editor_ucs: Option, + /// Snapshot of the document prior to creating an empty new block for editing. + /// If the session is closed without saving, this snapshot is restored so + /// no block record remains in the block table. + pub unpersisted_pre_document: Option, } // โ”€โ”€ BEDIT pick command โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -121,3 +120,9 @@ inventory::submit!(crate::command::CommandRegistration { inventory::submit!(crate::command::CommandRegistration { names: &["BEDIT_DISCARD"] }); +inventory::submit!(crate::command::CommandRegistration { + names: &["BSAVE", "BSAVEAS", "BCLOSE"] +}); +inventory::submit!(crate::command::CommandRegistration { + names: &["BTESTBLOCK", "BCLOSETESTBLOCK"] +}); diff --git a/src/modules/draw/modify/bvstate.rs b/src/modules/draw/modify/bvstate.rs new file mode 100644 index 000000000..3dfe7cb93 --- /dev/null +++ b/src/modules/draw/modify/bvstate.rs @@ -0,0 +1,200 @@ +// BVSTATE โ€” the visibility states of the block open in the Block Editor: +// make a new one, make one current, rename or delete one. The first use in a +// block makes it dynamic with a visibility parameter. + +use crate::command::{CadCommand, CmdOption, CmdResult, InputKind}; +use codec::Handle; +use glam::DVec3; + +/// Separates the fields of the `_BVSTATE` step the command hands over. +pub const SEP: char = '\u{1f}'; + +enum Step { + Option, + NewName, + SetName, + RenameFrom, + RenameTo(String), + DeleteName, +} + +pub struct BvStateCommand { + names: Vec, + current: usize, + step: Step, +} + +impl BvStateCommand { + pub fn new(names: Vec, current: usize) -> Self { + Self { + names, + current, + step: Step::Option, + } + } + + fn current_name(&self) -> String { + self.names.get(self.current).cloned().unwrap_or_default() + } + + fn known(&self, name: &str) -> Option { + self.names + .iter() + .find(|candidate| candidate.eq_ignore_ascii_case(name)) + .cloned() + } + + fn done(parts: &[&str]) -> CmdResult { + let mut command = String::from("_BVSTATE"); + for part in parts { + command.push(SEP); + command.push_str(part); + } + CmdResult::Dispatch(command) + } + + fn unknown(name: &str) -> CmdResult { + CmdResult::ReportError(crate::tf!("Visibility state \"{}\" not found.", name).into_owned()) + } +} + +impl CadCommand for BvStateCommand { + fn name(&self) -> &'static str { + "BVSTATE" + } + + fn prompt(&self) -> String { + match &self.step { + Step::Option => { + crate::t!("BVSTATE Enter an option [New/Set/Rename/Delete] :").into_owned() + } + Step::NewName => crate::t!("BVSTATE Enter name of new visibility state:").into_owned(), + Step::SetName => crate::tf!( + "BVSTATE Enter visibility state to make current <{}>:", + self.current_name() + ) + .into_owned(), + Step::RenameFrom => crate::tf!( + "BVSTATE Enter visibility state to rename <{}>:", + self.current_name() + ) + .into_owned(), + Step::RenameTo(_) => crate::t!("BVSTATE Enter new name:").into_owned(), + Step::DeleteName => { + crate::t!("BVSTATE Enter visibility state to delete:").into_owned() + } + } + } + + fn options(&self) -> Vec { + match self.step { + Step::Option => vec![ + CmdOption::new(crate::t!("New").as_ref(), "N"), + CmdOption::new(crate::t!("Set").as_ref(), "S"), + CmdOption::new(crate::t!("Rename").as_ref(), "R"), + CmdOption::new(crate::t!("Delete").as_ref(), "D"), + ], + _ => Vec::new(), + } + } + + fn wants_text_input(&self) -> bool { + true + } + + fn input_kind(&self) -> InputKind { + match self.step { + Step::Option => InputKind::SingleToken, + _ => InputKind::FreeText, + } + } + + fn on_text_input(&mut self, text: &str) -> Option { + let text = text.trim(); + match &self.step { + Step::Option => { + self.step = match text.trim_start_matches('_').to_ascii_uppercase().as_str() { + "N" | "NEW" => Step::NewName, + "" | "S" | "SET" => Step::SetName, + "R" | "RENAME" => Step::RenameFrom, + "D" | "DELETE" => Step::DeleteName, + _ => { + return Some(CmdResult::ReportError( + crate::t!("Invalid option keyword.").into_owned(), + )) + } + }; + Some(CmdResult::NeedPoint) + } + Step::NewName if text.is_empty() => Some(CmdResult::NeedPoint), + Step::NewName => match self.known(text) { + Some(_) => Some(CmdResult::ReportError( + crate::tf!("Visibility state \"{}\" already exists.", text).into_owned(), + )), + None => Some(Self::done(&["NEW", text])), + }, + Step::SetName => { + let name = if text.is_empty() { + self.current_name() + } else { + text.to_string() + }; + Some(match self.known(&name) { + Some(name) => Self::done(&["SET", &name]), + None => Self::unknown(&name), + }) + } + Step::RenameFrom => { + let name = if text.is_empty() { + self.current_name() + } else { + text.to_string() + }; + match self.known(&name) { + Some(name) => { + self.step = Step::RenameTo(name); + Some(CmdResult::NeedPoint) + } + None => Some(Self::unknown(&name)), + } + } + Step::RenameTo(_) if text.is_empty() => Some(CmdResult::NeedPoint), + Step::RenameTo(from) => { + let taken = self + .known(text) + .is_some_and(|existing| !existing.eq_ignore_ascii_case(from)); + Some(if taken { + CmdResult::ReportError( + crate::tf!("Visibility state \"{}\" already exists.", text).into_owned(), + ) + } else { + Self::done(&["RENAME", from, text]) + }) + } + Step::DeleteName if text.is_empty() => Some(CmdResult::NeedPoint), + Step::DeleteName => Some(match self.known(text) { + Some(_) if self.names.len() <= 1 => CmdResult::ReportError( + crate::t!("The only visibility state cannot be deleted.").into_owned(), + ), + Some(name) => Self::done(&["DELETE", &name]), + None => Self::unknown(text), + }), + } + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_entity_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + self.on_text_input("").unwrap_or(CmdResult::Cancel) + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["BVSTATE", "BVHIDE", "BVSHOW"] +}); diff --git a/src/modules/draw/modify/explode.rs b/src/modules/draw/modify/explode.rs index ba11835a4..441909933 100644 --- a/src/modules/draw/modify/explode.rs +++ b/src/modules/draw/modify/explode.rs @@ -23,8 +23,8 @@ use codec::entities::{ }; use codec::entities::{Polyline, Polyline2D}; use codec::tables::BlockRecord; -use codec::types::{Transform, Vector3}; -use codec::{CadDocument, Entity, EntityType, Handle}; +use codec::types::Vector3; +use codec::{CadDocument, EntityType, Handle}; use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::entities::curve::lwpolyline_world_xy; @@ -98,37 +98,11 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec { - let explodable = document - .block_records - .get(&ins.block_name) - .map_or(true, |br| br.explodable); - if !explodable { - vec![] - } else { - // The codec applies only `Insert::get_transform`, which does - // not know the block's base point, so shift the definition's - // contents by -base first: get_transform(P - base) is exactly - // world = insert_point + Rot*Scale*(P - base), the placement - // the render path uses. MINSERT cells and nested references - // get the same treatment because the shift happens on the - // contents, not on the insert. - let base = crate::scene::render_graph::block_base_point(document, &ins.block_name); - let to_block = Transform::from_translation(Vector3::new(-base.x, -base.y, -base.z)); - let entities: Vec = document - .entities_in_block(&ins.block_name) - .map(|entity| { - let mut shifted = entity.clone(); - Entity::apply_transform(shifted.as_entity_mut(), &to_block); - shifted - }) - .collect(); - ins.explode(&entities) - .into_iter() - .map(normalize_insert_entity) - .collect() - } - } + EntityType::Insert(ins) => ins + .explode_from_document(document) + .into_iter() + .map(normalize_insert_entity) + .collect(), EntityType::MLine(ml) => explode_mline(ml), EntityType::Dimension(dim) => explode_dimension(dim, document), EntityType::MText(t) => crate::entities::mtext::explode_mtext(t, document), diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index 2d37d8f84..3b2cf0fa7 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -1027,6 +1027,117 @@ fn rebuild_poly_from_trimmed_line( lwpoly_shorten_seg(poly, seg_idx, new_start, new_end) } +/// Open (if needed) and cut a polyline at a fillet with a line or arc, then +/// join the fillet arc and the trimmed object onto the cut end, as the +/// reference does: the polyline keeps the part on the picked side of the +/// segment, everything past the fillet point is dropped, and the other object +/// becomes part of the polyline. `moved` is the polyline as the plain fillet +/// rebuilt it, with the segment's moved end at the tangent point. `None` when +/// the pieces cannot be joined (curved segment, out-of-plane arc). +fn join_poly_fillet( + poly: &LwPolyline, + seg_idx: usize, + moved: &LwPolyline, + other: &EntityType, + fillet: Option<&EntityType>, +) -> Option { + let n = poly.vertices.len(); + if n < 2 || moved.vertices.len() != n || poly.vertices[seg_idx].bulge.abs() > 1e-12 { + return None; + } + let xy = |v: &LwVertex| [v.location.x, v.location.y]; + let near = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).hypot(a[1] - b[1]) < 1e-9; + let end_idx = (seg_idx + 1) % n; + let start_moved = !near(xy(&poly.vertices[seg_idx]), xy(&moved.vertices[seg_idx])); + let end_moved = !near(xy(&poly.vertices[end_idx]), xy(&moved.vertices[end_idx])); + if start_moved == end_moved { + return None; + } + let t = if start_moved { + xy(&moved.vertices[seg_idx]) + } else { + xy(&moved.vertices[end_idx]) + }; + + // The fillet arc runs from T to T2 on the other object. + let (t2, fillet_center) = match fillet { + Some(EntityType::Arc(a)) if a.normal.z > 0.0 => { + let (c, r, sa, ea, _) = arc_geom(a); + let p0 = [c[0] + r * sa.cos(), c[1] + r * sa.sin()]; + let p1 = [c[0] + r * ea.cos(), c[1] + r * ea.sin()]; + let dist = |p: [f64; 2]| (p[0] - t[0]).hypot(p[1] - t[1]); + (if dist(p0) > dist(p1) { p0 } else { p1 }, Some(c)) + } + Some(_) => return None, + None => (t, None), + }; + + // The other object from T2 to its far end, with the bulge that runs it + // in that direction. + let (far, far_bulge) = match other { + EntityType::Line(l) => { + let a = [l.start.x, l.start.y]; + let b = [l.end.x, l.end.y]; + let dist = |p: [f64; 2]| (p[0] - t2[0]).hypot(p[1] - t2[1]); + (if dist(a) > dist(b) { a } else { b }, 0.0) + } + EntityType::Arc(a) if a.normal.z > 0.0 => { + let (c, r, sa, ea, _) = arc_geom(a); + let p0 = [c[0] + r * sa.cos(), c[1] + r * sa.sin()]; + let p1 = [c[0] + r * ea.cos(), c[1] + r * ea.sin()]; + let quarter = (arc_span(sa, ea) / 4.0).tan(); + let dist = |p: [f64; 2]| (p[0] - t2[0]).hypot(p[1] - t2[1]); + if dist(p0) <= dist(p1) { + (p1, quarter) + } else { + (p0, -quarter) + } + } + _ => return None, + }; + + let vertex = |p: [f64; 2], bulge: f64| { + let mut v = poly.vertices[seg_idx].clone(); + v.location.x = p[0]; + v.location.y = p[1]; + v.bulge = bulge; + v + }; + // A closed polyline is opened at its closing segment first. + let mut open = poly.vertices.clone(); + if poly.is_closed { + open.push(vertex(xy(&poly.vertices[0]), 0.0)); + } + let mut vertices = Vec::with_capacity(open.len() + 3); + if end_moved { + // Keep the start side: vertices up to the segment, then T, the + // fillet, and the other object. + vertices.extend_from_slice(&open[..=seg_idx]); + let bulge = fillet_center.map_or(0.0, |c| compute_bulge(t, t2, c)); + vertices.push(vertex(t, bulge)); + if fillet_center.is_some() { + vertices.push(vertex(t2, far_bulge)); + } else if let Some(last) = vertices.last_mut() { + last.bulge = far_bulge; + } + vertices.push(vertex(far, 0.0)); + } else { + // Keep the end side: the other object, the fillet, then T and the + // vertices after the segment. + let other_back = -far_bulge; + vertices.push(vertex(far, other_back)); + if let Some(c) = fillet_center { + vertices.push(vertex(t2, compute_bulge(t2, t, c))); + } + vertices.push(vertex(t, 0.0)); + vertices.extend_from_slice(&open[seg_idx + 1..]); + } + let mut joined = moved.clone(); + joined.vertices = vertices; + joined.is_closed = false; + Some(joined) +} + /// Fillet a Line and an Arc. /// Fillets of `radius` tangent to both curves, nearest pick first. /// @@ -1861,6 +1972,54 @@ impl CadCommand for FilletCommand { .map(|arc| self.continue_after_addition(arc)) .unwrap_or(CmdResult::NeedPoint); } + // A polyline filleted with a line or arc takes + // the fillet and the other object in. + let joined = match (&e1, &e2, &new_e1, &new_e2) { + ( + FilletEntity::LwPoly { poly, seg_idx, .. }, + FilletEntity::Line(_) | FilletEntity::Arc(_), + EntityType::LwPolyline(moved), + other, + ) if !same_entity => { + join_poly_fillet( + poly, + *seg_idx, + moved, + other, + maybe_arc.as_ref(), + ) + .map(|poly| { + vec![ + (h1, vec![EntityType::LwPolyline(poly)]), + (handle, Vec::new()), + ] + }) + } + ( + FilletEntity::Line(_) | FilletEntity::Arc(_), + FilletEntity::LwPoly { poly, seg_idx, .. }, + other, + EntityType::LwPolyline(moved), + ) if !same_entity => { + join_poly_fillet( + poly, + *seg_idx, + moved, + other, + maybe_arc.as_ref(), + ) + .map(|poly| { + vec![ + (handle, vec![EntityType::LwPolyline(poly)]), + (h1, Vec::new()), + ] + }) + } + _ => None, + }; + if let Some(replacements) = joined { + return self.continue_after_fillet(replacements); + } let mut first_replacements = vec![new_e1]; if let Some(arc) = maybe_arc { first_replacements.push(arc); diff --git a/src/modules/draw/modify/join.rs b/src/modules/draw/modify/join.rs index 335e68743..8a0009350 100644 --- a/src/modules/draw/modify/join.rs +++ b/src/modules/draw/modify/join.rs @@ -445,14 +445,64 @@ fn segs_of(e: &EntityType) -> Option> { } } +/// Join `entities` end-to-end: into one entity when they form a single +/// chain, otherwise chain by chain, leaving alone what joins nothing (a +/// closed polyline, a lone object, a branching set). Returns +/// `(removed_handles, new_entities)`, or `None` when nothing joined. +pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec, Vec)> { + if let Some(joined) = join_chain(entities) { + return Some(joined); + } + let ends: Vec>> = entities + .iter() + .map(|(_, entity)| { + segs_of(entity).map(|segs| segs.iter().flat_map(|seg| [seg.a, seg.b]).collect()) + }) + .collect(); + fn root(group: &mut [usize], mut index: usize) -> usize { + while group[index] != index { + group[index] = group[group[index]]; + index = group[index]; + } + index + } + // ponytail: all-pairs endpoint test, a spatial grid if huge selections drag. + let mut group: Vec = (0..entities.len()).collect(); + for a in 0..entities.len() { + for b in a + 1..entities.len() { + let (Some(pa), Some(pb)) = (&ends[a], &ends[b]) else { + continue; + }; + if pa.iter().any(|p| pb.iter().any(|q| p.distance(*q) <= JOIN_EPS)) { + let (ra, rb) = (root(&mut group, a), root(&mut group, b)); + group[ra] = rb; + } + } + } + let mut chains: std::collections::BTreeMap> = + Default::default(); + for (index, entity) in entities.iter().enumerate() { + if ends[index].is_some() { + chains.entry(root(&mut group, index)).or_default().push(*entity); + } + } + let (mut removed, mut merged) = (Vec::new(), Vec::new()); + for chain in chains.values().filter(|chain| chain.len() < entities.len()) { + if let Some((gone, made)) = join_chain(chain) { + removed.extend(gone); + merged.extend(made); + } + } + (!removed.is_empty()).then_some((removed, merged)) +} + /// Join all `entities` end-to-end into a single entity. Segments join /// wherever their endpoints touch โ€” the angle between them is irrelevant. /// A collinear straight run collapses to one `Line`; a planar chain /// becomes an `LwPolyline` (arcs kept as bulges); a chain with varying Z -/// becomes a `Polyline3D` (straight segments only). Returns -/// `(removed_handles, new_entities)`, or `None` when the selection isn't a -/// single connected chain or holds an unsupported entity. -pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec, Vec)> { +/// becomes a `Polyline3D` (straight segments only). `None` when the +/// selection isn't a single connected chain or holds an unsupported entity. +fn join_chain(entities: &[(Handle, &EntityType)]) -> Option<(Vec, Vec)> { if entities.len() < 2 { return None; } @@ -736,6 +786,35 @@ mod join_tests { ); } + // Several separate squares of lines join square by square (#1765). + #[test] + fn separate_chains_join_one_by_one() { + let square = |o: f64, size: f64| { + [ + line(o, o, o + size, o, 0.0), + line(o + size, o, o + size, o + size, 0.0), + line(o + size, o + size, o, o + size, 0.0), + line(o, o + size, o, o, 0.0), + ] + }; + let lines: Vec = + square(0.0, 10.0).into_iter().chain(square(2.0, 6.0)).collect(); + let lone = line(50.0, 0.0, 60.0, 0.0, 0.0); + let mut pairs: Vec<(Handle, &EntityType)> = lines + .iter() + .enumerate() + .map(|(index, entity)| (Handle::new(index as u64 + 1), entity)) + .collect(); + pairs.push((Handle::new(99), &lone)); + let (removed, out) = join_entities(&pairs).expect("both squares join"); + assert_eq!(removed.len(), 8); + assert!(!removed.contains(&Handle::new(99))); + assert_eq!(out.len(), 2); + assert!(out + .iter() + .all(|entity| matches!(entity, EntityType::LwPolyline(pl) if pl.is_closed))); + } + #[test] fn source_join_consumes_only_compatible_lines_and_keeps_direction() { let source_handle = Handle::new(11); diff --git a/src/modules/draw/modify/mod.rs b/src/modules/draw/modify/mod.rs index d8e35bd8d..8087c4efa 100644 --- a/src/modules/draw/modify/mod.rs +++ b/src/modules/draw/modify/mod.rs @@ -1,9 +1,11 @@ pub mod align; pub mod array; +pub mod arrayedit; pub mod attedit; pub mod blend; pub mod block_edit; pub mod break_cmd; +pub mod bvstate; pub mod copy; pub mod delete; mod entity_index; @@ -30,3 +32,4 @@ pub mod stretch; pub mod torient; pub mod translate; pub mod trim; +pub mod txtexp; diff --git a/src/modules/draw/modify/refedit.rs b/src/modules/draw/modify/refedit.rs index 1dc922c31..b1d25e969 100644 --- a/src/modules/draw/modify/refedit.rs +++ b/src/modules/draw/modify/refedit.rs @@ -1,25 +1,44 @@ -// REFEDIT โ€” in-place block reference editing. +// REFEDIT / -REFEDIT / REFSET / REFCLOSE โ€” in-place reference editing. // // Workflow: -// 1. REFEDIT: user picks an INSERT entity. -// 2. The block's entities are copied into model space with the INSERT -// transform applied (translate + rotate + uniform scale). -// 3. The user edits them with normal commands (MOVE, COPY, DELETE, โ€ฆ). -// 4. REFCLOSE SAVE: temp entities are inverse-transformed back into the -// block definition; all INSERT references auto-update. -// REFCLOSE DISCARD: temp entities are removed, block unchanged. -// -// Limitation: non-uniform scale inserts (x_scale โ‰  y_scale) are rejected -// with an error message โ€” full matrix inversion for those cases would -// require per-entity matrix transforms not yet in EntityTransform. +// 1. REFEDIT (or -REFEDIT on the command line) picks a block reference or +// xref; the nesting chain under the pick is offered innermost first and +// `Next` steps outward. +// 2. The chosen definition's objects (all of them, or the nested objects +// picked) are copied into the current space with the composed placement +// of the chain. Everything else fades by XFADECTL. +// 3. The user edits them with normal commands; REFSET moves host objects +// into the working set or working-set objects out to the host. +// 4. REFCLOSE Save writes the working set back into the definition (and +// into the referenced file for an xref); Discard drops the copies. use crate::t; use codec::{EntityType, Handle}; use glam::DVec3; +use std::collections::HashSet; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdOption, CmdResult}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; +/// Right-edge side-toolbar buttons that move objects into or out of the +/// working set while a REFEDIT session is active (REFSET Add / Remove). +pub fn refedit_set_tools() -> Vec { + vec![ + ToolDef { + id: "REFSET_ADD", + label: "Add to Working Set", + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/refset_add.svg")), + event: ModuleEvent::Command("REFSET_ADD".to_string()), + }, + ToolDef { + id: "REFSET_REMOVE", + label: "Remove from Working Set", + icon: IconKind::Svg(include_bytes!("../../../../assets/icons/refset_remove.svg")), + event: ModuleEvent::Command("REFSET_REMOVE".to_string()), + }, + ] +} + /// Right-edge side-toolbar buttons shown while a REFEDIT session is active: /// save the in-place block edit, or discard it. (#136) pub fn refedit_tools() -> Vec { @@ -39,22 +58,33 @@ pub fn refedit_tools() -> Vec { ] } +/// Printed by REFSET and REFCLOSE outside a session. +pub const NOT_CHECKED_OUT: &str = + "** Command not allowed unless a reference is checked out with REFEDIT command **"; + // โ”€โ”€ Session state (held in DocumentTab) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /// Active REFEDIT session. Lives in `DocumentTab::refedit_session`. #[derive(Debug, Clone)] pub struct RefEditSession { - /// Name of the block being edited. + /// Name of the definition being edited (REFEDITNAME). pub block_name: String, /// Handle of the block record (owns the block entities). pub br_handle: Handle, - /// Handles of the temporary model-space entities added for editing. + /// The working copies of the extracted definition objects. pub temp_handles: Vec, - // โ”€โ”€ INSERT placement, as full affine transforms โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - /// Block-local โ†’ world (the INSERT's `get_transform`). Handles OCS, - /// rotation and non-uniform / mirrored scale in one matrix. + /// The definition objects that were copied out, index-aligned with + /// `temp_handles`. Objects not extracted (attribute definitions when they + /// are not displayed, objects not picked in Nested mode) stay in the + /// definition untouched. + pub extracted: Vec, + /// Host objects moved into the working set with REFSET Add. + pub added: HashSet, + /// Working-set objects moved out to the host with REFSET Remove. + pub removed: HashSet, + /// Definition-local โ†’ world: the composed placement of the chain. pub forward: codec::types::Transform, - /// World โ†’ block-local, applied on SAVE to bring edits back. + /// World โ†’ definition-local, applied on SAVE to bring edits back. pub inverse: codec::types::Transform, /// Handle watermark captured before the temp copies were created: every /// entity allocated at or above it during the session (drawn, pasted, @@ -62,51 +92,603 @@ pub struct RefEditSession { /// and Discard removes it (#423). Catches every creation path without /// per-command hooks. pub handle_watermark: u64, + /// The referenced file when the definition is an xref. + pub xref_path: Option, + /// Objects outside the working set can't be selected while it is open. + pub lock: bool, + /// The external reference's named objects the working set uses under + /// host names, and the host records made for the session. + pub names: RefEditNames, } -// โ”€โ”€ REFEDIT pick command โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +/// A kind of named object the working set's objects refer to. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SymbolKind { + Layer, + Linetype, + TextStyle, + Block, +} -/// Step 1: wait for the user to pick a single INSERT entity. -pub struct RefEditPickCommand; +/// How an external reference's dependent names (`XREF|NAME`) are used in +/// the host while its objects are checked out: as `$0$NAME` records of +/// their own (unique names), or under the plain name, sharing a host record +/// of that name when there is one. +#[derive(Debug, Clone, Default)] +pub struct RefEditNames { + /// (kind, name used in the host, the reference's dependent name). + pub renamed: Vec<(SymbolKind, String, String)>, + /// Records added to the host for the session, removed when it closes. + pub created: Vec<(SymbolKind, String)>, +} + +impl RefEditNames { + /// The reference's own name for a host name used in the session. + pub fn original(&self, kind: SymbolKind, name: &str) -> Option<&str> { + self.renamed + .iter() + .find(|(k, used, _)| *k == kind && used.eq_ignore_ascii_case(name)) + .map(|(_, _, original)| original.as_str()) + } +} + +/// Rewrites every named-object reference of an entity through `map` +/// (layer, linetype, text style, block). +pub fn rename_symbols(entity: &mut EntityType, map: &dyn Fn(SymbolKind, &str) -> Option) { + let common = entity.common_mut(); + if let Some(name) = map(SymbolKind::Layer, &common.layer) { + common.layer = name; + common.layer_handle = None; + } + if let Some(name) = map(SymbolKind::Linetype, &common.linetype) { + common.linetype = name; + common.linetype_handle = None; + } + match entity { + EntityType::Insert(insert) => { + if let Some(name) = map(SymbolKind::Block, &insert.block_name) { + insert.block_name = name; + } + } + EntityType::Text(text) => { + if let Some(name) = map(SymbolKind::TextStyle, &text.style) { + text.style = name; + } + } + EntityType::MText(text) => { + if let Some(name) = map(SymbolKind::TextStyle, &text.style) { + text.style = name; + } + } + EntityType::AttributeDefinition(attdef) => { + if let Some(name) = map(SymbolKind::TextStyle, &attdef.text_style) { + attdef.text_style = name; + } + } + _ => {} + } +} + +impl RefEditSession { + /// Whether `handle` (a host-space object) is in the working set. + pub fn in_working_set(&self, handle: Handle) -> bool { + if self.removed.contains(&handle) { + return false; + } + self.temp_handles.contains(&handle) + || self.added.contains(&handle) + || handle.value() >= self.handle_watermark + } +} + +// โ”€โ”€ Nesting chain under a pick โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/// One level of the nesting chain: the reference (top-level object handle for +/// level 0, the definition-owned INSERT handle below it) and its definition. +#[derive(Debug, Clone)] +pub struct ChainLevel { + pub insert: Handle, + pub block_name: String, + /// Definition-local โ†’ world for this level. + pub forward: codec::types::Transform, +} + +/// The pickable objects of one definition, in world space, for Nested +/// selection: (definition-owned handle, polyline paths of its leaves). +pub type LevelHits = Vec<(Handle, Vec>)>; + +/// World paths of an entity for hit-testing (curves sampled by the renderer; +/// block references recurse into their definitions). +pub fn hit_paths( + entity: &EntityType, + document: &codec::CadDocument, + depth: usize, +) -> Vec> { + use crate::entities::traits::EntityTypeOps; + use crate::scene::convert::acad_to_render::RenderObject; + if let EntityType::Insert(insert) = entity { + if depth > 8 { + return Vec::new(); + } + let Some(br) = document.block_records.get(&insert.block_name) else { + return Vec::new(); + }; + let base = crate::scene::render_graph::block_base_point(document, &insert.block_name); + let placement = crate::scene::render_graph::insert_transform_with_base(insert, base); + return br + .entity_handles + .iter() + .filter_map(|h| document.get_entity(*h)) + .filter(|e| !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_))) + .flat_map(|child| { + let mut child = child.clone(); + child.as_entity_mut().apply_transform(&placement); + hit_paths(&child, document, depth + 1) + }) + .collect(); + } + match entity + .to_render_entity(document) + .map(|render| render.object) + { + Some(RenderObject::Dot(point)) => vec![vec![point]], + Some( + RenderObject::Lines(points) + | RenderObject::SegmentedLines(points) + | RenderObject::TaperedLines(points, _) + | RenderObject::BoundaryLines { points, .. }, + ) => vec![points], + _ => Vec::new(), + } +} + +/// Distance from `point` to a set of paths. +pub fn paths_distance(paths: &[Vec<[f64; 3]>], point: DVec3) -> Option { + let p = kernel::space::Vec3::from(point.to_array()); + paths + .iter() + .filter_map(|path| { + if path.len() == 1 { + return Some(p.distance(kernel::space::Vec3::from(path[0]))); + } + path.windows(2) + .filter(|s| s.iter().flatten().all(|v| v.is_finite())) + .map(|s| p.distance_to_segment(s[0].into(), s[1].into())) + .min_by(f64::total_cmp) + }) + .min_by(f64::total_cmp) +} + +/// The world-space objects of `level`'s definition with their hit paths. +pub fn level_hits(document: &codec::CadDocument, level: &ChainLevel) -> LevelHits { + let Some(br) = document.block_records.get(&level.block_name) else { + return Vec::new(); + }; + br.entity_handles + .iter() + .filter_map(|h| document.get_entity(*h).map(|e| (*h, e))) + .filter(|(_, e)| { + !matches!( + e, + EntityType::Block(_) | EntityType::BlockEnd(_) | EntityType::AttributeDefinition(_) + ) + }) + .map(|(h, e)| { + let mut world = e.clone(); + world.as_entity_mut().apply_transform(&level.forward); + (h, hit_paths(&world, document, 0)) + }) + .collect() +} + +/// The nesting chain of `top` under `point`, outermost first: the top-level +/// reference, then every definition-owned reference the nearest object under +/// the point sits in. Without a point only the top level is returned. +pub fn nesting_chain( + document: &codec::CadDocument, + top: Handle, + point: Option, +) -> Vec { + let Some(EntityType::Insert(insert)) = document.get_entity(top) else { + return Vec::new(); + }; + let base = crate::scene::render_graph::block_base_point(document, &insert.block_name); + let mut chain = vec![ChainLevel { + insert: top, + block_name: insert.block_name.clone(), + forward: crate::scene::render_graph::insert_transform_with_base(insert, base), + }]; + let Some(point) = point else { + return chain; + }; + for _ in 0..16 { + let level = chain.last().expect("chain starts with the top level").clone(); + let nearest = level_hits(document, &level) + .into_iter() + .filter_map(|(h, paths)| paths_distance(&paths, point).map(|d| (h, d))) + .min_by(|a, b| a.1.total_cmp(&b.1)); + let Some((handle, _)) = nearest else { + break; + }; + let Some(EntityType::Insert(inner)) = document.get_entity(handle) else { + break; + }; + if chain + .iter() + .any(|l| l.block_name.eq_ignore_ascii_case(&inner.block_name)) + { + break; + } + let base = crate::scene::render_graph::block_base_point(document, &inner.block_name); + let local = crate::scene::render_graph::insert_transform_with_base(inner, base); + chain.push(ChainLevel { + insert: handle, + block_name: inner.block_name.clone(), + forward: local.then(&level.forward), + }); + } + chain +} + +// โ”€โ”€ REFEDIT / -REFEDIT pick command โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/// `Select reference:` โ€” a block reference or xref of the current space. +pub struct RefEditPickCommand { + name: &'static str, + references: HashSet, +} impl RefEditPickCommand { - pub fn new() -> Self { - Self + /// `command_line` selects -REFEDIT (prompts) over REFEDIT. + pub fn new(command_line: bool, references: HashSet) -> Self { + Self { + name: if command_line { "-REFEDIT" } else { "REFEDIT" }, + references, + } } } impl CadCommand for RefEditPickCommand { fn name(&self) -> &'static str { - "REFEDIT" + self.name } fn prompt(&self) -> String { - crate::t!("REFEDIT Select block reference to edit:").into_owned() + t!("Select reference:").into_owned() } fn needs_entity_pick(&self) -> bool { true } - fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { + fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { if handle.is_null() { - return CmdResult::NeedPoint; + return CmdResult::ReportError(t!("Reference not found.").into_owned()); } - // Signal the host to enter the editing session for this handle. - // We reuse Relaunch("REFEDIT_BEGIN:") as a convention. - CmdResult::Relaunch(format!("REFEDIT_BEGIN:{}", handle.value()), vec![handle]) + if !self.references.contains(&handle) { + return CmdResult::ReportError(t!("Object is not a valid reference").into_owned()); + } + CmdResult::Relaunch( + format!( + "REFEDIT_PICKED:{}:{}:{}:{}:{}", + (self.name == "-REFEDIT") as u8, + handle.value(), + pt.x, + pt.y, + pt.z + ), + vec![handle], + ) } + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::ReportError(t!("Reference not found.").into_owned()) + } + fn on_enter(&mut self) -> CmdResult { + CmdResult::ReportError(t!("Reference not found.").into_owned()) + } +} + +// โ”€โ”€ -REFEDIT options after the pick โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +enum OptionStep { + Nesting, + Method, + Nested, + Attributes, +} + +/// `Select nesting level [Ok/Next] :` โ†’ `Enter object selection method +/// [All/Nested] :` (โ†’ `Select nested objects:`) โ†’ `Display attribute +/// definitions [Yes/No] :`, then the session starts. +pub struct RefEditOptionsCommand { + top: Handle, + chain: Vec, + hits: Vec, + level: usize, + step: OptionStep, + nested: Vec, + /// The session settings (lock objects outside the working set, unique + /// names for an external reference's named objects). + lock: bool, + unique: bool, + /// Set when the Edit Reference dialog already chose the attribute + /// option: the nested pick then opens the session directly. + attributes: Option, +} + +impl RefEditOptionsCommand { + pub fn new( + top: Handle, + chain: Vec, + hits: Vec, + lock: bool, + unique: bool, + ) -> Self { + let level = chain.len().saturating_sub(1); + Self { + top, + chain, + hits, + level, + step: OptionStep::Nesting, + nested: Vec::new(), + lock, + unique, + attributes: None, + } + } + + /// `Select nested objects:` for the dialog's "pick in the drawing" + /// working set at `level`. + pub fn nested_pick( + top: Handle, + chain: Vec, + hits: Vec, + level: usize, + attributes: bool, + lock: bool, + unique: bool, + ) -> Self { + let mut command = Self::new(top, chain, hits, lock, unique); + command.level = level.min(command.chain.len().saturating_sub(1)); + command.step = OptionStep::Nested; + command.attributes = Some(attributes); + command + } + + fn begin(&self, attributes: bool) -> CmdResult { + let path: Vec = self.chain[1..=self.level] + .iter() + .map(|l| l.insert.value().to_string()) + .collect(); + let nested: Vec = self.nested.iter().map(|h| h.value().to_string()).collect(); + CmdResult::Relaunch( + format!( + "REFEDIT_BEGIN:{}:{}:{}:{}:{}:{}", + self.top.value(), + path.join(","), + attributes as u8, + nested.join(","), + self.lock as u8, + self.unique as u8 + ), + vec![], + ) + } + + fn pick_nested(&mut self, point: DVec3) -> CmdResult { + let nearest = self + .hits + .get(self.level) + .into_iter() + .flatten() + .filter_map(|(h, paths)| paths_distance(paths, point).map(|d| (*h, d))) + .min_by(|a, b| a.1.total_cmp(&b.1)); + match nearest { + Some((h, _)) if !self.nested.contains(&h) => { + self.nested.push(h); + CmdResult::ReportMeasurement(t!("1 entity added").into_owned()) + } + _ => CmdResult::NeedPoint, + } + } +} + +impl CadCommand for RefEditOptionsCommand { + fn name(&self) -> &'static str { + "-REFEDIT" + } + fn prompt(&self) -> String { + match self.step { + OptionStep::Nesting => t!("Select nesting level [Ok/Next] :").into_owned(), + OptionStep::Method => { + t!("Enter object selection method [All/Nested] :").into_owned() + } + OptionStep::Nested => t!("Select nested objects:").into_owned(), + OptionStep::Attributes => { + t!("Display attribute definitions [Yes/No] :").into_owned() + } + } + } + fn options(&self) -> Vec { + match self.step { + OptionStep::Nesting => vec![CmdOption::new("Ok", "O"), CmdOption::new("Next", "N")], + OptionStep::Method => vec![CmdOption::new("All", "A"), CmdOption::new("Nested", "N")], + OptionStep::Nested => Vec::new(), + OptionStep::Attributes => { + vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")] + } + } + } + fn wants_text_input(&self) -> bool { + !matches!(self.step, OptionStep::Nested) + } + fn needs_entity_pick(&self) -> bool { + matches!(self.step, OptionStep::Nested) + } + fn on_entity_pick(&mut self, _handle: Handle, pt: DVec3) -> CmdResult { + self.pick_nested(pt) + } + fn on_text_input(&mut self, text: &str) -> Option { + let word = text.trim().to_ascii_uppercase(); + if word.is_empty() { + return None; + } + let matches = |full: &str| full.starts_with(word.as_str()); + Some(match self.step { + OptionStep::Nesting if matches("OK") => { + self.step = OptionStep::Method; + CmdResult::NeedPoint + } + OptionStep::Nesting if matches("NEXT") => self.on_enter(), + OptionStep::Method if matches("ALL") => self.on_enter(), + OptionStep::Method if matches("NESTED") => { + self.step = OptionStep::Nested; + CmdResult::NeedPoint + } + OptionStep::Attributes if matches("YES") => self.begin(true), + OptionStep::Attributes if matches("NO") => self.begin(false), + OptionStep::Nested => return None, + _ => CmdResult::ReportError(t!("Invalid option keyword.").into_owned()), + }) + } + fn on_point(&mut self, pt: DVec3) -> CmdResult { + match self.step { + OptionStep::Nested => self.pick_nested(pt), + _ => CmdResult::NeedPoint, + } + } + fn on_enter(&mut self) -> CmdResult { + match self.step { + OptionStep::Nesting => { + // Next steps outward and wraps back to the innermost level. + self.level = if self.level == 0 { + self.chain.len().saturating_sub(1) + } else { + self.level - 1 + }; + CmdResult::NeedPoint + } + OptionStep::Method => { + self.step = OptionStep::Attributes; + CmdResult::NeedPoint + } + OptionStep::Nested => { + let n = self.nested.len(); + if n == 0 { + return CmdResult::CancelWithMessage(t!("0 items selected").into_owned()); + } + if let Some(attributes) = self.attributes { + return self.begin(attributes); + } + self.step = OptionStep::Attributes; + CmdResult::ReportMeasurement(if n == 1 { + t!("1 item selected").into_owned() + } else { + crate::tf!("{} items selected", n).into_owned() + }) + } + OptionStep::Attributes => self.begin(false), + } + } +} + +// โ”€โ”€ REFSET โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/// `Enter an option [Add/Remove] :` then `Select objects:`. +pub struct RefSetCommand { + add: bool, + gathering: bool, + selected: Vec, +} + +impl RefSetCommand { + pub fn new() -> Self { + Self { + add: true, + gathering: false, + selected: Vec::new(), + } + } + + /// Straight to `Select objects:` for Add (`add`) or Remove. + pub fn selecting(add: bool) -> Self { + Self { + add, + gathering: true, + selected: Vec::new(), + } + } +} + +impl CadCommand for RefSetCommand { + fn name(&self) -> &'static str { + "REFSET" + } + fn prompt(&self) -> String { + if self.gathering { + t!("Select objects:").into_owned() + } else { + t!("Enter an option [Add/Remove] :").into_owned() + } + } + fn options(&self) -> Vec { + if self.gathering { + Vec::new() + } else { + vec![CmdOption::new("Add", "A"), CmdOption::new("Remove", "R")] + } + } + fn wants_text_input(&self) -> bool { + !self.gathering + } + fn is_selection_gathering(&self) -> bool { + self.gathering + } + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + self.selected = handles; + CmdResult::NeedPoint + } + fn on_text_input(&mut self, text: &str) -> Option { + if self.gathering { + return None; + } + let word = text.trim().to_ascii_uppercase(); + if word.is_empty() { + return None; + } + Some(if "ADD".starts_with(&word) { + self.gathering = true; + CmdResult::NeedPoint + } else if "REMOVE".starts_with(&word) { + self.add = false; + self.gathering = true; + CmdResult::NeedPoint + } else { + CmdResult::ReportError(t!("Invalid option keyword.").into_owned()) + }) + } fn on_point(&mut self, _pt: DVec3) -> CmdResult { CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel + if !self.gathering { + self.gathering = true; + return CmdResult::NeedPoint; + } + if self.selected.is_empty() { + return CmdResult::Cancel; + } + CmdResult::Relaunch( + format!("REFSET_APPLY:{}", if self.add { "ADD" } else { "REMOVE" }), + std::mem::take(&mut self.selected), + ) } } // โ”€โ”€ REFCLOSE command โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -/// Step 4: prompt for SAVE or DISCARD. +/// `Enter option [Save/Discard reference changes] :` pub struct RefCloseCommand; impl RefCloseCommand { @@ -120,25 +702,30 @@ impl CadCommand for RefCloseCommand { "REFCLOSE" } fn prompt(&self) -> String { - t!("REFCLOSE [Save/Discard] :").into_owned() + t!("Enter option [Save/Discard reference changes] :").into_owned() + } + fn options(&self) -> Vec { + vec![CmdOption::new("Save", "S"), CmdOption::new("Discard", "D")] } fn wants_text_input(&self) -> bool { true } fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim().to_uppercase(); - let save = t.is_empty() || t == "S" || t == "SAVE"; - let discard = t == "D" || t == "DISCARD"; - if save { + if t.is_empty() { + return None; + } + if "SAVE".starts_with(&t) { Some(CmdResult::Relaunch("REFCLOSE_SAVE".into(), vec![])) - } else if discard { + } else if "DISCARD".starts_with(&t) { Some(CmdResult::Relaunch("REFCLOSE_DISCARD".into(), vec![])) } else { - Some(CmdResult::NeedPoint) // re-prompt + Some(CmdResult::ReportError( + t!("Invalid option keyword.").into_owned(), + )) } } fn on_enter(&mut self) -> CmdResult { - // Default: SAVE CmdResult::Relaunch("REFCLOSE_SAVE".into(), vec![]) } fn on_point(&mut self, _pt: DVec3) -> CmdResult { @@ -148,23 +735,48 @@ impl CadCommand for RefCloseCommand { // โ”€โ”€ Geometry helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -/// Apply the INSERT's forward transform to a block-local entity so it -/// appears at its correct world-space position. The full affine handles -/// OCS, rotation and non-uniform / mirrored scale at once. +/// Inverse of an affine placement (rotation / scale / mirror and translation), +/// or None when it is singular. +pub fn inverse_placement(t: &codec::types::Transform) -> Option { + use codec::types::{Matrix3, Matrix4}; + let m = &t.matrix.m; + let linear = Matrix3 { + m: [ + [m[0][0], m[0][1], m[0][2]], + [m[1][0], m[1][1], m[1][2]], + [m[2][0], m[2][1], m[2][2]], + ], + } + .inverse()?; + let r = linear.m; + let tr = [m[0][3], m[1][3], m[2][3]]; + let mut out = Matrix4::identity(); + for row in 0..3 { + for col in 0..3 { + out.m[row][col] = r[row][col]; + } + out.m[row][3] = -(r[row][0] * tr[0] + r[row][1] * tr[1] + r[row][2] * tr[2]); + } + Some(codec::types::Transform::from_matrix(out)) +} + + +/// Apply the session's forward transform to a definition-local entity so it +/// appears at its correct world-space position. pub fn apply_insert_transform(entity: &mut EntityType, session: &RefEditSession) { entity.as_entity_mut().apply_transform(&session.forward); } -/// Apply the INSERT's inverse transform to a world-space entity to bring it -/// back to block-local coordinates. +/// Apply the session's inverse transform to a world-space entity to bring it +/// back to definition-local coordinates. pub fn apply_insert_inverse_transform(entity: &mut EntityType, session: &RefEditSession) { entity.as_entity_mut().apply_transform(&session.inverse); } // โ”€โ”€ Autocomplete registry โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ inventory::submit!(crate::command::CommandRegistration { - names: &["REFCLOSE"] -}); // RefCloseCommand + names: &["REFCLOSE", "REFSET", "-REFEDIT"] +}); inventory::submit!(crate::command::CommandRegistration { names: &["REFEDIT"] }); // RefEditPickCommand diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 8e002a9d7..8c9faaeba 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3358,21 +3358,28 @@ pub struct ExtendCommand { impl ExtendCommand { pub fn new(all_entities: Vec) -> Self { + Self::with_boundary_edges(all_entities, Vec::new()) + } + + /// Objects selected before the command are its boundary edges, as TRIM + /// takes them as cutting edges. + pub fn with_boundary_edges(all_entities: Vec, initial_edges: Vec) -> Self { let all_entities: Vec = all_entities .iter() .map(entity_with_lwpolyline_world_xy) .collect(); let entity_index = ModifyEntityIndex::build(&all_entities); - let geos = build_geos(&all_entities); - Self { + let mut cmd = Self { all_entities, entity_index, - geos, + geos: Vec::new(), mode: TrimMode::Pick, - edge_set: Vec::new(), + edge_set: initial_edges, implied_edges: false, shift: false, - } + }; + cmd.rebuild_geos(); + cmd } fn rebuild_geos(&mut self) { diff --git a/src/modules/draw/modify/txtexp.rs b/src/modules/draw/modify/txtexp.rs new file mode 100644 index 000000000..adc851ad0 --- /dev/null +++ b/src/modules/draw/modify/txtexp.rs @@ -0,0 +1,87 @@ +// TXTEXP โ€” replace text with the outlines of its characters. +// +// Each glyph contour of a TEXT or MTEXT becomes a polyline on the text's +// layer and with its properties: a lightweight polyline when the text lies +// flat, a 3D polyline when it is tilted. + +use codec::entities::{LwPolyline, LwVertex, Polyline3D, Vertex3DPolyline}; +use codec::types::Vector2; +use codec::{CadDocument, EntityType, Handle}; + +use crate::scene::convert::acad_to_render::{convert, RenderObject}; + +/// The outlines replacing each explodable text of `selected`, as +/// (text, polylines) pairs ready for `apply_explode_replacements`. +pub fn plan_text_explode( + selected: &[(Handle, &EntityType)], + document: &CadDocument, +) -> Vec<(Handle, Vec)> { + selected + .iter() + .filter_map(|(handle, entity)| { + let outlines = text_outlines(entity, document); + (!outlines.is_empty()).then_some((*handle, outlines)) + }) + .collect() +} + +/// The character outlines of a TEXT or MTEXT; empty for anything else. +pub fn text_outlines(entity: &EntityType, document: &CadDocument) -> Vec { + let (common, elevation) = match entity { + EntityType::Text(text) => (&text.common, text.insertion_point.z), + EntityType::MText(text) => (&text.common, text.insertion_point.z), + _ => return Vec::new(), + }; + let Some(RenderObject::Text(groups)) = convert(entity, document).map(|render| render.object) + else { + return Vec::new(); + }; + let mut common = common.clone(); + common.handle = Handle::NULL; + let mut outlines = Vec::new(); + for group in &groups { + for stroke in group.strokes.iter().filter(|stroke| stroke.len() >= 2) { + let mut points: Vec<[f64; 3]> = stroke + .iter() + .map(|&[x, y]| { + let (x, y) = (x as f64, y as f64); + match group.plane { + Some(plane) => std::array::from_fn(|axis| { + plane.origin[axis] + plane.x_axis[axis] * x + plane.y_axis[axis] * y + }), + None => [group.origin[0] + x, group.origin[1] + y, elevation], + } + }) + .collect(); + let closed = points.len() > 2 && { + let (first, last) = (points[0], points[points.len() - 1]); + (0..3).all(|axis| (first[axis] - last[axis]).abs() <= 1e-9) + }; + if closed { + points.pop(); + } + let z = points[0][2]; + if points.iter().all(|point| (point[2] - z).abs() <= 1e-9) { + let mut polyline = LwPolyline::new(); + polyline.common = common.clone(); + polyline.is_closed = closed; + polyline.elevation = z; + polyline.vertices = points + .iter() + .map(|point| LwVertex::new(Vector2::new(point[0], point[1]))) + .collect(); + outlines.push(EntityType::LwPolyline(polyline)); + } else { + let mut polyline = Polyline3D::new(); + polyline.common = common.clone(); + polyline.flags.closed = closed; + polyline.vertices = points + .iter() + .map(|point| Vertex3DPolyline::from_xyz(point[0], point[1], point[2])) + .collect(); + outlines.push(EntityType::Polyline3D(polyline)); + } + } + } + outlines +} diff --git a/src/modules/draw/select.rs b/src/modules/draw/select.rs index f434bfeeb..ffe77ad73 100644 --- a/src/modules/draw/select.rs +++ b/src/modules/draw/select.rs @@ -41,6 +41,9 @@ pub struct SelectObjectsCommand { /// The command works on associative dimensions only; the host drops /// everything else from each completed selection. associative_dimensions_only: bool, + /// The kinds of object the command takes; the host drops the rest of + /// each completed selection silently, and Enter on nothing ends quietly. + keep: Option bool>, } impl SelectObjectsCommand { @@ -57,6 +60,7 @@ impl SelectObjectsCommand { auto_constrain_settings: false, prompt_text: None, associative_dimensions_only: false, + keep: None, } } @@ -74,6 +78,7 @@ impl SelectObjectsCommand { auto_constrain_settings: false, prompt_text: None, associative_dimensions_only: false, + keep: None, } } @@ -91,6 +96,7 @@ impl SelectObjectsCommand { auto_constrain_settings: false, prompt_text: None, associative_dimensions_only: false, + keep: None, } } @@ -108,6 +114,7 @@ impl SelectObjectsCommand { auto_constrain_settings: false, prompt_text: None, associative_dimensions_only: false, + keep: None, } } @@ -131,6 +138,18 @@ impl SelectObjectsCommand { command } + /// The command's own wording, keeping only what `keep` accepts. + pub fn filtered( + prompt_cmd: &str, + pending_cmd: &str, + prompt_text: &'static str, + keep: fn(&codec::EntityType) -> bool, + ) -> Self { + let mut command = Self::with_prompt(prompt_cmd, pending_cmd, prompt_text); + command.keep = Some(keep); + command + } + pub fn auto_constrain(pending_cmd: &str) -> Self { let mut command = Self::new(pending_cmd); command.auto_constrain_settings = true; @@ -203,6 +222,10 @@ impl CadCommand for SelectObjectsCommand { self.associative_dimensions_only } + fn selection_entity_filter(&self) -> Option bool> { + self.keep + } + fn selection_drops_locked_point_clouds(&self) -> bool { matches!( self.pending_cmd.as_str(), @@ -320,6 +343,9 @@ impl CadCommand for SelectObjectsCommand { } return CmdResult::NeedPoint; } + if self.keep.is_some() && self.handles.is_empty() { + return CmdResult::Relaunch(std::mem::take(&mut self.pending_cmd), Vec::new()); + } if !self.commit_on_enter || self.handles.is_empty() { return CmdResult::Cancel; } @@ -360,6 +386,7 @@ inventory::submit!(crate::command::CommandRegistration { "MOVE", "ROTATE", "SCALE", - "STRETCH" + "STRETCH", + "TXTEXP" ] }); // SelectObjectsCommand diff --git a/src/modules/insert/insert_block.rs b/src/modules/insert/insert_block.rs index b9e19c473..ed2225bd1 100644 --- a/src/modules/insert/insert_block.rs +++ b/src/modules/insert/insert_block.rs @@ -15,7 +15,7 @@ use crate::t; use codec::entities::{AttributeDefinition, AttributeEntity, Entity, Insert}; use codec::types::Vector3; use codec::EntityType; -use glam::{DVec3, Vec3}; +use glam::DVec3; use crate::app::settings::BlockInsertOptions; use crate::command::{CadCommand, CmdResult, DynField, InputKind, WorkingPlane}; @@ -130,6 +130,10 @@ pub struct InsertBlockCommand { /// through the same base-point convention the render path uses, so the /// host injects it when it resolves the block's ATTDEFs. block_base: Vector3, + /// Where the preview wires' block origin sits: zero for wires taken from + /// the definition, the clipboard base for paste-as-block, whose preview + /// is the clipboard in world coordinates. + preview_origin: DVec3, } impl InsertBlockCommand { @@ -157,6 +161,7 @@ impl InsertBlockCommand { pending_insert: None, plane: WorkingPlane::default(), block_base: Vector3::ZERO, + preview_origin: DVec3::ZERO, } } @@ -219,12 +224,13 @@ impl InsertBlockCommand { command } - /// Paste-as-block: the block is placed at the picked point. - pub fn new_for_block(name: String, preview_wires: Vec, base: Vec3) -> Self { + /// Paste-as-block: the block is placed at the picked point. The block + /// was defined around `base`, so its origin is the clipboard base. + pub fn new_for_block(name: String, preview_wires: Vec, base: DVec3) -> Self { let mut command = Self::base_state(Mode::Paste, Self::single_picker(&name)); command.name = name; command.preview = preview_wires; - command.base = base.as_dvec3(); + command.preview_origin = base; command.preset_scale = [Some(1.0); 3]; command.preset_rotation = Some(0.0); command.step = Step::Point; @@ -822,7 +828,7 @@ impl CadCommand for InsertBlockCommand { self.preset_scale[2].unwrap_or(1.0), ]; let (sin, cos) = self.preset_rotation.unwrap_or(0.0).sin_cos(); - let base = self.base; + let base = self.preview_origin + self.base; let origin = self.plane.to_local(pt); let plane = self.plane; self.preview diff --git a/src/modules/insert/mod.rs b/src/modules/insert/mod.rs index 19b88737b..685317b9e 100644 --- a/src/modules/insert/mod.rs +++ b/src/modules/insert/mod.rs @@ -26,7 +26,7 @@ mod snap_underlays; pub(crate) mod solid3d_cmds; mod underlay_layers; pub(crate) mod wblock; -mod xadjust; +pub(crate) mod xadjust; pub(crate) mod xattach; pub(crate) mod xclip; pub(crate) mod xref_cmd; @@ -50,55 +50,6 @@ impl CadModule for InsertModule { static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); GROUPS.get_or_init(|| { vec![ - // โ”€โ”€ Reference โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - RibbonGroup { - title: "Reference", - tools: vec![ - RibbonItem::LargeTool(xattach::tool()), - // PDF, DWF, DGN or a point cloud; the face runs the - // last one chosen. - RibbonItem::LargeDropdown { - id: "UNDERLAY_ATTACH", - label: "Attach Underlay", - icon: pdf_attach::ICON, - items: vec![ - ("PDFATTACH", "Attach PDF", pdf_attach::ICON), - ("DWFATTACH", "Attach DWF", pdf_attach::ICON), - ("DGNATTACH", "Attach DGN", pdf_attach::ICON), - ("POINTCLOUDATTACH", "Attach Point Cloud", pc_attach::ICON), - ], - default: "PDFATTACH", - }, - RibbonItem::LargeTool(xclip::tool()), - RibbonItem::LargeTool(xadjust::tool()), - RibbonItem::LabeledTool(underlay_layers::tool()), - // An empty label shows the chosen item's. - RibbonItem::LabeledDropdown { - id: "FRAMES_DROPDOWN", - label: "", - icon: FRAMES_ICON, - items: vec![ - ("FRAMES0", "Hide frames", FRAMES_ICON), - ("FRAMES1", "Display and plot frames", FRAMES_ICON), - ("FRAMES2", "Display but don't plot frames", FRAMES_ICON), - // Shown (not selectable) while the frame - // variables differ from each other. - ("FRAMES3", "*Frames vary*", FRAMES_ICON), - ], - default: "FRAMES1", - }, - RibbonItem::LabeledDropdown { - id: "UOSNAP_DROPDOWN", - label: "Snap to Underlays", - icon: snap_underlays::ICON, - items: vec![ - ("UOSNAP1", "Snap to Underlays ON", snap_underlays::ICON), - ("UOSNAP0", "Snap to Underlays OFF", snap_underlays::ICON), - ], - default: "UOSNAP1", - }, - ], - }, // โ”€โ”€ Block โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ RibbonGroup { title: "Block", @@ -139,6 +90,59 @@ impl CadModule for InsertModule { RibbonItem::LargeTool(edit_block::tool()), ], }, + // โ”€โ”€ Reference โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + RibbonGroup { + title: "Reference", + tools: vec![ + RibbonItem::LargeTool(xattach::tool()), + // PDF, DWF, DGN or a point cloud; the face runs the + // last one chosen. + RibbonItem::LargeDropdown { + id: "UNDERLAY_ATTACH", + label: "Attach Underlay", + icon: pdf_attach::ICON, + items: vec![ + ("PDFATTACH", "Attach PDF", pdf_attach::ICON), + ("DWFATTACH", "Attach DWF", pdf_attach::ICON), + ("DGNATTACH", "Attach DGN", pdf_attach::ICON), + ("POINTCLOUDATTACH", "Attach Point Cloud", pc_attach::ICON), + ], + default: "PDFATTACH", + }, + RibbonItem::LargeTool(xclip::tool()), + RibbonItem::LargeTool(xadjust::tool()), + RibbonItem::LabeledTool(underlay_layers::tool()), + // An empty label shows the chosen item's. + RibbonItem::LabeledDropdown { + id: "FRAMES_DROPDOWN", + label: "", + icon: FRAMES_ICON, + items: vec![ + ("FRAMES0", "Hide frames", FRAMES_ICON), + ("FRAMES1", "Display and plot frames", FRAMES_ICON), + ("FRAMES2", "Display but don't plot frames", FRAMES_ICON), + // Shown (not selectable) while the frame + // variables differ from each other. + ("FRAMES3", "*Frames vary*", FRAMES_ICON), + ], + default: "FRAMES1", + }, + RibbonItem::LabeledDropdown { + id: "UOSNAP_DROPDOWN", + // An empty label shows the current state. + label: "", + icon: snap_underlays::ICON, + items: vec![ + ("UOSNAP1", "Snap to Underlays ON", snap_underlays::ICON), + ("UOSNAP0", "Snap to Underlays OFF", snap_underlays::ICON), + // Shown (not selectable) while PDFOSNAP, DWFOSNAP + // and DGNOSNAP differ (UOSNAP 2). + ("UOSNAP2", "*Underlay OSnap's vary*", snap_underlays::ICON), + ], + default: "UOSNAP1", + }, + ], + }, // โ”€โ”€ Import โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ RibbonGroup { title: "Import", diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 3e3d1ab1b..81969ef8d 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -26,6 +26,9 @@ pub struct ExtrudeCommand { direction_start: Option, mode: ExtrudeMode, taper_angle: f64, + /// The taper prompt's default: the last angle given, which a new + /// extrusion does not apply until the Taper angle option is chosen. + taper_default: f64, last_height: Option, taper_return: ExtrudeStep, color: [f32; 4], @@ -80,7 +83,8 @@ impl ExtrudeCommand { profile_direction: None, direction_start: None, mode: defaults.mode, - taper_angle: defaults.taper_angle, + taper_angle: 0.0, + taper_default: defaults.taper_angle, last_height: defaults.height, taper_return: ExtrudeStep::Height, color, @@ -242,7 +246,7 @@ impl CadCommand for ExtrudeCommand { ExtrudeStep::Taper => format!( "{} <{}>:", t!("EXTRUDE Specify taper angle or [Expression]"), - crate::entities::common::format_angle(self.taper_angle) + crate::entities::common::format_angle(self.taper_default) ), ExtrudeStep::Expression => t!("EXTRUDE Enter height expression:").into_owned(), ExtrudeStep::TaperExpression => { @@ -415,7 +419,7 @@ impl CadCommand for ExtrudeCommand { if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 { return Some(CmdResult::NeedPoint); } - self.taper_angle = angle; + (self.taper_angle, self.taper_default) = (angle, angle); extrude_defaults() .lock() .unwrap_or_else(|error| error.into_inner()) @@ -432,7 +436,7 @@ impl CadCommand for ExtrudeCommand { if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 { return Some(CmdResult::NeedPoint); } - self.taper_angle = angle; + (self.taper_angle, self.taper_default) = (angle, angle); extrude_defaults() .lock() .unwrap_or_else(|error| error.into_inner()) @@ -459,6 +463,7 @@ impl CadCommand for ExtrudeCommand { .map(|height| self.finish(ExtrudeExtent::Height(height))) .unwrap_or(CmdResult::Cancel), ExtrudeStep::Taper => { + self.taper_angle = self.taper_default; self.step = self.taper_return; CmdResult::NeedPoint } diff --git a/src/modules/insert/underlay_layers.rs b/src/modules/insert/underlay_layers.rs index 59d039ad8..d0b52b061 100644 --- a/src/modules/insert/underlay_layers.rs +++ b/src/modules/insert/underlay_layers.rs @@ -6,6 +6,6 @@ pub fn tool() -> ToolDef { id: "UNDERLAYLAYERS", label: "Underlay\nLayers", icon: ICON, - event: ModuleEvent::Command("UNDERLAYLAYERS".to_string()), + event: ModuleEvent::Command("ULAYERS".to_string()), } } diff --git a/src/modules/insert/xadjust.rs b/src/modules/insert/xadjust.rs index 92e72fb57..debf67a5e 100644 --- a/src/modules/insert/xadjust.rs +++ b/src/modules/insert/xadjust.rs @@ -1,4 +1,30 @@ +// ADJUST / -IMAGEADJUST / PDFADJUST / DWFADJUST / DGNADJUST โ€” brightness, +// contrast and fade of raster images; fade, contrast and monochrome of +// PDF, DWF and DGN underlays. +// +// Select image or underlay: +// Enter image option [Contrast/Fade/Brightness] : +// Enter brightness value (0-100) <50>: +// +// Enter PDF underlay option [Fade/Contrast/Monochrome] : +// Enter fade value (0-100) <25>: +// Monochrome? [Yes/No] : +// +// An image value prompt offers the first image's own value. The underlay +// option and the fade / contrast values offered are the last ones used in +// the session, whatever the underlay kind. The host applies the value to +// every chosen object as one undo step. + +use std::sync::Mutex; + +use codec::entities::{UnderlayDisplayFlags, UnderlayType}; +use codec::types::Handle; +use codec::EntityType; + +use crate::command::{CadCommand, CmdOption, CmdResult, InputKind}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; +use crate::t; + pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/xadjust.svg")); pub fn tool() -> ToolDef { ToolDef { @@ -8,3 +34,330 @@ pub fn tool() -> ToolDef { event: ModuleEvent::Command("ADJUST".to_string()), } } + +/// What a command takes: `A` images or underlays, `I` images, `P` / `W` / +/// `G` PDF / DWF / DGN underlays. +pub fn keeps(mode: &str) -> fn(&EntityType) -> bool { + fn any(e: &EntityType) -> bool { + matches!(e, EntityType::RasterImage(_) | EntityType::Underlay(_)) + } + fn image(e: &EntityType) -> bool { + matches!(e, EntityType::RasterImage(_)) + } + fn pdf(e: &EntityType) -> bool { + matches!(e, EntityType::Underlay(u) if u.underlay_type == UnderlayType::Pdf) + } + fn dwf(e: &EntityType) -> bool { + matches!(e, EntityType::Underlay(u) if u.underlay_type == UnderlayType::Dwf) + } + fn dgn(e: &EntityType) -> bool { + matches!(e, EntityType::Underlay(u) if u.underlay_type == UnderlayType::Dgn) + } + match mode { + "I" => image, + "P" => pdf, + "W" => dwf, + "G" => dgn, + _ => any, + } +} + +/// The command each mode belongs to. +pub fn command_name(mode: &str) -> &'static str { + match mode { + "I" => "-IMAGEADJUST", + "P" => "PDFADJUST", + "W" => "DWFADJUST", + "G" => "DGNADJUST", + _ => "ADJUST", + } +} + +/// The selection prompt of each command. +pub fn select_prompt(mode: &str) -> &'static str { + match mode { + "I" => "Select image(s):", + "P" => "Select PDF underlay:", + "W" => "Select DWF underlay:", + "G" => "Select DGN underlay:", + _ => "Select image or underlay:", + } +} + +#[derive(Clone, Copy, PartialEq)] +enum Prop { + Brightness, + Contrast, + Fade, + Monochrome, +} + +impl Prop { + fn label(self) -> &'static str { + match self { + Prop::Brightness => "Brightness", + Prop::Contrast => "Contrast", + Prop::Fade => "Fade", + Prop::Monochrome => "Monochrome", + } + } + fn key(self) -> &'static str { + match self { + Prop::Brightness => "BRIGHTNESS", + Prop::Contrast => "CONTRAST", + Prop::Fade => "FADE", + Prop::Monochrome => "MONOCHROME", + } + } +} + +const IMAGE_OPTIONS: [Prop; 3] = [Prop::Contrast, Prop::Fade, Prop::Brightness]; +const UNDERLAY_OPTIONS: [Prop; 3] = [Prop::Fade, Prop::Contrast, Prop::Monochrome]; + +/// The underlay option, fade and contrast last used in the session. +static LAST_UNDERLAY: Mutex<(usize, u8, u8)> = Mutex::new((0, 25, 75)); + +fn last_underlay() -> (usize, u8, u8) { + LAST_UNDERLAY.lock().map(|v| *v).unwrap_or((0, 25, 75)) +} + +enum Subject { + Image { + brightness: u8, + contrast: u8, + fade: u8, + }, + Underlay { + kind: &'static str, + monochrome: bool, + }, +} + +pub struct AdjustCommand { + handles: Vec, + subject: Subject, + prop: Option, +} + +/// Why a chosen set cannot be adjusted together. +pub const MIXED: &str = + "*Invalid selection set. Select underlay objects of the same file type only.*"; + +impl AdjustCommand { + /// The command for the chosen objects (the first one supplies the image + /// values and the monochrome state), or the reason they do not go + /// together. `None` when nothing adjustable was chosen. + pub fn for_objects(objects: &[(Handle, EntityType)]) -> Option> { + let (_, first) = objects.first()?; + let kind = |e: &EntityType| match e { + EntityType::Underlay(u) => Some(u.underlay_type), + _ => None, + }; + if objects.iter().any(|(_, e)| kind(e) != kind(first)) { + return Some(Err(MIXED)); + } + let subject = match first { + EntityType::RasterImage(img) => Subject::Image { + brightness: img.brightness, + contrast: img.contrast, + fade: img.fade, + }, + EntityType::Underlay(u) => Subject::Underlay { + kind: match u.underlay_type { + UnderlayType::Pdf => "PDF", + UnderlayType::Dwf => "DWF", + UnderlayType::Dgn => "DGN", + }, + monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME), + }, + _ => return None, + }; + Some(Ok(Self { + handles: objects.iter().map(|(h, _)| *h).collect(), + subject, + prop: None, + })) + } + + fn options_list(&self) -> &'static [Prop; 3] { + match self.subject { + Subject::Image { .. } => &IMAGE_OPTIONS, + Subject::Underlay { .. } => &UNDERLAY_OPTIONS, + } + } + + fn default_option(&self) -> Prop { + match self.subject { + Subject::Image { .. } => Prop::Brightness, + Subject::Underlay { .. } => UNDERLAY_OPTIONS[last_underlay().0.min(2)], + } + } + + fn default_value(&self, prop: Prop) -> u8 { + match (&self.subject, prop) { + (Subject::Image { brightness, .. }, Prop::Brightness) => *brightness, + (Subject::Image { contrast, .. }, Prop::Contrast) => *contrast, + (Subject::Image { fade, .. }, _) => *fade, + (Subject::Underlay { .. }, Prop::Contrast) => last_underlay().2, + (Subject::Underlay { .. }, _) => last_underlay().1, + } + } + + fn apply(&self, prop: Prop, value: u8) -> CmdResult { + if let Subject::Underlay { .. } = self.subject { + if let Ok(mut last) = LAST_UNDERLAY.lock() { + last.0 = UNDERLAY_OPTIONS.iter().position(|p| *p == prop).unwrap_or(0); + match prop { + Prop::Fade => last.1 = value, + Prop::Contrast => last.2 = value, + _ => {} + } + } + } + let handles: Vec = self + .handles + .iter() + .map(|h| format!("{:X}", h.value())) + .collect(); + CmdResult::Dispatch(format!( + "_ADJUSTAPPLY {} {} {}", + prop.key(), + value, + handles.join(" ") + )) + } +} + +/// Whether `text` names `label` (its key letter or a longer prefix, in +/// English or in the interface language). +fn names(text: &str, label: &str) -> bool { + let english = label.to_uppercase(); + let local = t!(label).to_uppercase(); + !text.is_empty() && (english.starts_with(text) || local.starts_with(text)) +} + +impl CadCommand for AdjustCommand { + fn name(&self) -> &'static str { + "ADJUST" + } + + fn prompt(&self) -> String { + match (self.prop, &self.subject) { + (None, Subject::Image { .. }) => { + t!("Enter image option [Contrast/Fade/Brightness] :").into_owned() + } + (None, Subject::Underlay { kind, .. }) => t!( + "Enter %{kind} underlay option [Fade/Contrast/Monochrome] <%{option}>:", + kind = kind, + option = t!(self.default_option().label()) + ) + .into_owned(), + (Some(Prop::Monochrome), Subject::Underlay { monochrome, .. }) => t!( + "Monochrome? [Yes/No] <%{value}>:", + value = if *monochrome { t!("Yes") } else { t!("No") } + ) + .into_owned(), + (Some(Prop::Brightness), _) => t!( + "Enter brightness value (0-100) <%{value}>:", + value = self.default_value(Prop::Brightness) + ) + .into_owned(), + (Some(Prop::Contrast), _) => t!( + "Enter contrast value (0-100) <%{value}>:", + value = self.default_value(Prop::Contrast) + ) + .into_owned(), + (Some(_), _) => t!( + "Enter fade value (0-100) <%{value}>:", + value = self.default_value(Prop::Fade) + ) + .into_owned(), + } + } + + fn options(&self) -> Vec { + match self.prop { + None => self + .options_list() + .iter() + .map(|p| CmdOption::new(p.label(), &p.label()[..1])) + .collect(), + Some(Prop::Monochrome) => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")], + Some(_) => Vec::new(), + } + } + + fn input_kind(&self) -> InputKind { + InputKind::SingleToken + } + + fn on_text_input(&mut self, text: &str) -> Option { + let text = text.trim().to_uppercase(); + let invalid = || { + Some(CmdResult::ReportError( + t!("Invalid option keyword.").into_owned(), + )) + }; + match self.prop { + None => { + let chosen = if text.is_empty() { + Some(self.default_option()) + } else { + self.options_list() + .iter() + .copied() + .find(|p| names(&text, p.label())) + }; + let Some(prop) = chosen else { + return invalid(); + }; + self.prop = Some(prop); + Some(CmdResult::NeedPoint) + } + Some(Prop::Monochrome) => { + let Subject::Underlay { monochrome, .. } = self.subject else { + return Some(CmdResult::Cancel); + }; + let on = if text.is_empty() { + monochrome + } else if names(&text, "Yes") { + true + } else if names(&text, "No") { + false + } else { + return invalid(); + }; + Some(self.apply(Prop::Monochrome, on as u8)) + } + Some(prop) => { + let value = if text.is_empty() { + Some(self.default_value(prop)) + } else { + text.parse::().ok().filter(|v| *v <= 100) + }; + match (value, &self.subject) { + (Some(v), _) => Some(self.apply(prop, v)), + (None, Subject::Image { .. }) => Some(CmdResult::ReportError( + t!("Value must be an integer between 0 and 100.").into_owned(), + )), + (None, Subject::Underlay { .. }) => Some(CmdResult::ReportError( + crate::tf!("Requires an integer between {min} and {max}.", min = 0, max = 100) + .into_owned(), + )), + } + } + } + } + + fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + self.on_text_input("").unwrap_or(CmdResult::Cancel) + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["IMAGEADJUST", "-IMAGEADJUST", "PDFADJUST", "DWFADJUST", "DGNADJUST"] +}); diff --git a/src/modules/insert/xattach.rs b/src/modules/insert/xattach.rs index 18966b46e..c98005b87 100644 --- a/src/modules/insert/xattach.rs +++ b/src/modules/insert/xattach.rs @@ -147,6 +147,9 @@ pub struct XAttachCommand { const NONZERO: &str = "Value must be nonzero."; +/// Raster image files ATTACH places as images. +pub const IMAGE_EXTENSIONS: &[&str] = &["bmp", "gif", "jpg", "jpeg", "png", "tif", "tiff", "webp"]; + impl XAttachCommand { pub fn new(request: XrefAttachRequest, placement: XrefPlacement, label: &'static str) -> Self { let block_name = path_to_block_name(&request.path); diff --git a/src/modules/insert/xclip.rs b/src/modules/insert/xclip.rs index d96669786..675b5a311 100644 --- a/src/modules/insert/xclip.rs +++ b/src/modules/insert/xclip.rs @@ -550,9 +550,10 @@ impl CadCommand for ClipCommand { self.inner.as_mut().and_then(|c| c.on_text_input(text)) } fn on_enter(&mut self) -> CmdResult { + // Enter before an object is picked ends CLIP without a message. self.inner .as_mut() - .map_or(CmdResult::Cancel, |c| c.on_enter()) + .map_or(CmdResult::Measurement(String::new()), |c| c.on_enter()) } fn on_mouse_move(&mut self, pt: DVec3) -> Option { self.inner.as_mut().and_then(|c| c.on_mouse_move(pt)) diff --git a/src/modules/insert/xref_cmd.rs b/src/modules/insert/xref_cmd.rs index c0628ee4d..fb7e4645d 100644 --- a/src/modules/insert/xref_cmd.rs +++ b/src/modules/insert/xref_cmd.rs @@ -36,6 +36,14 @@ impl XrefCommand { } } + /// -XREF already at its Attach file prompt (XATTACH while FILEDIA is 0). + pub fn attach(saved_paths: Vec<(String, String)>) -> Self { + Self { + step: Step::File { overlay: false }, + saved_paths, + } + } + /// Whether any reference name matches `pattern` (`*` / `?` wildcards). fn matches(&self, pattern: &str) -> bool { self.saved_paths @@ -197,6 +205,106 @@ impl CadCommand for XrefCommand { } } +/// -ATTACH (and ATTACH while FILEDIA is 0): the file on the command line, +/// then the attach flow of its file type. +pub struct AttachCommand { + /// The drawing whose reference type is asked; `None` while asking for the file. + drawing: Option, +} + +impl AttachCommand { + pub fn new() -> Self { + Self { drawing: None } + } + + fn reference(path: &str, overlay: bool) -> CmdResult { + let verb = if overlay { + "XREFOVERLAYFILE" + } else { + "XREFATTACHFILE" + }; + CmdResult::Dispatch(format!("{verb} {path}")) + } +} + +impl CadCommand for AttachCommand { + fn name(&self) -> &'static str { + "-ATTACH" + } + + fn prompt(&self) -> String { + match self.drawing { + None => "-ATTACH Path to file to attach:".to_string(), + Some(_) => { + "-ATTACH Specify reference type [Attachment/Overlay] :".to_string() + } + } + } + + fn options(&self) -> Vec { + match self.drawing { + None => Vec::new(), + Some(_) => vec![ + CmdOption::new("Attachment", "A"), + CmdOption::new("Overlay", "O"), + ], + } + } + + fn input_kind(&self) -> InputKind { + match self.drawing { + None => InputKind::FreeText, + Some(_) => InputKind::SingleToken, + } + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_text_input(&mut self, text: &str) -> Option { + let value = text.trim().trim_matches('"'); + if let Some(path) = self.drawing.clone() { + return Some(match value.to_uppercase().as_str() { + "A" | "ATTACHMENT" => Self::reference(&path, false), + "O" | "OVERLAY" => Self::reference(&path, true), + _ => CmdResult::ReportError("Invalid option keyword.".to_string()), + }); + } + if value.is_empty() { + return Some(self.on_enter()); + } + if !std::path::Path::new(value).is_file() { + return Some(CmdResult::ReportError(format!("{value} not found."))); + } + let ext = std::path::Path::new(value) + .extension() + .map(|e| e.to_string_lossy().to_ascii_lowercase()) + .unwrap_or_default(); + Some(match ext.as_str() { + "pdf" => CmdResult::Dispatch(format!("-PDFATTACH {value}")), + "dwf" | "dwfx" => CmdResult::Dispatch(format!("-DWFATTACH {value}")), + "dgn" => CmdResult::Dispatch(format!("-DGNATTACH {value}")), + "rcp" | "rcs" => CmdResult::Dispatch(format!("-POINTCLOUDATTACH {value}")), + // Images have no dialog after the file: ATTACH places them. + e if crate::modules::insert::xattach::IMAGE_EXTENSIONS.contains(&e) => { + CmdResult::Dispatch(format!("ATTACH {value}")) + } + _ => { + self.drawing = Some(value.to_string()); + CmdResult::NeedPoint + } + }) + } + + fn on_enter(&mut self) -> CmdResult { + match self.drawing.clone() { + None => CmdResult::ReportError("Filename cannot be blank".to_string()), + Some(path) => Self::reference(&path, false), + } + } +} + /// A stored path shown the way the platform writes it. pub fn display_path(path: &str) -> String { if cfg!(windows) { diff --git a/src/modules/layout/mod.rs b/src/modules/layout/mod.rs index 699fdc5b4..eb1d6d2db 100644 --- a/src/modules/layout/mod.rs +++ b/src/modules/layout/mod.rs @@ -2,6 +2,7 @@ // This tab is only shown when the active layout is not "Model". pub mod mview; +pub mod mvsetup; pub mod vplayer; use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, ToolDef}; diff --git a/src/modules/layout/mvsetup.rs b/src/modules/layout/mvsetup.rs new file mode 100644 index 000000000..724b12089 --- /dev/null +++ b/src/modules/layout/mvsetup.rs @@ -0,0 +1,374 @@ +// MVSETUP โ€” set a drawing up for plotting. +// +// In model space without paper space it draws the sheet border at the +// drawing scale: +// Enable paper space? [No/Yes] : +// Enter the scale factor <1>: +// Enter the paper width: +// Enter the paper height: +// On a layout it lays out viewports or scales them: +// Enter an option [Create/Delete objects/Scale viewports]: +// Enter viewport layout [Single/Engineering/Array] : +// Specify first corner of bounding area for viewport(s) : +// Specify opposite corner: +// Enter number of viewports in X direction <2>: (Array) +// Enter number of viewports in Y direction <2>: (Array) +// Specify distance between viewports in X direction <0>: +// Specify distance between viewports in Y direction <0>: +// Select the objects to delete: +// Select the viewports to scale: +// Enter the number of paper space units <1>: +// Enter the number of model space units <1>: + +use crate::t; +use codec::entities::{LwPolyline, LwVertex, Viewport}; +use codec::types::{Vector2, Vector3}; +use codec::{EntityType, Handle}; +use glam::{DVec2, DVec3}; + +use crate::command::{CadCommand, CmdOption, CmdResult}; + +#[derive(Clone, Copy, PartialEq)] +enum Step { + PaperSpace, + Scale, + Width, + Height, + Option, + Arrangement, + FirstCorner, + SecondCorner, + Columns, + Rows, + GapX, + GapY, + Delete, + Viewports, + PaperUnits, + ModelUnits, +} + +#[derive(Clone, Copy, PartialEq)] +enum Arrangement { + Single, + Engineering, + Array, +} + +pub struct MvsetupCommand { + step: Step, + /// The layout model space switches to when paper space is enabled. + layout: String, + paper_bounds: ((f64, f64), (f64, f64)), + scale: f64, + width: f64, + arrangement: Arrangement, + first: DVec2, + area: (DVec2, DVec2), + columns: u32, + rows: u32, + gap_x: f64, + picked: Vec, + paper_units: f64, +} + +impl MvsetupCommand { + /// `layout` is `None` on a layout, or the layout to switch to from model + /// space; `paper_bounds` is the sheet's printable area. + pub fn new(layout: Option, paper_bounds: ((f64, f64), (f64, f64))) -> Self { + Self { + step: if layout.is_some() { + Step::PaperSpace + } else { + Step::Option + }, + layout: layout.unwrap_or_default(), + paper_bounds, + scale: 1.0, + width: 0.0, + arrangement: Arrangement::Single, + first: DVec2::ZERO, + area: (DVec2::ZERO, DVec2::ZERO), + columns: 2, + rows: 2, + gap_x: 0.0, + picked: Vec::new(), + paper_units: 1.0, + } + } + + fn after_area(&mut self) -> CmdResult { + self.step = match self.arrangement { + Arrangement::Single => return self.create(0.0), + Arrangement::Engineering => Step::GapX, + Arrangement::Array => Step::Columns, + }; + CmdResult::NeedPoint + } + + /// The viewports of the chosen arrangement, `gap_y` apart vertically. + fn create(&self, gap_y: f64) -> CmdResult { + let (columns, rows, gap_x) = match self.arrangement { + Arrangement::Single => (1, 1, 0.0), + Arrangement::Engineering => (2, 2, self.gap_x), + Arrangement::Array => (self.columns, self.rows, self.gap_x), + }; + let (min, max) = self.area; + let width = (max.x - min.x - gap_x * (columns - 1) as f64) / columns as f64; + let height = (max.y - min.y - gap_y * (rows - 1) as f64) / rows as f64; + if width <= 1e-6 || height <= 1e-6 { + return CmdResult::ReportError( + t!("MVSETUP The viewports do not fit in the bounding area.").into_owned(), + ); + } + // Engineering: top view over front view, right view beside it, and + // an isometric view in the remaining corner. + let directions = [ + Vector3::new(0.0, 0.0, 1.0), + Vector3::new(1.0, -1.0, 1.0), + Vector3::new(0.0, -1.0, 0.0), + Vector3::new(1.0, 0.0, 0.0), + ]; + let mut viewports = Vec::new(); + for row in 0..rows { + for column in 0..columns { + let mut viewport = Viewport::new(); + viewport.center = Vector3::new( + min.x + column as f64 * (width + gap_x) + width / 2.0, + max.y - row as f64 * (height + gap_y) - height / 2.0, + 0.0, + ); + viewport.width = width; + viewport.height = height; + viewport.id = 2; + if self.arrangement == Arrangement::Engineering { + viewport.view_direction = directions[(row * 2 + column) as usize].clone(); + } + viewports.push(EntityType::Viewport(viewport)); + } + } + CmdResult::CommitEntitiesAndExit(viewports) + } + + fn sheet_border(&self, height: f64) -> CmdResult { + let (width, height) = (self.width * self.scale, height * self.scale); + let mut border = LwPolyline::new(); + border.is_closed = true; + border.vertices = [(0.0, 0.0), (width, 0.0), (width, height), (0.0, height)] + .into_iter() + .map(|(x, y)| LwVertex::new(Vector2::new(x, y))) + .collect(); + CmdResult::CommitAndZoom { + entity: EntityType::LwPolyline(border), + p1: DVec3::ZERO, + p2: DVec3::new(width, height, 0.0), + } + } +} + +/// A positive number, or `default` for an empty answer. +fn positive(text: &str, default: f64) -> Option { + if text.is_empty() { + return Some(default); + } + crate::entities::common::parse_typed_length(text).filter(|value| *value > 0.0) +} + +impl CadCommand for MvsetupCommand { + fn name(&self) -> &'static str { + "MVSETUP" + } + + fn prompt(&self) -> String { + t!(match self.step { + Step::PaperSpace => "MVSETUP Enable paper space? [No/Yes] :", + Step::Scale => "MVSETUP Enter the scale factor <1>:", + Step::Width => "MVSETUP Enter the paper width:", + Step::Height => "MVSETUP Enter the paper height:", + Step::Option => "MVSETUP Enter an option [Create/Delete objects/Scale viewports]:", + Step::Arrangement => { + "MVSETUP Enter viewport layout [Single/Engineering/Array] :" + } + Step::FirstCorner => { + "MVSETUP Specify first corner of bounding area for viewport(s) :" + } + Step::SecondCorner => "MVSETUP Specify opposite corner:", + Step::Columns => "MVSETUP Enter number of viewports in X direction <2>:", + Step::Rows => "MVSETUP Enter number of viewports in Y direction <2>:", + Step::GapX => "MVSETUP Specify distance between viewports in X direction <0>:", + Step::GapY => "MVSETUP Specify distance between viewports in Y direction <0>:", + Step::Delete => "MVSETUP Select the objects to delete:", + Step::Viewports => "MVSETUP Select the viewports to scale:", + Step::PaperUnits => "MVSETUP Enter the number of paper space units <1>:", + Step::ModelUnits => "MVSETUP Enter the number of model space units <1>:", + }) + .into_owned() + } + + fn options(&self) -> Vec { + match self.step { + Step::PaperSpace => vec![CmdOption::new("No", "N"), CmdOption::new("Yes", "Y")], + Step::Option => vec![ + CmdOption::new("Create", "C"), + CmdOption::new("Delete objects", "D"), + CmdOption::new("Scale viewports", "S"), + ], + Step::Arrangement => vec![ + CmdOption::new("Single", "S"), + CmdOption::new("Engineering", "E"), + CmdOption::new("Array", "A"), + ], + _ => Vec::new(), + } + } + + fn wants_text_input(&self) -> bool { + !matches!(self.step, Step::Delete | Step::Viewports) + } + + fn needs_entity_pick(&self) -> bool { + matches!(self.step, Step::Delete | Step::Viewports) + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { + if !handle.is_null() && !self.picked.contains(&handle) { + self.picked.push(handle); + } + CmdResult::NeedPoint + } + + fn on_text_input(&mut self, text: &str) -> Option { + let text = text.trim(); + let word = text.trim_start_matches('_').to_ascii_uppercase(); + match self.step { + Step::PaperSpace => match word.as_str() { + "N" | "NO" => self.step = Step::Scale, + "" | "Y" | "YES" => { + self.step = Step::Option; + return Some(CmdResult::MviewSwitchLayout(self.layout.clone())); + } + _ => return None, + }, + Step::Scale => { + self.scale = positive(text, 1.0)?; + self.step = Step::Width; + } + Step::Width => { + self.width = positive(text, f64::NAN).filter(|value| value.is_finite())?; + self.step = Step::Height; + } + Step::Height => { + let height = positive(text, f64::NAN).filter(|value| value.is_finite())?; + return Some(self.sheet_border(height)); + } + Step::Option => match word.as_str() { + "C" | "CREATE" => self.step = Step::Arrangement, + "D" | "DELETE" => self.step = Step::Delete, + "S" | "SCALE" => self.step = Step::Viewports, + _ => return None, + }, + Step::Arrangement => { + self.arrangement = match word.as_str() { + "" | "S" | "SINGLE" | "1" => Arrangement::Single, + "E" | "ENGINEERING" | "2" => Arrangement::Engineering, + "A" | "ARRAY" | "3" => Arrangement::Array, + _ => return None, + }; + self.step = Step::FirstCorner; + } + Step::FirstCorner if text.is_empty() => { + let ((x0, y0), (x1, y1)) = self.paper_bounds; + self.area = ( + DVec2::new(x0.min(x1), y0.min(y1)), + DVec2::new(x0.max(x1), y0.max(y1)), + ); + return Some(self.after_area()); + } + Step::Columns | Step::Rows => { + let count = if text.is_empty() { + 2 + } else { + text.parse::().ok().filter(|count| (1..=100).contains(count))? + }; + if self.step == Step::Columns { + self.columns = count; + self.step = Step::Rows; + } else { + self.rows = count; + self.step = Step::GapX; + } + } + Step::GapX | Step::GapY => { + let gap = if text.is_empty() { + 0.0 + } else { + crate::entities::common::parse_typed_length(text).filter(|v| *v >= 0.0)? + }; + if self.step == Step::GapY { + return Some(self.create(gap)); + } + self.gap_x = gap; + self.step = Step::GapY; + } + Step::PaperUnits => { + self.paper_units = positive(text, 1.0)?; + self.step = Step::ModelUnits; + } + Step::ModelUnits => { + let model_units = positive(text, 1.0)?; + return Some(CmdResult::ScaleViewports { + handles: std::mem::take(&mut self.picked), + factor: self.paper_units / model_units, + }); + } + Step::FirstCorner | Step::SecondCorner | Step::Delete | Step::Viewports => { + return None + } + } + Some(CmdResult::NeedPoint) + } + + fn on_point(&mut self, pt: DVec3) -> CmdResult { + match self.step { + Step::FirstCorner => { + self.first = pt.truncate(); + self.step = Step::SecondCorner; + CmdResult::NeedPoint + } + Step::SecondCorner => { + let second = pt.truncate(); + self.area = (self.first.min(second), self.first.max(second)); + self.after_area() + } + _ => CmdResult::NeedPoint, + } + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + Step::Delete => { + self.step = Step::Option; + let erased = std::mem::take(&mut self.picked); + if erased.is_empty() { + return CmdResult::NeedPoint; + } + CmdResult::ReplaceManyContinue( + erased.into_iter().map(|handle| (handle, Vec::new())).collect(), + ) + } + Step::Viewports if self.picked.is_empty() => CmdResult::Cancel, + Step::Viewports => { + self.step = Step::PaperUnits; + CmdResult::NeedPoint + } + Step::Option | Step::Width | Step::Height | Step::SecondCorner => CmdResult::Cancel, + _ => self.on_text_input("").unwrap_or(CmdResult::NeedPoint), + } + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["MVSETUP"] }); diff --git a/src/modules/manage/compare.rs b/src/modules/manage/compare.rs new file mode 100644 index 000000000..45c4c05bd --- /dev/null +++ b/src/modules/manage/compare.rs @@ -0,0 +1,104 @@ +// COMPARE โ€” show what differs from another drawing and step through it. +// +// COMPARE only here (green), only in the other drawing +// (red) [Next/Previous/eXit]: + +use crate::command::{CadCommand, CmdOption, CmdResult}; +use crate::scene::compare::Comparison; +use crate::scene::WireModel; +use glam::DVec3; + +pub struct CompareCommand { + comparison: Comparison, + /// The difference shown last; `None` before the first step. + current: Option, +} + +impl CompareCommand { + pub fn new(comparison: Comparison) -> Self { + Self { + comparison, + current: None, + } + } + + fn show(&mut self, forward: bool) -> CmdResult { + let count = self.comparison.spots.len(); + if count == 0 { + return CmdResult::NeedPoint; + } + let next = match (self.current, forward) { + (None, true) => 0, + (None, false) => count - 1, + (Some(index), true) => (index + 1) % count, + (Some(index), false) => (index + count - 1) % count, + }; + self.current = Some(next); + let (min, max) = self.comparison.spots[next]; + // A margin around the difference so it is seen in its context. + let margin = ((max - min).length() * 0.5).max(1.0); + CmdResult::Show { + p1: min - DVec3::splat(margin), + p2: max + DVec3::splat(margin), + } + } +} + +impl CadCommand for CompareCommand { + fn name(&self) -> &'static str { + "COMPARE" + } + + fn prompt(&self) -> String { + let (added, removed) = (self.comparison.added, self.comparison.removed); + let position = match self.current { + Some(index) => format!(" {}/{}", index + 1, self.comparison.spots.len()), + None => String::new(), + }; + crate::tf!( + "COMPARE {added} only here (green), {removed} only in the other drawing (red){position} [Next/Previous/eXit]:" + ) + .into_owned() + } + + fn options(&self) -> Vec { + vec![ + CmdOption::new("Next", "N"), + CmdOption::new("Previous", "P"), + CmdOption::new("eXit", "X"), + ] + } + + fn wants_text_input(&self) -> bool { + true + } + + fn on_text_input(&mut self, text: &str) -> Option { + match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() { + "" | "N" | "NEXT" => Some(self.show(true)), + "P" | "PREVIOUS" => Some(self.show(false)), + "X" | "EXIT" => Some(CmdResult::Cancel), + _ => None, + } + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + self.show(true) + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_preview_wires(&mut self, _pt: DVec3) -> Vec { + // ponytail: the overlay is cloned per frame; cache it in the scene if + // comparisons of very large drawings stutter. + self.comparison.overlay.clone() + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["COMPARE"] }); diff --git a/src/modules/manage/mod.rs b/src/modules/manage/mod.rs index 48fda655e..7c7f3a7f0 100644 --- a/src/modules/manage/mod.rs +++ b/src/modules/manage/mod.rs @@ -2,6 +2,7 @@ mod about; mod audit; +pub mod compare; mod cui_export; mod cui_import; mod find_nonpurgeable; diff --git a/src/modules/mod.rs b/src/modules/mod.rs index abd8529ae..d3b7c9715 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -25,6 +25,7 @@ pub use ocs_plugin_api::ribbon::{ // โ”€โ”€ Module declarations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ pub mod annotate; +pub mod block_editor; pub mod draw; pub mod insert; pub mod layout; diff --git a/src/modules/model/slice_cmd.rs b/src/modules/model/slice_cmd.rs index 4b354252f..6c5b111de 100644 --- a/src/modules/model/slice_cmd.rs +++ b/src/modules/model/slice_cmd.rs @@ -335,8 +335,15 @@ impl CadCommand for SliceCommand { self.selected = entities .into_iter() .filter_map(|item| { - matches!(item.entity, EntityType::Solid3D(_) | EntityType::Surface(_)) - .then_some(item.handle) + matches!( + item.entity, + EntityType::Solid3D(_) + | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) + ) + .then_some(item.handle) }) .collect(); } diff --git a/src/modules/registry.rs b/src/modules/registry.rs index 14e7a3d31..26899beca 100644 --- a/src/modules/registry.rs +++ b/src/modules/registry.rs @@ -23,6 +23,7 @@ pub fn all_modules() -> Vec> { Box::new(super::view::ViewModule), Box::new(super::manage::ManageModule), Box::new(super::layout::LayoutModule), + Box::new(super::block_editor::BlockEditorModule), ] } diff --git a/src/modules/view/dview.rs b/src/modules/view/dview.rs new file mode 100644 index 000000000..bc8a95d02 --- /dev/null +++ b/src/modules/view/dview.rs @@ -0,0 +1,89 @@ +// DVIEW command โ€” turn the view about its line of sight (TWist). + +use crate::t; +use glam::DVec3; + +use crate::command::{CadCommand, CmdOption, CmdResult}; + +pub struct DviewCommand { + /// The view's twist when the command started or last changed, radians. + current: f64, + /// Earlier twists, for Undo. + history: Vec, + twist_prompt: bool, +} + +impl DviewCommand { + /// `current` is the view's present twist, counter-clockwise, in radians. + pub fn new(current: f64) -> Self { + Self { + current, + history: Vec::new(), + twist_prompt: false, + } + } +} + +impl CadCommand for DviewCommand { + fn name(&self) -> &'static str { + "DVIEW" + } + + fn prompt(&self) -> String { + if self.twist_prompt { + let current = crate::entities::common::format_angle(self.current); + crate::tf!("DVIEW Specify view twist angle <{current}>:").into_owned() + } else { + t!("DVIEW Enter option [TWist/Undo]:").into_owned() + } + } + + fn options(&self) -> Vec { + if self.twist_prompt { + return Vec::new(); + } + vec![CmdOption::new("TWist", "TW"), CmdOption::new("Undo", "U")] + } + + fn wants_text_input(&self) -> bool { + true + } + + fn on_text_input(&mut self, text: &str) -> Option { + let text = text.trim(); + if self.twist_prompt { + let twist = crate::entities::common::parse_angle(&text.replace(',', "."))?; + self.twist_prompt = false; + self.history.push(self.current); + self.current = twist; + return Some(CmdResult::TwistView(twist)); + } + match text.trim_start_matches('_').to_ascii_uppercase().as_str() { + "TW" | "TWIST" => { + self.twist_prompt = true; + Some(CmdResult::NeedPoint) + } + "U" | "UNDO" => { + let previous = self.history.pop()?; + self.current = previous; + Some(CmdResult::TwistView(previous)) + } + _ => None, + } + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + if self.twist_prompt { + // Enter keeps the current twist and returns to the options. + self.twist_prompt = false; + return CmdResult::NeedPoint; + } + CmdResult::Cancel + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["DVIEW"] }); diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs index eb448e4dc..978024674 100644 --- a/src/modules/view/mod.rs +++ b/src/modules/view/mod.rs @@ -5,6 +5,7 @@ mod count_palette; mod file_tabs; mod graphic_attributes_palette; mod layout_tabs; +pub mod dview; pub mod limits; mod orbit; mod ortho; diff --git a/src/modules/view/view_cmd.rs b/src/modules/view/view_cmd.rs index c29d9164b..29144a4bf 100644 --- a/src/modules/view/view_cmd.rs +++ b/src/modules/view/view_cmd.rs @@ -57,6 +57,8 @@ pub struct ViewInfo { pub category: String, pub visual_style: Option, pub live_section: Option, + /// Saved in paper space. + pub paper_space: bool, } /// What -VIEW checks typed names against. @@ -65,6 +67,13 @@ pub struct ViewNames { pub views: Vec, pub visual_styles: Vec, pub live_sections: Vec, + /// On a layout's sheet (no viewport entered): the layout's viewports as + /// (handle, [min x, min y, max x, max y]), one of which takes a + /// model-space view being restored. + pub sheet_viewports: Option>, + /// The pick box (PICKBOX pixels) in sheet units: a viewport is selected + /// by its frame, within this distance of it. + pub sheet_pick_tolerance: f64, } #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -106,6 +115,7 @@ enum Step { List, Delete, Restore, + RestoreViewport(String), Save, WindowName, WindowFirst(String), @@ -226,9 +236,16 @@ impl DashViewCommand { let t = text.trim(); match setting { Setting::Background => match keyword(t, &["Color", "Gradient", "Image", "None"]) { - // The other background types are chosen in their own dialogs. Some("None") => Self::apply(setting, &view, "NONE"), - Some(_) => CmdResult::Dispatch("-VIEW >SETTINGS".into()), + // The other types are set in the Background dialog. + Some(kind) => CmdResult::Dispatch(format!( + "-VIEW >BACKGROUND {view}\u{1f}{}", + match kind { + "Color" => "SOLID", + "Gradient" => "GRADIENT", + _ => "IMAGE", + } + )), None => { self.step = Step::SettingValue(setting, view); CmdResult::ReportError("Invalid option keyword.".into()) @@ -343,6 +360,7 @@ impl CadCommand for DashViewCommand { Step::List | Step::SettingViewList(_) => "Enter view name(s) to list <*>:".into(), Step::Delete => "Enter view name(s) to delete:".into(), Step::Restore => "Enter view name to restore:".into(), + Step::RestoreViewport(_) => "Select Viewport for view:".into(), Step::Save | Step::WindowName => "Enter view name to save:".into(), Step::WindowFirst(_) => "Specify first corner:".into(), Step::WindowSecond(..) => "Specify opposite corner:".into(), @@ -414,9 +432,19 @@ impl CadCommand for DashViewCommand { } } Step::Restore => match self.view(t) { + // On a sheet a model-space view goes into a viewport the + // user selects. + Some(view) if !view.paper_space && self.names.sheet_viewports.is_some() => { + self.step = Step::RestoreViewport(view.name.clone()); + CmdResult::ReportMeasurement("Restoring Model space view.".into()) + } Some(view) => CmdResult::Dispatch(format!("-VIEW >RESTORE {}", view.name)), None => Self::end(&format!("Cannot find view \"{t}\".")), }, + Step::RestoreViewport(name) => { + self.step = Step::RestoreViewport(name); + CmdResult::ReportError("*Invalid selection*".into()) + } Step::Save => CmdResult::Dispatch(format!("-VIEW >SAVE {t}")), Step::WindowName => { self.step = Step::WindowFirst(t.to_string()); @@ -487,6 +515,24 @@ impl CadCommand for DashViewCommand { "-VIEW >WINDOW {}\u{1f}{},{}\u{1f}{},{}", name, first.x, first.y, pt.x, pt.y )), + Step::RestoreViewport(name) => { + // The topmost viewport whose frame the pick box touches; a + // pick well inside a viewport selects nothing, as in the + // reference. + let tol = self.names.sheet_pick_tolerance.max(0.0); + let hit = self.names.sheet_viewports.iter().flatten().rev().find(|(_, [x0, y0, x1, y1])| { + let outside = pt.x < x0 - tol || pt.x > x1 + tol || pt.y < y0 - tol || pt.y > y1 + tol; + let inside = pt.x > x0 + tol && pt.x < x1 - tol && pt.y > y0 + tol && pt.y < y1 - tol; + !outside && !inside + }); + match hit { + Some((handle, _)) => CmdResult::Dispatch(format!("-VIEW >RESTOREVP {name}\u{1f}{handle:X}")), + None => { + self.step = Step::RestoreViewport(name); + CmdResult::NeedPoint + } + } + } step => { self.step = step; CmdResult::NeedPoint @@ -496,7 +542,7 @@ impl CadCommand for DashViewCommand { fn on_enter(&mut self) -> CmdResult { match std::mem::replace(&mut self.step, Step::Main) { - Step::Main | Step::Delete | Step::Restore | Step::Save | Step::WindowName => { + Step::Main | Step::Delete | Step::Restore | Step::RestoreViewport(_) | Step::Save | Step::WindowName => { CmdResult::Dispatch("-VIEW >END".into()) } Step::Orthographic => CmdResult::Dispatch("-VIEW >PRESET TOP".into()), diff --git a/src/modules/view/visual_style.rs b/src/modules/view/visual_style.rs index f973038fc..9c999de89 100644 --- a/src/modules/view/visual_style.rs +++ b/src/modules/view/visual_style.rs @@ -106,6 +106,38 @@ pub fn label_for(mode: Mode) -> &'static str { .unwrap_or("Wireframe 2D") } +/// The render mode a named visual style draws as, and whether the name is a +/// style of its own (Realistic, Conceptual, โ€ฆ) rather than one of the modes. +pub fn mode_for_style_name(name: &str) -> Option<(Mode, bool)> { + Some(match name { + "2dWireframe" => (Mode::Wireframe2D, false), + "Wireframe" => (Mode::Wireframe3D, false), + "Hidden" => (Mode::HiddenLine, false), + "Flat" => (Mode::FlatShaded, false), + "FlatWithEdges" => (Mode::FlatShadedWithEdges, false), + "Gouraud" => (Mode::GouraudShaded, false), + "GouraudWithEdges" => (Mode::GouraudShadedWithEdges, false), + "Shaded with edges" => (Mode::GouraudShadedWithEdges, true), + "Realistic" | "Shaded" | "Conceptual" | "Shades of Gray" | "Sketchy" | "X-Ray" => { + (Mode::GouraudShaded, true) + } + _ => return None, + }) +} + +/// The name of the visual style a render mode is saved as. +pub fn style_name_for(mode: Mode) -> &'static str { + match mode { + Mode::Wireframe2D => "2dWireframe", + Mode::Wireframe3D => "Wireframe", + Mode::HiddenLine => "Hidden", + Mode::FlatShaded => "Flat", + Mode::FlatShadedWithEdges => "FlatWithEdges", + Mode::GouraudShaded => "Gouraud", + Mode::GouraudShadedWithEdges => "GouraudWithEdges", + } +} + /// The style a style keyword names. Only the seven exist; nothing maps onto a /// nearest neighbour, because there is no longer anything else to map from. pub fn mode_for_keyword(keyword: &str) -> Option { diff --git a/src/scene/assoc_array.rs b/src/scene/assoc_array.rs new file mode 100644 index 000000000..7402a7f26 --- /dev/null +++ b/src/scene/assoc_array.rs @@ -0,0 +1,1115 @@ +//! Associative arrays as the drawing stores them: an AcDbAssocAction whose +//! body is an AcDbAssocArrayActionBody, with dependencies on the array INSERT +//! (read + write), the anonymous block holding the item INSERTs (write) and the +//! anonymous block holding the source objects (read). The action's value +//! parameters (Items, ItemSpacing, Rows, โ€ฆ) define where the items go. + +use codec::objects::{ + AssocAction, AssocArrayActionBody, AssocDependency, AssocEvalValue, AssocValueParam, + AssociativeData, ObjectType, +}; +use codec::{CadDocument, Handle}; +use glam::{DMat4, DVec3}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ArrayKind { + Rectangular, + Polar, + Path, +} + +/// One associative array, resolved from its array INSERT. +#[derive(Clone, Debug)] +pub struct AssocArray { + pub insert: Handle, + pub action: Handle, + pub body: Handle, + /// Block record holding the item INSERTs. + pub array_block: Handle, + /// Block record holding the source objects. + pub source_block: Handle, + pub kind: ArrayKind, +} + +fn dependency(document: &CadDocument, handle: Handle) -> Option<&AssocDependency> { + match document.objects.get(&handle)? { + ObjectType::Associative(object) => match &object.data { + AssociativeData::Dependency(dependency) => Some(dependency), + AssociativeData::GeomDependency(geom) => Some(&geom.dependency), + AssociativeData::ValueDependency(value) => Some(&value.dependency), + _ => None, + }, + _ => None, + } +} + +pub fn action(document: &CadDocument, handle: Handle) -> Option<&AssocAction> { + match document.objects.get(&handle)? { + ObjectType::Associative(object) => match &object.data { + AssociativeData::Action(action) => Some(action), + _ => None, + }, + _ => None, + } +} + +pub fn body(document: &CadDocument, handle: Handle) -> Option<&AssocArrayActionBody> { + match document.objects.get(&handle)? { + ObjectType::Associative(object) => match &object.data { + AssociativeData::ArrayActionBody(body) => Some(body), + AssociativeData::ArrayModifyActionBody(modify) => Some(&modify.body), + _ => None, + }, + _ => None, + } +} + +/// The associative array whose array INSERT is `insert`, if any. +pub fn array_of_insert(document: &CadDocument, insert: Handle) -> Option { + document.objects.iter().find_map(|(&handle, object)| { + let ObjectType::Associative(object) = object else { + return None; + }; + let AssociativeData::Action(action) = &object.data else { + return None; + }; + let array_body = body(document, action.action_body)?; + let (mut array_block, mut source_block, mut owns_insert) = + (Handle::NULL, Handle::NULL, false); + for link in &action.dependencies { + let Some(dependency) = dependency(document, link.dependency) else { + continue; + }; + match (dependency.is_read_dependency, dependency.is_write_dependency) { + (true, true) if dependency.dependent_on == insert => owns_insert = true, + (false, true) => array_block = dependency.dependent_on, + (true, false) if source_block.is_null() => { + source_block = dependency.dependent_on + } + _ => {} + } + } + let kind = match array_body.parameter_block.as_str() { + "AcDbAssocArrayRectangularParameters" => ArrayKind::Rectangular, + "AcDbAssocArrayPolarParameters" => ArrayKind::Polar, + "AcDbAssocArrayPathParameters" => ArrayKind::Path, + _ => return None, + }; + (owns_insert && !array_block.is_null() && !source_block.is_null()).then_some( + AssocArray { + insert, + action: handle, + body: action.action_body, + array_block, + source_block, + kind, + }, + ) + }) +} + +/// An array action whose array INSERT is gone (erased or exploded): it has +/// nothing left to drive and is dropped when the networks are written. +pub fn orphaned_array_action(document: &CadDocument, handle: Handle) -> bool { + let Some(action) = action(document, handle) else { + return false; + }; + if body(document, action.action_body).is_none() { + return false; + } + !action.dependencies.iter().any(|link| { + dependency(document, link.dependency).is_some_and(|dependency| { + dependency.is_read_dependency + && dependency.is_write_dependency + && document.get_entity(dependency.dependent_on).is_some() + }) + }) +} + +/// The value parameters of an array action: inline in the action (R2013+) +/// or in the body's parameter section (R2007). +pub fn value_params<'a>(document: &'a CadDocument, array: &AssocArray) -> &'a [AssocValueParam] { + match action(document, array.action) { + Some(action) if !action.values.is_empty() => &action.values, + _ => body(document, array.body).map_or(&[], |body| &body.parameter_body.values), + } +} + +/// The number a value parameter holds, whatever its stored type. +pub fn param_value(params: &[AssocValueParam], name: &str) -> Option { + let param = params.iter().find(|param| param.name == name)?; + match ¶m.variables.first()?.value.value { + AssocEvalValue::Real(value) => Some(*value), + AssocEvalValue::Long(value) => Some(f64::from(*value)), + AssocEvalValue::Short(value) => Some(f64::from(*value)), + AssocEvalValue::Byte(value) => Some(f64::from(*value)), + _ => None, + } +} + +/// Store `value` in a value parameter, keeping its stored type. +pub fn set_param_value(params: &mut [AssocValueParam], name: &str, value: f64) -> bool { + let Some(variable) = params + .iter_mut() + .find(|param| param.name == name) + .and_then(|param| param.variables.first_mut()) + else { + return false; + }; + variable.value.value = match variable.value.value { + AssocEvalValue::Long(_) => AssocEvalValue::Long(value.round() as i32), + AssocEvalValue::Short(_) => AssocEvalValue::Short(value.round() as i16), + AssocEvalValue::Byte(_) => AssocEvalValue::Byte(value.round() as u8), + _ => AssocEvalValue::Real(value), + }; + true +} + +/// The direction columns step along: the line of the action's "RowProfile" +/// parameter, X when the array has none. +pub fn column_axis(document: &CadDocument, array: &AssocArray) -> DVec3 { + let Some(action) = action(document, array.action) else { + return DVec3::X; + }; + action + .owned_parameters + .iter() + .find_map(|handle| { + let ObjectType::Associative(object) = document.objects.get(handle)? else { + return None; + }; + let AssociativeData::EdgeActionParam(edge) = &object.data else { + return None; + }; + if edge.single_dependency.action_param.name != "RowProfile" || edge.action_type != 19 + { + return None; + } + match edge.curve.get(1)? { + codec::objects::AssocCurveValue::Point(direction) => { + DVec3::new(direction.x, direction.y, direction.z).try_normalize() + } + _ => None, + } + }) + .unwrap_or(DVec3::X) +} + +/// The plane normal of an array: its "BaseNormal" vertex parameter, Z when +/// the array has none. +pub fn base_normal(document: &CadDocument, array: &AssocArray) -> DVec3 { + let Some(action) = action(document, array.action) else { + return DVec3::Z; + }; + action + .owned_parameters + .iter() + .find_map(|handle| { + let ObjectType::Associative(object) = document.objects.get(handle)? else { + return None; + }; + let AssociativeData::VertexActionParam(vertex) = &object.data else { + return None; + }; + if vertex.single_dependency.action_param.name != "BaseNormal" { + return None; + } + DVec3::new(vertex.point.x, vertex.point.y, vertex.point.z).try_normalize() + }) + .unwrap_or(DVec3::Z) +} + +/// Item placements of a rectangular array in array-local coordinates: +/// (column, row, level) and the offset of the item's base point. Columns +/// step along `axis`; rows run at AxesAngle from it about `normal`, and +/// levels along `normal`. +pub fn rectangular_items( + params: &[AssocValueParam], + axis: DVec3, + normal: DVec3, +) -> Vec<([i32; 3], DVec3)> { + let get = |name: &str, default: f64| param_value(params, name).unwrap_or(default); + let (columns, rows, levels) = ( + get("Items", 1.0).max(1.0) as i32, + get("Rows", 1.0).max(1.0) as i32, + get("Levels", 1.0).max(1.0) as i32, + ); + let column_step = axis * get("ItemSpacing", 0.0); + let axes = get("AxesAngle", std::f64::consts::FRAC_PI_2); + let row_axis = glam::DQuat::from_axis_angle(normal, axes) * axis; + let row_step = row_axis * get("RowSpacing", 0.0) + normal * get("RowElevation", 0.0); + let level_step = normal * get("LevelSpacing", 0.0); + let mut items = Vec::with_capacity((columns * rows * levels).max(0) as usize); + for level in 0..levels { + for row in 0..rows { + for column in 0..columns { + let offset = column_step * f64::from(column) + + row_step * f64::from(row) + + level_step * f64::from(level); + items.push(([column, row, level], offset)); + } + } + } + items +} + +/// An array item's placement in array-local coordinates, as stored. +pub fn stored_item_transform(item: &codec::objects::AssocArrayItem) -> DMat4 { + if item.uses_default_transform { + let offset = item.x_direction; + DMat4::from_translation(DVec3::new(offset.x, offset.y, offset.z)) + } else { + DMat4::from_cols_array(&item.transform).transpose() + } +} + +/// The in-plane X axis angles are measured from: world X for a plan array, +/// the arbitrary-axis direction otherwise. +fn plane_x(normal: DVec3) -> DVec3 { + if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 { + DVec3::Y.cross(normal).normalize() + } else { + DVec3::Z.cross(normal).normalize() + } +} + +/// The centre of a polar array, recovered from its first item and its +/// Radius and StartAngle. +pub fn polar_center(document: &CadDocument, array: &AssocArray) -> Option { + let params = value_params(document, array); + let first = body(document, array.body)? + .items + .iter() + .find(|item| item.location == [0, 0, 0])?; + let normal = base_normal(document, array); + let x = plane_x(normal); + let y = normal.cross(x); + let start = param_value(params, "StartAngle").unwrap_or(0.0); + let radius = param_value(params, "Radius")?; + Some( + stored_item_transform(first).w_axis.truncate() + - (x * start.cos() + y * start.sin()) * radius, + ) +} + +/// Item placements of a polar array about `center`: each item turns about +/// the centre by its multiple of ItemSpacing (rotated with it unless +/// AlignItems is 0); rows step the radius out, levels step along the normal. +pub fn polar_items( + params: &[AssocValueParam], + center: DVec3, + normal: DVec3, +) -> Vec<([i32; 3], DMat4)> { + let get = |name: &str, default: f64| param_value(params, name).unwrap_or(default); + let (count, rows, levels) = ( + get("Items", 1.0).max(1.0) as i32, + get("Rows", 1.0).max(1.0) as i32, + get("Levels", 1.0).max(1.0) as i32, + ); + // ponytail: Direction 0 read as clockwise; no sample with 0 to confirm. + let sign = if get("Direction", 1.0) == 0.0 { -1.0 } else { 1.0 }; + let align = get("AlignItems", 1.0) != 0.0; + let (spacing, start, radius) = + (get("ItemSpacing", 0.0), get("StartAngle", 0.0), get("Radius", 0.0)); + let x = plane_x(normal); + let y = normal.cross(x); + let start_dir = x * start.cos() + y * start.sin(); + let mut items = Vec::with_capacity((count * rows * levels).max(0) as usize); + for level in 0..levels { + for row in 0..rows { + let ring = radius + f64::from(row) * get("RowSpacing", 0.0); + let lift = normal + * (f64::from(row) * get("RowElevation", 0.0) + + f64::from(level) * get("LevelSpacing", 0.0)); + for item in 0..count { + let turn = glam::DQuat::from_axis_angle(normal, sign * f64::from(item) * spacing); + let position = center + turn * (start_dir * ring) + lift; + let placement = if align { + DMat4::from_rotation_translation(turn, position) + } else { + DMat4::from_translation(position) + }; + items.push(([item, row, level], placement)); + } + } + } + items +} + +/// The rows the Properties palette shows for an array: (field, parameter, +/// label, kind) where kind 0 is a count, 1 a length and 2 an angle. +fn property_rows(kind: ArrayKind) -> &'static [(&'static str, &'static str, &'static str, u8)] { + match kind { + ArrayKind::Rectangular => &[ + ("assoc_array_columns", "Items", "Columns", 0), + ("assoc_array_column_spacing", "ItemSpacing", "Column spacing", 1), + ("assoc_array_rows", "Rows", "Rows", 0), + ("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1), + ("assoc_array_levels", "Levels", "Levels", 0), + ("assoc_array_level_spacing", "LevelSpacing", "Level spacing", 1), + ], + ArrayKind::Polar => &[ + ("assoc_array_items", "Items", "Items", 0), + ("assoc_array_fill_angle", "FillAngle", "Fill angle", 2), + ("assoc_array_radius", "Radius", "Radius", 1), + ("assoc_array_rows", "Rows", "Rows", 0), + ("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1), + ("assoc_array_levels", "Levels", "Levels", 0), + ("assoc_array_level_spacing", "LevelSpacing", "Level spacing", 1), + ], + ArrayKind::Path => &[ + ("assoc_array_items", "Items", "Items", 0), + ("assoc_array_item_spacing", "ItemSpacing", "Item spacing", 1), + ("assoc_array_rows", "Rows", "Rows", 0), + ("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1), + ("assoc_array_levels", "Levels", "Levels", 0), + ("assoc_array_level_spacing", "LevelSpacing", "Level spacing", 1), + ], + } +} + +/// The Properties section of an associative array. Path arrays are shown +/// read-only: their items follow a curve this does not re-place yet. +pub fn property_section( + document: &CadDocument, + array: &AssocArray, +) -> crate::scene::model::object::PropSection { + use crate::entities::common::{format_angle, format_length}; + use crate::scene::model::object::{PropSection, PropValue, Property}; + let params = value_params(document, array); + let props = property_rows(array.kind) + .iter() + .filter_map(|&(field, name, label, kind)| { + let value = param_value(params, name)?; + let text = match kind { + 0 => format!("{}", value.round() as i64), + 1 => format_length(value), + _ => format_angle(value), + }; + Some(Property { + label: crate::t!(label).into_owned(), + field, + value: if array.kind == ArrayKind::Path { + PropValue::ReadOnly(text) + } else { + PropValue::EditText(text) + }, + }) + }) + .collect(); + let title = match array.kind { + ArrayKind::Rectangular => "Array (Rectangular)", + ArrayKind::Polar => "Array (Polar)", + ArrayKind::Path => "Array (Path)", + }; + PropSection { + title: crate::t!(title).into_owned(), + props, + } +} + +/// Write a Properties edit of an array row: the parameter changes and the +/// items are placed again. `text` is the typed value (counts, lengths in +/// drawing units, angles as typed angles). +pub fn apply_field( + scene: &mut crate::scene::Scene, + array: &AssocArray, + field: &str, + text: &str, +) -> Result<(), String> { + let Some(&(_, name, _, kind)) = + property_rows(array.kind).iter().find(|row| row.0 == field) + else { + return Err("unknown array field".into()); + }; + let value = match kind { + 0 => text + .trim() + .parse::() + .ok() + .filter(|v| *v >= 1.0 && *v <= 100_000.0) + .map(f64::round), + 1 => crate::entities::common::parse_typed_length(text), + _ => crate::entities::common::parse_typed_angle(text), + } + .filter(|v| v.is_finite()) + .ok_or("invalid value")?; + update(scene, array, &[(name, value)]) +} + +/// Change an array's parameters and place its items again: the item +/// INSERTs in the array block, the body's item records and the parameter +/// values move together, so the array stays associative on save. +pub fn update( + scene: &mut crate::scene::Scene, + array: &AssocArray, + changes: &[(&str, f64)], +) -> Result<(), String> { + use codec::objects::AssocArrayItem; + use codec::EntityType; + if array.kind == ArrayKind::Path { + return Err("path arrays cannot be edited yet".into()); + } + let document = &scene.document; + let mut params = value_params(document, array).to_vec(); + let center = match array.kind { + ArrayKind::Polar => polar_center(document, array).ok_or("array has no first item")?, + _ => DVec3::ZERO, + }; + for (name, value) in changes { + set_param_value(&mut params, name, *value); + } + if array.kind == ArrayKind::Polar + && changes.iter().any(|(name, _)| matches!(*name, "Items" | "FillAngle")) + { + let fill = param_value(¶ms, "FillAngle").unwrap_or(std::f64::consts::TAU); + let count = param_value(¶ms, "Items").unwrap_or(1.0).max(1.0); + let spacing = if (fill.abs() - std::f64::consts::TAU).abs() < 1e-9 { + fill / count + } else if count > 1.0 { + fill / (count - 1.0) + } else { + 0.0 + }; + set_param_value(&mut params, "ItemSpacing", spacing); + } + let normal = base_normal(document, array); + let placements: Vec<([i32; 3], DMat4)> = match array.kind { + ArrayKind::Rectangular => { + rectangular_items(¶ms, column_axis(document, array), normal) + .into_iter() + .map(|(location, offset)| (location, DMat4::from_translation(offset))) + .collect() + } + _ => polar_items(¶ms, center, normal), + }; + let current = body(document, array.body).ok_or("array body missing")?; + let template = current + .items + .iter() + .filter_map(|item| item.first_handle) + .find_map(|handle| match document.get_entity(handle) { + Some(EntityType::Insert(insert)) => Some(insert.clone()), + _ => None, + }) + .ok_or("array has no item reference")?; + let old_items: Vec = document + .block_records + .iter() + .find(|record| record.handle == array.array_block) + .map(|record| record.entity_handles.clone()) + .unwrap_or_default() + .into_iter() + .filter(|handle| matches!(document.get_entity(*handle), Some(EntityType::Insert(_)))) + .collect(); + + let document = &mut scene.document; + for handle in &old_items { + document.remove_entity(*handle); + } + let mut items = Vec::with_capacity(placements.len()); + let mut new_handles = Vec::with_capacity(placements.len()); + for (location, placement) in placements { + let mut insert = template.clone(); + insert.common.handle = Handle::NULL; + insert.common.owner_handle = array.array_block; + insert.common.reactors.clear(); + insert.common.xdictionary_handle = None; + let position = placement.w_axis.truncate(); + insert.insert_point = codec::types::Vector3::new(position.x, position.y, position.z); + insert.rotation = placement.x_axis.y.atan2(placement.x_axis.x); + let handle = document + .add_entity(EntityType::Insert(insert)) + .map_err(|error| error.to_string())?; + let turned = (placement.x_axis.truncate() - DVec3::X).length() > 1e-12 + || (placement.y_axis.truncate() - DVec3::Y).length() > 1e-12; + items.push(AssocArrayItem { + location, + flags: if turned { 12 } else { 8 }, + uses_default_transform: !turned, + x_direction: if turned { + codec::types::Vector3::ZERO + } else { + codec::types::Vector3::new(position.x, position.y, position.z) + }, + transform: if turned { + placement.transpose().to_cols_array() + } else { + [0.0; 16] + }, + first_handle: Some(handle), + ..Default::default() + }); + new_handles.push(handle); + } + if let Some(record) = document + .block_records + .iter_mut() + .find(|record| record.handle == array.source_block) + { + record.insert_handles.retain(|handle| !old_items.contains(handle)); + record.insert_handles.extend(new_handles); + } + let values_in_action = action(document, array.action).is_some_and(|a| !a.values.is_empty()); + if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&array.body) { + let array_body = match &mut object.data { + AssociativeData::ArrayActionBody(body) => Some(body), + AssociativeData::ArrayModifyActionBody(modify) => Some(&mut modify.body), + _ => None, + }; + if let Some(array_body) = array_body { + array_body.items = items; + if !values_in_action { + array_body.parameter_body.values = params.clone(); + } + } + } + if values_in_action { + if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&array.action) { + if let AssociativeData::Action(action) = &mut object.data { + action.values = params; + } + } + } + scene.bump_geometry(); + Ok(()) +} + +/// Class records the array objects need, with the values the reference +/// product writes: (DXF name, class name, DWG version, maintenance version). +const ARRAY_CLASSES: &[(&str, &str, i32, i32)] = &[ + ("ACDBASSOCNETWORK", "AcDbAssocNetwork", 27, 45), + ("ACDBASSOCACTION", "AcDbAssocAction", 27, 45), + ("ACDBASSOCDEPENDENCY", "AcDbAssocDependency", 27, 50), + ("ACDBASSOCVERTEXACTIONPARAM", "AcDbAssocVertexActionParam", 27, 50), + ("ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", 27, 50), + ("ACDBASSOCARRAYACTIONBODY", "AcDbAssocArrayActionBody", 29, 55), + ("ACDBASSOCARRAYRECTANGULARPARAMETERS", "AcDbAssocArrayRectangularParameters", 29, 55), + ("ACDBASSOCARRAYPOLARPARAMETERS", "AcDbAssocArrayPolarParameters", 29, 55), +]; + +fn register_array_classes(document: &mut CadDocument) { + for &(dxf_name, cpp_class_name, dwg_version, maintenance_version) in ARRAY_CLASSES { + if document.classes.get_by_name(dxf_name).is_some() { + continue; + } + document.classes.add_or_update(codec::classes::DxfClass { + dxf_name: dxf_name.to_string(), + cpp_class_name: cpp_class_name.to_string(), + application_name: "ObjectDBX Classes".to_string(), + proxy_flags: codec::classes::ProxyFlags(1025), + instance_count: 0, + was_zombie: false, + is_an_entity: false, + class_number: 0, + item_class_id: 0x1F3, + dwg_version, + maintenance_version, + unknown1: 0, + unknown2: 0, + }); + } +} + +fn next_anonymous_name(document: &CadDocument) -> String { + let next = document + .block_records + .iter() + .filter_map(|record| record.name.strip_prefix("*U")?.parse::().ok()) + .max() + .map_or(1, |n| n + 1); + format!("*U{next}") +} + +fn associative( + handle: Handle, + owner: Handle, + dxf_name: &str, + cpp_class_name: &str, + data: AssociativeData, +) -> ObjectType { + let mut object = codec::objects::AssociativeObject::new(dxf_name, cpp_class_name); + object.handle = handle; + object.owner = owner; + object.data = data; + ObjectType::Associative(object) +} + +fn dictionary_entry(document: &CadDocument, dictionary: Handle, key: &str) -> Option { + match document.objects.get(&dictionary)? { + ObjectType::Dictionary(dictionary) => dictionary.get(key), + _ => None, + } +} + +/// A dictionary stored under `key` in `parent`, created when absent. +fn child_dictionary(document: &mut CadDocument, parent: Handle, key: &str) -> Handle { + if let Some(handle) = dictionary_entry(document, parent, key) + .filter(|handle| matches!(document.objects.get(handle), Some(ObjectType::Dictionary(_)))) + { + return handle; + } + let handle = document.allocate_handle(); + let mut dictionary = codec::objects::Dictionary::new(); + dictionary.handle = handle; + dictionary.owner = parent; + dictionary.reactors.push(parent); + document.objects.insert(handle, ObjectType::Dictionary(dictionary)); + if let Some(ObjectType::Dictionary(parent)) = document.objects.get_mut(&parent) { + parent.add_entry(key, handle); + } + handle +} + +/// The network holding the actions of model space, created with the root +/// network above it when the drawing has none. +fn model_space_network(document: &mut CadDocument) -> Handle { + const KEY: &str = "ACAD_ASSOCNETWORK"; + let model = document.header.model_space_block_handle; + let scope = document.ensure_extension_dictionary(model); + let holder = child_dictionary(document, scope, KEY); + if let Some(network) = dictionary_entry(document, holder, KEY).filter(|handle| { + matches!( + document.objects.get(handle), + Some(ObjectType::Associative(object)) + if matches!(object.data, AssociativeData::Network(_)) + ) + }) { + return network; + } + let root_holder = child_dictionary(document, document.header.named_objects_dict_handle, KEY); + let root = match dictionary_entry(document, root_holder, KEY) { + Some(root) if matches!(document.objects.get(&root), Some(ObjectType::Associative(_))) => { + root + } + _ => { + let root = document.allocate_handle(); + let network = codec::objects::AssocNetwork { + action: AssocAction { + class_version: 2, + ..Default::default() + }, + ..Default::default() + }; + let mut object = associative( + root, + root_holder, + "ACDBASSOCNETWORK", + "AcDbAssocNetwork", + AssociativeData::Network(network), + ); + if let ObjectType::Associative(object) = &mut object { + object.reactors.push(root_holder); + } + document.objects.insert(root, object); + if let Some(ObjectType::Dictionary(holder)) = document.objects.get_mut(&root_holder) { + holder.add_entry(KEY, root); + } + root + } + }; + let network = document.allocate_handle(); + let mut object = associative( + network, + holder, + "ACDBASSOCNETWORK", + "AcDbAssocNetwork", + AssociativeData::Network(codec::objects::AssocNetwork { + action: AssocAction { + class_version: 2, + owning_network: root, + ..Default::default() + }, + ..Default::default() + }), + ); + if let ObjectType::Associative(object) = &mut object { + object.reactors.push(holder); + } + document.objects.insert(network, object); + if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&holder) { + dictionary.add_entry(KEY, network); + } + if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&root) { + if let AssociativeData::Network(root) = &mut object.data { + root.network_action_index += 1; + root.actions.push(codec::objects::AssocActionDependency { + is_owned: false, + dependency: network, + }); + } + } + network +} + +fn value_param(name: &str, unit_type: i32, value: f64, whole: bool) -> AssocValueParam { + let variant = if whole { + codec::objects::AssocEvalVariant { + code: 90, + value: AssocEvalValue::Long(value.round() as i32), + } + } else { + codec::objects::AssocEvalVariant { + code: 40, + value: AssocEvalValue::Real(value), + } + }; + AssocValueParam { + name: name.to_string(), + unit_type, + variables: vec![codec::objects::AssocValueParamVariable { + value: variant, + handle: Handle::NULL, + }], + ..Default::default() + } +} + +/// The value parameters of a new rectangular array, in the order the +/// reference product writes them. +pub fn rectangular_params( + columns: u32, + rows: u32, + column_spacing: f64, + row_spacing: f64, +) -> Vec { + vec![ + value_param("AlignItems", 0, 0.0, true), + value_param("AxesAngle", 2, std::f64::consts::FRAC_PI_2, false), + value_param("ItemSpacing", 1, column_spacing, false), + value_param("Items", 0, f64::from(columns), true), + value_param("LevelSpacing", 1, 1.0, false), + value_param("Levels", 0, 1.0, true), + value_param("RowElevation", 1, 0.0, false), + value_param("RowSpacing", 1, row_spacing, false), + value_param("Rows", 0, f64::from(rows), true), + ] +} + +/// The value parameters of a new polar array. +pub fn polar_params( + items: u32, + fill_angle: f64, + radius: f64, + start_angle: f64, +) -> Vec { + let count = f64::from(items.max(1)); + let full = (fill_angle.abs() - std::f64::consts::TAU).abs() < 1e-9; + let spacing = if full { + fill_angle / count + } else if items > 1 { + fill_angle / (count - 1.0) + } else { + 0.0 + }; + vec![ + value_param("FillAngle", 2, fill_angle, false), + value_param("ItemSpacing", 2, spacing, false), + value_param("Items", 0, count, true), + value_param("LevelSpacing", 1, 1.0, false), + value_param("Levels", 0, 1.0, true), + value_param("RowElevation", 1, 0.0, false), + value_param("RowSpacing", 1, radius.abs().max(1.0), false), + value_param("Rows", 0, 1.0, true), + value_param("Radius", 1, radius, false), + value_param("StartAngle", 2, start_angle, false), + ] +} + +/// What ARRAYRECT / ARRAYPOLAR ask for an associative array. +#[derive(Clone, Debug, PartialEq)] +pub enum AssocArraySpec { + Rectangular { + columns: u32, + rows: u32, + column_spacing: f64, + row_spacing: f64, + }, + Polar { + center: DVec3, + items: u32, + fill_angle: f64, + }, +} + +/// Turn `sources` into an associative array: they move into an anonymous +/// source block whose origin is `source_point`, an anonymous array block +/// holds one reference to it per item, and an INSERT of the array block at +/// `base` takes their place, tied together by an array action in the +/// model-space network. A polar array turns about `base`. Returns the +/// array INSERT. +pub fn create( + scene: &mut crate::scene::Scene, + sources: &[Handle], + source_point: DVec3, + base: DVec3, + kind: ArrayKind, + params: Vec, +) -> Result { + use codec::objects::{ + AssocActionBody, AssocActionDependency, AssocActionParam, AssocArrayItem, + AssocSingleDependencyActionParam, AssocVertexActionParam, + }; + use codec::EntityType; + let normal = DVec3::Z; + let placements: Vec<([i32; 3], DMat4)> = match kind { + ArrayKind::Rectangular => rectangular_items(¶ms, DVec3::X, normal) + .into_iter() + .map(|(location, offset)| (location, DMat4::from_translation(offset))) + .collect(), + ArrayKind::Polar => polar_items(¶ms, DVec3::ZERO, normal), + ArrayKind::Path => return Err("path arrays cannot be created yet".into()), + }; + let mut entities: Vec = sources + .iter() + .filter_map(|handle| scene.document.get_entity(*handle).cloned()) + .collect(); + if entities.is_empty() { + return Err("nothing to array".into()); + } + let layer = scene.document.header.current_layer_name.clone(); + for entity in &mut entities { + crate::scene::view::dispatch::apply_transform( + entity, + &crate::command::EntityTransform::Translate(-source_point), + ); + } + register_array_classes(&mut scene.document); + + let source_name = next_anonymous_name(&scene.document); + scene.define_block_raw(&source_name, codec::types::Vector3::ZERO, entities); + let item = |offset: DMat4| { + let position = offset.w_axis.truncate(); + let mut insert = codec::entities::Insert::new( + source_name.clone(), + codec::types::Vector3::new(position.x, position.y, position.z), + ); + insert.rotation = offset.x_axis.y.atan2(offset.x_axis.x); + insert.common.layer = "0".to_string(); + insert.common.color = codec::types::Color::ByBlock; + insert.common.linetype = "ByBlock".to_string(); + insert.common.line_weight = codec::types::LineWeight::ByBlock; + insert.common.transparency = codec::types::Transparency::ByBlock; + EntityType::Insert(insert) + }; + let array_name = next_anonymous_name(&scene.document); + scene.define_block_raw( + &array_name, + codec::types::Vector3::ZERO, + placements.iter().map(|(_, placement)| item(*placement)).collect(), + ); + let record = |document: &CadDocument, name: &str| { + document + .block_records + .get(name) + .map(|record| (record.handle, record.entity_handles.clone())) + }; + let (source_block, _) = record(&scene.document, &source_name).ok_or("source block")?; + let (array_block, item_handles) = record(&scene.document, &array_name).ok_or("array block")?; + for name in [&source_name, &array_name] { + if let Some(record) = scene.document.block_records.get_mut(name) { + record.flags.anonymous = true; + record.explodable = true; + record.scale_uniformly = *name == array_name; + } + } + if let Some(record) = scene.document.block_records.get_mut(&source_name) { + record.insert_handles = item_handles.clone(); + } + + let mut array_insert = codec::entities::Insert::new( + array_name.clone(), + codec::types::Vector3::new(base.x, base.y, base.z), + ); + array_insert.common.layer = layer; + let insert = scene.add_entity(EntityType::Insert(array_insert)); + scene.erase_entities(sources); + + let document = &mut scene.document; + let network = model_space_network(document); + let [action, body, write_dep, read_dep, both_dep] = + std::array::from_fn(|_| document.allocate_handle()); + let mut owned_parameters = Vec::new(); + let vertex_params = + [("BaseNormal", normal), ("Origin", DVec3::ZERO), ("basePoint", DVec3::ZERO)]; + for (name, point) in vertex_params { + let handle = document.allocate_handle(); + document.objects.insert( + handle, + associative( + handle, + action, + "ACDBASSOCVERTEXACTIONPARAM", + "AcDbAssocVertexActionParam", + AssociativeData::VertexActionParam(AssocVertexActionParam { + single_dependency: AssocSingleDependencyActionParam { + action_param: AssocActionParam { + is_r2013: 1, + version: 0, + name: name.to_string(), + }, + ..Default::default() + }, + point: codec::types::Vector3::new(point.x, point.y, point.z), + }), + ), + ); + owned_parameters.push(handle); + } + for (handle, read, write, order, target, id) in [ + (write_dep, false, true, 0, array_block, 1), + (read_dep, true, false, 0, source_block, 2), + (both_dep, true, true, i32::MIN, insert, 3), + ] { + document.objects.insert( + handle, + associative( + handle, + action, + "ACDBASSOCDEPENDENCY", + "AcDbAssocDependency", + AssociativeData::Dependency(AssocDependency { + class_version: 2, + is_read_dependency: read, + is_write_dependency: write, + is_attached_to_object: true, + is_delegating_to_owning_action: true, + order, + dependent_on: target, + dependency_body_id: id, + ..Default::default() + }), + ), + ); + } + if let Some(EntityType::Insert(array)) = document.get_entity_mut(insert) { + array.common.reactors.push(both_dep); + } + let items = placements + .iter() + .zip(&item_handles) + .map(|((location, placement), handle)| { + let position = placement.w_axis.truncate(); + let turned = (placement.x_axis.truncate() - DVec3::X).length() > 1e-12; + AssocArrayItem { + location: *location, + flags: if turned { 12 } else { 8 }, + uses_default_transform: !turned, + x_direction: if turned { + codec::types::Vector3::ZERO + } else { + codec::types::Vector3::new(position.x, position.y, position.z) + }, + transform: if turned { + placement.transpose().to_cols_array() + } else { + [0.0; 16] + }, + first_handle: Some(*handle), + ..Default::default() + } + }) + .collect(); + let parameter_block = match kind { + ArrayKind::Polar => "AcDbAssocArrayPolarParameters", + _ => "AcDbAssocArrayRectangularParameters", + }; + document.objects.insert( + body, + associative( + body, + action, + "ACDBASSOCARRAYACTIONBODY", + "AcDbAssocArrayActionBody", + AssociativeData::ArrayActionBody(AssocArrayActionBody { + action_body: AssocActionBody { version: 2 }, + version: 1, + parameter_block: parameter_block.to_string(), + item_class: "AcDbAssocArrayItem".to_string(), + items, + transform: DMat4::from_translation(base).transpose().to_cols_array(), + ..Default::default() + }), + ), + ); + let action_index = match document.objects.get_mut(&network) { + Some(ObjectType::Associative(object)) => match &mut object.data { + AssociativeData::Network(network) => { + network.network_action_index += 1; + network.actions.push(AssocActionDependency { + is_owned: true, + dependency: action, + }); + network.network_action_index + } + _ => 1, + }, + _ => 1, + }; + document.objects.insert( + action, + associative( + action, + network, + "ACDBASSOCACTION", + "AcDbAssocAction", + AssociativeData::Action(AssocAction { + class_version: 2, + owning_network: network, + action_body: body, + action_index, + max_dependency_index: 3, + dependencies: [write_dep, read_dep, both_dep] + .map(|dependency| AssocActionDependency { + is_owned: true, + dependency, + }) + .to_vec(), + owned_parameters, + values: params, + ..Default::default() + }), + ), + ); + scene.bump_geometry(); + Ok(insert) +} + +/// Build an associative array from `sources` per `spec`, its source block +/// based at their lower-left corner `source_point`. +pub fn create_from_spec( + scene: &mut crate::scene::Scene, + sources: &[Handle], + source_point: DVec3, + spec: &AssocArraySpec, +) -> Result { + match *spec { + AssocArraySpec::Rectangular { + columns, + rows, + column_spacing, + row_spacing, + } => create( + scene, + sources, + source_point, + source_point, + ArrayKind::Rectangular, + rectangular_params(columns, rows, column_spacing, row_spacing), + ), + AssocArraySpec::Polar { + center, + items, + fill_angle, + } => { + let offset = source_point - center; + let params = polar_params( + items, + fill_angle, + offset.truncate().length(), + offset.y.atan2(offset.x), + ); + create(scene, sources, source_point, center, ArrayKind::Polar, params) + } + } +} diff --git a/src/scene/block_base_point_tests.rs b/src/scene/block_base_point_tests.rs index d5a501d1f..230eb8da3 100644 --- a/src/scene/block_base_point_tests.rs +++ b/src/scene/block_base_point_tests.rs @@ -538,14 +538,16 @@ fn locked_redefine_with_a_changed_base_moves_the_instance() { ); // The redefine path: the same contents, but a new base point (0,20,0). - scene.redefine_block_raw( - NAME, - v(0.0, 20.0, 0.0), - vec![EntityType::Line(Line::from_points( - v(0.0, 0.0, 0.0), - v(20.0, 0.0, 0.0), - ))], - ); + assert!(scene + .redefine_block_raw( + NAME, + v(0.0, 20.0, 0.0), + vec![EntityType::Line(Line::from_points( + v(0.0, 0.0, 0.0), + v(20.0, 0.0, 0.0), + ))], + ) + .is_ok()); assert_close( block_base_point(&scene.document, NAME), [0.0, 20.0, 0.0], @@ -616,14 +618,16 @@ fn locked_noop_redefine_leaves_the_instance_where_it_is() { assert_close(end, [62.990381, 27.5, 0.0], 1e-6, "line end before"); // No-op redefine: identical contents, same base. - scene.redefine_block_raw( - NAME, - base, - vec![EntityType::Line(Line::from_points( - v(6.0, 4.0, 0.0), - v(16.0, 4.0, 0.0), - ))], - ); + assert!(scene + .redefine_block_raw( + NAME, + base, + vec![EntityType::Line(Line::from_points( + v(6.0, 4.0, 0.0), + v(16.0, 4.0, 0.0), + ))], + ) + .is_ok()); assert_close( block_base_point(&scene.document, NAME), [6.0, 4.0, 0.0], @@ -667,9 +671,8 @@ fn locked_block_definition_dialog_prefills_the_existing_definition_base_point() #[test] fn locked_explode_places_geometry_at_convention_positions() { - // base-point: `explode_entity` shifts the definition's contents by -base before - // handing them to the codec's explode, so the pieces land where the render - // path puts them (see the module header for the derivation). + // The document-aware codec explosion uses the definition's base point, + // so the pieces land where the render path puts them. let mut scene = scene_with_bp_test(); let insert = placed_instance(&mut scene); let pieces = crate::modules::draw::modify::explode::explode_entity( @@ -679,7 +682,7 @@ fn locked_explode_places_geometry_at_convention_positions() { let mut line_start = None; let mut line_end = None; let mut circle_center = None; - for piece in pieces { + for piece in &pieces { match piece { EntityType::Line(line) => { if line_end.is_none() { @@ -710,6 +713,20 @@ fn locked_explode_places_geometry_at_convention_positions() { 1e-6, "exploded circle center", ); + // Exploding an INSERT drops attribute values and the + // attribute definitions (tags) reappear as ATTDEF entities. + let attdef_piece = pieces.iter().find_map(|p| match p { + EntityType::AttributeDefinition(ad) => Some(ad.clone()), + _ => None, + }); + let ad = attdef_piece.expect("the exploded ATTDEF"); + assert_eq!(ad.tag, "TAG1"); + assert_close( + ad.insertion_point, + EXPECTED_ATTR_POINT, + 1e-6, + "exploded attdef insertion point", + ); // Same numbers as the render path: EXPLODE and the renderer agree. let (render_start, render_end) = instance_line_world(&scene, BP_TEST); assert_close(render_end, EXPECTED_LINE_END, 1e-6, "render path: line end"); @@ -721,6 +738,62 @@ fn locked_explode_places_geometry_at_convention_positions() { ); } +#[test] +fn locked_minsert_explode_restores_attdefs_in_each_rotated_cell() { + let scene = scene_with_bp_test(); + let mut insert = Insert::new(BP_TEST, v(100.0, 100.0, 0.0)) + .with_uniform_scale(2.0) + .with_rotation(std::f64::consts::FRAC_PI_2) + .with_array(2, 2, 10.0, 20.0); + insert + .attributes + .push(AttributeEntity::new("TAG1".into(), "Instance value".into())); + let pieces = crate::modules::draw::modify::explode::explode_entity( + &EntityType::Insert(insert), + &scene.document, + ); + let definitions: Vec<_> = pieces + .iter() + .filter_map(|entity| match entity { + EntityType::AttributeDefinition(definition) => Some(definition), + _ => None, + }) + .collect(); + assert_eq!(definitions.len(), 4); + for (definition, expected) in definitions.iter().zip([ + [94.0, 104.0, 0.0], + [94.0, 114.0, 0.0], + [74.0, 104.0, 0.0], + [74.0, 114.0, 0.0], + ]) { + assert_close(definition.insertion_point, expected, 1e-6, "array ATTDEF"); + assert_eq!(definition.tag, "TAG1"); + assert_eq!(definition.default_value, ""); + } + let lines: Vec<_> = pieces + .iter() + .filter_map(|entity| match entity { + EntityType::Line(line) => Some(line), + _ => None, + }) + .collect(); + assert_eq!(lines.len(), 4); + for (line, expected) in lines.iter().zip([ + [100.0, 100.0, 0.0], + [100.0, 110.0, 0.0], + [80.0, 100.0, 0.0], + [80.0, 110.0, 0.0], + ]) { + assert_close(line.start, expected, 1e-6, "array LINE"); + } + assert_close( + block_attdefs(&scene.document, BP_TEST)[0].insertion_point, + [12.0, 8.0, 0.0], + 1e-6, + "source ATTDEF stays in definition coordinates", + ); +} + #[test] fn locked_xclip_boundary_round_trips_through_the_base_point_convention() { // base-point follow-up fix: the XCLIP ring is stored as WCS points plus the @@ -1036,3 +1109,149 @@ fn locked_nested_reference_explode_matches_the_render_path() { "render path: inner line end", ); } + +#[test] +fn locked_nested_insert_with_attributes_exploded_one_level() { + // When an outer block containing an inner INSERT and attributes is exploded one level, + // the outer block's attributes drop their values and its ATTDEFs reappear, while + // the inner INSERT remains an intact INSERT with its attributes preserved. + const INNER: &str = "BP_ATTR_INNER"; + const OUTER: &str = "BP_ATTR_OUTER"; + let mut scene = Scene::new(); + + // INNER block: base (0,0,0), has a LINE and an ATTDEF + let inner_ad = AttributeDefinition { + insertion_point: v(1.0, 1.0, 0.0), + height: 1.0, + ..AttributeDefinition::new( + "INNER_TAG".to_string(), + "Inner prompt".to_string(), + "Inner val".to_string(), + ) + }; + scene.define_block_raw( + INNER, + v(0.0, 0.0, 0.0), + vec![ + EntityType::Line(Line::from_points(v(0.0, 0.0, 0.0), v(5.0, 0.0, 0.0))), + EntityType::AttributeDefinition(inner_ad), + ], + ); + + // OUTER block: base (10.0, 5.0, 0.0), has a nested INSERT of INNER (with an attribute instance) and an ATTDEF + let mut inner_ins = Insert::new(INNER.to_string(), v(5.0, 3.0, 0.0)); + let mut inner_attr = + AttributeEntity::new("INNER_TAG".to_string(), "Actual inner value".to_string()); + inner_attr.insertion_point = v(6.0, 4.0, 0.0); + inner_ins.attributes.push(inner_attr); + + let outer_ad = AttributeDefinition { + insertion_point: v(12.0, 8.0, 0.0), + height: 1.0, + ..AttributeDefinition::new( + "OUTER_TAG".to_string(), + "Outer prompt".to_string(), + "Outer val".to_string(), + ) + }; + + scene.define_block_raw( + OUTER, + v(10.0, 5.0, 0.0), + vec![ + EntityType::Insert(inner_ins), + EntityType::AttributeDefinition(outer_ad), + ], + ); + + // Model space: instance of OUTER at (100, 100), 30ยฐ, scale 2 + let mut outer_instance = Insert::new(OUTER.to_string(), v(100.0, 100.0, 0.0)); + outer_instance.set_x_scale(2.0); + outer_instance.set_y_scale(2.0); + outer_instance.set_z_scale(2.0); + outer_instance.rotation = DEG30; + let mut outer_attr = + AttributeEntity::new("OUTER_TAG".to_string(), "Actual outer value".to_string()); + outer_attr.insertion_point = v(100.464102, 107.196152, 0.0); + outer_instance.attributes.push(outer_attr); + + // Explode outer instance one level + let pieces = crate::modules::draw::modify::explode::explode_entity( + &EntityType::Insert(outer_instance), + &scene.document, + ); + + // Expected pieces: the nested inner INSERT (with its attribute still intact) and the outer ATTDEF + let nested_insert = pieces + .iter() + .find_map(|p| match p { + EntityType::Insert(ins) if ins.block_name == INNER => Some(ins.clone()), + _ => None, + }) + .expect("nested inner INSERT among pieces"); + + assert_eq!(nested_insert.attributes.len(), 1); + assert_eq!(nested_insert.attributes[0].tag, "INNER_TAG"); + assert_eq!( + nested_insert.attributes[0].get_value(), + "Actual inner value" + ); + + let outer_attdef = pieces + .iter() + .find_map(|p| match p { + EntityType::AttributeDefinition(ad) if ad.tag == "OUTER_TAG" => Some(ad.clone()), + _ => None, + }) + .expect("reappeared outer ATTDEF among pieces"); + + assert_close( + outer_attdef.insertion_point, + EXPECTED_ATTR_POINT, + 1e-6, + "outer attdef insertion point", + ); +} + +#[test] +fn locked_redefine_block_raw_refuses_cycle() { + let mut scene = Scene::new(); + let blk_a = "REDEF_A"; + let blk_b = "REDEF_B"; + + scene.define_block_raw( + blk_a, + Vector3::ZERO, + vec![EntityType::Line(Line::from_points( + Vector3::ZERO, + Vector3::new(1.0, 0.0, 0.0), + ))], + ); + + // B contains A + scene.define_block_raw( + blk_b, + Vector3::ZERO, + vec![EntityType::Insert(Insert::new( + blk_a.to_string(), + Vector3::ZERO, + ))], + ); + + // Redefining A to contain B would create cycle A -> B -> A; must refuse + let res = scene.redefine_block_raw( + blk_a, + Vector3::ZERO, + vec![EntityType::Insert(Insert::new( + blk_b.to_string(), + Vector3::ZERO, + ))], + ); + + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!( + err.contains("circular dependency detected"), + "expected cycle error, got: {err}" + ); +} diff --git a/src/scene/block_service.rs b/src/scene/block_service.rs index 0f646ead9..5b20152d2 100644 --- a/src/scene/block_service.rs +++ b/src/scene/block_service.rs @@ -16,7 +16,7 @@ use codec::objects::ObjectType; use codec::tables::BlockRecord; use codec::types::Handle; -use codec::{CadDocument, EntityType}; +use codec::{CadDocument, EntityType, TableEntry}; use std::collections::HashSet; use std::fmt; @@ -285,6 +285,29 @@ fn is_layout_record(document: &CadDocument, record: &BlockRecord) -> bool { ) } +/// Candidate block names for the Edit Block Definition (BEDIT) dialog: +/// excludes standard layouts (*MODEL_SPACE, *PAPER_SPACE), layout records, +/// anonymous blocks (*U, *D, *T, etc.), and externally referenced blocks. +/// Sorted alphabetically, case-insensitive. +pub fn bedit_candidate_block_names(document: &CadDocument) -> Vec { + let mut names: Vec = document + .block_records + .iter() + .filter(|record| { + !record.is_standard() + && !is_layout_record(document, record) + && !record.is_anonymous() + && !record.name.starts_with('*') + && !record.flags.is_xref + && !record.flags.is_xref_overlay + && !record.name.contains('|') + }) + .map(|record| record.name.clone()) + .collect(); + names.sort_by_key(|n| n.to_ascii_lowercase()); + names +} + /// The extension-dictionary key on a dynamic block's record. const ENHANCED_BLOCK_KEY: &str = "ACAD_ENHANCEDBLOCK"; @@ -598,7 +621,88 @@ fn record_of<'a>( .get(name) .ok_or_else(|| missing(name)) } + +/// Checks whether inserting `inserted_block` into `container_block` would create a circular +/// block nesting dependency, directly or transitively. +/// +/// Matching is case-insensitive, consistent with the rest of the block service. +/// +/// Xrefs (`|` in name or xref flags) and anonymous blocks (`*` prefix) are handled as follows: +/// - Anonymous blocks (`*U...`) are fully traversed like normal blocks because their geometry +/// and nested inserts live in the document. +/// - Externally referenced blocks (xrefs) have definitions that come from external files and +/// are not edited inline in the host document, but if an xref name is tested, it is matched +/// case-insensitively and its local block record entities (if any) are traversed. +pub fn would_create_cycle( + document: &CadDocument, + container_block: &str, + inserted_block: &str, +) -> bool { + let container = container_block.trim(); + let inserted = inserted_block.trim(); + if container.is_empty() || inserted.is_empty() { + return false; + } + if container.eq_ignore_ascii_case(inserted) { + return true; + } + + let mut visited: HashSet = HashSet::default(); + let mut stack: Vec = vec![inserted.to_ascii_uppercase()]; + + while let Some(current_upper) = stack.pop() { + if !visited.insert(current_upper.clone()) { + continue; + } + + // Find block record matching current_upper case-insensitively + let Some(record) = document.block_records.get(¤t_upper) else { + continue; + }; + + // Check entities owned by this block record: either registered in entity_handles + // or having owner_handle == record.handle in the document. + let mut sub_inserts = Vec::new(); + for &handle in &record.entity_handles { + if let Some(EntityType::Insert(sub_ins)) = document.get_entity(handle) { + sub_inserts.push(sub_ins.block_name.trim().to_string()); + } + } + for entity in document.entities() { + if entity.common().owner_handle == record.handle { + if let EntityType::Insert(sub_ins) = entity { + sub_inserts.push(sub_ins.block_name.trim().to_string()); + } + } + } + + for sub_name in sub_inserts { + if sub_name.eq_ignore_ascii_case(container) { + return true; + } + let sub_upper = sub_name.to_ascii_uppercase(); + if !visited.contains(&sub_upper) { + stack.push(sub_upper); + } + } + } + + false +} + +pub const CIRCULAR_BLOCK_DEPENDENCY_ERROR: &str = + "Cannot insert block: circular dependency detected."; + +/// Standard command-line error text when block insertion would create a circular dependency. +pub fn circular_dependency_error(container_block: &str, inserted_block: &str) -> String { + format!( + "Cannot insert block \"{}\" into block \"{}\": circular dependency detected.", + inserted_block, container_block + ) +} + #[cfg(test)] + mod tests { use super::*; use codec::entities::{ @@ -1097,4 +1201,92 @@ mod tests { "block \"A\" is open in the block editor; close the editor first" ); } + + #[test] + fn locked_would_create_cycle_self_insert() { + let mut scene = scene(); + define(&mut scene, "BLK_A"); + assert!(would_create_cycle(&scene.document, "BLK_A", "BLK_A")); + assert!(would_create_cycle(&scene.document, "blk_a", "BLK_A")); + assert!(would_create_cycle(&scene.document, "BLK_A", "blk_a")); + } + + #[test] + fn locked_would_create_cycle_direct_pair() { + let mut scene = scene(); + let blk_a = define(&mut scene, "BLK_A"); + define(&mut scene, "BLK_B"); + // A contains B + add_insert(&mut scene, "BLK_B", Some(blk_a)); + // Inserting A into B would create a cycle + assert!(would_create_cycle(&scene.document, "BLK_B", "BLK_A")); + // Inserting B into A is not allowed either since A already inserts B + assert!(!would_create_cycle(&scene.document, "BLK_A", "BLK_B")); + } + + #[test] + fn locked_would_create_cycle_three_level_chain() { + let mut scene = scene(); + let blk_a = define(&mut scene, "BLK_A"); + let blk_b = define(&mut scene, "BLK_B"); + define(&mut scene, "BLK_C"); + // A contains B, B contains C + add_insert(&mut scene, "BLK_B", Some(blk_a)); + add_insert(&mut scene, "BLK_C", Some(blk_b)); + // Inserting A into C creates cycle C -> A -> B -> C + assert!(would_create_cycle(&scene.document, "BLK_C", "BLK_A")); + // Inserting B into C creates cycle C -> B -> C + assert!(would_create_cycle(&scene.document, "BLK_C", "BLK_B")); + } + + #[test] + fn locked_would_create_cycle_shared_child_not_a_cycle() { + let mut scene = scene(); + let blk_a = define(&mut scene, "BLK_A"); + let blk_b = define(&mut scene, "BLK_B"); + define(&mut scene, "BLK_SHARED"); + // A contains SHARED, B contains SHARED. Inserting B into A is fine (diamond/DAG, not cycle). + add_insert(&mut scene, "BLK_SHARED", Some(blk_a)); + add_insert(&mut scene, "BLK_SHARED", Some(blk_b)); + assert!(!would_create_cycle(&scene.document, "BLK_A", "BLK_B")); + assert!(!would_create_cycle(&scene.document, "BLK_B", "BLK_A")); + } + + #[test] + fn locked_would_create_cycle_case_differences() { + let mut scene = scene(); + let blk_a = define(&mut scene, "BlockA"); + let _blk_b = define(&mut scene, "BlockB"); + add_insert(&mut scene, "BlockB", Some(blk_a)); + assert!(would_create_cycle(&scene.document, "blockb", "BLOCKA")); + assert!(!would_create_cycle(&scene.document, "blocka", "BLOCKB")); + } + + #[test] + fn locked_bedit_candidate_block_names_filters_and_sorts() { + let mut scene = scene(); + define(&mut scene, "zebra"); + define(&mut scene, "Alpha"); + define(&mut scene, "Beta"); + + // Anonymous block + let mut anon = BlockRecord::new("*U1"); + anon.handle = Handle::new(9901); + anon.flags.anonymous = true; + scene.document.block_records.add(anon).unwrap(); + + // Xref block + let mut xref = BlockRecord::new("XrefBlock"); + xref.handle = Handle::new(9902); + xref.flags.is_xref = true; + scene.document.block_records.add(xref).unwrap(); + + // Xref dependent symbol + let mut xref_dep = BlockRecord::new("HOST|CHILD"); + xref_dep.handle = Handle::new(9903); + scene.document.block_records.add(xref_dep).unwrap(); + + let candidates = bedit_candidate_block_names(&scene.document); + assert_eq!(candidates, vec!["Alpha", "Beta", "zebra"]); + } } diff --git a/src/scene/block_visibility.rs b/src/scene/block_visibility.rs new file mode 100644 index 000000000..01830d9b3 --- /dev/null +++ b/src/scene/block_visibility.rs @@ -0,0 +1,742 @@ +//! Visibility states of a dynamic block, authored in the Block Editor and +//! stored the way the format keeps them: a BLOCKVISIBILITYPARAMETER in the +//! block's `ACAD_ENHANCEDBLOCK` evaluation graph (with its grip and the +//! grip's two location expressions), `AcDbBlockRepETag` tags on the +//! definition and on each of its objects, and, behind every reference that +//! shows a state other than the definition's, an evaluated `*U` copy tagged +//! `AcDbBlockRepBTag` and linked back by `AcDbBlockRepresentationData`. +//! +//! The definition itself shows the first state, so a reference to it needs +//! no copy until another state is picked for it. + +use codec::objects::{ + BlockElement, BlockEvalExpression, BlockEvalValue, BlockEvaluationEdge, BlockEvaluationGraph, + BlockEvaluationNode, BlockGrip, BlockGripExpression, BlockParameterPropertyInfo, + BlockRepresentationData, BlockVisibilityParameter, BlockVisibilityState, DynamicBlockData, + DynamicBlockObject, ObjectType, +}; +use codec::types::Vector3; +use codec::xdata::{ExtendedDataRecord, XDataValue}; +use codec::{CadDocument, EntityType, Handle}; + +use super::Scene; + +const ELEMENT_TAG: &str = "AcDbBlockRepETag"; +const REPRESENTATION_TAG: &str = "AcDbBlockRepBTag"; +const TRUE_NAME: &str = "AcDbDynamicBlockTrueName"; +const GUID: &str = "AcDbDynamicBlockGUID"; + +/// Object version the parameter, grip and expressions carry. +const MAJOR: i32 = 33; +const MINOR: i32 = 329; +/// Value code of an expression that holds no value. +const NO_VALUE: i16 = -9999; + +/// (DXF name, class, DWG maintenance version) of the classes used here. +const CLASSES: &[(&str, &str, i32)] = &[ + ("ACAD_EVALUATION_GRAPH", "AcDbEvalGraph", 55), + ("BLOCKVISIBILITYPARAMETER", "AcDbBlockVisibilityParameter", 55), + ("BLOCKVISIBILITYGRIP", "AcDbBlockVisibilityGrip", 55), + ("BLOCKGRIPLOCATIONCOMPONENT", "AcDbBlockGripExpr", 20), + ("ACDB_DYNAMICBLOCKPURGEPREVENTER_VERSION", "AcDbDynamicBlockPurgePreventer", 61), + ("ACDB_BLOCKREPRESENTATION_DATA", "AcDbBlockRepresentationData", 58), +]; + +/// Register the classes with the flags and versions the reference writes +/// for them; a reader takes a class registered otherwise for a proxy. +fn register_classes(document: &mut CadDocument) { + for &(dxf_name, cpp_class_name, maintenance_version) in CLASSES { + if let Some(existing) = document.classes.get_mut_by_name(dxf_name) { + existing.proxy_flags = codec::classes::ProxyFlags(1153); + existing.dwg_version = 25; + existing.maintenance_version = maintenance_version; + continue; + } + document.classes.add_or_update(codec::classes::DxfClass { + dxf_name: dxf_name.to_string(), + cpp_class_name: cpp_class_name.to_string(), + application_name: "ObjectDBX Classes".to_string(), + proxy_flags: codec::classes::ProxyFlags(1153), + instance_count: 0, + was_zombie: false, + is_an_entity: false, + class_number: 0, + item_class_id: 0x1F3, + dwg_version: 25, + maintenance_version, + unknown1: 0, + unknown2: 0, + }); + } +} + +fn ensure_app_id(document: &mut CadDocument, name: &str) { + if !document.app_ids.contains(name) { + let mut app = codec::tables::AppId::new(name); + app.handle = document.allocate_handle(); + let _ = document.app_ids.add(app); + } +} + +fn expression(node_id: i32, value_code: i16, value: BlockEvalValue) -> BlockEvalExpression { + BlockEvalExpression { + parent_id: BlockEvalExpression::NO_PARENT, + major: MAJOR, + minor: MINOR, + value_code, + value, + node_id, + } +} + +fn dynamic( + handle: Handle, + owner: Handle, + dxf_name: &str, + cpp_class_name: &str, + data: DynamicBlockData, +) -> DynamicBlockObject { + let mut object = DynamicBlockObject::new(dxf_name, cpp_class_name); + object.handle = handle; + object.owner = owner; + object.data = data; + object +} + +/// The visibility parameter of block definition `definition`. +pub fn parameter(document: &CadDocument, definition: Handle) -> Option<&BlockVisibilityParameter> { + document.block_visibility_param_for_def(definition) +} + +/// Store an edited parameter in the document and its lookup view. +fn store(document: &mut CadDocument, parameter: BlockVisibilityParameter) { + document + .block_visibility_params + .insert(parameter.handle, parameter.clone()); + document.objects.insert( + parameter.handle, + ObjectType::BlockVisibilityParameter(parameter), + ); +} + +/// The objects of a definition, block begin and end left out. +fn members(document: &CadDocument, definition: Handle) -> Vec { + document + .block_records + .iter() + .find(|record| record.handle == definition) + .map(|record| { + record + .entity_handles + .iter() + .copied() + .filter(|handle| { + !matches!( + document.get_entity(*handle), + None | Some(EntityType::Block(_) | EntityType::BlockEnd(_)) + ) + }) + .collect() + }) + .unwrap_or_default() +} + +fn element_tag(id: i32, handle: Handle) -> ExtendedDataRecord { + let mut tag = ExtendedDataRecord::new(ELEMENT_TAG); + tag.add_value(XDataValue::Integer16(1)); + tag.add_value(XDataValue::Integer32(id)); + tag.add_value(XDataValue::Handle(handle)); + tag +} + +fn element_id(entity: &EntityType) -> Option { + entity + .common() + .extended_data + .get_record(ELEMENT_TAG)? + .values + .iter() + .find_map(|value| match value { + XDataValue::Integer32(id) => Some(*id), + _ => None, + }) +} + +fn set_entity_tag(document: &mut CadDocument, handle: Handle, record: ExtendedDataRecord) { + let app = document + .app_ids + .get(&record.application_name) + .map(|app| app.handle.value()); + if let Some(entity) = document.get_entity_mut(handle) { + let xdata = &mut entity.common_mut().extended_data; + // The fresh record, not bytes kept from a read, is what gets written. + if let Some(app) = app { + xdata.raw_dwg_eed.retain(|(owner, _)| *owner != app); + } + xdata.upsert_record(record); + } +} + +/// Give every object of the definition its element tag, numbering the new +/// ones after the highest id in use, and record the next id on the block. +fn tag_members(document: &mut CadDocument, definition: Handle) { + let members = members(document, definition); + let mut next = members + .iter() + .filter_map(|handle| document.get_entity(*handle).and_then(element_id)) + .max() + .map_or(0, |id| id + 1); + for handle in members { + let id = match document.get_entity(handle).and_then(element_id) { + Some(id) => id, + None => { + next += 1; + next - 1 + } + }; + set_entity_tag(document, handle, element_tag(id, handle)); + } + let mut tag = ExtendedDataRecord::new(ELEMENT_TAG); + tag.add_value(XDataValue::Integer16(1)); + tag.add_value(XDataValue::Integer32(next)); + document.set_object_xdata_record(definition, tag); +} + +/// A registry-format GUID for a new dynamic block. +fn new_guid() -> String { + let mut bytes = [0u8; 16]; + if getrandom::fill(&mut bytes).is_err() { + let seed = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |elapsed| elapsed.as_nanos()); + bytes = seed.to_le_bytes(); + } + let hex: String = bytes.iter().map(|byte| format!("{byte:02X}")).collect(); + format!( + "{{{}-{}-{}-{}-{}}}", + &hex[0..8], + &hex[8..12], + &hex[12..16], + &hex[16..20], + &hex[20..32] + ) +} + +/// Make block definition `definition` dynamic with a visibility parameter +/// whose grip sits at `location`, one state showing every object. Returns +/// the parameter, or the one the block has already. +pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3) -> Option { + if let Some(existing) = parameter(document, definition) { + return Some(existing.handle); + } + let name = document + .block_records + .iter() + .find(|record| record.handle == definition)? + .name + .clone(); + register_classes(document); + for app in [ELEMENT_TAG, REPRESENTATION_TAG, TRUE_NAME, GUID] { + ensure_app_id(document, app); + } + let dictionary = document.ensure_extension_dictionary(definition); + let [graph, parameter, grip, x, y, preventer] = + [(); 6].map(|_| document.allocate_handle()); + + store( + document, + BlockVisibilityParameter { + handle: parameter, + owner: graph, + eval_parent_id: BlockEvalExpression::NO_PARENT, + eval_major: MAJOR, + eval_minor: MINOR, + eval_value_code: NO_VALUE, + eval_value: BlockEvalValue::None, + eval_node_id: 1, + element_name: "Visibility State".to_string(), + element_major: MAJOR, + element_minor: MINOR, + element_eed_1071: 0, + show_properties: true, + chain_actions: false, + name: "Visibility1".to_string(), + description: String::new(), + def_point: location, + property_info: [ + BlockParameterPropertyInfo::default(), + BlockParameterPropertyInfo::default(), + ], + property_info_count: 2, + is_initialized: true, + unknown_bool: false, + all_blocks: Vec::new(), + states: vec![BlockVisibilityState { + name: "VisibilityState0".to_string(), + ..Default::default() + }], + }, + ); + let grip_object = dynamic( + grip, + graph, + "BLOCKVISIBILITYGRIP", + "AcDbBlockVisibilityGrip", + DynamicBlockData::VisibilityGrip(BlockGrip { + element: BlockElement { + eval: expression(2, NO_VALUE, BlockEvalValue::None), + name: "Grip".to_string(), + major: MAJOR, + minor: MINOR, + eed_1071: 0, + }, + flags_91: 3, + flags_92: 4, + location, + insert_cycling: false, + insert_cycling_weight: -1, + }), + ); + document + .objects + .insert(grip, ObjectType::DynamicBlock(grip_object)); + for (handle, node, axis) in [(x, 3, "UpdatedX"), (y, 4, "UpdatedY")] { + let component = dynamic( + handle, + graph, + "BLOCKGRIPLOCATIONCOMPONENT", + "AcDbBlockGripExpr", + DynamicBlockData::GripLocationComponent(BlockGripExpression { + eval: expression(node, 40, BlockEvalValue::Real(0.0)), + grip_type: 1, + expression: axis.to_string(), + }), + ); + document + .objects + .insert(handle, ObjectType::DynamicBlock(component)); + } + // The grip drives the parameter, whose position the two expressions + // read: node 0 the parameter, 1 the grip, 2 and 3 the expressions. + let node = |id: i32, expression: Handle, node_data: [i32; 4]| BlockEvaluationNode { + id, + edge_flags: 32, + next_id: id + 1, + expression, + node_data, + active_cycles: None, + }; + let edge = |id: i32, source_node: i32, destination_node: i32, outgoing_edges| { + BlockEvaluationEdge { + id, + next_id: 0, + incoming_edge: 1, + source_node, + destination_node, + outgoing_edges, + } + }; + let mut graph_object = dynamic( + graph, + dictionary, + "ACAD_EVALUATION_GRAPH", + "AcDbEvalGraph", + DynamicBlockData::EvaluationGraph(BlockEvaluationGraph { + first_node_id: 4, + first_node_id_copy: 4, + nodes: vec![ + node(0, parameter, [0, 0, 1, 2]), + node(1, grip, [-1, -1, 0, 0]), + node(2, x, [1, 1, -1, -1]), + node(3, y, [2, 2, -1, -1]), + ], + edges: vec![ + edge(0, 1, 0, [-1; 5]), + edge(1, 0, 2, [-1, -1, -1, 2, -1]), + edge(2, 0, 3, [-1, -1, 1, -1, -1]), + ], + }), + ); + graph_object.reactors.push(dictionary); + document + .objects + .insert(graph, ObjectType::DynamicBlock(graph_object)); + let mut preventer_object = dynamic( + preventer, + dictionary, + "ACDB_DYNAMICBLOCKPURGEPREVENTER_VERSION", + "AcDbDynamicBlockPurgePreventer", + DynamicBlockData::Representation(BlockRepresentationData { + flags: 1, + block: definition, + }), + ); + preventer_object.reactors.push(dictionary); + document + .objects + .insert(preventer, ObjectType::DynamicBlock(preventer_object)); + if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary) { + dictionary.add_entry("ACAD_ENHANCEDBLOCK", graph); + dictionary.add_entry("AcDbDynamicBlockRoundTripPurgePreventer", preventer); + } + + let mut true_name = ExtendedDataRecord::new(TRUE_NAME); + true_name.add_value(XDataValue::String(name)); + document.set_object_xdata_record(definition, true_name); + let mut guid = ExtendedDataRecord::new(GUID); + guid.add_value(XDataValue::String(new_guid())); + document.set_object_xdata_record(definition, guid); + tag_members(document, definition); + Some(parameter) +} + +/// The names of the states of `parameter`, in list order. +pub fn state_names(document: &CadDocument, parameter: Handle) -> Vec { + match document.objects.get(¶meter) { + Some(ObjectType::BlockVisibilityParameter(parameter)) => { + parameter.states.iter().map(|state| state.name.clone()).collect() + } + _ => Vec::new(), + } +} + +fn edit( + document: &mut CadDocument, + parameter: Handle, + change: impl FnOnce(&mut BlockVisibilityParameter) -> R, +) -> Option { + let Some(ObjectType::BlockVisibilityParameter(current)) = document.objects.get(¶meter) + else { + return None; + }; + let mut edited = current.clone(); + let result = change(&mut edited); + store(document, edited); + Some(result) +} + +/// Add state `name`, showing what state `like` shows. Returns its index, or +/// `None` when the name is taken. +pub fn add_state( + document: &mut CadDocument, + parameter: Handle, + name: &str, + like: usize, +) -> Option { + edit(document, parameter, |parameter| { + if parameter + .states + .iter() + .any(|state| state.name.eq_ignore_ascii_case(name)) + { + return None; + } + let visible_blocks = parameter + .states + .get(like) + .map(|state| state.visible_blocks.clone()) + .unwrap_or_default(); + parameter.states.push(BlockVisibilityState { + name: name.to_string(), + visible_blocks, + visible_params: Vec::new(), + }); + Some(parameter.states.len() - 1) + }) + .flatten() +} + +/// Rename state `index`; false when the new name is taken. +pub fn rename_state( + document: &mut CadDocument, + parameter: Handle, + index: usize, + name: &str, +) -> bool { + edit(document, parameter, |parameter| { + let taken = parameter + .states + .iter() + .enumerate() + .any(|(other, state)| other != index && state.name.eq_ignore_ascii_case(name)); + match parameter.states.get_mut(index) { + Some(state) if !taken => { + state.name = name.to_string(); + true + } + _ => false, + } + }) + .unwrap_or(false) +} + +/// Delete state `index`; the last state stays. +pub fn delete_state(document: &mut CadDocument, parameter: Handle, index: usize) -> bool { + edit(document, parameter, |parameter| { + if parameter.states.len() <= 1 || index >= parameter.states.len() { + return false; + } + parameter.states.remove(index); + true + }) + .unwrap_or(false) +} + +/// Show (`visible`) or hide `handles` in state `index`. An object the +/// parameter did not govern yet is shown in every other state, as it was. +pub fn set_visible( + document: &mut CadDocument, + parameter: Handle, + index: usize, + handles: &[Handle], + visible: bool, +) { + let definition = owner_definition(document, parameter); + edit(document, parameter, |parameter| { + for &handle in handles { + if !parameter.all_blocks.contains(&handle) { + parameter.all_blocks.push(handle); + for state in &mut parameter.states { + state.visible_blocks.push(handle); + } + } + let Some(state) = parameter.states.get_mut(index) else { + continue; + }; + state.visible_blocks.retain(|other| *other != handle); + if visible { + state.visible_blocks.push(handle); + } + } + }); + if let Some(definition) = definition { + tag_members(document, definition); + } +} + +/// The block definition `parameter` belongs to (graph, dictionary, record). +fn owner_definition(document: &CadDocument, parameter: Handle) -> Option { + let graph = match document.objects.get(¶meter)? { + ObjectType::BlockVisibilityParameter(parameter) => parameter.owner, + _ => return None, + }; + let dictionary = match document.objects.get(&graph)? { + ObjectType::DynamicBlock(graph) => graph.owner, + _ => return None, + }; + match document.objects.get(&dictionary)? { + ObjectType::Dictionary(dictionary) => Some(dictionary.owner), + _ => None, + } +} + +/// Show the definition's objects as state `index` does. +pub fn apply_state(document: &mut CadDocument, parameter: Handle, index: usize) { + let Some(ObjectType::BlockVisibilityParameter(current)) = document.objects.get(¶meter) + else { + return; + }; + let Some(state) = current.states.get(index) else { + return; + }; + let shown: Vec<(Handle, bool)> = current + .all_blocks + .iter() + .map(|handle| (*handle, state.visible_blocks.contains(handle))) + .collect(); + for (handle, visible) in shown { + if let Some(entity) = document.get_entity_mut(handle) { + entity.common_mut().invisible = !visible; + } + } +} + +/// The state whose objects are the ones the definition shows now. +pub fn shown_state(document: &CadDocument, parameter: Handle) -> usize { + let Some(ObjectType::BlockVisibilityParameter(parameter)) = document.objects.get(¶meter) + else { + return 0; + }; + parameter + .states + .iter() + .position(|state| { + parameter.all_blocks.iter().all(|handle| { + let shown = document + .get_entity(*handle) + .is_some_and(|entity| !entity.common().invisible); + shown == state.visible_blocks.contains(handle) + }) + }) + .unwrap_or(0) +} + +/// After the Block Editor: drop objects that were erased, tag new ones and +/// leave the definition showing its first state. +pub fn finish_editing(document: &mut CadDocument, definition: Handle) { + let Some(handle) = parameter(document, definition).map(|parameter| parameter.handle) else { + return; + }; + let live: Vec = members(document, definition); + edit(document, handle, |parameter| { + parameter.all_blocks.retain(|handle| live.contains(handle)); + for state in &mut parameter.states { + state.visible_blocks.retain(|handle| live.contains(handle)); + } + }); + tag_members(document, definition); + apply_state(document, handle, 0); +} + +impl Scene { + /// Give a reference to a dynamic definition its own evaluated copy, so a + /// state picked for it leaves the definition and the other references + /// alone. Returns the copy's name; a reference already showing a copy, + /// or not dynamic, keeps its block and gets `None`. + pub fn visibility_representation(&mut self, insert: Handle) -> Option { + let Some(EntityType::Insert(reference)) = self.document.get_entity(insert) else { + return None; + }; + let definition = self.document.block_records.get(&reference.block_name)?.handle; + parameter(&self.document, definition)?; + let name = { + let next = self + .document + .block_records + .iter() + .filter_map(|record| record.name.strip_prefix("*U")?.parse::().ok()) + .max() + .map_or(1, |n| n + 1); + format!("*U{next}") + }; + let base = self + .document + .block_records + .iter() + .find(|record| record.handle == definition) + .and_then(|record| match self.document.get_entity(record.block_entity_handle) { + Some(EntityType::Block(block)) => Some(block.base_point), + _ => None, + }) + .unwrap_or(Vector3::ZERO); + let originals = members(&self.document, definition); + let copies: Vec = originals + .iter() + .filter_map(|handle| self.document.get_entity(*handle).cloned()) + .collect(); + self.define_block_raw(&name, base, copies); + let record = self.document.block_records.get_mut(&name)?; + record.flags.anonymous = true; + let copy = record.handle; + // The copies keep their element ids, now naming themselves. + for handle in members(&self.document, copy) { + if let Some(id) = self.document.get_entity(handle).and_then(element_id) { + set_entity_tag(&mut self.document, handle, element_tag(id, handle)); + } + } + let mut tag = ExtendedDataRecord::new(REPRESENTATION_TAG); + tag.add_value(XDataValue::Integer16(1)); + tag.add_value(XDataValue::Handle(definition)); + self.document.set_object_xdata_record(copy, tag); + self.document.ensure_extension_dictionary(copy); + + // The reference: AcDbBlockRepresentation / AcDbRepData names the + // definition it was evaluated from. + let holder = self.document.ensure_extension_dictionary(insert); + let representation = self.document.allocate_handle(); + let data = self.document.allocate_handle(); + let mut dictionary = codec::objects::Dictionary::new(); + dictionary.handle = representation; + dictionary.owner = holder; + dictionary.reactors.push(holder); + dictionary.add_entry("AcDbRepData", data); + self.document + .objects + .insert(representation, ObjectType::Dictionary(dictionary)); + if let Some(ObjectType::Dictionary(holder)) = self.document.objects.get_mut(&holder) { + holder.add_entry("AcDbBlockRepresentation", representation); + } + let mut link = dynamic( + data, + representation, + "ACDB_BLOCKREPRESENTATION_DATA", + "AcDbBlockRepresentationData", + DynamicBlockData::Representation(BlockRepresentationData { + flags: 1, + block: definition, + }), + ); + link.reactors.push(representation); + self.document + .objects + .insert(data, ObjectType::DynamicBlock(link)); + self.document.block_representations.insert(data, definition); + if let Some(EntityType::Insert(reference)) = self.document.get_entity_mut(insert) { + reference.block_name = name.clone(); + } + for record in self.document.block_records.iter_mut() { + if record.handle == definition { + record.insert_handles.retain(|handle| *handle != insert); + } else if record.handle == copy { + record.insert_handles.push(insert); + } + } + self.bump_geometry(); + Some(name) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use codec::entities::{Circle, Insert, Line}; + + /// A block given visibility states round-trips them, and a reference + /// given its own copy still finds the definition and its states. + #[test] + fn states_and_a_reference_copy_survive_dwg_and_dxf() { + let mut scene = Scene::new(); + scene.define_block_raw( + "door", + Vector3::ZERO, + vec![ + EntityType::Line(Line::from_coords(0.0, 0.0, 0.0, 1.0, 0.0, 0.0)), + EntityType::Circle(Circle::from_center_radius(Vector3::new(0.5, 0.5, 0.0), 0.25)), + ], + ); + let definition = scene.document.block_records.get("door").unwrap().handle; + let parameter = create(&mut scene.document, definition, Vector3::ZERO).unwrap(); + let open = add_state(&mut scene.document, parameter, "Open", 0).unwrap(); + let circle = members(&scene.document, definition)[1]; + set_visible(&mut scene.document, parameter, open, &[circle], false); + finish_editing(&mut scene.document, definition); + + let insert = scene.add_entity(EntityType::Insert(Insert::new("door", Vector3::ZERO))); + let copy = scene.visibility_representation(insert).expect("a copy"); + assert!(copy.starts_with("*U")); + + for ext in ["dwg", "dxf"] { + let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version) + .unwrap_or_else(|error| panic!("save {ext}: {error}")); + let read = crate::io::load_bytes(&format!("door.{ext}"), bytes) + .unwrap_or_else(|error| panic!("load {ext}: {error}")); + let insert = read + .entities() + .find_map(|entity| match entity { + EntityType::Insert(insert) if insert.block_name.starts_with("*U") => { + Some(insert.common.handle) + } + _ => None, + }) + .unwrap_or_else(|| panic!("{ext}: reference to the copy")); + let (found, parameter) = read + .dynamic_visibility_for_insert(insert) + .unwrap_or_else(|| panic!("{ext}: no visibility parameter")); + let record = read.block_records.iter().find(|r| r.handle == found).unwrap(); + assert_eq!(record.name, "door", "{ext}"); + let names: Vec<&str> = parameter.states.iter().map(|s| s.name.as_str()).collect(); + assert_eq!(names, ["VisibilityState0", "Open"], "{ext}"); + assert_eq!(parameter.states[1].visible_blocks.len(), 0, "{ext}"); + } + } +} diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index a61ea9da5..5c1eb1826 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -1004,6 +1004,7 @@ impl Scene { } else { self.paper_sheet_wires_arc() }; + let wires = crate::scene::model::instance_model::expand_stubs(&wires); for wire in wires.iter() { let Some(handle) = Self::handle_from_wire_name(&wire.name) else { continue; diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 7e53ae706..2b8e3fba4 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -178,6 +178,7 @@ pub struct BlockCache { HashMap>>>>, >, clip_prototypes: std::sync::Mutex), u64>>, + cycle_warnings: std::sync::Mutex>, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -193,7 +194,10 @@ struct ExpansionPrototypeKey { #[derive(Debug)] struct CachedExpansion { translation: [f64; 3], + /// The wires every further reference repeats one by one. wires: Arc>, + /// The part a further reference carries as one stub wire, and its XY box. + stubbed: Option<(Arc>, [f32; 4])>, } impl BlockCache { @@ -219,6 +223,22 @@ impl BlockCache { Self::default() } + pub fn record_cycle_warning(&self, block_name: &str) -> bool { + let mut warnings = self + .cycle_warnings + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + warnings.insert(block_name.to_ascii_uppercase()) + } + + pub fn drain_cycle_warnings(&self) -> Vec { + let mut warnings = self + .cycle_warnings + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + warnings.drain().collect() + } + pub fn defn(&self, block_name: &str) -> Option<&Arc> { self.defns.get(block_name).or_else(|| { self.defns @@ -1038,7 +1058,20 @@ pub fn expand_insert( scale_policy, ); let name = ins_handle.value().to_string(); - let prototype_key = if !ins.is_array() && cache.prototype_blocks.contains(&ins.block_name) { + // Mask-bearing blocks must not reuse translated GPU prototypes: ordinary + // strokes and wipeout fills have different upload paths. Their shared + // source_id plus per-copy translated geometry can make a moved INSERT + // display stale wipeout geometry or disappear as the view changes. + // Expand these from their definition until both GPU paths share a + // verified identical instancing convention. + let has_masks = defn.subs.iter().any(|sub| match sub { + LocalSub::Wire(wire) => wire.canvas_color, + LocalSub::Nested(_) => false, + }); + let prototype_key = if !ins.is_array() + && !has_masks + && cache.prototype_blocks.contains(&ins.block_name) + { Some(expansion_prototype_key( ins, &xform, @@ -1083,16 +1116,26 @@ pub fn expand_insert( translation[1] - cached.translation[1], translation[2] - cached.translation[2], ]; - return Some( - cached - .wires - .iter() - .map(|wire| translated_prototype_wire(wire, &name, delta)) - .collect(), - ); + let mut wires: Vec = cached + .wires + .iter() + .map(|wire| translated_prototype_wire(wire, &name, delta)) + .collect(); + if let Some((prototype, aabb)) = &cached.stubbed { + let mut stub = crate::scene::model::instance_model::stub_wire( + Arc::clone(prototype), + *aabb, + delta, + 0.0, + ); + stub.name = name.to_string(); + stub.selected = selected; + wires.push(stub); + } + return Some(wires); } let mut batches = Batches::default(); - let mut visited: Vec = Vec::with_capacity(8); + let mut visited: Vec = vec![ins.block_name.clone()]; let offsets = crate::scene::render_graph::array_offsets(ins); let cell_xform = |offset: &[f64; 3]| { @@ -1213,9 +1256,20 @@ pub fn expand_insert( }); } } + // Further references carry the block-drawn part as one stub instead + // of a wire per primitive (#1698). + let (stubbed, repeated): (Vec<_>, Vec<_>) = wires + .iter() + .cloned() + .partition(crate::scene::model::instance_model::stubbable); + let stubbed = (!stubbed.is_empty()).then(|| { + let aabb = crate::scene::model::instance_model::prototype_aabb(&stubbed); + (Arc::new(stubbed), aabb) + }); let cached = Arc::new(CachedExpansion { translation, - wires: Arc::new(wires.clone()), + wires: Arc::new(repeated), + stubbed, }); **guard = Some(cached); } @@ -1282,9 +1336,24 @@ fn expansion_prototype_key( } } -fn translated_prototype_wire(source: &WireModel, name: &str, delta: [f64; 3]) -> WireModel { +pub(crate) fn translated_prototype_wire( + source: &WireModel, + name: &str, + delta: [f64; 3], +) -> WireModel { let mut wire = source.clone(); wire.name = name.to_string(); + if let Some(stub) = &source.stub { + wire.stub = Some(Arc::new(crate::scene::model::instance_model::InstanceStub { + prototype: Arc::clone(&stub.prototype), + delta: [ + stub.delta[0] + delta[0], + stub.delta[1] + delta[1], + stub.delta[2] + delta[2], + ], + depth_delta: stub.depth_delta, + })); + } if let Some(instance) = wire.render_instance.as_mut() { for axis in 0..3 { instance.translation[axis] += delta[axis]; @@ -1462,6 +1531,29 @@ pub(crate) fn fade_toward_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] { ] } +/// XFADECTL: how far objects outside the REFEDIT working set fade toward the +/// background, in percent (0โ€“90, default 70). +static REFEDIT_FADE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(70); + +pub fn refedit_fade_ctl() -> i32 { + REFEDIT_FADE.load(std::sync::atomic::Ordering::Relaxed) +} + +pub fn set_refedit_fade_ctl(value: i32) { + REFEDIT_FADE.store(value.clamp(0, 90), std::sync::atomic::Ordering::Relaxed); +} + +/// A colour outside the REFEDIT working set, faded by XFADECTL. +pub(crate) fn refedit_fade(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] { + let t = refedit_fade_ctl().clamp(0, 90) as f32 / 100.0; + [ + color[0] * (1.0 - t) + bg[0] * t, + color[1] * (1.0 - t) + bg[1] * t, + color[2] * (1.0 - t) + bg[2] * t, + color[3], + ] +} + /// XDWGFADECTL: how far referenced drawings fade toward the background, in /// percent (0โ€“90); zero or negative shows them unfaded. static XREF_FADE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(50); @@ -1593,7 +1685,10 @@ struct NestedPrototypeKey { struct NestedPrototype { translation: [f64; 3], depth_base: f32, + /// The wires every further copy repeats one by one. wires: Arc>, + /// The part a further copy carries as one stub wire, and its XY box. + stubbed: Option<(Arc>, [f32; 4])>, } /// Hard cap on point count for a single batched WireModel. Above this the @@ -1749,6 +1844,7 @@ impl Batches { fill_is_3d: b.fill_is_3d, fill_is_2d_solid: b.fill_is_2d_solid, render_instance: None, + stub: None, pick_tris: b.pick_tris, pick_tris_low: b.pick_tris_low, dash_from_start: false, @@ -1939,7 +2035,16 @@ fn expand_defn( emit_wire(attachment, &attachment_transform, &inner_ctx, out, d_range); } } - if visited.iter().any(|n| n == &nref.block_name) { + if visited + .iter() + .any(|n| n.eq_ignore_ascii_case(&nref.block_name)) + { + if ctx.cache.record_cycle_warning(&nref.block_name) { + eprintln!( + "Warning: circular block reference detected for block \"{}\"; cutting cycle", + nref.block_name + ); + } continue; } let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else { @@ -2074,11 +2179,22 @@ fn expand_defn( let depth_delta = nested_range.0 - cached.depth_base; out.extra_wires.extend(cached.wires.iter().map(|wire| { let mut wire = translated_prototype_wire(wire, "", delta); - if let Some(depth) = wire.depth_override.as_mut() { - *depth += depth_delta; - } + crate::scene::model::instance_model::shift_depth( + &mut wire, + depth_delta, + ); wire })); + if let Some((prototype, aabb)) = &cached.stubbed { + out.extra_wires.push( + crate::scene::model::instance_model::stub_wire( + Arc::clone(prototype), + *aabb, + delta, + depth_delta, + ), + ); + } } else { let mut sub = Batches::default(); expand_defn( @@ -2106,12 +2222,23 @@ fn expand_defn( } } out.extra_wires.extend(wires.iter().cloned()); + // Further copies carry the block-drawn part as one + // stub instead of a wire per primitive (#1698). + let (stubbed, repeated): (Vec<_>, Vec<_>) = wires + .into_iter() + .partition(crate::scene::model::instance_model::stubbable); + let stubbed = (!stubbed.is_empty()).then(|| { + let aabb = + crate::scene::model::instance_model::prototype_aabb(&stubbed); + (Arc::new(stubbed), aabb) + }); out.nested_prototypes.insert( key, NestedPrototype { translation, depth_base: nested_range.0, - wires: Arc::new(wires), + wires: Arc::new(repeated), + stubbed, }, ); } @@ -2618,8 +2745,13 @@ fn emit_wire( // MTEXT background (a dimension's DIMTFILL box) left at the bare insert // level sat in front of its own text whenever that text's rank was // below zero. - let local_depth = (lw.world_width > 0.0 || lw.is_fill_only || !lw.text_verts.is_empty()) - .then(|| d_range.0 + lw.local_rank * d_range.1); + // Every block member must retain its block-local draw rank, including + // ordinary thin LINE/LWPOLYLINE wires. Previously these wires had no + // depth_override, so they were drawn at the INSERT's bare depth while + // wipeout/hatch fills were positioned at the member rank. COPY and grip + // moves rebuild instance batches and exposed this mismatch in model, + // paper-space and PDF output. + let local_depth = Some(d_range.0 + lw.local_rank * d_range.1); let plot_visible = ctx.plot_visible && lw.plot_visible && (!lw.plot_l0 || ctx.l0_plottable); // Batched wires share one dash distance, which runs on across the NaN @@ -3466,4 +3598,97 @@ mod metrics_depth_tests { assert_ne!(root.metrics.aabb_local, [0.0; 4]); assert_eq!(root.metrics.aabb_local, tail.metrics.aabb_local); } + + #[test] + fn locked_cyclic_block_document_renders_without_hanging_and_warns_once() { + // Document built in code with pre-existing cycle: + // A contains B, B contains A + let mut document = CadDocument::new(); + let blk_a = add_block(&mut document, "CYC_A"); + let blk_b = add_block(&mut document, "CYC_B"); + + add_owned( + &mut document, + blk_a, + EntityType::Insert(Insert::new("CYC_B", Vector3::ZERO)), + ); + add_owned( + &mut document, + blk_b, + EntityType::Insert(Insert::new("CYC_A", Vector3::ZERO)), + ); + // Add a line in A so there is some actual geometry + add_owned( + &mut document, + blk_a, + EntityType::Line(Line::from_points( + Vector3::ZERO, + Vector3::new(10.0, 0.0, 0.0), + )), + ); + + // Model space references CYC_A + document + .add_entity(EntityType::Insert(Insert::new("CYC_A", Vector3::ZERO))) + .unwrap(); + + // Building the cache computes defns and metrics without hanging + let cache = BlockCache::build( + &document, + 1.0, + None, + true, + [0.0, 0.0, 0.0, 1.0], + None, + &HashMap::default(), + ); + + // Verify defns exist + assert!(cache.defns.contains_key("CYC_A")); + assert!(cache.defns.contains_key("CYC_B")); + + // Expanding / tessellating into wires does not hang and cuts the cycle + let ins = Insert::new("CYC_A", Vector3::ZERO); + let inherit_style = crate::scene::view::render::InheritStyle { + color: [1.0, 1.0, 1.0, 1.0], + pat_len: 1.0, + pat: [0.0; 8], + lw_px: 1.0, + }; + + let result = expand_insert( + &document, + &cache, + &ins, + Handle::new(1), + [1.0, 1.0, 1.0, 1.0], + 7, + 1.0, + [0.0; 8], + 1.0, + inherit_style, + 7, + true, + false, + 1.0, + None, + None, + false, + [0.0, 0.0, 0.0, 1.0], + 1.0, + crate::scene::BlockScalePolicy::FromInsert, + false, + ); + + assert!(result.is_some()); + + // Warning was recorded once + let warnings = cache.drain_cycle_warnings(); + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0], "CYC_A"); + + // Subsequent expansion does not warn again because warned set retains it + let warnings_after = cache.drain_cycle_warnings(); + assert!(warnings_after.is_empty()); + } } diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index d437bfac7..5e5b87628 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -214,8 +214,80 @@ impl Scene { /// Restore camera to a named view from the document view table. pub fn restore_named_view(&mut self, view: &codec::tables::View) { - use glam::Vec3; - let cam = &mut *self.camera.borrow_mut(); + let before = { + let cam = self.camera.borrow(); + (cam.rotation, cam.target, cam.distance) + }; + self.restore_named_view_camera(view); + let unchanged = { + let cam = self.camera.borrow(); + cam.rotation.abs_diff_eq(before.0, 1e-6) + && cam.target.abs_diff_eq(before.1, 1e-6 * before.1.length().max(1.0)) + && (cam.distance - before.2).abs() <= 1e-6 * before.2.abs().max(1.0) + }; + // The model viewport takes a copy of the view's background; a view + // without one clears it, unless the view is what the viewport already + // shows (the reference leaves the viewport as it is then). + if self.current_layout != "Model" && self.active_viewport.is_none() && view.paper_space { + self.set_sheet_background(view); + } + if self.current_layout == "Model" { + if view.background_handle.is_valid() || !unchanged { + let background = copy_background(&mut self.document, view.background_handle); + self.set_active_background(background); + } + if view.visual_style_handle.is_valid() { + if let Some(handle) = self.active_tile_vport_handle() { + if let Some(value) = self.document.vports.iter_mut().find(|v| v.handle == handle) { + value.visual_style_handle = view.visual_style_handle; + } + } + } + } + } + + fn restore_named_view_camera(&mut self, view: &codec::tables::View) { + named_view_camera(view, &mut self.camera.borrow_mut()); + self.camera_generation += 1; + } + + /// Restores a model-space view into a paper-space viewport: its camera, + /// a copy of the view's background (or none) and the view's visual style. + pub fn restore_named_view_in_viewport(&mut self, view: &codec::tables::View, viewport: Handle) { + let mut camera = self.navigation_camera(); + named_view_camera(view, &mut camera); + self.apply_navigation_camera(camera); + let background = copy_background(&mut self.document, view.background_handle); + let mut old = Handle::NULL; + if let Some(EntityType::Viewport(value)) = self.document.get_entity_mut(viewport) { + old = std::mem::replace(&mut value.background_handle, background); + if view.visual_style_handle.is_valid() { + value.visual_style_handle = view.visual_style_handle; + } + } + erase_background(&mut self.document, old); + self.notify_viewport_changed(viewport); + self.camera_generation += 1; + } + + /// The paper-space viewport of the current layout takes a copy of a + /// paper-space view's background (kept with the layout; the sheet itself + /// is not drawn over). + fn set_sheet_background(&mut self, view: &codec::tables::View) { + let sheet = self.current_layout_sheet_viewport_handle(); + let background = copy_background(&mut self.document, view.background_handle); + let old = match self.document.get_entity_mut(sheet) { + Some(EntityType::Viewport(value)) => std::mem::replace(&mut value.background_handle, background), + _ => background, + }; + erase_background(&mut self.document, old); + } +} + +/// Points a camera along a named view: direction, target and height. +fn named_view_camera(view: &codec::tables::View, cam: &mut view::camera::Camera) { + use glam::Vec3; + { // The stored direction points from the target toward the eye. let eye_dir = Vec3::new( view.direction.x as f32, @@ -242,7 +314,71 @@ impl Scene { } else { cam.distance }; + } +} + +impl Scene { + /// The background the active model-space viewport shows (null for the + /// drawing area colour). + pub fn active_background(&self) -> Handle { + if self.current_layout != "Model" { + // Inside a viewport, that viewport's; the sheet's is not saved + // with a paper-space view. + return match self.active_viewport.and_then(|handle| self.document.get_entity(handle)) { + Some(EntityType::Viewport(value)) => value.background_handle, + _ => Handle::NULL, + }; + } + self.active_tile_vport_handle() + .and_then(|handle| self.document.vports.iter().find(|value| value.handle == handle)) + .map_or(Handle::NULL, |value| value.background_handle) + } + + /// The visual style of the active Model tile's viewport record. + pub fn active_visual_style(&self) -> Handle { + if self.current_layout != "Model" { + return match self.active_viewport.and_then(|handle| self.document.get_entity(handle)) { + Some(EntityType::Viewport(value)) => value.visual_style_handle, + _ => Handle::NULL, + }; + } + self.active_tile_vport_handle() + .and_then(|handle| self.document.vports.iter().find(|value| value.handle == handle)) + .map_or(Handle::NULL, |value| value.visual_style_handle) + } + + /// Records the visual style named `name` on the active Model tile's + /// viewport record, when the drawing has it. + pub fn set_active_visual_style_named(&mut self, name: &str) { + let style = self.document.objects.iter().find_map(|(handle, object)| match object { + codec::objects::ObjectType::VisualStyle(style) if style.description == name => Some(*handle), + _ => None, + }); + let Some(style) = style else { return }; + if self.current_layout != "Model" { + if let Some(handle) = self.active_viewport { + if let Some(EntityType::Viewport(value)) = self.document.get_entity_mut(handle) { + value.visual_style_handle = style; + } + } + return; + } + let Some(handle) = self.active_tile_vport_handle() else { return }; + if let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) { + value.visual_style_handle = style; + } + } + + fn set_active_background(&mut self, background: Handle) { + let Some(handle) = self.active_tile_vport_handle() else { return }; + let Some(value) = self.document.vports.iter_mut().find(|value| value.handle == handle) else { + return; + }; + let old = std::mem::replace(&mut value.background_handle, background); self.camera_generation += 1; + if old != background { + erase_background(&mut self.document, old); + } } /// Save the current camera state into a new named view entry. @@ -1242,7 +1378,20 @@ impl Scene { // Every paper layout has a full-screen sheet viewport that holds // its view; create one if a loaded file lacks it. let layout = self.current_layout.clone(); + // A layout opening for the first time, as the reference tells + // it: its page setup was never initialized (a file's layout + // with no paper), or it was created in this session and not + // opened yet. Its page setup is initialized and, with + // LAYOUTCREATEVIEWPORT, it gets its first viewport. + let unopened = self.unopened_layouts.remove(&layout); + let initialized = self.initialize_layout_page_setup(&layout); + if unopened && !initialized { + self.fit_layout_sheet(&layout); + } self.ensure_sheet_viewport(&layout); + if unopened || initialized { + self.add_layout_default_viewport(&layout); + } self.apply_sheet_viewport_camera() }; if restored { @@ -1638,3 +1787,100 @@ impl Scene { } } } + +/// The drawing's ACAD_BACKGROUND dictionary, made when it has none. +fn background_dictionary(document: &mut codec::CadDocument) -> Option { + use codec::objects::{Dictionary, ObjectType}; + let root = document.header.named_objects_dict_handle; + let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get(&root) else { + return None; + }; + if let Some(existing) = root_dictionary.get("ACAD_BACKGROUND") { + if matches!(document.objects.get(&existing), Some(ObjectType::Dictionary(_))) { + return Some(existing); + } + } + let handle = document.allocate_handle(); + let mut dictionary = Dictionary::new(); + dictionary.handle = handle; + dictionary.owner = root; + dictionary.reactors.push(root); + document.objects.insert(handle, ObjectType::Dictionary(dictionary)); + if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) { + root_dictionary.add_entry("ACAD_BACKGROUND", handle); + } + Some(handle) +} + +/// A new background object in ACAD_BACKGROUND, keyed by its own handle as +/// the reference keys it. Null when the drawing has no named-objects root. +pub fn add_background( + document: &mut codec::CadDocument, + data: codec::objects::ClassObjectData, +) -> Handle { + use codec::objects::{ClassObject, ObjectType}; + let Some(dictionary) = background_dictionary(document) else { + return Handle::NULL; + }; + let handle = document.allocate_handle(); + let mut object = ClassObject::new(data); + object.handle = handle; + object.owner = dictionary; + object.reactors = vec![dictionary]; + document.objects.insert(handle, ObjectType::ClassObject(object)); + if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) { + entries.add_entry(format!("{:X}", handle.value()), handle); + } + handle +} + +/// Erases a background object once no view or viewport refers to it (the +/// reference erases a view's background when it is replaced, cleared or the +/// view deleted, and a viewport's when the viewport takes another). +pub fn erase_background(document: &mut codec::CadDocument, handle: Handle) { + use codec::objects::{ClassObjectData as D, ObjectType}; + if !handle.is_valid() + || document.views.iter().any(|view| view.background_handle == handle) + || document.vports.iter().any(|vport| vport.background_handle == handle) + || document.entities().any(|entity| { + matches!(entity, codec::EntityType::Viewport(viewport) if viewport.background_handle == handle) + }) + { + return; + } + let owner = match document.objects.get(&handle) { + Some(ObjectType::ClassObject(object)) + if matches!( + object.data, + D::SolidBackground(_) | D::GradientBackground(_) | D::ImageBackground(_) + | D::GroundPlaneBackground(_) | D::SkyLightBackground(_) | D::IblBackground(_) + ) => + { + object.owner + } + _ => return, + }; + document.objects.remove(&handle); + if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&owner) { + entries.entries.retain(|(_, entry)| *entry != handle); + } +} + +/// A copy of a background object (the reference gives each view and +/// viewport its own); null for none. +pub fn copy_background(document: &mut codec::CadDocument, handle: Handle) -> Handle { + use codec::objects::{ClassObjectData as D, ObjectType}; + match document.objects.get(&handle) { + Some(ObjectType::ClassObject(object)) + if matches!( + object.data, + D::SolidBackground(_) | D::GradientBackground(_) | D::ImageBackground(_) + | D::GroundPlaneBackground(_) | D::SkyLightBackground(_) | D::IblBackground(_) + ) => + { + let data = object.data.clone(); + add_background(document, data) + } + _ => Handle::NULL, + } +} diff --git a/src/scene/compare.rs b/src/scene/compare.rs new file mode 100644 index 000000000..dd18ef101 --- /dev/null +++ b/src/scene/compare.rs @@ -0,0 +1,151 @@ +//! COMPARE: what differs between this drawing and another one. Objects are +//! matched by what they draw โ€” type, layer and their tessellated geometry โ€” +//! so a copy that kept its geometry matches whatever its handle, and an +//! object that moved or changed shows as removed in one place and added in +//! the other. + +use codec::{CadDocument, Handle}; +use glam::DVec3; +use rustc_hash::FxHashMap; + +use crate::scene::{Scene, WireModel}; + +/// The result of a comparison, ready to draw over the current drawing. +pub struct Comparison { + /// Objects only this drawing has. + pub added: usize, + /// Objects only the other drawing has. + pub removed: usize, + /// Their outlines: added in green, removed in red. + pub overlay: Vec, + /// Each difference's extent, in reading order, to step through. + pub spots: Vec<(DVec3, DVec3)>, +} + +const ADDED: [f32; 4] = [0.25, 0.85, 0.35, 1.0]; +const REMOVED: [f32; 4] = [0.95, 0.3, 0.25, 1.0]; + +fn wires_by_entity(scene: &Scene) -> FxHashMap> { + let mut map: FxHashMap> = FxHashMap::default(); + for wire in scene.entity_wires().iter() { + if let Some(handle) = Scene::handle_from_wire_name(&wire.name) { + map.entry(handle).or_default().push(wire.clone()); + } + } + map +} + +fn point(wire: &WireModel, index: usize) -> DVec3 { + let high = wire.points[index]; + let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]); + DVec3::new( + f64::from(high[0]) + f64::from(low[0]), + f64::from(high[1]) + f64::from(low[1]), + f64::from(high[2]) + f64::from(low[2]), + ) +} + +/// What an object draws, as a comparable key: its type, its layer and its +/// outline points rounded to 1e-4 drawing units. +fn signature(document: &CadDocument, handle: Handle, wires: &[WireModel]) -> String { + let mut key = match document.get_entity(handle) { + Some(entity) => format!( + "{}|{}", + crate::entities::names::ui_name(entity), + entity.common().layer + ), + None => String::new(), + }; + for wire in wires { + key.push('#'); + for index in 0..wire.points.len() { + let p = point(wire, index); + if !p.is_finite() { + key.push('/'); + continue; + } + let q = (p * 1e4).round(); + key.push_str(&format!("{},{},{};", q.x, q.y, q.z)); + } + } + key +} + +fn extent(wires: &[WireModel]) -> Option<(DVec3, DVec3)> { + let mut bounds: Option<(DVec3, DVec3)> = None; + for wire in wires { + for index in 0..wire.points.len() { + let p = point(wire, index); + if !p.is_finite() { + continue; + } + bounds = Some(match bounds { + None => (p, p), + Some((min, max)) => (min.min(p), max.max(p)), + }); + } + } + bounds +} + +/// Compare `current` with the drawing `other`, in the space `current` +/// shows. +pub fn compare(current: &Scene, other: CadDocument) -> Comparison { + let mut other_scene = Scene::new(); + other_scene.document = other; + other_scene.bump_geometry(); + let here = wires_by_entity(current); + let there = wires_by_entity(&other_scene); + + let mut unmatched: FxHashMap> = FxHashMap::default(); + for (handle, wires) in &there { + unmatched + .entry(signature(&other_scene.document, *handle, wires)) + .or_default() + .push(*handle); + } + let mut overlay = Vec::new(); + let mut spots = Vec::new(); + let mut added = 0; + let mut mark = |wires: &[WireModel], color: [f32; 4]| { + if let Some(bounds) = extent(wires) { + spots.push(bounds); + } + overlay.extend(wires.iter().cloned().map(|mut wire| { + wire.color = color; + wire.selected = false; + wire.line_weight_px = wire.line_weight_px.max(2.0); + wire + })); + }; + for (handle, wires) in &here { + let key = signature(¤t.document, *handle, wires); + match unmatched.get_mut(&key).and_then(Vec::pop) { + Some(_) => {} + None => { + added += 1; + mark(wires, ADDED); + } + } + } + let mut removed = 0; + for handle in unmatched.into_values().flatten() { + if let Some(wires) = there.get(&handle) { + removed += 1; + mark(wires, REMOVED); + } + } + // Reading order: top to bottom, then left to right. + spots.sort_by(|a, b| { + b.1.y + .partial_cmp(&a.1.y) + .unwrap_or(std::cmp::Ordering::Equal) + .then(a.0.x.partial_cmp(&b.0.x).unwrap_or(std::cmp::Ordering::Equal)) + }); + Comparison { + added, + removed, + overlay, + spots, + } +} diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index ff9e2b0cb..24d6c8ab8 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1812,6 +1812,7 @@ pub fn tessellate( fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, + stub: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, @@ -1977,6 +1978,7 @@ pub fn tessellate( fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, + stub: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index d28c28c6e..80ffa9a55 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -1200,8 +1200,18 @@ impl Allocator<'_> { }, class_version: 0, enabled: true, - // Point and curve addressing lives on constraint-node data. - persistent_subent: AssocPersistentSubentId::default(), + // Point and curve addressing lives on constraint-node data; + // the dependency names the whole curve, as single-edge + // subentities do in authored constraint groups. An empty id + // (no class) crashes strict readers loading the drawing. + // ponytail: polyline segments get the single-edge id too; + // authored files address them with an index subentity (class + // code 3) whose fields are not decoded yet. + persistent_subent: AssocPersistentSubentId { + class_name: "AcDbAssocSingleEdgePersSubentId".to_string(), + class_code: 1, + ..Default::default() + }, }), ) } @@ -2479,9 +2489,15 @@ fn detach_preserved_scope( Some(ObjectType::Associative(object)) => match &object.data { AssociativeData::ConstraintGroup(_) | AssociativeData::Network(_) => false, AssociativeData::Variable(variable) => variable.name.trim().is_empty(), - // A surface action whose surface was erased goes with it. - AssociativeData::Action(_) => action_surface(document, *handle) - .is_none_or(|surface| document.get_entity(surface).is_some()), + // A surface action whose surface was erased goes with it, + // and an array action whose array reference was. + AssociativeData::Action(_) => { + action_surface(document, *handle) + .is_none_or(|surface| document.get_entity(surface).is_some()) + && !crate::scene::assoc_array::orphaned_array_action( + document, *handle, + ) + } _ => true, }, _ => false, diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 6d49abf79..741400e9d 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1263,19 +1263,35 @@ impl Scene { /// Replace the contents and base point of block `name`, defining it when /// absent. References keep pointing at it and show the new contents. + /// Refuses and returns Err if the new contents lead back to `name` directly or transitively. pub fn redefine_block_raw( &mut self, name: &str, base_point: codec::types::Vector3, entities: Vec, - ) { + ) -> Result<(), String> { + for entity in &entities { + if let EntityType::Insert(ins) = entity { + if crate::scene::block_service::would_create_cycle( + &self.document, + name, + &ins.block_name, + ) { + return Err(crate::scene::block_service::circular_dependency_error( + name, + &ins.block_name, + )); + } + } + } let Some((br_handle, block_handle)) = self .document .block_records .get(name) .map(|br| (br.handle, br.block_entity_handle)) else { - return self.define_block_raw(name, base_point, entities); + self.define_block_raw(name, base_point, entities); + return Ok(()); }; let owned: Vec = self .document @@ -1299,6 +1315,7 @@ impl Scene { let _ = self.document.add_entity(entity); } self.bump_geometry(); + Ok(()) } pub(super) fn synced_hatch_models( @@ -2228,7 +2245,14 @@ impl Scene { .colors .get(i) .and_then(|e| e.color.rgb()) - .map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, color[3]]) + .map(|(r, g, b)| { + [ + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + color[3], + ] + }) }; gradient_color1 = stop(0); let color1 = gradient_color1.unwrap_or(color); @@ -3090,10 +3114,15 @@ impl Scene { // The name alone leaves the header pointing at the previous style, and // a DWG writes the current style by handle only. self.document.header.current_dimstyle_handle = iso; + // The annotation multiplier depends on those units; Scene::new() + // left it at the unitless 1, which drew annotative objects 1000x too + // large until the first CANNOSCALE change shrank them. (#1674) + self.sync_annotation_scale_from_header(); for obj in self.document.objects.values_mut() { if let codec::objects::ObjectType::Layout(l) = obj { if l.name != "Model" { crate::scene::apply_default_page_setup(l, ""); + self.unopened_layouts.insert(l.name.clone()); } } } diff --git a/src/scene/layout.rs b/src/scene/layout.rs index e61291440..765683c04 100644 --- a/src/scene/layout.rs +++ b/src/scene/layout.rs @@ -24,6 +24,10 @@ impl Scene { /// cached layout-name list is invalidated on insert. pub fn add_layout(&mut self, name: &str) -> codec::Result { let handle = self.document.add_layout(name)?; + // Opening it fits the sheet the document made for it (placed at the + // origin) to its page and, with LAYOUTCREATEVIEWPORT, gives it its + // first viewport; saved before that, it is written with none. + self.unopened_layouts.insert(name.to_string()); self.bump_layout_epoch(); Ok(handle) } @@ -34,6 +38,9 @@ impl Scene { if let ObjectType::Layout(l) = obj { if l.name == old_name { l.name = new_name.to_string(); + if self.unopened_layouts.remove(old_name) { + self.unopened_layouts.insert(new_name.to_string()); + } self.bump_layout_epoch(); return; } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 978a780b6..3a137a715 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -6,6 +6,11 @@ // view โ€” camera, transforms, viewport, render pipeline driver // cache โ€” block-definition and property caches pub mod annotative; +pub mod assoc_array; +pub mod block_visibility; +pub mod compare; +pub mod section_cull; +pub mod sub_select; pub mod block_service; pub mod cache; pub mod convert; @@ -31,6 +36,7 @@ pub enum BlockScalePolicy { mod block_base_point_tests; mod boundary; mod camera_ops; +pub use camera_ops::{add_background, copy_background, erase_background}; pub(crate) mod centerline; pub(crate) mod centermark; pub(crate) mod dimension_assoc; @@ -881,12 +887,78 @@ pub(crate) struct ResidentWireLayout { marker_start: usize, } +/// One memoized entity tessellation. +#[derive(Clone)] +pub(crate) enum MemoRun { + /// The wires themselves: an entity with no run in the backing set + /// (hidden, or tessellated after the set was built). + Owned(Arc>), + /// Still valid, and the same wires the backing resident set holds for the + /// entity, so they are kept once instead of twice (#1542). + InSet, + /// Tessellated into the resident set being built, which becomes the + /// backing when it is done; until then the run reads as a miss. + Pending, +} + +/// The resident set `MemoRun::InSet` runs live in. Held weakly so the set +/// stays uniquely owned and can be patched in place; `keep` holds a set an +/// abandoned patch took apart until the following full build has read it. +#[derive(Clone, Default)] +pub(crate) struct MemoBacking { + set: std::sync::Weak>, + ranges: HashMap, + keep: Option>>, +} + +/// A memoized run resolved to its wires. +enum MemoHit { + Owned(Arc>), + Slice(Arc>, usize, usize), +} + +impl MemoHit { + fn wires(&self) -> &[WireModel] { + match self { + MemoHit::Owned(wires) => wires, + MemoHit::Slice(set, start, len) => &set[*start..*start + *len], + } + } +} + +/// Hands the set an abandoned resident patch was taking apart back to the +/// memo, whose `InSet` runs still read from it. +struct ReclaimOnDecline<'a> { + scene: &'a Scene, + wires: Option>, + backed: bool, +} + +impl Drop for ReclaimOnDecline<'_> { + fn drop(&mut self) { + if let Some(wires) = self.wires.take() { + if self.backed { + self.scene.keep_memo_backing(wires); + } + } + } +} + +/// Kill-switch: `OCS_NO_RESIDENT_MEMO` reverts the resident set to the old +/// full re-tessellation on every edit, in case a mutation site is ever found +/// that edits geometry without dropping its handle from the memo. +fn resident_memo_enabled() -> bool { + static EN: std::sync::OnceLock = std::sync::OnceLock::new(); + *EN.get_or_init(|| std::env::var("OCS_NO_RESIDENT_MEMO").is_err()) +} + #[derive(Clone)] pub struct PreparedOpenGeometry { pub wires: Arc>, pub interaction_index: Option>, /// Loader tessellations and their guard; a state mismatch clears the memo. - pub tess_memo: HashMap>>, + pub(crate) tess_memo: HashMap, + pub(crate) tess_memo_backing: MemoBacking, pub tess_guard: u64, /// The loader's submission-order layout for `wires`. /// @@ -1447,14 +1519,7 @@ pub fn prepare_open_geometry( scene.local_extent_max = caches.local_extent_max; scene.local_center = caches.local_center; scene.bg_color = model_bg; - let cannoscale_value = scene.document.header.annotation_scale_value; - let unit_factor = scene.annotation_scale_unit_factor(); - - scene.annotation_scale = if cannoscale_value > 1e-9 { - ((1.0 / cannoscale_value) / unit_factor) as f32 - } else { - (1.0 / unit_factor) as f32 - }; + scene.sync_annotation_scale_from_header(); scene.current_layout = "Model".to_string(); let camera = scene.camera.borrow().clone(); let perf = crate::perf::enabled(); @@ -1489,6 +1554,7 @@ pub fn prepare_open_geometry( } // Taken, not cloned: the scene is discarded on the next line. let tess_memo = std::mem::take(&mut *scene.resident_tess_memo.borrow_mut()); + let tess_memo_backing = std::mem::take(&mut *scene.resident_memo_backing.borrow_mut()); let tess_guard = scene.resident_tess_guard.get(); let draw_depths = scene.draw_depth_cache.borrow_mut().take(); // The layout the loader already built for exactly these wires. Taken by @@ -1514,6 +1580,7 @@ pub fn prepare_open_geometry( wires, interaction_index, tess_memo, + tess_memo_backing, tess_guard, block_defns, draw_depths, @@ -2057,8 +2124,14 @@ pub struct Scene { /// frozen-layer signature. Invalidated only when a light entity changes; /// ordinary geometry edits never rescan the drawing's lights. lighting_cache: RefCell>>, + /// The visibility state each dynamic block shows in the Block Editor, + /// by block record. + pub visibility_states: HashMap, /// Currently selected entity handles. pub selected: HashSet, + /// Polylines only part of which is selected (Ctrl+click): the picked + /// segment indices. + pub sub_segments: HashMap>, selected_order: Vec, /// The parametric-constraint glyph currently selected in the viewport. /// Entity selection changes clear it so the two selections stay exclusive. @@ -2078,6 +2151,8 @@ pub struct Scene { /// edited geometry stands out while the surrounding drawing stays visible /// for context. `None` = not editing. (#136) pub refedit_keep: Option>, + /// REFEDIT locks the objects outside the working set (`refedit_keep`). + pub refedit_lock: bool, /// COUNT mode colours: counted references in COUNTCOLOR, overlapping /// duplicates in COUNTERRORCOLOR, everything else faded. `None` outside /// count mode. @@ -2363,6 +2438,12 @@ pub struct Scene { >, /// Bump on any Layout object insert/remove/rename/reorder. layout_epoch: u64, + /// Layouts created in this session and not opened yet: the first time + /// one opens it gets the LAYOUTCREATEVIEWPORT viewport. + pub(crate) unopened_layouts: std::collections::HashSet, + /// The plot style table a layout's page setup takes when it is + /// initialized on first opening. + pub(crate) new_layout_plot_style: String, layout_names_cache: RefCell>)>>, /// Bump on any annotation-scale list mutation. scale_epoch: u64, @@ -2541,12 +2622,17 @@ pub struct Scene { /// `tess_memo_guard` (tol / view / anno / offset / bg); a guard mismatch /// (zoom, layout, โ€ฆ) clears it. `bump_geometry` clears it (structural /// change); `mark_entity_dirty` drops one handle (incremental edit). - tess_memo: RefCell>>>, + tess_memo: RefCell>, /// Hash of the tessellation parameters `tess_memo` was built under. When /// the current call's parameters differ, the memo is stale and cleared. tess_memo_guard: std::cell::Cell, /// Camera-independent model tessellation, separate from the culled memo. - resident_tess_memo: RefCell>>>, + resident_tess_memo: RefCell>, + /// The resident set `resident_tess_memo`'s `InSet` runs read from. + resident_memo_backing: RefCell, + /// Set while `resident_wires_for` assembles a set that will become the + /// memo's backing; new runs then go straight into it (`Pending`). + building_resident_set: std::cell::Cell, /// Annotation, background, viewport and glyph-atlas signature. resident_tess_guard: std::cell::Cell, /// Per-mutation delta journal: which handles changed on each `geometry_epoch` @@ -2690,13 +2776,16 @@ impl Scene { last_created_dimension: None, object_data_cache: crate::entities::object_data::ObjectDataCache::default(), lighting_cache: RefCell::new(HashMap::default()), + visibility_states: HashMap::default(), selected: HashSet::default(), + sub_segments: HashMap::default(), selected_order: Vec::new(), selected_constraint: None, object_isolation: ObjectIsolationState::default(), preview_hidden: HashSet::default(), command_preview_hidden: HashSet::default(), refedit_keep: None, + refedit_lock: false, count_display: None, hover_highlight: None, constraint_hover_highlights: HashSet::default(), @@ -2775,6 +2864,8 @@ impl Scene { unindexable_cache: RefCell::new(None), layout_type_names_cache: RefCell::new(None), layout_epoch: 0, + unopened_layouts: Default::default(), + new_layout_plot_style: String::new(), layout_names_cache: RefCell::new(None), scale_epoch: 0, scale_picker_cache: RefCell::new(None), @@ -2817,6 +2908,8 @@ impl Scene { tess_memo: RefCell::new(HashMap::default()), tess_memo_guard: std::cell::Cell::new(0), resident_tess_memo: RefCell::new(HashMap::default()), + resident_memo_backing: RefCell::new(MemoBacking::default()), + building_resident_set: std::cell::Cell::new(false), resident_tess_guard: std::cell::Cell::new(0), geometry_deltas: RefCell::new(std::collections::VecDeque::new()), pending_removed_categories: RefCell::new(HashMap::default()), @@ -3007,6 +3100,7 @@ impl Scene { // the entities it touched instead of rebuilding every one of them. if !prepared.tess_memo.is_empty() { *self.resident_tess_memo.borrow_mut() = prepared.tess_memo; + *self.resident_memo_backing.borrow_mut() = prepared.tess_memo_backing; self.resident_tess_guard.set(prepared.tess_guard); } // Reuse the loader's draw-order map with this scene's epoch. @@ -3901,6 +3995,7 @@ impl Scene { // Structural change โ€” drop both per-entity tessellation memos. self.tess_memo.borrow_mut().clear(); self.resident_tess_memo.borrow_mut().clear(); + *self.resident_memo_backing.borrow_mut() = MemoBacking::default(); self.invalidate_dependency_index(); // Can't name the changed handles โ†’ force every journal consumer to rebuild. self.push_geometry_delta(epoch, Vec::new(), true); @@ -4254,7 +4349,7 @@ impl Scene { ); if let Some(keep) = &self.refedit_keep { if !keep.contains(&h) { - material.diffuse = crate::scene::cache::block_cache::fade_toward_bg( + material.diffuse = crate::scene::cache::block_cache::refedit_fade( material.diffuse, bg, ); @@ -4310,7 +4405,7 @@ impl Scene { .is_some_and(|keep| !keep.contains(&handle)) { material.diffuse = - crate::scene::cache::block_cache::fade_toward_bg(material.diffuse, bg); + crate::scene::cache::block_cache::refedit_fade(material.diffuse, bg); } if self.meshes.contains_key(&handle) { material.diffuse = self.count_color(handle, material.diffuse, bg); @@ -4420,7 +4515,14 @@ impl Scene { /// keep set (no-op when not editing). The geometry is untouched, so /// hit-testing still works on faded entities. In count mode the counted /// references and the duplicates take the count colours instead. - fn apply_refedit_fade(&self, wires: &mut [WireModel], bg: [f32; 4]) { + fn apply_refedit_fade(&self, wires: &mut Vec, bg: [f32; 4]) { + // Block copies kept as stubs draw in their prototype's colour; while + // copies are recoloured one by one they are expanded instead. + if (self.count_display.is_some() || self.refedit_keep.is_some()) + && wires.iter().any(|wire| wire.stub.is_some()) + { + *wires = crate::scene::model::instance_model::expand_stubs(wires).into_owned(); + } if let Some(count) = &self.count_display { for w in wires.iter_mut() { let (color, class) = match Self::handle_from_wire_name(&w.name) { @@ -4448,7 +4550,7 @@ impl Scene { let keep_bright = Self::handle_from_wire_name(&w.name).is_some_and(|h| keep.contains(&h)); if !keep_bright { - w.color = crate::scene::cache::block_cache::fade_toward_bg(w.color, bg); + w.color = crate::scene::cache::block_cache::refedit_fade(w.color, bg); } } } @@ -4559,6 +4661,46 @@ impl Scene { crate::io::set_saved_active_layout(&mut self.document, &self.current_layout); } + /// Record the entered viewport (or the sheet, in paper space) as the + /// current layout's last active viewport, which reopens it (#1670). + pub fn sync_active_viewport_to_document(&mut self) { + let Some(handle) = self.current_layout_object_handle() else { + return; + }; + let sheet = match self.document.objects.get(&handle) { + Some(ObjectType::Layout(layout)) => { + Self::layout_sheet_viewport_handle(&self.document, layout) + } + _ => return, + }; + let active = self.active_viewport.unwrap_or(sheet); + if let Some(ObjectType::Layout(layout)) = self.document.objects.get_mut(&handle) { + layout.viewport = active; + } + } + + /// The model viewport the current layout was saved with entered: its + /// last active viewport when that is an on viewport other than the sheet. + pub fn saved_entered_viewport(&self) -> Option { + let handle = self.current_layout_object_handle()?; + let Some(ObjectType::Layout(layout)) = self.document.objects.get(&handle) else { + return None; + }; + let viewport = layout.viewport; + let sheet = Self::layout_sheet_viewport_handle(&self.document, layout); + if viewport.is_null() || viewport == sheet { + return None; + } + match self.document.get_entity(viewport) { + Some(EntityType::Viewport(vp)) + if vp.common.owner_handle == layout.block_record && !vp.status.is_off => + { + Some(viewport) + } + _ => None, + } + } + pub(crate) fn layout_sheet_viewport_handle( document: &codec::CadDocument, layout: &codec::objects::Layout, @@ -4668,10 +4810,11 @@ impl Scene { .unwrap_or(Handle::NULL) } - /// Ensure a paper layout has its overall (`id == 1`) viewport. - pub fn ensure_sheet_viewport(&mut self, layout_name: &str) { + /// Ensure a paper layout has its overall (`id == 1`) viewport; true when + /// it had none (the layout opens for the first time). + pub fn ensure_sheet_viewport(&mut self, layout_name: &str) -> bool { if layout_name == "Model" { - return; + return false; } // Locate the layout and its paper limits. let info = self.document.objects.iter().find_map(|(h, obj)| { @@ -4689,10 +4832,10 @@ impl Scene { None }); let Some((layout_handle, block_record, sheet, min_lim, max_lim)) = info else { - return; + return false; }; if block_record.is_null() { - return; + return false; } if sheet.is_valid() @@ -4701,31 +4844,14 @@ impl Scene { Some(EntityType::Viewport(vp)) if vp.common.owner_handle == block_record ) { - return; + return false; } // Create the full-screen overall viewport covering the paper limits. - let pw = (max_lim.0 - min_lim.0).abs().max(1.0); - let ph = (max_lim.1 - min_lim.1).abs().max(1.0); let mut vp = codec::entities::Viewport::new(); vp.id = 1; vp.status = codec::entities::ViewportStatusFlags::default_on(); - // Paper-space center is an (x, y) point with z = 0. - vp.center = codec::types::Vector3::new( - (min_lim.0 + max_lim.0) / 2.0, - (min_lim.1 + max_lim.1) / 2.0, - 0.0, - ); - vp.width = pw; - vp.height = ph; - // Frame the full sheet with a small margin. - vp.view_target = codec::types::Vector3::new( - (min_lim.0 + max_lim.0) / 2.0, - (min_lim.1 + max_lim.1) / 2.0, - 0.0, - ); - vp.view_center = codec::types::Vector3::ZERO; - vp.view_height = ph * 1.1; + fit_sheet_viewport(&mut vp, min_lim, max_lim); if let Ok(handle) = self .document .add_entity_to_layout(EntityType::Viewport(vp), layout_name) @@ -4735,6 +4861,74 @@ impl Scene { l.viewports.insert(0, handle); } } + true + } + + /// Fit a layout's sheet viewport to its page. (Taking the sheet out + /// instead would leave a viewport onto the model looking like the sheet.) + pub fn fit_layout_sheet(&mut self, layout_name: &str) { + let layout = self.document.objects.values().find_map(|object| match object { + ObjectType::Layout(layout) if layout.name == layout_name => Some(layout.clone()), + _ => None, + }); + let Some(layout) = layout else { + return; + }; + let sheet = Self::layout_sheet_viewport_handle(&self.document, &layout); + if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(sheet) { + fit_sheet_viewport(vp, layout.min_limits, layout.max_limits); + } + } + + /// Gives a layout whose page setup was never initialized (no paper size + /// and no media name, as the reference leaves a layout until it is first + /// opened) the default page setup; true when it did. + pub fn initialize_layout_page_setup(&mut self, layout_name: &str) -> bool { + let plot_style = self.new_layout_plot_style.clone(); + for object in self.document.objects.values_mut() { + if let ObjectType::Layout(layout) = object { + if layout.name == layout_name { + if !page_setup_uninitialized(layout) { + return false; + } + crate::scene::apply_default_page_setup(layout, &plot_style); + // A sheet written for a layout never set up is not the + // page's; it is fitted to the page. + self.fit_layout_sheet(layout_name); + self.bump_layout_epoch(); + return true; + } + } + } + false + } + + /// The viewport a layout gets when it first opens (LAYOUTCREATEVIEWPORT), + /// as the reference makes it: centred on the printable area, 0.8 of its + /// width and height, showing the model's extents. + pub fn add_layout_default_viewport(&mut self, layout_name: &str) { + if !layout_create_viewport() || !self.layout_content_viewports().is_empty() { + return; + } + let Some(((x0, y0), (x1, y1))) = self.printable_area_limits() else { + return; + }; + let mut viewport = codec::entities::Viewport::new(); + viewport.center = codec::types::Vector3::new((x0 + x1) / 2.0, (y0 + y1) / 2.0, 0.0); + viewport.width = (x1 - x0).abs() * 0.8; + viewport.height = (y1 - y0).abs() * 0.8; + viewport.id = 2; + viewport.status = codec::entities::ViewportStatusFlags::default_on(); + if let Ok(handle) = self + .document + .add_entity_to_layout(EntityType::Viewport(viewport), layout_name) + { + // The reference zooms the new viewport to the model's extents + // with a margin of under one percent. + self.fit_viewport_to_model(handle, 1.0075); + self.notify_viewport_changed(handle); + self.bump_entities(&[(handle, ChangeKind::Added)]); + } } fn is_content_viewport_in_layout( @@ -5215,8 +5409,23 @@ impl Scene { // corner. let f = Self::layout_unit_factor(l); let (pw, ph) = (pw * f, ph * f); - let ox = l.min_limits.0.min(0.0); - let oy = l.min_limits.1.min(0.0); + let (wx0, wy0, wx1, wy1) = ( + l.plot_window_min_x, + l.plot_window_min_y, + l.plot_window_max_x, + l.plot_window_max_y, + ); + // A window plot maps that window onto the sheet, so the + // sheet sits around it rather than at the origin. + let (ox, oy) = if l.plot_type == 4 && wx1 > wx0 && wy1 > wy0 { + if l.plot_flags.plot_centered { + ((wx0 + wx1 - pw) / 2.0, (wy0 + wy1 - ph) / 2.0) + } else { + (wx0 - l.plot_origin_x * f, wy0 - l.plot_origin_y * f) + } + } else { + (l.min_limits.0.min(0.0), l.min_limits.1.min(0.0)) + }; return Some(((ox, oy), (ox + pw, oy + ph))); } @@ -5350,6 +5559,22 @@ impl Scene { Self::DEFAULT_METRIC_SCALES } } + + /// Model-space annotation multiplier from the header's CANNOSCALEVALUE + /// (paper/drawing factor), converted from paper units into the drawing's + /// INSUNITS, so a metre drawing uses 0.001 model units per paper + /// millimetre at 1:1. Every path that starts or loads a drawing, or + /// changes its units, goes through here so it agrees with CANNOSCALE. + pub(crate) fn sync_annotation_scale_from_header(&mut self) { + let cannoscale_value = self.document.header.annotation_scale_value; + let unit_factor = self.annotation_scale_unit_factor(); + self.annotation_scale = if cannoscale_value > 1e-9 { + ((1.0 / cannoscale_value) / unit_factor) as f32 + } else { + (1.0 / unit_factor) as f32 + }; + } + /// Conversion from one drawing unit to the paper unit used by the /// standard annotation-scale family: millimetres for metric drawings /// and inches for imperial drawings. @@ -6513,6 +6738,12 @@ impl Scene { let Some(viewport) = self.explicit_viewport_handle() else { return false; }; + self.set_viewport_scale_factor_for(viewport, factor) + } + + /// Scale `viewport` to `factor` paper units per model unit, as + /// [`Self::set_viewport_scale_factor`] does for the active one. + pub fn set_viewport_scale_factor_for(&mut self, viewport: Handle, factor: f64) -> bool { let unit_factor = self.annotation_scale_unit_factor(); let named = self .document @@ -6843,6 +7074,7 @@ impl Scene { // set is zoom-independent (GPU analytical circles/arcs/ellipses). let t_tess = iced::time::Instant::now(); let mut atlas_gen = crate::scene::text::sdf_atlas::generation(); + self.building_resident_set.set(true); let mut wires = self.wires_for_block_culled( block, None, @@ -6881,6 +7113,7 @@ impl Scene { style_viewport, ); } + self.building_resident_set.set(false); let perf = crate::perf::enabled(); let t_post = perf.then(iced::time::Instant::now); // Synthesized nonprint markers (geo-location daisy) live in model space @@ -6908,6 +7141,16 @@ impl Scene { .set(t_tess.elapsed().as_secs_f32() * 1000.0); self.last_tess_wires.set(wires.len()); let arc = Arc::new(wires); + // The memo keeps one copy of what this set holds; read before the + // previous set is evicted below, which may hold runs it still needs. + if self.uses_resident_memo(block, frozen_layers) { + match &layout { + Some(layout) if self.resident_runs_match_memo(block) => { + self.bind_memo_to_set(&arc, &layout.ranges); + } + _ => self.detach_memo_backing(), + } + } let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed); self.last_model_wire_gen.set(gen); // A full rebuild of the Model set โ€” the GPU arena must rebuild too, not @@ -7001,6 +7244,98 @@ impl Scene { } } + /// The wires a memoized run stands for; `None` when an `InSet` run's + /// backing set is gone, so the caller tessellates afresh. + fn resolve_memo_run(&self, handle: Handle, run: &MemoRun) -> Option { + match run { + MemoRun::Owned(wires) => Some(MemoHit::Owned(Arc::clone(wires))), + MemoRun::InSet => { + let backing = self.resident_memo_backing.borrow(); + let set = backing.set.upgrade()?; + let &(start, len) = backing.ranges.get(&handle)?; + (start + len <= set.len()).then_some(MemoHit::Slice(set, start, len)) + } + MemoRun::Pending => None, + } + } + + /// Whether building `block` reads and fills the resident memo. + fn uses_resident_memo(&self, block: Handle, frozen_layers: Option<&HashSet>) -> bool { + block == self.model_space_block_handle() + && frozen_layers.is_none_or(|layers| layers.is_empty()) + && resident_memo_enabled() + } + + /// Whether a resident set's entity runs are exactly the memo's: nothing + /// recolours or replaces them after tessellation. + fn resident_runs_match_memo(&self, block: Handle) -> bool { + self.count_display.is_none() + && self.refedit_keep.is_none() + && self.active_live_section(block).is_none() + } + + /// Point the memo at `set`: an entity with a run there keeps no copy of + /// its own; a run only the previous backing held is copied out first. + fn bind_memo_to_set( + &self, + set: &Arc>, + ranges: &HashMap, + ) { + let old = std::mem::take(&mut *self.resident_memo_backing.borrow_mut()); + let old_set = old.set.upgrade(); + self.resident_tess_memo.borrow_mut().retain(|handle, run| { + if ranges.contains_key(handle) { + *run = MemoRun::InSet; + } else if let MemoRun::Pending = run { + return false; + } else if let MemoRun::InSet = run { + match old_set.as_ref().zip(old.ranges.get(handle)) { + Some((wires, &(start, len))) if start + len <= wires.len() => { + *run = MemoRun::Owned(Arc::new(wires[start..start + len].to_vec())); + } + _ => return false, + } + } + true + }); + *self.resident_memo_backing.borrow_mut() = MemoBacking { + set: Arc::downgrade(set), + ranges: ranges.clone(), + keep: None, + }; + } + + /// Give every `InSet` run a copy of its own and drop the backing, before + /// a build whose set will not match the memo. + fn detach_memo_backing(&self) { + let old = std::mem::take(&mut *self.resident_memo_backing.borrow_mut()); + let set = old.set.upgrade(); + self.resident_tess_memo.borrow_mut().retain(|handle, run| { + if let MemoRun::Pending = run { + return false; + } + if let MemoRun::InSet = run { + match set.as_ref().zip(old.ranges.get(handle)) { + Some((wires, &(start, len))) if start + len <= wires.len() => { + *run = MemoRun::Owned(Arc::new(wires[start..start + len].to_vec())); + } + _ => return false, + } + } + true + }); + } + + /// An abandoned patch took the backing set apart: keep it alive for the + /// full build that follows. Runs the patch left untouched are intact, and + /// the runs it blanked were copied out or dropped from the memo first. + fn keep_memo_backing(&self, wires: Vec) { + let keep = Arc::new(wires); + let mut backing = self.resident_memo_backing.borrow_mut(); + backing.set = Arc::downgrade(&keep); + backing.keep = Some(keep); + } + fn resident_wire_layout(wires: &[WireModel]) -> Option { let mut order = Vec::new(); let mut ranges = HashMap::default(); @@ -7085,9 +7420,16 @@ impl Scene { return declined("viewport-style"); } let t_patch = iced::time::Instant::now(); + // A patch whose set will not equal the memo's runs leaves the memo + // holding its own copies, taken while the set is still whole. + let shareable = self.uses_resident_memo(block, frozen_layers) + && self.resident_runs_match_memo(block); + if self.uses_resident_memo(block, frozen_layers) && !shareable { + self.detach_memo_backing(); + } // The entry must exist, be stale, and be uniquely held so we can move // its wires out rather than deep-clone them. - let cached_epoch = { + let (cached_epoch, backed_here) = { let sets = self.resident_wire_sets.borrow(); let Some(entry) = sets.get(&key) else { drop(sets); @@ -7107,7 +7449,13 @@ impl Scene { } return None; } - entry.epoch + let backed_here = shareable + && self + .resident_memo_backing + .borrow() + .set + .ptr_eq(&Arc::downgrade(&entry.wires)); + (entry.epoch, backed_here) }; let Some(deltas) = self.replay_since(cached_epoch) else { return declined("journal-too-old"); @@ -7116,7 +7464,7 @@ impl Scene { // Take ownership of the cached assembly (guaranteed unique above). // `prev_gen` is the content id the GPU currently holds for this set โ€” the // base the wire-arena patch replays from. - let (prev_gen, mut owned, mut layout) = { + let (prev_gen, owned, mut layout) = { let removed = self.resident_wire_sets.borrow_mut().remove(&key)?; ( removed.gen, @@ -7124,6 +7472,14 @@ impl Scene { removed.layout?, ) }; + // Every early return below abandons the patch; the memo's `InSet` + // runs then still need this set, so the guard hands it back. + let mut reclaim = ReclaimOnDecline { + scene: self, + wires: Some(owned), + backed: backed_here, + }; + let owned = reclaim.wires.as_mut()?; if layout.marker_start > owned.len() { return None; } @@ -7168,10 +7524,21 @@ impl Scene { } visible_changed.insert(*h); let memoized = memo_ok - .then(|| self.resident_tess_memo.borrow().get(h).cloned()) + .then(|| { + let memo = self.resident_tess_memo.borrow(); + match memo.get(h)? { + MemoRun::Owned(wires) => Some(wires.as_ref().clone()), + MemoRun::InSet if backed_here => { + let &(start, len) = layout.ranges.get(h)?; + owned.get(start..start + len).map(<[WireModel]>::to_vec) + } + run => self + .resolve_memo_run(*h, run) + .map(|hit| hit.wires().to_vec()), + } + }) .flatten(); - if let Some(raw) = memoized { - let mut faded = raw.as_ref().clone(); + if let Some(mut faded) = memoized { self.apply_refedit_fade(&mut faded, bg); new_runs.insert(*h, faded); continue; @@ -7201,7 +7568,7 @@ impl Scene { { let mut memo = self.resident_tess_memo.borrow_mut(); for (h, a) in memo_updates { - memo.insert(h, a); + memo.insert(h, MemoRun::Owned(a)); } } // Capture whether any replaced/removed OLD run fed the Face3D/fill @@ -7274,6 +7641,18 @@ impl Scene { if start + old_len > layout.marker_start { return None; } + // A hidden entity's memo run lives in these slots: copy it + // out before blanking them, for when it is shown again. + if backed_here && !matches!(kind, ChangeKind::Removed) { + let mut memo = self.resident_tess_memo.borrow_mut(); + if let Some(run) = memo.get_mut(&handle) { + if matches!(run, MemoRun::InSet) { + *run = MemoRun::Owned(Arc::new( + owned[start..start + old_len].to_vec(), + )); + } + } + } for slot in &mut owned[start..start + old_len] { *slot = WireModel::solid(String::new(), Vec::new(), [0.0; 4], false); slot.aabb = [ @@ -7399,7 +7778,7 @@ impl Scene { .is_some_and(|run| run.iter().any(|wire| !wire.fill_tris.is_empty())) }); - let arc = Arc::new(owned); + let arc = Arc::new(reclaim.wires.take().unwrap_or_default()); self.last_tess_wires.set(arc.len()); self.resident_patch_hits .set(self.resident_patch_hits.get() + 1); @@ -7421,6 +7800,9 @@ impl Scene { }), )); } + if shareable { + self.bind_memo_to_set(&arc, &layout.ranges); + } let cur_epoch = self.geometry_epoch; let mut sets = self.resident_wire_sets.borrow_mut(); sets.retain(|_, set| set.epoch == cur_epoch); @@ -7532,7 +7914,11 @@ impl Scene { /// the previous per-call deep clone was a full copy of every wire buffer /// (hundreds of MB on large mesh imports, #358). pub fn entity_wires(&self) -> Arc> { - self.entity_wires_arc() + let wires = self.entity_wires_arc(); + match crate::scene::model::instance_model::expand_stubs(&wires) { + std::borrow::Cow::Borrowed(_) => wires, + std::borrow::Cow::Owned(expanded) => Arc::new(expanded), + } } /// Return paper entities and projected model-viewport entities separately. @@ -7556,7 +7942,9 @@ impl Scene { } }; if self.current_layout == "Model" { - let mut wires = self.entity_wires_arc().as_ref().clone(); + let mut wires = + crate::scene::model::instance_model::expand_stubs(&self.entity_wires_arc()) + .into_owned(); apply_mode( &mut wires, render_mode_override.unwrap_or_else(|| self.active_model_tile_render_mode()), @@ -7578,7 +7966,9 @@ impl Scene { apply_mode(&mut wires, mode); model_wires.extend(wires); } - (self.paper_sheet_wires_arc().as_ref().clone(), model_wires) + let paper = crate::scene::model::instance_model::expand_stubs(&self.paper_sheet_wires_arc()) + .into_owned(); + (paper, model_wires) } pub(crate) fn plot_wire_depths(&self, wires: &[WireModel]) -> Vec { @@ -7917,15 +8307,19 @@ impl Scene { let cache = self.wipeout_cache.borrow(); match cache.get(&key) { Some((cached_epoch, arc)) - // wipeout_models scans the whole document for Wipeout - // entities; relevance = the changed handle is a Wipeout. + // The displayed wipeout set also contains masks + // expanded from INSERTs. COPY and grip edits modify or + // add INSERT handles without changing the child WIPEOUT + // entity, so the old predicate reused stale positions + // until REGEN. Track both categories, including + // removed INSERTs via the recorded removal bits. if self.category_cache_valid( *cached_epoch, - CACHE_CATEGORY_WIPEOUT, + CACHE_CATEGORY_WIPEOUT | CACHE_CATEGORY_INSERT_HATCH, |h| { matches!( self.document.get_entity(h), - Some(EntityType::Wipeout(_)) + Some(EntityType::Wipeout(_) | EntityType::Insert(_)) ) }, ) => @@ -8535,6 +8929,14 @@ impl Scene { body.wires = wires; body.silhouettes.clear(); } + // The cut mesh is drawn as the solid it bounds. + EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => { + let mut solid = codec::entities::Solid3D::new(); + solid.common = entity.common().clone(); + solid.point_of_reference = reference; + solid.wires = wires; + entity = EntityType::Solid3D(solid); + } _ => return Err(()), } Ok(Some(entity)) @@ -8560,6 +8962,12 @@ impl Scene { Some(EntityType::Surface(surface)) => { crate::scene::convert::solid3d_tess::kernel_surface_body(surface) } + // A mesh that closes a volume is cut as the solid it bounds. + Some( + mesh @ (EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_)), + ) => crate::entities::mesh::closed_mesh_body(mesh), _ => None, }?; Some(std::borrow::Cow::Owned(body)) @@ -8571,16 +8979,29 @@ impl Scene { slice_depth: Option, body_to_world: Option<&codec::types::Transform>, ) -> Result, ()> { - let Some((origin, tangent, vertical, viewing)) = section_frame(data) else { - return Err(()); - }; - let front = section_plane(origin, tangent, viewing, body_to_world).ok_or(())?; - let mut clipped = keep_section_positive(body, front)?; - let Some(mut current) = clipped.take() else { - return Ok(None); - }; + let planes = Self::section_planes(data, slice_depth, body_to_world).ok_or(())?; + let mut current = body.clone(); + for plane in planes { + match keep_section_positive(¤t, plane)? { + Some(next) => current = next, + None => return Ok(None), + } + } + Ok(Some(current)) + } + + /// The planes bounding what section `data` keeps, each facing inward: + /// the section plane, then the back of a slice, the ends of a boundary + /// and the top and bottom of a volume. + fn section_planes( + data: &codec::entities::SectionObjectData, + slice_depth: Option, + body_to_world: Option<&codec::types::Transform>, + ) -> Option> { + let (origin, tangent, vertical, viewing) = section_frame(data)?; + let mut planes = vec![section_plane(origin, tangent, viewing, body_to_world)?]; if data.state == 1 && slice_depth.is_none() { - return Ok(Some(current)); + return Some(planes); } let depth = slice_depth.unwrap_or_else(|| { @@ -8595,82 +9016,44 @@ impl Scene { }) }); if depth <= 1e-12 { - return Ok(Some(current)); + return Some(planes); } - let back = - section_plane(origin + viewing * depth, tangent, -viewing, body_to_world).ok_or(())?; - let Some(next) = keep_section_positive(¤t, back)? else { - return Ok(None); - }; - current = next; - + planes.push(section_plane( + origin + viewing * depth, + tangent, + -viewing, + body_to_world, + )?); if data.state == 1 { - return Ok(Some(current)); + return Some(planes); } - let span = data - .vertices - .first() - .zip(data.vertices.last()) - .map_or(0.0, |(first, last)| { - (glam::DVec3::new(last.x, last.y, last.z) - - glam::DVec3::new(first.x, first.y, first.z)) - .length() - }); - if span <= 1e-12 { - return Ok(Some(current)); + let first = data.vertices.first()?; + let last = data.vertices.last()?; + let first = glam::DVec3::new(first.x, first.y, first.z); + let last = glam::DVec3::new(last.x, last.y, last.z); + if (last - first).length() <= 1e-12 { + return Some(planes); } - - let first = data - .vertices - .first() - .map(|point| glam::DVec3::new(point.x, point.y, point.z)) - .ok_or(())?; - let last = data - .vertices - .last() - .map(|point| glam::DVec3::new(point.x, point.y, point.z)) - .ok_or(())?; - for plane in [ - section_plane(first, vertical, tangent, body_to_world), - section_plane(last, vertical, -tangent, body_to_world), - ] { - let Some(plane) = plane else { - return Err(()); - }; - let Some(next) = keep_section_positive(¤t, plane)? else { - return Ok(None); - }; - current = next; - } - + planes.push(section_plane(first, vertical, tangent, body_to_world)?); + planes.push(section_plane(last, vertical, -tangent, body_to_world)?); if data.state != 4 { - return Ok(Some(current)); + return Some(planes); } - for plane in [ - section_plane( - origin - vertical * data.bottom_height, - tangent, - vertical, - body_to_world, - ), - section_plane( - origin + vertical * data.top_height, - tangent, - -vertical, - body_to_world, - ), - ] { - let Some(plane) = plane else { - return Err(()); - }; - let Some(next) = keep_section_positive(¤t, plane)? else { - return Ok(None); - }; - current = next; - } - Ok(Some(current)) + planes.push(section_plane( + origin - vertical * data.bottom_height, + tangent, + vertical, + body_to_world, + )?); + planes.push(section_plane( + origin + vertical * data.top_height, + tangent, + -vertical, + body_to_world, + )?); + Some(planes) } /// True when `layer` is turned off or frozen โ€” entities on it never render. @@ -9080,6 +9463,13 @@ impl Scene { .layers .get(&entity.common().layer) .is_some_and(|l| l.is_locked()) + // REFEDIT with "Lock objects not in working set": only the + // working set can be edited. + || (self.refedit_lock + && self + .refedit_keep + .as_ref() + .is_some_and(|keep| !keep.contains(&handle))) } /// The name of the locked layer `handle` sits on, if any (for messages). @@ -9373,7 +9763,7 @@ impl Scene { }; if let Some(keep) = &self.refedit_keep { if !keep.contains(&h) { - c = crate::scene::cache::block_cache::fade_toward_bg(c, self.current_bg()); + c = crate::scene::cache::block_cache::refedit_fade(c, self.current_bg()); } } Some(c) @@ -9845,9 +10235,12 @@ impl Scene { if changed.contains(&handle) || self.document.get_entity(handle).is_none() { continue; } - if let Some(wire) = memo + let hit = memo .get(&handle) - .and_then(|entity_wires| entity_wires.get(ordinal as usize)) + .and_then(|run| self.resolve_memo_run(handle, run)); + if let Some(wire) = hit + .as_ref() + .and_then(|hit| hit.wires().get(ordinal as usize)) { base_slots.insert((handle_value, ordinal), wires.len() as u32); wires.push(wire.clone()); @@ -9888,6 +10281,17 @@ impl Scene { aabb: [f64; 4], allow_pending_empty: bool, area_only: bool, + ) -> crate::scene::pick::interaction_index::InteractionCandidates { + self.indexed_interaction_candidates_xy_stubbed(wires, aabb, allow_pending_empty, area_only) + .materialize_stubs() + } + + fn indexed_interaction_candidates_xy_stubbed( + &self, + wires: Arc>, + aabb: [f64; 4], + allow_pending_empty: bool, + area_only: bool, ) -> crate::scene::pick::interaction_index::InteractionCandidates { if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() { let perf = crate::perf::enabled(); @@ -9957,6 +10361,26 @@ impl Scene { eye: glam::DVec3, bounds: iced::Rectangle, allow_pending_empty: bool, + ) -> crate::scene::pick::interaction_index::InteractionCandidates { + self.indexed_interaction_candidates_screen_stubbed( + wires, + screen_rect, + view_rot, + eye, + bounds, + allow_pending_empty, + ) + .materialize_stubs() + } + + fn indexed_interaction_candidates_screen_stubbed( + &self, + wires: Arc>, + screen_rect: [f32; 4], + view_rot: glam::Mat4, + eye: glam::DVec3, + bounds: iced::Rectangle, + allow_pending_empty: bool, ) -> crate::scene::pick::interaction_index::InteractionCandidates { if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() { let perf = crate::perf::enabled(); @@ -10213,7 +10637,8 @@ impl Scene { if !self.interaction_source_is_resident(&wires, bounds.height) || !self.interaction_index_worthwhile(&wires) { - return crate::scene::pick::interaction_index::InteractionCandidates::all(wires); + return crate::scene::pick::interaction_index::InteractionCandidates::all(wires) + .materialize_stubs(); } let radius_px = self.indexed_interaction_pick_radius( &wires, @@ -10227,7 +10652,8 @@ impl Scene { if !flat_ortho { let clip = view_rot * (cursor - eye).as_vec3().extend(1.0); if !clip.is_finite() || clip.w <= 1e-6 { - return crate::scene::pick::interaction_index::InteractionCandidates::all(wires); + return crate::scene::pick::interaction_index::InteractionCandidates::all(wires) + .materialize_stubs(); } let ndc = clip.truncate() / clip.w; let screen = iced::Point::new( @@ -10257,7 +10683,8 @@ impl Scene { .sqrt(); let s = world_x_px.min(world_y_px); if s <= 1e-6 { - return crate::scene::pick::interaction_index::InteractionCandidates::all(wires); + return crate::scene::pick::interaction_index::InteractionCandidates::all(wires) + .materialize_stubs(); } let radius = radius_px.max(0.0) as f64 / s as f64; let query = [ @@ -10293,7 +10720,8 @@ impl Scene { if !self.interaction_source_is_resident(&wires, bounds.height) || !self.interaction_index_worthwhile(&wires) { - return crate::scene::pick::interaction_index::InteractionCandidates::all(wires); + return crate::scene::pick::interaction_index::InteractionCandidates::all(wires) + .materialize_stubs(); } let pad_px = self.indexed_interaction_pick_radius(&wires, 0.0, bounds.height, true); if flat_ortho { @@ -11294,13 +11722,6 @@ impl Scene { // bypasses the memo. let frozen_empty = frozen_layers.map(|f| f.is_empty()).unwrap_or(true); let base_ok = is_model_block && frozen_empty; - // Kill-switch: `OCS_NO_RESIDENT_MEMO` reverts the resident set to the old - // full re-tessellation on every edit, in case a mutation site is ever - // found that edits geometry without dropping its handle from the memo. - fn resident_memo_enabled() -> bool { - static EN: std::sync::OnceLock = std::sync::OnceLock::new(); - *EN.get_or_init(|| std::env::var("OCS_NO_RESIDENT_MEMO").is_err()) - } let resident = base_ok && view_aabb.is_none() && resident_memo_enabled(); let memo_active = base_ok && (view_aabb.is_some() || resident); // Reported in one line below. `tess_ms` / `memo_misses` are assigned by @@ -11340,14 +11761,14 @@ impl Scene { } // Classify (serial, cheap): reuse memoized Arcs, collect misses. let t_classify = perf.then(iced::time::Instant::now); - let mut hit_arcs: Vec>> = Vec::new(); + let mut hit_arcs: Vec = Vec::new(); let mut misses: Vec<&EntityType> = Vec::new(); { let memo = memo_cell.borrow(); for e in &visible { let h = e.common().handle; - match memo.get(&h) { - Some(a) => hit_arcs.push(Arc::clone(a)), + match memo.get(&h).and_then(|run| self.resolve_memo_run(h, run)) { + Some(hit) => hit_arcs.push(hit), None => misses.push(*e), } } @@ -11359,7 +11780,7 @@ impl Scene { let t_hits = perf.then(iced::time::Instant::now); let hit_wires: Vec = hit_arcs .par_iter() - .flat_map_iter(|a| a.iter().cloned()) + .flat_map_iter(|hit| hit.wires().iter().cloned()) .collect(); hits_ms = crate::perf::elapsed_ms(t_hits); let t_tess_miss = perf.then(iced::time::Instant::now); @@ -11394,9 +11815,16 @@ impl Scene { // with B-rep vertex snaps before the memo/assembly split. let mut wires = Arc::try_unwrap(a).unwrap_or_else(|a| a.as_ref().clone()); self.attach_solid_snaps(h, &mut wires); - let a = Arc::new(wires); - out.extend(a.iter().cloned()); - memo.insert(h, Arc::clone(&a)); + if resident && self.building_resident_set.get() { + // The resident set built from `out` becomes the + // memo's backing: no second copy (#1542, #1698). + out.extend(wires); + memo.insert(h, MemoRun::Pending); + } else { + let a = Arc::new(wires); + out.extend(a.iter().cloned()); + memo.insert(h, MemoRun::Owned(Arc::clone(&a))); + } } } materialize_ms = crate::perf::elapsed_ms(t_materialize); @@ -12635,28 +13063,60 @@ vis_index={:.1} visible_probe={:.1}", /// Set a newly created viewport's `view_target` and `view_height` so that /// all model-space content is visible at a reasonable scale. pub fn auto_fit_viewport(&mut self, vp_handle: Handle) { + self.fit_viewport_to_model(vp_handle, 1.1); + } + + /// Fit a viewport's view to the model-space content, with `margin` (1 is + /// tight) around it. + pub fn fit_viewport_to_model(&mut self, vp_handle: Handle, margin: f64) { let extents = self.model_space_extents(); let (min, max) = match extents { Some(e) => e, None => return, }; + let (min, max) = (min.as_dvec3(), max.as_dvec3()); let center = (min + max) * 0.5; - let content_w = (max.x - min.x).max(1e-3); - let content_h = (max.y - min.y).max(1e-3); let vp = match self.document.get_entity_mut(vp_handle) { Some(codec::EntityType::Viewport(vp)) => vp, _ => return, }; - // Set the view target to the model-space centroid (XY plane, z=0). - vp.view_target.x = center.x as f64; - vp.view_target.y = center.y as f64; - vp.view_target.z = 0.0; + let direction = glam::DVec3::new( + vp.view_direction.x, + vp.view_direction.y, + vp.view_direction.z, + ) + .normalize_or(glam::DVec3::Z); + // The extents box as seen along the view direction: its span across + // the view's right and up axes. Looking down Z that is its X/Y size. + let right = glam::DVec3::Z.cross(direction).normalize_or(glam::DVec3::X); + let up = direction.cross(right); + let (mut span_right, mut span_up) = ((f64::MAX, f64::MIN), (f64::MAX, f64::MIN)); + for corner in 0..8 { + let point = glam::DVec3::new( + if corner & 1 == 0 { min.x } else { max.x }, + if corner & 2 == 0 { min.y } else { max.y }, + if corner & 4 == 0 { min.z } else { max.z }, + ); + let (along_right, along_up) = (point.dot(right), point.dot(up)); + span_right = (span_right.0.min(along_right), span_right.1.max(along_right)); + span_up = (span_up.0.min(along_up), span_up.1.max(along_up)); + } + let content_w = (span_right.1 - span_right.0).max(1e-3); + let content_h = (span_up.1 - span_up.0).max(1e-3); - // Choose the scale that fits both dimensions with a small margin. - let margin = 1.1_f64; - let scale_w = vp.width / (content_w as f64 * margin); - let scale_h = vp.height / (content_h as f64 * margin); + // Aim at the model's centre; a plan view keeps it on the XY plane. + vp.view_target.x = center.x; + vp.view_target.y = center.y; + vp.view_target.z = if direction == glam::DVec3::Z { + 0.0 + } else { + center.z + }; + + // Choose the scale that fits both dimensions with the margin. + let scale_w = vp.width / (content_w * margin); + let scale_h = vp.height / (content_h * margin); let fit_scale = scale_w.min(scale_h).min(1000.0).max(1e-6); vp.custom_scale = fit_scale; @@ -12664,6 +13124,112 @@ vis_index={:.1} visible_probe={:.1}", } } +/// A page setup never initialized: no paper size and no media name (the +/// reference writes a layout so until it is first opened). +pub fn page_setup_uninitialized(layout: &codec::objects::Layout) -> bool { + layout.paper_size.trim().is_empty() && layout.paper_width <= 0.0 && layout.paper_height <= 0.0 +} + +/// A layout written as never opened: the page setup fields back to the +/// uninitialized state and no viewports, as the reference stores a layout +/// it has not opened yet. A layout already holding a viewport onto the model +/// (one added by automation) is plainly set up and is written as it is. +pub(crate) fn write_layout_unopened(document: &mut codec::CadDocument, layout_name: &str) { + let block = document.objects.values().find_map(|object| match object { + ObjectType::Layout(layout) if layout.name == layout_name => Some(layout.block_record), + _ => None, + }); + let has_model_view = document.entities().any(|entity| { + matches!(entity, EntityType::Viewport(viewport) + if Some(viewport.common.owner_handle) == block + && !Scene::is_sheet_viewport(document, viewport)) + }); + if has_model_view { + return; + } + let fresh = codec::objects::Layout::new(layout_name); + let mut block = Handle::NULL; + for object in document.objects.values_mut() { + if let ObjectType::Layout(layout) = object { + if layout.name == layout_name { + block = layout.block_record; + layout.paper_size = fresh.paper_size.clone(); + layout.paper_width = fresh.paper_width; + layout.paper_height = fresh.paper_height; + layout.plot_rotation = fresh.plot_rotation; + layout.plot_paper_units = fresh.plot_paper_units; + layout.min_limits = fresh.min_limits; + layout.max_limits = fresh.max_limits; + layout.min_extents = fresh.min_extents; + layout.max_extents = fresh.max_extents; + } + } + } + drop_layout_viewports(document, block); +} + +/// Size a sheet (overall paper) viewport to the paper limits `min`..`max`, +/// framing the whole sheet with a small margin. +fn fit_sheet_viewport(vp: &mut codec::entities::Viewport, min: (f64, f64), max: (f64, f64)) { + let height = (max.1 - min.1).abs().max(1.0); + // Paper-space center is an (x, y) point with z = 0. + let center = codec::types::Vector3::new((min.0 + max.0) / 2.0, (min.1 + max.1) / 2.0, 0.0); + vp.center = center; + vp.width = (max.0 - min.0).abs().max(1.0); + vp.height = height; + vp.view_target = center; + vp.view_center = codec::types::Vector3::ZERO; + vp.view_height = height * 1.1; +} + +/// Takes a layout's sheet (overall paper) viewport out of its block (entity +/// and membership). Viewports onto the model are the user's drawing and stay: +/// a file may carry them on a layout whose page setup was never filled in. +pub(crate) fn drop_layout_viewports(document: &mut codec::CadDocument, block: Handle) { + if block.is_null() { + return; + } + let viewports: Vec = document + .entities() + .filter_map(|entity| match entity { + EntityType::Viewport(viewport) + if viewport.common.owner_handle == block + && Scene::is_sheet_viewport(document, viewport) => + { + Some(viewport.common.handle) + } + _ => None, + }) + .collect(); + for handle in &viewports { + document.remove_entity(*handle); + } + if let Some(record) = document.block_records.iter_mut().find(|record| record.handle == block) { + record.entity_handles.retain(|handle| !viewports.contains(handle)); + } + for object in document.objects.values_mut() { + if let ObjectType::Layout(layout) = object { + if layout.block_record == block { + layout.viewports.retain(|handle| !viewports.contains(handle)); + if viewports.contains(&layout.viewport) { + layout.viewport = Handle::NULL; + } + } + } + } +} + +/// LAYOUTCREATEVIEWPORT: a layout opened for the first time gets a viewport. +static LAYOUT_CREATE_VIEWPORT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); + +pub fn layout_create_viewport() -> bool { + LAYOUT_CREATE_VIEWPORT.load(std::sync::atomic::Ordering::Relaxed) +} + +pub fn set_layout_create_viewport(on: bool) { + LAYOUT_CREATE_VIEWPORT.store(on, std::sync::atomic::Ordering::Relaxed); +} + impl Default for Scene { fn default() -> Self { Self::new() @@ -13319,6 +13885,7 @@ mod journal_tests { wires: Arc::new(Vec::new()), interaction_index: None, tess_memo: HashMap::default(), + tess_memo_backing: MemoBacking::default(), tess_guard: 0, block_defns: Vec::new(), draw_depths: Some(handed_over), @@ -13364,13 +13931,7 @@ mod journal_tests { scene.local_center = caches.local_center; scene.bg_color = model_bg; scene.current_layout = "Model".to_string(); - let scale = scene.document.header.annotation_scale_value; - let unit_factor = scene.annotation_scale_unit_factor(); - scene.annotation_scale = if scale > 1e-9 { - ((1.0 / scale) / unit_factor) as f32 - } else { - (1.0 / unit_factor) as f32 - }; + scene.sync_annotation_scale_from_header(); scene.install_prepared_open_geometry(prepared); let hits = scene.resident_patch_hits.get(); diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index 8bb860278..17b95b7b2 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -252,7 +252,12 @@ impl ImageModel { Some(Self { render_instance: None, file_path: img.file_path.clone(), - pixels: decoded.pixels, + pixels: adjusted_image_pixels( + &img.file_path, + &decoded.pixels, + img.brightness, + img.contrast, + ), width: decoded.width, height: decoded.height, opacity, @@ -650,6 +655,78 @@ pub fn clear_image_cache() { } } +/// Brightness and contrast (0โ€“100, 50 = unchanged) as one lookup table per +/// colour channel. Measured against the reference's display of a gradient: +/// brightness is a power curve x^g with g = ((100 โˆ’ B) / B)^0.72 (0 is black, +/// 100 white); contrast bends lightness around one half the same way, +/// 0.5 ยฑ 0.5ยท|2x โˆ’ 1|^h with h = ((100 โˆ’ C) / C)^0.72 (0 is flat grey, 100 a +/// hard step). Brightness is applied first. Fade is the model's opacity. +pub(crate) fn brightness_contrast_lut(brightness: u8, contrast: u8) -> [u8; 256] { + // ponytail: the 0.72 exponent is a least-squares fit of seven measured + // levels (within about 10/255); refine with more reference samples. + let curve = |v: u8| -> f64 { + let v = v.min(100); + (f64::from(100 - v) / f64::from(v.max(1))).powf(0.72) + }; + let mut lut = [0u8; 256]; + for (i, out) in lut.iter_mut().enumerate() { + let mut x = i as f64 / 255.0; + x = match brightness.min(100) { + 0 => 0.0, + 100 => if i == 0 { 0.0 } else { 1.0 }, + b => x.powf(curve(b)), + }; + x = match contrast.min(100) { + 0 => 0.5, + 100 => if x >= 0.5 { 1.0 } else { 0.0 }, + c => { + let d = 2.0 * x - 1.0; + 0.5 + 0.5 * d.signum() * d.abs().powf(curve(c)) + } + }; + *out = (x.clamp(0.0, 1.0) * 255.0).round() as u8; + } + lut +} + +/// The decoded raster with the entity's brightness and contrast applied, +/// memoised per source and setting (the GPU keeps textures per pixel buffer). +/// An entry holds its source buffer, so the address in its key cannot be +/// handed to another decode (a reloaded file) while the entry lives. +fn adjusted_image_pixels( + path: &str, + pixels: &Arc>, + brightness: u8, + contrast: u8, +) -> Arc> { + if brightness == 50 && contrast == 50 { + return Arc::clone(pixels); + } + type Key = (String, usize, u8, u8); + type Entry = (Arc>, Arc>); + static CACHE: OnceLock>> = OnceLock::new(); + let key = (path.to_string(), Arc::as_ptr(pixels) as usize, brightness, contrast); + let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); + if let Some((_, hit)) = cache.lock().unwrap_or_else(|p| p.into_inner()).get(&key) { + return Arc::clone(hit); + } + let lut = brightness_contrast_lut(brightness, contrast); + let mut out = pixels.as_ref().clone(); + for px in out.chunks_exact_mut(4) { + for c in &mut px[..3] { + *c = lut[*c as usize]; + } + } + let out = Arc::new(out); + let mut cache = cache.lock().unwrap_or_else(|p| p.into_inner()); + // ponytail: cleared wholesale past 64 entries; an LRU if many images are adjusted. + if cache.len() > 64 { + cache.clear(); + } + cache.insert(key, (Arc::clone(pixels), Arc::clone(&out))); + out +} + /// Resolve an image reference to decoded pixels, memoised per path. Handles a /// local file and an `http`/`https` URL (fetched synchronously natively; on /// the web the first resolve kicks a background fetch and returns `None` โ€” diff --git a/src/scene/model/instance_model.rs b/src/scene/model/instance_model.rs index c4a19f41a..7213f646f 100644 --- a/src/scene/model/instance_model.rs +++ b/src/scene/model/instance_model.rs @@ -1,4 +1,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +use super::wire_model::WireModel; static NEXT_SOURCE_ID: AtomicU64 = AtomicU64::new(1); @@ -11,3 +14,127 @@ pub struct RenderInstance { pub fn next_source_id() -> u64 { NEXT_SOURCE_ID.fetch_add(1, Ordering::Relaxed) } + +/// Translated copies of one nested block expansion, kept as a single wire +/// instead of one wire per copied primitive (#1698): the copy is `prototype` +/// moved by `delta`, its draw depths shifted by `depth_delta`. Prototype +/// wires are block-instanced (or stubs themselves), so the GPU draws them +/// from the shared source geometry and CPU users expand them on demand. +#[derive(Debug)] +pub struct InstanceStub { + pub prototype: Arc>, + pub delta: [f64; 3], + pub depth_delta: f32, +} + +/// Whether a prototype wire may be represented by a stub: one the block +/// renderer draws from its source geometry alone (no glyphs, fills or +/// markers of its own), or a stub already. +pub fn stubbable(wire: &WireModel) -> bool { + // Kill-switch: `OCS_NO_BLOCK_STUBS` keeps every copy as its own wires. + static OFF: std::sync::OnceLock = std::sync::OnceLock::new(); + if *OFF.get_or_init(|| std::env::var("OCS_NO_BLOCK_STUBS").is_ok()) { + return false; + } + wire.stub.is_some() + || (wire.render_instance.is_some() + && wire.text_verts.is_empty() + && wire.fill_tris.is_empty() + && wire.pick_tris.is_empty() + && !wire.fill_is_3d + && wire.point_marker.is_none() + && wire.searchable_text.is_empty()) +} + +/// The XY box a prototype's wires cover, before any copy's offset. +pub fn prototype_aabb(prototype: &[WireModel]) -> [f32; 4] { + prototype.iter().fold( + [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY], + |acc, wire| { + [ + acc[0].min(wire.aabb[0]), + acc[1].min(wire.aabb[1]), + acc[2].max(wire.aabb[2]), + acc[3].max(wire.aabb[3]), + ] + }, + ) +} + +/// A stub wire standing for `prototype` moved by `delta`. +pub fn stub_wire( + prototype: Arc>, + prototype_aabb: [f32; 4], + delta: [f64; 3], + depth_delta: f32, +) -> WireModel { + let aabb = [ + (prototype_aabb[0] as f64 + delta[0]) as f32, + (prototype_aabb[1] as f64 + delta[1]) as f32, + (prototype_aabb[2] as f64 + delta[0]) as f32, + (prototype_aabb[3] as f64 + delta[1]) as f32, + ]; + WireModel { + aabb, + stub: Some(Arc::new(InstanceStub { + prototype, + delta, + depth_delta, + })), + ..Default::default() + } +} + +/// Every wire `wires` stands for, stubs expanded into translated copies +/// named and selected like the stub that holds them. +pub fn expand_stubs(wires: &[WireModel]) -> std::borrow::Cow<'_, [WireModel]> { + if !wires.iter().any(|wire| wire.stub.is_some()) { + return std::borrow::Cow::Borrowed(wires); + } + let mut out = Vec::with_capacity(wires.len()); + for wire in wires { + if wire.stub.is_some() { + expand_stub_into(wire, &mut out); + } else { + out.push(wire.clone()); + } + } + std::borrow::Cow::Owned(out) +} + +/// Append the translated copies a stub wire stands for. +pub fn expand_stub_into(stub_wire: &WireModel, out: &mut Vec) { + let Some(stub) = stub_wire.stub.as_ref() else { + out.push(stub_wire.clone()); + return; + }; + for wire in stub.prototype.iter() { + let mut copy = crate::scene::cache::block_cache::translated_prototype_wire( + wire, + &stub_wire.name, + stub.delta, + ); + shift_depth(&mut copy, stub.depth_delta); + copy.selected = stub_wire.selected; + copy.display_visible &= stub_wire.display_visible; + if copy.stub.is_some() { + expand_stub_into(©, out); + } else { + out.push(copy); + } + } +} + +/// Shift a block-local draw depth by `delta`, through a stub as well. +pub fn shift_depth(wire: &mut WireModel, delta: f32) { + if let Some(depth) = wire.depth_override.as_mut() { + *depth += delta; + } + if let Some(stub) = wire.stub.as_mut() { + *stub = Arc::new(InstanceStub { + prototype: Arc::clone(&stub.prototype), + delta: stub.delta, + depth_delta: stub.depth_delta + delta, + }); + } +} diff --git a/src/scene/model/underlay_vector/dwf.rs b/src/scene/model/underlay_vector/dwf.rs index 6ca10a5c8..aa1274a5e 100644 --- a/src/scene/model/underlay_vector/dwf.rs +++ b/src/scene/model/underlay_vector/dwf.rs @@ -636,6 +636,13 @@ mod w2d { let size = r.i32()?; r.i = r.i.checked_add(size.max(0) as usize)?; } + b'O' => { + // Set origin: the absolute point later relative + // coordinates continue from (plot-optimized streams + // start each drawing with it). + r.i += 1; + at = [r.i32()? as f64, r.i32()? as f64]; + } b'V' => { visible = true; r.i += 1; diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 8ee9fbc0c..d0b7f6917 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -836,6 +836,9 @@ pub struct WireModel { pub fill_is_2d_solid: bool, /// Shared block-render source and this placement's translation. pub render_instance: Option, + /// Set on a wire that stands for a translated copy of a nested block + /// expansion; its own geometry is empty (see `InstanceStub`). + pub stub: Option>, } impl WireModel { @@ -911,6 +914,7 @@ impl WireModel { fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, + stub: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), text_verts: Vec::new(), @@ -1448,6 +1452,7 @@ impl Default for WireModel { fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, + stub: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), } diff --git a/src/scene/modify.rs b/src/scene/modify.rs index ae9e84058..279668755 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -313,6 +313,9 @@ impl Scene { | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) ) }) { @@ -578,6 +581,122 @@ impl Scene { } } + /// Deep-clone the extension-dictionary tree rooted at `src_root` (owned by + /// the entity `src_owner`) and return the clone's root, wired for + /// `new_owner`. In-drawing COPY parity: the copy gets its own tree, so + /// edits on it never touch the source. Dictionary entries, owners, + /// reactors, nested xdictionary links, and XRecord handle references that + /// land inside the tree are remapped onto fresh handles; references + /// leaving the tree are preserved verbatim. A cyclic tree terminates via + /// the visited set and remaps consistently. Atomic: nothing enters + /// `document.objects` unless the whole tree clones; a missing or dangling + /// root returns `None` so the caller leaves the copy without an extension + /// dictionary โ€” never shared. + pub(super) fn clone_xdict_tree( + doc: &mut codec::CadDocument, + src_owner: Handle, + src_root: Handle, + new_owner: Handle, + ) -> Option { + if src_root.is_null() { + return None; + } + // Walk every object reachable through dictionary entries and + // xdictionary links, in discovery order (root first). + let mut order: Vec = Vec::new(); + let mut index = rustc_hash::FxHashMap::default(); + let mut stack = vec![src_root]; + while let Some(h) = stack.pop() { + if index.contains_key(&h) { + continue; + } + let Some(obj) = doc.objects.get(&h) else { + if h == src_root { + return None; + } + continue; + }; + index.insert(h, order.len()); + order.push(h); + match obj { + codec::objects::ObjectType::Dictionary(d) => { + for (_, child) in &d.entries { + stack.push(*child); + } + if let Some(xd) = d.xdictionary_handle { + stack.push(xd); + } + } + codec::objects::ObjectType::XRecord(x) => { + if let Some(xd) = x.xdictionary_handle { + stack.push(xd); + } + } + _ => {} + } + } + // Clone and remap onto fresh handles; insert only once the whole tree + // is built so a mid-walk failure leaves the document untouched. + let mut remap = rustc_hash::FxHashMap::default(); + for &h in &order { + remap.insert(h, doc.allocate_handle()); + } + let rewrite = |handle: Handle| { + if handle == src_owner { + new_owner + } else { + *remap.get(&handle).unwrap_or(&handle) + } + }; + let mut clones = Vec::with_capacity(order.len()); + for &h in &order { + let new_h = remap[&h]; + let obj = doc.objects.get(&h)?; + let mut obj = obj.clone(); + match &mut obj { + codec::objects::ObjectType::Dictionary(d) => { + d.handle = new_h; + d.owner = rewrite(d.owner); + for (_, child) in &mut d.entries { + *child = rewrite(*child); + } + d.xdictionary_handle = d.xdictionary_handle.map(rewrite); + for r in &mut d.reactors { + *r = rewrite(*r); + } + } + codec::objects::ObjectType::XRecord(x) => { + x.handle = new_h; + x.owner = rewrite(x.owner); + x.xdictionary_handle = x.xdictionary_handle.map(rewrite); + for r in &mut x.reactors { + *r = rewrite(*r); + } + for entry in &mut x.entries { + if let codec::objects::XRecordValue::Handle(h) = &mut entry.value { + *h = rewrite(*h); + } + } + for entry in &mut x.object_references { + entry.handle = rewrite(entry.handle); + } + // The raw DWG stream describes the source's object bytes; + // the clone regenerates its own from the remapped entries. + x.raw_dwg_data = None; + x.raw_dwg_handle_bits = 0; + x.raw_dwg_version = None; + x.preserve_object_reference_stream = false; + } + _ => {} + } + clones.push((new_h, obj)); + } + for (new_h, obj) in clones { + doc.objects.insert(new_h, obj); + } + Some(remap[&src_root]) + } + /// Add a clone to the active space with fresh top-level and inline handles. /// Source owner handles are document-local and must not cross drawings. /// (#129, #934) @@ -586,6 +705,10 @@ impl Scene { let common = entity.common_mut(); common.handle = Handle::NULL; common.owner_handle = Handle::NULL; + // An extension dictionary is exclusively owned; the clone must not + // claim the source's, or the copy dangles into a document that does + // not own that object. + common.xdictionary_handle = None; common.entity_mode = Some( if self.current_layout != "Model" && self.active_viewport.is_none() { 1 @@ -752,15 +875,35 @@ impl Scene { } Self::reset_clone_subhandles(&mut self.document, &mut entity); - // An annotative LEADER/MTEXT pair must receive a fresh extension dictionary. - // Keeping this handle would make the copy share the source's context tree. - if !annotation_scales.is_empty() { + // An annotative entity's extension dictionary is a context tree + // recreated fresh below; any other copy's tree is deep-cloned + // after insertion, so the clone must never carry the source's + // handle through add_entity. + let src_xdict = if annotation_scales.is_empty() { + entity.common_mut().xdictionary_handle.take() + } else { entity.common_mut().xdictionary_handle = None; - } + None + }; entity.common_mut().handle = Handle::NULL; let h = self.document.add_entity(entity).unwrap_or(Handle::NULL); if !h.is_null() { + // Deep-clone the extension-dictionary tree so edits on the + // copy never touch the source. A dangling or missing root + // leaves the copy without one โ€” never shared. + if let Some(src_root) = src_xdict { + if let Some(new_root) = + Self::clone_xdict_tree(&mut self.document, src_handle, src_root, h) + { + if let Some(clone) = self.document.get_entity_mut(h) { + clone.common_mut().xdictionary_handle = Some(new_root); + } + // The cloned tree lives in document.objects โ€” outside + // a pure-entity delta; keep undo on the full snapshot. + self.poison_undo_recording(); + } + } if !annotation_scales.is_empty() { for scale_handle in annotation_scales { crate::scene::annotative::create_annotation_context( @@ -2319,3 +2462,483 @@ mod define_transformed_block_xdict_tests { } } } + +#[cfg(test)] +mod add_entity_clone_xdict_tests { + use super::*; + + /// A cross-document copy must not carry the source's extension dictionary: + /// the target document does not own that object, so sharing it leaves a + /// dangling reference. An extension dictionary is exclusively owned, and a + /// second claimant fails file validation with + /// "Extension dictionary Cannot access". + #[test] + fn exported_clone_does_not_share_source_xdict() { + let mut source = Scene::new(); + + let dict_handle = source.document.allocate_handle(); + let mut dict = codec::objects::Dictionary::new(); + dict.handle = dict_handle; + source + .document + .objects + .insert(dict_handle, codec::objects::ObjectType::Dictionary(dict)); + + let mut line = codec::entities::Line::new(); + line.common.xdictionary_handle = Some(dict_handle); + let src_handle = source + .document + .add_entity(codec::EntityType::Line(line)) + .unwrap(); + + let cloned = source.document.get_entity(src_handle).unwrap().clone(); + let mut target = Scene::new(); + let new_handle = target.add_entity_clone(cloned); + assert!(!new_handle.is_null(), "clone must be added"); + + // Source keeps its dictionary. + match source.document.get_entity(src_handle) { + Some(e) => assert_eq!( + e.common().xdictionary_handle, + Some(dict_handle), + "the source must keep its own dictionary" + ), + None => panic!("source entity missing"), + } + // The copy carries none. + match target.document.get_entity(new_handle) { + Some(e) => assert!( + e.common().xdictionary_handle.is_none(), + "exported clone must not carry an extension dictionary" + ), + None => panic!("exported clone missing"), + } + } +} + +#[cfg(test)] +mod copy_entities_xdict_tests { + use super::*; + use crate::command::EntityTransform; + use codec::objects::{ + Dictionary, ObjectType, ProxyObjectReference, ProxyReferenceKind, XRecord, XRecordValue, + }; + use glam::DVec3; + + /// Line with an extension-dictionary tree: + /// root { SUB -> sub { DEEP -> deep }, REC -> rec("CLONE_ME") }; + /// root owner and reactors both name the source entity. + #[allow(clippy::type_complexity)] + fn scene_with_xdict_tree() -> ( + Scene, + codec::Handle, + codec::Handle, + codec::Handle, + codec::Handle, + codec::Handle, + ) { + let mut scene = Scene::new(); + let root_h = scene.document.allocate_handle(); + let sub_h = scene.document.allocate_handle(); + let deep_h = scene.document.allocate_handle(); + let rec_h = scene.document.allocate_handle(); + + let mut deep = XRecord::named("DEEP"); + deep.handle = deep_h; + deep.owner = sub_h; + deep.add_string(1, "DEEP_VALUE"); + let mut sub = Dictionary::new(); + sub.handle = sub_h; + sub.owner = root_h; + sub.add_entry("DEEP", deep_h); + let mut rec = XRecord::named("REC"); + rec.handle = rec_h; + rec.owner = root_h; + rec.add_string(1, "CLONE_ME"); + let mut root = Dictionary::new(); + root.handle = root_h; + root.add_entry("SUB", sub_h); + root.add_entry("REC", rec_h); + + scene + .document + .objects + .insert(root_h, ObjectType::Dictionary(root)); + scene + .document + .objects + .insert(sub_h, ObjectType::Dictionary(sub)); + scene + .document + .objects + .insert(deep_h, ObjectType::XRecord(deep)); + scene + .document + .objects + .insert(rec_h, ObjectType::XRecord(rec)); + + let mut line = codec::entities::Line::new(); + line.common.xdictionary_handle = Some(root_h); + let src = scene + .document + .add_entity(codec::EntityType::Line(line)) + .expect("add source entity"); + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&root_h) { + d.owner = src; + d.reactors.push(src); + } + (scene, src, root_h, sub_h, deep_h, rec_h) + } + + /// Every object reachable from `root` through dictionary entries and + /// xdictionary links (cycle-safe). + fn tree_handles(doc: &codec::CadDocument, root: codec::Handle) -> Vec { + let mut out = Vec::new(); + let mut stack = vec![root]; + let mut seen = std::collections::BTreeSet::new(); + while let Some(h) = stack.pop() { + if !seen.insert(h) { + continue; + } + out.push(h); + match doc.objects.get(&h) { + Some(ObjectType::Dictionary(d)) => { + for (_, c) in &d.entries { + stack.push(*c); + } + if let Some(xd) = d.xdictionary_handle { + stack.push(xd); + } + } + Some(ObjectType::XRecord(x)) => { + if let Some(xd) = x.xdictionary_handle { + stack.push(xd); + } + } + _ => {} + } + } + out.sort_unstable(); + out + } + + /// Byte-for-byte image of a tree for source-unchanged assertions. + fn tree_image( + doc: &codec::CadDocument, + root: codec::Handle, + ) -> Vec<(codec::Handle, ObjectType)> { + tree_handles(doc, root) + .into_iter() + .map(|h| (h, doc.objects.get(&h).expect("tree object").clone())) + .collect() + } + + #[test] + fn copy_deep_clones_extension_dictionary_tree() { + let (mut scene, src, root, _sub, _deep, rec) = scene_with_xdict_tree(); + let before = tree_image(&scene.document, root); + + let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO)); + assert_eq!(copies.len(), 1); + let copy = copies[0]; + assert!(!copy.is_null() && copy != src); + + let copy_root = scene + .document + .get_entity(copy) + .expect("copy entity") + .common() + .xdictionary_handle + .expect("copy must own a deep-cloned extension dictionary"); + assert_ne!(copy_root, root, "copy must not claim the source's root"); + + // Structure mirrors the source shape on fresh handles. + let copy_tree = tree_handles(&scene.document, copy_root); + assert_eq!( + copy_tree.len(), + 4, + "cloned tree must mirror the source: {copy_tree:?}" + ); + for h in ©_tree { + assert!( + !tree_handles(&scene.document, root).contains(h), + "clone must not reuse source handles" + ); + } + + // Root wiring: owner and reactor rewritten to the copy. + let ObjectType::Dictionary(copy_root_obj) = scene + .document + .objects + .get(©_root) + .expect("copy root object") + else { + panic!("copy root must be a dictionary"); + }; + assert_eq!( + copy_root_obj.owner, copy, + "cloned root must be owned by the copy" + ); + assert_eq!( + copy_root_obj.reactors, + vec![copy], + "reactor naming the source must be rewritten to the copy" + ); + + // Children resolve by key with preserved content. + let sub_h = copy_root_obj.get("SUB").expect("SUB entry on copy"); + let copy_rec = copy_root_obj.get("REC").expect("REC entry on copy"); + let ObjectType::Dictionary(sub_obj) = + scene.document.objects.get(&sub_h).expect("copy sub object") + else { + panic!("copy SUB must be a dictionary"); + }; + assert_eq!( + sub_obj.owner, copy_root, + "nested owner must point at the cloned parent" + ); + let deep_h = sub_obj.get("DEEP").expect("DEEP entry on copy"); + let ObjectType::XRecord(deep_obj) = scene + .document + .objects + .get(&deep_h) + .expect("copy deep object") + else { + panic!("copy DEEP must be an XRecord"); + }; + assert_eq!(deep_obj.name, "DEEP"); + let ObjectType::XRecord(copy_rec_obj) = scene + .document + .objects + .get(©_rec) + .expect("copy REC object") + else { + panic!("copy REC must be an XRecord"); + }; + assert_eq!(copy_rec_obj.name, "REC"); + assert!( + copy_rec_obj + .entries + .iter() + .any(|e| matches!(&e.value, XRecordValue::String(s) if s == "CLONE_ME")), + "cloned XRecord must carry the string content" + ); + + // Mutation independence: edit the copy, source stays untouched. + if let Some(ObjectType::XRecord(x)) = scene.document.objects.get_mut(©_rec) { + if let Some(e) = x + .entries + .iter_mut() + .find(|e| matches!(&e.value, XRecordValue::String(s) if s == "CLONE_ME")) + { + e.value = XRecordValue::String("MUTATED".into()); + } + } + let ObjectType::XRecord(src_rec_obj) = + scene.document.objects.get(&rec).expect("source REC object") + else { + panic!("source REC must be an XRecord"); + }; + assert!( + src_rec_obj + .entries + .iter() + .any(|e| matches!(&e.value, XRecordValue::String(s) if s == "CLONE_ME")), + "source XRecord must be unaffected by edits on the copy" + ); + + // Source tree byte-for-byte unchanged. + let after = tree_image(&scene.document, root); + assert_eq!( + before, after, + "source extension-dictionary tree must be byte-for-byte unchanged" + ); + } + + #[test] + fn copy_without_xdict_adds_no_objects_and_keeps_delta_undo() { + let mut scene = Scene::new(); + let line = codec::entities::Line::new(); + let src = scene + .document + .add_entity(codec::EntityType::Line(line)) + .expect("add source entity"); + let objects_before = scene.document.objects.len(); + + scene.begin_undo_recording(); + let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO)); + let objects_after = scene.document.objects.len(); + + assert_eq!(copies.len(), 1); + assert_ne!(copies[0], src); + assert_eq!( + objects_before, objects_after, + "xdict-free copy must add no document.objects entries" + ); + let recording = scene.take_undo_recording().expect("recording open"); + assert!( + !recording.poisoned, + "xdict-free copy must stay on the delta-undo path" + ); + } + + #[test] + fn copy_with_dangling_xdict_root_leaves_copy_without_one() { + let mut scene = Scene::new(); + let missing = scene.document.allocate_handle(); + let mut line = codec::entities::Line::new(); + line.common.xdictionary_handle = Some(missing); + let src = scene + .document + .add_entity(codec::EntityType::Line(line)) + .expect("add source entity"); + let objects_before = scene.document.objects.len(); + + let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO)); + + let copy_root = scene + .document + .get_entity(copies[0]) + .expect("copy entity") + .common() + .xdictionary_handle; + assert!( + copy_root.is_none(), + "dangling root must leave the copy without an extension dictionary, never shared" + ); + assert_eq!( + objects_before, + scene.document.objects.len(), + "dangling root must not invent objects" + ); + } + + #[test] + fn copy_cloned_xdict_cycle_terminates_and_remaps() { + let (mut scene, src, root, ..) = scene_with_xdict_tree(); + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&root) { + d.add_entry("BACK", root); + } + + let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO)); + let copy_root = scene + .document + .get_entity(copies[0]) + .expect("copy entity") + .common() + .xdictionary_handle + .expect("copy dictionary"); + let ObjectType::Dictionary(d) = scene.document.objects.get(©_root).expect("dict") + else { + panic!("dictionary"); + }; + assert_eq!( + d.get("BACK"), + Some(copy_root), + "cyclic entry must be remapped to the cloned root, consistently" + ); + assert_eq!( + tree_handles(&scene.document, copy_root).len(), + 4, + "cycle must not duplicate the tree" + ); + } + + #[test] + fn copy_keeps_references_outside_the_cloned_tree() { + let (mut scene, src, root, ..) = scene_with_xdict_tree(); + let (_, rec) = { + let ObjectType::Dictionary(d) = scene.document.objects.get(&root).expect("root") else { + panic!("root must be a dictionary"); + }; + ((), d.get("REC").expect("REC entry")) + }; + // An entity recorded inside the tree (e.g. a field pointing back at a + // drawing entity) stays pointed at the original; entities are never + // part of the object-tree clone. + let mut other_line = codec::entities::Line::new(); + other_line.common.handle = Handle::NULL; + let other = scene + .document + .add_entity(codec::EntityType::Line(other_line)) + .expect("other entity"); + // An object outside the reachable closure, referenced only through + // the XRecord handle stream, likewise stays pointed at the original. + let outside_h = scene.document.allocate_handle(); + let mut outside = Dictionary::new(); + outside.handle = outside_h; + scene + .document + .objects + .insert(outside_h, ObjectType::Dictionary(outside)); + if let Some(ObjectType::XRecord(x)) = scene.document.objects.get_mut(&rec) { + x.add_handle(340, other); + x.object_references.push(ProxyObjectReference { + handle: outside_h, + kind: ProxyReferenceKind::SoftPointer, + }); + } + + let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO)); + let copy_root = scene + .document + .get_entity(copies[0]) + .expect("copy entity") + .common() + .xdictionary_handle + .expect("copy dictionary"); + let ObjectType::Dictionary(d) = scene.document.objects.get(©_root).expect("dict") + else { + panic!("dictionary"); + }; + let copy_rec = d.get("REC").expect("REC entry on copy"); + let ObjectType::XRecord(copy_rec_obj) = + scene.document.objects.get(©_rec).expect("copy REC") + else { + panic!("copy REC must be an XRecord"); + }; + assert!( + copy_rec_obj + .entries + .iter() + .any(|e| matches!(&e.value, XRecordValue::Handle(h) if *h == other)), + "entity references leaving the object tree must stay pointed at the original" + ); + assert!( + copy_rec_obj + .object_references + .iter() + .any(|r| r.handle == outside_h), + "handle-stream references leaving the source tree must stay pointed at the original" + ); + } + + #[test] + fn each_copy_gets_its_own_disjoint_tree() { + let (mut scene, src, root, ..) = scene_with_xdict_tree(); + let source_tree = tree_handles(&scene.document, root); + let mut all_trees = vec![source_tree]; + + for _ in 0..3 { + let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO)); + let copy_root = scene + .document + .get_entity(copies[0]) + .expect("copy entity") + .common() + .xdictionary_handle + .expect("each copy must own a dictionary"); + all_trees.push(tree_handles(&scene.document, copy_root)); + } + + for (i, a) in all_trees.iter().enumerate() { + for (j, b) in all_trees.iter().enumerate() { + if i == j { + continue; + } + let overlap = a.iter().any(|h| b.contains(h)); + assert!(!overlap, "tree {i} and tree {j} must be disjoint"); + } + } + } +} diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 7037908e9..833cf42ca 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -2535,6 +2535,9 @@ impl super::Scene { } crate::io::xref::strip_resolved_xref_content(&mut document); crate::entities::field::stamp_save_dates(&mut document); + for name in &self.unopened_layouts { + crate::scene::write_layout_unopened(&mut document, name); + } document } diff --git a/src/scene/pick/interaction_index.rs b/src/scene/pick/interaction_index.rs index 45769c73d..1b887dde8 100644 --- a/src/scene/pick/interaction_index.rs +++ b/src/scene/pick/interaction_index.rs @@ -1646,6 +1646,78 @@ impl InteractionCandidates { .map(|(rect, (view, eye, bounds))| (rect, view, eye, bounds)) } + /// The candidates with every block-copy stub among them expanded into + /// the wires it stands for, so snapping and picking see each copy. Only + /// the copies' wires near an XY query are kept. + pub(crate) fn materialize_stubs(self) -> Self { + if !self.iter().any(|wire| wire.stub.is_some()) { + return self; + } + let near = |wire: &WireModel, query: [f64; 4]| { + let [x0, y0, x1, y1] = wire.aabb.map(f64::from); + !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) + || (x1 >= query[0] && x0 <= query[2] && y1 >= query[1] && y0 <= query[3]) + }; + let mut wires = Vec::new(); + for wire in self.iter() { + if wire.stub.is_none() { + wires.push(wire.clone()); + continue; + } + let start = wires.len(); + crate::scene::model::instance_model::expand_stub_into(wire, &mut wires); + if let Some(query) = self.query_aabb { + let mut keep = start; + for index in start..wires.len() { + if near(&wires[index], query) { + wires.swap(keep, index); + keep += 1; + } + } + wires.truncate(keep); + } + } + Self { + query_aabb: self.query_aabb, + screen_rect: self.screen_rect, + screen_view: self.screen_view, + ..Self::all(Arc::new(wires)) + } + } + + /// Leave out every wire `drop` matches, with the segments, points and + /// triangles that belong to it. + pub(crate) fn exclude_wires(&mut self, drop: impl Fn(&WireModel) -> bool) { + let wires = Arc::clone(&self.wires); + let dropped = |wire: u32| wires.get(wire as usize).is_some_and(&drop); + match self.wire_indices.as_mut() { + Some(indices) => indices.retain(|&wire| !dropped(wire)), + None => { + self.wire_indices = Some( + (0..wires.len() as u32) + .filter(|&wire| !dropped(wire)) + .collect(), + ); + } + } + fn keep( + refs: &mut Option>, + wire: impl Fn(&T) -> u32, + dropped: impl Fn(u32) -> bool, + ) { + if let Some(refs) = refs.as_mut() { + refs.retain(|item| !dropped(wire(item))); + } + } + keep(&mut self.segments, |r| r.wire, dropped); + keep(&mut self.snap_points, |r| r.wire, dropped); + keep(&mut self.key_vertices, |r| r.wire, dropped); + keep(&mut self.key_segments, |r| r.wire, dropped); + keep(&mut self.fill_triangles, |r| r.wire, dropped); + keep(&mut self.pick_triangles, |r| r.wire, dropped); + keep(&mut self.glyphs, |r| r.wire, dropped); + } + pub(crate) fn extend_indexed_area(&mut self, other: Self) { debug_assert!(Arc::ptr_eq(&self.wires, &other.wires)); @@ -1879,6 +1951,14 @@ fn finite_wire_aabb3(wire: &WireModel) -> Option<[f64; 6]> { vertex.pos[2] as f64 + vertex.pos_low[2] as f64, ]); } + if let Some(stub) = &wire.stub { + for inner in stub.prototype.iter() { + if let Some(inner) = finite_wire_aabb3(inner) { + aabb[2] = aabb[2].min(inner[2] + stub.delta[2]); + aabb[5] = aabb[5].max(inner[5] + stub.delta[2]); + } + } + } if !aabb[2].is_finite() { aabb[2] = 0.0; aabb[5] = 0.0; diff --git a/src/scene/pipeline/circle_gpu.rs b/src/scene/pipeline/circle_gpu.rs index 8e3d8e06b..badb17851 100644 --- a/src/scene/pipeline/circle_gpu.rs +++ b/src/scene/pipeline/circle_gpu.rs @@ -264,7 +264,7 @@ pub fn extract_circle_instance_from_geom_indexed( } let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]); - let hw = wire.line_weight_px * 0.5; + let hw = wire.line_weight_px.max(1.0) * 0.5; let pat0 = [ wire.pattern[0], wire.pattern[1], diff --git a/src/scene/pipeline/ellipse_gpu.rs b/src/scene/pipeline/ellipse_gpu.rs index 666aa3963..87eb84e8a 100644 --- a/src/scene/pipeline/ellipse_gpu.rs +++ b/src/scene/pipeline/ellipse_gpu.rs @@ -235,7 +235,7 @@ pub fn extract_ellipse_instance_from_geom( } let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]); - let hw = wire.line_weight_px * 0.5; + let hw = wire.line_weight_px.max(1.0) * 0.5; let pat0 = [ wire.pattern[0], wire.pattern[1], diff --git a/src/scene/pipeline/gpu_tests.rs b/src/scene/pipeline/gpu_tests.rs index 5812166dd..54e0f8ea0 100644 --- a/src/scene/pipeline/gpu_tests.rs +++ b/src/scene/pipeline/gpu_tests.rs @@ -71,35 +71,45 @@ fn block_edits_preserve_cache_coordinates_and_arena_partition() { let original = pipeline.upload_block_wires( &device, &queue, - &[&first, &second], + &[(&first).into(), (&second).into()], &depth, &mut block_geometry, ); let unchanged = pipeline.upload_block_wires( &device, &queue, - &[&first, &second], + &[(&first).into(), (&second).into()], &depth, &mut block_geometry, ); assert_eq!(original[0].geometry_id(), unchanged[0].geometry_id()); assert_eq!(original[0].instance_count, 2); - let removed = - pipeline.upload_block_wires(&device, &queue, &[&second], &depth, &mut block_geometry); + let removed = pipeline.upload_block_wires( + &device, + &queue, + &[(&second).into()], + &depth, + &mut block_geometry, + ); assert_ne!( original[0].geometry_id(), removed[0].geometry_id(), "removing the base instance must replace vertices baked at its old position" ); let moved = block(2, 30.0); - let moved_gpu = - pipeline.upload_block_wires(&device, &queue, &[&moved], &depth, &mut block_geometry); + let moved_gpu = pipeline.upload_block_wires( + &device, + &queue, + &[(&moved).into()], + &depth, + &mut block_geometry, + ); assert_ne!(removed[0].geometry_id(), moved_gpu[0].geometry_id()); let restored = pipeline.upload_block_wires( &device, &queue, - &[&first, &second], + &[(&first).into(), (&second).into()], &depth, &mut block_geometry, ); diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 66278bcfd..1b1f26c52 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -136,6 +136,9 @@ pub struct Pipeline { /// Capability-selected hatch renderer. Storage and texture transports are /// private backends behind one upload/LOD/draw lifecycle. hatch_gpu: hatch_gpu::HatchGpu, + /// Hatches with transparency, drawn after the wires so the lines behind + /// them show through the blend instead of failing the depth test. + hatch_translucent_gpu: hatch_gpu::HatchGpu, image_pipeline: wgpu::RenderPipeline, point_cloud_pipeline: wgpu::RenderPipeline, /// SDF text-quad pipeline (Phase 2b): draws per-glyph quads sampling the @@ -1100,6 +1103,14 @@ impl Pipeline { device_caps, force_compat_renderer, ); + let hatch_translucent_gpu = hatch_gpu::HatchGpu::new( + device, + format, + &frame_bgl, + &content_stencil, + device_caps, + force_compat_renderer, + ); #[cfg(not(target_arch = "wasm32"))] if std::env::var_os("RUST_LOG").is_some() { eprintln!( @@ -2339,6 +2350,7 @@ impl Pipeline { gpu_error_epoch: gpu_errors_seen(), wipeout_pipeline, hatch_gpu, + hatch_translucent_gpu, image_pipeline, point_cloud_pipeline, text_pipeline, @@ -2474,7 +2486,7 @@ impl Pipeline { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, - wires: &[&WireModel], + wires: &[wire_gpu::BlockRef<'_>], depth_map: &rustc_hash::FxHashMap, block_geometry: &mut wire_gpu::BlockGeometryCache, ) -> Vec { @@ -2608,7 +2620,7 @@ impl Pipeline { // and alpha blending both depend on it. Scissor and mesh-edge stay // grouping keys because the draw loop sets one scissor per batch and let mut batches: Vec = Vec::new(); - let mut block_wires: Vec<&WireModel> = Vec::new(); + let mut block_wires: Vec> = Vec::new(); let mut i = 0; while i < wires.len() { let kind = wire_arena::classify_wire(&wires[i]); @@ -2621,7 +2633,7 @@ impl Pipeline { continue; } wire_arena::WireKind::Block => { - block_wires.push(&wires[i]); + wire_gpu::push_block_refs(&wires[i], &mut block_wires); i += 1; continue; } @@ -2709,7 +2721,7 @@ impl Pipeline { Vec, Vec, Vec<&'a WireModel>, - Vec<&'a WireModel>, + Vec>, ) { use crate::par::prelude::*; // Pure per-wire work, so it fans out; `collect` on an indexed @@ -2722,7 +2734,7 @@ impl Pipeline { )> = wires .par_iter() .map(|&wire| { - if wire.render_instance.is_some() { + if wire.render_instance.is_some() || wire.stub.is_some() { return (None, None, true); } let depth = wire_gpu::wire_draw_depth(wire, depth_map); @@ -2751,10 +2763,10 @@ impl Pipeline { let mut circles: Vec = Vec::new(); let mut ellipses: Vec = Vec::new(); let mut regular: Vec<&'a WireModel> = Vec::new(); - let mut blocks: Vec<&'a WireModel> = Vec::new(); + let mut blocks: Vec> = Vec::new(); for (&wire, (wire_circles, wire_ellipses, is_block)) in wires.iter().zip(per) { if is_block { - blocks.push(wire); + wire_gpu::push_block_refs(wire, &mut blocks); continue; } let shaped = wire_circles.is_some() || wire_ellipses.is_some(); @@ -3429,10 +3441,12 @@ analytic={:.1} regular={:.1} blocks={:.1}", clip_h: u32, ) { let perf_started = crate::perf::enabled().then(iced::time::Instant::now); - let instance_count = self.hatch_gpu.update_visibility(queue, |aabb| { + let visible = |aabb: [f32; 4]| { !aabb_below_pixel(aabb, view_rot, eye, clip_w, clip_h, 2.0) && !aabb_offscreen(aabb, view_rot, eye, clip_w, clip_h) - }); + }; + let instance_count = self.hatch_gpu.update_visibility(queue, visible) + + self.hatch_translucent_gpu.update_visibility(queue, visible); if instance_count == 0 { return; } @@ -3786,7 +3800,10 @@ analytic={:.1} regular={:.1} blocks={:.1}", .iter() .filter(|hatch| hatch.boundary.len() >= 3) .count(); - self.hatch_gpu.upload(device, queue, hatches); + let (translucent, opaque): (Vec, Vec) = + hatches.iter().cloned().partition(|hatch| hatch.color[3] < 0.999); + self.hatch_gpu.upload(device, queue, &opaque); + self.hatch_translucent_gpu.upload(device, queue, &translucent); if let Some(started) = perf_started { let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0; if elapsed_ms >= 1.0 { @@ -3996,7 +4013,8 @@ analytic={:.1} regular={:.1} blocks={:.1}", } let upload = |label: &'static str, image: Option<&crate::scene::model::image_model::DecodedImage>, - fallback: [u8; 4]| { + fallback: [u8; 4], + format: wgpu::TextureFormat| { let (width, height, pixels) = image .map(|image| (image.width, image.height, image.pixels.as_slice())) .unwrap_or((1, 1, fallback.as_slice())); @@ -4010,7 +4028,7 @@ analytic={:.1} regular={:.1} blocks={:.1}", mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, + format, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); @@ -4030,8 +4048,21 @@ analytic={:.1} regular={:.1} blocks={:.1}", ); texture }; - self.background_texture = upload("background.texture", background, [0, 0, 0, 255]); - self.environment_texture = upload("environment.texture", environment, [128, 128, 128, 255]); + // The background image is drawn as it is, like the solid and gradient + // colours (an sRGB view would darken it a second time); the + // environment map stays sRGB for lighting. + self.background_texture = upload( + "background.texture", + background, + [0, 0, 0, 255], + wgpu::TextureFormat::Rgba8Unorm, + ); + self.environment_texture = upload( + "environment.texture", + environment, + [128, 128, 128, 255], + wgpu::TextureFormat::Rgba8UnormSrgb, + ); self.rebuild_frame_bind_group(device); self.background_source_id = background_id; self.environment_source_id = environment_id; @@ -4819,6 +4850,12 @@ analytic={:.1} regular={:.1} blocks={:.1}", } } } + // Transparent hatches blend over the lines already drawn; the + // lines in front of them still win the depth test. + if !self.skip_hatch_frame { + self.hatch_translucent_gpu + .draw(&mut pass, &self.uniform_bind_group, stencil_ref); + } } // โ”€โ”€ Pass 5c: SDF text quads (drawn over wires) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -5311,6 +5348,7 @@ analytic={:.1} regular={:.1} blocks={:.1}", self.gpu_preview_circles.clear(); self.gpu_preview_ellipses.clear(); self.hatch_gpu.clear(); + self.hatch_translucent_gpu.clear(); self.gpu_wipeouts.clear(); self.wipeout_skip_flags.clear(); self.gpu_images.clear(); diff --git a/src/scene/pipeline/wipeout_gpu.rs b/src/scene/pipeline/wipeout_gpu.rs index 621304ad5..ae1b058cf 100644 --- a/src/scene/pipeline/wipeout_gpu.rs +++ b/src/scene/pipeline/wipeout_gpu.rs @@ -172,23 +172,16 @@ impl WipeoutGpu { models: &[HatchModel], bgl1: &wgpu::BindGroupLayout, ) -> Vec { - let mut slots = rustc_hash::FxHashMap::default(); - let mut groups: Vec> = Vec::new(); - for (index, model) in models.iter().enumerate() { - let key = model - .render_instance - .map(|instance| (true, instance.source_id)) - .unwrap_or((false, index as u64)); - let slot = *slots.entry(key).or_insert_with(|| { - let slot = groups.len(); - groups.push(Vec::new()); - slot - }); - groups[slot].push(model); - } - groups - .into_iter() - .map(|group| Self::new(device, queue, &group, bgl1)) + // A source_id only identifies reusable render geometry, not an + // interchangeable hatch boundary in world space. Grouping masks by + // source_id can incorrectly reuse the first model's boundary, origin, + // plane and uniform for later INSERT copies. Unlike wires, a mask's + // GPU shader clips against its own boundary, so build each mask from + // its actual transformed model until we have a geometry-local, + // translation-safe instancing path. + models + .iter() + .map(|model| Self::new(device, queue, &[model], bgl1)) .collect() } diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index 91c331f3c..0c7178041 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -146,7 +146,7 @@ pub fn classify_wire(wire: &WireModel) -> WireKind { if !wire.display_visible { return WireKind::Invisible; } - if wire.render_instance.is_some() { + if wire.render_instance.is_some() || wire.stub.is_some() { return WireKind::Block; } if !wire.tangent_geoms.is_empty() @@ -176,7 +176,7 @@ pub fn classify_wire(wire: &WireModel) -> WireKind { pub struct PartitionedWires<'a> { pub regular: Vec<&'a WireModel>, pub mesh: Vec<&'a WireModel>, - pub instanced: Vec<&'a WireModel>, + pub instanced: Vec>, pub circle_instances: Vec, pub ellipse_instances: Vec, /// Handles that contributed to the retained analytical uploads. @@ -213,11 +213,11 @@ pub fn partition_wires<'a>( if !wire.display_visible { continue; } - if wire.render_instance.is_some() { + if wire.render_instance.is_some() || wire.stub.is_some() { if let Some(handle) = handle_of(wire) { contributors.insert(handle); } - instanced.push(wire); + super::wire_gpu::push_block_refs(wire, &mut instanced); continue; } // Resolve draw depth only for wires eligible for analytical extraction. diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 54a560397..bee7f0267 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -863,7 +863,7 @@ pub(crate) fn emit_wire_packed( wire.pattern[6], wire.pattern[7], ]; - let half_width = wire.line_weight_px * 0.5; + let half_width = wire.line_weight_px.max(1.0) * 0.5; let n = wire.points.len(); let seg_count = n.saturating_sub(1); if seg_count == 0 { @@ -965,7 +965,7 @@ pub(crate) fn emit_wire_native( wire.pattern[6], wire.pattern[7], ], - half_width: wire.line_weight_px * 0.5, + half_width: wire.line_weight_px.max(1.0) * 0.5, pattern_length: wire.pattern_length, draw_depth, align_end, @@ -1043,6 +1043,83 @@ pub fn fast_parse_u64(s: &str) -> Option { /// wires that carry no document handle). A wire carrying a block-local /// `depth_override` (a wide-polyline band inside a block) composes it into /// the insert's own sub-range so the band orders against its block siblings. +/// One block-instanced wire to draw: a wire of the set, or a prototype wire +/// reached through a stub, moved by `delta` with its depth shifted by +/// `depth_delta` and owned (named, depth-ordered) by the stub `owner`. +#[derive(Clone, Copy)] +pub struct BlockRef<'a> { + pub wire: &'a WireModel, + pub delta: [f64; 3], + pub depth_delta: f32, + pub owner: Option<&'a WireModel>, +} + +impl<'a> From<&'a WireModel> for BlockRef<'a> { + fn from(wire: &'a WireModel) -> Self { + Self { + wire, + delta: [0.0; 3], + depth_delta: 0.0, + owner: None, + } + } +} + +impl<'a> BlockRef<'a> { + /// The wire whose name and visibility stand for this copy. + pub fn named(&self) -> &'a WireModel { + self.owner.unwrap_or(self.wire) + } + + /// The copy's draw depth: its owner's slot, the prototype wire's + /// block-local depth shifted for this copy. + pub fn draw_depth(&self, depth_map: &rustc_hash::FxHashMap) -> f32 { + let base = fast_parse_u64(&self.named().name).and_then(|h| depth_map.get(&h).copied()); + match (base, self.wire.depth_override) { + (Some([d, half]), Some(local)) => d + (local + self.depth_delta) * half, + (Some([d, _]), None) => d, + (None, _) => 0.0, + } + } +} + +/// Push the block-instanced wires `wire` stands for: itself, or every copy a +/// stub holds (stubs inside stubs included). +pub fn push_block_refs<'a>(wire: &'a WireModel, out: &mut Vec>) { + fn walk<'a>( + wire: &'a WireModel, + delta: [f64; 3], + depth_delta: f32, + owner: &'a WireModel, + out: &mut Vec>, + ) { + match &wire.stub { + Some(stub) => { + let delta = [ + delta[0] + stub.delta[0], + delta[1] + stub.delta[1], + delta[2] + stub.delta[2], + ]; + for inner in stub.prototype.iter() { + walk(inner, delta, depth_delta + stub.depth_delta, owner, out); + } + } + None if wire.render_instance.is_some() => out.push(BlockRef { + wire, + delta, + depth_delta, + owner: Some(owner), + }), + None => {} + } + } + if wire.stub.is_some() { + walk(wire, [0.0; 3], 0.0, wire, out); + } else if wire.render_instance.is_some() { + out.push(BlockRef::from(wire)); + } +} + pub(crate) fn wire_draw_depth( wire: &WireModel, depth_map: &rustc_hash::FxHashMap, @@ -1121,7 +1198,7 @@ impl BlockWireGpu { pub fn from_wires( device: &wgpu::Device, queue: &wgpu::Queue, - wires: &[&WireModel], + wires: &[BlockRef<'_>], depth_map: &rustc_hash::FxHashMap, color_override: Option<[f32; 4]>, target: BlockWireTarget<'_>, @@ -1129,9 +1206,12 @@ impl BlockWireGpu { ) -> Vec { let BlockWireTarget { const_bgl, mode } = target; let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> = rustc_hash::FxHashMap::default(); - let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new(); - for &wire in wires { - if color_override.is_none() && !wire.display_visible { + let mut groups: Vec<(bool, Vec>)> = Vec::new(); + for &block in wires { + let wire = block.wire; + if color_override.is_none() + && !(wire.display_visible && block.named().display_visible) + { continue; } let Some(instance) = wire.render_instance else { @@ -1147,7 +1227,7 @@ impl BlockWireGpu { groups.push((mesh_edge, Vec::new())); slot }); - groups[slot].1.push(wire); + groups[slot].1.push(block); } let mut arena = BlockArenaBuilder::new(device, mode); @@ -1157,7 +1237,10 @@ impl BlockWireGpu { // visible rather than assumed. let mut emitted_segments = 0usize; for (mesh_edge, group) in groups { - let Some(&source) = group.first() else { + // A copy reached through a stub carries the prototype's + // geometry at the prototype's placement, which is what `base` + // means; its own `delta` only moves the instance. + let Some(source) = group.first().map(|block| block.wire) else { continue; }; let Some(base) = source.render_instance else { @@ -1460,19 +1543,19 @@ impl BlockArenaBuilder { /// Placements and draw depths relative to the group's first instance. fn block_instances( - group: &[&WireModel], + group: &[BlockRef<'_>], base: crate::scene::model::instance_model::RenderInstance, mesh_edge: bool, depth_map: &rustc_hash::FxHashMap, ) -> Vec { group .iter() - .filter_map(|wire| { - let instance = wire.render_instance?; + .filter_map(|block| { + let instance = block.wire.render_instance?; let delta = [ - instance.translation[0] - base.translation[0], - instance.translation[1] - base.translation[1], - instance.translation[2] - base.translation[2], + instance.translation[0] + block.delta[0] - base.translation[0], + instance.translation[1] + block.delta[1] - base.translation[1], + instance.translation[2] + block.delta[2] - base.translation[2], ]; let high = delta.map(|value| value as f32); Some(BlockWireInstance { @@ -1486,7 +1569,7 @@ fn block_instances( if mesh_edge { 0.0 } else { - wire_draw_depth(wire, depth_map) + block.draw_depth(depth_map) }, 0.0, ], diff --git a/src/scene/project.rs b/src/scene/project.rs index 2fb8bfd45..a2433eadf 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -84,6 +84,7 @@ impl Scene { // path wants the full-fidelity (no-LOD-stub) wire list, // regardless of paper zoom. let model_wires = self.model_wires_for_viewport_arc(vp_handle, 0.0); + let model_wires = crate::scene::model::instance_model::expand_stubs(&model_wires); // โ”€โ”€ Project and clip wires into viewport โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ let vp_x0 = pcx - hw; diff --git a/src/scene/section_cull.rs b/src/scene/section_cull.rs new file mode 100644 index 000000000..66ef771ea --- /dev/null +++ b/src/scene/section_cull.rs @@ -0,0 +1,229 @@ +//! SECTIONCULL: keep only what lies inside a section object โ€” the part a +//! live section shows โ€” and remove the rest for good. Solids, surfaces and +//! closed meshes are cut to it, open meshes are clipped face by face, and +//! every other object wholly outside it is erased. Objects across its +//! boundary that cannot be cut stay whole. + +use codec::{EntityType, Handle}; +use glam::DVec3; +use rustc_hash::FxHashMap; + +use super::Scene; + +/// What culling to a section does to the space it lives in. +#[derive(Default)] +pub struct SectionCull { + /// Bodies the section crosses, with what stays of each. + pub cut: Vec<(Handle, kernel::brep::Body)>, + /// Open meshes the section crosses, clipped to it. + pub clipped: Vec, + /// Objects wholly outside the section. + pub outside: Vec, +} + +enum Place { + Inside, + Outside, + Across, +} + +const TOLERANCE: f64 = 1e-9; + +fn place(planes: &[kernel::space::Plane], points: &[[f64; 3]]) -> Place { + let mut inside = true; + for plane in planes { + let behind = points + .iter() + .filter(|point| plane.distance_to(**point).unwrap_or(0.0) < -TOLERANCE) + .count(); + if behind == points.len() { + return Place::Outside; + } + inside &= behind == 0; + } + if inside { + Place::Inside + } else { + Place::Across + } +} + +fn same_extent(a: &kernel::brep::Body, b: &kernel::brep::Body) -> bool { + match (kernel::brep::body_bounds(a), kernel::brep::body_bounds(b)) { + (Some(a), Some(b)) => (0..3).all(|axis| { + (a.min[axis] - b.min[axis]).abs() <= 1e-7 && (a.max[axis] - b.max[axis]).abs() <= 1e-7 + }), + _ => false, + } +} + +impl Scene { + /// The section SECTIONCULL works with: the selected section object, or + /// else the live section of the current space. + pub fn cull_section(&self) -> Option { + let is_section = |handle: &Handle| { + matches!( + self.document.get_entity(*handle), + Some(EntityType::Extended(extended)) + if matches!( + extended.data, + codec::entities::ExtendedEntityData::SectionObject(_) + ) + ) + }; + if let Some(handle) = self.selected_handles_in_order().into_iter().find(is_section) { + return Some(handle); + } + let block = self.current_layout_block_handle(); + self.document + .entities() + .filter(|entity| match entity { + EntityType::Extended(extended) => match &extended.data { + codec::entities::ExtendedEntityData::SectionObject(data) => { + let owner = extended.common.owner_handle; + data.flags & 1 != 0 + && !extended.common.invisible + && (owner.is_null() || owner == block) + } + _ => false, + }, + _ => false, + }) + .map(|entity| entity.common().handle) + .last() + } + + /// Plan culling the current space to section object `section`; `None` + /// when it is not a section object. + pub fn section_cull(&self, section: Handle) -> Option { + let Some(EntityType::Extended(extended)) = self.document.get_entity(section) else { + return None; + }; + let codec::entities::ExtendedEntityData::SectionObject(data) = &extended.data else { + return None; + }; + let slice_depth = crate::entities::extended::section_is_slice(extended).then(|| { + crate::entities::extended::section_slice_depth(extended).unwrap_or(0.0) + }); + let planes = Self::section_planes(data, slice_depth, None)?; + + // Every drawn point of every object in the space, from its wires and + // its shaded mesh. + let block = self.current_layout_block_handle(); + let mut points: FxHashMap> = FxHashMap::default(); + let wires = self.wires_for_block_culled( + block, + None, + None, + None, + None, + None, + true, + None, + ); + for wire in &wires { + let Some(handle) = Self::handle_from_wire_name(&wire.name) else { + continue; + }; + let entry = points.entry(handle).or_default(); + for (index, &[x, y, z]) in wire.points.iter().enumerate() { + let [lx, ly, lz] = wire.points_low.get(index).copied().unwrap_or([0.0; 3]); + let point = DVec3::new( + f64::from(x) + f64::from(lx), + f64::from(y) + f64::from(ly), + f64::from(z) + f64::from(lz), + ); + if point.is_finite() { + entry.push(point.to_array()); + } + } + } + for set in self.interaction_meshes_arc().iter() { + let handle = set.entity_handle(); + let (Some(handle), Some(aabb)) = (handle, super::mesh_interaction_aabb(set)) else { + continue; + }; + let entry = points.entry(handle).or_default(); + for corner in 0..8 { + entry.push([ + aabb[if corner & 1 == 0 { 0 } else { 3 }], + aabb[if corner & 2 == 0 { 1 } else { 4 }], + aabb[if corner & 4 == 0 { 2 } else { 5 }], + ]); + } + } + + let mut cull = SectionCull::default(); + // A live section hides what lies wholly outside it, so the space's + // own objects are walked too. + let mut handles: Vec = points + .keys() + .copied() + .chain( + self.document + .entities() + .filter(|entity| entity.common().owner_handle == block) + .map(|entity| entity.common().handle), + ) + .collect(); + handles.sort_unstable_by_key(|handle| handle.value()); + handles.dedup(); + for handle in handles { + if handle == section || self.is_layer_locked(handle) { + continue; + } + let Some(entity) = self.document.get_entity(handle) else { + continue; + }; + // Bodies are cut with the kernel: their drawn wires may already + // show the live cut, so they say nothing about the source. + if let Some(body) = self.section_source_body(handle) { + match Self::section_body(&body, data, slice_depth, None) { + Ok(None) => cull.outside.push(handle), + Ok(Some(kept)) if !same_extent(&body, &kept) => cull.cut.push((handle, kept)), + _ => {} + } + continue; + } + if let Some((vertices, _)) = crate::entities::mesh::mesh_polygons(entity) { + match place(&planes, &vertices) { + Place::Outside => cull.outside.push(handle), + Place::Inside => {} + Place::Across => { + let mut mesh = entity.clone(); + for plane in &planes { + let Some((vertices, faces)) = + crate::entities::mesh::mesh_polygons(&mesh) + else { + break; + }; + let side = |point: [f64; 3]| plane.distance_to(point).unwrap_or(0.0); + if vertices.iter().all(|point| side(*point) >= -TOLERANCE) { + continue; + } + let Some(mut clipped) = crate::entities::mesh::clipped_mesh( + &vertices, + &faces, + side, + true, + mesh.common(), + ) else { + break; + }; + clipped.common.handle = handle; + mesh = EntityType::Mesh(clipped); + } + cull.clipped.push(mesh); + } + } + continue; + } + if let Some(points) = points.get(&handle) { + if matches!(place(&planes, points), Place::Outside) { + cull.outside.push(handle); + } + } + } + Some(cull) + } +} diff --git a/src/scene/selection.rs b/src/scene/selection.rs index d80d5c7fb..f4671b6dc 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -165,6 +165,7 @@ impl Scene { pub fn deselect_all(&mut self) { self.selected_constraint = None; + self.sub_segments.clear(); if self.selected.is_empty() { return; } @@ -426,6 +427,27 @@ impl Scene { value: &str, mode: crate::app::QSelectMode, append: bool, + ) -> usize { + let condition = crate::app::QSelectCondition { + type_filter: type_name.map(str::to_owned), + property: property_field.map(|field| crate::app::QSelectPropertyChoice { + field: field.to_owned(), + label: field.to_owned(), + editor: crate::app::QSelectValueEditor::Text, + }), + operator: op, + value: value.to_owned(), + }; + self.qselect_conditions(scope, &[condition], mode, append) + } + + /// Evaluate all conditions against the same candidate set, then apply result behavior once. + pub fn qselect_conditions( + &mut self, + scope: crate::app::QSelectScope, + conditions: &[crate::app::QSelectCondition], + mode: crate::app::QSelectMode, + append: bool, ) -> usize { use crate::app::{QSelectMode, QSelectOp}; use crate::entities::traits::entity_type_name; @@ -436,33 +458,44 @@ impl Scene { let Some(e) = self.document.get_entity(h) else { continue; }; - let type_ok = type_name.is_none_or(|t| entity_type_name(e) == t); - let prop_ok = if !type_ok { - true - } else { - match (property_field, op) { - (None, _) | (_, QSelectOp::Any) => true, - (Some(field), op) => match self.entity_property_value(e, field) { - Some(actual) => match op { - QSelectOp::Eq => actual.eq_ignore_ascii_case(value), - QSelectOp::Neq => !actual.eq_ignore_ascii_case(value), - QSelectOp::Gt | QSelectOp::Lt => { - match ( - crate::entities::common::parse_f64(&actual), - crate::entities::common::parse_f64(value), - ) { - (Some(a), Some(b)) if matches!(op, QSelectOp::Gt) => a > b, - (Some(a), Some(b)) => a < b, - _ => false, + let matches_filter = conditions.iter().all(|condition| { + let type_ok = condition + .type_filter + .as_deref() + .is_none_or(|t| entity_type_name(e) == t); + let prop_ok = if !type_ok { + true + } else { + match ( + condition + .property + .as_ref() + .map(|property| property.field.as_str()), + condition.operator, + ) { + (None, _) | (_, QSelectOp::Any) => true, + (Some(field), op) => match self.entity_property_value(e, field) { + Some(actual) => match op { + QSelectOp::Eq => actual.eq_ignore_ascii_case(&condition.value), + QSelectOp::Neq => !actual.eq_ignore_ascii_case(&condition.value), + QSelectOp::Gt | QSelectOp::Lt => { + match ( + crate::entities::common::parse_f64(&actual), + crate::entities::common::parse_f64(&condition.value), + ) { + (Some(a), Some(b)) if matches!(op, QSelectOp::Gt) => a > b, + (Some(a), Some(b)) => a < b, + _ => false, + } } - } - QSelectOp::Any => true, + QSelectOp::Any => true, + }, + None => false, }, - None => false, - }, - } - }; - let matches_filter = type_ok && prop_ok; + } + }; + type_ok && prop_ok + }); let keep = match mode { QSelectMode::Include => matches_filter, QSelectMode::Exclude => !matches_filter, @@ -582,6 +615,17 @@ impl Scene { &self, type_name: Option<&str>, scope: crate::app::QSelectScope, + ) -> Vec { + self.qselect_properties_with_sample(type_name, scope, None) + } + + /// Use the picked entity's own property schema, including optional fields + /// that may not be present on the first entity of this type in the drawing. + fn qselect_properties_with_sample( + &self, + type_name: Option<&str>, + scope: crate::app::QSelectScope, + sample_handle: Option, ) -> Vec { use crate::app::{QSelectPropertyChoice, QSelectValueEditor}; use crate::entities::traits::{entity_type_name, EntityTypeOps}; @@ -680,11 +724,16 @@ impl Scene { .iter() .map(|s| s.name.clone()) .collect(); - let sample = candidate_handles - .iter() - .copied() - .filter_map(|h| self.document.get_entity(h)) - .find(|e| entity_type_name(e) == t); + let sample = sample_handle + .and_then(|handle| self.document.get_entity(handle)) + .filter(|entity| entity_type_name(entity) == t) + .or_else(|| { + candidate_handles + .iter() + .copied() + .filter_map(|h| self.document.get_entity(h)) + .find(|e| entity_type_name(e) == t) + }); if let Some(sample) = sample { let mut sections = vec![crate::scene::cache::properties::general_section(sample)]; if let Some(section) = @@ -836,6 +885,58 @@ impl Scene { out } + /// Build editable equality conditions without changing the current selection. + /// The unique handle and empty values are deliberately not sampled. + pub fn qselect_conditions_from_entity( + &self, + handle: Handle, + ) -> Vec { + use crate::app::{QSelectCondition, QSelectOp, QSelectScope, QSelectValueEditor}; + let Some(entity) = self.document.get_entity(handle) else { + return Vec::new(); + }; + let type_name = crate::entities::traits::entity_type_name(entity).to_owned(); + let mut conditions = vec![QSelectCondition { + type_filter: Some(type_name.clone()), + property: None, + operator: QSelectOp::Eq, + value: String::new(), + }]; + let properties = self.qselect_properties_with_sample( + Some(&type_name), + QSelectScope::CurrentSpace, + Some(handle), + ); + for mut property in properties { + if property.field == "handle" { + continue; + } + let Some(value) = self.entity_property_value(entity, &property.field) else { + continue; + }; + if value.trim().is_empty() { + continue; + } + // Ensure a sampled choice can also be edited, even outside the + // filter's current-selection scope. + if let QSelectValueEditor::Choice(options) = &mut property.editor { + if !options.contains(&value) { + options.push(value.clone()); + } + } + let condition = QSelectCondition { + type_filter: Some(type_name.clone()), + property: Some(property), + operator: QSelectOp::Eq, + value, + }; + if condition.validation_error().is_none() { + conditions.push(condition); + } + } + conditions + } + /// Reads a property value from an entity for QSELECT comparison. /// Returns the canonical string used as the left-hand side of the /// operator test. Common properties have hand-rolled formatting so @@ -1172,6 +1273,50 @@ impl Scene { #[cfg(test)] mod tests { + #[test] + fn qselect_sample_matches_identical_entities_without_unique_handles() { + use crate::app::{QSelectMode, QSelectScope}; + use codec::entities::Line; + use codec::types::Vector3; + let mut scene = Scene::new(); + let line = || { + EntityType::Line(Line::from_points( + Vector3::ZERO, + Vector3::new(10.0, 0.0, 0.0), + )) + }; + let first = scene.add_entity(line()); + let second = scene.add_entity(line()); + scene.select_entity(second, false); + let before = scene.selected.clone(); + let conditions = scene.qselect_conditions_from_entity(first); + assert_eq!( + scene.selected, before, + "sampling must not change the selection" + ); + assert!(conditions.iter().any(|condition| condition + .property + .as_ref() + .is_some_and(|property| property.field == "layer"))); + assert!(conditions.iter().all(|condition| condition + .property + .as_ref() + .is_none_or(|property| property.field != "handle"))); + assert!(conditions + .iter() + .all(|condition| condition.validation_error().is_none())); + assert_eq!( + scene.qselect_conditions( + QSelectScope::CurrentSpace, + &conditions, + QSelectMode::Include, + false, + ), + 2 + ); + assert!(scene.selected.contains(&first) && scene.selected.contains(&second)); + } + #[test] fn pe_url_of_reads_standard_hyperlink_xdata() { use codec::entities::Point; diff --git a/src/scene/sub_select.rs b/src/scene/sub_select.rs new file mode 100644 index 000000000..adf9d4364 --- /dev/null +++ b/src/scene/sub_select.rs @@ -0,0 +1,159 @@ +//! Picking part of an object: Ctrl+click on a polyline selects the segment +//! under the cursor. The polyline joins the selection, only its picked +//! segments are highlighted, and MOVE / ROTATE / SCALE / MIRROR then change +//! just the vertices of those segments, stretching the neighbours. + +use std::collections::BTreeSet; + +use codec::entities::LwPolyline; +use codec::{EntityType, Handle}; +use glam::DVec3; + +use super::Scene; +use crate::command::EntityTransform; + +/// Segment `index` of `polyline` on its own: its two vertices, with the +/// bulge of the first. +fn segment(polyline: &LwPolyline, index: usize) -> Option { + let count = polyline.vertices.len(); + let next = if index + 1 < count { + index + 1 + } else if polyline.is_closed && count > 2 { + 0 + } else { + return None; + }; + let mut part = polyline.clone(); + part.vertices = vec![ + polyline.vertices[index].clone(), + polyline.vertices[next].clone(), + ]; + part.vertices[1].bulge = 0.0; + part.is_closed = false; + Some(part) +} + +fn segment_points(polyline: &LwPolyline, index: usize) -> Vec<[f64; 3]> { + segment(polyline, index) + .and_then(|part| crate::entities::curve::lwpolyline_curve(&part)) + .map(|curve| crate::entities::curve::curve_points(&curve)) + .unwrap_or_default() +} + +impl Scene { + /// Ctrl+click: toggle the segment of polyline `handle` nearest `point` + /// and keep the polyline selected while any of its segments is. `false` + /// when `handle` is not a polyline. + pub fn toggle_sub_segment(&mut self, handle: Handle, point: DVec3) -> bool { + let Some(EntityType::LwPolyline(polyline)) = self.document.get_entity(handle) else { + return false; + }; + let nearest = (0..polyline.vertices.len()) + .filter_map(|index| { + let distance = segment_points(polyline, index) + .windows(2) + .map(|pair| { + let a = DVec3::from_array(pair[0]).truncate(); + let b = DVec3::from_array(pair[1]).truncate(); + let p = point.truncate(); + let t = ((p - a).dot(b - a) / (b - a).length_squared().max(1e-300)) + .clamp(0.0, 1.0); + p.distance(a + (b - a) * t) + }) + .fold(f64::INFINITY, f64::min); + distance.is_finite().then_some((index, distance)) + }) + .min_by(|a, b| a.1.total_cmp(&b.1)); + let Some((index, _)) = nearest else { + return false; + }; + if !self.selected.contains(&handle) { + self.sub_segments.remove(&handle); + } + let segments = self.sub_segments.entry(handle).or_default(); + if !segments.remove(&index) { + segments.insert(index); + } + if segments.is_empty() { + self.sub_segments.remove(&handle); + self.deselect_entity(handle); + } else { + self.select_entity(handle, false); + } + self.bump_selection_set(); + true + } + + /// The picked segments of a selected polyline, if only part of it is. + pub(crate) fn picked_segments(&self, handle: Handle) -> Option<&BTreeSet> { + self.selected + .contains(&handle) + .then(|| self.sub_segments.get(&handle)) + .flatten() + } + + /// World points of the picked segments of `handle`, NaN-separated, for + /// the selection highlight. + pub(crate) fn picked_segment_points(&self, handle: Handle) -> Option> { + let segments = self.picked_segments(handle)?; + let Some(EntityType::LwPolyline(polyline)) = self.document.get_entity(handle) else { + return None; + }; + let mut points = Vec::new(); + for &index in segments { + points.extend(segment_points(polyline, index)); + points.push([f64::NAN; 3]); + } + Some(points) + } + + /// Apply `transform` to just the vertices of the picked segments of each + /// partly selected polyline in `handles`. Returns the handles left to + /// transform whole. + pub fn transform_picked_segments( + &mut self, + handles: &[Handle], + transform: &EntityTransform, + ) -> Vec { + let mut whole = Vec::with_capacity(handles.len()); + for &handle in handles { + let (Some(segments), Some(EntityType::LwPolyline(polyline))) = ( + self.picked_segments(handle), + self.document.get_entity(handle), + ) else { + whole.push(handle); + continue; + }; + let count = polyline.vertices.len(); + let vertices: BTreeSet = segments + .iter() + .flat_map(|&index| [index, (index + 1) % count]) + .collect(); + let Some(plane) = crate::entities::curve::lwpolyline_curve(polyline) + .map(|curve| curve.plane) + else { + whole.push(handle); + continue; + }; + let mut moved = polyline.clone(); + for index in vertices { + let vertex = &mut moved.vertices[index]; + let world = plane.point_at([vertex.location.x, vertex.location.y]); + let mut marker = EntityType::Point(codec::entities::Point::at( + codec::types::Vector3::new(world[0], world[1], world[2]), + )); + super::view::dispatch::apply_transform(&mut marker, transform); + let EntityType::Point(marker) = marker else { + continue; + }; + let location = marker.location; + if let Some([x, y]) = plane.project([location.x, location.y, location.z]) { + vertex.location.x = x; + vertex.location.y = y; + } + } + self.update_entity(EntityType::LwPolyline(moved)); + } + whole + } +} diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index 56a3c6f92..e285497f4 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -386,6 +386,7 @@ pub fn apply_along( fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, + stub: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, diff --git a/src/scene/text/lff.rs b/src/scene/text/lff.rs index 1e1c5c73d..d942cfd52 100644 --- a/src/scene/text/lff.rs +++ b/src/scene/text/lff.rs @@ -518,10 +518,19 @@ fn fonts_map() -> &'static HashMap { pub fn is_builtin(name: &str) -> bool { let map = fonts_map(); let key = name.trim().to_ascii_uppercase(); - map.contains_key(&key) - || key - .rsplit_once('.') - .is_some_and(|(stem, _)| map.contains_key(stem)) + let stem = key.rsplit_once('.').map_or(key.as_str(), |(stem, _)| stem); + (map.contains_key(&key) || map.contains_key(stem)) && !installed_alias(stem, name) +} + +/// An alias names a real font (ISOCPEUR is a TrueType face): an installed +/// copy of exactly that font draws it, Cyrillic included, where the LFF +/// stand-in has no such glyphs (#1764). +fn installed_alias(stem: &str, name: &str) -> bool { + use crate::scene::text::sysfont; + ALIASES.iter().any(|(alias, _)| *alias == stem) + && (sysfont::family_for_file(name).is_some() + || sysfont::canonical_family_name(stem) + .is_some_and(|family| family.eq_ignore_ascii_case(stem))) } pub fn get_font(name: &str) -> &'static Font { diff --git a/src/scene/text/shx.rs b/src/scene/text/shx.rs index 3a93db7a7..49e9807d0 100644 --- a/src/scene/text/shx.rs +++ b/src/scene/text/shx.rs @@ -617,13 +617,21 @@ fn interpret( if n_oct == 0.0 { n_oct = 8.0; } + // The arc spans `n_oct` octants from `start_oct`, starting + // `so`/256 of an octant into the first one and ending `eo`/256 + // into the last (0 = on its far boundary) (#1638). let q = std::f64::consts::FRAC_PI_4; - let start = start_oct * q + so * q / 256.0 * if cw { -1.0 } else { 1.0 }; - let total = n_oct * q; - let sweep_mag = total - so * q / 256.0 - (255.0 - eo) * q / 256.0 - + if eo == 0.0 { 0.0 } else { 0.0 }; - let sweep = sweep_mag.max(0.0) * if cw { -1.0 } else { 1.0 }; - emit_arc(st, cur, out, radius, start, sweep); + let dir = if cw { -1.0 } else { 1.0 }; + let start = (start_oct + dir * so / 256.0) * q; + let end = if eo == 0.0 { + (start_oct + dir * n_oct) * q + } else { + (start_oct + dir * (n_oct - 1.0 + eo / 256.0)) * q + }; + let sweep = end - start; + if sweep * dir > 0.0 { + emit_arc(st, cur, out, radius, start, sweep); + } } } 0xC => { @@ -754,4 +762,30 @@ mod tests { ); assert!(font_glyph(&path, 'B' as u16).is_none()); } + + /// The `9` program of the ISOCP fonts: a fractional arc into the stem, + /// then octant arcs. Its ink spans 36 font units, the cap height; the + /// fractional arc used to end short and drop the glyph to ~0.68 (#1638). + #[test] + fn fractional_arc_spans_its_octants() { + const NINE: [u8; 25] = [ + 0x02, 0x08, 0x0a, 0x02, 0x01, 0x0b, 0x6c, 0x00, 0x00, 0x12, 0x62, 0xb4, 0x0a, 0x08, + 0x04, 0x4c, 0x0a, 0x08, 0x42, 0x80, 0x02, 0x08, 0x06, 0xee, 0x00, + ]; + let mut b = b"AutoCAD-86 unifont 1.0\r\n\x1a".to_vec(); + b.extend_from_slice(&2u32.to_le_bytes()); + let head: Vec = [b"UNI\x00".as_slice(), &[36u8, 8, 2, 0, 0, 0]].concat(); + b.extend_from_slice(&(head.len() as u16).to_le_bytes()); + b.extend_from_slice(&head); + let glyph: Vec = [b"\x00".as_slice(), &NINE].concat(); + b.extend_from_slice(&('9' as u16).to_le_bytes()); + b.extend_from_slice(&(glyph.len() as u16).to_le_bytes()); + b.extend_from_slice(&glyph); + let path = tmp("nine.shx", &b); + let g = font_glyph(&path, '9' as u16).expect("glyph for 9"); + let ys = g.strokes.iter().flatten().map(|p| p[1]); + let (lo, hi) = ys.fold((f32::MAX, f32::MIN), |(lo, hi), y| (lo.min(y), hi.max(y))); + // 36 font units at above = 36 โ†’ the 9-unit cap height. + assert!(((hi - lo) - 9.0).abs() < 0.05, "ink height {}", hi - lo); + } } diff --git a/src/scene/text/sysfont.rs b/src/scene/text/sysfont.rs index 60dfe96d4..0c2e25cae 100644 --- a/src/scene/text/sysfont.rs +++ b/src/scene/text/sysfont.rs @@ -22,8 +22,9 @@ struct SysFonts { families: Mutex>, /// Installed face file name (lower-case) โ†’ the family that face holds. /// A drawing references a TrueType style by *file* name, so this is what - /// makes such a style resolve โ€” see [`family_for_reference`]. - by_file: HashMap, + /// makes such a style resolve โ€” see [`family_for_reference`]. Files a host + /// registers at run time join it ([`register_font_file`]). + by_file: Mutex>, } static FONTS: OnceLock = OnceLock::new(); @@ -60,7 +61,7 @@ fn fonts() -> &'static SysFonts { SysFonts { db: Mutex::new(db), families: Mutex::new(families), - by_file, + by_file: Mutex::new(by_file), } }) } @@ -315,12 +316,17 @@ pub fn with_face_data_weighted( /// The family of the installed face whose file is named `name` โ€” a bare file /// name or any path to one, matched case-insensitively. -fn family_for_file(name: &str) -> Option { +pub(crate) fn family_for_file(name: &str) -> Option { let file = name.rsplit(['/', '\\']).next()?.trim(); if file.is_empty() { return None; } - fonts().by_file.get(&file.to_ascii_lowercase()).cloned() + fonts() + .by_file + .lock() + .unwrap() + .get(&file.to_ascii_lowercase()) + .cloned() } /// The installed family a drawing's font reference names, or `None` when @@ -371,10 +377,70 @@ pub fn register_web_font(bytes: &[u8]) { canonical_cache().lock().unwrap().clear(); } +/// Add a font file a host supplies at run time (the `font_register` control +/// op): its faces join the database and `file_name` resolves to the family of +/// the first face, so a text style naming that file finds it. Returns that +/// family and the number of faces the file added, or `None` when the bytes +/// hold no face fontdb can read. Like [`register_web_font`], the family list +/// is rebuilt and the canonical-name memo cleared; callers invalidate the glyph +/// caches and re-layout text. +pub fn register_font_file(file_name: &str, bytes: &[u8]) -> Option<(String, usize)> { + let file = file_name.rsplit(['/', '\\']).next()?.trim(); + if file.is_empty() { + return None; + } + let sys = fonts(); + let (family, faces, names) = { + let mut db = sys.db.lock().unwrap(); + let before: std::collections::HashSet = db.faces().map(|f| f.id).collect(); + db.load_font_data(bytes.to_vec()); + let added: Vec = db + .faces() + .filter(|face| !before.contains(&face.id)) + .filter_map(|face| face.families.first().map(|(name, _)| name.clone())) + .collect(); + let family = added.first()?.clone(); + (family, added.len(), family_names(&db)) + }; + sys.by_file + .lock() + .unwrap() + .insert(file.to_ascii_lowercase(), family.clone()); + *sys.families.lock().unwrap() = names; + canonical_cache().lock().unwrap().clear(); + Some((family, faces)) +} + #[cfg(test)] mod tests { use super::*; + /// A host-registered file resolves by its file name โ€” any case, bare or + /// with a path โ€” to the family inside it; bytes without a face are refused + /// and leave nothing behind. + #[test] + fn registered_font_file_resolves_by_file_name() { + let bytes = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/web/fonts/greek.ttf")) + .expect("bundled web font"); + let (family, faces) = + register_font_file("C:\\Fonts\\HostRegistered-Test.ttf", &bytes).expect("a face"); + assert!(faces >= 1); + assert!(!family.is_empty()); + assert_eq!( + family_for_reference("hostregistered-test.TTF").as_deref(), + Some(family.as_str()) + ); + assert_eq!( + family_for_reference("fonts/HostRegistered-Test.ttf").as_deref(), + Some(family.as_str()) + ); + assert!(has_family(&family)); + + assert_eq!(register_font_file("Broken-Test.ttf", b"not a font"), None); + assert_eq!(family_for_reference("Broken-Test.ttf"), None); + assert_eq!(register_font_file("", &bytes), None); + } + /// A DWG stores a style's TrueType font as a file name, so every installed /// face has to be reachable by the name of its file โ€” case-insensitively, /// and whether it comes as a bare name or as a path. diff --git a/src/scene/view/mod.rs b/src/scene/view/mod.rs index cae922acb..e8cef8c99 100644 --- a/src/scene/view/mod.rs +++ b/src/scene/view/mod.rs @@ -3,3 +3,4 @@ pub mod dispatch; pub mod render; pub mod transform; pub mod viewport_pane; +pub use render::device_scale; diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index c91d30ae2..7a8f9b99e 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -385,6 +385,7 @@ impl shader::Primitive for Primitive { pipeline.gpu_error_epoch = errors_at_entry; } let scale = viewport.scale_factor() as f32; + DEVICE_SCALE.store(scale.to_bits(), std::sync::atomic::Ordering::Relaxed); let instance_ids: Vec = self.viewports.iter().map(|vp| vp.instance_id).collect(); let slots = pipeline.resolve_slots(device, queue, &instance_ids); for (i, vp) in self.viewports.iter().enumerate() { @@ -431,7 +432,14 @@ impl shader::Primitive for Primitive { vp.background_image.as_ref(), vp.environment_image.as_ref(), ); - inner.upload_uniforms(device, queue, &vp.uniforms); + // The scene works in logical pixels; an image background keeps + // its size in device pixels. + let mut uniforms = vp.uniforms; + // The image's size and offset are in device pixels. + for value in &mut uniforms.background_image_transform { + *value /= scale.max(1e-3); + } + inner.upload_uniforms(device, queue, &uniforms); // โ”€โ”€ Scene-render cache โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // A pure cursor move โ€” or any frame where the view, geometry, @@ -2034,6 +2042,11 @@ text_atlas={:.1} wire_arena={:.1} block_geometry={:.1}", } impl Scene { + /// The model-space viewport record of the active tile. + pub(crate) fn active_tile_vport_handle(&self) -> Option { + self.model_tile_vport(self.active_model_tile.get()).map(|value| value.handle) + } + fn model_tile_vport(&self, index: usize) -> Option<&codec::tables::VPort> { let rect = self.model_tiles.borrow().get(index)?.rect; let lower_left = [rect.x as f64, (1.0 - rect.y - rect.height) as f64]; @@ -2547,30 +2560,32 @@ impl Scene { if viewport.paper_sheet { let handle = self.current_layout_sheet_viewport_handle(); if let Some(EntityType::Viewport(value)) = self.document.get_entity(handle) { - return build( - value.visual_style_handle, - value.brightness, - value.contrast, - value.background_handle, - ); + // The sheet keeps its page and desk: a paper-space view's + // background is kept with the layout but not drawn. + return build(value.visual_style_handle, value.brightness, value.contrast, Handle::NULL); } } else if viewport.handle.is_valid() { if let Some(EntityType::Viewport(value)) = self.document.get_entity(viewport.handle) { + let flat = value.render_mode == codec::entities::ViewportRenderMode::Wireframe2D; return build( value.visual_style_handle, value.brightness, value.contrast, - value.background_handle, + if flat { Handle::NULL } else { value.background_handle }, ); } } else if self.current_layout == "Model" { let index = viewport.tile_idx.unwrap_or(0); if let Some(value) = self.model_tile_vport(index) { + // A 2D wireframe view shows no background, as in the reference. + let flat = self.model_tiles.borrow().get(index).is_some_and(|tile| { + tile.render_mode == codec::entities::ViewportRenderMode::Wireframe2D + }); return build( value.visual_style_handle, value.brightness, value.contrast, - value.background_handle, + if flat { Handle::NULL } else { value.background_handle }, ); } } @@ -2973,7 +2988,8 @@ impl Scene { 2.0, background.horizon as f32, background.height as f32, - background.rotation as f32, + // Stored in degrees. + (background.rotation as f32).to_radians(), ]; result } @@ -2999,11 +3015,13 @@ impl Scene { background.use_tiling as u8 as f32, image.width as f32 / image.height.max(1) as f32, ]; + // Offset, then the image's size in pixels at its scale: + // an image not fitted to the screen keeps its pixel size. result.image_transform = [ background.offset.x as f32, background.offset.y as f32, - background.scale.x as f32, - background.scale.y as f32, + background.scale.x as f32 * image.width as f32, + background.scale.y as f32 * image.height as f32, ]; result.params[0] = 4.0; result.image = Some(image); @@ -3571,9 +3589,11 @@ pub struct InheritStyle { } /// Convert a concrete (already layer-resolved) lineweight to display pixels. +/// Kept below 1 px for thin weights so plots and SVG get the true width; +/// the GPU paths draw at least 1 px (#1749). pub(crate) fn lineweight_to_px(lw: &LineWeight) -> f32 { lw.millimeters() - .map(|mm| (mm as f32 * MM_TO_PX).max(1.0)) + .map(|mm| mm as f32 * MM_TO_PX) .unwrap_or(1.0) } @@ -4038,6 +4058,18 @@ impl Scene { } else { WireModel::HOVER }; + if let Some(points) = selected + .then(|| self.picked_segment_points(handle)) + .flatten() + { + wires.push(WireModel::solid_f64( + handle.value().to_string(), + points, + tint, + true, + )); + continue; + } for set in interaction_meshes .iter() .filter(|set| set.entity_handle() == Some(handle)) @@ -4890,13 +4922,23 @@ impl Scene { // (hatched) frame once it settles. Only applied to the on-screen // Model / paper content โ€” the paper *sheet* keeps its fills. skip_hatch: self.hatch_lod_enabled() && navigating, - skip_background: !inst.paper_sheet && self.current_layout != "Model", + // A floating viewport shows the page behind it unless it has a + // background of its own. + skip_background: !inst.paper_sheet + && self.current_layout != "Model" + && display.background.params[0] < 0.5, geometry_epoch: self.geometry_epoch, camera_generation: self.camera_generation, wire_content_id, wire_patch, selected_handles: Arc::new(if show_interaction { - self.selected.iter().copied().collect() + // A partly picked polyline lights only its picked segments, + // drawn with the highlight wires. + self.selected + .iter() + .copied() + .filter(|handle| self.picked_segments(*handle).is_none()) + .collect() } else { rustc_hash::FxHashSet::default() }), @@ -5419,3 +5461,11 @@ mod layer0_inherit_tests { assert!(scene.document_render_env_cache.borrow().is_some()); } } + +/// The display's device pixels per logical pixel, as the viewport last drew. +static DEVICE_SCALE: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0x3f80_0000); + +/// Device pixels per logical pixel of the drawing viewport. +pub fn device_scale() -> f32 { + f32::from_bits(DEVICE_SCALE.load(std::sync::atomic::Ordering::Relaxed)) +} diff --git a/src/schema_validation.rs b/src/schema_validation.rs new file mode 100644 index 000000000..b87d54cf3 --- /dev/null +++ b/src/schema_validation.rs @@ -0,0 +1,42 @@ +//! Offline validation of application-owned JSON Schema contracts. +use serde_json::Value; + +/// Only application-owned schemas enter this cache; client payloads are instances, +/// never schemas. Disable JSON Schema HTTP/file resolution at the dependency level. +pub fn validate_shape(schema: &Value, value: &Value, path: &str) -> Result<(), String> { + use std::{ + collections::HashMap, + sync::{Arc, Mutex, OnceLock}, + }; + static VALIDATORS: OnceLock>>> = + OnceLock::new(); + let key = schema.to_string(); + let validator = { + let mut cache = VALIDATORS + .get_or_init(Default::default) + .lock() + .expect("schema cache poisoned"); + if let Some(validator) = cache.get(&key) { + Arc::clone(validator) + } else { + let validator = Arc::new( + jsonschema::validator_for(schema).expect("application-owned schema must compile"), + ); + cache.insert(key, Arc::clone(&validator)); + validator + } + }; + validator.validate(value).map_err(|error| { + let field = match error.kind() { + jsonschema::error::ValidationErrorKind::Required { property } => property.as_str(), + jsonschema::error::ValidationErrorKind::AdditionalProperties { unexpected } => { + unexpected.first().map(String::as_str) + } + _ => None, + }; + let suffix = field + .map(|key| format!("/{}", key.replace('~', "~0").replace('/', "~1"))) + .unwrap_or_default(); + format!("{path}{}{suffix}: {error}", error.instance_path()) + }) +} diff --git a/src/shaders/background.wgsl b/src/shaders/background.wgsl index 3c933c0a9..dd4f7671b 100644 --- a/src/shaders/background.wgsl +++ b/src/shaders/background.wgsl @@ -81,18 +81,25 @@ fn equirect_uv(ndc: vec2, rotation: f32) -> vec2 { fn image_uv(source_uv: vec2) -> vec2 { var uv = source_uv; if u.background_image_params.x < 0.5 { - uv = (source_uv - u.background_image_transform.xy) - / max(abs(u.background_image_transform.zw), vec2(1e-6)); + // Not fitted: the image at its own pixel size times its scale (a + // negative scale mirrors it), centred on the viewport and moved by + // its offset in pixels, right and down. + let pixel = (source_uv - vec2(0.5)) * max(u.viewport_size, vec2(1.0)) + - u.background_image_transform.xy; + let size = u.background_image_transform.zw; + let safe = select(size, vec2(1e-6), abs(size) < vec2(1e-6)); + uv = pixel / safe + vec2(0.5); + return select(uv, fract(uv), u.background_image_params.z > 0.5); } + // Fitted: the image fills the viewport; keeping its aspect ratio it + // covers the viewport and the overflow is cut off evenly. if u.background_image_params.y > 0.5 { let image_aspect = max(u.background_image_params.w, 1e-6); let viewport_aspect = max(u.viewport_size.x / max(u.viewport_size.y, 1.0), 1e-6); if image_aspect > viewport_aspect { - let scale = viewport_aspect / image_aspect; - uv.y = (uv.y - 0.5) / scale + 0.5; + uv.x = (uv.x - 0.5) * (viewport_aspect / image_aspect) + 0.5; } else { - let scale = image_aspect / viewport_aspect; - uv.x = (uv.x - 0.5) / scale + 0.5; + uv.y = (uv.y - 0.5) * (image_aspect / viewport_aspect) + 0.5; } } if u.background_image_params.z > 0.5 { @@ -108,15 +115,25 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { if mode == 1u { color = u.viewport_background.rgb; } else if mode == 2u { + // Bottom to top along the "up" direction turned counter-clockwise by + // the rotation, in pixels from the viewport centre. As the reference + // draws it: the middle colour sits (horizon - 0.5) viewport widths + // from the centre and blends linearly into the top and bottom colours + // over height viewport heights on each side; two colours are one blend + // over a viewport height on each side of the centre. let angle = u.background_params.w; - let axis = vec2(-sin(angle), cos(angle)); - let coordinate = dot(in.uv - vec2(0.5), axis) + 0.5; - let horizon = clamp(u.background_params.y, 0.0, 1.0); - let half_height = max(abs(u.background_params.z) * 0.5, 1e-3); - let lower = smoothstep(horizon - half_height, horizon, coordinate); - let upper = smoothstep(horizon, horizon + half_height, coordinate); - let low_color = mix(u.background_bottom.rgb, u.background_middle.rgb, lower); - color = mix(low_color, u.background_top.rgb, upper); + let up = vec2(-sin(angle), cos(angle)); + let size = max(u.viewport_size, vec2(1.0)); + let pixel = (vec2(in.uv.x, 1.0 - in.uv.y) - vec2(0.5)) * size; + let along = dot(pixel, up); + let three = abs(u.background_params.z) > 1e-6; + let middle_at = select(0.0, (u.background_params.y - 0.5) * size.x, three); + let half_band = select(1.0, abs(u.background_params.z), three) * size.y; + let middle = select(mix(u.background_bottom.rgb, u.background_top.rgb, 0.5), + u.background_middle.rgb, three); + let lower = clamp((along - middle_at + half_band) / half_band, 0.0, 1.0); + let upper = clamp((along - middle_at) / half_band, 0.0, 1.0); + color = mix(mix(u.background_bottom.rgb, middle, lower), u.background_top.rgb, upper); } else if mode == 3u { let direction = world_direction(in.ndc); if direction.z >= 0.0 { diff --git a/src/shaders/draw_order.wgsl b/src/shaders/draw_order.wgsl index d0d243d85..d7f90e122 100644 --- a/src/shaders/draw_order.wgsl +++ b/src/shaders/draw_order.wgsl @@ -1,13 +1,12 @@ -// Positive order draws in front; zero retains geometric depth. Keep the usual -// 0.001 NDC offset away from the clipping planes, but reduce it near either -// plane: perspective depth can be much closer to 1 than this fixed offset. -// -// Using order / (1 + abs(order)) for the limited region keeps ordering monotonic -// (including the paper background's -2). Less than half the remaining depth is -// consumed, so valid vertices stay valid. Out-of-range/behind-eye vertices get -// no offset and remain subject to the real near/far clipping planes. +// Positive draw order moves geometry towards the camera. In dense blocks, +// child ranks occupy a narrow fraction of the INSERT depth band: a 0.001 +// NDC bias can quantize two overlapping lines/masks to the same depth value, +// especially after changing view/zoom. Use a wider, yet bounded range for +// CAD 2D draw order. Limit displacement to less than half the available +// clip-space room, so we never push valid geometry across near/far planes. +// This is a screen-only GPU change; PDF order uses its own stable sorting. fn apply_draw_order(clip: vec4, order: f32) -> vec4 { let room = 0.5 * max(0.0, min(clip.z, clip.w - clip.z)); - let scale = min(0.001 * max(clip.w, 0.0), room / (1.0 + abs(order))); + let scale = min(0.02 * max(clip.w, 0.0), room / (1.0 + abs(order))); return vec4(clip.xy, clip.z - order * scale, clip.w); } diff --git a/src/snap.rs b/src/snap.rs index 9e90d1514..bc8cd04a4 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -14,7 +14,7 @@ use kernel::geom2d::Curve; use crate::command::{DimensionAssociationSource, TangentObject}; use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel}; use crate::scene::pick::interaction_index::WireSource; -const DEFAULT_OSNAP_RADIUS_PX: f32 = 15.0; +const DEFAULT_OSNAP_RADIUS_PX: f32 = 25.0; // โ”€โ”€ Snap type โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -2205,7 +2205,9 @@ impl Snapper { let mut eval_tangent = |wire: &WireModel| { let mut line_cursor = 0usize; for tg in &wire.tangent_geoms { - let (world_pt, d2) = match tg { + // A tangent point computed from the rubber-band base. + let mut exact = false; + let (world_pt, mut d2) = match tg { TangentGeom::Line { p1, p2 } => { let (lp1, lp2) = tangent_line_endpoints(wire, *p1, *p2, &mut line_cursor); let sp0 = world_to_screen(lp1, view_rot, eye, bounds); @@ -2233,6 +2235,7 @@ impl Snapper { { let s0 = world_to_screen(t0.as_dvec3(), view_rot, eye, bounds); let s1 = world_to_screen(t1.as_dvec3(), view_rot, eye, bounds); + exact = true; if dist2(s0, cursor_screen) <= dist2(s1, cursor_screen) { t0 } else { @@ -2299,6 +2302,7 @@ impl Snapper { }) .unwrap_or_else(|| DVec3::from_array(*center)) }; + exact = candidate.is_some(); let world = candidate.unwrap_or_else(fallback); let edge_d2 = wire .points @@ -2348,6 +2352,7 @@ impl Snapper { }) .unwrap_or(cv) }; + exact = candidate.is_some(); let world = candidate.unwrap_or_else(fallback); let edge_d2 = wire .points @@ -2427,6 +2432,7 @@ impl Snapper { } }; + exact = candidate.is_some(); let world = candidate.unwrap_or(fallback); let edge_d2 = if wire.points.len() >= 2 { wire.points @@ -2454,11 +2460,17 @@ impl Snapper { } } }; - let (tier, sub) = ( - snap_tier(SnapType::Tangent), - snap_priority(SnapType::Tangent), - ); let screen_pt = world_to_screen(world_pt, view_rot, eye, bounds); + // Near the computed tangent point it is a point snap like + // the others, so the rim-hover Center no longer masks it. + let point_d2 = dist2(screen_pt, cursor_screen); + let tier = if exact && point_d2 < radius2 { + d2 = point_d2; + 0 + } else { + snap_tier(SnapType::Tangent) + }; + let sub = snap_priority(SnapType::Tangent); if d2 < radius2 && in_bounds(screen_pt) && snap_better(tier, d2, sub, (best_rank, best_d2, best_sub)) @@ -5492,6 +5504,57 @@ mod ext_tests { assert!(empty_pts.is_empty()); } + #[test] + fn tangent_point_beats_rim_center() { + // TAN and CEN on, the cursor on the circle at the tangent point seen + // from the base: Tangent wins, not the rim-hover Center (#1768). + let mut snapper = Snapper::default(); + snapper.snap_enabled = true; + snapper.enabled = [SnapType::Tangent, SnapType::Center].into_iter().collect(); + snapper.from_point = Some(DVec3::new(100.0, 0.0, 0.0)); + let points = (0..=64) + .map(|i| { + let a = i as f32 / 64.0 * std::f32::consts::TAU; + [50.0 * a.cos(), 50.0 * a.sin(), 0.0] + }) + .collect(); + let circle = WireModel { + points, + aabb: [-50.0, -50.0, 50.0, 50.0], + snap_pts: vec![(DVec3::ZERO, SnapHint::Center)], + tangent_geoms: vec![TangentGeom::Circle { + center: [0.0, 0.0, 0.0], + radius: 50.0, + }], + ..Default::default() + }; + let wires = vec![circle]; + let view_rot = Mat4::from_scale(Vec3::new(0.005, 0.005, 1.0)); + let bounds = Rectangle { + x: 0.0, + y: 0.0, + width: 1000.0, + height: 1000.0, + }; + let tangent = DVec3::new(25.0, 50.0 * 0.75f64.sqrt(), 0.0); + let cursor = world_to_screen(tangent, view_rot, DVec3::ZERO, bounds); + let snap = snapper + .snap( + tangent, + cursor, + wires.as_slice(), + view_rot, + DVec3::ZERO, + bounds, + Vec3::ZERO, + (Vec3::X, Vec3::Y, Vec3::Z), + None, + ) + .expect("a snap on the rim"); + assert_eq!(snap.snap_type, SnapType::Tangent); + assert!((snap.world - tangent).length() < 1e-3); + } + #[test] fn test_ellipse_arc_tangent_snap_ignores_break() { use std::f64::consts::PI; diff --git a/src/sys.rs b/src/sys.rs index 1ca7afae7..0867bc8e0 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -440,7 +440,6 @@ fn pasted_image_name(mime_type: &str) -> String { /// the candidate already names a registered source. The taken-check is /// injectable so this โ€” the part worth locking with tests โ€” runs natively; /// wasm passes [`crate::scene::model::image_model::has_source`]. -#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // tested everywhere, used on wasm pub(crate) fn unique_pasted_image_name( base: &str, ext: &str, @@ -587,6 +586,121 @@ pub fn file_dialog() -> rfd::AsyncFileDialog { rfd::AsyncFileDialog::new() } +// โ”€โ”€ FILEDIA and file pickers that keep their file type โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +static FILE_DIA: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); + +/// FILEDIA (a profile setting): commands that have a command-line form show +/// their file dialogs only while it is on. +pub fn file_dia() -> bool { + FILE_DIA.load(std::sync::atomic::Ordering::Relaxed) +} + +pub fn set_file_dia(on: bool) { + FILE_DIA.store(on, std::sync::atomic::Ordering::Relaxed); +} + +/// The file type ATTACH's picker was last left on (1-based; 7 = Drawing). +static ATTACH_FILTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(7); + +pub fn attach_filter() -> u32 { + ATTACH_FILTER.load(std::sync::atomic::Ordering::Relaxed) +} + +pub fn set_attach_filter(index: u32) { + ATTACH_FILTER.store(index, std::sync::atomic::Ordering::Relaxed); +} + +/// A file type of [`pick_file_with_filter`]: its label and extensions +/// (`"*"` for every file). +pub type FileFilter = (String, &'static [&'static str]); + +/// Open-file dialog that starts on file type `index` (1-based) and returns the +/// picked path with the file type the dialog was left on. +#[cfg(target_os = "windows")] +pub async fn pick_file_with_filter( + title: String, + filters: Vec, + index: u32, +) -> Option<(std::path::PathBuf, u32)> { + // The dialog blocks, so it runs on its own thread like the async picker. + let (tx, rx) = iced::futures::channel::oneshot::channel(); + std::thread::spawn(move || { + let _ = tx.send(open_file_dialog_blocking(&title, &filters, index)); + }); + rx.await.ok().flatten() +} + +#[cfg(target_os = "windows")] +fn open_file_dialog_blocking( + title: &str, + filters: &[FileFilter], + index: u32, +) -> Option<(std::path::PathBuf, u32)> { + use windows_sys::Win32::System::Com::{CoInitializeEx, COINIT_APARTMENTTHREADED}; + use windows_sys::Win32::UI::Controls::Dialogs::{ + GetOpenFileNameW, OFN_EXPLORER, OFN_FILEMUSTEXIST, OFN_HIDEREADONLY, OFN_NOCHANGEDIR, + OFN_PATHMUSTEXIST, OPENFILENAMEW, + }; + let wide = |s: &str| s.encode_utf16().chain([0]).collect::>(); + let mut filter = Vec::new(); + for (label, extensions) in filters { + let pattern = extensions + .iter() + .map(|e| format!("*.{e}")) + .collect::>() + .join(";"); + filter.extend(wide(label)); + filter.extend(wide(&pattern)); + } + filter.push(0); + let title = wide(title); + let dir = crate::config::last_dialog_dir().map(|d| wide(&d.to_string_lossy())); + let mut file = vec![0u16; 32768]; + let mut ofn = OPENFILENAMEW { + lStructSize: std::mem::size_of::() as u32, + lpstrFilter: filter.as_ptr(), + nFilterIndex: index, + lpstrFile: file.as_mut_ptr(), + nMaxFile: file.len() as u32, + lpstrTitle: title.as_ptr(), + lpstrInitialDir: dir.as_ref().map_or(std::ptr::null(), |d| d.as_ptr()), + Flags: OFN_EXPLORER + | OFN_FILEMUSTEXIST + | OFN_PATHMUSTEXIST + | OFN_NOCHANGEDIR + | OFN_HIDEREADONLY, + ..Default::default() + }; + // SAFETY: every pointer in `ofn` points into buffers that outlive the call. + let picked = unsafe { + CoInitializeEx(std::ptr::null(), COINIT_APARTMENTTHREADED as u32); + GetOpenFileNameW(&mut ofn) + }; + if picked == 0 { + return None; + } + let len = file.iter().position(|&c| c == 0).unwrap_or(file.len()); + let path = std::path::PathBuf::from(String::from_utf16_lossy(&file[..len])); + crate::config::remember_dialog_dir(&path); + Some((path, ofn.nFilterIndex)) +} + +/// Elsewhere the picker cannot start on a file type; the index passes through. +#[cfg(not(target_os = "windows"))] +pub async fn pick_file_with_filter( + title: String, + filters: Vec, + index: u32, +) -> Option<(std::path::PathBuf, u32)> { + let mut dialog = file_dialog().set_title(title); + for (label, extensions) in &filters { + dialog = dialog.add_filter(label, extensions); + } + let handle = dialog.pick_file().await?; + Some((handle_path(&handle), index)) +} + /// Trigger a browser download of `bytes` as `name`. Builds a Blob, points a /// hidden `` at it and clicks it programmatically โ€” because this /// runs inside the Save button's click (a user gesture), the file downloads @@ -597,10 +711,15 @@ pub fn download_bytes(name: &str, bytes: &[u8]) { let Some(window) = web_sys::window() else { return; }; + let array = js_sys::Uint8Array::from(bytes); + // A host page that embeds the app takes the file instead of a download. + if let Some(hook) = save_hook() { + let _ = hook.call2(&window, &wasm_bindgen::JsValue::from_str(name), &array); + return; + } let Some(document) = window.document() else { return; }; - let array = js_sys::Uint8Array::from(bytes); let parts = js_sys::Array::new(); parts.push(&array.buffer()); let Ok(blob) = web_sys::Blob::new_with_u8_array_sequence(&parts) else { @@ -618,6 +737,18 @@ pub fn download_bytes(name: &str, bytes: &[u8]) { let _ = web_sys::Url::revoke_object_url(&url); } +/// The `window.openCadStudioSaveFile(name, bytes)` function an embedding page +/// registers to receive saved and exported files, if any. +#[cfg(target_arch = "wasm32")] +pub fn save_hook() -> Option { + use wasm_bindgen::JsCast; + let window = web_sys::window()?; + js_sys::Reflect::get(window.as_ref(), &wasm_bindgen::JsValue::from_str("openCadStudioSaveFile")) + .ok()? + .dyn_into::() + .ok() +} + /// Show a plot preview as an in-page overlay. The plotted PDF arrives already /// rasterised to PNG pages: inline `` elements render in every browser, /// with no popup permission and no PDF viewer plugin โ€” both of which embedded diff --git a/src/ui/color_select/mod.rs b/src/ui/color_select/mod.rs index 6a1f752f6..445a52bbc 100644 --- a/src/ui/color_select/mod.rs +++ b/src/ui/color_select/mod.rs @@ -57,16 +57,13 @@ pub fn aci_string_to_color(s: &str) -> AcadColor { } /// Display name for a colour: the standard name for ACI 1-9 / ByLayer / -/// ByBlock, otherwise the "R,G,B" values (0-255) so unnamed palette colours -/// read meaningfully. +/// ByBlock, the index for any other palette colour and "R,G,B" for a true +/// colour, so the two kinds read apart (#1748). pub fn color_display_name(c: AcadColor) -> String { let (_, label) = acad_color_display(c); if label == "Index" || label == "Custom" { match c { - AcadColor::Index(i) => { - let (r, g, b) = codec::types::aci_table::aci_to_rgb(i).unwrap_or((128, 128, 128)); - format!("{r},{g},{b}") - } + AcadColor::Index(i) => i.to_string(), AcadColor::Rgb { r, g, b } => format!("{r},{g},{b}"), _ => label.to_string(), } diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index c7112f934..5995e5570 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -191,6 +191,12 @@ pub struct CommandLine { /// in milliseconds (0โ€“60000, default 3000). 0 means history never fades; /// the pinned step prompt still shows. fade_ms: u32, + /// COMMANDLINEOPACITY: how opaque the command line's panels are, in + /// percent (20โ€“100, default 80), so the drawing shows through. + opacity_percent: u8, + /// Since when the pointer rests on the overlay history: lines still + /// showing then stay until it leaves, and fade from that moment. + hover_since: Option, } impl Default for CommandLine { @@ -217,6 +223,8 @@ impl Default for CommandLine { step_options: Vec::new(), cliprompt_lines: 3, fade_ms: DEFAULT_COMMANDLINE_FADE_MS, + opacity_percent: 80, + hover_since: None, } } } @@ -509,12 +517,46 @@ impl CommandLine { self.fade_ms } + /// COMMANDLINEOPACITY mirror (percent, 20โ€“100). + pub fn set_opacity_percent(&mut self, percent: u8) { + self.opacity_percent = percent.clamp(20, 100); + } + + pub fn opacity_percent(&self) -> u8 { + self.opacity_percent + } + fn fade_secs(&self) -> f32 { self.fade_ms as f32 / 1000.0 } fn entry_visible(&self, e: &HistoryEntry) -> bool { - e.pinned || self.fade_ms == 0 || e.created_at.elapsed().as_secs_f32() < self.fade_secs() + let held = self.hover_since.is_some_and(|since| { + since.saturating_duration_since(e.created_at).as_secs_f32() < self.fade_secs() + }); + e.pinned + || self.fade_ms == 0 + || held + || e.created_at.elapsed().as_secs_f32() < self.fade_secs() + } + + /// The pointer entered or left the overlay history. Leaving restarts the + /// fade of every line it held. + pub fn set_history_hover(&mut self, hovered: bool) { + if hovered { + self.hover_since.get_or_insert_with(Instant::now); + return; + } + let Some(since) = self.hover_since.take() else { + return; + }; + let fade = self.fade_secs(); + let now = Instant::now(); + for entry in &mut self.history { + if since.saturating_duration_since(entry.created_at).as_secs_f32() < fade { + entry.created_at = now; + } + } } /// Height of the overlaid prompt lines currently shown above the input @@ -1127,17 +1169,24 @@ impl CommandLine { // The transient recent lines and the full archive are two views of the // same history. Showing both while the archive is open creates a fake // second history region and visually disconnects the input row (#555). + let opacity = f32::from(self.opacity_percent) / 100.0; let recent_history: Element<'a, Message> = if self.history_open || !has_recent_history { container(column![]).height(0).into() } else { - container(history_rows) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.base.color)), - ..Default::default() - }) - .width(Length::Fill) - .padding([2, 0]) - .into() + iced::widget::mouse_area( + container(history_rows) + .style(move |theme: &Theme| container::Style { + background: Some(Background::Color( + theme.palette().background.base.color.scale_alpha(opacity), + )), + ..Default::default() + }) + .width(Length::Fill) + .padding([2, 0]), + ) + .on_enter(Message::CommandHistoryHover(true)) + .on_exit(Message::CommandHistoryHover(false)) + .into() }; let history_divider: Element<'a, Message> = if self.history_open { rule::horizontal(1).into() @@ -1153,10 +1202,11 @@ impl CommandLine { recent_history, history_divider, container(input_row) - .style(|theme: &Theme| { + .style(move |theme: &Theme| { let palette = theme.palette(); + let color = palette.background.weakest.color.scale_alpha(opacity); container::Style { - background: Some(Background::Color(palette.background.weakest.color)), + background: Some(Background::Color(color)), ..Default::default() } }) @@ -1170,7 +1220,9 @@ impl CommandLine { .style(move |theme: &Theme| { let palette = theme.palette(); container::Style { - background: Some(Background::Color(palette.background.base.color)), + background: Some(Background::Color( + palette.background.base.color.scale_alpha(opacity), + )), border: Border { color: palette.background.neutral.color, width: if history_open { 1.0 } else { 0.0 }, diff --git a/src/ui/icons.rs b/src/ui/icons.rs index 9897fd9e3..78971aad0 100644 --- a/src/ui/icons.rs +++ b/src/ui/icons.rs @@ -83,6 +83,7 @@ pub static SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg"); pub static GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg"); pub static LIST: &[u8] = include_bytes!("../../assets/icons/ui/list.svg"); pub static SEARCH: &[u8] = include_bytes!("../../assets/icons/ui/search.svg"); +pub static PREVIEW: &[u8] = include_bytes!("../../assets/icons/ui/preview.svg"); pub static SNAP: &[u8] = include_bytes!("../../assets/icons/ui/snap.svg"); pub static DOC_NEW: &[u8] = include_bytes!("../../assets/icons/ui/doc_new.svg"); pub static DOC: &[u8] = include_bytes!("../../assets/icons/ui/doc.svg"); diff --git a/src/ui/menu_bar.rs b/src/ui/menu_bar.rs new file mode 100644 index 000000000..682e1b523 --- /dev/null +++ b/src/ui/menu_bar.rs @@ -0,0 +1,271 @@ +//! Classic pull-down menus above the ribbon (MENUBAR 1). Every entry runs a +//! command, so the menus stay a view of the command set rather than a second +//! implementation of it. + +use crate::app::Message; +use iced::widget::{button, container, text}; +use iced::{Background, Border, Element, Fill, Theme}; +use iced_aw::menu::{Item, Menu, MenuBar}; + +type Entries = &'static [(&'static str, &'static str)]; + +/// Menu title and its (label, command) entries, in menu-bar order. +const MENUS: &[(&str, Entries)] = &[ + ( + "File", + &[ + ("New", "QNEW"), + ("Open", "OPEN"), + ("Close", "CLOSE"), + ("Save", "QSAVE"), + ("Save As", "SAVEAS"), + ("Page Setup Manager", "PAGESETUP"), + ("Plot", "PLOT"), + ("Publish", "PUBLISH"), + ("Audit", "AUDIT"), + ("Purge", "PURGE"), + ("Exit", "QUIT"), + ], + ), + ( + "Edit", + &[ + ("Undo", "UNDO"), + ("Redo", "REDO"), + ("Cut", "CUTCLIP"), + ("Copy", "COPYCLIP"), + ("Copy with Base Point", "COPYBASE"), + ("Paste", "PASTECLIP"), + ("Paste as Block", "PASTEBLOCK"), + ("Paste to Original Coordinates", "PASTEORIG"), + ("Paste Special", "PASTESPEC"), + ("Erase", "ERASE"), + ("Find and Replace", "FIND"), + ], + ), + ( + "View", + &[ + ("Redraw", "REDRAW"), + ("Regen", "REGEN"), + ("Regen All", "REGENALL"), + ("Zoom", "ZOOM"), + ("Pan", "PAN"), + ("Orbit", "3DORBIT"), + ("Named Views", "VIEW"), + ("View Twist", "DVIEW"), + ("Viewports", "VPORTS"), + ("Clean Screen", "CLEANSCREEN"), + ], + ), + ( + "Insert", + &[ + ("Block", "INSERT"), + ("External References", "EXTERNALREFERENCES"), + ("Raster Image", "IMAGEATTACH"), + ("PDF Underlay", "PDFATTACH"), + ("Field", "FIELD"), + ("Hyperlink", "HYPERLINK"), + ], + ), + ( + "Format", + &[ + ("Layer", "LAYER"), + ("Color", "COLOR"), + ("Linetype", "LINETYPE"), + ("Text Style", "STYLE"), + ("Dimension Style", "DIMSTYLE"), + ("Table Style", "TABLESTYLE"), + ("Multileader Style", "MLEADERSTYLE"), + ("Multiline Style", "MLSTYLE"), + ("Point Style", "PTYPE"), + ("Units", "UNITS"), + ("Drawing Limits", "LIMITS"), + ], + ), + ( + "Tools", + &[ + ("Quick Select", "QSELECT"), + ("Draw Order", "DRAWORDER"), + ("Distance", "DIST"), + ("Area", "AREA"), + ("List", "LIST"), + ("ID Point", "ID"), + ("Run Script", "SCRIPT"), + ("New UCS", "UCS"), + ("Drafting Settings", "DSETTINGS"), + ("Options", "OPTIONS"), + ], + ), + ( + "Draw", + &[ + ("Line", "LINE"), + ("Ray", "RAY"), + ("Construction Line", "XLINE"), + ("Multiline", "MLINE"), + ("Polyline", "PLINE"), + ("3D Polyline", "3DPOLY"), + ("Polygon", "POLYGON"), + ("Rectangle", "RECTANG"), + ("Arc", "ARC"), + ("Circle", "CIRCLE"), + ("Donut", "DONUT"), + ("Spline", "SPLINE"), + ("Ellipse", "ELLIPSE"), + ("Block", "BLOCK"), + ("Table", "TABLE"), + ("Point", "POINT"), + ("Hatch", "HATCH"), + ("Boundary", "BOUNDARY"), + ("Region", "REGION"), + ("Wipeout", "WIPEOUT"), + ("Revision Cloud", "REVCLOUD"), + ("Text", "TEXT"), + ("Multiline Text", "MTEXT"), + ], + ), + ( + "Dimension", + &[ + ("Quick Dimension", "QDIM"), + ("Linear", "DIMLINEAR"), + ("Aligned", "DIMALIGNED"), + ("Arc Length", "DIMARC"), + ("Ordinate", "DIMORDINATE"), + ("Radius", "DIMRADIUS"), + ("Jogged", "DIMJOGGED"), + ("Diameter", "DIMDIAMETER"), + ("Angular", "DIMANGULAR"), + ("Baseline", "DIMBASELINE"), + ("Continue", "DIMCONTINUE"), + ("Multileader", "MLEADER"), + ("Tolerance", "TOLERANCE"), + ("Center Mark", "DIMCENTER"), + ("Dimension Style", "DIMSTYLE"), + ], + ), + ( + "Modify", + &[ + ("Properties", "PROPERTIES"), + ("Match Properties", "MATCHPROP"), + ("Erase", "ERASE"), + ("Copy", "COPY"), + ("Mirror", "MIRROR"), + ("Offset", "OFFSET"), + ("Array", "ARRAY"), + ("Move", "MOVE"), + ("Rotate", "ROTATE"), + ("Scale", "SCALE"), + ("Stretch", "STRETCH"), + ("Lengthen", "LENGTHEN"), + ("Trim", "TRIM"), + ("Extend", "EXTEND"), + ("Break", "BREAK"), + ("Join", "JOIN"), + ("Chamfer", "CHAMFER"), + ("Fillet", "FILLET"), + ("Explode", "EXPLODE"), + ("Explode Text", "TXTEXP"), + ("Edit Polyline", "PEDIT"), + ("Edit Spline", "SPLINEDIT"), + ("Edit Hatch", "HATCHEDIT"), + ], + ), + ( + "Parametric", + &[ + ("Auto Constrain", "AUTOCONSTRAIN"), + ("Coincident", "GCCOINCIDENT"), + ("Parallel", "PCONSTRAINT"), + ("Perpendicular", "GCPERPENDICULAR"), + ("Horizontal", "GCHORIZONTAL"), + ("Vertical", "GCVERTICAL"), + ("Tangent", "TCONSTRAINT"), + ("Equal", "GCEQUAL"), + ("Fix", "GCFIX"), + ("Dimensional", "DIMCONSTRAINT"), + ("Delete Constraints", "DELCONSTRAINT"), + ("Parameters Manager", "PARAMETERS"), + ], + ), + ("Help", &[("Help", "HELP"), ("About", "ABOUT")]), +]; + +fn entry_style(theme: &Theme, status: button::Status) -> button::Style { + let palette = theme.palette(); + let pair = match status { + button::Status::Hovered | button::Status::Pressed => palette.primary.weak, + _ => palette.background.base, + }; + button::Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + ..Default::default() + } +} + +/// The menu bar, one pull-down per menu. +pub fn view<'a>() -> Element<'a, Message> { + let roots = MENUS + .iter() + .map(|&(title, entries)| { + let items = entries + .iter() + .map(|&(label, command)| { + Item::new( + button(text(crate::t!(label).into_owned()).size(12)) + .on_press(Message::Command(command.to_string())) + .style(entry_style) + .padding([4, 12]) + .width(Fill), + ) + }) + .collect(); + Item::with_menu( + container(text(crate::t!(title).into_owned()).size(12)).padding([3, 8]), + Menu::new(items).width(240.0).offset(2.0), + ) + }) + .collect(); + MenuBar::new(roots) + .close_on_item_click_global(true) + .style(|theme: &Theme, _| { + let palette = theme.palette(); + iced_aw::style::menu_bar::Style { + bar_background: Background::Color(palette.background.weakest.color), + bar_border: Border::default(), + bar_shadow: Default::default(), + menu_background: Background::Color(palette.background.base.color), + menu_border: Border { + color: palette.background.strong.color, + width: 1.0, + radius: 3.0.into(), + }, + menu_shadow: Default::default(), + path: Background::Color(palette.primary.weak.color), + path_border: Border::default(), + } + }) + .width(Fill) + .into() +} + +#[cfg(test)] +mod tests { + #[test] + fn every_menu_entry_runs_a_known_command() { + let mut known = crate::command::all_registered_command_names(); + known.extend(["UNDO", "REDO", "QSAVE", "OPEN", "ZOOM"]); + let unknown: Vec<_> = super::MENUS + .iter() + .flat_map(|(_, entries)| entries.iter()) + .filter(|(_, command)| !known.contains(command)) + .collect(); + assert!(unknown.is_empty(), "{unknown:?}"); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b3220ae32..1cf35b847 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,6 +8,7 @@ pub mod dock; pub mod icons; pub mod modal; pub mod node_graph; +pub mod menu_bar; pub mod overlay; pub mod popup; pub mod properties; diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index c0d9daf0c..cbef3af27 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -71,6 +71,70 @@ fn draw_concentric_constraint_glyph(frame: &mut canvas::Frame, center: Point, co frame.stroke(&canvas::Path::circle(center, 2.15), stroke); } +/// Strokes a closed four-corner polygon โ€” the outline half of the brush glyph. +fn stroke_quad(frame: &mut canvas::Frame, corners: [Point; 4], stroke: &canvas::Stroke<'_>) { + let path = canvas::Path::new(|p| { + p.move_to(corners[0]); + p.line_to(corners[1]); + p.line_to(corners[2]); + p.line_to(corners[3]); + p.close(); + }); + frame.stroke(&path, stroke.clone()); +} + +/// MATCHPROP's paintbrush cursor. +/// +/// While the user is transferring properties the CAD crosshair is replaced by a +/// paintbrush whose tip sits exactly on the pick point, as the reference does. The +/// body leans up-and-right so it never covers the object being picked. +/// +/// Deliberately monochrome and geometric: the crosshair's own colour, a single +/// stroke weight and one filled wedge, so the glyph reads as part of the CAD +/// cursor family rather than clip-art. +fn draw_match_brush(frame: &mut canvas::Frame, tip: Point, color: Color) { + let inv = std::f32::consts::FRAC_1_SQRT_2; + // `along` runs up-and-right (handle direction); `across` is perpendicular. + let along = (inv, -inv); + let across = (inv, inv); + let at = |a: f32, w: f32| { + Point::new( + tip.x + along.0 * a + across.0 * w, + tip.y + along.1 * a + across.1 * w, + ) + }; + let stroke = canvas::Stroke { + width: 1.25, + style: canvas::Style::Solid(color), + line_join: canvas::LineJoin::Miter, + ..Default::default() + }; + + // Bristles: the only solid mass, so the working end carries the weight and + // the silhouette still reads at cursor size. + let bristles = canvas::Path::new(|p| { + p.move_to(at(0.0, 0.0)); + p.line_to(at(9.0, -3.2)); + p.line_to(at(12.0, -3.2)); + p.line_to(at(12.0, 3.2)); + p.line_to(at(9.0, 3.2)); + p.close(); + }); + frame.fill(&bristles, color); + + // Ferrule and handle stay outline-only: line work, not clip-art. + stroke_quad( + frame, + [at(12.0, -3.2), at(15.5, -3.2), at(15.5, 3.2), at(12.0, 3.2)], + &stroke, + ); + stroke_quad( + frame, + [at(15.5, -2.3), at(30.0, -1.9), at(30.0, 1.9), at(15.5, 2.3)], + &stroke, + ); +} + /// The reference bar's padlock: white with a small orange point marker /// when one point is held, red when a whole curve or segment is. fn draw_fixed_constraint_glyph( @@ -243,6 +307,8 @@ pub struct CrosshairOptions { /// Explicit object-snap marker colour; `None` picks one that reads on /// the canvas. pub snap_color: Option<[u8; 3]>, + /// Object-snap marker half-size in pixels (SNAPMARKERSIZE, 1โ€“20). + pub snap_size: f32, } /// Rendering style for the viewport grid. @@ -901,6 +967,8 @@ pub fn selection_overlay<'a>( hover_locked: bool, crosshair_bg: [f32; 4], crosshair: CrosshairOptions, + // MATCHPROP is armed: paintbrush cursor replaces the crosshair. + show_match_brush: bool, selection_visual: SelectionVisualOptions, constraint_glyphs: Arc<[GlyphEntry]>, // Per-glyph selection flags, parallel to `constraint_glyphs`, applied @@ -931,6 +999,7 @@ pub fn selection_overlay<'a>( hover_locked, crosshair_bg, crosshair, + show_match_brush, selection_visual, constraint_glyphs, constraint_glyph_selected, @@ -995,6 +1064,9 @@ struct SelectionCanvas { /// rather than the UI theme, which may be light over a dark model viewport. crosshair_bg: [f32; 4], crosshair: CrosshairOptions, + /// MATCHPROP is armed: the crosshair gives way to a paintbrush cursor + /// whose tip marks the pick point. + show_match_brush: bool, selection_visual: SelectionVisualOptions, /// Memoised constraint-glyph entries (precomputed anchor/outward/label + /// layout) shared with the hit-test and tooltip paths โ€” draw reads @@ -1609,110 +1681,231 @@ impl canvas::Program for SelectionCanvas { style: canvas::Style::Solid(marker), ..Default::default() }; - match snap_type { - SnapType::ObjectPick => { - // Target box + center dot (object-acquisition glyph). - let h = 7.0_f32; - let rect = canvas::Path::rectangle( - Point::new(sp.x - h, sp.y - h), - Size::new(h * 2.0, h * 2.0), - ); - frame.stroke(&rect, stroke.clone()); - let r = 3.0_f32; - frame.fill( - &canvas::Path::circle(sp, r), - Color { - r: 0.95, - g: 0.50, - b: 0.08, - a: 0.85, - }, - ); - } - SnapType::Endpoint => { - let h = 5.0_f32; - let rect = canvas::Path::rectangle( - Point::new(sp.x - h, sp.y - h), - Size::new(h * 2.0, h * 2.0), - ); - frame.stroke(&rect, stroke); - } - SnapType::Midpoint => { - let r = 6.0_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - r)); - b.line_to(Point::new(sp.x + r * 0.866, sp.y + r * 0.5)); - b.line_to(Point::new(sp.x - r * 0.866, sp.y + r * 0.5)); - b.close(); - }); - frame.stroke(&path, stroke); - } - SnapType::Center => { - let r = 5.5_f32; - let path = canvas::Path::circle(sp, r); - frame.stroke(&path, stroke); - } - SnapType::GeometricCenter => { - // Pentagon around a centre dot: the area centroid. - let r = 6.5_f32; - let path = canvas::Path::new(|b| { - for k in 0..5 { - let a = -std::f32::consts::FRAC_PI_2 - + k as f32 * std::f32::consts::TAU / 5.0; - let p = Point::new(sp.x + r * a.cos(), sp.y + r * a.sin()); - if k == 0 { - b.move_to(p); - } else { - b.line_to(p); + // SNAPMARKERSIZE scales the glyphs drawn for the default 5 px. + let scale = self.crosshair.snap_size.clamp(1.0, 20.0) / 5.0; + frame.with_save(|frame| { + frame.translate(iced::Vector::new(sp.x, sp.y)); + frame.scale(scale); + frame.translate(iced::Vector::new(-sp.x, -sp.y)); + match snap_type { + SnapType::ObjectPick => { + // Target box + center dot (object-acquisition glyph). + let h = 7.0_f32; + let rect = canvas::Path::rectangle( + Point::new(sp.x - h, sp.y - h), + Size::new(h * 2.0, h * 2.0), + ); + frame.stroke(&rect, stroke.clone()); + let r = 3.0_f32; + frame.fill( + &canvas::Path::circle(sp, r), + Color { + r: 0.95, + g: 0.50, + b: 0.08, + a: 0.85, + }, + ); + } + SnapType::Endpoint => { + let h = 5.0_f32; + let rect = canvas::Path::rectangle( + Point::new(sp.x - h, sp.y - h), + Size::new(h * 2.0, h * 2.0), + ); + frame.stroke(&rect, stroke); + } + SnapType::Midpoint => { + let r = 6.0_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - r)); + b.line_to(Point::new(sp.x + r * 0.866, sp.y + r * 0.5)); + b.line_to(Point::new(sp.x - r * 0.866, sp.y + r * 0.5)); + b.close(); + }); + frame.stroke(&path, stroke); + } + SnapType::Center => { + let r = 5.5_f32; + let path = canvas::Path::circle(sp, r); + frame.stroke(&path, stroke); + } + SnapType::GeometricCenter => { + // Pentagon around a centre dot: the area centroid. + let r = 6.5_f32; + let path = canvas::Path::new(|b| { + for k in 0..5 { + let a = -std::f32::consts::FRAC_PI_2 + + k as f32 * std::f32::consts::TAU / 5.0; + let p = Point::new(sp.x + r * a.cos(), sp.y + r * a.sin()); + if k == 0 { + b.move_to(p); + } else { + b.line_to(p); + } + } + b.close(); + }); + frame.stroke(&path, stroke.clone()); + frame.fill(&canvas::Path::circle(sp, 1.6), marker); + } + SnapType::Node => { + // Circle with an inscribed X. + let r = 5.5_f32; + let cpath = canvas::Path::circle(sp, r); + frame.stroke(&cpath, stroke.clone()); + let d = r * std::f32::consts::FRAC_1_SQRT_2; + let x1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - d, sp.y - d)); + b.line_to(Point::new(sp.x + d, sp.y + d)); + }); + let x2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - d, sp.y + d)); + b.line_to(Point::new(sp.x + d, sp.y - d)); + }); + frame.stroke(&x1, stroke.clone()); + frame.stroke(&x2, stroke); + } + SnapType::Quadrant => { + let r = 6.0_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y)); + b.line_to(Point::new(sp.x, sp.y + r)); + b.line_to(Point::new(sp.x - r, sp.y)); + b.close(); + }); + frame.stroke(&path, stroke); + } + SnapType::Intersection => { + // An extended intersection sets one or two extension bases; + // draw a dashed guide from each endpoint through the crossing + // so both contributing extension paths stay visible. A real + // on-segment crossing carries no bases, so nothing draws here. + // (#247, #259) + for base in [self.snap_ext_base, self.snap_ext_base2] + .into_iter() + .flatten() + { + let dx = sp.x - base.x; + let dy = sp.y - base.y; + let len = (dx * dx + dy * dy).sqrt(); + if len > 1e-3 { + let dash = canvas::Stroke { + line_dash: canvas::LineDash { + segments: &[4.0, 4.0], + offset: 0, + }, + ..canvas::Stroke::default().with_color(marker).with_width(1.0) + }; + let tip = Point::new(sp.x + dx / len * 18.0, sp.y + dy / len * 18.0); + // `base` is a projected entity endpoint: clip before + // stroking (see `clip_seg`). + if let Some((a, b)) = clip_seg(base, tip, bounds) { + frame.stroke(&canvas::Path::line(a, b), dash); + } } } - b.close(); - }); - frame.stroke(&path, stroke.clone()); - frame.fill(&canvas::Path::circle(sp, 1.6), marker); - } - SnapType::Node => { - // Circle with an inscribed X. - let r = 5.5_f32; - let cpath = canvas::Path::circle(sp, r); - frame.stroke(&cpath, stroke.clone()); - let d = r * std::f32::consts::FRAC_1_SQRT_2; - let x1 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - d, sp.y - d)); - b.line_to(Point::new(sp.x + d, sp.y + d)); - }); - let x2 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - d, sp.y + d)); - b.line_to(Point::new(sp.x + d, sp.y - d)); - }); - frame.stroke(&x1, stroke.clone()); - frame.stroke(&x2, stroke); - } - SnapType::Quadrant => { - let r = 6.0_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y)); - b.line_to(Point::new(sp.x, sp.y + r)); - b.line_to(Point::new(sp.x - r, sp.y)); - b.close(); - }); - frame.stroke(&path, stroke); - } - SnapType::Intersection => { - // An extended intersection sets one or two extension bases; - // draw a dashed guide from each endpoint through the crossing - // so both contributing extension paths stay visible. A real - // on-segment crossing carries no bases, so nothing draws here. - // (#247, #259) - for base in [self.snap_ext_base, self.snap_ext_base2] - .into_iter() - .flatten() - { - let dx = sp.x - base.x; - let dy = sp.y - base.y; - let len = (dx * dx + dy * dy).sqrt(); - if len > 1e-3 { + let r = 5.0_f32; + let p1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + }); + let p2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + }); + frame.stroke(&p1, stroke.clone()); + frame.stroke(&p2, stroke); + } + SnapType::ApparentIntersection => { + // X like Intersection, framed by a small square so the + // two are visually distinguishable. + let r = 5.0_f32; + let rect = canvas::Path::rectangle( + Point::new(sp.x - r, sp.y - r), + Size::new(r * 2.0, r * 2.0), + ); + frame.stroke(&rect, stroke.clone()); + let xr = r - 1.5; + let p1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - xr, sp.y - xr)); + b.line_to(Point::new(sp.x + xr, sp.y + xr)); + }); + let p2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - xr, sp.y + xr)); + b.line_to(Point::new(sp.x + xr, sp.y - xr)); + }); + frame.stroke(&p1, stroke.clone()); + frame.stroke(&p2, stroke); + } + SnapType::Insertion => { + // Two overlapping rectangles (a small "tag" glyph). + let r = 5.0_f32; + let inner = canvas::Path::rectangle( + Point::new(sp.x - r * 0.5, sp.y - r), + Size::new(r, r * 2.0), + ); + let outer = canvas::Path::rectangle( + Point::new(sp.x - r, sp.y - r * 0.5), + Size::new(r * 2.0, r), + ); + frame.stroke(&outer, stroke.clone()); + frame.stroke(&inner, stroke); + } + SnapType::Perpendicular => { + // Right-angle hook in the lower-left quadrant. + let r = 6.0_f32; + let p = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + }); + let foot = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y)); + b.line_to(Point::new(sp.x, sp.y)); + b.line_to(Point::new(sp.x, sp.y + r)); + }); + frame.stroke(&p, stroke.clone()); + frame.stroke(&foot, stroke); + } + SnapType::Tangent => { + // Circle with a tangent bar across the top. + let r = 5.5_f32; + let c = canvas::Path::circle(sp, r); + frame.stroke(&c, stroke.clone()); + let bar = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + }); + frame.stroke(&bar, stroke); + } + SnapType::Nearest => { + // Bowtie / hourglass โ€” two opposed triangles meeting at sp. + let r = 5.5_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + b.close(); + }); + frame.stroke(&path, stroke); + } + SnapType::Extension => { + // Unit direction from the endpoint the snap extends from + // toward the snap point, so the guide line and the three + // dots follow the actual extension path (#238). + let dir = self.snap_ext_base.and_then(|base| { + let dx = sp.x - base.x; + let dy = sp.y - base.y; + let len = (dx * dx + dy * dy).sqrt(); + (len > 1e-3).then(|| (base, Point::new(dx / len, dy / len))) + }); + // Dashed guide line from that endpoint, through the snap + // point and a little beyond, so the extension path is + // visible as the cursor tracks along it. + if let Some((base, u)) = dir { let dash = canvas::Stroke { line_dash: canvas::LineDash { segments: &[4.0, 4.0], @@ -1720,229 +1913,115 @@ impl canvas::Program for SelectionCanvas { }, ..canvas::Stroke::default().with_color(marker).with_width(1.0) }; - let tip = Point::new(sp.x + dx / len * 18.0, sp.y + dy / len * 18.0); + let tip = Point::new(sp.x + u.x * 18.0, sp.y + u.y * 18.0); // `base` is a projected entity endpoint: clip before // stroking (see `clip_seg`). if let Some((a, b)) = clip_seg(base, tip, bounds) { frame.stroke(&canvas::Path::line(a, b), dash); } } - } - let r = 5.0_f32; - let p1 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - }); - let p2 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y - r)); - }); - frame.stroke(&p1, stroke.clone()); - frame.stroke(&p2, stroke); - } - SnapType::ApparentIntersection => { - // X like Intersection, framed by a small square so the - // two are visually distinguishable. - let r = 5.0_f32; - let rect = canvas::Path::rectangle( - Point::new(sp.x - r, sp.y - r), - Size::new(r * 2.0, r * 2.0), - ); - frame.stroke(&rect, stroke.clone()); - let xr = r - 1.5; - let p1 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - xr, sp.y - xr)); - b.line_to(Point::new(sp.x + xr, sp.y + xr)); - }); - let p2 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - xr, sp.y + xr)); - b.line_to(Point::new(sp.x + xr, sp.y - xr)); - }); - frame.stroke(&p1, stroke.clone()); - frame.stroke(&p2, stroke); - } - SnapType::Insertion => { - // Two overlapping rectangles (a small "tag" glyph). - let r = 5.0_f32; - let inner = canvas::Path::rectangle( - Point::new(sp.x - r * 0.5, sp.y - r), - Size::new(r, r * 2.0), - ); - let outer = canvas::Path::rectangle( - Point::new(sp.x - r, sp.y - r * 0.5), - Size::new(r * 2.0, r), - ); - frame.stroke(&outer, stroke.clone()); - frame.stroke(&inner, stroke); - } - SnapType::Perpendicular => { - // Right-angle hook in the lower-left quadrant. - let r = 6.0_f32; - let p = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - }); - let foot = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y)); - b.line_to(Point::new(sp.x, sp.y)); - b.line_to(Point::new(sp.x, sp.y + r)); - }); - frame.stroke(&p, stroke.clone()); - frame.stroke(&foot, stroke); - } - SnapType::Tangent => { - // Circle with a tangent bar across the top. - let r = 5.5_f32; - let c = canvas::Path::circle(sp, r); - frame.stroke(&c, stroke.clone()); - let bar = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y - r)); - }); - frame.stroke(&bar, stroke); - } - SnapType::Nearest => { - // Bowtie / hourglass โ€” two opposed triangles meeting at sp. - let r = 5.5_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y - r)); - b.line_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - b.close(); - }); - frame.stroke(&path, stroke); - } - SnapType::Extension => { - // Unit direction from the endpoint the snap extends from - // toward the snap point, so the guide line and the three - // dots follow the actual extension path (#238). - let dir = self.snap_ext_base.and_then(|base| { - let dx = sp.x - base.x; - let dy = sp.y - base.y; - let len = (dx * dx + dy * dy).sqrt(); - (len > 1e-3).then(|| (base, Point::new(dx / len, dy / len))) - }); - // Dashed guide line from that endpoint, through the snap - // point and a little beyond, so the extension path is - // visible as the cursor tracks along it. - if let Some((base, u)) = dir { - let dash = canvas::Stroke { - line_dash: canvas::LineDash { - segments: &[4.0, 4.0], - offset: 0, - }, - ..canvas::Stroke::default().with_color(marker).with_width(1.0) - }; - let tip = Point::new(sp.x + u.x * 18.0, sp.y + u.y * 18.0); - // `base` is a projected entity endpoint: clip before - // stroking (see `clip_seg`). - if let Some((a, b)) = clip_seg(base, tip, bounds) { - frame.stroke(&canvas::Path::line(a, b), dash); + // Three dots at the snap point, strung along the extension + // direction (horizontal fallback when the base is unknown). + let u = dir.map(|(_, u)| u).unwrap_or(Point::new(1.0, 0.0)); + let r = 1.4_f32; + for k in [-7.0_f32, 0.0, 7.0] { + let dot = + canvas::Path::circle(Point::new(sp.x + u.x * k, sp.y + u.y * k), r); + frame.fill(&dot, marker); } } - // Three dots at the snap point, strung along the extension - // direction (horizontal fallback when the base is unknown). - let u = dir.map(|(_, u)| u).unwrap_or(Point::new(1.0, 0.0)); - let r = 1.4_f32; - for k in [-7.0_f32, 0.0, 7.0] { - let dot = - canvas::Path::circle(Point::new(sp.x + u.x * k, sp.y + u.y * k), r); - frame.fill(&dot, marker); + SnapType::Parallel => { + // Two short parallel diagonal bars. + let r = 6.0_f32; + let off = 3.0_f32; + let b1 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r - off, sp.y + r)); + b.line_to(Point::new(sp.x + r - off, sp.y - r)); + }); + let b2 = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r + off, sp.y + r)); + b.line_to(Point::new(sp.x + r + off, sp.y - r)); + }); + frame.stroke(&b1, stroke.clone()); + frame.stroke(&b2, stroke); + } + SnapType::Grid => { + let arm = 4.0_f32; + let h = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - arm, sp.y)); + b.line_to(Point::new(sp.x + arm, sp.y)); + }); + let v = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - arm)); + b.line_to(Point::new(sp.x, sp.y + arm)); + }); + frame.stroke(&h, stroke.clone()); + frame.stroke(&v, stroke); + } + SnapType::Vertex => { + // Filled square: the solid rhyme of Endpoint's hollow box. + let h = 5.0_f32; + frame.fill( + &canvas::Path::rectangle( + Point::new(sp.x - h, sp.y - h), + Size::new(h * 2.0, h * 2.0), + ), + marker, + ); + } + SnapType::EdgeMidpoint => { + // Filled triangle: the solid rhyme of Midpoint's outline. + let r = 6.0_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - r)); + b.line_to(Point::new(sp.x + r * 0.866, sp.y + r * 0.5)); + b.line_to(Point::new(sp.x - r * 0.866, sp.y + r * 0.5)); + b.close(); + }); + frame.fill(&path, marker); + } + SnapType::FaceCenter => { + // Filled disc: the solid rhyme of Center's outline. + frame.fill(&canvas::Path::circle(sp, 5.5_f32), marker); + } + SnapType::Knot => { + // Filled diamond: distinct from Quadrant's outline. + let r = 5.5_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y)); + b.line_to(Point::new(sp.x, sp.y + r)); + b.line_to(Point::new(sp.x - r, sp.y)); + b.close(); + }); + frame.fill(&path, marker); + } + SnapType::FacePerpendicular => { + // Right-angle hook like the 2D marker, plus a filled foot + // dot marking the face contact. + let r = 6.0_f32; + let p = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + }); + frame.stroke(&p, stroke.clone()); + frame.fill(&canvas::Path::circle(sp, 2.0_f32), marker); + } + SnapType::NearestFace => { + // Filled bowtie: the solid rhyme of Nearest's outline. + let r = 5.5_f32; + let path = canvas::Path::new(|b| { + b.move_to(Point::new(sp.x - r, sp.y - r)); + b.line_to(Point::new(sp.x + r, sp.y - r)); + b.line_to(Point::new(sp.x - r, sp.y + r)); + b.line_to(Point::new(sp.x + r, sp.y + r)); + b.close(); + }); + frame.fill(&path, marker); } } - SnapType::Parallel => { - // Two short parallel diagonal bars. - let r = 6.0_f32; - let off = 3.0_f32; - let b1 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r - off, sp.y + r)); - b.line_to(Point::new(sp.x + r - off, sp.y - r)); - }); - let b2 = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r + off, sp.y + r)); - b.line_to(Point::new(sp.x + r + off, sp.y - r)); - }); - frame.stroke(&b1, stroke.clone()); - frame.stroke(&b2, stroke); - } - SnapType::Grid => { - let arm = 4.0_f32; - let h = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - arm, sp.y)); - b.line_to(Point::new(sp.x + arm, sp.y)); - }); - let v = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - arm)); - b.line_to(Point::new(sp.x, sp.y + arm)); - }); - frame.stroke(&h, stroke.clone()); - frame.stroke(&v, stroke); - } - SnapType::Vertex => { - // Filled square: the solid rhyme of Endpoint's hollow box. - let h = 5.0_f32; - frame.fill( - &canvas::Path::rectangle( - Point::new(sp.x - h, sp.y - h), - Size::new(h * 2.0, h * 2.0), - ), - marker, - ); - } - SnapType::EdgeMidpoint => { - // Filled triangle: the solid rhyme of Midpoint's outline. - let r = 6.0_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - r)); - b.line_to(Point::new(sp.x + r * 0.866, sp.y + r * 0.5)); - b.line_to(Point::new(sp.x - r * 0.866, sp.y + r * 0.5)); - b.close(); - }); - frame.fill(&path, marker); - } - SnapType::FaceCenter => { - // Filled disc: the solid rhyme of Center's outline. - frame.fill(&canvas::Path::circle(sp, 5.5_f32), marker); - } - SnapType::Knot => { - // Filled diamond: distinct from Quadrant's outline. - let r = 5.5_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y)); - b.line_to(Point::new(sp.x, sp.y + r)); - b.line_to(Point::new(sp.x - r, sp.y)); - b.close(); - }); - frame.fill(&path, marker); - } - SnapType::FacePerpendicular => { - // Right-angle hook like the 2D marker, plus a filled foot - // dot marking the face contact. - let r = 6.0_f32; - let p = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - }); - frame.stroke(&p, stroke.clone()); - frame.fill(&canvas::Path::circle(sp, 2.0_f32), marker); - } - SnapType::NearestFace => { - // Filled bowtie: the solid rhyme of Nearest's outline. - let r = 5.5_f32; - let path = canvas::Path::new(|b| { - b.move_to(Point::new(sp.x - r, sp.y - r)); - b.line_to(Point::new(sp.x + r, sp.y - r)); - b.line_to(Point::new(sp.x - r, sp.y + r)); - b.line_to(Point::new(sp.x + r, sp.y + r)); - b.close(); - }); - frame.fill(&path, marker); - } - } + }); } // โ”€โ”€ CAD crosshair cursor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -1961,11 +2040,54 @@ impl canvas::Program for SelectionCanvas { // arrow (see `mouse_interaction`); drawing the CAD crosshair on // top of it would double up the visual feedback. let over_divider = self.divider_under(cursor, bounds); + // โ”€โ”€ MATCHPROP paintbrush cursor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // While MATCHPROP is armed the user is transferring properties from a + // source object onto others, so the crosshair gives way to a paintbrush + // whose tip rides the pick point, as the reference does. Drawn *instead of* + // the crosshair below, never on top of it. + if !over_viewcube + && !over_divider + && self.nav == NavCursor::None + && !self.suppressed + && self.show_match_brush + { + if let Some(cp) = self + .grips + .iter() + .find(|grip| grip.is_hovered) + .map(|grip| grip.pos) + .or(self.selection.borrow().input.last_move_pos) + { + // Inherit the crosshair's own colour (including a custom + // crosshair colour) so only the shape changes, never the ink. + let [cr, cg, cb, ca] = self.crosshair.color.map_or_else( + || { + crate::scene::view::render::adapt_to_bg( + [1.0, 1.0, 1.0, 0.90], + self.crosshair_bg, + ) + }, + |[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 0.90], + ); + draw_match_brush( + &mut frame, + cp, + Color { + r: cr, + g: cg, + b: cb, + a: ca, + }, + ); + } + } + // An armed navigation tool replaces the crosshair with its own cursor. if !over_viewcube && !over_divider && self.nav == NavCursor::None && !self.suppressed + && !self.show_match_brush && self.crosshair.cursor_type == CursorType::Crosshair { // Over a grip the crosshair locks onto it, so the click lands on diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 20e0eb9a8..5f59f7209 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -510,6 +510,13 @@ pub fn build_field_key_map(sections: &[PropSection]) -> HashMap iced::Task { + focused_widget_task(Message::PropSyncActive) +} + +/// Ask the widget tree which field holds keyboard focus. +pub fn focused_widget_task( + on_result: fn(Option) -> Message, +) -> iced::Task { use iced::advanced::widget::operation::{Focusable, Outcome}; use iced::advanced::widget::{Id, Operation}; #[derive(Default)] @@ -534,7 +541,7 @@ pub fn sync_active_field_task() -> iced::Task { Outcome::Some(self.focused.clone()) } } - iced::advanced::widget::operate(FocusedId::default()).map(Message::PropSyncActive) + iced::advanced::widget::operate(FocusedId::default()).map(on_result) } /// A translated choice label paired with the unchanged value stored in the @@ -614,6 +621,12 @@ pub struct PropertiesPanel { pub collapsed_sections: HashSet, /// Whether the editable-dropdown (block Name) option list is open. pub edit_choice_open: bool, + /// The open dropdown belongs to Quick Properties, not the docked panel: + /// both render these rows, and two open copies swallow each other's + /// clicks (#1766). + pub picker_in_quick: bool, + /// Set while `quick_view` builds its rows. + pub rendering_quick: std::cell::Cell, /// Field key of the value row currently being edited, or `None`. Marked when /// the user focuses an editable value field and cleared on commit / when the /// selection changes, so the active row can be highlighted. Geometry fields @@ -652,6 +665,8 @@ impl Default for PropertiesPanel { expanded_groups: HashSet::default(), collapsed_sections: HashSet::default(), edit_choice_open: false, + picker_in_quick: false, + rendering_quick: std::cell::Cell::new(false), active_field: None, field_key_by_id: HashMap::default(), } @@ -673,6 +688,11 @@ impl PropertiesPanel { self.field_key_by_id.get(id).cloned() } + /// Whether the open dropdown belongs to the surface being built. + fn picker_here(&self) -> bool { + self.picker_in_quick == self.rendering_quick.get() + } + pub fn selected_handles(&self) -> Vec { self.selected_group .as_ref() @@ -737,11 +757,13 @@ impl PropertiesPanel { for section in &self.sections { col = col.push(self.render_section(section)); } - scrollable(col.width(Length::Fill)) - .spacing(8) - .width(Length::Fill) - .height(Length::Fill) - .into() + Element::from( + scrollable(col.width(Length::Fill)) + .spacing(8) + .width(Length::Fill) + .height(Length::Fill), + ) + .map(|message| tag_picker(false, message)) }; container(column![header, title_bar, content]) @@ -790,14 +812,19 @@ impl PropertiesPanel { .padding([4, 10]); let mut sections = column![].spacing(0); + self.rendering_quick.set(true); for section in &self.sections { sections = sections.push(self.render_section(section)); } + self.rendering_quick.set(false); - let content = scrollable(sections.width(Length::Fill)) - .spacing(8) - .width(Length::Fill) - .height(Length::Shrink); + let content = Element::from( + scrollable(sections.width(Length::Fill)) + .spacing(8) + .width(Length::Fill) + .height(Length::Shrink), + ) + .map(|message| tag_picker(true, message)); Some( container(column![title, content].spacing(0)) @@ -995,7 +1022,7 @@ impl PropertiesPanel { if field == "background_color" { let selector = crate::ui::color_select::color_selector( color, - self.bg_color_picker_open, + self.bg_color_picker_open && self.picker_here(), crate::ui::color_select::ColorExtras { by_layer: true, by_block: true, @@ -1028,7 +1055,7 @@ impl PropertiesPanel { | "background_fill_color" | "indicator_fill_color" ) { - let open = self.open_color_field.as_deref() == Some(field); + let open = self.open_color_field.as_deref() == Some(field) && self.picker_here(); let fsel = field.to_string(); let full_palette_field = field.to_string(); let extras = if field == "dim_text_fill_color" { @@ -1071,7 +1098,7 @@ impl PropertiesPanel { let selector = crate::ui::color_select::color_selector_with_name( color, display_name, - self.color_picker_open, + self.color_picker_open && self.picker_here(), crate::ui::color_select::ColorExtras { by_layer: true, by_block: true, @@ -1086,7 +1113,7 @@ impl PropertiesPanel { fn render_color_varies_row<'a>(&'a self, label: &'a str) -> Element<'a, Message> { let selector = crate::ui::color_select::color_selector_varies( - self.color_picker_open, + self.color_picker_open && self.picker_here(), crate::ui::color_select::ColorExtras { by_layer: true, by_block: true, @@ -1345,7 +1372,7 @@ impl PropertiesPanel { }) .width(Length::Fill); let caret = button( - container(if self.edit_choice_open { + container(if self.edit_choice_open && self.picker_here() { crate::ui::icons::themed_arrow_up(9.0) } else { crate::ui::icons::themed_arrow_down(9.0) @@ -1398,7 +1425,7 @@ impl PropertiesPanel { }) .width(Length::Fill); - if !self.edit_choice_open { + if !(self.edit_choice_open && self.picker_here()) { return prop_row_with_active(label, head.into(), active); } @@ -1602,7 +1629,7 @@ impl PropertiesPanel { text(crate::ui::text_util::elide(current, 16)) .size(FONT_SZ) .width(Length::Fill), - if self.hatch_pattern_picker_open { + if self.hatch_pattern_picker_open && self.picker_here() { crate::ui::icons::themed_arrow_up(FONT_SZ) } else { crate::ui::icons::themed_arrow_down(FONT_SZ) @@ -1622,7 +1649,7 @@ impl PropertiesPanel { })), text_color: palette.background.base.text, border: Border { - color: if self.hatch_pattern_picker_open { + color: if self.hatch_pattern_picker_open && self.picker_here() { palette.primary.base.color } else { palette.background.neutral.color @@ -1636,7 +1663,7 @@ impl PropertiesPanel { .padding([COMBO_PAD_V, 6.0]) .width(Length::Fill); - if !self.hatch_pattern_picker_open { + if !(self.hatch_pattern_picker_open && self.picker_here()) { return prop_row_widget(label, head.into()); } @@ -2363,3 +2390,18 @@ mod tests { assert!((0.01..=100.0).contains(&scale)); } } + +/// Marks a dropdown toggle with the surface it came from (see +/// `PropertiesPanel::picker_in_quick`). +fn tag_picker(quick: bool, message: Message) -> Message { + match message { + Message::PropColorPickerToggle + | Message::PropBgColorPickerToggle + | Message::PropColorFieldToggle(_) + | Message::PropEditChoiceToggle + | Message::PropHatchPatternPickerToggle(_) => { + Message::PropPickerSurface(quick, Box::new(message)) + } + other => other, + } +} diff --git a/src/ui/ribbon/draw_panel.rs b/src/ui/ribbon/draw_panel.rs index a140ac1d4..ee0b84896 100644 --- a/src/ui/ribbon/draw_panel.rs +++ b/src/ui/ribbon/draw_panel.rs @@ -9,7 +9,7 @@ use super::widgets::{ make_icon, make_tip, muted_text_style, popup_panel_style, popup_row_style, tip_style, tool_btn_style, LARGE_DROPDOWN_ARROW_SIZE, }; -use super::{dropdown_backdrop, position_ribbon_dropdown, Ribbon}; +use super::{dropdown_backdrop, keytips, position_ribbon_dropdown, Ribbon}; use crate::app::Message; use crate::modules::IconKind; use crate::t; @@ -248,6 +248,32 @@ fn panel_for_group(title: &str, group_ids: &[&'static str]) -> Option { }) } +/// The slide-out under the group `title`, if it has one. +pub(super) fn slide_out_id(title: &str, group_ids: &[&'static str]) -> Option<&'static str> { + panel_for_group(title, group_ids).map(|panel| panel.id) +} + +/// What the open slide-out or tool submenu `id` offers, as (dropdown id, +/// command, opens a submenu) in display order. +pub(super) fn keytip_items(id: &str) -> Vec<(&'static str, &'static str, bool)> { + let Some(panel) = panel_for_dropdown(id) else { + return Vec::new(); + }; + if panel.id == id { + return panel + .tools + .iter() + .map(|tool| (panel.id, tool.command, !tool.options.is_empty())) + .collect(); + } + panel + .tools + .iter() + .filter(|tool| tool.command == id) + .flat_map(|tool| tool.options.iter().map(|(cmd, _)| (tool.command, *cmd, false))) + .collect() +} + /// Every command the slide-out of the group `title` offers: each tool and /// each of its submenu options. pub(super) fn slide_out_commands( @@ -311,7 +337,8 @@ pub(super) fn group_title<'a>( if panel.id != REFERENCE_PANEL_ID { return title_button; } - // The corner arrow opens the External References palette. + // The corner arrow opens the External References palette, or closes it + // when it is open. let launcher = tooltip( // A right arrow turned to point into the corner. button( @@ -323,10 +350,7 @@ pub(super) fn group_title<'a>( color: Some(theme.palette().background.base.text), }), ) - .on_press(Message::RibbonToolClick { - tool_id: "EXTERNALREFERENCES".to_string(), - event: crate::modules::ModuleEvent::Command("EXTERNALREFERENCES".to_string()), - }) + .on_press(Message::ToggleXrefManager) .style(move |theme: &Theme, status| tool_btn_style(theme, false, status)) .padding([0, 3]), make_tip(format!( @@ -339,6 +363,7 @@ pub(super) fn group_title<'a>( .delay(Duration::from_millis(400)) .style(tip_style); // Shrink-sized: a Fill here would make the panel claim the whole row. + let launcher = keytips::anchor(keytips::XREF_LAUNCHER, launcher); row![title_button, launcher] .spacing(6) .align_y(iced::Center) @@ -364,6 +389,7 @@ fn tool_button(tool: &Tool, active: bool, panel_id: &'static str) -> Element<'st .delay(Duration::from_millis(400)) .style(tip_style) .into(); + let face = keytips::anchor_with(|| keytips::item_anchor(panel_id, tool.command), face); if !tool.options.is_empty() { row![ face, @@ -403,6 +429,7 @@ fn reference_overlay<'a>(ribbon: &Ribbon, panel: Panel, win: (f32, f32)) -> Elem .style(popup_row_style) .width(Fill) .padding([3, 6]); + let edit = keytips::anchor_with(|| keytips::item_anchor(panel.id, tool.command), edit); let fade = ribbon.xref_fade; let on = fade > 0; let switch = button(make_icon( @@ -413,18 +440,23 @@ fn reference_overlay<'a>(ribbon: &Ribbon, panel: Panel, win: (f32, f32)) -> Elem .style(move |theme: &Theme, status| tool_btn_style(theme, on, status)) .padding(3); let amount = fade.unsigned_abs().min(90) as u8; + let switch = tooltip( + switch, + make_tip(format!( + "{}\n{} XDWGFADECTL", + t!("Xref fading"), + t!("Command:") + )), + tooltip::Position::Bottom, + ) + .delay(Duration::from_millis(400)) + .style(tip_style); + let switch = keytips::anchor_with( + || keytips::item_anchor(panel.id, keytips::XREF_FADE), + switch, + ); let fading = row![ - tooltip( - switch, - make_tip(format!( - "{}\n{} XDWGFADECTL", - t!("Xref fading"), - t!("Command:") - )), - tooltip::Position::Bottom - ) - .delay(Duration::from_millis(400)) - .style(tip_style), + switch, text(t!("Xref fading")).size(11), iced::widget::slider(0..=90u8, amount, Message::XrefFadeSlide) .on_release(Message::XrefFadeCommit) @@ -470,7 +502,7 @@ pub(super) fn overlay<'a>(ribbon: &Ribbon, id: &str, win: (f32, f32)) -> Element tool.options .iter() .map(|&(cmd, label)| { - button(text(t!(label)).size(11)) + let item = button(text(t!(label)).size(11)) .on_press(Message::DropdownSelectItem { dropdown_id: tool.command, cmd, @@ -478,8 +510,8 @@ pub(super) fn overlay<'a>(ribbon: &Ribbon, id: &str, win: (f32, f32)) -> Element .style(popup_row_style) .width(Fill) .height(OPTION_HEIGHT) - .padding([3, 7]) - .into() + .padding([3, 7]); + keytips::anchor_with(|| keytips::item_anchor(tool.command, cmd), item) }) .collect::>>(), ) diff --git a/src/ui/ribbon/keytips.rs b/src/ui/ribbon/keytips.rs new file mode 100644 index 000000000..a7a626eaf --- /dev/null +++ b/src/ui/ribbon/keytips.rs @@ -0,0 +1,1137 @@ +//! Key tips: a lone Alt shows a code on every ribbon tab and quick-access +//! button; typing a tab's code opens it and shows the codes of its buttons. +//! A button runs, a dropdown or slide-out opens and shows the codes of its +//! items, and Esc goes back one level. The codes come from the reference +//! ribbon; buttons it has no counterpart for get none. + +use std::cell::RefCell; +use std::sync::atomic::{AtomicBool, Ordering}; + +use iced::advanced::layout::{self, Layout}; +use iced::advanced::text::{self as advanced_text, Paragraph as _, Renderer as _}; +use iced::advanced::widget::{self, Widget}; +use iced::advanced::{mouse, overlay, renderer, Renderer as _}; +use iced::keyboard::{self, key::Named, Key}; +use iced::{ + Background, Border, Color, Element, Length, Pixels, Point, Rectangle, Renderer, Shadow, Size, + Theme, Vector, +}; + +use super::{draw_panel, item_id, Ribbon}; +use crate::app::Message; +use crate::modules::{RibbonItem, ToolDef}; +use crate::ui::wrap_bar::{fresh_bounds, PosReport}; + +/// Key tip input, from the keyboard listener or automation. +#[derive(Debug, Clone)] +pub enum KeyTipInput { + /// Alt was pressed and released on its own. + AltTap, + /// Show the tab level; `refocus` returns the focus to the command line + /// when the tips close. + Open { refocus: bool }, + /// One typed character (upper case). + Char(String), + /// Settle a code that also starts a longer one. + Enter, + Escape, + Hide, +} + +/// What a typed code leads to. +pub enum KeyTipStep { + /// Nothing matched; the key is ignored. + Ignore, + /// A partial code, or a tab that is now open. + Stay, + /// A dropdown, slide-out or collapsed panel opens; the tips stay. + Open(Message), + /// A command runs and the tips close. + Run(Message), +} + +#[derive(Clone)] +enum Action { + Tab(usize), + Open(Message), + Run(Message), +} + +/// One code shown on one widget. +pub struct Tip { + /// The id the widget reports its bounds under. + pub anchor: String, + /// The table key: tool id, dropdown id, group title or command. + pub target: &'static str, + pub code: Option, + action: Action, +} + +/// Open key tips. +pub(super) struct State { + /// Showing the tab codes rather than the open tab's buttons. + top: bool, + /// Codes of the levels entered so far. + path: Vec, + /// The start of a longer code. + typed: String, + /// The command line had the focus when the tips opened. + refocus: bool, +} + +static ACTIVE: AtomicBool = AtomicBool::new(false); +/// Alt is down and nothing else was pressed since. +static ALT_ALONE: AtomicBool = AtomicBool::new(false); + +thread_local! { + /// Anchors that carried a code in the last drawn frame. + static SHOWN: RefCell> = const { RefCell::new(Vec::new()) }; +} + +/// Id of the whole ribbon (tab row and tools), for the hint bar. +const RIBBON_AREA: &str = "KEYTIP_RIBBON_AREA"; +/// The Reference panel's corner arrow. +pub(super) const XREF_LAUNCHER: &str = "XREF_LAUNCHER"; +/// The xref fading switch in the Reference slide-out. +pub(super) const XREF_FADE: &str = "XREF_FADE"; +const COLLAPSED_PREFIX: &str = "panel:"; + +pub fn active() -> bool { + ACTIVE.load(Ordering::Relaxed) +} + +/// Record `el`'s bounds under `id` while key tips show. +pub(super) fn anchor<'a>(id: &'static str, el: impl Into>) -> Element<'a, Message> { + if active() { + PosReport::new(id, el).into() + } else { + el.into() + } +} + +/// `anchor` under an id built only while key tips show. +pub(super) fn anchor_with<'a>( + id: impl FnOnce() -> String, + el: impl Into>, +) -> Element<'a, Message> { + if active() { + PosReport::owned(id(), el).into() + } else { + el.into() + } +} + +/// Anchor id of item `cmd` in dropdown or slide-out `dropdown`. +pub(super) fn item_anchor(dropdown: &str, cmd: &str) -> String { + format!("{dropdown}/{cmd}") +} + +/// Anchor id of a quick-access button (its command can also be a tool's). +pub(super) fn quick_access_anchor(id: &str) -> String { + if id == super::widgets::UNDO_HISTORY_ID || id == super::widgets::REDO_HISTORY_ID { + id.to_string() + } else { + format!("qa:{id}") + } +} + +/// Anchor id of a collapsed panel's button. +pub(super) fn collapsed_anchor(title: &str) -> String { + format!("{COLLAPSED_PREFIX}{title}") +} + +/// Keyboard listener hook for a key press. `Some` while key tips own the +/// keyboard (the inner value is the message to send, if any). +pub fn key_pressed(key: &Key, modifiers: keyboard::Modifiers, repeat: bool) -> Option> { + let alt = matches!(key, Key::Named(Named::Alt)); + if !repeat { + ALT_ALONE.store( + alt && !modifiers.control() && !modifiers.shift() && !modifiers.logo(), + Ordering::Relaxed, + ); + } + if !active() { + return None; + } + Some(match key { + Key::Named(Named::Escape) => Some(Message::KeyTip(KeyTipInput::Escape)), + Key::Named(Named::Enter) => Some(Message::KeyTip(KeyTipInput::Enter)), + Key::Character(value) if !modifiers.control() && !modifiers.alt() => value + .chars() + .next() + .filter(|ch| ch.is_alphanumeric()) + // A dotted capital I (Turkish layouts) types the code letter I. + .map(|ch| if ch == 'ฤฐ' { 'I' } else { ch }) + .map(|ch| Message::KeyTip(KeyTipInput::Char(ch.to_uppercase().collect()))), + _ => None, + }) +} + +/// Keyboard listener hook for a key release: a lone Alt toggles the tips. +pub fn key_released(key: &Key) -> Option { + (matches!(key, Key::Named(Named::Alt)) && ALT_ALONE.swap(false, Ordering::Relaxed)) + .then_some(Message::KeyTip(KeyTipInput::AltTap)) +} + +/// A pointer press ends a lone Alt. +pub fn pointer_pressed() { + ALT_ALONE.store(false, Ordering::Relaxed); +} + +/// Codes of the ribbon tabs, by module id. +fn tab_code(module: &str) -> Option<&'static str> { + Some(match module { + "draw" => "H", + "parametric" => "PA", + "model" => "3D", + "insert" => "IN", + "annotate" => "AN", + "view" => "VI", + "manage" => "MA", + "block_editor" => "BE", + _ => return None, + }) +} + +/// Quick-access buttons in order; their codes are their positions. +const QUICK_ACCESS: &[&str] = &[ + "QNEW", + "OPEN", + "SAVE", + "SAVEAS", + "PRINT", + super::widgets::UNDO_HISTORY_ID, + super::widgets::REDO_HISTORY_ID, +]; + +/// Reference codes as (scope, target, code). In a tab's scope (its module id) +/// the target is a tool id, a dropdown id or a group title; a group title's +/// code opens the panel's slide-out or, while the panel is collapsed, its +/// flyout. In a dropdown's or slide-out's scope the target is an item's +/// command. +const CODES: &[(&str, &str, &str)] = &[ + // Draw (the reference Home tab). + ("draw", "Draw", "DW"), + ("draw", "LINE", "LI"), + ("draw", "PLINE", "PL"), + ("draw", "CIRCLE", "CI"), + ("draw", "ARC", "AR"), + ("draw", "SHAPES", "RE"), + ("draw", "ELLIPSE", "EL"), + ("draw", "HATCH", "HA"), + ("SHAPES", "RECT", "RE"), + ("SHAPES", "POLY", "PO"), + ("HATCH", "HATCH", "HA"), + ("HATCH", "GRADIENT", "GR"), + ("HATCH", "BOUNDARY", "BO"), + ("draw_extension", "SPLINE", "SF"), + ("draw_extension", "SPLINECV", "SC"), + ("draw_extension", "XLINE", "XL"), + ("draw_extension", "RAY", "RA"), + ("draw_extension", "DIVIDE", "DI"), + ("draw_extension", "MEASURE", "ME"), + ("draw_extension", "REGION", "RG"), + ("draw_extension", "HELIX", "HE"), + ("draw_extension", "DONUT", "DO"), + ("draw_extension", "MULTIPOINT", "MP"), + ("draw", "Modify", "MD"), + ("draw", "MOVE", "MO"), + ("draw", "COPY", "CO"), + ("draw", "STRETCH", "ST"), + ("draw", "ROTATE", "RO"), + ("draw", "MIRROR", "MI"), + ("draw", "SCALE", "SC"), + ("draw", "trim_extend", "TR"), + ("draw", "fillet_chamfer", "FI"), + ("draw", "array_type", "AY"), + ("draw", "ERASE", "ER"), + ("draw", "EXPLODE", "EX"), + ("draw", "OFFSET", "OF"), + ("modify_extension", "SETBYLAYER", "SL"), + ("modify_extension", "LENGTHEN", "LE"), + ("modify_extension", "PEDIT", "EP"), + ("modify_extension", "SPLINEDIT", "ES"), + ("modify_extension", "HATCHEDIT", "EH"), + ("modify_extension", "ALIGN", "AL"), + ("modify_extension", "BREAK", "BR"), + ("modify_extension", "BREAKATPOINT", "BP"), + ("modify_extension", "JOIN", "JO"), + ("modify_extension", "REVERSE", "RV"), + ("modify_extension", "NCOPY", "CN"), + ("modify_extension", "DRAWORDER_FRONT", "DR"), + ("draw", "Annotation", "AN"), + ("draw", "ANNOTATION_TEXT", "MT"), + ("draw", "ANNOTATION_DIMENSIONS", "DI"), + ("draw", "ANNOTATION_LEADER", "ML"), + ("draw", "Layers", "LL"), + ("draw", "LAYERS", "LP"), + ("draw", "LAYER_COMBO", "LA"), + ("draw", "LAYOFF", "LO"), + ("draw", "LAYFRZ", "LF"), + ("draw", "LAYLCK", "LK"), + ("draw", "LAYMCUR", "LC"), + ("draw", "LAYISO", "LS"), + ("draw", "LAYON", "LN"), + ("draw", "LAYTHW", "LW"), + ("draw", "LAYULK", "UL"), + ("draw", "LAYMATCH", "LM"), + ("draw", "LAYUNISO", "LU"), + ("draw", "Block", "BK"), + ("draw", "BLOCK", "BC"), + ("draw", "BEDIT", "BE"), + ("draw", "ATTEDIT", "AE"), + ("home_block_extension", "ATTDEF", "AD"), + ("home_block_extension", "ATTMAN", "AM"), + ("home_block_extension", "ATTSYNC", "AS"), + ("home_block_extension", "BASE", "BA"), + ("draw", "Properties", "PE"), + ("draw", "PROP_LINETYPE", "PY"), + ("draw", "PROP_LW", "PW"), + ("draw", "Groups", "GR"), + ("draw", "GROUP", "GG"), + ("draw", "UNGROUP", "GU"), + ("draw", "PASTE_MENU", "PA"), + ("draw", "COPYCLIP", "CC"), + ("draw", "CUTCLIP", "CX"), + ("draw", "MEASURE_MENU", "DT"), + // Parametric. + ("parametric", "AUTOCONSTRAIN", "AC"), + ("parametric", "CCONSTRAINT", "CI"), + ("parametric", "PCONSTRAINT", "PA"), + ("parametric", "TCONSTRAINT", "TA"), + ("parametric", "LCONSTRAINT", "CL"), + ("parametric", "QCONSTRAINT", "PE"), + ("parametric", "GCSMOOTH", "SM"), + ("parametric", "GCCONCENTRIC", "CO"), + ("parametric", "GCHORIZONTAL", "HO"), + ("parametric", "SYCONSTRAINT", "SY"), + ("parametric", "FXCONSTRAINT", "FI"), + ("parametric", "VCONSTRAINT", "VE"), + ("parametric", "ECONSTRAINT", "EQ"), + ("parametric", "GCVISIBILITY", "SH"), + ("parametric", "GCSHOWALL", "SA"), + ("parametric", "GCHIDEALL", "HA"), + ("parametric", "Dimensional", "DM"), + ("parametric", "DC_LINEAR_MENU", "DC"), + ("parametric", "DCALIGNED", "AL"), + ("parametric", "DCRADIUS", "RA"), + ("parametric", "DCDIAMETER", "DI"), + ("parametric", "DCANGULAR", "AN"), + ("parametric", "DCCONVERT", "DO"), + ("parametric", "DCVISIBILITY", "TD"), + ("parametric", "DCSHOWALL", "SD"), + ("parametric", "DCHIDEALL", "HD"), + ("parametric", "DELCONSTRAINT", "DE"), + ("parametric", "PARAMETERS", "PM"), + // Model (the reference Solid tab). + ("model", "MODEL_PRIMITIVES", "SS"), + ("MODEL_PRIMITIVES", "BOX", "BO"), + ("MODEL_PRIMITIVES", "CYLINDER", "CY"), + ("MODEL_PRIMITIVES", "CONE", "CO"), + ("MODEL_PRIMITIVES", "SPHERE", "SP"), + ("MODEL_PRIMITIVES", "PYRAMID", "PY"), + ("MODEL_PRIMITIVES", "WEDGE", "WE"), + ("MODEL_PRIMITIVES", "TORUS", "TO"), + ("MODEL_PRIMITIVES", "POLYSOLID", "SR"), + ("model", "EXTRUDE", "EX"), + ("model", "REVOLVE", "RV"), + ("model", "LOFT", "LO"), + ("model", "SWEEP", "SY"), + ("model", "PRESSPULL", "PU"), + ("model", "UNION", "UN"), + ("model", "SUBTRACT", "SU"), + ("model", "INTERSECT", "IN"), + ("model", "FILLETEDGE", "ED"), + ("model", "CHAMFEREDGE", "CE"), + ("model", "SHELL", "SH"), + // Insert. + ("insert", "Block", "BK"), + ("insert", "INSERT_GALLERY", "BI"), + ("insert", "ATTEDIT_DROPDOWN", "AE"), + ("insert", "ATTMODE_DROPDOWN", "AT"), + ("insert", "Block Definition", "BD"), + ("insert", "BLOCK", "BC"), + ("insert", "ATTDEF", "AD"), + ("insert", "ATTMAN", "AM"), + ("insert", "BEDIT", "BE"), + ("block_definition_extension", "BASE", "BA"), + ("block_definition_extension", "ATTSYNC", "AS"), + ("insert", "Reference", "RE"), + ("insert", XREF_LAUNCHER, "XR"), + ("insert", "XATTACH", "XA"), + ("insert", "UNDERLAY_ATTACH", "UA"), + ("insert", "CLIP", "CL"), + ("insert", "ADJUST", "AJ"), + ("insert", "UNDERLAYLAYERS", "UN"), + ("insert", "FRAMES_DROPDOWN", "FR"), + ("insert", "UOSNAP_DROPDOWN", "SN"), + ("reference_extension", "REFEDIT", "ER"), + ("reference_extension", XREF_FADE, "XF"), + // Annotate. + ("annotate", "Text", "TE"), + ("annotate", "ANNOTATE_TEXT", "MT"), + ("annotate", "TEXT_STYLE_COMBO", "TY"), + ("annotate", "Dimensions", "DM"), + ("annotate", "ANNOTATE_DIM", "DN"), + ("annotate", "DIM_STYLE_COMBO", "DY"), + ("annotate", "QDIM", "QD"), + ("annotate", "DIMCONTINUE", "CO"), + ("annotate", "TOLERANCE", "TO"), + ("annotate", "DIMEDIT", "DE"), + ("annotate", "DIMBREAK", "BR"), + ("annotate", "DIMSPACE", "SP"), + ("annotate", "DIMJOGLINE", "DJ"), + ("annotate", "Centerlines", "CE"), + ("annotate", "CENTERMARK", "CM"), + ("annotate", "CENTERLINE", "CL"), + ("annotate", "ANNOTATE_LEADER", "ML"), + ("annotate", "MLEADER_STYLE_COMBO", "MY"), + ("annotate", "MLEADERADD", "MAD"), + ("annotate", "MLEADERREMOVE", "MV"), + ("annotate", "MLEADERALIGN", "MAL"), + ("annotate", "MLEADERCOLLECT", "MC"), + ("annotate", "TABLE", "TB"), + ("annotate", "TABLE_STYLE_COMBO", "TL"), + ("annotate", "DATALINK", "DL"), + ("annotate", "WIPEOUT", "WI"), + ("annotate", "REVCLOUD", "AC"), + ("annotate", "SCALELISTEDIT", "SL"), + ("annotate", "OBJECTSCALE", "AS"), + ("annotate", "ANNORESET", "SS"), + // View. + ("view", "Viewport Tools", "VT"), + ("view", "UCSICON", "UI"), + ("view", "NAVVCUBE", "VC"), + ("view", "PAN", "PA"), + ("view", "3DORBIT", "OR"), + ("view", "Model Viewports", "VP"), + ("view", "VPORTS_NAMED", "NP"), + ("view", "VPJOIN", "JV"), + ("view", "VPORTS_RESTORE", "RV"), + ("view", "Visual Style", "VY"), + ("view", "VISUAL_STYLE", "VS"), + ("view", "TOOLPALETTES", "TP"), + ("view", "PROPERTIES", "PR"), + ("view", "COUNTLIST", "CT"), + ("view", "SHEETSET", "SS"), + ("view", "FILETAB", "FT"), + ("view", "LAYOUTTAB", "LT"), + ("view", "HORIZONTAL", "TH"), + ("view", "VERTICAL", "TV"), + ("view", "CASCADE", "CA"), + // Manage. + ("manage", "CUI", "CU"), + ("manage", "TOOLPALETTES", "TP"), + ("manage", "CUIIMPORT", "IM"), + ("manage", "CUIEXPORT", "EX"), + ("manage", "ALIASEDIT_DROPDOWN", "EA"), + ("ALIASEDIT_DROPDOWN", "ALIASEDIT", "EA"), + ("manage", "FINDNONPURGEABLE", "FN"), + ("manage", "PURGE", "PU"), + ("manage", "OVERKILL", "OK"), + ("manage", "AUDIT", "AU"), + // Block editor. + ("block_editor", "Open and Save", "BO"), + ("block_editor", "BEDIT", "BE"), + ("block_editor", "BSAVE", "BS"), + ("block_editor", "BTESTBLOCK", "TB"), + ("draw", "Close", "TC"), + ("draw", "BCLOSETESTBLOCK", "CT"), + ("block_editor", "ATTDEF", "AD"), + ("block_editor", "AUTOCONSTRAIN", "AC"), + ("block_editor", "CCONSTRAINT", "CI"), + ("block_editor", "PCONSTRAINT", "PA"), + ("block_editor", "TCONSTRAINT", "TA"), + // The reference gives Concentric the same code; Collinear comes first. + ("block_editor", "LCONSTRAINT", "CO"), + ("block_editor", "QCONSTRAINT", "PE"), + ("block_editor", "GCSMOOTH", "SM"), + ("block_editor", "GCHORIZONTAL", "HO"), + ("block_editor", "SYCONSTRAINT", "SY"), + ("block_editor", "FXCONSTRAINT", "FI"), + ("block_editor", "VCONSTRAINT", "VE"), + ("block_editor", "ECONSTRAINT", "EQ"), + ("block_editor", "GCVISIBILITY", "SH"), + ("block_editor", "GCSHOWALL", "SA"), + ("block_editor", "GCHIDEALL", "HA"), + ("block_editor", "DC_LINEAR_MENU", "DC"), + ("block_editor", "DCALIGNED", "AL"), + ("block_editor", "DCRADIUS", "RA"), + ("block_editor", "DCDIAMETER", "DI"), + ("block_editor", "DCANGULAR", "AN"), + ("block_editor", "DCCONVERT", "BV"), + ("block_editor", "DELCONSTRAINT", "DE"), + ("block_editor", "PARAMETERS", "PM"), +]; + +fn code(scope: &str, target: &str) -> Option<&'static str> { + CODES + .iter() + .find(|(s, t, _)| *s == scope && *t == target) + .map(|(_, _, code)| *code) +} + +fn tool_tip(scope: &str, tool: &ToolDef) -> Tip { + Tip { + anchor: tool.id.to_string(), + target: tool.id, + code: code(scope, tool.id).map(str::to_string), + action: Action::Run(Message::RibbonToolClick { + tool_id: tool.id.to_string(), + event: tool.event.clone(), + }), + } +} + +fn dropdown_tip(scope: &str, id: &'static str) -> Tip { + Tip { + anchor: id.to_string(), + target: id, + code: code(scope, id).map(str::to_string), + action: Action::Open(Message::ToggleRibbonDropdown(id.to_string())), + } +} + +impl Ribbon { + pub fn keytips_active(&self) -> bool { + self.keytips.is_some() + } + + pub fn show_keytips(&mut self, refocus: bool) { + self.close_dropdown(); + self.collapsed_open = None; + self.keytips = Some(State { + top: true, + path: Vec::new(), + typed: String::new(), + refocus, + }); + ACTIVE.store(true, Ordering::Relaxed); + // Bounds reported before now belong to an earlier frame. + crate::ui::wrap_bar::end_frame(); + } + + /// Close the tips; true when the command line should get the focus back. + pub fn hide_keytips(&mut self) -> bool { + ACTIVE.store(false, Ordering::Relaxed); + self.keytips.take().is_some_and(|state| state.refocus) + } + + /// Esc: drop a partial code, close the open dropdown or flyout, or go + /// back to the tab codes. False when the tips should close. + pub fn keytip_escape(&mut self) -> bool { + let dropdown_open = self.open_dropdown.is_some(); + let flyout_open = self.collapsed_open.is_some(); + let Some(state) = self.keytips.as_mut() else { + return false; + }; + if !state.typed.is_empty() { + state.typed.clear(); + return true; + } + if state.top { + return false; + } + state.path.pop(); + if dropdown_open { + // A submenu goes back to its slide-out. + if !self.escape_extension() { + self.close_dropdown(); + } + } else if flyout_open { + self.collapsed_open = None; + } else { + state.top = true; + state.path.clear(); + } + true + } + + /// One typed character; `None` confirms a code that is also the start of + /// a longer one (Enter). + pub fn keytip_type(&mut self, ch: Option<&str>) -> KeyTipStep { + let tips = self.keytip_list(); + let Some(state) = self.keytips.as_mut() else { + return KeyTipStep::Ignore; + }; + let codes = || tips.iter().filter_map(|tip| tip.code.as_deref()); + let exact = |typed: &str| tips.iter().find(|tip| tip.code.as_deref() == Some(typed)); + let longer = |typed: &str| codes().any(|code| code.len() > typed.len() && code.starts_with(typed)); + let typed = ch.map(|ch| format!("{}{ch}", state.typed)); + let chosen = match typed { + Some(typed) if codes().any(|code| code.starts_with(&typed)) => { + if longer(&typed) || exact(&typed).is_none() { + state.typed = typed; + return KeyTipStep::Stay; + } + typed + } + // A key that extends no code (or Enter) settles a code that is + // also the start of a longer one ("3" beside "3D"). + _ if exact(&state.typed).is_some() => std::mem::take(&mut state.typed), + _ => return KeyTipStep::Ignore, + }; + let Some(tip) = exact(&chosen) else { + return KeyTipStep::Ignore; + }; + state.typed.clear(); + state.path.push(chosen); + match tip.action.clone() { + Action::Tab(index) => { + state.top = false; + self.select(index); + KeyTipStep::Stay + } + Action::Open(message) => KeyTipStep::Open(message), + Action::Run(message) => KeyTipStep::Run(message), + } + } + + /// The codes of the current level, with the widgets they sit on. + pub fn keytip_list(&self) -> Vec { + let Some(state) = &self.keytips else { + return Vec::new(); + }; + if state.top { + return self.tab_tips(); + } + if let Some(open) = self.open_dropdown.as_deref() { + return self.item_tips(self.active, open); + } + self.module_tips(self.active) + } + + fn tab_tips(&self) -> Vec { + let tabs = self.modules.iter().enumerate().filter(|(_, module)| { + module.id() != "layout" + && (module.id() != "block_editor" || self.block_editing.is_some()) + }); + let mut tips: Vec = tabs + .map(|(index, module)| Tip { + anchor: format!("tab:{}", module.id()), + target: module.id(), + code: tab_code(module.id()).map(str::to_string), + action: Action::Tab(index), + }) + .collect(); + tips.extend(QUICK_ACCESS.iter().enumerate().map(|(index, &id)| Tip { + anchor: quick_access_anchor(id), + target: id, + code: Some((index + 1).to_string()), + action: Action::Run(match id { + super::widgets::UNDO_HISTORY_ID => Message::Undo, + super::widgets::REDO_HISTORY_ID => Message::Redo, + command => Message::Command(command.to_string()), + }), + })); + tips + } + + /// Every button, dropdown, slide-out and collapsed panel of tab `index`. + fn module_tips(&self, index: usize) -> Vec { + let Some(module) = self.modules.get(index) else { + return Vec::new(); + }; + let scope = module.id(); + let mut tips = Vec::new(); + for group in self.groups_for(index) { + for item in &group.tools { + match item { + RibbonItem::Tool(tool) + | RibbonItem::LabeledTool(tool) + | RibbonItem::LargeTool(tool) => tips.push(tool_tip(scope, tool)), + RibbonItem::Dropdown { id, .. } + | RibbonItem::LabeledDropdown { id, .. } + | RibbonItem::LargeDropdown { id, .. } => tips.push(dropdown_tip(scope, id)), + RibbonItem::ToolGrid { columns } => { + tips.extend(columns.iter().flatten().map(|tool| tool_tip(scope, tool))) + } + RibbonItem::LayerComboGroup { row2, row3 } => { + tips.push(dropdown_tip(scope, super::widgets::LAYER_COMBO_ID)); + tips.extend(row2.iter().chain(row3).map(|tool| tool_tip(scope, tool))); + } + RibbonItem::PropertiesGroup { match_prop } => { + tips.push(tool_tip(scope, match_prop)); + for id in [ + super::widgets::PROP_COLOR_ID, + super::widgets::PROP_LINETYPE_ID, + super::widgets::PROP_LW_ID, + ] { + tips.push(dropdown_tip(scope, id)); + } + } + RibbonItem::StyleComboGroup { combo_id, rows, .. } => { + tips.push(dropdown_tip(scope, combo_id)); + tips.extend(rows.iter().flatten().map(|tool| tool_tip(scope, tool))); + } + } + } + // The panel code opens the collapsed panel's flyout, or else the + // panel's slide-out. Both carry it; only the drawn one shows, and + // the form the panel had in the last frame comes first, so the + // code runs that one. + let ids: Vec<&'static str> = group.tools.iter().filter_map(item_id).collect(); + let collapsed = collapsed_anchor(group.title); + let was_collapsed = crate::ui::wrap_bar::recent_bounds(&collapsed).is_some(); + let flyout = Tip { + anchor: collapsed, + target: group.title, + code: code(scope, group.title).map(str::to_string), + action: Action::Open(Message::ToggleRibbonPanel(group.title.to_string())), + }; + let Some(id) = draw_panel::slide_out_id(group.title, &ids) else { + tips.push(flyout); + continue; + }; + let slide_out = Tip { + anchor: id.to_string(), + target: group.title, + code: code(scope, group.title).map(str::to_string), + action: Action::Open(Message::ToggleRibbonDropdown(id.to_string())), + }; + // In the open flyout the code reaches the slide-out. + if was_collapsed && self.collapsed_open.as_deref() != Some(group.title) { + tips.extend([flyout, slide_out]); + } else { + tips.extend([slide_out, flyout]); + } + if id == draw_panel::REFERENCE_PANEL_ID { + tips.push(Tip { + anchor: XREF_LAUNCHER.to_string(), + target: XREF_LAUNCHER, + code: code(scope, XREF_LAUNCHER).map(str::to_string), + action: Action::Run(Message::ToggleXrefManager), + }); + } + } + tips + } + + /// The items of the open dropdown, slide-out or submenu `open` of tab + /// `index`. Items the reference gives no code are numbered. + fn item_tips(&self, index: usize, open: &str) -> Vec { + let mut items = draw_panel::keytip_items(open); + if items.is_empty() { + for item in self.groups_for(index).flat_map(|group| &group.tools) { + if let RibbonItem::Dropdown { + id, items: list, .. + } + | RibbonItem::LabeledDropdown { + id, items: list, .. + } + | RibbonItem::LargeDropdown { + id, items: list, .. + } = item + { + if *id == open { + items = list + .iter() + .filter(|(cmd, _, _)| !super::widgets::is_disabled_item(cmd)) + .map(|(cmd, _, _)| (*id, *cmd, false)) + .collect(); + break; + } + } + } + } + let mut tips: Vec = items + .into_iter() + .enumerate() + .map(|(position, (dropdown_id, cmd, submenu))| Tip { + anchor: item_anchor(dropdown_id, cmd), + target: cmd, + code: Some(code(open, cmd).map_or_else(|| (position + 1).to_string(), str::to_string)), + action: if submenu { + Action::Open(Message::ToggleRibbonDropdown(cmd.to_string())) + } else { + Action::Run(Message::DropdownSelectItem { dropdown_id, cmd }) + }, + }) + .collect(); + if open == draw_panel::REFERENCE_PANEL_ID { + tips.push(Tip { + anchor: item_anchor(open, XREF_FADE), + target: XREF_FADE, + code: code(open, XREF_FADE).map(str::to_string), + action: Action::Run(Message::XrefFadeToggle), + }); + } + tips + } + + /// The state for automation: level, path, and the codes with whether + /// their widget showed in the last frame. `all` adds every tab's buttons + /// and their dropdown items, coded or not. + pub fn keytips_json(&self, all: bool) -> serde_json::Value { + use serde_json::json; + let shown = SHOWN.with(|shown| shown.borrow().clone()); + let entry = |tip: &Tip| { + json!({ + "code": tip.code, + "target": tip.target, + "anchor": tip.anchor, + "shown": shown.contains(&tip.anchor), + }) + }; + let mut out = match &self.keytips { + Some(state) => json!({ + "active": true, + "level": if state.top { "tabs" } else if self.open_dropdown.is_some() { "items" } else { "tab" }, + "tab": self.modules.get(self.active).map(|m| m.id()), + "open": self.open_dropdown, + "path": state.path, + "typed": state.typed, + "tips": self.keytip_list().iter().filter(|tip| tip.code.is_some()).map(entry).collect::>(), + }), + None => json!({"active": false}), + }; + if all { + let tabs: Vec<_> = (0..self.modules.len()) + .map(|index| { + let buttons: Vec<_> = self + .module_tips(index) + .iter() + .map(|tip| { + let mut value = entry(tip); + let items = self.item_tips(index, &tip.anchor); + if !items.is_empty() { + value["items"] = items.iter().map(entry).collect(); + } + value + }) + .collect(); + json!({"tab": self.modules[index].id(), "code": tab_code(self.modules[index].id()), "buttons": buttons}) + }) + .collect(); + out["all"] = json!(tabs); + } + out + } + + /// The hint bar layer and the codes layer. + pub fn keytip_layers(&self) -> Option<(Element<'_, Message>, Element<'_, Message>)> { + let state = self.keytips.as_ref()?; + let tips: Vec<(String, String)> = self + .keytip_list() + .into_iter() + .filter_map(|tip| { + let code = tip.code?; + code.starts_with(&state.typed).then_some((tip.anchor, code)) + }) + .collect(); + let mut path = state.path.join(" โ€บ "); + if !state.top { + path.push_str(" โ€บ "); + } + path.push_str(&state.typed); + if path.is_empty() { + path.push('โ€”'); + } + let hint = if state.top { + crate::tr!("keytips", "hint-tabs") + } else if self.open_dropdown.is_some() || self.collapsed_open.is_some() { + crate::tr!("keytips", "hint-items") + } else { + let module = &self.modules[self.active]; + crate::tr!( + "keytips", + "hint-tab", + tab = crate::i18n::ribbon_module_title(module.id(), module.title()) + ) + }; + let bar = KeyTipLayer { + tips: Vec::new(), + typed: 0, + bar: Some([ + crate::tr!("keytips", "title"), + crate::tr!("keytips", "typed", path = path), + hint, + ]), + }; + let codes = KeyTipLayer { + tips, + typed: state.typed.chars().count(), + bar: None, + }; + Some((Element::new(bar), Element::new(codes))) + } +} + +/// Fills the window without taking input. With `bar` it draws the hint bar +/// (beneath open dropdowns); otherwise it draws the codes in an overlay, so +/// they sit above flyouts and dropdowns, after their widgets reported where +/// they are. +struct KeyTipLayer { + tips: Vec<(String, String)>, + /// Characters of a partial code, shown dimmed on matching tips. + typed: usize, + bar: Option<[String; 3]>, +} + +impl Widget for KeyTipLayer { + fn size(&self) -> Size { + Size::new(Length::Fill, Length::Fill) + } + + fn layout( + &mut self, + _tree: &mut widget::Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::Node::new(limits.max()) + } + + fn draw( + &self, + _tree: &widget::Tree, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + if let Some(parts) = &self.bar { + draw_bar(renderer, theme, parts); + } + } + + fn overlay<'b>( + &'b mut self, + _tree: &'b mut widget::Tree, + _layout: Layout<'b>, + _renderer: &Renderer, + _viewport: &Rectangle, + _translation: Vector, + ) -> Option> { + self.bar + .is_none() + .then(|| overlay::Element::new(Box::new(KeyTipOverlay { layer: self }))) + } +} + +struct KeyTipOverlay<'b> { + layer: &'b KeyTipLayer, +} + +const PILL_H: f32 = 16.0; +const PILL_TEXT: f32 = 11.0; +const BAR_H: f32 = 24.0; +const BAR_TEXT: f32 = 12.0; + +fn bold(renderer: &Renderer) -> iced::Font { + iced::Font { + weight: iced::font::Weight::Semibold, + ..renderer.default_font() + } +} + +fn text_width(content: &str, size: f32, font: iced::Font) -> f32 { + ::Paragraph::with_text(advanced_text::Text { + content, + bounds: Size::INFINITE, + size: Pixels(size), + line_height: advanced_text::LineHeight::default(), + font, + align_x: advanced_text::Alignment::Left, + align_y: iced::alignment::Vertical::Top, + shaping: advanced_text::Shaping::Advanced, + wrapping: advanced_text::Wrapping::None, + ellipsis: advanced_text::Ellipsis::None, + hint_factor: None, + }) + .min_bounds() + .width +} + +fn draw_text( + renderer: &mut Renderer, + content: &str, + at: Point, + size: f32, + font: iced::Font, + color: Color, + centered: bool, + clip: Rectangle, +) { + renderer.fill_text( + advanced_text::Text { + content: content.to_string(), + bounds: Size::INFINITE, + size: Pixels(size), + line_height: advanced_text::LineHeight::default(), + font, + align_x: if centered { + advanced_text::Alignment::Center + } else { + advanced_text::Alignment::Left + }, + align_y: iced::alignment::Vertical::Center, + shaping: advanced_text::Shaping::Advanced, + wrapping: advanced_text::Wrapping::None, + ellipsis: advanced_text::Ellipsis::None, + hint_factor: None, + }, + at, + color, + clip, + ); +} + +/// Pill and text colours: a dark accent-edged pill on dark themes, a light +/// one on light themes. +fn colors(theme: &Theme) -> (Color, Color, Color) { + let palette = theme.palette(); + let accent = palette.primary.base.color; + if palette.is_dark { + ( + Color::from_rgb8(0x0f, 0x2a, 0x36), + accent, + Color::from_rgb8(0x7f, 0xe0, 0xff), + ) + } else { + ( + Color::from_rgb8(0xe3, 0xf5, 0xfd), + accent, + Color::from_rgb8(0x00, 0x4a, 0x6b), + ) + } +} + +impl overlay::Overlay for KeyTipOverlay<'_> { + fn layout(&mut self, _renderer: &Renderer, bounds: Size) -> layout::Node { + layout::Node::new(bounds) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + ) { + let window = layout.bounds(); + let (fill, edge, ink) = colors(theme); + let font = bold(renderer); + let mut shown = Vec::new(); + renderer.with_layer(window, |renderer| { + for (anchor, code) in &self.layer.tips { + let Some(b) = fresh_bounds(anchor) else { + continue; + }; + let width = text_width(code, PILL_TEXT, font) + 10.0; + let top_row = anchor.starts_with("tab:") + || anchor.starts_with("qa:") + || anchor == super::widgets::UNDO_HISTORY_ID + || anchor == super::widgets::REDO_HISTORY_ID; + let center = if top_row { + // Tabs and quick-access buttons: on the lower edge, so the + // label stays readable. + Point::new(b.center_x(), b.y + b.height - 2.0) + } else if anchor.contains('/') && b.width >= 60.0 { + // Menu rows: at the right end, clear of the label. + Point::new(b.x + b.width - width / 2.0 - 4.0, b.center_y()) + } else if anchor.contains('/') { + // Slide-out icons: on the lower edge. + Point::new(b.center_x(), b.y + b.height - 2.0) + } else if b.height >= 40.0 { + // Large buttons: over the label. + Point::new(b.center_x(), b.y + b.height - PILL_H / 2.0 - 2.0) + } else { + // Small buttons: at the lower left, over the icon. + Point::new(b.x + width / 2.0, b.y + b.height - PILL_H / 2.0 + 3.0) + }; + let x = (center.x - width / 2.0).clamp(window.x, (window.width - width).max(window.x)); + let y = (center.y - PILL_H / 2.0).clamp(window.y, (window.height - PILL_H).max(window.y)); + let pill = Rectangle::new(Point::new(x, y), Size::new(width, PILL_H)); + renderer.fill_quad( + renderer::Quad { + bounds: pill, + border: Border { + color: edge, + width: 1.0, + radius: (PILL_H / 2.0).into(), + }, + shadow: Shadow { + color: Color::BLACK.scale_alpha(0.6), + offset: Vector::new(0.0, 1.0), + blur_radius: 4.0, + }, + snap: true, + }, + Background::Color(fill), + ); + // The part already typed is dimmed. + let split = code + .char_indices() + .nth(self.layer.typed) + .map_or(code.len(), |(at, _)| at); + let (done, rest) = code.split_at(split); + let done_w = text_width(done, PILL_TEXT, font); + let rest_w = text_width(rest, PILL_TEXT, font); + let left = pill.center_x() - (done_w + rest_w) / 2.0; + let clip = Rectangle { + width: pill.width + 2.0, + ..pill + }; + if !done.is_empty() { + draw_text(renderer, done, Point::new(left, pill.center_y() - 0.5), PILL_TEXT, font, ink.scale_alpha(0.45), false, clip); + } + draw_text(renderer, rest, Point::new(left + done_w, pill.center_y() - 0.5), PILL_TEXT, font, ink, false, clip); + shown.push(anchor.clone()); + } + }); + SHOWN.with(|cell| *cell.borrow_mut() = shown); + crate::ui::wrap_bar::end_frame(); + } +} + +/// The hint bar under the ribbon: title, the typed path and what to do next. +fn draw_bar(renderer: &mut Renderer, theme: &Theme, parts: &[String; 3]) { + let Some(ribbon) = fresh_bounds(RIBBON_AREA) else { + return; + }; + let (fill, edge, ink) = colors(theme); + let font = bold(renderer); + let bar = Rectangle::new( + Point::new(ribbon.x, ribbon.y + ribbon.height), + Size::new(ribbon.width, BAR_H), + ); + renderer.fill_quad( + renderer::Quad { + bounds: bar, + border: Border::default(), + shadow: Shadow::default(), + snap: true, + }, + Background::Color(fill), + ); + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle::new(bar.position(), Size::new(bar.width, 1.0)), + border: Border::default(), + shadow: Shadow::default(), + snap: true, + }, + Background::Color(edge), + ); + let mut x = bar.x + 12.0; + let y = bar.center_y(); + let regular = renderer.default_font(); + for (index, part) in parts.iter().enumerate() { + let (part_font, color) = match index { + 0 => (font, ink), + 1 => (regular, ink), + _ => (regular, ink.scale_alpha(0.6)), + }; + draw_text(renderer, part, Point::new(x, y), BAR_TEXT, part_font, color, false, bar); + x += text_width(part, BAR_TEXT, part_font) + 18.0; + } +} + +/// Anchor for the whole ribbon (the hint bar sits under it). +pub(super) fn ribbon_area<'a>(el: impl Into>) -> Element<'a, Message> { + anchor(RIBBON_AREA, el) +} diff --git a/src/ui/ribbon/locate.rs b/src/ui/ribbon/locate.rs index e4dd32c35..18c3c3d9a 100644 --- a/src/ui/ribbon/locate.rs +++ b/src/ui/ribbon/locate.rs @@ -68,15 +68,11 @@ impl Ribbon { fn entries(&self) -> impl Iterator)> { let order = std::iter::once(self.active) .chain((0..self.modules.len()).filter(move |i| *i != self.active)); - order - .filter_map(|i| self.modules.get(i).map(|m| (i, m))) - .flat_map(|(i, module)| { - module - .ribbon_groups() - .iter() - .flat_map(group_entries) - .map(move |entry| (i, entry)) - }) + order.flat_map(|i| { + self.groups_for(i) + .flat_map(group_entries) + .map(move |entry| (i, entry)) + }) } /// The button id that runs the command line `cmd`: an exact match first, @@ -111,12 +107,7 @@ impl Ribbon { /// module has no tab (its tools are on the side toolbar). pub(super) fn tab_holding_active_tool(&self) -> Option { let id = self.active_tool.as_deref()?; - let holds = |i: usize| { - self.modules[i] - .ribbon_groups() - .iter() - .any(|group| group_holds(group, id)) - }; + let holds = |i: usize| self.groups_for(i).any(|group| group_holds(group, id)); if self .modules .get(self.active) diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index c67e39bdd..2172218cb 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -31,6 +31,12 @@ use widgets::{StyleContext, *}; pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID}; mod collapse; mod locate; +mod keytips; +pub use keytips::{ + active as keytips_active, key_pressed as keytip_key_pressed, + key_released as keytip_key_released, pointer_pressed as keytip_pointer_pressed, KeyTipInput, + KeyTipStep, +}; use crate::t; use crate::ui::wrap_bar::{PosReport, WrapBar, WrapFlow}; pub use collapse::CollapseMode; @@ -120,8 +126,14 @@ pub struct Ribbon { xref_ctx: bool, /// Only point clouds are selected: whether the first shows its crops. point_cloud_ctx: Option, + /// A block editor session is open: the tab that was active before the + /// Block Editor tab took over. + block_editing: Option, + block_testing: Option, /// XDWGFADECTL as the Reference slide-out shows it (negative = off). pub xref_fade: i32, + /// Open key tips (Alt). + keytips: Option, } /// Per-layer display data shown in the ribbon layer dropdown. @@ -240,7 +252,10 @@ impl Ribbon { underlay_ctx: None, xref_ctx: false, point_cloud_ctx: None, + block_editing: None, + block_testing: None, xref_fade: 50, + keytips: None, } } @@ -314,6 +329,62 @@ impl Ribbon { self.available_linetypes = items; } + /// Show the Block Editor tab while a block is being edited, and return to + /// the previous tab when the editing ends. + pub fn set_block_editing(&mut self, editing: bool) { + let Some(index) = self.modules.iter().position(|module| module.id() == "block_editor") + else { + return; + }; + match (editing, self.block_editing) { + (true, None) => { + self.block_editing = Some(self.active); + self.active = index; + } + (false, Some(previous)) => { + self.block_editing = None; + if self.active == index { + self.active = previous.min(self.modules.len().saturating_sub(1)); + } + } + _ => {} + } + } + + pub fn set_block_testing(&mut self, testing: bool) { + if testing != self.block_testing.is_some() { + self.close_dropdown(); + let _ = self.hide_keytips(); + } + let Some(index) = self.modules.iter().position(|module| module.id() == "draw") else { + return; + }; + match (testing, self.block_testing) { + (true, None) => { + self.block_testing = Some(self.active); + self.active = index; + } + (false, Some(previous)) => { + self.block_testing = None; + if self.active == index { + self.active = previous.min(self.modules.len().saturating_sub(1)); + } + } + _ => {} + } + } + + /// Contextual panels share their normal tab with the drawing tools. + fn groups_for(&self, index: usize) -> impl Iterator { + let testing = self.block_testing.is_some(); + self.modules.get(index).into_iter().flat_map(move |module| { + module + .ribbon_groups() + .iter() + .filter(move |group| module.id() != "draw" || group.title != "Close" || testing) + }) + } + pub fn select(&mut self, index: usize) { if index < self.modules.len() { self.active = index; @@ -571,12 +642,14 @@ impl Ribbon { |mut acc, (i, module)| { // The Layout module no longer has a ribbon tab โ€” its paper-space // tools live in the right-edge side toolbar (see ui::side_toolbar). - if module.id() == "layout" { + if module.id() == "layout" + || (module.id() == "block_editor" && self.block_editing.is_none()) + { return acc; } let is_active = i == self.active; - let is_contextual = module.id() == "layout"; + let is_contextual = matches!(module.id(), "layout" | "block_editor"); let holds_tool = holding_tab == Some(i); let btn = container( button( @@ -655,7 +728,7 @@ impl Ribbon { }, ..Default::default() }); - acc.push(btn.into()); + acc.push(keytips::anchor_with(|| format!("tab:{}", module.id()), btn)); acc }, ); @@ -717,38 +790,36 @@ impl Ribbon { } else { self.active }; - let tool_area: Element<'_, Message> = - if let Some(module) = self.modules.get(effective_active) { - let groups = module.ribbon_groups(); - let style_ctx = StyleContext { - text_style_names: &self.text_style_names, - active_text_style: &self.active_text_style, - dim_style_names: &self.dim_style_names, - active_dim_style: &self.active_dim_style, - mleader_style_names: &self.mleader_style_names, - active_mleader_style: &self.active_mleader_style, - table_style_names: &self.table_style_names, - active_table_style: &self.active_table_style, - }; - - // Adaptive tool area: panels sit on one row; when they don't all - // fit they degrade from the right โ€” a panel's large buttons first - // shrink to compact icon columns, then it collapses to a โ–พ flyout - // button. See `CollapsePanels`. - let ts = self.toggle_state(show_block_palette); - let panels: Vec> = groups - .iter() - .map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new())) - .collect(); - CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H) - .report_height(self.tool_bar_h.clone()) - .report_tight(self.collapse_tight.clone()) - .mode(self.collapse_mode) - .into() - } else { - text("").into() + let tool_area: Element<'_, Message> = if self.modules.get(effective_active).is_some() { + let groups = self.groups_for(effective_active); + let style_ctx = StyleContext { + text_style_names: &self.text_style_names, + active_text_style: &self.active_text_style, + dim_style_names: &self.dim_style_names, + active_dim_style: &self.active_dim_style, + mleader_style_names: &self.mleader_style_names, + active_mleader_style: &self.active_mleader_style, + table_style_names: &self.table_style_names, + active_table_style: &self.active_table_style, }; + // Adaptive tool area: panels sit on one row; when they don't all + // fit they degrade from the right โ€” a panel's large buttons first + // shrink to compact icon columns, then it collapses to a โ–พ flyout + // button. See `CollapsePanels`. + let ts = self.toggle_state(show_block_palette); + let panels: Vec> = groups + .map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new())) + .collect(); + CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H) + .report_height(self.tool_bar_h.clone()) + .report_tight(self.collapse_tight.clone()) + .mode(self.collapse_mode) + .into() + } else { + text("").into() + }; + let tool_bar: Element<'_, Message> = container(tool_area) .style(|theme: &Theme| container::Style { background: Some(Background::Color(theme.palette().background.weakest.color)), @@ -793,7 +864,7 @@ impl Ribbon { tool_bar }; - column![tab_bar, tool_bar].into() + keytips::ribbon_area(column![tab_bar, tool_bar]) } // โ”€โ”€ Dropdown overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -915,8 +986,7 @@ impl Ribbon { return Some(ov); } - let module = self.modules.get(self.active)?; - let groups = module.ribbon_groups(); + let groups = self.groups_for(self.active); let mut items_list: Option> = None; let mut dd_default = ""; let mut dd_id: &'static str = ""; @@ -964,7 +1034,7 @@ impl Ribbon { .then_some(theme.palette().background.base.text.scale_alpha(0.72)), }); - button( + let item = button( row![checkmark, icon_el, label_el] .spacing(4) .align_y(iced::Center), @@ -975,8 +1045,8 @@ impl Ribbon { })) .style(popup_row_style) .width(Fill) - .padding([4, 10]) - .into() + .padding([4, 10]); + keytips::anchor_with(|| keytips::item_anchor(dd_id, cmd), item) }) .collect(); @@ -1145,7 +1215,7 @@ impl Ribbon { /// Draw-tab dropdowns โ€” so the list grows to fit its entries instead of /// being clipped to the ribbon-row height. (#153) fn style_combo_overlay(&self, open_id: &str, win: (f32, f32)) -> Option> { - let groups = self.modules.get(self.active)?.ribbon_groups(); + let groups = self.groups_for(self.active); // Locate the open style combo; capture its style key + manager command. let mut found: Option<(crate::modules::StyleKey, Option<&'static str>)> = None; @@ -1568,7 +1638,10 @@ fn collapse_button<'a>( .style(opener_style(holds)) .width(Fill) .padding([3, 5]); - return automatic_large_button(localized_title, content.into()); + return keytips::anchor_with( + || keytips::collapsed_anchor(title), + automatic_large_button(localized_title, content.into()), + ); } // Collapsed (not yet tight): a large representative-tool face โ€” a live button @@ -1633,7 +1706,10 @@ fn collapse_button<'a>( .style(opener_style(holds && !face_lit)) .width(Fill) .padding([1, 4]); - let opener = automatic_large_button(localized_title, opener.into()); + let opener = keytips::anchor_with( + || keytips::collapsed_anchor(title), + automatic_large_button(localized_title, opener.into()), + ); // The large face fills a fixed slot so a collapsed panel is shorter than a full // 3-row panel, letting `CollapsePanels` shrink the ribbon row. @@ -1659,6 +1735,41 @@ mod tests { use super::widgets::is_active_tool; use super::Ribbon; + #[test] + fn block_test_close_panel_extends_draw_only_while_testing() { + let mut ribbon = Ribbon::default(); + let draw = ribbon + .modules + .iter() + .position(|module| module.id() == "draw") + .unwrap(); + assert!(!ribbon + .modules + .iter() + .any(|module| module.id() == "block_test")); + let ordinary: Vec<_> = ribbon.groups_for(draw).map(|group| group.title).collect(); + assert!(!ordinary.contains(&"Close")); + ribbon.set_block_editing(true); + ribbon.set_block_editing(false); + ribbon.set_block_testing(true); + assert_eq!(ribbon.active, draw); + let testing: Vec<_> = ribbon.groups_for(draw).map(|group| group.title).collect(); + assert_eq!(&testing[..ordinary.len()], ordinary.as_slice()); + assert_eq!(testing.last(), Some(&"Close")); + ribbon.toggle_collapsed_panel("Close"); + ribbon.set_block_testing(false); + assert!(ribbon.collapsed_open.is_none()); + assert_eq!( + ribbon + .groups_for(draw) + .map(|group| group.title) + .collect::>(), + ordinary + ); + ribbon.set_block_editing(true); + assert_eq!(ribbon.modules[ribbon.active].id(), "block_editor"); + } + #[test] fn block_palette_highlight_follows_the_app_state_passed_to_the_view() { // The Block Palette button highlight is derived from the app's diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index fdd53ee38..41b81ac71 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -99,7 +99,7 @@ thread_local! { /// Dropdown items that report a state and cannot be picked. pub(super) fn is_disabled_item(cmd: &str) -> bool { - cmd == "FRAMES3" + matches!(cmd, "FRAMES3" | "UOSNAP2") } fn ribbon_label_bounds( @@ -543,11 +543,13 @@ pub(super) fn render_small<'a>( .width(Length::Fixed(SMALL_W)) .height(ROW_H) .padding([4, 4]); - tooltip(btn, make_tip(tip_text), TipPos::Right) - .gap(6.0) - .delay(Duration::from_millis(400)) - .style(tip_style) - .into() + super::keytips::anchor( + t.id, + tooltip(btn, make_tip(tip_text), TipPos::Right) + .gap(6.0) + .delay(Duration::from_millis(400)) + .style(tip_style), + ) } RibbonItem::LabeledTool(t) => { @@ -568,11 +570,13 @@ pub(super) fn render_small<'a>( .width(Length::Fixed(LABELED_SMALL_W + ARROW_W)) .height(ROW_H) .padding([3, 4]); - tooltip(btn, make_tip(tip_text), TipPos::Right) - .gap(6.0) - .delay(Duration::from_millis(400)) - .style(tip_style) - .into() + super::keytips::anchor( + t.id, + tooltip(btn, make_tip(tip_text), TipPos::Right) + .gap(6.0) + .delay(Duration::from_millis(400)) + .style(tip_style), + ) } RibbonItem::Dropdown { @@ -895,7 +899,7 @@ fn tool_row<'a>(tools: &[ToolDef], active_tool: &Option) -> Element<'a, let event = t.event.clone(); let icon_el: Element = make_icon(t.icon, 16.0); let msg = module_event_to_message(event); - tooltip( + let btn = tooltip( button(icon_el) .on_press(msg) .style(move |theme: &Theme, status| tool_btn_style(theme, is_active, status)) @@ -905,8 +909,8 @@ fn tool_row<'a>(tools: &[ToolDef], active_tool: &Option) -> Element<'a, ) .gap(4.0) .delay(Duration::from_millis(400)) - .style(tip_style) - .into() + .style(tip_style); + super::keytips::anchor(t.id, btn) }) .collect(); row(btns).spacing(2).align_y(iced::Center).into() @@ -974,15 +978,17 @@ pub(super) fn render_large<'a>(item: &RibbonItem, ctx: &RenderCtx<'_>) -> Elemen bottom: 4.0, left: 4.0, }); - tooltip( - automatic_large_button(label, btn.into()), - make_tip(tip_text), - TipPos::Right, + super::keytips::anchor( + t.id, + tooltip( + automatic_large_button(label, btn.into()), + make_tip(tip_text), + TipPos::Right, + ) + .gap(6.0) + .delay(Duration::from_millis(400)) + .style(tip_style), ) - .gap(6.0) - .delay(Duration::from_millis(400)) - .style(tip_style) - .into() } RibbonItem::LargeDropdown { @@ -1313,11 +1319,13 @@ pub(super) fn quick_access_btn<'a>( .width(Length::Fixed(TOP_HIST_W)) .height(24) .padding([2, 0]); - tooltip(btn, make_tip(t!(label).into_owned()), TipPos::Bottom) - .gap(6.0) - .delay(Duration::from_millis(400)) - .style(tip_style) - .into() + super::keytips::anchor_with( + || super::keytips::quick_access_anchor(cmd), + tooltip(btn, make_tip(t!(label).into_owned()), TipPos::Bottom) + .gap(6.0) + .delay(Duration::from_millis(400)) + .style(tip_style), + ) } pub(super) fn render_history_control<'a>( diff --git a/src/ui/side_toolbar.rs b/src/ui/side_toolbar.rs index 5c415882c..941745f90 100644 --- a/src/ui/side_toolbar.rs +++ b/src/ui/side_toolbar.rs @@ -49,6 +49,53 @@ fn tip_panel(label: &'static str) -> Element<'static, Message> { .into() } +/// The REFEDIT session toolbar's head: the definition being edited as an +/// accent pill (REFEDITNAME) and how many objects the working set holds. +pub fn refedit_header(name: &str, count: usize) -> Element<'static, Message> { + // Long names are cut so the bar stays narrow; the full name is in the tip. + let short: String = if name.chars().count() > 7 { + format!("{}โ€ฆ", name.chars().take(6).collect::()) + } else { + name.to_string() + }; + let pill = container(text(short).size(10)) + .padding([1, 6]) + .style(|theme: &Theme| { + let palette = theme.palette(); + container::Style { + background: Some(Background::Color(palette.primary.base.color)), + border: Border { + radius: 8.0.into(), + ..Default::default() + }, + text_color: Some(palette.primary.base.text), + ..Default::default() + } + }); + let tip = container(text(name.to_string()).size(11)).padding([2, 6]).style(|theme: &Theme| { + let palette = theme.palette(); + container::Style { + background: Some(Background::Color(palette.background.strong.color)), + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 3.0.into(), + }, + text_color: Some(palette.background.strong.text), + ..Default::default() + } + }); + column![ + tooltip(pill, tip, tooltip::Position::Left).gap(6), + text(format!("{} {}", count, crate::tr!("ref-edit", "toolbar-objects"))) + .size(9) + .style(crate::ui::style::common::muted_style), + ] + .spacing(2) + .align_x(iced::Center) + .into() +} + /// Build the floating right-edge vertical toolbar from `tools`, vertically /// centred over the canvas. Returns `None` when `tools` is empty so the caller /// can skip pushing an overlay. @@ -61,12 +108,39 @@ pub fn view_with_active( tools: &[ToolDef], active: &dyn Fn(&str) -> bool, ) -> Option> { - if tools.is_empty() { + view_sections(None, &[tools], active) +} + +/// As [`view_with_active`], with `header` on top and a rule between the +/// groups of tools. +pub fn view_sections( + header: Option>, + groups: &[&[ToolDef]], + active: &dyn Fn(&str) -> bool, +) -> Option> { + if groups.iter().all(|g| g.is_empty()) { return None; } let mut col = column![].spacing(4).align_x(iced::Center); - for t in tools { + if let Some(header) = header { + col = col.push(header); + } + let tools = groups.iter().filter(|g| !g.is_empty()).enumerate().flat_map(|(group, tools)| { + tools.iter().enumerate().map(move |(k, t)| (group > 0 && k == 0, t)) + }); + for (starts_group, t) in tools { + if starts_group { + col = col.push( + container(iced::widget::Space::new()) + .width(Length::Fixed(BTN_SIZE - 4.0)) + .height(Length::Fixed(1.0)) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.neutral.color)), + ..Default::default() + }), + ); + } let on = active(t.id); let btn = button(icon_el(t.icon)) .on_press(Message::RibbonToolClick { diff --git a/src/ui/window/attribute_editor.rs b/src/ui/window/attribute_editor.rs index 75cf42dd7..35a9d34a6 100644 --- a/src/ui/window/attribute_editor.rs +++ b/src/ui/window/attribute_editor.rs @@ -59,6 +59,8 @@ pub struct AttrRow { pub color: AcadColor, pub linetype: String, pub line_weight: LineWeight, + /// Constant attributes cannot be edited. + pub is_constant: bool, } /// Combined justification options (horizontal ร— vertical), matching the @@ -215,7 +217,24 @@ fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { } } +fn disabled_field_style(theme: &Theme, _status: text_input::Status) -> text_input::Style { + let palette = theme.palette(); + text_input::Style { + background: Background::Color(palette.background.weak.color), + border: Border { + color: palette.background.neutral.color, + width: 1.0, + radius: 3.0.into(), + }, + icon: palette.background.base.text.scale_alpha(0.38), + placeholder: palette.background.base.text.scale_alpha(0.38), + value: palette.background.base.text.scale_alpha(0.48), + selection: palette.primary.base.color.scale_alpha(0.2), + } +} + /// One `label : widget` row with a fixed-width label column. + fn field_row<'a>( label: Cow<'static, str>, widget: Element<'a, Message>, @@ -469,14 +488,21 @@ fn attribute_tab<'a>( } let sel = selected.min(rows.len() - 1); - let value_box = text_input("", rows[sel].value.as_str()) - .on_input(move |v| Message::AttrEditorInput { idx: sel, value: v }) - .on_submit(Message::AttrEditorApply) - .style(field_style) + let is_constant = rows[sel].is_constant; + let mut value_box = text_input("", rows[sel].value.as_str()) .size(13) .padding([4, 6]) .width(width); + if is_constant { + value_box = value_box.style(disabled_field_style); + } else { + value_box = value_box + .on_input(move |v| Message::AttrEditorInput { idx: sel, value: v }) + .on_submit(Message::AttrEditorApply) + .style(field_style); + } + column![ head, scrollable(list).width(width).height(height), diff --git a/src/ui/window/block_edit_picker.rs b/src/ui/window/block_edit_picker.rs new file mode 100644 index 000000000..72b7a89a1 --- /dev/null +++ b/src/ui/window/block_edit_picker.rs @@ -0,0 +1,122 @@ +use crate::app::Message; +use crate::scene::WireModel; +use crate::ui::style::common::muted_style; +use crate::ui::style::form::{hdivider, vsep}; +use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; +use iced::{Element, Fill, Length, Theme}; + +pub(crate) struct State { + pub document_id: u64, + pub name: String, + pub existing_names: Vec, + pub preview: Vec, + pub description: String, + pub error: Option, +} + +fn list_item_style(selected: bool) -> impl Fn(&Theme, button::Status) -> button::Style { + move |theme: &Theme, status| { + if selected { + button::primary(theme, status) + } else { + button::subtle(theme, status) + } + } +} + +pub(crate) fn view(state: &State, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> { + let can_apply = state.error.is_none() && !state.name.trim().is_empty(); + + // โ”€โ”€ Left: Block name field and list of existing blocks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let name_input = text_input(crate::t!("Block name").as_ref(), &state.name) + .on_input(Message::BlockEditPickerName) + .on_submit_maybe(can_apply.then_some(Message::BlockEditPickerApply)) + .style(crate::ui::style::form::field_style); + + let list_items: Vec> = state + .existing_names + .iter() + .map(|name| { + let is_sel = name.eq_ignore_ascii_case(state.name.trim()); + button(text(name.clone()).size(12)) + .on_press(Message::BlockEditPickerSelect(name.clone())) + .style(list_item_style(is_sel)) + .padding([4, 8]) + .width(Fill) + .into() + }) + .collect(); + + let list_scrollable = container(scrollable(column(list_items).spacing(2)).height(Fill)) + .style(container::bordered_box) + .padding(2) + .height(Fill); + + let left_col = column![ + text(crate::t!("Block to create or edit").into_owned()).size(12), + name_input, + list_scrollable, + ] + .spacing(8) + .width(Length::Fixed(240.0)) + .height(Fill); + + // โ”€โ”€ Right: Preview and Description โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let preview_box = column![ + text(crate::t!("Preview").into_owned()).size(12), + container(super::block_palette::preview_canvas(&state.preview, 150.0)) + .style(container::bordered_box) + .width(Fill) + .height(Length::Fixed(150.0)), + ] + .spacing(4); + + let desc_text = if state.description.is_empty() { + crate::t!("(No description)").into_owned() + } else { + state.description.clone() + }; + + let desc_box = column![ + text(crate::t!("Description").into_owned()).size(12), + container(scrollable(text(desc_text).size(11).style(muted_style)).height(Fill)) + .style(container::bordered_box) + .padding(6) + .width(Fill) + .height(Fill), + ] + .spacing(4) + .height(Fill); + + let right_col = column![preview_box, desc_box] + .spacing(8) + .width(Fill) + .height(Fill); + + // โ”€โ”€ Body โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let body = row![left_col, vsep(sizing.height), right_col] + .spacing(12) + .height(Fill); + + // โ”€โ”€ Bottom: Error message and OK / Cancel buttons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + let error_text = text(state.error.clone().unwrap_or_default()) + .size(11) + .style(text::danger); + + let buttons = row![ + error_text, + Space::new().width(Fill), + button(text(crate::t!("OK").into_owned())) + .on_press_maybe(can_apply.then_some(Message::BlockEditPickerApply)) + .style(button::primary), + button(text(crate::t!("Cancel").into_owned())).on_press(Message::CloseModal), + ] + .spacing(8) + .align_y(iced::Center); + + container(column![body, hdivider(sizing.width), buttons].spacing(10)) + .padding(16) + .width(sizing.width) + .height(sizing.height) + .into() +} diff --git a/src/ui/window/block_save_as.rs b/src/ui/window/block_save_as.rs new file mode 100644 index 000000000..6aca890cb --- /dev/null +++ b/src/ui/window/block_save_as.rs @@ -0,0 +1,52 @@ +use crate::app::Message; +use crate::scene::WireModel; +use iced::widget::{button, column, container, pick_list, row, text, text_input, Space}; +use iced::{Element, Fill}; + +pub(crate) struct State { + pub document_id: u64, + pub source: codec::Handle, + pub name: String, + pub existing_names: Vec, + pub preview: Vec, + pub error: Option, +} + +pub(crate) fn view(state: &State, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> { + let field = text_input(crate::t!("Block name").as_ref(), &state.name) + .on_input(Message::BlockSaveAsName) + .on_submit_maybe(state.error.is_none().then_some(Message::BlockSaveAsApply)) + .style(crate::ui::style::form::field_style); + let names = pick_list( + None::, + state.existing_names.as_slice(), + |name: &String| name.clone(), + ) + .on_select(Message::BlockSaveAsName) + .placeholder(crate::t!("Existing blocks").into_owned()) + .width(Fill); + container( + column![ + text(crate::t!("Block name").into_owned()).size(13), + field, + names, + super::block_palette::preview_canvas(&state.preview, 160.0), + text(state.error.clone().unwrap_or_default()) + .size(12) + .style(text::danger), + row![ + Space::new().width(Fill), + button(text(crate::t!("OK").into_owned())) + .on_press_maybe(state.error.is_none().then_some(Message::BlockSaveAsApply)) + .style(button::primary), + button(text(crate::t!("Cancel").into_owned())).on_press(Message::CloseModal), + ] + .spacing(8), + ] + .spacing(10), + ) + .padding(16) + .width(sizing.width) + .height(sizing.height) + .into() +} diff --git a/src/ui/window/drafting_settings.rs b/src/ui/window/drafting_settings.rs index 97bb36c0a..e1a768e1b 100644 --- a/src/ui/window/drafting_settings.rs +++ b/src/ui/window/drafting_settings.rs @@ -44,6 +44,7 @@ pub struct DraftingSettingsState { pub polar_increment_deg: f32, // Object Snap pub osnap_on: bool, + pub osnap_aperture_input: String, pub otrack_on: bool, pub snap_modes: rustc_hash::FxHashSet, // 3D Object Snap @@ -78,6 +79,7 @@ impl DraftingSettingsState { || self.ortho_on != saved.ortho_on || (self.polar_increment_deg - saved.polar_increment_deg).abs() > 0.001 || self.osnap_on != saved.osnap_on + || self.osnap_aperture_input != saved.osnap_aperture_input || self.otrack_on != saved.otrack_on || self.snap_modes != saved.snap_modes || self.osnap3d_on != saved.osnap3d_on @@ -508,6 +510,14 @@ pub fn view_window<'a>( column![ row![osnap_on_toggle, Space::new().width(24), otrack_on_toggle].align_y(iced::Center), + row![ + text("OSNAP capture radius (pixels)").size(11), + text_input("25", &state.osnap_aperture_input) + .on_input(Message::DraftingSettingsOsnapApertureChanged) + .width(70), + ] + .spacing(10) + .align_y(iced::Center), action_buttons, Space::new().height(4), snap_modes_group, diff --git a/src/ui/window/layers.rs b/src/ui/window/layers.rs index 5b993b7e6..747b2a9d9 100644 --- a/src/ui/window/layers.rs +++ b/src/ui/window/layers.rs @@ -737,6 +737,13 @@ fn name_tip<'a>(name: &'a str) -> Element<'a, Message> { .into() } + +/// The layer-name field while a rename is open; a click that moves focus +/// elsewhere commits the typed name. +pub fn rename_input_id() -> iced::widget::Id { + iced::widget::Id::new("layer-rename-input") +} + fn layer_row<'a>( index: usize, layer: &'a Layer, @@ -803,6 +810,7 @@ fn layer_row<'a>( // Name cell let name_cell: Element<'_, Message> = if is_editing { text_input("", edit_buf) + .id(rename_input_id()) .on_input(Message::LayerRenameEdit) .on_submit(Message::LayerRenameCommit) .size(FONT_SZ) diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index c6c870366..005c2c99f 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -5,6 +5,8 @@ pub mod attdef_dialog; pub mod attribute_editor; pub mod auto_constrain_settings; pub mod block_definition; +pub mod block_edit_picker; +pub mod block_save_as; pub mod block_palette; pub mod browser; pub mod count_palette; @@ -29,6 +31,7 @@ pub mod plot; pub mod plugin_manager; pub mod print_all; pub mod recovery; +pub mod refedit_dialog; pub mod sheet_set; pub mod shortcuts; pub mod update_notice; diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index e91aabe35..ab7fb7776 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -53,6 +53,14 @@ pub struct AppPrefs { pub cliprompt_lines: i32, /// COMMANDLINEFADETIME: how long overlay history lines stay visible, in ms. pub commandline_fade_ms: i32, + /// COMMANDLINEOPACITY: command-line panel opacity in percent. + pub command_line_opacity: u8, + /// MENUBAR: the pull-down menus show above the ribbon. + pub show_menu_bar: bool, + /// The ribbon shows (RIBBON / RIBBONCLOSE). + pub show_ribbon: bool, + /// HPASSOC: new hatches follow their boundary objects. + pub hatch_associative: bool, /// ZOOMWHEEL: reverse the mouse-wheel zoom direction. pub zoom_wheel_reversed: bool, /// Scroll pans and Ctrl+scroll zooms (touchpads). @@ -856,6 +864,30 @@ pub fn view_window<'a>( .align_y(iced::Center), ) .push(Space::new().height(24)) + .push(text(crate::t!("Window Elements")).size(15)) + .push(Space::new().height(10)) + .push( + row![ + iced::widget::checkbox(prefs.show_menu_bar) + .on_toggle(Message::MenuBarChanged) + .size(15), + text(crate::t!("Show the menu bar (MENUBAR)")).size(12), + ] + .spacing(8) + .align_y(iced::Center), + ) + .push(Space::new().height(8)) + .push( + row![ + iced::widget::checkbox(prefs.show_ribbon) + .on_toggle(Message::RibbonVisibleChanged) + .size(15), + text(crate::t!("Show the ribbon (RIBBON / RIBBONCLOSE)")).size(12), + ] + .spacing(8) + .align_y(iced::Center), + ) + .push(Space::new().height(24)) .push(text(crate::t!("Command Line")).size(15)) .push(Space::new().height(10)) .push( @@ -912,6 +944,22 @@ pub fn view_window<'a>( )) .size(11) .width(sizing.width), + ) + .push(Space::new().height(12)) + .push( + row![ + text(crate::t!("Command line opacity")).size(12).width(140), + slider( + 20..=100, + prefs.command_line_opacity.clamp(20, 100), + Message::CommandLineOpacityChanged, + ) + .step(5u8) + .width(Fill), + text(format!("{}%", prefs.command_line_opacity)).size(11).width(44), + ] + .spacing(10) + .align_y(iced::Center), ); let display_element = display.spacing(0).width(sizing.width); @@ -1399,6 +1447,17 @@ pub fn view_window<'a>( ] .spacing(10) .align_y(iced::Center), + Space::new().height(24), + text(crate::t!("Hatch")).size(15), + Space::new().height(10), + row![ + iced::widget::checkbox(prefs.hatch_associative) + .on_toggle(Message::HatchAssociativeChanged) + .size(15), + text(crate::t!("New hatches follow their boundaries (HPASSOC)")).size(12), + ] + .spacing(8) + .align_y(iced::Center), ] .spacing(0) .width(sizing.width); @@ -1457,6 +1516,22 @@ pub fn view_window<'a>( ] .spacing(10) .align_y(iced::Center), + Space::new().height(14), + row![ + text(crate::t!("Snap marker size")).size(12).width(150), + slider( + 1..=20, + model_space.snap_marker_size.clamp(1, 20), + Message::SnapMarkerSizeChanged, + ) + .step(1) + .width(Fill), + text(format!("{} px", model_space.snap_marker_size.clamp(1, 20))) + .size(11) + .width(44), + ] + .spacing(10) + .align_y(iced::Center), Space::new().height(24), text(crate::t!("Snap and Grid")).size(15), Space::new().height(10), diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index 3c1e49d26..8c4d8932e 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -111,6 +111,35 @@ pub enum PdfDialogMsg { ImportOk, Options, Help(&'static str), + // View background + BgKind(BgKind), + /// Open the colour window for a slot: 0 solid, 1 top, 2 middle, 3 bottom. + BgPickColor(u8), + BgColor(u8, [u8; 3]), + BgThree(bool), + BgRotation(String), + BgImage(String), + BgBrowse, + BgPosition(BgPosition), + BgAspect(bool), + /// An image background's offset (pixels, x and y) and scale, as typed. + BgOffset([String; 2]), + BgScale([String; 2]), + /// A spinner step on offset x / y (0, 1) or scale x / y (2, 3). + BgStep(u8, i8), + /// Offset 0, scale 1. + BgReset, + BgApply, + // Image Adjust: rows 0 brightness, 1 contrast, 2 fade. + /// A row's value from its bar. + IaValue(u8, u8), + /// A row's box, as typed. + IaText(u8, String), + /// One step down (-1) or up (+1). + IaStep(u8, i8), + /// Brightness 50, contrast 50, fade 0. + IaReset, + IaApply, } fn msg(m: PdfDialogMsg) -> Message { @@ -283,7 +312,7 @@ pub(crate) fn well_style(theme: &Theme) -> container::Style { } /// A titled card. -fn card<'a>(title: String, content: impl Into>) -> Element<'a, Message> { +pub(crate) fn card<'a>(title: String, content: impl Into>) -> Element<'a, Message> { container( column![ text(title.to_uppercase()).size(10).style(accent_text), @@ -1018,10 +1047,16 @@ pub fn view_layers<'a>( ) -> Element<'a, Message> { let names: Vec = state.targets.iter().map(|t| t.name.clone()).collect(); let target = state.targets.get(state.current); + let has_underlays = !names.is_empty(); let underlay = pick_list(target.map(|t| t.name.clone()), names, |n: &String| { n.clone() - }) - .on_select(|n| msg(PdfDialogMsg::LayersUnderlay(n))) + }); + // No underlays: the list stays empty and disabled. + let underlay = if has_underlays { + underlay.on_select(|n| msg(PdfDialogMsg::LayersUnderlay(n))) + } else { + underlay + } .text_size(12) .padding([5, 8]) .width(Fill); @@ -2131,3 +2166,698 @@ pub fn view_pc_section<'a>( .width(sizing.width) .into() } + +// โ”€โ”€ View background (-VIEW sEttings Background) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/// What a named view shows behind the drawing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BgKind { + None, + Solid, + Gradient, + Image, +} + +/// Where an image background sits: centred, stretched over the view, or +/// repeated. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BgPosition { + Center, + Stretch, + Tile, +} + +impl BgPosition { + pub const ALL: [BgPosition; 3] = [BgPosition::Center, BgPosition::Stretch, BgPosition::Tile]; +} + +impl fmt::Display for BgPosition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&match self { + BgPosition::Center => crate::tr!("view-background", "center"), + BgPosition::Stretch => crate::tr!("view-background", "stretch"), + BgPosition::Tile => crate::tr!("view-background", "tile"), + }) + } +} + +/// A background image as the preview draws it. +pub struct BgPreview { + pub size: [u32; 2], + /// Unmirrored, mirrored in x, in y, in both. + pub handles: [image::Handle; 4], +} + +/// Loads a background image for the preview, at most 512 pixels a side. +pub fn bg_preview(path: &str) -> Option { + let decoded = ::image::open(path.trim()).ok()?; + let size = [decoded.width(), decoded.height()]; + let small = if size[0].max(size[1]) > 512 { decoded.thumbnail(512, 512) } else { decoded }.to_rgba8(); + let handle = |pixels: ::image::RgbaImage| image::Handle::from_rgba(pixels.width(), pixels.height(), pixels.into_raw()); + let flip_x = ::image::imageops::flip_horizontal(&small); + let flip_y = ::image::imageops::flip_vertical(&small); + let flip_xy = ::image::imageops::flip_vertical(&flip_x); + Some(BgPreview { size, handles: [handle(small), handle(flip_x), handle(flip_y), handle(flip_xy)] }) +} + +/// Image offsets the reference's Adjust Image sliders reach, in pixels. +pub const BG_OFFSET_LIMIT: f64 = 2000.0; +/// Image scales the reference's sliders reach (slider value / 10). +pub const BG_SCALE_LIMIT: f64 = 10.0; + +/// One spinner step on an image background field: a pixel for an offset, a +/// tenth for a scale, which skips 0 (the reference ignores a zero scale). +pub fn bg_step(field: u8, value: f64, dir: i8) -> f64 { + let dir = f64::from(dir.signum()); + if field < 2 { + return (value.round() + dir).clamp(-BG_OFFSET_LIMIT, BG_OFFSET_LIMIT); + } + let mut next = ((value * 10.0).round() + dir) / 10.0; + if next.abs() < 0.05 { + next += dir / 10.0; + } + next.clamp(-BG_SCALE_LIMIT, BG_SCALE_LIMIT) +} + +/// The Background dialog's fields for one named view. +pub struct ViewBackgroundState { + pub view: String, + pub kind: BgKind, + pub solid: [u8; 3], + pub top: [u8; 3], + pub middle: [u8; 3], + pub bottom: [u8; 3], + pub three: bool, + pub rotation: String, + pub image: String, + pub position: BgPosition, + pub aspect: bool, + /// The image's offset (pixels) and scale as typed, and the scale the + /// view's image had (kept for a zero scale). + pub offset: [String; 2], + pub scale: [String; 2], + pub scale_base: [f64; 2], + /// The chosen image for the preview: its size in pixels and a small copy + /// as it is and mirrored (x, y, both), for negative scales. + pub preview: Option, + /// The drawing viewport's size, which the preview stands for. + pub viewport: [f32; 2], + /// The drawing area colour, shown for no background. + pub canvas: [u8; 3], +} + +pub fn view_background<'a>( + state: &'a ViewBackgroundState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let kind = segmented( + vec![ + (BgKind::None, crate::tr!("view-background", "none")), + (BgKind::Solid, crate::tr!("view-background", "solid")), + (BgKind::Gradient, crate::tr!("view-background", "gradient")), + (BgKind::Image, crate::tr!("view-background", "image")), + ], + state.kind, + PdfDialogMsg::BgKind, + ); + let rgb = |c: [u8; 3]| iced::Color::from_rgb8(c[0], c[1], c[2]); + let fill = |background: Background| { + move |_: &Theme| container::Style { + background: Some(background), + border: Border { + width: 1.0, + radius: 4.0.into(), + color: iced::Color::from_rgb8(0x4a, 0x4a, 0x4a), + }, + ..Default::default() + } + }; + let note = |label: String| text(label).size(11).style(muted_style); + let preview: Element<'a, Message> = match state.kind { + BgKind::None => container(note(crate::tr!("view-background", "canvas"))) + .center(Fill) + .style(fill(Background::Color(rgb(state.canvas)))) + .into(), + BgKind::Solid => container(Space::new()) + .width(Fill) + .height(Fill) + .style(fill(Background::Color(rgb(state.solid)))) + .into(), + BgKind::Gradient => { + // Bottom colour at the start, top at the end, turned + // counter-clockwise by the rotation. + let angle = state.rotation.trim().parse::().unwrap_or(0.0); + let mut linear = iced::gradient::Linear::new(iced::Radians(-angle.to_radians())) + .add_stop(0.0, rgb(state.bottom)); + if state.three { + linear = linear.add_stop(0.5, rgb(state.middle)); + } + linear = linear.add_stop(1.0, rgb(state.top)); + container(Space::new()) + .width(Fill) + .height(Fill) + .style(fill(Background::Gradient(iced::Gradient::Linear(linear)))) + .into() + } + BgKind::Image => { + if std::path::Path::new(state.image.trim()).is_file() { + let canvas = Background::Color(rgb(state.canvas)); + if state.position == BgPosition::Stretch { + // Filling the view; keeping the aspect ratio it covers + // the view and the overflow is cut off. + let fit = if state.aspect { iced::ContentFit::Cover } else { iced::ContentFit::Fill }; + let handle = state.preview.as_ref().map_or_else( + || image::Handle::from_path(state.image.trim()), + |preview| preview.handles[0].clone(), + ); + container(image(handle).content_fit(fit).width(Fill).height(Fill)) + .center(Fill) + .clip(true) + .style(fill(canvas)) + .into() + } else { + // The image at its size times its scale, moved by its + // offset, in a view of the drawing viewport's shape. + let parse = |v: &String, d: f64| v.trim().parse::().ok().filter(|v| v.is_finite()).unwrap_or(d); + let offset = [parse(&state.offset[0], 0.0), parse(&state.offset[1], 0.0)]; + let scale = [0, 1].map(|k| { + let v = parse(&state.scale[k], state.scale_base[k]); + if v.abs() < 1e-9 { state.scale_base[k] } else { v } + }); + let [iw, ih] = state.preview.as_ref().map_or([1, 1], |preview| preview.size); + // A negative scale mirrors the image. + let mirrored = usize::from(scale[0] < 0.0) | usize::from(scale[1] < 0.0) << 1; + let handle = state.preview.as_ref().map_or_else( + || image::Handle::from_path(state.image.trim()), + |preview| preview.handles[mirrored].clone(), + ); + let viewport = state.viewport; + // The image's pixels and offsets are device pixels; the + // viewport is measured in logical ones. + let device = crate::scene::view::device_scale().max(0.1); + let tile = state.position == BgPosition::Tile; + container(iced::widget::responsive(move |size| { + let factor = size.height / viewport[1].max(1.0) / device; + let w = (iw as f32 * scale[0].abs() as f32 * factor).max(1.0); + let h = (ih as f32 * scale[1].abs() as f32 * factor).max(1.0); + let x = size.width / 2.0 + offset[0] as f32 * factor - w / 2.0; + let y = size.height / 2.0 + offset[1] as f32 * factor - h / 2.0; + let (cols, rows) = if tile { + ((size.width / w).ceil() as i32 + 1, (size.height / h).ceil() as i32 + 1) + } else { + (0, 0) + }; + let mut layers = iced::widget::Stack::new() + .width(Fill) + .height(Fill) + .push(container(Space::new()).width(Fill).height(Fill)); + let x0 = if tile { x - (x / w).ceil() * w } else { x }; + let y0 = if tile { y - (y / h).ceil() * h } else { y }; + for row_index in 0..=rows.min(60) { + for col_index in 0..=cols.min(60) { + let at = iced::Point::new(x0 + col_index as f32 * w, y0 + row_index as f32 * h); + layers = layers.push( + iced::widget::pin( + image(handle.clone()) + .content_fit(iced::ContentFit::Fill) + .width(Length::Fixed(w)) + .height(Length::Fixed(h)), + ) + .position(at), + ); + } + } + Element::::from(layers) + })) + .width(Fill) + .height(Fill) + .clip(true) + .style(fill(canvas)) + .into() + } + } else { + container(note(crate::tr!("view-background", "no-image"))) + .center(Fill) + .style(fill(Background::Color(iced::Color::from_rgb8(0x1a, 0x1a, 0x1a)))) + .into() + } + } + }; + let preview = card( + crate::tr!("view-background", "preview"), + container(preview).width(Fill).height(Length::Fixed(150.0)), + ); + let label = |value: String, enabled: bool| { + text(value) + .size(11) + .style(move |theme: &Theme| { + if enabled { + muted_style(theme) + } else { + text::Style { + color: Some(theme.palette().background.strong.color), + } + } + }) + .width(Length::Fixed(92.0)) + }; + let color_row = |name: String, slot: u8, color: [u8; 3], enabled: bool| -> Element<'a, Message> { + let mut pick = button(text(crate::tr!("view-background", "select")).size(11)) + .style(button_style(false)) + .padding([4, 10]); + if enabled { + pick = pick.on_press(msg(PdfDialogMsg::BgPickColor(slot))); + } + row![ + label(name, enabled), + swatch(color, 64.0, 22.0), + text(format!("{},{},{}", color[0], color[1], color[2])) + .size(11) + .style(muted_style), + Space::new().width(Fill), + pick, + ] + .spacing(8) + .align_y(iced::Center) + .into() + }; + let options: Element<'a, Message> = match state.kind { + BgKind::None => card( + crate::tr!("view-background", "info"), + note(crate::tr!("view-background", "none-info")), + ), + BgKind::Solid => card( + crate::tr!("view-background", "solid-color"), + color_row(crate::tr!("view-background", "color"), 0, state.solid, true), + ), + BgKind::Gradient => card( + crate::tr!("view-background", "gradient"), + column![ + row![ + chip( + crate::tr!("view-background", "three-colors"), + state.three, + msg(PdfDialogMsg::BgThree(!state.three)) + ), + Space::new().width(Fill), + text(crate::tr!("view-background", "rotation")) + .size(11) + .style(muted_style), + text_input("", &state.rotation) + .on_input(|v| msg(PdfDialogMsg::BgRotation(v))) + .size(11) + .padding([4, 8]) + .width(Length::Fixed(64.0)) + .style(field_style), + ] + .spacing(8) + .align_y(iced::Center), + color_row(crate::tr!("view-background", "top"), 1, state.top, true), + color_row(crate::tr!("view-background", "middle"), 2, state.middle, state.three), + color_row(crate::tr!("view-background", "bottom"), 3, state.bottom, true), + ] + .spacing(8), + ), + BgKind::Image => card( + crate::tr!("view-background", "image"), + column![ + row![ + text_input("", &state.image) + .on_input(|v| msg(PdfDialogMsg::BgImage(v))) + .size(11) + .padding([4, 8]) + .width(Fill) + .style(field_style), + button(text(crate::tr!("view-background", "browse")).size(11)) + .on_press(msg(PdfDialogMsg::BgBrowse)) + .style(button_style(false)) + .padding([4, 10]), + ] + .spacing(8) + .align_y(iced::Center), + row![ + label(crate::tr!("view-background", "position"), true), + pick_list(Some(state.position), BgPosition::ALL.to_vec(), |p: &BgPosition| { + p.to_string() + }) + .on_select(|p| msg(PdfDialogMsg::BgPosition(p))) + .text_size(12) + .padding([4, 8]) + .width(Fill), + ] + .spacing(8) + .align_y(iced::Center), + chip( + crate::tr!("view-background", "aspect"), + state.aspect, + msg(PdfDialogMsg::BgAspect(!state.aspect)) + ), + ] + .spacing(8), + ), + }; + let adjust = (state.kind == BgKind::Image).then(|| adjust_card(state)); + let mut body = column![kind, preview, options].spacing(10); + if let Some(adjust) = adjust { + body = body.push(adjust); + } + column![ + body, + footer( + "background", + None, + crate::tr!("view-background", "apply").into(), + PdfDialogMsg::BgApply + ), + ] + .spacing(10) + .padding([10, 12]) + .width(sizing.width) + .into() +} + +/// The image's offset and scale (the reference's Adjust Image): pixel offsets +/// and scales with spinners and a reset; inactive while the image is +/// stretched over the view, where the reference ignores them. +fn adjust_card<'a>(state: &'a ViewBackgroundState) -> Element<'a, Message> { + let enabled = state.position != BgPosition::Stretch; + let dim = move |theme: &Theme| { + if enabled { + muted_style(theme) + } else { + text::Style { color: Some(theme.palette().background.strong.color) } + } + }; + let spin = |field: u8| -> Element<'a, Message> { + let value = if field < 2 { &state.offset[field as usize] } else { &state.scale[field as usize - 2] }; + let mut input = text_input("", value) + .size(11) + .padding([4, 8]) + .width(Length::Fixed(64.0)) + .style(move |theme: &Theme, status| { + let mut style = field_style(theme, status); + if !enabled { + style.value = theme.palette().background.strong.color; + } + style + }); + if enabled { + input = input.on_input(move |v| { + if field < 2 { + let mut pair = state.offset.clone(); + pair[field as usize] = v; + msg(PdfDialogMsg::BgOffset(pair)) + } else { + let mut pair = state.scale.clone(); + pair[field as usize - 2] = v; + msg(PdfDialogMsg::BgScale(pair)) + } + }); + } + let arrow = |up: bool| { + let mut b = button(crate::ui::icons::themed_arrow_toggle(up, 7.0)) + .style(button_style(false)) + .padding([1, 4]); + if enabled { + b = b.on_press(msg(PdfDialogMsg::BgStep(field, if up { 1 } else { -1 }))); + } + b + }; + row![input, column![arrow(true), arrow(false)].spacing(1)].spacing(2).align_y(iced::Center).into() + }; + let axis = |name: &'static str| text(name).size(11).style(dim); + let line = |label: String, a: u8, b: u8, unit: bool| -> Element<'a, Message> { + let mut r = row![ + text(label).size(11).style(dim).width(Length::Fixed(72.0)), + axis("X"), + spin(a), + axis("Y"), + spin(b), + ] + .spacing(6) + .align_y(iced::Center); + if unit { + r = r.push(text(crate::tr!("view-background", "pixels")).size(11).style(dim)); + } + r.into() + }; + let mut reset = button(text(crate::tr!("view-background", "reset")).size(11)) + .style(button_style(false)) + .padding([3, 10]); + if enabled { + reset = reset.on_press(msg(PdfDialogMsg::BgReset)); + } + container( + column![ + row![ + text(crate::tr!("view-background", "adjust").to_uppercase()).size(10).style(move |theme: &Theme| { + if enabled { accent_text(theme) } else { dim(theme) } + }), + Space::new().width(Fill), + reset, + ] + .align_y(iced::Center), + line(crate::tr!("view-background", "offset"), 0, 1, true), + line(crate::tr!("view-background", "scale"), 2, 3, false), + ] + .spacing(8), + ) + .padding([10, 12]) + .width(Fill) + .style(card_style) + .into() +} + +// โ”€โ”€ Image Adjust (IMAGEADJUST) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/// The Image Adjust dialog: brightness, contrast and fade (0โ€“100) of the +/// chosen raster images, with a before / after preview of the first. +pub struct ImageAdjustState { + pub handles: Vec, + /// The first image's file name, shown over the preview. + pub name: String, + /// Brightness, contrast, fade. + pub values: [u8; 3], + /// The boxes as shown; a box may be empty while it is typed in. + pub texts: [String; 3], + /// The first image, at most 512 pixels a side, as RGBA. + pub source: Option<(u32, u32, Vec)>, + /// The drawing area colour the image fades into. + pub canvas: [u8; 3], + /// Left half as it is, right half adjusted, over the drawing area colour. + pub preview: Option, +} + +/// Brightness 50, contrast 50, fade 0. +pub const IMAGE_ADJUST_DEFAULTS: [u8; 3] = [50, 50, 0]; + +impl ImageAdjustState { + pub fn new(handles: Vec, path: &str, values: [u8; 3], canvas: [u8; 3]) -> Self { + let name = std::path::Path::new(path.trim()) + .file_name() + .map_or_else(|| path.trim().to_string(), |n| n.to_string_lossy().into_owned()); + let source = crate::scene::model::image_model::resolve_image(path).and_then(|decoded| { + let full = ::image::RgbaImage::from_raw(decoded.width, decoded.height, decoded.pixels.to_vec())?; + let small = if full.width().max(full.height()) > 512 { + ::image::imageops::thumbnail(&full, 512, 512) + } else { + full + }; + Some((small.width(), small.height(), small.into_raw())) + }); + let mut state = Self { + handles, + name, + values: [0; 3], + texts: Default::default(), + source, + canvas, + preview: None, + }; + state.set_all(values); + state + } + + pub fn set(&mut self, row: usize, value: u8) { + let mut values = self.values; + values[row] = value.min(100); + self.set_all(values); + } + + pub fn set_all(&mut self, values: [u8; 3]) { + self.values = values.map(|v| v.min(100)); + self.texts = self.values.map(|v| v.to_string()); + self.preview = self.source.as_ref().map(|(w, h, pixels)| { + // As the drawing shows it: brightness then contrast per channel, + // fade as the image's opacity over the drawing area. + let lut = crate::scene::model::image_model::brightness_contrast_lut(self.values[0], self.values[1]); + let opacity = 1.0 - f32::from(self.values[2]) / 100.0; + let mut out = Vec::with_capacity(pixels.len()); + for (k, px) in pixels.chunks_exact(4).enumerate() { + let after = k as u32 % w >= w / 2; + let alpha = f32::from(px[3]) / 255.0 * if after { opacity } else { 1.0 }; + for c in 0..3 { + let value = if after { lut[px[c] as usize] } else { px[c] }; + let bg = f32::from(self.canvas[c]); + out.push((bg + (f32::from(value) - bg) * alpha).round() as u8); + } + out.push(255); + } + image::Handle::from_rgba(*w, *h, out) + }); + } +} + +pub fn image_adjust<'a>( + state: &'a ImageAdjustState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let badge = |label: String, accent: bool| { + container(text(label).size(10)) + .padding([2, 8]) + .style(move |theme: &Theme| container::Style { + background: Some(Background::Color(if accent { + theme.palette().primary.base.color.scale_alpha(0.2) + } else { + iced::Color::from_rgba8(0x1e, 0x1e, 0x1e, 0.85) + })), + border: Border { + width: 1.0, + radius: 10.0.into(), + color: if accent { + theme.palette().primary.base.color + } else { + iced::Color::from_rgb8(0x4a, 0x4a, 0x4a) + }, + }, + text_color: Some(iced::Color::WHITE), + ..Default::default() + }) + }; + let canvas = iced::Color::from_rgb8(state.canvas[0], state.canvas[1], state.canvas[2]); + let picture: Element<'a, Message> = match &state.preview { + Some(handle) => image(handle.clone()) + .content_fit(iced::ContentFit::Cover) + .width(Fill) + .height(Fill) + .into(), + None => container(text(crate::tr!("image-adjust", "no-preview")).size(11).style(muted_style)) + .center(Fill) + .into(), + }; + let divider = container( + container(Space::new().width(2.0).height(Fill)).style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().primary.base.color)), + ..Default::default() + }), + ) + .center_x(Fill) + .height(Fill); + let badges = row![ + badge(crate::tr!("image-adjust", "before"), false), + Space::new().width(Fill), + badge(crate::tr!("image-adjust", "after"), true), + ] + .padding(8); + let mut layers = iced::widget::Stack::new().width(Fill).height(Fill).push(picture); + if state.preview.is_some() { + layers = layers.push(divider).push(badges); + } + let preview = card( + format!("{} ยท {}", crate::tr!("image-adjust", "preview"), state.name), + container(layers) + .width(Fill) + .height(Length::Fixed(150.0)) + .clip(true) + .style(move |_: &Theme| container::Style { + background: Some(Background::Color(canvas)), + border: Border { radius: 5.0.into(), ..Default::default() }, + ..Default::default() + }), + ); + let ramp = |row: u8| -> Background { + let (from, to) = match row { + // Contrast: grey, the flat image its zero gives. + 1 => (iced::Color::from_rgb8(0x77, 0x77, 0x77), iced::Color::from_rgb8(0x77, 0x77, 0x77)), + _ => (iced::Color::from_rgb8(0x22, 0x22, 0x22), iced::Color::from_rgb8(0xee, 0xee, 0xee)), + }; + Background::Gradient(iced::Gradient::Linear( + iced::gradient::Linear::new(iced::Radians(std::f32::consts::FRAC_PI_2)) + .add_stop(0.0, from) + .add_stop(1.0, to), + )) + }; + let line = |row: u8, label: String| -> Element<'a, Message> { + let k = row as usize; + let bar = container(container(Space::new()).width(Fill).height(8.0).style(move |_: &Theme| { + container::Style { + background: Some(ramp(row)), + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + } + })) + .center_y(16.0) + .width(Fill); + let slide = iced::widget::slider(0..=100u8, state.values[k], move |v| msg(PdfDialogMsg::IaValue(row, v))) + .height(16.0) + .style(|theme: &Theme, _| iced::widget::slider::Style { + rail: iced::widget::slider::Rail { + backgrounds: (iced::Color::TRANSPARENT.into(), iced::Color::TRANSPARENT.into()), + width: 0.0, + border: Border::default(), + }, + handle: iced::widget::slider::Handle { + shape: iced::widget::slider::HandleShape::Rectangle { width: 4, border_radius: 2.0.into() }, + background: theme.palette().primary.base.color.into(), + border_width: 0.0, + border_color: iced::Color::TRANSPARENT, + }, + }); + let step = |dir: i8| { + button(text(if dir < 0 { "\u{2212}" } else { "+" }).size(12)) + .on_press(msg(PdfDialogMsg::IaStep(row, dir))) + .style(button_style(false)) + .padding([2, 8]) + }; + let value = text_input("", &state.texts[k]) + .on_input(move |v| msg(PdfDialogMsg::IaText(row, v))) + .size(11) + .padding([3, 4]) + .width(Length::Fixed(40.0)) + .align_x(iced::alignment::Horizontal::Right) + .style(field_style); + row![ + text(label).size(11).width(Length::Fixed(72.0)), + iced::widget::Stack::new().width(Fill).push(bar).push(slide), + container(row![step(-1), value, step(1)].spacing(2).align_y(iced::Center)) + .padding(2) + .style(well_style), + ] + .spacing(10) + .align_y(iced::Center) + .into() + }; + let adjust = card( + crate::tr!("image-adjust", "adjust"), + column![ + line(0, crate::tr!("image-adjust", "brightness")), + line(1, crate::tr!("image-adjust", "contrast")), + line(2, crate::tr!("image-adjust", "fade")), + text(crate::tr!("image-adjust", "hint")).size(10).style(muted_style), + ] + .spacing(10), + ); + let reset = button(text(crate::tr!("image-adjust", "reset")).size(11).style(accent_text)) + .on_press(msg(PdfDialogMsg::IaReset)) + .style(button::text) + .padding([5, 4]); + column![ + preview, + adjust, + footer("imageadjust", Some(reset.into()), crate::tr!("image-adjust", "apply").into(), PdfDialogMsg::IaApply), + ] + .spacing(10) + .padding([10, 12]) + .width(sizing.width) + .into() +} diff --git a/src/ui/window/refedit_dialog.rs b/src/ui/window/refedit_dialog.rs new file mode 100644 index 000000000..e4fdb2fd3 --- /dev/null +++ b/src/ui/window/refedit_dialog.rs @@ -0,0 +1,247 @@ +//! Edit Reference dialog (REFEDIT): the nesting level to check out, shown as +//! a chain of chips beside a preview of the chosen definition, how the +//! working set is filled, whether attribute definitions come along, and the +//! session settings (lock what is outside the working set, give clashing +//! named objects of an external reference unique names). + +use iced::widget::{button, column, container, row, text, toggler, Space}; +use iced::{Border, Element, Fill, Length, Theme}; + +use crate::app::Message; +use crate::scene::model::wire_model::WireModel; +use crate::ui::style::common::muted_style; +use crate::ui::style::form::{button_style, dialog_button}; +use crate::ui::window::pdf_dialogs::{card, well_style}; + +/// One edit in the Edit Reference dialog. +#[derive(Debug, Clone)] +pub enum RefEditDialogMsg { + /// The nesting level picked in the chain (0 = the selected reference). + Level(usize), + /// Pick the working set's objects in the drawing (else all of them). + Nested(bool), + Attributes(bool), + Lock(bool), + Unique(bool), + Help, + Ok, +} + +fn msg(m: RefEditDialogMsg) -> Message { + Message::RefEditDialog(m) +} + +/// One level of the nesting chain as the dialog shows it. +pub struct RefEditLevel { + /// The reference's handle: the picked object for level 0, the + /// definition-owned reference below it. + pub insert: codec::Handle, + pub name: String, + /// The definition's preview. + pub wires: Vec, + /// The definition holds attribute definitions (the attribute switch is + /// only offered then). + pub has_attdefs: bool, + /// The referenced file of an external reference. + pub xref_path: Option, +} + +pub struct RefEditDialogState { + pub top: codec::Handle, + /// The picked chain, outermost first. + pub chain: Vec, + pub levels: Vec, + pub level: usize, + pub nested: bool, + pub attributes: bool, + pub lock: bool, + pub unique: bool, +} + +impl RefEditDialogState { + /// The attribute switch counts only where the chosen definition has + /// attribute definitions. + pub fn attributes_enabled(&self) -> bool { + self.levels.get(self.level).is_some_and(|l| l.has_attdefs) + } +} + +fn label<'a>(value: String, enabled: bool) -> Element<'a, Message> { + text(value) + .size(12) + .style(move |theme: &Theme| { + if enabled { + text::Style::default() + } else { + text::Style { + color: Some(theme.palette().background.strong.color), + } + } + }) + .width(Fill) + .into() +} + +/// A labelled switch on one row; inactive while `on_toggle` is None. +fn switch_row<'a>( + name: String, + value: bool, + on_toggle: Option RefEditDialogMsg>, +) -> Element<'a, Message> { + let enabled = on_toggle.is_some(); + let mut switch = toggler(value).size(18.0); + if let Some(on) = on_toggle { + switch = switch.on_toggle(move |v| msg(on(v))); + } + row![label(name, enabled), switch] + .spacing(8) + .align_y(iced::Center) + .into() +} + +/// A level of the chain as a pill; the chosen one framed in the accent colour. +fn level_chip<'a>(name: &str, index: usize, chosen: bool) -> Element<'a, Message> { + button(text(name.to_string()).size(12)) + .on_press(msg(RefEditDialogMsg::Level(index))) + .padding([5, 12]) + .style(move |theme: &Theme, status| { + let mut style = button_style(false)(theme, status); + style.border = Border { + width: if chosen { 1.5 } else { 1.0 }, + radius: 14.0.into(), + color: if chosen { + theme.palette().primary.base.color + } else { + theme.palette().background.strong.color + }, + }; + if chosen { + style.text_color = theme.palette().primary.base.color; + } + style + }) + .into() +} + +pub fn view<'a>( + state: &'a RefEditDialogState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let current = state.levels.get(state.level); + let preview: Element<'a, Message> = container(match current { + Some(level) => crate::ui::window::block_palette::preview_canvas(&level.wires, 104.0), + None => Space::new().into(), + }) + .width(Length::Fixed(150.0)) + .height(Length::Fixed(104.0)) + .padding(4) + .style(well_style) + .into(); + let mut chips = row![].spacing(6).align_y(iced::Center); + for (index, level) in state.levels.iter().enumerate() { + if index > 0 { + chips = chips.push(text("โ€บ").size(13).style(muted_style)); + } + chips = chips.push(level_chip(&level.name, index, index == state.level)); + } + let mut about = column![text(crate::tr!("ref-edit", "dialog-chain-hint")) + .size(11) + .style(muted_style)] + .spacing(4); + if let Some(path) = current.and_then(|l| l.xref_path.as_deref()) { + // A long path keeps its end, where the file name is. + const MAX: usize = 64; + let count = path.chars().count(); + let shown = if count > MAX { + format!("โ€ฆ{}", path.chars().skip(count - (MAX - 1)).collect::()) + } else { + path.to_string() + }; + about = about.push( + text(format!("{} {}", crate::tr!("ref-edit", "dialog-path"), shown)) + .size(11) + .style(muted_style), + ); + } + let reference = card( + crate::tr!("ref-edit", "dialog-reference"), + row![ + preview, + column![iced::widget::scrollable(chips).direction( + iced::widget::scrollable::Direction::Horizontal( + iced::widget::scrollable::Scrollbar::new().width(4).scroller_width(4) + ) + ), about] + .spacing(8) + .width(Fill) + ] + .spacing(12), + ); + + let segment = |nested: bool, name: String| { + button(text(name).size(12).width(Fill).align_x(iced::Center)) + .on_press(msg(RefEditDialogMsg::Nested(nested))) + .style(button_style(state.nested == nested)) + .padding([6, 8]) + .width(Fill) + }; + let method = container( + row![ + segment(false, crate::tr!("ref-edit", "dialog-all-objects")), + segment(true, crate::tr!("ref-edit", "dialog-pick-objects")), + ] + .spacing(2), + ) + .padding(2) + .width(Fill) + .style(well_style); + let attributes_enabled = state.attributes_enabled(); + let working_set = card( + crate::tr!("ref-edit", "dialog-working-set"), + column![ + method, + switch_row( + crate::tr!("ref-edit", "dialog-attributes"), + state.attributes && attributes_enabled, + attributes_enabled.then_some(RefEditDialogMsg::Attributes as fn(bool) -> _), + ), + ] + .spacing(10), + ); + let session = card( + crate::tr!("ref-edit", "dialog-session"), + column![ + switch_row( + crate::tr!("ref-edit", "dialog-lock"), + state.lock, + Some(RefEditDialogMsg::Lock), + ), + switch_row( + crate::tr!("ref-edit", "dialog-unique"), + state.unique, + Some(RefEditDialogMsg::Unique), + ), + ] + .spacing(10), + ); + let footer = row![ + button(text("?").size(12)) + .on_press(msg(RefEditDialogMsg::Help)) + .style(button_style(false)) + .padding([5, 11]), + Space::new().width(Fill), + dialog_button(crate::t!("Cancel"), Message::CloseModal, false), + dialog_button( + std::borrow::Cow::Owned(crate::tr!("ref-edit", "dialog-start")), + msg(RefEditDialogMsg::Ok), + true + ), + ] + .spacing(6) + .align_y(iced::Center); + column![reference, working_set, session, footer] + .spacing(10) + .padding([10, 12]) + .width(sizing.width) + .into() +} diff --git a/src/ui/wrap_bar.rs b/src/ui/wrap_bar.rs index d24ab31f0..68c41710a 100644 --- a/src/ui/wrap_bar.rs +++ b/src/ui/wrap_bar.rs @@ -34,14 +34,46 @@ use crate::app::Message; thread_local! { /// Screen bounds of every ribbon dropdown button, keyed by dropdown id, so /// an open dropdown's overlay can anchor directly below its widget at any - /// size. Written by `PosReport` on draw, read by `dropdown_bounds`. - static DD_BOUNDS: RefCell> = + /// size. Written by `PosReport` on draw, read by `dropdown_bounds`. Each + /// entry keeps the frame it was drawn in (see `fresh_bounds`). + static DD_BOUNDS: RefCell> = RefCell::new(FxHashMap::default()); + /// Advanced by `end_frame` once everything above the widgets has drawn. + static FRAME: Cell = const { Cell::new(0) }; } /// Last-drawn screen bounds of the dropdown button with this id. pub fn dropdown_bounds(id: &str) -> Option { - DD_BOUNDS.with(|m| m.borrow().get(id).copied()) + DD_BOUNDS.with(|m| m.borrow().get(id).map(|(bounds, _)| *bounds)) +} + +/// Bounds of the widget `id` only when it was drawn in the current frame, so +/// a widget that left the screen does not report its old place. +pub fn fresh_bounds(id: &str) -> Option { + let frame = FRAME.with(Cell::get); + DD_BOUNDS.with(|m| { + m.borrow() + .get(id) + .filter(|(_, drawn)| *drawn == frame) + .map(|(bounds, _)| *bounds) + }) +} + +/// Bounds of the widget `id` when it was drawn in the current or the last +/// finished frame (for use between frames). +pub fn recent_bounds(id: &str) -> Option { + let frame = FRAME.with(Cell::get); + DD_BOUNDS.with(|m| { + m.borrow() + .get(id) + .filter(|(_, drawn)| drawn + 1 >= frame) + .map(|(bounds, _)| *bounds) + }) +} + +/// Close the current frame for `fresh_bounds`. +pub fn end_frame() { + FRAME.with(|frame| frame.set(frame.get() + 1)); } pub struct WrapBar<'a> { @@ -1038,7 +1070,8 @@ impl<'a> Widget for PosReport<'a> { viewport: &Rectangle, ) { DD_BOUNDS.with(|m| { - m.borrow_mut().insert(self.id.to_string(), layout.bounds()); + m.borrow_mut() + .insert(self.id.to_string(), (layout.bounds(), FRAME.with(Cell::get))); }); self.child.as_widget().draw( &tree.children[0], diff --git a/tests/active_space_roundtrip.rs b/tests/active_space_roundtrip.rs index 7387be1ec..85739c387 100644 --- a/tests/active_space_roundtrip.rs +++ b/tests/active_space_roundtrip.rs @@ -77,3 +77,44 @@ fn ctab_is_created_when_absent_then_updated_in_place() { Some("Layout3") ); } + +/// A layout saved with a model viewport entered reopens inside it; saved +/// from paper space it reopens on the sheet (#1670). +#[test] +fn entered_viewport_survives_a_save_and_reload() { + use codec::{entities::Viewport, EntityType}; + for format in ["dxf", "dwg"] { + let mut scene = Scene::new(); + scene.set_current_layout("Layout1".to_string()); + let mut sheet = Viewport::new(); + sheet.id = 1; + sheet.width = 420.0; + sheet.height = 297.0; + let sheet = scene.add_entity(EntityType::Viewport(sheet)); + let mut model = Viewport::new(); + model.id = 2; + model.width = 100.0; + model.height = 80.0; + model.view_height = 80.0; + let model = scene.add_entity(EntityType::Viewport(model)); + assert_ne!(sheet, model); + + for entered in [Some(model), None] { + scene.active_viewport = entered; + scene.sync_active_viewport_to_document(); + let bytes = + OpenCADStudio::io::save_to_bytes(&scene.document, format, scene.document.version) + .expect("save"); + let doc = OpenCADStudio::io::load_bytes(&format!("roundtrip.{format}"), bytes) + .expect("reload"); + let mut reopened = Scene::new(); + reopened.document = doc; + reopened.current_layout = "Layout1".to_string(); + assert_eq!( + reopened.saved_entered_viewport().is_some(), + entered.is_some(), + "{format}: entered {entered:?}" + ); + } + } +}