Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 00c7613f1a Release 0.6.2: rebuild with the fixed shared release workflow
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 ThomasandClaude Opus 5.5 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 ThomasandClaude Opus 5.5 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 ThomasandClaude Opus 5.5 a12c85c585 Release prep: 0.6.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:44:20 -04:00
cyfung1031andClaude Opus 5.5 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
mayloglogandClaude Opus 5.5 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
aldozarandClaude Opus 5.5 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
aldozarandClaude Opus 5.5 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
aldozarandClaude Opus 5.5 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
aldozarandClaude Opus 5.5 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
LionandClaude Opus 5.5 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 OspinaandClaude Opus 5.5 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
rangoDJandClaude Opus 5.5 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
MarekSpodBiedryandClaude Opus 5.5 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
BooksandClaude Opus 5.5 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
SteamSmithandClaude Opus 5.5 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
rangoDJandClaude Opus 5.5 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
aldozarandClaude Opus 5.5 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 BrashearandClaude Opus 5.5 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
aldozarandClaude Opus 5.5 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
CaseyKleinmanandClaude Opus 5.5 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
markmonkeyandClaude Opus 5.5 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
aldozarandClaude Opus 5.5 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
PedroGuilleandClaude Opus 5.5 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
rangoDJandClaude Opus 5.5 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
PedroGuilleandClaude Opus 5.5 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-starandClaude Opus 5.5 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šákandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 - GPEandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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
aldozarandClaude Opus 5.5 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
STE1200andClaude Opus 5.5 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
aldozarandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 LussiandClaude Opus 5.5 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
MuhiandClaude Opus 5.5 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 - GPEandClaude Opus 5.5 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
rangoDJandClaude Opus 5.5 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
MarekSpodBiedryandClaude Opus 5.5 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
MarekSpodBiedryandClaude Opus 5.5 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 11905 additions and 676 deletions

No files matched your search

+1
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 }}
+4
View File
@@ -60,3 +60,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.2"
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.2"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -569,14 +577,14 @@ dependencies = [
[[package]]
name = "cadcraft-color"
version = "0.5.0"
version = "0.6.2"
dependencies = [
"serde",
]
[[package]]
name = "cadcraft-constraints"
version = "0.5.0"
version = "0.6.2"
dependencies = [
"cadcraft-doc",
"cadcraft-geom",
@@ -586,7 +594,7 @@ dependencies = [
[[package]]
name = "cadcraft-doc"
version = "0.5.0"
version = "0.6.2"
dependencies = [
"cadcraft-color",
"cadcraft-geom",
@@ -597,14 +605,14 @@ dependencies = [
[[package]]
name = "cadcraft-dwg"
version = "0.5.0"
version = "0.6.2"
dependencies = [
"acadrust",
]
[[package]]
name = "cadcraft-dxf"
version = "0.5.0"
version = "0.6.2"
dependencies = [
"encoding_rs",
"log",
@@ -613,7 +621,7 @@ dependencies = [
[[package]]
name = "cadcraft-engine"
version = "0.5.0"
version = "0.6.2"
dependencies = [
"cadcraft-color",
"cadcraft-constraints",
@@ -632,7 +640,7 @@ dependencies = [
[[package]]
name = "cadcraft-fonts"
version = "0.5.0"
version = "0.6.2"
dependencies = [
"cadcraft-geom",
"harfrust",
@@ -642,14 +650,14 @@ dependencies = [
[[package]]
name = "cadcraft-geom"
version = "0.5.0"
version = "0.6.2"
dependencies = [
"serde",
]
[[package]]
name = "cadcraft-io"
version = "0.5.0"
version = "0.6.2"
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.2"
dependencies = [
"cadcraft-engine",
"cadcraft-io",
@@ -677,19 +686,21 @@ dependencies = [
[[package]]
name = "cadcraft-render"
version = "0.5.0"
version = "0.6.2"
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.2"
dependencies = [
"cadcraft-color",
"cadcraft-doc",
@@ -710,7 +721,7 @@ dependencies = [
[[package]]
name = "cadcraft-web"
version = "0.5.0"
version = "0.6.2"
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.2"
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.2"
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://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
+1 -1
View File
@@ -111,7 +111,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
@@ -57,7 +57,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));
@@ -87,7 +87,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();
@@ -113,7 +113,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
@@ -108,6 +108,7 @@
Close All Alle schließen
Closed Geschlossen
Coincident Koinzident
Collapse Palettes Paletten einklappen
Collapse Tool Sets Werkzeuggruppen einklappen
Collinear Kollinear
Color Farbe
@@ -523,6 +524,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
@@ -993,3 +995,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.
108
109
110
111
112
113
114
524
525
526
527
528
529
530
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
+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)
Center Centro
@@ -100,28 +124,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
@@ -131,15 +168,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}
@@ -147,17 +194,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
@@ -169,9 +224,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...
@@ -181,6 +240,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
@@ -196,23 +256,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
@@ -259,14 +328,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
@@ -289,6 +363,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
@@ -296,24 +371,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}
@@ -323,24 +406,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
@@ -348,17 +439,27 @@
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
Join the ArtCraft Discord… Únete al Discord de ArtCraft…
Join us on Discord Únete a nosotros en Discord
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
@@ -367,8 +468,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
@@ -379,7 +480,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
@@ -392,9 +495,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
@@ -414,13 +520,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
@@ -434,16 +542,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
@@ -451,6 +563,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)
@@ -459,20 +572,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
@@ -480,17 +599,28 @@
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
One of the Crafting Apps by the ArtCraft team and community. Una de las Crafting Apps del equipo y la comunidad de ArtCraft.
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)
@@ -498,9 +628,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
@@ -522,7 +658,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
@@ -533,7 +670,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.
@@ -694,7 +898,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
@@ -731,9 +935,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
@@ -904,7 +1108,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í.
@@ -912,7 +1116,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
@@ -940,12 +1144,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
@@ -954,8 +1166,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
@@ -964,18 +1179,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)
@@ -993,3 +1214,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
124
125
126
127
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
224
225
226
227
228
229
230
231
232
233
234
235
236
240
241
242
243
244
245
246
256
257
258
259
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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
363
364
365
366
367
368
369
371
372
373
374
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
406
407
408
409
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
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
468
469
470
471
472
473
474
475
480
481
482
483
484
485
486
487
488
495
496
497
498
499
500
501
502
503
504
505
506
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
563
564
565
566
567
568
569
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
599
600
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
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
658
659
660
661
662
663
664
665
670
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
898
899
900
901
902
903
904
935
936
937
938
939
940
941
942
943
1108
1109
1110
1111
1112
1113
1114
1116
1117
1118
1119
1120
1121
1122
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
+1000
View File
@@ -0,0 +1,1000 @@
# 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
Join the ArtCraft Discord… Rejoindre le Discord d'ArtCraft…
Join us on Discord Rejoignez-nous sur Discord
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
One of the Crafting Apps by the ArtCraft team and community. L'une des applications Crafting Apps de l'équipe et de la communauté ArtCraft.
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 Join the ArtCraft Discord… Rejoindre le Discord d'ArtCraft…
360 Join us on Discord Rejoignez-nous sur Discord
361 Language Langue
362 Last Dernier
363 Last commit Dernière révision
364 Layer Calque
365 Layer Delete Supprimer le calque
366 Layer Match Faire correspondre le calque
367 Layer Merge Fusionner les calques
368 Layer Off Désactiver le calque
369 Layer Properties Propriétés du calque
370 Layer Properties Manager Gestionnaire des propriétés des calques
371 Layer Properties Manager — current viewport Gestionnaire des propriétés des calques — fenêtre courante
372 Layer State État des calques
373 Layer States Manager... Gestionnaire des états de calques...
374 Layer Tools Outils de calque
375 Layer contains objects (double-click to make current) Le calque contient des objets (double-cliquez pour le rendre courant)
376 Layer is empty (double-click to make current) Le calque est vide (double-cliquez pour le rendre courant)
377 Layers Calques
378 Layers Palette Palette des calques
379 Layers displayed: {shown} of {total}. Current layer: {layer} Calques affichés : {shown} sur {total}. Calque courant : {layer}
380 Layout Présentation
381 Layout list Liste des présentations
382 Leader Repère
383 Left Gauche
384 Length Longueur
385 Length X Longueur X
386 Length Y Longueur Y
387 Lengthen Modifier la longueur
388 Light Clair
389 Line Ligne
390 Line delta Variation de lignes
391 Linear Linéaire
392 Lines +/− Lignes +/−
393 Lines added Lignes ajoutées
394 Lines deleted Lignes supprimées
395 Linetype Type de ligne
396 Linetype Scale Échelle du type de ligne
397 Linetype scale Échelle du type de ligne
398 Ltscale Échelle du type de ligne
399 Linetype... Type de ligne...
400 Lineweight Épaisseur de ligne
401 Lineweight... Épaisseur de ligne...
402 List Liste
403 List Blocks Liste des blocs
404 List Dimension Styles Liste des styles de cote
405 List Groups Liste des groupes
406 List Layer States Liste des états de calques
407 List Layouts Liste des présentations
408 List System Variables Liste des variables système
409 List Text Styles Liste des styles de texte
410 Load all Tout charger
411 Load... Charger...
412 Loaded linetypes Types de ligne chargés
413 Lock Verrouiller
414 Lock Layer Verrouiller le calque
415 Lock/Unlock Verrouiller/Déverrouiller
416 Loft Loft
417 MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Non affilié à Autodesk, Inc.
418 MODEL MODÈLE
419 MText Texte multiligne
420 Magenta Magenta
421 Major line every Ligne principale tous les
422 Make Current Rendre courant
423 Make Object's Layer Current Rendre le calque de l'objet courant
424 Make... Créer...
425 Match Layer Faire correspondre le calque
426 Match Properties Copier les propriétés
427 Measure Mesurer
428 Measure Distance Mesurer la distance
429 Measure Geometry Mesurer la géométrie
430 Merge Cells Fusionner les cellules
431 Merged PRs PR fusionnés
432 Mid Between 2 Points Milieu entre 2 points
433 Midpoint Milieu
434 Mirror Miroir
435 Model Modèle
436 Model Space (in viewport) Espace objet (dans la fenêtre)
437 Modeling Modélisation
438 Models Modèles
439 Modify Modifier
440 Move Déplacer
441 Multileader Repère multiple
442 Multileader Style... Style de repère multiple...
443 Multileader style Style de repère multiple
444 Multiline Multiligne
445 Multiline Text Texte multiligne
446 Multiline Text... Texte multiligne...
447 Multiple Multiple
448 Multiple Point Points multiples
449 My Mes
450 My properties: not available yet Mes propriétés : pas encore disponibles
451 N N
452 Name Nom
453 Name (A–Z) Nom (A–Z)
454 Nearest Plus proche
455 New Nouveau
456 New (metric) Nouveau (métrique)
457 New Drawing Nouveau dessin
458 New Drawing (⌘N) Nouveau dessin (⌘N)
459 New Drawing... Nouveau dessin...
460 New Layer Nouveau calque
461 New Layout Nouvelle présentation
462 New VP Freeze Geler nouvelles fenêtres
463 New Viewports... Nouvelles fenêtres...
464 New layout Nouvelle présentation
465 New state name Nom du nouvel état
466 No Non
467 No blocks defined in this drawing. Aucun bloc défini dans ce dessin.
468 No contributor data was built into this copy. Cette version ne contient pas de données sur les contributeurs.
469 No model credits were built into this copy. Cette version ne contient pas de crédits de modèles.
470 No saved layer states Aucun état de calque enregistré
471 No selection Aucune sélection
472 Node Nœud
473 None Aucun
474 Not available Non disponible
475 Not available yet Pas encore disponible
476 OK OK
477 Object Objet
478 Object Snap Accrochage aux objets
479 Object Snap (F3) Accrochage aux objets (F3)
480 Object Snap On (F3) Activer l'accrochage aux objets (F3)
481 Object Snap Tracking Suivi de l'accrochage aux objets
482 Object Snap Tracking (F11) Suivi de l'accrochage aux objets (F11)
483 Object type: Type d'objet :
484 Off Désactivé
485 Offset Décalage
486 On Activé
487 On/Off Activer/Désactiver
488 One of the Crafting Apps by the ArtCraft team and community. L'une des applications Crafting Apps de l'équipe et de la communauté ArtCraft.
489 Open (⌘O) Ouvrir (⌘O)
490 Open Sample Drawing Ouvrir un dessin d'exemple
491 Open sample drawing Ouvrir un dessin d'exemple
492 Open... Ouvrir...
493 Operator: Opérateur :
494 Orbit Orbite
495 Ordinate Ordonnée
496 Ortho (F8) Ortho (F8)
497 Ortho Mode Mode Ortho
498 Ortho Mode (F8) Mode Ortho (F8)
499 Osnap Settings... Paramètres d'accrochage aux objets...
500 Out Zoom arrière
501 Override Outrepasser
502 PAPER PAPIER
503 Percent Pourcentage
504 Polar (distance < angle) Polaire (distance < angle)
505 PRs PR
506 Page Setup Manager Gestionnaire des mises en page
507 Page Setup Manager... Gestionnaire des mises en page...
508 Pan Panoramique
509 Pan Down Panoramique vers le bas
510 Pan Left Panoramique vers la gauche
511 Pan Right Panoramique vers la droite
512 Pan Up Panoramique vers le haut
513 Paper Space Espace papier
514 Parallel Parallèle
515 Parameters Manager Gestionnaire des paramètres
516 Parametric Paramétrique
517 Paste Coller
518 Paste as Block Coller en tant que bloc
519 Paste to Original Coordinates Coller aux coordonnées d'origine
520 Path Array Réseau sur trajectoire
521 Pattern Motif
522 Perpendicular Perpendiculaire
523 Plot Tracer
524 Plot (click: do not plot) Tracer (cliquer : ne pas tracer)
525 Plot Preview Aperçu du tracé
526 Plot style Style de tracé
527 Plot style attached to Style de tracé associé à
528 Plot style table Table de styles de tracé
529 Plot Style... Style de tracé...
530 Plot Style Manager Gestionnaire des styles de tracé
531 Plot Style Manager... Gestionnaire des styles de tracé...
532 Plot Styles... Styles de tracé...
533 New CTB Nouvelle CTB
534 New STB Nouvelle STB
535 New style Nouveau style
536 Reset Panel Layout Réinitialiser la disposition des panneaux
537 Use for this layout Utiliser pour cet espace
538 Display plot styles Afficher les styles de tracé
539 No plot style tables. Aucune table de styles de tracé.
540 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.
541 Keep an editable copy of this table in the drawing Conserver une copie modifiable de cette table dans le dessin
542 Layout {name}: {table} Espace {name} : {table}
543 Grayscale Niveaux de gris
544 Screening Trame
545 Use object linetype Utiliser le type de ligne de l'objet
546 Use object lineweight Utiliser l'épaisseur de ligne de l'objet
547 Remove Supprimer
548 Add Ajouter
549 Open a drawing first. Ouvrez d'abord un dessin.
550 Plot table type Type de table de tracé
551 Color-dependent Dépendante des couleurs
552 Named Nommée
553 Plus/minus Plus/moins
554 Point Point
555 Point Filters Filtres de points
556 Point Style... Style de point...
557 Polar Array Réseau polaire
558 Polar Tracking Recherche polaire
559 Polar Tracking (F10) Recherche polaire (F10)
560 Polar Tracking On (F10) Activer la recherche polaire (F10)
561 Polar: {distance} < {angle}° Polaire : {distance} < {angle}°
562 Polygon Polygone
563 Polygonal Polygonal
564 Polyline Polyligne
565 Position X Position X
566 Position Y Position Y
567 Position Z Position Z
568 Previous Précédent
569 Previous Layer Calque précédent
570 Primitives Primitives
571 Print Imprimer
572 Print... Imprimer...
573 Properties Propriétés
574 Properties Inspector Inspecteur de propriétés
575 Properties: Propriétés :
576 Proxy Objet proxy
577 Purge Purger
578 Pyramid Pyramide
579 Quadrant Quadrant
580 Query Entities Interroger les entités
581 Quick Rapide
582 Quick Dimension Cotation rapide
583 Quick Properties Propriétés rapides
584 Quick Select Sélection rapide
585 Quick Select Properties Propriétés de la sélection rapide
586 Quick Select... Sélection rapide...
587 QuickCalc Calculatrice rapide
588 Quit CADCraft Quitter CADCraft
589 Radius Rayon
590 Raster Image Image raster
591 Ray Rayon
592 Read-only Lecture seule
593 Real name Nom réel
594 Realtime Temps réel
595 Reassociate Dimensions Réassocier les cotes
596 Recent Récents
597 Recover... Récupérer...
598 Rectangle Rectangle
599 Rectangular Rectangulaire
600 Rectangular Array Réseau rectangulaire
601 Red Rouge
602 Redo Rétablir
603 Redo (⇧⌘Z) Rétablir (⇧⌘Z)
604 Redraw Redessiner
605 Regen Régénérer
606 Regen All Régénérer tout
607 Region Région
608 Region/Mass Properties Propriétés de région/masse
609 Relative Relatif
610 Remove Leader Supprimer la ligne de repère
611 Remove the viewport override Supprimer le remplacement de la fenêtre
612 Rename Dimension Style Renommer le style de cote
613 Rename Layer Renommer le calque
614 Rename Layer State Renommer l'état des calques
615 Rename Layout Renommer l'espace
616 Rename Text Style Renommer le style de texte
617 Rename... Renommer...
618 Repeat {command} Répéter {command}
619 Requires a point (x,y, @dx,dy or @d<a). Nécessite un point (x,y, @dx,dy ou @d<a).
620 Reset Palettes Réinitialiser les palettes
621 Restore Restaurer
622 Restore Layer State Restaurer l'état des calques
623 Reverse Inverser
624 Reverse the order Inverser l'ordre
625 Revision Cloud Nuage de révisions
626 Revolve Révolution
627 Right Droite
628 Rotate Pivoter
629 Rotation Rotation
630 S S
631 Save Enregistrer
632 Save (⌘S) Enregistrer (⌘S)
633 Save As Enregistrer sous
634 Save As... Enregistrer sous...
635 Save Layer State Enregistrer l'état des calques
636 Save State Enregistrer l'état
637 Scale Échelle
638 Search for layer Rechercher un calque
639 Search for parameter Rechercher un paramètre
640 Second and next points: Deuxième point et points suivants :
641 Select Sélectionner
642 Select 2D polyline Sélectionner une polyligne 2D
643 Select All Sélectionner tout
644 Select Objects Sélectionner les objets
645 Select a 2D polyline. Sélectionner une polyligne 2D.
646 Select a block reference. Sélectionner une référence de bloc.
647 Select a circle or arc. Sélectionner un cercle ou un arc.
648 Select a closed polyline Sélectionner une polyligne fermée
649 Select a closed polyline, circle, ellipse or closed spline. Sélectionner une polyligne fermée, un cercle, une ellipse ou une spline fermée.
650 Select a closed polyline. Sélectionner une polyligne fermée.
651 Select a curve as the path. Sélectionner une courbe comme trajectoire.
652 Select a dimension constraint type Sélectionner un type de contrainte dimensionnelle
653 Select a line object Sélectionner une ligne
654 Select a line object. Sélectionner une ligne.
655 Select a line or an arc. Sélectionner une ligne ou un arc.
656 Select a line or polyline segment Sélectionner une ligne ou un segment de polyligne
657 Select a line, arc or open polyline. Sélectionner une ligne, un arc ou une polyligne ouverte.
658 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.
659 Select a line, circle, arc or polyline segment. Sélectionner une ligne, un cercle, un arc ou un segment de polyligne.
660 Select a line. Sélectionner une ligne.
661 Select a polyline. Sélectionner une polyligne.
662 Select a text, mtext, attribute definition or dimension. Sélectionner un texte, un texte multiligne, une définition d'attribut ou une cote.
663 Select an annotation object Sélectionner un objet d'annotation
664 Select an arc or circle Sélectionner un arc ou un cercle
665 Select an arc. Sélectionner un arc.
666 Select an object Sélectionner un objet
667 Select an object to change or Sélectionner un objet à modifier ou
668 Select an object to measure or Sélectionner un objet à mesurer ou
669 Select an object. Sélectionner un objet.
670 Select an open curve. Sélectionner une courbe ouverte.
671 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.
672 Select arc or circle Sélectionner un arc ou un cercle
673 Select arc or polyline arc segment Sélectionner un arc ou un segment d'arc de polyligne
674 Select arc style Sélectionner un style d'arc
675 Select arc, circle, line Sélectionner un arc, un cercle, une ligne
676 Select at least two objects to join. Sélectionner au moins deux objets à joindre.
677 Select circle or arc to add center mark Sélectionner un cercle ou un arc pour ajouter une marque de centre
678 Select closed objects (regions, closed polylines, circles). Sélectionner des objets fermés (régions, polylignes fermées, cercles).
679 Select first line Sélectionner la première ligne
680 Select first line or arc Sélectionner la première ligne ou le premier arc
681 Select first object Sélectionner le premier objet
682 Select first object or Sélectionner le premier objet ou
683 Select first point Sélectionner le premier point
684 Select nested objects to copy or Sélectionner des objets imbriqués à copier ou
685 Select object Sélectionner un objet
686 Select object to dimension Sélectionner un objet à coter
687 Select object to divide Sélectionner un objet à diviser
688 Select object to extend or shift-select to trim Sélectionner un objet à prolonger ou sélectionner avec Maj pour rogner
689 Select object to measure Sélectionner un objet à mesurer
690 Select object to offset Sélectionner un objet à décaler
691 Select object to trim or shift-select to extend Sélectionner un objet à rogner ou sélectionner avec Maj pour prolonger
692 Select objects Sélectionner les objets
693 Select objects (crossing window first corner) Sélectionner les objets (premier coin de la fenêtre de capture)
694 Select objects to array. Sélectionner les objets à répartir en réseau.
695 Select objects to stretch by crossing-window or crossing-polygon... Sélectionner les objets à étirer par fenêtre de capture ou polygone de capture...
696 Select objects: Sélectionner les objets :
697 Select one polyline (Multiple is available through the JSON form). Sélectionner une polyligne (l'option Multiple est disponible via le formulaire JSON).
698 Select path curve Sélectionner la courbe de trajectoire
699 Select point or object Sélectionner un point ou un objet
700 Select polyline or Sélectionner une polyligne ou
701 Select second line Sélectionner la deuxième ligne
702 Select second line or shift-select to apply corner Sélectionner la deuxième ligne ou sélectionner avec Maj pour former le coin
703 Select second object Sélectionner le deuxième objet
704 Select second object or shift-select to apply corner Sélectionner le deuxième objet ou sélectionner avec Maj pour former le coin
705 Select second point Sélectionner le deuxième point
706 Select spline Sélectionner une spline
707 Select symmetry line Sélectionner l'axe de symétrie
708 Select two points Sélectionner deux points
709 Selection Cycling Cycle de sélection
710 Send to Back Envoyer à l'arrière-plan
711 Set Current Définir comme courant
712 Set Current Dimension Style Définir le style de cote courant
713 Set Current Text Style Définir le style de texte courant
714 Set Layer Properties Définir les propriétés du calque
715 Set Properties Définir les propriétés
716 Set Table Cell Définir la cellule de tableau
717 Set Variable Définir la variable
718 Set View Définir la vue
719 Share Partager
720 Show Afficher
721 Show All Afficher tout
722 Show All Constraint Bars Afficher toutes les barres de contrainte
723 Show All Dynamic Dimensions Afficher toutes les cotes dynamiques
724 Show Layer List Afficher la liste des calques
725 Show/Hide Lineweight Afficher/Masquer l'épaisseur des lignes
726 Show/Hide Transparency Afficher/Masquer la transparence
727 Single Line Text Texte sur une ligne
728 Single Point Point unique
729 Single... Unique...
730 Smooth Lissage
731 Snap Accrochage
732 Snap Mode Mode d'accrochage
733 Snap Mode (F9) Mode d'accrochage (F9)
734 Snap On (F9) Activer l'accrochage (F9)
735 Snap X spacing Pas d'accrochage X
736 Snap and Grid Accrochage et grille
737 Solid Solide
738 Sort Trier
739 Specify a point Spécifier un point
740 Specify a scale for the hatch pattern Spécifier une échelle pour le motif de hachures
741 Specify a second extension line origin Spécifier l'origine de la deuxième ligne d'attache
742 Specify an angle for the hatch pattern Spécifier un angle pour le motif de hachures
743 Specify angle end point Spécifier le point final de l'angle
744 Specify angle of dimension line Spécifier l'angle de la ligne de cote
745 Specify angle of dimension text Spécifier l'angle du texte de cote
746 Specify angle start point Spécifier le point de départ de l'angle
747 Specify angle vertex Spécifier le sommet de l'angle
748 Specify angle vertex point Spécifier le point du sommet de l'angle
749 Specify arc length dimension location Spécifier l'emplacement de la cote de longueur d'arc
750 Specify axis endpoint of ellipse Spécifier l'extrémité de l'axe de l'ellipse
751 Specify base diameter Spécifiez le diamètre de la base
752 Specify base point Spécifiez le point de base
753 Specify base point or Spécifiez le point de base ou
754 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)
755 Specify base radius or Spécifiez le rayon de base ou
756 Specify center of donut Spécifiez le centre de l'anneau
757 Specify center of ellipse Spécifiez le centre de l'ellipse
758 Specify center of polygon Spécifiez le centre du polygone
759 Specify center point for circle Spécifiez le point central du cercle
760 Specify center point of arc Spécifiez le point central de l'arc
761 Specify center point of array Spécifiez le point central de la matrice
762 Specify center point of base Spécifiez le point central de la base
763 Specify corner of viewport Spécifiez le coin de la fenêtre
764 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)
765 Specify diameter of circle Spécifiez le diamètre du cercle
766 Specify dimension arc line location Spécifiez l'emplacement de l'arc de cote
767 Specify dimension line location Spécifiez l'emplacement de la ligne de cote
768 Specify distance between turns Spécifiez la distance entre les spires
769 Specify distance to other axis Spécifiez la distance jusqu'à l'autre axe
770 Specify end angle Spécifiez l'angle de fin
771 Specify end point of arc Spécifiez le point final de l'arc
772 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)
773 Specify ending width Spécifiez la largeur de fin
774 Specify ending width for next segment Spécifiez la largeur de fin du segment suivant
775 Specify endpoint of arc Spécifiez le point final de l'arc
776 Specify endpoint of axis Spécifiez le point final de l'axe
777 Specify endpoint of line Spécifiez le point final de la ligne
778 Specify feature location Spécifiez l'emplacement de l'élément
779 Specify fillet radius Spécifiez le rayon de congé
780 Specify fillet radius for rectangles Spécifiez le rayon de congé des rectangles
781 Specify first angle endpoint Spécifiez le premier point final de l'angle
782 Specify first chamfer distance Spécifiez la première distance de chanfrein
783 Specify first chamfer distance for rectangles Spécifiez la première distance de chanfrein des rectangles
784 Specify first constraint point Spécifiez le premier point de contrainte
785 Specify first corner Spécifiez le premier coin
786 Specify first corner point Spécifiez le premier point d'angle
787 Specify first destination point Spécifiez le premier point de destination
788 Specify first end point of circle's diameter Spécifiez le premier point final du diamètre du cercle
789 Specify first endpoint of edge Spécifiez le premier point final de l'arête
790 Specify first extension line origin Spécifiez l'origine de la première ligne d'attache
791 Specify first point Spécifiez le premier point
792 Specify first point of mirror line Spécifiez le premier point de la ligne de symétrie
793 Specify first point on circle Spécifiez le premier point sur le cercle
794 Specify first point on circle: _tan to Spécifiez le premier point sur le cercle : _tan à
795 Specify first source point Spécifiez le premier point source
796 Specify height Spécifiez la hauteur
797 Specify helix height or Spécifiez la hauteur de l'hélice ou
798 Specify included angle Spécifiez l'angle inclus
799 Specify insertion base point Spécifiez le point de base d'insertion
800 Specify insertion point Spécifiez le point d'insertion
801 Specify inside diameter of donut Spécifiez le diamètre intérieur de l'anneau
802 Specify leader arrowhead location Spécifiez l'emplacement de la tête de flèche du repère
803 Specify leader endpoint Spécifiez le point final du repère
804 Specify leader landing location Spécifiez l'emplacement de la base du repère
805 Specify length for rectangles Spécifiez la longueur des rectangles
806 Specify length of chord Spécifiez la longueur de la corde
807 Specify length of segment Spécifiez la longueur du segment
808 Specify line width for rectangles Spécifiez la largeur de ligne des rectangles
809 Specify location for new vertex Spécifiez l'emplacement du nouveau sommet
810 Specify minimum length of arc Spécifiez la longueur minimale de l'arc
811 Specify new end point Spécifiez le nouveau point final
812 Specify new location for marked vertex Spécifiez le nouvel emplacement du sommet marqué
813 Specify new width for all segments Spécifiez la nouvelle largeur de tous les segments
814 Specify next point Spécifiez le point suivant
815 Specify offset distance Spécifiez la distance de décalage
816 Specify offset distance or Spécifiez la distance de décalage ou
817 Specify opposite corner Spécifiez le coin opposé
818 Specify other corner point Spécifiez l'autre point d'angle
819 Specify other endpoint of axis Spécifiez l'autre point final de l'axe
820 Specify outside diameter of donut Spécifiez le diamètre extérieur de l'anneau
821 Specify point Spécifiez le point
822 Specify point on object for first tangent of circle Spécifiez le point sur l'objet pour la première tangente du cercle
823 Specify point on object for second tangent of circle Spécifiez le point sur l'objet pour la deuxième tangente du cercle
824 Specify point on side to offset Spécifiez le point du côté à décaler
825 Specify radius of arc Spécifiez le rayon de l'arc
826 Specify radius of circle Spécifiez le rayon du cercle
827 Specify rotation angle Spécifiez l'angle de rotation
828 Specify rotation angle of text Spécifiez l'angle de rotation du texte
829 Specify rotation angle or Spécifiez l'angle de rotation ou
830 Specify scale factor Spécifiez le facteur d'échelle
831 Specify second angle endpoint Spécifiez le deuxième point final de l'angle
832 Specify second break point Spécifiez le deuxième point de rupture
833 Specify second chamfer distance for rectangles Spécifiez la deuxième distance de chanfrein des rectangles
834 Specify second constraint point Spécifiez le deuxième point de contrainte
835 Specify second destination point Spécifiez le deuxième point de destination
836 Specify second end point of circle's diameter Spécifiez le deuxième point final du diamètre du cercle
837 Specify second endpoint of edge Spécifiez le deuxième point final de l'arête
838 Specify second extension line origin Spécifiez l'origine de la deuxième ligne d'attache
839 Specify second point Spécifiez le deuxième point
840 Specify second point of arc Spécifiez le deuxième point de l'arc
841 Specify second point of mirror line Spécifiez le deuxième point de la ligne de symétrie
842 Specify second point on circle Spécifiez le deuxième point sur le cercle
843 Specify second point on circle: _tan to Spécifiez le deuxième point sur le cercle : _tan à
844 Specify second point or <use first point as displacement> Spécifiez le deuxième point ou <utiliser le premier point comme déplacement>
845 Specify second source point Spécifiez le deuxième point source
846 Specify side to offset Spécifiez le côté à décaler
847 Specify start angle Spécifiez l'angle de départ
848 Specify start point Spécifiez le point de départ
849 Specify start point of arc Spécifiez le point de départ de l'arc
850 Specify start point of polyline Spécifiez le point de départ de la polyligne
851 Specify start point of text Spécifiez le point de départ du texte
852 Specify starting width Spécifiez la largeur de départ
853 Specify starting width for next segment Spécifiez la largeur de départ du segment suivant
854 Specify tangent direction for the start point of arc Spécifiez la direction de la tangente au point de départ de l'arc
855 Specify the elevation for rectangles Spécifiez l'élévation des rectangles
856 Specify thickness for rectangles Spécifiez l'épaisseur des rectangles
857 Specify third point on circle Spécifiez le troisième point sur le cercle
858 Specify third point on circle: _tan to Spécifiez le troisième point sur le cercle : _tan à
859 Specify third source point or <continue> Spécifiez le troisième point source ou <continuer>
860 Specify through point Spécifiez le point de passage
861 Specify top diameter Spécifiez le diamètre supérieur
862 Specify top radius or Spécifiez le rayon supérieur ou
863 Specify total angle Spécifiez l'angle total
864 Specify total length or Spécifiez la longueur totale ou
865 Specify width for rectangles Spécifiez la largeur des rectangles
866 Sphere Sphère
867 Spline Spline
868 Stack (type 1/2 then select) Empilement (tapez 1/2, puis sélectionnez)
869 Standard Standard
870 Start Départ
871 Start Width Largeur de départ
872 Start X X de départ
873 Start Y Y de départ
874 Start angle Angle de départ
875 Start, Center, Angle Départ, Centre, Angle
876 Start, Center, End Départ, Centre, Fin
877 Start, Center, Length Départ, Centre, Longueur
878 Start, End, Angle Départ, Fin, Angle
879 Start, End, Direction Départ, Fin, Direction
880 Start, End, Radius Départ, Fin, Rayon
881 Status État
882 Status Bar Barre d'état
883 Stretch Étirer
884 Style: Standard Style : Standard
885 Subtract Soustraction
886 Sweep Balayage
887 Switch Drawing Changer de dessin
888 Switch Layout Changer de présentation
889 Switch between model space in a viewport and paper space Basculer entre l'espace modèle dans une fenêtre et l'espace papier
890 Symmetric Symétrie
891 Synchronize Attributes Synchroniser les attributs
892 Toggle PICKADD (off: each pick replaces the selection) Basculer PICKADD (désactivé : chaque sélection remplace la sélection existante)
893 Toggle PICKADD (on: picks add to the selection) Basculer PICKADD (activé : les sélections s'ajoutent à la sélection existante)
894 TOP HAUT
895 Table Tableau
896 Table Style... Style de tableau...
897 Table style Style de tableau
898 Table... Tableau...
899 Tan, Tan, Radius Tangente, Tangente, Rayon
900 Tan, Tan, Tan Tangente, Tangente, Tangente
901 Tangent Tangente
902 Text Texte
903 Text Editor Éditeur de texte
904 Text Height Hauteur du texte
905 Text Style... Style de texte...
906 Text height Hauteur du texte
907 Text override Texte de remplacement
908 Text style Style de texte
909 Thaw All Layers Dégeler tous les calques
910 There is nothing here yet. Il n'y a encore rien ici.
911 Thickness Épaisseur
912 Time Heure
913 To get started, create or open a drawing. Pour commencer, créez ou ouvrez un dessin.
914 Tool Bar Barre d'outils
915 Tool Sets Ensembles d'outils
916 Tools Outils
917 Torus Tore
918 Trace Trace
919 Transparency Transparence
920 Trim Ajuster
921 True Color Couleur vraie
922 Turn All Layers On Activer tous les calques
923 Turn Off Layer Désactiver le calque
924 Turn Quick Properties off (QPMODE) Désactiver les propriétés rapides (QPMODE)
925 Type Type
926 Type a command Tapez une commande
927 Type text; Enter starts a new paragraph Saisissez le texte ; Entrée crée un nouveau paragraphe
928 UCS Icon Icône du SCU
929 Undo Annuler
930 Undo (⌘Z) Annuler (⌘Z)
931 Ungroup Dissocier
932 Union Union
933 Unisolate Layer Annuler l'isolement des calques
934 Units... Unités...
935 Unlock Layer Déverrouiller le calque
936 Unmerge Cells Annuler la fusion des cellules
937 Unsaved Layer State État de calque non enregistré
938 Up Haut
939 Update Mettre à jour
940 Use System Setting Utiliser le paramètre système
941 User Utilisateur
942 User Parameters Paramètres utilisateur
943 Username Nom d'utilisateur
944 VP Color Couleur de la fenêtre
945 VP Freeze Gel dans la fenêtre
946 Value Valeur
947 Value: Valeur :
948 Version Version
949 Version {version} Version {version}
950 Vertical Vertical
951 VerticalDistance VerticalDistance
952 View Vue
953 ViewCube ViewCube
954 Viewport Fenêtre
955 Viewport Layer Freeze Gel du calque dans la fenêtre
956 Viewport Properties Propriétés de la fenêtre
957 Viewport {id} Fenêtre {id}
958 Viewports Fenêtres
959 W O
960 WCS ⌄ SCG ⌄
961 White Blanc
962 Width factor Facteur de largeur
963 Window Fenêtre
964 Wipeout Masquage
965 Workspace Switching Changement d'espace de travail
966 Write Block Enregistrer le bloc dans un fichier
967 Xline Ligne de construction
968 Yellow Jaune
969 Yes Oui
970 Zoom Zoom
971 Zoom All Zoom tout
972 Zoom Extents Zoom étendu
973 Zoom In Zoom avant
974 Zoom Object Zoom objet
975 Zoom Out Zoom arrière
976 Zoom Previous Zoom précédent
977 Zoom Window Zoom fenêtre
978 error erreur
979 {contributors} · {commits} {contributors} · {commits}
980 {kind} (constraint {id}) {kind} (contrainte {id})
981 {kind} constraint {name} = {expression} (double-click to edit) Contrainte {kind} {name} = {expression} (double-cliquez pour modifier)
982 {name}: {distance} < {angle}° {name} : {distance} < {angle}°
983 ƒx New User Parameter ƒx Nouveau paramètre utilisateur
984 ΔLOC ΔLOC
985 −Bin −Bin
986 −LOC −LOC
987 ⌘↩ to finish ⌘↩ pour terminer
988 @plural {n} commit {n} révision|{n} révisions
989 @plural {n} contributor {n} contributeur|{n} contributeurs
990 Drawing as bytes Dessin sous forme d'octets
991 Services Services
992 Hide CADCraft Masquer CADCraft
993 Hide Others Masquer les autres
994 Accessibility Accessibilité
995 Show System Cursor Afficher le curseur système
996 Import... Importer...
997 Export... Exporter...
998 Batch Publish... Publication par lots...
999 Hatch Pattern Motif de hachure
1000 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
@@ -108,6 +108,7 @@
Close All Fechar tudo
Closed Fechada
Coincident Coincidente
Collapse Palettes Recolher paletas
Collapse Tool Sets Recolher conjuntos de ferramentas
Collinear Colinear
Color Cor
@@ -523,6 +524,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
@@ -993,3 +995,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.
108
109
110
111
112
113
114
524
525
526
527
528
529
530
995
996
997
998
999
+1000
View File
@@ -0,0 +1,1000 @@
# 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 Объединить
Join the ArtCraft Discord… Присоединяйтесь к Discord ArtCraft…
Join us on Discord Присоединяйтесь к нам в Discord
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 Включить/выключить
One of the Crafting Apps by the ArtCraft team and community. Одна из программ Crafting Apps от команды и сообщества ArtCraft.
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 Join the ArtCraft Discord… Присоединяйтесь к Discord ArtCraft…
360 Join us on Discord Присоединяйтесь к нам в Discord
361 Language Язык
362 Last Последний
363 Last commit Последний коммит
364 Layer Слой
365 Layer Delete Удалить слой с объектами
366 Layer Match Согласовать слой
367 Layer Merge Объединить слои
368 Layer Off Выключить слой
369 Layer Properties Свойства слоя
370 Layer Properties Manager Диспетчер свойств слоев
371 Layer Properties Manager — current viewport Диспетчер свойств слоев — текущий видовой экран
372 Layer State Состояние слоев
373 Layer States Manager... Диспетчер состояний слоев...
374 Layer Tools Инструменты слоев
375 Layer contains objects (double-click to make current) Слой содержит объекты (дважды щелкните, чтобы сделать текущим)
376 Layer is empty (double-click to make current) Слой пуст (дважды щелкните, чтобы сделать текущим)
377 Layers Слои
378 Layers Palette Палитра слоев
379 Layers displayed: {shown} of {total}. Current layer: {layer} Показано слоев: {shown} из {total}. Текущий слой: {layer}
380 Layout Лист
381 Layout list Список листов
382 Leader Выноска
383 Left Слева
384 Length Длина
385 Length X Длина X
386 Length Y Длина Y
387 Lengthen Изменить длину
388 Light Светлая
389 Line Отрезок
390 Line delta Изменение количества строк
391 Linear Линейный
392 Lines +/− Строки +/−
393 Lines added Добавленные строки
394 Lines deleted Удаленные строки
395 Linetype Тип линии
396 Linetype Scale Масштаб типа линии
397 Linetype scale Масштаб типа линии
398 Ltscale Масштаб типа линии
399 Linetype... Тип линии...
400 Lineweight Вес линии
401 Lineweight... Вес линии...
402 List Список
403 List Blocks Список блоков
404 List Dimension Styles Список размерных стилей
405 List Groups Список групп
406 List Layer States Список состояний слоев
407 List Layouts Список листов
408 List System Variables Список системных переменных
409 List Text Styles Список текстовых стилей
410 Load all Загрузить все
411 Load... Загрузить...
412 Loaded linetypes Загруженные типы линий
413 Lock Заблокировать
414 Lock Layer Заблокировать слой
415 Lock/Unlock Заблокировать/разблокировать
416 Loft Тело по сечениям
417 MIT OR Apache-2.0. Not affiliated with Autodesk, Inc. MIT OR Apache-2.0. Не аффилировано с Autodesk, Inc.
418 MODEL МОДЕЛЬ
419 MText Многострочный текст
420 Magenta Пурпурный
421 Major line every Основная линия каждые
422 Make Current Сделать текущим
423 Make Object's Layer Current Сделать слой объекта текущим
424 Make... Создать...
425 Match Layer Согласовать слой
426 Match Properties Копирование свойств
427 Measure Разметить
428 Measure Distance Измерить расстояние
429 Measure Geometry Измерить геометрию
430 Merge Cells Объединить ячейки
431 Merged PRs Принятые PR
432 Mid Between 2 Points Середина между двумя точками
433 Midpoint Середина
434 Mirror Зеркальное отражение
435 Model Модель
436 Model Space (in viewport) Пространство модели (в видовом экране)
437 Modeling Моделирование
438 Models Модели
439 Modify Модификация
440 Move Переместить
441 Multileader Мультивыноска
442 Multileader Style... Стиль мультивыносок...
443 Multileader style Стиль мультивыносок
444 Multiline Мультилиния
445 Multiline Text Многострочный текст
446 Multiline Text... Многострочный текст...
447 Multiple Разные типы
448 Multiple Point Несколько точек
449 My Мои
450 My properties: not available yet Мои свойства: пока недоступно
451 N С
452 Name Название
453 Name (A–Z) Название (А–Я)
454 Nearest Ближайшая точка
455 New Создать
456 New (metric) Создать (метрический)
457 New Drawing Новый чертеж
458 New Drawing (⌘N) Новый чертеж (⌘N)
459 New Drawing... Новый чертеж...
460 New Layer Новый слой
461 New Layout Новый лист
462 New VP Freeze Заморозить в новых ВЭ
463 New Viewports... Новые видовые экраны...
464 New layout Новый лист
465 New state name Название нового состояния
466 No Нет
467 No blocks defined in this drawing. В этом чертеже нет определенных блоков.
468 No contributor data was built into this copy. В эту сборку не включены данные об участниках.
469 No model credits were built into this copy. В эту сборку не включены данные о моделях.
470 No saved layer states Нет сохраненных состояний слоев
471 No selection Ничего не выделено
472 Node Узел
473 None Нет
474 Not available Недоступно
475 Not available yet Пока недоступно
476 OK ОК
477 Object Объект
478 Object Snap Объектная привязка
479 Object Snap (F3) Объектная привязка (F3)
480 Object Snap On (F3) Включить объектную привязку (F3)
481 Object Snap Tracking Отслеживание объектной привязки
482 Object Snap Tracking (F11) Отслеживание объектной привязки (F11)
483 Object type: Тип объекта:
484 Off Выключено
485 Offset Смещение
486 On Включено
487 On/Off Включить/выключить
488 One of the Crafting Apps by the ArtCraft team and community. Одна из программ Crafting Apps от команды и сообщества ArtCraft.
489 Open (⌘O) Открыть (⌘O)
490 Open Sample Drawing Открыть образец чертежа
491 Open sample drawing Открыть образец чертежа
492 Open... Открыть...
493 Operator: Оператор:
494 Orbit Орбита
495 Ordinate Ординатный
496 Ortho (F8) Орто (F8)
497 Ortho Mode Режим Орто
498 Ortho Mode (F8) Режим Орто (F8)
499 Osnap Settings... Параметры объектной привязки...
500 Out Отдалить
501 Override Переопределить
502 PAPER ЛИСТ
503 Percent Процент
504 Polar (distance < angle) Полярные (расстояние < угол)
505 PRs PR
506 Page Setup Manager Диспетчер параметров листов
507 Page Setup Manager... Диспетчер параметров листов...
508 Pan Панорамирование
509 Pan Down Панорамировать вниз
510 Pan Left Панорамировать влево
511 Pan Right Панорамировать вправо
512 Pan Up Панорамировать вверх
513 Paper Space Пространство листа
514 Parallel Параллельность
515 Parameters Manager Диспетчер параметров
516 Parametric Параметризация
517 Paste Вставить
518 Paste as Block Вставить как блок
519 Paste to Original Coordinates Вставить в исходные координаты
520 Path Array Массив по траектории
521 Pattern Шаблон
522 Perpendicular Перпендикулярность
523 Plot Печать чертежа
524 Plot (click: do not plot) Печатать (щелкните, чтобы отключить печать)
525 Plot Preview Предварительный просмотр печати
526 Plot style Стиль печати
527 Plot style attached to Стиль печати применен к
528 Plot style table Таблица стилей печати
529 Plot Style... Стиль печати...
530 Plot Style Manager Диспетчер стилей печати
531 Plot Style Manager... Диспетчер стилей печати...
532 Plot Styles... Стили печати...
533 New CTB Новая CTB
534 New STB Новая STB
535 New style Новый стиль
536 Reset Panel Layout Сбросить расположение панелей
537 Use for this layout Использовать для этого листа
538 Display plot styles Показывать стили печати
539 No plot style tables. Нет таблиц стилей печати.
540 A built-in table: editing it keeps an edited copy in the drawing. Встроенная таблица: изменения сохраняются как ее копия в чертеже.
541 Keep an editable copy of this table in the drawing Сохранить в чертеже копию этой таблицы для редактирования
542 Layout {name}: {table} Лист {name}: {table}
543 Grayscale Оттенки серого
544 Screening Тонирование
545 Use object linetype Тип линии объекта
546 Use object lineweight Вес линии объекта
547 Remove Удалить
548 Add Добавить
549 Open a drawing first. Сначала откройте чертеж.
550 Plot table type Тип таблицы печати
551 Color-dependent Зависимая от цвета
552 Named Именованная
553 Plus/minus Плюс/минус
554 Point Точка
555 Point Filters Фильтры точек
556 Point Style... Стиль точек...
557 Polar Array Полярный массив
558 Polar Tracking Полярное отслеживание
559 Polar Tracking (F10) Полярное отслеживание (F10)
560 Polar Tracking On (F10) Включить полярное отслеживание (F10)
561 Polar: {distance} < {angle}° Полярное: {distance} < {angle}°
562 Polygon Многоугольник
563 Polygonal Многоугольный
564 Polyline Полилиния
565 Position X Позиция X
566 Position Y Позиция Y
567 Position Z Позиция Z
568 Previous Предыдущий
569 Previous Layer Предыдущий слой
570 Primitives Примитивы
571 Print Печать
572 Print... Печать...
573 Properties Свойства
574 Properties Inspector Инспектор свойств
575 Properties: Свойства:
576 Proxy Прокси-объект
577 Purge Очистить неиспользуемое
578 Pyramid Пирамида
579 Quadrant Квадрант
580 Query Entities Запрос объектов
581 Quick Быстрый
582 Quick Dimension Быстрый размер
583 Quick Properties Быстрые свойства
584 Quick Select Быстрый выбор
585 Quick Select Properties Свойства быстрого выбора
586 Quick Select... Быстрый выбор...
587 QuickCalc Быстрокалькулятор
588 Quit CADCraft Выйти из CADCraft
589 Radius Радиус
590 Raster Image Растровое изображение
591 Ray Луч
592 Read-only Только для чтения
593 Real name Настоящее имя
594 Realtime В реальном времени
595 Reassociate Dimensions Восстановить ассоциативность размеров
596 Recent Недавние
597 Recover... Восстановить...
598 Rectangle Прямоугольник
599 Rectangular Прямоугольный
600 Rectangular Array Прямоугольный массив
601 Red Красный
602 Redo Повторить
603 Redo (⇧⌘Z) Повторить (⇧⌘Z)
604 Redraw Перерисовать
605 Regen Регенерация
606 Regen All Регенерировать все
607 Region Область
608 Region/Mass Properties Свойства области/массы
609 Relative Относительные
610 Remove Leader Удалить выноску
611 Remove the viewport override Удалить переопределение видового экрана
612 Rename Dimension Style Переименовать размерный стиль
613 Rename Layer Переименовать слой
614 Rename Layer State Переименовать состояние слоев
615 Rename Layout Переименовать лист
616 Rename Text Style Переименовать текстовый стиль
617 Rename... Переименовать...
618 Repeat {command} Повторить {command}
619 Requires a point (x,y, @dx,dy or @d<a). Требуется точка (x,y, @dx,dy или @d<a).
620 Reset Palettes Сбросить палитры
621 Restore Восстановить
622 Restore Layer State Восстановить состояние слоев
623 Reverse Изменить направление
624 Reverse the order Изменить порядок на обратный
625 Revision Cloud Облако
626 Revolve Тело вращения
627 Right Справа
628 Rotate Повернуть
629 Rotation Угол поворота
630 S Ю
631 Save Сохранить
632 Save (⌘S) Сохранить (⌘S)
633 Save As Сохранить как
634 Save As... Сохранить как...
635 Save Layer State Сохранить состояние слоев
636 Save State Сохранить состояние
637 Scale Масштаб
638 Search for layer Поиск слоя
639 Search for parameter Поиск параметра
640 Second and next points: Вторая и следующие точки:
641 Select Выбрать
642 Select 2D polyline Выберите 2D-полилинию
643 Select All Выделить все
644 Select Objects Выбрать объекты
645 Select a 2D polyline. Выберите 2D-полилинию.
646 Select a block reference. Выберите вхождение блока.
647 Select a circle or arc. Выберите круг или дугу.
648 Select a closed polyline Выберите замкнутую полилинию
649 Select a closed polyline, circle, ellipse or closed spline. Выберите замкнутую полилинию, круг, эллипс или замкнутый сплайн.
650 Select a closed polyline. Выберите замкнутую полилинию.
651 Select a curve as the path. Выберите кривую в качестве траектории.
652 Select a dimension constraint type Выберите тип размерного ограничения
653 Select a line object Выберите отрезок
654 Select a line object. Выберите отрезок.
655 Select a line or an arc. Выберите отрезок или дугу.
656 Select a line or polyline segment Выберите отрезок или сегмент полилинии
657 Select a line, arc or open polyline. Выберите отрезок, дугу или незамкнутую полилинию.
658 Select a line, arc, circle, polyline, ellipse or spline as the path. Выберите отрезок, дугу, круг, полилинию, эллипс или сплайн в качестве траектории.
659 Select a line, circle, arc or polyline segment. Выберите отрезок, круг, дугу или сегмент полилинии.
660 Select a line. Выберите отрезок.
661 Select a polyline. Выберите полилинию.
662 Select a text, mtext, attribute definition or dimension. Выберите текст, многострочный текст, определение атрибута или размер.
663 Select an annotation object Выберите аннотационный объект
664 Select an arc or circle Выберите дугу или круг
665 Select an arc. Выберите дугу.
666 Select an object Выберите объект
667 Select an object to change or Выберите объект для изменения или
668 Select an object to measure or Выберите объект для измерения или
669 Select an object. Выберите объект.
670 Select an open curve. Выберите незамкнутую кривую.
671 Select an open line, arc, polyline, ellipse arc or spline. Выберите незамкнутый отрезок, дугу, полилинию, эллиптическую дугу или сплайн.
672 Select arc or circle Выберите дугу или круг
673 Select arc or polyline arc segment Выберите дугу или дуговой сегмент полилинии
674 Select arc style Выберите стиль дуги
675 Select arc, circle, line Выберите дугу, круг, отрезок
676 Select at least two objects to join. Выберите не менее двух объектов для объединения.
677 Select circle or arc to add center mark Выберите круг или дугу для знака центра
678 Select closed objects (regions, closed polylines, circles). Выберите замкнутые объекты (области, замкнутые полилинии, круги).
679 Select first line Выберите первый отрезок
680 Select first line or arc Выберите первый отрезок или дугу
681 Select first object Выберите первый объект
682 Select first object or Выберите первый объект или
683 Select first point Выберите первую точку
684 Select nested objects to copy or Выберите вложенные объекты для копирования или
685 Select object Выберите объект
686 Select object to dimension Выберите объект для нанесения размера
687 Select object to divide Выберите объект для деления
688 Select object to extend or shift-select to trim Выберите объект для удлинения или выберите с Shift для обрезки
689 Select object to measure Выберите объект для разметки
690 Select object to offset Выберите объект для смещения
691 Select object to trim or shift-select to extend Выберите объект для обрезки или выберите с Shift для удлинения
692 Select objects Выберите объекты
693 Select objects (crossing window first corner) Выберите объекты (первый угол секущей рамки)
694 Select objects to array. Выберите объекты для массива.
695 Select objects to stretch by crossing-window or crossing-polygon... Выберите объекты для растягивания секущей рамкой или секущим многоугольником...
696 Select objects: Выберите объекты:
697 Select one polyline (Multiple is available through the JSON form). Выберите одну полилинию (Multiple доступно через JSON).
698 Select path curve Выберите кривую траектории
699 Select point or object Выберите точку или объект
700 Select polyline or Выберите полилинию или
701 Select second line Выберите второй отрезок
702 Select second line or shift-select to apply corner Выберите второй отрезок или выберите с Shift, чтобы построить угол
703 Select second object Выберите второй объект
704 Select second object or shift-select to apply corner Выберите второй объект или выберите с Shift, чтобы построить угол
705 Select second point Выберите вторую точку
706 Select spline Выберите сплайн
707 Select symmetry line Выберите ось симметрии
708 Select two points Выберите две точки
709 Selection Cycling Циклический выбор
710 Send to Back На задний план
711 Set Current Сделать текущим
712 Set Current Dimension Style Сделать размерный стиль текущим
713 Set Current Text Style Сделать текстовый стиль текущим
714 Set Layer Properties Задать свойства слоя
715 Set Properties Задать свойства
716 Set Table Cell Задать ячейку таблицы
717 Set Variable Задать переменную
718 Set View Задать вид
719 Share Поделиться
720 Show Показать
721 Show All Показать все
722 Show All Constraint Bars Показать все панели ограничений
723 Show All Dynamic Dimensions Показать все динамические размеры
724 Show Layer List Показать список слоев
725 Show/Hide Lineweight Показать/скрыть вес линий
726 Show/Hide Transparency Показать/скрыть прозрачность
727 Single Line Text Однострочный текст
728 Single Point Одна точка
729 Single... Один...
730 Smooth Сглаживание
731 Snap Привязка
732 Snap Mode Шаговая привязка
733 Snap Mode (F9) Шаговая привязка (F9)
734 Snap On (F9) Включить шаговую привязку (F9)
735 Snap X spacing Шаг привязки X
736 Snap and Grid Шаговая привязка и сетка
737 Solid Тело
738 Sort Сортировка
739 Specify a point Укажите точку
740 Specify a scale for the hatch pattern Укажите масштаб шаблона штриховки
741 Specify a second extension line origin Укажите начало второй выносной линии
742 Specify an angle for the hatch pattern Укажите угол шаблона штриховки
743 Specify angle end point Укажите конечную точку угла
744 Specify angle of dimension line Укажите угол размерной линии
745 Specify angle of dimension text Укажите угол размерного текста
746 Specify angle start point Укажите начальную точку угла
747 Specify angle vertex Укажите вершину угла
748 Specify angle vertex point Укажите точку вершины угла
749 Specify arc length dimension location Укажите положение размера длины дуги
750 Specify axis endpoint of ellipse Укажите конечную точку оси эллипса
751 Specify base diameter Укажите базовый диаметр
752 Specify base point Укажите базовую точку
753 Specify base point or Укажите базовую точку или
754 Specify base point or displacement (drag with the middle mouse button for realtime pan) Укажите базовую точку или смещение (перетаскивайте средней кнопкой мыши для панорамирования)
755 Specify base radius or Укажите базовый радиус или
756 Specify center of donut Укажите центр кольца
757 Specify center of ellipse Укажите центр эллипса
758 Specify center of polygon Укажите центр многоугольника
759 Specify center point for circle Укажите центр круга
760 Specify center point of arc Укажите центр дуги
761 Specify center point of array Укажите центр массива
762 Specify center point of base Укажите центр основания
763 Specify corner of viewport Укажите угол видового экрана
764 Specify corner of window, enter a scale factor (nX or nXP) Укажите угол рамки, введите масштабный коэффициент (nX или nXP)
765 Specify diameter of circle Укажите диаметр круга
766 Specify dimension arc line location Укажите положение размерной дуги
767 Specify dimension line location Укажите положение размерной линии
768 Specify distance between turns Укажите расстояние между витками
769 Specify distance to other axis Укажите расстояние до другой оси
770 Specify end angle Укажите конечный угол
771 Specify end point of arc Укажите конечную точку дуги
772 Specify end point of arc (hold Ctrl to switch direction) Укажите конечную точку дуги (удерживайте Ctrl для смены направления)
773 Specify ending width Укажите конечную ширину
774 Specify ending width for next segment Укажите конечную ширину следующего сегмента
775 Specify endpoint of arc Укажите конечную точку дуги
776 Specify endpoint of axis Укажите конечную точку оси
777 Specify endpoint of line Укажите конечную точку отрезка
778 Specify feature location Укажите положение элемента
779 Specify fillet radius Укажите радиус сопряжения
780 Specify fillet radius for rectangles Укажите радиус сопряжения прямоугольников
781 Specify first angle endpoint Укажите первую конечную точку угла
782 Specify first chamfer distance Укажите первое расстояние фаски
783 Specify first chamfer distance for rectangles Укажите первое расстояние фаски прямоугольников
784 Specify first constraint point Укажите первую точку ограничения
785 Specify first corner Укажите первый угол
786 Specify first corner point Укажите первую угловую точку
787 Specify first destination point Укажите первую целевую точку
788 Specify first end point of circle's diameter Укажите первую конечную точку диаметра круга
789 Specify first endpoint of edge Укажите первую конечную точку ребра
790 Specify first extension line origin Укажите начало первой выносной линии
791 Specify first point Укажите первую точку
792 Specify first point of mirror line Укажите первую точку оси зеркального отражения
793 Specify first point on circle Укажите первую точку на круге
794 Specify first point on circle: _tan to Укажите первую точку на круге: _tan к
795 Specify first source point Укажите первую исходную точку
796 Specify height Укажите высоту
797 Specify helix height or Укажите высоту спирали или
798 Specify included angle Укажите угол между сторонами
799 Specify insertion base point Укажите базовую точку вставки
800 Specify insertion point Укажите точку вставки
801 Specify inside diameter of donut Укажите внутренний диаметр кольца
802 Specify leader arrowhead location Укажите положение стрелки выноски
803 Specify leader endpoint Укажите конечную точку выноски
804 Specify leader landing location Укажите положение полки выноски
805 Specify length for rectangles Укажите длину прямоугольников
806 Specify length of chord Укажите длину хорды
807 Specify length of segment Укажите длину сегмента
808 Specify line width for rectangles Укажите ширину линий прямоугольников
809 Specify location for new vertex Укажите положение новой вершины
810 Specify minimum length of arc Укажите минимальную длину дуги
811 Specify new end point Укажите новую конечную точку
812 Specify new location for marked vertex Укажите новое положение отмеченной вершины
813 Specify new width for all segments Укажите новую ширину всех сегментов
814 Specify next point Укажите следующую точку
815 Specify offset distance Укажите расстояние смещения
816 Specify offset distance or Укажите расстояние смещения или
817 Specify opposite corner Укажите противоположный угол
818 Specify other corner point Укажите другую угловую точку
819 Specify other endpoint of axis Укажите другую конечную точку оси
820 Specify outside diameter of donut Укажите внешний диаметр кольца
821 Specify point Укажите точку
822 Specify point on object for first tangent of circle Укажите точку на объекте для первой касательной круга
823 Specify point on object for second tangent of circle Укажите точку на объекте для второй касательной круга
824 Specify point on side to offset Укажите точку со стороны смещения
825 Specify radius of arc Укажите радиус дуги
826 Specify radius of circle Укажите радиус круга
827 Specify rotation angle Укажите угол поворота
828 Specify rotation angle of text Укажите угол поворота текста
829 Specify rotation angle or Укажите угол поворота или
830 Specify scale factor Укажите масштабный коэффициент
831 Specify second angle endpoint Укажите вторую конечную точку угла
832 Specify second break point Укажите вторую точку разрыва
833 Specify second chamfer distance for rectangles Укажите второе расстояние фаски прямоугольников
834 Specify second constraint point Укажите вторую точку ограничения
835 Specify second destination point Укажите вторую целевую точку
836 Specify second end point of circle's diameter Укажите вторую конечную точку диаметра круга
837 Specify second endpoint of edge Укажите вторую конечную точку ребра
838 Specify second extension line origin Укажите начало второй выносной линии
839 Specify second point Укажите вторую точку
840 Specify second point of arc Укажите вторую точку дуги
841 Specify second point of mirror line Укажите вторую точку оси зеркального отражения
842 Specify second point on circle Укажите вторую точку на круге
843 Specify second point on circle: _tan to Укажите вторую точку на круге: _tan к
844 Specify second point or <use first point as displacement> Укажите вторую точку или <использовать первую точку как смещение>
845 Specify second source point Укажите вторую исходную точку
846 Specify side to offset Укажите сторону смещения
847 Specify start angle Укажите начальный угол
848 Specify start point Укажите начальную точку
849 Specify start point of arc Укажите начальную точку дуги
850 Specify start point of polyline Укажите начальную точку полилинии
851 Specify start point of text Укажите начальную точку текста
852 Specify starting width Укажите начальную ширину
853 Specify starting width for next segment Укажите начальную ширину следующего сегмента
854 Specify tangent direction for the start point of arc Укажите направление касательной в начальной точке дуги
855 Specify the elevation for rectangles Укажите уровень прямоугольников
856 Specify thickness for rectangles Укажите высоту выдавливания прямоугольников
857 Specify third point on circle Укажите третью точку на круге
858 Specify third point on circle: _tan to Укажите третью точку на круге: _tan к
859 Specify third source point or <continue> Укажите третью исходную точку или <продолжить>
860 Specify through point Укажите точку, через которую проходит прямая
861 Specify top diameter Укажите диаметр вершины
862 Specify top radius or Укажите радиус вершины или
863 Specify total angle Укажите полный угол
864 Specify total length or Укажите общую длину или
865 Specify width for rectangles Укажите ширину прямоугольников
866 Sphere Сфера
867 Spline Сплайн
868 Stack (type 1/2 then select) Дробь (введите 1/2, затем выберите)
869 Standard Стандартный
870 Start Начало
871 Start Width Начальная ширина
872 Start X Начало X
873 Start Y Начало Y
874 Start angle Начальный угол
875 Start, Center, Angle Начало, центр, угол
876 Start, Center, End Начало, центр, конец
877 Start, Center, Length Начало, центр, длина
878 Start, End, Angle Начало, конец, угол
879 Start, End, Direction Начало, конец, направление
880 Start, End, Radius Начало, конец, радиус
881 Status Состояние
882 Status Bar Строка состояния
883 Stretch Растянуть
884 Style: Standard Стиль: Standard
885 Subtract Вычесть
886 Sweep Тело по траектории
887 Switch Drawing Переключить чертеж
888 Switch Layout Переключить лист
889 Switch between model space in a viewport and paper space Переключить между пространством модели в видовом экране и пространством листа
890 Symmetric Симметрия
891 Synchronize Attributes Синхронизировать атрибуты
892 Toggle PICKADD (off: each pick replaces the selection) Переключить PICKADD (выкл.: каждый выбор заменяет набор выделения)
893 Toggle PICKADD (on: picks add to the selection) Переключить PICKADD (вкл.: выделение добавляется к набору)
894 TOP СВЕРХУ
895 Table Таблица
896 Table Style... Стиль таблиц...
897 Table style Стиль таблиц
898 Table... Таблица...
899 Tan, Tan, Radius Касательная, касательная, радиус
900 Tan, Tan, Tan Касательная, касательная, касательная
901 Tangent Касательность
902 Text Текст
903 Text Editor Текстовый редактор
904 Text Height Высота текста
905 Text Style... Текстовый стиль...
906 Text height Высота текста
907 Text override Переопределение текста
908 Text style Текстовый стиль
909 Thaw All Layers Разморозить все слои
910 There is nothing here yet. Здесь пока ничего нет.
911 Thickness Высота выдавливания
912 Time Время
913 To get started, create or open a drawing. Чтобы начать, создайте или откройте чертеж.
914 Tool Bar Панель инструментов
915 Tool Sets Наборы инструментов
916 Tools Инструменты
917 Torus Тор
918 Trace Широкая линия
919 Transparency Прозрачность
920 Trim Обрезать
921 True Color Цвет RGB
922 Turn All Layers On Включить все слои
923 Turn Off Layer Выключить слой
924 Turn Quick Properties off (QPMODE) Выключить быстрые свойства (QPMODE)
925 Type Тип
926 Type a command Введите команду
927 Type text; Enter starts a new paragraph Введите текст; Enter начинает новый абзац
928 UCS Icon Значок ПСК
929 Undo Отменить
930 Undo (⌘Z) Отменить (⌘Z)
931 Ungroup Разгруппировать
932 Union Объединение
933 Unisolate Layer Отменить изоляцию слоев
934 Units... Единицы...
935 Unlock Layer Разблокировать слой
936 Unmerge Cells Разъединить ячейки
937 Unsaved Layer State Несохраненное состояние слоев
938 Up Вверх
939 Update Обновить
940 Use System Setting Как в системе
941 User Пользователь
942 User Parameters Пользовательские параметры
943 Username Имя пользователя
944 VP Color Цвет ВЭ
945 VP Freeze Заморозка ВЭ
946 Value Значение
947 Value: Значение:
948 Version Версия
949 Version {version} Версия {version}
950 Vertical Вертикальность
951 VerticalDistance ВертикальноеРасстояние
952 View Вид
953 ViewCube Видовой куб
954 Viewport Видовой экран
955 Viewport Layer Freeze Заморозить слой в видовом экране
956 Viewport Properties Свойства видового экрана
957 Viewport {id} Видовой экран {id}
958 Viewports Видовые экраны
959 W З
960 WCS ⌄ МСК ⌄
961 White Белый
962 Width factor Коэффициент ширины
963 Window Окно
964 Wipeout Маскировка
965 Workspace Switching Переключение рабочих сред
966 Write Block Записать блок в файл
967 Xline Вспомогательная прямая
968 Yellow Желтый
969 Yes Да
970 Zoom Показать
971 Zoom All Показать все
972 Zoom Extents Показать границы объектов
973 Zoom In Увеличить
974 Zoom Object Показать объект
975 Zoom Out Уменьшить
976 Zoom Previous Предыдущий масштаб
977 Zoom Window Показать рамкой
978 error ошибка
979 {contributors} · {commits} {contributors} · {commits}
980 {kind} (constraint {id}) {kind} (ограничение {id})
981 {kind} constraint {name} = {expression} (double-click to edit) Ограничение «{kind}» {name} = {expression} (дважды щелкните для редактирования)
982 {name}: {distance} < {angle}° {name}: {distance} < {angle}°
983 ƒx New User Parameter ƒx Новый пользовательский параметр
984 ΔLOC Δстрок
985 −Bin −двоич.
986 −LOC −строк
987 ⌘↩ to finish ⌘↩ для завершения
988 @plural {n} commit {n} коммит|{n} коммита|{n} коммитов
989 @plural {n} contributor {n} участник|{n} участника|{n} участников
990 Drawing as bytes Чертеж как байты
991 Services Службы
992 Hide CADCraft Скрыть CADCraft
993 Hide Others Скрыть остальные
994 Accessibility Специальные возможности
995 Show System Cursor Показывать системный курсор
996 Import... Импорт...
997 Export... Экспорт...
998 Batch Publish... Пакетная публикация...
999 Hatch Pattern Образец штриховки
1000 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
@@ -144,6 +144,7 @@
Closed Замкнений
Close Comparison Закрити порівняння
Coincident Збіг
Collapse Palettes Згорнути палітри
Collapse Tool Sets Згорнути набори інструментів
Collinear Колінеарність
Color Колір
@@ -252,6 +253,7 @@
Drawing Template (DWT) Шаблон креслення (DWT)
Drawing Utilities Утиліти креслення
Drawings (DWG, DXF) Креслення (DWG, DXF)
SVG drawings (SVG, SVGZ) Креслення SVG (SVG, SVGZ)
Dynamic Динамічно
Dynamic Dimensions Динамічні розміри
Dynamic Input Динамічне введення
@@ -620,6 +622,7 @@
Plot Друк креслення
Plot (click: do not plot) Друкувати (натисніть, щоб вимкнути друк)
Plot Preview Попередній перегляд друку
Plot Window Вікно друку
Plot style Стиль друку
Plot style attached to Стиль друку застосовано до
Plot style table Таблиця стилів друку
@@ -839,6 +842,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 Показати/приховати товщину ліній
@@ -1199,3 +1204,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.
144
145
146
147
148
149
150
253
254
255
256
257
258
259
622
623
624
625
626
627
628
842
843
844
845
846
847
848
849
1204
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
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.
@@ -204,6 +212,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 {
@@ -215,6 +227,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,
@@ -225,7 +238,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,
frame_ms: 0.0,
@@ -234,17 +247,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 {
@@ -304,18 +322,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;
}
@@ -390,9 +414,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.
@@ -411,6 +436,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"))]
@@ -476,8 +503,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")),
@@ -71,6 +74,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);
}
@@ -108,7 +114,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());
@@ -456,7 +468,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);
}
@@ -474,7 +486,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
@@ -185,8 +185,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>) {
@@ -283,6 +322,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."));
});
}
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more