Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor cb6311f151 Restore imports that the cadcraft batch dropped so the engine compiles
Explicit import lists missed names the files still use. Restore them and
the test-module globs.
2026-10-25 19:13:43 -04:00
Dylan M. Taylor cafb6332e7 refactor: replace wildcard imports with explicit ones (S2208, batch_07) 2026-10-25 19:13:43 -04:00
72 changed files with 1500 additions and 2480 deletions
-3
View File
@@ -1,3 +0,0 @@
# Minimum cargo-nextest for this repository: GHSA-vc25-qg58-w57c is fixed in 0.9.150 and CI
# installs exactly that version, so an older nextest refuses to run (exit 92 REQUIRED_VERSION_NOT_MET).
nextest-version = "0.9.150"
+4 -4
View File
@@ -24,10 +24,10 @@ Generated-in-code assets are original and have no file to list:
| Asset | Author | Source | Licence | Notes |
|---|---|---|---|---|
| `epaint_default_fonts::EGUI_ICONS`, `epaint_default_fonts::EMOJI_ICON` (dependency font inputs) | John Slegers | [egui 173cc737](https://github.com/storytold/egui/tree/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts), [emoji-icon-font](https://github.com/jslegers/emoji-icon-font) | MIT (Copyright (c) 2014 John Slegers; full upstream text in `NOTICE`) | egui-icons.ttf is the five-glyph subset; emoji-icon-font.ttf is the full monochrome fallback. Registered by the production default font definitions; dependency assets unmodified here, no font file copied into this repository. |
| `krilla 0.8.2 / icc/sGrey-v2-magic.icc` (dependency-embedded) | Clinton Ingram | [craft-libs 9f89fe8 vendor/krilla/icc/sGrey-v2-magic.icc](https://github.com/storytold/craft-libs/blob/9f89fe8f6d72ab4db4cc06f186381a6a254463ab/vendor/krilla/icc/sGrey-v2-magic.icc) (Krilla 0.8.2 upstream 3ffdf0588cf98050aad6edba51ca70162e1fb5b5) | CC0-1.0 | Embedded by the pinned PDF writer (`craft-pdf` 0.1.0, Krilla 0.8.2); unmodified dependency asset, no file copied into this repository. SHA-256 `4110169287e5f1b5149ca9f1da1814d32943905bba8030e849d1a3b729a19481`. |
| `krilla 0.8.2 / icc/sGrey-v4.icc` (dependency-embedded) | Clinton Ingram | [craft-libs 9f89fe8 vendor/krilla/icc/sGrey-v4.icc](https://github.com/storytold/craft-libs/blob/9f89fe8f6d72ab4db4cc06f186381a6a254463ab/vendor/krilla/icc/sGrey-v4.icc) (Krilla 0.8.2 upstream 3ffdf0588cf98050aad6edba51ca70162e1fb5b5) | CC0-1.0 | Embedded by the pinned PDF writer (`craft-pdf` 0.1.0, Krilla 0.8.2); unmodified dependency asset, no file copied into this repository. SHA-256 `00c0f94e09127520a17dc0e1d9264b5702081d96dbdb1549ee88e3631ce42a9d`. |
| `krilla 0.8.2 / icc/sRGB-v2-magic.icc` (dependency-embedded) | Clinton Ingram | [craft-libs 9f89fe8 vendor/krilla/icc/sRGB-v2-magic.icc](https://github.com/storytold/craft-libs/blob/9f89fe8f6d72ab4db4cc06f186381a6a254463ab/vendor/krilla/icc/sRGB-v2-magic.icc) (Krilla 0.8.2 upstream 3ffdf0588cf98050aad6edba51ca70162e1fb5b5) | CC0-1.0 | Embedded by the pinned PDF writer (`craft-pdf` 0.1.0, Krilla 0.8.2); unmodified dependency asset, no file copied into this repository. SHA-256 `af4efe28f6d311799865f325ba39184a2e978b113ca74124d60af7de22b105f4`. |
| `krilla 0.8.2 / icc/sRGB-v4.icc` (dependency-embedded) | Clinton Ingram | [craft-libs 9f89fe8 vendor/krilla/icc/sRGB-v4.icc](https://github.com/storytold/craft-libs/blob/9f89fe8f6d72ab4db4cc06f186381a6a254463ab/vendor/krilla/icc/sRGB-v4.icc) (Krilla 0.8.2 upstream 3ffdf0588cf98050aad6edba51ca70162e1fb5b5) | CC0-1.0 | Embedded by the pinned PDF writer (`craft-pdf` 0.1.0, Krilla 0.8.2); unmodified dependency asset, no file copied into this repository. SHA-256 `c56e1685d888f5edb92fe07f2750f387f8fe8e91b32ff8fb0b56bfbbb9458353`. |
| `krilla 0.8.2 / icc/sGrey-v2-magic.icc` (dependency-embedded) | Clinton Ingram | [craft-libs a091680 vendor/krilla/icc/sGrey-v2-magic.icc](https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/krilla/icc/sGrey-v2-magic.icc) (Krilla 0.8.2 upstream 3ffdf0588cf98050aad6edba51ca70162e1fb5b5) | CC0-1.0 | Embedded by the pinned PDF writer (`craft-pdf` 0.1.0, Krilla 0.8.2); unmodified dependency asset, no file copied into this repository. SHA-256 `4110169287e5f1b5149ca9f1da1814d32943905bba8030e849d1a3b729a19481`. |
| `krilla 0.8.2 / icc/sGrey-v4.icc` (dependency-embedded) | Clinton Ingram | [craft-libs a091680 vendor/krilla/icc/sGrey-v4.icc](https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/krilla/icc/sGrey-v4.icc) (Krilla 0.8.2 upstream 3ffdf0588cf98050aad6edba51ca70162e1fb5b5) | CC0-1.0 | Embedded by the pinned PDF writer (`craft-pdf` 0.1.0, Krilla 0.8.2); unmodified dependency asset, no file copied into this repository. SHA-256 `00c0f94e09127520a17dc0e1d9264b5702081d96dbdb1549ee88e3631ce42a9d`. |
| `krilla 0.8.2 / icc/sRGB-v2-magic.icc` (dependency-embedded) | Clinton Ingram | [craft-libs a091680 vendor/krilla/icc/sRGB-v2-magic.icc](https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/krilla/icc/sRGB-v2-magic.icc) (Krilla 0.8.2 upstream 3ffdf0588cf98050aad6edba51ca70162e1fb5b5) | CC0-1.0 | Embedded by the pinned PDF writer (`craft-pdf` 0.1.0, Krilla 0.8.2); unmodified dependency asset, no file copied into this repository. SHA-256 `af4efe28f6d311799865f325ba39184a2e978b113ca74124d60af7de22b105f4`. |
| `krilla 0.8.2 / icc/sRGB-v4.icc` (dependency-embedded) | Clinton Ingram | [craft-libs a091680 vendor/krilla/icc/sRGB-v4.icc](https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/krilla/icc/sRGB-v4.icc) (Krilla 0.8.2 upstream 3ffdf0588cf98050aad6edba51ca70162e1fb5b5) | CC0-1.0 | Embedded by the pinned PDF writer (`craft-pdf` 0.1.0, Krilla 0.8.2); unmodified dependency asset, no file copied into this repository. SHA-256 `c56e1685d888f5edb92fe07f2750f387f8fe8e91b32ff8fb0b56bfbbb9458353`. |
| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.cadcraft.png` | CADCraft contributors | generated from `assets/app-icon/cadcraft-1024.png` | MIT OR Apache-2.0 | Linux hicolor icon |
Generated
+127 -141
View File
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "acadrust"
version = "0.6.6"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8923c99ea1f151cb7fc17ce2fc14f8b1e330fa22611e535602d985c0cc3c89a2"
checksum = "5eaa72520f70eb6f7f09b06f44e1ed0d8fad5fa743364f2c3715c30df962ec7c"
dependencies = [
"anyhow",
"bitflags 2.13.2",
@@ -28,9 +28,9 @@ dependencies = [
[[package]]
name = "accesskit"
version = "0.25.2"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e883d1d28c5fd5568c79ed2d2bd48d9f2fd3945c8974ed0f53690ceddb051be"
checksum = "ad442f58ee04714aaa0ba0a2768c1ea1935b29507bb22ddece8cbbc76db02932"
dependencies = [
"enumn",
"serde",
@@ -39,9 +39,9 @@ dependencies = [
[[package]]
name = "accesskit_atspi_common"
version = "0.21.1"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24f5baf62e34a4a6c30e3d697e4782f821a7337049c44c3b40576a27dfbb580d"
checksum = "52c182f9c282ac9c5638d876d551d15e5f7d397ec263349a0c6a2b61595dd5e4"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -53,9 +53,9 @@ dependencies = [
[[package]]
name = "accesskit_consumer"
version = "0.39.2"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3da8f27636015191b2264ee1df681503170a632cafd832fdc1edfdd4e3a2fd3"
checksum = "8a45e2bb300bea6d5f33a82c3e051c3ddc6f41c209750f68a0acbe991a284344"
dependencies = [
"accesskit",
"hashbrown 0.17.1",
@@ -63,9 +63,9 @@ dependencies = [
[[package]]
name = "accesskit_ios"
version = "0.2.2"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5280724673bbfa8b30a838c013500b719392989be3b97c5a9964062c252c232"
checksum = "28e539d64b27fd4c14de8f78ada8f7c944c092f52f7e49fb63316507a57f6937"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -77,9 +77,9 @@ dependencies = [
[[package]]
name = "accesskit_macos"
version = "0.27.2"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "123828ceeec755b05255e3976cb0852993202de98a0626c97dd8d34246991f84"
checksum = "5701624bf6a11efb6d4c8e922bf44126ed96250e5faa9a7a0468c979635839a4"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -91,9 +91,9 @@ dependencies = [
[[package]]
name = "accesskit_unix"
version = "0.24.1"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5192871228c54f1b0fb4a85cef000e11a3804f25d2165f14afa0daa67f3f6"
checksum = "202f24df034a7476d07b7f74284de84f6d62aabd858dbe7ee9cad3b7ad6f8f9d"
dependencies = [
"accesskit",
"accesskit_atspi_common",
@@ -109,9 +109,9 @@ dependencies = [
[[package]]
name = "accesskit_windows"
version = "0.35.2"
version = "0.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498668b90da9995302a0055aaecad4756932333da2ddef6baee1c00844c430fc"
checksum = "ce63f35d6bdcf59f26b76b3379063f738e6412cef46999cc772d46aa3de35adb"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "accesskit_winit"
version = "0.34.2"
version = "0.34.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd4ab4437b520ff03088fbe48676a11e7d44a929535e64b31432c1073e48dbcc"
checksum = "ee96dd150375c05912d8a473f98c2fb155fb3234fac88292d396548a2460fa77"
dependencies = [
"accesskit",
"accesskit_ios",
@@ -343,7 +343,7 @@ checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -378,7 +378,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -444,9 +444,9 @@ checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "basic-udev"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "868a8681ed3a19fae3e302c705b1cabf5be53278cc8720a9b82e9805c9ab171d"
checksum = "5a45f9771ced8a774de5e5ebffbe520f52e3943bf5a9a6baa3a5d14a5de1afe6"
[[package]]
name = "bit-set"
@@ -541,7 +541,7 @@ checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -571,9 +571,7 @@ dependencies = [
"cadcraft-mcp",
"cadcraft-ui-egui",
"chrono",
"craft-ui",
"craft-update",
"craft-window",
"eframe",
"egui",
"hidapi",
@@ -742,7 +740,6 @@ dependencies = [
"chrono",
"craft-ui",
"craft-update",
"craft-window",
"egui",
"egui-wgpu",
"egui_kittest",
@@ -823,9 +820,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.10.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -865,9 +862,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
version = "5.4.3"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
@@ -910,9 +907,9 @@ dependencies = [
[[package]]
name = "combine"
version = "4.6.9"
version = "4.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92"
checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
dependencies = [
"bytes",
"memchr",
@@ -995,12 +992,12 @@ dependencies = [
[[package]]
name = "craft-docpath"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=9f89fe8f6d72ab4db4cc06f186381a6a254463ab#9f89fe8f6d72ab4db4cc06f186381a6a254463ab"
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
[[package]]
name = "craft-pdf"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=9f89fe8f6d72ab4db4cc06f186381a6a254463ab#9f89fe8f6d72ab4db4cc06f186381a6a254463ab"
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
dependencies = [
"hayro-syntax",
"krilla",
@@ -1010,7 +1007,7 @@ dependencies = [
[[package]]
name = "craft-ui"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=9f89fe8f6d72ab4db4cc06f186381a6a254463ab#9f89fe8f6d72ab4db4cc06f186381a6a254463ab"
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
dependencies = [
"egui",
"log",
@@ -1023,7 +1020,7 @@ dependencies = [
[[package]]
name = "craft-update"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=9f89fe8f6d72ab4db4cc06f186381a6a254463ab#9f89fe8f6d72ab4db4cc06f186381a6a254463ab"
source = "git+https://github.com/storytold/craft-libs?rev=495e5971bc024aa96e6ed318cb6e7cd3a0103184#495e5971bc024aa96e6ed318cb6e7cd3a0103184"
dependencies = [
"chrono",
"dunce",
@@ -1045,22 +1042,11 @@ dependencies = [
"zip",
]
[[package]]
name = "craft-window"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=9f89fe8f6d72ab4db4cc06f186381a6a254463ab#9f89fe8f6d72ab4db4cc06f186381a6a254463ab"
dependencies = [
"egui",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"wayland-client",
]
[[package]]
name = "crc32fast"
version = "1.5.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055"
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
dependencies = [
"cfg-if",
]
@@ -1176,7 +1162,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -1463,9 +1449,9 @@ source = "git+https://github.com/storytold/egui.git?rev=6d518fc799681571963842f9
[[package]]
name = "equivalent"
version = "1.0.3"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
@@ -1551,9 +1537,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.15"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "fixedbitset"
@@ -1636,7 +1622,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -1687,7 +1673,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -1995,7 +1981,7 @@ checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a"
[[package]]
name = "hayro-jbig2"
version = "0.3.0"
source = "git+https://github.com/storytold/craft-libs?rev=9f89fe8f6d72ab4db4cc06f186381a6a254463ab#9f89fe8f6d72ab4db4cc06f186381a6a254463ab"
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
dependencies = [
"hayro-ccitt",
]
@@ -2009,7 +1995,7 @@ checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1"
[[package]]
name = "hayro-syntax"
version = "0.7.2"
source = "git+https://github.com/storytold/craft-libs?rev=9f89fe8f6d72ab4db4cc06f186381a6a254463ab#9f89fe8f6d72ab4db4cc06f186381a6a254463ab"
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
dependencies = [
"hayro-jbig2",
"hayro-jpeg2000",
@@ -2074,9 +2060,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "icu_collections"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd"
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [
"displaydoc",
"potential_utf",
@@ -2088,9 +2074,9 @@ dependencies = [
[[package]]
name = "icu_locale_core"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0"
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [
"displaydoc",
"litemap",
@@ -2101,9 +2087,9 @@ dependencies = [
[[package]]
name = "icu_normalizer"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599"
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@@ -2115,15 +2101,15 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc"
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]]
name = "icu_properties"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f"
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [
"displaydoc",
"icu_collections",
@@ -2136,15 +2122,15 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf"
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]]
name = "icu_provider"
version = "2.4.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514"
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
dependencies = [
"displaydoc",
"icu_locale_core",
@@ -2371,7 +2357,7 @@ dependencies = [
[[package]]
name = "krilla"
version = "0.8.2"
source = "git+https://github.com/storytold/craft-libs?rev=9f89fe8f6d72ab4db4cc06f186381a6a254463ab#9f89fe8f6d72ab4db4cc06f186381a6a254463ab"
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
dependencies = [
"base64 0.22.1",
"bumpalo",
@@ -2443,7 +2429,7 @@ dependencies = [
"bitflags 2.13.2",
"libc",
"plain",
"redox_syscall 0.9.5",
"redox_syscall 0.9.4",
]
[[package]]
@@ -2472,9 +2458,9 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "litrs"
version = "1.0.1"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
@@ -2493,9 +2479,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lyon_geom"
version = "1.0.20"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82dd26aa4dc0b5b70b9115de59824842bcb43cf4a28e1b1e6d0ad148c93f93e"
checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92"
dependencies = [
"arrayvec",
"euclid",
@@ -2531,9 +2517,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "memmap2"
version = "0.9.12"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157"
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
dependencies = [
"libc",
]
@@ -3090,9 +3076,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "open"
version = "5.4.5"
version = "5.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95"
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
dependencies = [
"is-wsl",
"libc",
@@ -3116,9 +3102,9 @@ dependencies = [
[[package]]
name = "ordered-float"
version = "5.5.1"
version = "5.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f59006ce1e204c30956638535b74d317bca99477b9356dfaec54c37f6f600ab"
checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0"
dependencies = [
"num-traits",
]
@@ -3594,9 +3580,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.9.5"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd"
checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e"
dependencies = [
"bitflags 2.13.2",
]
@@ -3908,7 +3894,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -3932,14 +3918,14 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
name = "serde_spanned"
version = "1.1.2"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
@@ -4046,9 +4032,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.16.4"
version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
dependencies = [
"serde",
]
@@ -4194,9 +4180,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.9"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
@@ -4211,7 +4197,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -4292,7 +4278,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -4367,9 +4353,9 @@ dependencies = [
[[package]]
name = "tinystr"
version = "0.8.5"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a"
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [
"displaydoc",
"zerovec",
@@ -4383,18 +4369,18 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]]
name = "tokio"
version = "1.54.1"
version = "1.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
dependencies = [
"pin-project-lite",
]
[[package]]
name = "toml"
version = "1.1.8+spec-1.1.0"
version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [
"indexmap",
"serde_core",
@@ -4407,18 +4393,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.1.2+spec-1.1.0"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.17+spec-1.1.0"
version = "0.25.15+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
dependencies = [
"indexmap",
"toml_datetime",
@@ -4428,18 +4414,18 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.1.5+spec-1.1.0"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.3+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
name = "tracing"
@@ -4536,9 +4522,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.27"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "unicode-normalization"
@@ -4628,9 +4614,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.29.0"
version = "1.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
dependencies = [
"js-sys",
"serde_core",
@@ -4748,7 +4734,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"wasm-bindgen-shared",
]
@@ -4918,9 +4904,9 @@ dependencies = [
[[package]]
name = "webbrowser"
version = "1.2.5"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c8ddfbbf66e501434657400268cefe51b95140a0266236415cda7627732df7"
checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94"
dependencies = [
"jni",
"log",
@@ -5547,9 +5533,9 @@ dependencies = [
[[package]]
name = "yoke"
version = "0.8.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3"
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
dependencies = [
"stable_deref_trait",
"yoke-derive",
@@ -5564,7 +5550,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"synstructure",
]
@@ -5636,7 +5622,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"zbus_names",
"zvariant",
"zvariant_utils",
@@ -5644,9 +5630,9 @@ dependencies = [
[[package]]
name = "zbus_names"
version = "4.3.5"
version = "4.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd"
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
dependencies = [
"serde",
"winnow",
@@ -5676,18 +5662,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.63"
version = "0.8.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.63"
version = "0.8.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759"
dependencies = [
"proc-macro2",
"quote",
@@ -5711,7 +5697,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"synstructure",
]
@@ -5734,9 +5720,9 @@ dependencies = [
[[package]]
name = "zerovec"
version = "0.11.9"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40"
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
dependencies = [
"yoke",
"zerofrom",
@@ -5745,20 +5731,20 @@ dependencies = [
[[package]]
name = "zerovec-derive"
version = "0.11.7"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a"
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
name = "zip"
version = "9.1.1"
version = "9.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518b847fd864440c379748ad684c449f293af68978a485a4ff16f7c901c1f7c4"
checksum = "2886b00003a72c4ccee3f42cfc34908d33b2508a74d827dc2a4244706475c704"
dependencies = [
"crc32fast",
"flate2",
@@ -5812,9 +5798,9 @@ dependencies = [
[[package]]
name = "zvariant"
version = "5.15.1"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7"
checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
dependencies = [
"endi",
"enumflags2",
@@ -5827,14 +5813,14 @@ dependencies = [
[[package]]
name = "zvariant_derive"
version = "5.15.1"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d"
checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"zvariant_utils",
]
@@ -5847,7 +5833,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 3.0.9",
"syn 3.0.6",
"winnow",
]
+7 -8
View File
@@ -25,10 +25,10 @@ cadcraft-engine = { path = "crates/engine" }
cadcraft-ui-egui = { path = "crates/ui-egui" }
cadcraft-mcp = { path = "crates/mcp" }
# Which paths stored in a drawing (xrefs, data links) may be opened.
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "9f89fe8f6d72ab4db4cc06f186381a6a254463ab" }
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
# PDF writing (krilla) for plots, and reading them back in tests. The root patches below pin
# craft-pdf's patched backends to the same revision (Cargo ignores dependency-local patches).
craft-pdf = { version = "=0.1.0", git = "https://github.com/storytold/craft-libs", rev = "9f89fe8f6d72ab4db4cc06f186381a6a254463ab", default-features = false }
craft-pdf = { version = "=0.1.0", git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f", default-features = false }
# foundation
thiserror = "2"
@@ -53,15 +53,14 @@ encoding_rs = "0.8.35"
quick-xml = "0.42"
# ui
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "9f89fe8f6d72ab4db4cc06f186381a6a254463ab", features = ["serde"] }
craft-window = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "9f89fe8f6d72ab4db4cc06f186381a6a254463ab", features = ["egui"] }
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f", features = ["serde"] }
egui = { version = "0.37", default-features = false, features = ["default_fonts", "monochrome_emoji_fonts"] }
egui_kittest = "0.37"
eframe = { version = "0.37", default-features = false }
egui-wgpu = "0.37"
# Monotonic time on native and web (performance.now on wasm).
web-time = "1.1"
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "9f89fe8f6d72ab4db4cc06f186381a6a254463ab", features = ["egui"] }
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184", features = ["egui"] }
rfd = "0.17"
arboard = { version = "3.6", default-features = false }
@@ -91,9 +90,9 @@ debug = 0
# PDF backends for craft-pdf: pinned to the same reviewed craft-libs revision as craft-pdf itself
# (Cargo ignores dependency-local patches).
[patch.crates-io]
krilla = { version = "=0.8.2", git = "https://github.com/storytold/craft-libs", rev = "9f89fe8f6d72ab4db4cc06f186381a6a254463ab" }
hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "9f89fe8f6d72ab4db4cc06f186381a6a254463ab" }
hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "9f89fe8f6d72ab4db4cc06f186381a6a254463ab" }
krilla = { version = "=0.8.2", git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
# Keep the complete egui family and native event bridge on reviewed fork commits.
ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
+2 -2
View File
@@ -28,9 +28,9 @@ The PDF writer in the pinned craft-pdf 0.1.0 / Krilla 0.8.2 dependency embeds fo
Profiles by Clinton Ingram, under CC0-1.0. They are unmodified dependency assets; no profile file
is copied into this repository. Per-file sources and SHA-256 hashes are in ATTRIBUTION.md.
Source: https://github.com/LaurenzV/krilla/tree/3ffdf0588cf98050aad6edba51ca70162e1fb5b5/crates/krilla/icc
(pinned through https://github.com/storytold/craft-libs/tree/9f89fe8f6d72ab4db4cc06f186381a6a254463ab/vendor/krilla/icc).
(pinned through https://github.com/storytold/craft-libs/tree/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/krilla/icc).
CC0 1.0 Universal: https://creativecommons.org/publicdomain/zero/1.0/
Pinned licence text: https://github.com/storytold/craft-libs/blob/9f89fe8f6d72ab4db4cc06f186381a6a254463ab/vendor/krilla/icc/LICENSE
Pinned licence text: https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/krilla/icc/LICENSE
The UI's egui-icons.ttf subset and emoji-icon-font.ttf fallback are provided by epaint_default_fonts
and registered through egui's default font definitions. They derive from emoji-icon-font by
-5
View File
@@ -34,16 +34,11 @@ winit = { version = "0.30", default-features = false, features = ["x11"] }
zbus = { version = "=5.19.0", default-features = false, features = ["async-io", "blocking-api"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Floating panels open in their own OS windows on the desktop; the web build keeps them in the page.
craft-ui = { workspace = true, features = ["native-windows"] }
craft-update = { workspace = true }
rfd = { workspace = true }
sys-locale = "0.3"
[target.'cfg(target_os = "macos")'.dependencies]
# `title_bar::sync_movable`: AppKit's title-band drag leaves the window still under the row's
# controls and under floating panels.
craft-window = { workspace = true }
muda = { version = "0.21", default-features = false }
# View ▸ Accessibility ▸ Animation Effects ▸ Use System Setting: the system's Reduce motion
# (system_animations.rs). Already in Cargo.lock through muda.
+4 -19
View File
@@ -39,22 +39,11 @@ struct App {
workspace_error: Option<String>,
#[cfg(target_os = "macos")]
native_menu: Option<native_menu::NativeMenu>,
/// The `movable` flag last set on the main window.
#[cfg(target_os = "macos")]
movable: Option<bool>,
}
impl App {
/// `ctx` is `None` only for the final attempt at exit.
fn persist_workspace(&mut self, ctx: Option<&egui::Context>) {
let result = match ctx {
None => self.workspace.save_on_exit(&self.cad.ui),
Some(ctx) => self.workspace.save_if_changed(&self.cad.ui, std::time::Instant::now()).map(|wait| {
if let Some(wait) = wait {
ctx.request_repaint_after(wait);
}
}),
};
fn persist_workspace(&mut self, final_attempt: bool) {
let result = if final_attempt { self.workspace.save_on_exit(&self.cad.ui) } else { self.workspace.save_if_changed(&self.cad.ui) };
if let Err(error) = result {
if self.workspace_error.as_ref() != Some(&error) {
eprintln!("CADCraft workspace: {error}");
@@ -97,7 +86,7 @@ impl eframe::App for App {
}
self.spacemouse.poll(&mut self.cad, ctx);
// After `cad.logic`, so a control command's workspace change is saved in the same frame.
self.persist_workspace(Some(ctx));
self.persist_workspace(false);
}
if std::mem::take(&mut self.cad.quit_requested) {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
@@ -111,13 +100,11 @@ impl eframe::App for App {
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.cad.ui(ui);
#[cfg(target_os = "macos")]
craft_window::title_bar::sync_movable(ui.ctx(), &mut self.movable);
}
/// A normal exit: the workspace is saved, and this run's autosave files are not needed (a
/// crash leaves them for recovery).
fn on_exit(&mut self) {
self.persist_workspace(None);
self.persist_workspace(true);
self.cad.cancel_screenshots();
self.cad.shutdown_autosave();
}
@@ -418,8 +405,6 @@ fn main() -> eframe::Result {
workspace_error,
#[cfg(target_os = "macos")]
native_menu: None,
#[cfg(target_os = "macos")]
movable: None,
}))
}),
)
+2 -3
View File
@@ -118,7 +118,7 @@ impl NativeMenu {
if cmd == "ui.discord" {
ctx.open_url(egui::OpenUrl::new_tab("https://discord.gg/artcraft"));
} else {
menus::activate_native(app, ctx, &cmd.clone());
menus::activate(app, &cmd.clone());
}
ctx.request_repaint();
}
@@ -142,9 +142,8 @@ impl NativeMenu {
return;
}
self.last_refresh = now;
let typing = menus::text_field_window(ctx).is_some();
for (cmd, _, item) in self.items.values() {
let en = (typing && menus::edits_text(cmd)) || cadcraft_engine::find_command(cmd).is_none_or(|c| (c.enabled)(&app.session).is_ok());
let en = cadcraft_engine::find_command(cmd).is_none_or(|c| (c.enabled)(&app.session).is_ok());
item.set_enabled(en);
}
}
+42 -84
View File
@@ -5,15 +5,11 @@ use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use cadcraft_ui_egui::UiState;
use serde_json::{Value, json};
const MAX_BYTES: u64 = 64 * 1024;
/// Dragging a floating window changes its rect every frame; writes (each with an fsync) are
/// spaced at least this far apart, and the newest state is written when the interval ends.
const MIN_WRITE_INTERVAL: Duration = Duration::from_millis(500);
static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
pub struct Workspace {
@@ -21,7 +17,6 @@ pub struct Workspace {
last_attempt: Option<Value>,
read_failed: bool,
pending_write_error: Option<String>,
last_write: Option<Instant>,
}
fn config_dir_from(platform: &str, get: impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
@@ -49,7 +44,6 @@ fn snapshot(ui: &UiState) -> Result<Value, String> {
}
Ok(json!({
"docking": ui.docking,
"dockingVersion": cadcraft_ui_egui::docking::LAYOUT_VERSION,
"showToolsets": ui.show_toolsets,
"showPalettes": ui.show_palettes,
"showToolbar": ui.show_toolbar,
@@ -86,16 +80,10 @@ fn read(path: &Path) -> Result<Option<UiState>, String> {
value.get("workspace").filter(|value| value.is_object()).ok_or_else(|| format!("{}: missing workspace object", path.display()))?;
// Dock schemas can evolve independently of the other saved workspace preferences.
// Recover only this optional field; runtime UiState deserialization remains strict.
// A layout from before the current shape (split frames, saved without `dockingVersion`)
// gives way to the default, its hidden panels with it.
let mut fields = workspace.clone();
let (docking, version) = match fields.as_object_mut() {
Some(fields) => (fields.remove("docking"), fields.remove("dockingVersion")),
None => (None, None),
};
let current = version.and_then(|version| version.as_u64()).is_some_and(|version| version >= u64::from(cadcraft_ui_egui::docking::LAYOUT_VERSION));
let docking = fields.as_object_mut().and_then(|fields| fields.remove("docking"));
let mut ui: UiState = serde_json::from_value(fields).map_err(|error| format!("{}: invalid workspace: {error}", path.display()))?;
if let Some(docking) = docking.filter(|_| current)
if let Some(docking) = docking
&& let Ok(docking) = serde_json::from_value::<cadcraft_ui_egui::docking::Workspace>(docking)
&& cadcraft_ui_egui::docking::validate(&docking).is_ok()
{
@@ -148,47 +136,38 @@ impl Workspace {
match result {
Ok(ui) => {
let initial = ui.as_ref().cloned().unwrap_or_default();
(Self { path, last_attempt: snapshot(&initial).ok(), read_failed: false, pending_write_error: None, last_write: None }, ui, None)
(Self { path, last_attempt: snapshot(&initial).ok(), read_failed: false, pending_write_error: None }, ui, None)
}
Err(error) => (
Self { path, last_attempt: None, read_failed: true, pending_write_error: None, last_write: None },
Self { path, last_attempt: None, read_failed: true, pending_write_error: None },
None,
Some(format!("Workspace could not be restored; its file was left unchanged. {error}")),
),
}
}
/// Writes `ui` if it differs from the last state written. `Ok(Some(wait))` means a change
/// is held back by [`MIN_WRITE_INTERVAL`]; call again after `wait` to write it.
pub fn save_if_changed(&mut self, ui: &UiState, now: Instant) -> Result<Option<Duration>, String> {
let Some(path) = &self.path else { return Ok(None) };
pub fn save_if_changed(&mut self, ui: &UiState) -> Result<(), String> {
let Some(path) = &self.path else { return Ok(()) };
if self.read_failed {
// Preserve a corrupt, oversized or newer-version file until the user repairs it.
return Ok(None);
return Ok(());
}
let value = snapshot(ui)?;
if self.last_attempt.as_ref() == Some(&value) {
return self.pending_write_error.clone().map_or(Ok(None), Err);
}
if let Some(wait) = self.last_write.and_then(|last| MIN_WRITE_INTERVAL.checked_sub(now.saturating_duration_since(last)))
&& !wait.is_zero()
{
return Ok(Some(wait));
return self.pending_write_error.clone().map_or(Ok(()), Err);
}
// Keep a failed state's warning without retrying disk I/O every rendered frame.
self.last_attempt = Some(value.clone());
self.last_write = Some(now);
let result = write(path, &value);
self.pending_write_error = result.as_ref().err().cloned();
result.map(|()| None)
result
}
pub fn save_on_exit(&mut self, ui: &UiState) -> Result<(), String> {
if self.pending_write_error.is_some() {
self.last_attempt = None;
}
self.last_write = None;
self.save_if_changed(ui, Instant::now()).map(|_| ())
self.save_if_changed(ui)
}
}
@@ -237,7 +216,7 @@ mod tests {
start_tab: true,
..Default::default()
};
first.save_if_changed(&ui, Instant::now()).unwrap();
first.save_if_changed(&ui).unwrap();
let bytes = fs::read(&path).unwrap();
let value: Value = serde_json::from_slice(&bytes).unwrap();
assert!(value["workspace"].get("dialog").is_none());
@@ -250,42 +229,37 @@ mod tests {
let mut transient_only = ui.clone();
transient_only.dialog = None;
transient_only.start_tab = false;
first.save_if_changed(&transient_only, Instant::now()).unwrap();
first.save_if_changed(&transient_only).unwrap();
assert_eq!(fs::read(&path).unwrap(), bytes);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn dock_columns_floating_and_hidden_panels_survive_native_workspace_file_reload() {
use cadcraft_ui_egui::docking::{LAYOUT_VERSION, Panel, permissions, reveal_panel};
fn docked_floating_and_hidden_panels_survive_native_workspace_file_reload() {
use cadcraft_ui_egui::docking::{Panel, permissions};
let directory = temp_dir();
let path = directory.join("workspace.json");
let (mut store, _, _) = Workspace::at(Some(path.clone()));
let mut ui = UiState::default();
let mut app = cadcraft_ui_egui::CadApp::new(cadcraft_engine::Session::default(), Default::default());
app.run("ui.dock", json!({"operation":"move", "panel":"layers", "target":"properties", "zone":"left"})).unwrap();
app.run("ui.dock", json!({"operation":"float", "panel":"properties", "rect":[120,90,340,410]})).unwrap();
app.run("ui.dock", json!({"operation":"float", "panel":"layers", "rect":[120,90,340,410]})).unwrap();
ui.docking = app.ui.docking;
ui.docking.set_visible(Panel::ToolSets, false, None, permissions).unwrap();
ui.show_toolsets = false;
store.save_if_changed(&ui, Instant::now()).unwrap();
let saved: Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
assert_eq!(saved["workspace"]["dockingVersion"], json!(LAYOUT_VERSION));
store.save_if_changed(&ui).unwrap();
let (_, reloaded, error) = Workspace::at(Some(path));
assert!(error.is_none());
let mut reloaded = reloaded.unwrap();
assert_eq!(serde_json::to_value(&reloaded.docking).unwrap(), serde_json::to_value(&ui.docking).unwrap());
assert!(!reloaded.show_toolsets);
reveal_panel(&mut reloaded.docking, Panel::ToolSets).unwrap();
let tools = reloaded.docking.layout.location(&Panel::ToolSets).unwrap();
assert_eq!(serde_json::to_value(&tools.group).unwrap()["column"]["side"], json!("Start"), "{tools:?}");
reloaded.docking.set_visible(Panel::ToolSets, true, None, permissions).unwrap();
assert!(reloaded.docking.layout.contains(&Panel::ToolSets));
assert_eq!(reloaded.docking.layout.floating[0].rect, [120.0, 90.0, 340.0, 410.0]);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn split_frame_layout_file_loads_as_the_dock_default_with_its_other_settings() {
use cadcraft_ui_egui::docking::default_workspace;
fn version_one_docking_file_still_loads() {
use cadcraft_ui_egui::docking::{Panel, reveal_panel};
// Written as CADCraft 0.7 writes it: craft-ui's v1 layout with no docking version, a
// floating window as one `{panels, active, rect}` group, Tool Sets closed.
let file = r#"{"version":1,"workspace":{
@@ -297,18 +271,28 @@ mod tests {
"floating":[{"panels":["layers","externalReferences"],"active":1,"rect":[120.0,90.0,340.0,410.0]}]},
"hidden":[["toolSets",{"floating":null,"anchor":"canvas","placement":{"Split":"Left"},
"size":{"FixedFirst":220.0},"stack":null,"sibling":["canvas","properties"]}]]},
"showToolsets":false,"showPalettes":true,"showToolbar":false,"toolsetTab":"Modeling","historyLines":4}}"#;
"showToolsets":false,"showPalettes":true,"toolsetTab":"Modeling","historyLines":4}}"#;
let directory = temp_dir();
let path = directory.join("workspace.json");
fs::write(&path, file).unwrap();
let (_, restored, error) = Workspace::at(Some(path));
assert!(error.is_none(), "{error:?}");
let ui = restored.unwrap();
let mut ui = restored.unwrap();
assert_eq!(ui.toolset_tab, "Modeling");
assert_eq!(ui.history_lines, 4);
assert!(!ui.show_toolsets && ui.show_palettes && !ui.show_toolbar);
assert_eq!(ui.docking.layout, default_workspace().layout);
assert!(ui.docking.hidden.is_empty(), "{:?}", ui.docking.hidden);
let docking = &ui.docking;
assert_eq!(docking.layout.floating.len(), 1);
let window = &docking.layout.floating[0];
assert_eq!(window.rect, [120.0, 90.0, 340.0, 410.0]);
assert_eq!(serde_json::to_value(&window.root).unwrap(), json!({"Tabs":{"panels":["layers","externalReferences"],"active":1}}));
assert_eq!(
serde_json::to_value(&docking.layout.root).unwrap()["Split"]["second"],
json!({"Stack":{"entries":[{"panel":"properties","open":false,"height":null}]}})
);
assert!(!docking.layout.contains(&Panel::ToolSets));
reveal_panel(&mut ui.docking, Panel::ToolSets).unwrap();
let tools = ui.docking.layout.location(&Panel::ToolSets).unwrap();
assert_eq!(tools.anchor, Some(Panel::Canvas));
assert_eq!(serde_json::to_value((&tools.placement, &tools.size)).unwrap(), json!([{"Split":"Left"}, {"FixedFirst":220.0}]));
fs::remove_dir_all(directory).unwrap();
}
@@ -327,7 +311,7 @@ mod tests {
let (mut store, ui, error) = Workspace::at(Some(path.clone()));
assert!(ui.is_none());
assert!(error.as_deref().is_some_and(|error| error.contains("left unchanged")));
store.save_if_changed(&UiState { show_palettes: false, ..Default::default() }, Instant::now()).unwrap();
store.save_if_changed(&UiState { show_palettes: false, ..Default::default() }).unwrap();
assert_eq!(fs::read(&path).unwrap(), bytes);
}
fs::remove_dir_all(directory).unwrap();
@@ -342,8 +326,7 @@ mod tests {
json!({"layout":false,"hidden":[]}),
json!({"layout":{"root":null,"floating":[]},"hidden":[["futurePanel",{}]]}),
] {
let workspace = json!({"showToolbar":false,"showPalettes":false,"toolsetTab":"Modeling", "collapsedGroups":["Draw"], "historyLines":7,"docking":docking,
"dockingVersion":cadcraft_ui_egui::docking::LAYOUT_VERSION});
let workspace = json!({"showToolbar":false,"showPalettes":false,"toolsetTab":"Modeling", "collapsedGroups":["Draw"], "historyLines":7,"docking":docking});
assert!(serde_json::from_value::<UiState>(workspace.clone()).is_err(), "runtime state must stay strict");
fs::write(&path, serde_json::to_vec(&json!({"version":1,"workspace":workspace})).unwrap()).unwrap();
let (_, restored, error) = Workspace::at(Some(path.clone()));
@@ -359,41 +342,16 @@ mod tests {
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn rapid_changes_are_written_once_per_interval_with_the_newest_state() {
let directory = temp_dir();
let path = directory.join("workspace.json");
let (mut store, _, _) = Workspace::at(Some(path.clone()));
let start = Instant::now();
let mut ui = UiState { history_lines: 1, ..Default::default() };
assert_eq!(store.save_if_changed(&ui, start).unwrap(), None);
// A drag: a new state every frame, none of them written until the interval ends.
for (frame, lines) in (2..=5).enumerate() {
ui.history_lines = lines;
let now = start + Duration::from_millis(16 * (frame as u64 + 1));
let wait = store.save_if_changed(&ui, now).unwrap();
assert_eq!(wait, Some(MIN_WRITE_INTERVAL - (now - start)));
}
assert_eq!(read(&path).unwrap().unwrap().history_lines, 1);
assert_eq!(store.save_if_changed(&ui, start + MIN_WRITE_INTERVAL).unwrap(), None);
assert_eq!(read(&path).unwrap().unwrap().history_lines, 5);
ui.history_lines = 6;
assert!(store.save_if_changed(&ui, start + MIN_WRITE_INTERVAL).unwrap().is_some());
store.save_on_exit(&ui).unwrap();
assert_eq!(read(&path).unwrap().unwrap().history_lines, 6);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn invalid_update_cannot_replace_last_good_workspace() {
let directory = temp_dir();
let path = directory.join("workspace.json");
let (mut store, _, _) = Workspace::at(Some(path.clone()));
let mut ui = UiState { show_palettes: false, ..Default::default() };
store.save_if_changed(&ui, Instant::now()).unwrap();
store.save_if_changed(&ui).unwrap();
let good = fs::read(&path).unwrap();
ui.collapsed_groups = vec!["x".repeat(257)];
assert!(store.save_if_changed(&ui, Instant::now()).is_err());
assert!(store.save_if_changed(&ui).is_err());
assert_eq!(fs::read(&path).unwrap(), good);
fs::remove_dir_all(directory).unwrap();
}
@@ -406,9 +364,9 @@ mod tests {
// Simulate a destination becoming unavailable after startup.
fs::create_dir(&path).unwrap();
let ui = UiState { show_palettes: false, ..Default::default() };
let first_error = store.save_if_changed(&ui, Instant::now()).unwrap_err();
let first_error = store.save_if_changed(&ui).unwrap_err();
fs::remove_dir(&path).unwrap();
assert_eq!(store.save_if_changed(&ui, Instant::now()).unwrap_err(), first_error);
assert_eq!(store.save_if_changed(&ui).unwrap_err(), first_error);
assert!(!path.exists(), "ordinary frames must not retry disk writes");
store.save_on_exit(&ui).unwrap();
assert!(!read(&path).unwrap().unwrap().show_palettes);
+40 -51
View File
@@ -358,56 +358,6 @@ fn components<'a>(cons: &[&'a Constraint]) -> Vec<Vec<&'a Constraint>> {
groups.into_values().collect()
}
/// The solver weight of each unknown of `model`: kept handles weigh more, preferred-to-move ones
/// less, angles and ratios by size², and contact parameters move freely.
fn solver_weights(model: &Model, opts: &SolveOptions) -> Vec<f64> {
let mut weights = vec![1.0; model.x0.len()];
for s in &model.slots {
let w = if opts.keep.contains(&s.handle) {
1e4
} else if opts.prefer_move.contains(&s.handle) {
1e-2
} else {
1.0
};
for j in s.off..s.off + s.len {
if let Some(slot) = weights.get_mut(j) {
*slot = w;
}
}
for (j, f) in slot_scales(model, s) {
if let Some(slot) = weights.get_mut(j) {
*slot *= f;
}
}
}
// Contact parameters move freely.
let geo = model.slots.iter().map(|s| s.off + s.len).max().unwrap_or(0);
for w in weights.iter_mut().skip(geo) {
*w = 1e-3;
}
weights
}
/// Per-unknown factors for the angles and ratios of one slot: size², so that the movement of
/// points is what is minimised.
fn slot_scales(model: &Model, s: &model::Slot) -> Vec<(usize, f64)> {
let sq = |j: usize| model.x0.get(j).map_or(1.0, |r| (r * r).max(1e-6));
match s.shape {
model::Shape::Arc => vec![(s.off + 3, sq(s.off + 2)), (s.off + 4, sq(s.off + 2))],
model::Shape::Ellipse { arc } => {
let a2 = (sq(s.off + 2) + sq(s.off + 3)).max(1e-6);
let mut v = vec![(s.off + 4, a2)];
if arc {
v.extend([(s.off + 5, a2), (s.off + 6, a2)]);
}
v
}
model::Shape::Insert { .. } => vec![(s.off + 2, model.rigid_extent2(s.handle).max(1.0))],
_ => Vec::new(),
}
}
/// Build the component systems. Constraints that cannot be interpreted are returned as skipped.
pub(crate) fn build(d: &Drawing, cons: &[&Constraint], opts: &SolveOptions) -> Result<(Vec<Comp>, Vec<(u32, String)>), SolveError> {
let mut members_left = model::MAX_RIGID_WORK;
@@ -446,7 +396,46 @@ pub(crate) fn build_budgeted(
if built.is_empty() {
continue;
}
let weights = solver_weights(&model, opts);
let mut weights = vec![1.0; model.x0.len()];
for s in &model.slots {
let w = if opts.keep.contains(&s.handle) {
1e4
} else if opts.prefer_move.contains(&s.handle) {
1e-2
} else {
1.0
};
for j in s.off..s.off + s.len {
if let Some(slot) = weights.get_mut(j) {
*slot = w;
}
}
// Angles and ratios: weight by size² so the movement of points is what is minimised.
let sq = |j: usize| model.x0.get(j).map_or(1.0, |r| (r * r).max(1e-6));
let scaled: Vec<(usize, f64)> = match s.shape {
model::Shape::Arc => vec![(s.off + 3, sq(s.off + 2)), (s.off + 4, sq(s.off + 2))],
model::Shape::Ellipse { arc } => {
let a2 = (sq(s.off + 2) + sq(s.off + 3)).max(1e-6);
let mut v = vec![(s.off + 4, a2)];
if arc {
v.extend([(s.off + 5, a2), (s.off + 6, a2)]);
}
v
}
model::Shape::Insert { .. } => vec![(s.off + 2, model.rigid_extent2(s.handle).max(1.0))],
_ => Vec::new(),
};
for (j, f) in scaled {
if let Some(slot) = weights.get_mut(j) {
*slot *= f;
}
}
}
// Contact parameters move freely.
let geo = model.slots.iter().map(|s| s.off + s.len).max().unwrap_or(0);
for w in weights.iter_mut().skip(geo) {
*w = 1e-3;
}
let held = opts.hold.iter().filter_map(|r| model.lock_indices(r)).flatten().collect();
let tol = scale_tol(&model.x0);
comps.push(Comp { model, built, weights, held, tol });
+65 -72
View File
@@ -191,77 +191,6 @@ impl Model {
Self::build_budgeted(d, handles, &mut members_left)
}
/// Add the parameters of one supported entity to `m`, and return its shape; `None` when it is
/// left out (too large, degenerate or unsupported).
fn add_params(m: &mut Model, d: &Drawing, h: Handle, kind: &EntityKind, off: usize, members_left: &mut usize) -> Option<Shape> {
let shape = match kind {
EntityKind::Line(l) => {
m.x0.extend([l.a.x, l.a.y, l.b.x, l.b.y]);
Shape::Line
}
EntityKind::Circle(c) => {
m.x0.extend([c.center.x, c.center.y, c.radius]);
Shape::Circle
}
EntityKind::Arc(a) => {
m.x0.extend([a.center.x, a.center.y, a.radius, a.start, a.end]);
Shape::Arc
}
EntityKind::LwPolyline(p) => {
if p.vertices.len() < 2 || off + p.vertices.len() * 2 > MAX_PARAMS {
return None;
}
for v in &p.vertices {
m.x0.extend([v.p.x, v.p.y]);
}
Shape::Poly { n: p.vertices.len(), closed: p.closed }
}
EntityKind::Point(p) => {
m.x0.extend([p.p.x, p.p.y]);
Shape::Point
}
EntityKind::Ellipse(el) => {
let ell = cadcraft_geom::Ellipse { center: el.center.xy(), major: el.major.xy(), ratio: el.ratio, start: el.start, end: el.end };
let arc = !ell.is_full();
m.x0.extend([el.center.x, el.center.y, el.major.x, el.major.y, el.ratio]);
if arc {
m.x0.extend([el.start, el.end]);
}
Shape::Ellipse { arc }
}
EntityKind::Spline(sp) => {
let fit = fit_mode(sp);
let pts = if fit { &sp.fit } else { &sp.control };
let n = pts.len();
if !(2..=MAX_SPLINE_POINTS).contains(&n) || off + n * 2 > MAX_PARAMS || (!fit && !sp.is_valid()) {
return None;
}
for p in pts {
m.x0.extend([p.x, p.y]);
}
// Fit points interpolate exactly, so the curve's ends are the first and last
// fit point; a control polygon's ends are its first and last points when the
// knots are clamped.
let ends = !sp.closed && (fit || (clamped(sp, false) && clamped(sp, true)));
// Missing weights default to 1 in eval; only excess weights are unused.
let mut kept = sp.clone();
kept.weights.truncate(kept.control.len());
m.splines.insert(h, kept);
Shape::Spline { n, fit, ends }
}
EntityKind::Insert(ins) => {
let base = Vec2::new(ins.insert.x, ins.insert.y);
let pts: Vec<Vec2> = rigid_points_budgeted(d, ins, members_left).into_iter().map(|p| (p - base).rotate(-ins.rotation)).collect();
m.x0.extend([ins.insert.x, ins.insert.y, ins.rotation]);
let n = pts.len();
m.rigid.insert(h, pts);
Shape::Insert { n }
}
_ => return None,
};
Some(shape)
}
/// Build solver parameters for supported entities while sharing rigid-member sampling work with other models.
pub(crate) fn build_budgeted(d: &Drawing, handles: &[Handle], members_left: &mut usize) -> Model {
let mut m = Model::default();
@@ -271,7 +200,71 @@ impl Model {
}
let Some(e) = d.entity(h) else { continue };
let off = m.x0.len();
let Some(shape) = Self::add_params(&mut m, d, h, &e.kind, off, members_left) else { continue };
let shape = match &e.kind {
EntityKind::Line(l) => {
m.x0.extend([l.a.x, l.a.y, l.b.x, l.b.y]);
Shape::Line
}
EntityKind::Circle(c) => {
m.x0.extend([c.center.x, c.center.y, c.radius]);
Shape::Circle
}
EntityKind::Arc(a) => {
m.x0.extend([a.center.x, a.center.y, a.radius, a.start, a.end]);
Shape::Arc
}
EntityKind::LwPolyline(p) => {
if p.vertices.len() < 2 || off + p.vertices.len() * 2 > MAX_PARAMS {
continue;
}
for v in &p.vertices {
m.x0.extend([v.p.x, v.p.y]);
}
Shape::Poly { n: p.vertices.len(), closed: p.closed }
}
EntityKind::Point(p) => {
m.x0.extend([p.p.x, p.p.y]);
Shape::Point
}
EntityKind::Ellipse(el) => {
let ell = cadcraft_geom::Ellipse { center: el.center.xy(), major: el.major.xy(), ratio: el.ratio, start: el.start, end: el.end };
let arc = !ell.is_full();
m.x0.extend([el.center.x, el.center.y, el.major.x, el.major.y, el.ratio]);
if arc {
m.x0.extend([el.start, el.end]);
}
Shape::Ellipse { arc }
}
EntityKind::Spline(sp) => {
let fit = fit_mode(sp);
let pts = if fit { &sp.fit } else { &sp.control };
let n = pts.len();
if !(2..=MAX_SPLINE_POINTS).contains(&n) || off + n * 2 > MAX_PARAMS || (!fit && !sp.is_valid()) {
continue;
}
for p in pts {
m.x0.extend([p.x, p.y]);
}
// Fit points interpolate exactly, so the curve's ends are the first and last
// fit point; a control polygon's ends are its first and last points when the
// knots are clamped.
let ends = !sp.closed && (fit || (clamped(sp, false) && clamped(sp, true)));
// Missing weights default to 1 in eval; only excess weights are unused.
let mut kept = sp.clone();
kept.weights.truncate(kept.control.len());
m.splines.insert(h, kept);
Shape::Spline { n, fit, ends }
}
EntityKind::Insert(ins) => {
let base = Vec2::new(ins.insert.x, ins.insert.y);
let pts: Vec<Vec2> = rigid_points_budgeted(d, ins, members_left).into_iter().map(|p| (p - base).rotate(-ins.rotation)).collect();
m.x0.extend([ins.insert.x, ins.insert.y, ins.rotation]);
let n = pts.len();
m.rigid.insert(h, pts);
Shape::Insert { n }
}
_ => continue,
};
if m.x0.len() > MAX_PARAMS || m.x0.get(off..).unwrap_or(&[]).iter().any(|v| !v.is_finite()) {
m.x0.truncate(off);
continue;
+33 -45
View File
@@ -441,49 +441,6 @@ impl DimStyle {
.or_else(|| ["name", "annotative"].into_iter().find(|f| f.eq_ignore_ascii_case(name)))
}
/// The value `v` takes for a field whose current value is `old`: coerced to the field's type where
/// the text or number can be read as it.
fn coerce_value(old: &serde_json::Value, v: &serde_json::Value, field: &str) -> serde_json::Value {
use serde_json::Value;
match (old, v) {
(Value::Bool(_), Value::Number(n)) => Value::Bool(n.as_f64().is_some_and(|x| x != 0.0)),
(Value::Bool(_), Value::String(t)) => Value::Bool(matches!(t.to_ascii_lowercase().as_str(), "1" | "on" | "true" | "yes")),
(Value::Number(_), Value::String(t)) => match t.trim().parse::<f64>() {
Ok(x) => serde_json::json!(x),
// Lineweight names.
Err(_) if matches!(field, "dimLineweight" | "extLineweight") => match t.trim().to_ascii_lowercase().as_str() {
"bylayer" => serde_json::json!(-1),
"byblock" => serde_json::json!(-2),
"default" => serde_json::json!(-3),
_ => v.clone(),
},
Err(_) => v.clone(),
},
(Value::Number(_), Value::Bool(b)) => serde_json::json!(u8::from(*b)),
(Value::String(_), Value::Number(n)) => Value::String(n.to_string()),
(Value::Object(_), Value::Number(n)) => {
let c = n.as_i64().and_then(|i| i16::try_from(i).ok()).map(Color::from_aci);
c.and_then(|c| serde_json::to_value(c).ok()).unwrap_or_else(|| v.clone())
}
(Value::Object(_), Value::String(t)) => Color::parse(t).and_then(|c| serde_json::to_value(c).ok()).unwrap_or_else(|| v.clone()),
_ => v.clone(),
}
}
/// Whether `coerced` is outside the range its `field` can take.
fn out_of_range(field: &str, coerced: &serde_json::Value) -> bool {
let x = coerced.as_f64();
match field {
"textHeight" | "tolScale" => x.is_some_and(|x| x <= 0.0),
"scale" | "arrowSize" | "tickSize" | "fixedExtLength" => x.is_some_and(|x| x < 0.0),
"fit" | "angularZeroSuppression" => x.is_some_and(|x| x > 3.0),
"textMove" | "tolJust" | "arcSymbol" | "textFill" => x.is_some_and(|x| x > 2.0),
"dimLineweight" | "extLineweight" => x.is_some_and(|x| !(-3.0..=211.0).contains(&x)),
"jogAngle" => x.is_some_and(|x| !(5.0..=90.0).contains(&x)),
_ => false,
}
}
/// Set fields from a JSON object of field or DIM* variable names (numbers, booleans as
/// 0/1, colours as ACI numbers or names). Returns the names that were not recognised or
/// had unusable values.
@@ -501,7 +458,29 @@ impl DimStyle {
rejected.push(k.clone());
continue;
};
let coerced = Self::coerce_value(&old, v, field);
let coerced = match (&old, v) {
(Value::Bool(_), Value::Number(n)) => Value::Bool(n.as_f64().is_some_and(|x| x != 0.0)),
(Value::Bool(_), Value::String(t)) => Value::Bool(matches!(t.to_ascii_lowercase().as_str(), "1" | "on" | "true" | "yes")),
(Value::Number(_), Value::String(t)) => match t.trim().parse::<f64>() {
Ok(x) => serde_json::json!(x),
// Lineweight names.
Err(_) if matches!(field, "dimLineweight" | "extLineweight") => match t.trim().to_ascii_lowercase().as_str() {
"bylayer" => serde_json::json!(-1),
"byblock" => serde_json::json!(-2),
"default" => serde_json::json!(-3),
_ => v.clone(),
},
Err(_) => v.clone(),
},
(Value::Number(_), Value::Bool(b)) => serde_json::json!(u8::from(*b)),
(Value::String(_), Value::Number(n)) => Value::String(n.to_string()),
(Value::Object(_), Value::Number(n)) => {
let c = n.as_i64().and_then(|i| i16::try_from(i).ok()).map(Color::from_aci);
c.and_then(|c| serde_json::to_value(c).ok()).unwrap_or_else(|| v.clone())
}
(Value::Object(_), Value::String(t)) => Color::parse(t).and_then(|c| serde_json::to_value(c).ok()).unwrap_or_else(|| v.clone()),
_ => v.clone(),
};
// Integers stored in u8 fields: round floats.
let coerced = match (&old, &coerced) {
// Lineweights (i16): whole hundredths of a mm or -1/-2/-3.
@@ -523,7 +502,16 @@ impl DimStyle {
_ => coerced,
};
// Sizes that can't be negative (text height and tolerance scale: not zero either).
let out_of_range = Self::out_of_range(field, &coerced);
let x = coerced.as_f64();
let out_of_range = match field {
"textHeight" | "tolScale" => x.is_some_and(|x| x <= 0.0),
"scale" | "arrowSize" | "tickSize" | "fixedExtLength" => x.is_some_and(|x| x < 0.0),
"fit" | "angularZeroSuppression" => x.is_some_and(|x| x > 3.0),
"textMove" | "tolJust" | "arcSymbol" | "textFill" => x.is_some_and(|x| x > 2.0),
"dimLineweight" | "extLineweight" => x.is_some_and(|x| !(-3.0..=211.0).contains(&x)),
"jogAngle" => x.is_some_and(|x| !(5.0..=90.0).contains(&x)),
_ => false,
};
if out_of_range {
rejected.push(k.clone());
continue;
+24 -23
View File
@@ -120,7 +120,7 @@ fn style(s: &Session) -> String {
s.doc().map(|d| d.header.str("DIMSTYLE", "Standard")).unwrap_or_else(|_| "Standard".into())
}
fn dim(s: &Session, kind: DimKind, defpt: Vec2, p13: Vec2, p14: Vec2, p15: Vec2, text: &str) -> EntityKind {
fn dim(s: &Session, kind: DimKind, defpt: Vec2, p13: Vec2, p14: Vec2, p15: Vec2, p16: Vec2, text: &str) -> EntityKind {
EntityKind::Dimension(Dimension {
kind,
defpt: v3(defpt),
@@ -128,7 +128,7 @@ fn dim(s: &Session, kind: DimKind, defpt: Vec2, p13: Vec2, p14: Vec2, p15: Vec2,
p13: v3(p13),
p14: v3(p14),
p15: v3(p15),
p16: v3(Vec2::ZERO),
p16: v3(p16),
text: text.into(),
style: style(s),
measurement: 0.0,
@@ -205,10 +205,7 @@ fn angular_from_lines(s: &Session, h1: Handle, h2: Handle, at: Vec2) -> Option<E
// The picked leg end goes last in each pair, so the renderer measures between p14 and p15.
let (o1, t1) = if s1 == AssocSnap::Start { (a2, AssocSnap::End) } else { (a1, AssocSnap::Start) };
let (o2, t2) = if s2 == AssocSnap::Start { (b2, AssocSnap::End) } else { (b1, AssocSnap::Start) };
let mut k = dim(s, DimKind::Angular, o2, o1, p1, p2, "");
if let EntityKind::Dimension(d) = &mut k {
d.p16 = v3(at);
}
let k = dim(s, DimKind::Angular, o2, o1, p1, p2, at, "");
let mut assoc = Vec::new();
if assoc_enabled(s) {
assoc = vec![
@@ -228,7 +225,7 @@ fn run_linear(s: &mut Session, p: &Value) -> Result<Value> {
};
let at = point_req("dimlinear", p, "at")?;
let rot = p.get("rotation").and_then(Value::as_f64).map(f64::to_radians);
let k = dim(s, linear_kind(a, b, at, rot, false), at, a, b, Vec2::ZERO, str_param(p, "text").unwrap_or(""));
let k = dim(s, linear_kind(a, b, at, rot, false), at, a, b, Vec2::ZERO, Vec2::ZERO, str_param(p, "text").unwrap_or(""));
Ok(json!({ "handle": add_dim(s, k)?.hex() }))
}
@@ -236,7 +233,7 @@ fn run_aligned(s: &mut Session, p: &Value) -> Result<Value> {
let a = point_req("dimaligned", p, "p1")?;
let b = point_req("dimaligned", p, "p2")?;
let at = point_req("dimaligned", p, "at")?;
let k = dim(s, DimKind::Aligned, at, a, b, Vec2::ZERO, str_param(p, "text").unwrap_or(""));
let k = dim(s, DimKind::Aligned, at, a, b, Vec2::ZERO, Vec2::ZERO, str_param(p, "text").unwrap_or(""));
Ok(json!({ "handle": add_dim(s, k)?.hex() }))
}
@@ -264,9 +261,9 @@ fn radial(s: &mut Session, p: &Value, diameter: bool) -> Result<Value> {
return Err(bad(id, "`point` must differ from `center`"));
}
let k = if diameter {
dim(s, DimKind::Diameter, c + (c - pt), Vec2::ZERO, Vec2::ZERO, pt, "")
dim(s, DimKind::Diameter, c + (c - pt), Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "")
} else {
dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, pt, "")
dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "")
};
let k = match at {
Some(at) => text_at(k, at),
@@ -311,7 +308,7 @@ fn run_angular(s: &mut Session, p: &Value) -> Result<Value> {
_ => return Err(bad("dimangular", "object is not an arc")),
};
let g = cadcraft_geom::Arc::new(a.center.xy(), a.radius, a.start, a.end);
let k = dim(s, DimKind::Angular3P, at, g.start_point(), g.end_point(), g.center, "");
let k = dim(s, DimKind::Angular3P, at, g.start_point(), g.end_point(), g.center, Vec2::ZERO, "");
return Ok(json!({ "handle": add_dim(s, k)?.hex() }));
}
let v = point_req("dimangular", p, "vertex")?;
@@ -321,7 +318,7 @@ fn run_angular(s: &mut Session, p: &Value) -> Result<Value> {
if v.near(a, 1e-12) || v.near(b, 1e-12) {
return Err(bad("dimangular", "`p1` and `p2` must differ from `vertex`"));
}
let k = dim(s, DimKind::Angular3P, at, a, b, v, "");
let k = dim(s, DimKind::Angular3P, at, a, b, v, Vec2::ZERO, "");
Ok(json!({ "handle": add_dim(s, k)?.hex() }))
}
@@ -333,7 +330,7 @@ fn run_arclen(s: &mut Session, p: &Value) -> Result<Value> {
_ => return Err(bad("dimarc", "object is not an arc")),
};
let ga = cadcraft_geom::Arc::new(a.center.xy(), a.radius, a.start, a.end);
let k = dim(s, DimKind::ArcLength, at, ga.start_point(), ga.end_point(), ga.center, "");
let k = dim(s, DimKind::ArcLength, at, ga.start_point(), ga.end_point(), ga.center, Vec2::ZERO, "");
Ok(json!({ "handle": add_dim(s, k)?.hex() }))
}
@@ -341,7 +338,7 @@ fn run_ordinate(s: &mut Session, p: &Value) -> Result<Value> {
let f = point_req("dimordinate", p, "feature")?;
let l = point_req("dimordinate", p, "leader")?;
let xtype = p.get("xtype").and_then(Value::as_bool).unwrap_or((l - f).y.abs() > (l - f).x.abs());
let k = dim(s, DimKind::Ordinate { x_type: xtype }, Vec2::ZERO, f, l, Vec2::ZERO, "");
let k = dim(s, DimKind::Ordinate { x_type: xtype }, Vec2::ZERO, f, l, Vec2::ZERO, Vec2::ZERO, "");
Ok(json!({ "handle": add_dim(s, k)?.hex() }))
}
@@ -1063,7 +1060,7 @@ impl Interactive for LinearM {
}
(_, Input::Point(at)) => {
let (Some(&a), Some(&b)) = (self.pts.first(), self.pts.get(1)) else { return Ok(Step::Continue) };
let k = self.text.apply(dim(s, linear_kind(a, b, at, self.rotation, self.aligned), at, a, b, Vec2::ZERO, ""));
let k = self.text.apply(dim(s, linear_kind(a, b, at, self.rotation, self.aligned), at, a, b, Vec2::ZERO, Vec2::ZERO, ""));
add_dim(s, k)?;
if let Some(EntityKind::Dimension(d)) = s.last_dim.and_then(|h| s.doc().ok()?.entity(h).map(|e| e.kind.clone())) {
let st = s.doc()?.dim_style(&d.style).cloned().unwrap_or_default();
@@ -1090,7 +1087,7 @@ impl Interactive for LinearM {
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
match self.pts.as_slice() {
[a] => vec![super::helpers::line(*a, c)],
[a, b] => vec![self.text.apply(dim(s, linear_kind(*a, *b, c, self.rotation, self.aligned), c, *a, *b, Vec2::ZERO, ""))],
[a, b] => vec![self.text.apply(dim(s, linear_kind(*a, *b, c, self.rotation, self.aligned), c, *a, *b, Vec2::ZERO, Vec2::ZERO, ""))],
_ => Vec::new(),
}
}
@@ -1109,9 +1106,9 @@ impl RadialM {
fn kind(&self, s: &Session, c: Vec2, r: f64, at: Vec2) -> EntityKind {
let pt = c + (at - c).normalized() * r;
let k = self.text.apply(if self.diameter {
dim(s, DimKind::Diameter, c + (c - pt), Vec2::ZERO, Vec2::ZERO, pt, "")
dim(s, DimKind::Diameter, c + (c - pt), Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "")
} else {
dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, pt, "")
dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, pt, Vec2::ZERO, "")
});
text_at(k, at)
}
@@ -1244,7 +1241,7 @@ impl Interactive for AngularM {
([h1, h2], None) => angular_from_lines(s, *h1, *h2, at),
_ => None,
};
let k = k.unwrap_or_else(|| dim(s, DimKind::Angular3P, at, a, b, v, ""));
let k = k.unwrap_or_else(|| dim(s, DimKind::Angular3P, at, a, b, v, Vec2::ZERO, ""));
add_dim(s, self.text.apply(k))?;
Ok(Step::Done)
}
@@ -1310,7 +1307,9 @@ impl Interactive for AngularM {
}
}
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
self.geometry().map(|(v, a, b)| vec![self.text.apply(dim(s, DimKind::Angular3P, self.located(v, c), a, b, v, ""))]).unwrap_or_default()
self.geometry()
.map(|(v, a, b)| vec![self.text.apply(dim(s, DimKind::Angular3P, self.located(v, c), a, b, v, Vec2::ZERO, ""))])
.unwrap_or_default()
}
}
@@ -1386,7 +1385,7 @@ impl Interactive for ArcLenM {
Ok(Step::Continue)
}
(Some(g), Input::Point(at)) => {
add_dim(s, self.text.apply(dim(s, DimKind::ArcLength, at, g.start_point(), g.end_point(), g.center, "")))?;
add_dim(s, self.text.apply(dim(s, DimKind::ArcLength, at, g.start_point(), g.end_point(), g.center, Vec2::ZERO, "")))?;
Ok(Step::Done)
}
(Some(_), Input::Keyword(k)) => {
@@ -1405,7 +1404,9 @@ impl Interactive for ArcLenM {
if self.partial.is_some() {
return Vec::new();
}
self.arc.map(|g| vec![self.text.apply(dim(s, DimKind::ArcLength, c, g.start_point(), g.end_point(), g.center, ""))]).unwrap_or_default()
self.arc
.map(|g| vec![self.text.apply(dim(s, DimKind::ArcLength, c, g.start_point(), g.end_point(), g.center, Vec2::ZERO, ""))])
.unwrap_or_default()
}
}
@@ -1420,7 +1421,7 @@ struct OrdinateM {
impl OrdinateM {
fn kind(&self, s: &Session, f: Vec2, l: Vec2) -> EntityKind {
let x = self.xtype.unwrap_or((l - f).y.abs() > (l - f).x.abs());
self.text.apply(dim(s, DimKind::Ordinate { x_type: x }, Vec2::ZERO, f, l, Vec2::ZERO, ""))
self.text.apply(dim(s, DimKind::Ordinate { x_type: x }, Vec2::ZERO, f, l, Vec2::ZERO, Vec2::ZERO, ""))
}
}
+1 -1
View File
@@ -283,7 +283,7 @@ fn jogged_kind(s: &Session, c: Vec2, r: f64, center: Vec2, at: Vec2, jog: Option
}
let tip = c + dir * r;
let jog = jog.unwrap_or_else(|| center.mid(tip));
let mut k = dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, tip, text);
let mut k = dim(s, DimKind::Radius, c, Vec2::ZERO, Vec2::ZERO, tip, Vec2::ZERO, text);
if let EntityKind::Dimension(dm) = &mut k {
dm.marks.jogged = Some(JoggedRadius { center: v3(center), jog: v3(jog) });
if at.dist(c) > r + 1e-9 {
+5 -4
View File
@@ -96,9 +96,9 @@ fn plan(
let dir = if dir == Vec2::ZERO || !dir.is_finite() { Vec2::from_angle(std::f64::consts::FRAC_PI_4) } else { dir };
let p = *c + dir * *r;
out.push(if mode == Mode::Diameter {
dim(s, DimKind::Diameter, *c - dir * *r, Vec2::ZERO, Vec2::ZERO, p, "")
dim(s, DimKind::Diameter, *c - dir * *r, Vec2::ZERO, Vec2::ZERO, p, Vec2::ZERO, "")
} else {
dim(s, DimKind::Radius, *c, Vec2::ZERO, Vec2::ZERO, p, "")
dim(s, DimKind::Radius, *c, Vec2::ZERO, Vec2::ZERO, p, Vec2::ZERO, "")
});
}
if out.is_empty() {
@@ -131,7 +131,7 @@ fn plan(
.iter()
.map(|p| {
let end = if vertical { Vec2::new(at.x, p.y) } else { Vec2::new(p.x, at.y) };
dim(s, DimKind::Ordinate { x_type: !vertical }, origin, *p, end, Vec2::ZERO, "")
dim(s, DimKind::Ordinate { x_type: !vertical }, origin, *p, end, Vec2::ZERO, Vec2::ZERO, "")
})
.collect());
}
@@ -147,7 +147,8 @@ fn plan(
let mid = bounds.center();
let out_dir =
if vertical { Vec2::new(if at.x >= mid.x { 1.0 } else { -1.0 }, 0.0) } else { Vec2::new(0.0, if at.y >= mid.y { 1.0 } else { -1.0 }) };
let lin = |a: Vec2, b: Vec2, k: usize| dim(s, DimKind::Linear { rotation: rot }, at + out_dir * (step * k as f64), a, b, Vec2::ZERO, "");
let lin =
|a: Vec2, b: Vec2, k: usize| dim(s, DimKind::Linear { rotation: rot }, at + out_dir * (step * k as f64), a, b, Vec2::ZERO, Vec2::ZERO, "");
Ok(match mode {
Mode::Baseline => {
// From the datum (the point nearest it, by default the first) to every other point.
+1 -2
View File
@@ -16,8 +16,7 @@ use serde_json::{Value, json};
use super::super::helpers::v3;
use super::super::machines::{SelOutcome, SelectPhase, number};
use super::super::{CommandSpec, bad, bool_or, point_param, targets};
use super::super::*;
use super::{KEYWORDS, angle, check_size, count, fmt, nesting, other, yes, yes_no};
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
+1 -3
View File
@@ -5,9 +5,7 @@ use std::sync::Arc;
use serde_json::{Value, json};
use super::{CommandSpec, bad, bool_or, file, str_param};
use super::always;
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
pub fn specs() -> Vec<CommandSpec> {
+1 -3
View File
@@ -12,9 +12,7 @@ use serde_json::{Value, json};
use super::helpers::v3;
use super::machines::{SelOutcome, SelectPhase, number};
use super::{CommandSpec, bad, bool_or, f64_or, find_command, ok, point_param, point_req, point_value, str_param, targets};
use super::has_doc;
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
mod attedit;
+1 -2
View File
@@ -14,8 +14,7 @@ use cadcraft_geom::{Line, Vec2};
use serde_json::{Value, json};
use super::machines::{SelOutcome, SelectPhase};
use super::{CommandSpec, bad, bool_or, f64_or, point_value, str_param, targets};
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
macro_rules! gc {
+2 -7
View File
@@ -5,15 +5,10 @@ use cadcraft_doc::{EntityKind, HAlign, MText, Point, RayLine, Text, VAlign};
use cadcraft_geom::{Arc, Circle, Ellipse, PolyVertex, Polyline, Spline, Vec2, arc_to_bulge, bulge_to_arc};
use serde_json::{Value, json};
use super::helpers::{arc, circle, line, lwpoly, polygon_from_edge, polygon_typed_radius_point, polygon_vertices, v3};
use super::helpers::*;
use super::machines::number;
use super::pline_opts::{Outcome, PlineAsk};
use super::{
CommandSpec, bad, bool_or, distinct_points, f64_or, f64_req, find_command, point_param, point_req, point_value, points_param, size_param,
str_param,
};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step, snap};
/// Every draw command's spec: JSON parameters, menu path, aliases and the interactive prompt
+26 -35
View File
@@ -7,11 +7,9 @@ use cadcraft_geom::{Arc, Circle, Ellipse, Line, PolyVertex, Polyline, Spline, TA
use serde_json::{Value, json};
use super::curves::{self, Chain, MAX_GEN, TanObj};
use super::helpers::{arc, circle, line, lwpoly, rect_vertices, rect_with_corners, v3};
use super::helpers::*;
use super::machines::number;
use super::{CommandSpec, bad, bool_or, f64_or, f64_req, point_param, point_req, point_value, points_param, size_param, str_param, targets};
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
pub(crate) mod breakline;
@@ -1574,7 +1572,7 @@ impl Interactive for XlineM2 {
// RECTANG with Area / Dimensions / Rotation / Width / Elevation / Thickness
// =====================================================================================
fn rect_kind(first: Vec2, (w, h): (f64, f64), rot: f64, fillet: f64, chamfer: (f64, f64), width: f64, elevation: f64) -> EntityKind {
fn rect_kind(first: Vec2, w: f64, h: f64, rot: f64, fillet: f64, chamfer: (f64, f64), width: f64, elevation: f64) -> EntityKind {
let local = rect_with_corners(Vec2::ZERO, Vec2::new(w, h), fillet, chamfer);
let vs = local.into_iter().map(|v| PolyVertex { p: first + v.p.rotate(rot), ..v }).collect();
let mut k = lwpoly(vs, true);
@@ -1585,10 +1583,25 @@ fn rect_kind(first: Vec2, (w, h): (f64, f64), rot: f64, fillet: f64, chamfer: (f
k
}
/// The width and height of a rectangle from its `dimensions`, its `p2` corner, its `area` with
/// `length` or `breadth`, or its second corner `p2`, rotated by `rot` about `a`.
fn rect_size(p: &Value, a: Vec2, rot: f64, centered: bool) -> Result<(f64, f64)> {
Ok(if let Some(d) = p.get("dimensions").and_then(Value::as_array) {
/// Draws a rectangle from the JSON form: `p1` is a corner, or the centre when `center` is
/// true, and the size comes from `p2`, `dimensions` or `area` plus `length`/`breadth`.
pub(crate) fn run_rectang2(s: &mut Session, p: &Value) -> Result<Value> {
let a = point_req("rectang", p, "p1")?;
// Centre mode: `p1` is the rectangle's centre, so the rectangle spans it evenly.
let centered = bool_or(p, "center", false);
let rot = f64_or(p, "rotation", 0.0).to_radians();
let ch = p
.get("chamfer")
.and_then(Value::as_array)
.map(|c| (c.first().and_then(Value::as_f64).unwrap_or(0.0), c.get(1).and_then(Value::as_f64).unwrap_or(0.0)))
.map(|(x, y)| (if x.is_finite() { x } else { 0.0 }, if y.is_finite() { y } else { 0.0 }))
.unwrap_or((0.0, 0.0));
if ch.0 < 0.0 || ch.1 < 0.0 {
return Err(bad("rectang", "`chamfer` distances must be zero or positive"));
}
let fillet = size_param("rectang", p, "fillet", true)?.unwrap_or(0.0);
let width = size_param("rectang", p, "width", true)?.unwrap_or(0.0);
let (w, h) = if let Some(d) = p.get("dimensions").and_then(Value::as_array) {
let l = d.first().and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad("rectang", "dimensions: [length, width]"))?;
let wd = d.get(1).and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad("rectang", "dimensions: [length, width]"))?;
let (sx, sy) = match point_param(p, "p2") {
@@ -1613,33 +1626,12 @@ fn rect_size(p: &Value, a: Vec2, rot: f64, centered: bool) -> Result<(f64, f64)>
let loc = (b - a).rotate(-rot);
// In centre mode the picked point is one corner, mirrored across the centre.
if centered { (2.0 * loc.x, 2.0 * loc.y) } else { (loc.x, loc.y) }
})
}
/// Draws a rectangle from the JSON form: `p1` is a corner, or the centre when `center` is
/// true, and the size comes from `p2`, `dimensions` or `area` plus `length`/`breadth`.
pub(crate) fn run_rectang2(s: &mut Session, p: &Value) -> Result<Value> {
let a = point_req("rectang", p, "p1")?;
// Centre mode: `p1` is the rectangle's centre, so the rectangle spans it evenly.
let centered = bool_or(p, "center", false);
let rot = f64_or(p, "rotation", 0.0).to_radians();
let ch = p
.get("chamfer")
.and_then(Value::as_array)
.map(|c| (c.first().and_then(Value::as_f64).unwrap_or(0.0), c.get(1).and_then(Value::as_f64).unwrap_or(0.0)))
.map(|(x, y)| (if x.is_finite() { x } else { 0.0 }, if y.is_finite() { y } else { 0.0 }))
.unwrap_or((0.0, 0.0));
if ch.0 < 0.0 || ch.1 < 0.0 {
return Err(bad("rectang", "`chamfer` distances must be zero or positive"));
}
let fillet = size_param("rectang", p, "fillet", true)?.unwrap_or(0.0);
let width = size_param("rectang", p, "width", true)?.unwrap_or(0.0);
let (w, h) = rect_size(p, a, rot, centered)?;
};
if w.abs() < 1e-12 || h.abs() < 1e-12 || !w.is_finite() || !h.is_finite() {
return Err(bad("rectang", "rectangle has no area"));
}
let first = if centered { a - Vec2::new(w / 2.0, h / 2.0).rotate(rot) } else { a };
let k = rect_kind(first, (w, h), rot, fillet, ch, width, f64_or(p, "elevation", 0.0));
let k = rect_kind(first, w, h, rot, fillet, ch, width, f64_or(p, "elevation", 0.0));
let hd = s.add_entity(k)?;
// A rectangle has its own thickness option and does not take THICKNESS.
let th = f64_or(p, "thickness", 0.0);
@@ -1691,8 +1683,7 @@ impl RectM2 {
return Err(other("The rectangle has no area."));
}
let first = self.origin(w, h).unwrap_or(a);
let hd = s.add_entity(rect_kind(first, (w, h), self.rotation, self.fillet, self.chamfer, self.width, self.elevation))?;
// A rectangle has its own thickness option and does not take THICKNESS.
let hd = s.add_entity(rect_kind(first, w, h, self.rotation, self.fillet, self.chamfer, self.width, self.elevation))?;
let t = self.thickness;
s.doc_mut()?.modify_entity(hd, |e| e.common.thickness = t)?;
Ok(Step::Done)
@@ -1870,7 +1861,7 @@ impl Interactive for RectM2 {
}
match self.other_corner(c).and_then(|(w, h)| self.origin(w, h).map(|a| (a, w, h))) {
Some((a, w, h)) if w.abs() > 1e-12 && h.abs() > 1e-12 => {
vec![rect_kind(a, (w, h), self.rotation, self.fillet, self.chamfer, self.width, self.elevation)]
vec![rect_kind(a, w, h, self.rotation, self.fillet, self.chamfer, self.width, self.elevation)]
}
_ => Vec::new(),
}
+2 -3
View File
@@ -9,11 +9,10 @@ use cadcraft_doc::{Drawing, EntityKind};
use cadcraft_geom::{PolyVertex, Vec2};
use serde_json::Value;
use super::super::machines::number;
use super::added;
use super::curves::MAX_GEN;
use super::{CommandSpec, line, lwpoly, point_param, point_req, size_param, str_param};
use super::machines::number;
use super::*;
use crate::cmd::modify2::as_lwpoly;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
+2 -4
View File
@@ -9,11 +9,9 @@ use cadcraft_doc::{
use cadcraft_geom::{Line, Vec2, line_line_infinite};
use serde_json::{Value, json};
use super::super::machines::number;
use super::super::{distinct_points, ok};
use super::curves::{self, MAX_GEN};
use super::{CommandSpec, bad, bool_or, f64_or, point_req, points_param, str_param};
use super::machines::number;
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
pub fn specs() -> Vec<CommandSpec> {
+1 -2
View File
@@ -8,8 +8,7 @@ use cadcraft_doc::{EntityKind, Handle};
use serde_json::{Value, json};
use super::machines::{SelOutcome, SelectPhase, SelectRun};
use super::{CommandSpec, EngineError, bad, ok, targets};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
pub fn specs() -> Vec<CommandSpec> {
+1 -2
View File
@@ -5,8 +5,7 @@ use cadcraft_geom::{Bounds2, Mat3, Vec2};
use serde_json::{Value, json};
use super::machines::{SelOutcome, SelectPhase, SelectRun};
use super::{CommandSpec, EngineError, bad, bool_or, clipboard, f64_or, has_selection, point_param, point_req, points_param, str_param, targets};
use super::*;
use crate::undoctl::Control;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
+1 -5
View File
@@ -18,8 +18,7 @@ use cadcraft_render::raster::{RasterOptions, View};
use serde_json::{Value, json};
use super::file::{base64_decode, base64_encode, io};
use super::{CommandSpec, bad, point_value, str_param};
use super::*;
use crate::{EngineError, Result, Session};
/// Formats EXPORT writes (the io layer picks the writer from the extension).
@@ -558,10 +557,7 @@ mod tests {
use cadcraft_geom::Vec3;
use super::super::file::{IoHooks, set_io};
use super::super::resolve_alias;
use super::*;
use serde_json::json;
static DELIVERED: std::sync::Mutex<Vec<(String, String, usize)>> = std::sync::Mutex::new(Vec::new());
+1 -3
View File
@@ -11,8 +11,7 @@ use cadcraft_doc::Drawing;
pub use cadcraft_dxf::Version;
use serde_json::{Value, json};
use super::{CommandSpec, EngineError, Interactive, always, bad, bool_or, find_command, ok, str_param};
use super::*;
use crate::{Accept, Input, Prompt, Result, Session, Step};
/// The rules the Command Aliases dialog checks before it sends an alias to ALIASEDIT.
@@ -732,7 +731,6 @@ mod tests {
#[cfg(test)]
mod pr193_tests {
use super::*;
use crate::Session;
#[test]
fn failed_store_keeps_unsaved_changes_and_delivered_save_clears_them() {
+1 -3
View File
@@ -7,9 +7,7 @@ use std::collections::{HashMap, HashSet};
use cadcraft_doc::Handle;
use serde_json::{Value, json};
use super::super::{CommandSpec, bad};
use super::super::always;
use super::super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
pub fn specs() -> Vec<CommandSpec> {
-2
View File
@@ -11,13 +11,11 @@ use serde_json::{Value, json};
use super::helpers::lwpoly;
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Picker, Prompt, Result, Session, Step};
mod assoc;
mod edit;
mod pick;
use super::has_doc;
pub(crate) use assoc::update as refill_associative;
pub use pick::{HatchPicks, PickMode};
+1 -1
View File
@@ -13,7 +13,7 @@ use cadcraft_doc::library::{PatternLine, pattern, standard_patterns};
use serde_json::{Value, json};
use super::super::helpers::support_text;
use super::{Result, Session, bad, bool_or};
use super::*;
/// Most user patterns held at once.
pub const MAX_USER_PATTERNS: usize = 4096;
+45 -57
View File
@@ -6,8 +6,7 @@ use cadcraft_doc::{Entity, EntityKind, Handle};
use cadcraft_geom::{Polyline, Vec2, Vec3};
use serde_json::{Value, json};
use super::{CommandSpec, bad, bool_or, f64_req, point_req, point_value, points_param, targets};
use super::*;
use crate::units::{format_angle, format_distance};
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
@@ -963,57 +962,6 @@ impl AreaM {
AreaMode::Subtract => self.total - v,
}
}
/// Input while a height is pending: typed, or given by two points (their distance).
fn height_input(&mut self, s: &mut Session, i: Input) -> Step {
match (i, self.height_from) {
(Input::Point(p), None) => self.height_from = Some(p),
(Input::Point(p), Some(from)) => {
self.height_from = None;
return self.height(s, p.dist(from));
}
(Input::Text(t), None) => match crate::units::parse_distance(&t).filter(|v| v.is_finite()) {
Some(h) => return self.height(s, h),
None => s.echo("Requires a distance."),
},
(Input::Enter, None) => s.echo("Requires a height."),
(Input::Enter, Some(_)) => s.echo("Requires a second point."),
(Input::Text(_), Some(_)) => s.echo("Requires a point."),
_ => {}
}
Step::Continue
}
/// Input while the objects to measure are picked.
fn object_input(&mut self, s: &mut Session, i: Input) -> Step {
match i {
Input::Pick(hs) => {
for h in hs {
match object_area(s, h) {
Ok((a, per)) => {
if self.measured(s, a, per) == Step::Done {
return Step::Done;
}
// A single volume takes the first object with an area.
if self.volume && self.mode == AreaMode::Single {
break;
}
}
Err(_) => s.echo("The selected object has no area."),
}
}
}
Input::Enter => {
self.object = false;
if self.mode == AreaMode::Single {
return Step::Done;
}
}
Input::Text(_) | Input::Keyword(_) => s.echo("*Invalid selection*"),
_ => {}
}
Step::Continue
}
/// Measure the oldest pending area's volume with height `h`.
fn height(&mut self, s: &mut Session, h: f64) -> Step {
match self.pending.first().map(|a| a * h.abs()) {
@@ -1093,10 +1041,52 @@ impl Interactive for AreaM {
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if !self.pending.is_empty() {
return Ok(self.height_input(s, i));
// A height is typed, or given by two points (their distance).
match (i, self.height_from) {
(Input::Point(p), None) => self.height_from = Some(p),
(Input::Point(p), Some(from)) => {
self.height_from = None;
return Ok(self.height(s, p.dist(from)));
}
(Input::Text(t), None) => match crate::units::parse_distance(&t).filter(|v| v.is_finite()) {
Some(h) => return Ok(self.height(s, h)),
None => s.echo("Requires a distance."),
},
(Input::Enter, None) => s.echo("Requires a height."),
(Input::Enter, Some(_)) => s.echo("Requires a second point."),
(Input::Text(_), Some(_)) => s.echo("Requires a point."),
_ => {}
}
return Ok(Step::Continue);
}
if self.object {
return Ok(self.object_input(s, i));
match i {
Input::Pick(hs) => {
for h in hs {
match object_area(s, h) {
Ok((a, per)) => {
if self.measured(s, a, per) == Step::Done {
return Ok(Step::Done);
}
// A single volume takes the first object with an area.
if self.volume && self.mode == AreaMode::Single {
break;
}
}
Err(_) => s.echo("The selected object has no area."),
}
}
}
Input::Enter => {
self.object = false;
if self.mode == AreaMode::Single {
return Ok(Step::Done);
}
}
Input::Text(_) | Input::Keyword(_) => s.echo("*Invalid selection*"),
_ => {}
}
return Ok(Step::Continue);
}
if self.length {
if let Some(p) = length_input(&self.pts, i)? {
@@ -1140,8 +1130,6 @@ impl Interactive for AreaM {
#[cfg(test)]
mod tests {
use cadcraft_doc::Handle;
use super::*;
/// #1440 review: a value one ulp past its bound (a border rectangle's shoelace area against
+1 -2
View File
@@ -9,8 +9,7 @@ use cadcraft_doc::{Drawing, Entity, EntityKind, Handle, Insert, MAX_BLOCK_DEPTH,
use cadcraft_geom::{Mat3, Vec2};
use serde_json::{Value, json};
use super::super::{bad, bool_or, point_req, str_param, targets};
use super::super::*;
use super::SelfCheck;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
+64 -83
View File
@@ -13,8 +13,7 @@ use cadcraft_doc::{EntityKind, Handle};
use cadcraft_geom::{Arc as GeoArc, EPS, Vec2};
use serde_json::{Value, json};
use super::super::bad;
use super::super::*;
use crate::{Result, Session};
const CMD: &str = "loops";
@@ -117,7 +116,21 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
// Endpoints closer than the tolerance meet. Transitive components of three or more ends are
// branch junctions; a component of exactly two is a simple joint.
let mut parent = merge_ends(&ends, tol);
let mut parent: Vec<usize> = (0..n).collect();
for i in 0..n {
let Some(a) = ends.get(i) else { continue };
for j in (i + 1)..n {
let Some(b) = ends.get(j) else { continue };
if a.at.dist(b.at) <= tol {
let (ra, rb) = (find(&mut parent, i), find(&mut parent, j));
if ra != rb
&& let Some(slot) = parent.get_mut(ra)
{
*slot = rb;
}
}
}
}
let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
for i in 0..n {
let root = find(&mut parent, i);
@@ -126,8 +139,54 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
// Junctions are reported and their pieces left out of the chains: not resolved.
let mut excluded = vec![false; pieces.len()];
let (joints, junctions) = sort_groups(&groups, &ends, &pieces, &mut excluded);
let link = joint_links(joints, &ends, &pieces, &excluded);
let mut junctions: Vec<Value> = Vec::new();
let mut joints: Vec<(usize, usize)> = Vec::new();
for group in groups.values() {
match group.as_slice() {
&[a, b] => joints.push((a, b)),
[_, _, ..] => {
let scale = group.len() as f64;
let mut at = Vec2::ZERO;
let mut members: Vec<Handle> = Vec::new();
for &e in group {
let Some(end) = ends.get(e) else { continue };
at += end.at / scale;
let Some(piece) = pieces.get(end.piece) else { continue };
if !members.contains(&piece.handle) {
members.push(piece.handle);
}
if let Some(slot) = excluded.get_mut(end.piece) {
*slot = true;
}
}
junctions.push(json!({
"at": [at.x, at.y],
"ends": group.len(),
"members": members.iter().map(|h| h.hex()).collect::<Vec<_>>(),
}));
}
_ => {}
}
}
// A joint links the two ends for the walk. A joint into an excluded piece is not a link, and
// a line whose own ends meet within a huge tolerance is a spur, not a zero-area loop.
let mut link: Vec<Option<usize>> = vec![None; n];
for (a, b) in joints {
let (Some(ea), Some(eb)) = (ends.get(a), ends.get(b)) else { continue };
if ea.piece == eb.piece && pieces.get(ea.piece).is_some_and(|p| p.arc.is_none()) {
continue;
}
if excluded.get(ea.piece).copied().unwrap_or(true) || excluded.get(eb.piece).copied().unwrap_or(true) {
continue;
}
if let Some(slot) = link.get_mut(a) {
*slot = Some(b);
}
if let Some(slot) = link.get_mut(b) {
*slot = Some(a);
}
}
let mut visited = vec![false; pieces.len()];
let mut loops: Vec<Value> = Vec::new();
@@ -165,84 +224,6 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
}
/// A report with nothing analyzed: an empty drawing, or one past the pair cap.
/// Union-find over the ends: ends closer than `tol` share a root. Returns each end's parent.
fn merge_ends(ends: &[End], tol: f64) -> Vec<usize> {
let n = ends.len();
let mut parent: Vec<usize> = (0..n).collect();
for i in 0..n {
let Some(a) = ends.get(i) else { continue };
for j in (i + 1)..n {
let Some(b) = ends.get(j) else { continue };
if a.at.dist(b.at) <= tol {
let (ra, rb) = (find(&mut parent, i), find(&mut parent, j));
if ra != rb
&& let Some(slot) = parent.get_mut(ra)
{
*slot = rb;
}
}
}
}
parent
}
/// Each group of two ends is a joint (end indices); each of three or more is a junction, reported
/// with its pieces marked `excluded`.
fn sort_groups(groups: &BTreeMap<usize, Vec<usize>>, ends: &[End], pieces: &[Piece], excluded: &mut [bool]) -> (Vec<(usize, usize)>, Vec<Value>) {
let mut joints: Vec<(usize, usize)> = Vec::new();
let mut junctions: Vec<Value> = Vec::new();
for group in groups.values() {
match group.as_slice() {
&[a, b] => joints.push((a, b)),
[_, _, ..] => {
let scale = group.len() as f64;
let mut at = Vec2::ZERO;
let mut members: Vec<Handle> = Vec::new();
for &e in group {
let Some(end) = ends.get(e) else { continue };
at += end.at / scale;
let Some(piece) = pieces.get(end.piece) else { continue };
if !members.contains(&piece.handle) {
members.push(piece.handle);
}
if let Some(slot) = excluded.get_mut(end.piece) {
*slot = true;
}
}
junctions.push(json!({
"at": [at.x, at.y],
"ends": group.len(),
"members": members.iter().map(|h| h.hex()).collect::<Vec<_>>(),
}));
}
_ => {}
}
}
(joints, junctions)
}
/// The link each end takes from the joints. A joint into an excluded piece is not a link, and
/// a line whose own ends meet within a huge tolerance is a spur, not a zero-area loop.
fn joint_links(joints: Vec<(usize, usize)>, ends: &[End], pieces: &[Piece], excluded: &[bool]) -> Vec<Option<usize>> {
let mut link: Vec<Option<usize>> = vec![None; ends.len()];
for (a, b) in joints {
let (Some(ea), Some(eb)) = (ends.get(a), ends.get(b)) else { continue };
if ea.piece == eb.piece && pieces.get(ea.piece).is_some_and(|p| p.arc.is_none()) {
continue;
}
if excluded.get(ea.piece).copied().unwrap_or(true) || excluded.get(eb.piece).copied().unwrap_or(true) {
continue;
}
if let Some(slot) = link.get_mut(a) {
*slot = Some(b);
}
if let Some(slot) = link.get_mut(b) {
*slot = Some(a);
}
}
link
}
fn empty_report(offset: usize, tol: f64, pair_tests: usize, pair_tests_needed: usize, pair_limit: usize, capped: bool, skipped: Skipped) -> Value {
json!({
"schema": 1,
+1 -2
View File
@@ -11,8 +11,7 @@ use cadcraft_geom::{Line, Vec2, line_line, point_in_polygon};
use serde_json::{Value, json};
use super::super::machines::{SelOutcome, SelectPhase, SelectRun};
use super::super::{CommandSpec, bad, targets};
use super::super::*;
use super::SelfCheck;
use crate::{EngineError, Input, Interactive, Prompt, Result, Session, Step};
+1 -3
View File
@@ -6,9 +6,7 @@ use cadcraft_doc::{Entity, EntityKind};
use cadcraft_geom::{Bounds2, Polyline, Vec2, intersect_segments};
use serde_json::{Value, json};
use super::super::bad;
use super::super::point_value;
use super::super::*;
use super::{SelfCheck, area_of, object_area};
use crate::{Result, Session};
+9 -8
View File
@@ -8,9 +8,8 @@ use std::collections::{HashMap, HashSet};
use cadcraft_doc::{Drawing, Entity, EntityKind};
use serde_json::{Value, json};
use super::super::bad;
use super::super::blocks::purge;
use super::super::*;
use crate::{Result, Session};
const CMD: &str = "usage";
@@ -281,9 +280,11 @@ impl Unused {
fn item_json(
c: Category,
name: &str,
(entities, block_entities): (usize, usize),
entities: usize,
block_entities: usize,
references: usize,
(used, empty): (bool, bool),
used: bool,
empty: bool,
current: Option<bool>,
unused: &Unused,
) -> Value {
@@ -313,13 +314,13 @@ fn items(scan: &Scan, unused: &Unused, d: &Drawing) -> Vec<Value> {
let (e, b) = scan.layers.get(&l.name.to_ascii_lowercase()).copied().unwrap_or_default();
let uses = e.saturating_add(b);
let cur = l.name.eq_ignore_ascii_case(&current_layer);
out.push(item_json(Category::Layer, &l.name, (e, b), 0, (uses > 0, uses == 0), Some(cur), unused));
out.push(item_json(Category::Layer, &l.name, e, b, 0, uses > 0, uses == 0, Some(cur), unused));
}
for b in d.blocks.values() {
let (e, n) = scan.blocks.get(&b.name.to_ascii_lowercase()).copied().unwrap_or_default();
// A definition is used when a reference to it is reachable, as PURGE counts it.
let used = scan.used_blocks.contains(&b.name.to_ascii_uppercase());
out.push(item_json(Category::Block, &b.name, (e, n), 0, (used, e.saturating_add(n) == 0), None, unused));
out.push(item_json(Category::Block, &b.name, e, n, 0, used, e.saturating_add(n) == 0, None, unused));
}
let current_style = d.header.str("TEXTSTYLE", "Standard");
for t in &d.text_styles {
@@ -327,7 +328,7 @@ fn items(scan: &Scan, unused: &Unused, d: &Drawing) -> Vec<Value> {
let r = scan.style_refs.get(&t.name.to_ascii_lowercase()).copied().unwrap_or(0);
let uses = e.saturating_add(b).saturating_add(r);
let cur = t.name.eq_ignore_ascii_case(&current_style);
out.push(item_json(Category::TextStyle, &t.name, (e, b), r, (uses > 0, uses == 0), Some(cur), unused));
out.push(item_json(Category::TextStyle, &t.name, e, b, r, uses > 0, uses == 0, Some(cur), unused));
}
let current_linetype = d.header.str("CELTYPE", "ByLayer");
for t in &d.linetypes {
@@ -335,7 +336,7 @@ fn items(scan: &Scan, unused: &Unused, d: &Drawing) -> Vec<Value> {
let r = scan.linetype_refs.get(&t.name.to_ascii_lowercase()).copied().unwrap_or(0);
let uses = e.saturating_add(b).saturating_add(r);
let cur = t.name.eq_ignore_ascii_case(&current_linetype);
out.push(item_json(Category::Linetype, &t.name, (e, b), r, (uses > 0, uses == 0), Some(cur), unused));
out.push(item_json(Category::Linetype, &t.name, e, b, r, uses > 0, uses == 0, Some(cur), unused));
}
out
}
+1 -2
View File
@@ -7,8 +7,7 @@ use cadcraft_doc::{Layer, Lineweight};
use serde_json::{Value, json};
use super::machines::SelectRun;
use super::{CommandSpec, bad, bool_or, ok, str_param, targets};
use super::*;
use crate::{Result, Session};
pub fn specs() -> Vec<CommandSpec> {
-1
View File
@@ -10,7 +10,6 @@ use serde_json::{Value, json};
use super::file::{base64_encode, io};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
use viewports::MviewM;
+1 -2
View File
@@ -8,8 +8,7 @@
use cadcraft_doc::{Drawing, PlotStyle, PlotStyleKind, PlotStyleTable};
use serde_json::{Value, json};
use super::super::targets;
use super::{CommandSpec, Result, Session, bad, str_param};
use super::*;
pub fn specs() -> Vec<CommandSpec> {
vec![
+1 -2
View File
@@ -9,8 +9,7 @@ use serde_json::{Value, json};
use super::helpers::v3;
use super::machines::{SelOutcome, SelectPhase};
use super::{CommandSpec, bad, point_req, points_param, str_param, targets};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
/// Most arrowheads added by one call, and most multileaders aligned / collected by one call.
+132 -131
View File
@@ -7,12 +7,10 @@ use cadcraft_geom::{
};
use serde_json::{Value, json};
use super::helpers::{arc, circle, line, lwpoly, v3};
use super::helpers::*;
use super::machines::{SelOutcome, SelectPhase, number};
use super::trimextend::{self, TrimM};
use super::{CommandSpec, bad, bool_or, f64_or, f64_req, point_param, point_req, points_param, str_param, targets};
use super::*;
use crate::select::{SelectKind, SelectMode};
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
@@ -1296,128 +1294,6 @@ fn run_chamfer(s: &mut Session, p: &Value) -> Result<Value> {
// ---------------- explode / array / order / break / join ----------------
fn explode_block_ref(d: &cadcraft_doc::Drawing, e: &Entity, ins: &cadcraft_doc::Insert) -> Option<Vec<Entity>> {
if let Some(items) = super::array::explode_array(d, e) {
return Some(items);
}
let blk = d.block(&ins.block)?;
let m = ins.transform(blk.base.xy());
Some(
blk.entities
.iter()
.map(|be| {
let mut ne = (**be).clone();
ne.kind.transform(&m);
if ne.common.layer == "0" {
ne.common.layer = e.common.layer.clone();
}
if ne.common.color == cadcraft_color::Color::ByBlock {
ne.common.color = e.common.color;
}
ne
})
.collect(),
)
}
fn explode_mtext(e: &Entity, m: &cadcraft_doc::MText) -> Vec<Entity> {
cadcraft_fonts::plain_mtext(&m.contents)
.split('\n')
.enumerate()
.map(|(i, l)| {
let at = m.insert.xy() + Vec2::from_angle(m.rotation - std::f64::consts::FRAC_PI_2) * (m.height * (1.0 + 5.0 / 3.0 * i as f64));
Entity {
handle: Handle(0),
common: e.common.clone(),
kind: EntityKind::Text(cadcraft_doc::Text {
insert: v3(at),
align_pt: None,
height: m.height,
value: l.into(),
rotation: m.rotation,
width_factor: 1.0,
oblique: 0.0,
style: m.style.clone(),
halign: Default::default(),
valign: Default::default(),
}),
}
})
.collect()
}
/// The entities of a boundary's lines and arcs, on the object's layer and style.
fn segment_entities(e: &Entity, segs: impl IntoIterator<Item = Segment>) -> Vec<Entity> {
segs.into_iter()
.map(|s| {
let k = match s {
Segment::Line(l) => line(l.a, l.b),
Segment::Arc { arc: a, .. } => arc(&a),
};
Entity { handle: Handle(0), common: e.common.clone(), kind: k }
})
.collect()
}
fn explode_dimension(d: &cadcraft_doc::Drawing, e: &Entity, dm: &cadcraft_doc::Dimension) -> Vec<Entity> {
// The dimension as drawn: its overrides, DIMSCALE (0 = the drawing's) and text font.
let style = d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides);
let g = cadcraft_render::dimension_in(d, dm);
let line_entity = |l: &Vec<Vec2>| {
if l.len() == 2 {
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: line(*l.first()?, *l.get(1)?) })
} else {
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: lwpoly(l.iter().map(|p| PolyVertex::new(*p)).collect(), false) })
}
};
// The text's marks (arc length symbol, DIMINSPECT frame) go after the text background.
let text_marks = &g.text_marks;
let marks = |want: bool| g.lines.iter().enumerate().filter(move |(i, _)| text_marks.contains(i) == want).map(|(_, l)| l);
let mut v: Vec<Entity> = marks(false).filter_map(&line_entity).collect();
for t in &g.fills {
if let [a, b, c] = t.as_slice() {
v.push(Entity {
handle: Handle(0),
common: e.common.clone(),
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*a), v3(*b), v3(*c), v3(*c)] }),
});
}
}
// DIMTFILL 2: the text background as a SOLID in DIMTFILLCLR, over the lines and
// arrowheads, under the text and its marks (SOLID corners go 1, 2, 4, 3).
if style.text_fill == 2
&& let [p0, p1, p2, p3] = g.text_fill.as_slice()
{
let fill = style.text_fill_color;
let color = if fill == cadcraft_doc::color::Color::ByBlock { e.common.color } else { fill };
v.push(Entity {
handle: Handle(0),
common: cadcraft_doc::Common { color, ..e.common.clone() },
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*p0), v3(*p1), v3(*p3), v3(*p2)] }),
});
}
v.extend(marks(true).filter_map(&line_entity));
if !g.value.is_empty() {
v.push(Entity {
handle: Handle(0),
common: e.common.clone(),
kind: EntityKind::MText(cadcraft_doc::MText {
insert: v3(g.text_pos),
height: g.text_height,
width: 0.0,
attach: 5,
rotation: g.text_angle,
style: style.text_style.clone(),
contents: g.mtext_contents(),
line_spacing: 1.0,
line_spacing_exact: false,
columns: None,
}),
});
}
v
}
pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<Entity>> {
// An annotative object explodes into plain objects as it draws at the current annotation scale.
if cadcraft_doc::is_annotative(e) {
@@ -1431,9 +1307,42 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<
match &e.kind {
EntityKind::LwPolyline(p) => {
let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed };
Some(segment_entities(e, pl.segments()))
Some(
pl.segments()
.into_iter()
.map(|s| {
let k = match s {
Segment::Line(l) => line(l.a, l.b),
Segment::Arc { arc: a, .. } => arc(&a),
};
Entity { handle: Handle(0), common: e.common.clone(), kind: k }
})
.collect(),
)
}
EntityKind::Insert(ins) => {
if let Some(items) = super::array::explode_array(d, e) {
return Some(items);
}
let blk = d.block(&ins.block)?;
let m = ins.transform(blk.base.xy());
Some(
blk.entities
.iter()
.map(|be| {
let mut ne = (**be).clone();
ne.kind.transform(&m);
if ne.common.layer == "0" {
ne.common.layer = e.common.layer.clone();
}
if ne.common.color == cadcraft_color::Color::ByBlock {
ne.common.color = e.common.color;
}
ne
})
.collect(),
)
}
EntityKind::Insert(ins) => explode_block_ref(d, e, ins),
// A helix explodes into its spline (seen from above).
EntityKind::Helix(h) => Some(vec![Entity { handle: Handle(0), common: e.common.clone(), kind: EntityKind::Spline(h.spline()) }]),
EntityKind::Polyline3d(p) => Some(
@@ -1446,10 +1355,102 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<
.collect(),
),
EntityKind::MLine(m) => Some(super::draw2::mline::explode(e, m)),
EntityKind::MText(m) => Some(explode_mtext(e, m)),
EntityKind::MText(m) => Some(
cadcraft_fonts::plain_mtext(&m.contents)
.split('\n')
.enumerate()
.map(|(i, l)| {
let at = m.insert.xy() + Vec2::from_angle(m.rotation - std::f64::consts::FRAC_PI_2) * (m.height * (1.0 + 5.0 / 3.0 * i as f64));
Entity {
handle: Handle(0),
common: e.common.clone(),
kind: EntityKind::Text(cadcraft_doc::Text {
insert: v3(at),
align_pt: None,
height: m.height,
value: l.into(),
rotation: m.rotation,
width_factor: 1.0,
oblique: 0.0,
style: m.style.clone(),
halign: Default::default(),
valign: Default::default(),
}),
}
})
.collect(),
),
// A region explodes into its boundary's lines and arcs.
EntityKind::Region(r) => Some(segment_entities(e, r.segments())),
EntityKind::Dimension(dm) => Some(explode_dimension(d, e, dm)),
EntityKind::Region(r) => Some(
r.segments()
.into_iter()
.map(|s| {
let k = match s {
Segment::Line(l) => line(l.a, l.b),
Segment::Arc { arc: a, .. } => arc(&a),
};
Entity { handle: Handle(0), common: e.common.clone(), kind: k }
})
.collect(),
),
EntityKind::Dimension(dm) => {
// The dimension as drawn: its overrides, DIMSCALE (0 = the drawing's) and text font.
let style = d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides);
let g = cadcraft_render::dimension_in(d, dm);
let line_entity = |l: &Vec<Vec2>| {
if l.len() == 2 {
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: line(*l.first()?, *l.get(1)?) })
} else {
Some(Entity { handle: Handle(0), common: e.common.clone(), kind: lwpoly(l.iter().map(|p| PolyVertex::new(*p)).collect(), false) })
}
};
// The text's marks (arc length symbol, DIMINSPECT frame) go after the text background.
let text_marks = &g.text_marks;
let marks = |want: bool| g.lines.iter().enumerate().filter(move |(i, _)| text_marks.contains(i) == want).map(|(_, l)| l);
let mut v: Vec<Entity> = marks(false).filter_map(&line_entity).collect();
for t in &g.fills {
if let [a, b, c] = t.as_slice() {
v.push(Entity {
handle: Handle(0),
common: e.common.clone(),
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*a), v3(*b), v3(*c), v3(*c)] }),
});
}
}
// DIMTFILL 2: the text background as a SOLID in DIMTFILLCLR, over the lines and
// arrowheads, under the text and its marks (SOLID corners go 1, 2, 4, 3).
if style.text_fill == 2
&& let [p0, p1, p2, p3] = g.text_fill.as_slice()
{
let fill = style.text_fill_color;
let color = if fill == cadcraft_doc::color::Color::ByBlock { e.common.color } else { fill };
v.push(Entity {
handle: Handle(0),
common: cadcraft_doc::Common { color, ..e.common.clone() },
kind: EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*p0), v3(*p1), v3(*p3), v3(*p2)] }),
});
}
v.extend(marks(true).filter_map(&line_entity));
if !g.value.is_empty() {
v.push(Entity {
handle: Handle(0),
common: e.common.clone(),
kind: EntityKind::MText(cadcraft_doc::MText {
insert: v3(g.text_pos),
height: g.text_height,
width: 0.0,
attach: 5,
rotation: g.text_angle,
style: style.text_style.clone(),
contents: g.mtext_contents(),
line_spacing: 1.0,
line_spacing_exact: false,
columns: None,
}),
});
}
Some(v)
}
EntityKind::Tolerance(t) => Some(super::tolerance::explode(d, e, t)),
_ => None,
}
+2 -4
View File
@@ -9,11 +9,9 @@ use cadcraft_geom::{
use serde_json::{Value, json};
use super::curves::{self, Chain, MAX_GEN, TanObj};
use super::helpers::{arc, line, lwpoly, v3};
use super::helpers::*;
use super::machines::{SelOutcome, SelectPhase, number};
use super::{CommandSpec, bad, bool_or, f64_or, f64_req, point_param, point_req, str_param, targets};
use super::*;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
mod curve_edits;
@@ -80,7 +80,8 @@ fn same_polyline(
s: &mut Session,
h: Handle,
pl: &LwPolyline,
(p1, p2): (Vec2, Vec2),
p1: Vec2,
p2: Vec2,
r: f64,
chamfer: Option<ChamferDistances>,
trim: bool,
@@ -222,7 +223,7 @@ pub(crate) fn fillet_pline(
_ => None,
};
let made = match (&e1.kind, &e2.kind) {
(EntityKind::LwPolyline(pl), _) if h1 == h2 => same_polyline(s, h1, pl, (p1, p2), r, chamfer, trim)?,
(EntityKind::LwPolyline(pl), _) if h1 == h2 => same_polyline(s, h1, pl, p1, p2, r, chamfer, trim)?,
(EntityKind::LwPolyline(pl), k) if as_line(k).is_some() => {
let ln = as_line(k).ok_or_else(|| other("Select a line."))?;
polyline_and_line(s, (h1, pl, p1), (h2, ln, p2), true, r, chamfer, trim)?
@@ -1,10 +1,7 @@
//! PEDIT Edit vertex: Break and Straighten between two marked vertices, Tangent directions
//! that Fit honours, and Regen.
use super::super::point_value;
use super::{
EntityKind, Handle, Input, LwPolyline, PPhase, PeditM, PolyVertex, Result, Session, Value, Vec2, bad, curves, json, other, push_entity, set_kind,
};
use super::*;
fn ordered(pl: &LwPolyline, i: usize, j: usize) -> Result<(usize, usize)> {
let n = pl.vertices.len();
+1 -5
View File
@@ -6,11 +6,7 @@
//! parametrisation, end tangents and fit tolerance. Control-point edits change the curve
//! directly and drop the fit data, as the curve no longer follows it.
use super::super::{point_value, points_param};
use super::{
Accept, Entity, EntityKind, Handle, Input, Interactive, LwPolyline, MAX_GEN, PolyVertex, Prompt, Result, SelOutcome, SelectPhase, Session, Space,
Spline, Step, Value, Vec2, bad, bool_or, curves, f64_req, json, line, number, other, point_req, set_kind, str_param, targets,
};
use super::*;
/// The spline after a JSON SPLINEDIT option (`option` plus that option's parameters).
pub(super) fn run_splinedit(s: &mut Session, p: &Value) -> Result<Value> {
+1 -2
View File
@@ -9,8 +9,7 @@ use cadcraft_doc::{Drawing, Entity, EntityKind, Handle};
use serde_json::{Value, json};
use super::machines::{SelOutcome, SelectPhase, SelectRun};
use super::{CommandSpec, bad, targets};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
/// Most objects one call looks at (a hostile drawing can be huge).
+1 -2
View File
@@ -2,8 +2,7 @@
use serde_json::{Value, json};
use super::{CommandSpec, always, bad, point_param, str_param};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
pub(super) mod iso;
+1 -2
View File
@@ -12,8 +12,7 @@ use cadcraft_geom::Vec2;
use serde_json::{Value, json};
use super::helpers::v3;
use super::{CommandSpec, bad, point_req, size_param, str_param};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
/// Caps for hostile parameters.
+1 -2
View File
@@ -5,8 +5,7 @@ use cadcraft_geom::{PolyVertex, Vec2};
use serde_json::{Value, json};
use super::helpers::{line, lwpoly, v3};
use super::{CommandSpec, bad, bool_or, f64_or, point_req, str_param};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
pub fn specs() -> Vec<CommandSpec> {
+1 -2
View File
@@ -4,8 +4,7 @@ use cadcraft_doc::{EntityKind, Handle};
use cadcraft_geom::{Segment, Vec2};
use serde_json::{Value, json};
use super::{CommandSpec, always, bad, bool_or, point_param, points_param, str_param, targets};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
pub fn specs() -> Vec<CommandSpec> {
+1 -2
View File
@@ -6,8 +6,7 @@ use cadcraft_geom::{Bounds2, Vec2};
use serde_json::{Value, json};
use super::machines::{SelectRun, number};
use super::{CommandSpec, bad, bool_or, f64_or, f64_req, ok, point_param, point_req, str_param, targets};
use super::*;
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step, View};
mod named_views;
+36 -43
View File
@@ -785,48 +785,6 @@ fn glob_cached(
Ok(matched)
}
/// For the bracket class opening at `pat[p]`, whether `text[t]` is in it: the index of its closing
/// `]` when it is, `None` when it is not or the class is never closed.
fn class_end(text: &[char], pat: &[char], p: usize, t: usize, budget: &mut u64) -> Result<Option<usize>, ()> {
let mut close = p;
while let Some(c) = pat.get(close) {
charge_match(budget)?;
if *c == ']' {
break;
}
close += 1;
}
if close == pat.len() {
return Ok(None);
}
let Some(&c) = text.get(t) else { return Ok(None) };
let set = &pat[p + 1..close];
let (neg, set) = match set.split_first() {
Some(('~', r)) if !r.is_empty() => (true, r),
_ => (false, set),
};
let mut hit = false;
let mut i = 0;
while i < set.len() {
charge_match(budget)?;
if set.get(i + 1) == Some(&'-') && i + 2 < set.len() {
if set[i] <= c && c <= set[i + 2] {
hit = true;
}
i += 3;
} else {
if set[i] == c {
hit = true;
}
i += 1;
}
}
if hit == neg {
return Ok(None);
}
Ok(Some(close))
}
/// Evaluate wildcard tokens and backtracking branches under shared work and recursion limits.
fn glob_inner(
text: &[char],
@@ -864,7 +822,42 @@ fn glob_inner(
p += 2;
}
'[' => {
let Some(close) = class_end(text, pat, p, t, budget)? else { return Ok(false) };
let mut close = p;
while let Some(c) = pat.get(close) {
charge_match(budget)?;
if *c == ']' {
break;
}
close += 1;
}
if close == pat.len() {
return Ok(false);
}
let Some(&c) = text.get(t) else { return Ok(false) };
let set = &pat[p + 1..close];
let (neg, set) = match set.split_first() {
Some(('~', r)) if !r.is_empty() => (true, r),
_ => (false, set),
};
let mut hit = false;
let mut i = 0;
while i < set.len() {
charge_match(budget)?;
if set.get(i + 1) == Some(&'-') && i + 2 < set.len() {
if set[i] <= c && c <= set[i + 2] {
hit = true;
}
i += 3;
} else {
if set[i] == c {
hit = true;
}
i += 1;
}
}
if hit == neg {
return Ok(false);
}
t += 1;
p = close + 1;
}
+38 -65
View File
@@ -236,46 +236,6 @@ pub fn tangent_circles(c1: &TanCurve, c2: &TanCurve, r: f64, near: &[Vec2]) -> V
if !(r.is_finite() && r >= 0.0) {
return Vec::new();
}
let Some(win) = search_window(c1, c2, near) else { return Vec::new() };
let size = win.width().max(win.height()).max(r).max(1e-9);
let win = win.expand(2.0 * r + 0.25 * size);
let tol = 1e-9 * size.max(1.0);
let (Some(r1), Some(r2)) = (c1.range(&win), c2.range(&win)) else { return Vec::new() };
let signs: &[(f64, f64)] = if r > 0.0 { &[(1.0, 1.0), (1.0, -1.0), (-1.0, 1.0), (-1.0, -1.0)] } else { &[(0.0, 0.0)] };
let mut out: Vec<CornerFit> = Vec::new();
let mut work = 16_000_000usize;
for (s1, s2) in signs {
let (d1, d2) = (s1 * r, s2 * r);
let (o1, o2) = (sample(c1, &r1, d1), sample(c2, &r2, d2));
let search = TanSearch { c1, r1: &r1, d1, c2, r2: &r2, d2, tol };
// Refine each pair before visiting the next: raw duplicate seeds in an early run must
// not consume all candidate slots. A separate request-wide budget bounds actual work.
for a in &o1 {
for b in &o2 {
if !refine_pair(&search, a, b, &mut work, &mut out) {
return out;
}
}
}
}
out
}
/// The two curves of one tangent search for one sign choice: their ranges and offsets, and the
/// tolerance.
struct TanSearch<'a> {
c1: &'a TanCurve,
r1: &'a Range,
d1: f64,
c2: &'a TanCurve,
r2: &'a Range,
d2: f64,
tol: f64,
}
/// The window the tangent search looks in: the pick points and the extents of the curves that
/// are not lines. `None` when it is empty or not finite.
fn search_window(c1: &TanCurve, c2: &TanCurve, near: &[Vec2]) -> Option<Bounds2> {
let mut win = Bounds2::from_points(near.iter().copied().filter(|p| p.is_finite()));
for c in [c1, c2] {
match c {
@@ -291,34 +251,47 @@ fn search_window(c1: &TanCurve, c2: &TanCurve, near: &[Vec2]) -> Option<Bounds2>
}
}
if win.is_empty() || !(win.width().is_finite() && win.height().is_finite()) {
return None;
return Vec::new();
}
Some(win)
}
/// Refine the crossings of the samples `a` and `b` into circles, adding the distinct ones to
/// `out`. `false` when the work budget or the solution cap ends the search.
fn refine_pair(search: &TanSearch, a: &[(f64, Vec2)], b: &[(f64, Vec2)], work: &mut usize, out: &mut Vec<CornerFit>) -> bool {
let mut seeds = Vec::new();
crossings(a, b, &mut seeds, work);
seeds.sort_by(|x, y| x.0.total_cmp(&y.0).then(x.1.total_cmp(&y.1)));
seeds.dedup_by(|x, y| (x.0 - y.0).abs() < 1e-9 && (x.1 - y.1).abs() < 1e-9);
for seed in seeds {
// Newton refinement has a fixed iteration cap and several curve evaluations.
let Some(left) = work.checked_sub(128) else { return false };
*work = left;
let TanSearch { c1, r1, d1, c2, r2, d2, tol } = *search;
let Some((t1, t2)) = refine(c1, r1, d1, c2, r2, d2, seed, tol) else { continue };
let (Some(center), p1, p2) = (c1.offset(t1, d1), c1.point(t1), c2.point(t2)) else { continue };
let near_dup = |f: &CornerFit| f.center.near(center, tol * 1e3) && f.p1.near(p1, tol * 1e3) && f.p2.near(p2, tol * 1e3);
if !out.iter().any(near_dup) {
out.push(CornerFit { t1, t2, p1, p2, center });
}
if out.len() >= MAX_SOLUTIONS {
return false;
let size = win.width().max(win.height()).max(r).max(1e-9);
let win = win.expand(2.0 * r + 0.25 * size);
let tol = 1e-9 * size.max(1.0);
let (Some(r1), Some(r2)) = (c1.range(&win), c2.range(&win)) else { return Vec::new() };
let signs: &[(f64, f64)] = if r > 0.0 { &[(1.0, 1.0), (1.0, -1.0), (-1.0, 1.0), (-1.0, -1.0)] } else { &[(0.0, 0.0)] };
let mut out: Vec<CornerFit> = Vec::new();
let mut work = 16_000_000usize;
for (s1, s2) in signs {
let (d1, d2) = (s1 * r, s2 * r);
let (o1, o2) = (sample(c1, &r1, d1), sample(c2, &r2, d2));
// Refine each pair before visiting the next: raw duplicate seeds in an early run must
// not consume all candidate slots. A separate request-wide budget bounds actual work.
for a in &o1 {
for b in &o2 {
let mut seeds = Vec::new();
crossings(a, b, &mut seeds, &mut work);
seeds.sort_by(|x, y| x.0.total_cmp(&y.0).then(x.1.total_cmp(&y.1)));
seeds.dedup_by(|x, y| (x.0 - y.0).abs() < 1e-9 && (x.1 - y.1).abs() < 1e-9);
for seed in seeds {
// Newton refinement has a fixed iteration cap and several curve evaluations.
let Some(left) = work.checked_sub(128) else { return out };
work = left;
let Some((t1, t2)) = refine(c1, &r1, d1, c2, &r2, d2, seed, tol) else { continue };
let (Some(center), p1, p2) = (c1.offset(t1, d1), c1.point(t1), c2.point(t2)) else { continue };
let near_dup = |f: &CornerFit| f.center.near(center, tol * 1e3) && f.p1.near(p1, tol * 1e3) && f.p2.near(p2, tol * 1e3);
if !out.iter().any(near_dup) {
out.push(CornerFit { t1, t2, p1, p2, center });
}
if out.len() >= MAX_SOLUTIONS {
return out;
}
}
if work == 0 {
return out;
}
}
}
}
*work != 0
out
}
#[cfg(test)]
+32 -40
View File
@@ -447,44 +447,6 @@ fn islands(g: &Graph, outer: &[usize], p: Vec2) -> Vec<Island> {
out
}
/// The point of the nearest other curve within `snap` of the end `e` of curve `i`, as (curve,
/// segment, parameter, point). `None` when the end touches a curve, or none is near. `Err` when
/// the budget of segment checks runs out.
fn nearest_curve(polys: &[Vec<Vec2>], i: usize, e: Vec2, snap: f64, tol: f64, budget: &mut usize) -> Result<Option<(usize, usize, f64, Vec2)>, ()> {
let mut best: Option<(f64, usize, usize, f64, Vec2)> = None;
let mut touching = false;
for (j, other) in polys.iter().enumerate() {
if j == i {
continue;
}
for (k, w) in other.windows(2).enumerate() {
if *budget == 0 {
return Err(());
}
*budget -= 1;
let [a, b] = w else { continue };
if !(a.is_finite() && b.is_finite())
|| e.x < a.x.min(b.x) - snap
|| e.x > a.x.max(b.x) + snap
|| e.y < a.y.min(b.y) - snap
|| e.y > a.y.max(b.y) + snap
{
continue;
}
let line = Line::new(*a, *b);
let t = line.param_of(e).clamp(0.0, 1.0);
let q = line.at(t);
let d = q.dist(e);
if d <= tol {
touching = true;
} else if d <= snap && best.is_none_or(|(bd, ..)| d < bd) {
best = Some((d, j, k, t, q));
}
}
}
Ok(best.filter(|_| !touching).map(|(_, j, k, t, q)| (j, k, t, q)))
}
/// The curves with each open end that stops within `snap` of another curve moved onto that
/// curve (which gains a vertex there), so the two meet exactly. A curve that is flattened
/// into chords (an ellipse, a spline) lies up to the flattening tolerance off the true curve,
@@ -512,8 +474,38 @@ pub fn snap_ends(polys: &[Vec<Vec2>], snap: f64, tol: f64) -> Vec<Vec<Vec2>> {
if !e.is_finite() {
continue;
}
let Ok(near) = nearest_curve(polys, i, e, snap, tol, &mut budget) else { break 'ends };
let Some((j, k, t, q)) = near else { continue };
let mut best: Option<(f64, usize, usize, f64, Vec2)> = None;
let mut touching = false;
for (j, other) in polys.iter().enumerate() {
if j == i {
continue;
}
for (k, w) in other.windows(2).enumerate() {
if budget == 0 {
break 'ends;
}
budget -= 1;
let [a, b] = w else { continue };
if !(a.is_finite() && b.is_finite())
|| e.x < a.x.min(b.x) - snap
|| e.x > a.x.max(b.x) + snap
|| e.y < a.y.min(b.y) - snap
|| e.y > a.y.max(b.y) + snap
{
continue;
}
let line = Line::new(*a, *b);
let t = line.param_of(e).clamp(0.0, 1.0);
let q = line.at(t);
let d = q.dist(e);
if d <= tol {
touching = true;
} else if d <= snap && best.is_none_or(|(bd, ..)| d < bd) {
best = Some((d, j, k, t, q));
}
}
}
let Some((_, j, k, t, q)) = best.filter(|_| !touching) else { continue };
let Some(slot) = out.get_mut(i).and_then(|v| if at_end { v.last_mut() } else { v.first_mut() }) else { continue };
*slot = q;
let ends = polys.get(j).and_then(|o| o.get(k).zip(o.get(k + 1)));
+326 -396
View File
@@ -249,130 +249,6 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> {
}
/// Parse one entity while charging materialized table cells to the caller's shared import budget.
/// An ARC, with its angles run the other way in a mirrored OCS.
fn arc(c: &Common, t: &T) -> Arc {
let mut a = Arc { center: ocs(c, t.p(10)), radius: t.fd(40, 1.0).abs(), start: t.fd(50, 0.0).to_radians(), end: t.fd(51, 360.0).to_radians() };
if c.extrusion.z < 0.0 {
// Mirrored OCS: angles run the other way.
let (s, e) = (std::f64::consts::PI - a.end, std::f64::consts::PI - a.start);
a.start = cadcraft_geom::norm_angle(s);
a.end = cadcraft_geom::norm_angle(e);
}
a
}
fn ellipse(c: &Common, t: &T) -> Ellipse {
let mut e = Ellipse {
center: t.p(10),
major: t.p(11),
ratio: t.fd(40, 1.0).clamp(1e-9, 1.0),
start: t.fd(41, 0.0),
end: t.fd(42, std::f64::consts::TAU),
};
if c.extrusion.z < 0.0 && (cadcraft_geom::ccw_sweep(e.start, e.end) - std::f64::consts::TAU).abs() > 1e-9 {
// Centre and major axis are WCS, but the minor axis is extrusion × major: with (0,0,-1)
// it points the other way, so parameter t lands where -t does in the XY plane. A full
// ellipse is symmetric and stays as it is.
let (s, e2) = (-e.end, -e.start);
e.start = cadcraft_geom::norm_angle(s);
e.end = cadcraft_geom::norm_angle(e2);
}
e
}
/// A LWPOLYLINE, or the region CADCraft wrote as one.
fn lwpolyline(tags: &[Tag], c: &Common, t: &T) -> Option<EntityKind> {
let mut verts = Vec::new();
let mut cur: Option<PolyVertex> = None;
for tg in tags {
match tg.code {
10 => {
if let Some(v) = cur.take() {
verts.push(v);
}
cur = Some(PolyVertex { p: Vec2::new(tg.f64(), 0.0), ..Default::default() });
}
20 => {
if let Some(v) = cur.as_mut() {
v.p.y = tg.f64();
}
}
40 => {
if let Some(v) = cur.as_mut() {
v.start_width = tg.f64();
}
}
41 => {
if let Some(v) = cur.as_mut() {
v.end_width = tg.f64();
}
}
42 => {
if let Some(v) = cur.as_mut() {
v.bulge = tg.f64();
}
}
_ => {}
}
}
verts.extend(cur);
let flags = t.i(70).unwrap_or(0);
if c.extrusion.z < 0.0 {
for v in &mut verts {
v.p.x = -v.p.x;
v.bulge = -v.bulge;
}
}
let pl = LwPolyline { vertices: verts, closed: flags & 1 != 0, const_width: t.fd(43, 0.0), elevation: t.fd(38, 0.0), plinegen: flags & 128 != 0 };
// A region CADCraft wrote as polylines (`dxf_region`).
crate::dxf_region::from_polyline(tags, pl)
}
fn spline(t: &T) -> Spline {
let degree = t.i(71).unwrap_or(3).clamp(1, 10) as usize;
let knots = t.all_f(40);
let control = t.pts(10);
let weights = t.all_f(41);
let fit = t.pts(11);
let tangent = |c: i32| t.f(c).map(|_| t.p(c).xy());
let fit_opts =
cadcraft_geom::FitOptions { knots: Default::default(), start_tangent: tangent(12), end_tangent: tangent(13), tolerance: t.fd(44, 0.0) }
.sanitized();
let closed = t.i(70).unwrap_or(0) & 1 != 0;
let mut sp = Spline { degree, knots, control, weights, fit: fit.clone(), closed, fit_opts };
if !sp.is_valid() && fit.len() >= 2 {
sp = Spline::fit_with(&fit, sp.closed, fit_opts);
} else if !sp.is_valid() && sp.control.len() >= 2 {
sp = Spline::from_control(sp.control.clone(), degree);
} else if fit.len() >= 3 {
// DXF keeps the knots, not how they were spaced: recover it for later refits.
sp.fit_opts.knots = sp.infer_knot_param();
}
sp
}
/// An ATTDEF, whose multiline text is embedded MTEXT after its own groups.
fn attdef(tags: &[Tag], c: &Common) -> Attrib {
// A multiline definition has its text as embedded MTEXT after its own groups.
let (own, embedded) = split_embedded(tags);
let multiline = multiline_attrib(embedded, c);
let own = T(own);
let mut text = text_from(&own);
if let Some(m) = &multiline {
text.value.clone_from(&m.read_as);
}
Attrib {
tag: own.s(2).unwrap_or_default(),
text,
invisible: own.i(70).unwrap_or(0) & 1 != 0,
constant: own.i(70).unwrap_or(0) & 2 != 0,
prompt: own.s(3).unwrap_or_default(),
props: Default::default(),
multiline,
modes: attrib_modes(own.0),
}
}
fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_left: &mut usize) -> Option<(Common, EntityKind)> {
// The paper-space viewport our writer adds (dxf_write::paper_view) stays implicit.
if kind == "VIEWPORT" && crate::dxf_ext::is_paper_view(tags) {
@@ -384,16 +260,138 @@ fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_left: &mut usi
"LINE" => EntityKind::Line(Line { a: t.p(10), b: t.p(11) }),
"POINT" => EntityKind::Point(Point { p: t.p(10), angle: t.fd(50, 0.0).to_radians() }),
"CIRCLE" => EntityKind::Circle(Circle { center: ocs(&c, t.p(10)), radius: t.fd(40, 1.0).abs() }),
"ARC" => EntityKind::Arc(arc(&c, &t)),
"ELLIPSE" => EntityKind::Ellipse(ellipse(&c, &t)),
"LWPOLYLINE" => lwpolyline(tags, &c, &t)?,
"SPLINE" => EntityKind::Spline(spline(&t)),
"ARC" => {
let mut a =
Arc { center: ocs(&c, t.p(10)), radius: t.fd(40, 1.0).abs(), start: t.fd(50, 0.0).to_radians(), end: t.fd(51, 360.0).to_radians() };
if c.extrusion.z < 0.0 {
// Mirrored OCS: angles run the other way.
let (s, e) = (std::f64::consts::PI - a.end, std::f64::consts::PI - a.start);
a.start = cadcraft_geom::norm_angle(s);
a.end = cadcraft_geom::norm_angle(e);
}
EntityKind::Arc(a)
}
"ELLIPSE" => {
let mut e = Ellipse {
center: t.p(10),
major: t.p(11),
ratio: t.fd(40, 1.0).clamp(1e-9, 1.0),
start: t.fd(41, 0.0),
end: t.fd(42, std::f64::consts::TAU),
};
if c.extrusion.z < 0.0 && (cadcraft_geom::ccw_sweep(e.start, e.end) - std::f64::consts::TAU).abs() > 1e-9 {
// Centre and major axis are WCS, but the minor axis is extrusion × major: with (0,0,-1)
// it points the other way, so parameter t lands where -t does in the XY plane. A full
// ellipse is symmetric and stays as it is.
let (s, e2) = (-e.end, -e.start);
e.start = cadcraft_geom::norm_angle(s);
e.end = cadcraft_geom::norm_angle(e2);
}
EntityKind::Ellipse(e)
}
"LWPOLYLINE" => {
let mut verts = Vec::new();
let mut cur: Option<PolyVertex> = None;
for tg in tags {
match tg.code {
10 => {
if let Some(v) = cur.take() {
verts.push(v);
}
cur = Some(PolyVertex { p: Vec2::new(tg.f64(), 0.0), ..Default::default() });
}
20 => {
if let Some(v) = cur.as_mut() {
v.p.y = tg.f64();
}
}
40 => {
if let Some(v) = cur.as_mut() {
v.start_width = tg.f64();
}
}
41 => {
if let Some(v) = cur.as_mut() {
v.end_width = tg.f64();
}
}
42 => {
if let Some(v) = cur.as_mut() {
v.bulge = tg.f64();
}
}
_ => {}
}
}
verts.extend(cur);
let flags = t.i(70).unwrap_or(0);
if c.extrusion.z < 0.0 {
for v in &mut verts {
v.p.x = -v.p.x;
v.bulge = -v.bulge;
}
}
let pl = LwPolyline {
vertices: verts,
closed: flags & 1 != 0,
const_width: t.fd(43, 0.0),
elevation: t.fd(38, 0.0),
plinegen: flags & 128 != 0,
};
// A region CADCraft wrote as polylines (`dxf_region`).
crate::dxf_region::from_polyline(tags, pl)?
}
"SPLINE" => {
let degree = t.i(71).unwrap_or(3).clamp(1, 10) as usize;
let knots = t.all_f(40);
let control = t.pts(10);
let weights = t.all_f(41);
let fit = t.pts(11);
let tangent = |c: i32| t.f(c).map(|_| t.p(c).xy());
let fit_opts = cadcraft_geom::FitOptions {
knots: Default::default(),
start_tangent: tangent(12),
end_tangent: tangent(13),
tolerance: t.fd(44, 0.0),
}
.sanitized();
let closed = t.i(70).unwrap_or(0) & 1 != 0;
let mut sp = Spline { degree, knots, control, weights, fit: fit.clone(), closed, fit_opts };
if !sp.is_valid() && fit.len() >= 2 {
sp = Spline::fit_with(&fit, sp.closed, fit_opts);
} else if !sp.is_valid() && sp.control.len() >= 2 {
sp = Spline::from_control(sp.control.clone(), degree);
} else if fit.len() >= 3 {
// DXF keeps the knots, not how they were spaced: recover it for later refits.
sp.fit_opts.knots = sp.infer_knot_param();
}
EntityKind::Spline(sp)
}
"HELIX" => EntityKind::Helix(helix(tags)),
"RAY" => EntityKind::Ray(RayLine { base: t.p(10), dir: t.p(11) }),
"XLINE" => EntityKind::XLine(RayLine { base: t.p(10), dir: t.p(11) }),
"TEXT" => EntityKind::Text(text_from(&t)),
"MTEXT" => EntityKind::MText(mtext_from(tags, &c)),
"ATTDEF" => EntityKind::AttDef(attdef(tags, &c)),
"ATTDEF" => {
// A multiline definition has its text as embedded MTEXT after its own groups.
let (own, embedded) = split_embedded(tags);
let multiline = multiline_attrib(embedded, &c);
let own = T(own);
let mut text = text_from(&own);
if let Some(m) = &multiline {
text.value.clone_from(&m.read_as);
}
EntityKind::AttDef(Attrib {
tag: own.s(2).unwrap_or_default(),
text,
invisible: own.i(70).unwrap_or(0) & 1 != 0,
constant: own.i(70).unwrap_or(0) & 2 != 0,
prompt: own.s(3).unwrap_or_default(),
props: Default::default(),
multiline,
modes: attrib_modes(own.0),
})
}
"INSERT" => {
// A mirrored block reference comes with extrusion (0,0,-1): its insertion point, scale,
// rotation and column spacing are in that OCS, where x runs the other way. In the WCS that
@@ -1111,91 +1109,6 @@ fn entity_extras(kind: &str, tags: &[Tag], h: Handle, k: &mut EntityKind, rx: &m
/// Parse a run of entity records (ENTITIES section or a block's body) into entities. Records that
/// can't be placed are counted in `skipped`.
/// The entity of a POLYLINE, with its mesh faces (advancing `i` past its VERTEX records). `None`
/// for a mesh without faces.
fn polyline_entity(recs: &[(String, Vec<Tag>)], i: &mut usize, t: &T) -> Option<(Common, EntityKind, Vec<EntityKind>)> {
let flags = t.i(70).unwrap_or(0);
let mut pts: Vec<(Vec3, f64, f64, f64)> = Vec::new();
// Polyface face records (VERTEX 70 bit 128 without 64): vertex indices 71–74.
let mut faces: Vec<[i64; 4]> = Vec::new();
while let Some((k2, t2)) = recs.get(*i) {
if k2 == "VERTEX" {
let tt = T(t2);
let vflags = tt.i(70).unwrap_or(0);
if vflags & 128 != 0 && vflags & 64 == 0 {
faces.push([71, 72, 73, 74].map(|c| tt.i(c).unwrap_or(0)));
} else if vflags & 16 == 0 {
pts.push((tt.p(10), tt.fd(42, 0.0), tt.fd(40, 0.0), tt.fd(41, 0.0)));
}
*i += 1;
} else {
if k2 == "SEQEND" {
*i += 1;
}
break;
}
}
let c = common(t);
let mut extra: Vec<EntityKind> = Vec::new();
let k = if flags & (16 | 64) != 0 {
// Polyface (64) and polygon (16) meshes become 3D faces; the first takes the
// polyline's place.
let verts: Vec<Vec3> = pts.iter().map(|p| p.0).collect();
let mut fs = if flags & 64 != 0 { polyface_faces(&verts, &faces) } else { mesh_faces(&verts, t, flags) }.into_iter().map(EntityKind::Face3d);
let first = fs.next()?;
extra.extend(fs);
first
} else if flags & 8 != 0 {
EntityKind::Polyline3d(Polyline3d { points: pts.iter().map(|p| p.0).collect(), closed: flags & 1 != 0 })
} else {
// A 2D polyline's vertices are in its OCS: mirrored like LWPOLYLINE.
let m = if c.extrusion.z < 0.0 { -1.0 } else { 1.0 };
EntityKind::LwPolyline(LwPolyline {
vertices: pts.iter().map(|p| PolyVertex { p: Vec2::new(m * p.0.x, p.0.y), bulge: m * p.1, start_width: p.2, end_width: p.3 }).collect(),
closed: flags & 1 != 0,
const_width: t.fd(40, 0.0).min(t.fd(41, 0.0)).max(0.0),
elevation: t.p(10).z,
plinegen: flags & 128 != 0,
})
};
Some((c, k, extra))
}
/// The ATTRIB records after an INSERT, advancing `i` past them and their SEQEND.
fn insert_attribs(recs: &[(String, Vec<Tag>)], i: &mut usize, ins_layer: &str) -> Vec<Attrib> {
let mut attribs = Vec::new();
while let Some((k2, t2)) = recs.get(*i) {
if k2 == "ATTRIB" {
let tt = T(t2);
// A multiline attribute has its text as embedded MTEXT after its own groups.
let (own, embedded) = split_embedded(t2);
let multiline = multiline_attrib(embedded, &common(&tt));
let own = T(own);
let mut text = text_from(&own);
if let Some(m) = &multiline {
text.value.clone_from(&m.read_as);
}
attribs.push(Attrib {
tag: own.s(2).unwrap_or_default(),
text,
invisible: own.i(70).unwrap_or(0) & 1 != 0,
constant: false,
prompt: String::new(),
props: attrib_props(&tt, ins_layer),
multiline,
modes: attrib_modes(own.0),
});
*i += 1;
} else {
if k2 == "SEQEND" {
*i += 1;
}
break;
}
}
attribs
}
fn parse_entities(
recs: &[(String, Vec<Tag>)],
d: &mut Drawing,
@@ -1221,17 +1134,92 @@ fn parse_entities(
i += 1;
match kind.as_str() {
"POLYLINE" => {
let Some((c, k, more)) = polyline_entity(recs, &mut i, &t) else {
*skipped = skipped.saturating_add(1);
continue;
let flags = t.i(70).unwrap_or(0);
let mut pts: Vec<(Vec3, f64, f64, f64)> = Vec::new();
// Polyface face records (VERTEX 70 bit 128 without 64): vertex indices 71–74.
let mut faces: Vec<[i64; 4]> = Vec::new();
while let Some((k2, t2)) = recs.get(i) {
if k2 == "VERTEX" {
let tt = T(t2);
let vflags = tt.i(70).unwrap_or(0);
if vflags & 128 != 0 && vflags & 64 == 0 {
faces.push([71, 72, 73, 74].map(|c| tt.i(c).unwrap_or(0)));
} else if vflags & 16 == 0 {
pts.push((tt.p(10), tt.fd(42, 0.0), tt.fd(40, 0.0), tt.fd(41, 0.0)));
}
i += 1;
} else {
if k2 == "SEQEND" {
i += 1;
}
break;
}
}
let c = common(&t);
let k = if flags & (16 | 64) != 0 {
// Polyface (64) and polygon (16) meshes become 3D faces; the first takes the
// polyline's place.
let verts: Vec<Vec3> = pts.iter().map(|p| p.0).collect();
let mut fs = if flags & 64 != 0 { polyface_faces(&verts, &faces) } else { mesh_faces(&verts, &t, flags) }
.into_iter()
.map(EntityKind::Face3d);
let Some(first) = fs.next() else {
*skipped = skipped.saturating_add(1);
continue;
};
extra.extend(fs);
first
} else if flags & 8 != 0 {
EntityKind::Polyline3d(Polyline3d { points: pts.iter().map(|p| p.0).collect(), closed: flags & 1 != 0 })
} else {
// A 2D polyline's vertices are in its OCS: mirrored like LWPOLYLINE.
let m = if c.extrusion.z < 0.0 { -1.0 } else { 1.0 };
EntityKind::LwPolyline(LwPolyline {
vertices: pts
.iter()
.map(|p| PolyVertex { p: Vec2::new(m * p.0.x, p.0.y), bulge: m * p.1, start_width: p.2, end_width: p.3 })
.collect(),
closed: flags & 1 != 0,
const_width: t.fd(40, 0.0).min(t.fd(41, 0.0)).max(0.0),
elevation: t.p(10).z,
plinegen: flags & 128 != 0,
})
};
extra = more;
kindent = Some((c, k));
}
"INSERT" => {
if t.i(66) == Some(1) {
let mut attribs = Vec::new();
let ins_layer = kindent.as_ref().map(|(c, _)| c.layer.clone()).unwrap_or_default();
let attribs = insert_attribs(recs, &mut i, &ins_layer);
while let Some((k2, t2)) = recs.get(i) {
if k2 == "ATTRIB" {
let tt = T(t2);
// A multiline attribute has its text as embedded MTEXT after its own groups.
let (own, embedded) = split_embedded(t2);
let multiline = multiline_attrib(embedded, &common(&tt));
let own = T(own);
let mut text = text_from(&own);
if let Some(m) = &multiline {
text.value.clone_from(&m.read_as);
}
attribs.push(Attrib {
tag: own.s(2).unwrap_or_default(),
text,
invisible: own.i(70).unwrap_or(0) & 1 != 0,
constant: false,
prompt: String::new(),
props: attrib_props(&tt, &ins_layer),
multiline,
modes: attrib_modes(own.0),
});
i += 1;
} else {
if k2 == "SEQEND" {
i += 1;
}
break;
}
}
if let Some((_, EntityKind::Insert(ins))) = kindent.as_mut() {
ins.attribs = attribs;
}
@@ -1700,13 +1688,6 @@ pub fn read(bytes: &[u8]) -> Result<Drawing> {
read_budgeted(bytes, 4_000_000)
}
/// A block definition read from BLOCKS: name, base point, description, entities (owner, paper
/// flag, entity) and block flags.
type BlockDef = (String, Vec3, String, Vec<(Option<String>, bool, Entity)>, i64);
/// A layout read from OBJECTS: name, tab order, paper block handle and page setup.
type LayoutDef = (String, u32, String, PageSetup);
/// Import DXF sections while sharing one materialized-table-cell budget across blocks and entity sections.
fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
let tags = cadcraft_dxf::parse(bytes).map_err(|e| IoError::Format(e.to_string()))?;
@@ -1719,8 +1700,8 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
// A file without $INSUNITS declares no units (#1209); only new drawings take the template's.
d.header.set_i64("INSUNITS", 0);
let mut rx = Rx { pstyles: plot_style_names(&secs), ..Rx::default() };
let mut blocks: Vec<BlockDef> = Vec::new();
let mut layout_objs: Vec<LayoutDef> = Vec::new();
let mut blocks: Vec<(String, Vec3, String, Vec<(Option<String>, bool, Entity)>, i64)> = Vec::new();
let mut layout_objs: Vec<(String, u32, String, PageSetup)> = Vec::new();
let mut entities = Vec::new();
let mut objs = Objects::default();
// Every handle another program's file already uses is taken before anything gets a new one:
@@ -1740,109 +1721,73 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {
match s.name.as_str() {
"HEADER" => header(&s.tags, &mut d),
"TABLES" => tables(&s.tags, &mut d, &mut rx),
"BLOCKS" => blocks.extend(read_blocks(&s.tags, &mut d, &mut rx, &mut table_cells_left, &mut skipped)),
"BLOCKS" => {
let recs = records(&s.tags);
for (k, tg) in &recs {
let t = T(tg);
if k == "BLOCK" && t.i(70).unwrap_or(0) & 4 != 0 {
rx.xref_paths.insert(t.s(2).unwrap_or_default().to_ascii_uppercase(), t.s(1).unwrap_or_default());
}
}
let mut n = 0;
let mut i = 0;
while let Some((k, tg)) = recs.get(i) {
if k == "BLOCK" {
let t = T(tg);
let name = t.s(2).unwrap_or_default();
let base = t.p(10);
let description = t.s(4).unwrap_or_default();
// Block flags: 4 xref, 8 overlay.
let xref = t.i(70).unwrap_or(0) & 12;
let mut j = i + 1;
while recs.get(j).is_some_and(|(k, _)| k != "ENDBLK") {
j += 1;
}
let body = recs.get(i + 1..j).unwrap_or(&[]);
let ents = parse_entities(body, &mut d, &mut rx, &mut n, &mut table_cells_left);
blocks.push((name, base, description, ents, xref));
i = j + 1;
} else {
// A record outside any BLOCK ... ENDBLK pair.
if k != "ENDBLK" {
n = n.saturating_add(1);
}
i += 1;
}
}
count_skipped(&mut skipped, "BLOCKS", n);
}
// A damaged file may split its entities over several ENTITIES sections (#865).
"ENTITIES" => {
let mut n = 0;
entities.extend(parse_entities(&records(&s.tags), &mut d, &mut rx, &mut n, &mut table_cells_left));
count_skipped(&mut skipped, "ENTITIES", n);
}
"OBJECTS" => read_objects(&s.tags, &mut objs, &mut layout_objs),
"OBJECTS" => {
for (k, tg) in records(&s.tags) {
objs.add(&k, &tg);
if k == "LAYOUT" {
// Fields after the AcDbLayout marker (AcDbPlotSettings also has a code 1).
let start = tg.iter().position(|x| x.code == 100 && x.str() == "AcDbLayout").unwrap_or(0);
let tail = tg.get(start..).unwrap_or(&[]);
let t = T(tail);
// LAYOUT has two 330s: owner dict and the block record; take the last.
let br = tg.iter().rfind(|x| x.code == 330).map(Tag::str).unwrap_or_default();
let plot = T(tg.get(..start).unwrap_or(&[]));
layout_objs.push((t.s(1).unwrap_or_default(), t.i(71).unwrap_or(0) as u32, br.to_ascii_uppercase(), page_setup(&plot)));
}
}
}
_ => {}
}
}
// A file CADCraft wrote names its own next handle (#495): its table records and objects
// are numbered above it and rebuilt on save, so following $HANDSEED would drift every save.
let own_seed = objs.xrecord_text(crate::dxf_ext::HANDSEED_KEY).and_then(|t| Handle::parse_hex(t.trim()));
let (paper_block_for, first_layout) = setup_layouts(&mut d, &mut layout_objs, &rx);
place_blocks(&mut d, blocks, &mut rx, &paper_block_for, &first_layout);
place_entities(&mut d, entities, &rx, &paper_block_for, &first_layout);
apply_sortents(&mut d, &objs.sortents, &rx.brs, &paper_block_for, &first_layout);
objs.apply(&mut d, &rx);
crate::dxf_image::resolve(&secs, &mut d);
crate::dxf_mline::resolve(&secs, &mut d);
// Keep handles unique against the header's seed.
let seed = d.header.str("HANDSEED", "");
if let Some(h) = own_seed.or_else(|| Handle::parse_hex(&seed)) {
d.bump_handseed(Handle(h.0.saturating_sub(1)));
}
if d.layer("0").is_none() {
d.layers.insert(0, Layer::default());
}
crate::dxf_raw::read(&secs, &mut d);
d.skipped_records = skipped;
Ok(d)
}
/// Read the BLOCKS section: the xref paths of external blocks, then each block's entities.
fn read_blocks(
tags: &[Tag],
d: &mut Drawing,
rx: &mut Rx,
table_cells_left: &mut usize,
skipped: &mut std::collections::BTreeMap<String, usize>,
) -> Vec<BlockDef> {
let recs = records(tags);
for (k, tg) in &recs {
let t = T(tg);
if k == "BLOCK" && t.i(70).unwrap_or(0) & 4 != 0 {
rx.xref_paths.insert(t.s(2).unwrap_or_default().to_ascii_uppercase(), t.s(1).unwrap_or_default());
}
}
let mut blocks = Vec::new();
let mut n = 0;
let mut i = 0;
while let Some((k, tg)) = recs.get(i) {
if k == "BLOCK" {
let t = T(tg);
let name = t.s(2).unwrap_or_default();
let base = t.p(10);
let description = t.s(4).unwrap_or_default();
// Block flags: 4 xref, 8 overlay.
let xref = t.i(70).unwrap_or(0) & 12;
let mut j = i + 1;
while recs.get(j).is_some_and(|(k, _)| k != "ENDBLK") {
j += 1;
}
let body = recs.get(i + 1..j).unwrap_or(&[]);
let ents = parse_entities(body, d, rx, &mut n, table_cells_left);
blocks.push((name, base, description, ents, xref));
i = j + 1;
} else {
// A record outside any BLOCK ... ENDBLK pair.
if k != "ENDBLK" {
n = n.saturating_add(1);
}
i += 1;
}
}
count_skipped(skipped, "BLOCKS", n);
blocks
}
/// Read the OBJECTS section into `objs`, and each LAYOUT into `layout_objs`.
fn read_objects(tags: &[Tag], objs: &mut Objects, layout_objs: &mut Vec<LayoutDef>) {
for (k, tg) in records(tags) {
objs.add(&k, &tg);
if k == "LAYOUT" {
// Fields after the AcDbLayout marker (AcDbPlotSettings also has a code 1).
let start = tg.iter().position(|x| x.code == 100 && x.str() == "AcDbLayout").unwrap_or(0);
let tail = tg.get(start..).unwrap_or(&[]);
let t = T(tail);
// LAYOUT has two 330s: owner dict and the block record; take the last.
let br = tg.iter().rfind(|x| x.code == 330).map(Tag::str).unwrap_or_default();
let plot = T(tg.get(..start).unwrap_or(&[]));
layout_objs.push((t.s(1).unwrap_or_default(), t.i(71).unwrap_or(0) as u32, br.to_ascii_uppercase(), page_setup(&plot)));
}
}
}
/// Add the drawing's layouts, and return the layout each paper-space block belongs to, and the
/// layout that takes paper-space entities without an owner.
fn setup_layouts(d: &mut Drawing, layout_objs: &mut [LayoutDef], rx: &Rx) -> (HashMap<String, String>, String) {
// Layouts.
let mut paper_block_for: HashMap<String, String> = HashMap::new();
layout_objs.sort_by_key(|l| l.1);
for (name, order, br, page) in layout_objs.iter() {
for (name, order, br, page) in &layout_objs {
if name.eq_ignore_ascii_case("Model") {
continue;
}
@@ -1861,12 +1806,7 @@ fn setup_layouts(d: &mut Drawing, layout_objs: &mut [LayoutDef], rx: &Rx) -> (Ha
let first_tab = d.layouts.iter().min_by_key(|l| l.tab_order).map(|l| l.name.clone());
d.active_layout = active.clone().filter(|a| Some(a) != first_tab.as_ref());
let first_layout = active.or_else(|| d.layouts.first().map(|l| l.name.clone())).unwrap_or_else(|| "Layout1".into());
(paper_block_for, first_layout)
}
/// Place the BLOCKS entities: model and paper space blocks go to their space, the rest become
/// block definitions.
fn place_blocks(d: &mut Drawing, blocks: Vec<BlockDef>, rx: &mut Rx, paper_block_for: &HashMap<String, String>, first_layout: &str) {
// Blocks: model/paper space blocks hold entities in newer files.
for (name, base, description, ents, xref) in blocks {
let up = name.to_ascii_uppercase();
if up == "*MODEL_SPACE" {
@@ -1877,7 +1817,7 @@ fn place_blocks(d: &mut Drawing, blocks: Vec<BlockDef>, rx: &mut Rx, paper_block
continue;
}
if up.starts_with("*PAPER_SPACE") {
let lname = paper_block_for.get(&up).cloned().unwrap_or_else(|| first_layout.to_string());
let lname = paper_block_for.get(&up).cloned().unwrap_or_else(|| first_layout.clone());
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
for (_, _, e) in ents {
l.entities.push(e);
@@ -1917,22 +1857,13 @@ fn place_blocks(d: &mut Drawing, blocks: Vec<BlockDef>, rx: &mut Rx, paper_block
}
d.blocks.insert(name, std::sync::Arc::new(b));
}
}
/// Place the ENTITIES section: model space, or paper space when flagged (67=1).
fn place_entities(
d: &mut Drawing,
entities: Vec<(Option<String>, bool, Entity)>,
rx: &Rx,
paper_block_for: &HashMap<String, String>,
first_layout: &str,
) {
// ENTITIES section: model space, or paper space when flagged (67=1).
for (owner, paper, e) in entities {
d.ensure_layer(&e.common.layer);
let owner_name = owner.and_then(|o| rx.brs.get(&o.trim().to_ascii_uppercase()).cloned()).map(|n| n.to_ascii_uppercase());
let to_paper = paper || owner_name.as_deref().is_some_and(|n| n.starts_with("*PAPER_SPACE"));
if to_paper {
let lname = owner_name.and_then(|n| paper_block_for.get(&n).cloned()).unwrap_or_else(|| first_layout.to_string());
let lname = owner_name.and_then(|n| paper_block_for.get(&n).cloned()).unwrap_or_else(|| first_layout.clone());
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == lname) {
l.entities.push(e);
}
@@ -1940,6 +1871,21 @@ fn place_entities(
d.model.push(e);
}
}
apply_sortents(&mut d, &objs.sortents, &rx.brs, &paper_block_for, &first_layout);
objs.apply(&mut d, &rx);
crate::dxf_image::resolve(&secs, &mut d);
crate::dxf_mline::resolve(&secs, &mut d);
// Keep handles unique against the header's seed.
let seed = d.header.str("HANDSEED", "");
if let Some(h) = own_seed.or_else(|| Handle::parse_hex(&seed)) {
d.bump_handseed(Handle(h.0.saturating_sub(1)));
}
if d.layer("0").is_none() {
d.layers.insert(0, Layer::default());
}
crate::dxf_raw::read(&secs, &mut d);
d.skipped_records = skipped;
Ok(d)
}
fn count_skipped(skipped: &mut std::collections::BTreeMap<String, usize>, section: &str, n: usize) {
@@ -2108,7 +2054,13 @@ impl Objects {
Some(text)
}
fn apply_table_styles(&self, d: &mut Drawing) {
fn apply(self, d: &mut Drawing, rx: &Rx) {
if let Some(v) = self.wipeout_frame {
d.header.set_i64("WIPEOUTFRAME", v);
}
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 };
@@ -2118,9 +2070,6 @@ impl Objects {
None => d.table_styles.push(st),
}
}
}
fn apply_table_style_fixes(&self, d: &mut Drawing, rx: &Rx) {
for (h, sh) in &rx.table_style_fix {
let name = self.names.get(sh).cloned();
// A table from another writer has no scale of its own: its text height is its
@@ -2141,61 +2090,6 @@ impl Objects {
}
});
}
}
fn apply_mleader_styles(&self, d: &mut Drawing, rx: &Rx) {
// Multileader styles (named by their ACAD_MLEADERSTYLE dictionary entries).
for (h, tags) in &self.mleader_styles {
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
let t = T(tags);
let def = MLeaderStyle::default();
let num = |code: i32, d: f64, min: f64| t.f(code).filter(|v| v.is_finite() && *v >= min).unwrap_or(d);
let text_style = t.s(342).and_then(|sh| rx.styles.get(&sh.trim().to_ascii_uppercase()).cloned()).filter(|n| !n.is_empty());
let st = MLeaderStyle {
name: name.clone(),
arrow_size: num(44, def.arrow_size, 0.0),
text_height: t.f(45).filter(|v| v.is_finite() && *v > 0.0).unwrap_or(def.text_height),
landing_gap: num(42, def.landing_gap, 0.0),
dogleg: num(43, def.dogleg, 0.0),
text_style: text_style.unwrap_or(def.text_style),
annotative: t.i(296).unwrap_or(0) != 0,
};
match d.mleader_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(name)) {
Some(x) => *x = st,
None => d.mleader_styles.push(st),
}
}
}
fn apply_groups(&self, d: &mut Drawing) {
// Groups (named by their ACAD_GROUP dictionary entries); members that exist.
for (h, tags) in &self.groups {
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
let t = T(tags);
let members: Vec<Handle> = tags
.iter()
.filter(|x| x.code == 340)
.filter_map(|x| Handle::parse_hex(&x.str()))
.filter(|m| d.entity(*m).is_some())
.take(MAX_OBJECTS)
.collect();
let g = Group { name: name.clone(), description: t.s(300).unwrap_or_default(), selectable: t.i(71).unwrap_or(1) != 0, members };
match d.groups.iter_mut().find(|x| x.name.eq_ignore_ascii_case(name)) {
Some(x) => *x = g,
None => d.groups.push(g),
}
}
}
fn apply(self, d: &mut Drawing, rx: &Rx) {
if let Some(v) = self.wipeout_frame {
d.header.set_i64("WIPEOUTFRAME", v);
}
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);
}
self.apply_table_styles(d);
self.apply_table_style_fixes(d, rx);
// Parametric constraints and parameters.
if let Some((c, p)) = self.xrecord_text(crate::dxf_ext::CONSTRAINTS_KEY).and_then(|t| crate::dxf_ext::parse_constraints(&t)) {
d.constraints = c;
@@ -2219,14 +2113,50 @@ impl Objects {
if let Some(tables) = self.xrecord_text(crate::dxf_ext::PLOT_STYLES_KEY).and_then(|t| crate::dxf_ext::parse_plot_styles(&t)) {
d.plot_style_tables = tables;
}
self.apply_mleader_styles(d, rx);
// Multileader styles (named by their ACAD_MLEADERSTYLE dictionary entries).
for (h, tags) in &self.mleader_styles {
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
let t = T(tags);
let def = MLeaderStyle::default();
let num = |code: i32, d: f64, min: f64| t.f(code).filter(|v| v.is_finite() && *v >= min).unwrap_or(d);
let text_style = t.s(342).and_then(|sh| rx.styles.get(&sh.trim().to_ascii_uppercase()).cloned()).filter(|n| !n.is_empty());
let st = MLeaderStyle {
name: name.clone(),
arrow_size: num(44, def.arrow_size, 0.0),
text_height: t.f(45).filter(|v| v.is_finite() && *v > 0.0).unwrap_or(def.text_height),
landing_gap: num(42, def.landing_gap, 0.0),
dogleg: num(43, def.dogleg, 0.0),
text_style: text_style.unwrap_or(def.text_style),
annotative: t.i(296).unwrap_or(0) != 0,
};
match d.mleader_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(name)) {
Some(x) => *x = st,
None => d.mleader_styles.push(st),
}
}
crate::dxf_mleader::apply(d, &rx.mleader_fix, &self.names);
for (name, view) in &self.layout_views {
if let Some(l) = d.layouts.iter_mut().find(|l| l.name == *name) {
l.view = Some(*view);
}
}
self.apply_groups(d);
// Groups (named by their ACAD_GROUP dictionary entries); members that exist.
for (h, tags) in &self.groups {
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
let t = T(tags);
let members: Vec<Handle> = tags
.iter()
.filter(|x| x.code == 340)
.filter_map(|x| Handle::parse_hex(&x.str()))
.filter(|m| d.entity(*m).is_some())
.take(MAX_OBJECTS)
.collect();
let g = Group { name: name.clone(), description: t.s(300).unwrap_or_default(), selectable: t.i(71).unwrap_or(1) != 0, members };
match d.groups.iter_mut().find(|x| x.name.eq_ignore_ascii_case(name)) {
Some(x) => *x = g,
None => d.groups.push(g),
}
}
// Standard associativity, for dimensions without CADCraft's exact links.
for tags in &self.dimassocs {
dimassoc(d, tags);
+11 -11
View File
@@ -2373,7 +2373,7 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
// ---------------- BLOCKS ----------------
w.s(0, "SECTION");
w.s(2, "BLOCKS");
let block = |w: &mut W, (name, desc): (&str, &str), brh: &str, base: Vec3, flags: i64, ents: &mut dyn Iterator<Item = &Entity>, paper: bool| {
let block = |w: &mut W, name: &str, desc: &str, brh: &str, base: Vec3, flags: i64, ents: &mut dyn Iterator<Item = &Entity>, paper: bool| {
let bh = w.h();
w.s(0, "BLOCK");
w.s(5, bh);
@@ -2406,16 +2406,16 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
w.s(8, "0");
w.s(100, "AcDbBlockEnd");
};
block(&mut w, ("*Model_Space", ""), &ms_br, Vec3::ZERO, 0, &mut std::iter::empty(), false);
block(&mut w, "*Model_Space", "", &ms_br, Vec3::ZERO, 0, &mut std::iter::empty(), false);
for (i, (n, h, l)) in ps_brs.iter().enumerate() {
if i == 0 {
block(&mut w, (n, ""), h, Vec3::ZERO, 0, &mut std::iter::empty(), true);
block(&mut w, n, "", h, Vec3::ZERO, 0, &mut std::iter::empty(), true);
} else {
block(&mut w, (n, ""), h, Vec3::ZERO, 0, &mut sheet_first(l).into_iter(), true);
block(&mut w, n, "", h, Vec3::ZERO, 0, &mut sheet_first(l).into_iter(), true);
}
}
for (n, h, b) in &user_blocks {
block(&mut w, (n, &b.description), h, b.base, if b.anonymous { 1 } else { 0 }, &mut b.entities.iter().map(|e| e.as_ref()), false);
block(&mut w, n, &b.description, h, b.base, if b.anonymous { 1 } else { 0 }, &mut b.entities.iter().map(|e| e.as_ref()), false);
}
// Generated geometry entities (dimension, arrowhead and table blocks) need handles.
for (_, _, ents) in dim_defs.iter_mut().chain(arrow_defs.iter_mut()).chain(table_defs.iter_mut()) {
@@ -2425,13 +2425,13 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
}
}
for (n, h, ents) in &dim_defs {
block(&mut w, (n, ""), h, Vec3::ZERO, 1, &mut ents.iter(), false);
block(&mut w, n, "", h, Vec3::ZERO, 1, &mut ents.iter(), false);
}
for (n, h, ents) in &arrow_defs {
block(&mut w, (n, ""), h, Vec3::ZERO, 0, &mut ents.iter(), false);
block(&mut w, n, "", h, Vec3::ZERO, 0, &mut ents.iter(), false);
}
for (n, h, ents) in &table_defs {
block(&mut w, (n, ""), h, Vec3::ZERO, 1, &mut ents.iter(), false);
block(&mut w, n, "", h, Vec3::ZERO, 1, &mut ents.iter(), false);
}
w.s(0, "ENDSEC");
@@ -2612,7 +2612,7 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
w.s(3, &l.name);
w.s(350, layout_handles.get(i).cloned().unwrap_or_default());
}
let layout_obj = |w: &mut W, h: &str, (name, tab): (&str, i64), brh: &str, flags: i64, page: &PageSetup, view: Option<(Vec2, f64)>| {
let layout_obj = |w: &mut W, h: &str, name: &str, tab: i64, brh: &str, flags: i64, page: &PageSetup, view: Option<(Vec2, f64)>| {
// Plot scale: a standard scale when fitting or 1:1, otherwise custom (142 paper units
// per 143 drawing units).
let scale = if page.scale.is_finite() && page.scale > 0.0 { page.scale } else { 1.0 };
@@ -2697,10 +2697,10 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
// plot type 5, "Layout information" (DXF Reference, PLOTSETTINGS group 74), which means
// nothing for model space: another program's Plot dialog then shows no plot area or scale.
let model_page = PageSetup { plot_area: "extents".into(), scale_to_fit: true, ..PageSetup::default() };
layout_obj(&mut w, &model_layout, ("Model", 0), &ms_br, 1, &model_page, None);
layout_obj(&mut w, &model_layout, "Model", 0, &ms_br, 1, &model_page, None);
for (i, (_, brh, l)) in ps_brs.iter().enumerate() {
let h = layout_handles.get(i).cloned().unwrap_or_default();
layout_obj(&mut w, &h, (&l.name, i64::from(l.tab_order.max(1))), brh, 1, &l.page, l.view);
layout_obj(&mut w, &h, &l.name, i64::from(l.tab_order.max(1)), brh, 1, &l.page, l.view);
}
// Raster image definitions.
w.t.extend(crate::dxf_image::objects(&cx.images, &root_dict));
+84 -110
View File
@@ -17,7 +17,7 @@ use std::fmt::Write as _;
use cadcraft_color::Rgb;
use cadcraft_doc::{Drawing, Handle, PageSetup, Space};
use cadcraft_geom::{Bounds2, Vec2};
use cadcraft_render::{DPrim, Kind, Sheet, clip, paper};
use cadcraft_render::{Kind, Sheet, clip, paper};
use serde_json::Value;
use craft_pdf::write::backend as kr;
@@ -808,7 +808,7 @@ fn krilla_failure(e: KrillaError) -> Failure {
/// as text instead: the glyph at font size 1 under its glyph matrix (the matrix axes are one
/// em).
fn marks(s: &mut Surface<'_>, p: &SheetPage, groups: &Groups, fonts: &mut fonts::Fonts) -> std::result::Result<(), Failure> {
let list = &p.list;
let (list, map) = (&p.list, &p.map);
let texts = fonts::texts(list);
let white = Rgb(255, 255, 255);
let mut batch: Option<(StrokeStyle, PathBuilder)> = None;
@@ -829,126 +829,100 @@ fn marks(s: &mut Surface<'_>, p: &SheetPage, groups: &Groups, fonts: &mut fonts:
let t = if prim.kind == Kind::Mask { 0 } else { prim.transparency.min(cadcraft_render::MAX_TRANSPARENCY) };
let w = f64::from(prim.lw) * PT_PER_MM;
let w = if w.is_finite() && w > 0.0 { w.min(100.0) } else { 0.0 };
let m = Mark { rgb, t, w };
match prim.kind {
Kind::Polyline | Kind::Infinite { .. } => polyline(s, &mut batch, p, prim, raw, m),
Kind::Polyline | Kind::Infinite { .. } => {
let pts: Vec<Vec2> = match prim.kind {
Kind::Infinite { ray } => match (raw.first(), raw.get(1)) {
(Some(base), Some(dir)) => {
let a = map.pt(*base);
let dir_pt = map.pt(*base + *dir) - a;
match clip::clip_infinite(a, dir_pt, ray, &p.clip_pt) {
Some((x, y)) => vec![x, y],
None => continue,
}
}
_ => continue,
},
_ => raw.iter().map(|q| map.pt(*q)).collect(),
};
if pts.len() < 2 {
continue;
}
// With a vertex out of range, keep the pieces inside the plot area, widened so
// a wide stroke just outside it still shows its edge.
let runs = if pts.iter().all(|q| coord(*q).is_some()) { vec![pts] } else { clip::clip_polyline(&pts, &p.clip_pt.expand(w + 1.0)) };
let style = StrokeStyle { rgb, width: w as f32, transparency: t };
if t > 0 || batch.as_ref().is_some_and(|(b, _)| *b != style) {
flush(s, &mut batch);
}
for run in runs {
let Some(pts) = run.iter().map(|q| coord(*q)).collect::<Option<Vec<_>>>() else { continue };
// The writer leaves out a path without extent, so a zero-length line is
// painted as the round-cap dot its stroke would make.
if let Some(&(x, y)) = pts.first()
&& pts.iter().all(|q| *q == (x, y))
{
flush(s, &mut batch);
let d = if w > 0.0 { w as f32 } else { HAIRLINE_DOT_PT };
if let Some(path) = circle_path(x, y, d / 2.0) {
fill_path(s, &path, rgb, t);
}
continue;
}
let (_, pb) = batch.get_or_insert_with(|| (style, PathBuilder::new()));
for (j, (x, y)) in pts.iter().enumerate() {
if j == 0 {
pb.move_to(*x, *y);
} else {
pb.line_to(*x, *y);
}
}
}
}
Kind::Tris | Kind::Mask => {
flush(s, &mut batch);
// A wipeout's mask is paper white.
let m = if prim.kind == Kind::Mask { Mark { rgb: white, ..m } } else { m };
let text_font =
u32::try_from(i).ok().and_then(|i| texts.get(&i).copied()).and_then(|text| fonts.font(&text.glyph.face).map(|font| (text, font)));
match text_font {
Some((text, font)) => glyph(s, font, text, &p.map, m.rgb, m.t),
None => fill_tris(s, p, raw, m.rgb, m.t),
let rgb = if prim.kind == Kind::Mask { white } else { rgb };
if let Some(text) = u32::try_from(i).ok().and_then(|i| texts.get(&i))
&& let Some(font) = fonts.font(&text.glyph.face)
{
glyph(s, font, text, map, rgb, t);
continue;
}
let mapped: Vec<Vec2> = raw.iter().map(|q| map.pt(*q)).collect();
// With a vertex out of range, keep the parts inside the plot area.
let tris = if mapped.iter().all(|q| coord(*q).is_some()) { mapped } else { clip::clip_triangles(&mapped, &p.clip_pt) };
let mut pb = PathBuilder::new();
for tri in tris.as_chunks::<3>().0 {
let Some(v) = tri.iter().map(|q| coord(*q)).collect::<Option<Vec<_>>>() else { continue };
for (j, (x, y)) in v.iter().enumerate() {
if j == 0 {
pb.move_to(*x, *y);
} else {
pb.line_to(*x, *y);
}
}
pb.close();
}
if let Some(path) = pb.finish() {
fill_path(s, &path, rgb, t);
}
}
Kind::Point => {
let Some(q) = raw.first() else { continue };
flush(s, &mut batch);
let half = (w.max(0.5)) / 2.0;
let c = map.pt(*q);
if let Some(path) = rect_path(c - Vec2::new(half, half), c + Vec2::new(half, half)) {
fill_path(s, &path, rgb, t);
}
}
Kind::Point => point(s, &mut batch, &p.map, raw, m),
}
}
flush(s, &mut batch);
layer_changed(s, layer, None)
}
/// How a primitive is painted: colour, transparency (percent) and lineweight (points).
#[derive(Clone, Copy)]
struct Mark {
rgb: Rgb,
t: u8,
w: f64,
}
/// Stroke a polyline or infinite line, clipped to the plot area, into the open batch.
fn polyline(s: &mut Surface<'_>, batch: &mut Option<(StrokeStyle, PathBuilder)>, p: &SheetPage, prim: &DPrim, raw: &[Vec2], m: Mark) {
let Some(pts) = polyline_points(prim, raw, &p.map, &p.clip_pt) else { return };
if pts.len() < 2 {
return;
}
// With a vertex out of range, keep the pieces inside the plot area, widened so
// a wide stroke just outside it still shows its edge.
let runs = if pts.iter().all(|q| coord(*q).is_some()) { vec![pts] } else { clip::clip_polyline(&pts, &p.clip_pt.expand(m.w + 1.0)) };
let style = StrokeStyle { rgb: m.rgb, width: m.w as f32, transparency: m.t };
if m.t > 0 || batch.as_ref().is_some_and(|(b, _)| *b != style) {
flush(s, batch);
}
for run in runs {
stroke_run(s, batch, &run, style, m);
}
}
/// The page points of a polyline, or of an infinite line clipped to the plot area; `None` when
/// there is nothing to draw.
fn polyline_points(prim: &DPrim, raw: &[Vec2], map: &Map, clip_pt: &Bounds2) -> Option<Vec<Vec2>> {
match prim.kind {
Kind::Infinite { ray } => {
let (Some(base), Some(dir)) = (raw.first(), raw.get(1)) else { return None };
let a = map.pt(*base);
let dir_pt = map.pt(*base + *dir) - a;
let (x, y) = clip::clip_infinite(a, dir_pt, ray, clip_pt)?;
Some(vec![x, y])
}
_ => Some(raw.iter().map(|q| map.pt(*q)).collect()),
}
}
/// Add one run of a polyline to the batch, or paint it as a dot when it has no extent.
fn stroke_run(s: &mut Surface<'_>, batch: &mut Option<(StrokeStyle, PathBuilder)>, run: &[Vec2], style: StrokeStyle, m: Mark) {
let Some(pts) = run.iter().map(|q| coord(*q)).collect::<Option<Vec<_>>>() else { return };
// The writer leaves out a path without extent, so a zero-length line is
// painted as the round-cap dot its stroke would make.
if let Some(&(x, y)) = pts.first()
&& pts.iter().all(|q| *q == (x, y))
{
flush(s, batch);
let d = if m.w > 0.0 { m.w as f32 } else { HAIRLINE_DOT_PT };
if let Some(path) = circle_path(x, y, d / 2.0) {
fill_path(s, &path, m.rgb, m.t);
}
return;
}
let (_, pb) = batch.get_or_insert_with(|| (style, PathBuilder::new()));
for (j, (x, y)) in pts.iter().enumerate() {
if j == 0 {
pb.move_to(*x, *y);
} else {
pb.line_to(*x, *y);
}
}
}
/// Fill a set of triangles, clipped to the plot area.
fn fill_tris(s: &mut Surface<'_>, p: &SheetPage, raw: &[Vec2], rgb: Rgb, t: u8) {
let mapped: Vec<Vec2> = raw.iter().map(|q| p.map.pt(*q)).collect();
// With a vertex out of range, keep the parts inside the plot area.
let tris = if mapped.iter().all(|q| coord(*q).is_some()) { mapped } else { clip::clip_triangles(&mapped, &p.clip_pt) };
let mut pb = PathBuilder::new();
for tri in tris.as_chunks::<3>().0 {
let Some(v) = tri.iter().map(|q| coord(*q)).collect::<Option<Vec<_>>>() else { continue };
for (j, (x, y)) in v.iter().enumerate() {
if j == 0 {
pb.move_to(*x, *y);
} else {
pb.line_to(*x, *y);
}
}
pb.close();
}
if let Some(path) = pb.finish() {
fill_path(s, &path, rgb, t);
}
}
/// Paint a point marker: a small square at the point.
fn point(s: &mut Surface<'_>, batch: &mut Option<(StrokeStyle, PathBuilder)>, map: &Map, raw: &[Vec2], m: Mark) {
let Some(q) = raw.first() else { return };
flush(s, batch);
let half = (m.w.max(0.5)) / 2.0;
let c = map.pt(*q);
if let Some(path) = rect_path(c - Vec2::new(half, half), c + Vec2::new(half, half)) {
fill_path(s, &path, m.rgb, m.t);
}
}
/// Write one glyph as text in its embedded font: font size 1, placed by the glyph's matrix (one
/// em along each axis). A glyph whose matrix is out of range or collapses is left out.
fn glyph(s: &mut Surface<'_>, font: kr::text::Font, t: &cadcraft_render::PlotText, map: &Map, rgb: Rgb, transparency: u8) {
+14 -14
View File
@@ -165,7 +165,7 @@ fn style(d: &mut Drawing, name: &str, font: &str) {
d.text_styles.push(TextStyle { name: name.into(), font: font.into(), ..TextStyle::default() });
}
fn text(d: &mut Drawing, space: &Space, style: &str, at: (f64, f64), value: &str, (rotation, width_factor, oblique): (f64, f64, f64)) {
fn text(d: &mut Drawing, space: &Space, style: &str, at: (f64, f64), value: &str, rotation: f64, width_factor: f64, oblique: f64) {
let t = Text {
insert: Vec3::new(at.0, at.1, 0.0),
align_pt: None,
@@ -225,7 +225,7 @@ fn font_files(doc: &Doc) -> usize {
#[test]
fn truetype_text_is_embedded_as_text() {
let mut d = drawing();
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "ABBA", (0.0, 1.0, 0.0));
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "ABBA", 0.0, 1.0, 0.0);
let doc = pdf(&d, &Space::Model, json!({}));
let page = &doc.pages[0];
// A Type0 font over a CIDFontType2 subset with a ToUnicode map.
@@ -260,7 +260,7 @@ fn truetype_text_is_embedded_as_text() {
#[test]
fn the_subset_is_a_valid_truetype_font() {
let mut d = drawing();
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "A", (0.0, 1.0, 0.0));
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "A", 0.0, 1.0, 0.0);
let doc = pdf(&d, &Space::Model, json!({}));
let file = doc.pages[0].fonts[0].font_file.clone().expect("an embedded font file");
let face = cadcraft_fonts::ttf::Face::new(std::sync::Arc::new(file.clone()), 0).expect("the subset parses");
@@ -273,9 +273,9 @@ fn the_subset_is_a_valid_truetype_font() {
fn rotation_width_factor_and_obliquing_go_into_the_text_matrix() {
let mut d = drawing();
let quarter = std::f64::consts::FRAC_PI_2;
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "A", (quarter, 1.0, 0.0));
text(&mut d, &Space::Model, "Real", (0.0, 50.0), "A", (0.0, 2.0, 0.0));
text(&mut d, &Space::Model, "Real", (0.0, 100.0), "A", (0.0, 1.0, 15f64.to_radians()));
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "A", quarter, 1.0, 0.0);
text(&mut d, &Space::Model, "Real", (0.0, 50.0), "A", 0.0, 2.0, 0.0);
text(&mut d, &Space::Model, "Real", (0.0, 100.0), "A", 0.0, 1.0, 15f64.to_radians());
let m = matrices(&pdf(&d, &Space::Model, json!({})));
assert_eq!(m.len(), 3, "{m:?}");
// Rotated a quarter turn: the baseline points up, "up" points left.
@@ -294,7 +294,7 @@ fn rotation_width_factor_and_obliquing_go_into_the_text_matrix() {
#[test]
fn a_font_the_writer_cannot_subset_plots_as_outlines() {
let mut d = drawing();
text(&mut d, &Space::Model, "Short", (0.0, 0.0), "AB", (0.0, 1.0, 0.0));
text(&mut d, &Space::Model, "Short", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
let p = pdf(&d, &Space::Model, json!({}));
assert!(font_files(&p) == 0 && pdf_read::count(p.content(), "BT") == 0 && fills(&p) > 0, "outlines: {}", p.content());
}
@@ -302,16 +302,16 @@ fn a_font_the_writer_cannot_subset_plots_as_outlines() {
#[test]
fn fonts_that_forbid_embedding_and_the_geometry_option_draw_outlines() {
let mut d = drawing();
text(&mut d, &Space::Model, "Locked", (0.0, 0.0), "AB", (0.0, 1.0, 0.0));
text(&mut d, &Space::Model, "Locked", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
let p = pdf(&d, &Space::Model, json!({}));
assert!(font_files(&p) == 0 && pdf_read::count(p.content(), "BT") == 0 && fills(&p) > 0, "restricted licence: outlines");
let mut d = drawing();
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "AB", (0.0, 1.0, 0.0));
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
let p = pdf(&d, &Space::Model, json!({"text": "geometry"}));
assert!(font_files(&p) == 0 && pdf_read::count(p.content(), "BT") == 0 && fills(&p) > 0, "text as geometry");
// The stroke font is always geometry.
let mut d = drawing();
text(&mut d, &Space::Model, "Standard", (0.0, 0.0), "AB", (0.0, 1.0, 0.0));
text(&mut d, &Space::Model, "Standard", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
let p = pdf(&d, &Space::Model, json!({}));
assert!(pdf_read::count(p.content(), "BT") == 0 && pdf_read::count(p.content(), "S") > 0);
}
@@ -321,8 +321,8 @@ fn text_seen_through_a_viewport_and_on_layers() {
let mut d = drawing();
let paper = Space::Paper("Layout1".into());
// Model: one word well inside the viewport, one cut by its right edge.
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "AB", (0.0, 1.0, 0.0));
text(&mut d, &Space::Model, "Real", (88.0, 0.0), "BA", (0.0, 1.0, 0.0));
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
text(&mut d, &Space::Model, "Real", (88.0, 0.0), "BA", 0.0, 1.0, 0.0);
let vp = Viewport {
center: Vec3::new(150.0, 100.0, 0.0),
width: 100.0,
@@ -337,7 +337,7 @@ fn text_seen_through_a_viewport_and_on_layers() {
on: true,
};
d.add(&paper, Common::default(), EntityKind::Viewport(vp)).unwrap();
text(&mut d, &paper, "Real", (20.0, 20.0), "B", (0.0, 1.0, 0.0));
text(&mut d, &paper, "Real", (20.0, 20.0), "B", 0.0, 1.0, 0.0);
let p = pdf(&d, &paper, json!({}));
// "AB" inside, the paper-space "B", and the glyph of "BA" left of the edge: real text. The
// glyph the edge cuts stays a (clipped) outline.
@@ -353,7 +353,7 @@ fn text_seen_through_a_viewport_and_on_layers() {
#[test]
fn text_sits_in_its_layers_marked_content() {
let mut d = drawing();
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "AB", (0.0, 1.0, 0.0));
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
let p = pdf(&d, &Space::Model, json!({}));
let ops = pdf_read::ops(p.content());
let first_bt = ops.iter().position(|o| o.op == "BT").unwrap();
+31 -42
View File
@@ -235,46 +235,6 @@ pub struct Op {
pub names: Vec<String>,
}
/// The index just past the string literal opening at `b[i]`, honouring escapes and nesting.
fn skip_string(b: &[char], mut i: usize) -> usize {
let mut depth = 0;
while i < b.len() {
match b[i] {
'\\' => i += 1,
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
i += 1;
}
i + 1
}
/// The index just past the inline dictionary opening at `b[i]` (`<<`).
fn skip_dict(b: &[char], mut i: usize) -> usize {
let mut depth = 0;
while i < b.len() {
if b[i] == '<' && b.get(i + 1) == Some(&'<') {
depth += 1;
i += 1;
} else if b[i] == '>' && b.get(i + 1) == Some(&'>') {
depth -= 1;
i += 1;
if depth == 0 {
i += 1;
break;
}
}
i += 1;
}
i
}
/// The operators of content text in order, each with its numeric operands (other operands
/// are skipped).
pub fn ops(content: &str) -> Vec<Op> {
@@ -293,10 +253,39 @@ pub fn ops(content: &str) -> Vec<Op> {
i += 1;
}
} else if c == '(' {
i = skip_string(&b, i);
let mut depth = 0;
while i < b.len() {
match b[i] {
'\\' => i += 1,
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
i += 1;
}
i += 1;
} else if c == '<' && b.get(i + 1) == Some(&'<') {
// An inline dictionary (marked-content properties): skip to its end.
i = skip_dict(&b, i);
let mut depth = 0;
while i < b.len() {
if b[i] == '<' && b.get(i + 1) == Some(&'<') {
depth += 1;
i += 1;
} else if b[i] == '>' && b.get(i + 1) == Some(&'>') {
depth -= 1;
i += 1;
if depth == 0 {
i += 1;
break;
}
}
i += 1;
}
} else if c == '<' {
while i < b.len() && b[i] != '>' {
i += 1;
+28 -40
View File
@@ -465,26 +465,6 @@ impl Family<'_> {
self.origin + p.rotate(self.ang)
}
/// The range of the lines swept, in this frame: the boundary's extent, cut to the view's
/// when there is one, and the view's x extent that spans are cut to. Spans are cut to the
/// view's extent only: they already lie within the boundary, and cutting them to its extent
/// too would move an end that sits on a boundary corner by a rounding error, so a view
/// covering the hatch would change its lines. `None` when the view misses the boundary.
fn lines_range(&self, bb: &Bounds2, view: Option<&Bounds2>) -> Option<(Vec2, Vec2, Option<(f64, f64)>)> {
let (mut lo, mut hi) = (bb.min, bb.max);
let mut span_cut = None;
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 None;
}
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));
span_cut = Some((vb.min.x, vb.max.x));
}
Some((lo, hi, span_cut))
}
/// Intersect a pattern family with boundary loops, respecting view, output, cancellation, and shared dash-work limits.
fn sweep(&self, loops: &[Vec<Vec2>], px: f64, view: Option<&Bounds2>, cancel: Option<&AtomicBool>, out: &mut Vec<Vec<Vec2>>, work: &mut u64) {
let local: Vec<Vec<Vec2>> = loops.iter().map(|l| l.iter().map(|p| self.to_local(*p)).collect()).collect();
@@ -492,7 +472,21 @@ impl Family<'_> {
if bb.is_empty() {
return;
}
let Some((lo, hi, span_cut)) = self.lines_range(&bb, view) else { return };
// The lines swept: the boundary's extent, cut to the view's (its corners in this frame)
// when there is one. Spans are cut to the view's extent only: they already lie within the
// boundary, and cutting them to its extent too would move an end that sits on a boundary
// corner by a rounding error, so a view covering the hatch would change its lines.
let (mut lo, mut hi) = (bb.min, bb.max);
let mut span_cut = None;
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));
span_cut = Some((vb.min.x, vb.max.x));
}
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;
@@ -510,7 +504,17 @@ impl Family<'_> {
return;
}
let y = k as f64 * self.spacing;
let xs = crossings(&local, y);
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 };
@@ -524,14 +528,14 @@ impl Family<'_> {
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, work);
self.dash(x0, x1, y, offset, pat_len, out, work);
}
}
}
}
/// The dashes and dots of the span `x0..x1` of the line at `y`, phase anchored at x = `offset`.
fn dash(&self, (x0, x1): (f64, f64), y: f64, offset: f64, pat_len: f64, out: &mut Vec<Vec<Vec2>>, work: &mut u64) {
fn dash(&self, x0: f64, x1: f64, y: f64, offset: f64, pat_len: f64, out: &mut Vec<Vec<Vec2>>, work: &mut u64) {
let start_rep = ((x0 - offset) / pat_len).floor();
let mut x = offset + start_rep * pat_len;
let mut guard = 0;
@@ -561,22 +565,6 @@ impl Family<'_> {
}
}
/// The x positions where the boundary loops cross the horizontal line at `y`, in order.
fn crossings(local: &[Vec<Vec2>], y: f64) -> Vec<f64> {
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);
xs
}
#[cfg(test)]
mod tests {
use super::*;
-3
View File
@@ -17,7 +17,6 @@ cadcraft-doc = { workspace = true }
cadcraft-render = { workspace = true }
cadcraft-engine = { workspace = true }
cadcraft-fonts = { workspace = true }
craft-window = { workspace = true }
egui = { workspace = true }
# GPU canvas paint callback (same egui-wgpu/wgpu eframe uses with its "wgpu" feature).
egui-wgpu = { workspace = true }
@@ -32,8 +31,6 @@ web-time = { workspace = true }
craft-update = { workspace = true }
[dev-dependencies]
# Tests build the dock area as the desktop app does (headless contexts keep floats in-window).
craft-ui = { workspace = true, features = ["native-windows"] }
# The workspace `egui` dependency enables `default_fonts` and `monochrome_emoji_fonts` in
# production too; the tests exercise the same installed chain as the app.
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
+12 -20
View File
@@ -4,8 +4,7 @@
use std::sync::Arc;
use cadcraft_engine::cmd::file::compare::Comparison;
use cadcraft_engine::doc::{Drawing, Handle};
use cadcraft_engine::doc::Handle;
use cadcraft_render::DisplayList;
use egui::{Rect, RichText, Stroke, vec2};
use serde_json::json;
@@ -30,23 +29,6 @@ fn id() -> egui::Id {
egui::Id::unique("cadcraft-compare-overlay")
}
/// The display lists of the differences for this frame: the stored ones when they still match
/// `key`, otherwise rebuilt at zoom `scale` and stored.
fn cache_for(ctx: &egui::Context, key: (u64, u64, usize, usize, i64), doc: &Drawing, c: &Comparison, scale: f64) -> Cache {
let mut cache: Cache = ctx.data_mut(|d| d.get_temp(id())).unwrap_or_default();
if cache.key != key || cache.ours.is_none() {
let opts = cadcraft_render::Options { tolerance: 0.5 / scale.max(1e-12), ..Default::default() };
let ours: Vec<Handle> = c.added.iter().copied().chain(c.changed.iter().map(|(a, _)| *a)).take(MAX_DRAWN).collect();
let theirs: Vec<Handle> = c.removed.iter().copied().chain(c.changed.iter().map(|(_, b)| *b)).take(MAX_DRAWN).collect();
let list = |d: &Drawing, hs: &[Handle]| {
Arc::new(cadcraft_render::build_entities(d, hs.iter().filter_map(|h| d.entity(*h)).map(|e| e.as_ref()), &opts))
};
cache = Cache { key, ours: Some(list(doc, &ours)), theirs: Some(list(&c.other, &theirs)) };
ctx.data_mut(|d| d.insert_temp(id(), cache.clone()));
}
cache
}
/// Draw the comparison over the canvas when it was made for the drawing and tab shown.
pub(super) fn paint(app: &mut CadApp, ctx: &egui::Context, p: &egui::Painter, xf: &Xf, rect: Rect) {
let t = Tokens::get();
@@ -56,7 +38,17 @@ pub(super) fn paint(app: &mut CadApp, ctx: &egui::Context, p: &egui::Painter, xf
p.rect_filled(rect, 0.0, t.canvas.gamma_multiply(0.75));
let band = xf.scale.max(1e-300).log2().round() as i64;
let key = (st.uid, st.revision, Arc::as_ptr(&c.other) as usize, c.differences(), band);
let cache = cache_for(ctx, key, &st.doc, c, xf.scale);
let mut cache: Cache = ctx.data_mut(|d| d.get_temp(id())).unwrap_or_default();
if cache.key != key || cache.ours.is_none() {
let opts = cadcraft_render::Options { tolerance: 0.5 / xf.scale.max(1e-12), ..Default::default() };
let ours: Vec<Handle> = c.added.iter().copied().chain(c.changed.iter().map(|(a, _)| *a)).take(MAX_DRAWN).collect();
let theirs: Vec<Handle> = c.removed.iter().copied().chain(c.changed.iter().map(|(_, b)| *b)).take(MAX_DRAWN).collect();
let list = |d: &cadcraft_engine::doc::Drawing, hs: &[Handle]| {
Arc::new(cadcraft_render::build_entities(d, hs.iter().filter_map(|h| d.entity(*h)).map(|e| e.as_ref()), &opts))
};
cache = Cache { key, ours: Some(list(&st.doc, &ours)), theirs: Some(list(&c.other, &theirs)) };
ctx.data_mut(|d| d.insert_temp(id(), cache.clone()));
}
for (list, color) in [(&cache.theirs, t.compare_other), (&cache.ours, t.compare_this)] {
if let Some(l) = list {
let all: Vec<usize> = (0..l.prims.len()).collect();
+10 -2
View File
@@ -69,8 +69,6 @@ pub fn title_and_toolbar(app: &mut CadApp, ui: &mut egui::Ui) {
egui::Panel::top("cc_title_toolbar").exact_size(title_h + tb_h).frame(egui::Frame::NONE.fill(t.chrome)).show(ui, |ui| {
let r = ui.max_rect();
if title_h > 0.0 {
// The empty row moves the window; it goes first so later widgets take their own clicks.
craft_window::title_bar::empty_area(ui, Rect::from_min_size(r.min, vec2(r.width(), title_h)));
let title = match app.session.state() {
Ok(st) if !app.ui.start_tab => format!("CADCraft {}{}", st.title, if st.title.contains('.') { "" } else { ".dwg" }),
_ => format!("CADCraft {}", crate::tl!("Start")),
@@ -80,6 +78,16 @@ pub fn title_and_toolbar(app: &mut CadApp, ui: &mut egui::Ui) {
let galley = ui.painter().layout_no_wrap(title, egui::FontId::proportional(13.0), t.text_dim);
let x = title_left(r.left(), r.width(), galley.size().x);
ui.painter().galley(pos2(x, r.top() + (title_h - galley.size().y) / 2.0), galley, t.text_dim);
// Allow dragging the window by the title row.
let tr = Rect::from_min_size(r.min, vec2(r.width(), title_h));
let resp = ui.interact(tr, ui.scope_id().with("titledrag"), Sense::CLICK | Sense::DRAG);
if resp.drag_started() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag);
}
if resp.double_clicked() {
let max = ui.ctx().input(|i| i.viewport().maximized.unwrap_or(false));
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Maximized(!max));
}
}
if tb_h > 0.0 {
let row = Rect::from_min_size(pos2(r.left(), r.top() + title_h), vec2(r.width(), tb_h));
+48 -194
View File
@@ -2,8 +2,8 @@
//! Document contents remain owned by their existing view implementations.
use craft_ui::docking::{
Action, Column, DockArea, DockCommand, DockContent, DockHeights, DockOperation, DockStyle, FloatSpace, Group, Layout, Location, Node,
PanelLimits, Permissions, Placement, Side, Sides, Target, UiDock, Zone,
DockArea, DockCommand, DockOperation, DockStyle, FloatSpace, Layout, Location, Node, PanelLimits, Permissions, Placement, StackEntry, UiDock,
Zone,
};
use craft_ui::layout::{SplitAxis, SplitSize};
use serde::{Deserialize, Serialize};
@@ -43,18 +43,6 @@ impl Panel {
Self::ExternalRefs => "External References",
}
}
/// The icon a palette shows in a column collapsed to icons.
pub fn icon(self) -> Option<crate::icons::Icon> {
use crate::icons::Icon;
match self {
Self::Canvas => None,
Self::ToolSets => Some(Icon::Switcher),
Self::QuickCalc => Some(Icon::Table),
Self::Layers => Some(Icon::Layers),
Self::Properties => Some(Icon::Properties),
Self::ExternalRefs => Some(Icon::Attach),
}
}
/// Palettes outside the default arrangement, opened on request (QUICKCALC).
pub const ON_DEMAND: &[Self] = &[Self::QuickCalc];
}
@@ -75,106 +63,31 @@ fn split(axis: SplitAxis, size: SplitSize, first: Node<Panel>, second: Node<Pane
/// The saved panel arrangement: craft-ui's workspace over CADCraft's panels.
pub type Workspace = craft_ui::docking::Workspace<Panel>;
/// The panel layout's shape version, saved beside it. Layouts saved without it, or with an
/// older one, are the split frames from before the dock columns and give way to the default.
pub const LAYOUT_VERSION: u32 = 2;
/// Width of the left dock's Tool Sets column.
pub const TOOL_SETS_WIDTH: f32 = 220.0;
/// Width of the right dock's palettes column.
pub const PALETTES_WIDTH: f32 = 300.0;
/// Layers' body height above Properties in a dock column.
const LAYERS_HEIGHT: f32 = 280.0;
fn dock(side: Side, width: f32, groups: &[Panel]) -> Node<Panel> {
let groups = groups.iter().map(|panel| Group::new(vec![*panel], 0)).collect();
Node::Dock { side, columns: vec![Column::new(groups, width)] }
}
/// Tool Sets in the left dock, the drawing, and Layers above Properties in the right dock.
/// Tool Sets on the left, the drawing, and the Layers and Properties accordion on the right.
pub fn default_workspace() -> Workspace {
Workspace::new(Layout {
root: Some(split(
SplitAxis::Horizontal,
SplitSize::FixedFirst(TOOL_SETS_WIDTH),
dock(Side::Start, TOOL_SETS_WIDTH, &[Panel::ToolSets]),
SplitSize::FixedFirst(220.0),
tabs(Panel::ToolSets),
split(
SplitAxis::Horizontal,
SplitSize::FixedSecond(PALETTES_WIDTH),
SplitSize::FixedSecond(300.0),
tabs(Panel::Canvas),
dock(Side::End, PALETTES_WIDTH, &[Panel::Layers, Panel::Properties]),
Node::Stack {
entries: vec![
StackEntry { panel: Panel::Layers, open: true, height: Some(280.0) },
StackEntry { panel: Panel::Properties, open: true, height: None },
],
},
),
)),
floating: Vec::new(),
})
}
/// The dock a panel belongs in: Tool Sets on the left, the palettes on the right.
fn home_side(panel: Panel) -> (Side, f32) {
if panel == Panel::ToolSets { (Side::Start, TOOL_SETS_WIDTH) } else { (Side::End, PALETTES_WIDTH) }
}
/// The panels of every dock group on `side`: columns from the window edge inwards, groups from
/// top to bottom.
fn dock_groups(layout: &Layout<Panel>, side: Side) -> Vec<Vec<Vec<Panel>>> {
let mut nodes: Vec<&Node<Panel>> = layout.root.iter().collect();
let mut seen = 0usize;
while let Some(node) = nodes.pop() {
seen = seen.saturating_add(1);
if seen > 4 * craft_ui::docking::MAX_PANELS {
break;
}
match node {
Node::Split { first, second, .. } => nodes.extend([&**first, &**second]),
Node::Dock { side: s, columns } if *s == side => {
return columns.iter().map(|column| column.groups.iter().map(|group| group.panels.clone()).collect()).collect();
}
_ => {}
}
}
Vec::new()
}
fn docked(layout: &Layout<Panel>, panel: Panel) -> bool {
[Side::Start, Side::End].into_iter().any(|side| dock_groups(layout, side).iter().flatten().flatten().any(|p| *p == panel))
}
/// The docked neighbour a panel with no recorded place opens beside, as a new group above
/// (`false`) or below (`true`) it: Layers above Properties, External References and QuickCalc
/// below them, Tool Sets at the top of the left dock.
fn home_anchor(layout: &Layout<Panel>, panel: Panel) -> Option<(Panel, bool)> {
let neighbours: &[(Panel, bool)] = match panel {
Panel::Canvas | Panel::ToolSets => &[],
Panel::Layers => &[(Panel::Properties, false), (Panel::ExternalRefs, false)],
Panel::Properties => &[(Panel::Layers, true), (Panel::ExternalRefs, false)],
Panel::ExternalRefs => &[(Panel::Properties, true), (Panel::Layers, true)],
Panel::QuickCalc => &[(Panel::ExternalRefs, true), (Panel::Properties, true), (Panel::Layers, true)],
};
if let Some(found) = neighbours.iter().copied().find(|(neighbour, _)| docked(layout, *neighbour)) {
return Some(found);
}
let (side, _) = home_side(panel);
let columns = dock_groups(layout, side);
let column = columns.first()?;
if panel == Panel::ToolSets {
column.first().and_then(|group| group.first()).map(|anchor| (*anchor, false))
} else {
column.last().and_then(|group| group.first()).map(|anchor| (*anchor, true))
}
}
/// The actions that open a panel with no recorded place in its dock: a new group beside its
/// docked neighbour, else a new dock on its side at its default width.
fn home(layout: &Layout<Panel>, panel: Panel) -> Vec<Action<Panel>> {
if let Some((anchor, below)) = home_anchor(layout, panel) {
return vec![Action::OpenAt { panel, anchor, placement: Placement::Group { below, height: None } }];
}
let (side, width) = home_side(panel);
vec![Action::Open { panel, anchor: None }, Action::MovePanel { panel, to: Target::NewDock { side } }, Action::ResizeColumn { panel, width }]
}
/// craft-ui's checks plus the drawing view's: a saved or hand-edited file cannot hide, float,
/// group, collapse or dock it, or restore a palette over it.
/// group or collapse it, or restore a palette over it.
pub fn validate(workspace: &Workspace) -> Result<(), String> {
workspace.validate().map_err(|error| error.to_string())?;
if !workspace.layout.contains(&Panel::Canvas) {
@@ -211,17 +124,11 @@ pub fn validate(workspace: &Workspace) -> Result<(), String> {
Ok(())
}
/// Show a panel and select it at its recorded place, keeping the drawing view's rules. A panel
/// with no recorded place opens in its dock ([`home`]); one whose recorded place is gone joins
/// its docked neighbour's group.
/// Show a panel and select it at its recorded place (craft-ui places a panel it has no place
/// for), keeping the drawing view's rules.
pub fn reveal_panel(workspace: &mut Workspace, panel: Panel) -> Result<(), String> {
let mut next = workspace.clone();
if next.layout.contains(&panel) || next.hidden.iter().any(|(id, _)| *id == panel) {
let fallback = home_anchor(&next.layout, panel).map(|(anchor, _)| anchor);
next.set_visible(panel, true, fallback, permissions).map_err(|error| error.to_string())?;
} else {
next.apply_all(home(&next.layout, panel).into_iter().chain([Action::Activate { panel }]), permissions).map_err(|error| error.to_string())?;
}
next.set_visible(panel, true, None, permissions).map_err(|error| error.to_string())?;
validate(&next)?;
*workspace = next;
Ok(())
@@ -281,7 +188,7 @@ fn sync(workspace: &mut Workspace, app: &crate::CadApp) -> Result<(), String> {
set_visible(workspace, Panel::Layers, false)?;
set_visible(workspace, Panel::Properties, false)?;
} else if !workspace.layout.contains(&Panel::Layers) && !workspace.layout.contains(&Panel::Properties) {
// Reopen the palette that closed last before the one recorded beside it.
// Reopen the last surviving accordion entry before restoring its earlier sibling.
set_visible(workspace, Panel::Properties, true)?;
set_visible(workspace, Panel::Layers, true)?;
}
@@ -300,10 +207,6 @@ pub fn command(app: &mut crate::CadApp, reset: bool, params: &Value) -> Result<V
} else {
sync(&mut workspace, app)?;
apply_command(&mut workspace, params)?;
// An opened panel's floating window comes to the front, in its own OS window too.
if matches!(params.get("operation").and_then(Value::as_str), Some("open" | "activate")) {
app.raise_panel = params.get("panel").and_then(|panel| serde_json::from_value(panel.clone()).ok());
}
}
publish_visibility(app, &workspace);
app.ui.docking = workspace;
@@ -332,30 +235,39 @@ pub fn show(app: &mut crate::CadApp, ui: &mut egui::Ui) {
style.border = egui::Stroke::new(1.0, t.border);
style.accent = t.accent;
style.font = crate::theme::body();
// AutoCAD's palettes dock only in the left and right dock columns: a window edge without a
// dock makes one, and neither the drawing nor the top and bottom edges take a drop.
// Floating title bars only move windows.
style.side_edge_zones = false;
style.new_dock_zones = true;
style.drop_edges = Sides::NONE;
// Panels arrange in a split tree around the drawing; floating title bars only move windows.
style.new_dock_zones = false;
style.title_bar_docks = false;
let mut area = DockArea::new(egui::Id::unique("cadcraft-docking")).input_blocked(native_input_blocked(app, ui.ctx()));
if let Some(panel) = app.raise_panel.take() {
area = area.raise_panel(panel);
}
let mut output = area.show_content(ui, &workspace.layout, &style, &mut Panels { app });
// Keys typed in a floating panel's own OS window run the main window's shortcuts and
// command line; files dropped on one open like files dropped on the main window.
app.forward_native_input(ui.ctx(), std::mem::take(&mut output.forwarded_events), output.forwarded_modifiers);
#[cfg(not(target_arch = "wasm32"))]
for files in std::mem::take(&mut output.window_files) {
for file in files.dropped {
let path = file.path().to_string_lossy().to_string();
if !path.is_empty() {
app.open_path(&path);
let output = DockArea::new(egui::Id::unique("cadcraft-docking")).show_with_limits(
ui,
&workspace.layout,
&style,
|panel| crate::i18n::t(panel.label()).into(),
permissions,
panel_limits,
|ui, panel| match panel {
Panel::Canvas => {
ui.painter().rect_filled(ui.max_rect(), 0.0, t.canvas);
crate::canvas::show(app, ui);
}
}
}
Panel::ToolSets => crate::palettes::toolsets(app, ui),
Panel::QuickCalc => crate::palettes::quickcalc::show(app, ui),
Panel::Layers => {
egui::ScrollArea::vertical()
.id_salt("dock-layers")
.auto_shrink([false, false])
.show(ui, |ui| crate::palettes::layers_section(app, ui));
}
Panel::Properties => {
egui::ScrollArea::vertical().id_salt("dock-properties").auto_shrink([false, false]).show(ui, |ui| {
crate::shortcut_menus::palette(app, ui, crate::palettes::properties_section, crate::shortcut_menus::properties_entries)
});
}
Panel::ExternalRefs => {
egui::ScrollArea::both().id_salt("dock-xrefs").auto_shrink([false, false]).show(ui, |ui| crate::xrefs::section(app, ui));
}
},
);
// Existing panel close buttons and commands still own their visibility switches.
let rendered = workspace;
let mut workspace = app.ui.docking.clone();
@@ -378,64 +290,6 @@ pub fn show(app: &mut crate::CadApp, ui: &mut egui::Ui) {
}
}
/// Whether the floating panels' own OS windows ignore input: while a modal dialog is up, since
/// egui's `Modal` blocks only the main window. egui's modal layer is last frame's; the app's
/// state covers the frame a modal opens in.
pub fn native_input_blocked(app: &crate::CadApp, ctx: &egui::Context) -> bool {
app.shows_modal() || ctx.memory(|memory| memory.top_modal_layer()).is_some()
}
/// The panels' contents, names, size limits and icons for the dock area.
pub struct Panels<'a> {
pub app: &'a mut crate::CadApp,
}
impl DockContent<Panel> for Panels<'_> {
fn body(&mut self, ui: &mut egui::Ui, panel: &Panel) {
let app = &mut *self.app;
match panel {
Panel::Canvas => {
ui.painter().rect_filled(ui.max_rect(), 0.0, crate::theme::Tokens::get().canvas);
crate::canvas::show(app, ui);
}
Panel::ToolSets => crate::palettes::toolsets(app, ui),
Panel::QuickCalc => crate::palettes::quickcalc::show(app, ui),
Panel::Layers => {
egui::ScrollArea::vertical()
.id_salt("dock-layers")
.auto_shrink([false, false])
.show(ui, |ui| crate::palettes::layers_section(app, ui));
}
Panel::Properties => {
egui::ScrollArea::vertical().id_salt("dock-properties").auto_shrink([false, false]).show(ui, |ui| {
crate::shortcut_menus::palette(app, ui, crate::palettes::properties_section, crate::shortcut_menus::properties_entries)
});
}
Panel::ExternalRefs => {
egui::ScrollArea::both().id_salt("dock-xrefs").auto_shrink([false, false]).show(ui, |ui| crate::xrefs::section(app, ui));
}
}
}
fn logical_label(&self, panel: &Panel) -> String {
crate::i18n::t(panel.label()).into()
}
fn permissions(&self, panel: &Panel) -> Permissions {
permissions(panel)
}
fn limits(&self, panel: &Panel) -> PanelLimits {
panel_limits(panel)
}
fn dock_heights(&self, panel: &Panel) -> Option<DockHeights> {
// Layers keeps its list's height above Properties, which takes the rest of the column.
(*panel == Panel::Layers).then(|| DockHeights::new(LAYERS_HEIGHT))
}
fn paint_icon(&mut self, painter: &egui::Painter, rect: egui::Rect, panel: &Panel, _color: egui::Color32) -> bool {
let Some(icon) = panel.icon() else { return false };
crate::icons::paint(painter, rect, icon, false);
true
}
}
fn panel_limits(panel: &Panel) -> PanelLimits {
let (min, max_width) = match panel {
Panel::Canvas => (egui::vec2(300.0, 180.0), 1_000_000.0),
+1 -60
View File
@@ -235,15 +235,6 @@ pub struct CadApp {
/// Admitted `ui.screenshot` requests, including completed-but-undrained ones.
pub(crate) shots: capture::Shots,
pub synthetic: Vec<egui::Event>,
/// Keys, text and clipboard events a floating panel's own OS window left over, with the
/// modifiers held there, for the main window's next input ([`Self::forward_native_input`]).
forwarded: Vec<egui::Event>,
forwarded_modifiers: Option<egui::Modifiers>,
/// The main window's own modifiers, put back the frame after forwarded keys went in.
restore_modifiers: Option<egui::Modifiers>,
/// A panel opened from a menu or command, whose floating window comes to the front on the
/// next frame (craft-ui's `DockArea::raise_panel`).
pub raise_panel: Option<docking::Panel>,
/// One synthetic batch has been put into `RawInput` and is waiting for a real `App::ui` call.
synthetic_injected: bool,
/// Counts settled UI passes (one per painted frame; never hidden logic-only ticks or
@@ -312,10 +303,6 @@ impl CadApp {
capture_identity_epoch: 0,
shots: capture::Shots::default(),
synthetic: Vec::new(),
forwarded: Vec::new(),
forwarded_modifiers: None,
restore_modifiers: None,
raise_panel: None,
synthetic_injected: false,
ui_frames: 0,
#[cfg(not(target_arch = "wasm32"))]
@@ -617,9 +604,6 @@ impl CadApp {
if ctx.current_pass_index() != 0 {
return;
}
if self.restore_modifiers.is_some() {
ctx.request_repaint();
}
// Closing the window with unsaved changes asks first. `run_logic` refreshes viewport
// close/focus state, so this must also work while hidden.
if ctx.input(|i| i.viewport().close_requested()) && self.request_quit() {
@@ -641,56 +625,13 @@ impl CadApp {
capture::request_hidden_paint(self, ctx);
}
/// Queue keys typed in a floating panel's own OS window (craft-ui's
/// `DockOutput::forwarded_events` and `forwarded_modifiers`) for the main window's next
/// input. egui gives each OS window only its own input, and the shortcuts, the command line
/// and the canvas all read the main window's, so the keys go in there as if typed in it.
pub fn forward_native_input(&mut self, ctx: &egui::Context, events: Vec<egui::Event>, modifiers: Option<egui::Modifiers>) {
if events.is_empty() {
return;
}
self.forwarded.extend(events);
if modifiers.is_some() {
self.forwarded_modifiers = modifiers;
}
if self.restore_modifiers.is_none() {
self.restore_modifiers = Some(ctx.input(|i| i.modifiers));
}
ctx.request_repaint_of(egui::ViewportId::ROOT);
}
/// Whether a modal dialog is up: the "Save changes?" prompt or Drawing Recovery. The
/// floating panels' own OS windows take no input meanwhile (egui's `Modal` blocks only the
/// main window). The app's other dialogs are ordinary windows that leave the drawing usable.
pub fn shows_modal(&self) -> bool {
self.close_prompt().is_some() || !self.recovery.items.is_empty()
}
/// Input hook: refresh the raw surface snapshot, put in keys forwarded from floating panels'
/// OS windows, and release at most one synthetic batch.
/// Input hook: refresh the raw surface snapshot and release at most one synthetic batch.
///
/// Eframe keeps the unused `RawInput` across hidden logic-only ticks, so a batch already in
/// the buffer is never injected again; the next batch waits for a real `ui` pass to consume
/// the previous one.
pub fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
capture::note_raw_geometry(self, ctx, raw);
// The forwarded window's modifiers hold for the frame its keys go in (Shift+Copy is
// COPYBASE); the next frame puts the main window's back, before its own changes.
if !self.forwarded.is_empty() {
// A modifier the main window changed meanwhile is the one to put back.
if let Some(own) = raw.events.iter().rev().find_map(|event| match event {
egui::Event::ModifiersChanged(modifiers) => Some(*modifiers),
_ => None,
}) {
self.restore_modifiers = Some(own);
}
if let Some(modifiers) = self.forwarded_modifiers.take() {
raw.events.push(egui::Event::ModifiersChanged(modifiers));
}
raw.events.append(&mut self.forwarded);
} else if let Some(modifiers) = self.restore_modifiers.take() {
raw.events.insert(0, egui::Event::ModifiersChanged(modifiers));
}
if self.synthetic_injected || self.synthetic.is_empty() {
return;
}
+1 -56
View File
@@ -172,7 +172,7 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
let pick = app.services.pick_lisp.as_ref()?;
let dir = app.ui.last_lisp_dir.clone().unwrap_or_default();
let Some(path) = pick(&dir) else { return Some(Ok(Value::Null)) };
if let Some(dir) = std::path::Path::new(&path).parent().and_then(std::path::Path::to_str).filter(|d| !d.is_empty()) {
if let Some(dir) = std::path::Path::new(&path).parent().and_then(|p| p.to_str()).filter(|d| !d.is_empty()) {
app.ui.last_lisp_dir = Some(dir.to_string());
}
if let Err(e) = app.session.start("appload") {
@@ -649,61 +649,6 @@ pub fn activate(app: &mut CadApp, id: &str) {
app.start(id);
}
/// Activate the menu command `id` from a native menu bar, which takes the Edit menu's key
/// equivalents (⌘C, ⌘X, ⌘V, ⌘A, ⌘Z, ⇧⌘Z) before any window sees them: while a text field has
/// the keyboard, in the main window or a floating panel's own window, the field gets the key
/// and the drawing command does not run.
pub fn activate_native(app: &mut CadApp, ctx: &egui::Context, id: &str) {
let (Some(window), Some(text)) = (text_field_window(ctx), text_command(id, ctx.os().is_mac())) else {
activate(app, id);
return;
};
match text {
TextCommand::Paste => ctx.send_viewport_cmd_to(window, egui::ViewportCommand::RequestPaste),
TextCommand::Events(events) if window == egui::ViewportId::ROOT => {
app.synthetic.extend(events);
ctx.request_repaint();
}
TextCommand::Events(events) => craft_ui::docking::send_keys(ctx, window, events),
}
}
/// The window whose text field has the keyboard: a floating panel's own OS window, or the main
/// window.
pub fn text_field_window(ctx: &egui::Context) -> Option<egui::ViewportId> {
craft_ui::docking::typing_window(ctx).or_else(|| ctx.text_edit_focused().then_some(egui::ViewportId::ROOT))
}
/// Whether a focused text field takes the menu command `id` ([`activate_native`]): a native
/// menu keeps such items enabled while one has the keyboard, whatever the drawing allows.
pub fn edits_text(id: &str) -> bool {
text_command(id, false).is_some()
}
/// What a focused text field does for an Edit menu command.
#[derive(Debug, PartialEq)]
enum TextCommand {
/// The clipboard text, which the window asks for and receives as `Paste`.
Paste,
Events(Vec<egui::Event>),
}
fn text_command(id: &str, mac: bool) -> Option<TextCommand> {
let key = |key, shift| {
let modifiers = egui::Modifiers { mac_cmd: mac, ctrl: !mac, command: true, shift, alt: false };
TextCommand::Events([true, false].map(|pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }).into())
};
Some(match id {
"copyclip" => TextCommand::Events(vec![egui::Event::Copy]),
"cutclip" => TextCommand::Events(vec![egui::Event::Cut]),
"pasteclip" => TextCommand::Paste,
"u" => key(egui::Key::Z, false),
"redo" => key(egui::Key::Z, true),
"selectall" => key(egui::Key::A, false),
_ => return None,
})
}
/// Run `id` with default parameters without cancelling the active command, as the drafting
/// toggles (F3, F7-F12, the status bar buttons) are in AutoCAD.
pub fn run_transparent(app: &mut CadApp, id: &str) {
+63 -75
View File
@@ -217,15 +217,73 @@ fn display(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
/// Files tab: save format and default version, new-drawing units, autosave interval and autosave file location; edits accumulate in `changes`.
fn files(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
grid(ui, "options_files", |ui| {
save_format_row(ui, app, changes);
ui.label(crate::tl!("Save new drawings as"));
ui.horizontal(|ui| {
for f in crate::SaveFormat::ALL {
let label = match f {
crate::SaveFormat::Dxf => crate::tl!("DXF"),
crate::SaveFormat::Dwg => crate::tl!("DWG"),
};
if ui.radio(app.ui.save_format == f, label).clicked() && app.ui.save_format != f {
changes.insert("saveFormat".into(), json!(f.as_str()));
}
}
});
ui.end_row();
save_version_row(ui, app, changes);
let version_caption = ui.label(crate::tl!("Default save version"));
let current = crate::SaveVersion::parse(&app.ui.save_version).unwrap_or_default();
let mut version = current;
let version_combo = 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());
}
});
version_combo.response.labelled_by(version_caption.id);
if version != current {
changes.insert("saveVersion".into(), json!(version.year()));
}
ui.end_row();
units_row(ui, app, changes);
ui.label(crate::tl!("Units of new drawings"));
ui.horizontal(|ui| {
let metric = app.session.settings.measureinit;
if ui.radio(!metric, crate::tl!("Imperial (inches)")).clicked() && metric {
changes.insert("MEASUREINIT".into(), json!(0));
}
if ui.radio(metric, crate::tl!("Metric (millimetres)")).clicked() && !metric {
changes.insert("MEASUREINIT".into(), json!(1));
}
});
ui.end_row();
autosave_row(ui, changes);
let minutes_caption = 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"))))
.labelled_by(minutes_caption.id)
.changed()
{
changes.insert("SAVETIME".into(), json!(minutes));
}
ui.end_row();
savefile_row(ui, changes);
let savefile_label = ui.label(crate::tl!("Automatic save file location"));
// Edited as text; the folder changes when the field loses focus.
let id = egui::Id::unique("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)).labelled_by(savefile_label.id);
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!("New and the first drawing at startup use these units unless a default template is set (QNEWTEMPLATE)."));
@@ -233,76 +291,6 @@ fn files(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
hint(ui, crate::tl!("Drawings with no file yet are saved in the default version. 0 minutes turns automatic save off."));
}
fn save_format_row(ui: &mut egui::Ui, app: &CadApp, changes: &mut Map<String, Value>) {
ui.label(crate::tl!("Save new drawings as"));
ui.horizontal(|ui| {
for f in crate::SaveFormat::ALL {
let label = match f {
crate::SaveFormat::Dxf => crate::tl!("DXF"),
crate::SaveFormat::Dwg => crate::tl!("DWG"),
};
if ui.radio(app.ui.save_format == f, label).clicked() && app.ui.save_format != f {
changes.insert("saveFormat".into(), json!(f.as_str()));
}
}
});
}
fn save_version_row(ui: &mut egui::Ui, app: &CadApp, changes: &mut Map<String, Value>) {
let version_caption = ui.label(crate::tl!("Default save version"));
let current = crate::SaveVersion::parse(&app.ui.save_version).unwrap_or_default();
let mut version = current;
let version_combo = 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());
}
});
version_combo.response.labelled_by(version_caption.id);
if version != current {
changes.insert("saveVersion".into(), json!(version.year()));
}
}
fn units_row(ui: &mut egui::Ui, app: &CadApp, changes: &mut Map<String, Value>) {
ui.label(crate::tl!("Units of new drawings"));
ui.horizontal(|ui| {
let metric = app.session.settings.measureinit;
if ui.radio(!metric, crate::tl!("Imperial (inches)")).clicked() && metric {
changes.insert("MEASUREINIT".into(), json!(0));
}
if ui.radio(metric, crate::tl!("Metric (millimetres)")).clicked() && !metric {
changes.insert("MEASUREINIT".into(), json!(1));
}
});
}
fn autosave_row(ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
let minutes_caption = 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")))).labelled_by(minutes_caption.id).changed() {
changes.insert("SAVETIME".into(), json!(minutes));
}
}
fn savefile_row(ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
let savefile_label = ui.label(crate::tl!("Automatic save file location"));
// Edited as text; the folder changes when the field loses focus.
let id = egui::Id::unique("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)).labelled_by(savefile_label.id);
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()));
}
}
}
/// Drafting tab: object-snap marker size and aperture previews plus the Drafting Settings entry; edits accumulate in `changes` and the entry may request a command through `start`.
fn drafting(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>, start: &mut Option<&'static str>) {
let t = Tokens::get();
+12 -3
View File
@@ -37,10 +37,19 @@ pub fn route(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value,
}
}
/// Show the palette. The first time it docks below Properties in the right dock; after that it
/// comes back where it was.
/// Show the palette. The first time it docks below the Layers palette (where the palettes column
/// is); after that it comes back where it was.
fn open_palette(app: &mut CadApp) -> Result<Value, String> {
crate::docking::command(app, false, &json!({ "operation": "open", "panel": Panel::ExternalRefs.id() }))
let panel = Panel::ExternalRefs;
let first = !app.ui.docking.layout.contains(&panel) && !app.ui.docking.hidden.iter().any(|(p, _)| *p == panel);
let r = crate::docking::command(app, false, &json!({ "operation": "open", "panel": panel.id() }))?;
if first && app.ui.docking.layout.contains(&Panel::Layers) {
let moved = json!({ "operation": "move", "panel": panel.id(), "target": Panel::Layers.id(), "zone": "bottom" });
if let Ok(r) = crate::docking::command(app, false, &moved) {
return Ok(r);
}
}
Ok(r)
}
/// A picked file inside the host drawing's folder is attached by its relative path, so the
+86 -261
View File
@@ -1,8 +1,7 @@
//! Panel moves use the real app shell, retain commands, and survive a complete UI-state reload.
use cadcraft_engine::Session;
use cadcraft_ui_egui::{CadApp, Services, UiState, docking::Panel, menus};
use craft_ui::docking::{Column, Divider, DockArea, Hit, Layout, Node, Side};
use cadcraft_ui_egui::{CadApp, Services, UiState, docking::Panel};
use egui::{Event, Modifiers, PointerButton, vec2};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
@@ -43,110 +42,6 @@ fn drag(harness: &mut Harness<'_, CadApp>, from: egui::Pos2, to: egui::Pos2) {
harness.run_steps(4);
}
fn area() -> egui::Id {
egui::Id::unique("cadcraft-docking")
}
/// The rect the dock area drew last frame for `hit`.
fn hit(h: &Harness<'_, CadApp>, hit: Hit<Panel>) -> egui::Rect {
let hits = DockArea::<Panel>::hit_rects(&h.ctx, area());
hits.iter().find(|(drawn, _)| *drawn == hit).map(|(_, rect)| *rect).unwrap_or_else(|| panic!("{hit:?} is not drawn: {hits:?}"))
}
/// The columns of the dock on `side`, from the window edge inwards.
fn columns(layout: &Layout<Panel>, side: Side) -> Vec<Column<Panel>> {
let mut nodes: Vec<&Node<Panel>> = layout.root.iter().collect();
while let Some(node) = nodes.pop() {
match node {
Node::Split { first, second, .. } => nodes.extend([&**first, &**second]),
Node::Dock { side: s, columns } if *s == side => return columns.clone(),
_ => {}
}
}
Vec::new()
}
/// The panels of each group of each column of the dock on `side`.
fn dock_panels(layout: &Layout<Panel>, side: Side) -> Vec<Vec<Vec<Panel>>> {
columns(layout, side).iter().map(|column| column.groups.iter().map(|group| group.panels.clone()).collect()).collect()
}
/// The tab groups outside the docks: the frames of the split tree.
fn frames(layout: &Layout<Panel>) -> Vec<Vec<Panel>> {
let mut out = Vec::new();
let mut nodes: Vec<&Node<Panel>> = layout.root.iter().collect();
while let Some(node) = nodes.pop() {
match node {
Node::Split { first, second, .. } => nodes.extend([&**first, &**second]),
Node::Tabs { panels, .. } => out.push(panels.clone()),
Node::Stack { entries } => out.push(entries.iter().map(|entry| entry.panel).collect()),
_ => {}
}
}
out
}
#[test]
fn default_layout_is_tool_sets_left_and_the_palettes_right_in_dock_columns() {
use cadcraft_ui_egui::docking::{PALETTES_WIDTH, TOOL_SETS_WIDTH, default_workspace, validate};
let workspace = default_workspace();
validate(&workspace).unwrap();
assert!(workspace.hidden.is_empty() && workspace.layout.floating.is_empty());
assert_eq!(frames(&workspace.layout), [[Panel::Canvas]]);
let start = columns(&workspace.layout, Side::Start);
let end = columns(&workspace.layout, Side::End);
assert_eq!(dock_panels(&workspace.layout, Side::Start), [[[Panel::ToolSets]]]);
assert_eq!(dock_panels(&workspace.layout, Side::End), [vec![vec![Panel::Layers], vec![Panel::Properties]]]);
assert_eq!((start[0].width, end[0].width), (TOOL_SETS_WIDTH, PALETTES_WIDTH));
assert!(start.iter().chain(&end).all(|column| !column.iconic));
assert!(start.iter().chain(&end).flat_map(|column| &column.groups).all(|group| group.height.is_none() && !group.minimized));
assert!(!workspace.layout.contains(&Panel::QuickCalc) && !workspace.layout.contains(&Panel::ExternalRefs));
// Drawn: Tool Sets left of the drawing, Layers above Properties right of it, Layers at its
// own height and Properties taking the rest of the column.
let h = harness(None, 1280.0, 1.0);
let drawing = h.state().canvas.rect.expect("the drawing view is drawn");
let tools = hit(&h, Hit::Tab(Panel::ToolSets));
let layers = hit(&h, Hit::Tab(Panel::Layers));
let properties = hit(&h, Hit::Tab(Panel::Properties));
assert!(tools.right() <= drawing.left() + 1.0, "{tools:?} {drawing:?}");
assert!(layers.left() >= drawing.right() - 1.0 && properties.left() >= drawing.right() - 1.0, "{layers:?} {properties:?} {drawing:?}");
let seam = hit(&h, Hit::Divider(Divider::Group(Panel::Layers)));
assert!((seam.center().y - layers.bottom() - 280.0).abs() < 8.0, "{layers:?} {seam:?}");
assert!(properties.top() > seam.top());
}
#[test]
fn palette_icons_are_painted_in_code_and_shown_in_an_iconic_column() {
use cadcraft_ui_egui::docking::Panels;
let mut app = CadApp::new(Session::default(), Services::default());
let ctx = egui::Context::default();
let mut paint = |panel: Panel| {
let mut painted = false;
let mut content = Panels { app: &mut app };
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
let painter = ui.ctx().layer_painter(egui::LayerId::background());
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(20.0, 20.0));
painted = craft_ui::docking::DockContent::paint_icon(&mut content, &painter, rect, &panel, egui::Color32::RED);
});
output.textures_delta.clear();
(painted, output.shapes.len())
};
for panel in [Panel::ToolSets, Panel::QuickCalc, Panel::Layers, Panel::Properties, Panel::ExternalRefs] {
let (painted, shapes) = paint(panel);
assert!(painted && shapes > 0, "{panel:?}: {painted} {shapes}");
}
assert_eq!(paint(Panel::Canvas), (false, 0));
let mut h = harness(None, 1280.0, 1.0);
h.state_mut().run("ui.dock", json!({"action":{"SetIconic":{"panel":"layers","iconic":true}}})).unwrap();
h.run_steps(4);
let drawing = h.state().canvas.rect.unwrap();
let layers = hit(&h, Hit::Icon(Panel::Layers));
let properties = hit(&h, Hit::Icon(Panel::Properties));
assert!(layers.left() >= drawing.right() - 1.0 && layers.top() < properties.top(), "{layers:?} {properties:?}");
}
#[test]
fn real_panel_drag_float_close_reopen_and_full_workspace_reload() {
let mut harness = harness(None, 1280.0, 1.0);
@@ -199,11 +94,9 @@ fn invalid_requests_leave_document_and_panel_layout_unchanged() {
json!({"operation":"move", "panel":"layers", "target":"properties", "zone":"right", "before":null}),
json!({"operation":"spin", "panel":"layers"}),
json!({"action":{"Float":{"panel":"canvas","rect":[0,0,100,100]}}}),
json!({"action":{"MovePanel":{"panel":"canvas","to":{"NewDock":{"side":"Start"}}}}}),
json!({"action":{"Move":{"panel":"layers","anchor":"canvas","placement":{"Tab":{"before":null}}}}}),
json!(null),
] {
assert!(harness.state_mut().run("ui.dock", params.clone()).is_err(), "{params}");
assert!(harness.state_mut().run("ui.dock", params).is_err());
assert_eq!(serde_json::to_value(&harness.state().ui.docking).unwrap(), before);
assert_eq!(harness.state().session.doc().unwrap(), &drawing);
}
@@ -238,39 +131,48 @@ fn docking_renders_default_float_and_redocked_at_normal_and_narrow_sizes() {
}
#[test]
fn automation_reaches_column_and_group_resizes_tab_reorder_and_minimize_through_the_same_handler() {
use craft_ui::docking::Action;
fn automation_reaches_split_stack_resize_and_tab_reorder_through_the_same_handler() {
use craft_ui::{docking::Action, layout::SplitSize};
let mut h = harness(None, 1280.0, 1.0);
let act = |h: &mut Harness<'_, CadApp>, action: Action<Panel>| {
h.state_mut().run("ui.dock", json!({"action": serde_json::to_value(action).unwrap()})).unwrap();
};
act(&mut h, Action::ResizeColumn { panel: Panel::ToolSets, width: 280.0 });
act(&mut h, Action::ResizeGroup { panel: Panel::Layers, height: Some(160.0) });
let layout = &h.state().ui.docking.layout;
assert_eq!(columns(layout, Side::Start)[0].width, 280.0);
assert_eq!(columns(layout, Side::End)[0].groups[0].height, Some(160.0));
h.state_mut()
.run(
"ui.dock",
json!({"action":serde_json::to_value(Action::<Panel>::ResizeSplit { path: vec![], size: SplitSize::FixedFirst(280.0) }).unwrap()}),
)
.unwrap();
h.state_mut().run("ui.dock", json!({"operation":"setStackOpen","panel":"layers","open":false})).unwrap();
h.state_mut().run("ui.dock", json!({"operation":"resizeStack","panel":"layers","height":160.0})).unwrap();
assert_eq!(h.state().ui.docking.layout.location(&Panel::Layers).unwrap().stack, Some((false, Some(160.0))));
h.state_mut().run("ui.dock", json!({"operation":"move","panel":"layers","target":"properties","zone":"center","before":null})).unwrap();
assert_eq!(dock_panels(&h.state().ui.docking.layout, Side::End), [[[Panel::Properties, Panel::Layers]]]);
act(&mut h, Action::SetMinimized { panel: Panel::Layers, minimized: true });
h.run_steps(4);
assert!(columns(&h.state().ui.docking.layout, Side::End)[0].groups[0].minimized);
assert_eq!(columns(&h.state().ui.docking.layout, Side::Start)[0].width, 280.0);
let panels = h.state().ui.docking.layout.panels();
assert!(panels.iter().position(|p| **p == Panel::Properties).unwrap() < panels.iter().position(|p| **p == Panel::Layers).unwrap());
assert_eq!(h.state().ui.docking.layout.location(&Panel::ToolSets).unwrap().size, Some(SplitSize::FixedFirst(280.0)));
}
#[test]
fn group_divider_resizes_by_pointer_and_keyboard_without_changing_the_drawing() {
fn stack_boundary_resizes_by_pointer_and_keyboard_without_changing_the_drawing() {
let mut h = harness(None, 1280.0, 1.0);
let drawing = h.state().session.doc().unwrap().clone();
let layers_height = |h: &Harness<'_, CadApp>| columns(&h.state().ui.docking.layout, Side::End)[0].groups[0].height;
let center = hit(&h, Hit::Divider(Divider::Group(Panel::Layers))).center();
let center = h
.get_all_by_role_and_label(egui::accesskit::Role::Splitter, "Resize panels")
.into_iter()
.find(|node| node.rect().width() > node.rect().height())
.unwrap()
.rect()
.center();
drag(&mut h, center, center + vec2(0.0, -45.0));
let height = layers_height(&h).expect("the drag sets the Layers group's height");
h.ctx.memory_mut(|memory| memory.request_focus(DockArea::<Panel>::hit_id(area(), &Hit::Divider(Divider::Group(Panel::Layers)))));
let height = h.state().ui.docking.layout.location(&Panel::Layers).unwrap().stack.unwrap().1.unwrap();
h.get_all_by_role_and_label(egui::accesskit::Role::Splitter, "Resize panels")
.into_iter()
.find(|node| node.rect().width() > node.rect().height())
.unwrap()
.focus();
h.run_steps(2);
h.key_press(egui::Key::ArrowDown);
h.run_steps(4);
let after = layers_height(&h).unwrap();
assert!(after > height, "keyboard resize must reach the same panel command handler: {height} {after}");
let after = h.state().ui.docking.layout.location(&Panel::Layers).unwrap().stack.unwrap().1.unwrap();
assert!(after > height, "keyboard resize must reach the same panel command handler");
assert_eq!(h.state().session.doc().unwrap(), &drawing);
}
@@ -317,67 +219,6 @@ fn palettes_off_and_on_reopen_a_floating_group_as_one_window() {
assert!(floating[0].contains(&Panel::Layers) && floating[0].contains(&Panel::Properties), "{floating:?}");
}
#[test]
fn palettes_off_and_on_restore_the_right_dock_column() {
let mut h = harness(None, 1280.0, 1.0);
let before = h.state().ui.docking.layout.clone();
h.state_mut().run("ui.toggle.palettes", json!({"on": false})).unwrap();
h.run_steps(3);
assert!(!h.state().ui.docking.layout.contains(&Panel::Layers) && !h.state().ui.docking.layout.contains(&Panel::Properties));
h.state_mut().run("ui.toggle.palettes", json!({"on": true})).unwrap();
h.run_steps(3);
let layout = &h.state().ui.docking.layout;
assert_eq!(dock_panels(layout, Side::End), dock_panels(&before, Side::End));
assert_eq!(columns(layout, Side::End)[0].width, columns(&before, Side::End)[0].width);
}
#[test]
fn window_menu_reopens_closed_and_new_palettes_in_their_dock_groups() {
use cadcraft_ui_egui::docking::{PALETTES_WIDTH, TOOL_SETS_WIDTH};
let mut h = harness(None, 1280.0, 1.0);
// A closed palette comes back where it was.
h.state_mut().run("ui.dock", json!({"operation":"close","panel":"layers"})).unwrap();
h.run_steps(3);
menus::activate(h.state_mut(), "ui.panel.layers");
h.run_steps(3);
assert_eq!(dock_panels(&h.state().ui.docking.layout, Side::End), [vec![vec![Panel::Layers], vec![Panel::Properties]]]);
// Tool Sets turned off and on in the Window menu comes back in the left dock at its width.
menus::activate(h.state_mut(), "ui.toggle.toolsets");
h.run_steps(3);
assert!(!h.state().ui.docking.layout.contains(&Panel::ToolSets));
menus::activate(h.state_mut(), "ui.toggle.toolsets");
h.run_steps(3);
let layout = &h.state().ui.docking.layout;
assert_eq!(dock_panels(layout, Side::Start), [[[Panel::ToolSets]]]);
assert_eq!(columns(layout, Side::Start)[0].width, TOOL_SETS_WIDTH);
// After Reset nothing has a recorded place: External References opens below Properties.
h.state_mut().run("ui.dock.reset", json!({})).unwrap();
h.state_mut().run("xref", serde_json::Value::Null).unwrap();
h.run_steps(3);
assert_eq!(dock_panels(&h.state().ui.docking.layout, Side::End), [vec![vec![Panel::Layers], vec![Panel::Properties], vec![Panel::ExternalRefs]]]);
// Tool Sets with no recorded place and no left dock makes one at its width.
let mut workspace = cadcraft_ui_egui::docking::default_workspace();
workspace.apply(craft_ui::docking::Action::Close { panel: Panel::ToolSets }, cadcraft_ui_egui::docking::permissions).unwrap();
workspace.hidden.clear();
h.state_mut().ui.docking = workspace;
h.state_mut().ui.show_toolsets = false;
menus::activate(h.state_mut(), "ui.toggle.toolsets");
h.run_steps(3);
let layout = &h.state().ui.docking.layout;
assert_eq!(dock_panels(layout, Side::Start), [[[Panel::ToolSets]]]);
assert_eq!(columns(layout, Side::Start)[0].width, TOOL_SETS_WIDTH);
// With the palettes off, External References makes a new right dock at the palettes' width.
h.state_mut().run("ui.dock.reset", json!({})).unwrap();
h.state_mut().run("ui.toggle.palettes", json!({"on": false})).unwrap();
h.run_steps(3);
h.state_mut().run("xref", serde_json::Value::Null).unwrap();
h.run_steps(3);
let layout = &h.state().ui.docking.layout;
assert_eq!(dock_panels(layout, Side::End), [[[Panel::ExternalRefs]]]);
assert_eq!(columns(layout, Side::End)[0].width, PALETTES_WIDTH);
assert_eq!(frames(layout), [[Panel::Canvas]]);
}
#[test]
fn explicit_properties_open_restores_closed_panel_and_activates_inactive_tab() {
let mut harness = harness(None, 1000.0, 1.0);
@@ -435,8 +276,15 @@ fn properties_typed_in_rendered_command_line_reopens_and_selects_actual_workspac
let drawing = h.state().session.doc().unwrap().clone();
let tools = h.state().ui.docking.layout.location(&Panel::ToolSets).unwrap();
type_command(&mut h, "PROPERTIES");
assert_eq!(dock_panels(&h.state().ui.docking.layout, Side::End), [vec![vec![Panel::Layers], vec![Panel::Properties]]]);
assert_eq!(h.state().ui.docking.layout.location(&Panel::ToolSets).unwrap(), tools);
assert!(h.state().ui.docking.layout.contains(&Panel::Properties));
assert!(h.state().ui.docking.layout.contains(&Panel::Layers));
let restored_tools = h.state().ui.docking.layout.location(&Panel::ToolSets).unwrap();
assert_eq!(restored_tools.anchor, tools.anchor);
assert_eq!(restored_tools.placement, tools.placement);
assert_eq!(restored_tools.size, tools.size);
assert_eq!(restored_tools.floating, tools.floating);
assert_eq!(restored_tools.stack, tools.stack);
assert!(restored_tools.sibling.contains(&Panel::Properties));
h.state_mut().run("ui.dock", json!({"operation":"float","panel":"layers","rect":[400,250,300,350]})).unwrap();
h.state_mut().run("ui.dock", json!({"operation":"move","panel":"properties","target":"layers","zone":"center"})).unwrap();
@@ -462,27 +310,34 @@ fn reset_typed_in_canvas_preserves_authoritative_workspace_replacement() {
}
#[test]
fn canvas_reset_during_column_drag_discards_old_resize_and_ends_gesture() {
fn canvas_reset_during_splitter_drag_discards_old_resize_and_ends_gesture() {
use craft_ui::docking::Action;
use craft_ui::layout::SplitSize;
let mut h = harness(None, 1280.0, 1.0);
// Give the Tool Sets column a width different from Reset's.
// Give the old root divider a size different from Reset's 220px ToolSets pane.
h.state_mut()
.run("ui.dock", json!({"action": serde_json::to_value(Action::ResizeColumn { panel: Panel::ToolSets, width: 280.0 }).unwrap()}))
.run(
"ui.dock",
json!({"action":Action::<Panel>::ResizeSplit {
path: Vec::new(), size: SplitSize::FixedFirst(280.0),
}}),
)
.unwrap();
h.run_steps(3);
let drawing = h.state().session.doc().unwrap().clone();
let divider = Hit::Divider(Divider::Column(Panel::ToolSets));
let handle = DockArea::<Panel>::hit_id(area(), &divider);
let from = hit(&h, divider).center();
let splitter = egui::Id::unique("cadcraft-docking").with(("split", Vec::<bool>::new()));
let from = h.ctx.read_response(splitter).expect("rendered root divider").rect.center();
h.event(Event::PointerMoved(from));
h.run_steps(1);
h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
h.run_steps(1);
assert_eq!(h.ctx.dragged_id(), Some(handle), "the column's divider owns the pointer");
// Canvas is visited before the ancestor ResizeHandle. The command runs before
// that ancestor queues this frame's ResizeSplit against the old immutable tree.
assert_eq!(h.ctx.dragged_id(), Some(splitter), "actual divider owns the pointer");
assert!(h.ctx.input(|input| input.pointer.primary_down()));
// A divider takes keyboard focus on press. Release only that keyboard focus so the real
// Canvas command line can submit while the pointer remains captured.
h.ctx.memory_mut(|memory| memory.surrender_focus(handle));
// A ResizeHandle takes keyboard focus on press. Release only that keyboard focus
// so the real Canvas command line can submit while the pointer remains captured.
h.ctx.memory_mut(|memory| memory.surrender_focus(splitter));
assert!(!h.ctx.egui_wants_keyboard_input());
let to = from + vec2(36.0, 0.0);
h.event(Event::PointerMoved(to));
@@ -491,10 +346,10 @@ fn canvas_reset_during_column_drag_discards_old_resize_and_ends_gesture() {
h.run_steps(1);
let expected = cadcraft_ui_egui::docking::default_workspace();
assert!(h.state().cmd.buffer.is_empty(), "Canvas submitted the actual reset command");
assert_eq!(h.state().ui.docking.layout, expected.layout, "old-frame resize must not modify Reset's layout");
assert_eq!(h.state().ui.docking.layout, expected.layout, "old-frame resize must not modify Reset's tree");
assert_eq!(h.state().ui.docking.hidden, expected.hidden);
// Keep the physical button down: replacement must end the old divider gesture rather than
// letting it go on resizing the new workspace.
// Keep the physical button down: replacement must terminate the old divider gesture,
// rather than letting its stable root ID continue resizing the new workspace.
let later = to + vec2(24.0, 0.0);
h.event(Event::PointerMoved(later));
h.run_steps(2);
@@ -531,65 +386,35 @@ fn collapsed_tool_sets_leave_a_visible_control_that_expands_them() {
}
#[test]
fn palettes_dock_only_in_the_left_and_right_columns() {
let mut h = harness(None, 1280.0, 1.0);
let document = h.state().session.doc().unwrap().clone();
let drawing = h.state().canvas.rect.expect("the drawing view is drawn");
// Neither the drawing nor the bottom edge takes a drop: the palette floats there.
for to in [drawing.center(), egui::pos2(drawing.center().x, drawing.bottom() - 4.0)] {
h.state_mut().run("ui.dock.reset", json!({})).unwrap();
h.run_steps(3);
let from = hit(&h, Hit::Tab(Panel::Layers)).center();
drag(&mut h, from, to);
let layout = &h.state().ui.docking.layout;
assert_eq!(frames(layout), [[Panel::Canvas]], "{to:?}: {:?}", layout.root);
assert_eq!(dock_panels(layout, Side::End), [[[Panel::Properties]]], "{to:?}");
assert!(layout.floating.iter().any(|window| window.contains(&Panel::Layers)), "{to:?}: {layout:?}");
}
// The left window edge, where Tool Sets docks, takes the palette as a new outer column.
h.state_mut().run("ui.dock.reset", json!({})).unwrap();
h.run_steps(3);
let column = hit(&h, Hit::ColumnBar(Panel::ToolSets));
let from = hit(&h, Hit::Tab(Panel::Layers)).center();
drag(&mut h, from, egui::pos2(column.left() + 3.0, drawing.center().y));
let layout = &h.state().ui.docking.layout;
assert_eq!(dock_panels(layout, Side::Start), [[[Panel::Layers]], [[Panel::ToolSets]]], "{:?}", layout.root);
assert_eq!(frames(layout), [[Panel::Canvas]]);
// Without a left dock, the left window edge makes one.
h.state_mut().run("ui.dock.reset", json!({})).unwrap();
h.state_mut().run("ui.toggle.toolsets", json!({"on": false})).unwrap();
h.run_steps(4);
assert!(columns(&h.state().ui.docking.layout, Side::Start).iter().all(|column| column.groups.is_empty()));
let drawing = h.state().canvas.rect.unwrap();
let from = hit(&h, Hit::Tab(Panel::Layers)).center();
drag(&mut h, from, egui::pos2(drawing.left() + 3.0, drawing.center().y));
let layout = &h.state().ui.docking.layout;
assert_eq!(dock_panels(layout, Side::Start), [[[Panel::Layers]]], "{:?}", layout.root);
assert_eq!(frames(layout), [[Panel::Canvas]]);
assert_eq!(h.state().session.doc().unwrap(), &document);
}
#[test]
fn group_drag_bar_moves_the_whole_group_into_the_other_dock() {
fn group_drag_bar_moves_the_whole_group_beside_the_drawing() {
use craft_ui::docking::{DockArea, Hit, Node};
let mut h = harness(None, 1280.0, 1.0);
h.state_mut().run("ui.dock", json!({"operation":"move", "panel":"layers", "target":"toolSets", "zone":"center", "before":null})).unwrap();
h.run_steps(4);
let bar = DockArea::<Panel>::hit_rects(&h.ctx, area())
.into_iter()
.find(|(hit, _)| matches!(hit, Hit::DragBar(panel) if *panel == Panel::ToolSets || *panel == Panel::Layers))
.map(|(_, rect)| rect)
.expect("the Tool Sets group draws a drag bar");
// Into the middle of the Properties group, as tabs.
let properties = hit(&h, Hit::Tab(Panel::Properties));
let column = hit(&h, Hit::ColumnBar(Panel::Properties));
let to = egui::pos2(column.center().x, properties.bottom() + 120.0);
let bar = |h: &Harness<'_, CadApp>| {
DockArea::<Panel>::hit_rects(&h.ctx, egui::Id::unique("cadcraft-docking"))
.into_iter()
.find(|(hit, _)| matches!(hit, Hit::DragBar(panel) if *panel == Panel::ToolSets || *panel == Panel::Layers))
.map(|(_, rect)| rect)
};
let from = bar(&h).expect("the Tool Sets group draws a drag bar").center();
let drawing = h.state().canvas.rect.expect("the drawing view is drawn");
let to = egui::pos2(drawing.right() - 10.0, drawing.center().y);
let document = h.state().session.doc().unwrap().clone();
drag(&mut h, bar.center(), to);
drag(&mut h, from, to);
let layout = &h.state().ui.docking.layout;
assert!(layout.floating.is_empty(), "{layout:?}");
let end = dock_panels(layout, Side::End);
assert_eq!(end.len(), 1, "{end:?}");
assert!(end[0].iter().any(|group| group.contains(&Panel::ToolSets) && group.contains(&Panel::Layers)), "{end:?}");
assert_eq!(frames(layout), [[Panel::Canvas]]);
assert!(layout.floating.is_empty(), "a drop beside the drawing docks the group");
// The drawing's frame splits: the drawing on the left, the whole group on the right.
let mut nodes: Vec<&Node<Panel>> = layout.root.iter().collect();
let mut beside = false;
while let Some(node) = nodes.pop() {
if let Node::Split { first, second, .. } = node {
beside |= matches!((&**first, &**second), (Node::Tabs { panels: left, .. }, Node::Tabs { panels: right, .. })
if left == &[Panel::Canvas] && right == &[Panel::ToolSets, Panel::Layers]);
nodes.push(first);
nodes.push(second);
}
}
assert!(beside, "the group moves as one to the drawing's right: {:?}", layout.root);
assert_eq!(h.state().session.doc().unwrap(), &document);
}
-2
View File
@@ -17,8 +17,6 @@ mod lisp_files;
mod measure_distance_snapping;
mod menubar_hover;
mod mspace_viewport_view;
mod native_menu_text_keys;
mod native_windows;
mod options_dialog;
mod parameters_rename;
mod pickdrag;
@@ -1,65 +0,0 @@
//! The macOS menu bar takes the Edit menu's key equivalents before the window does: with a text
//! field focused they edit the field, and only without one do they run the drawing command.
use cadcraft_engine::Session;
use cadcraft_ui_egui::{CadApp, Services, menus};
use egui::{accesskit::Role, vec2};
use egui_kittest::Harness;
use egui_kittest::kittest::{NodeT, Queryable};
use serde_json::json;
fn harness() -> Harness<'static, CadApp> {
let mut app = CadApp::new(Session::default(), Services::default());
app.ui.start_tab = false;
app.ui.in_window_menu = false;
let mut h = Harness::builder().with_size(vec2(1280.0, 800.0)).build_ui_state(
|ui, app: &mut CadApp| {
app.logic(&ui.ctx().clone());
app.ui(ui);
},
app,
);
h.ctx.set_os(egui::os::OperatingSystem::Mac);
h.run_steps(5);
h
}
/// A native menu choice, then the frames that deliver what it queued for the main window.
fn menu(h: &mut Harness<'_, CadApp>, id: &str) {
let ctx = h.ctx.clone();
menus::activate_native(h.state_mut(), &ctx, id);
let mut raw = egui::RawInput::default();
h.state_mut().raw_input_hook(&ctx, &mut raw);
h.input_mut().events.extend(raw.events);
h.run_steps(3);
}
fn expression<'a>(h: &'a Harness<'_, CadApp>) -> egui_kittest::Node<'a> {
h.get_by_role_and_label(Role::Tab, "QuickCalc").parent().unwrap().get_all_by_role(Role::TextInput).next().unwrap()
}
#[test]
fn edit_menu_keys_edit_the_focused_text_field_and_otherwise_the_drawing() {
let mut h = harness();
h.state_mut().run("line", json!({"points": [[0, 0], [10, 0]]})).unwrap();
let drawing = h.state().session.doc().unwrap().clone();
menus::activate(h.state_mut(), "ui.toggle.quickcalc");
h.run_steps(5);
expression(&h).click();
h.run_steps(3);
expression(&h).type_text("2+3");
h.run_steps(3);
assert_eq!(menus::text_field_window(&h.ctx), Some(egui::ViewportId::ROOT));
assert!(menus::edits_text("pasteclip") && !menus::edits_text("line"));
menu(&mut h, "selectall");
menu(&mut h, "cutclip");
assert_eq!(expression(&h).value().as_deref(), Some(""), "Select All and Cut act on the field");
assert!(h.state().session.running.is_none());
assert_eq!(h.state().session.doc().unwrap(), &drawing);
h.ctx.memory_mut(|m| m.stop_text_input());
h.run_steps(3);
assert_eq!(menus::text_field_window(&h.ctx), None);
menu(&mut h, "u");
assert_ne!(h.state().session.doc().unwrap(), &drawing, "without a focused field, Undo undoes the line");
}
-116
View File
@@ -1,116 +0,0 @@
//! Floating panels in their own OS windows: a modal dialog blocks their input, and keys typed in
//! one run the main window's shortcuts and command line.
use cadcraft_engine::Session;
use cadcraft_engine::autosave::Recoverable;
use cadcraft_ui_egui::closing::Choice;
use cadcraft_ui_egui::docking::native_input_blocked;
use cadcraft_ui_egui::{CadApp, Services};
use egui::{Event, Key, Modifiers};
use serde_json::json;
fn frame(app: &mut CadApp, ctx: &egui::Context) {
frame_with(app, ctx, Vec::new());
}
/// A frame whose main-window input carries `events`.
fn frame_with(app: &mut CadApp, ctx: &egui::Context, events: Vec<Event>) {
let mut raw =
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1280.0, 800.0))), events, ..Default::default() };
app.raw_input_hook(ctx, &mut raw);
cadcraft_ui_egui::cmdline::capture_tab(ctx, &mut raw);
let mut out = ctx.run_ui(raw, |ui| {
app.logic(ui.ctx());
app.ui(ui);
});
out.textures_delta.clear();
}
fn pump(app: &mut CadApp, ctx: &egui::Context) {
for _ in 0..3 {
frame(app, ctx);
}
}
fn key(key: Key, pressed: bool, modifiers: Modifiers) -> Event {
Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }
}
fn running(app: &CadApp) -> Option<&str> {
app.session.running.as_ref().map(|command| command.id.as_str())
}
#[test]
fn modal_dialogs_block_native_panel_windows() {
let ctx = egui::Context::default();
let mut app = CadApp::new(Session::new(), Services::default());
pump(&mut app, &ctx);
assert!(!native_input_blocked(&app, &ctx));
// "Save changes?" while a dirty drawing closes.
app.run("circle", json!({"center": [0, 0], "radius": 1})).unwrap();
app.request_close(None);
assert!(native_input_blocked(&app, &ctx), "close prompt");
pump(&mut app, &ctx);
assert!(native_input_blocked(&app, &ctx), "close prompt shown");
app.answer_close(Choice::Cancel);
pump(&mut app, &ctx);
assert!(!native_input_blocked(&app, &ctx), "after Cancel");
// Drawing Recovery.
app.recovery.items.push(Recoverable { path: "missing.sv$".into(), drawing: "Plan.dxf".into(), modified: 0, bytes: 0 });
assert!(native_input_blocked(&app, &ctx), "drawing recovery");
app.recovery.items.clear();
pump(&mut app, &ctx);
assert!(!native_input_blocked(&app, &ctx));
// Any other egui modal, from the frame after it is shown.
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
egui::Modal::new(egui::Id::unique("test_modal")).show(ui.ctx(), |ui| ui.label("modal"));
});
out.textures_delta.clear();
assert!(native_input_blocked(&app, &ctx), "egui modal");
}
#[test]
fn keys_forwarded_from_a_native_window_reach_the_command_line_and_shortcuts() {
let ctx = egui::Context::default();
let mut app = CadApp::new(Session::new(), Services::default());
pump(&mut app, &ctx);
let typed = vec![Event::Text("LINE".into()), key(Key::Enter, true, Modifiers::NONE), key(Key::Enter, false, Modifiers::NONE)];
app.forward_native_input(&ctx, typed, Some(Modifiers::NONE));
pump(&mut app, &ctx);
assert_eq!(running(&app), Some("line"));
assert_eq!(app.cmd.history, ["LINE"]);
let escape = vec![key(Key::Escape, true, Modifiers::NONE), key(Key::Escape, false, Modifiers::NONE)];
app.forward_native_input(&ctx, escape, Some(Modifiers::NONE));
pump(&mut app, &ctx);
assert_eq!(running(&app), None);
let ortho = app.session.settings.orthomode;
app.forward_native_input(&ctx, vec![key(Key::F8, true, Modifiers::NONE), key(Key::F8, false, Modifiers::NONE)], Some(Modifiers::NONE));
pump(&mut app, &ctx);
assert_eq!(app.session.settings.orthomode, !ortho, "F8");
// Shift held in the native window: Cmd/Ctrl+Shift+C arrives as Copy and runs COPYBASE. The
// main window's own modifiers come back afterwards.
app.forward_native_input(&ctx, vec![Event::Copy], Some(Modifiers::SHIFT | Modifiers::COMMAND));
pump(&mut app, &ctx);
assert_eq!(running(&app), Some("copybase"));
assert_eq!(ctx.input(|i| i.modifiers), Modifiers::NONE);
}
#[test]
fn a_main_window_modifier_change_outlasts_keys_forwarded_meanwhile() {
let ctx = egui::Context::default();
let mut app = CadApp::new(Session::new(), Services::default());
pump(&mut app, &ctx);
frame_with(&mut app, &ctx, vec![Event::ModifiersChanged(Modifiers::SHIFT)]);
app.forward_native_input(&ctx, vec![key(Key::F8, true, Modifiers::COMMAND), key(Key::F8, false, Modifiers::COMMAND)], Some(Modifiers::COMMAND));
// Shift is released in the main window in the frame the forwarded keys go in.
frame_with(&mut app, &ctx, vec![Event::ModifiersChanged(Modifiers::NONE)]);
pump(&mut app, &ctx);
assert_eq!(ctx.input(|i| i.modifiers), Modifiers::NONE, "the released Shift is not put back");
}