44 Commits

Author SHA1 Message Date
Marcos Paulo 26b79dc929 chore: release 0.6.3
Release / release (push) Has been cancelled
2026-10-12 21:40:44 -03:00
Marcos Paulo 5569ce264a feat(ui): Catppuccin themes; use the masi/craft-libs fork
Adds Latte, Frappé, Macchiato and Mocha to the interface themes (Options, View >
Interface Theme, ui.theme command). craft-ui now comes from the masi/craft-libs fork.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 20:53:10 -03:00
Marcos Paulo efe325f178 chore: debrand ArtCraft and Discord references
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 20:53:10 -03:00
Brandon Thomas 00c7613f1a Release 0.6.2: rebuild with the fixed shared release workflow
Packaging lint / lint (push) Has been cancelled
The previous release build failed in craft-common-actions, not in this
repo: riscv64 packaging got --formats deb,rpm,tar (fixed in
craft-common-actions#3), Linux jobs lacked the ALSA headers
(craft-common-actions#5), and Windows signing needed the AZURE_* secrets
listed in the caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:11:19 -04:00
Brandon Thomas 0ecffdab4a Release workflow: pass the Azure signing secrets so Windows builds are signed (#647)
Since the release workflow moved to storytold/craft-common-actions, it has
passed only the Apple and WINDOWS_CERTIFICATE secrets. The signing job reads
values from the release environment, but only for names the caller lists, so
the AZURE_* names that Trusted Signing uses came through empty and Windows
builds would ship unsigned. Same fix as storytold/craft-launcher#174.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:08:09 -04:00
Brandon Thomas 4cb9e8a535 Release 0.6.1: check out craft-fonts in release builds
The shared release workflow sets CRAFT_FONTS_DIR with CRAFT_FONTS_REQUIRED=1
but only checks craft-fonts out when craft-fonts-ref is given. Since #321
cadcraft-fonts reads that directory, so every native release job failed
with "fonts/manifest.txt: No such file or directory". Pin craft-fonts
3d9ab3a (current main, with the Arabic faces the web build embeds).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:06:24 -04:00
Brandon Thomas a12c85c585 Release prep: 0.6.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:44:20 -04:00
cyfung1031 890f7f1c6f Performance: reuse nearby snap geometry for tracking acquisition (#698)
Squash of PR #698 by @cyfung1031.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:35:13 -04:00
mayloglog 4bb3acb913 Add Chinese language support to preferences (#684)
Squash of PR #684 by @mayloglog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:28:17 -04:00
aldozar 0675648de8 Hatch: pick the pattern from swatches instead of typing its name (fixes #669) (#679)
Squash of PR #679 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:21:49 -04:00
aldozar c2f8782cc2 Plot dialog: device, plot style table, Window <, and a preview of its settings (#673)
Squash of PR #673 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:21:33 -04:00
aldozar d365b60704 MIRROR and PASTE previews on the GPU; GPU previews in layouts too (#655)
Squash of PR #655 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:21:18 -04:00
aldozar cf797cbafe Build the screen's model-space display list on all cores; read-write locks for glyph caches (#654)
Squash of PR #654 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:21:04 -04:00
Lion 935cb98eed SCRIPT command: run .scr files from the command line, menus and JSON (gaps.md E2) (#693)
Squash of PR #693 by @Lion-1209.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:13:51 -04:00
Diego Ospina 0178858e79 i18n(es): add 219 missing rows, fix mistranslations and terms (#680)
Squash of PR #680 by @diegozpy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:13:36 -04:00
rangoDJ 875f99d26f Deduplicate identical boundary loops to prevent empty hatch fills (#671)
Squash of PR #671 by @rangoDJ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:13:20 -04:00
MarekSpodBiedry 69ea207617 Cache active viewport highlights per zoom band so zoom and pan stay smooth (#687) (#695)
Squash of PR #695 by @MarekSpodBiedry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:13:04 -04:00
Books 546eefc58a Zoom the canvas by each mouse-wheel notch in the frame it arrives (#692)
Squash of PR #692 by @SBaksa.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:12:49 -04:00
SteamSmith 61c018845a control server: bound request lines and concurrent connections (#678)
Squash of PR #678 by @SteamSmith.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:12:34 -04:00
rangoDJ 9a0d1b3a62 Prioritize command aliases in autocomplete and display shortcut match (#656)
Squash of PR #656 by @rangoDJ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:12:17 -04:00
aldozar 507c5e5b34 File tabs: cut long drawing names short with an ellipsis (#657)
Squash of PR #657 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:11:59 -04:00
Jason Brashear bc6dc997ef AUNITS: degrees/minutes/seconds angles round at the displayed precision (#672)
Squash of PR #672 by @webdevtodayjason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:11:42 -04:00
aldozar 6ca8cec97d BRICK: draw each vertical joint once (#664)
Squash of PR #664 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:11:25 -04:00
CaseyKleinman f8cb50eaa3 DXF: write kept application data groups as read, not as handle lists (#681)
Squash of PR #681 by @CaseyKleinman.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:11:09 -04:00
markmonkey b6f2e86f78 Show block insertion XYZ in Properties Inspector (#682)
Squash of PR #682 by @markmonkey.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:10:53 -04:00
aldozar c57879a5c8 GPU errors and device loss: keep running, autosave at once, tell the user (#652)
Squash of PR #652 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:42:45 -04:00
PedroGuille 2404719c3b Fix filled text zoom freezes with cached glyph meshes (#651)
Squash of PR #651 by @PedroGuille.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:42:29 -04:00
rangoDJ 90cd443000 Add regression tests for Plot Preview toolbar button (fixes #442) (#653)
Squash of PR #653 by @rangoDJ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:42:14 -04:00
PedroGuille ac9483c9c1 Add raw-HID SpaceMouse 2D navigation (#650)
Squash of PR #650 by @PedroGuille.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:29:48 -04:00
pechnikov2026-star ade5469a24 i18n: add Russian, Spanish, French, Portuguese and German interfaces (#624)
Squash of PR #624 by @pechnikov2026-star.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:22:47 -04:00
Daniel Lošák 6d7c652b65 i18n: add Czech (cs) interface language (#639)
Squash of PR #639 by @Losminek.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:11:51 -04:00
Valentin Klein 3aeedf4856 docs: refresh gaps.md and parity docs for landed features (#625)
Squash of PR #625 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:11:37 -04:00
Gert Pellin - GPE f7c6293d09 feat: SVG import — read SVG and SVGZ into native entities (#636)
Squash of PR #636 by @switch87.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:11:22 -04:00
Valentin Klein 89064fd82a Drafting/plot/UI follow-ups: PolarSnap over isometric snap, MVIEW Restore, plot margins, PNG/SVG pens, PE_URL links, OPTIONS settings (part of #626) (#633)
Squash of PR #633 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:11:06 -04:00
aldozar 1e1f68bf40 Fix fatal wgpu buffer overflow; draw MOVE/COPY/ROTATE/SCALE previews and the selection highlight on the GPU (#642)
Squash of PR #642 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:10:51 -04:00
STE1200 244feb6e89 Render: level of detail and view clipping for pattern hatches on screen (fixes #544) (#638)
Squash of PR #638 by @STE1200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:10:37 -04:00
aldozar 135c928fea Hatches: draw the pattern definition stored in the file; scale library patterns in metric drawings (#643)
Squash of PR #643 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:54:18 -04:00
Valentin Klein 9477f37503 File-format follow-ups: version-gated MULTILEADER and DIMSTYLE groups, kept $DWGCODEPAGE, PURGE multiline styles (#632)
Squash of PR #632 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:54:03 -04:00
Fabio Lussi 370150186b Linux: Theme > System follows the desktop's light/dark setting (#641)
Squash of PR #641 by @itrycodinganditsbad.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:53:48 -04:00
Muhi 2fa967db65 fix(io): write DXF CLASS instance counts and preserve current leader style (#635)
Squash of PR #635 by @The-Great-Nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:53:33 -04:00
Gert Pellin - GPE 4e276db858 Tool Sets: the gear on a category header opens that category's options (#620)
Squash of PR #620 by @switch87.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:53:19 -04:00
rangoDJ 9b3674e01f Make right palette collapse arrow interactive (fix #617) (#640)
Squash of PR #640 by @rangoDJ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:53:05 -04:00
MarekSpodBiedry c5ebf4d1cd Keep inter-word spaces in stroke-font MTEXT (#628) (#644)
Squash of PR #644 by @MarekSpodBiedry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:52:51 -04:00
MarekSpodBiedry 83b8a84050 Add PTYPE as an alias of the Point Style command (#622) (#645)
Squash of PR #645 by @MarekSpodBiedry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:52:37 -04:00
117 changed files with 11899 additions and 676 deletions
+7
View File
@@ -24,6 +24,7 @@ jobs:
with:
app-name: cadcraft
app-title: CADCraft
craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
version: ${{ inputs.version }}
secrets:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -34,3 +35,9 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }}
+4
View File
@@ -50,3 +50,7 @@ Generated-in-code assets are original and have no file to list:
| `crates/ui-egui/src/i18n/de.tsv` | the CADCraft contributors | Original German translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
| `crates/ui-egui/src/i18n/es.tsv` | @PedroGuille | Original Spanish translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
| `crates/ui-egui/src/i18n/pt_br.tsv` | @WladmirJunior | Original Brazilian Portuguese translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
| `crates/ui-egui/src/i18n/cs.tsv` | @Losminek (AI-assisted) | Original Czech translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
| `crates/ui-egui/src/i18n/ru.tsv` | @pechnikov2026-star | Original Russian translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
| `crates/ui-egui/src/i18n/fr.tsv` | @pechnikov2026-star | Original French translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
| `crates/ui-egui/src/i18n/zh.tsv` | @mayloglog | Original Simplified Chinese translations of CADCraft English strings | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | No proprietary localization resources copied |
Generated
+92 -16
View File
@@ -433,6 +433,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "basic-udev"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a45f9771ced8a774de5e5ebffbe520f52e3943bf5a9a6baa3a5d14a5de1afe6"
[[package]]
name = "bit-set"
version = "0.10.0"
@@ -540,13 +546,14 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadcraft"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
"cadcraft-ui-egui",
"eframe",
"egui",
"hidapi",
"log",
"muda",
"rfd",
@@ -554,11 +561,12 @@ dependencies = [
"sys-locale",
"winit",
"winresource",
"zbus",
]
[[package]]
name = "cadcraft-cli"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -569,14 +577,14 @@ dependencies = [
[[package]]
name = "cadcraft-color"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"serde",
]
[[package]]
name = "cadcraft-constraints"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-doc",
"cadcraft-geom",
@@ -586,7 +594,7 @@ dependencies = [
[[package]]
name = "cadcraft-doc"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-color",
"cadcraft-geom",
@@ -597,14 +605,14 @@ dependencies = [
[[package]]
name = "cadcraft-dwg"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"acadrust",
]
[[package]]
name = "cadcraft-dxf"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"encoding_rs",
"log",
@@ -613,7 +621,7 @@ dependencies = [
[[package]]
name = "cadcraft-engine"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-color",
"cadcraft-constraints",
@@ -632,7 +640,7 @@ dependencies = [
[[package]]
name = "cadcraft-fonts"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-geom",
"harfrust",
@@ -642,14 +650,14 @@ dependencies = [
[[package]]
name = "cadcraft-geom"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"serde",
]
[[package]]
name = "cadcraft-io"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-color",
"cadcraft-doc",
@@ -659,6 +667,7 @@ dependencies = [
"cadcraft-geom",
"cadcraft-render",
"miniz_oxide 0.8.9",
"quick-xml",
"serde",
"serde_json",
"thiserror 2.0.21",
@@ -666,7 +675,7 @@ dependencies = [
[[package]]
name = "cadcraft-mcp"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -677,19 +686,21 @@ dependencies = [
[[package]]
name = "cadcraft-render"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-color",
"cadcraft-doc",
"cadcraft-fonts",
"cadcraft-geom",
"lyon_tessellation",
"rayon",
"serde_json",
"tiny-skia",
]
[[package]]
name = "cadcraft-ui-egui"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-color",
"cadcraft-doc",
@@ -710,7 +721,7 @@ dependencies = [
[[package]]
name = "cadcraft-web"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -1390,6 +1401,12 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "float_next_after"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
[[package]]
name = "foldhash"
version = "0.2.0"
@@ -1742,6 +1759,21 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hidapi"
version = "2.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "818c0e1d27887aaf76fe737042e27a66b796a7b099e6d2e1a72d106c2dff3fa6"
dependencies = [
"basic-udev",
"cc",
"cfg-if",
"libc",
"nix",
"pkg-config",
"windows-sys 0.61.2",
]
[[package]]
name = "icu_collections"
version = "2.3.0"
@@ -2119,6 +2151,38 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lyon_geom"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92"
dependencies = [
"arrayvec",
"euclid",
"num-traits",
]
[[package]]
name = "lyon_path"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e"
dependencies = [
"lyon_geom",
"num-traits",
]
[[package]]
name = "lyon_tessellation"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005"
dependencies = [
"float_next_after",
"lyon_path",
"num-traits",
]
[[package]]
name = "memchr"
version = "2.8.3"
@@ -2272,6 +2336,18 @@ dependencies = [
"jni-sys 0.3.1",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
@@ -4696,7 +4772,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.5.0"
version = "0.6.3"
dependencies = [
"serde_json",
]
+4 -1
View File
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
# default-members
[workspace.package]
version = "0.5.0"
version = "0.6.3"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.90"
@@ -34,6 +34,7 @@ rayon = "1"
# rendering / formats
tiny-skia = { version = "0.11", default-features = false, features = ["std", "simd", "png-format"] }
lyon_tessellation = "1.0.22"
skrifa = "0.47"
# Text shaping (OpenType GSUB/GPOS, HarfBuzz port on read-fonts; the version egui's epaint uses)
# and the Unicode Bidirectional Algorithm for right-to-left drawing text.
@@ -41,6 +42,8 @@ harfrust = "0.12"
unicode-bidi = "0.3.18"
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
encoding_rs = "0.8.35"
# XML for the SVG reader (streaming parser, no entity expansion beyond the five predefined ones)
quick-xml = "0.41"
# ui
craft-ui = { version = "0.1.0", git = "https://git.marcospaulo.dev.br/masi/craft-libs", rev = "22544b6d619d4f16779b8bdfa3f0b642a86fc065", features = ["serde"] }
+1 -1
View File
@@ -86,7 +86,7 @@ numbers):
| Layouts & plotting | Paper-space layouts, viewports (scale, lock, per-viewport layer freeze), MSPACE/PSPACE through viewports, page setups, PLOT and EXPORTPDF |
| Parametric | Geometric and dimensional constraints, AUTOCONSTRAIN, PARAMETERS with expressions, conflict detection; constraints re-solve after every edit |
| Grips | Hot grips with Space to cycle Stretch, Move, Rotate, Scale and Mirror |
| Interface languages | English, Ukrainian (Українська) and German (Deutsch), live switching and a remembered Auto/system-language choice; [details](docs/localization.md) |
| Interface languages | English, Ukrainian (Українська), Brazilian Portuguese (Português (Brasil)), German (Deutsch), Spanish (Español), Czech (Čeština), Russian (Русский), French (Français) and Simplified Chinese (简体中文, with an installed CJK font), live switching and a remembered Auto/system-language choice; [details](docs/localization.md) |
| Automation | MCP server, JSON control channel, `cadcraft-cli` (info, convert, run, commands, mcp) |
## Quick start
+46 -1
View File
@@ -7,6 +7,7 @@
//! cadcraft-cli commands [FILTER] command catalog (JSON)
//! cadcraft-cli mcp [--connect HOST:PORT] MCP server on stdio (headless or bridged to the app)
//! cadcraft-cli perf [N] timing table on a synthetic N-entity drawing
//! cadcraft-cli perf FILE canvas display-list build of a drawing, zoomed in step by step
//! cadcraft-cli sample (bracket|floorplan) OUT write a built-in sample drawing
//! ```
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
@@ -60,7 +61,7 @@ const USAGE: &str = "usage:
cadcraft-cli run [FILE | --sample | --metric] [--script TEXT] [--script-file F.scr] [--cmd 'id {json}']... [--save OUT.dxf] [--export OUT.(png|svg|pdf)] [FRAMING]
cadcraft-cli commands [FILTER]
cadcraft-cli mcp [--connect HOST:PORT]
cadcraft-cli perf [N]
cadcraft-cli perf [N | FILE]
cadcraft-cli sample (bracket|floorplan) OUT.(dxf|dwg|svg|png)
cadcraft-cli --version
@@ -393,11 +394,55 @@ fn synthetic(n: usize, side: f64) -> Result<cadcraft_engine::doc::Drawing, Strin
Ok(dr)
}
/// `perf FILE`: the display list built as the canvas builds it (a 1600 x 1000 px view at the
/// drawing's centre: tolerance, dash level of detail and view window from the zoom band), at
/// zoom extents and zoomed in 4x at a time, next to the full-detail build plots use.
fn perf_file(path: &str) -> Result<(), String> {
use cadcraft_engine::geom::{Bounds2, Vec2};
use cadcraft_engine::render::{Options, build};
use std::time::Instant;
let ms = |t: Instant| t.elapsed().as_secs_f64() * 1000.0;
let mut s = Session::empty();
let t = Instant::now();
open(&mut s, path)?;
let open_ms = ms(t);
let st = s.state().map_err(|e| e.to_string())?;
let (d, space) = (&st.doc, &st.space);
let ext = d.extents(space);
let size = ext.width().max(ext.height());
if ext.is_empty() || !(size.is_finite() && size > 0.0) {
return Err("perf: the drawing is empty".into());
}
outln!("open {open_ms:.0} ms, {} entities, extents {:.1} x {:.1}", d.entity_count(), ext.width(), ext.height());
outln!("{:>8} {:>10} {:>10}", "zoom", "build ms", "prims");
let mut finest = Options::default();
for k in 0..5 {
let zoom = 4f64.powi(k);
let h = size / zoom;
let pixel = 2f64.powi((h / 1000.0).max(1e-300).log2().floor() as i32);
let half = Vec2::new(h * 0.8, h * 0.5);
let visible = Bounds2::new(ext.center() - half, ext.center() + half);
let screen = Options { tolerance: pixel * 0.5, min_dash: pixel * 2.0, view: Some(visible.expand(h * 0.8)), ..Default::default() };
let t = Instant::now();
let n = build(d, space, &screen).prims.len();
outln!("{:>7}x {:>10.1} {n:>10}", zoom, ms(t));
finest = screen;
}
// Plots: every pattern line, everywhere (last, so its large list doesn't weigh on the above).
let t = Instant::now();
let n = build(d, space, &Options { min_dash: 0.0, view: None, ..finest }).prims.len();
outln!("{:>8} {:>10.1} {n:>10}", "full", ms(t));
Ok(())
}
fn perf(args: &[String]) -> Result<(), String> {
use cadcraft_engine::doc::Space;
use cadcraft_engine::geom::{Bounds2, Vec2};
use cadcraft_engine::{select, snap};
use std::time::Instant;
if let Some(path) = args.first().filter(|a| a.parse::<usize>().is_err()) {
return perf_file(path);
}
let n: usize = match args.first() {
Some(a) => a.parse().map_err(|_| format!("perf: bad entity count {a}"))?,
None => 200_000,
+11
View File
@@ -24,6 +24,10 @@ winresource = "0.1"
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
winit = { version = "0.30", default-features = false, features = ["x11"] }
# View ▸ Theme ▸ System: the desktop's light/dark choice from the XDG desktop portal
# (desktop_theme.rs). Already in Cargo.lock through accesskit; pinned to that version so nothing
# is built twice.
zbus = { version = "=5.19.0", default-features = false, features = ["async-io", "blocking-api"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rfd = { workspace = true }
@@ -31,3 +35,10 @@ sys-locale = "0.3"
[target.'cfg(target_os = "macos")'.dependencies]
muda = { version = "0.21", default-features = false }
# Rust HID backends: no proprietary SDK or C HID library.
[target.'cfg(target_os = "windows")'.dependencies]
hidapi = { version = "2.6.7", default-features = false, features = ["windows-native"] }
[target.'cfg(target_os = "linux")'.dependencies]
hidapi = { version = "2.6.7", default-features = false, features = ["linux-native-basic-udev"] }
+149 -11
View File
@@ -1,14 +1,25 @@
//! Loopback JSON-lines control server: one request per line, one reply per line.
//! This is the transport the MCP server (`cadcraft-cli mcp --connect`) wraps.
//!
//! Lines are bounded ([`MAX_LINE`]): a longer one is refused and the connection closed, without
//! buffering the rest. So is the number of connections served at once ([`MAX_CONNECTIONS`]).
use std::io::{BufRead, BufReader, Write};
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration;
use cadcraft_ui_egui::ControlRequest;
use serde_json::{Value, json};
/// Longest accepted request line (bytes, without the newline). Generous, so requests that carry
/// whole drawings or images as base64 still fit.
const MAX_LINE: usize = 256 * 1024 * 1024;
/// Connections served at once; further ones get an error line and are closed.
const MAX_CONNECTIONS: usize = 16;
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
let (tx, rx) = channel::<ControlRequest>();
let listener = match TcpListener::bind(("127.0.0.1", port)) {
@@ -19,25 +30,82 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
}
};
log::info!("control server listening on 127.0.0.1:{port}");
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let tx = tx.clone();
let ctx = ctx.clone();
std::thread::spawn(move || serve(stream, tx, ctx));
}
});
std::thread::spawn(move || accept_loop(listener, tx, ctx));
rx
}
/// A connection's place among [`MAX_CONNECTIONS`], given back when the connection ends (also when
/// its thread can't be started, or panics).
struct Slot(Arc<AtomicUsize>);
impl Drop for Slot {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::SeqCst);
}
}
fn accept_loop(listener: TcpListener, tx: Sender<ControlRequest>, ctx: egui::Context) {
let open = Arc::new(AtomicUsize::new(0));
for mut stream in listener.incoming().flatten() {
if open.fetch_add(1, Ordering::SeqCst) >= MAX_CONNECTIONS {
open.fetch_sub(1, Ordering::SeqCst);
let _ = writeln!(stream, "{}", json!({"ok": false, "error": "too many control connections"}));
continue;
}
let slot = Slot(Arc::clone(&open));
let tx = tx.clone();
let ctx = ctx.clone();
let _ = std::thread::Builder::new().spawn(move || {
let _slot = slot;
serve(stream, tx, ctx);
});
}
}
/// Read one `\n`-terminated line of at most `max` bytes into `buf` (newline stripped).
/// `Ok(false)` at end of stream; `Err` when the line is too long or the read fails.
fn read_bounded_line(reader: &mut impl BufRead, buf: &mut Vec<u8>, max: usize) -> Result<bool, String> {
buf.clear();
// `take` bounds the bytes buffered for one line: a peer can't make us allocate more than `max`.
let limit = u64::try_from(max).unwrap_or(u64::MAX).saturating_add(1);
let n = reader.take(limit).read_until(b'\n', buf).map_err(|e| e.to_string())?;
if n == 0 {
return Ok(false);
}
if buf.last() == Some(&b'\n') {
buf.pop();
if buf.last() == Some(&b'\r') {
buf.pop();
}
return Ok(true);
}
if buf.len() > max {
return Err(format!("request line longer than {max} bytes; closing the connection"));
}
// Last line without a newline, then end of stream.
Ok(true)
}
fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
let Ok(read) = stream.try_clone() else { return };
let mut out = stream;
for line in BufReader::new(read).lines() {
let Ok(line) = line else { break };
let mut reader = BufReader::new(read);
let mut buf = Vec::new();
loop {
match read_bounded_line(&mut reader, &mut buf, MAX_LINE) {
Ok(true) => {}
Ok(false) => break,
Err(e) => {
let _ = writeln!(out, "{}", json!({"ok": false, "error": e}));
break;
}
}
// Bytes that aren't UTF-8 end the connection, as `BufRead::lines` did.
let Ok(line) = std::str::from_utf8(&buf) else { break };
if line.trim().is_empty() {
continue;
}
let reply = match serde_json::from_str::<Value>(&line) {
let reply = match serde_json::from_str::<Value>(line) {
Ok(msg) => {
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
@@ -62,3 +130,73 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lines_are_read_up_to_the_limit() {
let mut buf = Vec::new();
let mut input: &[u8] = b"aaaaaaaaaa\nbb\r\ncc";
assert_eq!(read_bounded_line(&mut input, &mut buf, 10), Ok(true));
assert_eq!(buf, b"aaaaaaaaaa", "a line exactly at the limit is accepted");
assert_eq!(read_bounded_line(&mut input, &mut buf, 10), Ok(true));
assert_eq!(buf, b"bb", "CRLF is stripped");
assert_eq!(read_bounded_line(&mut input, &mut buf, 10), Ok(true));
assert_eq!(buf, b"cc", "a last line without a newline is still read");
assert_eq!(read_bounded_line(&mut input, &mut buf, 10), Ok(false), "then end of stream");
}
#[test]
fn an_overlong_line_is_refused_without_reading_the_rest() {
let mut buf = Vec::new();
let long = [b'x'; 100];
let mut input: &[u8] = &long;
assert!(read_bounded_line(&mut input, &mut buf, 10).is_err());
assert!(buf.len() <= 11, "at most limit + 1 bytes are buffered, got {}", buf.len());
assert_eq!(input.len(), 100 - 11, "the rest of the line is left unread");
}
/// The control server on a loopback port, with a stand-in UI that echoes each method.
fn serve_echo() -> std::net::SocketAddr {
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = channel::<ControlRequest>();
std::thread::spawn(move || accept_loop(listener, tx, egui::Context::default()));
std::thread::spawn(move || {
for req in rx {
let _ = req.reply.send(json!({"ok": true, "method": req.method}));
}
});
addr
}
#[test]
fn requests_are_answered_with_their_ids() {
let addr = serve_echo();
let mut c = TcpStream::connect(addr).unwrap();
c.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
c.write_all(b"{\"id\":3,\"method\":\"ping\"}\r\n\n{\"id\":4,\"method\":\"pong\"}\n").unwrap();
let mut r = BufReader::new(c);
for (id, method) in [(3, "ping"), (4, "pong")] {
let mut line = String::new();
r.read_line(&mut line).unwrap();
let v: Value = serde_json::from_str(&line).unwrap();
assert_eq!((v["id"].as_i64(), v["method"].as_str()), (Some(id), Some(method)), "{line}");
}
}
#[test]
fn extra_connections_are_refused() {
let addr = serve_echo();
// Idle connections are accepted in order and held open.
let held: Vec<TcpStream> = (0..MAX_CONNECTIONS).map(|_| TcpStream::connect(addr).unwrap()).collect();
let mut extra = TcpStream::connect(addr).unwrap();
extra.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut line = String::new();
BufReader::new(&mut extra).read_line(&mut line).unwrap();
assert!(line.contains("too many control connections"), "{line}");
drop(held);
}
}
+174
View File
@@ -0,0 +1,174 @@
//! The desktop's own light/dark choice, for View ▸ Theme ▸ System.
//!
//! winit reports no system theme on Linux and the BSDs (`RawInput::system_theme` stays `None`),
//! so the System choice fell back to dark whatever the desktop said. This asks the desktop itself:
//! the XDG Desktop Portal's Settings interface, which answers on GNOME, KDE and XFCE alike, over
//! X11 and Wayland alike. The answer is handed to egui as the system theme
//! (`App::raw_input_hook`), so the app and egui's own widgets follow it as they follow macOS and
//! Windows.
//!
//! Ported from PdfCraft (storytold/pdfcraft#881).
use std::sync::mpsc::{Receiver, TryRecvError, channel};
use std::time::Duration;
/// How often the watcher re-reads the desktop's answer. The portal also publishes a change
/// signal, but subscribing needs a connection kept alive for the life of the window; one round
/// trip every few seconds is cheap and reacts soon enough after a theme switch.
const POLL: Duration = Duration::from_secs(4);
/// Watches the desktop and reports its choice when it changes.
#[derive(Default)]
pub struct DesktopTheme {
/// `None` when no watcher thread could be started (or on a platform where winit answers).
changes: Option<Receiver<Option<egui::Theme>>>,
/// The desktop's last answer.
current: Option<egui::Theme>,
}
impl DesktopTheme {
/// Start watching. The first answer is sent straight away; `wake` is called after each
/// answer, so an idle window repaints and picks it up.
pub fn start(wake: impl Fn() + Send + 'static) -> Self {
if !ASKS_THE_DESKTOP {
return Self::default();
}
let (tx, rx) = channel();
let started = std::thread::Builder::new().name("cadcraft-desktop-theme".into()).spawn(move || {
let mut last = read();
if tx.send(last).is_err() {
return;
}
wake();
loop {
std::thread::sleep(POLL);
let now = read();
if now != last {
last = now;
if tx.send(now).is_err() {
return;
}
wake();
}
}
});
Self { changes: started.ok().map(|_| rx), current: None }
}
/// The desktop's choice, `None` when it states none or there is nobody to ask (no session
/// bus, no portal). Polled once a frame; a desktop that stops answering keeps the last answer.
pub fn current(&mut self) -> Option<egui::Theme> {
if let Some(changes) = &self.changes {
loop {
match changes.try_recv() {
Ok(Some(theme)) => self.current = Some(theme),
Ok(None) => {}
Err(TryRecvError::Empty) | Err(TryRecvError::Disconnected) => break,
}
}
}
self.current
}
}
/// Where winit has no answer and the portal does.
const ASKS_THE_DESKTOP: bool = cfg!(all(unix, not(target_os = "macos")));
/// Ask the portal once. D-Bus calls block, so this belongs off the UI thread: the watcher's
/// thread is its only caller.
#[cfg(all(unix, not(target_os = "macos")))]
fn read() -> Option<egui::Theme> {
const PORTAL: &str = "org.freedesktop.portal.Desktop";
const PATH: &str = "/org/freedesktop/portal/desktop";
const SETTINGS: &str = "org.freedesktop.portal.Settings";
// The standard key first (GNOME and KDE answer it: 0 no preference, 1 dark, 2 light); then
// GNOME's own schema, which XFCE's portal proxies without the standard key.
const KEYS: [(&str, &str); 2] = [("org.freedesktop.appearance", "color-scheme"), ("org.gnome.desktop.interface", "color-scheme")];
let connection = zbus::blocking::Connection::session().ok()?;
for (namespace, key) in KEYS {
// `ReadOne` (Settings version 2) answers the value; older portals only have `Read`, which
// wraps it in one more variant. `parse` unwraps either.
let reply = connection
.call_method(Some(PORTAL), PATH, Some(SETTINGS), "ReadOne", &(namespace, key))
.or_else(|_| connection.call_method(Some(PORTAL), PATH, Some(SETTINGS), "Read", &(namespace, key)));
let Ok(reply) = reply else { continue };
let Ok((answer,)) = reply.body().deserialize::<(zbus::zvariant::OwnedValue,)>() else { continue };
if let Some(scheme) = parse(&answer, 0) {
return scheme;
}
}
None
}
/// The portal's `color-scheme` answer: `Some(choice)` when it read as an answer (`Some(None)`
/// for "no preference"), `None` when it didn't. Variants are unwrapped a few levels deep.
#[cfg(all(unix, not(target_os = "macos")))]
fn parse(value: &zbus::zvariant::Value<'_>, depth: u8) -> Option<Option<egui::Theme>> {
use zbus::zvariant::Value;
match value {
Value::U32(1) => Some(Some(egui::Theme::Dark)),
Value::U32(2) => Some(Some(egui::Theme::Light)),
Value::U32(_) => Some(None),
Value::Str(s) => Some(match s.as_str() {
"prefer-dark" => Some(egui::Theme::Dark),
"prefer-light" => Some(egui::Theme::Light),
_ => None,
}),
Value::Value(inner) if depth < 4 => parse(inner, depth + 1),
_ => None,
}
}
#[cfg(not(all(unix, not(target_os = "macos"))))]
fn read() -> Option<egui::Theme> {
None
}
#[cfg(test)]
mod tests {
use super::*;
/// The standard key answers a number, GNOME's schema a string; either may come wrapped in
/// variants (the older `Read`). Only "dark" and "light" are a choice.
#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn the_portal_answer_reads_as_a_choice_or_as_none() {
use zbus::zvariant::Value;
assert_eq!(parse(&Value::U32(1), 0), Some(Some(egui::Theme::Dark)));
assert_eq!(parse(&Value::U32(2), 0), Some(Some(egui::Theme::Light)));
assert_eq!(parse(&Value::U32(0), 0), Some(None));
assert_eq!(parse(&Value::U32(7), 0), Some(None));
assert_eq!(parse(&Value::from("prefer-dark"), 0), Some(Some(egui::Theme::Dark)));
assert_eq!(parse(&Value::from("prefer-light"), 0), Some(Some(egui::Theme::Light)));
assert_eq!(parse(&Value::from("default"), 0), Some(None));
let wrapped = Value::Value(Box::new(Value::Value(Box::new(Value::U32(1)))));
assert_eq!(parse(&wrapped, 0), Some(Some(egui::Theme::Dark)));
assert_eq!(parse(&Value::Bool(true), 0), None);
let mut deep = Value::U32(1);
for _ in 0..10 {
deep = Value::Value(Box::new(deep));
}
assert_eq!(parse(&deep, 0), None, "nesting is bounded");
}
/// A watcher that was never started is inert rather than a panic.
#[test]
fn a_watcher_without_a_thread_is_inert() {
assert_eq!(DesktopTheme::default().current(), None);
}
/// Answers arrive as changes; "no preference" or silence keeps the last real answer.
#[test]
fn the_last_answer_stands_until_a_new_one() {
let (tx, rx) = channel();
let mut watcher = DesktopTheme { changes: Some(rx), current: None };
assert_eq!(watcher.current(), None);
tx.send(Some(egui::Theme::Light)).unwrap();
assert_eq!(watcher.current(), Some(egui::Theme::Light));
tx.send(Some(egui::Theme::Dark)).unwrap();
tx.send(None).unwrap();
assert_eq!(watcher.current(), Some(egui::Theme::Dark));
drop(tx);
assert_eq!(watcher.current(), Some(egui::Theme::Dark));
}
}
+47 -1
View File
@@ -15,11 +15,13 @@
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod control_server;
mod desktop_theme;
#[cfg(any(target_os = "windows", test))]
mod graphics;
mod logging;
#[cfg(target_os = "macos")]
mod native_menu;
mod spacemouse;
mod workspace;
use cadcraft_engine::Session;
@@ -27,6 +29,9 @@ use cadcraft_ui_egui::{CadApp, Services, i18n};
struct App {
cad: CadApp,
spacemouse: spacemouse::SpaceMouse,
/// The desktop's light/dark choice where winit reports none (Linux, the BSDs).
desktop_theme: desktop_theme::DesktopTheme,
workspace: workspace::Workspace,
workspace_error: Option<String>,
#[cfg(target_os = "macos")]
@@ -48,6 +53,14 @@ impl App {
}
}
/// View ▸ Theme ▸ System resolves against `raw.system_theme`, which winit leaves empty on Linux
/// and the BSDs: the desktop's answer, where it gives one, stands in for it.
fn follow_desktop(raw: &mut egui::RawInput, desktop: Option<egui::Theme>) {
if desktop.is_some() {
raw.system_theme = desktop;
}
}
impl eframe::App for App {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
#[cfg(target_os = "macos")]
@@ -60,12 +73,14 @@ impl eframe::App for App {
}
}
self.cad.logic(ctx);
self.spacemouse.poll(&mut self.cad, ctx);
self.persist_workspace(false);
if std::mem::take(&mut self.cad.quit_requested) {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
}
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
follow_desktop(raw, self.desktop_theme.current());
self.cad.raw_input_hook(raw);
cadcraft_ui_egui::cmdline::capture_tab(ctx, raw);
}
@@ -107,6 +122,7 @@ fn services() -> Services {
pick_open: Some(Box::new(|| {
rfd::FileDialog::new()
.add_filter(i18n::t("Drawings (DWG, DXF)"), &["dwg", "dxf", "dwt"])
.add_filter(i18n::t("SVG drawings (SVG, SVGZ)"), &["svg", "svgz"])
.add_filter(i18n::t("All files"), &["*"])
.pick_file()
.map(|p| p.to_string_lossy().to_string())
@@ -240,6 +256,17 @@ fn main() -> eframe::Result {
// Before eframe creates the wgpu instance: default Windows to DirectX 12 with FXC (see graphics.rs).
#[cfg(target_os = "windows")]
graphics::configure(&mut options, eframe::wgpu::Backends::from_env(), eframe::wgpu::Dx12Compiler::from_env());
// Command previews (COPY, MOVE...) of large selections are tessellated by egui, whose index
// buffer outgrew wgpu's default 256 MiB cap; the validation error is fatal. Ask for what the
// adapter actually supports.
if let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup {
let base = std::sync::Arc::clone(&create.device_descriptor);
create.device_descriptor = std::sync::Arc::new(move |adapter| {
let mut d = base(adapter);
d.required_limits.max_buffer_size = d.required_limits.max_buffer_size.max(adapter.limits().max_buffer_size);
d
});
}
// winit has no file drag-and-drop on Wayland (only on X11), so dropping files from the file
// manager showed a "no" cursor. Run through XWayland when it's there; CADCRAFT_WAYLAND=1
// keeps the native Wayland backend.
@@ -298,8 +325,12 @@ fn main() -> eframe::Result {
if app.session.docs.is_empty() {
app.session.new_drawing(false);
}
let ctx = cc.egui_ctx.clone();
let desktop_theme = desktop_theme::DesktopTheme::start(move || ctx.request_repaint());
Ok(Box::new(App {
desktop_theme,
cad: app,
spacemouse: spacemouse::SpaceMouse::start(cc.egui_ctx.clone()),
workspace,
workspace_error,
#[cfg(target_os = "macos")]
@@ -311,7 +342,7 @@ fn main() -> eframe::Result {
#[cfg(test)]
mod tests {
use super::{config_dir_from, control_port_from, filters_for_save};
use super::{config_dir_from, control_port_from, filters_for_save, follow_desktop};
use std::ffi::OsString;
use std::path::PathBuf;
@@ -377,4 +408,19 @@ mod tests {
Some(PathBuf::from("C:\\Users\\u\\AppData\\Roaming").join("CADCraft"))
);
}
/// View ▸ Theme ▸ System on Linux: winit reports no theme, so the desktop's answer stands in;
/// without one (or where winit answers and the desktop doesn't) the input is left alone.
#[test]
fn the_desktop_answers_for_the_system_theme() {
let mut raw = egui::RawInput::default();
follow_desktop(&mut raw, Some(egui::Theme::Light));
assert_eq!(raw.system_theme, Some(egui::Theme::Light));
let mut raw = egui::RawInput { system_theme: Some(egui::Theme::Dark), ..Default::default() };
follow_desktop(&mut raw, None);
assert_eq!(raw.system_theme, Some(egui::Theme::Dark));
let mut raw = egui::RawInput::default();
follow_desktop(&mut raw, None);
assert_eq!(raw.system_theme, None);
}
}
+3 -3
View File
@@ -55,7 +55,7 @@ impl NativeMenu {
items.insert("cc-settings".into(), ("options".into(), "Settings...".into(), settings));
let _ = menu.append(&app_menu);
for (title, entries) in menus::tree(app) {
let sub = Submenu::new(i18n::tr(language, &title), true);
let sub = Submenu::new(i18n::tr_menu(language, &title), true);
build(&sub, &entries, &mut items, &mut submenus, &mut counter, language);
let _ = menu.append(&sub);
submenus.push((title, sub));
@@ -81,7 +81,7 @@ impl NativeMenu {
item.set_text(i18n::tr(language, label));
}
for (label, submenu) in &self.submenus {
submenu.set_text(i18n::tr(language, label));
submenu.set_text(i18n::tr_menu(language, label));
}
}
let now = cadcraft_ui_egui::now_ms();
@@ -107,7 +107,7 @@ fn build(
for e in entries {
match e {
Entry::Sub { label, children } => {
let sub = Submenu::new(i18n::tr(language, label), true);
let sub = Submenu::new(i18n::tr_menu(language, label), true);
build(&sub, children, items, submenus, counter, language);
let _ = parent.append(&sub);
submenus.push((label.clone(), sub));
+242
View File
@@ -0,0 +1,242 @@
//! Native 3Dconnexion input. The reader publishes one latest sample, never an input queue;
//! only movement starting/stopping or status/buttons wake egui. Active motion runs at 60 Hz.
#[cfg(any(target_os = "windows", target_os = "linux"))]
mod motion;
use cadcraft_ui_egui::{CadApp, spacemouse::Status};
#[cfg(any(target_os = "windows", target_os = "linux"))]
use std::time::Duration;
use std::{
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Instant,
};
#[derive(Default)]
struct Shared {
status: Status,
device: Option<String>,
detail: Option<String>,
#[cfg(any(target_os = "windows", target_os = "linux"))]
motion: motion::Motion,
#[cfg(any(target_os = "windows", target_os = "linux"))]
last_report: Option<Instant>,
buttons: u8,
recalibrate: bool,
}
pub struct SpaceMouse {
shared: Arc<Mutex<Shared>>,
stop: Arc<AtomicBool>,
last_frame: Instant,
#[cfg(any(target_os = "windows", target_os = "linux"))]
moving: bool,
}
impl SpaceMouse {
pub fn start(ctx: egui::Context) -> Self {
let shared = Arc::new(Mutex::new(Shared::default()));
let stop = Arc::new(AtomicBool::new(false));
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
let (input, quit, context) = (Arc::clone(&shared), Arc::clone(&stop), ctx.clone());
if let Err(error) = std::thread::Builder::new().name("spacemouse-hid".into()).spawn(move || read(input, quit, context)) {
set_status(&shared, &ctx, Status::Error, None, Some(error.to_string()));
}
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
let _ = ctx;
Self {
shared,
stop,
last_frame: Instant::now(),
#[cfg(any(target_os = "windows", target_os = "linux"))]
moving: false,
}
}
pub fn poll(&mut self, app: &mut CadApp, ctx: &egui::Context) {
let now = Instant::now();
let elapsed = now.saturating_duration_since(self.last_frame).as_secs_f64().min(0.05);
self.last_frame = now;
let Ok(mut shared) = self.shared.lock() else { return };
if std::mem::take(&mut app.spacemouse.calibrate_requested) {
shared.recalibrate = true;
}
let changed = app.spacemouse.status != shared.status || app.spacemouse.device != shared.device || app.spacemouse.detail != shared.detail;
app.spacemouse.status = shared.status;
app.spacemouse.device.clone_from(&shared.device);
app.spacemouse.detail.clone_from(&shared.detail);
if changed && !matches!(shared.status, Status::Unavailable | Status::Searching) {
app.set_status(cadcraft_ui_egui::i18n::tr(app.language(), shared.status.message()));
}
let buttons = std::mem::take(&mut shared.buttons);
#[cfg(any(target_os = "windows", target_os = "linux"))]
let motion = if shared.last_report.is_some_and(|at| now.saturating_duration_since(at) < Duration::from_millis(250)) {
shared.motion
} else {
motion::Motion::default()
};
drop(shared);
let allowed = app.spacemouse.settings.enabled
&& ctx.input(|i| i.focused)
&& !app.ui.start_tab
&& app.ui.dialog.is_none()
&& app.closing.is_none()
&& app.canvas.rect.is_some()
&& !egui::Popup::is_any_open(ctx);
if allowed && buttons & 1 != 0 {
let _ = app.run("zoom", serde_json::json!({"mode": "extents"}));
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
let moving = allowed && motion.active();
if moving {
if self.moving {
let settings = app.spacemouse.settings;
let rate = if settings.reverse_zoom { -settings.zoom_speed } else { settings.zoom_speed };
if let Err(error) = app.run(
"ui.spacemouse.navigate",
serde_json::json!({
"pan": motion.pan.map(|v| v * settings.pan_speed * elapsed),
"zoom": (motion.zoom * rate * elapsed).exp()
}),
) {
app.set_status(error);
}
}
ctx.request_repaint_after(Duration::from_millis(16));
}
self.moving = moving;
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
let _ = elapsed;
}
}
impl Drop for SpaceMouse {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
}
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn set_status(shared: &Mutex<Shared>, ctx: &egui::Context, status: Status, device: Option<String>, detail: Option<String>) {
let Ok(mut state) = shared.lock() else { return };
let changed = state.status != status || state.device != device || state.detail != detail;
if !changed {
return;
}
if let Some(error) = &detail {
log::warn!("3D mouse: {error}");
}
state.status = status;
state.device = device;
state.detail = detail;
state.motion = motion::Motion::default();
state.last_report = None;
state.buttons = 0;
drop(state);
ctx.request_repaint();
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn retry(stop: &AtomicBool) {
for _ in 0..40 {
if stop.load(Ordering::Relaxed) {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn read(shared: Arc<Mutex<Shared>>, stop: Arc<AtomicBool>, ctx: egui::Context) {
while !stop.load(Ordering::Relaxed) {
let mut api = match hidapi::HidApi::new() {
Ok(api) => api,
Err(error) => {
set_status(&shared, &ctx, Status::Error, None, Some(error.to_string()));
retry(&stop);
continue;
}
};
while !stop.load(Ordering::Relaxed) {
if let Err(error) = api.refresh_devices() {
set_status(&shared, &ctx, Status::Error, None, Some(error.to_string()));
break;
}
let device =
api.device_list().find(|device| matches!(device.vendor_id(), 0x046d | 0x256f) && device.usage_page() == 1 && device.usage() == 8);
let Some(info) = device else {
set_status(&shared, &ctx, Status::Searching, None, None);
retry(&stop);
continue;
};
let name = info.product_string().unwrap_or("3Dconnexion SpaceMouse").to_string();
match info.open_device(&api) {
Ok(device) => {
set_status(&shared, &ctx, Status::Calibrating, Some(name.clone()), None);
if let Err(error) = read_device(&device, &shared, &stop, &ctx, &name) {
set_status(&shared, &ctx, Status::Disconnected, Some(name), Some(error.to_string()));
}
}
Err(error) => set_status(&shared, &ctx, Status::OpenFailed, Some(name), Some(error.to_string())),
}
retry(&stop);
}
retry(&stop);
}
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn read_device(device: &hidapi::HidDevice, shared: &Mutex<Shared>, stop: &AtomicBool, ctx: &egui::Context, name: &str) -> hidapi::HidResult<()> {
let mut filter = motion::Filter::default();
let mut buffer = [0; 64];
let mut buttons = 0;
while !stop.load(Ordering::Relaxed) {
let recalibrate = if let Ok(mut state) = shared.lock() { std::mem::take(&mut state.recalibrate) } else { return Ok(()) };
if recalibrate {
filter.recalibrate();
set_status(shared, ctx, Status::Calibrating, Some(name.into()), None);
}
let len = device.read_timeout(&mut buffer, 50)?;
let now = Instant::now();
let report = buffer.get(..len).and_then(motion::decode);
let Ok(mut state) = shared.lock() else { return Ok(()) };
let mut wake = false;
match report {
Some(motion::Report::Buttons(next)) => {
let pressed = next & !buttons;
buttons = next;
state.buttons |= pressed & 1;
state.recalibrate |= pressed & 2 != 0;
wake = pressed != 0;
}
Some(motion::Report::Axes { values, mask }) => {
let motion = filter.update(values, mask, now);
wake = state.motion.active() != motion.active();
state.motion = motion;
state.last_report = Some(now);
if filter.calibrated() && state.status != Status::Connected {
state.status = Status::Connected;
wake = true;
}
}
None => {
if state.motion.active() && state.last_report.is_some_and(|at| now.saturating_duration_since(at) >= Duration::from_millis(250)) {
state.motion = motion::Motion::default();
wake = true;
}
}
}
drop(state);
if wake {
ctx.request_repaint();
}
}
Ok(())
}
+158
View File
@@ -0,0 +1,158 @@
//! Bounded, dependency-free decoding and rest calibration of 3Dconnexion HID input reports.
use std::time::{Duration, Instant};
const FULL_SCALE: f64 = 500.0;
const DEADZONE: f64 = 0.04;
const REST_LIMIT: f64 = 60.0;
const REST_RANGE: f64 = 10.0;
const CALIBRATION_TIME: Duration = Duration::from_millis(400);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Report {
/// Mask 1 = translation, 2 = rotation, 3 = a combined six-axis report.
Axes {
values: [i16; 6],
mask: u8,
},
Buttons(u8),
}
/// hidapi includes the report ID. A legacy motion report has six payload bytes; combined
/// report 1 has twelve. Windows may pad the short button report to the input-report size.
pub fn decode(bytes: &[u8]) -> Option<Report> {
let (&id, payload) = bytes.split_first()?;
if id == 3 {
return payload.first().copied().map(Report::Buttons);
}
let (mask, offset, count) = match id {
1 if payload.len() >= 12 => (3, 0, 6),
1 if payload.len() >= 6 => (1, 0, 3),
2 if payload.len() >= 6 => (2, 3, 3),
_ => return None,
};
let mut values = [0; 6];
for (out, pair) in values.iter_mut().skip(offset).take(count).zip(payload.as_chunks::<2>().0.iter()) {
*out = i16::from_le_bytes([*pair.first()?, *pair.get(1)?]);
}
Some(Report::Axes { values, mask })
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Motion {
pub pan: [f64; 2],
pub zoom: f64,
}
impl Motion {
pub fn active(self) -> bool {
self.pan.iter().any(|v| *v != 0.0) || self.zoom != 0.0
}
}
#[derive(Default)]
struct Rest {
since: Option<Instant>,
count: u32,
sum: [f64; 6],
min: [f64; 6],
max: [f64; 6],
}
impl Rest {
fn observe(&mut self, raw: [f64; 6], now: Instant) -> Option<[f64; 6]> {
if raw.iter().any(|v| v.abs() > REST_LIMIT) {
*self = Self::default();
return None;
}
if self.since.is_none() || raw.iter().zip(self.min.iter().zip(&self.max)).any(|(v, (min, max))| v.max(*max) - v.min(*min) > REST_RANGE) {
*self = Self { since: Some(now), count: 1, sum: raw, min: raw, max: raw };
return None;
}
for ((min, max), value) in self.min.iter_mut().zip(&mut self.max).zip(raw) {
*min = min.min(value);
*max = max.max(value);
}
// A fixed bound even if a device sends far more than the expected 125 reports/sec.
if self.count < 128 {
self.count += 1;
for (sum, value) in self.sum.iter_mut().zip(raw) {
*sum += value;
}
}
if self.count >= 16 && self.since.is_some_and(|since| now.saturating_duration_since(since) >= CALIBRATION_TIME) {
Some(self.sum.map(|sum| sum / f64::from(self.count)))
} else {
None
}
}
}
#[derive(Default)]
pub struct Filter {
raw: [f64; 6],
bias: Option<[f64; 6]>,
rest: Rest,
parts: u8,
seen: [Option<Instant>; 2],
idle_since: Option<Instant>,
}
impl Filter {
pub fn calibrated(&self) -> bool {
self.bias.is_some()
}
pub fn recalibrate(&mut self) {
*self = Self::default();
}
pub fn update(&mut self, values: [i16; 6], mask: u8, now: Instant) -> Motion {
for (index, seen) in self.seen.iter_mut().enumerate() {
if mask & (1 << index) != 0 {
*seen = Some(now);
}
}
for (index, (raw, value)) in self.raw.iter_mut().zip(values).enumerate() {
if mask & if index < 3 { 1 } else { 2 } != 0 {
*raw = f64::from(value);
}
}
self.parts |= mask & 3;
let complete = self.parts == 3;
if complete {
self.parts = 0;
}
if self.bias.is_none() && complete {
self.bias = self.rest.observe(self.raw, now);
}
let Some(mut bias) = self.bias else { return Motion::default() };
// Follow small resting drift only after two seconds entirely inside the deadzone.
if complete {
if self.raw.iter().zip(bias).all(|(raw, bias)| (*raw - bias).abs() <= FULL_SCALE * DEADZONE) {
let since = *self.idle_since.get_or_insert(now);
if now.saturating_duration_since(since) >= Duration::from_secs(2) {
for (bias, raw) in bias.iter_mut().zip(self.raw) {
*bias += (raw - *bias) * 0.01;
}
self.bias = Some(bias);
}
} else {
self.idle_since = None;
}
}
let mut axes = [0.0; 6];
for ((axis, raw), bias) in axes.iter_mut().zip(self.raw).zip(bias) {
let value = ((raw - bias) / FULL_SCALE).clamp(-1.0, 1.0);
*axis = if value.abs() <= DEADZONE { 0.0 } else { value.signum() * (value.abs() - DEADZONE) / (1.0 - DEADZONE) };
}
// A missing rotation report must not keep zooming while translations still arrive.
for (group, seen) in axes.as_chunks_mut::<3>().0.iter_mut().zip(self.seen) {
if seen.is_none_or(|at| now.saturating_duration_since(at) >= Duration::from_millis(250)) {
group.fill(0.0);
}
}
let [tx, ty, tz, _rx, _ry, rz] = axes;
Motion { pan: [tx, ty], zoom: (tz + rz).clamp(-1.0, 1.0) }
}
}
+11
View File
@@ -22,6 +22,14 @@ real_name = "Brandon Thomas"
display_name = "Brandon Thomas"
sync_github_name = false
[people.switch87]
real_name = "Gert Pellin"
display_name = "Gert Pellin"
sync_github_name = false
[people.rangoDJ]
sync_github_name = true
[people.Tru10]
display_name = "by Truitt"
sync_github_name = false
@@ -37,3 +45,6 @@ sync_github_name = true
[people.WladmirJunior]
real_name = "Wladmir Júnior"
sync_github_name = true
[people.Losminek]
sync_github_name = true
+68 -21
View File
@@ -153,9 +153,11 @@ pub fn standard_patterns() -> Vec<HatchPattern> {
name: "BRICK",
description: "Brick or masonry-type surface",
lines: vec![
// Running bond: courses 0.25 high, bricks 0.5 long. Each vertical family draws the
// joints of every other course, 0.5 apart; the second is offset half a brick.
pl(0.0, (0.0, 0.0), (0.0, 0.25), &[]),
pl(90.0, (0.0, 0.0), (0.25, 0.25), &[0.25, -0.25]),
pl(90.0, (0.25, 0.0), (0.25, 0.25), &[-0.25, 0.25]),
pl(90.0, (0.0, 0.0), (0.0, 0.5), &[0.25, -0.25]),
pl(90.0, (0.25, 0.0), (0.0, 0.5), &[-0.25, 0.25]),
],
},
HatchPattern {
@@ -203,28 +205,73 @@ pub fn standard_patterns() -> Vec<HatchPattern> {
description: "Staircase effect",
lines: vec![pl(0.0, (0.0, 0.0), (0.125, 0.125), &[0.125, -0.125]), pl(90.0, (0.125, 0.0), (0.125, 0.125), &[0.125, -0.125])],
},
HatchPattern {
name: "AR-CONC",
description: "Random dot and stone pattern",
lines: vec![
pl(50.0, (0.0, 0.0), (0.4, 0.25), &[0.15, -0.6]),
pl(355.0, (0.0, 0.0), (0.27, 0.3), &[0.12, -0.5]),
pl(100.0, (0.12, 0.05), (0.35, 0.33), &[0.1, -0.55]),
pl(0.0, (0.07, 0.02), (0.21, 0.17), &[0.0, -0.4]),
],
},
HatchPattern {
name: "AR-SAND",
description: "Random dot pattern",
lines: vec![
pl(37.5, (0.0, 0.0), (0.112, 0.0912), &[0.0, -0.22, 0.0, -0.14]),
pl(7.5, (0.0, 0.0), (0.157, 0.0724), &[0.0, -0.19, 0.0, -0.31]),
pl(-32.5, (0.0, 0.0), (0.091, 0.1312), &[0.0, -0.27, 0.0, -0.11]),
],
},
// Drawn at the scale of the pattern CAD programs ship under this name (about 16x
// coarser than first drawn here: AR-CONC hatches came out hundreds of times too dense).
scaled_pattern(
HatchPattern {
name: "AR-CONC",
description: "Random dot and stone pattern",
lines: vec![
pl(50.0, (0.0, 0.0), (0.4, 0.25), &[0.15, -0.6]),
pl(355.0, (0.0, 0.0), (0.27, 0.3), &[0.12, -0.5]),
pl(100.0, (0.12, 0.05), (0.35, 0.33), &[0.1, -0.55]),
pl(0.0, (0.07, 0.02), (0.21, 0.17), &[0.0, -0.4]),
],
},
16.0,
),
// Drawn at the scale of the pattern CAD programs ship under this name (about 16x
// coarser than first drawn here: AR-CONC hatches came out hundreds of times too dense).
scaled_pattern(
HatchPattern {
name: "AR-SAND",
description: "Random dot pattern",
lines: vec![
pl(37.5, (0.0, 0.0), (0.112, 0.0912), &[0.0, -0.22, 0.0, -0.14]),
pl(7.5, (0.0, 0.0), (0.157, 0.0724), &[0.0, -0.19, 0.0, -0.31]),
pl(-32.5, (0.0, 0.0), (0.091, 0.1312), &[0.0, -0.27, 0.0, -0.11]),
],
},
16.0,
),
]
}
pub fn pattern(name: &str) -> Option<HatchPattern> {
standard_patterns().into_iter().find(|p| p.name.eq_ignore_ascii_case(name))
}
/// The library pattern `name` as drawn in `d`. In a metric drawing (`MEASUREMENT` 1) every
/// length is multiplied by 25.4, as CAD programs use ISO-scaled pattern definitions there (as
/// for linetypes, see [`standard_linetypes_for`]): a hatch at scale 1 measures in millimetres
/// what it measures in inches in an imperial drawing.
pub fn pattern_for(d: &Drawing, name: &str) -> Option<HatchPattern> {
let p = pattern(name)?;
Some(if d.header.i64("MEASUREMENT", 0) == 1 { scaled_pattern(p, 25.4) } else { p })
}
/// `p` with every length (origins, offsets, dashes) multiplied by `k`.
fn scaled_pattern(mut p: HatchPattern, k: f64) -> HatchPattern {
for l in &mut p.lines {
l.origin = (l.origin.0 * k, l.origin.1 * k);
l.delta = (l.delta.0 * k, l.delta.1 * k);
for d in &mut l.dashes {
*d *= k;
}
}
p
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metric_drawings_draw_iso_scaled_patterns() {
let imp = pattern_for(&Drawing::new_imperial(), "ANSI31").map(|p| p.lines[0].delta.1);
let met = pattern_for(&Drawing::new_metric(), "ansi31").map(|p| p.lines[0].delta.1);
assert_eq!(imp, Some(0.125));
assert!(met.is_some_and(|m| (m - 0.125 * 25.4).abs() < 1e-12), "{met:?}");
assert!(pattern_for(&Drawing::new_metric(), "NOPE").is_none());
}
}
+21 -1
View File
@@ -162,6 +162,12 @@ impl Autosaver {
/// `dir`: write drawings changed `minutes` ago or earlier and not autosaved since, delete the
/// files of drawings that were saved (no unsaved changes left) or closed.
pub fn plan(&mut self, s: &Session, now: f64, minutes: u32, dir: &str) -> Vec<Job> {
self.plan_with(s, now, minutes, dir, false)
}
/// [`Autosaver::plan`], with `urgent` writing every drawing with changes not yet autosaved
/// now instead of `minutes` after the change (autosave still off when `minutes` is 0).
fn plan_with(&mut self, s: &Session, now: f64, minutes: u32, dir: &str, urgent: bool) -> Vec<Job> {
let mut jobs = Vec::new();
let live: HashSet<u64> = s.docs.iter().map(|d| d.uid).collect();
let busy = &self.busy;
@@ -193,7 +199,7 @@ impl Autosaver {
continue;
}
let since = *t.since.get_or_insert(now);
if minutes == 0 || dir.trim().is_empty() || now - since < f64::from(minutes) * 60.0 {
if minutes == 0 || dir.trim().is_empty() || (!urgent && now - since < f64::from(minutes) * 60.0) {
continue;
}
let path = t
@@ -351,6 +357,20 @@ mod native {
self.next_due(now, minutes).map(Duration::from_secs_f64)
}
/// Autosave every drawing with changes not yet autosaved right away, without waiting for
/// SAVETIME (the app may not survive what just happened, e.g. a GPU error). Returns how
/// many drawings are being written.
pub fn save_now(&mut self, s: &Session) -> usize {
let now = self.start.elapsed().as_secs_f64();
let jobs = self.plan_with(s, now, savetime(), &savefilepath(), true);
let writes = jobs.iter().filter(|j| matches!(j, Job::Write { .. })).count();
self.perform(jobs);
// The lock file marks the files as this running app's (Drawing Recovery skips them).
self.last_beat = None;
self.heartbeat(&savefilepath(), now);
writes
}
/// Carry out jobs from [`Autosaver::plan`]: writes on a background thread, deletes now.
pub fn perform(&mut self, jobs: Vec<Job>) {
for job in jobs {
+3
View File
@@ -447,6 +447,9 @@ impl Interactive for ArrayM {
fn preview(&self, s: &Session, cursor: Vec2) -> Vec<EntityKind> {
self.inner.as_ref().map(|m| m.preview(s, cursor)).unwrap_or_default()
}
fn drag(&self, s: &Session, cursor: Vec2) -> Option<crate::prompt::Drag> {
self.inner.as_ref().and_then(|m| m.drag(s, cursor))
}
}
// ---------------- ARRAYRECT ----------------
+28 -6
View File
@@ -1,12 +1,12 @@
//! PURGE / -PURGE: remove unused named objects (blocks, layers, linetypes, styles, groups) and
//! zero-length geometry or empty text.
//! PURGE / -PURGE: remove unused named objects (blocks, layers, linetypes, styles, multiline
//! styles, groups) and zero-length geometry or empty text.
//!
//! The JSON form takes `{type?, names?}` and purges without asking. Typed, it asks for the type,
//! the names and whether to verify each one, as AutoCAD's -PURGE does.
use std::collections::{HashSet, VecDeque};
use cadcraft_doc::{Drawing, EntityKind, Handle};
use cadcraft_doc::{Drawing, EntityKind, Handle, STANDARD_MLINE_STYLE};
use serde_json::{Map, Value, json};
use super::super::qselect::wildcard;
@@ -21,6 +21,7 @@ enum Kind {
Layers,
Linetypes,
MleaderStyles,
MlineStyles,
TableStyles,
TextStyles,
ZeroLength,
@@ -43,7 +44,7 @@ const KINDS: &[(&str, &[&str], Kind)] = &[
("Plotstyles", &["plotstyles"], Kind::Absent),
("SHapes", &["shapes"], Kind::Absent),
("textSTyles", &["textstyles", "textstyle", "styles", "style"], Kind::TextStyles),
("Mlinestyles", &["mlinestyles"], Kind::Absent),
("Mlinestyles", &["mlinestyles", "mlinestyle"], Kind::MlineStyles),
("SEctionviewstyles", &["sectionviewstyles"], Kind::Absent),
("Tablestyles", &["tablestyles", "tablestyle"], Kind::TableStyles),
("Visualstyles", &["visualstyles"], Kind::Absent),
@@ -56,8 +57,17 @@ const KINDS: &[(&str, &[&str], Kind)] = &[
/// The named kinds `All` purges, in an order where purging one frees the next (blocks free
/// layers, layers free linetypes, dimension styles free text styles).
const ALL: &[Kind] =
&[Kind::Blocks, Kind::Groups, Kind::Dimstyles, Kind::MleaderStyles, Kind::TableStyles, Kind::Layers, Kind::Linetypes, Kind::TextStyles];
const ALL: &[Kind] = &[
Kind::Blocks,
Kind::Groups,
Kind::Dimstyles,
Kind::MleaderStyles,
Kind::MlineStyles,
Kind::TableStyles,
Kind::Layers,
Kind::Linetypes,
Kind::TextStyles,
];
fn norm(s: &str) -> String {
s.chars().filter(char::is_ascii_alphanumeric).collect::<String>().to_ascii_lowercase()
@@ -77,6 +87,7 @@ fn noun(kind: Kind) -> &'static str {
Kind::Layers => "layer",
Kind::Linetypes => "linetype",
Kind::MleaderStyles => "multileader style",
Kind::MlineStyles => "multiline style",
Kind::TableStyles => "table style",
Kind::TextStyles => "text style",
Kind::ZeroLength => "zero-length object",
@@ -93,6 +104,7 @@ fn json_key(kind: Kind) -> &'static str {
Kind::Layers => "layers",
Kind::Linetypes => "linetypes",
Kind::MleaderStyles => "mleaderstyles",
Kind::MlineStyles => "mlinestyles",
Kind::TableStyles => "tablestyles",
Kind::TextStyles => "textstyles",
Kind::ZeroLength => "zeroLength",
@@ -198,6 +210,15 @@ fn candidates(d: &Drawing, kind: Kind) -> Vec<String> {
.map(|t| t.name.clone())
.collect()
}
Kind::MlineStyles => {
let used = lower(&mut entities(d).filter_map(|e| if let EntityKind::MLine(m) = &e.kind { Some(m.style.clone()) } else { None }));
let cur = d.header.str("CMLSTYLE", STANDARD_MLINE_STYLE);
d.mline_styles
.iter()
.filter(|t| !is(&t.name, &[STANDARD_MLINE_STYLE, &cur]) && !used.contains(&t.name.to_ascii_lowercase()))
.map(|t| t.name.clone())
.collect()
}
Kind::TableStyles => {
let used = lower(&mut entities(d).filter_map(|e| if let EntityKind::Table(t) = &e.kind { Some(t.style.clone()) } else { None }));
let cur = d.header.str("CTABLESTYLE", "Standard");
@@ -238,6 +259,7 @@ fn purge_one(s: &mut Session, kind: Kind, name: &str) -> Result<bool> {
Kind::Linetypes => retain(&mut d.linetypes, |t| &t.name, name),
Kind::Dimstyles => retain(&mut d.dim_styles, |t| &t.name, name),
Kind::MleaderStyles => retain(&mut d.mleader_styles, |t| &t.name, name),
Kind::MlineStyles => retain(&mut d.mline_styles, |t| &t.name, name),
Kind::TableStyles => retain(&mut d.table_styles, |t| &t.name, name),
Kind::Groups => retain(&mut d.groups, |g| &g.name, name),
Kind::ZeroLength | Kind::EmptyText | Kind::Absent | Kind::All => false,
+6
View File
@@ -609,6 +609,12 @@ impl Interactive for PasteM {
_ => Ok(Step::Continue),
}
}
fn drag(&self, s: &Session, c: Vec2) -> Option<crate::prompt::Drag> {
if self.mode == PasteMode::Orig || s.clipboard.is_empty() {
return None;
}
Some(crate::prompt::Drag { handles: Vec::new(), matrix: Mat3::translate(c - s.clipboard_base), extra: Vec::new(), clipboard: true })
}
fn preview(&self, s: &Session, c: Vec2) -> Vec<cadcraft_doc::EntityKind> {
let m = Mat3::translate(c - s.clipboard_base);
s.clipboard
+114 -1
View File
@@ -12,7 +12,7 @@ pub use cadcraft_dxf::Version;
use serde_json::{Value, json};
use super::*;
use crate::{Result, Session};
use crate::{Accept, Input, Prompt, Result, Session, Step};
pub(crate) use pgp::{specs as alias_specs, user_alias};
pub use template::{qnew_template, set_qnew_template};
@@ -153,9 +153,69 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("document.bytes", "Drawing as bytes", run_bytes)
.params("{format?: dxf, version?: 2000…2018, binary?: bool} → {data: base64}")
.noundo(),
CommandSpec::new("script", "Run Script...", run_script)
.menu(&["Tools", "Run Script..."])
.alias(&["scr"])
.params("{path: script (.scr) file} | {data: base64} → {lines}")
.noundo()
.interactive(|_| Ok(Box::new(ScriptM))),
]
}
/// SCRIPT: run the lines of a `.scr` file through [`Session::script`] — the same execution the
/// CLI, MCP and the control channel already use, now reachable from the command line and menus.
fn run_script(s: &mut Session, p: &Value) -> Result<Value> {
let text = if let Some(_path) = str_param(p, "path") {
#[cfg(not(target_arch = "wasm32"))]
let path = _path;
#[cfg(not(target_arch = "wasm32"))]
{
let bytes = std::fs::read(path).map_err(|e| bad("script", format!("{path}: {e}")))?;
String::from_utf8_lossy(&bytes).into_owned()
}
#[cfg(target_arch = "wasm32")]
return Err(bad("script", "paths are not available on the web; pass `data`"));
} else {
let data = str_param(p, "data").ok_or_else(|| bad("script", "`path` or `data` (base64) is required"))?;
let bytes = base64_decode(data).ok_or_else(|| bad("script", "invalid base64"))?;
String::from_utf8_lossy(&bytes).into_owned()
};
let lines = text.lines().count();
s.script(&text)?;
Ok(json!({ "lines": lines }))
}
/// SCRIPT typed at the command line or chosen from the menu: asks for the script file by name
/// (the prompt form; the menu item runs this same machine).
struct ScriptM;
impl Interactive for ScriptM {
fn name(&self) -> &'static str {
"SCRIPT"
}
fn prompt(&self, _: &Session) -> Prompt {
Prompt::new("Enter name of script file", Accept::TEXT)
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if matches!(i, Input::Cancel | Input::Enter) {
return Ok(Step::Cancel);
}
let Input::Text(t) = i else { return Ok(Step::Continue) };
let t = t.trim();
if t.is_empty() {
return Ok(Step::Cancel);
}
match run_script(s, &json!({ "path": t })) {
Ok(_) => Ok(Step::Done),
Err(EngineError::BadParams { msg, .. }) => {
s.echo(msg);
Ok(Step::Done)
}
Err(e) => Err(e),
}
}
}
fn run_new(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(r) = template::run_new(s, p)? {
return Ok(r);
@@ -171,6 +231,9 @@ fn file_name(path: &str) -> String {
pub(crate) fn open_bytes(s: &mut Session, bytes: &[u8], name: &str, path: Option<String>) -> Result<usize> {
let hooks = io().ok_or_else(|| bad("open", "file formats are not available in this build"))?;
let d = (hooks.read)(bytes, name).map_err(|e| bad("open", e))?;
// A file that isn't a drawing (an imported SVG) gives the drawing no file name: QSAVE then
// asks where to save instead of exporting over the original.
let path = path.filter(|p| is_drawing_file(p));
let i = s.open_drawing(d, &file_name(name), path);
if let Some(st) = s.docs.get_mut(i) {
// QSAVE keeps a binary DXF binary.
@@ -462,6 +525,56 @@ mod tests {
s.state().unwrap().title.clone()
}
fn temp_script(name: &str, text: &str) -> String {
let path = std::env::temp_dir().join(format!("cadcraft-script-{}-{name}", std::process::id()));
std::fs::write(&path, text).unwrap();
path.to_string_lossy().into_owned()
}
#[test]
fn script_command_runs_a_scr_file() {
let path = temp_script("run.scr", "; a comment\ncircle 50,25 20\nrectang 0,0 100,60\n");
let mut s = Session::new();
let r = s.execute("script", &json!({ "path": path })).unwrap();
assert_eq!(r["lines"], 3);
assert_eq!(s.state().unwrap().doc.model.len(), 2, "the comment is skipped, circle and rectang draw");
}
#[test]
fn script_command_reports_a_missing_file() {
let mut s = Session::new();
let e = s.execute("script", &json!({ "path": "no/such/script.scr" })).unwrap_err().to_string();
assert!(e.contains("no/such/script.scr"), "{e}");
assert_eq!(s.state().unwrap().doc.model.len(), 0);
}
/// A script that runs itself must be refused, not recurse until the stack dies.
#[test]
fn a_script_cannot_start_a_script() {
let inner = temp_script("inner.scr", "circle 0,0 1\n");
let outer = temp_script("outer.scr", &format!("script {}\ncircle 0,0 2\n", json!({ "path": inner })));
let mut s = Session::new();
let e = s.execute("script", &json!({ "path": outer })).unwrap_err().to_string();
assert!(e.contains("already running"), "{e}");
}
#[test]
fn typed_script_prompts_for_the_file_then_runs_or_reports_it() {
let good = temp_script("typed.scr", "circle 0,0 1\n");
let mut s = Session::new();
s.cmdline("script").unwrap();
assert!(s.running.is_some(), "SCRIPT asks for the file name");
s.cmdline(&good).unwrap();
assert!(s.running.is_none());
assert_eq!(s.state().unwrap().doc.model.len(), 1);
// A file that cannot be read: the error is echoed, the command still ends.
s.cmdline("script").unwrap();
s.cmdline("no/such/script.scr").unwrap();
assert!(s.running.is_none());
assert!(s.log.iter().any(|l| l.contains("no/such/script.scr")), "{:?}", s.log);
}
#[test]
fn closing_earlier_inactive_drawing_keeps_active_drawing() {
let mut s = Session::new();
+164 -7
View File
@@ -1,6 +1,7 @@
//! HATCH, GRADIENT, BOUNDARY, REGION: boundaries from pick points or selected closed objects.
mod patfile;
pub use patfile::{PatternChoice, pattern_choices};
pub(super) use patfile::{lines_for, run_patterns};
use cadcraft_color::Color;
@@ -10,7 +11,7 @@ use serde_json::{Value, json};
use super::helpers::lwpoly;
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
use crate::{Accept, EngineError, Input, Interactive, Picker, Prompt, Result, Session, Step};
mod assoc;
mod edit;
@@ -355,9 +356,63 @@ fn make(s: &mut Session, p: &Value, gradient: bool) -> Result<Value> {
Ok(json!({ "handle": hex.first(), "handles": hex }))
}
/// True when two loops have the same vertex sequence (ignoring cyclic shift and winding order).
fn same_loop(a: &HatchLoop, b: &HatchLoop) -> bool {
if a.vertices.len() != b.vertices.len() {
return false;
}
let n = a.vertices.len();
if n == 0 {
return true;
}
let near = |v1: &PolyVertex, v2: &PolyVertex| v1.p.near(v2.p, 1e-6) && (v1.bulge - v2.bulge).abs() < 1e-6;
(0..n).any(|k| (0..n).all(|i| near(&a.vertices[i], &b.vertices[(i + k) % n])))
|| (0..n).any(|k| {
(0..n).all(|i| {
let j = (k + n - (i % n)) % n;
let prev_j = (j + n - 1) % n;
a.vertices[i].p.near(b.vertices[j].p, 1e-6) && (a.vertices[i].bulge + b.vertices[prev_j].bulge).abs() < 1e-6
})
})
}
/// Deduplicate boundary loops that describe the same boundary.
fn dedup_loops(loops: &mut Vec<HatchLoop>) {
let mut unique: Vec<HatchLoop> = Vec::with_capacity(loops.len());
for l in loops.drain(..) {
if let Some(existing) = unique.iter_mut().find(|u| same_loop(u, &l)) {
for s in l.sources {
if !existing.sources.contains(&s) {
existing.sources.push(s);
}
}
existing.outer = existing.outer || l.outer;
} else {
unique.push(l);
}
}
*loops = unique;
}
/// Add the hatches for groups of loops: one per group when separate, otherwise one for all.
fn add_hatches(s: &mut Session, st: &HatchSettings, groups: Vec<Vec<HatchLoop>>) -> Result<Vec<Handle>> {
let groups: Vec<Vec<HatchLoop>> = if st.separate { groups } else { vec![groups.into_iter().flatten().collect()] };
let groups: Vec<Vec<HatchLoop>> = if st.separate {
let mut unique_groups: Vec<Vec<HatchLoop>> = Vec::new();
for mut g in groups {
dedup_loops(&mut g);
if g.is_empty() {
continue;
}
if !unique_groups.iter().any(|u| u.len() == g.len() && u.iter().zip(&g).all(|(a, b)| same_loop(a, b))) {
unique_groups.push(g);
}
}
unique_groups
} else {
let mut all: Vec<HatchLoop> = groups.into_iter().flatten().collect();
dedup_loops(&mut all);
vec![all]
};
let mut out = Vec::new();
for loops in groups.into_iter().filter(|g| !g.is_empty()) {
let h = s.add_entity(st.hatch(loops))?;
@@ -528,7 +583,11 @@ impl Interactive for HatchM {
fn prompt(&self, s: &Session) -> Prompt {
// The sub-prompts offer the current value: Enter keeps it.
match self.asking {
Some(HatchAsk::Pattern) => return Prompt::new("Enter a pattern name", Accept::TEXT).default(self.settings.pattern.clone()),
Some(HatchAsk::Pattern) => {
return Prompt::new("Enter a pattern name", Accept::TEXT)
.default(self.settings.pattern.clone())
.picker(Picker::HatchPattern(self.settings.pattern.clone()));
}
Some(HatchAsk::Scale) => {
return Prompt::new("Specify a scale for the hatch pattern", Accept::NUMBER).default(format!("{:.4}", self.settings.scale));
}
@@ -542,8 +601,10 @@ impl Interactive for HatchM {
Some(HatchAsk::Origin) => return Prompt::new("Specify the pattern origin", Accept::POINT).kw(&ORIGIN_KW).default("Use current"),
None => {}
}
// The pattern picker stays offered while boundaries are picked: a choice sets the pattern.
let picker = |p: Prompt| if self.gradient { p } else { p.picker(Picker::HatchPattern(self.settings.pattern.clone())) };
if self.selecting {
return Prompt::new("Select objects", Accept::SELECT).kw(&[
return picker(Prompt::new("Select objects", Accept::SELECT).kw(&[
"picK internal point",
"Undo",
"seTtings",
@@ -551,9 +612,9 @@ impl Interactive for HatchM {
"Angle",
"Origin",
"sEparate",
]);
]));
}
Prompt::new("Pick internal point", Accept::POINT).kw(&[
picker(Prompt::new("Pick internal point", Accept::POINT).kw(&[
"Select objects",
"Undo",
"seTtings",
@@ -562,7 +623,7 @@ impl Interactive for HatchM {
"Angle",
"Origin",
"sEparate",
])
]))
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if let Some(a) = self.asking.take() {
@@ -616,6 +677,11 @@ impl Interactive for HatchM {
}
Ok(Step::Continue)
}
// A pattern chosen in the picker while boundaries are picked.
Input::Text(t) if !self.gradient => {
self.answer(s, HatchAsk::Pattern, &t)?;
Ok(Step::Continue)
}
// Enter ends boundary picking in either mode and creates the hatch.
Input::Enter => self.finish(s),
_ => Ok(Step::Continue),
@@ -683,4 +749,95 @@ mod tests {
assert!(!hatch.solid);
assert!(hatch.gradient.is_none());
}
#[test]
fn the_pattern_picker_is_offered_and_its_choice_sets_the_pattern() {
let mut s = Session::new();
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
s.start("hatch").unwrap();
let picker = s.current_prompt().and_then(|p| p.picker);
assert_eq!(picker, Some(Picker::HatchPattern(HatchSettings::from(&s, &Value::Null, false).pattern)));
// A choice made in the picker while boundaries are picked.
s.input(Input::Text("AR-CONC".into())).unwrap();
assert_eq!(s.current_prompt().and_then(|p| p.picker), Some(Picker::HatchPattern("AR-CONC".into())));
// And at the pattern prompt.
s.input(Input::Keyword("Pattern".into())).unwrap();
assert!(s.current_prompt().unwrap().picker.is_some());
s.input(Input::Text("BRICK".into())).unwrap();
s.input(Input::Point(Vec2::new(5.0, 5.0))).unwrap();
s.input(Input::Enter).unwrap();
assert_eq!(only_hatch(&s).1.pattern, "BRICK");
// An unknown name keeps the pattern.
s.start("hatch").unwrap();
s.input(Input::Text("NO-SUCH".into())).unwrap();
assert_eq!(s.current_prompt().and_then(|p| p.picker), Some(Picker::HatchPattern("BRICK".into())));
// GRADIENT offers no pattern picker.
s.cancel();
s.start("gradient").unwrap();
assert!(s.current_prompt().unwrap().picker.is_none());
}
#[test]
fn pattern_choices_list_the_library_with_its_lines() {
let all = pattern_choices();
assert_eq!(all.first().map(|c| c.name.as_str()), Some("SOLID"));
assert!(all[0].lines.is_empty());
let ansi31 = all.iter().find(|c| c.name == "ANSI31").unwrap();
assert_eq!(ansi31.lines.len(), 1);
assert!(!ansi31.description.is_empty());
}
#[test]
fn repeated_picks_and_handles_in_one_region_do_not_produce_duplicate_loops() {
// One internal point
let mut s = Session::new();
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
s.execute("hatch", &json!({ "points": [[5, 5]], "pattern": "SOLID" })).unwrap();
let (_, h1) = only_hatch(&s);
assert_eq!(h1.loops.len(), 1);
// Two different points in the same rectangle
let mut s = Session::new();
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
s.execute("hatch", &json!({ "points": [[2, 2], [8, 8]], "pattern": "SOLID" })).unwrap();
let (_, h2) = only_hatch(&s);
assert_eq!(h2.loops.len(), 1, "two points in same region must deduplicate loops");
// The same point twice
let mut s = Session::new();
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
s.execute("hatch", &json!({ "points": [[5, 5], [5, 5]], "pattern": "SOLID" })).unwrap();
let (_, h3) = only_hatch(&s);
assert_eq!(h3.loops.len(), 1, "identical points must deduplicate loops");
// Three points in the same rectangle
let mut s = Session::new();
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
s.execute("hatch", &json!({ "points": [[2, 2], [5, 5], [8, 8]], "pattern": "SOLID" })).unwrap();
let (_, h4) = only_hatch(&s);
assert_eq!(h4.loops.len(), 1);
// Same boundary handle twice
let mut s = Session::new();
let r = s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
let handle = r["handle"].as_str().unwrap();
s.execute("hatch", &json!({ "handles": [handle, handle], "pattern": "SOLID" })).unwrap();
let (_, h5) = only_hatch(&s);
assert_eq!(h5.loops.len(), 1, "repeated boundary handle must deduplicate loops");
// Two points in disjoint rectangles: both regions preserved
let mut s = Session::new();
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
s.execute("rectang", &json!({ "p1": [20, 0], "p2": [30, 10] })).unwrap();
s.execute("hatch", &json!({ "points": [[5, 5], [25, 5]], "pattern": "SOLID" })).unwrap();
let (_, h6) = only_hatch(&s);
assert_eq!(h6.loops.len(), 2, "disjoint rectangles must both be kept");
// Gradient with duplicate points in the same rectangle
let mut s = Session::new();
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 10] })).unwrap();
s.execute("gradient", &json!({ "points": [[2, 2], [8, 8]], "color1": "1", "color2": "3" })).unwrap();
let (_, hg) = only_hatch(&s);
assert_eq!(hg.loops.len(), 1, "gradient with repeated picks in same region must deduplicate loops");
}
}
+5 -2
View File
@@ -10,7 +10,7 @@ use super::assoc::{detach, same_shape};
use super::{ORIGIN_KW, OriginMode, closed_loop, island_style};
use crate::cmd::helpers::lwpoly;
use crate::cmd::{bad, bool_or, point_param, str_param, targets};
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
use crate::{Accept, Input, Interactive, Picker, Prompt, Result, Session, Step};
/// Upper bound on hatches edited by one call.
const MAX_EDITED: usize = 10_000;
@@ -328,7 +328,10 @@ impl Interactive for HatchEditM {
};
Prompt::new("Island detection style", Accept::TEXT).kw(&["Ignore", "Outer", "Normal"]).default(cur)
}
Ask::Pattern => Prompt::new("Enter a pattern name", Accept::TEXT).default(h.map(|h| h.pattern).unwrap_or_default()),
Ask::Pattern => {
let current = h.map(|h| h.pattern).unwrap_or_default();
Prompt::new("Enter a pattern name", Accept::TEXT).default(current.clone()).picker(Picker::HatchPattern(current))
}
Ask::Scale => Prompt::new("Specify a scale for the hatch pattern", Accept::NUMBER).default(format!("{:.4}", h.map_or(1.0, |h| h.scale))),
Ask::Angle => Prompt::new("Specify an angle for the hatch pattern", Accept::NUMBER)
.default(format!("{:.0}", h.map_or(0.0, |h| h.angle.to_degrees()))),
+19
View File
@@ -44,6 +44,25 @@ pub(crate) fn names() -> Vec<String> {
v
}
/// A pattern offered for new hatches: a library pattern or one a `.pat` file loaded.
#[derive(Clone, Debug, PartialEq)]
pub struct PatternChoice {
pub name: String,
pub description: String,
/// The line families at unit scale (none for SOLID).
pub lines: Vec<PatternLine>,
}
/// Every pattern offered for new hatches, in the order of [`names`], for a pattern picker.
pub fn pattern_choices() -> Vec<PatternChoice> {
let mut v: Vec<PatternChoice> = standard_patterns()
.into_iter()
.map(|p| PatternChoice { name: p.name.to_string(), description: p.description.to_string(), lines: p.lines })
.collect();
v.extend(store().iter().map(|p| PatternChoice { name: p.name.clone(), description: p.description.clone(), lines: p.lines.clone() }));
v
}
/// Add the patterns of a `.pat` file's text (a pattern loaded before under the same name is
/// replaced). Returns the names added and the file's problems.
pub(crate) fn load(text: &str) -> (Vec<String>, Vec<String>) {
+1 -1
View File
@@ -34,7 +34,7 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("mview", "Viewports", run_mview)
.alias(&["mv"])
.params(
"{option?: fit|polygonal|object|on|off|lock|restore, p1?, p2? (default: printable area), count?: 1-4, arrangement?: vertical|horizontal|left|right|above|below, points?: [[x,y]…] + bulges? (polygonal), object? (closed object to clip by), handles? + locked? (on/off/lock), layout?}",
"{option?: fit|polygonal|object|on|off|lock|restore, p1?, p2? (default: printable area), count?: 1-4, arrangement?: vertical|horizontal|left|right|above|below, points?: [[x,y]…] + bulges? (polygonal), object? (closed object to clip by), handles? + locked? (on/off/lock), name? (restore: a model space viewport configuration, default *Active; its viewports fill p1-p2 or the printable area), layout?}",
)
.interactive(|_| Ok(Box::new(MviewM::new(1, true)))),
CommandSpec::new("mview.polygonal", "Polygonal Viewport", |s, p| run_mview(s, &viewports::with(p.clone(), "option", json!("polygonal"))))
+146 -7
View File
@@ -1,22 +1,25 @@
//! Clipped viewports and MVIEW's options: polygonal and object viewports (MVIEW Polygonal /
//! Object), VPCLIP, ON/OFF, Lock and Restore, and the typed MVIEW prompts.
//! Object), VPCLIP, ON/OFF, Lock and Restore (a model space viewport configuration as layout
//! viewports), and the typed MVIEW prompts.
//!
//! A clipped viewport keeps its rectangle as the boundary's extents and points at a paper-space
//! entity (a closed polyline, circle, ellipse or spline) whose outline clips what it shows
//! ([`Viewport::clip`], DXF group 340).
use cadcraft_doc::{Drawing, EntityKind, Handle, Space, Viewport};
use cadcraft_doc::{Drawing, EntityKind, Handle, Space, Viewport, VportConfig, VportTile};
use cadcraft_geom::{Bounds2, PolyVertex, Vec2};
use serde_json::{Value, json};
use super::super::curves::{self, handle_value};
use super::super::helpers::{lwpoly, rect_vertices};
use super::super::{bad, bool_or, points_param, str_param, targets};
use super::{fit_view, layout_viewports, mview, split, target_layout};
use super::super::{bad, bool_or, point_param, points_param, str_param, targets};
use super::{fit_view, layout_viewports, mview, printable, split, target_layout};
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
/// Most vertices a typed or JSON polygonal boundary may have.
const MAX_VERTICES: usize = 10_000;
/// Most viewports MVIEW Restore creates (a configuration read from a file may hold more tiles).
const MAX_RESTORED: usize = 64;
/// The polygon outline's extents of a closed paper-space entity usable as a clip boundary.
fn boundary_bounds(k: &EntityKind) -> Option<Bounds2> {
@@ -210,6 +213,66 @@ fn mview_lock(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "viewports": hs.iter().map(|h| h.hex()).collect::<Vec<_>>(), "locked": locked }))
}
/// Where tile `t` of a viewport configuration lands when the whole configuration fills `r`; `None`
/// for a tile with no usable area (configurations come from files).
fn tile_rect(t: &VportTile, r: Bounds2) -> Option<Bounds2> {
let f = |v: f64| v.is_finite().then(|| v.clamp(0.0, 1.0));
let (x0, y0, x1, y1) = (f(t.min.x)?, f(t.min.y)?, f(t.max.x)?, f(t.max.y)?);
if x1 - x0 < 1e-6 || y1 - y0 < 1e-6 {
return None;
}
let at = |x: f64, y: f64| Vec2::new(r.min.x + x * r.width(), r.min.y + y * r.height());
Some(Bounds2::new(at(x0, y0), at(x1, y1)))
}
/// The view a restored tile shows in a viewport of size `r`: its saved centre, and its height
/// grown when the viewport is narrower than the tile was, so the whole saved view still shows.
fn tile_view(t: &VportTile, r: Bounds2) -> Option<(Vec2, f64)> {
if !(t.center.is_finite() && t.height.is_finite() && t.height > 0.0) {
return None;
}
let aspect = r.width() / r.height().max(1e-12);
let h = if t.aspect.is_finite() && t.aspect > 0.0 && aspect > 0.0 { t.height.max(t.height * t.aspect / aspect) } else { t.height };
(h.is_finite() && h > 0.0).then_some((t.center, h))
}
/// MVIEW Restore: `{name? (default *Active), p1?, p2? (default: the printable area), layout?}`.
/// One viewport per tile of the model space viewport configuration, arranged as the tiles are
/// over the window, each showing its tile's view.
fn mview_restore(s: &mut Session, p: &Value) -> Result<Value> {
let space = target_layout(s, "mview", p)?;
let Space::Paper(layout) = &space else { return Err(bad("mview", "not a layout")) };
let name = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).unwrap_or(VportConfig::ACTIVE).to_string();
let tiles = super::super::view::config_tiles(s, &name)
.ok_or_else(|| EngineError::Other(format!("Could not find viewport configuration \"{}\".", name.chars().take(64).collect::<String>())))?;
let rect = match (point_param(p, "p1"), point_param(p, "p2")) {
(Some(a), Some(b)) => Bounds2::new(a, b),
_ => printable(s, layout)?,
};
if rect.is_empty() || !rect.min.is_finite() || !rect.max.is_finite() || rect.width() < 1e-6 || rect.height() < 1e-6 {
return Err(bad("mview", "the viewport has no area"));
}
if rect.width() > 1e7 || rect.height() > 1e7 {
return Err(bad("mview", "the viewport is too large"));
}
let common = s.current_common();
let d = s.doc_mut()?;
let mut out = Vec::new();
for t in tiles.iter().take(MAX_RESTORED) {
let Some(r) = tile_rect(t, rect) else { continue };
let id = next_viewport_id(d);
let mut vp = new_viewport(d, r, id);
if let Some((c, h)) = tile_view(t, r) {
(vp.view_center, vp.view_height) = (c, h);
}
out.push(d.add(&space, common.clone(), EntityKind::Viewport(vp))?);
}
if out.is_empty() {
return Err(EngineError::Other(format!("Viewport configuration \"{name}\" has no viewports to restore.")));
}
Ok(json!({ "configuration": name, "viewports": handles_json(&out) }))
}
/// MVIEW's options other than corners and Fit; `None` when `option` is not one of them.
pub(super) fn mview_option(s: &mut Session, p: &Value) -> Option<Result<Value>> {
let opt = str_param(p, "option")?.trim().to_ascii_lowercase();
@@ -219,7 +282,7 @@ pub(super) fn mview_option(s: &mut Session, p: &Value) -> Option<Result<Value>>
"on" => mview_on(s, p, true),
"off" => mview_on(s, p, false),
"lock" | "l" => mview_lock(s, p),
"restore" | "r" => Err(bad("mview", "named viewport configurations are not available yet")),
"restore" | "r" => mview_restore(s, p),
"fit" | "f" | "corners" | "" => return None,
_ => Err(bad("mview", "option must be fit, polygonal, object, on, off, lock or restore")),
})
@@ -471,6 +534,10 @@ enum Stage {
Polygon(PolygonInput),
Object,
Lock,
/// Restore: the configuration's name, then where its viewports go.
RestoreName,
RestoreCorner(String),
RestoreOpposite(String, Vec2),
/// ON/OFF (Some(on)) or Lock (None, with the chosen state) on picked viewports.
Select {
on: Option<bool>,
@@ -507,6 +574,18 @@ impl MviewM {
s.echo(format!("{n} viewport(s) created, zoomed to the model extents."));
Ok(Step::Done)
}
/// MVIEW Restore with the typed name, over `rect` (None: Fit).
fn restore(s: &mut Session, name: &str, rect: Option<Bounds2>) -> Result<Step> {
let mut p = json!({ "option": "restore", "name": name });
if let Some(r) = rect {
p = with(p, "p1", json!([r.min.x, r.min.y]));
p = with(p, "p2", json!([r.max.x, r.max.y]));
}
let r = mview_restore(s, &p)?;
let n = r.get("viewports").and_then(Value::as_array).map(Vec::len).unwrap_or(0);
s.echo(format!("{n} viewport(s) restored from \"{name}\"."));
Ok(Step::Done)
}
fn corner_keyword(&mut self, s: &mut Session, k: &str) -> Result<Step> {
if k == "fit" || k == "f" {
return self.create(s, None);
@@ -524,7 +603,7 @@ impl MviewM {
"lock" | "l" => Stage::Lock,
"object" | "o" => Stage::Object,
"polygonal" | "p" => Stage::Polygon(PolygonInput::default()),
"restore" | "r" => return Err(EngineError::Other("Named viewport configurations are not available yet.".into())),
"restore" | "r" => Stage::RestoreName,
_ => return Err(EngineError::Other("Requires a point or option keyword.".into())),
};
Ok(Step::Continue)
@@ -562,6 +641,9 @@ impl Interactive for MviewM {
Stage::Polygon(pi) => pi.prompt(),
Stage::Object => Prompt::new("Select object to clip viewport", Accept::POINT),
Stage::Lock => Prompt::new("Lock the view of viewports?", curves::KW).kw(&["ON", "OFF"]),
Stage::RestoreName => Prompt::new("Enter viewport configuration name", Accept::TEXT).kw(&["?"]).default(VportConfig::ACTIVE),
Stage::RestoreCorner(_) => Prompt::new("Specify first corner", Accept::POINT).kw(&["Fit"]).default("Fit"),
Stage::RestoreOpposite(_, a) => Prompt::new("Specify opposite corner", Accept::POINT).base(*a),
Stage::Select { .. } => Prompt::new("Select viewports", Accept::POINT),
}
}
@@ -653,6 +735,53 @@ impl Interactive for MviewM {
};
Ok(Step::Continue)
}
Stage::RestoreName => {
let name = match &i {
Input::Enter => VportConfig::ACTIVE.to_string(),
Input::Text(t) | Input::Keyword(t) if !t.trim().is_empty() => t.trim().to_string(),
_ => {
self.stage = Stage::RestoreName;
return Ok(Step::Continue);
}
};
self.stage = Stage::RestoreName;
if name == "?" {
let names = super::super::view::config_names(s);
s.echo(if names.is_empty() { "No saved viewport configurations.".to_string() } else { names.join(", ") });
return Ok(Step::Continue);
}
if super::super::view::config_tiles(s, &name).is_none() {
return Err(EngineError::Other(format!(
"Could not find viewport configuration \"{}\".",
name.chars().take(64).collect::<String>()
)));
}
self.stage = Stage::RestoreCorner(name);
Ok(Step::Continue)
}
Stage::RestoreCorner(name) => match i {
Input::Enter => Self::restore(s, &name, None),
Input::Keyword(k) | Input::Text(k) if matches!(k.trim().to_ascii_lowercase().as_str(), "fit" | "f") => Self::restore(s, &name, None),
Input::Point(a) => {
self.stage = Stage::RestoreOpposite(name, a);
Ok(Step::Continue)
}
Input::Keyword(_) | Input::Text(_) => {
self.stage = Stage::RestoreCorner(name);
Err(EngineError::Other("Requires a point or option keyword.".into()))
}
_ => {
self.stage = Stage::RestoreCorner(name);
Ok(Step::Continue)
}
},
Stage::RestoreOpposite(name, a) => match i {
Input::Point(b) => Self::restore(s, &name, Some(Bounds2::new(a, b))),
_ => {
self.stage = Stage::RestoreOpposite(name, a);
Ok(Step::Continue)
}
},
Stage::Select { on, locked, mut picked } => match i {
Input::Enter => {
if picked.is_empty() {
@@ -673,12 +802,22 @@ impl Interactive for MviewM {
},
}
}
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
match &self.stage {
Stage::Opposite(a) => {
split(Bounds2::new(*a, c), self.count, &self.arrangement).into_iter().map(|r| lwpoly(rect_vertices(r.min, r.max), true)).collect()
}
Stage::Polygon(pi) => pi.preview(c),
Stage::RestoreOpposite(name, a) => {
let r = Bounds2::new(*a, c);
super::super::view::config_tiles(s, name)
.unwrap_or_default()
.iter()
.take(MAX_RESTORED)
.filter_map(|t| tile_rect(t, r))
.map(|r| lwpoly(rect_vertices(r.min, r.max), true))
.collect()
}
_ => Vec::new(),
}
}
+61
View File
@@ -1712,6 +1712,26 @@ impl SelectThen {
}
}
/// Objects whose text MIRROR keeps readable instead of mirroring it with the geometry.
fn keeps_reading_when_mirrored(k: &EntityKind) -> bool {
match k {
EntityKind::Insert(i) => !i.attribs.is_empty(),
k => matches!(
k,
EntityKind::Text(_)
| EntityKind::AttDef(_)
| EntityKind::MText(_)
| EntityKind::Dimension(_)
| EntityKind::MLeader(_)
| EntityKind::Table(_)
| EntityKind::Tolerance(_)
),
}
}
/// Text objects transformed for each frame of a MIRROR preview (on the CPU); more are not drawn.
const MAX_MIRROR_TEXT_PREVIEW: usize = 1000;
/// A finite length greater than zero (rejects 0, negatives, NaN and infinity).
fn positive_length(v: Option<f64>) -> Option<f64> {
v.filter(|f| f.is_finite() && *f > 1e-12)
@@ -1927,6 +1947,41 @@ impl Interactive for SelectThen {
out.push(line(base, c));
out
}
fn drag(&self, s: &Session, c: Vec2) -> Option<crate::prompt::Drag> {
if (self.op == Op::Rotate && self.reference) || !self.sel.done {
return None;
}
let base = self.pts.first().copied()?;
let matrix = match self.op {
Op::Move | Op::Copy => Mat3::translate(c - base),
Op::Rotate => Mat3::rotate_about(base, base.angle_to(c)),
Op::Scale if !self.reference => Mat3::scale_about(base, base.dist(c).max(1e-9)),
Op::Scale => Mat3::scale_about(base, self.ref_len.and_then(|r| positive_length(Some(base.dist(c) / r)))?),
Op::Mirror if !self.ask_erase => Mat3::mirror(base, c),
_ => return None,
};
let mut extra = vec![line(base, c)];
if self.op != Op::Mirror {
return Some(crate::prompt::Drag { handles: self.objs.clone(), matrix, extra, clipboard: false });
}
// Mirrored text keeps reading left to right (MIRRTEXT 0), which the matrix alone can't
// draw: those objects are transformed one by one, the rest is mirrored on the GPU.
let d = s.doc().ok()?;
let mut handles = Vec::with_capacity(self.objs.len());
let mut texts = 0;
for h in &self.objs {
let Some(e) = d.entity(*h) else { continue };
if !keeps_reading_when_mirrored(&e.kind) {
handles.push(*h);
} else if texts < MAX_MIRROR_TEXT_PREVIEW {
texts += 1;
let mut k = e.kind.clone();
k.transform(&matrix);
extra.push(k);
}
}
Some(crate::prompt::Drag { handles, matrix, extra, clipboard: false })
}
}
/// An OFFSET option prompt opened from "Specify offset distance".
@@ -2730,6 +2785,12 @@ impl Interactive for DisplaceM {
}
self.inner.preview(s, c)
}
fn drag(&self, s: &Session, c: Vec2) -> Option<crate::prompt::Drag> {
if self.displacement {
return None;
}
self.inner.drag(s, c)
}
}
pub(crate) fn _unused(_: Circle) {}
+66 -1
View File
@@ -11,6 +11,7 @@
use std::sync::{Mutex, OnceLock};
use cadcraft_doc::{DevicePaper, Orientation, PageSetup, PlotOutput, PlotterConfig};
use cadcraft_geom::{Bounds2, Vec2};
use serde_json::{Value, json};
use super::*;
@@ -233,7 +234,7 @@ fn unknown_device(cmd: &str, name: &str) -> EngineError {
/// paper's printable area. A device change also takes the device's default orientation unless
/// `landscape` is given. Returns whether the paper was set here (else `paper`, if any, is a
/// standard size for the caller to look up).
pub(crate) fn page_device(cmd: &str, p: &Value, page: &mut PageSetup) -> Result<bool> {
pub fn page_device(cmd: &str, p: &Value, page: &mut PageSetup) -> Result<bool> {
let given = match p.get("device") {
Some(v) => Some(v.as_str().ok_or_else(|| bad(cmd, "`device` must be a plotter name"))?.trim()),
None => None,
@@ -325,10 +326,74 @@ pub(crate) fn plot_options(s: &Session, p: &Value, cmd: &str) -> Result<Value> {
Ok(out)
}
// ---------- PLOTWINDOW ----------
fn run_plotwindow(s: &mut Session, p: &Value) -> Result<Value> {
let (a, b) = (point_req("plotwindow", p, "p1")?, point_req("plotwindow", p, "p2")?);
set_plot_window(s, a, b)
}
fn set_plot_window(s: &mut Session, a: Vec2, b: Vec2) -> Result<Value> {
let w = Bounds2::new(a, b);
if !(w.width() > 0.0 && w.height() > 0.0) {
return Err(bad("plotwindow", "the window needs a width and a height"));
}
s.plot_window = Some(w);
Ok(json!({ "window": [[w.min.x, w.min.y], [w.max.x, w.max.y]] }))
}
/// PLOTWINDOW: two corners picked in the drawing, with the window's rectangle following the cursor.
#[derive(Default)]
struct PlotWindowM {
first: Option<Vec2>,
}
impl Interactive for PlotWindowM {
fn name(&self) -> &'static str {
"PLOTWINDOW"
}
fn prompt(&self, _s: &Session) -> Prompt {
match self.first {
None => Prompt::new("Specify first corner", Accept::POINT),
Some(a) => Prompt::new("Specify opposite corner", Accept::POINT).base(a),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match (i, self.first) {
(Input::Point(p), None) => {
self.first = Some(p);
Ok(Step::Continue)
}
(Input::Point(p), Some(a)) => {
set_plot_window(s, a, p)?;
Ok(Step::Done)
}
(Input::Cancel, _) => Ok(Step::Cancel),
_ => Ok(Step::Continue),
}
}
fn preview(&self, _s: &Session, c: Vec2) -> Vec<cadcraft_doc::EntityKind> {
let Some(a) = self.first else { return Vec::new() };
let corners = [a, Vec2::new(c.x, a.y), c, Vec2::new(a.x, c.y), a];
corners
.windows(2)
.filter_map(|w| match w {
[p, q] => Some(super::helpers::line(*p, *q)),
_ => None,
})
.collect()
}
}
// ---------- PLOTTERMANAGER ----------
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("plotwindow", "Plot Window", run_plotwindow)
.params("{p1: [x,y], p2: [x,y]} (typed: two corners): the window the Plot dialog's \"Window <\" plots, kept in the session")
.enabled(has_doc)
.noundo()
.interactive(|_| Ok(Box::new(PlotWindowM::default()))),
CommandSpec::new("plottermanager", "Plotter Manager...", run_plottermanager)
.menu(&["File", "Plotter Manager..."])
.params(
+6 -1
View File
@@ -60,7 +60,10 @@ pub fn specs() -> Vec<CommandSpec> {
.menu(&["Format", "Multileader Style..."])
.alias(&["mls"])
.params("{name, arrowSize?, textHeight?, landingGap?, dogleg?, textStyle?, annotative?, current?} → styles"),
CommandSpec::new("ddptype", "Point Style...", run_ptype).menu(&["Format", "Point Style..."]).params("{pdmode, pdsize}"),
CommandSpec::new("ddptype", "Point Style...", run_ptype)
.menu(&["Format", "Point Style..."])
.alias(&["ptype"])
.params("{pdmode, pdsize}"),
CommandSpec::new("rename", "Rename...", run_rename)
.menu(&["Format", "Rename..."])
.alias(&["ren", "-rename"])
@@ -371,6 +374,8 @@ fn run_set(s: &mut Session, p: &Value) -> Result<Value> {
h.pattern_lines = super::hatch::lines_for(v);
}
h.pattern = v.to_ascii_uppercase();
// The old pattern's definition lines no longer apply.
h.pattern_lines.clear();
h.solid = h.pattern == "SOLID";
if !h.solid {
// A named pattern replaces the gradient fill; the renderer draws the gradient first when it is set.
+4 -77
View File
@@ -1,11 +1,11 @@
//! Utilities: CAL (expression calculator), FIND (find and replace text), MASSPROP, MEASUREGEOM.
use cadcraft_doc::{EntityKind, Handle};
use cadcraft_geom::{Polyline, Vec2};
use cadcraft_geom::Vec2;
use serde_json::{Value, json};
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
pub fn specs() -> Vec<CommandSpec> {
vec![
@@ -26,10 +26,6 @@ pub fn specs() -> Vec<CommandSpec> {
.noundo()
.interactive(|_| Ok(Box::<PickM>::default())),
CommandSpec::new("find", "Find...", run_find).menu(&["Edit", "Find..."]).params("{find, replace?, matchCase?: bool, wholeWord?: bool}"),
CommandSpec::new("massprop", "Region/Mass Properties", run_massprop)
.menu(&["Tools", "Inquiry", "Region/Mass Properties"])
.params("{handles?}")
.noundo(),
CommandSpec::new("measuregeom", "Measure Geometry", run_measuregeom).params("{mode: distance|radius|angle|area, ...}").noundo(),
CommandSpec::new("radius.measure", "Radius", run_radius).menu(&["Tools", "Inquiry", "Radius"]).params("{handle}").noundo(),
CommandSpec::new("angle.measure", "Angle", run_angle).menu(&["Tools", "Inquiry", "Angle"]).params("{vertex, p1, p2} | {h1, h2}").noundo(),
@@ -276,69 +272,8 @@ fn run_find(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "matches": found, "total": total, "message": msg }))
}
// ---------------- MASSPROP / MEASUREGEOM ----------------
/// Area, perimeter, centroid and second moments of a closed boundary.
fn region_props(pts: &[Vec2]) -> Option<(f64, f64, Vec2, f64, f64)> {
let n = pts.len();
if n < 3 {
return None;
}
let (mut a, mut cx, mut cy, mut ixx, mut iyy, mut per) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
for i in 0..n {
let (p, q) = (*pts.get(i)?, *pts.get((i + 1) % n)?);
let c = p.cross(q);
a += c;
cx += (p.x + q.x) * c;
cy += (p.y + q.y) * c;
ixx += (p.y * p.y + p.y * q.y + q.y * q.y) * c;
iyy += (p.x * p.x + p.x * q.x + q.x * q.x) * c;
per += p.dist(q);
}
a /= 2.0;
if a.abs() < 1e-15 {
return None;
}
let c = Vec2::new(cx / (6.0 * a), cy / (6.0 * a));
Some((a.abs(), per, c, (ixx / 12.0).abs(), (iyy / 12.0).abs()))
}
fn closed_points(k: &EntityKind) -> Option<Vec<Vec2>> {
match k {
EntityKind::Circle(c) => {
let mut v = Vec::new();
cadcraft_geom::Circle::new(c.center.xy(), c.radius).tessellate(c.radius * 1e-5, &mut v);
v.pop();
Some(v)
}
EntityKind::LwPolyline(p) => {
let mut v = Polyline { vertices: p.vertices.clone(), closed: true }.tessellate(1e-5);
if v.len() > 1 && v.first().zip(v.last()).is_some_and(|(a, b)| a.near(*b, 1e-12)) {
v.pop();
}
Some(v)
}
EntityKind::Hatch(h) => h.loops.first().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate(1e-5)),
_ => None,
}
}
fn run_massprop(s: &mut Session, p: &Value) -> Result<Value> {
let hs = targets(s, p)?;
let d = s.doc()?;
let mut out = Vec::new();
let mut lines = Vec::new();
for h in hs {
let Some(e) = d.entity(h) else { continue };
let Some((a, per, c, ixx, iyy)) = closed_points(&e.kind).and_then(|v| region_props(&v)) else { continue };
lines.push(format!("---------------- REGIONS ----------------\nArea: {a:.4}\nPerimeter: {per:.4}\nCentroid: X: {:.4} Y: {:.4}\nMoments of inertia: X: {ixx:.4} Y: {iyy:.4}", c.x, c.y));
out.push(json!({ "handle": h.hex(), "area": a, "perimeter": per, "centroid": [c.x, c.y], "ixx": ixx, "iyy": iyy }));
}
if out.is_empty() {
return Err(EngineError::Other("Select closed objects (regions, closed polylines, circles).".into()));
}
Ok(json!({ "objects": out, "message": lines.join("\n") }))
}
// ---------------- MEASUREGEOM ----------------
// (MASSPROP is in `inquiry/regions.rs`.)
fn run_radius(s: &mut Session, p: &Value) -> Result<Value> {
let h = targets(s, p)?.first().copied().ok_or_else(|| bad("radius.measure", "`handle` is required"))?;
@@ -410,12 +345,4 @@ mod tests {
assert_eq!(replace_in("aaa", "a", "b", true, false), ("bbb".to_string(), 3));
assert_eq!(replace_in("x", "", "y", true, false).1, 0);
}
#[test]
fn rectangle_mass_properties() {
let (a, per, c, _, _) = region_props(&[Vec2::ZERO, Vec2::new(4.0, 0.0), Vec2::new(4.0, 2.0), Vec2::new(0.0, 2.0)]).unwrap();
assert!((a - 8.0).abs() < 1e-12);
assert!((per - 12.0).abs() < 1e-12);
assert!(c.near(Vec2::new(2.0, 1.0), 1e-12));
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step, View};
mod named_views;
mod vports;
pub(super) use vports::{model_vports, tile_model};
pub(super) use vports::{config_names, config_tiles, model_vports, tile_model};
pub fn specs() -> Vec<CommandSpec> {
let mut v = vec![
+16
View File
@@ -71,6 +71,22 @@ fn with_option(p: &Value, option: &str) -> Value {
p
}
/// The tiles of viewport configuration `name` (any case), for MVIEW Restore: `*Active` is the
/// model space as shown now (one full tile with the model view when it isn't tiled), any other
/// name a saved configuration. `None` when there is no such configuration.
pub(in crate::cmd) fn config_tiles(s: &Session, name: &str) -> Option<Vec<VportTile>> {
let name = name.trim();
if name.eq_ignore_ascii_case(VportConfig::ACTIVE) {
return Some(live(s.state().ok()?, s.viewport_px).0);
}
s.doc().ok()?.vports.iter().find(|c| !c.is_active() && c.name.eq_ignore_ascii_case(name)).map(|c| c.tiles.clone())
}
/// The saved viewport configurations' names.
pub(in crate::cmd) fn config_names(s: &Session) -> Vec<String> {
s.doc().map(|d| d.vports.iter().filter(|c| !c.is_active()).map(|c| c.name.clone()).collect()).unwrap_or_default()
}
// ---------------- tile state ----------------
type Rect = (Vec2, Vec2);
+9
View File
@@ -75,6 +75,15 @@ impl Interactive for Guarded {
Vec::new()
})
}
fn drag(&self, s: &Session, cursor: Vec2) -> Option<crate::prompt::Drag> {
if self.broken.get().is_some() {
return None;
}
catch_unwind(AssertUnwindSafe(|| self.inner.drag(s, cursor))).unwrap_or_else(|_| {
self.broken.set(Some("panic in preview"));
None
})
}
}
#[cfg(test)]
+31 -1
View File
@@ -37,8 +37,9 @@ use serde_json::Value;
pub use cadcraft_doc as doc;
pub use cadcraft_geom as geom;
pub use cadcraft_render as render;
pub use cmd::hatch::{PatternChoice, pattern_choices};
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
pub use prompt::{Accept, Input, Interactive, Prompt, Step};
pub use prompt::{Accept, Drag, Input, Interactive, Picker, Prompt, Step};
#[derive(Debug, thiserror::Error)]
pub enum EngineError {
@@ -542,6 +543,8 @@ pub struct Session {
pub active: usize,
pub settings: Settings,
pub running: Option<Running>,
/// A script is running ([`Session::script`]); a SCRIPT command inside it is refused.
in_script: bool,
/// The command suspended while a transparent command (`'ZOOM`, `'PAN`…) prompts inside it;
/// it resumes when that command ends. One level, as in AutoCAD.
pub suspended: Option<Running>,
@@ -582,6 +585,8 @@ pub struct Session {
pub last_used: LastUsed,
/// Where `clipboard` came from and the layers, styles and blocks its objects need.
pub clipboard_source: Option<cmd::clipboard::ClipSource>,
/// The window last picked with PLOTWINDOW (the Plot dialog's "Window <").
pub plot_window: Option<cadcraft_geom::Bounds2>,
}
impl Default for Session {
@@ -603,6 +608,7 @@ impl Session {
active: 0,
settings: Settings::default(),
running: None,
in_script: false,
suspended: None,
log: Vec::new(),
last_command: None,
@@ -624,6 +630,7 @@ impl Session {
last_dim: None,
last_used: LastUsed::default(),
clipboard_source: None,
plot_window: None,
}
}
pub fn new_drawing(&mut self, metric: bool) -> usize {
@@ -1183,6 +1190,16 @@ impl Session {
r.machine.preview(self, cursor).into_iter().map(|k| Entity { handle: Handle(0), common: common.clone(), kind: k }).collect()
}
/// The active command's preview as a transform of existing objects (see
/// [`Interactive::drag`]), with its extra rubber-band geometry as entities.
pub fn drag(&self, cursor: Vec2) -> Option<(Drag, Vec<Entity>)> {
let r = self.running.as_ref()?;
let d = r.machine.drag(self, cursor)?;
let common = self.current_common();
let extra = d.extra.iter().map(|k| Entity { handle: Handle(0), common: common.clone(), kind: k.clone() }).collect();
Some((d, extra))
}
/// Process one line typed at the command line (Enter pressed). Spaces separate inputs as in a
/// script line (see [`Session::script`]), so `MOVE L 0,0 5,5` ends the selection with the
/// second space and `ERASE L ` with the trailing one. Input after the command ends is ignored.
@@ -1300,7 +1317,20 @@ impl Session {
/// Run a script, as AutoCAD reads one: a space or a line end ends an input, and an empty
/// input (two spaces in a row, a space at the end of a line, an empty line) is Enter. At
/// free-text prompts spaces are part of the text. Lines starting with `;` are comments.
///
/// A script cannot run a script: the SCRIPT command (or another `cmd {json}` call) inside a
/// running one is refused, so two files that run each other cannot recurse until the stack dies.
pub fn script(&mut self, text: &str) -> Result<()> {
if self.in_script {
return Err(EngineError::BadParams { cmd: "script".into(), msg: "a script is already running; nested scripts are not supported".into() });
}
self.in_script = true;
let r = self.script_lines(text);
self.in_script = false;
r
}
fn script_lines(&mut self, text: &str) -> Result<()> {
for line in text.lines() {
// Trailing spaces are inputs (Enter); other trailing whitespace (`\r`) is not.
let line = line.trim_end_matches(|c: char| c.is_whitespace() && c != ' ');
+37 -2
View File
@@ -1,7 +1,7 @@
//! Interactive commands: prompts, inputs and the command-line coordinate grammar.
use cadcraft_doc::{EntityKind, Handle};
use cadcraft_geom::Vec2;
use cadcraft_geom::{Mat3, Vec2};
use serde::Serialize;
use crate::{Result, Session};
@@ -45,17 +45,34 @@ pub struct Prompt {
/// index): Dynamic Input shows its dimensions ([`crate::grip_dims`]).
#[serde(skip_serializing_if = "Option::is_none")]
pub grip: Option<(Handle, usize)>,
/// A visual chooser the UI can show for this prompt; a choice made in it is entered as
/// [`Input::Text`].
#[serde(skip_serializing_if = "Option::is_none")]
pub picker: Option<Picker>,
}
/// A visual chooser for a prompt ([`Prompt::picker`]).
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Picker {
/// A hatch pattern by name (see [`crate::pattern_choices`]); holds the current pattern.
HatchPattern(String),
}
impl Prompt {
pub fn new(message: impl Into<String>, accept: Accept) -> Self {
Prompt { message: message.into(), keywords: Vec::new(), default: None, accept, base: None, deferred: false, grip: None }
Prompt { message: message.into(), keywords: Vec::new(), default: None, accept, base: None, deferred: false, grip: None, picker: None }
}
/// The grip being stretched (the `grip` field).
pub fn grip(mut self, g: (Handle, usize)) -> Self {
self.grip = Some(g);
self
}
/// The visual chooser the UI can show (the `picker` field).
pub fn picker(mut self, p: Picker) -> Self {
self.picker = Some(p);
self
}
pub fn kw(mut self, k: &[&str]) -> Self {
self.keywords = k.iter().map(|s| s.to_string()).collect();
self
@@ -165,6 +182,24 @@ pub trait Interactive: Send {
fn preview(&self, _s: &Session, _cursor: Vec2) -> Vec<EntityKind> {
Vec::new()
}
/// The preview as existing objects under one transform, when it is that (MOVE, COPY,
/// ROTATE, SCALE). The UI then draws them from geometry uploaded once instead of rebuilding
/// the transformed copies every frame; `None` falls back to [`Interactive::preview`].
fn drag(&self, _s: &Session, _cursor: Vec2) -> Option<Drag> {
None
}
}
/// See [`Interactive::drag`].
#[derive(Clone, Debug)]
pub struct Drag {
pub handles: Vec<Handle>,
pub matrix: Mat3,
/// Rubber-band geometry drawn as well (the base-to-cursor line), and dragged objects the
/// matrix can't draw (text MIRROR keeps readable), already transformed.
pub extra: Vec<EntityKind>,
/// The dragged objects are the clipboard's ([`Session::clipboard`]), not `handles`.
pub clipboard: bool,
}
/// Parse a typed point: `x,y[,z]`, `@dx,dy`, `@dist<angle`, `dist<angle`, `#x,y` (absolute),
+40 -5
View File
@@ -154,13 +154,31 @@ pub(crate) fn osnap_with(
base: Option<Vec2>,
deferred: bool,
) -> Option<SnapHit> {
osnap_with_neighborhood(d, space, ix, cursor, aperture, osmode, base, deferred).0
}
/// Find the best object snap and retain the local-space primitives gathered for that query.
#[allow(clippy::too_many_arguments)]
pub(crate) fn osnap_with_neighborhood(
d: &Drawing,
space: &Space,
ix: &Option<std::sync::Arc<crate::spatial::SpatialIndex>>,
cursor: Vec2,
aperture: f64,
osmode: u32,
base: Option<Vec2>,
deferred: bool,
) -> (Option<SnapHit>, Option<Vec<Prim>>) {
if osmode == 0 || osmode & mode::OFF != 0 || !cursor.is_finite() || !(aperture.is_finite() && aperture > 0.0) {
return None;
return (None, None);
}
let mut best = osnap_in(d, space, ix, cursor, aperture, osmode, base, deferred, &[], None);
let (mut best, near_prims) = match osnap_in_with_prims(d, space, ix, cursor, aperture, osmode, base, deferred, &[], None) {
Some((hit, prims)) => (hit, Some(prims)),
None => (None, None),
};
let views = viewport::near(d, space, cursor, aperture);
if views.is_empty() {
return best;
return (best, near_prims);
}
let model = Space::Model;
let model_ix = crate::spatial::index(d, &model);
@@ -190,7 +208,7 @@ pub(crate) fn osnap_with(
best = Some(hit);
}
}
best
(best, near_prims)
}
#[allow(clippy::too_many_arguments)]
@@ -206,6 +224,22 @@ fn osnap_in(
frozen: &[String],
clip: Option<Bounds2>,
) -> Option<SnapHit> {
osnap_in_with_prims(d, space, ix, cursor, aperture, osmode, base, deferred, frozen, clip).and_then(|(hit, _)| hit)
}
#[allow(clippy::too_many_arguments)]
fn osnap_in_with_prims(
d: &Drawing,
space: &Space,
ix: &Option<std::sync::Arc<crate::spatial::SpatialIndex>>,
cursor: Vec2,
aperture: f64,
osmode: u32,
base: Option<Vec2>,
deferred: bool,
frozen: &[String],
clip: Option<Bounds2>,
) -> Option<(Option<SnapHit>, Vec<Prim>)> {
let (mut cands, near_prims) = gather_in(d, space, ix, cursor, aperture, osmode, frozen)?;
// Intersections between nearby primitives; Apparent Intersection also where their extensions
// meet (lines extended, arcs as full circles: in 2D, the "apparent" crossing of objects
@@ -236,7 +270,8 @@ fn osnap_in(
}
}
}
finish(cands, &near_prims, cursor, aperture, osmode, base, deferred, clip)
let hit = finish(cands, &near_prims, cursor, aperture, osmode, base, deferred, clip);
Some((hit, near_prims))
}
/// Whether a primitive has a meaningful extension (a line, an arc, a circle, an xline or ray).
+3 -2
View File
@@ -116,9 +116,10 @@ pub fn lattice_snap(p: Vec2, d: f64, origin: Vec2, angle: f64) -> Vec2 {
/// Grid snap for a cursor update: with snap on and isometric style, the cursor snapped to the
/// isometric lattice, and the settings with grid snap off so the rectangular grid leaves it be.
/// Otherwise both unchanged.
/// Otherwise both unchanged, also when PolarSnap (SNAPTYPE 1) is the snap type: snap mode then
/// steps along the tracking paths instead.
pub(crate) fn grid_snap(doc: &cadcraft_doc::Drawing, mut s: Settings, p: Vec2) -> (Vec2, Settings) {
if !(s.snapmode && s.isodraft) {
if !(s.snapmode && s.isodraft) || s.polarsnap {
return (p, s);
}
let (origin, angle) = super::grid_frame(doc);
+37 -5
View File
@@ -587,14 +587,42 @@ pub fn candidate(
cfg: &TrackConfig,
base: Option<Vec2>,
) -> Option<Acquired> {
let nothing_to_acquire = !cfg.otrack && !cfg.running(mode::EXT | mode::PAR | mode::APP);
if nothing_to_acquire || !(cursor.is_finite() && aperture.is_finite() && aperture > 0.0) {
if !can_acquire(cursor, aperture, cfg) {
return None;
}
let ix = crate::spatial::index(d, space);
let prims = super::gather(d, space, &ix, cursor, aperture, mode::NEA).map(|(_, p)| p).unwrap_or_default();
candidate_from_prims(cursor, hit, aperture, cfg, base, &prims)
}
pub(crate) fn candidate_with_prims(
cursor: Vec2,
hit: Option<&SnapHit>,
aperture: f64,
cfg: &TrackConfig,
base: Option<Vec2>,
prims: &[Prim],
) -> Option<Acquired> {
if !can_acquire(cursor, aperture, cfg) {
return None;
}
candidate_from_prims(cursor, hit, aperture, cfg, base, prims)
}
fn can_acquire(cursor: Vec2, aperture: f64, cfg: &TrackConfig) -> bool {
(cfg.otrack || cfg.running(mode::EXT | mode::PAR | mode::APP)) && cursor.is_finite() && aperture.is_finite() && aperture > 0.0
}
fn candidate_from_prims(
cursor: Vec2,
hit: Option<&SnapHit>,
aperture: f64,
cfg: &TrackConfig,
base: Option<Vec2>,
prims: &[Prim],
) -> Option<Acquired> {
let hit = hit.filter(|h| h.deferred.is_none() && h.mode != mode::NEA && h.point.is_finite());
let ext_end = if cfg.running(mode::EXT) { extension_near(&prims, hit.map_or(cursor, |h| h.point), aperture) } else { None };
let ext_end = if cfg.running(mode::EXT) { extension_near(prims, hit.map_or(cursor, |h| h.point), aperture) } else { None };
if let Some((at, ext)) = ext_end
&& hit.is_none_or(|h| h.point.dist(at) <= cadcraft_geom::EPS * (1.0 + at.len()))
{
@@ -761,8 +789,12 @@ impl crate::Session {
let last = self.last_segment.filter(|(_, b)| q.base.is_some_and(|base| base.near(*b, 1e-9 * (1.0 + b.len())))).map(|(a, b)| a.angle_to(b));
let cfg = TrackConfig::relative_to(&s, angbase, last);
let osmode = s.osmode | if s.osnaphatch { mode::HATCH } else { 0 };
let hit = super::osnap(&doc, &space, raw, q.aperture, osmode, q.base, q.deferred);
let cand = candidate(&doc, &space, raw, hit.as_ref(), q.aperture, &cfg, q.base);
let ix = crate::spatial::index(&doc, &space);
let (hit, near_prims) = super::osnap_with_neighborhood(&doc, &space, &ix, raw, q.aperture, osmode, q.base, q.deferred);
let cand = match (osmode & mode::HATCH, near_prims.as_deref()) {
(0, Some(prims)) => candidate_with_prims(raw, hit.as_ref(), q.aperture, &cfg, q.base, prims),
_ => candidate(&doc, &space, raw, hit.as_ref(), q.aperture, &cfg, q.base),
};
let wait = self.tracking.hover(cand, q.now, cfg.shift_acquire, q.shift);
if let Some(h) = hit {
self.tracking.last = None;
+44
View File
@@ -342,6 +342,50 @@ fn copy_rotate_scale_mirror() {
assert_eq!(s.doc().unwrap().model.len(), 5);
}
#[test]
fn copy_and_rotate_drag_as_a_transform_of_the_selection() {
fn start(cmd: &str) -> Session {
let mut s = Session::new();
s.execute("line", &json!({"points": [[0, 0], [1, 0]]})).unwrap();
for t in [cmd, "all", "", "0,0"] {
s.cmdline(t).unwrap();
}
s
}
let s = start("copy");
let (d, extra) = s.drag(Vec2::new(3.0, 4.0)).expect("COPY drags its selection");
assert_eq!(d.handles.len(), 1);
assert!(d.matrix.apply(Vec2::new(1.0, 0.0)).near(Vec2::new(4.0, 4.0), 1e-9));
assert_eq!(extra.len(), 1, "the base-to-cursor line");
let (d, _) = start("rotate").drag(Vec2::new(0.0, 5.0)).expect("ROTATE drags its selection");
assert!(d.matrix.apply(Vec2::new(1.0, 0.0)).near(Vec2::new(0.0, 1.0), 1e-9));
// MIRROR: geometry is mirrored by the matrix; text, which stays readable, is transformed
// object by object and comes with the extra geometry.
let mut s = Session::new();
s.execute("line", &json!({"points": [[0, 0], [1, 0]]})).unwrap();
s.execute("text", &json!({"at": [2, 2], "height": 1, "text": "A"})).unwrap();
for t in ["mirror", "all", "", "0,0"] {
s.cmdline(t).unwrap();
}
let (d, extra) = s.drag(Vec2::new(0.0, 5.0)).expect("MIRROR drags its selection");
assert_eq!(d.handles.len(), 1, "the line is mirrored by the matrix");
assert!(d.matrix.apply(Vec2::new(1.0, 0.0)).near(Vec2::new(-1.0, 0.0), 1e-9));
assert_eq!(extra.len(), 2, "the base-to-cursor line and the text");
let EntityKind::Text(t) = &extra[1].kind else { panic!("expected the text") };
assert!(t.insert.xy().near(Vec2::new(-2.0, 2.0), 1e-9) && t.rotation.abs() < 1e-9, "mirrored, still reading left to right");
// PASTECLIP drags the clipboard.
let mut s = Session::new();
let l = s.execute("line", &json!({"points": [[0, 0], [1, 0]]})).unwrap();
s.execute("copyclip", &json!({"handles": [l["handles"][0].clone()]})).unwrap();
s.cmdline("pasteclip").unwrap();
let (d, _) = s.drag(Vec2::new(5.0, 5.0)).expect("PASTECLIP drags the clipboard");
assert!(d.clipboard && d.handles.is_empty());
assert!(d.matrix.apply(s.clipboard_base).near(Vec2::new(5.0, 5.0), 1e-9));
}
#[test]
fn scale_reference_typed_and_picked() {
// Issue #17: SCALE ▸ Reference scales by new length / reference length.
+48 -4
View File
@@ -65,10 +65,24 @@ pub fn format_angle(a: f64, aunits: i64, auprec: i64) -> String {
let deg = cadcraft_geom::norm_angle(a).to_degrees();
match aunits {
1 => {
let d = deg.floor();
let m = ((deg - d) * 60.0).floor();
let s = (deg - d - m / 60.0) * 3600.0;
format!("{}d{}'{:.*}\"", d as i64, m as i64, prec.saturating_sub(4), s)
// Round at the displayed precision before splitting, carrying through a full turn.
let scale = 10i64.pow(prec.saturating_sub(4) as u32);
let seconds_per_unit = match prec {
0 => 3600,
1 | 2 => 60,
_ => 1,
};
let units = (deg * (3600 / seconds_per_unit * scale) as f64).round() as i64;
let total = units * seconds_per_unit % (360 * 3600 * scale);
let (d, m) = (total / (3600 * scale), (total / (60 * scale)) % 60);
match prec {
0 => format!("{d}d"),
1 | 2 => format!("{d}d{m}'"),
_ => {
let s = (total % (60 * scale)) as f64 / scale as f64;
format!("{d}d{m}'{:.*}\"", prec.saturating_sub(4), s)
}
}
}
2 => format!("{:.*}g", prec, deg / 0.9),
3 => format!("{:.*}r", prec, cadcraft_geom::norm_angle(a)),
@@ -280,6 +294,36 @@ mod tests {
assert_eq!(format_distance(30.0, 3, 2), "2'-6.00\"");
}
#[test]
fn dms_display_precision() {
let angle = (12.0 + 34.0 / 60.0 + 56.78912 / 3600.0_f64).to_radians();
let expected = ["13d", "12d35'", "12d35'", "12d34'57\"", "12d34'57\"", "12d34'56.8\"", "12d34'56.79\"", "12d34'56.789\"", "12d34'56.7891\""];
for (prec, expected) in expected.into_iter().enumerate() {
assert_eq!(format_angle(angle, 1, prec as i64), expected, "AUPREC {prec}");
}
}
#[test]
fn dms_rounding_carries() {
let dms = |d: f64, m: f64, s: f64| (d + m / 60.0 + s / 3600.0).to_radians();
let negative = (-0.0001_f64).to_radians();
let cases = [
(dms(29.0, 59.0, 59.6), 0, "30d"),
(dms(29.0, 59.0, 59.6), 2, "30d0'"),
(dms(29.0, 59.0, 59.6), 4, "30d0'0\""),
(dms(29.0, 0.0, 59.99996), 2, "29d1'"),
(dms(29.0, 0.0, 59.99996), 4, "29d1'0\""),
(dms(29.0, 59.0, 59.99996), 5, "30d0'0.0\""),
(dms(29.0, 59.0, 59.99996), 8, "30d0'0.0000\""),
(negative, 0, "0d"),
(negative, 4, "0d0'0\""),
(negative, 5, "359d59'59.6\""),
];
for (angle, prec, expected) in cases {
assert_eq!(format_angle(angle, 1, prec), expected, "AUPREC {prec}");
}
}
#[test]
fn parse_distances() {
assert_eq!(parse_distance("12.5"), Some(12.5));
+7
View File
@@ -198,6 +198,13 @@ fn autosave_files_are_written_in_the_background_and_removed_on_exit() {
assert!(dir.join(format!("{}.lock", a.instance)).exists());
assert!(autosave::recoverable(&dir, "someone-else").is_empty());
// After a GPU error a change made a moment ago is autosaved right away, not SAVETIME later.
assert!(a.tick(&s).is_none(), "no change waiting");
draw(&mut s);
assert!(a.tick(&s).is_some(), "SAVETIME (3 minutes) is not up yet");
assert_eq!(a.save_now(&s), 1);
assert_eq!(a.save_now(&s), 0, "being written or written: nothing more to save");
a.shutdown();
assert!(!std::path::Path::new(&path).exists());
assert!(!dir.join(format!("{}.lock", a.instance)).exists());
+150
View File
@@ -0,0 +1,150 @@
//! Drafting follow-ups (#626): isometric snap gives way to PolarSnap, and MVIEW Restore puts a
//! model space viewport configuration on a layout.
use cadcraft_doc::{EntityKind, Space, Viewport, VportTile};
use cadcraft_engine::Session;
use cadcraft_engine::geom::{Bounds2, Vec2};
use cadcraft_engine::snap::tracking::CursorQuery;
use serde_json::{Value, json};
fn v(x: f64, y: f64) -> Vec2 {
Vec2::new(x, y)
}
fn setvar(s: &mut Session, name: &str, value: Value) {
s.execute("setvar", &json!({ "name": name, "value": value })).unwrap();
}
/// Move the cursor to `raw` as the canvas does.
fn hover(s: &mut Session, raw: Vec2, base: Option<Vec2>) -> Vec2 {
let q = CursorQuery { base, deferred: false, aperture: 0.5, tol: 0.1, now: 100.0, shift: false };
let r = s.snap_cursor(raw, &q);
s.cursor = r.point;
r.point
}
#[test]
fn isometric_snap_gives_way_to_polarsnap() {
let mut s = Session::new();
setvar(&mut s, "OSMODE", json!(0));
s.execute("isodraft", &json!({ "plane": "left" })).unwrap();
setvar(&mut s, "SNAPMODE", json!(1));
setvar(&mut s, "SNAPUNIT", json!([1, 1]));
s.cmdline("line 0,0").unwrap();
let base = Some(v(0.0, 0.0));
// Grid snap (SNAPTYPE 0): the isometric lattice.
let off = v(0.3, 2.2);
let lattice = hover(&mut s, off, base);
assert!(!lattice.near(off, 1e-6), "snapped to the lattice: {lattice:?}");
// PolarSnap: steps of POLARDIST along the polar paths, and the cursor is free off them.
setvar(&mut s, "SNAPTYPE", json!(1));
setvar(&mut s, "POLARDIST", json!(2.5));
let p = hover(&mut s, v(0.02, 6.1), base);
assert!(p.near(v(0.0, 5.0), 1e-9), "PolarSnap along 90°: {p:?}");
let q = hover(&mut s, off, base);
assert!(q.near(off, 1e-9), "off the paths the isometric lattice no longer snaps: {q:?}");
// Back to grid snap: the lattice again.
setvar(&mut s, "SNAPTYPE", json!(0));
assert!(hover(&mut s, off, base).near(lattice, 1e-9));
}
/// The layout viewports with the given handles, in order.
fn viewports(s: &Session, layout: &str, handles: &Value) -> Vec<Viewport> {
let d = s.doc().unwrap();
let st = d.space(&Space::Paper(layout.into())).unwrap();
handles
.as_array()
.unwrap()
.iter()
.map(|h| {
let h = cadcraft_doc::Handle::parse_hex(h.as_str().unwrap()).unwrap();
match &st.get(h).unwrap().kind {
EntityKind::Viewport(v) => v.clone(),
other => panic!("not a viewport: {other:?}"),
}
})
.collect()
}
/// A model space with two side-by-side tiles saved as "Two" (the left tile zoomed in).
fn two_tiles() -> (Session, Vec<VportTile>) {
let mut s = Session::new();
s.execute("line", &json!({ "points": [[0, 0], [100, 50]] })).unwrap();
s.execute("zoom", &json!({ "mode": "extents" })).unwrap();
s.execute("-vports", &json!({ "option": "2", "arrangement": "vertical" })).unwrap();
s.execute("zoom", &json!({ "mode": "center", "center": [10, 10], "height": 8 })).unwrap();
s.execute("-vports", &json!({ "option": "save", "name": "Two" })).unwrap();
let tiles = s.doc().unwrap().vports.iter().find(|c| c.name == "Two").unwrap().tiles.clone();
assert_eq!(tiles.len(), 2);
(s, tiles)
}
fn check_restored(vps: &[Viewport], tiles: &[VportTile], window: Bounds2) {
assert_eq!(vps.len(), tiles.len());
for (vp, t) in vps.iter().zip(tiles) {
let at = |f: Vec2| v(window.min.x + f.x * window.width(), window.min.y + f.y * window.height());
let (lo, hi) = (at(t.min), at(t.max));
assert!(vp.center.xy().near((lo + hi) * 0.5, 1e-9), "{vp:?} {t:?}");
assert!((vp.width - (hi.x - lo.x)).abs() < 1e-9 && (vp.height - (hi.y - lo.y)).abs() < 1e-9);
assert!(vp.view_center.near(t.center, 1e-9), "each viewport shows its tile's view");
assert!(vp.view_height >= t.height - 1e-9);
}
}
#[test]
fn mview_restore_puts_a_saved_configuration_on_a_layout() {
let (mut s, tiles) = two_tiles();
s.execute("layout.set", &json!({ "name": "Layout1" })).unwrap();
let window = Bounds2::new(v(10.0, 10.0), v(210.0, 110.0));
let r = s.execute("mview", &json!({ "option": "restore", "name": "two", "p1": [10, 10], "p2": [210, 110] })).unwrap();
assert_eq!(r["configuration"], "two");
check_restored(&viewports(&s, "Layout1", &r["viewports"]), &tiles, window);
// Typed: name (? lists the saved ones), then two corners.
s.cmdline("mview").unwrap();
s.cmdline("r").unwrap();
assert!(s.prompt_text().contains("Enter viewport configuration name"), "{}", s.prompt_text());
s.cmdline("?").unwrap();
assert!(s.log.iter().any(|l| l.contains("Two")), "{:?}", s.log);
assert!(s.cmdline("nope").is_err() || s.running.is_some(), "an unknown name asks again");
assert!(s.prompt_text().contains("configuration name"), "{}", s.prompt_text());
s.cmdline("Two").unwrap();
s.cmdline("10,120").unwrap();
s.cmdline("110,170").unwrap();
assert!(s.running.is_none());
assert!(s.log.iter().any(|l| l.contains("2 viewport(s) restored")), "{:?}", s.log);
// Enter takes *Active (the tiles on show) and Fit.
let before = s.doc().unwrap().space(&Space::Paper("Layout1".into())).unwrap().iter().count();
s.cmdline("mview").unwrap();
s.cmdline("r").unwrap();
s.cmdline("").unwrap();
s.cmdline("").unwrap();
assert!(s.running.is_none());
let after = s.doc().unwrap().space(&Space::Paper("Layout1".into())).unwrap().iter().count();
assert_eq!(after, before + 2, "the model space shows two tiles");
// Unknown names, model space and hostile windows are refused.
assert!(s.execute("mview", &json!({ "option": "restore", "name": "nope" })).is_err());
assert!(s.execute("mview", &json!({ "option": "restore", "p1": [0, 0], "p2": [0, 5] })).is_err());
assert!(s.execute("mview", &json!({ "option": "restore", "p1": [0, 0], "p2": [1e300, 5] })).is_err());
assert!(s.execute("mview", &json!({ "option": "restore", "layout": "Model" })).is_err());
}
#[test]
fn mview_restore_ignores_unusable_tiles_from_files() {
let (mut s, _) = two_tiles();
let bad = [
VportTile { min: v(f64::NAN, 0.0), max: v(1.0, 1.0), center: v(0.0, 0.0), height: 1.0, aspect: 1.0 },
VportTile { min: v(0.5, 0.5), max: v(0.5, 0.9), center: v(0.0, 0.0), height: 1.0, aspect: 1.0 },
VportTile { min: v(-3.0, -3.0), max: v(9.0, 9.0), center: v(f64::INFINITY, 0.0), height: -1.0, aspect: f64::NAN },
];
if let Some(c) = s.doc_mut().unwrap().vports.iter_mut().find(|c| c.name == "Two") {
c.tiles = bad.to_vec();
}
s.execute("layout.set", &json!({ "name": "Layout1" })).unwrap();
let r = s.execute("mview", &json!({ "option": "restore", "name": "Two", "p1": [0, 0], "p2": [100, 50] })).unwrap();
let vps = viewports(&s, "Layout1", &r["viewports"]);
assert_eq!(vps.len(), 1, "only the third tile has an area (clamped to the window)");
assert!((vps[0].width - 100.0).abs() < 1e-9 && vps[0].view_height.is_finite() && vps[0].view_height > 0.0);
}
+30
View File
@@ -0,0 +1,30 @@
//! Opening a file that isn't a drawing (SVG, read into entities): the drawing gets no file name,
//! so QSAVE asks for one instead of writing CADCraft's SVG export over the original.
use cadcraft_engine::Session;
use cadcraft_engine::cmd::file::{IoHooks, set_io};
use cadcraft_engine::doc::Drawing;
use serde_json::json;
#[test]
fn an_opened_svg_is_not_saved_over() {
set_io(IoHooks { read: |_, _| Ok(Drawing::new_metric()), write: |_, _| Ok(b"exported".to_vec()), plot: None });
let dir = std::env::temp_dir().join(format!("cadcraft-open-svg-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let svg = dir.join("Logo.svg");
std::fs::write(&svg, b"<svg/>").unwrap();
let mut s = Session::empty();
s.execute("open", &json!({"path": svg.to_string_lossy()})).unwrap();
let st = s.state().unwrap();
assert_eq!(st.title, "Logo.svg");
assert_eq!(st.path, None);
assert!(s.execute("qsave", &json!({})).is_err(), "QSAVE asks for a file name");
assert_eq!(std::fs::read(&svg).unwrap(), b"<svg/>", "the original is untouched");
// A drawing file keeps its path.
let dxf = dir.join("Plan.dxf");
std::fs::write(&dxf, b"0\nEOF\n").unwrap();
s.execute("open", &json!({"path": dxf.to_string_lossy()})).unwrap();
assert_eq!(s.state().unwrap().path.as_deref(), Some(dxf.to_string_lossy().as_ref()));
let _ = std::fs::remove_dir_all(&dir);
}
@@ -0,0 +1,52 @@
//! Model space is built on all cores for the screen: the chunks are joined in drawing order, so
//! the display list is exactly what one thread builds (vertex and triangle indices included).
use cadcraft_engine::Session;
use cadcraft_engine::doc::Space;
use cadcraft_engine::render::{DisplayList, Options, build, build_entities};
use serde_json::json;
fn same(a: &DisplayList, b: &DisplayList) {
assert_eq!(a.prims.len(), b.prims.len());
assert!(a.prims == b.prims, "primitives differ");
assert!(a.verts == b.verts, "vertices differ");
assert!(a.tris == b.tris, "triangles differ");
assert_eq!((a.bounds.min, a.bounds.max), (b.bounds.min, b.bounds.max));
assert_eq!(a.expanded, b.expanded);
}
#[test]
fn parallel_model_space_matches_one_thread() {
let mut s = Session::new();
// A block with a hatch in it, inserted many times: block expansion in every chunk.
let r = s.execute("rectang", &json!({"p1": [0, 0], "p2": [4, 3]})).unwrap();
let h = r["handle"].clone();
s.execute("hatch", &json!({ "handles": [h.clone()] })).unwrap();
s.execute("block", &json!({"name": "B", "base": [0, 0], "handles": [h], "keep": "delete"})).unwrap();
// Well over two chunks of mixed entities: lines, circles (curves), text, inserts, points,
// solid fills (triangles), so both vertex and triangle offsets are exercised.
for i in 0..700 {
let x = f64::from(i) * 10.0;
match i % 7 {
0 => s.execute("line", &json!({"points": [[x, 0], [x + 5.0, 5]]})),
1 => s.execute("circle", &json!({"center": [x, 20], "radius": 3})),
2 => s.execute("text", &json!({"at": [x, 40], "height": 2, "text": format!("T{i}")})),
3 => s.execute("insert", &json!({"name": "B", "at": [x, 60]})),
4 => s.execute("point", &json!({"at": [x, 80]})),
5 => {
let r = s.execute("rectang", &json!({"p1": [x, 100], "p2": [x + 4.0, 104]})).unwrap();
s.execute("hatch", &json!({ "handles": [r["handle"].clone()], "pattern": "SOLID" }))
}
_ => s.execute("line", &json!({"points": [[x, 120], [x, 125]]})),
}
.unwrap();
}
let st = s.state().unwrap();
let d = &st.doc;
let n = d.space(&Space::Model).map(|m| m.len()).unwrap_or(0);
assert!(n > 600, "{n} entities: enough for several parallel chunks");
let one_thread = build_entities(d, d.space(&Space::Model).unwrap().iter().map(|e| e.as_ref()), &Options::default());
let parallel = build(d, &Space::Model, &Options::default());
assert!(!parallel.tris.is_empty() && !parallel.verts.is_empty());
same(&parallel, &one_thread);
}
+18
View File
@@ -172,3 +172,21 @@ fn hostile_names_and_values_are_refused() {
assert!(s.execute("plottermanager", &p).is_err(), "{p}");
}
}
#[test]
fn plotwindow_keeps_the_picked_window_for_the_plot_dialog() {
let mut s = Session::new();
// Typed: two corners, with the window's rectangle following the cursor after the first.
s.cmdline("plotwindow").unwrap();
assert_eq!(s.current_prompt().unwrap().message, "Specify first corner");
s.cmdline("10,20").unwrap();
assert_eq!(s.preview(cadcraft_engine::geom::Vec2::new(40.0, 5.0)).len(), 4, "the rubber-band rectangle");
s.cmdline("40,5").unwrap();
assert!(s.running.is_none());
let w = s.plot_window.expect("the picked window");
assert_eq!((w.min.x, w.min.y, w.max.x, w.max.y), (10.0, 5.0, 40.0, 20.0));
// JSON, and a window without area is refused.
let r = s.execute("plotwindow", &json!({"p1": [0, 0], "p2": [3, 4]})).unwrap();
assert_eq!(r["window"], json!([[0.0, 0.0], [3.0, 4.0]]));
assert!(s.execute("plotwindow", &json!({"p1": [0, 0], "p2": [3, 0]})).is_err());
}
+76
View File
@@ -0,0 +1,76 @@
//! PURGE removes multiline styles no multiline uses (#626): not STANDARD, not the current
//! style, not one used by a multiline in model space or in a block that is still inserted.
use std::sync::Arc;
use cadcraft_engine::Session;
use cadcraft_engine::doc::{Block, Common, Entity, EntityKind, Handle, Insert, MLine, MLineJust, MLineStyle, Space};
use cadcraft_engine::geom::{Vec2, Vec3};
use serde_json::json;
fn mline(style: &MLineStyle) -> EntityKind {
EntityKind::MLine(MLine::new(style, &[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0)], false, MLineJust::Zero, 1.0))
}
fn block(name: &str, style: &MLineStyle, handle: u64) -> Arc<Block> {
let mut b = Block::new(name);
b.entities.push(Entity::new(Handle(handle), mline(style)));
Arc::new(b)
}
/// Styles: Model (used in model space), InBlock (used in the inserted block WALLS), Gone (used
/// only in the unreferenced block OLD), Spare (unused), Current (unused but current).
fn session() -> Session {
let mut s = Session::new();
let d = s.doc_mut().unwrap();
let style = |n: &str| MLineStyle { name: n.into(), ..MLineStyle::default() };
for n in ["Model", "InBlock", "Gone", "Spare", "Current"] {
d.mline_styles.push(style(n));
}
d.header.set_str("CMLSTYLE", "Current");
d.add(&Space::Model, Common::default(), mline(&style("Model"))).unwrap();
d.blocks.insert("WALLS".into(), block("WALLS", &style("InBlock"), 0x9000));
d.blocks.insert("OLD".into(), block("OLD", &style("Gone"), 0x9001));
let ins = Insert {
block: "WALLS".into(),
insert: Vec3::ZERO,
scale: Vec3::new(1.0, 1.0, 1.0),
rotation: 0.0,
attribs: Vec::new(),
cols: 1,
rows: 1,
col_spacing: 0.0,
row_spacing: 0.0,
};
d.add(&Space::Model, Common::default(), EntityKind::Insert(ins)).unwrap();
s
}
fn styles(s: &Session) -> Vec<String> {
s.doc().unwrap().mline_styles.iter().map(|m| m.name.clone()).collect()
}
#[test]
fn purge_mlinestyles_keeps_used_standard_and_current() {
let mut s = session();
let r = s.execute("purge", &json!({ "type": "mlinestyles" })).unwrap();
assert_eq!(styles(&s), ["STANDARD", "Model", "InBlock", "Gone", "Current"], "{r}");
assert_eq!(r["purged"]["mlinestyles"], json!(["Spare"]));
}
#[test]
fn purge_all_frees_styles_of_purged_blocks() {
let mut s = session();
s.script("-PURGE A * N\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert!(s.doc().unwrap().block("OLD").is_none());
assert_eq!(styles(&s), ["STANDARD", "Model", "InBlock", "Current"], "{:?}", s.log);
}
#[test]
fn purge_mlinestyles_at_the_command_line() {
let mut s = session();
s.script("-PURGE M Sp* N\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(styles(&s), ["STANDARD", "Model", "InBlock", "Gone", "Current"]);
}
+63 -2
View File
@@ -1,9 +1,9 @@
//! Object snap tracking (OTRACK, F11) and the Extension, Parallel and Apparent Intersection
//! snaps, driven through `Session::snap_cursor` as the UI does, without the UI.
use cadcraft_doc::EntityKind;
use cadcraft_doc::{Common, EntityKind, Insert, Line, Space};
use cadcraft_engine::Session;
use cadcraft_engine::geom::Vec2;
use cadcraft_engine::geom::{Vec2, Vec3};
use cadcraft_engine::snap::tracking::{Acquired, CursorQuery, CursorSnap, DWELL, MAX_ACQUIRED, PathKind, polar_angles};
use cadcraft_engine::snap::{self, mode};
use serde_json::json;
@@ -25,6 +25,28 @@ fn line(s: &mut Session, a: (f64, f64), b: (f64, f64)) {
s.execute("line", &json!({ "points": [[a.0, a.1], [b.0, b.1]] })).unwrap();
}
fn insert_block(s: &mut Session, name: &str, at: Vec2) -> String {
s.doc_mut()
.unwrap()
.add(
&Space::Model,
Common::default(),
EntityKind::Insert(Insert {
block: name.into(),
insert: Vec3::new(at.x, at.y, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
rotation: 0.0,
attribs: Vec::new(),
cols: 1,
rows: 1,
col_spacing: 0.0,
row_spacing: 0.0,
}),
)
.unwrap()
.hex()
}
fn setvar(s: &mut Session, name: &str, value: serde_json::Value) {
s.execute("setvar", &json!({ "name": name, "value": value })).unwrap();
}
@@ -37,6 +59,45 @@ fn at(s: &mut Session, cursor: Vec2, base: Option<Vec2>) -> CursorSnap {
s.snap_cursor(cursor, &query(base, 100.0))
}
#[test]
fn default_cursor_tracking_acquires_nested_block_geometry() {
let mut s = session(mode::END | mode::CEN | mode::INT | mode::EXT);
assert!(s.settings.otrack);
let line = s
.doc_mut()
.unwrap()
.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::new(0.0, 0.0, 0.0), b: Vec3::new(10.0, 0.0, 0.0) }))
.unwrap()
.hex();
s.execute("block", &json!({ "name": "INNER", "base": [0, 0], "handles": [line], "keep": "delete" })).unwrap();
let inner = insert_block(&mut s, "INNER", Vec2::ZERO);
s.execute("block", &json!({ "name": "OUTER", "base": [0, 0], "handles": [inner], "keep": "delete" })).unwrap();
insert_block(&mut s, "OUTER", Vec2::ZERO);
rest(&mut s, v(10.02, 0.02), None);
assert_eq!(s.tracking.acquired.len(), 1);
assert!(matches!(
s.tracking.acquired.first(),
Some(Acquired::Point { at, name, ext: Some(_) }) if at.near(v(10.0, 0.0), 1e-9) && *name == "Endpoint"
));
}
#[test]
fn osnaphatch_geometry_is_not_used_for_tracking_extension() {
let mut s = session(mode::END | mode::EXT);
s.execute("rectang", &json!({ "p1": [0, 0], "p2": [10, 5] })).unwrap();
let boundary = s.doc().unwrap().model.iter().last().unwrap().handle.hex();
s.execute("hatch", &json!({ "handles": [boundary] })).unwrap();
s.execute("erase", &json!({ "handles": [boundary] })).unwrap();
setvar(&mut s, "OSNAPHATCH", json!(1));
rest(&mut s, v(10.02, 4.98), None);
assert_eq!(s.tracking.acquired.len(), 1);
assert!(matches!(s.tracking.acquired.first(), Some(Acquired::Point { ext: None, .. })));
}
/// Rest the cursor at `p` long enough to acquire (or release) what is there, then move off.
fn rest(s: &mut Session, p: Vec2, base: Option<Vec2>) {
s.snap_cursor(p, &query(base, 0.0));
+2 -2
View File
@@ -162,8 +162,8 @@ fn on_off_lock_restore_and_arrangements() {
assert!(viewport(&s, &vh).locked, "{:?}", s.log);
s.execute("mview", &json!({"option": "lock", "locked": false, "handles": [vh]})).unwrap();
assert!(!viewport(&s, &vh).locked);
// Restore needs named viewport configurations.
assert!(s.execute("mview", &json!({"option": "restore"})).is_err());
// Restore needs a viewport configuration that exists (followups_626.rs covers the rest).
assert!(s.execute("mview", &json!({"option": "restore", "name": "none saved"})).is_err());
// 3 viewports arranged Above: the large one on top.
s.cmdline("mview 3 a 200,0 300,100").unwrap();
let vps = layout_viewports(&s);
+11
View File
@@ -213,6 +213,17 @@ pub fn line_width(s: &str, height: f64, width_factor: f64) -> f64 {
(n - stroke::GAP / stroke::CAP * height * width_factor).max(0.0)
}
/// Width of the gap MTEXT leaves between two words. Words are measured without their trailing
/// letter gap, so the stroke font's gap is added on both sides of the space, as TEXT lays it out.
pub fn space_width(font: &TextFont, height: f64, width_factor: f64) -> f64 {
if let TextFont::Outline(face) = font
&& let Some(w) = ttf::width(face, " ", height, width_factor)
{
return w;
}
stroke_advance(' ', height, width_factor) + stroke::GAP / stroke::CAP * height * width_factor
}
/// The font a piece of text is set in: the built-in stroke font or an installed TrueType /
/// OpenType face (outlines). Either way, characters the font lacks come from a fallback font.
#[derive(Clone, Debug, Default)]
+7 -1
View File
@@ -571,7 +571,7 @@ pub fn layout_mtext_with(contents: &str, p: &MTextParams) -> MTextLayout {
}
Atom::Word(segs) => {
let ww: f64 = segs.iter().map(|(sg, f)| seg_width(sg, fmt(*f))).sum();
let sw = pending_space.map(|f| text_width(&fmt(f).font, " ", fmt(f).height, fmt(f).wf)).unwrap_or(0.0);
let sw = pending_space.map(|f| crate::space_width(&fmt(f).font, fmt(f).height, fmt(f).wf)).unwrap_or(0.0);
if width > 0.0 && !line.items.is_empty() && line.width + sw + ww > width + 1e-9 {
finish(&mut lines, &mut line, fmt(last_fmt), para);
} else if !line.items.is_empty() || pending_space.is_some() {
@@ -693,6 +693,12 @@ mod tests {
assert!((c.x - 5.0).abs() < 1e-9 && (c.y - 5.0).abs() < 1e-9);
}
#[test]
fn stroke_word_space_matches_text() {
let l = layout_mtext("A B", Vec2::ZERO, 1.0, 0.0, 1, 0.0, 1.0);
assert!((l.bounds.width() - crate::line_width("A B", 1.0, 1.0)).abs() < 1e-9);
}
fn lay(s: &str, width: f64) -> MTextLayout {
layout_mtext(s, Vec2::ZERO, 1.0, width, 1, 0.0, 1.0)
}
+1 -1
View File
@@ -426,7 +426,7 @@ pub(super) fn layout(contents: &str, p: &MTextParams, base: &Fmt) -> MTextLayout
let paras = Paragraphs::scan(contents);
let (atoms, fmts) = parse(contents, base.clone());
let fmt = |i: usize| fmts.get(i).unwrap_or(base);
let space_w = |f: usize| crate::text_width(&fmt(f).font, " ", fmt(f).height, fmt(f).wf);
let space_w = |f: usize| crate::space_width(&fmt(f).font, fmt(f).height, fmt(f).wf);
let mut lines: Vec<PLine> = Vec::new();
let mut k = 0usize;
+16 -9
View File
@@ -16,7 +16,7 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, RwLock};
use cadcraft_geom::Vec2;
use skrifa::instance::{LocationRef, Size};
@@ -580,7 +580,7 @@ pub fn fallback_for(c: char) -> Option<Face> {
/// font's cap height.
#[derive(Default)]
pub(crate) struct GlyphOutline {
pub(crate) contours: Vec<Vec<Vec2>>,
pub(crate) contours: Arc<[Vec<Vec2>]>,
pub(crate) advance: f64,
pub(crate) cap: f64,
/// The font maps the character (false: the missing-glyph shape).
@@ -598,12 +598,13 @@ enum GlyphKey {
}
/// Cached glyph outlines keyed by (font bytes address, length, face, glyph). Fonts live for the
/// whole process in the font table, so the address identifies them.
/// whole process in the font table, so the address identifies them. A read-write lock: display
/// lists are built on all cores and nearly every lookup is a hit, which a mutex serialized.
type GlyphCache = HashMap<(usize, usize, u32, GlyphKey), Arc<GlyphOutline>>;
fn glyph_cache() -> &'static Mutex<GlyphCache> {
static C: OnceLock<Mutex<GlyphCache>> = OnceLock::new();
C.get_or_init(|| Mutex::new(HashMap::new()))
fn glyph_cache() -> &'static RwLock<GlyphCache> {
static C: OnceLock<RwLock<GlyphCache>> = OnceLock::new();
C.get_or_init(|| RwLock::new(HashMap::new()))
}
const MAX_CACHED_GLYPHS: usize = 40_000;
@@ -691,9 +692,15 @@ pub(crate) fn glyph_by_id(face: &Face, gid: u32) -> Arc<GlyphOutline> {
outline(face, GlyphKey::Id(gid))
}
/// Closed, flattened contours of a glyph at 1000 font units per em. Shared with the outline
/// cache so renderers can tessellate once before applying each occurrence's affine placement.
pub fn glyph_contours(face: &Face, gid: u16) -> Arc<[Vec<Vec2>]> {
glyph_by_id(face, u32::from(gid)).contours.clone()
}
fn outline(face: &Face, k: GlyphKey) -> Arc<GlyphOutline> {
let key = (face.bytes.as_ptr() as usize, face.bytes.len(), face.index, k);
if let Some(g) = glyph_cache().lock().unwrap_or_else(PoisonError::into_inner).get(&key) {
if let Some(g) = glyph_cache().read().unwrap_or_else(PoisonError::into_inner).get(&key) {
return g.clone();
}
let Some(f) = face.font() else { return Arc::new(GlyphOutline { cap: 1.0, ..Default::default() }) };
@@ -709,8 +716,8 @@ fn outline(face: &Face, k: GlyphKey) -> Arc<GlyphOutline> {
}
let advance = f64::from(f.glyph_metrics(Size::new(UPEM), LocationRef::default()).advance_width(gid).unwrap_or(UPEM * 0.5));
let (cap, mapped, gid) = (cap_height(&f), mapped.is_some(), gid.to_u32());
let g = Arc::new(GlyphOutline { contours: pen.contours, advance, cap, mapped, gid, face: Some(face.clone()) });
let mut cache = glyph_cache().lock().unwrap_or_else(PoisonError::into_inner);
let g = Arc::new(GlyphOutline { contours: pen.contours.into(), advance, cap, mapped, gid, face: Some(face.clone()) });
let mut cache = glyph_cache().write().unwrap_or_else(PoisonError::into_inner);
if cache.len() > MAX_CACHED_GLYPHS {
cache.clear();
}
+1
View File
@@ -20,3 +20,4 @@ thiserror = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
miniz_oxide = "0.8"
quick-xml = { workspace = true }
+12 -6
View File
@@ -6,13 +6,14 @@
//!
//! CADCraft's own data lives under the registered application `CADCRAFT` (xdata) and the
//! named-object-dictionary entries `CADCRAFT_CONSTRAINTS`, `CADCRAFT_LAYERSTATES`,
//! `CADCRAFT_PLOTSTYLES` and `CADCRAFT_HANDSEED` (XRECORDs). Other readers keep or ignore them.
//! `CADCRAFT_PLOTSTYLES`, `CADCRAFT_HANDSEED` and `CADCRAFT_MLEADERSTYLE` (XRECORDs). Other
//! readers keep or ignore them.
use std::collections::HashMap;
use cadcraft_color::Color;
use cadcraft_doc::{ArrayParams, AssocSnap, Constraint, DimAssoc, DimStyle, Handle, LayerState, Parametric, PlotStyleTable};
use cadcraft_dxf::Tag;
use cadcraft_dxf::{Tag, Version};
use cadcraft_render::Arrowhead;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -32,6 +33,8 @@ pub(crate) const ANNOTATION_KEY: &str = "CADCRAFT_ANNOTATION";
pub(crate) const PLOT_STYLE_NAMES_KEY: &str = "ACAD_PLOTSTYLENAME";
/// Named-object-dictionary key of the XRECORD holding the drawing's next entity handle (hex).
pub(crate) const HANDSEED_KEY: &str = "CADCRAFT_HANDSEED";
/// Named-object-dictionary key of the XRECORD holding the current multileader style.
pub(crate) const MLEADER_STYLE_KEY: &str = "CADCRAFT_MLEADERSTYLE";
/// Most plot style tables and plot style names read from a file.
pub(crate) const MAX_PLOT_STYLE_TABLES: usize = 1000;
pub(crate) const MAX_PLOT_STYLE_NAMES: usize = 100_000;
@@ -138,11 +141,14 @@ pub(crate) const DIM_CODES: &[(&str, i32, K)] = &[
/// DIMSAH: separate arrow blocks in use.
pub(crate) const DIMSAH: i32 = 173;
/// Variables newer than the DXF version written (R2000): DIMFXL, DIMJOGANG, DIMARCSYM,
/// DIMFXLON, DIMLTYPE, DIMLTEX1, DIMLTEX2. The writer puts them in a style's `CADCRAFT`
/// "DIMVARS" xdata (`1070 <code>` then the value, like DSTYLE) instead of DIMSTYLE groups that
/// an R2000 reader doesn't expect; the reader takes both.
/// Variables newer than R2000: DIMFXL, DIMJOGANG, DIMARCSYM, DIMFXLON, DIMLTYPE, DIMLTEX1,
/// DIMLTEX2. Files of [`NEWER_SINCE`] or later get them as DIMSTYLE groups; R2000 files get them
/// in a style's `CADCRAFT` "DIMVARS" xdata (`1070 <code>` then the value, like DSTYLE) instead of
/// groups an R2000 reader doesn't expect. The reader takes both.
pub(crate) const NEWER_CODES: &[i32] = &[49, 50, 90, 290, 345, 346, 347];
/// The first version with [`NEWER_CODES`] as DIMSTYLE groups: all came with AutoCAD 2006, whose
/// files are the 2004 format (AC1018).
pub(crate) const NEWER_SINCE: Version = Version::R2004;
/// Xdata list name of [`NEWER_CODES`] values.
pub(crate) const DIMVARS_LIST: &str = "DIMVARS";
+24 -2
View File
@@ -23,8 +23,17 @@ use cadcraft_dxf::{Section, Tag};
/// starts with `CADCRAFT`): they are read into the model, not kept raw. A change that starts
/// modelling another entry adds it here. (An entry the writer writes is also never written
/// twice: the kept one is left out.)
const MODELLED_KEYS: &[&str] =
&["ACAD_GROUP", "ACAD_LAYOUT", "ACAD_MLEADERSTYLE", "ACAD_PLOTSTYLENAME", "ACAD_TABLESTYLE", "ACAD_IMAGE_DICT", "ACAD_IMAGE_VARS"];
const MODELLED_KEYS: &[&str] = &[
"ACAD_GROUP",
"ACAD_LAYOUT",
"ACAD_MLEADERSTYLE",
"ACAD_MLINESTYLE",
"ACAD_PLOTSTYLENAME",
"ACAD_SCALELIST",
"ACAD_TABLESTYLE",
"ACAD_IMAGE_DICT",
"ACAD_IMAGE_VARS",
];
/// Extension dictionary entries not kept raw: dimension associativity (CADCraft reads and
/// rewrites it), draw order and annotation scale contexts (written from their own models by the
@@ -589,6 +598,19 @@ impl Map<'_> {
while let Some(r) = o.tags.get(i) {
i += 1;
if r.code == 102 && r.value.trim().starts_with('{') {
// A `{NAME` group is a handle list (`{ACAD_REACTORS 330 … }`) only when every
// group inside it is a reference. Application data groups also use 102 brackets
// with mixed values and their own closing form — the AcDs thumbnail schema's
// `{ATTRRECORD 341 h 2 name 280 1 291 1 ATTRRECORD}` — and collapsing those into
// a handle list rewrote the handle under the LAST code read (291, a bool) and
// dropped the rest, which strict DXF readers reject. Those are written as read,
// references remapped, by the plain path below.
let end = (i..o.tags.len()).find(|&k| o.tags[k].code == 102).unwrap_or(o.tags.len());
let inner = &o.tags[i..end];
if !inner.is_empty() && !inner.iter().all(|n| is_pointer(n.code)) {
out.push(tag(r));
continue;
}
let name = r.value.trim().trim_start_matches('{').to_string();
let mut hs = Vec::new();
let mut code = 330;
+7 -3
View File
@@ -742,9 +742,10 @@ fn hatch(tags: &[Tag], t: &T) -> Option<EntityKind> {
}
let solid = solid || pattern.eq_ignore_ascii_case("SOLID");
let (scale, angle) = (t.fd(41, 1.0), t.fd(52, 0.0).to_radians());
// Definition lines matter only for a pattern the library doesn't define.
let pattern_lines =
if solid || cadcraft_doc::library::pattern(&pattern).is_some() { Vec::new() } else { pattern_definition(tags, scale, angle, origin) };
// Keep the file's definition lines even for a name the library knows: the library's versions
// of some patterns (AR-CONC) only approximate AutoCAD's, and drawing them at the file's scale
// could be hundreds of times denser than the hatch the author saw.
let pattern_lines = if solid { Vec::new() } else { pattern_definition(tags, scale, angle, origin) };
Some(EntityKind::Hatch(Hatch {
pattern: pattern.to_ascii_uppercase(),
solid,
@@ -1843,6 +1844,9 @@ impl Objects {
}
fn apply(self, d: &mut Drawing, rx: &Rx) {
if let Some(style) = self.xrecord_text(crate::dxf_ext::MLEADER_STYLE_KEY).filter(|s| !s.trim().is_empty()) {
d.header.set_str("CMLEADERSTYLE", &style);
}
// Table styles (named by their ACAD_TABLESTYLE dictionary entries).
for (h, tags) in &self.table_styles {
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
+105 -12
View File
@@ -1,6 +1,7 @@
//! Drawing → DXF (AC1015 / R2000 structure, readable by every DXF consumer since 2000). Later
//! versions keep that structure (the groups they add are optional, DXF Reference) and differ in
//! `$ACADVER` and, from 2007, in UTF-8 text; the caller picks the text encoding.
//! `$ACADVER`, from 2004 in the newer DIMSTYLE groups, and from 2007 in UTF-8 text (the caller
//! picks the text encoding) and MULTILEADER entities (older files get LEADER + MTEXT).
use std::collections::HashMap;
@@ -121,9 +122,17 @@ struct Ctx {
block_records: HashMap<String, String>,
/// Raster images' IMAGEDEF, IMAGEDEF_REACTOR and dictionary handles.
images: crate::dxf_image::Plan,
/// The version the file ends up in (the DWG version for a DWG save): it decides which
/// entity types exist (MULTILEADER from 2007).
target: Version,
}
impl Ctx {
/// Whether multileaders are written as MULTILEADER (AutoCAD 2007 / AC1021 and later);
/// older files get LEADER + MTEXT/INSERT.
fn native_mleader(&self) -> bool {
self.target >= Version::R2007
}
fn style(&self, name: &str) -> Option<String> {
self.styles.get(&name.to_ascii_uppercase()).cloned()
}
@@ -833,6 +842,7 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
w.s(1000, c);
}
}
EntityKind::MLeader(m) if !cx.native_mleader() => mleader_fallback(w, d, e, m, owner, paper, cx),
EntityKind::MLeader(m) => {
w.s(0, "MULTILEADER");
common(w, e, owner, paper, "AcDbMLeader");
@@ -914,9 +924,12 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
w.f(52, h.angle.to_degrees());
w.f(41, h.scale);
w.i(77, 0);
let pat = cadcraft_doc::library::pattern(&h.pattern);
// A pattern the library doesn't define keeps the lines read from its file.
let lines = pat.map(|p| p.lines).unwrap_or_else(|| h.pattern_lines.clone());
// The lines read from the file, else the library's pattern of that name.
let lines = if h.pattern_lines.is_empty() {
cadcraft_doc::library::pattern_for(d, &h.pattern).map(|p| p.lines).unwrap_or_default()
} else {
h.pattern_lines.clone()
};
w.i(78, lines.len() as i64);
for pl in lines {
let ang = pl.angle.to_radians() + h.angle;
@@ -1016,6 +1029,36 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
}
}
/// `$DWGCODEPAGE`: before 2007 the code page read from the file when CADCraft knows it (the text
/// is ASCII with `\U+XXXX` escapes, right in any code page, but the file keeps its declared one);
/// otherwise, and from 2007 (UTF-8 text), `ANSI_1252`.
fn code_page(d: &Drawing, version: Version) -> String {
let read = d.header.get("DWGCODEPAGE").and_then(HVal::as_str).map(str::trim).unwrap_or_default();
let known = read.is_ascii() && read.len() <= 32 && cadcraft_dxf::decode_code_page(b"", read).is_some();
if version < Version::R2007 && known { read.to_string() } else { "ANSI_1252".into() }
}
/// A multileader for a file older than 2007 (no MULTILEADER): a LEADER per leader line, ending
/// at the landing, plus its MTEXT or its block's INSERT, with the multileader's properties.
fn mleader_fallback(w: &mut W, d: &Drawing, e: &Entity, m: &MLeader, owner: &str, paper: bool, cx: &Ctx) {
let part = |w: &mut W, kind: EntityKind| Entity { handle: Handle::parse_hex(&w.h()).unwrap_or(Handle(0)), common: e.common.clone(), kind };
let style = d.header.str("DIMSTYLE", "Standard");
for arm in m.leaders.iter().take(64) {
let mut vertices = arm.clone();
vertices.push(m.landing);
let le = part(w, EntityKind::Leader(Leader { vertices, arrow: true, spline: m.spline, style: style.clone() }));
entity(w, d, &le, owner, paper, cx);
}
if let Some(t) = &m.text {
let te = part(w, EntityKind::MText(t.clone()));
entity(w, d, &te, owner, paper, cx);
}
if let Some(ins) = &m.block {
let ie = part(w, EntityKind::Insert(ins.clone()));
entity(w, d, &ie, owner, paper, cx);
}
}
/// A hatch's gradient fill (DXF Reference, HATCH group codes 450–470): two-colour gradient
/// with its rotation (radians), shift (0 = centered, 1 = shifted) and per-colour 463 records
/// carrying ACI (63) and, for true colours, the RGB value (421). Those groups belong to R2004+
@@ -1406,6 +1449,18 @@ pub fn write(d: &Drawing) -> String {
/// The drawing's DXF tags, declaring `version` in `$ACADVER`.
pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
tags_for(d, version, version)
}
/// The R2000 DXF the DWG bridge converts to a `dwg` version DWG: its entity types are the DWG
/// version's (MULTILEADER from 2007).
pub(crate) fn dwg_bridge(d: &Drawing, dwg: Version) -> String {
cadcraft_dxf::write_ascii(&tags_for(d, Version::R2000, dwg))
}
/// The drawing's DXF tags, declaring `version` in `$ACADVER`, with what `target` (the version the
/// file ends up in: `version`, or the DWG version the bridge writes) has.
fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
// Structural handles start above every entity handle.
let mut w = W { t: Vec::new(), next: d.handseed.max(0x100).saturating_add(0x1000), pstyles: HashMap::new() };
// Pre-allocate handles for block records.
@@ -1430,7 +1485,7 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
.collect();
user_blocks.iter_mut().for_each(|b| b.1 = format!("{:X}", 0));
let user_blocks: Vec<(String, String, &Block)> = user_blocks.into_iter().map(|(n, _, b)| (n, w.h(), b)).collect();
let mut cx = Ctx { paper_views: paper_views(d), ..Ctx::default() };
let mut cx = Ctx { paper_views: paper_views(d), target, ..Ctx::default() };
cx.block_records = user_blocks.iter().map(|(n, h, _)| (n.to_ascii_uppercase(), h.clone())).collect();
// Dimension blocks.
let mut dim_defs: Vec<(String, String, Vec<Entity>)> = Vec::new(); // filled below
@@ -1555,6 +1610,9 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
// The drawing's own next handle: table records and objects are numbered above it, so
// reading it back (instead of $HANDSEED) keeps their handles across saves (#495).
xrecords.push((dxf_ext::HANDSEED_KEY, w.h(), vec![format!("{:X}", d.handseed)]));
if let Some(name) = d.header.get("CMLEADERSTYLE").and_then(HVal::as_str) {
xrecords.push((dxf_ext::MLEADER_STYLE_KEY, w.h(), vec![name.to_string()]));
}
if let Some(chunks) = dxf_ext::constraint_chunks(&d.constraints, &d.parametric) {
xrecords.push((dxf_ext::CONSTRAINTS_KEY, w.h(), chunks));
}
@@ -1592,7 +1650,7 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
w.s(9, "$ACADVER");
w.s(1, version.code());
w.s(9, "$DWGCODEPAGE");
w.s(3, "ANSI_1252");
w.s(3, code_page(d, version));
let ext = d.extents(&Space::Model);
if !ext.is_empty() {
w.s(9, "$EXTMIN");
@@ -1601,11 +1659,6 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
w.p(10, ext.max.to3(0.0));
}
header_vars(&mut w, d);
// The current multileader style (an R2007 variable; older readers skip it).
if let Some(name) = d.header.get("CMLEADERSTYLE").and_then(HVal::as_str) {
w.s(9, "$CMLEADERSTYLE");
w.s(2, name);
}
let seed_pos = w.t.len();
w.s(9, "$HANDSEED");
w.s(5, "0");
@@ -1629,7 +1682,7 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
if !cx.assoc.is_empty() {
classes.push(("DIMASSOC", "AcDbDimAssoc", 0, false));
}
if every.iter().any(|e| matches!(e.kind, EntityKind::MLeader(_))) {
if cx.native_mleader() && every.iter().any(|e| matches!(e.kind, EntityKind::MLeader(_))) {
classes.push(("MULTILEADER", "AcDbMLeader", 1025, true));
}
if every.iter().any(|e| matches!(e.kind, EntityKind::Wipeout(_))) {
@@ -1867,6 +1920,11 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
let Some(enc) = v.get(*field).and_then(|x| cx.encode(*kind, x)) else { continue };
if !dxf_ext::NEWER_CODES.contains(code) {
w.dimval(*code, enc, false);
continue;
}
// Table groups where the file version has them, else xdata.
if version >= dxf_ext::NEWER_SINCE {
w.dimval(*code, enc, false);
} else if v.get(*field) != defaults.get(*field) {
newer.i(1070, i64::from(*code));
newer.dimval(*code, enc, true);
@@ -2255,10 +2313,45 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
*t = Tag::s(5, seed);
}
crate::dxf_raw::finish(&mut w.t, d);
add_class_instance_counts(&mut w.t);
mark_xrefs(&mut w.t, d);
w.t
}
/// Write DXF Reference group 91 on every CLASS record. Count records after `dxf_raw::finish`
/// restores foreign classes and objects so every class gets its actual instance count.
fn add_class_instance_counts(tags: &mut Vec<Tag>) {
let mut counts: HashMap<String, i64> = HashMap::new();
for t in tags.iter().filter(|t| t.code == 0) {
let kind = t.str();
if kind != "CLASS" {
*counts.entry(kind.to_ascii_uppercase()).or_default() += 1;
}
}
let mut start = 0;
while start < tags.len() {
if tags.get(start).is_none_or(|t| t.code != 0 || t.str() != "CLASS") {
start += 1;
continue;
}
let end = tags[start + 1..].iter().position(|t| t.code == 0).map_or(tags.len(), |i| start + 1 + i);
let name = tags[start + 1..end].iter().find(|t| t.code == 1).map(Tag::str).unwrap_or_default().to_ascii_uppercase();
let count = counts.get(&name).copied().unwrap_or(0);
let inserted = if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 91) {
tags[start + 1 + i] = Tag::i(91, count);
false
} else if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 90) {
tags.insert(start + 2 + i, Tag::i(91, count));
true
} else {
false
};
// Inserting the group shifts the next record's start by one.
start = end + usize::from(inserted);
}
}
/// Xref blocks (DXF Reference, BLOCK): bit 4 of group 70 and the external drawing's path in
/// group 1, set on the written BLOCK records of blocks that have an `xref_path`.
fn mark_xrefs(tags: &mut [Tag], d: &Drawing) {
+6 -3
View File
@@ -1,4 +1,4 @@
//! CADCraft file formats: DXF read/write, SVG, PNG and PDF export (plotting).
//! CADCraft file formats: DXF/DWG read, SVG and SVGZ import, SVG/PNG/PDF export (plotting).
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
@@ -15,6 +15,7 @@ mod dxf_xref;
pub mod pdf;
pub mod plotter;
pub mod svg;
mod svg_read;
/// Multilines and their styles (MLINE, MLINESTYLE).
mod dxf_mline;
@@ -54,13 +55,15 @@ fn ext(name: &str) -> String {
std::path::Path::new(name).extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default()
}
/// Read a drawing; the format comes from the content (DXF ASCII/binary) or the name.
/// Read a drawing; the format comes from the content (DXF ASCII/binary, DWG) or the name
/// (SVG and gzip-compressed SVGZ).
pub fn read(bytes: &[u8], name: &str) -> Result<Drawing> {
if cadcraft_dwg::is_dwg(bytes) {
let dxf = cadcraft_dwg::dwg_to_dxf(bytes).map_err(IoError::Format)?;
return dxf_read::read(&dxf).map_err(|e| IoError::Format(format!("{e} (in the DXF converted from this DWG)")));
}
match ext(name).as_str() {
"svg" | "svgz" => svg_read::read(bytes),
"dxf" | "" => dxf_read::read(bytes),
e if bytes.len() > 4 => dxf_read::read(bytes).map_err(|_| IoError::Unsupported(e.to_string())),
e => Err(IoError::Unsupported(e.to_string())),
@@ -92,7 +95,7 @@ pub fn write_with(d: &Drawing, name: &str, opts: &SaveOptions) -> Result<Vec<u8>
// gradients, so a drawing from an R2000 DXF doesn't lose them by default.
"dwg" if !opts.binary => {
let version = opts.version.unwrap_or_else(|| version.max(Version::R2004));
cadcraft_dwg::dxf_to_dwg_as(dxf_write::write(d).as_bytes(), version.code()).map_err(IoError::Format)
cadcraft_dwg::dxf_to_dwg_as(dxf_write::dwg_bridge(d, version).as_bytes(), version.code()).map_err(IoError::Format)
}
e if opts.binary => Err(IoError::Format(format!("binary output is for DXF files, not `{e}`"))),
_ => write_framed(d, name, None),
+68 -5
View File
@@ -70,6 +70,12 @@ pub struct PdfOptions {
/// embedded font subset (like AutoCAD's PDFSHX for its own fonts). Stroke-font text is
/// always geometry.
pub text_as_geometry: bool,
/// Margins (left, bottom, right, top; mm) of the plot device's paper, bounding the printable
/// area of a model-space plot. `None` keeps the page setup's (none for model space).
pub margins: Option<[f64; 4]>,
/// Entity hyperlinks read from a drawing file (`PE_URL` extended data) become link
/// annotations too, after [`PdfOptions::links`]. Default on.
pub hyperlinks: bool,
}
impl Default for PdfOptions {
@@ -89,13 +95,15 @@ impl Default for PdfOptions {
include_off_layers: false,
links: Vec::new(),
text_as_geometry: false,
margins: None,
hyperlinks: true,
}
}
}
impl PdfOptions {
/// Parse `{paper?, width?, height?, landscape?, fit?, scale?, lineweights?, compress?, title?,
/// window?: [[x1, y1], [x2, y2]]}`.
/// window?: [[x1, y1], [x2, y2]], margins?: [l, b, r, t] (mm), hyperlinks?: bool}`.
pub fn from_json(v: &Value) -> PdfOptions {
let num = |k: &str| v.get(k).and_then(Value::as_f64).filter(|x| x.is_finite());
let paper_mm = match (num("width"), num("height")) {
@@ -119,10 +127,21 @@ impl PdfOptions {
compress: v.get("compress").and_then(Value::as_bool).unwrap_or(true),
title: v.get("title").and_then(Value::as_str).unwrap_or("").to_string(),
window: window(v.get("window")),
margins: margins(v.get("margins")),
hyperlinks: v.get("hyperlinks").and_then(Value::as_bool).unwrap_or(true),
}
}
}
/// `[l, b, r, t]` margins in mm (finite, 0–1000), else `None`.
pub(crate) fn margins(v: Option<&Value>) -> Option<[f64; 4]> {
let a: Vec<f64> = v?.as_array()?.iter().take(5).filter_map(Value::as_f64).collect();
match a[..] {
[l, b, r, t] if a.iter().all(|x| x.is_finite() && (0.0..=1000.0).contains(x)) => Some([l, b, r, t]),
_ => None,
}
}
/// A plot window from `[[x1, y1], [x2, y2]]`: finite corners and a positive size, else `None`.
fn window(v: Option<&Value>) -> Option<Bounds2> {
let corner = |i: usize| {
@@ -214,6 +233,10 @@ pub fn page_for(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<PageSetup>
if let Some(t) = &o.plot_style_table {
page.plot_style_table = t.clone();
}
// The plot device's margins bound a model-space plot (a layout's come with its page setup).
if let (Space::Model, Some(m)) = (space, o.margins) {
page.margins_mm = m;
}
page.width_mm = page.width_mm.clamp(1.0, MAX_SHEET_MM);
page.height_mm = page.height_mm.clamp(1.0, MAX_SHEET_MM);
Ok(page)
@@ -269,7 +292,8 @@ pub fn pdf_sheets(d: &Drawing, spaces: &[Space], o: &PdfOptions) -> Result<Vec<u
} else {
d
};
let pages = spaces.iter().map(|s| sheet(drawn, s, o)).collect::<Result<Vec<_>>>()?;
let links = all_links(d, o);
let pages = spaces.iter().map(|s| sheet(drawn, s, o, &links)).collect::<Result<Vec<_>>>()?;
let title = if o.title.is_empty() { pages.first().map(|p| p.label.clone()).unwrap_or_default() } else { o.title.clone() };
let fonts = if o.text_as_geometry { fonts::Fonts::default() } else { fonts::Fonts::collect(pages.iter().map(|p| &p.list)) };
Ok(assemble(d, &pages, &fonts, o, &title))
@@ -292,7 +316,7 @@ struct SheetPage {
}
/// Plot one space.
fn sheet(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<SheetPage> {
fn sheet(d: &Drawing, space: &Space, o: &PdfOptions, links_to: &[(Handle, String)]) -> Result<SheetPage> {
let page = page_for(d, space, o)?;
let unit_mm = paper::paper_unit_mm(d);
let sheet = Sheet::from_page(&page, unit_mm);
@@ -301,7 +325,16 @@ fn sheet(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<SheetPage> {
Space::Model => true,
});
let plot_style_table = cadcraft_doc::plot_style_table(d, &page.plot_style_table).map(std::sync::Arc::new);
let ropts = cadcraft_render::Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights, view_height: 0.0, plot_style_table };
let ropts = cadcraft_render::Options {
tolerance: 0.001,
min_dash: 0.0,
text: true,
fill: true,
lineweights,
view_height: 0.0,
plot_style_table,
view: None,
};
let k = unit_mm * PT_PER_MM;
let window = o.window.filter(|w| matches!(space, Space::Model) && !w.is_empty());
let fit = o.fit.unwrap_or(window.is_some() || matches!(space, Space::Model));
@@ -339,10 +372,40 @@ fn sheet(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<SheetPage> {
}
}
}
let links = links(&list, &map, &clip_pt, &o.links);
let links = links(&list, &map, &clip_pt, links_to);
Ok(SheetPage { media, list, map, clip_pt, layers, links, label: sheet_name(space) })
}
/// The hyperlinks to plot: the options' own, then (with [`PdfOptions::hyperlinks`]) those the
/// drawing file gave its entities as `PE_URL` extended data, at most [`MAX_LINKS`] in all.
fn all_links(d: &Drawing, o: &PdfOptions) -> Vec<(Handle, String)> {
let mut out: Vec<(Handle, String)> = o.links.iter().take(MAX_LINKS).cloned().collect();
if !o.hyperlinks {
return out;
}
let given: std::collections::HashSet<Handle> = out.iter().map(|(h, _)| *h).collect();
for (h, extra) in &d.foreign.entities {
if out.len() >= MAX_LINKS {
break;
}
if given.contains(h) {
continue;
}
if let Some(url) = pe_url(&extra.xdata) {
out.push((*h, url));
}
}
out
}
/// The link target of `PE_URL` extended data: the first string (group 1000) after
/// `1001 PE_URL`, before the next application's data.
fn pe_url(xdata: &[cadcraft_doc::RawTag]) -> Option<String> {
let start = xdata.iter().position(|t| t.code == 1001 && t.value.trim().eq_ignore_ascii_case("PE_URL"))?;
let url = xdata.get(start + 1..)?.iter().take_while(|t| t.code != 1001).find(|t| t.code == 1000)?.value.trim();
(!url.is_empty() && url.len() <= MAX_URL).then(|| url.to_string())
}
/// Link annotation rectangles: the area (in points) each linked entity covers on the sheet,
/// clipped to the plot area.
fn links(list: &cadcraft_render::DisplayList, map: &Map, clip_pt: &Bounds2, links: &[(Handle, String)]) -> Vec<(Bounds2, String)> {
+33 -28
View File
@@ -19,9 +19,10 @@ use crate::{IoError, PdfOptions, Result, pdf};
pub const MAX_PNG_PIXELS: f64 = 40.0e6;
/// Plot `space` with JSON options (the engine's `plot` hook): PDF unless `output` is `png` or
/// `svg`. `margins: [l, b, r, t]` (mm) replaces a layout's printable area for this plot.
/// `svg`. `margins: [l, b, r, t]` (mm) bound the printable area of this plot (model space or a
/// layout).
pub fn plot(d: &Drawing, space: &Space, opts: &Value) -> Result<Vec<u8>> {
let margins = margins(opts.get("margins"));
let margins = pdf::margins(opts.get("margins"));
let with_margins;
let d = match (space, margins) {
(Space::Paper(n), Some(m)) => {
@@ -36,28 +37,15 @@ pub fn plot(d: &Drawing, space: &Space, opts: &Value) -> Result<Vec<u8>> {
};
match opts.get("output").and_then(Value::as_str).map(str::to_ascii_lowercase).as_deref() {
None | Some("pdf") => pdf::plot(d, space, opts),
Some("png") => png(d, space, opts, margins),
Some("png") => png(d, space, opts),
Some("svg") => svg(d, space, opts),
Some(other) => Err(IoError::Unsupported(other.to_string())),
}
}
/// `[l, b, r, t]` margins in mm (finite, 0–1000), else `None`.
fn margins(v: Option<&Value>) -> Option<[f64; 4]> {
let a: Vec<f64> = v?.as_array()?.iter().filter_map(Value::as_f64).collect();
match a[..] {
[l, b, r, t] if a.iter().all(|x| x.is_finite() && (0.0..=1000.0).contains(x)) => Some([l, b, r, t]),
_ => None,
}
}
/// The sheet a plot draws on, and whether the drawing is fitted to its printable area (model
/// space, or a layout asked to `fit`).
fn sheet(d: &Drawing, space: &Space, o: &PdfOptions, margins: Option<[f64; 4]>) -> Result<(Sheet, cadcraft_doc::PageSetup)> {
let mut page = pdf::page_for(d, space, o)?;
if let (Space::Model, Some(m)) = (space, margins) {
page.margins_mm = m;
}
/// The sheet a plot draws on (a model-space plot's printable area inside the device margins).
fn sheet(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<(Sheet, cadcraft_doc::PageSetup)> {
let page = pdf::page_for(d, space, o)?;
Ok((Sheet::from_page(&page, paper::paper_unit_mm(d)), page))
}
@@ -75,9 +63,9 @@ fn fit_scale(b: &Bounds2, sheet: &Sheet, o: &PdfOptions) -> f64 {
if s.is_finite() && s > 0.0 { s } else { 1.0 }
}
fn png(d: &Drawing, space: &Space, opts: &Value, margins: Option<[f64; 4]>) -> Result<Vec<u8>> {
fn png(d: &Drawing, space: &Space, opts: &Value) -> Result<Vec<u8>> {
let o = PdfOptions::from_json(opts);
let (sheet, page) = sheet(d, space, &o, margins)?;
let (sheet, page) = sheet(d, space, &o)?;
let dpi = opts.get("resolution").and_then(Value::as_f64).filter(|r| r.is_finite()).unwrap_or(300.0).clamp(50.0, 2400.0);
// Pixels per paper unit, lowered so the image stays within the caps.
let mut k = sheet.unit_mm / 25.4 * dpi;
@@ -101,11 +89,14 @@ fn png(d: &Drawing, space: &Space, opts: &Value, margins: Option<[f64; 4]>) -> R
// Chord tolerance: a fifth of a pixel.
let tolerance = (0.2 / (k * est)).max(1e-9);
let ropts = cadcraft_render::Options { tolerance, lineweights, plot_style_table: table, ..cadcraft_render::Options::default() };
let list = cadcraft_render::build_plot(d, space, &ropts);
let mut list = cadcraft_render::build_plot(d, space, &ropts);
let half = Vec2::new(f64::from(width), f64::from(height)) / (2.0 * k);
let b = window.unwrap_or(list.bounds);
let s = if fit { fit_scale(&b, &sheet, &o) } else { 1.0 };
// Plot style ends, joins and fill patterns at the plot scale, as PDF draws them (a layout's
// are already drawn).
cadcraft_render::pen::apply(&mut list, s * sheet.unit_mm);
let view = if fit {
let b = window.unwrap_or(list.bounds);
let s = fit_scale(&b, &sheet, &o);
let from = if b.is_empty() { Vec2::ZERO } else { b.center() };
// The world point at the image centre, with `from` on the printable area's centre.
let center = from + (half - sheet.printable.center()) / s;
@@ -120,11 +111,25 @@ fn png(d: &Drawing, space: &Space, opts: &Value, margins: Option<[f64; 4]>) -> R
fn svg(d: &Drawing, space: &Space, opts: &Value) -> Result<Vec<u8>> {
let o = PdfOptions::from_json(opts);
let window = match space {
Space::Paper(_) => Some(sheet(d, space, &o, None)?.0.bounds()),
Space::Model => o.window.filter(|w| !w.is_empty()),
let Space::Model = space else {
let window = sheet(d, space, &o)?.0.bounds();
return Ok(crate::svg::export_window(d, space, Some(window)).into_bytes());
};
Ok(crate::svg::export_window(d, space, window).into_bytes())
// Model space as plotted: the plot style table, lineweights, and the table's ends, joins and
// fills at the scale that fits the window (or the extents) on the device's paper.
let (sheet, page) = sheet(d, space, &o)?;
let window = o.window.filter(|w| !w.is_empty());
let table = cadcraft_doc::plot_style_table(d, &page.plot_style_table).map(Arc::new);
let lineweights = o.lineweights.unwrap_or(true);
let est = fit_scale(&window.unwrap_or_else(|| d.extents(space)), &sheet, &o);
// Chord tolerance: about 0.05 mm on paper.
let tolerance = (0.05 / (sheet.unit_mm * est)).max(1e-9);
let tolerance = if tolerance.is_finite() { tolerance } else { cadcraft_render::Options::default().tolerance };
let ropts = cadcraft_render::Options { tolerance, lineweights, plot_style_table: table, ..cadcraft_render::Options::default() };
let mut list = cadcraft_render::build_plot(d, space, &ropts);
let s = fit_scale(&window.unwrap_or(list.bounds), &sheet, &o);
cadcraft_render::pen::apply(&mut list, s * sheet.unit_mm);
Ok(crate::svg::export_list(&list, window).into_bytes())
}
#[cfg(test)]
+5
View File
@@ -16,6 +16,11 @@ pub fn export(d: &Drawing, space: &Space) -> String {
/// or the extents with a 2% margin when `window` is `None`.
pub fn export_window(d: &Drawing, space: &Space, window: Option<Bounds2>) -> String {
let list = cadcraft_render::build(d, space, &cadcraft_render::Options::default());
export_list(&list, window)
}
/// Write a display list (as [`export_window`] does for a space's own).
pub fn export_list(list: &cadcraft_render::DisplayList, window: Option<Bounds2>) -> String {
let window = window.filter(|w| !w.is_empty());
let b = window.unwrap_or(list.bounds);
let (w, h) = if b.is_empty() { (1.0, 1.0) } else { (b.width().max(1e-9), b.height().max(1e-9)) };
+945
View File
@@ -0,0 +1,945 @@
//! SVG → Drawing: the static W3C SVG subset that drawing tools emit.
//!
//! Reads shapes (`rect`, `circle`, `ellipse`, `line`, `polyline`, `polygon`), `path`
//! outlines (`M L H V C S Q T A Z`, absolute and relative), `g` grouping with `transform`
//! attributes, `viewBox` and unit-bearing `width`/`height`, and Inkscape layers
//! (`inkscape:groupmode="layer"`). Geometry becomes centre-line entities in millimetres with
//! the page's top-left corner at the origin and Y pointing up — where a laser or pen job
//! wants it.
//!
//! Not read (each is a follow-up): CSS class rules (`<style>`), `use` references, fills,
//! gradients and hatch patterns, text, embedded raster images, rounded rectangle corners
//! (`rx`/`ry`), and elliptical arcs under non-uniform transforms (those are flattened into
//! line segments instead of losing the shape).
use std::collections::HashSet;
use cadcraft_color::Color;
use cadcraft_doc::*;
use cadcraft_geom::{Mat3, PolyVertex, Spline, Vec2, Vec3};
use quick_xml::Reader;
use quick_xml::XmlVersion;
use quick_xml::events::{BytesStart, Event};
use crate::{IoError, Result};
/// A decompressed SVGZ payload larger than this is refused (decompression bomb guard).
const MAX_DECOMPRESSED: usize = 64 << 20;
/// Shape elements per file (rect, circle, path, …; each counted once).
const MAX_ELEMENTS: usize = 500_000;
/// Path data segments per `d` attribute.
const MAX_PATH_SEGMENTS: usize = 100_000;
/// Vertices in one polyline, and control points in one spline.
const MAX_CURVE_POINTS: usize = 100_001;
/// Layers created from Inkscape layer groups.
const MAX_LAYERS: usize = 256;
/// Nested `<g>`/`<svg>` transforms.
const MAX_TRANSFORM_DEPTH: usize = 64;
/// Flattened chords per elliptical arc under a non-uniform transform.
const MAX_ARC_CHORDS: usize = 10_000;
/// Elements whose whole subtree is skipped (paint servers, text, raster images, metadata).
const SKIP: &[&[u8]] = &[
b"defs",
b"symbol",
b"clipPath",
b"mask",
b"pattern",
b"marker",
b"linearGradient",
b"radialGradient",
b"filter",
b"style",
b"text",
b"image",
b"title",
b"desc",
b"metadata",
];
/// Elements that hold children and push a transform stack frame.
const CONTAINERS: &[&[u8]] = &[b"g", b"svg", b"a", b"switch"];
/// Read an SVG document, or a gzip-compressed one (`.svgz`, Inkscape's "Compressed SVG").
pub fn read(bytes: &[u8]) -> Result<Drawing> {
let unpacked;
let plain: &[u8] = if bytes.starts_with(&[0x1f, 0x8b]) {
unpacked = gunzip(bytes)?;
&unpacked
} else {
bytes
};
let text = std::str::from_utf8(plain).map_err(|_| IoError::Format("SVG files must be UTF-8".into()))?;
let mut parser = Parser::new();
parser.parse(text)?;
Ok(parser.drawing)
}
/// Decompress a gzip member (RFC 1952) with a hard output cap.
fn gunzip(bytes: &[u8]) -> Result<Vec<u8>> {
let damaged = || IoError::Format("this SVGZ file's gzip stream is damaged".into());
if bytes.get(2) != Some(&8) {
return Err(damaged());
}
let flg = bytes.get(3).copied().unwrap_or(0);
let mut pos = 10usize;
if flg & 0x04 != 0 {
// FEXTRA
let extra = bytes.get(pos..pos + 2).ok_or_else(damaged)?;
pos += 2 + usize::from(u16::from_le_bytes([extra[0], extra[1]]));
}
if flg & 0x08 != 0 {
pos = skip_cstring(bytes, pos).ok_or_else(damaged)?; // FNAME
}
if flg & 0x10 != 0 {
pos = skip_cstring(bytes, pos).ok_or_else(damaged)?; // FCOMMENT
}
if flg & 0x02 != 0 {
pos = pos.saturating_add(2); // FHCRC
}
let end = bytes.len().checked_sub(8).filter(|e| *e >= pos).ok_or_else(damaged)?;
miniz_oxide::inflate::decompress_to_vec_with_limit(&bytes[pos..end], MAX_DECOMPRESSED).map_err(|_| damaged())
}
/// Skip a NUL-terminated field; `None` when the terminator is missing.
fn skip_cstring(bytes: &[u8], from: usize) -> Option<usize> {
let rest = bytes.get(from..)?;
let nul = rest.iter().position(|b| *b == 0)?;
Some(from + nul + 1)
}
fn parse_err<E: std::fmt::Display>(e: E) -> IoError {
IoError::Format(format!("SVG: {e}"))
}
fn v3(p: Vec2) -> Vec3 {
Vec3::new(p.x, p.y, 0.0)
}
struct Parser {
drawing: Drawing,
/// Root viewBox/unit mapping composed with every open container transform.
stack: Vec<Mat3>,
/// Current Inkscape layer name.
layer: String,
layers: HashSet<String>,
layer_color: u8,
elements: usize,
/// > 0 while inside a skipped subtree; counts the starts seen there.
skip: usize,
}
impl Parser {
fn new() -> Self {
Parser {
drawing: Drawing::new_metric(),
stack: vec![Mat3::IDENTITY],
layer: "0".into(),
layers: HashSet::from(["0".to_string()]),
layer_color: 1,
elements: 0,
skip: 0,
}
}
fn parse(&mut self, text: &str) -> Result<()> {
let mut reader = Reader::from_str(text);
reader.config_mut().trim_text(true);
loop {
match reader.read_event().map_err(parse_err)? {
Event::Start(e) => {
let name = e.name();
self.on_start(name.as_ref(), &e)?;
}
Event::Empty(e) => {
let name = e.name();
self.on_element(name.as_ref(), &e)?;
}
Event::End(e) => {
let name = e.name();
self.on_end(name.as_ref());
}
Event::Eof => break,
_ => {}
}
}
Ok(())
}
/// A start tag: open a transform frame, descend into a skipped subtree, or treat a
/// shape written as start+end the same as a self-closed one.
fn on_start(&mut self, name: &[u8], e: &BytesStart) -> Result<()> {
if self.skip > 0 {
self.skip += 1;
return Ok(());
}
if SKIP.contains(&name) {
self.skip = 1;
return Ok(());
}
if hidden(e) {
self.skip = 1;
return Ok(());
}
if CONTAINERS.contains(&name) { self.enter_group(name, e) } else { self.on_element(name, e) }
}
fn on_element(&mut self, name: &[u8], e: &BytesStart) -> Result<()> {
if self.skip > 0 {
return Ok(());
}
if SKIP.contains(&name) || hidden(e) {
return Ok(());
}
match name {
b"rect" => self.rect(e),
b"circle" => self.circle(e),
b"ellipse" => self.ellipse(e),
b"line" => self.line(e),
b"polyline" => self.poly_points(e, false),
b"polygon" => self.poly_points(e, true),
b"path" => self.path(e),
_ => Ok(()),
}
}
fn on_end(&mut self, name: &[u8]) {
if self.skip > 0 {
self.skip -= 1;
} else if CONTAINERS.contains(&name) && self.stack.len() > 1 {
self.stack.pop();
}
}
fn enter_group(&mut self, name: &[u8], e: &BytesStart) -> Result<()> {
if name == b"svg" && self.stack.len() == 1 {
self.stack[0] = viewport(e)?;
}
if name == b"g" && attr(e, b"inkscape:groupmode").as_deref() == Some("layer") {
let raw = attr(e, b"inkscape:label").or_else(|| attr(e, b"id"));
if let Some(raw) = raw {
self.set_layer(&raw);
}
}
if self.stack.len() >= MAX_TRANSFORM_DEPTH {
return Err(IoError::Format(format!("SVG: more than {MAX_TRANSFORM_DEPTH} nested groups")));
}
let current = *self.stack.last().unwrap_or(&Mat3::IDENTITY);
let next = match attr(e, b"transform").as_deref().and_then(parse_transform) {
Some(local) => current.then_before(local),
None => current,
};
self.stack.push(next);
Ok(())
}
fn set_layer(&mut self, raw: &str) {
let name = sanitize_layer(raw);
if !self.layers.contains(&name) {
if self.layers.len() >= MAX_LAYERS {
return;
}
let color = Color::Index(self.layer_color);
self.layer_color = if self.layer_color >= 7 { 1 } else { self.layer_color + 1 };
self.drawing.layers.push(Layer { name: name.clone(), color, ..Layer::default() });
self.layers.insert(name.clone());
}
self.layer = name;
}
/// The composed mapping for the current nesting: SVG user units → drawing units (mm).
fn cur(&self) -> Mat3 {
*self.stack.last().unwrap_or(&Mat3::IDENTITY)
}
fn map(&self, p: Vec2) -> Vec2 {
self.cur().apply(p)
}
fn map_vec(&self, v: Vec2) -> Vec2 {
self.cur().apply_vec(v)
}
fn add(&mut self, kind: EntityKind) -> Result<()> {
self.elements += 1;
if self.elements > MAX_ELEMENTS {
return Err(IoError::Format(format!("SVG: more than {MAX_ELEMENTS} elements")));
}
let common = Common { layer: self.layer.clone(), ..Common::default() };
self.drawing.add(&Space::Model, common, kind).map(|_| ()).map_err(parse_err)
}
// --- shapes -------------------------------------------------------------
fn rect(&mut self, e: &BytesStart) -> Result<()> {
let x = num(e, b"x", 0.0);
let y = num(e, b"y", 0.0);
let w = num(e, b"width", 0.0);
let h = num(e, b"height", 0.0);
if w <= 0.0 || h <= 0.0 {
return Ok(());
}
let pts = [Vec2::new(x, y), Vec2::new(x + w, y), Vec2::new(x + w, y + h), Vec2::new(x, y + h)];
self.add_polyline(&pts.map(|p| self.map(p)), true)
}
fn circle(&mut self, e: &BytesStart) -> Result<()> {
let r = num(e, b"r", 0.0);
if r <= 0.0 {
return Ok(());
}
self.add_rounded(Vec2::new(num(e, b"cx", 0.0), num(e, b"cy", 0.0)), r, r)
}
fn ellipse(&mut self, e: &BytesStart) -> Result<()> {
let rx = num(e, b"rx", 0.0);
let ry = num(e, b"ry", 0.0);
if rx <= 0.0 || ry <= 0.0 {
return Ok(());
}
self.add_rounded(Vec2::new(num(e, b"cx", 0.0), num(e, b"cy", 0.0)), rx, ry)
}
/// A circle or ellipse through a transform. A circle stays a `CIRCLE` under rotations,
/// uniform scales and reflections; anything else becomes an `ELLIPSE` holding the axes
/// of the transformed shape — a linear map sends the unit circle to an ellipse whose
/// semi-axes are the square roots of the eigenvalues of `A·Aᵀ`.
fn add_rounded(&mut self, center_user: Vec2, rx: f64, ry: f64) -> Result<()> {
let center = self.map(center_user);
let bx = self.map_vec(Vec2::new(rx, 0.0));
let by = self.map_vec(Vec2::new(0.0, ry));
let len = |v: Vec2| v.x.hypot(v.y);
let (lbx, lby, dot) = (len(bx), len(by), bx.x * by.x + bx.y * by.y);
if lbx.is_finite() && lby.is_finite() && (lbx - lby).abs() <= 1e-9 * (lbx + lby) && dot.abs() <= 1e-9 * lbx * lby {
return self.add(EntityKind::Circle(Circle { center: v3(center), radius: lbx }));
}
let xx = bx.x * bx.x + bx.y * bx.y;
let yy = by.x * by.x + by.y * by.y;
let xy = bx.x * by.x + bx.y * by.y;
let trace = xx + yy;
let disc = (trace * trace - 4.0 * (xx * yy - xy * xy)).max(0.0).sqrt();
let l_max = 0.5 * (trace + disc);
let l_min = 0.5 * (trace - disc);
let s_max = l_max.max(0.0).sqrt();
if s_max <= f64::EPSILON {
return Ok(());
}
let axis = if xy.abs() <= 1e-12 * trace.max(1.0) {
if yy > xx { Vec2::new(0.0, 1.0) } else { Vec2::new(1.0, 0.0) }
} else {
let v = Vec2::new(xy, l_max - xx);
let l = len(v);
if l <= f64::EPSILON {
return Ok(());
}
v / l
};
let ratio = (l_min.max(0.0).sqrt() / s_max).clamp(1e-9, 1.0);
let major = center + axis * s_max;
self.add(EntityKind::Ellipse(Ellipse { center: v3(center), major: v3(major), ratio, start: 0.0, end: std::f64::consts::TAU }))
}
fn line(&mut self, e: &BytesStart) -> Result<()> {
let a = Vec2::new(num(e, b"x1", 0.0), num(e, b"y1", 0.0));
let b = Vec2::new(num(e, b"x2", 0.0), num(e, b"y2", 0.0));
self.add(EntityKind::Line(Line { a: v3(self.map(a)), b: v3(self.map(b)) }))
}
fn poly_points(&mut self, e: &BytesStart, closed: bool) -> Result<()> {
let Some(raw) = attr(e, b"points") else {
return Ok(());
};
let nums: Vec<f64> = Numbers::new(&raw).collect();
let mut pts: Vec<Vec2> = nums.as_chunks::<2>().0.iter().map(|[x, y]| self.map(Vec2::new(*x, *y))).collect();
if pts.len() > MAX_CURVE_POINTS {
pts.truncate(MAX_CURVE_POINTS);
}
self.add_polyline(&pts, closed)
}
fn add_polyline(&mut self, pts: &[Vec2], closed: bool) -> Result<()> {
if pts.len() < 2 {
return Ok(());
}
let vertices = pts.iter().map(|p| PolyVertex::new(*p)).collect();
self.add(EntityKind::LwPolyline(LwPolyline { vertices, closed, const_width: 0.0, elevation: 0.0, plinegen: false }))
}
// --- path data ----------------------------------------------------------
fn path(&mut self, e: &BytesStart) -> Result<()> {
let Some(d) = attr(e, b"d") else {
return Ok(());
};
for sp in parse_path(&d)? {
self.emit_subpath(&sp)?;
}
Ok(())
}
/// Emit one subpath: runs of lines and arcs become an `LWPOLYLINE` (an arc becomes a
/// bulge vertex, exactly like DXF's polyline bulge), runs of Bézier curves become a
/// `SPLINE`. Under a non-uniform transform a circular arc is no longer circular, so it
/// is flattened into line vertices instead.
fn emit_subpath(&mut self, sp: &Subpath) -> Result<()> {
let m = self.cur();
let det = m.det();
let ex = m.apply_vec(Vec2::new(1.0, 0.0));
let ey = m.apply_vec(Vec2::new(0.0, 1.0));
let mag = (ex.x * ex.x + ex.y * ex.y + ey.x * ey.x + ey.y * ey.y).max(1.0);
let uniform =
(ex.x * ex.x + ex.y * ex.y - (ey.x * ey.x + ey.y * ey.y)).abs() <= 1e-9 * mag && (ex.x * ey.x + ex.y * ey.y).abs() <= 1e-9 * mag;
let scale = det.abs().sqrt();
let mut run: Vec<PolyVertex> = Vec::new();
let mut ctrl: Vec<Vec2> = Vec::new();
let mut cur_user = sp.start;
let mut cur = m.apply(sp.start);
for seg in &sp.segs {
match seg {
Seg::Line(p) => {
if run.is_empty() {
run.push(PolyVertex::new(cur));
}
cur = m.apply(*p);
run.push(PolyVertex::new(cur));
cur_user = *p;
}
Seg::Cubic(c1, c2, p) => {
self.flush_run(&mut run, false)?;
if ctrl.is_empty() {
ctrl.push(cur);
}
ctrl.push(m.apply(*c1));
ctrl.push(m.apply(*c2));
cur = m.apply(*p);
ctrl.push(cur);
cur_user = *p;
if ctrl.len() > MAX_CURVE_POINTS {
return Err(IoError::Format("SVG: one path has too many curve points".into()));
}
}
Seg::Arc { rx, ry, rot, large, sweep, end } => {
if uniform && scale > 0.0 {
let r = rx * scale;
let flipped = det < 0.0;
if r > 0.0
&& let Some((_, delta)) = arc_center_circular(cur, m.apply(*end), r, *large, *sweep != flipped)
{
if run.is_empty() {
run.push(PolyVertex::new(cur));
}
if let Some(last) = run.last_mut() {
last.bulge = (delta / 4.0).tan();
}
run.push(PolyVertex::new(m.apply(*end)));
cur = m.apply(*end);
cur_user = *end;
}
} else if *rx > 0.0 && *ry > 0.0 {
self.flush_run(&mut run, false)?;
self.flush_ctrl(&mut ctrl)?;
for p in flatten_arc(&m, cur_user, *end, *rx, *ry, *rot, *large, *sweep)? {
if run.is_empty() {
run.push(PolyVertex::new(cur));
}
run.push(PolyVertex::new(p));
}
cur = m.apply(*end);
cur_user = *end;
}
}
}
if run.len() > MAX_CURVE_POINTS {
return Err(IoError::Format("SVG: one path has too many vertices".into()));
}
}
self.flush_run(&mut run, sp.closed)?;
self.flush_ctrl(&mut ctrl)?;
Ok(())
}
fn flush_run(&mut self, run: &mut Vec<PolyVertex>, closed: bool) -> Result<()> {
if run.len() >= 2 {
let vertices = std::mem::take(run);
self.add(EntityKind::LwPolyline(LwPolyline { vertices, closed, const_width: 0.0, elevation: 0.0, plinegen: false }))?;
} else {
run.clear();
}
Ok(())
}
fn flush_ctrl(&mut self, ctrl: &mut Vec<Vec2>) -> Result<()> {
if ctrl.len() >= 4 {
let spline = Spline::from_control(std::mem::take(ctrl), 3);
self.add(EntityKind::Spline(spline))?;
} else {
ctrl.clear();
}
Ok(())
}
}
/// The element is not rendered (`display="none"`, or a style that says so).
fn hidden(e: &BytesStart) -> bool {
if attr(e, b"display").as_deref() == Some("none") {
return true;
}
attr(e, b"style").is_some_and(|s| s.replace(' ', "").contains("display:none"))
}
fn attr(e: &BytesStart, name: &[u8]) -> Option<String> {
for a in e.attributes().flatten() {
if a.key.as_ref() == name {
return a.normalized_value(XmlVersion::Explicit1_0).ok().map(|v| v.into_owned());
}
}
None
}
fn num(e: &BytesStart, name: &[u8], fallback: f64) -> f64 {
attr(e, name).as_deref().and_then(parse_length_mm).unwrap_or(fallback)
}
/// A bare number, or a number with an absolute CSS/SVG unit: millimetres here, everything
/// else (px, the default; pt, pc, in, cm) through the CSS reference pixel (96 per inch).
fn parse_length_mm(s: &str) -> Option<f64> {
let s = s.trim();
let cut = s.find(|c: char| !(c.is_ascii_digit() || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E')).unwrap_or(s.len());
let (n, unit) = s.split_at(cut);
let value: f64 = n.trim().parse().ok()?;
if !value.is_finite() {
return None;
}
match unit.trim() {
"" | "px" => Some(value),
"mm" => Some(value),
"cm" => Some(value * 10.0),
"in" => Some(value * 25.4),
"pt" => Some(value * 25.4 / 72.0),
"pc" => Some(value * 25.4 / 6.0),
_ => Some(value * 25.4 / 96.0),
}
}
/// The root mapping: `viewBox` and unit-bearing `width`/`height` become millimetres per user
/// unit, and the page's Y axis is flipped so the drawing keeps its appearance with Y up.
fn viewport(e: &BytesStart) -> Result<Mat3> {
let vb = attr(e, b"viewBox").and_then(|raw| {
let nums: Vec<f64> = Numbers::new(&raw).collect();
match nums.as_slice() {
[x, y, w, h] if w.is_finite() && h.is_finite() && *w > 0.0 && *h > 0.0 => Some((*x, *y, *w, *h)),
_ => None,
}
});
let w_mm = attr(e, b"width").as_deref().and_then(parse_length_mm);
let h_mm = attr(e, b"height").as_deref().and_then(parse_length_mm);
let unit = match (vb, w_mm, h_mm) {
(Some((_, _, vw, _)), Some(w), _) if w > 0.0 => w / vw,
(Some((_, _, _, vh)), _, Some(h)) if h > 0.0 => h / vh,
(Some(_), _, _) => 25.4 / 96.0,
(None, _, _) => 25.4 / 96.0,
};
let (vx, vy, vh) = match vb {
Some((x, y, _, h)) => (x, y, h),
None => (0.0, 0.0, 0.0),
};
Ok(Mat3 { a: unit, b: 0.0, c: 0.0, d: -unit, e: -vx * unit, f: (vy + vh) * unit })
}
/// `transform="…"`: a list of functions, applied left to right.
fn parse_transform(s: &str) -> Option<Mat3> {
let mut out: Option<Mat3> = None;
let mut rest = s.trim();
while !rest.is_empty() {
let open = rest.find('(')?;
let name = rest[..open].trim();
let close = rest[open..].find(')')? + open;
let args: Vec<f64> = Numbers::new(&rest[open + 1..close]).collect();
let m = match (name, args.as_slice()) {
("matrix", [a, b, c, d, e, f]) => Mat3 { a: *a, b: *b, c: *c, d: *d, e: *e, f: *f },
("translate", [x]) => Mat3::translate(Vec2::new(*x, 0.0)),
("translate", [x, y]) => Mat3::translate(Vec2::new(*x, *y)),
("scale", [s]) => Mat3::scale(*s, *s),
("scale", [sx, sy]) => Mat3::scale(*sx, *sy),
("rotate", [a]) => Mat3::rotate(a.to_radians()),
("rotate", [a, cx, cy]) => Mat3::rotate_about(Vec2::new(*cx, *cy), a.to_radians()),
("skewX", [a]) => Mat3 { a: 1.0, b: 0.0, c: a.to_radians().tan(), d: 1.0, e: 0.0, f: 0.0 },
("skewY", [a]) => Mat3 { a: 1.0, b: a.to_radians().tan(), c: 0.0, d: 1.0, e: 0.0, f: 0.0 },
_ => return None,
};
out = Some(match out {
Some(prev) => prev.then_before(m),
None => m,
});
rest = rest[close + 1..].trim_start_matches(|c: char| c == ',' || c.is_whitespace());
}
out
}
fn sanitize_layer(raw: &str) -> String {
let cleaned: String =
raw.trim().chars().map(|c| if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '$') { c } else { '_' }).take(128).collect();
let cleaned = cleaned.trim_matches('_').to_string();
if cleaned.is_empty() { "SVG".into() } else { cleaned }
}
/// A scanner over an SVG number list: separators are whitespace and commas, and a sign
/// starts a new number, so `10-20` is two numbers and `1e-5 2` is two numbers.
struct Numbers<'a> {
s: &'a [u8],
i: usize,
}
impl<'a> Numbers<'a> {
fn new(s: &'a str) -> Self {
Numbers { s: s.as_bytes(), i: 0 }
}
fn skip_seps(&mut self) {
while matches!(self.s.get(self.i), Some(b' ' | b'\t' | b'\r' | b'\n' | b',')) {
self.i += 1;
}
}
fn next_num(&mut self) -> Option<f64> {
self.skip_seps();
let start = self.i;
if matches!(self.s.get(self.i), Some(b'+' | b'-')) {
self.i += 1;
}
let mut digits = 0;
let mut dot = false;
let mut exp = false;
while let Some(c) = self.s.get(self.i) {
match c {
b'0'..=b'9' => {
digits += 1;
self.i += 1;
}
b'.' if !dot && !exp => {
dot = true;
self.i += 1;
}
b'e' | b'E' if !exp && digits > 0 => {
exp = true;
self.i += 1;
if matches!(self.s.get(self.i), Some(b'+' | b'-')) {
self.i += 1;
}
}
_ => break,
}
}
if digits == 0 {
self.i = start;
return None;
}
std::str::from_utf8(&self.s[start..self.i]).ok()?.parse::<f64>().ok().filter(|v| v.is_finite())
}
}
impl Iterator for Numbers<'_> {
type Item = f64;
fn next(&mut self) -> Option<f64> {
self.next_num()
}
}
/// One `M …` run of a `d` attribute.
struct Subpath {
start: Vec2,
segs: Vec<Seg>,
closed: bool,
}
enum Seg {
Line(Vec2),
Cubic(Vec2, Vec2, Vec2),
Arc { rx: f64, ry: f64, rot: f64, large: bool, sweep: bool, end: Vec2 },
}
/// Tokens of a `d` attribute: command letters and numbers, in order.
enum Tok {
Cmd(u8),
Num(f64),
}
struct Tokenizer<'a> {
s: &'a [u8],
i: usize,
}
impl<'a> Tokenizer<'a> {
fn new(s: &'a str) -> Self {
Tokenizer { s: s.as_bytes(), i: 0 }
}
fn next_tok(&mut self) -> Option<Tok> {
let mut nums = Numbers { s: self.s, i: self.i };
nums.skip_seps();
self.i = nums.i;
let c = self.s.get(self.i).copied()?;
if c.is_ascii_alphabetic() {
self.i += 1;
return Some(Tok::Cmd(c));
}
let tok = nums.next_num().map(Tok::Num);
self.i = nums.i;
tok
}
}
/// Split `d` into subpaths, resolving relative commands and the S/T control reflections.
fn parse_path(d: &str) -> Result<Vec<Subpath>> {
let mut tk = Tokenizer::new(d);
let mut lookahead: Option<Tok> = None;
let mut cmd = 0u8;
let mut cur = Vec2::ZERO;
let mut start = Vec2::ZERO;
let mut segs: Vec<Seg> = Vec::new();
let mut out: Vec<Subpath> = Vec::new();
let closed = false;
let mut prev_cubic: Option<Vec2> = None;
let mut prev_quad: Option<Vec2> = None;
let mut segments = 0usize;
let next_num = |tk: &mut Tokenizer, look: &mut Option<Tok>| -> Result<f64> {
match look.take() {
Some(Tok::Num(v)) => Ok(v),
Some(other @ Tok::Cmd(_)) => {
*look = Some(other);
Err(IoError::Format("SVG: path command is missing coordinates".into()))
}
None => match tk.next_tok() {
Some(Tok::Num(v)) => Ok(v),
Some(other @ Tok::Cmd(_)) => {
*look = Some(other);
Err(IoError::Format("SVG: path command is missing coordinates".into()))
}
None => Err(IoError::Format("SVG: path command is missing coordinates".into())),
},
}
};
while let Some(tok) = lookahead.take().or_else(|| tk.next_tok()) {
let rel = match tok {
Tok::Cmd(c) => {
if c.eq_ignore_ascii_case(&b'z') {
if !segs.is_empty() {
out.push(Subpath { start, segs, closed: true });
segs = Vec::new();
}
cur = start;
cmd = 0;
continue;
}
cmd = c;
c.is_ascii_lowercase()
}
tok @ Tok::Num(_) => {
if cmd == 0 {
return Err(IoError::Format("SVG: path data does not start with a command".into()));
}
lookahead = Some(tok);
cmd.is_ascii_lowercase()
}
};
let abs = cmd.to_ascii_uppercase();
let count = match abs {
b'M' | b'L' => 2,
b'H' | b'V' | b'T' => 1,
b'C' => 6,
b'S' | b'Q' => 4,
b'A' => 7,
_ => return Err(IoError::Format(format!("SVG: unknown path command `{}`", cmd as char))),
};
let mut args = [0.0f64; 7];
for slot in args.iter_mut().take(count) {
*slot = next_num(&mut tk, &mut lookahead)?;
}
let point = |x: f64, y: f64| if rel { cur + Vec2::new(x, y) } else { Vec2::new(x, y) };
match abs {
b'M' => {
if !segs.is_empty() {
out.push(Subpath { start, segs, closed: false });
segs = Vec::new();
}
cur = point(args[0], args[1]);
start = cur;
cmd = if rel { b'l' } else { b'L' };
prev_cubic = None;
prev_quad = None;
}
b'L' => {
cur = point(args[0], args[1]);
segs.push(Seg::Line(cur));
prev_cubic = None;
prev_quad = None;
}
b'H' => {
cur = Vec2::new(if rel { cur.x + args[0] } else { args[0] }, cur.y);
segs.push(Seg::Line(cur));
prev_cubic = None;
prev_quad = None;
}
b'V' => {
cur = Vec2::new(cur.x, if rel { cur.y + args[0] } else { args[0] });
segs.push(Seg::Line(cur));
prev_cubic = None;
prev_quad = None;
}
b'C' => {
let (c1, c2, p) = (point(args[0], args[1]), point(args[2], args[3]), point(args[4], args[5]));
segs.push(Seg::Cubic(c1, c2, p));
prev_cubic = Some(c2);
prev_quad = None;
cur = p;
}
b'S' => {
let c1 = prev_cubic.map_or(cur, |c2| cur * 2.0 - c2);
let (c2, p) = (point(args[0], args[1]), point(args[2], args[3]));
segs.push(Seg::Cubic(c1, c2, p));
prev_cubic = Some(c2);
prev_quad = None;
cur = p;
}
b'Q' => {
let (q, p) = (point(args[0], args[1]), point(args[2], args[3]));
segs.push(Seg::Cubic(cur + (q - cur) * (2.0 / 3.0), p + (q - p) * (2.0 / 3.0), p));
prev_quad = Some(q);
prev_cubic = None;
cur = p;
}
b'T' => {
let reflect = prev_quad.map_or(cur, |q| cur * 2.0 - q);
let p = point(args[0], args[1]);
segs.push(Seg::Cubic(cur + (reflect - cur) * (2.0 / 3.0), p + (reflect - p) * (2.0 / 3.0), p));
prev_quad = Some(reflect);
prev_cubic = None;
cur = p;
}
b'A' => {
let end = point(args[5], args[6]);
segs.push(Seg::Arc { rx: args[0].abs(), ry: args[1].abs(), rot: args[2], large: args[3] != 0.0, sweep: args[4] != 0.0, end });
cur = end;
prev_cubic = None;
prev_quad = None;
}
_ => return Err(IoError::Format("SVG: unsupported path command".into())),
}
segments += 1;
if segments > MAX_PATH_SEGMENTS {
return Err(IoError::Format("SVG: path has too many segments".into()));
}
if segs.len() > MAX_CURVE_POINTS {
return Err(IoError::Format("SVG: path has too many segments".into()));
}
}
if !segs.is_empty() {
out.push(Subpath { start, segs, closed });
}
Ok(out)
}
/// The centre and signed sweep of a circular arc from `p0` to `p1` in a Y-up frame, per the
/// SVG endpoint parameterisation (F.6.5). Radii that cannot span the chord are scaled up,
/// as the specification requires.
fn arc_center_circular(p0: Vec2, p1: Vec2, r: f64, large: bool, sweep: bool) -> Option<(Vec2, f64)> {
if r <= 0.0 || !r.is_finite() {
return None;
}
let d = p1 - p0;
let dist = d.x.hypot(d.y);
let r = if dist > 2.0 * r { dist / 2.0 } else { r };
if dist <= f64::EPSILON {
return None;
}
let mid = (p0 + p1) * 0.5;
let h = (r * r - (dist * dist) / 4.0).max(0.0).sqrt();
let n = Vec2::new(-d.y / dist, d.x / dist);
let sign = if large != sweep { 1.0 } else { -1.0 };
let center = mid + n * (h * sign);
let a0 = (p0.y - center.y).atan2(p0.x - center.x);
let a1 = (p1.y - center.y).atan2(p1.x - center.x);
let ccw = (a1 - a0).rem_euclid(std::f64::consts::TAU);
Some((center, pick_sweep(ccw, large, sweep)))
}
/// The signed sweep (CCW positive) for a chord sweep `ccw` in `[0, 2π)`: the centre is
/// already chosen so that the magnitude matches `large`, so only the direction is left.
fn pick_sweep(ccw: f64, large: bool, sweep: bool) -> f64 {
let tau = std::f64::consts::TAU;
let eps = f64::EPSILON;
if sweep {
if ccw <= eps { if large { tau } else { eps } } else { ccw }
} else if ccw >= tau - eps {
if large { -tau } else { -eps }
} else {
ccw - tau
}
}
/// Flatten a general elliptical arc (any `rx`/`ry`/rotation) from `p0` to `p1` in user
/// space, then map every sample through `m`.
fn flatten_arc(m: &Mat3, p0: Vec2, p1: Vec2, rx: f64, ry: f64, rot_deg: f64, large: bool, sweep: bool) -> Result<Vec<Vec2>> {
let (c, theta, delta) = elliptical_params(p0, p1, rx, ry, rot_deg.to_radians(), large, sweep)
.ok_or_else(|| IoError::Format("SVG: elliptical arc cannot be converted".into()))?;
let axes = Mat3::rotate(rot_deg.to_radians());
let rmax = rx.max(ry);
let tol = 0.05_f64;
let steps = if rmax > tol {
let step = 2.0 * (1.0 - (tol / rmax).min(1.0)).acos().max(1e-6);
((delta.abs() / step).ceil() as usize).clamp(8, MAX_ARC_CHORDS)
} else {
8
};
let mut out = Vec::with_capacity(steps + 1);
for k in 0..=steps {
let t = theta + delta * (k as f64) / (steps as f64);
let local = Vec2::new(rx * t.cos(), ry * t.sin());
out.push(m.apply(c + axes.apply_vec(local)));
}
Ok(out)
}
/// Endpoint → centre parameterisation for a rotated elliptical arc (SVG F.6.5), in the
/// frame the points are given in. Returns the centre, the start parameter and the signed
/// sweep.
fn elliptical_params(p0: Vec2, p1: Vec2, rx: f64, ry: f64, phi: f64, large: bool, sweep: bool) -> Option<(Vec2, f64, f64)> {
if rx <= 0.0 || ry <= 0.0 {
return None;
}
let (sphi, cphi) = phi.sin_cos();
let dx = (p0.x - p1.x) / 2.0;
let dy = (p0.y - p1.y) / 2.0;
let x1 = cphi * dx + sphi * dy;
let y1 = -sphi * dx + cphi * dy;
let (rx, ry) = {
let lambda = x1 * x1 / (rx * rx) + y1 * y1 / (ry * ry);
if lambda > 1.0 {
let s = lambda.sqrt();
(rx * s, ry * s)
} else {
(rx, ry)
}
};
let num = rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1;
let den = rx * rx * y1 * y1 + ry * ry * x1 * x1;
let coef = if sweep != large { 1.0 } else { -1.0 } * (num.max(0.0) / den.max(f64::MIN_POSITIVE)).sqrt();
let cxp = coef * rx * y1 / ry;
let cyp = -coef * ry * x1 / rx;
let center = Vec2::new(cphi * cxp - sphi * cyp + (p0.x + p1.x) / 2.0, sphi * cxp + cphi * cyp + (p0.y + p1.y) / 2.0);
let angle = |ux: f64, uy: f64, vx: f64, vy: f64| {
let dot = ux * vx + uy * vy;
let det = ux * vy - uy * vx;
let denom = (ux.hypot(uy) * vx.hypot(vy)).max(f64::MIN_POSITIVE);
(dot / denom).clamp(-1.0, 1.0).acos().copysign(det)
};
let (ux, uy) = ((x1 - cxp) / rx, (y1 - cyp) / ry);
let (vx, vy) = ((-x1 - cxp) / rx, (-y1 - cyp) / ry);
let theta = angle(1.0, 0.0, ux, uy);
let ccw = angle(ux, uy, vx, vy).rem_euclid(std::f64::consts::TAU);
Some((center, theta, pick_sweep(ccw, large, sweep)))
}
+192 -1
View File
@@ -1379,7 +1379,10 @@ fn mleader_is_written_as_multileader() {
block: None,
};
d.add(&Space::Model, Default::default(), EntityKind::MLeader(m.clone())).unwrap();
let back = roundtrip(&d);
// MULTILEADER is a 2007 entity (R2000/2004 files get LEADER + MTEXT).
let back =
crate::read(&crate::write_with(&d, "x.dxf", &crate::SaveOptions { version: Some(crate::Version::R2007), binary: false }).unwrap(), "x.dxf")
.unwrap();
assert!(back.model.iter().any(|e| matches!(&e.kind, EntityKind::MLeader(x) if *x == m)));
assert!(!back.model.iter().any(|e| matches!(e.kind, EntityKind::Leader(_) | EntityKind::MText(_))));
}
@@ -1821,3 +1824,191 @@ fn mirrored_mtext_extrusion_keeps_its_footprint() {
let b = cadcraft_render::build(&d, &Space::Model, &cadcraft_render::Options::default()).bounds;
assert!(b.min.x > -0.2 && b.max.x > 0.5 && b.min.y > -0.2 && b.max.y > 0.5, "box right of and above (0,0): {b:?}");
}
// --- SVG import ---
/// One 20mm×10mm page whose user units are millimetres; the reader flips Y, so a point at
/// SVG y lands at `10 - y` in the drawing.
const SVG_PAGE: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="20mm" height="10mm" viewBox="0 0 20 10">"#;
fn kinds(d: &Drawing) -> Vec<&'static str> {
d.model.iter().map(|e| e.kind.type_name()).collect()
}
fn gzip_of(data: &[u8]) -> Vec<u8> {
let mut crc: u32 = 0xffff_ffff;
for b in data {
crc ^= u32::from(*b);
for _ in 0..8 {
crc = (crc >> 1) ^ (0xedb8_8320u32 & 0u32.wrapping_sub(crc & 1));
}
}
let mut out = vec![0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 3];
out.extend_from_slice(&miniz_oxide::deflate::compress_to_vec(data, 6));
out.extend_from_slice(&(!crc).to_le_bytes());
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
out
}
#[test]
fn svg_shapes_become_entities_in_millimetres() {
let svg = format!(
"{SVG_PAGE}<rect x=\"1\" y=\"1\" width=\"4\" height=\"3\"/><circle cx=\"10\" cy=\"5\" r=\"2\"/>\
<ellipse cx=\"10\" cy=\"5\" rx=\"4\" ry=\"2\"/><line x1=\"0\" y1=\"0\" x2=\"20\" y2=\"10\"/>\
<polyline points=\"0,0 5,0 5,5\"/><polygon points=\"0,0 2,0 1,1\"/></svg>"
);
let d = read(svg.as_bytes(), "shapes.svg").unwrap();
assert_eq!(kinds(&d), ["Polyline", "Circle", "Ellipse", "Line", "Polyline", "Polyline"]);
let r = first(&d, |k| if let EntityKind::LwPolyline(p) = k { Some(p.clone()) } else { None });
assert!(r.closed);
let got: Vec<Vec2> = r.vertices.iter().map(|v| v.p).collect();
let want = [Vec2::new(1.0, 9.0), Vec2::new(5.0, 9.0), Vec2::new(5.0, 6.0), Vec2::new(1.0, 6.0)];
for (g, w) in got.iter().zip(want.iter()) {
assert!((*g - *w).len() < 1e-9, "{g:?} vs {w:?}");
}
let c = first(&d, |k| if let EntityKind::Circle(c) = k { Some(c.clone()) } else { None });
assert!((c.radius - 2.0).abs() < 1e-9 && (c.center.x - 10.0).abs() < 1e-9 && (c.center.y - 5.0).abs() < 1e-9, "{c:?}");
let e = first(&d, |k| if let EntityKind::Ellipse(e) = k { Some(e.clone()) } else { None });
let major = Vec2::new(e.major.x - e.center.x, e.major.y - e.center.y);
assert!((major.len() - 4.0).abs() < 1e-9 && (e.ratio - 0.5).abs() < 1e-9, "{e:?}");
let l = first(&d, |k| if let EntityKind::Line(l) = k { Some(l.clone()) } else { None });
assert!((l.a.y - 10.0).abs() < 1e-9 && (l.b.y).abs() < 1e-9, "{l:?}");
}
#[test]
fn svg_path_lines_curves_and_arcs() {
let svg = format!("{SVG_PAGE}<path d=\"M1 1 L5 1 C6 1 7 2 8 3\"/><path d=\"M0 0 A5 5 0 0 1 10 0 Z\"/></svg>");
let d = read(svg.as_bytes(), "paths.svg").unwrap();
assert_eq!(kinds(&d), ["Polyline", "Spline", "Polyline"]);
let line_run = first(&d, |k| if let EntityKind::LwPolyline(p) = k { Some(p.clone()) } else { None });
assert!(!line_run.closed && line_run.vertices.len() == 2, "{line_run:?}");
let spline = first(&d, |k| if let EntityKind::Spline(s) = k { Some(s.clone()) } else { None });
assert!(spline.is_valid(), "{spline:?}");
let arc = d
.model
.iter()
.find_map(|e| match &e.kind {
EntityKind::LwPolyline(p) if p.vertices.first().is_some_and(|v| v.bulge != 0.0) => Some(p.clone()),
_ => None,
})
.expect("an arc becomes a bulge vertex");
assert!(arc.closed && arc.vertices.len() == 2, "{arc:?}");
let bulge = arc.vertices[0].bulge;
assert!((bulge.abs() - 1.0).abs() < 1e-6, "semicircle bulge is ±1, got {bulge}");
}
#[test]
fn svg_group_transforms_apply() {
let svg = format!("{SVG_PAGE}<g transform=\"translate(5,2)\"><line x1=\"0\" y1=\"0\" x2=\"10\" y2=\"0\"/></g></svg>");
let d = read(svg.as_bytes(), "t.svg").unwrap();
let l = first(&d, |k| if let EntityKind::Line(l) = k { Some(l.clone()) } else { None });
assert!((l.a - Vec3::new(5.0, 8.0, 0.0)).len() < 1e-9, "{l:?}");
assert!((l.b - Vec3::new(15.0, 8.0, 0.0)).len() < 1e-9, "{l:?}");
}
#[test]
fn svg_viewbox_scaling_and_mm_units() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="20mm" height="10mm" viewBox="0 0 200 100"><circle cx="100" cy="50" r="50"/></svg>"#;
let d = read(svg.as_bytes(), "vb.svg").unwrap();
let c = first(&d, |k| if let EntityKind::Circle(c) = k { Some(c.clone()) } else { None });
assert!((c.radius - 5.0).abs() < 1e-9, "{c:?}");
assert!((c.center.x - 10.0).abs() < 1e-9 && (c.center.y - 5.0).abs() < 1e-9, "{c:?}");
}
#[test]
fn svg_inkscape_layers_are_kept() {
let svg = format!(
"{SVG_PAGE}<g inkscape:groupmode=\"layer\" inkscape:label=\"Cut\"><rect x=\"0\" y=\"0\" width=\"2\" height=\"2\"/></g>\
<g inkscape:groupmode=\"layer\" inkscape:label=\"Engrave\"><circle cx=\"5\" cy=\"5\" r=\"1\"/></g></svg>",
SVG_PAGE = SVG_PAGE.replace("<svg ", "<svg xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" ")
);
let d = read(svg.as_bytes(), "layers.svg").unwrap();
let layers: Vec<&str> = d.layers.iter().map(|l| l.name.as_str()).collect();
assert!(layers.contains(&"Cut") && layers.contains(&"Engrave"), "{layers:?}");
let per_layer = |name: &str| d.model.iter().filter(|e| e.common.layer == name).count();
assert_eq!(per_layer("Cut"), 1);
assert_eq!(per_layer("Engrave"), 1);
}
#[test]
fn svg_hidden_and_defs_subtrees_are_skipped() {
let svg = format!(
"{SVG_PAGE}<defs><rect x=\"0\" y=\"0\" width=\"9\" height=\"9\"/></defs><text x=\"0\" y=\"0\">hi</text>\
<rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" display=\"none\"/><rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" style=\"fill:red;display:none\"/>\
<rect x=\"1\" y=\"1\" width=\"2\" height=\"2\"/></svg>"
);
let d = read(svg.as_bytes(), "skip.svg").unwrap();
assert_eq!(kinds(&d), ["Polyline"]);
}
#[test]
fn svgz_gunzips_to_the_same_drawing() {
let svg = format!("{SVG_PAGE}<rect x=\"1\" y=\"1\" width=\"4\" height=\"3\"/></svg>");
let plain = read(svg.as_bytes(), "a.svg").unwrap();
let packed = read(&gzip_of(svg.as_bytes()), "a.svgz").unwrap();
assert_eq!(kinds(&plain), kinds(&packed));
assert_eq!(plain.model.iter().map(|e| &e.kind).collect::<Vec<_>>(), packed.model.iter().map(|e| &e.kind).collect::<Vec<_>>());
}
#[test]
fn svg_read_dispatches_by_extension() {
let svg = format!("{SVG_PAGE}<circle cx=\"5\" cy=\"5\" r=\"1\"/></svg>");
assert!(read(svg.as_bytes(), "a.svg").is_ok());
assert!(read(&gzip_of(svg.as_bytes()), "a.svgz").is_ok());
}
#[test]
fn svg_hostile_input_errors_without_panicking() {
let deep = format!("{SVG_PAGE}{}<line x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"/>{}</svg>", "<g>".repeat(200), "</g>".repeat(200));
for (bytes, name) in [
(b"not xml at all".to_vec(), "a.svg"),
(Vec::new(), "a.svg"),
(b"<svg><rect x=\"1e309\" width=\"NaN\" height=\"inf\"/></svg>".to_vec(), "a.svg"),
(deep.clone().into_bytes(), "a.svg"),
(b"<svg><path d=\"M0 0 X5 5\"/></svg>".to_vec(), "a.svg"),
(b"<svg><path d=\"M0 0 L\"/></svg>".to_vec(), "a.svg"),
(b"<svg><path d=\"M0 0 A1 1 0 5 1 2 2\"/></svg>".to_vec(), "a.svg"),
(b"<svg><polyline points=\"1 2 3\"/></svg>".to_vec(), "a.svg"),
(b"<svg><g transform=\"bogus(1)\"><rect width=\"1\" height=\"1\"/></g></svg>".to_vec(), "a.svg"),
(vec![0x1f, 0x8b, 8, 0, b'j', b'u', b'n', b'k', 0, 0, 0, 0, 0, 0, 0, 0], "a.svgz"),
(vec![0xff, 0xfe, 0x00, 0x01, 0x02, 0x03], "a.svg"),
] {
let _ = read(&bytes, name);
}
let deep = read(deep.as_bytes(), "a.svg");
assert!(deep.is_err(), "200 nested groups are refused, not walked");
}
#[test]
fn svg_no_geometry_yields_an_empty_drawing() {
let svg = format!("{SVG_PAGE}</svg>");
let d = read(svg.as_bytes(), "empty.svg").unwrap();
assert!(kinds(&d).is_empty());
}
/// An application data group inside a kept XRECORD — the AcDs thumbnail schema's
/// `{ATTRRECORD 341 h 2 name 280 1 291 1 ATTRRECORD}` — is written back as read. It used to be
/// collapsed into a handle list: the handle came out under group 291 (a bool) and the name and
/// flags were dropped, which strict DXF readers (ezdxf) reject outright.
#[test]
fn kept_xrecord_application_group_roundtrips_as_read() {
let dxf = "0\nSECTION\n2\nOBJECTS\n\
0\nDICTIONARY\n5\nC\n330\n0\n100\nAcDbDictionary\n3\nTD_SCHEMA\n350\n20\n\
0\nXRECORD\n5\n20\n330\nC\n100\nAcDbXrecord\n280\n1\n1\nAcDb_Thumbnail_Schema\n\
102\n{ATTRRECORD\n341\n21\n2\nAcDbDs::TreatedAsObjectData\n280\n1\n291\n1\n102\nATTRRECORD}\n\
0\nXRECORD\n5\n21\n330\n20\n100\nAcDbXrecord\n280\n1\n1\nTarget\n\
0\nENDSEC\n0\nEOF\n";
let d = read_dxf(dxf.as_bytes()).expect("read");
let out = write_dxf(&d);
let lines: Vec<&str> = out.lines().map(str::trim).collect();
let at = lines.iter().position(|l| *l == "{ATTRRECORD").expect("group kept");
// The group keeps its own shape: a reference under 341, then the name, flag and bool.
assert_eq!(lines[at + 1], "341", "reference group code: {:?}", &lines[at..at + 12]);
assert!(lines[at + 2] != "0" && !lines[at + 2].is_empty());
assert_eq!(&lines[at + 3..at + 5], &["2", "AcDbDs::TreatedAsObjectData"]);
assert_eq!(&lines[at + 5..at + 7], &["280", "1"]);
assert_eq!(&lines[at + 7..at + 9], &["291", "1"]);
assert_eq!(&lines[at + 9..at + 11], &["102", "ATTRRECORD}"]);
// And no handle is written under a bool code anywhere.
assert!(!out.contains("291\n2"), "no handle may be written under a bool code");
}
+219
View File
@@ -0,0 +1,219 @@
//! File-format follow-ups (#626): MULTILEADER only from 2007 (older files get LEADER + MTEXT /
//! INSERT, DXF and DWG), post-R2000 DIMSTYLE variables as table groups from 2004, `$DWGCODEPAGE`
//! kept as read in pre-2007 files, and named-dictionary entries the writer writes itself not
//! kept a second time.
use cadcraft_doc::*;
use cadcraft_geom::{Vec2, Vec3};
use cadcraft_io::{SaveOptions, Version};
fn save(d: &Drawing, name: &str, v: Version) -> Vec<u8> {
cadcraft_io::write_with(d, name, &SaveOptions { version: Some(v), binary: false }).unwrap()
}
fn dxf(d: &Drawing, v: Version) -> String {
String::from_utf8(save(d, "x.dxf", v)).unwrap()
}
fn lines(text: &str) -> Vec<&str> {
text.lines().map(str::trim).collect()
}
/// Number of records of `kind` (`0` / `kind` pairs) in DXF text.
fn records(text: &str, kind: &str) -> usize {
lines(text).chunks(2).filter(|p| *p == ["0", kind]).count()
}
fn count(d: &Drawing, f: impl Fn(&EntityKind) -> bool) -> usize {
d.model.iter().filter(|e| f(&e.kind)).count()
}
fn mleader_drawing() -> (Drawing, MLeader) {
let mut d = Drawing::new_metric();
let m = MLeader {
leaders: vec![vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 3.0, 0.0)], vec![Vec3::new(1.0, -2.0, 0.0)]],
landing: Vec3::new(5.0, 4.0, 0.0),
dogleg: 0.75,
text: Some(MText {
insert: Vec3::new(5.9, 4.0, 0.0),
height: 0.4,
width: 6.0,
attach: 4,
rotation: 0.0,
style: "Standard".into(),
contents: "NOTE".into(),
line_spacing: 1.0,
line_spacing_exact: false,
columns: None,
}),
style: "Standard".into(),
arrow_size: 1.0,
spline: false,
block: None,
};
d.add(&Space::Model, Common { layer: "0".into(), ..Common::default() }, EntityKind::MLeader(m.clone())).unwrap();
(d, m)
}
#[test]
fn multileader_before_2007_is_leader_and_mtext() {
let (d, _) = mleader_drawing();
for v in [Version::R2000, Version::R2004] {
let text = dxf(&d, v);
assert_eq!(records(&text, "MULTILEADER"), 0, "{v:?}: no MULTILEADER entity");
assert!(!text.contains("AcDbMLeader\n"), "{v:?}: no MULTILEADER class");
assert_eq!(records(&text, "LEADER"), 2, "{v:?}: one LEADER per leader line");
let back = cadcraft_io::read_dxf(text.as_bytes()).unwrap();
assert_eq!(count(&back, |k| matches!(k, EntityKind::MLeader(_))), 0);
assert_eq!(count(&back, |k| matches!(k, EntityKind::Leader(_))), 2);
assert_eq!(count(&back, |k| matches!(k, EntityKind::MText(t) if t.contents == "NOTE")), 1);
// The leader lines end at the landing.
assert!(back.model.iter().all(|e| match &e.kind {
EntityKind::Leader(l) => l.vertices.last() == Some(&Vec3::new(5.0, 4.0, 0.0)),
_ => true,
}));
}
}
#[test]
fn multileader_from_2007_is_native() {
let (d, m) = mleader_drawing();
for v in [Version::R2007, Version::R2018] {
let text = dxf(&d, v);
assert_eq!(records(&text, "MULTILEADER"), 1, "{v:?}");
assert_eq!(records(&text, "LEADER"), 0, "{v:?}");
let back = cadcraft_io::read_dxf(text.as_bytes()).unwrap();
let got: Vec<&MLeader> = back.model.iter().filter_map(|e| if let EntityKind::MLeader(m) = &e.kind { Some(m) } else { None }).collect();
assert_eq!(got, vec![&m], "{v:?}");
}
}
#[test]
fn multileader_in_dwg_follows_the_dwg_version() {
let (d, _) = mleader_drawing();
let old = cadcraft_io::read(&save(&d, "x.dwg", Version::R2004), "x.dwg").unwrap();
assert_eq!(count(&old, |k| matches!(k, EntityKind::MLeader(_))), 0, "2004 DWG has no MULTILEADER");
assert_eq!(count(&old, |k| matches!(k, EntityKind::Leader(_))), 2);
assert_eq!(count(&old, |k| matches!(k, EntityKind::MText(_))), 1);
let new = cadcraft_io::read(&save(&d, "x.dwg", Version::R2018), "x.dwg").unwrap();
assert_eq!(count(&new, |k| matches!(k, EntityKind::Leader(_))), 0, "2018 DWG keeps the multileader whole");
}
fn dim_drawing() -> (Drawing, DimStyle) {
let mut d = Drawing::new_imperial();
d.linetypes.push(Linetype::simple("DASHED", "Dashed", &[0.5, -0.25]));
d.linetypes.push(Linetype::simple("CENTER", "Center", &[1.25, -0.25, 0.25, -0.25]));
let st = DimStyle {
name: "Detail".into(),
arc_symbol: 1,
dim_linetype: "DASHED".into(),
ext_linetype1: "CENTER".into(),
ext_linetype2: "DASHED".into(),
fixed_ext: true,
fixed_ext_length: 0.75,
jog_angle: 30.0,
..DimStyle::default()
};
d.dim_styles.push(st.clone());
(d, st)
}
/// The DIMSTYLE record named `name`.
fn dimstyle_record<'a>(text: &'a str, name: &str) -> Vec<&'a str> {
let l = lines(text);
let at = l.windows(2).position(|w| w == ["2", name]).unwrap();
let end = l[at..].windows(2).position(|w| w == ["0", "DIMSTYLE"] || w == ["0", "ENDTAB"]).map_or(l.len(), |e| at + e);
l[at..end].to_vec()
}
#[test]
fn newer_dimstyle_variables_are_table_groups_from_2004() {
let (d, st) = dim_drawing();
let r2000 = dxf(&d, Version::R2000);
let rec = dimstyle_record(&r2000, "Detail");
assert!(rec.windows(4).any(|w| w == ["1001", "CADCRAFT", "1000", "DIMVARS"]), "R2000: xdata");
assert!(!rec.chunks(2).any(|p| p[0] == "345"), "R2000: no DIMLTYPE group");
for v in [Version::R2004, Version::R2018] {
let text = dxf(&d, v);
let rec = dimstyle_record(&text, "Detail");
assert!(!rec.contains(&"DIMVARS"), "{v:?}: no DIMVARS xdata");
for code in ["49", "50", "90", "290", "345", "346", "347"] {
assert!(rec.chunks(2).any(|p| p[0] == code), "{v:?}: group {code}");
}
let jog = rec.chunks(2).find(|p| p[0] == "50").unwrap()[1].parse::<f64>().unwrap();
assert!((jog - 30f64.to_radians()).abs() < 1e-9, "DIMJOGANG in radians: {jog}");
let back = cadcraft_io::read_dxf(text.as_bytes()).unwrap();
assert_eq!(back.dim_style("Detail").unwrap(), &st, "{v:?}");
}
assert_eq!(cadcraft_io::read_dxf(r2000.as_bytes()).unwrap().dim_style("Detail").unwrap(), &st);
}
fn code_page(text: &str) -> &str {
let l = lines(text);
let at = l.iter().position(|s| *s == "$DWGCODEPAGE").unwrap();
l[at + 2]
}
#[test]
fn code_page_is_kept_before_2007() {
let mut d = Drawing::new_metric();
d.header.set_str("DWGCODEPAGE", "ANSI_1251");
d.add(
&Space::Model,
Common::default(),
EntityKind::Text(Text {
insert: Vec3::ZERO,
align_pt: None,
height: 2.5,
value: "Привет".into(),
rotation: 0.0,
width_factor: 1.0,
oblique: 0.0,
style: "Standard".into(),
halign: HAlign::Left,
valign: VAlign::Baseline,
}),
)
.unwrap();
for v in [Version::R2000, Version::R2004] {
let bytes = save(&d, "x.dxf", v);
assert!(bytes.is_ascii(), "{v:?}: ASCII with escapes");
let text = String::from_utf8(bytes).unwrap();
assert_eq!(code_page(&text), "ANSI_1251", "{v:?}");
let back = cadcraft_io::read_dxf(text.as_bytes()).unwrap();
assert_eq!(back.header.str("DWGCODEPAGE", ""), "ANSI_1251");
assert!(back.model.iter().any(|e| matches!(&e.kind, EntityKind::Text(t) if t.value == "Привет")));
}
assert_eq!(code_page(&dxf(&d, Version::R2018)), "ANSI_1252", "2007+: UTF-8 text");
// Without one, or with one CADCraft doesn't know: ANSI_1252.
d.header.set_str("DWGCODEPAGE", "NO_SUCH\n0\nEOF");
assert_eq!(code_page(&dxf(&d, Version::R2000)), "ANSI_1252");
assert_eq!(code_page(&dxf(&Drawing::new_metric(), Version::R2000)), "ANSI_1252");
}
#[test]
fn modelled_dictionaries_are_not_kept_twice() {
let mut d = Drawing::new_metric();
let wall = MLineStyle { name: "Wall".into(), ..MLineStyle::default() };
d.mline_styles.push(wall.clone());
d.add(
&Space::Model,
Common::default(),
EntityKind::MLine(MLine::new(&wall, &[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0)], false, MLineJust::Zero, 1.0)),
)
.unwrap();
let first = dxf(&d, Version::R2018);
let read = cadcraft_io::read_dxf(first.as_bytes()).unwrap();
let kept: Vec<&str> = read.foreign.objects.iter().map(|o| o.kind.as_str()).collect();
assert!(kept.is_empty(), "nothing CADCraft writes itself is kept raw: {kept:?}");
let again = dxf(&read, Version::R2018);
let third = dxf(&cadcraft_io::read_dxf(again.as_bytes()).unwrap(), Version::R2018);
for kind in ["MLINESTYLE", "SCALE", "MLEADERSTYLE", "TABLESTYLE", "DICTIONARY", "XRECORD", "GROUP", "LAYOUT"] {
assert_eq!(records(&again, kind), records(&first, kind), "{kind} after one round trip");
assert_eq!(records(&third, kind), records(&first, kind), "{kind} after two");
}
let l = lines(&again);
for key in ["ACAD_MLINESTYLE", "ACAD_SCALELIST", "ACAD_GROUP", "ACAD_MLEADERSTYLE", "ACAD_PLOTSTYLENAME", "CADCRAFT_HANDSEED"] {
assert_eq!(l.windows(2).filter(|w| *w == ["3", key]).count(), 1, "{key} once in the named object dictionary");
}
}
+20 -2
View File
@@ -98,9 +98,27 @@ fn custom_pattern_lines_roundtrip_scaled_and_rotated() {
}
#[test]
fn library_patterns_keep_no_definition() {
fn library_named_patterns_keep_the_files_definition() {
// The library's AR-CONC only approximates AutoCAD's: the file's own lines are kept and saved.
let h = first_hatch(&save(hatch("AR-CONC", 2.0, 0.0, Vec2::ZERO, custom_lines())));
assert!(near(&h.pattern_lines, &custom_lines()), "{:?}", h.pattern_lines);
// A hatch without lines of its own saves the library's definition, which reads back as lines.
let h = first_hatch(&save(hatch("ANSI31", 1.0, 0.0, Vec2::ZERO, vec![])));
assert!(h.pattern_lines.is_empty());
assert_eq!(h.pattern_lines.len(), 1);
}
#[test]
fn file_definition_is_drawn_instead_of_the_librarys() {
let prims = |lines: Vec<PatternLine>| {
let mut d = Drawing::new_imperial();
d.add(&Space::Model, Common::default(), EntityKind::Hatch(hatch("ANSI31", 1.0, 0.0, Vec2::ZERO, lines))).expect("add");
cadcraft_render::build(&d, &Space::Model, &cadcraft_render::Options::default()).prims.len()
};
// One horizontal family 5 units apart from y = 2.5 across the 10 × 10 square: two lines.
let sparse = vec![PatternLine { angle: 0.0, origin: (0.0, 2.5), delta: (0.0, 5.0), dashes: vec![] }];
assert_eq!(prims(sparse), 2);
// Without file lines, the library's ANSI31 (0.125 apart) is drawn.
assert!(prims(vec![]) > 100);
}
#[test]
+3 -1
View File
@@ -4,8 +4,10 @@
use cadcraft_doc::*;
use cadcraft_geom::Vec3;
/// Saved as 2007 DXF: older versions have no MULTILEADER (they get LEADER + MTEXT / INSERT).
fn roundtrip(d: &Drawing) -> (String, Drawing) {
let text = cadcraft_io::write_dxf(d);
let opts = cadcraft_io::SaveOptions { version: Some(cadcraft_io::Version::R2007), binary: false };
let text = String::from_utf8(cadcraft_io::write_with(d, "x.dxf", &opts).unwrap()).unwrap();
let back = cadcraft_io::read_dxf(text.as_bytes()).unwrap();
(text, back)
}
+119
View File
@@ -0,0 +1,119 @@
//! Plot follow-ups (#626): model-space plots use the plot device's paper margins, PNG and SVG
//! plots draw plot style ends, joins and fills as PDF does, and hyperlinks a drawing file gives
//! its entities (`PE_URL` extended data) become PDF link annotations.
use cadcraft_doc::{Common, Drawing, EndStyle, EntityKind, Line, PlotStyle, PlotStyleTable, Space};
use cadcraft_geom::Vec3;
use cadcraft_io::{plotter, read_dxf, write_dxf};
use serde_json::{Value, json};
fn line(d: &mut Drawing, a: (f64, f64), b: (f64, f64)) {
let k = EntityKind::Line(Line { a: Vec3::new(a.0, a.1, 0.0), b: Vec3::new(b.0, b.1, 0.0) });
d.add(&Space::Model, Common::default(), k).unwrap();
}
fn pdf_text(d: &Drawing, opts: Value) -> String {
let mut o = json!({"compress": false});
if let (Some(m), Some(e)) = (o.as_object_mut(), opts.as_object()) {
m.extend(e.clone());
}
String::from_utf8_lossy(&plotter::plot(d, &Space::Model, &o).unwrap()).into_owned()
}
/// The plot area's clip rectangle (`x y w h re W n`) of the first page.
fn clip(pdf: &str) -> Vec<f64> {
let line = pdf.lines().find(|l| l.ends_with(" re W n")).unwrap_or("");
line.split_whitespace().take(4).filter_map(|t| t.parse().ok()).collect()
}
#[test]
fn model_space_pdf_uses_the_device_margins() {
let mut d = Drawing::new_metric();
line(&mut d, (0.0, 0.0), (100.0, 50.0));
let mm = 72.0 / 25.4;
// No device: the page setup's printable area, as before.
let plain = clip(&pdf_text(&d, json!({})));
assert!(plain.first().is_some_and(|x| (x - 20.0 * mm).abs() > 1.0), "{plain:?}");
// A device paper with 20 mm margins: the printable area starts 20 mm in.
let r = clip(&pdf_text(&d, json!({"width": 210, "height": 297, "margins": [20, 20, 20, 20]})));
assert_eq!(r.len(), 4, "{r:?}");
assert!((r[0] - 20.0 * mm).abs() < 0.01 && (r[1] - 20.0 * mm).abs() < 0.01, "{r:?}");
let (w, h) = (r[2].min(r[3]), r[2].max(r[3]));
assert!((w - 170.0 * mm).abs() < 0.01 && (h - 257.0 * mm).abs() < 0.01, "{r:?}");
// Hostile margins are ignored.
for bad in [json!([1e9, 0, 0, 0]), json!([1, 2, 3]), json!("wide"), json!([f64::NAN, 1, 1, 1])] {
assert_eq!(clip(&pdf_text(&d, json!({"margins": bad}))), plain, "{bad}");
}
}
/// A drawing with two lines in a 2 mm wide style with round ends, and a plot style table holding it.
fn styled() -> Drawing {
let mut d = Drawing::new_metric();
line(&mut d, (0.0, 0.0), (100.0, 0.0));
line(&mut d, (0.0, 40.0), (100.0, 60.0));
let round = PlotStyle { name: "Round".into(), lineweight: Some(200), end: EndStyle::Round, ..PlotStyle::default() };
d.plot_style_tables.push(PlotStyleTable::named("round.stb", "", vec![round]));
let hs: Vec<_> = d.model.iter().map(|e| e.handle).collect();
for h in hs {
d.modify_entity(h, |e| e.common.plot_style = "Round".into()).unwrap();
}
d
}
#[test]
fn png_and_svg_plots_draw_plot_style_ends() {
let d = styled();
let png = |table: &str| plotter::plot(&d, &Space::Model, &json!({"output": "png", "resolution": 72, "plotStyleTable": table})).unwrap();
assert_ne!(png("round.stb"), png("None"), "round ends change the image");
let svg = |table: &str| String::from_utf8(plotter::plot(&d, &Space::Model, &json!({"output": "svg", "plotStyleTable": table})).unwrap()).unwrap();
let plain = svg("None");
assert!(plain.contains("<polyline") && !plain.contains("<path"), "{plain}");
let round = svg("round.stb");
assert!(round.contains("<path") && !round.contains("<polyline"), "wide lines with round ends are outlines: {round}");
}
/// DXF text of `d` with `PE_URL` extended data on each LINE, one URL per line in order.
fn with_pe_urls(d: &Drawing, urls: &[&str]) -> String {
let text = write_dxf(d);
let lines: Vec<&str> = text.lines().map(str::trim).collect();
let mut out: Vec<String> = Vec::new();
let mut urls = urls.iter();
let mut in_line = false;
// Extended data goes last in an entity, before the next `0` group.
for pair in lines.chunks(2) {
let [code, value] = pair else { continue };
if *code == "0" {
if in_line && let Some(url) = urls.next() {
for (c, v) in [("1001", "PE_URL"), ("1000", url), ("1002", "{"), ("1000", "A link"), ("1002", "}")] {
out.push(c.to_string());
out.push(v.to_string());
}
}
in_line = *value == "LINE";
}
out.push(code.to_string());
out.push(value.to_string());
}
out.join("\n") + "\n"
}
#[test]
fn pe_url_hyperlinks_become_link_annotations() {
let mut d = Drawing::new_metric();
line(&mut d, (0.0, 0.0), (100.0, 0.0));
line(&mut d, (0.0, 20.0), (100.0, 20.0));
line(&mut d, (0.0, 40.0), (100.0, 40.0));
let text = with_pe_urls(&d, &["https://example.com/plan", "javascript:alert(1)", "data:text/html,x"]);
assert_eq!(text.matches("PE_URL").count(), 3, "the test drawing carries the links");
let read = read_dxf(text.as_bytes()).unwrap();
let pdf = pdf_text(&read, json!({}));
assert_eq!(pdf.matches("/Subtype /Link").count(), 1, "only the web link: {pdf}");
assert!(pdf.contains("https://example.com/plan"));
assert!(!pdf.contains("javascript") && !pdf.contains("data:text"));
// Turned off, and given explicitly for the same object (the explicit one wins).
assert_eq!(pdf_text(&read, json!({"hyperlinks": false})).matches("/Subtype /Link").count(), 0);
let first = read.model.iter().next().unwrap().handle.hex();
let own = pdf_text(&read, json!({"links": [{"handle": first, "url": "https://example.org/own"}]}));
assert!(own.contains("https://example.org/own") && !own.contains("example.com/plan"), "{own}");
}
+15
View File
@@ -878,6 +878,21 @@ fn saving_twice_writes_the_same_records() {
assert_eq!(a.counts(), b.counts());
}
#[test]
fn class_records_have_instance_counts_for_generated_and_preserved_records() {
let (_, a, b) = saves(&fixture());
for out in [&a, &b] {
let counts = out.counts();
for class in out.in_section("CLASSES", "CLASS") {
let name = s(class, 1);
let instance_count = class.iter().find(|t| t.code == 91).map(Tag::i64);
assert_eq!(instance_count, counts.get(&name).map(|n| *n as i64), "CLASS {name}");
}
assert!(out.in_section("CLASSES", "CLASS").iter().any(|c| s(c, 1) == "ACME_OBJECT"));
assert_eq!(counts.get("ACME_OBJECT"), Some(&1));
}
}
#[test]
fn erased_entities_take_their_extension_data_with_them() {
let mut d = read_dxf(fixture().as_bytes()).expect("read");
+5 -1
View File
@@ -73,7 +73,11 @@ fn multileader_styles_and_current_style_round_trip() {
};
d.mleader_styles.push(callout);
d.header.set_str("CMLEADERSTYLE", "Callout");
let r = round_trip(&d);
let text = write_dxf(&d);
let tags = cadcraft_dxf::parse(text.as_bytes()).expect("parse written DXF");
let header = cadcraft_dxf::sections(&tags).into_iter().find(|s| s.name == "HEADER").expect("HEADER");
assert!(header.tags.iter().all(|t| t.code != 9 || t.str() != "$CMLEADERSTYLE"));
let r = read_dxf(text.as_bytes()).expect("reopen");
assert_eq!(r.mleader_styles, d.mleader_styles);
assert_eq!(r.header.str("CMLEADERSTYLE", ""), "Callout");
}
+5
View File
@@ -14,6 +14,11 @@ cadcraft-color = { workspace = true }
cadcraft-doc = { workspace = true }
cadcraft-fonts = { workspace = true }
tiny-skia = { workspace = true }
lyon_tessellation = { workspace = true }
# Display lists of large drawings are built on all cores (not on the web, which has no threads).
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rayon = { workspace = true }
[dev-dependencies]
serde_json = { workspace = true }
+303 -70
View File
@@ -1,11 +1,17 @@
//! Hatch drawing: island detection picks the boundary loops that take part, then a solid,
//! gradient or background fill (even-odd triangles) and the pattern lines (each pattern line
//! family swept across the boundary, even-odd clipped, then dashed).
//!
//! On-screen builds get a level of detail (`Options::min_dash` > 0 is two pixels, `Options::view`
//! the area being looked at): a pattern whose lines run together on screen is drawn as a solid
//! fill of its boundary ([`reads_solid`]), a dash or dot repeat under two pixels as a continuous
//! line (as linetypes already are), and nothing is generated outside the view. Plots, exports and
//! picks pass `min_dash = 0` and `view = None` and get every line.
use super::{Builder, Ctx, Ink, fill, ink};
use super::{Builder, Ctx, Ink, fill, ink, max_scale};
use cadcraft_color::{Color, Rgb};
use cadcraft_doc::library::pattern;
use cadcraft_doc::{Entity, Gradient, Hatch};
use cadcraft_doc::library::{PatternLine, pattern_for};
use cadcraft_doc::{Drawing, Entity, Gradient, Hatch};
use cadcraft_geom::{Bounds2, Polyline, Vec2, point_in_polygon};
/// Draw a hatch entity: `rgb` is its resolved colour, `tol` the chord tolerance.
@@ -14,7 +20,12 @@ pub(super) fn draw(b: &mut Builder, ctx: &Ctx, e: &Entity, h: &Hatch, rgb: Ink,
// Gradient and background colours given ByLayer follow the hatch's own layer.
let layer = if e.common.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { e.common.layer.as_str() };
let layer_color = ctx.layer_color(layer).unwrap_or(Color::Index(7));
if h.solid || h.pattern.eq_ignore_ascii_case("SOLID") || h.gradient.is_some() {
// Level of detail: a pixel in the hatch's own units, along its most stretched axis (so lines
// that show apart in any direction are never merged); 0 = full detail.
let stretch = max_scale(&ctx.xf);
let px = if stretch > 1e-12 { b.opts.min_dash / 2.0 / stretch } else { 0.0 };
let dense = b.opts.fill && reads_solid(ctx.d, h, px);
if h.solid || h.pattern.eq_ignore_ascii_case("SOLID") || h.gradient.is_some() || dense {
if b.opts.fill {
let tris = fill::triangulate_evenodd(&loops);
match &h.gradient {
@@ -29,7 +40,12 @@ pub(super) fn draw(b: &mut Builder, ctx: &Ctx, e: &Entity, h: &Hatch, rgb: Ink,
{
b.tris(ctx, ink(bg, layer_color, ctx.block_color), &fill::triangulate_evenodd(&loops));
}
for seg in pattern_lines(h, &loops) {
// The area being looked at, in the hatch's units.
let view = b.opts.view.and_then(|v| {
let inv = ctx.xf.inverse()?;
Some(Bounds2::from_points(v.corners().iter().map(|p| inv.apply(*p))))
});
for seg in pattern_lines(ctx.d, h, &loops, px, view.as_ref()) {
if seg.len() == 1 {
if let Some(p) = seg.first() {
b.point(ctx, rgb, *p);
@@ -44,12 +60,39 @@ pub(super) fn draw(b: &mut Builder, ctx: &Ctx, e: &Entity, h: &Hatch, rgb: Ink,
/// every loop with every other); past it all loops are kept (odd parity).
const MAX_ISLAND_WORK: usize = 20_000_000;
/// True when two polygons have the same vertices (ignoring cyclic shift and winding order).
fn same_polygon(a: &[Vec2], b: &[Vec2]) -> bool {
if a.len() != b.len() {
return false;
}
let n = a.len();
if n == 0 {
return true;
}
let near = |p1: Vec2, p2: Vec2| p1.near(p2, 1e-4);
(0..n).any(|k| (0..n).all(|i| near(a[i], b[(i + k) % n]))) || (0..n).any(|k| (0..n).all(|i| near(a[i], b[(k + n - (i % n)) % n])))
}
fn dedup_polygons(loops: &mut Vec<Vec<Vec2>>) {
let mut unique: Vec<Vec<Vec2>> = Vec::with_capacity(loops.len());
for l in loops.drain(..) {
if l.len() < 3 {
continue;
}
if !unique.iter().any(|u| same_polygon(u, &l)) {
unique.push(l);
}
}
*loops = unique;
}
/// The tessellated boundary loops that take part in the fill under the hatch's island
/// detection style (DXF group 75), filled with odd parity: 0 normal keeps every loop (islands
/// within islands alternate), 1 outer keeps the outermost loops and their first islands, 2
/// ignore keeps only the outermost loops. Nesting depth = how many other loops enclose a loop.
pub(crate) fn fill_loops(h: &Hatch, tol: f64) -> Vec<Vec<Vec2>> {
let loops: Vec<Vec<Vec2>> = h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate(tol)).collect();
let mut loops: Vec<Vec<Vec2>> = h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate(tol)).collect();
dedup_polygons(&mut loops);
let max_depth = match h.style {
1 => 1,
2 => 0,
@@ -208,16 +251,86 @@ impl Field {
/// Upper bound on the line families drawn from a pattern definition read from a file.
const MAX_PATTERN_LINES: usize = 1024;
/// Upper bound on the runs of one hatch (AutoCAD's HPMAXLINES default), checked as runs are made
/// (a hostile or mis-scaled pattern can't exhaust memory). A view-limited build makes at most the
/// runs of a full build, so it never leaves out runs that a full build draws.
const MAX_RUNS: usize = 1_000_000;
/// Upper bound on the lines one family sweeps over the whole hatch; past it the family is left
/// out (a full-detail build of a pattern far too fine for its area).
const MAX_FAMILY_LINES: i64 = 20_000;
/// The same bound when the sweep is limited to a view: the count then follows the view's size
/// on screen, and lines closer than a pixel already read as solid, so only very large displays
/// come near it.
const MAX_VIEW_FAMILY_LINES: i64 = 100_000;
/// Line families closer than this on screen (pixels) run together: the hatch reads as solid.
pub const SOLID_SPACING_PX: f64 = 1.0;
/// Average marks per pixel from which a pattern reads as solid (scattered marks leave about
/// e^-2 ≈ 14% of the pixels empty there).
pub const SOLID_COVERAGE: f64 = 2.0;
/// The line families a hatch draws with: the lines its file defined (as its author's CAD drew
/// them), else the library's pattern of that name as drawn in `d`, else ANSI31.
fn lines_of(d: &Drawing, h: &Hatch) -> Vec<PatternLine> {
if h.pattern_lines.is_empty() {
pattern_for(d, &h.pattern).or_else(|| pattern_for(d, "ANSI31")).map(|p| p.lines).unwrap_or_default()
} else {
h.pattern_lines.iter().take(MAX_PATTERN_LINES).cloned().collect()
}
}
/// The hatch's pattern scale (1 when unusable).
fn scale_of(h: &Hatch) -> f64 {
if h.scale.is_finite() && h.scale > 1e-9 { h.scale } else { 1.0 }
}
/// True when, at `px` hatch units per pixel, the pattern covers its area on screen, so drawing
/// it line by line would fill (nearly) every pixel anyway. Each line, dash or dot is drawn at
/// least a pixel wide, so that is the case when
/// - some line family has its lines closer than [`SOLID_SPACING_PX`] and no gap along them a
/// pixel or longer (continuous lines, or a repeat that reads as a continuous line), or
/// - the families together put on average [`SOLID_COVERAGE`] or more marks on every pixel
/// (dense dot or dash patterns made of several families).
///
/// A sparse dot or dash pattern keeps its look. `px <= 0` (full detail) is never solid.
pub fn reads_solid(d: &Drawing, h: &Hatch, px: f64) -> bool {
if !(px.is_finite() && px > 0.0) {
return false;
}
let scale = scale_of(h);
let mut coverage = 0.0;
for f in lines_of(d, h) {
let spacing = (f.delta.1 * scale).abs();
if !(spacing.is_finite() && spacing > 0.0) {
continue;
}
let pat_len: f64 = f.dashes.iter().map(|d| (d * scale).abs()).sum();
// The share of a line inked on screen (a dash or dot is at least a pixel long there).
let ink: f64 = f.dashes.iter().filter(|d| **d >= 0.0).map(|d| (d * scale).max(px)).sum();
let inked = if f.dashes.is_empty() || (ink > 0.0 && pat_len < 2.0 * px) {
1.0
} else if pat_len > 0.0 {
(ink / pat_len).min(1.0)
} else {
0.0
};
let gap = f.dashes.iter().filter(|d| **d < 0.0).map(|d| (d * scale).abs()).fold(0.0, f64::max);
let draws = f.dashes.is_empty() || ink > 0.0;
if spacing < SOLID_SPACING_PX * px && draws && (inked >= 1.0 || gap < px) {
return true;
}
coverage += inked * px / spacing;
}
coverage.is_finite() && coverage >= SOLID_COVERAGE
}
/// Hatch line runs in world coordinates (single-point runs are dots) for the fill `loops`.
pub fn pattern_lines(h: &Hatch, loops: &[Vec<Vec2>]) -> Vec<Vec<Vec2>> {
// A pattern the library doesn't define draws with the lines its file defined, else as ANSI31.
let lines = match pattern(&h.pattern) {
Some(p) => p.lines,
None if !h.pattern_lines.is_empty() => h.pattern_lines.iter().take(MAX_PATTERN_LINES).cloned().collect(),
None => pattern("ANSI31").map(|p| p.lines).unwrap_or_default(),
};
let scale = if h.scale.is_finite() && h.scale > 1e-9 { h.scale } else { 1.0 };
/// `px`: a pixel in the hatch's units for the level of detail (a dash or dot repeat under two
/// pixels is one continuous run); 0 = full detail, as plots, exports and picks use. `view`: only
/// the part of the hatch inside it is generated; `None` = all of it.
pub fn pattern_lines(d: &Drawing, h: &Hatch, loops: &[Vec<Vec2>], px: f64, view: Option<&Bounds2>) -> Vec<Vec<Vec2>> {
let lines = lines_of(d, h);
let scale = scale_of(h);
let px = if px.is_finite() && px > 0.0 { px } else { 0.0 };
let mut out = Vec::new();
for fam in &lines {
let ang = fam.angle.to_radians() + h.angle;
@@ -228,76 +341,196 @@ pub fn pattern_lines(h: &Hatch, loops: &[Vec<Vec2>]) -> Vec<Vec<Vec2>> {
continue;
}
let dashes: Vec<f64> = fam.dashes.iter().map(|d| d * scale).collect();
family(loops, origin, ang, delta, spacing, &dashes, &mut out);
if out.len() > 500_000 {
Family { origin, ang, delta, spacing, dashes: &dashes }.sweep(loops, px, view, &mut out);
if out.len() >= MAX_RUNS {
break;
}
}
out
}
fn family(loops: &[Vec<Vec2>], origin: Vec2, ang: f64, delta: Vec2, spacing: f64, dashes: &[f64], out: &mut Vec<Vec<Vec2>>) {
// Work in a frame where the family's lines are horizontal.
let to_local = |p: Vec2| (p - origin).rotate(-ang);
let to_world = |p: Vec2| origin + p.rotate(ang);
let local: Vec<Vec<Vec2>> = loops.iter().map(|l| l.iter().map(|p| to_local(*p)).collect()).collect();
let bb = Bounds2::from_points(local.iter().flatten().copied());
if bb.is_empty() {
return;
/// Repeats of a pattern across a swatch ([`pattern_swatch`]).
const SWATCH_REPEATS: f64 = 6.0;
/// Most runs a swatch holds.
const MAX_SWATCH_RUNS: usize = 4000;
/// A preview of a pattern for a pattern picker: the runs of its line families `lines` inside the
/// `side` × `side` square with a corner at the origin, at a scale that shows a few repeats of the
/// pattern (each family's line spacing or dash repeat, whichever is longer). Empty for a pattern
/// with no lines (SOLID).
pub fn pattern_swatch(lines: &[PatternLine], side: f64) -> Vec<Vec<Vec2>> {
let repeat =
lines.iter().map(|l| l.delta.1.abs().max(l.dashes.iter().map(|d| d.abs()).sum::<f64>())).filter(|r| r.is_finite()).fold(0.0, f64::max);
if !side.is_finite() || side <= 0.0 || repeat < 1e-9 {
return Vec::new();
}
let shift_per_line = delta.rotate(-ang).x;
let k0 = (bb.min.y / spacing).floor() as i64;
let k1 = (bb.max.y / spacing).ceil() as i64;
if k1.saturating_sub(k0) > 20_000 {
return;
let h = Hatch {
pattern: String::new(),
solid: false,
loops: Vec::new(),
scale: side / (SWATCH_REPEATS * repeat),
angle: 0.0,
associative: false,
style: 0,
elevation: 0.0,
gradient: None,
origin: Vec2::ZERO,
background: None,
pattern_lines: lines.to_vec(),
};
let square = vec![Vec2::new(0.0, 0.0), Vec2::new(side, 0.0), Vec2::new(side, side), Vec2::new(0.0, side)];
let mut runs = pattern_lines(&Drawing::default(), &h, &[square], 0.0, None);
runs.truncate(MAX_SWATCH_RUNS);
runs
}
/// One pattern line family, scaled and placed: parallel lines `spacing` apart through `origin`
/// at angle `ang`, each shifted along itself by `delta`'s along-line part from the last.
struct Family<'a> {
origin: Vec2,
ang: f64,
delta: Vec2,
spacing: f64,
dashes: &'a [f64],
}
impl Family<'_> {
/// Into the frame where the family's lines are horizontal.
fn to_local(&self, p: Vec2) -> Vec2 {
(p - self.origin).rotate(-self.ang)
}
let pat_len: f64 = dashes.iter().map(|d| d.abs()).sum();
for k in k0..=k1 {
let y = k as f64 * spacing;
let mut xs: Vec<f64> = Vec::new();
for l in &local {
let n = l.len();
for i in 0..n {
let (Some(a), Some(b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
if (a.y > y) != (b.y > y) {
xs.push(a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y));
fn to_world(&self, p: Vec2) -> Vec2 {
self.origin + p.rotate(self.ang)
}
fn sweep(&self, loops: &[Vec<Vec2>], px: f64, view: Option<&Bounds2>, out: &mut Vec<Vec<Vec2>>) {
let local: Vec<Vec<Vec2>> = loops.iter().map(|l| l.iter().map(|p| self.to_local(*p)).collect()).collect();
let bb = Bounds2::from_points(local.iter().flatten().copied());
if bb.is_empty() {
return;
}
// The lines and spans swept: the boundary's extent, cut to the view's (its corners in
// this frame) when there is one.
let (mut lo, mut hi) = (bb.min, bb.max);
if let Some(v) = view {
let vb = Bounds2::from_points(v.corners().iter().map(|p| self.to_local(*p)));
if vb.is_empty() || !vb.intersects(&bb) {
return;
}
lo = Vec2::new(lo.x.max(vb.min.x), lo.y.max(vb.min.y));
hi = Vec2::new(hi.x.min(vb.max.x), hi.y.min(vb.max.y));
}
let shift_per_line = self.delta.rotate(-self.ang).x;
let k0 = (lo.y / self.spacing).floor() as i64;
let k1 = (hi.y / self.spacing).ceil() as i64;
let max_lines = if view.is_some() { MAX_VIEW_FAMILY_LINES } else { MAX_FAMILY_LINES };
if k1.saturating_sub(k0) > max_lines {
return;
}
let pat_len: f64 = self.dashes.iter().map(|d| d.abs()).sum();
// Level of detail: a whole repeat under two pixels reads as a continuous line (when the
// family draws anything at all).
let draws = self.dashes.iter().any(|d| *d >= 0.0);
let solid_line = self.dashes.is_empty() || pat_len < 1e-12 || (px > 0.0 && pat_len < 2.0 * px && draws);
for k in k0..=k1 {
if out.len() >= MAX_RUNS {
return;
}
let y = k as f64 * self.spacing;
let mut xs: Vec<f64> = Vec::new();
for l in &local {
let n = l.len();
for i in 0..n {
let (Some(a), Some(b)) = (l.get(i), l.get((i + 1) % n)) else { continue };
if (a.y > y) != (b.y > y) {
xs.push(a.x + (b.x - a.x) * (y - a.y) / (b.y - a.y));
}
}
}
xs.sort_by(f64::total_cmp);
let offset = shift_per_line * k as f64;
for span in xs.chunks(2) {
let [x0, x1] = span else { continue };
let (x0, x1) = if view.is_some() { (x0.max(lo.x), x1.min(hi.x)) } else { (*x0, *x1) };
if view.is_some() && x1 <= x0 {
continue;
}
if solid_line {
out.push(vec![self.to_world(Vec2::new(x0, y)), self.to_world(Vec2::new(x1, y))]);
} else {
self.dash(x0, x1, y, offset, pat_len, out);
}
}
}
xs.sort_by(f64::total_cmp);
let offset = shift_per_line * k as f64;
for span in xs.chunks(2) {
let [x0, x1] = span else { continue };
if dashes.is_empty() || pat_len < 1e-12 {
out.push(vec![to_world(Vec2::new(*x0, y)), to_world(Vec2::new(*x1, y))]);
continue;
}
// Dash phase anchored at x = offset.
let start_rep = ((x0 - offset) / pat_len).floor();
let mut x = offset + start_rep * pat_len;
let mut guard = 0;
'outer: while x < *x1 {
for d in dashes {
guard += 1;
if guard > 100_000 {
break 'outer;
}
let len = d.abs();
if *d > 0.0 {
let a = x.max(*x0);
let b = (x + len).min(*x1);
if b > a {
out.push(vec![to_world(Vec2::new(a, y)), to_world(Vec2::new(b, y))]);
}
} else if *d == 0.0 && x >= *x0 && x <= *x1 {
out.push(vec![to_world(Vec2::new(x, y))]);
}
x += len;
if x >= *x1 {
break 'outer;
}
/// The dashes and dots of the span `x0..x1` of the line at `y`, phase anchored at x = `offset`.
fn dash(&self, x0: f64, x1: f64, y: f64, offset: f64, pat_len: f64, out: &mut Vec<Vec<Vec2>>) {
let start_rep = ((x0 - offset) / pat_len).floor();
let mut x = offset + start_rep * pat_len;
let mut guard = 0;
'outer: while x < x1 {
for d in self.dashes {
guard += 1;
if guard > 100_000 || out.len() >= MAX_RUNS {
break 'outer;
}
let len = d.abs();
if *d > 0.0 {
let a = x.max(x0);
let b = (x + len).min(x1);
if b > a {
out.push(vec![self.to_world(Vec2::new(a, y)), self.to_world(Vec2::new(b, y))]);
}
} else if *d == 0.0 && x >= x0 && x <= x1 {
out.push(vec![self.to_world(Vec2::new(x, y))]);
}
x += len;
if x >= x1 {
break 'outer;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use cadcraft_doc::{Hatch, HatchLoop};
use cadcraft_geom::PolyVertex;
#[test]
fn fill_loops_deduplicates_coincident_loops() {
let loop1 = HatchLoop {
vertices: vec![
PolyVertex::new(Vec2::new(0.0, 0.0)),
PolyVertex::new(Vec2::new(10.0, 0.0)),
PolyVertex::new(Vec2::new(10.0, 10.0)),
PolyVertex::new(Vec2::new(0.0, 10.0)),
],
outer: true,
sources: vec![],
};
let loop2 = loop1.clone();
let hatch = Hatch {
origin: Vec2::ZERO,
pattern: "SOLID".into(),
solid: true,
loops: vec![loop1, loop2],
scale: 1.0,
angle: 0.0,
associative: false,
style: 0,
elevation: 0.0,
gradient: None,
background: None,
pattern_lines: vec![],
};
let loops = fill_loops(&hatch, 1e-4);
assert_eq!(loops.len(), 1, "fill_loops must deduplicate duplicate loops");
let tris = fill::triangulate_evenodd(&loops);
let area: f64 = tris.chunks(3).map(|t| ((t[1] - t[0]).cross(t[2] - t[0]) / 2.0).abs()).sum();
assert!((area - 100.0).abs() < 1e-6, "triangulated area must be 100.0, not zero");
}
}
+113 -13
View File
@@ -19,6 +19,7 @@ pub mod pen;
mod plotstyle;
mod point;
pub mod raster;
mod text;
mod tolerance;
pub mod units;
mod wide;
@@ -30,6 +31,7 @@ use pen::Pen;
pub use dim::{ArrowGeom, Arrowhead, DimGeometry, DimText, LineRole, arrowhead, dimension_geometry, dimension_geometry_with, format_linear_value};
pub use fill::triangulate_evenodd;
pub use hatch::pattern_swatch;
pub use paper::{PAPER_SIZES, PaperSize, Sheet, paper_size, sheet};
pub use tolerance::{gdt_points, tolerance_in};
@@ -172,7 +174,8 @@ impl DisplayList {
pub struct Options {
/// Chord tolerance in world units (about half a pixel at the current zoom).
pub tolerance: f64,
/// Dashes shorter than this (world units) are drawn as continuous lines.
/// Two pixels in world units: shorter dashes are continuous and filled font text uses
/// screen-size detail. Zero keeps full detail (plots and image/vector exports).
pub min_dash: f64,
/// Draw text (off for very coarse previews).
pub text: bool,
@@ -185,14 +188,28 @@ pub struct Options {
/// Plot style table to draw with. When `None`, [`build`] of a layout whose page setup has
/// "Display plot styles" on uses the layout's table; [`build_plot`] uses none.
pub plot_style_table: Option<std::sync::Arc<cadcraft_doc::PlotStyleTable>>,
/// The area being looked at (world units of the space being built), for on-screen builds:
/// hatch patterns are only generated inside it. `None` = everywhere (plots, exports, picks).
pub view: Option<Bounds2>,
}
impl Default for Options {
fn default() -> Self {
Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights: false, view_height: 0.0, plot_style_table: None }
Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights: false, view_height: 0.0, plot_style_table: None, view: None }
}
}
/// The largest axis scale of a transform's linear part (its largest singular value): a length
/// `l` in the transform's own units measures at most `l * max_scale` after it, in any direction.
/// A uniform scale gives its factor, where [`Mat3::scale_factor`] gives the mean of the axes.
fn max_scale(m: &Mat3) -> f64 {
let (a, b, c, d) = (m.a, m.b, m.c, m.d);
let e = (a * a + b * b + c * c + d * d) / 2.0;
let f = (((a * a + b * b - c * c - d * d) / 2.0).powi(2) + (a * c + b * d).powi(2)).sqrt();
let s = (e + f).max(0.0).sqrt();
if s.is_finite() { s } else { 0.0 }
}
/// Graphics state inherited through block references.
#[derive(Clone)]
struct Ctx<'a> {
@@ -234,6 +251,8 @@ struct Builder<'a> {
styles: Option<&'a plotstyle::Styler>,
/// Named plot style (table index) of the entity being drawn.
named: Option<usize>,
/// The block-expansion budget shared by the parts of a list built in parallel.
shared_budget: Option<&'a std::sync::atomic::AtomicUsize>,
}
/// Upper bound on the block contents drawn per display list (each block reference, MINSERT copy
@@ -275,6 +294,9 @@ impl Builder<'_> {
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw, kind: Kind::Polyline, start, len: pts.len() as u32 });
}
fn tris(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2]) {
self.tris_at(ctx, color, tris, ctx.xf);
}
fn tris_at(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2], xf: Mat3) {
if tris.len() < 3 {
return;
}
@@ -282,7 +304,7 @@ impl Builder<'_> {
let (color, _) = self.styled(color, 0.0);
let start = self.list.tris.len() as u32;
for p in tris {
let q = ctx.xf.apply(*p);
let q = xf.apply(*p);
self.list.bounds.add(q);
self.list.tris.push(q);
}
@@ -298,16 +320,26 @@ impl Builder<'_> {
}
let fill = self.opts.fill && ctx.d.header.i64("TEXTFILL", 1) != 0;
// Plots note which filled outlines are font glyphs (see `DisplayList::texts`).
let mut text = if self.plotting && fill { sh.text.iter().peekable() } else { [].iter().peekable() };
let mut text = sh.text.iter().peekable();
let pixel = if self.plotting { 0.0 } else { self.opts.min_dash * 0.5 };
for (i, g) in sh.glyphs.iter().enumerate() {
if fill {
let tris = fill::triangulate_evenodd(g);
let at = self.list.prims.len();
self.tris(ctx, color, &tris);
while text.next_if(|t| t.outline < i).is_some() {}
if let Some(t) = text.next_if(|t| t.outline == i)
let glyph = text.next_if(|t| t.outline == i);
let at = self.list.prims.len();
let len = if let Some(t) = glyph {
let xf = crate::text::placement(t, ctx.xf);
let mesh = crate::text::mesh(t, xf, pixel);
self.tris_at(ctx, color, &mesh.triangles, xf);
mesh.triangles.len()
} else {
let tris = fill::triangulate_evenodd(g);
self.tris(ctx, color, &tris);
tris.len()
};
if let Some(t) = glyph.filter(|_| self.plotting)
&& self.list.prims.len() > at
&& let (Ok(prim), Ok(verts)) = (u32::try_from(at), u32::try_from(tris.len()))
&& let (Ok(prim), Ok(verts)) = (u32::try_from(at), u32::try_from(len))
{
let xf = &ctx.xf;
let glyph = cadcraft_fonts::TextGlyph { origin: xf.apply(t.origin), x: xf.apply_vec(t.x), y: xf.apply_vec(t.y), ..t.clone() };
@@ -339,7 +371,10 @@ impl Builder<'_> {
/// Take one unit of the block-expansion budget; false once it is spent.
fn expand(&mut self) -> bool {
self.list.expanded = self.list.expanded.saturating_add(1);
self.list.expanded <= MAX_BLOCK_EXPANSION
match self.shared_budget {
Some(spent) => spent.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < MAX_BLOCK_EXPANSION,
None => self.list.expanded <= MAX_BLOCK_EXPANSION,
}
}
}
@@ -382,12 +417,17 @@ fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> Di
_ => None,
});
let styler = table.map(|t| plotstyle::Styler::new(d, t));
let mut b = Builder { list: DisplayList::default(), opts, plotting, styles: styler.as_ref(), named: None };
let mut b = Builder { list: DisplayList::default(), opts, plotting, styles: styler.as_ref(), named: None, shared_budget: None };
if let Space::Paper(name) = space {
b.list.sheet = paper::sheet(d, name);
}
if let Some(store) = d.space(space) {
let paper = matches!(space, Space::Paper(_));
#[cfg(not(target_arch = "wasm32"))]
if !paper && !plotting && b.styles.is_none() && store.len() >= 2 * PARALLEL_CHUNK {
build_parallel(&mut b, d, store);
return b.list;
}
let mut viewports = 0usize;
for e in store.iter() {
if paper && let EntityKind::Viewport(vp) = &e.kind {
@@ -410,6 +450,66 @@ fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> Di
b.list
}
/// Top-level entities per task when a display list is built on all cores.
#[cfg(not(target_arch = "wasm32"))]
const PARALLEL_CHUNK: usize = 256;
/// Build model space for the screen on all cores: the entities are split into chunks built
/// side by side, then joined in drawing order, so the list is the same as a one-thread build.
/// The canvas rebuilds the whole list at every 2x zoom step, on the UI thread; one core made
/// that a visible stall on real drawings. Plots, layouts and plot styles keep the one-thread
/// path (they run once, and their pens and per-primitive layers are built along the way).
#[cfg(not(target_arch = "wasm32"))]
fn build_parallel(b: &mut Builder, d: &Drawing, store: &cadcraft_doc::EntityStore) {
use rayon::prelude::*;
let ents: Vec<&std::sync::Arc<Entity>> = store.iter().collect();
let budget = std::sync::atomic::AtomicUsize::new(0);
let (opts, plotting) = (b.opts, b.plotting);
let parts: Vec<DisplayList> = ents
.par_chunks(PARALLEL_CHUNK)
.map(|chunk| {
let mut part = Builder { list: DisplayList::default(), opts, plotting, styles: None, named: None, shared_budget: Some(&budget) };
for e in chunk {
entity(&mut part, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
part.list
})
.collect();
// One allocation per array for the joined list.
b.list.prims.reserve(parts.iter().map(|p| p.prims.len()).sum());
b.list.verts.reserve(parts.iter().map(|p| p.verts.len()).sum());
b.list.tris.reserve(parts.iter().map(|p| p.tris.len()).sum());
for part in parts {
append(&mut b.list, part);
}
}
/// Append `src` to `dst`: its primitives index into `dst`'s vertex and triangle arrays.
#[cfg(not(target_arch = "wasm32"))]
fn append(dst: &mut DisplayList, src: DisplayList) {
let (verts, tris, prims) = (dst.verts.len() as u32, dst.tris.len() as u32, dst.prims.len() as u32);
// Pens are parallel to the primitives, with missing entries meaning "use object".
if !src.pens.is_empty() {
dst.pens.resize(dst.prims.len(), Pen::default());
}
dst.prims.extend(src.prims.into_iter().map(|mut p| {
p.start += if matches!(p.kind, Kind::Tris | Kind::Mask) { tris } else { verts };
p
}));
dst.verts.extend(src.verts);
dst.tris.extend(src.tris);
dst.prim_layers.extend(src.prim_layers);
dst.texts.extend(src.texts.into_iter().map(|mut t| {
if t.prim < PlotText::CLIPPED {
t.prim += prims;
}
t
}));
dst.pens.extend(src.pens);
dst.bounds = dst.bounds.union(&src.bounds);
dst.expanded = dst.expanded.saturating_add(src.expanded);
}
/// `opts` with a view height for relative point sizes: the extents height when none is given
/// and points draw as figures sized relative to the view.
fn with_point_view(d: &Drawing, space: &Space, opts: &Options) -> Options {
@@ -464,7 +564,7 @@ fn viewport(b: &mut Builder, d: &Drawing, e: &Entity, vp: &cadcraft_doc::Viewpor
let mcenter = vp.view_center + (rect.center() - center) / s;
// Model window with slack: entity bounds are approximate (text, dimensions).
let win = Bounds2::new(mcenter - mhalf, mcenter + mhalf).expand(mhalf.x.max(mhalf.y) * 0.1);
let mut sub = Builder { list: DisplayList::default(), opts: b.opts, plotting: b.plotting, styles: b.styles, named: None };
let mut sub = Builder { list: DisplayList::default(), opts: b.opts, plotting: b.plotting, styles: b.styles, named: None, shared_budget: None };
// PSLTSCALE on: model-space dashes are scaled so they measure the same on paper as in
// paper space, whatever the viewport scale.
let lt_factor = if d.header.i64("PSLTSCALE", 1) != 0 { 1.0 / s } else { 1.0 };
@@ -602,7 +702,7 @@ pub fn build_entities<'a, I: IntoIterator<Item = &'a Entity>>(d: &Drawing, ents:
} else {
opts
};
let mut b = Builder { list: DisplayList::default(), opts, plotting: false, styles: None, named: None };
let mut b = Builder { list: DisplayList::default(), opts, plotting: false, styles: None, named: None, shared_budget: None };
for e in ents {
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
+198
View File
@@ -385,3 +385,201 @@ fn table_title_is_centred_and_merged() {
let b = Bounds2::from_points(text);
assert!((b.center().x - 3.0).abs() < 0.05, "{b:?}");
}
// ---------- hatch level of detail (on-screen builds, issue #544) ----------
fn pattern_hatch(pattern: &str, scale: f64, (x0, y0, x1, y1): (f64, f64, f64, f64)) -> Hatch {
let sq = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)].map(|(x, y)| PolyVertex::new(Vec2::new(x, y)));
Hatch {
pattern: pattern.into(),
solid: false,
loops: vec![HatchLoop { vertices: sq.to_vec(), outer: true, sources: Vec::new() }],
scale,
angle: 0.0,
associative: false,
style: 0,
elevation: 0.0,
gradient: None,
origin: Vec2::ZERO,
background: None,
pattern_lines: Vec::new(),
}
}
fn drawing_with(h: Hatch) -> Drawing {
let mut d = Drawing::new_imperial();
d.add(&Space::Model, Common::default(), EntityKind::Hatch(h)).unwrap();
d
}
fn loops_of(h: &Hatch) -> Vec<Vec<Vec2>> {
crate::hatch::fill_loops(h, 0.001)
}
/// On-screen options for a view where one pixel is `px` drawing units.
fn screen(px: f64, view: Option<Bounds2>) -> Options {
Options { tolerance: px / 2.0, min_dash: px * 2.0, view, ..Default::default() }
}
#[test]
fn dense_hatch_on_screen_is_one_solid_fill() {
// ANSI31 at scale 0.01: lines 0.00125 apart over a 4x4 square. On a 1000 px view of it a
// pixel is 0.004, so the lines run together: one solid fill. Plots keep every line.
let d = drawing_with(pattern_hatch("ANSI31", 0.01, (0.0, 0.0, 4.0, 4.0)));
let plot = build(&d, &Space::Model, &Options::default());
assert!(plot.prims.len() > 4000, "plots keep every line: {}", plot.prims.len());
let l = build(&d, &Space::Model, &screen(0.004, None));
assert_eq!(l.prims.len(), 1);
assert_eq!(l.prims[0].kind, Kind::Tris);
}
#[test]
fn sparse_dots_keep_their_look_when_lines_are_close() {
// Dot rows half a pixel apart but dots five pixels apart along them: not solid colour.
let fam = |dashes: &[f64]| cadcraft_doc::library::PatternLine { angle: 0.0, origin: (0.0, 0.0), delta: (0.0, 0.5), dashes: dashes.to_vec() };
let sparse = Hatch { pattern_lines: vec![fam(&[0.0, -5.0])], ..pattern_hatch("MYDOTS", 1.0, (0.0, 0.0, 40.0, 40.0)) };
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &sparse, 1.0));
// Dots closer than a pixel along rows closer than a pixel: solid.
let dense = Hatch { pattern_lines: vec![fam(&[0.0, -0.5])], ..sparse.clone() };
assert!(crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &dense, 1.0));
// A family with only gaps draws nothing, so it never reads as solid.
let blank = Hatch { pattern_lines: vec![fam(&[-0.5])], ..sparse.clone() };
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &blank, 1.0));
assert!(crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &blank, &loops_of(&blank), 1.0, None).is_empty());
// Full detail is never solid.
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &dense, 0.0));
}
#[test]
fn scattered_dot_pattern_reads_solid_once_dots_fill_the_pixels() {
// AR-SAND at scale 3/16 (the library pattern is drawn 16x coarser since #643, so this is the
// geometry of the old scale 3): three dot families, no single one dense enough on its own at
// 0.5 per pixel, but together about four dots on every pixel. Zoomed in 4x the dots stand apart.
let h = pattern_hatch("AR-SAND", 3.0 / 16.0, (0.0, 0.0, 100.0, 100.0));
assert!(crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &h, 0.5));
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &h, 0.125));
}
#[test]
fn normal_zoom_pattern_is_unchanged() {
// Lines and dash repeats a few pixels long or more: the level of detail changes nothing.
for (pattern, px) in [("ANSI31", 0.05), ("ANSI33", 0.05), ("AR-CONC", 0.02), ("AR-SAND", 0.01), ("DOTS", 0.01)] {
let h = pattern_hatch(pattern, 1.0, (0.0, 0.0, 6.0, 4.0));
let loops = loops_of(&h);
let full = crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, 0.0, None);
assert!(!full.is_empty(), "{pattern}");
assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &h, px), "{pattern}");
assert_eq!(crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, px, None), full, "{pattern}");
// A view covering the whole hatch changes nothing either.
let all = Bounds2::new(Vec2::new(-1.0, -1.0), Vec2::new(7.0, 5.0));
assert_eq!(crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, px, Some(&all)), full, "{pattern}");
}
}
#[test]
fn dash_repeat_under_two_pixels_is_one_run() {
// ANSI33's dashed family repeats every 0.1875: at 0.1 per pixel it reads as a continuous line,
// drawn as one run per span covering the same length as the full-detail span.
let h = pattern_hatch("ANSI33", 1.0, (0.0, 0.0, 6.0, 4.0));
let loops = loops_of(&h);
let full = crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, 0.0, None);
let lod = crate::hatch::pattern_lines(&cadcraft_doc::Drawing::new_imperial(), &h, &loops, 0.1, None);
assert!(lod.len() < full.len(), "{} vs {}", lod.len(), full.len());
assert!(lod.iter().all(|r| r.len() == 2));
// The continuous family: identical runs in both.
let solid_runs = |v: &[Vec<Vec2>]| v.iter().filter(|r| r.len() == 2).count();
assert!(solid_runs(&lod) > 0 && solid_runs(&full) > solid_runs(&lod));
}
#[test]
fn hatch_pattern_only_inside_the_view() {
// A 40x40 ANSI31 hatch looked at through a 2x2 view in a corner, at full pixel detail.
let d = drawing_with(pattern_hatch("ANSI31", 1.0, (0.0, 0.0, 40.0, 40.0)));
let full = build(&d, &Space::Model, &Options::default());
let far = Some(Bounds2::new(Vec2::new(100.0, 100.0), Vec2::new(110.0, 110.0)));
assert_eq!(build(&d, &Space::Model, &Options { view: far, ..Default::default() }).prims.len(), 0);
let corner = Bounds2::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0));
let part = build(&d, &Space::Model, &Options { view: Some(corner), ..Default::default() });
assert!(!part.prims.is_empty() && part.prims.len() * 10 < full.prims.len(), "{} of {}", part.prims.len(), full.prims.len());
// Runs are cut in the family's rotated frame: a 2x2 view at 45° reaches 1 unit out, plus one
// line spacing (0.125 × √2 along an axis) from rounding the line range outwards.
let near = corner.expand(1.0 + 0.125 * 2f64.sqrt() + 1e-9);
assert!(part.verts.iter().all(|p| near.contains(*p)));
// Nothing is lost inside the view: the drawn length within an inner box is the same.
let inner = Bounds2::new(Vec2::new(0.2, 0.2), Vec2::new(1.8, 1.8));
let (a, b) = (length_inside(&full, &inner), length_inside(&part, &inner));
assert!(a > 1.0 && (a - b).abs() < 1e-9, "{a} vs {b}");
}
/// Total length of a display list's polylines inside an axis-aligned box (Liang–Barsky).
fn length_inside(l: &DisplayList, bx: &Bounds2) -> f64 {
let mut total = 0.0;
for p in l.prims.iter().filter(|p| p.kind == Kind::Polyline) {
for w in l.points(p).windows(2) {
let (p0, d) = (w[0], w[1] - w[0]);
let (mut t0, mut t1) = (0.0f64, 1.0f64);
let mut ok = true;
for (pp, q) in [(-d.x, p0.x - bx.min.x), (d.x, bx.max.x - p0.x), (-d.y, p0.y - bx.min.y), (d.y, bx.max.y - p0.y)] {
if pp == 0.0 {
ok &= q >= 0.0;
} else if pp < 0.0 {
t0 = t0.max(q / pp);
} else {
t1 = t1.min(q / pp);
}
}
if ok && t1 > t0 {
total += d.len() * (t1 - t0);
}
}
}
total
}
#[test]
fn big_hatch_seen_through_a_wide_view_is_drawn() {
// ANSI31 (0.125 apart) over ±1e5 seen through a 3000-unit view: about 34,000 lines in the
// family's rotated frame, past the 20,000-line guard of a whole-hatch sweep, but still drawn
// when the sweep is limited to the view.
let d = drawing_with(pattern_hatch("ANSI31", 1.0, (-1e5, -1e5, 1e5, 1e5)));
let view = Some(Bounds2::new(Vec2::new(-1500.0, -1500.0), Vec2::new(1500.0, 1500.0)));
let l = build(&d, &Space::Model, &Options { view, ..Default::default() });
assert!(l.prims.len() > 20_000, "{}", l.prims.len());
}
#[test]
fn hatch_in_a_squashed_block_keeps_lines_that_show() {
// A block hatch inserted 100x wider than tall: along x its lines are spread far apart on
// screen, so it is drawn with lines, not merged into a fill.
let mut d = Drawing::new_imperial();
let mut blk = cadcraft_doc::Block::new("B");
let mut e = cadcraft_doc::Entity::new(cadcraft_doc::Handle(1), EntityKind::Hatch(pattern_hatch("ANSI31", 1.0, (0.0, 0.0, 4.0, 4.0))));
e.common = Common::default();
blk.entities.push(e);
d.blocks.insert("B".into(), std::sync::Arc::new(blk));
let ins = cadcraft_doc::Insert {
block: "B".into(),
insert: Vec3::new(0.0, 0.0, 0.0),
scale: Vec3::new(100.0, 1.0, 1.0),
rotation: 0.0,
attribs: vec![],
cols: 1,
rows: 1,
col_spacing: 0.0,
row_spacing: 0.0,
};
d.add(&Space::Model, Common::default(), EntityKind::Insert(ins)).unwrap();
// One pixel = 1 unit: 0.125 spacing × 100 along x is far more than a pixel.
let l = build(&d, &Space::Model, &screen(1.0, None));
assert!(l.prims.iter().all(|p| p.kind != Kind::Tris), "merged into a fill");
assert!(l.prims.len() > 10, "{}", l.prims.len());
}
#[test]
fn max_scale_is_the_most_stretched_axis() {
let m = Mat3::scale(1.0, 0.1);
assert!((max_scale(&m) - 1.0).abs() < 1e-12);
let r = Mat3::rotate(0.7).then(Mat3::scale(2.0, 2.0));
assert!((max_scale(&r) - 2.0).abs() < 1e-12);
assert!((max_scale(&Mat3::IDENTITY) - 1.0).abs() < 1e-12);
}
+192
View File
@@ -0,0 +1,192 @@
//! Filled font glyphs: bounded font-unit meshes shared across placements and zoom rebuilds.
use std::collections::HashMap;
use std::sync::{Arc, OnceLock, PoisonError, RwLock};
use cadcraft_fonts::{TextGlyph, ttf};
use cadcraft_geom::{Bounds2, Mat3, Vec2};
use lyon_tessellation::geometry_builder::{BuffersBuilder, VertexBuffers};
use lyon_tessellation::math::point;
use lyon_tessellation::path::Path;
use lyon_tessellation::{FillOptions, FillTessellator, FillVertex};
const MAX_POINTS: usize = 16_384;
const MAX_ENTRIES: usize = 8192;
const MAX_CACHE_BYTES: usize = 64 << 20;
const MAX_SIMPLIFY_STEPS: usize = 1_000_000;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum Detail {
Full,
/// A power-of-two tolerance in font units, rounded toward finer detail.
Reduced(i16),
/// An em occupies at most two pixels in every direction: show the ink's extent.
Box,
}
type Key = (usize, usize, u32, u16, Detail);
pub(super) struct Mesh {
contours: Arc<[Vec<Vec2>]>,
pub triangles: Vec<Vec2>,
}
struct Entry {
// Keep the bytes alive: registering a replacement font must never reuse this key's address.
_face: ttf::Face,
mesh: Arc<Mesh>,
}
#[derive(Default)]
struct Cache {
entries: HashMap<Key, Entry>,
bytes: usize,
}
/// A read-write lock: display lists are built on all cores and nearly every lookup is a hit.
fn cache() -> &'static RwLock<Cache> {
static CACHE: OnceLock<RwLock<Cache>> = OnceLock::new();
CACHE.get_or_init(|| RwLock::new(Cache::default()))
}
/// World-space placement of font units, including the block/viewport transform.
pub(super) fn placement(g: &TextGlyph, xf: Mat3) -> Mat3 {
xf.then_before(Mat3 { a: g.x.x / 1000.0, b: g.x.y / 1000.0, c: g.y.x / 1000.0, d: g.y.y / 1000.0, e: g.origin.x, f: g.origin.y })
}
fn detail(xf: Mat3, pixel: f64) -> Detail {
// The Frobenius norm bounds the largest stretch, including shear, mirroring and unequal
// block scales. A font-unit error below pixel / stretch is below a pixel in every direction.
let stretch = xf.a.hypot(xf.b).hypot(xf.c.hypot(xf.d));
if !(pixel.is_finite() && pixel > 0.0 && stretch.is_finite() && stretch > 0.0) {
return Detail::Full;
}
if stretch * 1000.0 <= pixel * 2.0 {
return Detail::Box;
}
let tolerance = pixel * 0.25 / stretch;
if !tolerance.is_finite() || tolerance < 0.0625 { Detail::Full } else { Detail::Reduced(tolerance.log2().floor().clamp(-4.0, 16.0) as i16) }
}
pub(super) fn mesh(g: &TextGlyph, xf: Mat3, pixel: f64) -> Arc<Mesh> {
let detail = detail(xf, pixel);
let key = (g.face.bytes.as_ptr() as usize, g.face.bytes.len(), g.face.index, g.gid, detail);
if let Some(entry) = cache().read().unwrap_or_else(PoisonError::into_inner).entries.get(&key) {
return entry.mesh.clone();
}
let contours = ttf::glyph_contours(&g.face, g.gid);
let points = contours.iter().map(Vec::len).try_fold(0usize, usize::checked_add);
// Font data is untrusted; don't hand unbounded or invalid paths to the tessellator.
if !points.is_some_and(|n| n <= MAX_POINTS) || contours.iter().flatten().any(|p| !p.is_finite() || p.x.abs().max(p.y.abs()) > 1.0e6) {
return Arc::new(Mesh { contours: Arc::default(), triangles: Vec::new() });
}
let contours = match detail {
Detail::Full => contours,
Detail::Reduced(band) => contours.iter().map(|c| simplify(c, 2f64.powi(i32::from(band)))).collect::<Vec<_>>().into(),
Detail::Box => {
let bb = Bounds2::from_points(contours.iter().flatten().copied());
if bb.is_empty() {
Arc::default()
} else {
vec![vec![bb.min, Vec2::new(bb.max.x, bb.min.y), bb.max, Vec2::new(bb.min.x, bb.max.y), bb.min]].into()
}
}
};
let mesh = Arc::new(Mesh { triangles: triangulate(&contours), contours });
let points = mesh.contours.iter().map(Vec::capacity).sum::<usize>().saturating_add(mesh.triangles.capacity());
let bytes = points
.saturating_mul(std::mem::size_of::<Vec2>())
.saturating_add(mesh.contours.len().saturating_mul(std::mem::size_of::<Vec<Vec2>>()))
.saturating_add(std::mem::size_of::<Entry>() + std::mem::size_of::<Key>() + std::mem::size_of::<Mesh>());
if bytes <= MAX_CACHE_BYTES {
let mut c = cache().write().unwrap_or_else(PoisonError::into_inner);
if c.entries.len() >= MAX_ENTRIES || c.bytes.saturating_add(bytes) > MAX_CACHE_BYTES {
c.entries.clear();
c.bytes = 0;
}
if !c.entries.contains_key(&key) {
c.bytes = c.bytes.saturating_add(bytes);
c.entries.insert(key, Entry { _face: g.face.clone(), mesh: mesh.clone() });
}
}
mesh
}
/// Iterative Douglas-Peucker on a closed contour. Never collapse a hole to a line or recurse
/// on font data; preserve the original if the work budget is exhausted.
fn simplify(contour: &[Vec2], tolerance: f64) -> Vec<Vec2> {
if contour.len() < 5 {
return contour.to_vec();
}
let mut points = contour.to_vec();
if let (Some(&first), Some(&last)) = (points.first(), points.last())
&& first != last
{
points.push(first);
}
let last = points.len().saturating_sub(1);
let mut keep = vec![false; points.len()];
if let Some(k) = keep.first_mut() {
*k = true;
}
if let Some(k) = keep.last_mut() {
*k = true;
}
let mut pending = vec![(0usize, last)];
let mut steps = 0usize;
while let Some((start, end)) = pending.pop() {
let (Some(&a), Some(&b)) = (points.get(start), points.get(end)) else { continue };
let ab = b - a;
let len2 = ab.len2();
let mut furthest = None;
let mut distance = tolerance;
for (i, &p) in points.iter().enumerate().take(end).skip(start + 1) {
steps += 1;
if steps > MAX_SIMPLIFY_STEPS {
return contour.to_vec();
}
let t = if len2 > 0.0 { ((p - a).dot(ab) / len2).clamp(0.0, 1.0) } else { 0.0 };
let d = p.dist(a + ab * t);
if d > distance {
distance = d;
furthest = Some(i);
}
}
if let Some(i) = furthest {
if let Some(k) = keep.get_mut(i) {
*k = true;
}
pending.extend([(start, i), (i, end)]);
}
}
let result: Vec<_> = points.into_iter().zip(keep).filter_map(|(p, keep)| keep.then_some(p)).collect();
if result.len() >= 4 { result } else { contour.to_vec() }
}
/// Monotone tessellation with even-odd holes and intersections. Work in font units so f32
/// precision is independent of drawing coordinates; the placement stays f64. The existing
/// slab filler remains the fallback when tessellation fails (including its u16 vertex limit).
fn triangulate(contours: &[Vec<Vec2>]) -> Vec<Vec2> {
let mut path = Path::builder();
for contour in contours {
let Some(&first) = contour.first().filter(|_| contour.len() >= 3) else { continue };
path.begin(point(first.x as f32, first.y as f32));
for p in contour.iter().skip(1) {
path.line_to(point(p.x as f32, p.y as f32));
}
path.end(true);
}
let mut buffers: VertexBuffers<Vec2, u16> = VertexBuffers::new();
let result = FillTessellator::new().tessellate_path(
&path.build(),
&FillOptions::even_odd(),
&mut BuffersBuilder::new(&mut buffers, |v: FillVertex<'_>| {
let p = v.position();
Vec2::new(f64::from(p.x), f64::from(p.y))
}),
);
if result.is_err() {
return crate::fill::triangulate_evenodd(contours);
}
buffers.indices.into_iter().filter_map(|i| buffers.vertices.get(usize::from(i)).copied()).collect()
}
+47
View File
@@ -164,3 +164,50 @@ fn pattern_lines_from_a_file_are_drawn_for_unknown_names() {
let p = l.prims.iter().find(|p| p.kind == Kind::Polyline).map(|p| l.points(p)).expect("lines");
assert!(((p[1] - p[0]).angle().to_degrees().rem_euclid(180.0) - 45.0).abs() < 1e-6);
}
#[test]
fn pattern_swatches_stay_in_their_square_and_show_a_few_repeats() {
for p in cadcraft_doc::library::standard_patterns() {
let runs = cadcraft_render::pattern_swatch(&p.lines, 1.0);
if p.lines.is_empty() {
assert!(runs.is_empty(), "{}", p.name);
continue;
}
assert!(runs.len() >= 3, "{} has {} runs", p.name, runs.len());
assert!(runs.len() <= 4000, "{} has {} runs", p.name, runs.len());
for v in runs.iter().flatten() {
assert!((-1e-9..=1.0 + 1e-9).contains(&v.x) && (-1e-9..=1.0 + 1e-9).contains(&v.y), "{} at {v:?}", p.name);
}
}
assert!(cadcraft_render::pattern_swatch(&[], 1.0).is_empty());
}
#[test]
fn brick_joints_are_drawn_once_half_a_brick_apart_course_to_course() {
let l = draw(hatch("BRICK", vec![square(0.0, 2.0)]), "0");
// Vertical joints: (x, course) for every vertical segment, rounded.
let mut joints: Vec<(i64, i64)> = l
.prims
.iter()
.filter(|p| p.kind == Kind::Polyline)
.filter_map(|p| match l.points(p) {
[a, b] if (a.x - b.x).abs() < 1e-9 && (a.y - b.y).abs() > 1e-9 => {
Some(((a.x * 1000.0).round() as i64, ((a.y + b.y) / 2.0 / 0.25).floor() as i64))
}
_ => None,
})
.collect();
let n = joints.len();
joints.sort_unstable();
joints.dedup();
assert_eq!(joints.len(), n, "a joint drawn twice");
// Within a course joints are a brick (0.5) apart; the next course is offset half a brick.
let xs = |course: i64| joints.iter().filter(|j| j.1 == course).map(|j| j.0).collect::<Vec<_>>();
for course in 0..4 {
let x = xs(course);
assert!(x.len() >= 3, "course {course}: {x:?}");
assert!(x.windows(2).all(|w| w[1] - w[0] == 500), "course {course}: {x:?}");
let next = xs(course + 1);
assert!(next.iter().all(|nx| (nx - x[0]).rem_euclid(500) == 250), "courses {course}/{}: {x:?} {next:?}", course + 1);
}
}
+63
View File
@@ -29,6 +29,14 @@ pub fn load_prefs(json: &str) {
}
}
/// What the GPU error notice says (see [`gpu_notice`]).
#[derive(Clone, Debug, PartialEq)]
pub struct GpuNotice {
pub fault: crate::gpu::GpuFault,
/// Drawings autosaved because of it (0: nothing unsaved, or autosave is off).
pub autosaved: usize,
}
/// Drawing Recovery: autosave files left behind by an app that stopped, listed once at start.
#[derive(Clone, Debug, Default)]
pub struct Recovery {
@@ -81,6 +89,19 @@ impl CadApp {
}
}
/// Every frame: after a GPU error or device loss, autosave unsaved drawings at once (the
/// session may not survive it) and show [`gpu_notice`].
pub fn gpu_fault_tick(&mut self, ctx: &egui::Context) {
let Some(fault) = self.gpu_faults.take() else { return };
#[cfg(not(target_arch = "wasm32"))]
let saved = self.autosave.as_mut().map_or(0, |a| a.save_now(&self.session));
#[cfg(target_arch = "wasm32")]
let saved = 0;
let autosaved = self.gpu_notice.as_ref().map_or(0, |n| n.autosaved) + saved;
self.gpu_notice = Some(GpuNotice { fault, autosaved });
ctx.request_repaint();
}
/// The app closes normally: this app's autosave files are not needed any more.
pub fn shutdown_autosave(&mut self) {
#[cfg(not(target_arch = "wasm32"))]
@@ -163,6 +184,48 @@ pub fn show(app: &mut CadApp, ctx: &egui::Context) {
}
}
/// The notice after a GPU error or device loss: what happened, what was autosaved, and what to
/// do. Not modal, so drawings can still be saved while the device works.
pub fn gpu_notice(app: &mut CadApp, ctx: &egui::Context) {
let Some(n) = &app.gpu_notice else { return };
let t = Tokens::get();
let mut dismiss = false;
egui::Window::new("Graphics error")
.id(egui::Id::new("cc_gpu_notice"))
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_TOP, vec2(0.0, 48.0))
.show(ctx, |ui| {
ui.set_width(440.0);
if n.fault.lost {
ui.label("The graphics device stopped working (a driver reset or a GPU that stopped responding). CADCraft can't draw again until it restarts.");
} else {
let times = if n.fault.errors == 1 { String::new() } else { format!(" ({} times)", n.fault.errors) };
ui.label(format!("The graphics driver reported an error{times}. CADCraft kept running, but the drawing may not display correctly."));
}
ui.add_space(4.0);
match n.autosaved {
0 => ui.label("Nothing needed autosaving."),
1 => ui.label("A recovery copy of the open drawing with unsaved changes was autosaved."),
k => ui.label(format!("Recovery copies of {k} drawings with unsaved changes were autosaved.")),
};
ui.label(RichText::new("Save your work and restart CADCraft. Recovery copies are offered at the next start if needed.").strong());
ui.add_space(4.0);
ui.collapsing("Details", |ui| {
ui.label(RichText::new(&n.fault.message).color(t.text_dim).monospace());
});
// A row just as tall as the button (a plain right-to-left layout takes the rest).
ui.allocate_ui_with_layout(vec2(ui.available_width(), 0.0), egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Dismiss").clicked() {
dismiss = true;
}
});
});
if dismiss {
app.gpu_notice = None;
}
}
#[cfg(test)]
mod tests {
#[test]
+327 -40
View File
@@ -38,7 +38,10 @@ impl Xf {
#[derive(Default)]
pub struct CanvasState {
pub list: Option<DisplayList>,
key: (u64, u64, i32, bool, usize),
key: (u64, u64, i32, i32, bool, usize),
/// The area `list` was built for (hatch patterns exist only inside it); a view leaving it
/// rebuilds the list.
window: Option<Bounds2>,
/// Bumped whenever `list` is rebuilt (identifies it for [`PrimIndex`]).
list_gen: u64,
prim_index: PrimIndex,
@@ -48,6 +51,20 @@ pub struct CanvasState {
mesh_key: Option<u64>,
mesh_slot: crate::gpu::MeshSlot,
mesh_origin: Vec2,
/// The dragged objects of a MOVE/COPY/ROTATE/SCALE preview, uploaded once per drag (see
/// [`draw_drag_gpu`]).
drag_key: Option<u64>,
drag_slot: crate::gpu::MeshSlot,
drag_origin: Vec2,
/// The selection highlight on the GPU (see [`draw_highlight_gpu`]).
hl_key: Option<u64>,
hl_slot: crate::gpu::MeshSlot,
hl_origin: Vec2,
/// Selected infinite lines (clipped and drawn on the CPU each frame).
hl_infinite: Vec<usize>,
/// The highlight geometry of the active viewport's model space, with the key it was built for
/// (see [`viewport_highlight_tolerance`]).
vp_hl: Option<(u64, DisplayList)>,
/// Indices of infinite-line primitives in `list` (drawn on the CPU in GPU mode).
infinite: Vec<usize>,
/// Milliseconds spent converting the display list to vertex data (last rebuild).
@@ -142,32 +159,65 @@ fn color32(c: Rgb) -> Color32 {
Color32::from_rgb(c.0, c.1, c.2)
}
fn ensure_list(app: &mut CadApp, px: f64) {
/// Rebuild the display list when needed. `px`: drawing units per pixel in the current view;
/// `views`: every area on screen that shows the list (the current view, other model tiles) with
/// its drawing units per pixel.
fn ensure_list(app: &mut CadApp, px: f64, views: &[(Bounds2, f64)]) {
let Ok(st) = app.session.state() else { return };
// Rebuild when the drawing changes or the zoom moves by more than 2x (tessellation band).
let band = px.max(1e-300).log2().floor() as i32;
let key = (st.uid, st.revision, band, app.session.settings.lwdisplay, std::sync::Arc::as_ptr(&st.doc) as usize);
if app.canvas.list.is_some() && app.canvas.key == key {
// Rebuild when the drawing changes, the zoom moves by more than 2x (tessellation band) or a
// view leaves the area the list was built for.
let band = |px: f64| px.max(1e-300).log2().floor() as i32;
// Hatch level of detail follows the finest view showing the list.
let lod_band = views.iter().map(|v| band(v.1)).fold(band(px), i32::min);
let key = (st.uid, st.revision, band(px), lod_band, app.session.settings.lwdisplay, std::sync::Arc::as_ptr(&st.doc) as usize);
let visible = views.iter().fold(Bounds2::EMPTY, |acc, v| acc.union(&v.0));
let inside = app.canvas.window.is_none_or(|w| visible.is_empty() || w.contains_box(&visible));
if app.canvas.list.is_some() && app.canvas.key == key && inside {
return;
}
let t0 = crate::now_ms();
let opts = cadcraft_render::Options {
tolerance: px * 0.5 * 2f64.powi(band) / px.max(1e-300) * px.min(1.0e300),
min_dash: px * 2.0,
tolerance: 2f64.powi(band(px)) * 0.5,
// Two pixels at the band's finest zoom: the list serves every zoom in the band, so its
// level of detail must hold at the finest one.
min_dash: 2f64.powi(lod_band) * 2.0,
text: true,
fill: true,
lineweights: app.session.settings.lwdisplay,
view_height: st.paper_view().height,
plot_style_table: None,
view: build_window(views),
};
let opts = cadcraft_render::Options { tolerance: 2f64.powi(band) * 0.5, ..opts };
let space = st.space.clone();
app.canvas.list = Some(cadcraft_render::build(&st.doc, &space, &opts));
app.canvas.key = key;
app.canvas.window = opts.view;
app.canvas.list_gen = app.canvas.list_gen.wrapping_add(1);
app.canvas.build_ms = crate::now_ms() - t0;
}
/// Tessellation tolerance for the highlights inside the active viewport: `scale` (pixels per
/// drawing unit) snapped to its 2x zoom band, as in [`ensure_list`], so zooming and panning
/// inside the band doesn't rebuild the geometry.
fn viewport_highlight_tolerance(scale: f64) -> f64 {
2f64.powi((1.0 / scale.max(1e-300)).log2().floor() as i32) * 0.5
}
/// The area a display list is built for: each view with half its larger side of margin all
/// round (panning by up to that much doesn't rebuild), joined. `None` (everything) when a view
/// isn't usable.
fn build_window(views: &[(Bounds2, f64)]) -> Option<Bounds2> {
let mut w = Bounds2::EMPTY;
for (v, _) in views {
let side = v.width().max(v.height());
if v.is_empty() || !side.is_finite() {
return None;
}
w = w.union(&v.expand(side * 0.5));
}
(!w.is_empty()).then_some(w)
}
/// Grid spacing adapted to the zoom: multiply the unit until lines are at least `min_px` apart.
fn adaptive(unit: f64, scale: f64, min_px: f64) -> f64 {
let mut u = unit.max(1e-12);
@@ -322,6 +372,117 @@ fn draw_list(p: &egui::Painter, xf: &Xf, list: &DisplayList, bg: Rgb, lwdisplay:
p.extend(shapes);
}
/// Draw the selection highlight (dashed, `width` points) through the GPU: its vertex data is
/// rebuilt only when the display list, the selection or the vertex origin changes. Dashing every
/// selected line and tessellating it as egui shapes each frame made large selections sluggish.
#[allow(clippy::too_many_arguments)]
fn draw_highlight_gpu(
p: &egui::Painter,
xf: &Xf,
list: &DisplayList,
list_gen: u64,
prim_index: &mut PrimIndex,
sel: &[Handle],
color: Color32,
width: f32,
hl: (&mut Option<u64>, &crate::gpu::MeshSlot, &mut Vec2, &mut Vec<usize>),
) {
let (hl_key, hl_slot, hl_origin, hl_infinite) = hl;
let origin = crate::gpu::choose_origin(&list.bounds, xf.center, f64::from(xf.rect.height()) / xf.scale.max(1e-300));
let key = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
(list_gen, sel, origin.x.to_bits(), origin.y.to_bits(), color.to_array(), width.to_bits()).hash(&mut h);
h.finish()
};
if *hl_key != Some(key) {
let prims = prim_index.prims(list_gen, list, sel);
let mesh = crate::gpu::build_highlight_mesh(list, &prims, origin, color.to_array(), width);
*hl_infinite = prims.into_iter().filter(|&i| list.prims.get(i).is_some_and(|p| matches!(p.kind, Kind::Infinite { .. }))).collect();
if let Ok(mut slot) = hl_slot.lock() {
*slot = Some(mesh);
}
*hl_key = Some(key);
*hl_origin = origin;
}
let center = xf.rect.center();
p.add(egui_wgpu::Callback::new_paint_callback(
xf.rect,
crate::gpu::CanvasCallback {
key,
slot: hl_slot.clone(),
offset: [(hl_origin.x - xf.center.x) as f32, (hl_origin.y - xf.center.y) as f32],
scale: xf.scale as f32,
center: [center.x, center.y],
layer: crate::gpu::LAYER_HIGHLIGHT,
xform: crate::gpu::IDENTITY_XFORM,
dash: 10.0,
},
));
}
/// Draw a MOVE/COPY/ROTATE/SCALE preview the way the drawing itself is drawn: the dragged
/// objects are tessellated and uploaded once (again only when the drawing, the selection or the
/// zoom band changes), and each frame only the transform in the view uniform moves. Rebuilding
/// the transformed copies as egui shapes every frame was slow with heavy selections and could
/// overflow the GPU buffer limit.
/// `clipboard` is the session's clipboard and its serial (dragged when `drag.clipboard`, by PASTE).
fn draw_drag_gpu(
c: &mut CanvasState,
p: &egui::Painter,
xf: &Xf,
doc: &cadcraft_doc::Drawing,
drag: &cadcraft_engine::Drag,
clipboard: (&[cadcraft_doc::Entity], u64),
bg: Rgb,
) {
// Tessellate for a power-of-two zoom band so zooming mid-drag rebuilds only every 2x.
let band = xf.scale.max(1e-300).log2().round() as i32;
let key = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
(c.key, band, bg.0, bg.1, bg.2, &drag.handles).hash(&mut h);
if drag.clipboard {
clipboard.1.hash(&mut h);
}
h.finish()
};
if c.drag_key != Some(key) {
let band_scale = 2f64.powi(band);
let opts = cadcraft_render::Options { tolerance: 0.5 / band_scale, min_dash: 2.0 / band_scale, ..Default::default() };
let list = if drag.clipboard {
cadcraft_render::build_entities(doc, clipboard.0.iter(), &opts)
} else {
cadcraft_render::build_entities(doc, drag.handles.iter().filter_map(|h| doc.entity(*h)).map(|e| e.as_ref()), &opts)
};
let origin = if list.bounds.is_empty() { Vec2::ZERO } else { list.bounds.center() };
let mesh = crate::gpu::build_mesh(&list, origin, bg, false);
if let Ok(mut slot) = c.drag_slot.lock() {
*slot = Some(mesh);
}
c.drag_key = Some(key);
c.drag_origin = origin;
}
// Positions are relative to the origin o: M(o + p) = L·p + (M(o) − o).
let m = drag.matrix;
let o = c.drag_origin;
let t = m.apply(o) - o;
let center = xf.rect.center();
p.add(egui_wgpu::Callback::new_paint_callback(
xf.rect,
crate::gpu::CanvasCallback {
key,
slot: c.drag_slot.clone(),
offset: [(o.x - xf.center.x) as f32, (o.y - xf.center.y) as f32],
scale: xf.scale as f32,
center: [center.x, center.y],
layer: crate::gpu::LAYER_DRAG,
xform: [m.a as f32, m.b as f32, m.c as f32, m.d as f32, t.x as f32, t.y as f32],
dash: 0.0,
},
));
}
/// Draw the display list through the GPU paint callback (uploading it when it changed), plus
/// the infinite lines, which are clipped to the view on the CPU every frame.
fn draw_list_gpu(c: &mut CanvasState, p: &egui::Painter, xf: &Xf, bg: Rgb, lwdisplay: bool) {
@@ -330,7 +491,7 @@ fn draw_list_gpu(c: &mut CanvasState, p: &egui::Painter, xf: &Xf, bg: Rgb, lwdis
let key = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
(c.key, origin.x.to_bits(), origin.y.to_bits(), bg.0, bg.1, bg.2, lwdisplay).hash(&mut h);
(c.key, c.list_gen, origin.x.to_bits(), origin.y.to_bits(), bg.0, bg.1, bg.2, lwdisplay).hash(&mut h);
h.finish()
};
if c.mesh_key != Some(key) {
@@ -353,6 +514,9 @@ fn draw_list_gpu(c: &mut CanvasState, p: &egui::Painter, xf: &Xf, bg: Rgb, lwdis
offset: [(c.mesh_origin.x - xf.center.x) as f32, (c.mesh_origin.y - xf.center.y) as f32],
scale: xf.scale as f32,
center: [center.x, center.y],
layer: crate::gpu::LAYER_DRAWING,
xform: crate::gpu::IDENTITY_XFORM,
dash: 0.0,
},
));
let mut shapes = Vec::new();
@@ -556,6 +720,31 @@ fn effective_point(app: &mut CadApp, raw: Vec2, xf: &Xf, ctx: &egui::Context) ->
r.point
}
/// The vertical wheel scroll this frame that zooms, in points, read from the raw wheel events so
/// each notch zooms by its whole step at once, as in standard CAD (#660). egui's smoothed delta
/// spreads a notch over several frames, so quick notches lag behind the wheel. Units convert to
/// points as egui does (a line is `line_scroll_speed` points, a page the view's height), and
/// events egui turns into a pinch zoom (⌘/Ctrl) or a sideways scroll (⇧) are left out, as it does.
fn wheel_zoom_points(events: &[egui::Event], opts: &egui::InputOptions, page: f32) -> f32 {
let points: f32 = events
.iter()
.filter_map(|e| match e {
egui::Event::MouseWheel { unit, delta, modifiers, .. }
if !modifiers.matches_any(opts.zoom_modifier) && !modifiers.matches_any(opts.horizontal_scroll_modifier) =>
{
let per = match unit {
egui::MouseWheelUnit::Point => 1.0,
egui::MouseWheelUnit::Line => opts.line_scroll_speed,
egui::MouseWheelUnit::Page => page,
};
Some(delta.y * per)
}
_ => None,
})
.sum();
if points.is_finite() { points } else { 0.0 }
}
pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
let t = Tokens::get();
let full = ui.available_rect_before_wrap();
@@ -602,26 +791,29 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
}
}
});
if inside && scroll != egui::Vec2::ZERO {
if app.canvas.scroll_pans {
// Two-finger swipe pans: the drawing follows the fingers, like a middle-drag.
// Pinch (and Cmd+scroll) still zoom via `zoom_delta` below.
if let Ok(st) = app.session.state_mut() {
let v = st.view();
st.set_view_quiet(cadcraft_engine::View {
center: v.center - Vec2::new(f64::from(scroll.x) / scale, -f64::from(scroll.y) / scale),
height: v.height,
});
}
} else if scroll.y.abs() > 0.0
&& let Some(hp) = hover_pos
{
// Zoom with the mouse wheel about the cursor.
let f = app.session.wheel_zoom_factor(f64::from(scroll.y));
let about = xf.to_world(hp);
let _ = app.session.zoom_about(f, about);
if inside && app.canvas.scroll_pans && scroll != egui::Vec2::ZERO {
// Two-finger swipe pans: the drawing follows the fingers, like a middle-drag.
// Pinch (and Cmd+scroll) still zoom via `zoom_delta` below.
if let Ok(st) = app.session.state_mut() {
let v = st.view();
st.set_view_quiet(cadcraft_engine::View {
center: v.center - Vec2::new(f64::from(scroll.x) / scale, -f64::from(scroll.y) / scale),
height: v.height,
});
}
}
let opts = ui.ctx().options(|o| o.input_options);
let wheel = ui.input(|i| wheel_zoom_points(&i.raw.events, &opts, rect.height()));
if inside
&& !app.canvas.scroll_pans
&& wheel != 0.0
&& let Some(hp) = hover_pos
{
// Zoom with the mouse wheel about the cursor, each notch at once.
let f = app.session.wheel_zoom_factor(f64::from(wheel));
let about = xf.to_world(hp);
let _ = app.session.zoom_about(f, about);
}
let zoom_pinch = ui.input(|i| i.zoom_delta());
if inside
&& (zoom_pinch - 1.0).abs() > 1e-4
@@ -852,7 +1044,12 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
// ---------- paint ----------
painter.rect_filled(rect, 0.0, t.canvas);
let t0 = crate::now_ms();
ensure_list(app, 1.0 / xf_paper.scale);
let mut views = vec![(xf_paper.world_bounds(), 1.0 / xf_paper.scale)];
views.extend(tiles.iter().filter(|(r, tile)| !tile.active && r.width() >= 1.0 && r.height() >= 1.0).map(|(r, tile)| {
let xf = tile_xf(*r, tile);
(xf.world_bounds(), 1.0 / xf.scale)
}));
ensure_list(app, 1.0 / xf_paper.scale, &views);
let sheet = app.canvas.list.as_ref().and_then(|l| l.sheet);
let bg = match sheet {
Some(sh) => {
@@ -904,23 +1101,40 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
hs.extend(app.canvas.hover.filter(|h| !sel.contains(h)));
let ents: Vec<_> = hs.iter().filter_map(|h| st.doc.model.get(*h)).map(|e| e.as_ref()).collect();
if !ents.is_empty() {
let list = cadcraft_render::build_entities(&st.doc, ents, &cadcraft_render::Options { tolerance: 0.5 / scale, ..Default::default() });
let clipped = painter.with_clip_rect(r.intersect(rect));
if let Some(h) = app.canvas.hover
&& !sel.contains(&h)
{
draw_highlight(&clipped, &xf, &list, &prims_of(&list, &[h]), t.hover, 2.0, false);
let tolerance = viewport_highlight_tolerance(scale);
let key = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
(st.uid, st.revision, std::sync::Arc::as_ptr(&st.doc) as usize, tolerance.to_bits(), &hs).hash(&mut h);
h.finish()
};
if app.canvas.vp_hl.as_ref().is_none_or(|(k, _)| *k != key) {
let built = cadcraft_render::build_entities(&st.doc, ents, &cadcraft_render::Options { tolerance, ..Default::default() });
app.canvas.vp_hl = Some((key, built));
}
if let Some((_, list)) = &app.canvas.vp_hl {
let clipped = painter.with_clip_rect(r.intersect(rect));
if let Some(h) = app.canvas.hover
&& !sel.contains(&h)
{
draw_highlight(&clipped, &xf, list, &prims_of(list, &[h]), t.hover, 2.0, false);
}
draw_highlight(&clipped, &xf, list, &prims_of(list, &sel), t.selection, 1.5, true);
}
draw_highlight(&clipped, &xf, &list, &prims_of(&list, &sel), t.selection, 1.5, true);
}
}
} else if let CanvasState { list: Some(list), list_gen, prim_index, hover, .. } = &mut app.canvas {
} else if let CanvasState { list: Some(list), list_gen, prim_index, hover, gpu, hl_key, hl_slot, hl_origin, hl_infinite, .. } = &mut app.canvas {
if let Some(h) = *hover
&& !sel.contains(&h)
{
draw_highlight(&painter, &xf, list, &prim_index.prims(*list_gen, list, &[h]), t.hover, 2.0, false);
}
draw_highlight(&painter, &xf, list, &prim_index.prims(*list_gen, list, &sel), t.selection, 1.5, true);
if gpu.is_some() && !sel.is_empty() {
draw_highlight_gpu(&painter, &xf, list, *list_gen, prim_index, &sel, t.selection, 1.5, (hl_key, hl_slot, hl_origin, &mut *hl_infinite));
draw_highlight(&painter, &xf, list, hl_infinite, t.selection, 1.5, true);
} else {
draw_highlight(&painter, &xf, list, &prim_index.prims(*list_gen, list, &sel), t.selection, 1.5, true);
}
}
app.canvas.draw_ms = crate::now_ms() - t0;
compare::paint(app, ui.ctx(), &painter, &xf_paper, rect);
@@ -973,7 +1187,23 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
}
}
// Rubber band preview of the active command.
if let (Some(c), true) = (app.canvas.cursor, app.session.running.is_some()) {
// `xf` is the space being edited: paper space, or model space inside the active viewport.
let drag = match (app.canvas.cursor, app.canvas.gpu.is_some()) {
(Some(c), true) => app.session.drag(c),
_ => None,
};
if let Some((drag, extra)) = drag {
if let Ok(st) = app.session.state() {
let serial = app.session.clipboard_source.as_ref().map_or(0, |c| c.serial);
draw_drag_gpu(&mut app.canvas, &painter, &xf, &st.doc, &drag, (&app.session.clipboard, serial), bg);
let list = cadcraft_render::build_entities(
&st.doc,
extra.iter(),
&cadcraft_render::Options { tolerance: 0.5 / scale, min_dash: 2.0 / scale, ..Default::default() },
);
draw_list(&painter, &xf, &list, bg, false);
}
} else if let (Some(c), true) = (app.canvas.cursor, app.session.running.is_some()) {
let ents = app.session.preview(c);
if !ents.is_empty()
&& let Ok(st) = app.session.state()
@@ -1056,6 +1286,11 @@ fn tile_rects(app: &CadApp, full: Rect) -> Vec<(Rect, cadcraft_engine::ModelTile
.collect()
}
/// The world ↔ screen transform of a model tile drawn in `r`.
fn tile_xf(r: Rect, tile: &cadcraft_engine::ModelTile) -> Xf {
Xf { rect: r, center: tile.view.center, scale: f64::from(r.height()) / tile.view.height.max(1e-12) }
}
/// The tiles other than the current one (each with its own view, clipped to it), then the
/// tile borders: thin, and heavy around the current tile.
fn draw_tiles(app: &CadApp, ui: &egui::Ui, tiles: &[(Rect, cadcraft_engine::ModelTile)]) {
@@ -1063,7 +1298,7 @@ fn draw_tiles(app: &CadApp, ui: &egui::Ui, tiles: &[(Rect, cadcraft_engine::Mode
let bg = Rgb(t.canvas.r(), t.canvas.g(), t.canvas.b());
for (r, tile) in tiles.iter().filter(|(r, tile)| !tile.active && r.width() >= 1.0 && r.height() >= 1.0) {
let p = ui.painter_at(*r);
let xf = Xf { rect: *r, center: tile.view.center, scale: f64::from(r.height()) / tile.view.height.max(1e-12) };
let xf = tile_xf(*r, tile);
p.rect_filled(*r, 0.0, t.canvas);
draw_grid(app, &p, &xf);
if let Some(list) = &app.canvas.list {
@@ -1520,6 +1755,15 @@ mod tests {
v.iter().map(|&h| Handle(h)).collect()
}
#[test]
fn viewport_highlight_tolerance_is_constant_within_a_zoom_band() {
assert_eq!(viewport_highlight_tolerance(4.0), viewport_highlight_tolerance(3.0));
assert_ne!(viewport_highlight_tolerance(4.0), viewport_highlight_tolerance(1.5));
for s in [0.3, 1.0, 3.0, 4.0, 250.0] {
assert!(viewport_highlight_tolerance(s) <= 0.5 / s);
}
}
#[test]
fn prim_index_matches_a_scan_and_follows_list_rebuilds() {
let a = list(&[3, 1, 3, 2, 7, 3]);
@@ -1533,6 +1777,49 @@ mod tests {
let b = list(&[3, 9]);
assert_eq!(ix.prims(2, &b, &hs(&[9, 3])), vec![0, 1]);
}
fn wheel(unit: egui::MouseWheelUnit, dy: f32, modifiers: egui::Modifiers) -> egui::Event {
egui::Event::MouseWheel { unit, delta: vec2(0.0, dy), phase: egui::TouchPhase::Move, modifiers }
}
/// Wheel events convert to points as egui does; ⌘/Ctrl (egui's pinch zoom) and ⇧ (a sideways
/// scroll) don't zoom here, and every notch of the frame counts (#660).
#[test]
fn wheel_zoom_reads_every_notch_of_the_frame() {
use egui::{Modifiers, MouseWheelUnit as U};
let o = egui::InputOptions::default();
let none = Modifiers::NONE;
assert_eq!(wheel_zoom_points(&[wheel(U::Line, 1.0, none)], &o, 500.0), o.line_scroll_speed);
assert_eq!(wheel_zoom_points(&[wheel(U::Line, -2.0, none)], &o, 500.0), -2.0 * o.line_scroll_speed);
assert_eq!(wheel_zoom_points(&[wheel(U::Point, 7.5, none)], &o, 500.0), 7.5);
assert_eq!(wheel_zoom_points(&[wheel(U::Page, 1.0, none)], &o, 500.0), 500.0);
// Three quick notches in one frame zoom three steps at once.
let three = [wheel(U::Line, 1.0, none), wheel(U::Line, 1.0, none), wheel(U::Line, 1.0, none)];
assert_eq!(wheel_zoom_points(&three, &o, 500.0), 3.0 * o.line_scroll_speed);
assert_eq!(wheel_zoom_points(&[wheel(U::Line, 1.0, Modifiers::COMMAND)], &o, 500.0), 0.0);
assert_eq!(wheel_zoom_points(&[wheel(U::Line, 1.0, Modifiers::SHIFT)], &o, 500.0), 0.0);
assert_eq!(wheel_zoom_points(&[wheel(U::Line, f32::NAN, none)], &o, 500.0), 0.0);
assert_eq!(wheel_zoom_points(&[egui::Event::PointerGone], &o, 500.0), 0.0);
}
/// The frame a notch arrives, egui's smoothed scroll holds only part of it (the rest trickles
/// in over later frames, which made quick notches lag), while the raw reading zooms the
/// whole notch at once (#660).
#[test]
fn a_wheel_notch_zooms_whole_in_its_own_frame() {
let ctx = egui::Context::default();
let o = ctx.options(|o| o.input_options);
let input = egui::RawInput { events: vec![wheel(egui::MouseWheelUnit::Line, 1.0, egui::Modifiers::NONE)], ..Default::default() };
let mut seen = (0.0, 0.0);
ctx.run_ui(input, |ui| {
seen = ui.input(|i| (i.smooth_scroll_delta.y, wheel_zoom_points(&i.raw.events, &o, 500.0)));
})
.textures_delta
.clear();
let (smoothed, raw) = seen;
assert_eq!(raw, o.line_scroll_speed, "the whole notch");
assert!(smoothed < raw, "egui's smoothed delta lags behind: {smoothed} of {raw}");
}
}
/// The COMPARE overlay.
+59 -45
View File
@@ -165,7 +165,7 @@ pub fn title_and_toolbar(app: &mut CadApp, ui: &mut egui::Ui) {
}
}
});
response.on_hover_text("More toolbar commands");
response.on_hover_text(crate::tl!("More toolbar commands"));
}
if let Some(c) = clicked {
app.start(c);
@@ -192,55 +192,59 @@ pub fn file_tabs(app: &mut CadApp, ui: &mut egui::Ui) {
app.ui.start_tab = false;
}
x += 24.0;
let mut tab = |ui: &mut egui::Ui, label: &str, active: bool, closable: bool, w: f32, id: egui::Id| -> (bool, bool, egui::Response) {
let tr = Rect::from_min_size(pos2(x, r.top()), vec2(w, r.height()));
let resp = ui.interact(tr, id, Sense::click());
ui.painter().rect_filled(
tr,
0.0,
if active {
t.tab_active
} else if resp.hovered() {
t.chrome
} else {
t.chrome_dark
},
);
ui.painter().text(
pos2(tr.left() + 8.0, tr.center().y),
egui::Align2::LEFT_CENTER,
label,
crate::theme::body(),
if active { t.text } else { t.text_dim },
);
let mut close = false;
if closable && (active || resp.hovered()) {
let cr = Rect::from_center_size(pos2(tr.right() - 11.0, tr.center().y), vec2(12.0, 12.0));
let cresp = ui.interact(cr, id.with("x"), Sense::click());
icons::paint(ui.painter(), cr, Icon::Close, false);
icons::describe_control(ui, &cresp, &format!("Close {label}"), None, false);
close = cresp.clicked();
}
icons::describe_control(ui, &resp, label, Some(active), true);
ui.painter().vline(tr.right(), tr.y_range(), Stroke::new(1.0, t.border));
x += w;
(resp.clicked(), close, resp)
};
let (c, _, _) = tab(ui, crate::tl!("Start"), app.ui.start_tab || app.session.docs.is_empty(), false, 110.0, ui.id().with("tab_start"));
// `label` is the drawing's name; `dirty` adds the unsaved-changes mark, kept visible when a
// long name is cut short.
let mut tab =
|ui: &mut egui::Ui, label: &str, dirty: bool, active: bool, closable: bool, w: f32, id: egui::Id| -> (bool, bool, egui::Response) {
let tr = Rect::from_min_size(pos2(x, r.top()), vec2(w, r.height()));
let resp = ui.interact(tr, id, Sense::click());
ui.painter().rect_filled(
tr,
0.0,
if active {
t.tab_active
} else if resp.hovered() {
t.chrome
} else {
t.chrome_dark
},
);
// The name ends in "…" when it doesn't fit before the close button (#513).
let color = if active { t.text } else { t.text_dim };
let mark = if dirty { "*" } else { "" };
let text_w = (w - 8.0 - if closable { 24.0 } else { 8.0 }).max(0.0);
let mark_w = ui.painter().layout_no_wrap(mark.to_string(), crate::theme::body(), color).size().x;
let (galley, cut) = elided(ui.painter(), label, crate::theme::body(), color, text_w - mark_w);
let pos = pos2(tr.left() + 8.0, tr.center().y - galley.size().y / 2.0);
let name_w = galley.size().x;
ui.painter().galley(pos, galley, color);
if dirty {
ui.painter().text(pos2(pos.x + name_w, tr.center().y), egui::Align2::LEFT_CENTER, mark, crate::theme::body(), color);
}
let resp = if cut { resp.on_hover_text(format!("{label}{mark}")) } else { resp };
let mut close = false;
if closable && (active || resp.hovered()) {
let cr = Rect::from_center_size(pos2(tr.right() - 11.0, tr.center().y), vec2(12.0, 12.0));
let cresp = ui.interact(cr, id.with("x"), Sense::click());
icons::paint(ui.painter(), cr, Icon::Close, false);
icons::describe_control(ui, &cresp, &format!("Close {label}"), None, false);
close = cresp.clicked();
}
icons::describe_control(ui, &resp, label, Some(active), true);
ui.painter().vline(tr.right(), tr.y_range(), Stroke::new(1.0, t.border));
x += w;
(resp.clicked(), close, resp)
};
let (c, _, _) = tab(ui, crate::tl!("Start"), false, app.ui.start_tab || app.session.docs.is_empty(), false, 110.0, ui.id().with("tab_start"));
if c {
app.ui.start_tab = true;
}
let mut switch_to = None;
let mut close = None;
let titles: Vec<(usize, String, bool)> = app
.session
.docs
.iter()
.enumerate()
.map(|(i, d)| (i, format!("{}{}", d.title, if d.is_dirty() { "*" } else { "" }), i == app.session.active))
.collect();
for (i, title, active) in titles {
let (c, x, resp) = tab(ui, &title, active && !app.ui.start_tab, true, 170.0, ui.id().with(("tab", i)));
let titles: Vec<(usize, String, bool, bool)> =
app.session.docs.iter().enumerate().map(|(i, d)| (i, d.title.clone(), d.is_dirty(), i == app.session.active)).collect();
for (i, title, dirty, active) in titles {
let (c, x, resp) = tab(ui, &title, dirty, active && !app.ui.start_tab, true, 170.0, ui.id().with(("tab", i)));
crate::shortcut_menus::attach(app, &resp, |app| crate::shortcut_menus::drawing_tab_entries(app, i));
if c {
switch_to = Some(i);
@@ -259,6 +263,16 @@ pub fn file_tabs(app: &mut CadApp, ui: &mut egui::Ui) {
});
}
/// `text` laid out on one line no wider than `max_width`, ending in "…" when it had to be cut;
/// and whether it was.
fn elided(p: &egui::Painter, text: &str, font: egui::FontId, color: egui::Color32, max_width: f32) -> (std::sync::Arc<egui::Galley>, bool) {
let mut job = egui::text::LayoutJob::simple_singleline(text.to_string(), font, color);
job.wrap = egui::text::TextWrapping { max_width: max_width.max(0.0), max_rows: 1, break_anywhere: true, overflow_character: Some('…') };
let galley = p.layout_job(job);
let cut = galley.elided;
(galley, cut)
}
/// Status bar: layout tabs on the left, coordinates and drafting toggles on the right.
pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {
let t = Tokens::get();
+117 -15
View File
@@ -1,6 +1,8 @@
//! The floating command line: history, the active prompt with clickable keywords, typed input
//! (keyboard focus stays on the drawing — typing anywhere goes here) and AutoComplete.
use std::collections::HashSet;
use cadcraft_engine::Input;
use egui::{Key, Pos2, Rect, Sense, Stroke, pos2, vec2};
@@ -90,25 +92,76 @@ fn tab(app: &mut CadApp) {
{
// Both values typed: Tab locks the second one as well.
app.cmd.locked_second = Some(app.cmd.buffer.clone());
} else if let Some((id, _)) = suggestions(&app.cmd.buffer).first() {
app.cmd.buffer = id.clone();
} else if let Some(s) = suggestions(&app.cmd.buffer).first() {
app.cmd.buffer = s.id.clone();
}
}
/// An AutoComplete suggestion for the command line.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Suggestion {
/// Command identifier (uppercase, e.g. "RECTANG"), used for buffer completion and execution.
pub id: String,
/// Display text shown in the AutoComplete list (e.g. "REC (RECTANG)" or "RECOVER").
pub display: String,
/// Human-readable command label (e.g. "Rectangle").
pub label: &'static str,
}
/// Matching command names/aliases for AutoComplete.
fn suggestions(prefix: &str) -> Vec<(String, &'static str)> {
fn suggestions(prefix: &str) -> Vec<Suggestion> {
if prefix.is_empty() || prefix.contains(' ') {
return Vec::new();
}
let p = prefix.to_ascii_lowercase();
let mut v: Vec<(String, &'static str)> = cadcraft_engine::command_specs()
.iter()
.filter(|c| !c.id.contains('.') && (c.id.starts_with(&p) || c.aliases.iter().any(|a| *a == p)))
.map(|c| (c.id.to_ascii_uppercase(), c.label))
.collect();
v.sort_by_key(|(id, _)| (id.len(), id.clone()));
v.truncate(8);
v
let mut out: Vec<Suggestion> = Vec::new();
let mut seen_ids: HashSet<&'static str> = HashSet::new();
// 1. Exact match: alias resolution or exact command name match.
let resolved = cadcraft_engine::cmd::resolve_alias(&p);
if let Some(spec) = cadcraft_engine::cmd::find_command(&resolved)
&& !spec.id.contains('.')
{
let display =
if spec.id == p { spec.id.to_ascii_uppercase() } else { format!("{} ({})", p.to_ascii_uppercase(), spec.id.to_ascii_uppercase()) };
out.push(Suggestion { id: spec.id.to_ascii_uppercase(), display, label: spec.label });
seen_ids.insert(spec.id);
}
// 2. Command ID prefix matches (excluding already matched exact command).
let mut id_matches: Vec<&'static cadcraft_engine::CommandSpec> =
cadcraft_engine::command_specs().iter().filter(|c| !c.id.contains('.') && !seen_ids.contains(c.id) && c.id.starts_with(&p)).collect();
id_matches.sort_by_key(|c| (c.id.len(), c.id));
for c in id_matches {
out.push(Suggestion { id: c.id.to_ascii_uppercase(), display: c.id.to_ascii_uppercase(), label: c.label });
seen_ids.insert(c.id);
}
// 3. Command alias prefix matches (for commands not yet added).
let mut alias_matches: Vec<(&'static str, &'static cadcraft_engine::CommandSpec)> = Vec::new();
for c in cadcraft_engine::command_specs() {
if c.id.contains('.') || seen_ids.contains(c.id) {
continue;
}
let matching_alias = c.aliases.iter().copied().filter(|a| a.starts_with(&p)).min_by_key(|a| (a.len(), *a));
if let Some(alias) = matching_alias {
alias_matches.push((alias, c));
}
}
alias_matches.sort_by_key(|(alias, c)| (alias.len(), *alias, c.id));
for (alias, c) in alias_matches {
if seen_ids.insert(c.id) {
out.push(Suggestion {
id: c.id.to_ascii_uppercase(),
display: format!("{} ({})", alias.to_ascii_uppercase(), c.id.to_ascii_uppercase()),
label: c.label,
});
}
}
out.truncate(8);
out
}
impl CmdLine {
@@ -462,23 +515,23 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui, canvas: Rect) {
);
p.rect_filled(lr, 3.0, t.list_bg);
p.rect_stroke(lr, 3.0, Stroke::new(1.0, t.cmd_border), egui::StrokeKind::Inside);
for (i, (id, label)) in sug.iter().enumerate() {
for (i, s) in sug.iter().enumerate() {
let r = Rect::from_min_size(pos2(lr.left(), lr.top() + rh * i as f32), vec2(lr.width(), rh));
let resp = ui.interact(r, ui.id().with(("sug", i)), Sense::click());
if resp.hovered() || i == 0 {
p.rect_filled(r.shrink(1.0), 2.0, if resp.hovered() { t.cmd_keyword_hover } else { t.list_row });
}
p.text(Pos2::new(r.left() + 8.0, r.center().y), egui::Align2::LEFT_CENTER, id, crate::theme::body(), t.text);
p.text(Pos2::new(r.left() + 8.0, r.center().y), egui::Align2::LEFT_CENTER, &s.display, crate::theme::body(), t.text);
p.text(
Pos2::new(r.right() - 8.0, r.center().y),
egui::Align2::RIGHT_CENTER,
crate::i18n::t(label),
crate::i18n::t(s.label),
crate::theme::small(),
t.text_faint,
);
if resp.clicked() {
app.cmd.buffer.clear();
app.start(&id.to_ascii_lowercase());
app.start(&s.id.to_ascii_lowercase());
}
}
}
@@ -715,3 +768,52 @@ mod delete_key_tests {
assert!(!c.typed_since_selection);
}
}
#[cfg(test)]
mod autocomplete_tests {
use super::*;
#[test]
fn exact_alias_is_first_suggestion_with_alias_display() {
let rec = suggestions("rec");
assert!(!rec.is_empty());
assert_eq!(rec[0].id, "RECTANG");
assert_eq!(rec[0].display, "REC (RECTANG)");
assert_eq!(rec[0].label, "Rectangle");
// RECOVER should follow RECTANG, not precede it
assert!(rec.iter().any(|s| s.id == "RECOVER"));
assert_eq!(rec[1].id, "RECOVER");
assert_eq!(rec[1].display, "RECOVER");
let l = suggestions("l");
assert_eq!(l[0].id, "LINE");
assert_eq!(l[0].display, "L (LINE)");
let c = suggestions("c");
assert_eq!(c[0].id, "CIRCLE");
assert_eq!(c[0].display, "C (CIRCLE)");
let co = suggestions("co");
assert_eq!(co[0].id, "COPY");
assert_eq!(co[0].display, "CO (COPY)");
}
#[test]
fn prefix_without_alias_shows_command_id() {
let rect = suggestions("rect");
assert_eq!(rect[0].id, "RECTANG");
assert_eq!(rect[0].display, "RECTANG");
let line = suggestions("line");
assert_eq!(line[0].id, "LINE");
assert_eq!(line[0].display, "LINE");
}
#[test]
fn tab_autocompletes_alias_to_resolved_command() {
let mut app = CadApp::new(cadcraft_engine::Session::empty(), crate::Services::default());
app.cmd.buffer = "rec".to_string();
tab(&mut app);
assert_eq!(app.cmd.buffer, "RECTANG");
}
}
+7 -4
View File
@@ -6,7 +6,8 @@
//! Pan/Zoom, Quick Select, Find, Properties and Options.
//! - **While a command runs** ([`command_entries`]): a quick right-click is Enter; holding the
//! button for [`HOLD_SECS`] opens Enter, Cancel, Recent Input, the prompt's options, Snap
//! Overrides and Pan/Zoom instead (AutoCAD's time-sensitive right-click).
//! Overrides and Pan/Zoom instead (AutoCAD's time-sensitive right-click). With that turned off
//! in OPTIONS (`timeSensitiveRightClick`), every right-click opens the menu.
//! - **Shift/Ctrl+right-click** ([`snap_entries`]): the object snap menu. Its items type the point
//! modifiers and snap overrides (`_from`, `_endp`, `_non`…) at the current point prompt.
//!
@@ -302,7 +303,8 @@ struct Press(f64, egui::Pos2);
/// The canvas's right button while a command runs (Shift/Ctrl+right-click is the snap menu's): a
/// quick click is Enter — returns `true` when the caller should send it — and a click held for
/// [`HOLD_SECS`] opens the command menu ([`command_entries`]) at the pointer. Call it every frame
/// [`HOLD_SECS`] opens the command menu ([`command_entries`]) at the pointer (any click does with
/// the time-sensitive right-click turned off). Call it every frame
/// with whether the pointer is on the canvas and whether a right-click ended there; it also shows
/// the menu while it is open.
pub fn in_command(app: &mut CadApp, ui: &egui::Ui, inside: bool, right_clicked: bool) -> bool {
@@ -323,9 +325,10 @@ pub fn in_command(app: &mut CadApp, ui: &egui::Ui, inside: bool, right_clicked:
if released && let Some(Press(t0, p0)) = ctx.data_mut(|d| d.remove_temp::<Press>(press_id)) {
// A drag isn't a click; a click held long enough asks for the menu.
let still = pos.is_some_and(|p| p.distance(p0) <= 6.0);
open = running && still && now - t0 >= HOLD_SECS;
let held = now - t0 >= HOLD_SECS || !app.options.time_sensitive_right_click;
open = running && still && held;
}
let enter = running && inside && right_clicked && !open;
let enter = running && inside && right_clicked && !open && app.options.time_sensitive_right_click;
if !running {
return false;
}
+3 -1
View File
@@ -14,6 +14,7 @@ pub fn show(app: &mut CadApp, ctx: &egui::Context) {
let Some(d) = app.ui.dialog.clone() else { return };
let mut open = true;
match d.as_str() {
"spacemouse" => crate::spacemouse::dialog(app, ctx, &mut open),
"language" => language(app, ctx, &mut open),
"layers" => crate::layers::dialog(app, ctx, &mut open),
"qselect" => crate::quick::qselect_dialog(app, ctx, &mut open),
@@ -28,7 +29,8 @@ pub fn show(app: &mut CadApp, ctx: &egui::Context) {
o if o == "options" || o.starts_with("options:") => crate::options::dialog(app, ctx, o, &mut open),
other => crate::managers::dialog(app, ctx, other, &mut open),
}
if !open {
// A dialog that closed by opening another (Plot Preview back to Plot) keeps the new one.
if !open && app.ui.dialog.as_deref() == Some(d.as_str()) {
app.ui.dialog = None;
}
}
+40 -3
View File
@@ -59,8 +59,34 @@ struct Preview {
shown: Result<(TextureHandle, String), String>,
}
/// Render the plot preview of the current layout (or model) as a texture.
/// A preview the Plot dialog asked for: its PLOT parameters (PNG output) and a caption.
#[derive(Clone)]
struct FromPlot {
params: Value,
info: String,
}
const FROM_PLOT: &str = "cc_plot_preview_from_plot";
/// Show the Plot dialog's settings as they plot (PLOT with `params`, a PNG); Print... and Close
/// return to the dialog.
pub(crate) fn preview_plot(app: &mut CadApp, ctx: &egui::Context, params: Value, info: String) {
PREVIEW_GEN.fetch_add(1, Ordering::Relaxed);
ctx.data_mut(|d| d.insert_temp(egui::Id::new(FROM_PLOT), FromPlot { params, info }));
app.ui.dialog = Some("preview".into());
}
/// Render the plot preview of the current layout (or model), or of the Plot dialog's settings,
/// as a texture.
fn render(app: &mut CadApp, ctx: &egui::Context) -> Result<(TextureHandle, String), String> {
if let Some(f) = ctx.data(|d| d.get_temp::<FromPlot>(egui::Id::new(FROM_PLOT))) {
let r = app.session.execute("plot", &f.params).map_err(|e| e.to_string())?;
let png = r["data"].as_str().and_then(cadcraft_engine::cmd::file::base64_decode).ok_or("the preview has no image")?;
let img = image::load_from_memory_with_format(&png, image::ImageFormat::Png).map_err(|e| e.to_string())?.to_rgba8();
let size = [img.width() as usize, img.height() as usize];
let tex = ctx.load_texture("plot-preview", egui::ColorImage::from_rgba_unmultiplied(size, img.as_raw()), egui::TextureOptions::LINEAR);
return Ok((tex, f.info));
}
let r = app.session.execute("preview", &json!({ "width": 1600 })).map_err(|e| e.to_string())?;
let png = r["data"].as_str().and_then(cadcraft_engine::cmd::file::base64_decode).ok_or("the preview has no image")?;
let img = image::load_from_memory_with_format(&png, image::ImageFormat::Png).map_err(|e| e.to_string())?.to_rgba8();
@@ -111,10 +137,18 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
if close || print {
*open = false;
}
let from_plot = ctx.data(|d| d.get_temp::<FromPlot>(egui::Id::new(FROM_PLOT))).is_some();
if !*open {
ctx.data_mut(|d| d.remove::<Preview>(id));
ctx.data_mut(|d| {
d.remove::<Preview>(id);
d.remove::<FromPlot>(egui::Id::new(FROM_PLOT));
});
}
if print {
if from_plot && !*open {
// Back to the Plot dialog with its settings; Print... plots them.
crate::managers::set_flag(ctx, crate::managers::PLOT_NOW, print);
app.ui.dialog = Some("plot".into());
} else if print {
app.start("plot");
}
}
@@ -157,6 +191,9 @@ mod tests {
app.cmdline("pre");
assert_eq!(app.ui.dialog.as_deref(), Some("preview"));
app.ui.dialog = None;
app.start("preview");
assert_eq!(app.ui.dialog.as_deref(), Some("preview"));
app.ui.dialog = None;
let _ = app.run("export", json!({ "format": "svg" }));
let _ = app.run("import", json!({}));
+206 -41
View File
@@ -25,8 +25,9 @@ use egui_wgpu::wgpu::util::DeviceExt;
/// Line/triangle vertex: position (2 × f32, relative to the origin) + sRGB colour (4 × u8).
const VERTEX_SIZE: usize = 12;
/// Wide-line / point instance: a, b (2 × f32 each) + colour (4 × u8) + width in points (f32).
const INSTANCE_SIZE: usize = 24;
/// Wide-line / point instance: a, b (2 × f32 each) + colour (4 × u8) + width in points (f32) +
/// length along its polyline before `a`, in world units (f32; dashing).
const INSTANCE_SIZE: usize = 28;
/// Vertices (or instances) per GPU buffer; divisible by 2 and 3 so no primitive straddles two.
const CHUNK: usize = 4_194_300;
/// Point markers are 1.5 × 1.5 points (as on the CPU path).
@@ -127,12 +128,13 @@ fn push_vertex(out: &mut Vec<u8>, p: [f32; 2], c: [u8; 4]) {
out.extend_from_slice(&c);
}
fn push_instance(out: &mut Vec<u8>, a: [f32; 2], b: [f32; 2], c: [u8; 4], width: f32) {
fn push_instance(out: &mut Vec<u8>, a: [f32; 2], b: [f32; 2], c: [u8; 4], width: f32, start: f32) {
for v in [a[0], a[1], b[0], b[1]] {
out.extend_from_slice(&v.to_le_bytes());
}
out.extend_from_slice(&c);
out.extend_from_slice(&width.to_le_bytes());
out.extend_from_slice(&start.to_le_bytes());
}
/// Lineweight in screen points for a primitive (as on the CPU path), or `None` for a hairline.
@@ -161,7 +163,7 @@ pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) ->
Some(w) => {
for s in pts.windows(2) {
if let [a, b] = s {
push_instance(&mut m.quads, rel(*a, origin), rel(*b, origin), col, w);
push_instance(&mut m.quads, rel(*a, origin), rel(*b, origin), col, w, 0.0);
}
}
}
@@ -192,7 +194,7 @@ pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) ->
Kind::Point => {
if let Some(q) = pts.first() {
let p = rel(*q, origin);
push_instance(&mut m.quads, p, p, col, POINT_SIZE);
push_instance(&mut m.quads, p, p, col, POINT_SIZE, 0.0);
}
}
// Drawn on the CPU each frame (clipped to the view).
@@ -202,6 +204,48 @@ pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) ->
m
}
/// Vertex data drawing primitives `prims` of `list` as `width`-point lines in `color` (the
/// selection highlight; dashed by the callback's `dash`). Fills are outlined by their bounding
/// box, as on the CPU path; infinite lines are left to the CPU (they are clipped per frame).
pub fn build_highlight_mesh(list: &DisplayList, prims: &[usize], origin: Vec2, color: [u8; 4], width: f32) -> MeshData {
let mut m = MeshData::default();
for prim in prims.iter().filter_map(|&i| list.prims.get(i)) {
let pts = list.points(prim);
match prim.kind {
Kind::Polyline => {
let mut run = 0.0;
for s in pts.windows(2) {
if let [a, b] = s {
push_instance(&mut m.quads, rel(*a, origin), rel(*b, origin), color, width, run as f32);
run += a.dist(*b);
}
}
}
Kind::Point => {
if let Some(q) = pts.first() {
let p = rel(*q, origin);
push_instance(&mut m.quads, p, p, color, 3.0, 0.0);
}
}
Kind::Tris | Kind::Mask => {
let bb = Bounds2::from_points(pts.iter().copied());
if !bb.is_empty() {
let c = [bb.min, Vec2::new(bb.max.x, bb.min.y), bb.max, Vec2::new(bb.min.x, bb.max.y), bb.min];
let mut run = 0.0;
for s in c.windows(2) {
if let [a, b] = s {
push_instance(&mut m.quads, rel(*a, origin), rel(*b, origin), color, 1.0, run as f32);
run += a.dist(*b);
}
}
}
}
Kind::Infinite { .. } => {}
}
}
m
}
/// The vertex origin for a display list seen at `view_center` / `view_height`: the list's centre
/// normally; at deep zoom (the view much smaller than the drawing) a point near the view, snapped
/// to a coarse grid so panning re-uploads only every few dozen screens.
@@ -232,20 +276,32 @@ pub struct CanvasCallback {
pub scale: f32,
/// Canvas rect centre in points.
pub center: [f32; 2],
/// Which buffers to draw: [`LAYER_DRAWING`] or [`LAYER_DRAG`].
pub layer: usize,
/// Affine transform applied to the origin-relative positions before `offset`: the linear
/// part (a, b, c, d as in `Mat3`) and the translation. [`IDENTITY_XFORM`] for the drawing.
pub xform: [f32; 6],
/// Dash period in points for wide lines (0 = solid): the selection highlight.
pub dash: f32,
}
/// The drawing's display list.
pub const LAYER_DRAWING: usize = 0;
/// The objects a MOVE/COPY/ROTATE/SCALE preview drags, uploaded once per drag.
pub const LAYER_DRAG: usize = 1;
/// The selection highlight, rebuilt only when the selection or the drawing changes.
pub const LAYER_HIGHLIGHT: usize = 2;
pub const IDENTITY_XFORM: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
struct Batch {
buffer: wgpu::Buffer,
count: u32,
}
struct Resources {
line_pipeline: wgpu::RenderPipeline,
tri_pipeline: wgpu::RenderPipeline,
quad_pipeline: wgpu::RenderPipeline,
/// One uploaded mesh with its own view uniform (each callback's transform differs).
struct Layer {
uniform: wgpu::Buffer,
bind_group: wgpu::BindGroup,
linear_out: bool,
key: Option<u64>,
lines: Vec<Batch>,
tris: Vec<Batch>,
@@ -253,23 +309,36 @@ struct Resources {
breaks: Vec<[usize; 3]>,
}
const UNIFORM_SIZE: u64 = 48;
struct Resources {
line_pipeline: wgpu::RenderPipeline,
tri_pipeline: wgpu::RenderPipeline,
quad_pipeline: wgpu::RenderPipeline,
linear_out: bool,
layers: Vec<Layer>,
}
const UNIFORM_SIZE: u64 = 80;
const SHADER: &str = r#"
struct View {
a: vec4<f32>, // offset x, offset y (world), scale (device px per unit), 1 = linear output
b: vec4<f32>, // canvas centre x, y (device px), screen w, h (device px)
c: vec4<f32>, // pixels per point
c: vec4<f32>, // pixels per point, dash period in points (0 = solid)
d: vec4<f32>, // transform linear part: a, b, c, d (x' = a x + c y, y' = b x + d y)
e: vec4<f32>, // transform translation x, y (world, origin-relative)
};
@group(0) @binding(0) var<uniform> view: View;
struct VOut {
@builtin(position) pos: vec4<f32>,
@location(0) color: vec4<f32>,
// Device pixels along a wide line from its start (dashing).
@location(1) along: f32,
};
fn to_px(p: vec2<f32>) -> vec2<f32> {
let w = (p + view.a.xy) * view.a.z;
let q = vec2<f32>(view.d.x * p.x + view.d.z * p.y, view.d.y * p.x + view.d.w * p.y) + view.e.xy;
let w = (q + view.a.xy) * view.a.z;
return vec2<f32>(view.b.x + w.x, view.b.y - w.y);
}
@@ -291,6 +360,7 @@ fn vs_line(@location(0) pos: vec2<f32>, @location(1) color: vec4<f32>) -> VOut {
var o: VOut;
o.pos = px_to_clip(to_px(pos));
o.color = out_color(color);
o.along = 0.0;
return o;
}
@@ -301,6 +371,7 @@ fn vs_quad(
@location(1) b: vec2<f32>,
@location(2) color: vec4<f32>,
@location(3) width: f32,
@location(4) start: f32,
) -> VOut {
let pa = to_px(a);
let pb = to_px(b);
@@ -319,11 +390,18 @@ fn vs_quad(
var o: VOut;
o.pos = px_to_clip(along + n * hw * s);
o.color = out_color(color);
o.along = start * view.a.z + mix(-hw, len + hw, t);
return o;
}
@fragment
fn fs_main(v: VOut) -> @location(0) vec4<f32> {
if (view.c.y > 0.0) {
// 6 on, 4 off (as the CPU selection highlight's dashes).
if (fract(v.along / (view.c.y * view.c.x)) > 0.6) {
discard;
}
}
return v.color;
}
"#;
@@ -334,7 +412,7 @@ fn vertex_layout() -> wgpu::VertexBufferLayout<'static> {
}
fn instance_layout() -> wgpu::VertexBufferLayout<'static> {
const ATTRS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Unorm8x4, 3 => Float32];
const ATTRS: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Unorm8x4, 3 => Float32, 4 => Float32];
wgpu::VertexBufferLayout { array_stride: INSTANCE_SIZE as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &ATTRS }
}
@@ -344,7 +422,7 @@ impl Resources {
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("cad_canvas"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
let entry = |binding: u32| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
count: None,
};
@@ -354,17 +432,20 @@ impl Resources {
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cad_canvas_view"),
size: UNIFORM_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cad_canvas"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
});
let layer = || {
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cad_canvas_view"),
size: UNIFORM_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cad_canvas"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
});
Layer { uniform, bind_group, key: None, lines: Vec::new(), tris: Vec::new(), quads: Vec::new(), breaks: Vec::new() }
};
let pipeline = |label: &str, vs: &str, topology: wgpu::PrimitiveTopology, buffer: wgpu::VertexBufferLayout<'static>| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
@@ -396,14 +477,8 @@ impl Resources {
line_pipeline: pipeline("cad_lines", "vs_line", wgpu::PrimitiveTopology::LineList, vertex_layout()),
tri_pipeline: pipeline("cad_fills", "vs_line", wgpu::PrimitiveTopology::TriangleList, vertex_layout()),
quad_pipeline: pipeline("cad_quads", "vs_quad", wgpu::PrimitiveTopology::TriangleList, instance_layout()),
uniform,
bind_group,
linear_out: target.format.is_srgb(),
key: None,
lines: Vec::new(),
tris: Vec::new(),
quads: Vec::new(),
breaks: Vec::new(),
layers: vec![layer(), layer(), layer()],
}
}
}
@@ -419,9 +494,79 @@ fn upload(device: &wgpu::Device, label: &str, bytes: &[u8], stride: usize, usage
.collect()
}
/// Create the canvas pipelines in the renderer's callback resources. Call once with the app's
/// render state (eframe `CreationContext::wgpu_render_state`).
pub fn install(rs: &egui_wgpu::RenderState) -> GpuTarget {
/// GPU trouble wgpu reported: uncaptured errors (validation, out of memory) and the device being
/// lost (a driver reset or a hung GPU). wgpu's default handler panics on the first uncaptured
/// error, closing the app with any unsaved work (#602); the handlers [`install`] registers record
/// them here instead, and the app autosaves and tells the user (see `CadApp::gpu_fault_tick`).
#[derive(Debug, Default)]
pub struct GpuFaults {
state: Mutex<FaultState>,
}
#[derive(Debug, Default)]
struct FaultState {
errors: u64,
errors_seen: u64,
lost: bool,
lost_seen: bool,
last: String,
}
/// What [`GpuFaults::take`] reports.
#[derive(Clone, Debug, PartialEq)]
pub struct GpuFault {
/// Uncaptured errors so far (an invalid resource can fail again every frame).
pub errors: u64,
/// The device was lost: nothing more can be drawn until the app restarts.
pub lost: bool,
/// The latest error or device-lost message.
pub message: String,
}
impl GpuFaults {
/// Errors logged in full before only every 1000th is (a repeating error would flood the log).
const LOGGED: u64 = 10;
pub fn error(&self, message: String) {
let Ok(mut s) = self.state.lock() else { return };
s.errors += 1;
if s.errors <= Self::LOGGED || s.errors % 1000 == 0 {
log::error!("wgpu error #{}: {message}", s.errors);
}
s.last = message;
}
pub fn device_lost(&self, reason: wgpu::DeviceLostReason, message: String) {
// `Destroyed` is the device being dropped on purpose (the app closing), not a failure.
if reason == wgpu::DeviceLostReason::Destroyed {
return;
}
log::error!("wgpu device lost ({reason:?}): {message}");
let Ok(mut s) = self.state.lock() else { return };
s.lost = true;
s.last = message;
}
/// Faults reported since the last call, if any.
pub fn take(&self) -> Option<GpuFault> {
let Ok(mut s) = self.state.lock() else { return None };
if s.errors == s.errors_seen && s.lost == s.lost_seen {
return None;
}
s.errors_seen = s.errors;
s.lost_seen = s.lost;
Some(GpuFault { errors: s.errors, lost: s.lost, message: s.last.clone() })
}
}
/// Create the canvas pipelines in the renderer's callback resources, and report wgpu errors and
/// device loss to `faults` instead of panicking. Call once with the app's render state (eframe
/// `CreationContext::wgpu_render_state`).
pub fn install(rs: &egui_wgpu::RenderState, faults: &Arc<GpuFaults>) -> GpuTarget {
let f = Arc::clone(faults);
rs.device.on_uncaptured_error(Arc::new(move |e: wgpu::Error| f.error(e.to_string())));
let f = Arc::clone(faults);
rs.device.set_device_lost_callback(move |reason, message| f.device_lost(reason, message));
let target = GpuTarget { format: rs.target_format };
let res = Resources::new(&rs.device, target);
rs.renderer.write().callback_resources.insert(res);
@@ -439,6 +584,8 @@ impl egui_wgpu::CallbackTrait for CanvasCallback {
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let Some(res) = resources.get_mut::<Resources>() else { return Vec::new() };
let linear_out = res.linear_out;
let Some(res) = res.layers.get_mut(self.layer) else { return Vec::new() };
if res.key != Some(self.key) {
let pending = self.slot.lock().ok().and_then(|mut s| s.take());
if let Some(m) = pending {
@@ -452,16 +599,20 @@ impl egui_wgpu::CallbackTrait for CanvasCallback {
}
let ppp = screen.pixels_per_point;
let [w, h] = screen.size_in_pixels;
let a = [self.offset[0], self.offset[1], self.scale * ppp, if res.linear_out { 1.0 } else { 0.0 }];
let a = [self.offset[0], self.offset[1], self.scale * ppp, if linear_out { 1.0 } else { 0.0 }];
let b = [self.center[0] * ppp, self.center[1] * ppp, w.max(1) as f32, h.max(1) as f32];
let c = [ppp, 0.0, 0.0, 0.0];
let bytes: Vec<u8> = a.iter().chain(&b).chain(&c).flat_map(|v| v.to_le_bytes()).collect();
let c = [ppp, self.dash, 0.0, 0.0];
let [ma, mb, mc, md, tx, ty] = self.xform;
let d = [ma, mb, mc, md];
let e = [tx, ty, 0.0, 0.0];
let bytes: Vec<u8> = a.iter().chain(&b).chain(&c).chain(&d).chain(&e).flat_map(|v| v.to_le_bytes()).collect();
queue.write_buffer(&res.uniform, 0, &bytes);
Vec::new()
}
fn paint(&self, info: egui::PaintCallbackInfo, pass: &mut wgpu::RenderPass<'static>, resources: &egui_wgpu::CallbackResources) {
let Some(res) = resources.get::<Resources>() else { return };
let Some(shared) = resources.get::<Resources>() else { return };
let Some(res) = shared.layers.get(self.layer) else { return };
if res.key != Some(self.key) {
return;
}
@@ -478,7 +629,7 @@ impl egui_wgpu::CallbackTrait for CanvasCallback {
let mut from = [0usize; 3];
for to in res.breaks.iter().copied().chain(std::iter::once(all)) {
for (i, (pipeline, batches)) in
[(&res.tri_pipeline, &res.tris), (&res.line_pipeline, &res.lines), (&res.quad_pipeline, &res.quads)].into_iter().enumerate()
[(&shared.tri_pipeline, &res.tris), (&shared.line_pipeline, &res.lines), (&shared.quad_pipeline, &res.quads)].into_iter().enumerate()
{
let parts = batch_ranges(batches.iter().map(|b| b.count), from[i]..to[i]);
if parts.is_empty() {
@@ -564,6 +715,20 @@ mod tests {
l
}
#[test]
fn highlight_dashes_run_on_along_each_polyline() {
let l = list();
// The 3-point polyline (two unit segments) and the point marker.
let m = build_highlight_mesh(&l, &[0, 2], Vec2::new(1e6, 1e6), [1, 2, 3, 255], 1.5);
assert_eq!(m.quad_instances(), 3);
let start = |i: usize| {
let o = i * INSTANCE_SIZE + 24;
f32::from_le_bytes([m.quads[o], m.quads[o + 1], m.quads[o + 2], m.quads[o + 3]])
};
assert_eq!([start(0), start(1), start(2)], [0.0, 1.0, 0.0]);
assert!(m.lines.is_empty() && m.tris.is_empty());
}
#[test]
fn mesh_counts_and_precision() {
let l = list();
File diff suppressed because it is too large Load Diff
+22
View File
@@ -112,6 +112,7 @@
Close All Alle schließen
Closed Geschlossen
Coincident Koinzident
Collapse Palettes Paletten einklappen
Collapse Tool Sets Werkzeuggruppen einklappen
Collinear Kollinear
Color Farbe
@@ -524,6 +525,7 @@
Plot Plotten
Plot (click: do not plot) Plotten (Klick: nicht plotten)
Plot Preview Plotvorschau
Plot Window Plotfenster
Plot style Plotstil
Plot style attached to Plotstil zugeordnet zu
Plot style table Plotstiltabelle
@@ -994,3 +996,23 @@
Hide Others Andere ausblenden
Accessibility Bedienungshilfen
Show System Cursor Systemzeiger anzeigen
3D Mouse... 3D-Maus...
3D Mouse 3D-Maus
Calibrate 3D Mouse 3D-Maus kalibrieren
3D Mouse Navigation Navigation mit der 3D-Maus
3D mouse input is unavailable on this platform 3D-Mauseingabe ist auf dieser Plattform nicht verfügbar
No supported 3D mouse found Keine unterstützte 3D-Maus gefunden
Release the 3D mouse to calibrate its resting position 3D-Maus loslassen, um ihre Ruheposition zu kalibrieren
3D mouse connected 3D-Maus verbunden
3D mouse busy, possibly 3DxWare or missing device permissions 3D-Maus belegt, möglicherweise durch 3DxWare oder fehlende Geräteberechtigungen
3D mouse disconnected 3D-Maus getrennt
3D mouse input could not be started 3D-Mauseingabe konnte nicht gestartet werden
Enable 3D mouse navigation Navigation mit der 3D-Maus aktivieren
Pan sensitivity Empfindlichkeit beim Verschieben
Zoom sensitivity Zoom-Empfindlichkeit
Reverse zoom direction Zoomrichtung umkehren
Calibrate Kalibrieren
Move sideways to pan; push/pull or twist to zoom. Seitlich bewegen zum Verschieben; drücken, ziehen oder drehen zum Zoomen.
Left device button: zoom extents. Right device button: calibrate. Linke Gerätetaste: auf Grenzen zoomen. Rechte Gerätetaste: kalibrieren.
Hatch Pattern Schraffurmuster
Search for pattern Muster suchen
1 # CADCraft German interface catalog — original translations by the CADCraft contributors.
112
113
114
115
116
117
118
525
526
527
528
529
530
531
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
+259 -18
View File
@@ -27,22 +27,34 @@
<> Not Equal <> Distinto de
= Equals = Igual a
> Greater than > Mayor que
A drawing opened from a file keeps that file's format. Un dibujo abierto desde un archivo conserva el formato de ese archivo.
A variable name starts with a letter and has only letters, digits and _ El nombre de una variable empieza por una letra y solo lleva letras, dígitos y _
About Acerca de
About CADCraft Acerca de CADCraft
Absolute Absolutas
Add Current Scale Añadir escala actual
Add Leader Añadir directriz
Add Vertex Añadir vértice
Add/Delete Scales... Añadir/eliminar escalas...
Additional angles Ángulos adicionales
Align Alinear
Align Text Alinear texto
Aligned Alineada
All Todas
All Annotation Objects Todos los objetos de anotación
All Scales Todas las escalas
All Used Layers Todas las capas usadas
All files Todos los archivos
An array that would create more objects than this is refused. Se rechaza una matriz que crearía más objetos que este límite.
Angle Ángulo
Angular Angular
Annotation Visibility Visibilidad de anotaciones
Annotation scale Escala de anotación
Annotative Object Scale Escala de objeto anotativo
Annotative Update Actualizar anotativos
Apparent Intersection Intersección aparente
Append to current selection set Añadir al conjunto de selección actual
Apply Aplicar
Apply to: Aplicar a:
Apply true color Aplicar color verdadero
Arc Arco
@@ -51,6 +63,7 @@
Arrange Panel Organizar panel
Array Matriz
Attach Adjuntar
Attach DWG... Adjuntar DWG...
Attribute Atributo
Attribute Definition Definición de atributo
Attribute Display Visualización de atributos
@@ -58,12 +71,18 @@
Auto (system language) Automático (idioma del sistema)
Auto Constrain Restricciones automáticas
AutoConstrain Restricciones automáticas
Automatic save every Guardado automático cada
Automatic save file location Ubicación de los archivos de guardado automático
Axis, End Eje, extremo
B N
Base Base
Baseline Línea base
Batch Publish Publicar en lote
Batch Publish... Publicar en lote...
Binary assets added Recursos binarios añadidos
Binary assets removed Recursos binarios eliminados
Bind Unir
Bind... Unir...
Blend Fusión de curvas
Block Bloque
Block Attribute Manager... Administrador de atributos de bloque...
@@ -79,11 +98,16 @@
Box Prisma rectangular
Break Partir
Break At Point Partir en un punto
Bring Above Objects Traer por encima de objetos
Bring Annotations to Front Traer las anotaciones al frente
Bring to Front Traer al frente
Bulleted list Lista con viñetas
ByBlock PorBloque
ByColor PorColor
ByLayer PorCapa
CADCraft CADCraft
CADCraft Help Ayuda de CADCraft
CADCraft keeps these preferences, object snap modes and drafting toggles for your user account and restores them when it starts. CADCraft guarda estas preferencias, los modos de referencia a objetos y los ajustes de dibujo para tu cuenta de usuario y los restaura al iniciarse.
Cancel Cancelar
Cartesian (x, y) Cartesianas (x, y)
Catppuccin Latte Catppuccin Latte
@@ -104,28 +128,41 @@
Chamfer Chaflán
Change Space Cambiar espacio
Change to Current Layer Cambiar a la capa actual
Changed: {n} Modificados: {n}
Changes apply at once and are kept for next time. Los cambios se aplican al instante y se conservan para la próxima vez.
Circle Círculo
Circumference Circunferencia
Clear Limpiar
Clear Command History Borrar el historial de comandos
Clear History Borrar historial
Clip Delimitar
Clip Viewport Delimitar ventana gráfica
Clipboard Portapapeles
Close Cerrar
Close All Cerrar todo
Close All Other Drawings Cerrar todos los demás dibujos
Close Array Editing Cerrar la edición de matriz
Close Comparison Cerrar comparación
Closed Cerrada
Coincident Coincidente
Collapse Tool Sets Contraer herramientas de dibujo
Collapse Palettes Contraer paletas
Collapse Tool Sets Contraer conjuntos de herramientas
Collinear Colineal
Color Color
Color 8 Color 8
Color 9 Color 9
Color Index Índice de color
Color... Color...
Command Aliases Alias de comandos
Command History Historial de comandos
Command Line Línea de comandos
Command Line History Lines Líneas del historial de comandos
Command Reference Referencia de comandos
Command line history Historial de la línea de comandos
Commits Commits
Company Empresa
Computer-aided design and drafting: an open-source, clean-room CAD application written in pure Rust. Diseño y dibujo asistidos por computadora: una aplicación CAD de código abierto, desarrollada de forma independiente en Rust.
Compare Drawings... Comparar dibujos...
Computer-aided design and drafting: an open-source, clean-room CAD application written in pure Rust. Diseño y dibujo asistidos por computadora: una aplicación CAD de código abierto, desarrollada de forma independiente y escrita íntegramente en Rust.
Concentric Concéntrica
Cone Cono
Constraint Bars Barras de restricciones
@@ -135,15 +172,25 @@
Continue Continuar
Contributors Colaboradores
Control Vertices Vértices de control
Convert Convertir
Convert to Arc Convertir en arco
Convert to Line Convertir en línea
Copy Copiar
Copy Full File Path Copiar la ruta completa del archivo
Copy History Copiar historial
Copy Layout Copiar presentación
Copy Nested Objects Copiar objetos anidados
Copy Selection Copiar selección
Copy as JSON Copiar como JSON
Copy with Base Point Copiar con punto base
Count Contar
Count List Lista de recuento
Count Table Tabla de recuento
Create Block Crear bloque
Creates a user parameter Crea un parámetro de usuario
Ctrl+Enter to finish Ctrl+Enter para terminar
Crosshair size Tamaño del cursor en cruz
Ctrl+Enter to finish Ctrl+Intro para terminar
Current Scale Only Solo la escala actual
Current layer Capa actual
Current selection Selección actual
Current: {color} Actual: {color}
@@ -151,17 +198,25 @@
Cyan Cian
Cylinder Cilindro
DWG DWG
DWG Reference... Referencia DWG...
DXF DXF
Dark Oscuro
Data Extraction... Extracción de datos...
Data Link... Vínculo de datos...
Default Predeterminado
Default Save Format Formato de guardado predeterminado
Default Save Version Versión de guardado predeterminada
Default save version Versión de guardado predeterminada
Define Attributes Definir atributos
Define Attributes... Definir atributos...
Defined width Anchura definida
Degree Grado
Degrees of Freedom Grados de libertad
Degrees of freedom: {n} Grados de libertad: {n}
Delete Eliminar
Delete Column Eliminar columna
Delete Constraints Eliminar restricciones
Delete Current Scale Eliminar escala actual
Delete Dimension Style Eliminar estilo de cota
Delete Duplicate Objects Eliminar objetos duplicados
Delete Layer Eliminar capa
@@ -173,9 +228,13 @@
Delta Y Incremento Y
Description Descripción
Deselect All Deseleccionar todo
Detach Desenlazar
Diameter Diámetro
Dim style Estilo de cota
Dimension Acotación
Dimension Break Partir cota
Dimension Edit Editar cota
Dimension Objects Only Solo objetos de cota
Dimension Space Espaciado de cotas
Dimension Style Override Modificar estilo de cota
Dimension Style... Estilo de cota...
@@ -185,6 +244,7 @@
Dimensional Constraint Parameters Parámetros de restricciones dimensionales
Dimensional Constraints Restricciones dimensionales
Disassociate Dimensions Desasociar cotas
Display Visualización
Display name Nombre visible
Distance Distancia
Divide Dividir
@@ -200,23 +260,32 @@
Drawing (DWG) Dibujo (DWG)
Drawing (DXF) Dibujo (DXF)
Drawing Limits Límites del dibujo
Drawing Recovery Manager... Administrador de recuperación de dibujos...
Drawing Template (DWT) Plantilla de dibujo (DWT)
Drawing Utilities Utilidades de dibujo
Drawings (DWG, DXF) Dibujos (DWG, DXF)
Drawings with no file yet are saved in the default version. 0 minutes turns automatic save off. Los dibujos que aún no tienen archivo se guardan en la versión predeterminada. 0 minutos desactiva el guardado automático.
Dynamic Dinámico
Dynamic Dimensions Cotas dinámicas
Dynamic Input Entrada dinámica
Dynamic Input (F12) Entrada dinámica (F12)
E E
Edit Edición
Edit Array Editar matriz
Edit Attributes... Editar atributos...
Edit Table Cell Editar celda de tabla
Edit Text... Editar texto...
Elevation Elevación
Ellipse Elipse
Elliptical Arc Arco elíptico
Enable Dynamic Input (F12) Activar entrada dinámica (F12)
End Object Isolation Terminar aislamiento de objetos
End Width Anchura final
End X X final
End Y Y final
End angle Ángulo final
Endpoint Punto final
Enter Intro
Enter Top, Zero or Bottom. Introduce Top, Zero o Bottom.
Enter a fit data option Introduce una opción de datos de ajuste
Enter a pattern name Introduce un nombre de patrón
@@ -263,14 +332,19 @@
Entire drawing Todo el dibujo
Equal Igual
Erase Borrar
Evaluate Evaluar
Exclude from new selection set Excluir del nuevo conjunto de selección
Explode Descomponer
Export Exportar
Export Table Exportar tabla
Export to PDF... Exportar a PDF...
Export... Exportar...
Expression Expresión
Extend Alargar
Extension Extensión
Extents Extensión
External Reference Referencia externa
External References Referencias externas
Extrude Extruir
File Archivo
File Tab Pestañas de archivos
@@ -293,6 +367,7 @@
Freeze/Thaw in all viewports Inutilizar/reutilizar en todas las ventanas gráficas
Freeze/Thaw in the current viewport Inutilizar/reutilizar en la ventana gráfica actual
From Desde
From Template... Desde plantilla...
General General
Geometric Center Centro geométrico
Geometric Constraint Restricción geométrica
@@ -300,24 +375,32 @@
Geometry Geometría
Get Variable Consultar variable
Get View Consultar vista
Get from the Drawing Obtener del dibujo
Global Global
Global width Anchura global
Grab bag Lista
Grab bag Miscelánea
Gradient Degradado
Gradient... Degradado...
Green Verde
Grid (F7) Rejilla (F7)
Grid Display Mostrar rejilla
Grid On (F7) Activar rejilla (F7)
Grid snap Forzar cursor a la rejilla
Grid spacing Espaciado de rejilla
Grip Edit Editar con pinzamientos
Grip Mirror Simetría con pinzamientos
Grip Option Opción de pinzamiento
Grip Rotate Girar con pinzamientos
Grip Scale Escalar con pinzamientos
Grip Stretch Estirar con pinzamientos
Grip size Tamaño de los pinzamientos
Group Agrupar
Group Edit Editar grupo
Group Options Opciones de grupo
Group Selection Selección de grupo
Hatch Sombreado
Hatch Edit Editar sombreado
Hatch Patterns Patrones de sombreado
Hatch... Sombreado...
Height Altura
Height: {height} Altura: {height}
@@ -327,24 +410,32 @@
Hide All Constraint Bars Ocultar todas las barras de restricciones
Hide All Dynamic Dimensions Ocultar todas las cotas dinámicas
Hide Layer List Ocultar lista de capas
Hide Objects Ocultar objetos
Hide Palette Ocultar paleta
Hide Palettes Ocultar paletas
History Historial
Hold Shift to add to the selection Mantener pulsada Mayús para añadir a la selección
Home Inicio
Horizontal Horizontal
HorizontalDistance Distancia horizontal
How to apply: Cómo aplicar:
ID Point Coordenadas de un punto
Import Importar
Import... Importar...
In Acercar
In-window Menu Bar Barra de menús en la ventana
Include in new selection set Incluir en el nuevo conjunto de selección
Increment angle Incremento angular
Index Color Color indexado
Index color: {index} Color de índice: {index}
Index color: {index} Color indexado: {index}
Infer Constraints Inferir restricciones
Inquiry Consultar
Insert Insertar
Insert Block Insertar bloque
Insert Column Insertar columna
Insert Row Insertar fila
Insert Table Formula Insertar fórmula en tabla
Insert tab Insertar tabulación
Insert this block Insertar este bloque
Insertion Inserción
Insertion X Inserción X
@@ -352,15 +443,25 @@
Insertion Z Inserción Z
Inspect Constraints Inspeccionar restricciones
Inspect Drawing Inspeccionar dibujo
Inspection Inspección
Interface Language Idioma de la interfaz
Interface Theme Tema de la interfaz
Interface language Idioma de la interfaz
Interface theme Tema de la interfaz
Intersect Intersección
Intersection Intersección
Isolate Aislar
Isolate Layer Aislar capa
Isolate Objects Aislar objetos
Isometric Drafting Dibujo isométrico
Isometric Plane Plano isométrico
Isometric snap Forzar cursor isométrico
Jogged Con recodo
Jogged Linear Lineal con recodo
Join Unir
Keep the system cursor visible over the drawing area Mantener visible el cursor del sistema sobre el área de dibujo
Language Idioma
Largest array Matriz más grande
Last Último
Last commit Último commit
Layer Capa
@@ -369,8 +470,8 @@
Layer Merge Fusionar capas
Layer Off Desactivar capa
Layer Properties Propiedades de capa
Layer Properties Manager Administrador de capas
Layer Properties Manager — current viewport Administrador de capas — ventana gráfica actual
Layer Properties Manager Administrador de propiedades de capas
Layer Properties Manager — current viewport Administrador de propiedades de capas — ventana gráfica actual
Layer State Estado de capas
Layer States Manager... Administrador de estados de capas...
Layer Tools Herramientas de capas
@@ -381,7 +482,9 @@
Layers displayed: {shown} of {total}. Current layer: {layer} Capas mostradas: {shown} de {total}. Capa actual: {layer}
Layout Presentación
Layout list Lista de presentaciones
Layout paper Papel de la presentación
Leader Directriz
Leaders Only Solo directrices
Left Izquierda
Length Longitud
Length X Longitud X
@@ -394,9 +497,12 @@
Lines +/− Líneas +/−
Lines added Líneas añadidas
Lines deleted Líneas eliminadas
Lines of Command History Líneas del historial de comandos
Linetype Tipo de línea
Linetype Scale Escala del tipo de línea
Linetype scale Escala del tipo de línea
Loaded Cargada
Lock viewport Bloquear ventana gráfica
Ltscale Escala de tipo de línea
Linetype... Tipo de línea...
Lineweight Grosor de línea
@@ -416,13 +522,15 @@
Lock Layer Bloquear capa
Lock/Unlock Bloquear/desbloquear
Loft Solevación
MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT O Apache-2.0. Sin afiliación con Autodesk, Inc.
MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Sin afiliación con Autodesk, Inc.
MODEL MODELO
MText Texto multilínea
Magenta Magenta
Major line every Línea principal cada
Make Current Establecer actual
Make Object's Layer Current Usar como actual la capa del objeto
Make it a block of this drawing, keeping its layer names Convertirla en un bloque de este dibujo, conservando los nombres de sus capas
Make it a block of this drawing; its layers become NAME$0$LAYER Convertirla en un bloque de este dibujo; sus capas pasan a ser NAME$0$LAYER
Make... Crear...
Match Layer Igualar capa
Match Properties Igualar propiedades
@@ -436,16 +544,20 @@
Mirror Simetría
Model Modelo
Model Space (in viewport) Espacio modelo (en ventana gráfica)
Model background Fondo del modelo
Modeling Modelado
Models Modelos
Modify Modificar
More toolbar commands Más comandos de la barra de herramientas
Move Desplazar
Multileader Directriz múltiple
Multileader Style... Estilo de directriz múltiple...
Multileader style Estilo de directriz múltiple
Multiline Línea múltiple
Multiline Style... Estilo de línea múltiple...
Multiline Text Texto multilínea
Multiline Text... Texto multilínea...
Multiline... Línea múltiple...
Multiple Varios
Multiple Point Puntos múltiples
My Mías
@@ -453,6 +565,7 @@
N N
Name Nombre
Name (A–Z) Nombre (A–Z)
Named Views... Vistas con nombre...
Nearest Cercano
New Nuevo
New (metric) Nuevo (métrico)
@@ -461,20 +574,26 @@
New Drawing... Nuevo dibujo...
New Layer Nueva capa
New Layout Nueva presentación
New VP Freeze Inutilizar en nuevas ventanas
New VP Freeze Inutilizar en nuevas ventanas gráficas
New Viewports... Nuevas ventanas gráficas...
New from Template... Nuevo desde plantilla...
New layout Nueva presentación
New state name Nombre del nuevo estado
Next Change Cambio siguiente
No No
No blocks defined in this drawing. No hay bloques definidos en este dibujo.
No contributor data was built into this copy. Esta copia no incluye datos de colaboradores.
No external references are attached. No hay referencias externas adjuntas.
No model credits were built into this copy. Esta copia no incluye créditos de modelos.
No saved layer states No hay estados de capas guardados
No selection Sin selección
Node Nodo
None Ninguno
Not Found No encontrada
Not available No disponible
Not available yet Aún no disponible
Not loaded No cargada
Numbered list Lista numerada
OK Aceptar
Object Objeto
Object Snap Referencia a objetos
@@ -482,16 +601,27 @@
Object Snap On (F3) Activar referencia a objetos (F3)
Object Snap Tracking Rastreo de referencia a objetos
Object Snap Tracking (F11) Rastreo de referencia a objetos (F11)
Object Snap Tracking Settings Configuración del rastreo de referencia a objetos
Object snap aperture Apertura de referencia a objetos
Object snap marker size Tamaño del marcador de referencia a objetos
Object type: Tipo de objeto:
Oblique Oblicua
Off Desactivado
Offset Desfase
On Activado
On/Off Activar/desactivar
Only in this drawing: {n} Solo en este dibujo: {n}
Only in {name}: {n} Solo en {name}: {n}
Open Abrir
Open (⌘O) Abrir (⌘O)
Open Sample Drawing Abrir dibujo de ejemplo
Open Xref Abrir referencia externa
Open and Save Abrir y guardar
Open sample drawing Abrir dibujo de ejemplo
Open... Abrir...
Operator: Operador:
Options Opciones
Options... Opciones...
Orbit Órbita
Ordinate Coordenadas
Ortho (F8) Ortogonal (F8)
@@ -499,9 +629,15 @@
Ortho Mode (F8) Modo ortogonal (F8)
Osnap Settings... Configuración de referencia a objetos...
Out Alejar
Over-constrained Restricciones excesivas
Overlay Superposición
Override Modificar
PAPER PAPEL
Paste to Command Line Pegar en la línea de comandos
Percent Porcentaje
Pickbox size Tamaño de la caja de selección
Plotter Manager Administrador de trazadores
Plotter Manager... Administrador de trazadores...
Polar (distance < angle) Polares (distancia < ángulo)
PRs PRs
Page Setup Manager Administrador de configuración de página
@@ -523,7 +659,8 @@
Perpendicular Perpendicular
Plot Trazar
Plot (click: do not plot) Se trazará (clic: no trazar)
Plot Preview Vista previa de impresión
Plot Preview Vista previa de trazado
Plot Window Ventana de trazado
Plot style Estilo de trazado
Plot style attached to Estilo de trazado asociado a
Plot style table Tabla de estilos de trazado
@@ -534,7 +671,74 @@
New CTB Nueva CTB
New STB Nueva STB
New style Nuevo estilo
Polar angle measurement Medición del ángulo polar
Polar distance Distancia polar
PolarSnap Forzar cursor polar
Polygonal Viewport Ventana gráfica poligonal
Polyline Edit Editar polilínea
Previous Change Cambio anterior
Profiles Perfiles
QuickCalc Pick Designar para la calculadora rápida
Recalculate Table Formulas Recalcular fórmulas de tabla
Recent Commands Comandos recientes
Recent Input Entradas recientes
Rectangular snap Forzar cursor rectangular
Reference Manager Administrador de referencias
Reference Name Nombre de la referencia
References: {n} Referencias: {n}
Reinitialize Reinicializar
Relative to last segment Relativo al último segmento
Reload Volver a cargar
Reload All Volver a cargar todas
Remove Vertex Eliminar vértice
Reset Panel Layout Restablecer disposición de paneles
Reset keeps the interface language. Restablecer conserva el idioma de la interfaz.
Reset to Defaults Restablecer valores predeterminados
SVG drawings (SVG, SVGZ) Dibujos SVG (SVG, SVGZ)
Save new drawings as Guardar los dibujos nuevos como
Saved Path Ruta guardada
Scale List... Lista de escalas...
Screen magnifiers, screen readers and recorders follow the system cursor. Las lupas, los lectores de pantalla y las grabadoras siguen el cursor del sistema.
Select Similar Seleccionar similares
Select objects first, then choose a command Seleccionar primero los objetos y luego elegir un comando
Selection Selección
Send Hatches to Back Enviar sombreados al fondo
Send Under Objects Enviar por debajo de objetos
Set Current Viewport Establecer ventana gráfica actual
Set Table Cell Style Establecer estilo de celda de tabla
Settings... Configuración...
Show Annotative Objects Mostrar objetos anotativos
Show Quick Properties for selected objects Mostrar las propiedades rápidas de los objetos seleccionados
Show and hide the tools of this category Mostrar y ocultar las herramientas de esta categoría
Show command prompting near the crosshairs Mostrar las solicitudes de comandos junto al cursor en cruz
Show in Tool Sets Mostrar en los conjuntos de herramientas
Snap Overrides Anulaciones de referencia a objetos
Snap type Tipo de forzado de cursor
Snap, grid, polar tracking and Dynamic Input: Forzar cursor, rejilla, rastreo polar y entrada dinámica:
Solid Editing Edición de sólidos
Spline Edit Editar spline
Stretch Vertex Estirar vértice
Synchronize Multiple-scale Positions Sincronizar posiciones de varias escalas
Table Breaks Divisiones de tabla
Table Cell Style Estilo de celda de tabla
Temporary track point Punto de rastreo temporal
Text Objects Only Solo objetos de texto
Thaw Layer Reutilizar capa
The aperture is how near the cursor must come to a point for an object snap to find it. La apertura indica cuánto debe acercarse el cursor a un punto para que la referencia a objetos lo encuentre.
Time-sensitive right-click: a quick click is Enter, holding opens the menu Clic derecho según duración: un clic rápido equivale a Intro; mantenerlo abre el menú
To A
Tolerance... Tolerancia...
Track orthogonally only Rastrear solo ortogonalmente
Track using all polar angle settings Rastrear con todos los ángulos polares configurados
Turn On Layer Activar capa
Turned off, a right-click while a command runs always opens the shortcut menu. Desactivado, un clic derecho durante un comando siempre abre el menú contextual.
Units Conversion Conversión de unidades
Unload Descargar
Unloaded Descargada
Unlock viewport Desbloquear ventana gráfica
Unreferenced Sin referencias
Unresolved Sin resolver
Update Data Links Actualizar vínculos de datos
Use for this layout Usar en esta presentación
Display plot styles Mostrar estilos de trazado
No plot style tables. No hay tablas de estilos de trazado.
@@ -695,7 +899,7 @@
Select objects to array. Selecciona objetos para crear la matriz.
Select objects to stretch by crossing-window or crossing-polygon... Selecciona objetos para estirar mediante ventana o polígono de captura...
Select objects: Selecciona objetos:
Select one polyline (Multiple is available through the JSON form). Selecciona una polilínea (Múltiple está disponible mediante JSON).
Select one polyline (Multiple is available through the JSON form). Selecciona una polilínea (la opción Multiple está disponible en la forma JSON).
Select path curve Selecciona la curva del camino
Select point or object Selecciona un punto u objeto
Select polyline or Selecciona una polilínea o
@@ -732,9 +936,9 @@
Snap Referencia
Snap Mode Forzar cursor
Snap Mode (F9) Forzar cursor (F9)
Snap On (F9) Forzar cursor (F9)
Snap X spacing Intervalo de resolución X
Snap and Grid Resolución y rejilla
Snap On (F9) Activar forzar cursor (F9)
Snap X spacing Intervalo X de forzado de cursor
Snap and Grid Forzar cursor y rejilla
Solid Sólido
Sort Ordenar
Specify a point Indica un punto
@@ -905,7 +1109,7 @@
Text Height Altura del texto
Text Style... Estilo de texto...
Text height Altura del texto
Text override Texto alternativo
Text override Sustitución de texto
Text style Estilo de texto
Thaw All Layers Reutilizar todas las capas
There is nothing here yet. Todavía no hay dibujos aquí.
@@ -913,7 +1117,7 @@
Time Tiempo
To get started, create or open a drawing. Para empezar, crea o abre un dibujo.
Tool Bar Barra de herramientas
Tool Sets Herramientas de dibujo
Tool Sets Conjuntos de herramientas
Tools Herramientas
Torus Toroide
Trace Trazo
@@ -941,12 +1145,20 @@
Use System Setting Usar configuración del sistema
User Usuario
User Parameters Parámetros de usuario
User Preferences Preferencias de usuario
Username Nombre de usuario
VP Color Color en ventana
VP Freeze Inutilizar en ventana
VP Color Color en ventana gráfica
VP Freeze Inutilizar en ventana gráfica
Value Valor
Value: Valor:
Variables Variables
Version Versión
Version 2000 Versión 2000
Version 2004 Versión 2004
Version 2007 Versión 2007
Version 2010 Versión 2010
Version 2013 Versión 2013
Version 2018 Versión 2018
Version {version} Versión {version}
Vertical Vertical
VerticalDistance Distancia vertical
@@ -955,8 +1167,11 @@
Viewport Ventana gráfica
Viewport Layer Freeze Inutilizar capas en ventana gráfica
Viewport Properties Propiedades de ventana gráfica
Viewport is locked La ventana gráfica está bloqueada
Viewport scale Escala de la ventana gráfica
Viewport {id} Ventana gráfica {id}
Viewports Ventanas gráficas
Volume Volumen
W O
WCS ⌄ SCU ⌄
White Blanco
@@ -965,18 +1180,24 @@
Wipeout Cobertura
Workspace Switching Cambiar espacio de trabajo
Write Block Escribir bloque
X X
Xline Línea auxiliar
Y Y
Yellow Amarillo
Yes Sí
Zoom Zoom
Zoom All Zoom a todo
Zoom Dynamic Zoom dinámico
Zoom Extents Zoom a extensión
Zoom In Acercar
Zoom Object Zoom a objeto
Zoom Out Alejar
Zoom Previous Zoom anterior
Zoom Realtime Zoom en tiempo real
Zoom Window Zoom por ventana
error error
min min
objects objetos
{contributors} · {commits} {contributors} · {commits}
{kind} (constraint {id}) {kind} (restricción {id})
{kind} constraint {name} = {expression} (double-click to edit) Restricción {kind} {name} = {expression} (doble clic para editar)
@@ -994,3 +1215,23 @@
Hide Others Ocultar otros
Accessibility Accesibilidad
Show System Cursor Mostrar cursor del sistema
3D Mouse... Ratón 3D...
3D Mouse Ratón 3D
Calibrate 3D Mouse Calibrar ratón 3D
3D Mouse Navigation Navegación con ratón 3D
3D mouse input is unavailable on this platform El ratón 3D no está disponible en esta plataforma
No supported 3D mouse found No se encontró un ratón 3D compatible
Release the 3D mouse to calibrate its resting position Suelta el ratón 3D para calibrar su posición de reposo
3D mouse connected Ratón 3D conectado
3D mouse busy, possibly 3DxWare or missing device permissions Ratón 3D ocupado, posiblemente por 3DxWare o por falta de permisos del dispositivo
3D mouse disconnected Ratón 3D desconectado
3D mouse input could not be started No se pudo iniciar el ratón 3D
Enable 3D mouse navigation Activar navegación con ratón 3D
Pan sensitivity Sensibilidad del encuadre
Zoom sensitivity Sensibilidad de zoom
Reverse zoom direction Invertir dirección del zoom
Calibrate Calibrar
Move sideways to pan; push/pull or twist to zoom. Mueve el mando lateralmente para encuadrar; empuja, tira o gira para hacer zoom.
Left device button: zoom extents. Right device button: calibrate. Botón izquierdo del dispositivo: zoom a la extensión. Botón derecho: calibrar.
Hatch Pattern Patrón de sombreado
Search for pattern Buscar patrón
1 # CADCraft Spanish interface catalog — original translations.
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
63
64
65
66
67
68
69
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
228
229
230
231
232
233
234
235
236
237
238
239
240
244
245
246
247
248
249
250
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
367
368
369
370
371
372
373
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
470
471
472
473
474
475
476
477
482
483
484
485
486
487
488
489
490
497
498
499
500
501
502
503
504
505
506
507
508
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
565
566
567
568
569
570
571
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
659
660
661
662
663
664
665
666
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
899
900
901
902
903
904
905
936
937
938
939
940
941
942
943
944
1109
1110
1111
1112
1113
1114
1115
1117
1118
1119
1120
1121
1122
1123
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
+997
View File
@@ -0,0 +1,997 @@
# CADCraft French interface catalog — original translations by @pechnikov2026-star.
# License: MIT OR Apache-2.0 (the repository LICENSE-MIT and LICENSE-APACHE).
# Written from CADCraft's English strings; no proprietary translations were copied.
# UTF-8: context<TAB>English source<TAB>French translation.
# Empty context = ordinary label; @plural = one|other forms with {n}.
# Command IDs, typed keywords, technology/product names and user data stay unchanged.
Cancel Annuler
OK OK
or [ ou [
% of all commits % de toutes les révisions
* Wildcard Match * Correspondance générique
*VARIES* *DIVER*
+ | Top | 2D Wireframe + | Dessus | Filaire 2D
+Bin +bin.
+LOC +lignes
1 Viewport 1 fenêtre
2 Points 2 points
2 Viewports 2 fenêtres
2D Solid Solide 2D
3 Points 3 points
3 Viewports 3 fenêtres
3D Face Face 3D
3D Operations Opérations 3D
3D Polyline Polyligne 3D
4 Viewports 4 fenêtres
< Less than < Inférieur à
<> Not Equal <> Différent de
= Equals = Égal à
> Greater than > Supérieur à
About À propos
About CADCraft À propos de CADCraft
Absolute Absolu
Add Leader Ajouter un repère
Align Aligner
Align Text Aligner le texte
Aligned Aligné
All Tous
All Used Layers Tous les calques utilisés
All files Tous les fichiers
Angle Angle
Angular Angulaire
Annotation Visibility Visibilité des annotations
Annotation scale Échelle des annotations
Apparent Intersection Intersection apparente
Append to current selection set Ajouter au jeu de sélection actuel
Apply to: Appliquer à :
Apply true color Appliquer la couleur RGB
Arc Arc
Arc Length Longueur d'arc
Area Aire
Arrange Panel Réorganiser le panneau
Array Réseau
Attach Joindre
Attribute Attribut
Attribute Definition Définition d'attribut
Attribute Display Affichage des attributs
Audit Vérifier
Auto (system language) Auto (langue du système)
Auto Constrain Contraintes automatiques
AutoConstrain ContraintesAuto
Axis, End Axe, extrémité
Base Base
Baseline Ligne de base
Batch Publish Publication par lots
Binary assets added Ressources binaires ajoutées
Binary assets removed Ressources binaires supprimées
Blend Raccord lissé
Block Bloc
Block Attribute Manager... Gestionnaire des attributs de bloc...
Block Editor Éditeur de blocs
Block Reference Référence de bloc
Block... Bloc...
Blocks Blocs
Blue Bleu
Bold Gras
Boolean Opérations booléennes
Boundary Frontière
Boundary... Frontière...
Box Pavé
Break Rupture
Break At Point Rupture en un point
Bring to Front Mettre au premier plan
ByBlock DuBloc
ByColor Par couleur
ByLayer DuCalque
CADCraft Help Aide de CADCraft
Cancel Annuler
Cartesian (x, y) Cartésien (x, y)
Center Centre
Center Line Ligne d'axe
Center Mark Marque de centre
Center X Centre X
Center Y Centre Y
Center Z Centre Z
Center, Diameter Centre, diamètre
Center, Radius Centre, rayon
Center, Start, Angle Centre, départ, angle
Center, Start, End Centre, départ, fin
Center, Start, Length Centre, départ, longueur
Chamfer Chanfrein
Change Space Changer d'espace
Change to Current Layer Déplacer vers le calque courant
Circle Cercle
Circumference Circonférence
Clear Effacer
Clipboard Presse-papiers
Close Fermer
Close All Tout fermer
Closed Fermé
Coincident Coïncidence
Collapse Tool Sets Réduire les jeux d'outils
Collinear Colinéarité
Color Couleur
Color 8 Couleur 8
Color 9 Couleur 9
Color Index Indice de couleur
Color... Couleur...
Command History Historique des commandes
Command Line Ligne de commande
Command Line History Lines Lignes d'historique de la ligne de commande
Command Reference Référence des commandes
Commits Révisions
Company Société
Computer-aided design and drafting: an open-source, clean-room CAD application written in pure Rust. Conception et dessin assistés par ordinateur : application de CAO open source, développée de manière indépendante en pur Rust.
Concentric Concentricité
Cone Cône
Constraint Bars Barres de contraintes
Constraint Settings Paramètres de contraintes
Construction Line Ligne de construction
Contents Sommaire
Continue Continuer
Contributors Contributeurs
Control Vertices Sommets de contrôle
Copy Copier
Copy Layout Copier la présentation
Copy Nested Objects Copier les objets imbriqués
Copy Selection Copier la sélection
Copy with Base Point Copier avec point de base
Count Nombre
Create Block Créer un bloc
Creates a user parameter Crée un paramètre utilisateur
Ctrl+Enter to finish Ctrl+Entrée pour terminer
Current layer Calque courant
Current selection Sélection actuelle
Current: {color} Actuel : {color}
Cut Couper
Cyan Cyan
Cylinder Cylindre
DWG DWG
DXF DXF
Dark Sombre
Default Par défaut
Default Save Format Format d'enregistrement par défaut
Define Attributes Définir les attributs
Define Attributes... Définir les attributs...
Defined width Largeur définie
Degree Degré
Delete Supprimer
Delete Column Supprimer la colonne
Delete Constraints Supprimer les contraintes
Delete Dimension Style Supprimer le style de cotation
Delete Duplicate Objects Supprimer les objets en double
Delete Layer Supprimer le calque
Delete Layer State Supprimer l'état des calques
Delete Layout Supprimer la présentation
Delete Row Supprimer la ligne
Delete Text Style Supprimer le style de texte
Delta X Écart X
Delta Y Écart Y
Description Description
Deselect All Désélectionner tout
Diameter Diamètre
Dim style Style de cotation
Dimension Cotation
Dimension Space Espace entre cotations
Dimension Style Override Substitution de style de cotation
Dimension Style... Style de cotation...
Dimension Text Edit Modifier le texte de cotation
Dimension style Style de cotation
Dimensional Constraint Contrainte dimensionnelle
Dimensional Constraint Parameters Paramètres de contraintes dimensionnelles
Dimensional Constraints Contraintes dimensionnelles
Disassociate Dimensions Dissocier les cotations
Display name Nom d'affichage
Distance Distance
Divide Diviser
Do not plot (click: plot) Ne pas tracer (cliquer pour tracer)
Donut Anneau
Double-click to edit the description Double-cliquez pour modifier la description
Down Bas
Drafting Dessin
Drafting Settings Paramètres de dessin
Drafting Settings... Paramètres de dessin...
Draw Dessin
Draw Order Ordre de dessin
Drawing (DWG) Dessin (DWG)
Drawing (DXF) Dessin (DXF)
Drawing Limits Limites du dessin
Drawing Utilities Utilitaires de dessin
Drawings (DWG, DXF) Dessins (DWG, DXF)
Dynamic Dimensions Cotations dynamiques
Dynamic Input Saisie dynamique
Dynamic Input (F12) Saisie dynamique (F12)
E Est
Edit Édition
Edit Text... Modifier le texte...
Elevation Élévation
Ellipse Ellipse
Elliptical Arc Arc elliptique
Enable Dynamic Input (F12) Activer la saisie dynamique (F12)
End Width Largeur finale
End X Extrémité X
End Y Extrémité Y
End angle Angle final
Endpoint Extrémité
Enter Top, Zero or Bottom. Entrez Top, Zero ou Bottom.
Enter a fit data option Entrez une option de données d'ajustage
Enter a pattern name Entrez un nom de motif
Enter a vertex editing option Entrez une option d'édition de sommet
Enter an option Entrez une option
Enter angle of xline Entrez l'angle de la ligne de construction
Enter angle of xline (0) or Entrez l'angle de la ligne de construction (0) ou
Enter area of rectangle in current units Entrez l'aire du rectangle dans les unités courantes
Enter attribute prompt Entrez l'invite de l'attribut
Enter attribute tag name Entrez le nom de l'étiquette d'attribut
Enter attribute value Entrez la valeur de l'attribut
Enter block name Entrez le nom du bloc
Enter constraint type Entrez le type de contrainte
Enter continuity Entrez la continuité
Enter default attribute value Entrez la valeur par défaut de l'attribut
Enter degree of spline Entrez le degré de la spline
Enter delta angle Entrez l'écart d'angle
Enter delta length or Entrez l'écart de longueur ou
Enter dimension text Entrez le texte de cotation
Enter dimension text (<> = measurement) Entrez le texte de cotation (<> = mesure)
Enter dimension value or [name=expression] Entrez la valeur de cotation ou [name=expression]
Enter dimension variable name to override Entrez le nom de la variable de cotation à substituer
Enter justification type Entrez le type de justification
Enter leader text Entrez le texte du repère
Enter mline scale Entrez l'échelle de la multiligne
Enter mode Entrez le mode
Enter name of block to insert Entrez le nom du bloc à insérer
Enter new text Entrez le nouveau texte
Enter next point Entrez le point suivant
Enter number of columns Entrez le nombre de colonnes
Enter number of data rows Entrez le nombre de lignes de données
Enter number of items Entrez le nombre d'éléments
Enter number of items along path Entrez le nombre d'éléments le long du tracé
Enter number of sides Entrez le nombre de côtés
Enter number of turns Entrez le nombre de tours
Enter percentage length Entrez la longueur en pourcentage
Enter rectangle length Entrez la longueur du rectangle
Enter rectangle width Entrez la largeur du rectangle
Enter scale factor Entrez le facteur d'échelle
Enter text Entrer le texte
Enter text (use \P for new paragraphs) Entrer le texte (utilisez \P pour les nouveaux paragraphes)
Enter the number of segments Entrer le nombre de segments
Enter twist direction of helix Entrer le sens de vrillage de l'hélice
Entire drawing Tout le dessin
Equal Égalité
Erase Effacer
Exclude from new selection set Exclure du nouveau jeu de sélection
Explode Décomposer
Export Exporter
Export to PDF... Exporter en PDF...
Expression Expression
Extend Prolonger
Extension Prolongement
Extents Étendue
Extrude Extruder
File Fichier
File Tab Onglet Fichier
Fillet Congé
Filter Filtre
Filters Filtres
Find and Replace Rechercher et remplacer
Find... Rechercher...
First Premier
First commit Première révision
Fit Points Points d'ajustement
Fix Fixation
Flatten Objects Aplatir les objets
Format Format
Freehand À main levée
Freeze Geler
Freeze Layer Geler le calque
Freeze in new viewports Geler dans les nouvelles fenêtres
Freeze/Thaw Geler/Dégeler
Freeze/Thaw in all viewports Geler/Dégeler dans toutes les fenêtres
Freeze/Thaw in the current viewport Geler/Dégeler dans la fenêtre courante
From À partir de
General Général
Geometric Center Centre géométrique
Geometric Constraint Contrainte géométrique
Geometric Constraints Contraintes géométriques
Geometry Géométrie
Get Variable Obtenir la variable
Get View Obtenir la vue
Global Global
Global width Largeur globale
Grab bag Liste
Gradient Dégradé
Gradient... Dégradé...
Green Vert
Grid (F7) Grille (F7)
Grid Display Affichage de la grille
Grid On (F7) Activer la grille (F7)
Grid spacing Espacement de la grille
Grip Mirror Miroir par poignées
Grip Rotate Rotation par poignées
Grip Scale Échelle par poignées
Grip Stretch Étirement par poignées
Group Grouper
Group Edit Modifier le groupe
Hatch Hachures
Hatch Edit Modifier les hachures
Hatch... Hachures...
Height Hauteur
Height: {height} Hauteur : {height}
Helix Hélice
Help Aide
Hide All Tout masquer
Hide All Constraint Bars Masquer toutes les barres de contraintes
Hide All Dynamic Dimensions Masquer toutes les cotes dynamiques
Hide Layer List Masquer la liste des calques
Hide Palettes Masquer les palettes
Home Vue d'accueil
Horizontal Horizontal
HorizontalDistance DistanceHorizontale
How to apply: Mode d'application :
ID Point ID du point
Import Importer
In Zoom avant
In-window Menu Bar Barre de menus dans la fenêtre
Include in new selection set Inclure dans le nouveau jeu de sélection
Increment angle Incrément d'angle
Index Color Couleur indexée
Index color: {index} Couleur indexée : {index}
Inquiry Interroger
Insert Insertion
Insert Block Insérer un bloc
Insert Column Insérer une colonne
Insert Row Insérer une ligne
Insert this block Insérer ce bloc
Insertion Point d'insertion
Insertion X Insertion X
Insertion Y Insertion Y
Insertion Z Insertion Z
Inspect Constraints Inspecter les contraintes
Inspect Drawing Inspecter le dessin
Interface Language Langue de l'interface
Interface Theme Thème de l'interface
Intersect Intersection
Intersection Intersection
Isolate Isoler
Isolate Layer Isoler le calque
Isometric Drafting Dessin isométrique
Join Rejoindre
Language Langue
Last Dernier
Last commit Dernière révision
Layer Calque
Layer Delete Supprimer le calque
Layer Match Faire correspondre le calque
Layer Merge Fusionner les calques
Layer Off Désactiver le calque
Layer Properties Propriétés du calque
Layer Properties Manager Gestionnaire des propriétés des calques
Layer Properties Manager — current viewport Gestionnaire des propriétés des calques — fenêtre courante
Layer State État des calques
Layer States Manager... Gestionnaire des états de calques...
Layer Tools Outils de calque
Layer contains objects (double-click to make current) Le calque contient des objets (double-cliquez pour le rendre courant)
Layer is empty (double-click to make current) Le calque est vide (double-cliquez pour le rendre courant)
Layers Calques
Layers Palette Palette des calques
Layers displayed: {shown} of {total}. Current layer: {layer} Calques affichés : {shown} sur {total}. Calque courant : {layer}
Layout Présentation
Layout list Liste des présentations
Leader Repère
Left Gauche
Length Longueur
Length X Longueur X
Length Y Longueur Y
Lengthen Modifier la longueur
Light Clair
Line Ligne
Line delta Variation de lignes
Linear Linéaire
Lines +/− Lignes +/−
Lines added Lignes ajoutées
Lines deleted Lignes supprimées
Linetype Type de ligne
Linetype Scale Échelle du type de ligne
Linetype scale Échelle du type de ligne
Ltscale Échelle du type de ligne
Linetype... Type de ligne...
Lineweight Épaisseur de ligne
Lineweight... Épaisseur de ligne...
List Liste
List Blocks Liste des blocs
List Dimension Styles Liste des styles de cote
List Groups Liste des groupes
List Layer States Liste des états de calques
List Layouts Liste des présentations
List System Variables Liste des variables système
List Text Styles Liste des styles de texte
Load all Tout charger
Load... Charger...
Loaded linetypes Types de ligne chargés
Lock Verrouiller
Lock Layer Verrouiller le calque
Lock/Unlock Verrouiller/Déverrouiller
Loft Loft
MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Non affilié à Autodesk, Inc.
MODEL MODÈLE
MText Texte multiligne
Magenta Magenta
Major line every Ligne principale tous les
Make Current Rendre courant
Make Object's Layer Current Rendre le calque de l'objet courant
Make... Créer...
Match Layer Faire correspondre le calque
Match Properties Copier les propriétés
Measure Mesurer
Measure Distance Mesurer la distance
Measure Geometry Mesurer la géométrie
Merge Cells Fusionner les cellules
Merged PRs PR fusionnés
Mid Between 2 Points Milieu entre 2 points
Midpoint Milieu
Mirror Miroir
Model Modèle
Model Space (in viewport) Espace objet (dans la fenêtre)
Modeling Modélisation
Models Modèles
Modify Modifier
Move Déplacer
Multileader Repère multiple
Multileader Style... Style de repère multiple...
Multileader style Style de repère multiple
Multiline Multiligne
Multiline Text Texte multiligne
Multiline Text... Texte multiligne...
Multiple Multiple
Multiple Point Points multiples
My Mes
My properties: not available yet Mes propriétés : pas encore disponibles
N N
Name Nom
Name (A–Z) Nom (A–Z)
Nearest Plus proche
New Nouveau
New (metric) Nouveau (métrique)
New Drawing Nouveau dessin
New Drawing (⌘N) Nouveau dessin (⌘N)
New Drawing... Nouveau dessin...
New Layer Nouveau calque
New Layout Nouvelle présentation
New VP Freeze Geler nouvelles fenêtres
New Viewports... Nouvelles fenêtres...
New layout Nouvelle présentation
New state name Nom du nouvel état
No Non
No blocks defined in this drawing. Aucun bloc défini dans ce dessin.
No contributor data was built into this copy. Cette version ne contient pas de données sur les contributeurs.
No model credits were built into this copy. Cette version ne contient pas de crédits de modèles.
No saved layer states Aucun état de calque enregistré
No selection Aucune sélection
Node Nœud
None Aucun
Not available Non disponible
Not available yet Pas encore disponible
OK OK
Object Objet
Object Snap Accrochage aux objets
Object Snap (F3) Accrochage aux objets (F3)
Object Snap On (F3) Activer l'accrochage aux objets (F3)
Object Snap Tracking Suivi de l'accrochage aux objets
Object Snap Tracking (F11) Suivi de l'accrochage aux objets (F11)
Object type: Type d'objet :
Off Désactivé
Offset Décalage
On Activé
On/Off Activer/Désactiver
Open (⌘O) Ouvrir (⌘O)
Open Sample Drawing Ouvrir un dessin d'exemple
Open sample drawing Ouvrir un dessin d'exemple
Open... Ouvrir...
Operator: Opérateur :
Orbit Orbite
Ordinate Ordonnée
Ortho (F8) Ortho (F8)
Ortho Mode Mode Ortho
Ortho Mode (F8) Mode Ortho (F8)
Osnap Settings... Paramètres d'accrochage aux objets...
Out Zoom arrière
Override Outrepasser
PAPER PAPIER
Percent Pourcentage
Polar (distance < angle) Polaire (distance < angle)
PRs PR
Page Setup Manager Gestionnaire des mises en page
Page Setup Manager... Gestionnaire des mises en page...
Pan Panoramique
Pan Down Panoramique vers le bas
Pan Left Panoramique vers la gauche
Pan Right Panoramique vers la droite
Pan Up Panoramique vers le haut
Paper Space Espace papier
Parallel Parallèle
Parameters Manager Gestionnaire des paramètres
Parametric Paramétrique
Paste Coller
Paste as Block Coller en tant que bloc
Paste to Original Coordinates Coller aux coordonnées d'origine
Path Array Réseau sur trajectoire
Pattern Motif
Perpendicular Perpendiculaire
Plot Tracer
Plot (click: do not plot) Tracer (cliquer : ne pas tracer)
Plot Preview Aperçu du tracé
Plot style Style de tracé
Plot style attached to Style de tracé associé à
Plot style table Table de styles de tracé
Plot Style... Style de tracé...
Plot Style Manager Gestionnaire des styles de tracé
Plot Style Manager... Gestionnaire des styles de tracé...
Plot Styles... Styles de tracé...
New CTB Nouvelle CTB
New STB Nouvelle STB
New style Nouveau style
Reset Panel Layout Réinitialiser la disposition des panneaux
Use for this layout Utiliser pour cet espace
Display plot styles Afficher les styles de tracé
No plot style tables. Aucune table de styles de tracé.
A built-in table: editing it keeps an edited copy in the drawing. Table intégrée : sa modification conserve une copie modifiée dans le dessin.
Keep an editable copy of this table in the drawing Conserver une copie modifiable de cette table dans le dessin
Layout {name}: {table} Espace {name} : {table}
Grayscale Niveaux de gris
Screening Trame
Use object linetype Utiliser le type de ligne de l'objet
Use object lineweight Utiliser l'épaisseur de ligne de l'objet
Remove Supprimer
Add Ajouter
Open a drawing first. Ouvrez d'abord un dessin.
Plot table type Type de table de tracé
Color-dependent Dépendante des couleurs
Named Nommée
Plus/minus Plus/moins
Point Point
Point Filters Filtres de points
Point Style... Style de point...
Polar Array Réseau polaire
Polar Tracking Recherche polaire
Polar Tracking (F10) Recherche polaire (F10)
Polar Tracking On (F10) Activer la recherche polaire (F10)
Polar: {distance} < {angle}° Polaire : {distance} < {angle}°
Polygon Polygone
Polygonal Polygonal
Polyline Polyligne
Position X Position X
Position Y Position Y
Position Z Position Z
Previous Précédent
Previous Layer Calque précédent
Primitives Primitives
Print Imprimer
Print... Imprimer...
Properties Propriétés
Properties Inspector Inspecteur de propriétés
Properties: Propriétés :
Proxy Objet proxy
Purge Purger
Pyramid Pyramide
Quadrant Quadrant
Query Entities Interroger les entités
Quick Rapide
Quick Dimension Cotation rapide
Quick Properties Propriétés rapides
Quick Select Sélection rapide
Quick Select Properties Propriétés de la sélection rapide
Quick Select... Sélection rapide...
QuickCalc Calculatrice rapide
Quit CADCraft Quitter CADCraft
Radius Rayon
Raster Image Image raster
Ray Rayon
Read-only Lecture seule
Real name Nom réel
Realtime Temps réel
Reassociate Dimensions Réassocier les cotes
Recent Récents
Recover... Récupérer...
Rectangle Rectangle
Rectangular Rectangulaire
Rectangular Array Réseau rectangulaire
Red Rouge
Redo Rétablir
Redo (⇧⌘Z) Rétablir (⇧⌘Z)
Redraw Redessiner
Regen Régénérer
Regen All Régénérer tout
Region Région
Region/Mass Properties Propriétés de région/masse
Relative Relatif
Remove Leader Supprimer la ligne de repère
Remove the viewport override Supprimer le remplacement de la fenêtre
Rename Dimension Style Renommer le style de cote
Rename Layer Renommer le calque
Rename Layer State Renommer l'état des calques
Rename Layout Renommer l'espace
Rename Text Style Renommer le style de texte
Rename... Renommer...
Repeat {command} Répéter {command}
Requires a point (x,y, @dx,dy or @d<a). Nécessite un point (x,y, @dx,dy ou @d<a).
Reset Palettes Réinitialiser les palettes
Restore Restaurer
Restore Layer State Restaurer l'état des calques
Reverse Inverser
Reverse the order Inverser l'ordre
Revision Cloud Nuage de révisions
Revolve Révolution
Right Droite
Rotate Pivoter
Rotation Rotation
S S
Save Enregistrer
Save (⌘S) Enregistrer (⌘S)
Save As Enregistrer sous
Save As... Enregistrer sous...
Save Layer State Enregistrer l'état des calques
Save State Enregistrer l'état
Scale Échelle
Search for layer Rechercher un calque
Search for parameter Rechercher un paramètre
Second and next points: Deuxième point et points suivants :
Select Sélectionner
Select 2D polyline Sélectionner une polyligne 2D
Select All Sélectionner tout
Select Objects Sélectionner les objets
Select a 2D polyline. Sélectionner une polyligne 2D.
Select a block reference. Sélectionner une référence de bloc.
Select a circle or arc. Sélectionner un cercle ou un arc.
Select a closed polyline Sélectionner une polyligne fermée
Select a closed polyline, circle, ellipse or closed spline. Sélectionner une polyligne fermée, un cercle, une ellipse ou une spline fermée.
Select a closed polyline. Sélectionner une polyligne fermée.
Select a curve as the path. Sélectionner une courbe comme trajectoire.
Select a dimension constraint type Sélectionner un type de contrainte dimensionnelle
Select a line object Sélectionner une ligne
Select a line object. Sélectionner une ligne.
Select a line or an arc. Sélectionner une ligne ou un arc.
Select a line or polyline segment Sélectionner une ligne ou un segment de polyligne
Select a line, arc or open polyline. Sélectionner une ligne, un arc ou une polyligne ouverte.
Select a line, arc, circle, polyline, ellipse or spline as the path. Sélectionner une ligne, un arc, un cercle, une polyligne, une ellipse ou une spline comme trajectoire.
Select a line, circle, arc or polyline segment. Sélectionner une ligne, un cercle, un arc ou un segment de polyligne.
Select a line. Sélectionner une ligne.
Select a polyline. Sélectionner une polyligne.
Select a text, mtext, attribute definition or dimension. Sélectionner un texte, un texte multiligne, une définition d'attribut ou une cote.
Select an annotation object Sélectionner un objet d'annotation
Select an arc or circle Sélectionner un arc ou un cercle
Select an arc. Sélectionner un arc.
Select an object Sélectionner un objet
Select an object to change or Sélectionner un objet à modifier ou
Select an object to measure or Sélectionner un objet à mesurer ou
Select an object. Sélectionner un objet.
Select an open curve. Sélectionner une courbe ouverte.
Select an open line, arc, polyline, ellipse arc or spline. Sélectionner une ligne, un arc, une polyligne, un arc elliptique ou une spline ouverts.
Select arc or circle Sélectionner un arc ou un cercle
Select arc or polyline arc segment Sélectionner un arc ou un segment d'arc de polyligne
Select arc style Sélectionner un style d'arc
Select arc, circle, line Sélectionner un arc, un cercle, une ligne
Select at least two objects to join. Sélectionner au moins deux objets à joindre.
Select circle or arc to add center mark Sélectionner un cercle ou un arc pour ajouter une marque de centre
Select closed objects (regions, closed polylines, circles). Sélectionner des objets fermés (régions, polylignes fermées, cercles).
Select first line Sélectionner la première ligne
Select first line or arc Sélectionner la première ligne ou le premier arc
Select first object Sélectionner le premier objet
Select first object or Sélectionner le premier objet ou
Select first point Sélectionner le premier point
Select nested objects to copy or Sélectionner des objets imbriqués à copier ou
Select object Sélectionner un objet
Select object to dimension Sélectionner un objet à coter
Select object to divide Sélectionner un objet à diviser
Select object to extend or shift-select to trim Sélectionner un objet à prolonger ou sélectionner avec Maj pour rogner
Select object to measure Sélectionner un objet à mesurer
Select object to offset Sélectionner un objet à décaler
Select object to trim or shift-select to extend Sélectionner un objet à rogner ou sélectionner avec Maj pour prolonger
Select objects Sélectionner les objets
Select objects (crossing window first corner) Sélectionner les objets (premier coin de la fenêtre de capture)
Select objects to array. Sélectionner les objets à répartir en réseau.
Select objects to stretch by crossing-window or crossing-polygon... Sélectionner les objets à étirer par fenêtre de capture ou polygone de capture...
Select objects: Sélectionner les objets :
Select one polyline (Multiple is available through the JSON form). Sélectionner une polyligne (l'option Multiple est disponible via le formulaire JSON).
Select path curve Sélectionner la courbe de trajectoire
Select point or object Sélectionner un point ou un objet
Select polyline or Sélectionner une polyligne ou
Select second line Sélectionner la deuxième ligne
Select second line or shift-select to apply corner Sélectionner la deuxième ligne ou sélectionner avec Maj pour former le coin
Select second object Sélectionner le deuxième objet
Select second object or shift-select to apply corner Sélectionner le deuxième objet ou sélectionner avec Maj pour former le coin
Select second point Sélectionner le deuxième point
Select spline Sélectionner une spline
Select symmetry line Sélectionner l'axe de symétrie
Select two points Sélectionner deux points
Selection Cycling Cycle de sélection
Send to Back Envoyer à l'arrière-plan
Set Current Définir comme courant
Set Current Dimension Style Définir le style de cote courant
Set Current Text Style Définir le style de texte courant
Set Layer Properties Définir les propriétés du calque
Set Properties Définir les propriétés
Set Table Cell Définir la cellule de tableau
Set Variable Définir la variable
Set View Définir la vue
Share Partager
Show Afficher
Show All Afficher tout
Show All Constraint Bars Afficher toutes les barres de contrainte
Show All Dynamic Dimensions Afficher toutes les cotes dynamiques
Show Layer List Afficher la liste des calques
Show/Hide Lineweight Afficher/Masquer l'épaisseur des lignes
Show/Hide Transparency Afficher/Masquer la transparence
Single Line Text Texte sur une ligne
Single Point Point unique
Single... Unique...
Smooth Lissage
Snap Accrochage
Snap Mode Mode d'accrochage
Snap Mode (F9) Mode d'accrochage (F9)
Snap On (F9) Activer l'accrochage (F9)
Snap X spacing Pas d'accrochage X
Snap and Grid Accrochage et grille
Solid Solide
Sort Trier
Specify a point Spécifier un point
Specify a scale for the hatch pattern Spécifier une échelle pour le motif de hachures
Specify a second extension line origin Spécifier l'origine de la deuxième ligne d'attache
Specify an angle for the hatch pattern Spécifier un angle pour le motif de hachures
Specify angle end point Spécifier le point final de l'angle
Specify angle of dimension line Spécifier l'angle de la ligne de cote
Specify angle of dimension text Spécifier l'angle du texte de cote
Specify angle start point Spécifier le point de départ de l'angle
Specify angle vertex Spécifier le sommet de l'angle
Specify angle vertex point Spécifier le point du sommet de l'angle
Specify arc length dimension location Spécifier l'emplacement de la cote de longueur d'arc
Specify axis endpoint of ellipse Spécifier l'extrémité de l'axe de l'ellipse
Specify base diameter Spécifiez le diamètre de la base
Specify base point Spécifiez le point de base
Specify base point or Spécifiez le point de base ou
Specify base point or displacement (drag with the middle mouse button for realtime pan) Spécifiez le point de base ou le déplacement (faites glisser avec le bouton central de la souris pour un panoramique en temps réel)
Specify base radius or Spécifiez le rayon de base ou
Specify center of donut Spécifiez le centre de l'anneau
Specify center of ellipse Spécifiez le centre de l'ellipse
Specify center of polygon Spécifiez le centre du polygone
Specify center point for circle Spécifiez le point central du cercle
Specify center point of arc Spécifiez le point central de l'arc
Specify center point of array Spécifiez le point central de la matrice
Specify center point of base Spécifiez le point central de la base
Specify corner of viewport Spécifiez le coin de la fenêtre
Specify corner of window, enter a scale factor (nX or nXP) Spécifiez le coin de la fenêtre, entrez un facteur d'échelle (nX ou nXP)
Specify diameter of circle Spécifiez le diamètre du cercle
Specify dimension arc line location Spécifiez l'emplacement de l'arc de cote
Specify dimension line location Spécifiez l'emplacement de la ligne de cote
Specify distance between turns Spécifiez la distance entre les spires
Specify distance to other axis Spécifiez la distance jusqu'à l'autre axe
Specify end angle Spécifiez l'angle de fin
Specify end point of arc Spécifiez le point final de l'arc
Specify end point of arc (hold Ctrl to switch direction) Spécifiez le point final de l'arc (maintenez Ctrl enfoncé pour changer de direction)
Specify ending width Spécifiez la largeur de fin
Specify ending width for next segment Spécifiez la largeur de fin du segment suivant
Specify endpoint of arc Spécifiez le point final de l'arc
Specify endpoint of axis Spécifiez le point final de l'axe
Specify endpoint of line Spécifiez le point final de la ligne
Specify feature location Spécifiez l'emplacement de l'élément
Specify fillet radius Spécifiez le rayon de congé
Specify fillet radius for rectangles Spécifiez le rayon de congé des rectangles
Specify first angle endpoint Spécifiez le premier point final de l'angle
Specify first chamfer distance Spécifiez la première distance de chanfrein
Specify first chamfer distance for rectangles Spécifiez la première distance de chanfrein des rectangles
Specify first constraint point Spécifiez le premier point de contrainte
Specify first corner Spécifiez le premier coin
Specify first corner point Spécifiez le premier point d'angle
Specify first destination point Spécifiez le premier point de destination
Specify first end point of circle's diameter Spécifiez le premier point final du diamètre du cercle
Specify first endpoint of edge Spécifiez le premier point final de l'arête
Specify first extension line origin Spécifiez l'origine de la première ligne d'attache
Specify first point Spécifiez le premier point
Specify first point of mirror line Spécifiez le premier point de la ligne de symétrie
Specify first point on circle Spécifiez le premier point sur le cercle
Specify first point on circle: _tan to Spécifiez le premier point sur le cercle : _tan à
Specify first source point Spécifiez le premier point source
Specify height Spécifiez la hauteur
Specify helix height or Spécifiez la hauteur de l'hélice ou
Specify included angle Spécifiez l'angle inclus
Specify insertion base point Spécifiez le point de base d'insertion
Specify insertion point Spécifiez le point d'insertion
Specify inside diameter of donut Spécifiez le diamètre intérieur de l'anneau
Specify leader arrowhead location Spécifiez l'emplacement de la tête de flèche du repère
Specify leader endpoint Spécifiez le point final du repère
Specify leader landing location Spécifiez l'emplacement de la base du repère
Specify length for rectangles Spécifiez la longueur des rectangles
Specify length of chord Spécifiez la longueur de la corde
Specify length of segment Spécifiez la longueur du segment
Specify line width for rectangles Spécifiez la largeur de ligne des rectangles
Specify location for new vertex Spécifiez l'emplacement du nouveau sommet
Specify minimum length of arc Spécifiez la longueur minimale de l'arc
Specify new end point Spécifiez le nouveau point final
Specify new location for marked vertex Spécifiez le nouvel emplacement du sommet marqué
Specify new width for all segments Spécifiez la nouvelle largeur de tous les segments
Specify next point Spécifiez le point suivant
Specify offset distance Spécifiez la distance de décalage
Specify offset distance or Spécifiez la distance de décalage ou
Specify opposite corner Spécifiez le coin opposé
Specify other corner point Spécifiez l'autre point d'angle
Specify other endpoint of axis Spécifiez l'autre point final de l'axe
Specify outside diameter of donut Spécifiez le diamètre extérieur de l'anneau
Specify point Spécifiez le point
Specify point on object for first tangent of circle Spécifiez le point sur l'objet pour la première tangente du cercle
Specify point on object for second tangent of circle Spécifiez le point sur l'objet pour la deuxième tangente du cercle
Specify point on side to offset Spécifiez le point du côté à décaler
Specify radius of arc Spécifiez le rayon de l'arc
Specify radius of circle Spécifiez le rayon du cercle
Specify rotation angle Spécifiez l'angle de rotation
Specify rotation angle of text Spécifiez l'angle de rotation du texte
Specify rotation angle or Spécifiez l'angle de rotation ou
Specify scale factor Spécifiez le facteur d'échelle
Specify second angle endpoint Spécifiez le deuxième point final de l'angle
Specify second break point Spécifiez le deuxième point de rupture
Specify second chamfer distance for rectangles Spécifiez la deuxième distance de chanfrein des rectangles
Specify second constraint point Spécifiez le deuxième point de contrainte
Specify second destination point Spécifiez le deuxième point de destination
Specify second end point of circle's diameter Spécifiez le deuxième point final du diamètre du cercle
Specify second endpoint of edge Spécifiez le deuxième point final de l'arête
Specify second extension line origin Spécifiez l'origine de la deuxième ligne d'attache
Specify second point Spécifiez le deuxième point
Specify second point of arc Spécifiez le deuxième point de l'arc
Specify second point of mirror line Spécifiez le deuxième point de la ligne de symétrie
Specify second point on circle Spécifiez le deuxième point sur le cercle
Specify second point on circle: _tan to Spécifiez le deuxième point sur le cercle : _tan à
Specify second point or <use first point as displacement> Spécifiez le deuxième point ou <utiliser le premier point comme déplacement>
Specify second source point Spécifiez le deuxième point source
Specify side to offset Spécifiez le côté à décaler
Specify start angle Spécifiez l'angle de départ
Specify start point Spécifiez le point de départ
Specify start point of arc Spécifiez le point de départ de l'arc
Specify start point of polyline Spécifiez le point de départ de la polyligne
Specify start point of text Spécifiez le point de départ du texte
Specify starting width Spécifiez la largeur de départ
Specify starting width for next segment Spécifiez la largeur de départ du segment suivant
Specify tangent direction for the start point of arc Spécifiez la direction de la tangente au point de départ de l'arc
Specify the elevation for rectangles Spécifiez l'élévation des rectangles
Specify thickness for rectangles Spécifiez l'épaisseur des rectangles
Specify third point on circle Spécifiez le troisième point sur le cercle
Specify third point on circle: _tan to Spécifiez le troisième point sur le cercle : _tan à
Specify third source point or <continue> Spécifiez le troisième point source ou <continuer>
Specify through point Spécifiez le point de passage
Specify top diameter Spécifiez le diamètre supérieur
Specify top radius or Spécifiez le rayon supérieur ou
Specify total angle Spécifiez l'angle total
Specify total length or Spécifiez la longueur totale ou
Specify width for rectangles Spécifiez la largeur des rectangles
Sphere Sphère
Spline Spline
Stack (type 1/2 then select) Empilement (tapez 1/2, puis sélectionnez)
Standard Standard
Start Départ
Start Width Largeur de départ
Start X X de départ
Start Y Y de départ
Start angle Angle de départ
Start, Center, Angle Départ, Centre, Angle
Start, Center, End Départ, Centre, Fin
Start, Center, Length Départ, Centre, Longueur
Start, End, Angle Départ, Fin, Angle
Start, End, Direction Départ, Fin, Direction
Start, End, Radius Départ, Fin, Rayon
Status État
Status Bar Barre d'état
Stretch Étirer
Style: Standard Style : Standard
Subtract Soustraction
Sweep Balayage
Switch Drawing Changer de dessin
Switch Layout Changer de présentation
Switch between model space in a viewport and paper space Basculer entre l'espace modèle dans une fenêtre et l'espace papier
Symmetric Symétrie
Synchronize Attributes Synchroniser les attributs
Toggle PICKADD (off: each pick replaces the selection) Basculer PICKADD (désactivé : chaque sélection remplace la sélection existante)
Toggle PICKADD (on: picks add to the selection) Basculer PICKADD (activé : les sélections s'ajoutent à la sélection existante)
TOP HAUT
Table Tableau
Table Style... Style de tableau...
Table style Style de tableau
Table... Tableau...
Tan, Tan, Radius Tangente, Tangente, Rayon
Tan, Tan, Tan Tangente, Tangente, Tangente
Tangent Tangente
Text Texte
Text Editor Éditeur de texte
Text Height Hauteur du texte
Text Style... Style de texte...
Text height Hauteur du texte
Text override Texte de remplacement
Text style Style de texte
Thaw All Layers Dégeler tous les calques
There is nothing here yet. Il n'y a encore rien ici.
Thickness Épaisseur
Time Heure
To get started, create or open a drawing. Pour commencer, créez ou ouvrez un dessin.
Tool Bar Barre d'outils
Tool Sets Ensembles d'outils
Tools Outils
Torus Tore
Trace Trace
Transparency Transparence
Trim Ajuster
True Color Couleur vraie
Turn All Layers On Activer tous les calques
Turn Off Layer Désactiver le calque
Turn Quick Properties off (QPMODE) Désactiver les propriétés rapides (QPMODE)
Type Type
Type a command Tapez une commande
Type text; Enter starts a new paragraph Saisissez le texte ; Entrée crée un nouveau paragraphe
UCS Icon Icône du SCU
Undo Annuler
Undo (⌘Z) Annuler (⌘Z)
Ungroup Dissocier
Union Union
Unisolate Layer Annuler l'isolement des calques
Units... Unités...
Unlock Layer Déverrouiller le calque
Unmerge Cells Annuler la fusion des cellules
Unsaved Layer State État de calque non enregistré
Up Haut
Update Mettre à jour
Use System Setting Utiliser le paramètre système
User Utilisateur
User Parameters Paramètres utilisateur
Username Nom d'utilisateur
VP Color Couleur de la fenêtre
VP Freeze Gel dans la fenêtre
Value Valeur
Value: Valeur :
Version Version
Version {version} Version {version}
Vertical Vertical
VerticalDistance VerticalDistance
View Vue
ViewCube ViewCube
Viewport Fenêtre
Viewport Layer Freeze Gel du calque dans la fenêtre
Viewport Properties Propriétés de la fenêtre
Viewport {id} Fenêtre {id}
Viewports Fenêtres
W O
WCS ⌄ SCG ⌄
White Blanc
Width factor Facteur de largeur
Window Fenêtre
Wipeout Masquage
Workspace Switching Changement d'espace de travail
Write Block Enregistrer le bloc dans un fichier
Xline Ligne de construction
Yellow Jaune
Yes Oui
Zoom Zoom
Zoom All Zoom tout
Zoom Extents Zoom étendu
Zoom In Zoom avant
Zoom Object Zoom objet
Zoom Out Zoom arrière
Zoom Previous Zoom précédent
Zoom Window Zoom fenêtre
error erreur
{contributors} · {commits} {contributors} · {commits}
{kind} (constraint {id}) {kind} (contrainte {id})
{kind} constraint {name} = {expression} (double-click to edit) Contrainte {kind} {name} = {expression} (double-cliquez pour modifier)
{name}: {distance} < {angle}° {name} : {distance} < {angle}°
ƒx New User Parameter ƒx Nouveau paramètre utilisateur
ΔLOC ΔLOC
−Bin −Bin
−LOC −LOC
⌘↩ to finish ⌘↩ pour terminer
@plural {n} commit {n} révision|{n} révisions
@plural {n} contributor {n} contributeur|{n} contributeurs
Drawing as bytes Dessin sous forme d'octets
Services Services
Hide CADCraft Masquer CADCraft
Hide Others Masquer les autres
Accessibility Accessibilité
Show System Cursor Afficher le curseur système
Import... Importer...
Export... Exporter...
Batch Publish... Publication par lots...
Hatch Pattern Motif de hachure
Search for pattern Rechercher un motif
1 # CADCraft French interface catalog — original translations by @pechnikov2026-star.
2 # License: MIT OR Apache-2.0 (the repository LICENSE-MIT and LICENSE-APACHE).
3 # Written from CADCraft's English strings; no proprietary translations were copied.
4 # UTF-8: context<TAB>English source<TAB>French translation.
5 # Empty context = ordinary label; @plural = one|other forms with {n}.
6 # Command IDs, typed keywords, technology/product names and user data stay unchanged.
7 Cancel Annuler
8 OK OK
9 or [ ou [
10 % of all commits % de toutes les révisions
11 * Wildcard Match * Correspondance générique
12 *VARIES* *DIVER*
13 + | Top | 2D Wireframe + | Dessus | Filaire 2D
14 +Bin +bin.
15 +LOC +lignes
16 1 Viewport 1 fenêtre
17 2 Points 2 points
18 2 Viewports 2 fenêtres
19 2D Solid Solide 2D
20 3 Points 3 points
21 3 Viewports 3 fenêtres
22 3D Face Face 3D
23 3D Operations Opérations 3D
24 3D Polyline Polyligne 3D
25 4 Viewports 4 fenêtres
26 < Less than < Inférieur à
27 <> Not Equal <> Différent de
28 = Equals = Égal à
29 > Greater than > Supérieur à
30 About À propos
31 About CADCraft À propos de CADCraft
32 Absolute Absolu
33 Add Leader Ajouter un repère
34 Align Aligner
35 Align Text Aligner le texte
36 Aligned Aligné
37 All Tous
38 All Used Layers Tous les calques utilisés
39 All files Tous les fichiers
40 Angle Angle
41 Angular Angulaire
42 Annotation Visibility Visibilité des annotations
43 Annotation scale Échelle des annotations
44 Apparent Intersection Intersection apparente
45 Append to current selection set Ajouter au jeu de sélection actuel
46 Apply to: Appliquer à :
47 Apply true color Appliquer la couleur RGB
48 Arc Arc
49 Arc Length Longueur d'arc
50 Area Aire
51 Arrange Panel Réorganiser le panneau
52 Array Réseau
53 Attach Joindre
54 Attribute Attribut
55 Attribute Definition Définition d'attribut
56 Attribute Display Affichage des attributs
57 Audit Vérifier
58 Auto (system language) Auto (langue du système)
59 Auto Constrain Contraintes automatiques
60 AutoConstrain ContraintesAuto
61 Axis, End Axe, extrémité
62 Base Base
63 Baseline Ligne de base
64 Batch Publish Publication par lots
65 Binary assets added Ressources binaires ajoutées
66 Binary assets removed Ressources binaires supprimées
67 Blend Raccord lissé
68 Block Bloc
69 Block Attribute Manager... Gestionnaire des attributs de bloc...
70 Block Editor Éditeur de blocs
71 Block Reference Référence de bloc
72 Block... Bloc...
73 Blocks Blocs
74 Blue Bleu
75 Bold Gras
76 Boolean Opérations booléennes
77 Boundary Frontière
78 Boundary... Frontière...
79 Box Pavé
80 Break Rupture
81 Break At Point Rupture en un point
82 Bring to Front Mettre au premier plan
83 ByBlock DuBloc
84 ByColor Par couleur
85 ByLayer DuCalque
86 CADCraft Help Aide de CADCraft
87 Cancel Annuler
88 Cartesian (x, y) Cartésien (x, y)
89 Center Centre
90 Center Line Ligne d'axe
91 Center Mark Marque de centre
92 Center X Centre X
93 Center Y Centre Y
94 Center Z Centre Z
95 Center, Diameter Centre, diamètre
96 Center, Radius Centre, rayon
97 Center, Start, Angle Centre, départ, angle
98 Center, Start, End Centre, départ, fin
99 Center, Start, Length Centre, départ, longueur
100 Chamfer Chanfrein
101 Change Space Changer d'espace
102 Change to Current Layer Déplacer vers le calque courant
103 Circle Cercle
104 Circumference Circonférence
105 Clear Effacer
106 Clipboard Presse-papiers
107 Close Fermer
108 Close All Tout fermer
109 Closed Fermé
110 Coincident Coïncidence
111 Collapse Tool Sets Réduire les jeux d'outils
112 Collinear Colinéarité
113 Color Couleur
114 Color 8 Couleur 8
115 Color 9 Couleur 9
116 Color Index Indice de couleur
117 Color... Couleur...
118 Command History Historique des commandes
119 Command Line Ligne de commande
120 Command Line History Lines Lignes d'historique de la ligne de commande
121 Command Reference Référence des commandes
122 Commits Révisions
123 Company Société
124 Computer-aided design and drafting: an open-source, clean-room CAD application written in pure Rust. Conception et dessin assistés par ordinateur : application de CAO open source, développée de manière indépendante en pur Rust.
125 Concentric Concentricité
126 Cone Cône
127 Constraint Bars Barres de contraintes
128 Constraint Settings Paramètres de contraintes
129 Construction Line Ligne de construction
130 Contents Sommaire
131 Continue Continuer
132 Contributors Contributeurs
133 Control Vertices Sommets de contrôle
134 Copy Copier
135 Copy Layout Copier la présentation
136 Copy Nested Objects Copier les objets imbriqués
137 Copy Selection Copier la sélection
138 Copy with Base Point Copier avec point de base
139 Count Nombre
140 Create Block Créer un bloc
141 Creates a user parameter Crée un paramètre utilisateur
142 Ctrl+Enter to finish Ctrl+Entrée pour terminer
143 Current layer Calque courant
144 Current selection Sélection actuelle
145 Current: {color} Actuel : {color}
146 Cut Couper
147 Cyan Cyan
148 Cylinder Cylindre
149 DWG DWG
150 DXF DXF
151 Dark Sombre
152 Default Par défaut
153 Default Save Format Format d'enregistrement par défaut
154 Define Attributes Définir les attributs
155 Define Attributes... Définir les attributs...
156 Defined width Largeur définie
157 Degree Degré
158 Delete Supprimer
159 Delete Column Supprimer la colonne
160 Delete Constraints Supprimer les contraintes
161 Delete Dimension Style Supprimer le style de cotation
162 Delete Duplicate Objects Supprimer les objets en double
163 Delete Layer Supprimer le calque
164 Delete Layer State Supprimer l'état des calques
165 Delete Layout Supprimer la présentation
166 Delete Row Supprimer la ligne
167 Delete Text Style Supprimer le style de texte
168 Delta X Écart X
169 Delta Y Écart Y
170 Description Description
171 Deselect All Désélectionner tout
172 Diameter Diamètre
173 Dim style Style de cotation
174 Dimension Cotation
175 Dimension Space Espace entre cotations
176 Dimension Style Override Substitution de style de cotation
177 Dimension Style... Style de cotation...
178 Dimension Text Edit Modifier le texte de cotation
179 Dimension style Style de cotation
180 Dimensional Constraint Contrainte dimensionnelle
181 Dimensional Constraint Parameters Paramètres de contraintes dimensionnelles
182 Dimensional Constraints Contraintes dimensionnelles
183 Disassociate Dimensions Dissocier les cotations
184 Display name Nom d'affichage
185 Distance Distance
186 Divide Diviser
187 Do not plot (click: plot) Ne pas tracer (cliquer pour tracer)
188 Donut Anneau
189 Double-click to edit the description Double-cliquez pour modifier la description
190 Down Bas
191 Drafting Dessin
192 Drafting Settings Paramètres de dessin
193 Drafting Settings... Paramètres de dessin...
194 Draw Dessin
195 Draw Order Ordre de dessin
196 Drawing (DWG) Dessin (DWG)
197 Drawing (DXF) Dessin (DXF)
198 Drawing Limits Limites du dessin
199 Drawing Utilities Utilitaires de dessin
200 Drawings (DWG, DXF) Dessins (DWG, DXF)
201 Dynamic Dimensions Cotations dynamiques
202 Dynamic Input Saisie dynamique
203 Dynamic Input (F12) Saisie dynamique (F12)
204 E Est
205 Edit Édition
206 Edit Text... Modifier le texte...
207 Elevation Élévation
208 Ellipse Ellipse
209 Elliptical Arc Arc elliptique
210 Enable Dynamic Input (F12) Activer la saisie dynamique (F12)
211 End Width Largeur finale
212 End X Extrémité X
213 End Y Extrémité Y
214 End angle Angle final
215 Endpoint Extrémité
216 Enter Top, Zero or Bottom. Entrez Top, Zero ou Bottom.
217 Enter a fit data option Entrez une option de données d'ajustage
218 Enter a pattern name Entrez un nom de motif
219 Enter a vertex editing option Entrez une option d'édition de sommet
220 Enter an option Entrez une option
221 Enter angle of xline Entrez l'angle de la ligne de construction
222 Enter angle of xline (0) or Entrez l'angle de la ligne de construction (0) ou
223 Enter area of rectangle in current units Entrez l'aire du rectangle dans les unités courantes
224 Enter attribute prompt Entrez l'invite de l'attribut
225 Enter attribute tag name Entrez le nom de l'étiquette d'attribut
226 Enter attribute value Entrez la valeur de l'attribut
227 Enter block name Entrez le nom du bloc
228 Enter constraint type Entrez le type de contrainte
229 Enter continuity Entrez la continuité
230 Enter default attribute value Entrez la valeur par défaut de l'attribut
231 Enter degree of spline Entrez le degré de la spline
232 Enter delta angle Entrez l'écart d'angle
233 Enter delta length or Entrez l'écart de longueur ou
234 Enter dimension text Entrez le texte de cotation
235 Enter dimension text (<> = measurement) Entrez le texte de cotation (<> = mesure)
236 Enter dimension value or [name=expression] Entrez la valeur de cotation ou [name=expression]
237 Enter dimension variable name to override Entrez le nom de la variable de cotation à substituer
238 Enter justification type Entrez le type de justification
239 Enter leader text Entrez le texte du repère
240 Enter mline scale Entrez l'échelle de la multiligne
241 Enter mode Entrez le mode
242 Enter name of block to insert Entrez le nom du bloc à insérer
243 Enter new text Entrez le nouveau texte
244 Enter next point Entrez le point suivant
245 Enter number of columns Entrez le nombre de colonnes
246 Enter number of data rows Entrez le nombre de lignes de données
247 Enter number of items Entrez le nombre d'éléments
248 Enter number of items along path Entrez le nombre d'éléments le long du tracé
249 Enter number of sides Entrez le nombre de côtés
250 Enter number of turns Entrez le nombre de tours
251 Enter percentage length Entrez la longueur en pourcentage
252 Enter rectangle length Entrez la longueur du rectangle
253 Enter rectangle width Entrez la largeur du rectangle
254 Enter scale factor Entrez le facteur d'échelle
255 Enter text Entrer le texte
256 Enter text (use \P for new paragraphs) Entrer le texte (utilisez \P pour les nouveaux paragraphes)
257 Enter the number of segments Entrer le nombre de segments
258 Enter twist direction of helix Entrer le sens de vrillage de l'hélice
259 Entire drawing Tout le dessin
260 Equal Égalité
261 Erase Effacer
262 Exclude from new selection set Exclure du nouveau jeu de sélection
263 Explode Décomposer
264 Export Exporter
265 Export to PDF... Exporter en PDF...
266 Expression Expression
267 Extend Prolonger
268 Extension Prolongement
269 Extents Étendue
270 Extrude Extruder
271 File Fichier
272 File Tab Onglet Fichier
273 Fillet Congé
274 Filter Filtre
275 Filters Filtres
276 Find and Replace Rechercher et remplacer
277 Find... Rechercher...
278 First Premier
279 First commit Première révision
280 Fit Points Points d'ajustement
281 Fix Fixation
282 Flatten Objects Aplatir les objets
283 Format Format
284 Freehand À main levée
285 Freeze Geler
286 Freeze Layer Geler le calque
287 Freeze in new viewports Geler dans les nouvelles fenêtres
288 Freeze/Thaw Geler/Dégeler
289 Freeze/Thaw in all viewports Geler/Dégeler dans toutes les fenêtres
290 Freeze/Thaw in the current viewport Geler/Dégeler dans la fenêtre courante
291 From À partir de
292 General Général
293 Geometric Center Centre géométrique
294 Geometric Constraint Contrainte géométrique
295 Geometric Constraints Contraintes géométriques
296 Geometry Géométrie
297 Get Variable Obtenir la variable
298 Get View Obtenir la vue
299 Global Global
300 Global width Largeur globale
301 Grab bag Liste
302 Gradient Dégradé
303 Gradient... Dégradé...
304 Green Vert
305 Grid (F7) Grille (F7)
306 Grid Display Affichage de la grille
307 Grid On (F7) Activer la grille (F7)
308 Grid spacing Espacement de la grille
309 Grip Mirror Miroir par poignées
310 Grip Rotate Rotation par poignées
311 Grip Scale Échelle par poignées
312 Grip Stretch Étirement par poignées
313 Group Grouper
314 Group Edit Modifier le groupe
315 Hatch Hachures
316 Hatch Edit Modifier les hachures
317 Hatch... Hachures...
318 Height Hauteur
319 Height: {height} Hauteur : {height}
320 Helix Hélice
321 Help Aide
322 Hide All Tout masquer
323 Hide All Constraint Bars Masquer toutes les barres de contraintes
324 Hide All Dynamic Dimensions Masquer toutes les cotes dynamiques
325 Hide Layer List Masquer la liste des calques
326 Hide Palettes Masquer les palettes
327 Home Vue d'accueil
328 Horizontal Horizontal
329 HorizontalDistance DistanceHorizontale
330 How to apply: Mode d'application :
331 ID Point ID du point
332 Import Importer
333 In Zoom avant
334 In-window Menu Bar Barre de menus dans la fenêtre
335 Include in new selection set Inclure dans le nouveau jeu de sélection
336 Increment angle Incrément d'angle
337 Index Color Couleur indexée
338 Index color: {index} Couleur indexée : {index}
339 Inquiry Interroger
340 Insert Insertion
341 Insert Block Insérer un bloc
342 Insert Column Insérer une colonne
343 Insert Row Insérer une ligne
344 Insert this block Insérer ce bloc
345 Insertion Point d'insertion
346 Insertion X Insertion X
347 Insertion Y Insertion Y
348 Insertion Z Insertion Z
349 Inspect Constraints Inspecter les contraintes
350 Inspect Drawing Inspecter le dessin
351 Interface Language Langue de l'interface
352 Interface Theme Thème de l'interface
353 Intersect Intersection
354 Intersection Intersection
355 Isolate Isoler
356 Isolate Layer Isoler le calque
357 Isometric Drafting Dessin isométrique
358 Join Rejoindre
359 Language Langue
360 Last Dernier
361 Last commit Dernière révision
362 Layer Calque
363 Layer Delete Supprimer le calque
364 Layer Match Faire correspondre le calque
365 Layer Merge Fusionner les calques
366 Layer Off Désactiver le calque
367 Layer Properties Propriétés du calque
368 Layer Properties Manager Gestionnaire des propriétés des calques
369 Layer Properties Manager — current viewport Gestionnaire des propriétés des calques — fenêtre courante
370 Layer State État des calques
371 Layer States Manager... Gestionnaire des états de calques...
372 Layer Tools Outils de calque
373 Layer contains objects (double-click to make current) Le calque contient des objets (double-cliquez pour le rendre courant)
374 Layer is empty (double-click to make current) Le calque est vide (double-cliquez pour le rendre courant)
375 Layers Calques
376 Layers Palette Palette des calques
377 Layers displayed: {shown} of {total}. Current layer: {layer} Calques affichés : {shown} sur {total}. Calque courant : {layer}
378 Layout Présentation
379 Layout list Liste des présentations
380 Leader Repère
381 Left Gauche
382 Length Longueur
383 Length X Longueur X
384 Length Y Longueur Y
385 Lengthen Modifier la longueur
386 Light Clair
387 Line Ligne
388 Line delta Variation de lignes
389 Linear Linéaire
390 Lines +/− Lignes +/−
391 Lines added Lignes ajoutées
392 Lines deleted Lignes supprimées
393 Linetype Type de ligne
394 Linetype Scale Échelle du type de ligne
395 Linetype scale Échelle du type de ligne
396 Ltscale Échelle du type de ligne
397 Linetype... Type de ligne...
398 Lineweight Épaisseur de ligne
399 Lineweight... Épaisseur de ligne...
400 List Liste
401 List Blocks Liste des blocs
402 List Dimension Styles Liste des styles de cote
403 List Groups Liste des groupes
404 List Layer States Liste des états de calques
405 List Layouts Liste des présentations
406 List System Variables Liste des variables système
407 List Text Styles Liste des styles de texte
408 Load all Tout charger
409 Load... Charger...
410 Loaded linetypes Types de ligne chargés
411 Lock Verrouiller
412 Lock Layer Verrouiller le calque
413 Lock/Unlock Verrouiller/Déverrouiller
414 Loft Loft
415 MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Non affilié à Autodesk, Inc.
416 MODEL MODÈLE
417 MText Texte multiligne
418 Magenta Magenta
419 Major line every Ligne principale tous les
420 Make Current Rendre courant
421 Make Object's Layer Current Rendre le calque de l'objet courant
422 Make... Créer...
423 Match Layer Faire correspondre le calque
424 Match Properties Copier les propriétés
425 Measure Mesurer
426 Measure Distance Mesurer la distance
427 Measure Geometry Mesurer la géométrie
428 Merge Cells Fusionner les cellules
429 Merged PRs PR fusionnés
430 Mid Between 2 Points Milieu entre 2 points
431 Midpoint Milieu
432 Mirror Miroir
433 Model Modèle
434 Model Space (in viewport) Espace objet (dans la fenêtre)
435 Modeling Modélisation
436 Models Modèles
437 Modify Modifier
438 Move Déplacer
439 Multileader Repère multiple
440 Multileader Style... Style de repère multiple...
441 Multileader style Style de repère multiple
442 Multiline Multiligne
443 Multiline Text Texte multiligne
444 Multiline Text... Texte multiligne...
445 Multiple Multiple
446 Multiple Point Points multiples
447 My Mes
448 My properties: not available yet Mes propriétés : pas encore disponibles
449 N N
450 Name Nom
451 Name (A–Z) Nom (A–Z)
452 Nearest Plus proche
453 New Nouveau
454 New (metric) Nouveau (métrique)
455 New Drawing Nouveau dessin
456 New Drawing (⌘N) Nouveau dessin (⌘N)
457 New Drawing... Nouveau dessin...
458 New Layer Nouveau calque
459 New Layout Nouvelle présentation
460 New VP Freeze Geler nouvelles fenêtres
461 New Viewports... Nouvelles fenêtres...
462 New layout Nouvelle présentation
463 New state name Nom du nouvel état
464 No Non
465 No blocks defined in this drawing. Aucun bloc défini dans ce dessin.
466 No contributor data was built into this copy. Cette version ne contient pas de données sur les contributeurs.
467 No model credits were built into this copy. Cette version ne contient pas de crédits de modèles.
468 No saved layer states Aucun état de calque enregistré
469 No selection Aucune sélection
470 Node Nœud
471 None Aucun
472 Not available Non disponible
473 Not available yet Pas encore disponible
474 OK OK
475 Object Objet
476 Object Snap Accrochage aux objets
477 Object Snap (F3) Accrochage aux objets (F3)
478 Object Snap On (F3) Activer l'accrochage aux objets (F3)
479 Object Snap Tracking Suivi de l'accrochage aux objets
480 Object Snap Tracking (F11) Suivi de l'accrochage aux objets (F11)
481 Object type: Type d'objet :
482 Off Désactivé
483 Offset Décalage
484 On Activé
485 On/Off Activer/Désactiver
486 Open (⌘O) Ouvrir (⌘O)
487 Open Sample Drawing Ouvrir un dessin d'exemple
488 Open sample drawing Ouvrir un dessin d'exemple
489 Open... Ouvrir...
490 Operator: Opérateur :
491 Orbit Orbite
492 Ordinate Ordonnée
493 Ortho (F8) Ortho (F8)
494 Ortho Mode Mode Ortho
495 Ortho Mode (F8) Mode Ortho (F8)
496 Osnap Settings... Paramètres d'accrochage aux objets...
497 Out Zoom arrière
498 Override Outrepasser
499 PAPER PAPIER
500 Percent Pourcentage
501 Polar (distance < angle) Polaire (distance < angle)
502 PRs PR
503 Page Setup Manager Gestionnaire des mises en page
504 Page Setup Manager... Gestionnaire des mises en page...
505 Pan Panoramique
506 Pan Down Panoramique vers le bas
507 Pan Left Panoramique vers la gauche
508 Pan Right Panoramique vers la droite
509 Pan Up Panoramique vers le haut
510 Paper Space Espace papier
511 Parallel Parallèle
512 Parameters Manager Gestionnaire des paramètres
513 Parametric Paramétrique
514 Paste Coller
515 Paste as Block Coller en tant que bloc
516 Paste to Original Coordinates Coller aux coordonnées d'origine
517 Path Array Réseau sur trajectoire
518 Pattern Motif
519 Perpendicular Perpendiculaire
520 Plot Tracer
521 Plot (click: do not plot) Tracer (cliquer : ne pas tracer)
522 Plot Preview Aperçu du tracé
523 Plot style Style de tracé
524 Plot style attached to Style de tracé associé à
525 Plot style table Table de styles de tracé
526 Plot Style... Style de tracé...
527 Plot Style Manager Gestionnaire des styles de tracé
528 Plot Style Manager... Gestionnaire des styles de tracé...
529 Plot Styles... Styles de tracé...
530 New CTB Nouvelle CTB
531 New STB Nouvelle STB
532 New style Nouveau style
533 Reset Panel Layout Réinitialiser la disposition des panneaux
534 Use for this layout Utiliser pour cet espace
535 Display plot styles Afficher les styles de tracé
536 No plot style tables. Aucune table de styles de tracé.
537 A built-in table: editing it keeps an edited copy in the drawing. Table intégrée : sa modification conserve une copie modifiée dans le dessin.
538 Keep an editable copy of this table in the drawing Conserver une copie modifiable de cette table dans le dessin
539 Layout {name}: {table} Espace {name} : {table}
540 Grayscale Niveaux de gris
541 Screening Trame
542 Use object linetype Utiliser le type de ligne de l'objet
543 Use object lineweight Utiliser l'épaisseur de ligne de l'objet
544 Remove Supprimer
545 Add Ajouter
546 Open a drawing first. Ouvrez d'abord un dessin.
547 Plot table type Type de table de tracé
548 Color-dependent Dépendante des couleurs
549 Named Nommée
550 Plus/minus Plus/moins
551 Point Point
552 Point Filters Filtres de points
553 Point Style... Style de point...
554 Polar Array Réseau polaire
555 Polar Tracking Recherche polaire
556 Polar Tracking (F10) Recherche polaire (F10)
557 Polar Tracking On (F10) Activer la recherche polaire (F10)
558 Polar: {distance} < {angle}° Polaire : {distance} < {angle}°
559 Polygon Polygone
560 Polygonal Polygonal
561 Polyline Polyligne
562 Position X Position X
563 Position Y Position Y
564 Position Z Position Z
565 Previous Précédent
566 Previous Layer Calque précédent
567 Primitives Primitives
568 Print Imprimer
569 Print... Imprimer...
570 Properties Propriétés
571 Properties Inspector Inspecteur de propriétés
572 Properties: Propriétés :
573 Proxy Objet proxy
574 Purge Purger
575 Pyramid Pyramide
576 Quadrant Quadrant
577 Query Entities Interroger les entités
578 Quick Rapide
579 Quick Dimension Cotation rapide
580 Quick Properties Propriétés rapides
581 Quick Select Sélection rapide
582 Quick Select Properties Propriétés de la sélection rapide
583 Quick Select... Sélection rapide...
584 QuickCalc Calculatrice rapide
585 Quit CADCraft Quitter CADCraft
586 Radius Rayon
587 Raster Image Image raster
588 Ray Rayon
589 Read-only Lecture seule
590 Real name Nom réel
591 Realtime Temps réel
592 Reassociate Dimensions Réassocier les cotes
593 Recent Récents
594 Recover... Récupérer...
595 Rectangle Rectangle
596 Rectangular Rectangulaire
597 Rectangular Array Réseau rectangulaire
598 Red Rouge
599 Redo Rétablir
600 Redo (⇧⌘Z) Rétablir (⇧⌘Z)
601 Redraw Redessiner
602 Regen Régénérer
603 Regen All Régénérer tout
604 Region Région
605 Region/Mass Properties Propriétés de région/masse
606 Relative Relatif
607 Remove Leader Supprimer la ligne de repère
608 Remove the viewport override Supprimer le remplacement de la fenêtre
609 Rename Dimension Style Renommer le style de cote
610 Rename Layer Renommer le calque
611 Rename Layer State Renommer l'état des calques
612 Rename Layout Renommer l'espace
613 Rename Text Style Renommer le style de texte
614 Rename... Renommer...
615 Repeat {command} Répéter {command}
616 Requires a point (x,y, @dx,dy or @d<a). Nécessite un point (x,y, @dx,dy ou @d<a).
617 Reset Palettes Réinitialiser les palettes
618 Restore Restaurer
619 Restore Layer State Restaurer l'état des calques
620 Reverse Inverser
621 Reverse the order Inverser l'ordre
622 Revision Cloud Nuage de révisions
623 Revolve Révolution
624 Right Droite
625 Rotate Pivoter
626 Rotation Rotation
627 S S
628 Save Enregistrer
629 Save (⌘S) Enregistrer (⌘S)
630 Save As Enregistrer sous
631 Save As... Enregistrer sous...
632 Save Layer State Enregistrer l'état des calques
633 Save State Enregistrer l'état
634 Scale Échelle
635 Search for layer Rechercher un calque
636 Search for parameter Rechercher un paramètre
637 Second and next points: Deuxième point et points suivants :
638 Select Sélectionner
639 Select 2D polyline Sélectionner une polyligne 2D
640 Select All Sélectionner tout
641 Select Objects Sélectionner les objets
642 Select a 2D polyline. Sélectionner une polyligne 2D.
643 Select a block reference. Sélectionner une référence de bloc.
644 Select a circle or arc. Sélectionner un cercle ou un arc.
645 Select a closed polyline Sélectionner une polyligne fermée
646 Select a closed polyline, circle, ellipse or closed spline. Sélectionner une polyligne fermée, un cercle, une ellipse ou une spline fermée.
647 Select a closed polyline. Sélectionner une polyligne fermée.
648 Select a curve as the path. Sélectionner une courbe comme trajectoire.
649 Select a dimension constraint type Sélectionner un type de contrainte dimensionnelle
650 Select a line object Sélectionner une ligne
651 Select a line object. Sélectionner une ligne.
652 Select a line or an arc. Sélectionner une ligne ou un arc.
653 Select a line or polyline segment Sélectionner une ligne ou un segment de polyligne
654 Select a line, arc or open polyline. Sélectionner une ligne, un arc ou une polyligne ouverte.
655 Select a line, arc, circle, polyline, ellipse or spline as the path. Sélectionner une ligne, un arc, un cercle, une polyligne, une ellipse ou une spline comme trajectoire.
656 Select a line, circle, arc or polyline segment. Sélectionner une ligne, un cercle, un arc ou un segment de polyligne.
657 Select a line. Sélectionner une ligne.
658 Select a polyline. Sélectionner une polyligne.
659 Select a text, mtext, attribute definition or dimension. Sélectionner un texte, un texte multiligne, une définition d'attribut ou une cote.
660 Select an annotation object Sélectionner un objet d'annotation
661 Select an arc or circle Sélectionner un arc ou un cercle
662 Select an arc. Sélectionner un arc.
663 Select an object Sélectionner un objet
664 Select an object to change or Sélectionner un objet à modifier ou
665 Select an object to measure or Sélectionner un objet à mesurer ou
666 Select an object. Sélectionner un objet.
667 Select an open curve. Sélectionner une courbe ouverte.
668 Select an open line, arc, polyline, ellipse arc or spline. Sélectionner une ligne, un arc, une polyligne, un arc elliptique ou une spline ouverts.
669 Select arc or circle Sélectionner un arc ou un cercle
670 Select arc or polyline arc segment Sélectionner un arc ou un segment d'arc de polyligne
671 Select arc style Sélectionner un style d'arc
672 Select arc, circle, line Sélectionner un arc, un cercle, une ligne
673 Select at least two objects to join. Sélectionner au moins deux objets à joindre.
674 Select circle or arc to add center mark Sélectionner un cercle ou un arc pour ajouter une marque de centre
675 Select closed objects (regions, closed polylines, circles). Sélectionner des objets fermés (régions, polylignes fermées, cercles).
676 Select first line Sélectionner la première ligne
677 Select first line or arc Sélectionner la première ligne ou le premier arc
678 Select first object Sélectionner le premier objet
679 Select first object or Sélectionner le premier objet ou
680 Select first point Sélectionner le premier point
681 Select nested objects to copy or Sélectionner des objets imbriqués à copier ou
682 Select object Sélectionner un objet
683 Select object to dimension Sélectionner un objet à coter
684 Select object to divide Sélectionner un objet à diviser
685 Select object to extend or shift-select to trim Sélectionner un objet à prolonger ou sélectionner avec Maj pour rogner
686 Select object to measure Sélectionner un objet à mesurer
687 Select object to offset Sélectionner un objet à décaler
688 Select object to trim or shift-select to extend Sélectionner un objet à rogner ou sélectionner avec Maj pour prolonger
689 Select objects Sélectionner les objets
690 Select objects (crossing window first corner) Sélectionner les objets (premier coin de la fenêtre de capture)
691 Select objects to array. Sélectionner les objets à répartir en réseau.
692 Select objects to stretch by crossing-window or crossing-polygon... Sélectionner les objets à étirer par fenêtre de capture ou polygone de capture...
693 Select objects: Sélectionner les objets :
694 Select one polyline (Multiple is available through the JSON form). Sélectionner une polyligne (l'option Multiple est disponible via le formulaire JSON).
695 Select path curve Sélectionner la courbe de trajectoire
696 Select point or object Sélectionner un point ou un objet
697 Select polyline or Sélectionner une polyligne ou
698 Select second line Sélectionner la deuxième ligne
699 Select second line or shift-select to apply corner Sélectionner la deuxième ligne ou sélectionner avec Maj pour former le coin
700 Select second object Sélectionner le deuxième objet
701 Select second object or shift-select to apply corner Sélectionner le deuxième objet ou sélectionner avec Maj pour former le coin
702 Select second point Sélectionner le deuxième point
703 Select spline Sélectionner une spline
704 Select symmetry line Sélectionner l'axe de symétrie
705 Select two points Sélectionner deux points
706 Selection Cycling Cycle de sélection
707 Send to Back Envoyer à l'arrière-plan
708 Set Current Définir comme courant
709 Set Current Dimension Style Définir le style de cote courant
710 Set Current Text Style Définir le style de texte courant
711 Set Layer Properties Définir les propriétés du calque
712 Set Properties Définir les propriétés
713 Set Table Cell Définir la cellule de tableau
714 Set Variable Définir la variable
715 Set View Définir la vue
716 Share Partager
717 Show Afficher
718 Show All Afficher tout
719 Show All Constraint Bars Afficher toutes les barres de contrainte
720 Show All Dynamic Dimensions Afficher toutes les cotes dynamiques
721 Show Layer List Afficher la liste des calques
722 Show/Hide Lineweight Afficher/Masquer l'épaisseur des lignes
723 Show/Hide Transparency Afficher/Masquer la transparence
724 Single Line Text Texte sur une ligne
725 Single Point Point unique
726 Single... Unique...
727 Smooth Lissage
728 Snap Accrochage
729 Snap Mode Mode d'accrochage
730 Snap Mode (F9) Mode d'accrochage (F9)
731 Snap On (F9) Activer l'accrochage (F9)
732 Snap X spacing Pas d'accrochage X
733 Snap and Grid Accrochage et grille
734 Solid Solide
735 Sort Trier
736 Specify a point Spécifier un point
737 Specify a scale for the hatch pattern Spécifier une échelle pour le motif de hachures
738 Specify a second extension line origin Spécifier l'origine de la deuxième ligne d'attache
739 Specify an angle for the hatch pattern Spécifier un angle pour le motif de hachures
740 Specify angle end point Spécifier le point final de l'angle
741 Specify angle of dimension line Spécifier l'angle de la ligne de cote
742 Specify angle of dimension text Spécifier l'angle du texte de cote
743 Specify angle start point Spécifier le point de départ de l'angle
744 Specify angle vertex Spécifier le sommet de l'angle
745 Specify angle vertex point Spécifier le point du sommet de l'angle
746 Specify arc length dimension location Spécifier l'emplacement de la cote de longueur d'arc
747 Specify axis endpoint of ellipse Spécifier l'extrémité de l'axe de l'ellipse
748 Specify base diameter Spécifiez le diamètre de la base
749 Specify base point Spécifiez le point de base
750 Specify base point or Spécifiez le point de base ou
751 Specify base point or displacement (drag with the middle mouse button for realtime pan) Spécifiez le point de base ou le déplacement (faites glisser avec le bouton central de la souris pour un panoramique en temps réel)
752 Specify base radius or Spécifiez le rayon de base ou
753 Specify center of donut Spécifiez le centre de l'anneau
754 Specify center of ellipse Spécifiez le centre de l'ellipse
755 Specify center of polygon Spécifiez le centre du polygone
756 Specify center point for circle Spécifiez le point central du cercle
757 Specify center point of arc Spécifiez le point central de l'arc
758 Specify center point of array Spécifiez le point central de la matrice
759 Specify center point of base Spécifiez le point central de la base
760 Specify corner of viewport Spécifiez le coin de la fenêtre
761 Specify corner of window, enter a scale factor (nX or nXP) Spécifiez le coin de la fenêtre, entrez un facteur d'échelle (nX ou nXP)
762 Specify diameter of circle Spécifiez le diamètre du cercle
763 Specify dimension arc line location Spécifiez l'emplacement de l'arc de cote
764 Specify dimension line location Spécifiez l'emplacement de la ligne de cote
765 Specify distance between turns Spécifiez la distance entre les spires
766 Specify distance to other axis Spécifiez la distance jusqu'à l'autre axe
767 Specify end angle Spécifiez l'angle de fin
768 Specify end point of arc Spécifiez le point final de l'arc
769 Specify end point of arc (hold Ctrl to switch direction) Spécifiez le point final de l'arc (maintenez Ctrl enfoncé pour changer de direction)
770 Specify ending width Spécifiez la largeur de fin
771 Specify ending width for next segment Spécifiez la largeur de fin du segment suivant
772 Specify endpoint of arc Spécifiez le point final de l'arc
773 Specify endpoint of axis Spécifiez le point final de l'axe
774 Specify endpoint of line Spécifiez le point final de la ligne
775 Specify feature location Spécifiez l'emplacement de l'élément
776 Specify fillet radius Spécifiez le rayon de congé
777 Specify fillet radius for rectangles Spécifiez le rayon de congé des rectangles
778 Specify first angle endpoint Spécifiez le premier point final de l'angle
779 Specify first chamfer distance Spécifiez la première distance de chanfrein
780 Specify first chamfer distance for rectangles Spécifiez la première distance de chanfrein des rectangles
781 Specify first constraint point Spécifiez le premier point de contrainte
782 Specify first corner Spécifiez le premier coin
783 Specify first corner point Spécifiez le premier point d'angle
784 Specify first destination point Spécifiez le premier point de destination
785 Specify first end point of circle's diameter Spécifiez le premier point final du diamètre du cercle
786 Specify first endpoint of edge Spécifiez le premier point final de l'arête
787 Specify first extension line origin Spécifiez l'origine de la première ligne d'attache
788 Specify first point Spécifiez le premier point
789 Specify first point of mirror line Spécifiez le premier point de la ligne de symétrie
790 Specify first point on circle Spécifiez le premier point sur le cercle
791 Specify first point on circle: _tan to Spécifiez le premier point sur le cercle : _tan à
792 Specify first source point Spécifiez le premier point source
793 Specify height Spécifiez la hauteur
794 Specify helix height or Spécifiez la hauteur de l'hélice ou
795 Specify included angle Spécifiez l'angle inclus
796 Specify insertion base point Spécifiez le point de base d'insertion
797 Specify insertion point Spécifiez le point d'insertion
798 Specify inside diameter of donut Spécifiez le diamètre intérieur de l'anneau
799 Specify leader arrowhead location Spécifiez l'emplacement de la tête de flèche du repère
800 Specify leader endpoint Spécifiez le point final du repère
801 Specify leader landing location Spécifiez l'emplacement de la base du repère
802 Specify length for rectangles Spécifiez la longueur des rectangles
803 Specify length of chord Spécifiez la longueur de la corde
804 Specify length of segment Spécifiez la longueur du segment
805 Specify line width for rectangles Spécifiez la largeur de ligne des rectangles
806 Specify location for new vertex Spécifiez l'emplacement du nouveau sommet
807 Specify minimum length of arc Spécifiez la longueur minimale de l'arc
808 Specify new end point Spécifiez le nouveau point final
809 Specify new location for marked vertex Spécifiez le nouvel emplacement du sommet marqué
810 Specify new width for all segments Spécifiez la nouvelle largeur de tous les segments
811 Specify next point Spécifiez le point suivant
812 Specify offset distance Spécifiez la distance de décalage
813 Specify offset distance or Spécifiez la distance de décalage ou
814 Specify opposite corner Spécifiez le coin opposé
815 Specify other corner point Spécifiez l'autre point d'angle
816 Specify other endpoint of axis Spécifiez l'autre point final de l'axe
817 Specify outside diameter of donut Spécifiez le diamètre extérieur de l'anneau
818 Specify point Spécifiez le point
819 Specify point on object for first tangent of circle Spécifiez le point sur l'objet pour la première tangente du cercle
820 Specify point on object for second tangent of circle Spécifiez le point sur l'objet pour la deuxième tangente du cercle
821 Specify point on side to offset Spécifiez le point du côté à décaler
822 Specify radius of arc Spécifiez le rayon de l'arc
823 Specify radius of circle Spécifiez le rayon du cercle
824 Specify rotation angle Spécifiez l'angle de rotation
825 Specify rotation angle of text Spécifiez l'angle de rotation du texte
826 Specify rotation angle or Spécifiez l'angle de rotation ou
827 Specify scale factor Spécifiez le facteur d'échelle
828 Specify second angle endpoint Spécifiez le deuxième point final de l'angle
829 Specify second break point Spécifiez le deuxième point de rupture
830 Specify second chamfer distance for rectangles Spécifiez la deuxième distance de chanfrein des rectangles
831 Specify second constraint point Spécifiez le deuxième point de contrainte
832 Specify second destination point Spécifiez le deuxième point de destination
833 Specify second end point of circle's diameter Spécifiez le deuxième point final du diamètre du cercle
834 Specify second endpoint of edge Spécifiez le deuxième point final de l'arête
835 Specify second extension line origin Spécifiez l'origine de la deuxième ligne d'attache
836 Specify second point Spécifiez le deuxième point
837 Specify second point of arc Spécifiez le deuxième point de l'arc
838 Specify second point of mirror line Spécifiez le deuxième point de la ligne de symétrie
839 Specify second point on circle Spécifiez le deuxième point sur le cercle
840 Specify second point on circle: _tan to Spécifiez le deuxième point sur le cercle : _tan à
841 Specify second point or <use first point as displacement> Spécifiez le deuxième point ou <utiliser le premier point comme déplacement>
842 Specify second source point Spécifiez le deuxième point source
843 Specify side to offset Spécifiez le côté à décaler
844 Specify start angle Spécifiez l'angle de départ
845 Specify start point Spécifiez le point de départ
846 Specify start point of arc Spécifiez le point de départ de l'arc
847 Specify start point of polyline Spécifiez le point de départ de la polyligne
848 Specify start point of text Spécifiez le point de départ du texte
849 Specify starting width Spécifiez la largeur de départ
850 Specify starting width for next segment Spécifiez la largeur de départ du segment suivant
851 Specify tangent direction for the start point of arc Spécifiez la direction de la tangente au point de départ de l'arc
852 Specify the elevation for rectangles Spécifiez l'élévation des rectangles
853 Specify thickness for rectangles Spécifiez l'épaisseur des rectangles
854 Specify third point on circle Spécifiez le troisième point sur le cercle
855 Specify third point on circle: _tan to Spécifiez le troisième point sur le cercle : _tan à
856 Specify third source point or <continue> Spécifiez le troisième point source ou <continuer>
857 Specify through point Spécifiez le point de passage
858 Specify top diameter Spécifiez le diamètre supérieur
859 Specify top radius or Spécifiez le rayon supérieur ou
860 Specify total angle Spécifiez l'angle total
861 Specify total length or Spécifiez la longueur totale ou
862 Specify width for rectangles Spécifiez la largeur des rectangles
863 Sphere Sphère
864 Spline Spline
865 Stack (type 1/2 then select) Empilement (tapez 1/2, puis sélectionnez)
866 Standard Standard
867 Start Départ
868 Start Width Largeur de départ
869 Start X X de départ
870 Start Y Y de départ
871 Start angle Angle de départ
872 Start, Center, Angle Départ, Centre, Angle
873 Start, Center, End Départ, Centre, Fin
874 Start, Center, Length Départ, Centre, Longueur
875 Start, End, Angle Départ, Fin, Angle
876 Start, End, Direction Départ, Fin, Direction
877 Start, End, Radius Départ, Fin, Rayon
878 Status État
879 Status Bar Barre d'état
880 Stretch Étirer
881 Style: Standard Style : Standard
882 Subtract Soustraction
883 Sweep Balayage
884 Switch Drawing Changer de dessin
885 Switch Layout Changer de présentation
886 Switch between model space in a viewport and paper space Basculer entre l'espace modèle dans une fenêtre et l'espace papier
887 Symmetric Symétrie
888 Synchronize Attributes Synchroniser les attributs
889 Toggle PICKADD (off: each pick replaces the selection) Basculer PICKADD (désactivé : chaque sélection remplace la sélection existante)
890 Toggle PICKADD (on: picks add to the selection) Basculer PICKADD (activé : les sélections s'ajoutent à la sélection existante)
891 TOP HAUT
892 Table Tableau
893 Table Style... Style de tableau...
894 Table style Style de tableau
895 Table... Tableau...
896 Tan, Tan, Radius Tangente, Tangente, Rayon
897 Tan, Tan, Tan Tangente, Tangente, Tangente
898 Tangent Tangente
899 Text Texte
900 Text Editor Éditeur de texte
901 Text Height Hauteur du texte
902 Text Style... Style de texte...
903 Text height Hauteur du texte
904 Text override Texte de remplacement
905 Text style Style de texte
906 Thaw All Layers Dégeler tous les calques
907 There is nothing here yet. Il n'y a encore rien ici.
908 Thickness Épaisseur
909 Time Heure
910 To get started, create or open a drawing. Pour commencer, créez ou ouvrez un dessin.
911 Tool Bar Barre d'outils
912 Tool Sets Ensembles d'outils
913 Tools Outils
914 Torus Tore
915 Trace Trace
916 Transparency Transparence
917 Trim Ajuster
918 True Color Couleur vraie
919 Turn All Layers On Activer tous les calques
920 Turn Off Layer Désactiver le calque
921 Turn Quick Properties off (QPMODE) Désactiver les propriétés rapides (QPMODE)
922 Type Type
923 Type a command Tapez une commande
924 Type text; Enter starts a new paragraph Saisissez le texte ; Entrée crée un nouveau paragraphe
925 UCS Icon Icône du SCU
926 Undo Annuler
927 Undo (⌘Z) Annuler (⌘Z)
928 Ungroup Dissocier
929 Union Union
930 Unisolate Layer Annuler l'isolement des calques
931 Units... Unités...
932 Unlock Layer Déverrouiller le calque
933 Unmerge Cells Annuler la fusion des cellules
934 Unsaved Layer State État de calque non enregistré
935 Up Haut
936 Update Mettre à jour
937 Use System Setting Utiliser le paramètre système
938 User Utilisateur
939 User Parameters Paramètres utilisateur
940 Username Nom d'utilisateur
941 VP Color Couleur de la fenêtre
942 VP Freeze Gel dans la fenêtre
943 Value Valeur
944 Value: Valeur :
945 Version Version
946 Version {version} Version {version}
947 Vertical Vertical
948 VerticalDistance VerticalDistance
949 View Vue
950 ViewCube ViewCube
951 Viewport Fenêtre
952 Viewport Layer Freeze Gel du calque dans la fenêtre
953 Viewport Properties Propriétés de la fenêtre
954 Viewport {id} Fenêtre {id}
955 Viewports Fenêtres
956 W O
957 WCS ⌄ SCG ⌄
958 White Blanc
959 Width factor Facteur de largeur
960 Window Fenêtre
961 Wipeout Masquage
962 Workspace Switching Changement d'espace de travail
963 Write Block Enregistrer le bloc dans un fichier
964 Xline Ligne de construction
965 Yellow Jaune
966 Yes Oui
967 Zoom Zoom
968 Zoom All Zoom tout
969 Zoom Extents Zoom étendu
970 Zoom In Zoom avant
971 Zoom Object Zoom objet
972 Zoom Out Zoom arrière
973 Zoom Previous Zoom précédent
974 Zoom Window Zoom fenêtre
975 error erreur
976 {contributors} · {commits} {contributors} · {commits}
977 {kind} (constraint {id}) {kind} (contrainte {id})
978 {kind} constraint {name} = {expression} (double-click to edit) Contrainte {kind} {name} = {expression} (double-cliquez pour modifier)
979 {name}: {distance} < {angle}° {name} : {distance} < {angle}°
980 ƒx New User Parameter ƒx Nouveau paramètre utilisateur
981 ΔLOC ΔLOC
982 −Bin −Bin
983 −LOC −LOC
984 ⌘↩ to finish ⌘↩ pour terminer
985 @plural {n} commit {n} révision|{n} révisions
986 @plural {n} contributor {n} contributeur|{n} contributeurs
987 Drawing as bytes Dessin sous forme d'octets
988 Services Services
989 Hide CADCraft Masquer CADCraft
990 Hide Others Masquer les autres
991 Accessibility Accessibilité
992 Show System Cursor Afficher le curseur système
993 Import... Importer...
994 Export... Exporter...
995 Batch Publish... Publication par lots...
996 Hatch Pattern Motif de hachure
997 Search for pattern Rechercher un motif
+60 -2
View File
@@ -23,10 +23,29 @@ pub enum Preference {
German,
#[serde(rename = "es")]
Spanish,
#[serde(rename = "cs")]
Czech,
#[serde(rename = "ru")]
Russian,
#[serde(rename = "fr")]
French,
#[serde(rename = "zh")]
Chinese,
}
impl Preference {
pub const ALL: [Self; 6] = [Self::Auto, Self::English, Self::Ukrainian, Self::BrazilianPortuguese, Self::German, Self::Spanish];
pub const ALL: [Self; 10] = [
Self::Auto,
Self::English,
Self::Ukrainian,
Self::BrazilianPortuguese,
Self::German,
Self::Spanish,
Self::Czech,
Self::Russian,
Self::French,
Self::Chinese,
];
/// The codes a preference can be given as, for messages ("auto, en or uk").
pub fn codes() -> String {
@@ -45,6 +64,10 @@ impl Preference {
Self::BrazilianPortuguese => "pt-BR",
Self::German => "de",
Self::Spanish => "es",
Self::Czech => "cs",
Self::Russian => "ru",
Self::French => "fr",
Self::Chinese => "zh",
}
}
@@ -56,6 +79,10 @@ impl Preference {
Self::BrazilianPortuguese => "Português (Brasil)",
Self::German => "Deutsch",
Self::Spanish => "Español",
Self::Czech => "Čeština",
Self::Russian => "Русский",
Self::French => "Français",
Self::Chinese => "简体中文",
}
}
@@ -75,6 +102,10 @@ impl Preference {
Self::BrazilianPortuguese => "pt-BR",
Self::German => "de",
Self::Spanish => "es",
Self::Czech => "cs",
Self::Russian => "ru",
Self::French => "fr",
Self::Chinese => "zh",
Self::Auto => locales.iter().find_map(|s| locale_language(s)).unwrap_or("en"),
}
}
@@ -91,7 +122,7 @@ pub struct Language {
catalog: OnceLock<Catalog<'static>>,
}
pub static LANGUAGES: [Language; 5] = [
pub static LANGUAGES: [Language; 9] = [
Language { code: "en", name: "English", source: "", plural: plural_en, plural_forms: 2, catalog: OnceLock::new() },
Language {
code: "uk", name: "Українська", source: include_str!("uk.tsv"), plural: plural_uk, plural_forms: 3, catalog: OnceLock::new()
@@ -108,6 +139,14 @@ pub static LANGUAGES: [Language; 5] = [
Language { code: "de", name: "Deutsch", source: include_str!("de.tsv"), plural: plural_en, plural_forms: 2, catalog: OnceLock::new() },
// Spanish integer plurals follow the English rule too.
Language { code: "es", name: "Español", source: include_str!("es.tsv"), plural: plural_en, plural_forms: 2, catalog: OnceLock::new() },
Language { code: "cs", name: "Čeština", source: include_str!("cs.tsv"), plural: plural_cs, plural_forms: 3, catalog: OnceLock::new() },
// Russian integer plurals follow the Ukrainian rule: one (1, 21), few (2–4), many.
Language { code: "ru", name: "Русский", source: include_str!("ru.tsv"), plural: plural_uk, plural_forms: 3, catalog: OnceLock::new() },
// French integer plurals (CLDR `fr`): one (0, 1), other, the Portuguese rule.
Language { code: "fr", name: "Français", source: include_str!("fr.tsv"), plural: plural_pt, plural_forms: 2, catalog: OnceLock::new() },
// Chinese has no grammatical plural: both `@plural` forms read the same. Its glyphs come from
// an installed CJK font (`theme::cjk_font`); without one the interface stays English.
Language { code: "zh", name: "简体中文", source: include_str!("zh.tsv"), plural: plural_en, plural_forms: 2, catalog: OnceLock::new() },
];
/// BCP 47 and POSIX locale tags, including `uk-UA`, `uk_UA.UTF-8` and `pt_BR.UTF-8`.
@@ -168,6 +207,16 @@ pub fn t(source: &str) -> &str {
CURRENT.with(|l| tr(l.get(), source))
}
/// Menu headings can differ from command or object labels (e.g. Czech Kótování / Kóta).
/// Catalogs without a `menu` entry retain their ordinary translation.
pub fn tr_menu<'a>(language: &str, source: &'a str) -> &'a str {
catalog(language).and_then(|c| c.get(&("menu", source))).copied().unwrap_or_else(|| tr(language, source))
}
pub fn menu(source: &str) -> &str {
CURRENT.with(|l| tr_menu(l.get(), source))
}
/// Named placeholders allow translations to reorder values. Inserted values are never looked up
/// or substituted again, so a user's name containing `{n}` is preserved verbatim.
pub fn format(source: &str, values: &[(&str, String)]) -> String {
@@ -213,6 +262,15 @@ fn plural_en(n: u64) -> usize {
usize::from(n != 1)
}
/// Czech integer plurals: one (1), few (2–4), other (0 and 5 or more).
pub fn plural_cs(n: u64) -> usize {
match n {
1 => 0,
2..=4 => 1,
_ => 2,
}
}
pub fn count(n: u64, one: &str, other: &str) -> String {
let language = CURRENT.get();
let source = catalog(language).and_then(|c| c.get(&("@plural", one))).copied();
+4
View File
@@ -112,6 +112,7 @@
Close All Fechar tudo
Closed Fechada
Coincident Coincidente
Collapse Palettes Recolher paletas
Collapse Tool Sets Recolher conjuntos de ferramentas
Collinear Colinear
Color Cor
@@ -524,6 +525,7 @@
Plot Plotar
Plot (click: do not plot) Plotar (clique para não plotar)
Plot Preview Visualizar plotagem
Plot Window Janela de plotagem
Plot style Estilo de plotagem
Plot style attached to Estilo de plotagem anexado a
Plot style table Tabela de estilos de plotagem
@@ -994,3 +996,5 @@
Hide Others Ocultar outros
Accessibility Acessibilidade
Show System Cursor Exibir cursor do sistema
Hatch Pattern Padrão de hachura
Search for pattern Pesquisar padrão
1 # CADCraft Brazilian Portuguese (pt-BR) interface catalog.
112
113
114
115
116
117
118
525
526
527
528
529
530
531
996
997
998
999
1000
+997
View File
@@ -0,0 +1,997 @@
# CADCraft Russian interface catalog — original translations by @pechnikov2026-star.
# License: MIT OR Apache-2.0 (the repository LICENSE-MIT and LICENSE-APACHE).
# Written from CADCraft's English strings; no proprietary translations were copied.
# UTF-8: context<TAB>English source<TAB>Russian translation.
# Empty context = ordinary label; @plural = one|few|many forms with {n}.
# Command IDs, typed keywords, technology/product names and user data stay unchanged.
Cancel Отмена
OK ОК
or [ или [
% of all commits % всех коммитов
* Wildcard Match * По шаблону
*VARIES* *РАЗНЫЕ*
+ | Top | 2D Wireframe + | Сверху | 2D-каркас
+Bin +двоич.
+LOC +строк
1 Viewport 1 видовой экран
2 Points 2 точки
2 Viewports 2 видовых экрана
2D Solid 2D-заливка
3 Points 3 точки
3 Viewports 3 видовых экрана
3D Face 3D-грань
3D Operations 3D-операции
3D Polyline 3D-полилиния
4 Viewports 4 видовых экрана
< Less than < Меньше чем
<> Not Equal <> Не равно
= Equals = Равно
> Greater than > Больше чем
About О программе
About CADCraft О CADCraft
Absolute Абсолютные
Add Leader Добавить выноску
Align Выровнять
Align Text Выровнять текст
Aligned Параллельный
All Все
All Used Layers Все используемые слои
All files Все файлы
Angle Угол
Angular Угловой
Annotation Visibility Видимость аннотаций
Annotation scale Масштаб аннотаций
Apparent Intersection Мнимое пересечение
Append to current selection set Добавить к текущему набору выделения
Apply to: Применить к:
Apply true color Применить цвет RGB
Arc Дуга
Arc Length Длина дуги
Area Площадь
Arrange Panel Расположить панель
Array Массив
Attach Присоединить
Attribute Атрибут
Attribute Definition Определение атрибута
Attribute Display Отображение атрибутов
Audit Проверка чертежа
Auto (system language) Авто (язык системы)
Auto Constrain Автоограничение
AutoConstrain Автоограничение
Axis, End Ось, конец
Base База
Baseline Базовый
Batch Publish Пакетная публикация
Binary assets added Добавленные двоичные ресурсы
Binary assets removed Удаленные двоичные ресурсы
Blend Сгладить
Block Блок
Block Attribute Manager... Диспетчер атрибутов блоков...
Block Editor Редактор блоков
Block Reference Вхождение блока
Block... Блок...
Blocks Блоки
Blue Синий
Bold Жирный
Boolean Булевы операции
Boundary Граница
Boundary... Граница...
Box Параллелепипед
Break Разорвать
Break At Point Разорвать в точке
Bring to Front На передний план
ByBlock Поблоку
ByColor ПоЦвету
ByLayer Послою
CADCraft Help Справка CADCraft
Cancel Отмена
Cartesian (x, y) Декартовы (x, y)
Center Центр
Center Line Осевая линия
Center Mark Знак центра
Center X Центр X
Center Y Центр Y
Center Z Центр Z
Center, Diameter Центр, диаметр
Center, Radius Центр, радиус
Center, Start, Angle Центр, начало, угол
Center, Start, End Центр, начало, конец
Center, Start, Length Центр, начало, длина
Chamfer Фаска
Change Space Изменить пространство
Change to Current Layer Перенести на текущий слой
Circle Круг
Circumference Длина окружности
Clear Очистить
Clipboard Буфер обмена
Close Закрыть
Close All Закрыть все
Closed Замкнутый
Coincident Совпадение
Collapse Tool Sets Свернуть наборы инструментов
Collinear Коллинеарность
Color Цвет
Color 8 Цвет 8
Color 9 Цвет 9
Color Index Индекс цвета
Color... Цвет...
Command History Журнал команд
Command Line Командная строка
Command Line History Lines Строки журнала командной строки
Command Reference Справочник команд
Commits Коммиты
Company Организация
Computer-aided design and drafting: an open-source, clean-room CAD application written in pure Rust. Автоматизированное проектирование и черчение: CAD-приложение с открытым кодом, независимо разработанное на Rust.
Concentric Концентричность
Cone Конус
Constraint Bars Панели ограничений
Constraint Settings Параметры ограничений
Construction Line Вспомогательная прямая
Contents Содержание
Continue Продолжить
Contributors Участники
Control Vertices Управляющие вершины
Copy Копировать
Copy Layout Копировать лист
Copy Nested Objects Копировать вложенные объекты
Copy Selection Копировать выделение
Copy with Base Point Копировать с базовой точкой
Count Количество
Create Block Создать блок
Creates a user parameter Создает пользовательский параметр
Ctrl+Enter to finish Ctrl+Enter для завершения
Current layer Текущий слой
Current selection Текущее выделение
Current: {color} Текущий: {color}
Cut Вырезать
Cyan Голубой
Cylinder Цилиндр
DWG DWG
DXF DXF
Dark Темная
Default По умолчанию
Default Save Format Формат сохранения по умолчанию
Define Attributes Определить атрибуты
Define Attributes... Определить атрибуты...
Defined width Заданная ширина
Degree Градус
Delete Удалить
Delete Column Удалить столбец
Delete Constraints Удалить ограничения
Delete Dimension Style Удалить размерный стиль
Delete Duplicate Objects Удалить дубликаты объектов
Delete Layer Удалить слой
Delete Layer State Удалить состояние слоев
Delete Layout Удалить лист
Delete Row Удалить строку
Delete Text Style Удалить текстовый стиль
Delta X Смещение X
Delta Y Смещение Y
Description Описание
Deselect All Снять выделение со всего
Diameter Диаметр
Dim style Размерный стиль
Dimension Размеры
Dimension Space Расстояние между размерами
Dimension Style Override Переопределение размерного стиля
Dimension Style... Размерный стиль...
Dimension Text Edit Редактировать размерный текст
Dimension style Размерный стиль
Dimensional Constraint Размерное ограничение
Dimensional Constraint Parameters Параметры размерных ограничений
Dimensional Constraints Размерные ограничения
Disassociate Dimensions Разъединить размеры
Display name Отображаемое имя
Distance Расстояние
Divide Поделить
Do not plot (click: plot) Не выводить на печать (щелкните для печати)
Donut Кольцо
Double-click to edit the description Дважды щелкните, чтобы изменить описание
Down Вниз
Drafting Черчение
Drafting Settings Режимы черчения
Drafting Settings... Режимы черчения...
Draw Рисование
Draw Order Порядок отрисовки
Drawing (DWG) Чертеж (DWG)
Drawing (DXF) Чертеж (DXF)
Drawing Limits Лимиты чертежа
Drawing Utilities Утилиты чертежа
Drawings (DWG, DXF) Чертежи (DWG, DXF)
Dynamic Dimensions Динамические размеры
Dynamic Input Динамический ввод
Dynamic Input (F12) Динамический ввод (F12)
E В
Edit Редактирование
Edit Text... Редактировать текст...
Elevation Уровень
Ellipse Эллипс
Elliptical Arc Эллиптическая дуга
Enable Dynamic Input (F12) Включить динамический ввод (F12)
End Width Конечная ширина
End X Конец X
End Y Конец Y
End angle Конечный угол
Endpoint Конечная точка
Enter Top, Zero or Bottom. Введите Top, Zero или Bottom.
Enter a fit data option Введите параметр аппроксимации
Enter a pattern name Введите имя шаблона
Enter a vertex editing option Введите параметр редактирования вершины
Enter an option Введите параметр
Enter angle of xline Введите угол вспомогательной прямой
Enter angle of xline (0) or Введите угол вспомогательной прямой (0) или
Enter area of rectangle in current units Введите площадь прямоугольника в текущих единицах
Enter attribute prompt Введите подсказку атрибута
Enter attribute tag name Введите имя тега атрибута
Enter attribute value Введите значение атрибута
Enter block name Введите имя блока
Enter constraint type Введите тип ограничения
Enter continuity Введите тип непрерывности
Enter default attribute value Введите значение атрибута по умолчанию
Enter degree of spline Введите степень сплайна
Enter delta angle Введите изменение угла
Enter delta length or Введите изменение длины или
Enter dimension text Введите размерный текст
Enter dimension text (<> = measurement) Введите размерный текст (<> = измеренное значение)
Enter dimension value or [name=expression] Введите размерное значение или [имя=выражение]
Enter dimension variable name to override Введите имя размерной переменной для переопределения
Enter justification type Введите тип выравнивания
Enter leader text Введите текст выноски
Enter mline scale Введите масштаб мультилинии
Enter mode Введите режим
Enter name of block to insert Введите имя блока для вставки
Enter new text Введите новый текст
Enter next point Введите следующую точку
Enter number of columns Введите количество столбцов
Enter number of data rows Введите количество строк данных
Enter number of items Введите количество элементов
Enter number of items along path Введите количество элементов вдоль траектории
Enter number of sides Введите количество сторон
Enter number of turns Введите количество витков
Enter percentage length Введите длину в процентах
Enter rectangle length Введите длину прямоугольника
Enter rectangle width Введите ширину прямоугольника
Enter scale factor Введите масштабный коэффициент
Enter text Введите текст
Enter text (use \P for new paragraphs) Введите текст (\P начинает новый абзац)
Enter the number of segments Введите количество сегментов
Enter twist direction of helix Введите направление закручивания спирали
Entire drawing Весь чертеж
Equal Равенство
Erase Стереть
Exclude from new selection set Исключить из нового набора выделения
Explode Расчленить
Export Экспортировать
Export to PDF... Экспортировать в PDF...
Expression Выражение
Extend Удлинить
Extension Продолжение
Extents Границы
Extrude Выдавить
File Файл
File Tab Вкладка файла
Fillet Сопряжение
Filter Фильтр
Filters Фильтры
Find and Replace Найти и заменить
Find... Найти...
First Первый
First commit Первый коммит
Fit Points Точки сглаживания
Fix Фиксация
Flatten Objects Спроецировать объекты на плоскость
Format Формат
Freehand От руки
Freeze Заморозить
Freeze Layer Заморозить слой
Freeze in new viewports Заморозить в новых видовых экранах
Freeze/Thaw Заморозить/разморозить
Freeze/Thaw in all viewports Заморозить/разморозить во всех видовых экранах
Freeze/Thaw in the current viewport Заморозить/разморозить в текущем видовом экране
From От точки
General Общие
Geometric Center Геометрический центр
Geometric Constraint Геометрическое ограничение
Geometric Constraints Геометрические ограничения
Geometry Геометрия
Get Variable Получить переменную
Get View Получить вид
Global Глобально
Global width Общая ширина
Grab bag Список
Gradient Градиент
Gradient... Градиент...
Green Зеленый
Grid (F7) Сетка (F7)
Grid Display Отображение сетки
Grid On (F7) Включить сетку (F7)
Grid spacing Шаг сетки
Grip Mirror Зеркальное отражение ручками
Grip Rotate Поворот ручками
Grip Scale Масштабирование ручками
Grip Stretch Растягивание ручками
Group Сгруппировать
Group Edit Редактировать группу
Hatch Штриховка
Hatch Edit Редактировать штриховку
Hatch... Штриховка...
Height Высота
Height: {height} Высота: {height}
Helix Спираль
Help Справка
Hide All Скрыть все
Hide All Constraint Bars Скрыть все панели ограничений
Hide All Dynamic Dimensions Скрыть все динамические размеры
Hide Layer List Скрыть список слоев
Hide Palettes Скрыть палитры
Home Исходный вид
Horizontal Горизонтальность
HorizontalDistance ГоризонтальноеРасстояние
How to apply: Способ применения:
ID Point Координаты точки
Import Импортировать
In Приблизить
In-window Menu Bar Строка меню в окне
Include in new selection set Включить в новый набор выделения
Increment angle Шаг угла
Index Color Индексированный цвет
Index color: {index} Индексированный цвет: {index}
Inquiry Измерение
Insert Вставка
Insert Block Вставить блок
Insert Column Вставить столбец
Insert Row Вставить строку
Insert this block Вставить этот блок
Insertion Точка вставки
Insertion X Точка вставки X
Insertion Y Точка вставки Y
Insertion Z Точка вставки Z
Inspect Constraints Просмотреть ограничения
Inspect Drawing Просмотреть чертеж
Interface Language Язык интерфейса
Interface Theme Тема интерфейса
Intersect Пересечение
Intersection Пересечение
Isolate Изолировать
Isolate Layer Изолировать слой
Isometric Drafting Изометрическое черчение
Join Объединить
Language Язык
Last Последний
Last commit Последний коммит
Layer Слой
Layer Delete Удалить слой с объектами
Layer Match Согласовать слой
Layer Merge Объединить слои
Layer Off Выключить слой
Layer Properties Свойства слоя
Layer Properties Manager Диспетчер свойств слоев
Layer Properties Manager — current viewport Диспетчер свойств слоев — текущий видовой экран
Layer State Состояние слоев
Layer States Manager... Диспетчер состояний слоев...
Layer Tools Инструменты слоев
Layer contains objects (double-click to make current) Слой содержит объекты (дважды щелкните, чтобы сделать текущим)
Layer is empty (double-click to make current) Слой пуст (дважды щелкните, чтобы сделать текущим)
Layers Слои
Layers Palette Палитра слоев
Layers displayed: {shown} of {total}. Current layer: {layer} Показано слоев: {shown} из {total}. Текущий слой: {layer}
Layout Лист
Layout list Список листов
Leader Выноска
Left Слева
Length Длина
Length X Длина X
Length Y Длина Y
Lengthen Изменить длину
Light Светлая
Line Отрезок
Line delta Изменение количества строк
Linear Линейный
Lines +/− Строки +/−
Lines added Добавленные строки
Lines deleted Удаленные строки
Linetype Тип линии
Linetype Scale Масштаб типа линии
Linetype scale Масштаб типа линии
Ltscale Масштаб типа линии
Linetype... Тип линии...
Lineweight Вес линии
Lineweight... Вес линии...
List Список
List Blocks Список блоков
List Dimension Styles Список размерных стилей
List Groups Список групп
List Layer States Список состояний слоев
List Layouts Список листов
List System Variables Список системных переменных
List Text Styles Список текстовых стилей
Load all Загрузить все
Load... Загрузить...
Loaded linetypes Загруженные типы линий
Lock Заблокировать
Lock Layer Заблокировать слой
Lock/Unlock Заблокировать/разблокировать
Loft Тело по сечениям
MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Не аффилировано с Autodesk, Inc.
MODEL МОДЕЛЬ
MText Многострочный текст
Magenta Пурпурный
Major line every Основная линия каждые
Make Current Сделать текущим
Make Object's Layer Current Сделать слой объекта текущим
Make... Создать...
Match Layer Согласовать слой
Match Properties Копирование свойств
Measure Разметить
Measure Distance Измерить расстояние
Measure Geometry Измерить геометрию
Merge Cells Объединить ячейки
Merged PRs Принятые PR
Mid Between 2 Points Середина между двумя точками
Midpoint Середина
Mirror Зеркальное отражение
Model Модель
Model Space (in viewport) Пространство модели (в видовом экране)
Modeling Моделирование
Models Модели
Modify Модификация
Move Переместить
Multileader Мультивыноска
Multileader Style... Стиль мультивыносок...
Multileader style Стиль мультивыносок
Multiline Мультилиния
Multiline Text Многострочный текст
Multiline Text... Многострочный текст...
Multiple Разные типы
Multiple Point Несколько точек
My Мои
My properties: not available yet Мои свойства: пока недоступно
N С
Name Название
Name (A–Z) Название (А–Я)
Nearest Ближайшая точка
New Создать
New (metric) Создать (метрический)
New Drawing Новый чертеж
New Drawing (⌘N) Новый чертеж (⌘N)
New Drawing... Новый чертеж...
New Layer Новый слой
New Layout Новый лист
New VP Freeze Заморозить в новых ВЭ
New Viewports... Новые видовые экраны...
New layout Новый лист
New state name Название нового состояния
No Нет
No blocks defined in this drawing. В этом чертеже нет определенных блоков.
No contributor data was built into this copy. В эту сборку не включены данные об участниках.
No model credits were built into this copy. В эту сборку не включены данные о моделях.
No saved layer states Нет сохраненных состояний слоев
No selection Ничего не выделено
Node Узел
None Нет
Not available Недоступно
Not available yet Пока недоступно
OK ОК
Object Объект
Object Snap Объектная привязка
Object Snap (F3) Объектная привязка (F3)
Object Snap On (F3) Включить объектную привязку (F3)
Object Snap Tracking Отслеживание объектной привязки
Object Snap Tracking (F11) Отслеживание объектной привязки (F11)
Object type: Тип объекта:
Off Выключено
Offset Смещение
On Включено
On/Off Включить/выключить
Open (⌘O) Открыть (⌘O)
Open Sample Drawing Открыть образец чертежа
Open sample drawing Открыть образец чертежа
Open... Открыть...
Operator: Оператор:
Orbit Орбита
Ordinate Ординатный
Ortho (F8) Орто (F8)
Ortho Mode Режим Орто
Ortho Mode (F8) Режим Орто (F8)
Osnap Settings... Параметры объектной привязки...
Out Отдалить
Override Переопределить
PAPER ЛИСТ
Percent Процент
Polar (distance < angle) Полярные (расстояние < угол)
PRs PR
Page Setup Manager Диспетчер параметров листов
Page Setup Manager... Диспетчер параметров листов...
Pan Панорамирование
Pan Down Панорамировать вниз
Pan Left Панорамировать влево
Pan Right Панорамировать вправо
Pan Up Панорамировать вверх
Paper Space Пространство листа
Parallel Параллельность
Parameters Manager Диспетчер параметров
Parametric Параметризация
Paste Вставить
Paste as Block Вставить как блок
Paste to Original Coordinates Вставить в исходные координаты
Path Array Массив по траектории
Pattern Шаблон
Perpendicular Перпендикулярность
Plot Печать чертежа
Plot (click: do not plot) Печатать (щелкните, чтобы отключить печать)
Plot Preview Предварительный просмотр печати
Plot style Стиль печати
Plot style attached to Стиль печати применен к
Plot style table Таблица стилей печати
Plot Style... Стиль печати...
Plot Style Manager Диспетчер стилей печати
Plot Style Manager... Диспетчер стилей печати...
Plot Styles... Стили печати...
New CTB Новая CTB
New STB Новая STB
New style Новый стиль
Reset Panel Layout Сбросить расположение панелей
Use for this layout Использовать для этого листа
Display plot styles Показывать стили печати
No plot style tables. Нет таблиц стилей печати.
A built-in table: editing it keeps an edited copy in the drawing. Встроенная таблица: изменения сохраняются как ее копия в чертеже.
Keep an editable copy of this table in the drawing Сохранить в чертеже копию этой таблицы для редактирования
Layout {name}: {table} Лист {name}: {table}
Grayscale Оттенки серого
Screening Тонирование
Use object linetype Тип линии объекта
Use object lineweight Вес линии объекта
Remove Удалить
Add Добавить
Open a drawing first. Сначала откройте чертеж.
Plot table type Тип таблицы печати
Color-dependent Зависимая от цвета
Named Именованная
Plus/minus Плюс/минус
Point Точка
Point Filters Фильтры точек
Point Style... Стиль точек...
Polar Array Полярный массив
Polar Tracking Полярное отслеживание
Polar Tracking (F10) Полярное отслеживание (F10)
Polar Tracking On (F10) Включить полярное отслеживание (F10)
Polar: {distance} < {angle}° Полярное: {distance} < {angle}°
Polygon Многоугольник
Polygonal Многоугольный
Polyline Полилиния
Position X Позиция X
Position Y Позиция Y
Position Z Позиция Z
Previous Предыдущий
Previous Layer Предыдущий слой
Primitives Примитивы
Print Печать
Print... Печать...
Properties Свойства
Properties Inspector Инспектор свойств
Properties: Свойства:
Proxy Прокси-объект
Purge Очистить неиспользуемое
Pyramid Пирамида
Quadrant Квадрант
Query Entities Запрос объектов
Quick Быстрый
Quick Dimension Быстрый размер
Quick Properties Быстрые свойства
Quick Select Быстрый выбор
Quick Select Properties Свойства быстрого выбора
Quick Select... Быстрый выбор...
QuickCalc Быстрокалькулятор
Quit CADCraft Выйти из CADCraft
Radius Радиус
Raster Image Растровое изображение
Ray Луч
Read-only Только для чтения
Real name Настоящее имя
Realtime В реальном времени
Reassociate Dimensions Восстановить ассоциативность размеров
Recent Недавние
Recover... Восстановить...
Rectangle Прямоугольник
Rectangular Прямоугольный
Rectangular Array Прямоугольный массив
Red Красный
Redo Повторить
Redo (⇧⌘Z) Повторить (⇧⌘Z)
Redraw Перерисовать
Regen Регенерация
Regen All Регенерировать все
Region Область
Region/Mass Properties Свойства области/массы
Relative Относительные
Remove Leader Удалить выноску
Remove the viewport override Удалить переопределение видового экрана
Rename Dimension Style Переименовать размерный стиль
Rename Layer Переименовать слой
Rename Layer State Переименовать состояние слоев
Rename Layout Переименовать лист
Rename Text Style Переименовать текстовый стиль
Rename... Переименовать...
Repeat {command} Повторить {command}
Requires a point (x,y, @dx,dy or @d<a). Требуется точка (x,y, @dx,dy или @d<a).
Reset Palettes Сбросить палитры
Restore Восстановить
Restore Layer State Восстановить состояние слоев
Reverse Изменить направление
Reverse the order Изменить порядок на обратный
Revision Cloud Облако
Revolve Тело вращения
Right Справа
Rotate Повернуть
Rotation Угол поворота
S Ю
Save Сохранить
Save (⌘S) Сохранить (⌘S)
Save As Сохранить как
Save As... Сохранить как...
Save Layer State Сохранить состояние слоев
Save State Сохранить состояние
Scale Масштаб
Search for layer Поиск слоя
Search for parameter Поиск параметра
Second and next points: Вторая и следующие точки:
Select Выбрать
Select 2D polyline Выберите 2D-полилинию
Select All Выделить все
Select Objects Выбрать объекты
Select a 2D polyline. Выберите 2D-полилинию.
Select a block reference. Выберите вхождение блока.
Select a circle or arc. Выберите круг или дугу.
Select a closed polyline Выберите замкнутую полилинию
Select a closed polyline, circle, ellipse or closed spline. Выберите замкнутую полилинию, круг, эллипс или замкнутый сплайн.
Select a closed polyline. Выберите замкнутую полилинию.
Select a curve as the path. Выберите кривую в качестве траектории.
Select a dimension constraint type Выберите тип размерного ограничения
Select a line object Выберите отрезок
Select a line object. Выберите отрезок.
Select a line or an arc. Выберите отрезок или дугу.
Select a line or polyline segment Выберите отрезок или сегмент полилинии
Select a line, arc or open polyline. Выберите отрезок, дугу или незамкнутую полилинию.
Select a line, arc, circle, polyline, ellipse or spline as the path. Выберите отрезок, дугу, круг, полилинию, эллипс или сплайн в качестве траектории.
Select a line, circle, arc or polyline segment. Выберите отрезок, круг, дугу или сегмент полилинии.
Select a line. Выберите отрезок.
Select a polyline. Выберите полилинию.
Select a text, mtext, attribute definition or dimension. Выберите текст, многострочный текст, определение атрибута или размер.
Select an annotation object Выберите аннотационный объект
Select an arc or circle Выберите дугу или круг
Select an arc. Выберите дугу.
Select an object Выберите объект
Select an object to change or Выберите объект для изменения или
Select an object to measure or Выберите объект для измерения или
Select an object. Выберите объект.
Select an open curve. Выберите незамкнутую кривую.
Select an open line, arc, polyline, ellipse arc or spline. Выберите незамкнутый отрезок, дугу, полилинию, эллиптическую дугу или сплайн.
Select arc or circle Выберите дугу или круг
Select arc or polyline arc segment Выберите дугу или дуговой сегмент полилинии
Select arc style Выберите стиль дуги
Select arc, circle, line Выберите дугу, круг, отрезок
Select at least two objects to join. Выберите не менее двух объектов для объединения.
Select circle or arc to add center mark Выберите круг или дугу для знака центра
Select closed objects (regions, closed polylines, circles). Выберите замкнутые объекты (области, замкнутые полилинии, круги).
Select first line Выберите первый отрезок
Select first line or arc Выберите первый отрезок или дугу
Select first object Выберите первый объект
Select first object or Выберите первый объект или
Select first point Выберите первую точку
Select nested objects to copy or Выберите вложенные объекты для копирования или
Select object Выберите объект
Select object to dimension Выберите объект для нанесения размера
Select object to divide Выберите объект для деления
Select object to extend or shift-select to trim Выберите объект для удлинения или выберите с Shift для обрезки
Select object to measure Выберите объект для разметки
Select object to offset Выберите объект для смещения
Select object to trim or shift-select to extend Выберите объект для обрезки или выберите с Shift для удлинения
Select objects Выберите объекты
Select objects (crossing window first corner) Выберите объекты (первый угол секущей рамки)
Select objects to array. Выберите объекты для массива.
Select objects to stretch by crossing-window or crossing-polygon... Выберите объекты для растягивания секущей рамкой или секущим многоугольником...
Select objects: Выберите объекты:
Select one polyline (Multiple is available through the JSON form). Выберите одну полилинию (Multiple доступно через JSON).
Select path curve Выберите кривую траектории
Select point or object Выберите точку или объект
Select polyline or Выберите полилинию или
Select second line Выберите второй отрезок
Select second line or shift-select to apply corner Выберите второй отрезок или выберите с Shift, чтобы построить угол
Select second object Выберите второй объект
Select second object or shift-select to apply corner Выберите второй объект или выберите с Shift, чтобы построить угол
Select second point Выберите вторую точку
Select spline Выберите сплайн
Select symmetry line Выберите ось симметрии
Select two points Выберите две точки
Selection Cycling Циклический выбор
Send to Back На задний план
Set Current Сделать текущим
Set Current Dimension Style Сделать размерный стиль текущим
Set Current Text Style Сделать текстовый стиль текущим
Set Layer Properties Задать свойства слоя
Set Properties Задать свойства
Set Table Cell Задать ячейку таблицы
Set Variable Задать переменную
Set View Задать вид
Share Поделиться
Show Показать
Show All Показать все
Show All Constraint Bars Показать все панели ограничений
Show All Dynamic Dimensions Показать все динамические размеры
Show Layer List Показать список слоев
Show/Hide Lineweight Показать/скрыть вес линий
Show/Hide Transparency Показать/скрыть прозрачность
Single Line Text Однострочный текст
Single Point Одна точка
Single... Один...
Smooth Сглаживание
Snap Привязка
Snap Mode Шаговая привязка
Snap Mode (F9) Шаговая привязка (F9)
Snap On (F9) Включить шаговую привязку (F9)
Snap X spacing Шаг привязки X
Snap and Grid Шаговая привязка и сетка
Solid Тело
Sort Сортировка
Specify a point Укажите точку
Specify a scale for the hatch pattern Укажите масштаб шаблона штриховки
Specify a second extension line origin Укажите начало второй выносной линии
Specify an angle for the hatch pattern Укажите угол шаблона штриховки
Specify angle end point Укажите конечную точку угла
Specify angle of dimension line Укажите угол размерной линии
Specify angle of dimension text Укажите угол размерного текста
Specify angle start point Укажите начальную точку угла
Specify angle vertex Укажите вершину угла
Specify angle vertex point Укажите точку вершины угла
Specify arc length dimension location Укажите положение размера длины дуги
Specify axis endpoint of ellipse Укажите конечную точку оси эллипса
Specify base diameter Укажите базовый диаметр
Specify base point Укажите базовую точку
Specify base point or Укажите базовую точку или
Specify base point or displacement (drag with the middle mouse button for realtime pan) Укажите базовую точку или смещение (перетаскивайте средней кнопкой мыши для панорамирования)
Specify base radius or Укажите базовый радиус или
Specify center of donut Укажите центр кольца
Specify center of ellipse Укажите центр эллипса
Specify center of polygon Укажите центр многоугольника
Specify center point for circle Укажите центр круга
Specify center point of arc Укажите центр дуги
Specify center point of array Укажите центр массива
Specify center point of base Укажите центр основания
Specify corner of viewport Укажите угол видового экрана
Specify corner of window, enter a scale factor (nX or nXP) Укажите угол рамки, введите масштабный коэффициент (nX или nXP)
Specify diameter of circle Укажите диаметр круга
Specify dimension arc line location Укажите положение размерной дуги
Specify dimension line location Укажите положение размерной линии
Specify distance between turns Укажите расстояние между витками
Specify distance to other axis Укажите расстояние до другой оси
Specify end angle Укажите конечный угол
Specify end point of arc Укажите конечную точку дуги
Specify end point of arc (hold Ctrl to switch direction) Укажите конечную точку дуги (удерживайте Ctrl для смены направления)
Specify ending width Укажите конечную ширину
Specify ending width for next segment Укажите конечную ширину следующего сегмента
Specify endpoint of arc Укажите конечную точку дуги
Specify endpoint of axis Укажите конечную точку оси
Specify endpoint of line Укажите конечную точку отрезка
Specify feature location Укажите положение элемента
Specify fillet radius Укажите радиус сопряжения
Specify fillet radius for rectangles Укажите радиус сопряжения прямоугольников
Specify first angle endpoint Укажите первую конечную точку угла
Specify first chamfer distance Укажите первое расстояние фаски
Specify first chamfer distance for rectangles Укажите первое расстояние фаски прямоугольников
Specify first constraint point Укажите первую точку ограничения
Specify first corner Укажите первый угол
Specify first corner point Укажите первую угловую точку
Specify first destination point Укажите первую целевую точку
Specify first end point of circle's diameter Укажите первую конечную точку диаметра круга
Specify first endpoint of edge Укажите первую конечную точку ребра
Specify first extension line origin Укажите начало первой выносной линии
Specify first point Укажите первую точку
Specify first point of mirror line Укажите первую точку оси зеркального отражения
Specify first point on circle Укажите первую точку на круге
Specify first point on circle: _tan to Укажите первую точку на круге: _tan к
Specify first source point Укажите первую исходную точку
Specify height Укажите высоту
Specify helix height or Укажите высоту спирали или
Specify included angle Укажите угол между сторонами
Specify insertion base point Укажите базовую точку вставки
Specify insertion point Укажите точку вставки
Specify inside diameter of donut Укажите внутренний диаметр кольца
Specify leader arrowhead location Укажите положение стрелки выноски
Specify leader endpoint Укажите конечную точку выноски
Specify leader landing location Укажите положение полки выноски
Specify length for rectangles Укажите длину прямоугольников
Specify length of chord Укажите длину хорды
Specify length of segment Укажите длину сегмента
Specify line width for rectangles Укажите ширину линий прямоугольников
Specify location for new vertex Укажите положение новой вершины
Specify minimum length of arc Укажите минимальную длину дуги
Specify new end point Укажите новую конечную точку
Specify new location for marked vertex Укажите новое положение отмеченной вершины
Specify new width for all segments Укажите новую ширину всех сегментов
Specify next point Укажите следующую точку
Specify offset distance Укажите расстояние смещения
Specify offset distance or Укажите расстояние смещения или
Specify opposite corner Укажите противоположный угол
Specify other corner point Укажите другую угловую точку
Specify other endpoint of axis Укажите другую конечную точку оси
Specify outside diameter of donut Укажите внешний диаметр кольца
Specify point Укажите точку
Specify point on object for first tangent of circle Укажите точку на объекте для первой касательной круга
Specify point on object for second tangent of circle Укажите точку на объекте для второй касательной круга
Specify point on side to offset Укажите точку со стороны смещения
Specify radius of arc Укажите радиус дуги
Specify radius of circle Укажите радиус круга
Specify rotation angle Укажите угол поворота
Specify rotation angle of text Укажите угол поворота текста
Specify rotation angle or Укажите угол поворота или
Specify scale factor Укажите масштабный коэффициент
Specify second angle endpoint Укажите вторую конечную точку угла
Specify second break point Укажите вторую точку разрыва
Specify second chamfer distance for rectangles Укажите второе расстояние фаски прямоугольников
Specify second constraint point Укажите вторую точку ограничения
Specify second destination point Укажите вторую целевую точку
Specify second end point of circle's diameter Укажите вторую конечную точку диаметра круга
Specify second endpoint of edge Укажите вторую конечную точку ребра
Specify second extension line origin Укажите начало второй выносной линии
Specify second point Укажите вторую точку
Specify second point of arc Укажите вторую точку дуги
Specify second point of mirror line Укажите вторую точку оси зеркального отражения
Specify second point on circle Укажите вторую точку на круге
Specify second point on circle: _tan to Укажите вторую точку на круге: _tan к
Specify second point or <use first point as displacement> Укажите вторую точку или <использовать первую точку как смещение>
Specify second source point Укажите вторую исходную точку
Specify side to offset Укажите сторону смещения
Specify start angle Укажите начальный угол
Specify start point Укажите начальную точку
Specify start point of arc Укажите начальную точку дуги
Specify start point of polyline Укажите начальную точку полилинии
Specify start point of text Укажите начальную точку текста
Specify starting width Укажите начальную ширину
Specify starting width for next segment Укажите начальную ширину следующего сегмента
Specify tangent direction for the start point of arc Укажите направление касательной в начальной точке дуги
Specify the elevation for rectangles Укажите уровень прямоугольников
Specify thickness for rectangles Укажите высоту выдавливания прямоугольников
Specify third point on circle Укажите третью точку на круге
Specify third point on circle: _tan to Укажите третью точку на круге: _tan к
Specify third source point or <continue> Укажите третью исходную точку или <продолжить>
Specify through point Укажите точку, через которую проходит прямая
Specify top diameter Укажите диаметр вершины
Specify top radius or Укажите радиус вершины или
Specify total angle Укажите полный угол
Specify total length or Укажите общую длину или
Specify width for rectangles Укажите ширину прямоугольников
Sphere Сфера
Spline Сплайн
Stack (type 1/2 then select) Дробь (введите 1/2, затем выберите)
Standard Стандартный
Start Начало
Start Width Начальная ширина
Start X Начало X
Start Y Начало Y
Start angle Начальный угол
Start, Center, Angle Начало, центр, угол
Start, Center, End Начало, центр, конец
Start, Center, Length Начало, центр, длина
Start, End, Angle Начало, конец, угол
Start, End, Direction Начало, конец, направление
Start, End, Radius Начало, конец, радиус
Status Состояние
Status Bar Строка состояния
Stretch Растянуть
Style: Standard Стиль: Standard
Subtract Вычесть
Sweep Тело по траектории
Switch Drawing Переключить чертеж
Switch Layout Переключить лист
Switch between model space in a viewport and paper space Переключить между пространством модели в видовом экране и пространством листа
Symmetric Симметрия
Synchronize Attributes Синхронизировать атрибуты
Toggle PICKADD (off: each pick replaces the selection) Переключить PICKADD (выкл.: каждый выбор заменяет набор выделения)
Toggle PICKADD (on: picks add to the selection) Переключить PICKADD (вкл.: выделение добавляется к набору)
TOP СВЕРХУ
Table Таблица
Table Style... Стиль таблиц...
Table style Стиль таблиц
Table... Таблица...
Tan, Tan, Radius Касательная, касательная, радиус
Tan, Tan, Tan Касательная, касательная, касательная
Tangent Касательность
Text Текст
Text Editor Текстовый редактор
Text Height Высота текста
Text Style... Текстовый стиль...
Text height Высота текста
Text override Переопределение текста
Text style Текстовый стиль
Thaw All Layers Разморозить все слои
There is nothing here yet. Здесь пока ничего нет.
Thickness Высота выдавливания
Time Время
To get started, create or open a drawing. Чтобы начать, создайте или откройте чертеж.
Tool Bar Панель инструментов
Tool Sets Наборы инструментов
Tools Инструменты
Torus Тор
Trace Широкая линия
Transparency Прозрачность
Trim Обрезать
True Color Цвет RGB
Turn All Layers On Включить все слои
Turn Off Layer Выключить слой
Turn Quick Properties off (QPMODE) Выключить быстрые свойства (QPMODE)
Type Тип
Type a command Введите команду
Type text; Enter starts a new paragraph Введите текст; Enter начинает новый абзац
UCS Icon Значок ПСК
Undo Отменить
Undo (⌘Z) Отменить (⌘Z)
Ungroup Разгруппировать
Union Объединение
Unisolate Layer Отменить изоляцию слоев
Units... Единицы...
Unlock Layer Разблокировать слой
Unmerge Cells Разъединить ячейки
Unsaved Layer State Несохраненное состояние слоев
Up Вверх
Update Обновить
Use System Setting Как в системе
User Пользователь
User Parameters Пользовательские параметры
Username Имя пользователя
VP Color Цвет ВЭ
VP Freeze Заморозка ВЭ
Value Значение
Value: Значение:
Version Версия
Version {version} Версия {version}
Vertical Вертикальность
VerticalDistance ВертикальноеРасстояние
View Вид
ViewCube Видовой куб
Viewport Видовой экран
Viewport Layer Freeze Заморозить слой в видовом экране
Viewport Properties Свойства видового экрана
Viewport {id} Видовой экран {id}
Viewports Видовые экраны
W З
WCS ⌄ МСК ⌄
White Белый
Width factor Коэффициент ширины
Window Окно
Wipeout Маскировка
Workspace Switching Переключение рабочих сред
Write Block Записать блок в файл
Xline Вспомогательная прямая
Yellow Желтый
Yes Да
Zoom Показать
Zoom All Показать все
Zoom Extents Показать границы объектов
Zoom In Увеличить
Zoom Object Показать объект
Zoom Out Уменьшить
Zoom Previous Предыдущий масштаб
Zoom Window Показать рамкой
error ошибка
{contributors} · {commits} {contributors} · {commits}
{kind} (constraint {id}) {kind} (ограничение {id})
{kind} constraint {name} = {expression} (double-click to edit) Ограничение «{kind}» {name} = {expression} (дважды щелкните для редактирования)
{name}: {distance} < {angle}° {name}: {distance} < {angle}°
ƒx New User Parameter ƒx Новый пользовательский параметр
ΔLOC Δстрок
−Bin −двоич.
−LOC −строк
⌘↩ to finish ⌘↩ для завершения
@plural {n} commit {n} коммит|{n} коммита|{n} коммитов
@plural {n} contributor {n} участник|{n} участника|{n} участников
Drawing as bytes Чертеж как байты
Services Службы
Hide CADCraft Скрыть CADCraft
Hide Others Скрыть остальные
Accessibility Специальные возможности
Show System Cursor Показывать системный курсор
Import... Импорт...
Export... Экспорт...
Batch Publish... Пакетная публикация...
Hatch Pattern Образец штриховки
Search for pattern Поиск образца
1 # CADCraft Russian interface catalog — original translations by @pechnikov2026-star.
2 # License: MIT OR Apache-2.0 (the repository LICENSE-MIT and LICENSE-APACHE).
3 # Written from CADCraft's English strings; no proprietary translations were copied.
4 # UTF-8: context<TAB>English source<TAB>Russian translation.
5 # Empty context = ordinary label; @plural = one|few|many forms with {n}.
6 # Command IDs, typed keywords, technology/product names and user data stay unchanged.
7 Cancel Отмена
8 OK ОК
9 or [ или [
10 % of all commits % всех коммитов
11 * Wildcard Match * По шаблону
12 *VARIES* *РАЗНЫЕ*
13 + | Top | 2D Wireframe + | Сверху | 2D-каркас
14 +Bin +двоич.
15 +LOC +строк
16 1 Viewport 1 видовой экран
17 2 Points 2 точки
18 2 Viewports 2 видовых экрана
19 2D Solid 2D-заливка
20 3 Points 3 точки
21 3 Viewports 3 видовых экрана
22 3D Face 3D-грань
23 3D Operations 3D-операции
24 3D Polyline 3D-полилиния
25 4 Viewports 4 видовых экрана
26 < Less than < Меньше чем
27 <> Not Equal <> Не равно
28 = Equals = Равно
29 > Greater than > Больше чем
30 About О программе
31 About CADCraft О CADCraft
32 Absolute Абсолютные
33 Add Leader Добавить выноску
34 Align Выровнять
35 Align Text Выровнять текст
36 Aligned Параллельный
37 All Все
38 All Used Layers Все используемые слои
39 All files Все файлы
40 Angle Угол
41 Angular Угловой
42 Annotation Visibility Видимость аннотаций
43 Annotation scale Масштаб аннотаций
44 Apparent Intersection Мнимое пересечение
45 Append to current selection set Добавить к текущему набору выделения
46 Apply to: Применить к:
47 Apply true color Применить цвет RGB
48 Arc Дуга
49 Arc Length Длина дуги
50 Area Площадь
51 Arrange Panel Расположить панель
52 Array Массив
53 Attach Присоединить
54 Attribute Атрибут
55 Attribute Definition Определение атрибута
56 Attribute Display Отображение атрибутов
57 Audit Проверка чертежа
58 Auto (system language) Авто (язык системы)
59 Auto Constrain Автоограничение
60 AutoConstrain Автоограничение
61 Axis, End Ось, конец
62 Base База
63 Baseline Базовый
64 Batch Publish Пакетная публикация
65 Binary assets added Добавленные двоичные ресурсы
66 Binary assets removed Удаленные двоичные ресурсы
67 Blend Сгладить
68 Block Блок
69 Block Attribute Manager... Диспетчер атрибутов блоков...
70 Block Editor Редактор блоков
71 Block Reference Вхождение блока
72 Block... Блок...
73 Blocks Блоки
74 Blue Синий
75 Bold Жирный
76 Boolean Булевы операции
77 Boundary Граница
78 Boundary... Граница...
79 Box Параллелепипед
80 Break Разорвать
81 Break At Point Разорвать в точке
82 Bring to Front На передний план
83 ByBlock Поблоку
84 ByColor ПоЦвету
85 ByLayer Послою
86 CADCraft Help Справка CADCraft
87 Cancel Отмена
88 Cartesian (x, y) Декартовы (x, y)
89 Center Центр
90 Center Line Осевая линия
91 Center Mark Знак центра
92 Center X Центр X
93 Center Y Центр Y
94 Center Z Центр Z
95 Center, Diameter Центр, диаметр
96 Center, Radius Центр, радиус
97 Center, Start, Angle Центр, начало, угол
98 Center, Start, End Центр, начало, конец
99 Center, Start, Length Центр, начало, длина
100 Chamfer Фаска
101 Change Space Изменить пространство
102 Change to Current Layer Перенести на текущий слой
103 Circle Круг
104 Circumference Длина окружности
105 Clear Очистить
106 Clipboard Буфер обмена
107 Close Закрыть
108 Close All Закрыть все
109 Closed Замкнутый
110 Coincident Совпадение
111 Collapse Tool Sets Свернуть наборы инструментов
112 Collinear Коллинеарность
113 Color Цвет
114 Color 8 Цвет 8
115 Color 9 Цвет 9
116 Color Index Индекс цвета
117 Color... Цвет...
118 Command History Журнал команд
119 Command Line Командная строка
120 Command Line History Lines Строки журнала командной строки
121 Command Reference Справочник команд
122 Commits Коммиты
123 Company Организация
124 Computer-aided design and drafting: an open-source, clean-room CAD application written in pure Rust. Автоматизированное проектирование и черчение: CAD-приложение с открытым кодом, независимо разработанное на Rust.
125 Concentric Концентричность
126 Cone Конус
127 Constraint Bars Панели ограничений
128 Constraint Settings Параметры ограничений
129 Construction Line Вспомогательная прямая
130 Contents Содержание
131 Continue Продолжить
132 Contributors Участники
133 Control Vertices Управляющие вершины
134 Copy Копировать
135 Copy Layout Копировать лист
136 Copy Nested Objects Копировать вложенные объекты
137 Copy Selection Копировать выделение
138 Copy with Base Point Копировать с базовой точкой
139 Count Количество
140 Create Block Создать блок
141 Creates a user parameter Создает пользовательский параметр
142 Ctrl+Enter to finish Ctrl+Enter для завершения
143 Current layer Текущий слой
144 Current selection Текущее выделение
145 Current: {color} Текущий: {color}
146 Cut Вырезать
147 Cyan Голубой
148 Cylinder Цилиндр
149 DWG DWG
150 DXF DXF
151 Dark Темная
152 Default По умолчанию
153 Default Save Format Формат сохранения по умолчанию
154 Define Attributes Определить атрибуты
155 Define Attributes... Определить атрибуты...
156 Defined width Заданная ширина
157 Degree Градус
158 Delete Удалить
159 Delete Column Удалить столбец
160 Delete Constraints Удалить ограничения
161 Delete Dimension Style Удалить размерный стиль
162 Delete Duplicate Objects Удалить дубликаты объектов
163 Delete Layer Удалить слой
164 Delete Layer State Удалить состояние слоев
165 Delete Layout Удалить лист
166 Delete Row Удалить строку
167 Delete Text Style Удалить текстовый стиль
168 Delta X Смещение X
169 Delta Y Смещение Y
170 Description Описание
171 Deselect All Снять выделение со всего
172 Diameter Диаметр
173 Dim style Размерный стиль
174 Dimension Размеры
175 Dimension Space Расстояние между размерами
176 Dimension Style Override Переопределение размерного стиля
177 Dimension Style... Размерный стиль...
178 Dimension Text Edit Редактировать размерный текст
179 Dimension style Размерный стиль
180 Dimensional Constraint Размерное ограничение
181 Dimensional Constraint Parameters Параметры размерных ограничений
182 Dimensional Constraints Размерные ограничения
183 Disassociate Dimensions Разъединить размеры
184 Display name Отображаемое имя
185 Distance Расстояние
186 Divide Поделить
187 Do not plot (click: plot) Не выводить на печать (щелкните для печати)
188 Donut Кольцо
189 Double-click to edit the description Дважды щелкните, чтобы изменить описание
190 Down Вниз
191 Drafting Черчение
192 Drafting Settings Режимы черчения
193 Drafting Settings... Режимы черчения...
194 Draw Рисование
195 Draw Order Порядок отрисовки
196 Drawing (DWG) Чертеж (DWG)
197 Drawing (DXF) Чертеж (DXF)
198 Drawing Limits Лимиты чертежа
199 Drawing Utilities Утилиты чертежа
200 Drawings (DWG, DXF) Чертежи (DWG, DXF)
201 Dynamic Dimensions Динамические размеры
202 Dynamic Input Динамический ввод
203 Dynamic Input (F12) Динамический ввод (F12)
204 E В
205 Edit Редактирование
206 Edit Text... Редактировать текст...
207 Elevation Уровень
208 Ellipse Эллипс
209 Elliptical Arc Эллиптическая дуга
210 Enable Dynamic Input (F12) Включить динамический ввод (F12)
211 End Width Конечная ширина
212 End X Конец X
213 End Y Конец Y
214 End angle Конечный угол
215 Endpoint Конечная точка
216 Enter Top, Zero or Bottom. Введите Top, Zero или Bottom.
217 Enter a fit data option Введите параметр аппроксимации
218 Enter a pattern name Введите имя шаблона
219 Enter a vertex editing option Введите параметр редактирования вершины
220 Enter an option Введите параметр
221 Enter angle of xline Введите угол вспомогательной прямой
222 Enter angle of xline (0) or Введите угол вспомогательной прямой (0) или
223 Enter area of rectangle in current units Введите площадь прямоугольника в текущих единицах
224 Enter attribute prompt Введите подсказку атрибута
225 Enter attribute tag name Введите имя тега атрибута
226 Enter attribute value Введите значение атрибута
227 Enter block name Введите имя блока
228 Enter constraint type Введите тип ограничения
229 Enter continuity Введите тип непрерывности
230 Enter default attribute value Введите значение атрибута по умолчанию
231 Enter degree of spline Введите степень сплайна
232 Enter delta angle Введите изменение угла
233 Enter delta length or Введите изменение длины или
234 Enter dimension text Введите размерный текст
235 Enter dimension text (<> = measurement) Введите размерный текст (<> = измеренное значение)
236 Enter dimension value or [name=expression] Введите размерное значение или [имя=выражение]
237 Enter dimension variable name to override Введите имя размерной переменной для переопределения
238 Enter justification type Введите тип выравнивания
239 Enter leader text Введите текст выноски
240 Enter mline scale Введите масштаб мультилинии
241 Enter mode Введите режим
242 Enter name of block to insert Введите имя блока для вставки
243 Enter new text Введите новый текст
244 Enter next point Введите следующую точку
245 Enter number of columns Введите количество столбцов
246 Enter number of data rows Введите количество строк данных
247 Enter number of items Введите количество элементов
248 Enter number of items along path Введите количество элементов вдоль траектории
249 Enter number of sides Введите количество сторон
250 Enter number of turns Введите количество витков
251 Enter percentage length Введите длину в процентах
252 Enter rectangle length Введите длину прямоугольника
253 Enter rectangle width Введите ширину прямоугольника
254 Enter scale factor Введите масштабный коэффициент
255 Enter text Введите текст
256 Enter text (use \P for new paragraphs) Введите текст (\P начинает новый абзац)
257 Enter the number of segments Введите количество сегментов
258 Enter twist direction of helix Введите направление закручивания спирали
259 Entire drawing Весь чертеж
260 Equal Равенство
261 Erase Стереть
262 Exclude from new selection set Исключить из нового набора выделения
263 Explode Расчленить
264 Export Экспортировать
265 Export to PDF... Экспортировать в PDF...
266 Expression Выражение
267 Extend Удлинить
268 Extension Продолжение
269 Extents Границы
270 Extrude Выдавить
271 File Файл
272 File Tab Вкладка файла
273 Fillet Сопряжение
274 Filter Фильтр
275 Filters Фильтры
276 Find and Replace Найти и заменить
277 Find... Найти...
278 First Первый
279 First commit Первый коммит
280 Fit Points Точки сглаживания
281 Fix Фиксация
282 Flatten Objects Спроецировать объекты на плоскость
283 Format Формат
284 Freehand От руки
285 Freeze Заморозить
286 Freeze Layer Заморозить слой
287 Freeze in new viewports Заморозить в новых видовых экранах
288 Freeze/Thaw Заморозить/разморозить
289 Freeze/Thaw in all viewports Заморозить/разморозить во всех видовых экранах
290 Freeze/Thaw in the current viewport Заморозить/разморозить в текущем видовом экране
291 From От точки
292 General Общие
293 Geometric Center Геометрический центр
294 Geometric Constraint Геометрическое ограничение
295 Geometric Constraints Геометрические ограничения
296 Geometry Геометрия
297 Get Variable Получить переменную
298 Get View Получить вид
299 Global Глобально
300 Global width Общая ширина
301 Grab bag Список
302 Gradient Градиент
303 Gradient... Градиент...
304 Green Зеленый
305 Grid (F7) Сетка (F7)
306 Grid Display Отображение сетки
307 Grid On (F7) Включить сетку (F7)
308 Grid spacing Шаг сетки
309 Grip Mirror Зеркальное отражение ручками
310 Grip Rotate Поворот ручками
311 Grip Scale Масштабирование ручками
312 Grip Stretch Растягивание ручками
313 Group Сгруппировать
314 Group Edit Редактировать группу
315 Hatch Штриховка
316 Hatch Edit Редактировать штриховку
317 Hatch... Штриховка...
318 Height Высота
319 Height: {height} Высота: {height}
320 Helix Спираль
321 Help Справка
322 Hide All Скрыть все
323 Hide All Constraint Bars Скрыть все панели ограничений
324 Hide All Dynamic Dimensions Скрыть все динамические размеры
325 Hide Layer List Скрыть список слоев
326 Hide Palettes Скрыть палитры
327 Home Исходный вид
328 Horizontal Горизонтальность
329 HorizontalDistance ГоризонтальноеРасстояние
330 How to apply: Способ применения:
331 ID Point Координаты точки
332 Import Импортировать
333 In Приблизить
334 In-window Menu Bar Строка меню в окне
335 Include in new selection set Включить в новый набор выделения
336 Increment angle Шаг угла
337 Index Color Индексированный цвет
338 Index color: {index} Индексированный цвет: {index}
339 Inquiry Измерение
340 Insert Вставка
341 Insert Block Вставить блок
342 Insert Column Вставить столбец
343 Insert Row Вставить строку
344 Insert this block Вставить этот блок
345 Insertion Точка вставки
346 Insertion X Точка вставки X
347 Insertion Y Точка вставки Y
348 Insertion Z Точка вставки Z
349 Inspect Constraints Просмотреть ограничения
350 Inspect Drawing Просмотреть чертеж
351 Interface Language Язык интерфейса
352 Interface Theme Тема интерфейса
353 Intersect Пересечение
354 Intersection Пересечение
355 Isolate Изолировать
356 Isolate Layer Изолировать слой
357 Isometric Drafting Изометрическое черчение
358 Join Объединить
359 Language Язык
360 Last Последний
361 Last commit Последний коммит
362 Layer Слой
363 Layer Delete Удалить слой с объектами
364 Layer Match Согласовать слой
365 Layer Merge Объединить слои
366 Layer Off Выключить слой
367 Layer Properties Свойства слоя
368 Layer Properties Manager Диспетчер свойств слоев
369 Layer Properties Manager — current viewport Диспетчер свойств слоев — текущий видовой экран
370 Layer State Состояние слоев
371 Layer States Manager... Диспетчер состояний слоев...
372 Layer Tools Инструменты слоев
373 Layer contains objects (double-click to make current) Слой содержит объекты (дважды щелкните, чтобы сделать текущим)
374 Layer is empty (double-click to make current) Слой пуст (дважды щелкните, чтобы сделать текущим)
375 Layers Слои
376 Layers Palette Палитра слоев
377 Layers displayed: {shown} of {total}. Current layer: {layer} Показано слоев: {shown} из {total}. Текущий слой: {layer}
378 Layout Лист
379 Layout list Список листов
380 Leader Выноска
381 Left Слева
382 Length Длина
383 Length X Длина X
384 Length Y Длина Y
385 Lengthen Изменить длину
386 Light Светлая
387 Line Отрезок
388 Line delta Изменение количества строк
389 Linear Линейный
390 Lines +/− Строки +/−
391 Lines added Добавленные строки
392 Lines deleted Удаленные строки
393 Linetype Тип линии
394 Linetype Scale Масштаб типа линии
395 Linetype scale Масштаб типа линии
396 Ltscale Масштаб типа линии
397 Linetype... Тип линии...
398 Lineweight Вес линии
399 Lineweight... Вес линии...
400 List Список
401 List Blocks Список блоков
402 List Dimension Styles Список размерных стилей
403 List Groups Список групп
404 List Layer States Список состояний слоев
405 List Layouts Список листов
406 List System Variables Список системных переменных
407 List Text Styles Список текстовых стилей
408 Load all Загрузить все
409 Load... Загрузить...
410 Loaded linetypes Загруженные типы линий
411 Lock Заблокировать
412 Lock Layer Заблокировать слой
413 Lock/Unlock Заблокировать/разблокировать
414 Loft Тело по сечениям
415 MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Не аффилировано с Autodesk, Inc.
416 MODEL МОДЕЛЬ
417 MText Многострочный текст
418 Magenta Пурпурный
419 Major line every Основная линия каждые
420 Make Current Сделать текущим
421 Make Object's Layer Current Сделать слой объекта текущим
422 Make... Создать...
423 Match Layer Согласовать слой
424 Match Properties Копирование свойств
425 Measure Разметить
426 Measure Distance Измерить расстояние
427 Measure Geometry Измерить геометрию
428 Merge Cells Объединить ячейки
429 Merged PRs Принятые PR
430 Mid Between 2 Points Середина между двумя точками
431 Midpoint Середина
432 Mirror Зеркальное отражение
433 Model Модель
434 Model Space (in viewport) Пространство модели (в видовом экране)
435 Modeling Моделирование
436 Models Модели
437 Modify Модификация
438 Move Переместить
439 Multileader Мультивыноска
440 Multileader Style... Стиль мультивыносок...
441 Multileader style Стиль мультивыносок
442 Multiline Мультилиния
443 Multiline Text Многострочный текст
444 Multiline Text... Многострочный текст...
445 Multiple Разные типы
446 Multiple Point Несколько точек
447 My Мои
448 My properties: not available yet Мои свойства: пока недоступно
449 N С
450 Name Название
451 Name (A–Z) Название (А–Я)
452 Nearest Ближайшая точка
453 New Создать
454 New (metric) Создать (метрический)
455 New Drawing Новый чертеж
456 New Drawing (⌘N) Новый чертеж (⌘N)
457 New Drawing... Новый чертеж...
458 New Layer Новый слой
459 New Layout Новый лист
460 New VP Freeze Заморозить в новых ВЭ
461 New Viewports... Новые видовые экраны...
462 New layout Новый лист
463 New state name Название нового состояния
464 No Нет
465 No blocks defined in this drawing. В этом чертеже нет определенных блоков.
466 No contributor data was built into this copy. В эту сборку не включены данные об участниках.
467 No model credits were built into this copy. В эту сборку не включены данные о моделях.
468 No saved layer states Нет сохраненных состояний слоев
469 No selection Ничего не выделено
470 Node Узел
471 None Нет
472 Not available Недоступно
473 Not available yet Пока недоступно
474 OK ОК
475 Object Объект
476 Object Snap Объектная привязка
477 Object Snap (F3) Объектная привязка (F3)
478 Object Snap On (F3) Включить объектную привязку (F3)
479 Object Snap Tracking Отслеживание объектной привязки
480 Object Snap Tracking (F11) Отслеживание объектной привязки (F11)
481 Object type: Тип объекта:
482 Off Выключено
483 Offset Смещение
484 On Включено
485 On/Off Включить/выключить
486 Open (⌘O) Открыть (⌘O)
487 Open Sample Drawing Открыть образец чертежа
488 Open sample drawing Открыть образец чертежа
489 Open... Открыть...
490 Operator: Оператор:
491 Orbit Орбита
492 Ordinate Ординатный
493 Ortho (F8) Орто (F8)
494 Ortho Mode Режим Орто
495 Ortho Mode (F8) Режим Орто (F8)
496 Osnap Settings... Параметры объектной привязки...
497 Out Отдалить
498 Override Переопределить
499 PAPER ЛИСТ
500 Percent Процент
501 Polar (distance < angle) Полярные (расстояние < угол)
502 PRs PR
503 Page Setup Manager Диспетчер параметров листов
504 Page Setup Manager... Диспетчер параметров листов...
505 Pan Панорамирование
506 Pan Down Панорамировать вниз
507 Pan Left Панорамировать влево
508 Pan Right Панорамировать вправо
509 Pan Up Панорамировать вверх
510 Paper Space Пространство листа
511 Parallel Параллельность
512 Parameters Manager Диспетчер параметров
513 Parametric Параметризация
514 Paste Вставить
515 Paste as Block Вставить как блок
516 Paste to Original Coordinates Вставить в исходные координаты
517 Path Array Массив по траектории
518 Pattern Шаблон
519 Perpendicular Перпендикулярность
520 Plot Печать чертежа
521 Plot (click: do not plot) Печатать (щелкните, чтобы отключить печать)
522 Plot Preview Предварительный просмотр печати
523 Plot style Стиль печати
524 Plot style attached to Стиль печати применен к
525 Plot style table Таблица стилей печати
526 Plot Style... Стиль печати...
527 Plot Style Manager Диспетчер стилей печати
528 Plot Style Manager... Диспетчер стилей печати...
529 Plot Styles... Стили печати...
530 New CTB Новая CTB
531 New STB Новая STB
532 New style Новый стиль
533 Reset Panel Layout Сбросить расположение панелей
534 Use for this layout Использовать для этого листа
535 Display plot styles Показывать стили печати
536 No plot style tables. Нет таблиц стилей печати.
537 A built-in table: editing it keeps an edited copy in the drawing. Встроенная таблица: изменения сохраняются как ее копия в чертеже.
538 Keep an editable copy of this table in the drawing Сохранить в чертеже копию этой таблицы для редактирования
539 Layout {name}: {table} Лист {name}: {table}
540 Grayscale Оттенки серого
541 Screening Тонирование
542 Use object linetype Тип линии объекта
543 Use object lineweight Вес линии объекта
544 Remove Удалить
545 Add Добавить
546 Open a drawing first. Сначала откройте чертеж.
547 Plot table type Тип таблицы печати
548 Color-dependent Зависимая от цвета
549 Named Именованная
550 Plus/minus Плюс/минус
551 Point Точка
552 Point Filters Фильтры точек
553 Point Style... Стиль точек...
554 Polar Array Полярный массив
555 Polar Tracking Полярное отслеживание
556 Polar Tracking (F10) Полярное отслеживание (F10)
557 Polar Tracking On (F10) Включить полярное отслеживание (F10)
558 Polar: {distance} < {angle}° Полярное: {distance} < {angle}°
559 Polygon Многоугольник
560 Polygonal Многоугольный
561 Polyline Полилиния
562 Position X Позиция X
563 Position Y Позиция Y
564 Position Z Позиция Z
565 Previous Предыдущий
566 Previous Layer Предыдущий слой
567 Primitives Примитивы
568 Print Печать
569 Print... Печать...
570 Properties Свойства
571 Properties Inspector Инспектор свойств
572 Properties: Свойства:
573 Proxy Прокси-объект
574 Purge Очистить неиспользуемое
575 Pyramid Пирамида
576 Quadrant Квадрант
577 Query Entities Запрос объектов
578 Quick Быстрый
579 Quick Dimension Быстрый размер
580 Quick Properties Быстрые свойства
581 Quick Select Быстрый выбор
582 Quick Select Properties Свойства быстрого выбора
583 Quick Select... Быстрый выбор...
584 QuickCalc Быстрокалькулятор
585 Quit CADCraft Выйти из CADCraft
586 Radius Радиус
587 Raster Image Растровое изображение
588 Ray Луч
589 Read-only Только для чтения
590 Real name Настоящее имя
591 Realtime В реальном времени
592 Reassociate Dimensions Восстановить ассоциативность размеров
593 Recent Недавние
594 Recover... Восстановить...
595 Rectangle Прямоугольник
596 Rectangular Прямоугольный
597 Rectangular Array Прямоугольный массив
598 Red Красный
599 Redo Повторить
600 Redo (⇧⌘Z) Повторить (⇧⌘Z)
601 Redraw Перерисовать
602 Regen Регенерация
603 Regen All Регенерировать все
604 Region Область
605 Region/Mass Properties Свойства области/массы
606 Relative Относительные
607 Remove Leader Удалить выноску
608 Remove the viewport override Удалить переопределение видового экрана
609 Rename Dimension Style Переименовать размерный стиль
610 Rename Layer Переименовать слой
611 Rename Layer State Переименовать состояние слоев
612 Rename Layout Переименовать лист
613 Rename Text Style Переименовать текстовый стиль
614 Rename... Переименовать...
615 Repeat {command} Повторить {command}
616 Requires a point (x,y, @dx,dy or @d<a). Требуется точка (x,y, @dx,dy или @d<a).
617 Reset Palettes Сбросить палитры
618 Restore Восстановить
619 Restore Layer State Восстановить состояние слоев
620 Reverse Изменить направление
621 Reverse the order Изменить порядок на обратный
622 Revision Cloud Облако
623 Revolve Тело вращения
624 Right Справа
625 Rotate Повернуть
626 Rotation Угол поворота
627 S Ю
628 Save Сохранить
629 Save (⌘S) Сохранить (⌘S)
630 Save As Сохранить как
631 Save As... Сохранить как...
632 Save Layer State Сохранить состояние слоев
633 Save State Сохранить состояние
634 Scale Масштаб
635 Search for layer Поиск слоя
636 Search for parameter Поиск параметра
637 Second and next points: Вторая и следующие точки:
638 Select Выбрать
639 Select 2D polyline Выберите 2D-полилинию
640 Select All Выделить все
641 Select Objects Выбрать объекты
642 Select a 2D polyline. Выберите 2D-полилинию.
643 Select a block reference. Выберите вхождение блока.
644 Select a circle or arc. Выберите круг или дугу.
645 Select a closed polyline Выберите замкнутую полилинию
646 Select a closed polyline, circle, ellipse or closed spline. Выберите замкнутую полилинию, круг, эллипс или замкнутый сплайн.
647 Select a closed polyline. Выберите замкнутую полилинию.
648 Select a curve as the path. Выберите кривую в качестве траектории.
649 Select a dimension constraint type Выберите тип размерного ограничения
650 Select a line object Выберите отрезок
651 Select a line object. Выберите отрезок.
652 Select a line or an arc. Выберите отрезок или дугу.
653 Select a line or polyline segment Выберите отрезок или сегмент полилинии
654 Select a line, arc or open polyline. Выберите отрезок, дугу или незамкнутую полилинию.
655 Select a line, arc, circle, polyline, ellipse or spline as the path. Выберите отрезок, дугу, круг, полилинию, эллипс или сплайн в качестве траектории.
656 Select a line, circle, arc or polyline segment. Выберите отрезок, круг, дугу или сегмент полилинии.
657 Select a line. Выберите отрезок.
658 Select a polyline. Выберите полилинию.
659 Select a text, mtext, attribute definition or dimension. Выберите текст, многострочный текст, определение атрибута или размер.
660 Select an annotation object Выберите аннотационный объект
661 Select an arc or circle Выберите дугу или круг
662 Select an arc. Выберите дугу.
663 Select an object Выберите объект
664 Select an object to change or Выберите объект для изменения или
665 Select an object to measure or Выберите объект для измерения или
666 Select an object. Выберите объект.
667 Select an open curve. Выберите незамкнутую кривую.
668 Select an open line, arc, polyline, ellipse arc or spline. Выберите незамкнутый отрезок, дугу, полилинию, эллиптическую дугу или сплайн.
669 Select arc or circle Выберите дугу или круг
670 Select arc or polyline arc segment Выберите дугу или дуговой сегмент полилинии
671 Select arc style Выберите стиль дуги
672 Select arc, circle, line Выберите дугу, круг, отрезок
673 Select at least two objects to join. Выберите не менее двух объектов для объединения.
674 Select circle or arc to add center mark Выберите круг или дугу для знака центра
675 Select closed objects (regions, closed polylines, circles). Выберите замкнутые объекты (области, замкнутые полилинии, круги).
676 Select first line Выберите первый отрезок
677 Select first line or arc Выберите первый отрезок или дугу
678 Select first object Выберите первый объект
679 Select first object or Выберите первый объект или
680 Select first point Выберите первую точку
681 Select nested objects to copy or Выберите вложенные объекты для копирования или
682 Select object Выберите объект
683 Select object to dimension Выберите объект для нанесения размера
684 Select object to divide Выберите объект для деления
685 Select object to extend or shift-select to trim Выберите объект для удлинения или выберите с Shift для обрезки
686 Select object to measure Выберите объект для разметки
687 Select object to offset Выберите объект для смещения
688 Select object to trim or shift-select to extend Выберите объект для обрезки или выберите с Shift для удлинения
689 Select objects Выберите объекты
690 Select objects (crossing window first corner) Выберите объекты (первый угол секущей рамки)
691 Select objects to array. Выберите объекты для массива.
692 Select objects to stretch by crossing-window or crossing-polygon... Выберите объекты для растягивания секущей рамкой или секущим многоугольником...
693 Select objects: Выберите объекты:
694 Select one polyline (Multiple is available through the JSON form). Выберите одну полилинию (Multiple доступно через JSON).
695 Select path curve Выберите кривую траектории
696 Select point or object Выберите точку или объект
697 Select polyline or Выберите полилинию или
698 Select second line Выберите второй отрезок
699 Select second line or shift-select to apply corner Выберите второй отрезок или выберите с Shift, чтобы построить угол
700 Select second object Выберите второй объект
701 Select second object or shift-select to apply corner Выберите второй объект или выберите с Shift, чтобы построить угол
702 Select second point Выберите вторую точку
703 Select spline Выберите сплайн
704 Select symmetry line Выберите ось симметрии
705 Select two points Выберите две точки
706 Selection Cycling Циклический выбор
707 Send to Back На задний план
708 Set Current Сделать текущим
709 Set Current Dimension Style Сделать размерный стиль текущим
710 Set Current Text Style Сделать текстовый стиль текущим
711 Set Layer Properties Задать свойства слоя
712 Set Properties Задать свойства
713 Set Table Cell Задать ячейку таблицы
714 Set Variable Задать переменную
715 Set View Задать вид
716 Share Поделиться
717 Show Показать
718 Show All Показать все
719 Show All Constraint Bars Показать все панели ограничений
720 Show All Dynamic Dimensions Показать все динамические размеры
721 Show Layer List Показать список слоев
722 Show/Hide Lineweight Показать/скрыть вес линий
723 Show/Hide Transparency Показать/скрыть прозрачность
724 Single Line Text Однострочный текст
725 Single Point Одна точка
726 Single... Один...
727 Smooth Сглаживание
728 Snap Привязка
729 Snap Mode Шаговая привязка
730 Snap Mode (F9) Шаговая привязка (F9)
731 Snap On (F9) Включить шаговую привязку (F9)
732 Snap X spacing Шаг привязки X
733 Snap and Grid Шаговая привязка и сетка
734 Solid Тело
735 Sort Сортировка
736 Specify a point Укажите точку
737 Specify a scale for the hatch pattern Укажите масштаб шаблона штриховки
738 Specify a second extension line origin Укажите начало второй выносной линии
739 Specify an angle for the hatch pattern Укажите угол шаблона штриховки
740 Specify angle end point Укажите конечную точку угла
741 Specify angle of dimension line Укажите угол размерной линии
742 Specify angle of dimension text Укажите угол размерного текста
743 Specify angle start point Укажите начальную точку угла
744 Specify angle vertex Укажите вершину угла
745 Specify angle vertex point Укажите точку вершины угла
746 Specify arc length dimension location Укажите положение размера длины дуги
747 Specify axis endpoint of ellipse Укажите конечную точку оси эллипса
748 Specify base diameter Укажите базовый диаметр
749 Specify base point Укажите базовую точку
750 Specify base point or Укажите базовую точку или
751 Specify base point or displacement (drag with the middle mouse button for realtime pan) Укажите базовую точку или смещение (перетаскивайте средней кнопкой мыши для панорамирования)
752 Specify base radius or Укажите базовый радиус или
753 Specify center of donut Укажите центр кольца
754 Specify center of ellipse Укажите центр эллипса
755 Specify center of polygon Укажите центр многоугольника
756 Specify center point for circle Укажите центр круга
757 Specify center point of arc Укажите центр дуги
758 Specify center point of array Укажите центр массива
759 Specify center point of base Укажите центр основания
760 Specify corner of viewport Укажите угол видового экрана
761 Specify corner of window, enter a scale factor (nX or nXP) Укажите угол рамки, введите масштабный коэффициент (nX или nXP)
762 Specify diameter of circle Укажите диаметр круга
763 Specify dimension arc line location Укажите положение размерной дуги
764 Specify dimension line location Укажите положение размерной линии
765 Specify distance between turns Укажите расстояние между витками
766 Specify distance to other axis Укажите расстояние до другой оси
767 Specify end angle Укажите конечный угол
768 Specify end point of arc Укажите конечную точку дуги
769 Specify end point of arc (hold Ctrl to switch direction) Укажите конечную точку дуги (удерживайте Ctrl для смены направления)
770 Specify ending width Укажите конечную ширину
771 Specify ending width for next segment Укажите конечную ширину следующего сегмента
772 Specify endpoint of arc Укажите конечную точку дуги
773 Specify endpoint of axis Укажите конечную точку оси
774 Specify endpoint of line Укажите конечную точку отрезка
775 Specify feature location Укажите положение элемента
776 Specify fillet radius Укажите радиус сопряжения
777 Specify fillet radius for rectangles Укажите радиус сопряжения прямоугольников
778 Specify first angle endpoint Укажите первую конечную точку угла
779 Specify first chamfer distance Укажите первое расстояние фаски
780 Specify first chamfer distance for rectangles Укажите первое расстояние фаски прямоугольников
781 Specify first constraint point Укажите первую точку ограничения
782 Specify first corner Укажите первый угол
783 Specify first corner point Укажите первую угловую точку
784 Specify first destination point Укажите первую целевую точку
785 Specify first end point of circle's diameter Укажите первую конечную точку диаметра круга
786 Specify first endpoint of edge Укажите первую конечную точку ребра
787 Specify first extension line origin Укажите начало первой выносной линии
788 Specify first point Укажите первую точку
789 Specify first point of mirror line Укажите первую точку оси зеркального отражения
790 Specify first point on circle Укажите первую точку на круге
791 Specify first point on circle: _tan to Укажите первую точку на круге: _tan к
792 Specify first source point Укажите первую исходную точку
793 Specify height Укажите высоту
794 Specify helix height or Укажите высоту спирали или
795 Specify included angle Укажите угол между сторонами
796 Specify insertion base point Укажите базовую точку вставки
797 Specify insertion point Укажите точку вставки
798 Specify inside diameter of donut Укажите внутренний диаметр кольца
799 Specify leader arrowhead location Укажите положение стрелки выноски
800 Specify leader endpoint Укажите конечную точку выноски
801 Specify leader landing location Укажите положение полки выноски
802 Specify length for rectangles Укажите длину прямоугольников
803 Specify length of chord Укажите длину хорды
804 Specify length of segment Укажите длину сегмента
805 Specify line width for rectangles Укажите ширину линий прямоугольников
806 Specify location for new vertex Укажите положение новой вершины
807 Specify minimum length of arc Укажите минимальную длину дуги
808 Specify new end point Укажите новую конечную точку
809 Specify new location for marked vertex Укажите новое положение отмеченной вершины
810 Specify new width for all segments Укажите новую ширину всех сегментов
811 Specify next point Укажите следующую точку
812 Specify offset distance Укажите расстояние смещения
813 Specify offset distance or Укажите расстояние смещения или
814 Specify opposite corner Укажите противоположный угол
815 Specify other corner point Укажите другую угловую точку
816 Specify other endpoint of axis Укажите другую конечную точку оси
817 Specify outside diameter of donut Укажите внешний диаметр кольца
818 Specify point Укажите точку
819 Specify point on object for first tangent of circle Укажите точку на объекте для первой касательной круга
820 Specify point on object for second tangent of circle Укажите точку на объекте для второй касательной круга
821 Specify point on side to offset Укажите точку со стороны смещения
822 Specify radius of arc Укажите радиус дуги
823 Specify radius of circle Укажите радиус круга
824 Specify rotation angle Укажите угол поворота
825 Specify rotation angle of text Укажите угол поворота текста
826 Specify rotation angle or Укажите угол поворота или
827 Specify scale factor Укажите масштабный коэффициент
828 Specify second angle endpoint Укажите вторую конечную точку угла
829 Specify second break point Укажите вторую точку разрыва
830 Specify second chamfer distance for rectangles Укажите второе расстояние фаски прямоугольников
831 Specify second constraint point Укажите вторую точку ограничения
832 Specify second destination point Укажите вторую целевую точку
833 Specify second end point of circle's diameter Укажите вторую конечную точку диаметра круга
834 Specify second endpoint of edge Укажите вторую конечную точку ребра
835 Specify second extension line origin Укажите начало второй выносной линии
836 Specify second point Укажите вторую точку
837 Specify second point of arc Укажите вторую точку дуги
838 Specify second point of mirror line Укажите вторую точку оси зеркального отражения
839 Specify second point on circle Укажите вторую точку на круге
840 Specify second point on circle: _tan to Укажите вторую точку на круге: _tan к
841 Specify second point or <use first point as displacement> Укажите вторую точку или <использовать первую точку как смещение>
842 Specify second source point Укажите вторую исходную точку
843 Specify side to offset Укажите сторону смещения
844 Specify start angle Укажите начальный угол
845 Specify start point Укажите начальную точку
846 Specify start point of arc Укажите начальную точку дуги
847 Specify start point of polyline Укажите начальную точку полилинии
848 Specify start point of text Укажите начальную точку текста
849 Specify starting width Укажите начальную ширину
850 Specify starting width for next segment Укажите начальную ширину следующего сегмента
851 Specify tangent direction for the start point of arc Укажите направление касательной в начальной точке дуги
852 Specify the elevation for rectangles Укажите уровень прямоугольников
853 Specify thickness for rectangles Укажите высоту выдавливания прямоугольников
854 Specify third point on circle Укажите третью точку на круге
855 Specify third point on circle: _tan to Укажите третью точку на круге: _tan к
856 Specify third source point or <continue> Укажите третью исходную точку или <продолжить>
857 Specify through point Укажите точку, через которую проходит прямая
858 Specify top diameter Укажите диаметр вершины
859 Specify top radius or Укажите радиус вершины или
860 Specify total angle Укажите полный угол
861 Specify total length or Укажите общую длину или
862 Specify width for rectangles Укажите ширину прямоугольников
863 Sphere Сфера
864 Spline Сплайн
865 Stack (type 1/2 then select) Дробь (введите 1/2, затем выберите)
866 Standard Стандартный
867 Start Начало
868 Start Width Начальная ширина
869 Start X Начало X
870 Start Y Начало Y
871 Start angle Начальный угол
872 Start, Center, Angle Начало, центр, угол
873 Start, Center, End Начало, центр, конец
874 Start, Center, Length Начало, центр, длина
875 Start, End, Angle Начало, конец, угол
876 Start, End, Direction Начало, конец, направление
877 Start, End, Radius Начало, конец, радиус
878 Status Состояние
879 Status Bar Строка состояния
880 Stretch Растянуть
881 Style: Standard Стиль: Standard
882 Subtract Вычесть
883 Sweep Тело по траектории
884 Switch Drawing Переключить чертеж
885 Switch Layout Переключить лист
886 Switch between model space in a viewport and paper space Переключить между пространством модели в видовом экране и пространством листа
887 Symmetric Симметрия
888 Synchronize Attributes Синхронизировать атрибуты
889 Toggle PICKADD (off: each pick replaces the selection) Переключить PICKADD (выкл.: каждый выбор заменяет набор выделения)
890 Toggle PICKADD (on: picks add to the selection) Переключить PICKADD (вкл.: выделение добавляется к набору)
891 TOP СВЕРХУ
892 Table Таблица
893 Table Style... Стиль таблиц...
894 Table style Стиль таблиц
895 Table... Таблица...
896 Tan, Tan, Radius Касательная, касательная, радиус
897 Tan, Tan, Tan Касательная, касательная, касательная
898 Tangent Касательность
899 Text Текст
900 Text Editor Текстовый редактор
901 Text Height Высота текста
902 Text Style... Текстовый стиль...
903 Text height Высота текста
904 Text override Переопределение текста
905 Text style Текстовый стиль
906 Thaw All Layers Разморозить все слои
907 There is nothing here yet. Здесь пока ничего нет.
908 Thickness Высота выдавливания
909 Time Время
910 To get started, create or open a drawing. Чтобы начать, создайте или откройте чертеж.
911 Tool Bar Панель инструментов
912 Tool Sets Наборы инструментов
913 Tools Инструменты
914 Torus Тор
915 Trace Широкая линия
916 Transparency Прозрачность
917 Trim Обрезать
918 True Color Цвет RGB
919 Turn All Layers On Включить все слои
920 Turn Off Layer Выключить слой
921 Turn Quick Properties off (QPMODE) Выключить быстрые свойства (QPMODE)
922 Type Тип
923 Type a command Введите команду
924 Type text; Enter starts a new paragraph Введите текст; Enter начинает новый абзац
925 UCS Icon Значок ПСК
926 Undo Отменить
927 Undo (⌘Z) Отменить (⌘Z)
928 Ungroup Разгруппировать
929 Union Объединение
930 Unisolate Layer Отменить изоляцию слоев
931 Units... Единицы...
932 Unlock Layer Разблокировать слой
933 Unmerge Cells Разъединить ячейки
934 Unsaved Layer State Несохраненное состояние слоев
935 Up Вверх
936 Update Обновить
937 Use System Setting Как в системе
938 User Пользователь
939 User Parameters Пользовательские параметры
940 Username Имя пользователя
941 VP Color Цвет ВЭ
942 VP Freeze Заморозка ВЭ
943 Value Значение
944 Value: Значение:
945 Version Версия
946 Version {version} Версия {version}
947 Vertical Вертикальность
948 VerticalDistance ВертикальноеРасстояние
949 View Вид
950 ViewCube Видовой куб
951 Viewport Видовой экран
952 Viewport Layer Freeze Заморозить слой в видовом экране
953 Viewport Properties Свойства видового экрана
954 Viewport {id} Видовой экран {id}
955 Viewports Видовые экраны
956 W З
957 WCS ⌄ МСК ⌄
958 White Белый
959 Width factor Коэффициент ширины
960 Window Окно
961 Wipeout Маскировка
962 Workspace Switching Переключение рабочих сред
963 Write Block Записать блок в файл
964 Xline Вспомогательная прямая
965 Yellow Желтый
966 Yes Да
967 Zoom Показать
968 Zoom All Показать все
969 Zoom Extents Показать границы объектов
970 Zoom In Увеличить
971 Zoom Object Показать объект
972 Zoom Out Уменьшить
973 Zoom Previous Предыдущий масштаб
974 Zoom Window Показать рамкой
975 error ошибка
976 {contributors} · {commits} {contributors} · {commits}
977 {kind} (constraint {id}) {kind} (ограничение {id})
978 {kind} constraint {name} = {expression} (double-click to edit) Ограничение «{kind}» {name} = {expression} (дважды щелкните для редактирования)
979 {name}: {distance} < {angle}° {name}: {distance} < {angle}°
980 ƒx New User Parameter ƒx Новый пользовательский параметр
981 ΔLOC Δстрок
982 −Bin −двоич.
983 −LOC −строк
984 ⌘↩ to finish ⌘↩ для завершения
985 @plural {n} commit {n} коммит|{n} коммита|{n} коммитов
986 @plural {n} contributor {n} участник|{n} участника|{n} участников
987 Drawing as bytes Чертеж как байты
988 Services Службы
989 Hide CADCraft Скрыть CADCraft
990 Hide Others Скрыть остальные
991 Accessibility Специальные возможности
992 Show System Cursor Показывать системный курсор
993 Import... Импорт...
994 Export... Экспорт...
995 Batch Publish... Пакетная публикация...
996 Hatch Pattern Образец штриховки
997 Search for pattern Поиск образца
+157 -4
View File
@@ -15,7 +15,7 @@ fn catalog_is_valid_and_preserves_placeholders_and_ellipses() {
let (catalog, errors) = parse(language.source);
assert!(errors.is_empty(), "{}: {errors:?}", language.code);
for ((context, key), value) in catalog {
assert!(["", "@plural"].contains(&context), "unknown context: {context}");
assert!(["", "menu", "@plural"].contains(&context), "unknown context: {context}");
let forms: Vec<_> = if context == "@plural" { value.split('|').collect() } else { vec![value] };
if context == "@plural" {
assert_eq!(forms.len(), language.plural_forms, "{}: {key}", language.code);
@@ -39,7 +39,7 @@ fn catalog_is_valid_and_preserves_placeholders_and_ellipses() {
/// to English for what they don't cover yet; their gaps are reported, not enforced, so adding a
/// command doesn't require a row in every language.
const REQUIRED: &[&str] = &["uk"];
const REPORTED: &[&str] = &["pt-BR", "de", "es"];
const REPORTED: &[&str] = &["pt-BR", "de", "es", "cs", "ru", "fr", "zh"];
fn check(language: &str, what: &str, missing: Vec<String>) {
if REQUIRED.contains(&language) {
@@ -94,6 +94,7 @@ fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
include_str!("../cmdline.rs"),
include_str!("../credits.rs"),
include_str!("../palettes/quickcalc.rs"),
include_str!("../palettes/patterns.rs"),
include_str!("../dialogs.rs"),
include_str!("../layers.rs"),
include_str!("../canvas/compare.rs"),
@@ -101,6 +102,7 @@ fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
include_str!("../parametric.rs"),
include_str!("../quick.rs"),
include_str!("../vpscale.rs"),
include_str!("../options/dialog.rs"),
];
let mut missing = BTreeSet::new();
for source in sources {
@@ -145,14 +147,18 @@ fn locale_and_saved_preference_resolution() {
for tag in ["de", "de-DE", "de_DE.UTF-8", "DE-at", "de-CH"] {
assert_eq!(locale_language(tag), Some("de"), "{tag}");
}
for tag in ["ua", "ukulele", "ru-RU", "", "garbage", "💙"] {
for (tag, code) in [("ru", "ru"), ("ru-RU", "ru"), ("ru_RU.UTF-8", "ru"), ("fr", "fr"), ("fr-FR", "fr"), ("fr_CA.UTF-8", "fr")] {
assert_eq!(locale_language(tag), Some(code), "{tag}");
}
for tag in ["ua", "ukulele", "pl-PL", "", "garbage", "💙"] {
assert_eq!(locale_language(tag), None, "{tag}");
}
for tag in ["pt", "pt-BR", "pt_BR.UTF-8", "PT-br", "pt-PT"] {
assert_eq!(locale_language(tag), Some("pt-BR"), "{tag}");
}
assert_eq!(locale_language("ptolemy"), None);
assert_eq!(Preference::Auto.resolve(&["fr-FR".into(), "pt-BR".into()]), "pt-BR");
assert_eq!(Preference::Auto.resolve(&["pl-PL".into(), "pt-BR".into()]), "pt-BR");
assert_eq!(Preference::Auto.resolve(&["fr-FR".into(), "pt-BR".into()]), "fr");
for tag in ["es", "es-ES", "es_MX.UTF-8", "ES-ar"] {
assert_eq!(locale_language(tag), Some("es"), "{tag}");
}
@@ -388,3 +394,150 @@ fn default_fonts_cover_every_ukrainian_letter_in_the_catalog() {
}
});
}
#[test]
fn czech_locales_preferences_and_plural_forms() {
for tag in ["cs", "cs-CZ", "cs_CZ.UTF-8", "CS-cz", " cs-CZ ", "cs_CZ@euro"] {
assert_eq!(locale_language(tag), Some("cs"), "{tag}");
}
for tag in ["cz", "csharp", "csomething"] {
assert_eq!(locale_language(tag), None, "{tag}");
}
assert_eq!(Preference::parse("cs"), Some(Preference::Czech));
assert_eq!(Preference::Czech.name(), "Čeština");
assert_eq!(Preference::Czech.resolve(&["en-US".into()]), "cs");
assert_eq!(Preference::Auto.resolve(&["pl-PL".into(), "cs-CZ".into(), "en-US".into()]), "cs");
let mut app = CadApp::new(Session::new(), Services::default());
app.ui.interface_language = Preference::Czech;
let mut restored = CadApp::new(Session::new(), Services::default());
restored.load_prefs(&app.prefs_json());
assert_eq!(restored.ui.interface_language, Preference::Czech);
with_language("cs", || {
for (n, form) in [(0, 2), (1, 0), (2, 1), (4, 1), (5, 2), (11, 2), (21, 2), (22, 2), (25, 2), (u64::MAX, 2)] {
assert_eq!(plural_cs(n), form, "{n}");
let contributor = ["přispěvatel", "přispěvatelé", "přispěvatelů"][form];
let commit = ["commit", "commity", "commitů"][form];
assert_eq!(count(n, "{n} contributor", "{n} contributors"), format!("{n} {contributor}"));
assert_eq!(count(n, "{n} commit", "{n} commits"), format!("{n} {commit}"));
}
assert_eq!(format("Current: {color}", &[("color", "{n} Open".into())]), "Aktuální: {n} Open");
});
}
#[test]
fn czech_menu_headings_keep_object_labels_and_other_catalogs() {
with_language("cs", || {
assert_eq!(menu("View"), "Zobrazení");
assert_eq!(t("View"), "Pohled");
assert_eq!(menu("Dimension"), "Kótování");
assert_eq!(t("Dimension"), "Kóta");
assert_eq!(menu("Draw"), "Kreslení");
assert_eq!(menu("Modify"), "Modifikace");
assert_eq!(menu("Edit"), "Úpravy");
assert_eq!(menu("File"), "Soubor");
});
for language in ["en", "uk", "de", "es", "pt-BR", "unknown"] {
for source in crate::menus::MENUS {
assert_eq!(tr_menu(language, source), tr(language, source));
}
}
}
#[test]
fn live_czech_switch_preserves_drawing_command_and_saved_choice() {
let mut app = CadApp::new(Session::new(), Services::default());
app.ui.in_window_menu = true;
app.session.state_mut().unwrap().title = "Open".into();
let ctx = egui::Context::default();
app.logic(&ctx);
assert!(render(&mut app, &ctx).contains("Properties"));
app.start("line");
let prompt = crate::control::cmdline_state(&app);
let commands = crate::control::all_commands(&app);
app.run("ui.language", json!({"lang":"cs"})).unwrap();
let text = render(&mut app, &ctx);
for label in ["Vlastnosti", "Soubor", "Zobrazení", "Kreslení", "Kótování", "Určete první bod"] {
assert!(text.contains(label), "missing {label}: {text}");
}
assert_eq!(app.language(), "cs");
assert_eq!(app.session.state().unwrap().title, "Open");
assert_eq!(crate::control::cmdline_state(&app), prompt);
assert_eq!(crate::control::all_commands(&app), commands);
assert_eq!(t("Properties"), "Properties");
app.run("ui.resetpalettes", json!({})).unwrap();
assert_eq!(app.language(), "cs");
app.run("ui.language", json!({"lang":"en"})).unwrap();
assert!(render(&mut app, &ctx).contains("Properties"));
}
#[test]
fn czech_dialogs_render_with_stable_ids() {
for (dialog, heading) in [
("dsettings", "Nastavení kreslení"),
("about", "O aplikaci CADCraft"),
("commands", "Přehled příkazů"),
("blocks", "Bloky"),
("layers", "Správce vlastností hladin"),
("qselect", "Rychlý výběr"),
("parameters", "Správce parametrů"),
("language", "Čeština"),
("options:preferences", "Uživatelské předvolby"),
] {
let mut app = CadApp::new(Session::new(), Services::default());
app.ui.interface_language = Preference::Czech;
app.ui.dialog = Some(dialog.into());
let ctx = egui::Context::default();
app.logic(&ctx);
let text = render(&mut app, &ctx);
assert!(text.contains(heading), "{dialog}: {text}");
assert_eq!(app.ui.dialog.as_deref(), Some(dialog));
}
}
#[test]
fn default_fonts_cover_czech_letters_and_typographic_quotes() {
let ctx = egui::Context::default();
let mut output = ctx.run_ui(Default::default(), |_| {});
output.textures_delta.clear();
let letters: BTreeSet<_> = "ÁČĎÉĚÍŇÓŘŠŤÚŮÝŽáčďéěíňóřšťúůýž„“".chars().collect();
ctx.fonts_mut(|view| {
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
let mut font = view.fonts.font(&family);
for c in &letters {
assert!(font.characters().contains_key(c), "missing {c} in {family:?}");
}
}
});
}
#[test]
fn russian_and_french_plurals() {
with_language("ru", || {
for (n, word) in [(1, "участник"), (2, "участника"), (5, "участников"), (21, "участник"), (22, "участника"), (11, "участников")]
{
assert_eq!(count(n, "{n} contributor", "{n} contributors"), format!("{n} {word}"));
}
});
with_language("fr", || {
for (n, word) in [(0, "contributeur"), (1, "contributeur"), (2, "contributeurs"), (10, "contributeurs")] {
assert_eq!(count(n, "{n} contributor", "{n} contributors"), format!("{n} {word}"));
}
});
}
/// Chinese shows only with an installed CJK font (none is bundled); without one the interface
/// stays English instead of drawing boxes, and choosing it says why.
#[test]
fn chinese_needs_an_installed_cjk_font() {
assert_eq!(locale_language("zh-CN"), Some("zh"));
let mut app = CadApp::new(Session::new(), Services::default());
let r = app.run("ui.language", json!({"lang": "zh"})).unwrap();
assert_eq!(r["interfaceLanguage"], "zh");
let font = crate::theme::cjk_font();
assert_eq!(app.language(), if font.is_some() { "zh" } else { "en" });
assert_eq!(r["shown"], app.language());
assert!(crate::theme::can_show("uk") && crate::theme::can_show("en"));
if font.is_none() {
assert!(app.status.as_ref().is_some_and(|(s, _)| s.contains("No Chinese font")));
}
}
+34
View File
@@ -148,6 +148,7 @@
Closed Замкнений
Close Comparison Закрити порівняння
Coincident Збіг
Collapse Palettes Згорнути палітри
Collapse Tool Sets Згорнути набори інструментів
Collinear Колінеарність
Color Колір
@@ -256,6 +257,7 @@
Drawing Template (DWT) Шаблон креслення (DWT)
Drawing Utilities Утиліти креслення
Drawings (DWG, DXF) Креслення (DWG, DXF)
SVG drawings (SVG, SVGZ) Креслення SVG (SVG, SVGZ)
Dynamic Динамічно
Dynamic Dimensions Динамічні розміри
Dynamic Input Динамічне введення
@@ -621,6 +623,7 @@
Plot Друк креслення
Plot (click: do not plot) Друкувати (натисніть, щоб вимкнути друк)
Plot Preview Попередній перегляд друку
Plot Window Вікно друку
Plot style Стиль друку
Plot style attached to Стиль друку застосовано до
Plot style table Таблиця стилів друку
@@ -840,6 +843,8 @@
Show All Показати всі
Show All Constraint Bars Показати всі панелі обмежень
Show All Dynamic Dimensions Показати всі динамічні розміри
Show and hide the tools of this category Показати й сховати інструменти цієї категорії
Show in Tool Sets Показувати в наборах інструментів
Show command prompting near the crosshairs Показувати запити команд біля перехрестя
Show Layer List Показати список шарів
Show/Hide Lineweight Показати/приховати товщину ліній
@@ -1200,3 +1205,32 @@
Multiline... Мультилінія...
Multiline Style... Стиль мультилінії...
Solid Editing Редагування тіл
3D Mouse... 3D-миша...
3D Mouse 3D-миша
Calibrate 3D Mouse Калібрувати 3D-мишу
3D Mouse Navigation Навігація за допомогою 3D-миші
3D mouse input is unavailable on this platform Введення з 3D-миші недоступне на цій платформі
No supported 3D mouse found Сумісну 3D-мишу не знайдено
Release the 3D mouse to calibrate its resting position Відпустіть 3D-мишу, щоб відкалібрувати її положення спокою
3D mouse connected 3D-мишу підключено
3D mouse busy, possibly 3DxWare or missing device permissions 3D-миша зайнята, можливо, через 3DxWare або недостатні права доступу до пристрою
3D mouse disconnected 3D-мишу відключено
3D mouse input could not be started Не вдалося запустити введення з 3D-миші
Enable 3D mouse navigation Увімкнути навігацію 3D-мишею
Pan sensitivity Чутливість панорамування
Zoom sensitivity Чутливість масштабування
Reverse zoom direction Змінити напрямок масштабування
Calibrate Калібрувати
Move sideways to pan; push/pull or twist to zoom. Рухайте вбік для панорамування; натискайте, тягніть або обертайте для масштабування.
Left device button: zoom extents. Right device button: calibrate. Ліва кнопка пристрою: масштабування до меж. Права кнопка: калібрування.
More toolbar commands Інші команди панелі інструментів
Default save version Типова версія збереження
Automatic save every Автозбереження кожні
min хв
Automatic save file location Папка файлів автозбереження
Drawings with no file yet are saved in the default version. 0 minutes turns automatic save off. Креслення, що ще не мають файлу, зберігаються в типовій версії. 0 хвилин вимикає автозбереження.
Time-sensitive right-click: a quick click is Enter, holding opens the menu Клік правою кнопкою з урахуванням часу: швидкий клік — Enter, утримання відкриває меню
Turned off, a right-click while a command runs always opens the shortcut menu. Якщо вимкнено, клік правою кнопкою під час команди завжди відкриває контекстне меню.
Hatch Pattern Зразок штрихування
Search for pattern Пошук зразка
Run Script... Виконати сценарій...
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
148
149
150
151
152
153
154
257
258
259
260
261
262
263
623
624
625
626
627
628
629
843
844
845
846
847
848
849
850
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
File diff suppressed because it is too large Load Diff
+39 -10
View File
@@ -19,6 +19,7 @@ pub mod control;
pub mod credits;
pub mod dialogs;
pub mod docking;
pub mod spacemouse;
// The External References palette.
pub mod xrefs;
@@ -77,6 +78,9 @@ pub struct UiState {
/// "Drafting" or "Modeling".
pub toolset_tab: String,
pub collapsed_groups: Vec<String>,
/// Tools the user took out of the Tool Sets palette (command ids); each category's gear
/// menu puts them back.
pub hidden_tools: Vec<String>,
pub properties_all: bool,
/// Show an in-window menu bar (platforms without a native menu bar, or on request).
pub in_window_menu: bool,
@@ -150,6 +154,7 @@ impl Default for UiState {
system_cursor: std::env::var("CADCRAFT_SYSTEM_CURSOR").is_ok_and(|v| !matches!(v.trim(), "" | "0" | "false" | "off")),
toolset_tab: "Drafting".into(),
collapsed_groups: Vec::new(),
hidden_tools: Vec::new(),
properties_all: true,
in_window_menu: !cfg!(target_os = "macos"),
start_tab: false,
@@ -177,6 +182,8 @@ pub struct CadApp {
pub options: options::UiOptions,
pub services: Services,
pub canvas: canvas::CanvasState,
/// Desktop HID status and navigation preferences; the web host supplies no HID backend.
pub spacemouse: spacemouse::State,
pub cmd: cmdline::CmdLine,
pub status: Option<(String, f64)>,
pub integrated_titlebar: bool,
@@ -188,7 +195,8 @@ pub struct CadApp {
/// Caps the frame rate when presenting without vsync (see [`gpu::FrameCap`]).
#[cfg(not(target_arch = "wasm32"))]
pub frame_cap: Option<gpu::FrameCap>,
styled: bool,
/// Interface fonts installed, and whether with a CJK font (`None` until the first frame).
fonts_cjk: Option<bool>,
/// The theme last installed into egui (`None` until the first frame).
shown_theme: Option<egui::Theme>,
/// Whether the last applied theme was handed to egui as the System preference.
@@ -205,6 +213,10 @@ pub struct CadApp {
pub autosave: Option<cadcraft_engine::autosave::Autosaver>,
/// Drawing Recovery: autosave files a crashed app left behind.
pub recovery: autosave::Recovery,
/// GPU errors and device loss reported by wgpu ([`gpu::GpuFaults`]).
pub gpu_faults: std::sync::Arc<gpu::GpuFaults>,
/// The notice shown after one (see [`autosave::gpu_notice`]).
pub gpu_notice: Option<autosave::GpuNotice>,
}
impl CadApp {
@@ -216,6 +228,7 @@ impl CadApp {
options: options::UiOptions::default(),
services,
canvas: canvas::CanvasState::default(),
spacemouse: spacemouse::State::default(),
cmd: cmdline::CmdLine::default(),
status: None,
integrated_titlebar: false,
@@ -226,7 +239,7 @@ impl CadApp {
synthetic: Vec::new(),
#[cfg(not(target_arch = "wasm32"))]
frame_cap: None,
styled: false,
fonts_cjk: None,
shown_theme: None,
shown_follows_system: false,
shown_flavor: None,
@@ -236,17 +249,22 @@ impl CadApp {
closing: None,
autosave: None,
recovery: autosave::Recovery::default(),
gpu_faults: std::sync::Arc::default(),
gpu_notice: None,
}
}
/// The interface language shown: the chosen one, or English when this computer has no font
/// for it (Chinese without an installed CJK font would be boxes).
pub fn language(&self) -> &'static str {
self.ui.interface_language.resolve(&self.system_languages)
let l = self.ui.interface_language.resolve(&self.system_languages);
if theme::can_show(l) { l } else { "en" }
}
/// Draw the canvas on the GPU with the app's wgpu render state (eframe's
/// `CreationContext::wgpu_render_state`). Without it the canvas draws on the CPU.
pub fn set_wgpu(&mut self, rs: &egui_wgpu::RenderState) {
self.canvas.gpu = Some(gpu::install(rs));
self.canvas.gpu = Some(gpu::install(rs, &self.gpu_faults));
}
pub fn with_control(mut self, rx: Receiver<ControlRequest>) -> Self {
@@ -306,18 +324,24 @@ impl CadApp {
/// Preferences kept across restarts, as JSON for the host's storage ([`PREFS_KEY`]).
pub fn prefs_json(&self) -> String {
json!({
let mut prefs = json!({
"theme": self.ui.theme.as_str(),
"interfaceLanguage": self.ui.interface_language.code(),
"saveFormat": self.ui.save_format.as_str(),
"saveVersion": self.ui.save_version,
})
.to_string()
});
if self.spacemouse.settings != spacemouse::Settings::default() {
prefs["spacemouse"] = json!(self.spacemouse.settings);
}
prefs.to_string()
}
/// Restore preferences saved by [`Self::prefs_json`]; unknown or malformed values are ignored.
pub fn load_prefs(&mut self, json: &str) {
let Ok(v) = serde_json::from_str::<Value>(json) else { return };
if let Some(prefs) = v.get("spacemouse") {
let _ = self.spacemouse.load_settings(prefs);
}
if let Some(t) = v.get("theme").and_then(Value::as_str).and_then(theme::ThemePref::parse) {
self.ui.theme = t;
}
@@ -392,9 +416,10 @@ impl CadApp {
if ctx.input(|i| i.viewport().close_requested()) && self.request_quit() {
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
}
if !self.styled {
theme::install_fonts(ctx);
self.styled = true;
let cjk = self.language().starts_with("zh");
if self.fonts_cjk != Some(cjk) {
theme::install_fonts(ctx, cjk);
self.fonts_cjk = Some(cjk);
}
// Resolved every frame, so a System choice follows the OS appearance live (winit and the
// browser report changes as input); the saved choice itself never changes here.
@@ -416,6 +441,8 @@ impl CadApp {
}
self.collect_screenshots(ctx);
self.issue_screenshots(ctx);
self.gpu_fault_tick(ctx);
managers::plot_window_resume(self, ctx);
self.autosave_tick(ctx);
menus::shortcuts(self, ctx);
#[cfg(not(target_arch = "wasm32"))]
@@ -481,8 +508,10 @@ impl CadApp {
}
});
dialogs::show(self, ui.ctx());
palettes::patterns::prompt_picker(self, ui.ctx());
closing::prompt(self, ui.ctx());
autosave::show(self, ui.ctx());
autosave::gpu_notice(self, ui.ctx());
self.frame_ms = now_ms() - t0;
}
+257 -27
View File
@@ -26,7 +26,7 @@ pub fn route(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value,
let dialog = match id {
"battman" => "battman",
"linetype" | "lt" | "ltype" => "linetype",
"ddptype" => "ptype",
"ddptype" | "ptype" => "ptype",
"style" | "st" => "textstyle",
"tablestyle" | "ts" => "tablestyle",
"mleaderstyle" | "mls" => "mleaderstyle",
@@ -484,6 +484,7 @@ fn page_setup(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action
.or_else(|| layouts.first().map(|l| l.1.clone()))
.unwrap_or_default();
let mut action = None;
let choices = PageChoices::new(ctx, d);
egui::Window::new("Page Setup Manager").open(open).default_size(vec2(560.0, 300.0)).resizable(true).show(ctx, |ui| {
if layouts.is_empty() {
ui.label("This drawing has no layouts.");
@@ -504,7 +505,7 @@ fn page_setup(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action
// Layout plotting always prints the whole sheet, so only that area is offered.
let rows = PageRows { areas: &["layout"], scale: true, center: true };
egui::Grid::new("pagesetup_grid").num_columns(2).spacing(vec2(10.0, 6.0)).show(right, |ui| {
if let Some((k, v)) = page_rows(ui, egui::Id::new(("pagesetup", name)), page, &rows) {
if let Some((k, v)) = page_rows(ui, egui::Id::new(("pagesetup", name)), page, &rows, &choices) {
action = Some(("pagesetup", json!({ "layout": name, k: v })));
}
});
@@ -524,6 +525,43 @@ struct PageRows {
center: bool,
}
/// The devices and plot style tables the page-setup rows offer.
struct PageChoices {
devices: std::sync::Arc<Vec<cadcraft_doc::PlotterConfig>>,
styles: Vec<String>,
}
impl PageChoices {
fn new(ctx: &egui::Context, d: &cadcraft_doc::Drawing) -> Self {
PageChoices { devices: device_choices(ctx), styles: cadcraft_doc::plot_style_table_names(d) }
}
}
/// The plotter devices (built-in, then the user's files), read again at most every 2 s: the
/// dialogs ask every frame and user devices are files.
fn device_choices(ctx: &egui::Context) -> std::sync::Arc<Vec<cadcraft_doc::PlotterConfig>> {
type Cached = (f64, std::sync::Arc<Vec<cadcraft_doc::PlotterConfig>>);
let now = crate::now_ms();
if let Some((at, list)) = temp::<Cached>(ctx, "plot_devices")
&& now - at < 2000.0
{
return list;
}
let list = std::sync::Arc::new(cadcraft_engine::cmd::plotters::devices());
set_temp::<Cached>(ctx, "plot_devices", (now, list.clone()));
list
}
/// A page setup's device, or `"None"` (plot to PDF on the page's paper).
fn device_label(page: &cadcraft_doc::PageSetup) -> &str {
if page.device.trim().is_empty() { "None" } else { page.device.as_str() }
}
/// A page setup's plot style table, or `"None"`.
fn style_label(page: &cadcraft_doc::PageSetup) -> &str {
if cadcraft_doc::no_plot_style_table(&page.plot_style_table) { "None" } else { page.plot_style_table.as_str() }
}
fn area_label(a: &str) -> &str {
match a {
"layout" => "Layout",
@@ -544,17 +582,51 @@ fn parse_ratio(t: &str) -> Option<f64> {
(v.is_finite() && v > 0.0).then_some(v)
}
/// The page-setup rows (paper, orientation, plot area, fit, scale, center, lineweights) shared by
/// the Page Setup Manager and the Plot dialog, inside a two-column grid. Returns the setting the
/// user changed as a `pagesetup` parameter (`paper`, `landscape`, `plotArea`, `scaleToFit`,
/// `scale`, `center` or `lineweights`).
fn page_rows(ui: &mut egui::Ui, salt: egui::Id, page: &cadcraft_doc::PageSetup, rows: &PageRows) -> Option<(&'static str, Value)> {
/// The page-setup rows (device, paper, orientation, plot area, fit, scale, center, lineweights,
/// plot style table) shared by the Page Setup Manager and the Plot dialog, inside a two-column
/// grid. Returns the setting the user changed as a `pagesetup` parameter (`device`, `paper`,
/// `landscape`, `plotArea`, `scaleToFit`, `scale`, `center`, `lineweights` or `plotStyleTable`).
fn page_rows(
ui: &mut egui::Ui,
salt: egui::Id,
page: &cadcraft_doc::PageSetup,
rows: &PageRows,
choices: &PageChoices,
) -> Option<(&'static str, Value)> {
let mut change = None;
ui.label("Printer/plotter");
egui::ComboBox::from_id_salt(salt.with("device")).selected_text(device_label(page)).width(190.0).show_ui(ui, |ui| {
if ui.selectable_label(device_label(page) == "None", "None").on_hover_text("PDF on the paper chosen below").clicked()
&& device_label(page) != "None"
{
change = Some(("device", json!("None")));
}
for dev in choices.devices.iter() {
let on = dev.name.eq_ignore_ascii_case(&page.device);
if ui.selectable_label(on, &dev.name).on_hover_text(format!("{} ({})", dev.description, dev.output.as_str().to_uppercase())).clicked()
&& !on
{
change = Some(("device", json!(dev.name)));
}
}
});
ui.end_row();
// The device's papers, else the standard sizes.
let device = choices.devices.iter().find(|dv| dv.name.eq_ignore_ascii_case(&page.device));
ui.label("Paper size");
egui::ComboBox::from_id_salt(salt.with("paper")).selected_text(page.paper.as_str()).width(190.0).show_ui(ui, |ui| {
for p in cadcraft_render::PAPER_SIZES {
if ui.selectable_label(p.name == page.paper, p.name).clicked() && p.name != page.paper {
change = Some(("paper", json!(p.name)));
egui::ComboBox::from_id_salt(salt.with("paper")).selected_text(page.paper.as_str()).width(190.0).show_ui(ui, |ui| match device {
Some(dev) => {
for p in &dev.papers {
if ui.selectable_label(p.name == page.paper, &p.name).clicked() && p.name != page.paper {
change = Some(("paper", json!(p.name)));
}
}
}
None => {
for p in cadcraft_render::PAPER_SIZES {
if ui.selectable_label(p.name == page.paper, p.name).clicked() && p.name != page.paper {
change = Some(("paper", json!(p.name)));
}
}
}
});
@@ -611,19 +683,42 @@ fn page_rows(ui: &mut egui::Ui, salt: egui::Id, page: &cadcraft_doc::PageSetup,
change = Some(("lineweights", json!(lw)));
}
ui.end_row();
ui.label("Plot style table");
egui::ComboBox::from_id_salt(salt.with("styles")).selected_text(style_label(page)).width(190.0).show_ui(ui, |ui| {
if ui.selectable_label(style_label(page) == "None", "None").clicked() && style_label(page) != "None" {
change = Some(("plotStyleTable", json!("None")));
}
for name in &choices.styles {
let on = name.eq_ignore_ascii_case(&page.plot_style_table);
if ui.selectable_label(on, name).clicked() && !on {
change = Some(("plotStyleTable", json!(name)));
}
}
});
ui.end_row();
change
}
/// Apply a [`page_rows`] change to a page setup held by a dialog.
/// Device and paper changes follow PAGESETUP's rules (`plotters::page_device`): a device brings
/// its papers, orientation and printable areas.
fn apply_page_change(page: &mut cadcraft_doc::PageSetup, key: &str, v: &Value) {
use cadcraft_engine::cmd::plotters::page_device;
match key {
"device" => {
let _ = page_device("plot", &json!({ "device": v }), page);
}
"paper" => {
if let Some(ps) = v.as_str().and_then(cadcraft_render::paper_size) {
// A device's paper, else a standard size (no device).
if !page_device("plot", &json!({ "paper": v }), page).unwrap_or(false)
&& let Some(ps) = v.as_str().and_then(cadcraft_render::paper_size)
{
page.paper = ps.name.into();
page.width_mm = ps.width_mm;
page.height_mm = ps.height_mm;
}
}
"plotStyleTable" => page.plot_style_table = v.as_str().unwrap_or_default().chars().take(260).collect(),
"plotArea" => page.plot_area = v.as_str().unwrap_or("extents").into(),
"scale" => page.scale = v.as_f64().filter(|s| s.is_finite() && *s > 0.0).unwrap_or(page.scale),
_ => {
@@ -644,6 +739,31 @@ fn apply_page_change(page: &mut cadcraft_doc::PageSetup, key: &str, v: &Value) {
/// Bumped each time PLOT opens its dialog, so it starts again from the page setup.
static PLOT_GEN: AtomicU32 = AtomicU32::new(0);
/// Set while PLOTWINDOW picks the Plot dialog's window ("Window <").
const PICKING_WINDOW: &str = "plot_picking_window";
/// Set by Print... in a preview opened from the Plot dialog: plot as soon as the dialog is back.
pub(crate) const PLOT_NOW: &str = "plot_now";
/// Every frame: once PLOTWINDOW (started by the Plot dialog's "Window <") ends, bring the dialog
/// back with its settings, plotting the picked window (unchanged if the pick was cancelled).
pub fn plot_window_resume(app: &mut CadApp, ctx: &egui::Context) {
if temp::<bool>(ctx, PICKING_WINDOW) != Some(true) || app.session.running.is_some() {
return;
}
set_temp(ctx, PICKING_WINDOW, false);
if let (Some(w), Some(mut st)) = (app.session.plot_window.take(), temp::<PlotState>(ctx, "plot_state")) {
st.window = [w.min.x, w.min.y, w.max.x, w.max.y];
st.page.plot_area = "window".into();
set_temp(ctx, "plot_state", st);
}
app.ui.dialog = Some("plot".into());
}
/// Set a flag the Plot dialog reads (see [`PLOT_NOW`]).
pub(crate) fn set_flag(ctx: &egui::Context, id: &str, on: bool) {
set_temp(ctx, id, on);
}
/// Plot areas for model space (a layout always plots its sheet).
const MODEL_AREAS: &[&str] = &["extents", "display", "limits", "window"];
@@ -677,8 +797,9 @@ fn plot_page(d: &cadcraft_doc::Drawing, target: &str) -> cadcraft_doc::PageSetup
page
}
/// The `plot` parameters for the dialog's settings (without the output path).
fn plot_params(st: &PlotState) -> Value {
/// The `plot` parameters for the dialog's settings (without the output path). `preview_dpi`
/// asks for a PNG at that resolution instead of the device's output, on the same sheet.
fn plot_params(st: &PlotState, preview_dpi: Option<f64>) -> Value {
let page = &st.page;
let mut p = json!({
"layout": st.target,
@@ -687,11 +808,33 @@ fn plot_params(st: &PlotState) -> Value {
"lineweights": page.lineweights,
});
if let Some(o) = p.as_object_mut() {
if cadcraft_render::paper_size(&page.paper).is_some() {
o.insert("paper".into(), json!(page.paper));
} else {
o.insert("width".into(), json!(page.width_mm));
o.insert("height".into(), json!(page.height_mm));
let device = device_label(page) != "None";
match preview_dpi {
// The page's own sheet, so a device's paper and printable area show as they plot.
Some(dpi) => {
o.insert("device".into(), json!("None"));
o.insert("output".into(), json!("png"));
o.insert("resolution".into(), json!(dpi));
o.insert("width".into(), json!(page.width_mm));
o.insert("height".into(), json!(page.height_mm));
o.insert("margins".into(), json!(page.margins_mm));
}
None if device => {
o.insert("device".into(), json!(page.device));
o.insert("paper".into(), json!(page.paper));
}
None if cadcraft_render::paper_size(&page.paper).is_some() => {
o.insert("device".into(), json!("None"));
o.insert("paper".into(), json!(page.paper));
}
None => {
o.insert("device".into(), json!("None"));
o.insert("width".into(), json!(page.width_mm));
o.insert("height".into(), json!(page.height_mm));
}
}
if style_label(page) != "None" {
o.insert("plotStyleTable".into(), json!(page.plot_style_table));
}
if st.target == "Model" {
o.insert("plotArea".into(), json!(page.plot_area));
@@ -732,12 +875,13 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
PlotState { generation, page: plot_page(d, &target), target, window: [ext.min.x, ext.min.y, ext.max.x, ext.max.y] }
});
let model = st.target == "Model";
let (mut ok, mut cancel, mut action) = (false, false, None);
let choices = PageChoices::new(ctx, d);
// Print... in a preview opened from here plots with these settings.
let mut ok = temp::<bool>(ctx, PLOT_NOW) == Some(true);
set_temp(ctx, PLOT_NOW, false);
let (mut cancel, mut action, mut pick, mut preview) = (false, None, false, false);
egui::Window::new("Plot").open(open).collapsible(false).resizable(false).show(ctx, |ui| {
egui::Grid::new("plot_target").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label("Printer/plotter");
ui.label("PDF (vector)").on_hover_text("CADCraft plots to vector PDF files");
ui.end_row();
ui.label("Layout");
egui::ComboBox::from_id_salt("plot_layout").selected_text(st.target.as_str()).width(190.0).show_ui(ui, |ui| {
for n in &targets {
@@ -757,7 +901,7 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
} else {
PageRows { areas: &["layout"], scale: false, center: false }
};
if let Some((k, v)) = page_rows(ui, egui::Id::new(("plot", st.target.as_str())), &st.page, &rows) {
if let Some((k, v)) = page_rows(ui, egui::Id::new(("plot", st.target.as_str())), &st.page, &rows, &choices) {
apply_page_change(&mut st.page, k, &v);
}
if model && st.page.plot_area == "window" {
@@ -776,6 +920,9 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
});
ui.end_row();
}
ui.label("");
pick = ui.button("Window <").on_hover_text("Pick the window's corners in the drawing").clicked();
ui.end_row();
}
});
sheet_sketch(ui, &st.page, &t);
@@ -789,22 +936,42 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
json!({ "layout": st.target, "paper": page.paper, "landscape": page.landscape, "scaleToFit": page.scale_to_fit, "lineweights": page.lineweights }),
));
}
preview = ui.button("Preview").on_hover_text("The sheet as these settings plot it").clicked();
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
cancel = ui.button("Cancel").clicked();
ok = ui.button("OK").clicked();
ok |= ui.button("OK").clicked();
});
});
});
// The output file type follows the device.
let ext = choices.devices.iter().find(|dv| dv.name.eq_ignore_ascii_case(&st.page.device)).map_or("pdf", |dv| dv.output.as_str());
set_temp(ctx, "plot_state", st.clone());
if cancel {
*open = false;
}
if pick {
// The dialog comes back with these settings and the picked window (`plot_window_resume`).
set_temp(ctx, PICKING_WINDOW, true);
*open = false;
app.start("plotwindow");
return action;
}
if preview {
// A sheet about 1600 pixels on its long side.
let long_in = st.page.width_mm.max(st.page.height_mm).max(1.0) / 25.4;
let mut info = format!("{} · {}", st.target, st.page.paper);
if st.page.landscape {
info.push_str(" · landscape");
}
crate::exchange::preview_plot(app, ctx, plot_params(&st, Some((1600.0 / long_in).clamp(50.0, 300.0))), info);
return action;
}
if ok {
let mut params = plot_params(&st);
let mut params = plot_params(&st, None);
if let Some(pick) = app.services.pick_save.as_ref() {
let title = app.session.state().map(|s| s.title.clone()).unwrap_or_default();
let stem = std::path::Path::new(&title).file_stem().map(|s| s.to_string_lossy().to_string()).filter(|s| !s.is_empty());
let suggested = format!("{}-{}.pdf", stem.unwrap_or_else(|| "Drawing".into()), st.target);
let suggested = format!("{}-{}.{ext}", stem.unwrap_or_else(|| "Drawing".into()), st.target);
// Cancelling the file dialog keeps the Plot dialog open with its settings.
let Some(path) = pick(&suggested) else { return action };
if let Some(o) = params.as_object_mut() {
@@ -910,3 +1077,66 @@ fn constraint_settings(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -
});
action
}
#[cfg(test)]
mod plot_dialog_tests {
use super::*;
use cadcraft_engine::Session;
fn state(target: &str) -> PlotState {
let d = cadcraft_doc::Drawing::new_metric();
PlotState { generation: 0, page: plot_page(&d, target), target: target.into(), window: [0.0, 0.0, 10.0, 10.0] }
}
#[test]
fn a_device_brings_its_papers_and_the_plot_names_them() {
let mut st = state("Model");
apply_page_change(&mut st.page, "device", &json!("CADCraft PNG"));
assert_eq!(st.page.device, "CADCraft PNG");
let dev = cadcraft_engine::cmd::plotters::find_device("CADCraft PNG").unwrap();
assert!(dev.papers.iter().any(|p| p.name == st.page.paper), "a paper of the device: {}", st.page.paper);
assert!(st.page.margins_mm.iter().any(|m| *m > 0.0), "the paper's printable area");
apply_page_change(&mut st.page, "plotStyleTable", &json!("monochrome.ctb"));
let p = plot_params(&st, None);
assert_eq!((p["device"].as_str(), p["paper"].as_str()), (Some("CADCraft PNG"), Some(st.page.paper.as_str())));
assert_eq!(p["plotStyleTable"], json!("monochrome.ctb"));
// The preview plots the same sheet to PNG, whatever the device.
let q = plot_params(&st, Some(150.0));
assert_eq!((q["output"].as_str(), q["device"].as_str()), (Some("png"), Some("None")));
assert_eq!(q["width"].as_f64(), Some(st.page.width_mm));
assert_eq!(q["margins"], json!(st.page.margins_mm));
// Back to no device: a standard paper, no device sent.
apply_page_change(&mut st.page, "device", &json!("None"));
apply_page_change(&mut st.page, "paper", &json!("A3"));
let p = plot_params(&st, None);
let a3 = cadcraft_render::paper_size("A3").unwrap().name;
assert_eq!((p["device"].as_str(), p["paper"].as_str()), (Some("None"), Some(a3)));
apply_page_change(&mut st.page, "plotStyleTable", &json!("None"));
assert!(plot_params(&st, None).get("plotStyleTable").is_none());
}
#[test]
fn window_pick_brings_the_dialog_back_with_the_picked_window() {
let ctx = egui::Context::default();
let mut app = CadApp::new(Session::new(), crate::Services::default());
set_temp(&ctx, "plot_state", state("Model"));
set_temp(&ctx, PICKING_WINDOW, true);
app.start("plotwindow");
plot_window_resume(&mut app, &ctx);
assert_eq!(app.ui.dialog, None, "still picking");
app.session.cmdline("2,3").unwrap();
app.session.cmdline("12,9").unwrap();
plot_window_resume(&mut app, &ctx);
assert_eq!(app.ui.dialog.as_deref(), Some("plot"));
let st = temp::<PlotState>(&ctx, "plot_state").unwrap();
assert_eq!((st.page.plot_area.as_str(), st.window), ("window", [2.0, 3.0, 12.0, 9.0]));
// A cancelled pick brings the dialog back unchanged.
app.ui.dialog = None;
set_temp(&ctx, PICKING_WINDOW, true);
app.start("plotwindow");
app.session.input(cadcraft_engine::Input::Cancel).ok();
plot_window_resume(&mut app, &ctx);
assert_eq!(app.ui.dialog.as_deref(), Some("plot"));
assert_eq!(temp::<PlotState>(&ctx, "plot_state").unwrap().window, [2.0, 3.0, 12.0, 9.0]);
}
}
+15 -3
View File
@@ -10,6 +10,9 @@ pub const MENUS: &[&str] = &["File", "Edit", "View", "Insert", "Format", "Tools"
/// UI-only commands: (id, label, menu path, shortcut).
pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
("ui.spacemouse", "3D Mouse...", &["Tools", "3D Mouse..."], None),
("ui.spacemouse.calibrate", "Calibrate 3D Mouse", &[], None),
("ui.spacemouse.navigate", "3D Mouse Navigation", &[], None),
("ui.dock", "Arrange Panel", &[], None),
("ui.dock.reset", "Reset Panel Layout", &["Window", "Reset Panel Layout"], None),
("ui.open", "Open...", &[], Some("Cmd+O")),
@@ -75,6 +78,9 @@ pub fn run_ui_command(app: &mut CadApp, id: &str, params: &Value) -> Option<Resu
}
fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value, String>> {
if let Some(result) = crate::spacemouse::route(app, id, params) {
return Some(result);
}
if let Some(r) = crate::xrefs::route(app, id, params) {
return Some(r);
}
@@ -112,7 +118,13 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
return Some(Err(format!("language must be {}", crate::i18n::Preference::codes())));
};
app.ui.interface_language = preference;
Ok(json!({"interfaceLanguage": preference.code()}))
let shown = app.language();
if shown != preference.resolve(&app.system_languages) {
app.set_status(
"No Chinese font is installed, so the interface stays in English. Install one (Noto Sans CJK, for example) to use Chinese.",
);
}
Ok(json!({"interfaceLanguage": preference.code(), "shown": shown}))
}
"ui.open" | "open" if no_path => {
let picked = app.services.pick_open.as_ref().and_then(|f| f());
@@ -466,7 +478,7 @@ pub(crate) fn entry_ui(ui: &mut egui::Ui, e: &Entry, clicked: &mut Option<String
}
}
Entry::Sub { label, children } => {
ui.menu_button(crate::i18n::t(label), |ui| {
ui.menu_button(crate::i18n::menu(label), |ui| {
for c in children {
entry_ui(ui, c, clicked);
}
@@ -484,7 +496,7 @@ pub fn menu_bar(app: &mut CadApp, ui: &mut egui::Ui) {
|ui| {
egui::MenuBar::new().ui(ui, |ui| {
for (name, entries) in &tree {
ui.menu_button(crate::i18n::t(name), |ui| {
ui.menu_button(crate::i18n::menu(name), |ui| {
for e in entries {
entry_ui(ui, e, &mut clicked);
}
+45
View File
@@ -189,8 +189,47 @@ fn files(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
}
});
ui.end_row();
ui.label(crate::tl!("Default save version"));
let current = crate::SaveVersion::parse(&app.ui.save_version).unwrap_or_default();
let mut version = current;
egui::ComboBox::from_id_salt("options_save_version").selected_text(version.year()).show_ui(ui, |ui| {
for v in crate::SaveVersion::ALL {
ui.selectable_value(&mut version, v, v.year());
}
});
if version != current {
changes.insert("saveVersion".into(), json!(version.year()));
}
ui.end_row();
ui.label(crate::tl!("Automatic save every"));
let mut minutes = cadcraft_engine::autosave::savetime();
let max = cadcraft_engine::autosave::MAX_SAVETIME;
if ui.add(egui::DragValue::new(&mut minutes).range(0..=max).suffix(format!(" {}", crate::tl!("min")))).changed() {
changes.insert("SAVETIME".into(), json!(minutes));
}
ui.end_row();
ui.label(crate::tl!("Automatic save file location"));
// Edited as text; the folder changes when the field loses focus.
let id = egui::Id::new("cc_options_savefilepath");
let current = cadcraft_engine::autosave::savefilepath();
let mut path = ui.data(|d| d.get_temp::<String>(id)).unwrap_or_else(|| current.clone());
let r = ui.add(egui::TextEdit::singleline(&mut path).desired_width(330.0));
if r.changed() {
ui.data_mut(|d| d.insert_temp(id, path.clone()));
}
if r.lost_focus() {
ui.data_mut(|d| d.remove::<String>(id));
if path.trim() != current {
changes.insert("SAVEFILEPATH".into(), json!(path.trim()));
}
}
ui.end_row();
});
hint(ui, crate::tl!("A drawing opened from a file keeps that file's format."));
hint(ui, crate::tl!("Drawings with no file yet are saved in the default version. 0 minutes turns automatic save off."));
}
fn drafting(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>, start: &mut Option<&'static str>) {
@@ -287,6 +326,12 @@ fn preferences(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>
ui.end_row();
});
hint(ui, crate::tl!("An array that would create more objects than this is refused."));
ui.add_space(6.0);
let mut timed = app.options.time_sensitive_right_click;
if ui.checkbox(&mut timed, crate::tl!("Time-sensitive right-click: a quick click is Enter, holding opens the menu")).changed() {
changes.insert("timeSensitiveRightClick".into(), json!(timed));
}
hint(ui, crate::tl!("Turned off, a right-click while a command runs always opens the shortcut menu."));
}
fn profiles(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
+58 -3
View File
@@ -9,13 +9,20 @@
//! | name | value |
//! |---|---|
//! | `theme` | `"system"`, `"light"` or `"dark"` |
//! | `interfaceLanguage` | `"auto"`, `"en"` or `"uk"` |
//! | `interfaceLanguage` | `"auto"`, `"en"`, `"uk"`, `"pt-BR"`, `"de"`, `"es"`, `"cs"`, `"ru"`, `"fr"` or `"zh"` |
//! | `saveFormat` | `"dxf"` or `"dwg"`: the format Save As suggests for new drawings |
//! | `saveVersion` | `"2000"` … `"2018"`: the version drawings with no file yet are saved in |
//! | `SAVETIME` | minutes between automatic saves, 0–600 (0: off) |
//! | `SAVEFILEPATH` | the folder automatic saves go to |
//! | `timeSensitiveRightClick` | while a command runs, a quick right-click is Enter and holding the button opens the shortcut menu (default); `false`: a right-click always opens the menu |
//! | `systemCursor` | keep the system cursor visible over the drawing area |
//! | `historyLines` | command-line history lines, 0–12 |
//! | `modelBackground`, `paperBackground` | `"#rrggbb"`, or `null` for the default |
//! | `snapMarkerSize` | object snap marker size in points, 5–31 |
//!
//! SAVETIME and SAVEFILEPATH are the engine's autosave settings, kept with the autosave state
//! rather than in the profile.
//!
//! The profile variables and the interface preferences survive restarts: the host saves
//! [`CadApp::profile_json`] under [`PROFILE_KEY`] next to the interface prefs and restores it with
//! [`CadApp::load_profile`].
@@ -36,6 +43,9 @@ pub const PROFILE_KEY: &str = "cadcraft.profile";
/// The largest saved profile [`CadApp::load_profile`] reads.
const MAX_PROFILE_BYTES: usize = 64 * 1024;
/// The longest SAVEFILEPATH the JSON form accepts.
const MAX_PATH_CHARS: usize = 1024;
/// Object snap marker sizes the Options dialog offers, in points.
pub const SNAP_MARKER_SIZES: std::ops::RangeInclusive<f32> = 5.0..=31.0;
@@ -48,11 +58,14 @@ pub struct UiOptions {
pub paper_background: Option<Color32>,
/// Object snap marker size in points.
pub snap_marker_size: f32,
/// While a command runs: a quick right-click is Enter and a held one opens the shortcut menu
/// (AutoCAD's time-sensitive right-click). Off: a right-click always opens the menu.
pub time_sensitive_right_click: bool,
}
impl Default for UiOptions {
fn default() -> Self {
UiOptions { model_background: None, paper_background: None, snap_marker_size: theme::SNAP_MARKER }
UiOptions { model_background: None, paper_background: None, snap_marker_size: theme::SNAP_MARKER, time_sensitive_right_click: true }
}
}
@@ -106,6 +119,10 @@ struct UiChanges {
model_background: Option<Option<Color32>>,
paper_background: Option<Option<Color32>>,
snap_marker_size: Option<f32>,
save_version: Option<crate::SaveVersion>,
savetime: Option<u32>,
savefilepath: Option<String>,
time_sensitive_right_click: Option<bool>,
}
fn color_param(key: &str, v: &Value) -> Result<Option<Color32>, String> {
@@ -137,6 +154,23 @@ fn parse_ui(key: &str, v: &Value, c: &mut UiChanges) -> Result<bool, String> {
let n = v.as_f64().filter(|n| SNAP_MARKER_SIZES.contains(&(*n as f32)) && n.is_finite()).ok_or("snapMarkerSize must be from 5 to 31")?;
c.snap_marker_size = Some(n as f32);
}
"saveVersion" => {
let name = v.as_str().map_or_else(|| v.to_string(), str::to_string);
c.save_version = Some(crate::SaveVersion::parse(name.trim()).ok_or("saveVersion must be 2000, 2004, 2007, 2010, 2013 or 2018")?);
}
"timeSensitiveRightClick" => c.time_sensitive_right_click = Some(v.as_bool().ok_or("timeSensitiveRightClick must be true or false")?),
k if k.eq_ignore_ascii_case("SAVETIME") => {
let max = cadcraft_engine::autosave::MAX_SAVETIME;
let n = v
.as_f64()
.filter(|n| n.is_finite() && (0.0..=f64::from(max)).contains(n))
.ok_or_else(|| format!("SAVETIME must be from 0 to {max} minutes"))?;
c.savetime = Some(n.round() as u32);
}
k if k.eq_ignore_ascii_case("SAVEFILEPATH") => {
let p = v.as_str().map(str::trim).filter(|p| p.chars().count() <= MAX_PATH_CHARS && !p.chars().any(char::is_control));
c.savefilepath = Some(p.ok_or("SAVEFILEPATH must be a folder path")?.to_string());
}
_ => return Ok(false),
}
Ok(true)
@@ -154,6 +188,10 @@ fn ui_json(app: &CadApp) -> Map<String, Value> {
m.insert("modelBackground".into(), color_json(o.model_background));
m.insert("paperBackground".into(), color_json(o.paper_background));
m.insert("snapMarkerSize".into(), json!(o.snap_marker_size));
m.insert("saveVersion".into(), json!(app.ui.save_version));
m.insert("SAVETIME".into(), json!(cadcraft_engine::autosave::savetime()));
m.insert("SAVEFILEPATH".into(), json!(cadcraft_engine::autosave::savefilepath()));
m.insert("timeSensitiveRightClick".into(), json!(o.time_sensitive_right_click));
m
}
@@ -206,6 +244,18 @@ fn run_json(app: &mut CadApp, params: &Value) -> Result<Value, String> {
if let Some(n) = ui.snap_marker_size {
app.options.snap_marker_size = n;
}
if let Some(v) = ui.save_version {
app.ui.save_version = v.year().to_string();
}
if let Some(m) = ui.savetime {
cadcraft_engine::autosave::set_savetime(m);
}
if let Some(p) = &ui.savefilepath {
cadcraft_engine::autosave::set_savefilepath(p);
}
if let Some(b) = ui.time_sensitive_right_click {
app.options.time_sensitive_right_click = b;
}
Ok(report(app))
}
@@ -214,6 +264,7 @@ fn reset_ui(app: &mut CadApp) {
let d = crate::UiState::default();
app.ui.theme = d.theme;
app.ui.save_format = d.save_format;
app.ui.save_version = d.save_version;
app.ui.system_cursor = d.system_cursor;
app.ui.history_lines = d.history_lines;
app.options = UiOptions::default();
@@ -232,6 +283,7 @@ impl CadApp {
"modelBackground": color_json(self.options.model_background),
"paperBackground": color_json(self.options.paper_background),
"snapMarkerSize": self.options.snap_marker_size,
"timeSensitiveRightClick": self.options.time_sensitive_right_click,
})
.to_string()
}
@@ -247,7 +299,7 @@ impl CadApp {
cadcraft_engine::cmd::options::restore(&mut self.session, vars);
}
let mut c = UiChanges::default();
for key in ["systemCursor", "modelBackground", "paperBackground", "snapMarkerSize"] {
for key in ["systemCursor", "modelBackground", "paperBackground", "snapMarkerSize", "timeSensitiveRightClick"] {
if let Some(value) = v.get(key) {
let _ = parse_ui(key, value, &mut c);
}
@@ -265,6 +317,9 @@ impl CadApp {
if let Some(n) = c.snap_marker_size {
self.options.snap_marker_size = n;
}
if let Some(b) = c.time_sensitive_right_click {
self.options.time_sensitive_right_click = b;
}
}
}
+53 -8
View File
@@ -9,6 +9,8 @@ use crate::CadApp;
use crate::icons::{self, Icon};
use crate::theme::Tokens;
/// The hatch pattern picker.
pub mod patterns;
/// The QuickCalc palette.
pub mod quickcalc;
@@ -160,6 +162,8 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) {
let body = Rect::from_min_max(pos2(r.left(), r.top() + tab_h), r.max);
let mut clicked: Option<&'static str> = None;
let mut toggle_group = None;
let mut tool_visible: Option<(String, bool)> = None;
let mut restore: Vec<String> = Vec::new();
ui.scope_builder(egui::UiBuilder::new().max_rect(body), |ui| {
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
@@ -182,11 +186,28 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) {
egui::FontId::proportional(12.5),
t.text,
);
icons::paint(pp, Rect::from_center_size(pos2(hr.right() - 12.0, hr.center().y), vec2(11.0, 11.0)), Icon::Gear, false);
let gear = Rect::from_center_size(pos2(hr.right() - 12.0, hr.center().y), vec2(13.0, 13.0));
let gresp = ui.interact(gear, ui.id().with(("ts_gear", name)), Sense::click());
icons::paint(pp, Rect::from_center_size(gear.center(), vec2(11.0, 11.0)), Icon::Gear, false);
icons::describe_control(ui, &hresp, name, Some(!collapsed), false);
icons::describe_control(ui, &gresp, &format!("{name} options"), None, false);
let gresp = gresp.on_hover_text(crate::tl!("Show and hide the tools of this category"));
if hresp.clicked() {
toggle_group = Some(name.to_string());
}
let hidden = |cmd: &str| app.ui.hidden_tools.iter().any(|h| h == cmd);
egui::Popup::from_toggle_button_response(&gresp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
ui.label(crate::tl!("Show in Tool Sets"));
for (_, cmd, tip) in large.iter().chain(small.iter()) {
let mut shown = !hidden(cmd);
if ui.checkbox(&mut shown, *tip).changed() {
tool_visible = Some((cmd.to_string(), shown));
}
}
if ui.selectable_label(false, crate::tl!("Show All")).clicked() {
restore = large.iter().chain(small.iter()).map(|(_, cmd, _)| cmd.to_string()).collect();
}
});
if !collapsed {
ui.add_space(4.0);
// Large icons.
@@ -195,6 +216,9 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) {
ui.add_space(8.0);
ui.spacing_mut().item_spacing = vec2(8.0, 0.0);
for (icon, cmd, tip) in large.iter() {
if hidden(cmd) {
continue;
}
ui.add_enabled_ui(command_known(cmd), |ui| {
if icons::button(ui, *icon, 40.0, tip, false).clicked() {
clicked = Some(cmd);
@@ -204,7 +228,8 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) {
});
}
// Small icons, 8 per row.
for row in small.chunks(8) {
let visible: Vec<Tool> = small.iter().copied().filter(|(_, cmd, _)| !hidden(cmd)).collect();
for row in visible.chunks(8) {
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.add_space(8.0);
@@ -225,6 +250,16 @@ pub fn toolsets(app: &mut CadApp, ui: &mut egui::Ui) {
}
});
});
if let Some((cmd, shown)) = tool_visible {
if shown {
app.ui.hidden_tools.retain(|h| h != &cmd);
} else if !app.ui.hidden_tools.contains(&cmd) {
app.ui.hidden_tools.push(cmd);
}
}
if !restore.is_empty() {
app.ui.hidden_tools.retain(|h| !restore.contains(h));
}
if let Some(g) = toggle_group {
if let Some(i) = app.ui.collapsed_groups.iter().position(|x| *x == g) {
app.ui.collapsed_groups.remove(i);
@@ -274,6 +309,9 @@ fn swatch(p: &egui::Painter, r: Rect, c: Color) {
}
pub fn right_palettes(app: &mut CadApp, ui: &mut egui::Ui) {
if !app.ui.show_palettes {
return;
}
let t = Tokens::get();
egui::Panel::right("cc_palettes").exact_size(300.0).resizable(false).frame(egui::Frame::NONE.fill(t.panel)).show(ui, |ui| {
let r = ui.max_rect();
@@ -285,7 +323,13 @@ pub fn right_palettes(app: &mut CadApp, ui: &mut egui::Ui) {
let br = Rect::from_center_size(pos2(r.left() + 30.0 + i as f32 * 52.0, r.top() + tab_h / 2.0), vec2(20.0, 20.0));
icons::paint(&p, br, *icon, false);
}
icons::paint(&p, Rect::from_center_size(pos2(r.right() - 14.0, r.top() + tab_h / 2.0), vec2(14.0, 14.0)), Icon::ChevronRight, false);
let cr = Rect::from_center_size(pos2(r.right() - 14.0, r.top() + tab_h / 2.0), vec2(14.0, 14.0));
let cresp = ui.interact(cr, ui.id().with("palettes_collapse"), Sense::click());
icons::paint(&p, cr, Icon::ChevronRight, false);
icons::describe_control(ui, &cresp, "Collapse Palettes", None, false);
if cresp.on_hover_text(crate::tl!("Collapse Palettes")).clicked() {
app.ui.show_palettes = false;
}
let body = Rect::from_min_max(pos2(r.left(), r.top() + tab_h), r.max);
ui.scope_builder(egui::UiBuilder::new().max_rect(body), |ui| {
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
@@ -972,11 +1016,9 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
EntityKind::Hatch(hh) => {
prop_row(ui, "Pattern", |ui| {
ui.menu_button(hh.pattern.clone(), |ui| {
for pat in cadcraft_doc::library::standard_patterns() {
if ui.button(pat.name).clicked() {
set = Some(json!({ "handles": ids, "pattern": pat.name }));
ui.close();
}
if let Some(name) = patterns::menu(ui, &hh.pattern) {
set = Some(json!({ "handles": ids, "pattern": name }));
ui.close();
}
});
});
@@ -997,6 +1039,9 @@ pub(crate) fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) {
}
EntityKind::Insert(i) => {
prop_row(ui, "Name", |ui| read_only(ui, &i.block));
prop_row(ui, "Position X", |ui| read_only(ui, &format!("{:.4}", i.insert.x)));
prop_row(ui, "Position Y", |ui| read_only(ui, &format!("{:.4}", i.insert.y)));
prop_row(ui, "Position Z", |ui| read_only(ui, &format!("{:.4}", i.insert.z)));
num(ui, "Rotation", "rotation", i.rotation.to_degrees(), &mut set, &ids);
num(ui, "Scale", "scale", i.scale.x, &mut set, &ids);
}
+151
View File
@@ -0,0 +1,151 @@
//! The hatch pattern picker: a swatch of every pattern offered for new hatches, beside HATCH's
//! and HATCHEDIT's prompts and in the Properties palette, so a pattern is chosen by its look
//! instead of by typing its name.
use std::cell::RefCell;
use std::sync::Arc;
use cadcraft_engine::geom::Vec2;
use cadcraft_engine::{Input, PatternChoice, Picker, pattern_choices};
use egui::{Align2, Rect, Sense, Stroke, pos2, vec2};
use crate::CadApp;
use crate::theme::Tokens;
/// A swatch's side, in points.
const CELL: f32 = 56.0;
/// Room under a swatch for the pattern's name.
const LABEL_H: f32 = 16.0;
/// Space between swatches.
const GAP: f32 = 6.0;
/// Swatches per row in the Properties palette's menu.
const MENU_COLUMNS: f32 = 6.0;
/// A pattern as the picker draws it: its runs in the unit square (y up), or a solid fill.
struct Swatch {
name: String,
description: String,
runs: Vec<Vec<Vec2>>,
solid: bool,
}
/// The patterns' names and line family counts, and their swatches.
type Swatches = (Vec<(String, usize)>, Arc<Vec<Swatch>>);
thread_local! {
static SWATCHES: RefCell<Swatches> = RefCell::new((Vec::new(), Arc::new(Vec::new())));
}
/// The swatches of the patterns offered now, made again only when the list changes (a `.pat`
/// file loaded more patterns).
fn swatches() -> Arc<Vec<Swatch>> {
let choices = pattern_choices();
let key: Vec<(String, usize)> = choices.iter().map(|c| (c.name.clone(), c.lines.len())).collect();
SWATCHES.with(|cell| {
let mut cache = cell.borrow_mut();
if cache.0 != key {
*cache = (key, Arc::new(choices.into_iter().map(swatch).collect()));
}
cache.1.clone()
})
}
fn swatch(c: PatternChoice) -> Swatch {
let solid = c.lines.is_empty();
Swatch { runs: cadcraft_engine::render::pattern_swatch(&c.lines, 1.0), name: c.name, description: c.description, solid }
}
/// Draw one swatch in `r` (the square and the name under it).
fn paint(p: &egui::Painter, r: Rect, s: &Swatch, selected: bool, hovered: bool) {
let t = Tokens::get();
let square = Rect::from_min_size(r.min, vec2(CELL, CELL));
p.rect_filled(square, 3.0, t.field);
let inner = square.shrink(3.0);
if s.solid {
p.rect_filled(inner, 1.0, t.text);
} else {
let at = |v: &Vec2| pos2(inner.min.x + v.x as f32 * inner.width(), inner.max.y - v.y as f32 * inner.height());
let stroke = Stroke::new(1.0, t.text);
for run in &s.runs {
match run.as_slice() {
[] => {}
[dot] => {
p.circle_filled(at(dot), 0.7, t.text);
}
pts => {
p.add(egui::Shape::line(pts.iter().map(at).collect(), stroke));
}
}
}
}
let border = match (selected, hovered) {
(true, _) => Stroke::new(2.0, t.accent),
(false, true) => Stroke::new(1.0, t.accent),
_ => Stroke::new(1.0, t.border),
};
p.rect_stroke(square, 3.0, border, egui::StrokeKind::Inside);
let label = Rect::from_min_max(pos2(r.min.x, square.max.y), r.max);
let color = if selected { t.accent } else { t.text_dim };
p.with_clip_rect(label).text(label.center_top() + vec2(0.0, 2.0), Align2::CENTER_TOP, &s.name, crate::theme::small(), color);
}
/// The swatches whose name or description holds `filter`, `current` marked; returns the
/// pattern clicked.
pub fn grid(ui: &mut egui::Ui, current: &str, filter: &str) -> Option<String> {
let filter = filter.trim().to_lowercase();
let mut chosen = None;
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = vec2(GAP, GAP);
let shown = swatches();
for s in
shown.iter().filter(|s| filter.is_empty() || s.name.to_lowercase().contains(&filter) || s.description.to_lowercase().contains(&filter))
{
let (r, resp) = ui.allocate_exact_size(vec2(CELL, CELL + LABEL_H), Sense::click());
if ui.is_rect_visible(r) {
paint(ui.painter(), r, s, s.name.eq_ignore_ascii_case(current), resp.hovered());
}
let tip = if s.description.is_empty() { s.name.clone() } else { format!("{}\n{}", s.name, s.description) };
if resp.on_hover_text(tip).clicked() {
chosen = Some(s.name.clone());
}
}
});
chosen
}
/// The Properties palette's pattern menu: the swatch grid, scrolling; returns the pattern
/// clicked.
pub fn menu(ui: &mut egui::Ui, current: &str) -> Option<String> {
ui.set_width(MENU_COLUMNS * (CELL + GAP));
egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| grid(ui, current, "")).inner
}
/// The picker window while the prompt offers one (HATCH's boundary and pattern prompts,
/// HATCHEDIT's pattern prompt): a click enters the pattern's name at the prompt.
pub fn prompt_picker(app: &mut CadApp, ctx: &egui::Context) {
let Some(Picker::HatchPattern(current)) = app.session.current_prompt().and_then(|p| p.picker) else { return };
let filter_id = egui::Id::new("hatch_pattern_filter");
let mut filter: String = ctx.data_mut(|d| d.get_temp(filter_id)).unwrap_or_default();
let mut chosen = None;
let area = ctx.content_rect();
egui::Window::new(crate::tl!("Hatch Pattern"))
.id(egui::Id::new("Hatch Pattern"))
.collapsible(true)
.resizable(true)
.default_size(vec2(5.0 * (CELL + GAP) + 12.0, 5.0 * (CELL + LABEL_H + GAP) + 40.0))
.default_pos(pos2(area.max.x - 5.0 * (CELL + GAP) - 330.0, area.min.y + 120.0))
.show(ctx, |ui| {
ui.add(egui::TextEdit::singleline(&mut filter).hint_text(crate::tl!("Search for pattern")).desired_width(f32::INFINITY));
ui.add_space(4.0);
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
chosen = grid(ui, &current, &filter);
});
});
ctx.data_mut(|d| d.insert_temp(filter_id, filter));
if let Some(name) = chosen {
app.cmd.buffer.clear();
if let Err(e) = app.session.input(Input::Text(name)) {
app.session.echo(e.to_string());
}
}
}
+162
View File
@@ -0,0 +1,162 @@
//! Host-independent 3D mouse preferences and navigation. Device I/O lives in the desktop app.
use cadcraft_engine::View;
use cadcraft_geom::Vec2;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::CadApp;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Settings {
pub enabled: bool,
/// Visible extents per second at full deflection.
pub pan_speed: f64,
/// Exponential zoom rate per second at full deflection.
pub zoom_speed: f64,
pub reverse_zoom: bool,
}
impl Default for Settings {
fn default() -> Self {
Self { enabled: true, pan_speed: 0.8, zoom_speed: 1.6, reverse_zoom: false }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Status {
#[default]
Unavailable,
Searching,
Calibrating,
Connected,
OpenFailed,
Disconnected,
Error,
}
impl Status {
pub fn message(self) -> &'static str {
match self {
Self::Unavailable => "3D mouse input is unavailable on this platform",
Self::Searching => "No supported 3D mouse found",
Self::Calibrating => "Release the 3D mouse to calibrate its resting position",
Self::Connected => "3D mouse connected",
Self::OpenFailed => "3D mouse busy, possibly 3DxWare or missing device permissions",
Self::Disconnected => "3D mouse disconnected",
Self::Error => "3D mouse input could not be started",
}
}
}
#[derive(Default)]
pub struct State {
pub settings: Settings,
pub status: Status,
pub device: Option<String>,
pub detail: Option<String>,
/// The native host consumes this request; no device I/O runs on the UI thread.
pub calibrate_requested: bool,
}
impl State {
pub fn load_settings(&mut self, value: &Value) -> Result<(), String> {
let settings: Settings = serde_json::from_value(value.clone()).map_err(|e| e.to_string())?;
if !settings.pan_speed.is_finite()
|| !(0.1..=3.0).contains(&settings.pan_speed)
|| !settings.zoom_speed.is_finite()
|| !(0.1..=3.0).contains(&settings.zoom_speed)
{
return Err("3D mouse sensitivity must be between 0.1 and 3.0".into());
}
self.settings = settings;
Ok(())
}
fn summary(&self) -> Value {
json!({"settings": self.settings, "status": self.status, "device": self.device, "detail": self.detail})
}
}
pub fn route(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value, String>> {
match id {
"ui.spacemouse" => {
if params.is_null() {
app.ui.dialog = Some("spacemouse".into());
} else if let Some(settings) = params.get("settings")
&& let Err(error) = app.spacemouse.load_settings(settings)
{
return Some(Err(error));
}
Some(Ok(app.spacemouse.summary()))
}
"ui.spacemouse.calibrate" => {
if !matches!(app.spacemouse.status, Status::Connected | Status::Calibrating) {
return Some(Err("Connect a 3D mouse before calibrating".into()));
}
app.spacemouse.calibrate_requested = true;
Some(Ok(Value::Null))
}
"ui.spacemouse.navigate" => Some(navigate(app, params)),
_ => None,
}
}
/// One frame of fractional pan and magnification, shared by device input and the control API.
/// Quiet view updates keep model/paper/tiled/floating viewport handling in the engine and do
/// not flood ZOOM Previous's history with one entry per frame.
fn navigate(app: &mut CadApp, params: &Value) -> Result<Value, String> {
let pan = params.get("pan").and_then(Value::as_array).filter(|p| p.len() == 2);
let x = pan.and_then(|p| p.first()).and_then(Value::as_f64);
let y = pan.and_then(|p| p.get(1)).and_then(Value::as_f64);
let zoom = params.get("zoom").and_then(Value::as_f64);
let (Some(x), Some(y), Some(zoom)) = (x, y, zoom) else { return Err("Expected {pan: [x, y], zoom: factor}".into()) };
if !x.is_finite() || !y.is_finite() || x.abs() > 0.25 || y.abs() > 0.25 || !zoom.is_finite() || !(0.5..=2.0).contains(&zoom) {
return Err("3D mouse navigation frame is outside its limits".into());
}
if !app.spacemouse.settings.enabled {
return Ok(Value::Null);
}
let (width, height) = app.session.viewport_px;
let aspect = width / height.max(1.0);
let state = app.session.state_mut().map_err(|e| e.to_string())?;
let view = state.view();
let center = view.center - Vec2::new(x * view.height * aspect, y * view.height);
let height = (view.height / zoom).clamp(1e-9, 1e12);
if !center.is_finite() || !height.is_finite() {
return Err("3D mouse navigation would produce an invalid view".into());
}
state.set_view_quiet(View { center, height });
Ok(json!({"center": [center.x, center.y], "height": height}))
}
pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(crate::tl!("3D Mouse")).id(egui::Id::new("spacemouse")).open(open).resizable(false).show(ctx, |ui| {
ui.label(crate::i18n::t(app.spacemouse.status.message()));
if let Some(device) = &app.spacemouse.device {
ui.label(device);
}
if let Some(detail) = &app.spacemouse.detail {
ui.label(detail);
}
ui.separator();
let mut settings = app.spacemouse.settings;
let mut changed = ui.checkbox(&mut settings.enabled, crate::tl!("Enable 3D mouse navigation")).changed();
changed |= ui.add(egui::Slider::new(&mut settings.pan_speed, 0.1..=3.0).text(crate::tl!("Pan sensitivity"))).changed();
changed |= ui.add(egui::Slider::new(&mut settings.zoom_speed, 0.1..=3.0).text(crate::tl!("Zoom sensitivity"))).changed();
changed |= ui.checkbox(&mut settings.reverse_zoom, crate::tl!("Reverse zoom direction")).changed();
if changed {
let _ = app.run("ui.spacemouse", json!({"settings": settings}));
}
if ui
.add_enabled(matches!(app.spacemouse.status, Status::Connected | Status::Calibrating), egui::Button::new(crate::tl!("Calibrate")))
.clicked()
{
let _ = app.run("ui.spacemouse.calibrate", Value::Null);
}
ui.label(crate::tl!("Move sideways to pan; push/pull or twist to zoom."));
ui.label(crate::tl!("Left device button: zoom extents. Right device button: calibrate."));
});
}

Some files were not shown because too many files have changed in this diff Show More