forked from 1b-agent/opencadstudio-upstream
feat(measure): add radius and angle tools with dynamic measurement overlay
(cherry picked from commit aac44906a3)
This commit is contained in:
1 parent
843cbcd607
commit
3c55e6d7e1
42 files changed
+1624
-53
No files matched your search
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20.5 19H3.5L15.5 5" stroke="#B4B6B9" stroke-width="1.5"/>
|
||||
<path d="M11.5 19a8 8 0 0 0-2.794-6.074" stroke="#6DB7ED" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 278 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="8.5" stroke="#B4B6B9" stroke-width="1.5"/>
|
||||
<path d="M12 12 18.01 5.99m-3.6 1.1 3.6-1.1-1.1 3.6" stroke="#6DB7ED" stroke-width="1.5"/>
|
||||
<circle cx="12" cy="12" r="1" fill="#6DB7ED"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 341 B |
@@ -0,0 +1,173 @@
|
||||
# Mérés eszköztár – fejlesztési csomag
|
||||
|
||||
Kiindulás: a feltöltött `measurement-development-full-006be890.zip`, Open CAD Studio `006be890`. A csomag kizárólag az ehhez képest új vagy módosított fájlokat tartalmazza, projektgyökérhez viszonyított útvonalakkal. Távoli tároló módosítása és Pull Request nem történt.
|
||||
|
||||
## Változások
|
||||
|
||||
- A meglévő `MEASURE_MENU` / `LargeDropdown` sorrendje: **Distance (DIST), Radius (MEASURERADIUS), Angle (MEASUREANGLE), Area (AREA)**. A komponens felépítése változatlan. A Distance és Area eredeti SVG-je változatlan.
|
||||
- A `MEASURERADIUS` és `MEASUREANGLE` a meglévő `MeasureGeomCommand` külön kezdőállapotai. Közös kijelölési és számítási kódot használnak; a parancsregisztráció, menüindítás és parancssori indítás össze van kötve. A `MEASUREGEOM` és `MEA` továbbra is elérhető négy mérési móddal.
|
||||
- Sugármérés: kör és körív, sugár és átmérő; más típus és hibás sugár esetén hibaüzenet, a parancs továbbra is vár érvényes kijelölést. Az objektum feletti kurzormozgás előzetes értéket mutat.
|
||||
- Szögmérés: csúcs, első szár pontja, második szár pontja. Az első lépésben `Lines` / `L` két LINE objektum mérésére vált; onnan az első kijelölés előtt `Points` / `P` visszavált. Azonos objektum, nulla hosszúságú szár, nem LINE objektum és nem véges koordináta kezelve van.
|
||||
- A szögszámítás `f64` vektorokat használ, skálázott normalizálással és `atan2`-vel. A végeredmény fokban jelenik meg, az `AUPREC` pontosságával. A hossz- és területadatok a meglévő formázókat használják.
|
||||
- Közös, dokumentumfülenkénti, átmeneti mérési felirat. A parancsok csak az élő eredményszöveget szolgáltatják; a pozicionálás, végleges eredmény megőrzése és eltüntetése közös. A végleges kiírás a befejezés helyén marad, egérmozgatásra nem követi a kurzort.
|
||||
- A rajzterületi bal, jobb vagy középső gomb lenyomása eltünteti a végleges feliratot, majd a normál eseménykezelés folytatódik. A befejező kattintás felengedése nem törli az éppen elkészült eredményt. ESC és új parancs indítása is töröl.
|
||||
- A felirat nem vesz át egéreseményt, a rajzterület széléhez és a parancssorhoz igazodik, és a téma színeit használja. Az élő megjelenítés a DYN kapcsolótól független, alaphelyzetben a kurzor fölött van. A meglévő geometriai előnézet és tárgyraszter megmarad.
|
||||
- Az AREA objektumkijelölési előnézete kijelölésváltozáskor számol; az összetett görbék területét nem integrálja újra minden képkockában. Add/Subtract közben az aktuális terület és az előzetes összeg látható, az összeg csak a mérés elfogadásakor változik.
|
||||
- A felirat nem rajzi entitás, nem módosítja a DWG tartalmát vagy a dokumentum módosításjelzőjét. A meglévő parancssori eredmény megmarad.
|
||||
- Új sugár- és szög-SVG, magyar és angol szövegek. A többi 20 nyelvi katalógus új kulcsai angol szöveget kaptak, hogy a katalógusok kulcskészlete egyezzen.
|
||||
- Nincs új külső függőség; a `Cargo.toml` és `Cargo.lock` változatlan.
|
||||
|
||||
## Beillesztés PowerShellből
|
||||
|
||||
Zárd be a futó CAD-alkalmazást. A következő parancsok tiszta Git-munkafát és pontosan a kiinduló commitot követelik meg; eltérés esetén megállnak. Saját módosításaidat előbb mentsd el vagy commitold. A ZIP-et nem egy új almappába, hanem közvetlenül a projekt gyökerébe kell kicsomagolni.
|
||||
|
||||
```powershell
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$project = 'C:\CAD-Development\OpenCADStudio-Dev'
|
||||
$patch = Join-Path $env:USERPROFILE 'Downloads\measurement-tools-patch.zip'
|
||||
Set-Location -LiteralPath $project
|
||||
if (-not (Test-Path -LiteralPath $patch -PathType Leaf)) { throw 'A ZIP nem található.' }
|
||||
$status = git status --porcelain
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Nem elérhető Git-munkafa.' }
|
||||
if ($status) { throw 'A munkafa nem tiszta. Előbb mentsd a saját módosításaidat.' }
|
||||
$baseCommit = git rev-parse '006be890^{commit}'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'A kiinduló commit nem található.' }
|
||||
$currentCommit = git rev-parse HEAD
|
||||
if ($LASTEXITCODE -ne 0 -or $currentCommit -ne $baseCommit) { throw 'Nem a 006be890 commiton állsz.' }
|
||||
$currentBranch = git branch --show-current
|
||||
if ($currentBranch -ne 'feature/measurement-tools') {
|
||||
git switch -c feature/measurement-tools
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Az ág nem hozható létre. Ellenőrizd a már létező ágakat.' }
|
||||
}
|
||||
Expand-Archive -LiteralPath $patch -DestinationPath $project -Force
|
||||
git status --short
|
||||
git diff --stat
|
||||
git diff --check
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Whitespace-ellenőrzési hiba.' }
|
||||
```
|
||||
|
||||
Az új fájlokat a `git status --short` mutatja; a `git diff --stat` csak a már követett fájlokat sorolja fel. A fenti művelet nem commitol és nem pushol.
|
||||
|
||||
## Fordítás és célzott tesztek PowerShellből
|
||||
|
||||
A projekt meglévő Windows/Rust/MSVC előfeltételei és a függőségek letöltéséhez internetelérés szükséges.
|
||||
|
||||
```powershell
|
||||
Set-Location 'C:\CAD-Development\OpenCADStudio-Dev'
|
||||
cargo check --locked
|
||||
if ($LASTEXITCODE -ne 0) { throw 'A cargo check sikertelen.' }
|
||||
cargo test --locked --lib measurement -- --test-threads=1
|
||||
if ($LASTEXITCODE -ne 0) { throw 'A mérési tesztek sikertelenek.' }
|
||||
cargo test --locked --lib modules::draw::inquiry -- --test-threads=1
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Az inquiry tesztek sikertelenek.' }
|
||||
cargo fmt --check
|
||||
# A kiinduló projektben is vannak formázási eltérések; lásd az eredményeket.
|
||||
cargo run --locked
|
||||
```
|
||||
|
||||
A `--test-threads=1` a formázási beállításokat módosító tesztek és az alkalmazásállapot-tesztek kiszámítható futását segíti. Memóriahiány esetén a tesztek előtt megadható `$env:CARGO_BUILD_JOBS = '3'` és `$env:CARGO_PROFILE_TEST_DEBUG = '0'`. Ez újraépítést okozhat; a változókat a vizsgálat után `Remove-Item Env:CARGO_BUILD_JOBS, Env:CARGO_PROFILE_TEST_DEBUG -ErrorAction SilentlyContinue` paranccsal törölheted.
|
||||
|
||||
## Kézi ellenőrzés Windows alatt
|
||||
|
||||
1. A Mérés menüben ellenőrizd a négy elemet és azok sorrendjét; indítsd őket menüből és begépelt paranccsal is. Ellenőrizd külön a `MEASUREGEOM` négy opcióját és a `MEA` aliast.
|
||||
2. DIST: mérj a `(0,0,0)` és `(3,4,0)` pontok között: 5 egység. Az első pont után mozgasd a kurzort; a felirat és a geometriai előnézet frissüljön. Próbáld ki tárgyraszterrel, DYN be- és kikapcsolva, más hosszformátummal/pontossággal, valamint nagy koordináták közelében.
|
||||
3. MEASURERADIUS: mérj 10 sugarú körön és köríven: sugár 10, átmérő 20. Előbb csak vidd föléjük a kurzort, aztán kattints. Egyenes kijelölésekor hibaüzenetet várj, utána körrel lehessen befejezni. Üres terület fölött ne maradjon az előző objektum élő értéke.
|
||||
4. MEASUREANGLE: csúcs `(0,0,0)`, első szár `(1,0,0)`, második `(0,1,0)` → 90°; második `(-1,1,0)` → 135°. A csúcs ismételt kiválasztása érvénytelen szár, és nem fejezheti be a mérést. Próbáld ki 3D pontokkal és eltérő AUPREC értékekkel is.
|
||||
5. MEASUREANGLE → Lines: két 45°-ban álló LINE → 45°, merőlegesek → 90°, párhuzamosak → 0°. Fordított végpontsorrenddel ugyanaz az eredmény várható. Ugyanazon egyenes kétszeri, kör vagy nulla hosszúságú egyenes kijelölése ne adjon hamis eredményt.
|
||||
6. AREA: `(0,0,0)`, `(4,0,0)`, `(0,3,0)`, Enter → terület 6, kerület 12. A harmadik pont mozgatásakor legyen élő érték. Ellenőrizd Objects módban a kört, zárt polylinet, és Add/Subtract módban több egymás utáni részterület összegzését.
|
||||
7. Mindegyik befejezett mérés után mozgasd az egeret: a végleges felirat maradjon a helyén. A következő rajzterületi kattintás törölje, és egyben kezdje meg a normál kijelölést / folytassa az AREA következő részterületét. Próbálj közvetlenül a felirat szövegére is kattintani. A befejező kattintás felengedése ne törölje rögtön a feliratot.
|
||||
8. ESC, új mérés, jobb és középső rajzterületi kattintás is törölje a végleges feliratot. Válts világos/sötét témát; teszteld a sarkokat és a nyitott parancssort. Válts dokumentumfület: másik rajz eredménye ne jelenjen meg az aktuális rajzon.
|
||||
9. Csak mérés után ne nőjön az entitásszám és ne váljon módosítottá a tiszta rajz. Mentés és újranyitás után ne maradjon mérési felirat a DWG-ben. A parancssori előzményben továbbra is legyen olvasható az eredmény.
|
||||
|
||||
## Ismert korlátok
|
||||
|
||||
- A két egyenes mód az irányok kisebb, 0–90° közötti szögét adja, a végpontsorrendtől függetlenül. Térben kitérő egyeneseken is az irányszög mérhető; nincs metszéspont- vagy szögtartomány-kiválasztás. A hárompontos mód 0–180°-ot ad, nem irányított/reflex szöget.
|
||||
- Az AREA és a MEASUREGEOM Area meglévő számítási jelentése megmarad: előbbi a meglévő térbeli/görbealapú mérés, utóbbi a munkasíkra vetített sokszög. Nem síkbeli pontsorokon ezért eltérhetnek.
|
||||
- A végleges felirat képernyőpozícióhoz kötött átmeneti UI, nem modellhez kötött méretfelirat. Nem nyomtatható és nem szerkeszthető rajzi objektumként.
|
||||
- A további 20 nyelv új szövegeinek anyanyelvi fordítása külön feladat.
|
||||
- A Windows alatti interaktív kézi ellenőrzést a fenti útmutató alapján a helyi gépen kell elvégezni; itt nem történt Windows GUI-próba.
|
||||
|
||||
## Automatikus tesztlefedettség
|
||||
|
||||
22 új Rust-teszt került a forrásba: kör és körív sugara, érvénytelen/stale kijelölés, ismert és szélsőséges skálájú szögek, degenerált szárak, két egyenes irányszöge, előnézet, f64/pontosság/hosszformátum, AREA élő érték és Add/Subtract, objektumkijelölési előnézet, regisztráció és menüindítás, dokumentum változatlansága, felirat élettartama, valódi viewport-kattintások továbbfeldolgozása, valamint az Iced felirat egéreseményei és szélekhez igazítása. A tesztkód a következő öt fájlban van: `measuregeom.rs`, `dist.rs`, `area.rs`, `src/app/measurement.rs`, `src/app/view/overlay.rs`.
|
||||
|
||||
## Automatikus ellenőrzési eredmények
|
||||
|
||||
Ellenőrzési környezet: Linux x86_64, Rust/Cargo 1.98.0, 2026.10.14. A fordításnál és tesztelésnél a memóriaigény csökkentésére 3 build szálat, kikapcsolt debug információt és a függőségeknél 0 optimalizációs szintet használtam. A projekt profilfájljai változatlanok.
|
||||
|
||||
| Ellenőrzés | Tényleges eredmény |
|
||||
|---|---|
|
||||
| `cargo check --locked` | Sikeres, kilépési kód 0. |
|
||||
| `cargo check --locked --tests` | Sikeres, kilépési kód 0. A tesztkód is lefordítható. |
|
||||
| `cargo test --locked --lib measurement -- --test-threads=1` | 26 sikeres, 0 hibás, 1 kihagyott. Mind a 22 új mérési teszt sikeres. |
|
||||
| `cargo test --locked --lib modules::draw::inquiry -- --test-threads=1` | 18 sikeres, 0 hibás, 0 kihagyott. |
|
||||
| `cargo test --locked --lib i18n::tests::every_catalog_covers_and_formats_the_source_catalog -- --exact --test-threads=1` | 1 sikeres, 0 hibás. A katalógusok kulcskészlete és formázása ellenőrizve. |
|
||||
| `cargo fmt --check` | Sikertelen a kiinduló és a módosított forráson is: ugyanaz a 72 fájl tartalmaz örökölt formázási eltéréseket. A feladattól független fájlokat nem formáztam át. |
|
||||
| `git diff --check` | Sikeres, nincs whitespace-hiba a patchben. |
|
||||
| `python scripts/test_locales.py` | A kiinduló projektben 135, a módosított projektben 134 hiányzó régi literál-leképezés. Új hiányzó leképezés nincs. Ez az eredeti ellenőrzőszkript mindkét állapoton hibát jelez. |
|
||||
| SVG-k | XML-ként érvényesek; az új ikonok világos és sötét háttéren megtekintve. Az eredeti Distance/Area SVG és a két Cargo-fájl byte szerint változatlan. |
|
||||
| Windows GUI / DWG-mentés kézi próbája | Ebben a környezetben nem futott; a fenti kézi ellenőrzési listával végezhető el. |
|
||||
|
||||
A fenti két tesztcsoport részben átfed, a darabszámokat nem szabad összeadni egyedi tesztszámként. A kihagyott teszt meglévő `viewport_dimension_external_fixture_keeps_unchanged_measurements`: az `OPENCAD_VIEWPORT_REGRESSION_DXF` változóban megadott külső DXF-et igényli. Az új tesztek egyike sincs kihagyva.
|
||||
|
||||
A Linux alatt használt pontos parancsok (a `+1.98.0` a telepített toolchaint választja):
|
||||
|
||||
```sh
|
||||
CARGO_BUILD_JOBS=3 CARGO_PROFILE_DEV_DEBUG=0 cargo +1.98.0 check --locked --config 'profile.dev.package."*".opt-level=0'
|
||||
CARGO_BUILD_JOBS=3 CARGO_PROFILE_DEV_DEBUG=0 cargo +1.98.0 check --locked --tests --config 'profile.dev.package."*".opt-level=0'
|
||||
CARGO_BUILD_JOBS=3 CARGO_PROFILE_TEST_DEBUG=0 cargo +1.98.0 test --locked --config 'profile.test.package."*".opt-level=0' --lib measurement -- --test-threads=1
|
||||
CARGO_BUILD_JOBS=3 CARGO_PROFILE_TEST_DEBUG=0 cargo +1.98.0 test --locked --config 'profile.test.package."*".opt-level=0' --lib modules::draw::inquiry -- --test-threads=1
|
||||
CARGO_BUILD_JOBS=3 CARGO_PROFILE_TEST_DEBUG=0 cargo +1.98.0 test --locked --config 'profile.test.package."*".opt-level=0' --lib i18n::tests::every_catalog_covers_and_formats_the_source_catalog -- --exact --test-threads=1
|
||||
cargo +1.98.0 fmt --check
|
||||
```
|
||||
|
||||
A környezetben frissen telepített Rust 1.99 már `rustc -vV` alatt SIGBUS hibával leállt; ezen a toolchainen a cargo check/test nem érte el a projekt fordítását. A fenti sikeres eredmények Rust 1.98 alatt születtek. Ebből nem következik, hogy a helyi Windows 1.99 toolchain hibás lenne. A teljes projekt összes tesztjét és release buildjét nem futtattam.
|
||||
|
||||
## A csomag összes fájlja
|
||||
|
||||
42 fájl: 4 új, 38 módosított.
|
||||
|
||||
| Állapot | Fájl |
|
||||
|---|---|
|
||||
| Új | `assets/icons/measure-angle.svg` |
|
||||
| Új | `assets/icons/measure-radius.svg` |
|
||||
| Új | `docs/measurement-tools.md` |
|
||||
| Módosított | `locales/ar-SA/opencadstudio.ftl` |
|
||||
| Módosított | `locales/bg-BG/opencadstudio.ftl` |
|
||||
| Módosított | `locales/cs-CZ/opencadstudio.ftl` |
|
||||
| Módosított | `locales/de-DE/opencadstudio.ftl` |
|
||||
| Módosított | `locales/el-GR/opencadstudio.ftl` |
|
||||
| Módosított | `locales/en-US/opencadstudio.ftl` |
|
||||
| Módosított | `locales/es-ES/opencadstudio.ftl` |
|
||||
| Módosított | `locales/fi-FI/opencadstudio.ftl` |
|
||||
| Módosított | `locales/fr-FR/opencadstudio.ftl` |
|
||||
| Módosított | `locales/hi-IN/opencadstudio.ftl` |
|
||||
| Módosított | `locales/hu-HU/opencadstudio.ftl` |
|
||||
| Módosított | `locales/it-IT/opencadstudio.ftl` |
|
||||
| Módosított | `locales/ja-JP/opencadstudio.ftl` |
|
||||
| Módosított | `locales/ko-KR/opencadstudio.ftl` |
|
||||
| Módosított | `locales/lo-LA/opencadstudio.ftl` |
|
||||
| Módosított | `locales/nl-NL/opencadstudio.ftl` |
|
||||
| Módosított | `locales/pl-PL/opencadstudio.ftl` |
|
||||
| Módosított | `locales/pt-BR/opencadstudio.ftl` |
|
||||
| Módosított | `locales/ru-RU/opencadstudio.ftl` |
|
||||
| Módosított | `locales/tr-TR/opencadstudio.ftl` |
|
||||
| Módosított | `locales/zh-CN/opencadstudio.ftl` |
|
||||
| Módosított | `locales/zh-TW/opencadstudio.ftl` |
|
||||
| Módosított | `src/app/command_driver/mod.rs` |
|
||||
| Módosított | `src/app/commands/draw.rs` |
|
||||
| Módosított | `src/app/commands/mod.rs` |
|
||||
| Módosított | `src/app/control/mod.rs` |
|
||||
| Módosított | `src/app/document.rs` |
|
||||
| Új | `src/app/measurement.rs` |
|
||||
| Módosított | `src/app/mod.rs` |
|
||||
| Módosított | `src/app/update/command.rs` |
|
||||
| Módosított | `src/app/update/mod.rs` |
|
||||
| Módosított | `src/app/update/viewport.rs` |
|
||||
| Módosított | `src/app/view/mod.rs` |
|
||||
| Módosított | `src/app/view/overlay.rs` |
|
||||
| Módosított | `src/command.rs` |
|
||||
| Módosított | `src/modules/draw/inquiry/area.rs` |
|
||||
| Módosított | `src/modules/draw/inquiry/dist.rs` |
|
||||
| Módosított | `src/modules/draw/inquiry/measuregeom.rs` |
|
||||
| Módosított | `src/modules/draw/mod.rs` |
|
||||
@@ -6582,3 +6582,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6570,3 +6570,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6567,3 +6567,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6568,3 +6568,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6570,3 +6570,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6569,3 +6569,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6570,3 +6570,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6567,3 +6567,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6570,3 +6570,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6558,3 +6558,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6554,3 +6554,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Ideiglenes geometriai mérési eredmények.
|
||||
measurement =
|
||||
.distance = Távolság = { $value }
|
||||
.angle-start = { $command } Adja meg a szög csúcspontját vagy [Vonalak]:
|
||||
.first-line = { $command } Jelölje ki az első vonalat vagy [Pontok] (kisebb szög):
|
||||
.second-line = { $command } Jelölje ki a második vonalat (kisebb szög):
|
||||
.lines = Vonalak
|
||||
.points = Pontok
|
||||
.invalid-radius = Jelöljön ki véges, pozitív sugarú kört vagy körívet.
|
||||
.invalid-line = Jelöljön ki véges koordinátájú, nem nulla hosszúságú vonalat.
|
||||
.different-line = Jelöljön ki egy másik vonalat.
|
||||
.invalid-point = Adjon meg véges koordinátájú pontot.
|
||||
.degenerate-ray = A szár végpontja nem egyezhet meg a szög csúcspontjával.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Előzetes összterület = { $total }
|
||||
@@ -6569,3 +6569,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6558,3 +6558,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6569,3 +6569,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6570,3 +6570,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6568,3 +6568,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6569,3 +6569,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6570,3 +6570,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6571,3 +6571,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6491,3 +6491,20 @@ block-def =
|
||||
.name-unsupported-chars = Ad desteklenmeyen karakterler içeriyor. Şu karakterler desteklenmez: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = Blok tanımı değişti. Yeniden tanımlamak istiyor musunuz?
|
||||
.create-drawing-file = Çizim Dosyası Oluştur
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6553,3 +6553,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -6565,3 +6565,20 @@ block-def =
|
||||
.name-unsupported-chars = The name contains unsupported characters. The following characters are not supported: \ < > / ? " : ; * | , = `
|
||||
.definition-changed-redefine = The block definition has changed. Do you want to redefine it ?
|
||||
.create-drawing-file = Create Drawing File
|
||||
|
||||
# Transient geometry inquiry readouts.
|
||||
measurement =
|
||||
.distance = Distance = { $value }
|
||||
.angle-start = { $command } Specify vertex point or [Lines]:
|
||||
.first-line = { $command } Select first line or [Points] (smaller angle):
|
||||
.second-line = { $command } Select second line (smaller angle):
|
||||
.lines = Lines
|
||||
.points = Points
|
||||
.invalid-radius = Select a circle or arc with a finite, positive radius.
|
||||
.invalid-line = Select a nonzero, finite line.
|
||||
.different-line = Select a different line.
|
||||
.invalid-point = Specify a point with finite coordinates.
|
||||
.degenerate-ray = The ray endpoint must differ from the angle vertex.
|
||||
.area-preview =
|
||||
{ $current }
|
||||
Preview total area = { $total }
|
||||
@@ -1163,6 +1163,7 @@ impl OpenCADStudio {
|
||||
|
||||
fn handle_cancel(&mut self, cancel: CmdResult) {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.clear();
|
||||
let space_changed = matches!(cancel, CmdResult::CancelForSpaceChange);
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
if !space_changed && self.tabs[i].suspended_cmd.is_some() {
|
||||
@@ -1304,6 +1305,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
fn handle_measurement(&mut self, msg: String) {
|
||||
self.remember_measurement(&msg);
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
@@ -1315,6 +1317,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
fn handle_report_measurement(&mut self, msg: String) {
|
||||
self.remember_measurement(&msg);
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
@@ -1336,6 +1339,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
fn handle_report_measurement_and_deselect(&mut self, msg: String) {
|
||||
self.remember_measurement(&msg);
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
|
||||
@@ -598,9 +598,13 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"MEASUREGEOM" => {
|
||||
"MEASUREGEOM" | "MEASURERADIUS" | "MEASUREANGLE" => {
|
||||
use crate::modules::draw::inquiry::measuregeom::MeasureGeomCommand;
|
||||
let new_cmd = MeasureGeomCommand::new();
|
||||
let new_cmd = match cmd {
|
||||
"MEASURERADIUS" => MeasureGeomCommand::radius(),
|
||||
"MEASUREANGLE" => MeasureGeomCommand::angle(),
|
||||
_ => MeasureGeomCommand::new(),
|
||||
};
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ impl OpenCADStudio {
|
||||
/// (the block palette), so they don't inherit the previous command's last
|
||||
/// point, snaps or dynamic-input values. (#1525)
|
||||
pub(in crate::app) fn reset_command_start_state(&mut self, i: usize) {
|
||||
self.tabs[i].measurement.clear();
|
||||
// A command parked behind a transparent zoom / MTP goes with it.
|
||||
self.tabs[i].suspended_cmd = None;
|
||||
self.tabs[i].transparent_resume = false;
|
||||
|
||||
+11
-1
@@ -308,7 +308,17 @@ fn command_category(name: &str) -> &'static str {
|
||||
.contains(&name)
|
||||
{
|
||||
"modify"
|
||||
} else if ["DIST", "ID", "AREA", "MEASURE", "MEASUREGEOM"].contains(&name) {
|
||||
} else if [
|
||||
"DIST",
|
||||
"ID",
|
||||
"AREA",
|
||||
"MEASURE",
|
||||
"MEASUREGEOM",
|
||||
"MEASURERADIUS",
|
||||
"MEASUREANGLE",
|
||||
]
|
||||
.contains(&name)
|
||||
{
|
||||
"inspect"
|
||||
} else if ["ZOOM", "PAN", "UCS", "PLAN", "VPORTS", "3DORBIT"].contains(&name) {
|
||||
"view"
|
||||
|
||||
@@ -157,6 +157,7 @@ pub(super) struct DocumentTab {
|
||||
pub(super) properties: PropertiesPanel,
|
||||
pub(super) layers: LayerPanel,
|
||||
pub(super) active_cmd: Option<Box<dyn CadCommand>>,
|
||||
pub(super) measurement: super::measurement::MeasurementDisplay,
|
||||
/// Remaining command tokens queued behind a PAUSE (or `\`) awaiting user interaction.
|
||||
pub(super) pending_pause_tokens: Option<Vec<String>>,
|
||||
/// The selection set the most recent command worked on, captured when a
|
||||
@@ -644,6 +645,7 @@ impl DocumentTab {
|
||||
properties: PropertiesPanel::empty(),
|
||||
layers: LayerPanel::default(),
|
||||
active_cmd: None,
|
||||
measurement: super::measurement::MeasurementDisplay::default(),
|
||||
pending_pause_tokens: None,
|
||||
last_cmd: None,
|
||||
last_draw_anchor: None,
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
//! Per-drawing, transient inquiry UI; never serialized into document entities.
|
||||
use super::OpenCADStudio;
|
||||
use iced::Point;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct MeasurementReadout {
|
||||
pub text: String,
|
||||
pub anchor: Point,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct MeasurementDisplay {
|
||||
pub result: Option<MeasurementReadout>,
|
||||
pub live_visible: bool,
|
||||
}
|
||||
|
||||
impl MeasurementDisplay {
|
||||
pub fn clear(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
/// Called on press before ordinary input processing. It cannot capture
|
||||
/// the event, so this same press can also pick/select/pan/finish a command.
|
||||
pub fn dismiss_result(&mut self) {
|
||||
self.result = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn remember_measurement(&mut self, text: &str) {
|
||||
let tab = &mut self.tabs[self.active_tab];
|
||||
if tab
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.has_measurement_readout())
|
||||
{
|
||||
tab.measurement.result = Some(MeasurementReadout {
|
||||
text: text.to_owned(),
|
||||
anchor: tab.last_cursor_screen,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn measurement_readout(&self, index: usize) -> Option<MeasurementReadout> {
|
||||
let tab = &self.tabs[index];
|
||||
if let Some(result) = &tab.measurement.result {
|
||||
return Some(result.clone());
|
||||
}
|
||||
if !tab.measurement.live_visible {
|
||||
return None;
|
||||
}
|
||||
let text = tab
|
||||
.active_cmd
|
||||
.as_ref()?
|
||||
.measurement_readout(tab.last_cursor_world)?;
|
||||
Some(MeasurementReadout {
|
||||
text,
|
||||
anchor: tab.last_cursor_screen,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod measurement_tests {
|
||||
use super::*;
|
||||
use crate::app::Message;
|
||||
use crate::command::{all_registered_command_names, StepInput};
|
||||
use crate::modules::{CadModule, ModuleEvent, RibbonItem};
|
||||
use glam::DVec3;
|
||||
|
||||
fn app() -> OpenCADStudio {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
|
||||
app.snapper.snap_enabled = false;
|
||||
app.snapper.grid_snap_on = false;
|
||||
app.snapper.otrack_enabled = false;
|
||||
app.ortho_mode = false;
|
||||
app.polar_mode = false;
|
||||
app.dyn_input = false;
|
||||
app
|
||||
}
|
||||
fn finish_distance(app: &mut OpenCADStudio) {
|
||||
let _ = app.dispatch_command("DIST");
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::ZERO));
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(3.0, 4.0, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_menu_registry_and_dispatch() {
|
||||
let names = all_registered_command_names();
|
||||
let module = crate::modules::draw::DrawModule;
|
||||
let menu = module
|
||||
.ribbon_groups()
|
||||
.iter()
|
||||
.flat_map(|g| &g.tools)
|
||||
.find(|item| {
|
||||
matches!(
|
||||
item,
|
||||
RibbonItem::LargeDropdown {
|
||||
id: "MEASURE_MENU",
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let RibbonItem::LargeDropdown { items, default, .. } = menu else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(*default, "DIST");
|
||||
assert_eq!(
|
||||
items.iter().map(|item| item.0).collect::<Vec<_>>(),
|
||||
["DIST", "MEASURERADIUS", "MEASUREANGLE", "AREA"]
|
||||
);
|
||||
let mut app = app();
|
||||
for name in [
|
||||
"DIST",
|
||||
"MEASURERADIUS",
|
||||
"MEASUREANGLE",
|
||||
"AREA",
|
||||
"MEASUREGEOM",
|
||||
] {
|
||||
assert!(names.contains(&name));
|
||||
let _ = app.dispatch_command(name);
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
|
||||
name
|
||||
);
|
||||
let _ = app.update(Message::RibbonToolClick {
|
||||
tool_id: name.into(),
|
||||
event: ModuleEvent::Command(name.into()),
|
||||
});
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_result_persists_without_document_changes() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let count = app.tabs[i].scene.document.entity_count();
|
||||
let dirty = app.tabs[i].dirty;
|
||||
let revision = app.tabs[i].edit_revision;
|
||||
let _ = app.update(Message::ViewportMove(Point::new(350.0, 250.0)));
|
||||
finish_distance(&mut app);
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
let result = app.measurement_readout(i).unwrap();
|
||||
assert!(app.command_line.history_plain_text().contains(&result.text));
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
let _ = app.update(Message::ViewportMove(Point::new(450.0, 300.0)));
|
||||
let retained = app.measurement_readout(i).unwrap();
|
||||
assert_eq!(result.text, retained.text);
|
||||
assert_eq!(result.anchor, retained.anchor);
|
||||
assert_eq!(app.tabs[i].scene.document.entity_count(), count);
|
||||
assert_eq!(app.tabs[i].dirty, dirty);
|
||||
assert_eq!(app.tabs[i].edit_revision, revision);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_dismiss_click_also_starts_selection() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
finish_distance(&mut app);
|
||||
let _ = app.update(Message::ViewportMove(Point::new(350.0, 250.0)));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
assert!(app.measurement_readout(i).is_none());
|
||||
assert!(app.tabs[i].scene.selection.borrow().input.left_down);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.selection
|
||||
.borrow()
|
||||
.gesture
|
||||
.box_anchor
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_final_pick_survives_its_own_release() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let _ = app.dispatch_command("DIST");
|
||||
for point in [Point::new(350.0, 250.0), Point::new(430.0, 280.0)] {
|
||||
let _ = app.update(Message::ViewportMove(point));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
}
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
assert!(app.measurement_readout(i).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_live_without_dyn_and_escape() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let _ = app.dispatch_command("DIST");
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::ZERO));
|
||||
let _ = app.update(Message::ViewportMove(Point::new(410.0, 310.0)));
|
||||
let first = app.measurement_readout(i).unwrap().text;
|
||||
let _ = app.update(Message::ViewportMove(Point::new(510.0, 310.0)));
|
||||
assert_ne!(first, app.measurement_readout(i).unwrap().text);
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert!(app.measurement_readout(i).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_escape_new_command_and_mouse_buttons_clear_result() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
for event in [
|
||||
Message::CommandEscape,
|
||||
Message::ViewportRightPress,
|
||||
Message::ViewportMiddlePress,
|
||||
] {
|
||||
finish_distance(&mut app);
|
||||
assert!(app.measurement_readout(i).is_some());
|
||||
let _ = app.update(event);
|
||||
assert!(app.measurement_readout(i).is_none());
|
||||
}
|
||||
finish_distance(&mut app);
|
||||
let _ = app.dispatch_command("MEASURERADIUS");
|
||||
assert!(app.measurement_readout(i).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_area_add_dismiss_click_is_also_next_corner() {
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let _ = app.dispatch_command("AREA");
|
||||
let _ = app.feed_command(StepInput::Text("ADD".into()));
|
||||
for p in [DVec3::ZERO, DVec3::X * 4.0, DVec3::Y * 3.0] {
|
||||
let _ = app.feed_command(StepInput::Point(p));
|
||||
}
|
||||
let _ = app.feed_command(StepInput::Enter);
|
||||
assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "AREA");
|
||||
assert!(app.measurement_readout(i).is_some());
|
||||
for p in [Point::new(350.0, 250.0), Point::new(410.0, 250.0)] {
|
||||
let _ = app.update(Message::ViewportMove(p));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
}
|
||||
assert!(app.tabs[i].measurement.result.is_none());
|
||||
assert!(
|
||||
app.measurement_readout(i).is_some(),
|
||||
"both corners must reach AREA"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measurement_radius_and_lines_real_entity_injection() {
|
||||
use codec::{
|
||||
entities::{Circle, Line},
|
||||
types::Vector3,
|
||||
EntityType,
|
||||
};
|
||||
let mut app = app();
|
||||
let i = app.active_tab;
|
||||
let circle = app.tabs[i]
|
||||
.scene
|
||||
.add_entity(EntityType::Circle(Circle::from_coords(0.0, 0.0, 0.0, 2.0)));
|
||||
let first = app.tabs[i]
|
||||
.scene
|
||||
.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
)));
|
||||
let second = app.tabs[i]
|
||||
.scene
|
||||
.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_Y,
|
||||
)));
|
||||
let _ = app.dispatch_command("MEASURERADIUS");
|
||||
let _ = app.feed_command(StepInput::EntityPick(circle, DVec3::X * 2.0));
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
assert!(app.measurement_readout(i).unwrap().text.contains("Radius"));
|
||||
let _ = app.dispatch_command("MEASUREANGLE");
|
||||
let _ = app.feed_command(StepInput::Text("LINES".into()));
|
||||
let _ = app.feed_command(StepInput::EntityPick(first, DVec3::X));
|
||||
let _ = app.feed_command(StepInput::EntityPick(second, DVec3::Y));
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
assert!(app.measurement_readout(i).unwrap().text.contains("90"));
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ mod find_replace;
|
||||
pub(crate) mod helpers;
|
||||
mod history;
|
||||
mod layers;
|
||||
mod measurement;
|
||||
mod model_ops;
|
||||
mod mtext_editor;
|
||||
mod navigation;
|
||||
|
||||
@@ -1249,6 +1249,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
pub(super) fn on_command_escape(&mut self) -> Task<Message> {
|
||||
self.tabs[self.active_tab].measurement.clear();
|
||||
// Escape answers a pending client getpoint request first: no point.
|
||||
if self.control.get_point.is_some() {
|
||||
self.resolve_get_point(None);
|
||||
|
||||
@@ -4564,6 +4564,7 @@ impl OpenCADStudio {
|
||||
Message::ViewportRightPress => {
|
||||
self.graphic_attributes.open_menu = None;
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.dismiss_result();
|
||||
self.ribbon.close_dropdown();
|
||||
// Shift+RMB: the one-shot snap override menu at the cursor —
|
||||
// pick a snap for just the next point, then it expires (#337).
|
||||
|
||||
@@ -1236,6 +1236,7 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.live_visible = true;
|
||||
if let Some(task) = self.start_drag_move(i, p) {
|
||||
return Task::batch([task, self.on_viewport_move(p)]);
|
||||
}
|
||||
@@ -3162,6 +3163,7 @@ impl OpenCADStudio {
|
||||
|
||||
pub(crate) fn on_viewport_exit(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.live_visible = false;
|
||||
self.hover_dwell = None;
|
||||
if self.tabs[i]
|
||||
.active_cmd
|
||||
@@ -3658,6 +3660,7 @@ impl OpenCADStudio {
|
||||
|
||||
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.dismiss_result();
|
||||
self.block_palette.pressed = None;
|
||||
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||
command.set_ctrl(self.ctrl_down);
|
||||
@@ -5916,6 +5919,7 @@ properties={:.1}ms picked={}",
|
||||
|
||||
pub(super) fn on_viewport_middle_press(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].measurement.dismiss_result();
|
||||
self.clear_navigation_hover(i);
|
||||
self.ribbon.close_dropdown();
|
||||
self.tabs[i].scene.remember_current_view();
|
||||
|
||||
@@ -1809,6 +1809,15 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
} else {
|
||||
34.0 + self.command_line.overlay_lines_height()
|
||||
};
|
||||
if !tab.is_start {
|
||||
if let Some(readout) = self.measurement_readout(self.active_tab) {
|
||||
viewport_stack = viewport_stack.push(overlay::measurement_overlay(
|
||||
readout.anchor,
|
||||
command_line_inset,
|
||||
readout.text,
|
||||
));
|
||||
}
|
||||
}
|
||||
// Quick Properties: stay near the selection cursor, flipping around
|
||||
// it as needed to remain inside the visible drawing area.
|
||||
if self.quick_properties && !tab.is_start {
|
||||
|
||||
+119
-1
@@ -928,6 +928,7 @@ fn position_canvas_overlay_clamped<'a>(
|
||||
bottom_inset: bottom_inset.max(0.0),
|
||||
gap: 0.0,
|
||||
offset,
|
||||
passive: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -944,6 +945,48 @@ pub(super) fn position_canvas_overlay_near_cursor<'a>(
|
||||
bottom_inset: bottom_inset.max(0.0),
|
||||
gap: 12.0,
|
||||
offset: Vector::ZERO,
|
||||
passive: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Measured, wrapping text in the existing clamped panel layout. Input goes
|
||||
/// straight through, including clicks directly on the result text.
|
||||
pub(super) fn measurement_overlay<'a>(
|
||||
anchor: iced::Point,
|
||||
bottom_inset: f32,
|
||||
value: String,
|
||||
) -> Element<'a, Message> {
|
||||
// Inquiry commands return a compact, comma-separated summary for the
|
||||
// command line. Show its fields on separate lines in the transient HUD,
|
||||
// without altering the command-line history or the measured precision.
|
||||
let display_value = if value.contains("\n") {
|
||||
value.replace(", ", "\n").replace(", ", "\n")
|
||||
} else {
|
||||
value
|
||||
};
|
||||
let panel = container(text(display_value).size(12))
|
||||
.padding([6, 8])
|
||||
.width(Length::Fit.max(300.0))
|
||||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
text_color: Some(palette.background.weak.text),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
Element::new(ClampedPin {
|
||||
content: panel.into(),
|
||||
anchor,
|
||||
bottom_inset: bottom_inset.max(0.0),
|
||||
gap: 18.0,
|
||||
offset: Vector::ZERO,
|
||||
passive: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -954,6 +997,8 @@ struct ClampedPin<'a> {
|
||||
gap: f32,
|
||||
/// Preferred placement shift from the anchor (applied before fitting).
|
||||
offset: Vector,
|
||||
/// Readouts prefer above the cursor and never participate in input.
|
||||
passive: bool,
|
||||
}
|
||||
|
||||
impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
@@ -1008,7 +1053,9 @@ impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
let max_y = (max.height - bottom_inset - MARGIN - content.height).max(MARGIN);
|
||||
let below = self.anchor.y + self.gap + self.offset.y;
|
||||
let above = self.anchor.y - self.gap - content.height;
|
||||
let y = if below <= max_y {
|
||||
let y = if self.passive && above >= MARGIN && above <= max_y {
|
||||
above
|
||||
} else if below <= max_y {
|
||||
below.max(MARGIN)
|
||||
} else if above >= MARGIN {
|
||||
above
|
||||
@@ -1016,6 +1063,9 @@ impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
max_y
|
||||
};
|
||||
|
||||
// A cursor over the command-line inset can put even the "above"
|
||||
// candidate below the safe area. Keep this passive readout inside it.
|
||||
let y = if self.passive { y.min(max_y) } else { y };
|
||||
node = node.move_to(iced::Point::new(x, y));
|
||||
let size = limits.resolve(Length::Fill, Length::Fill, node.size());
|
||||
layout::Node::with_children(size, vec![node])
|
||||
@@ -1046,6 +1096,9 @@ impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
if self.passive {
|
||||
return;
|
||||
}
|
||||
self.content.as_widget_mut().update(
|
||||
tree,
|
||||
event,
|
||||
@@ -1065,6 +1118,9 @@ impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
viewport: &Rectangle,
|
||||
renderer: &iced::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if self.passive {
|
||||
return mouse::Interaction::None;
|
||||
}
|
||||
self.content.as_widget().mouse_interaction(
|
||||
tree,
|
||||
layout.children().next().unwrap(),
|
||||
@@ -1872,3 +1928,65 @@ fn qselect_content<'a>(
|
||||
|
||||
panel.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod measurement_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn measurement_overlay_passes_all_clicks_on_text() {
|
||||
let underlay = mouse_area(Space::new().width(Fill).height(Fill))
|
||||
.on_press(Message::ViewportLeftPress)
|
||||
.on_right_press(Message::ViewportRightPress)
|
||||
.on_middle_press(Message::ViewportMiddlePress);
|
||||
let mut ui = iced_test::simulator(stack![
|
||||
underlay,
|
||||
measurement_overlay(
|
||||
iced::Point::new(400.0, 300.0),
|
||||
60.0,
|
||||
"measurement-result".into()
|
||||
)
|
||||
]);
|
||||
let center = ui
|
||||
.find("measurement-result")
|
||||
.unwrap()
|
||||
.visible_bounds()
|
||||
.unwrap()
|
||||
.center();
|
||||
ui.point_at(center);
|
||||
for button in [
|
||||
iced::mouse::Button::Left,
|
||||
iced::mouse::Button::Right,
|
||||
iced::mouse::Button::Middle,
|
||||
] {
|
||||
ui.simulate([Event::Mouse(iced::mouse::Event::ButtonPressed(button))]);
|
||||
ui.simulate([Event::Mouse(iced::mouse::Event::ButtonReleased(button))]);
|
||||
}
|
||||
let messages: Vec<_> = ui.into_messages().collect();
|
||||
assert!(messages
|
||||
.iter()
|
||||
.any(|m| matches!(m, Message::ViewportLeftPress)));
|
||||
assert!(messages
|
||||
.iter()
|
||||
.any(|m| matches!(m, Message::ViewportRightPress)));
|
||||
assert!(messages
|
||||
.iter()
|
||||
.any(|m| matches!(m, Message::ViewportMiddlePress)));
|
||||
}
|
||||
#[test]
|
||||
fn measurement_overlay_stays_inside_canvas_above_command_line() {
|
||||
for anchor in [iced::Point::new(5.0, 5.0), iced::Point::new(790.0, 595.0)] {
|
||||
let mut ui = iced_test::simulator::Simulator::with_size(
|
||||
Default::default(),
|
||||
iced::Size::new(800.0, 600.0),
|
||||
measurement_overlay(anchor, 90.0, "measurement-result".into()),
|
||||
);
|
||||
let bounds = ui
|
||||
.find("measurement-result")
|
||||
.unwrap()
|
||||
.visible_bounds()
|
||||
.unwrap();
|
||||
assert!(bounds.x >= 4.0 && bounds.y >= 4.0);
|
||||
assert!(bounds.x + bounds.width <= 796.0 && bounds.y + bounds.height <= 506.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2876,6 +2876,17 @@ pub trait CadCommand: Send {
|
||||
|
||||
fn inject_hover_entity(&mut self, _handle: Handle, _entity: EntityType) {}
|
||||
|
||||
/// Opt in to transient measurement text; unrelated Measurement users keep
|
||||
/// their existing command-line-only behavior.
|
||||
fn has_measurement_readout(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Live inquiry text at the same full-precision point as the preview.
|
||||
fn measurement_readout(&self, _cursor: DVec3) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Called on every mouse-move in the viewport.
|
||||
/// Return `Some(WireModel)` to update the rubber-band preview, `None` to skip.
|
||||
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
|
||||
|
||||
@@ -29,6 +29,9 @@ pub struct AreaCommand {
|
||||
points: Vec<DVec3>,
|
||||
objects_gathering: bool,
|
||||
selected_entities: Vec<SelectionEntity>,
|
||||
// Recompute exact curve measurements only when the selection changes,
|
||||
// not on every UI frame while showing the live readout.
|
||||
selection_preview: Option<AreaMeasurement>,
|
||||
preview_regions: Vec<AreaPreviewRegion>,
|
||||
total_area: f64,
|
||||
total_perimeter: f64,
|
||||
@@ -41,6 +44,7 @@ impl AreaCommand {
|
||||
points: Vec::new(),
|
||||
objects_gathering: false,
|
||||
selected_entities: Vec::new(),
|
||||
selection_preview: None,
|
||||
preview_regions: Vec::new(),
|
||||
total_area: 0.0,
|
||||
total_perimeter: 0.0,
|
||||
@@ -225,6 +229,7 @@ impl AreaCommand {
|
||||
self.points.clear();
|
||||
self.objects_gathering = false;
|
||||
self.selected_entities.clear();
|
||||
self.selection_preview = None;
|
||||
let message = self.running_result_message(measurement);
|
||||
if deselect {
|
||||
CmdResult::ReportMeasurementAndDeselect(message)
|
||||
@@ -297,6 +302,7 @@ impl CadCommand for AreaCommand {
|
||||
}
|
||||
self.objects_gathering = false;
|
||||
self.selected_entities.clear();
|
||||
self.selection_preview = None;
|
||||
return Some(CmdResult::DeselectAndContinue);
|
||||
}
|
||||
return Some(CmdResult::NeedPoint);
|
||||
@@ -323,6 +329,7 @@ impl CadCommand for AreaCommand {
|
||||
|
||||
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
|
||||
self.selected_entities = entities;
|
||||
self.selection_preview = self.selection_measurement().map(|(value, _)| value);
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
||||
@@ -341,6 +348,7 @@ impl CadCommand for AreaCommand {
|
||||
if self.objects_gathering {
|
||||
let Some((measurement, handles)) = self.selection_measurement() else {
|
||||
self.selected_entities.clear();
|
||||
self.selection_preview = None;
|
||||
return CmdResult::ReportMeasurementAndDeselect(crate::tr!(
|
||||
"area",
|
||||
"objects-not-measurable"
|
||||
@@ -373,6 +381,39 @@ impl CadCommand for AreaCommand {
|
||||
self.finish_measurement(measurement, false)
|
||||
}
|
||||
|
||||
fn has_measurement_readout(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn measurement_readout(&self, cursor: DVec3) -> Option<String> {
|
||||
let measurement = if self.objects_gathering {
|
||||
self.selection_preview?
|
||||
} else {
|
||||
if self.points.len() < 2 || !cursor.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let mut points = self.points.clone();
|
||||
points.push(cursor);
|
||||
Self::point_measurement(&points, true)
|
||||
};
|
||||
if self.mode == AreaMode::Single {
|
||||
Some(Self::result_message(measurement))
|
||||
} else {
|
||||
let sign = if self.mode == AreaMode::Add {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
};
|
||||
Some(crate::tr!(
|
||||
"measurement",
|
||||
"area-preview",
|
||||
current = Self::result_message(measurement),
|
||||
total =
|
||||
crate::entities::common::format_area(self.total_area + sign * measurement.area)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
if self.objects_gathering || self.points.is_empty() {
|
||||
return None;
|
||||
@@ -451,3 +492,65 @@ impl CadCommand for AreaCommand {
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["AREA"] });
|
||||
|
||||
#[cfg(test)]
|
||||
mod measurement_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn live_area_matches_final_result_without_changing_points() {
|
||||
let mut command = AreaCommand::new();
|
||||
command.on_point(DVec3::ZERO);
|
||||
assert!(command.measurement_readout(DVec3::Y).is_none());
|
||||
command.on_point(DVec3::X * 4.0);
|
||||
let cursor = DVec3::Y * 3.0;
|
||||
let preview = command.measurement_readout(cursor).unwrap();
|
||||
assert_eq!(command.points.len(), 2);
|
||||
command.on_point(cursor);
|
||||
let CmdResult::Measurement(text) = command.on_enter() else {
|
||||
panic!("result");
|
||||
};
|
||||
assert_eq!(preview, text);
|
||||
}
|
||||
#[test]
|
||||
fn add_subtract_preview_does_not_change_total() {
|
||||
let mut command = AreaCommand::new();
|
||||
command.on_text_input("ADD");
|
||||
for p in [DVec3::ZERO, DVec3::X * 4.0, DVec3::Y * 3.0] {
|
||||
command.on_point(p);
|
||||
}
|
||||
assert!(matches!(
|
||||
command.on_enter(),
|
||||
CmdResult::ReportMeasurement(_)
|
||||
));
|
||||
command.on_text_input("SUBTRACT");
|
||||
command.on_point(DVec3::ZERO);
|
||||
command.on_point(DVec3::X * 2.0);
|
||||
assert!(command
|
||||
.measurement_readout(DVec3::Y * 2.0)
|
||||
.unwrap()
|
||||
.contains(&crate::entities::common::format_area(4.0)));
|
||||
assert_eq!(command.total_area, 6.0);
|
||||
}
|
||||
#[test]
|
||||
fn object_readout_refreshes_with_selection_and_clears_when_empty() {
|
||||
let mut command = AreaCommand::new();
|
||||
command.on_text_input("OBJECTS");
|
||||
let circle = |radius| SelectionEntity {
|
||||
handle: Handle::new(1),
|
||||
entity: EntityType::Circle(codec::Circle::from_coords(0.0, 0.0, 0.0, radius)),
|
||||
surface_area: None,
|
||||
};
|
||||
command.inject_selection_entities(vec![circle(2.0)]);
|
||||
let first = command.measurement_readout(DVec3::ZERO).unwrap();
|
||||
command.inject_selection_entities(vec![circle(3.0)]);
|
||||
let preview = command.measurement_readout(DVec3::X).unwrap();
|
||||
assert_ne!(first, preview);
|
||||
let CmdResult::Measurement(result) = command.on_enter() else {
|
||||
panic!("result")
|
||||
};
|
||||
assert_eq!(preview, result);
|
||||
command.on_text_input("OBJECTS");
|
||||
command.inject_selection_entities(Vec::new());
|
||||
assert!(command.measurement_readout(DVec3::ZERO).is_none());
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,17 @@ impl CadCommand for DistCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_measurement_readout(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn measurement_readout(&self, cursor: DVec3) -> Option<String> {
|
||||
let distance = self.plane.vector_to_local(cursor - self.first?).length();
|
||||
distance
|
||||
.is_finite()
|
||||
.then(|| crate::tr!("measurement", "distance", value = format_length(distance)))
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if let Some(p1) = self.first {
|
||||
let delta = self.plane.vector_to_local(pt - p1);
|
||||
@@ -252,3 +263,31 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod measurement_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn live_distance_keeps_f64_and_unit_settings() {
|
||||
let previous = unit_context();
|
||||
let mut ctx = previous;
|
||||
ctx.lunits = 2;
|
||||
ctx.luprec = 4;
|
||||
crate::entities::common::set_unit_context(ctx);
|
||||
let mut command = DistCommand::new();
|
||||
let first = DVec3::new(500000.001, 600000.002, 0.0);
|
||||
assert!(command.measurement_readout(first).is_none());
|
||||
command.on_point(first);
|
||||
assert!(command
|
||||
.measurement_readout(first + DVec3::X * 0.001)
|
||||
.unwrap()
|
||||
.contains("0.0010"));
|
||||
ctx.lunits = 4;
|
||||
crate::entities::common::set_unit_context(ctx);
|
||||
assert!(command
|
||||
.measurement_readout(first + DVec3::X * 18.0)
|
||||
.unwrap()
|
||||
.contains("1'-6\""));
|
||||
crate::entities::common::set_unit_context(previous);
|
||||
}
|
||||
}
|
||||
@@ -44,11 +44,21 @@ enum Mode {
|
||||
Area,
|
||||
Angle,
|
||||
Radius,
|
||||
AngleLines,
|
||||
}
|
||||
|
||||
// ── Command implementation ──────────────────────────────────────────────────
|
||||
|
||||
pub const RADIUS_ICON: IconKind = IconKind::Svg(include_bytes!(
|
||||
"../../../../assets/icons/measure-radius.svg"
|
||||
));
|
||||
pub const ANGLE_ICON: IconKind =
|
||||
IconKind::Svg(include_bytes!("../../../../assets/icons/measure-angle.svg"));
|
||||
|
||||
pub struct MeasureGeomCommand {
|
||||
name: &'static str,
|
||||
hovered: Option<(Handle, EntityType)>,
|
||||
first_line: Option<(Handle, DVec3)>,
|
||||
mode: Mode,
|
||||
/// Picked points for the active point-pick mode.
|
||||
points: Vec<DVec3>,
|
||||
@@ -60,6 +70,9 @@ pub struct MeasureGeomCommand {
|
||||
impl MeasureGeomCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
name: "MEASUREGEOM",
|
||||
hovered: None,
|
||||
first_line: None,
|
||||
mode: Mode::Choose,
|
||||
points: vec![],
|
||||
picked: None,
|
||||
@@ -67,6 +80,72 @@ impl MeasureGeomCommand {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn radius() -> Self {
|
||||
Self {
|
||||
name: "MEASURERADIUS",
|
||||
mode: Mode::Radius,
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn angle() -> Self {
|
||||
Self {
|
||||
name: "MEASUREANGLE",
|
||||
mode: Mode::Angle,
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn radius_msg(radius: f64) -> String {
|
||||
t!(
|
||||
"Radius = %{radius}, Diameter = %{diameter}",
|
||||
radius = crate::entities::common::format_length(radius),
|
||||
diameter = crate::entities::common::format_length(radius * 2.0)
|
||||
)
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
// Normalize only after rescaling, retaining f64 precision without an
|
||||
// arbitrary drawing-unit tolerance or overflowing on long vectors.
|
||||
fn direction(vector: DVec3) -> Option<DVec3> {
|
||||
if !vector.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let scale = vector.abs().max_element();
|
||||
if scale == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let vector = vector / scale;
|
||||
Some(vector / vector.length())
|
||||
}
|
||||
|
||||
fn angle_degrees(a: DVec3, b: DVec3) -> Option<f64> {
|
||||
let a = Self::direction(a)?;
|
||||
let b = Self::direction(b)?;
|
||||
Some(a.cross(b).length().atan2(a.dot(b)).to_degrees())
|
||||
}
|
||||
|
||||
fn line_direction(entity: &EntityType) -> Option<DVec3> {
|
||||
let EntityType::Line(line) = entity else {
|
||||
return None;
|
||||
};
|
||||
Self::direction(
|
||||
DVec3::new(line.end.x, line.end.y, line.end.z)
|
||||
- DVec3::new(line.start.x, line.start.y, line.start.z),
|
||||
)
|
||||
}
|
||||
|
||||
// Unoriented lines: the smaller angle, independent of endpoint order.
|
||||
fn line_angle(a: DVec3, b: DVec3) -> Option<f64> {
|
||||
Self::angle_degrees(a, b).map(|angle| angle.min(180.0 - angle))
|
||||
}
|
||||
|
||||
fn angle_value_msg(angle: f64) -> String {
|
||||
let aprec = crate::entities::common::unit_context().auprec.max(0) as usize;
|
||||
let angle = crate::entities::common::without_negative_zero(format!("{angle:.aprec$}"));
|
||||
t!("Angle = %{angle}°", angle = angle).into_owned()
|
||||
}
|
||||
|
||||
/// Build a cyan preview wire connecting the picked points and the cursor.
|
||||
fn preview_wire(name: &str, pts: Vec<[f32; 3]>) -> WireModel {
|
||||
WireModel {
|
||||
@@ -110,11 +189,12 @@ impl MeasureGeomCommand {
|
||||
|
||||
/// Extract (radius) from a Circle or Arc; `None` for anything else.
|
||||
fn radius_of(entity: &EntityType) -> Option<f64> {
|
||||
match entity {
|
||||
EntityType::Circle(c) => Some(c.radius),
|
||||
EntityType::Arc(a) => Some(a.radius),
|
||||
_ => None,
|
||||
}
|
||||
let radius = match entity {
|
||||
EntityType::Circle(circle) => circle.radius,
|
||||
EntityType::Arc(arc) => arc.radius,
|
||||
_ => return None,
|
||||
};
|
||||
(radius.is_finite() && radius > 0.0 && (radius * 2.0).is_finite()).then_some(radius)
|
||||
}
|
||||
|
||||
/// DISTANCE readout for the two collected points.
|
||||
@@ -172,27 +252,14 @@ impl MeasureGeomCommand {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// ANGLE readout: angle at `vertex` between the rays to `a` and `b`.
|
||||
fn angle_msg(vertex: DVec3, a: DVec3, b: DVec3) -> String {
|
||||
let va = a - vertex;
|
||||
let vb = b - vertex;
|
||||
let la = va.length();
|
||||
let lb = vb.length();
|
||||
let aprec = crate::entities::common::unit_context().auprec.max(0) as usize;
|
||||
if la == 0.0 || lb == 0.0 {
|
||||
let zero = crate::entities::common::without_negative_zero(format!("{:.aprec$}", 0.0));
|
||||
return t!("Angle = %{angle}° (degenerate rays)", angle = zero).into_owned();
|
||||
}
|
||||
let cos = (va.dot(vb) / (la * lb)).clamp(-1.0, 1.0);
|
||||
let angle = cos.acos().to_degrees();
|
||||
let angle_s = crate::entities::common::without_negative_zero(format!("{angle:.aprec$}"));
|
||||
t!("Angle = %{angle}°", angle = angle_s).into_owned()
|
||||
fn angle_msg(vertex: DVec3, a: DVec3, b: DVec3) -> Option<String> {
|
||||
Self::angle_degrees(a - vertex, b - vertex).map(Self::angle_value_msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MeasureGeomCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"MEASUREGEOM"
|
||||
self.name
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
@@ -220,12 +287,20 @@ impl CadCommand for MeasureGeomCommand {
|
||||
}
|
||||
}
|
||||
Mode::Angle => match self.points.len() {
|
||||
0 => t!("MEASUREGEOM Specify vertex point:").into_owned(),
|
||||
0 => return crate::tr!("measurement", "angle-start", command = self.name),
|
||||
1 => t!("MEASUREGEOM Specify first ray point:").into_owned(),
|
||||
_ => t!("MEASUREGEOM Specify second ray point:").into_owned(),
|
||||
},
|
||||
Mode::Radius => t!("MEASUREGEOM Select arc or circle:").into_owned(),
|
||||
Mode::AngleLines => {
|
||||
return if self.first_line.is_none() {
|
||||
crate::tr!("measurement", "first-line", command = self.name)
|
||||
} else {
|
||||
crate::tr!("measurement", "second-line", command = self.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
.replace("MEASUREGEOM", self.name)
|
||||
}
|
||||
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
@@ -233,14 +308,64 @@ impl CadCommand for MeasureGeomCommand {
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
// Only the opening mode-keyword step reads a typed token.
|
||||
self.mode == Mode::Choose
|
||||
self.mode == Mode::Choose || self.point_step_accepts_keywords()
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
(self.mode == Mode::Angle && self.points.is_empty())
|
||||
|| (self.mode == Mode::AngleLines && self.first_line.is_none())
|
||||
}
|
||||
|
||||
fn has_measurement_readout(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn measurement_readout(&self, cursor: DVec3) -> Option<String> {
|
||||
match self.mode {
|
||||
Mode::Distance if !self.points.is_empty() && cursor.is_finite() => Some(crate::tr!(
|
||||
"measurement",
|
||||
"distance",
|
||||
value = crate::entities::common::format_length((cursor - self.points[0]).length())
|
||||
)),
|
||||
Mode::Area if self.points.len() >= 2 && cursor.is_finite() => {
|
||||
let mut points = self.points.clone();
|
||||
points.push(cursor);
|
||||
Some(Self::area_msg(self.plane, &points))
|
||||
}
|
||||
Mode::Angle if self.points.len() >= 2 => {
|
||||
Self::angle_msg(self.points[0], self.points[1], cursor)
|
||||
}
|
||||
Mode::Radius => self
|
||||
.hovered
|
||||
.as_ref()
|
||||
.and_then(|(_, e)| Self::radius_of(e))
|
||||
.map(Self::radius_msg),
|
||||
Mode::AngleLines => {
|
||||
let (first_handle, first) = self.first_line?;
|
||||
let (handle, entity) = self.hovered.as_ref()?;
|
||||
if first_handle == *handle {
|
||||
return None;
|
||||
}
|
||||
Self::line_angle(first, Self::line_direction(entity)?).map(Self::angle_value_msg)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
use crate::command::CmdOption;
|
||||
if self.mode != Mode::Choose {
|
||||
return Vec::new();
|
||||
return match self.mode {
|
||||
Mode::Angle if self.points.is_empty() => vec![CmdOption {
|
||||
label: crate::tr!("measurement", "lines"),
|
||||
keyword: "LINES".into(),
|
||||
}],
|
||||
Mode::AngleLines if self.first_line.is_none() => vec![CmdOption {
|
||||
label: crate::tr!("measurement", "points"),
|
||||
keyword: "POINTS".into(),
|
||||
}],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
}
|
||||
vec![
|
||||
CmdOption::new("Distance", "D"),
|
||||
@@ -251,6 +376,22 @@ impl CadCommand for MeasureGeomCommand {
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let input = text.trim().to_ascii_uppercase();
|
||||
if self.mode == Mode::Angle
|
||||
&& self.points.is_empty()
|
||||
&& matches!(input.as_str(), "L" | "LINES")
|
||||
{
|
||||
self.mode = Mode::AngleLines;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if self.mode == Mode::AngleLines
|
||||
&& self.first_line.is_none()
|
||||
&& matches!(input.as_str(), "P" | "POINTS")
|
||||
{
|
||||
self.mode = Mode::Angle;
|
||||
self.hovered = None;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if self.mode != Mode::Choose {
|
||||
return None;
|
||||
}
|
||||
@@ -266,7 +407,7 @@ impl CadCommand for MeasureGeomCommand {
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.mode == Mode::Radius
|
||||
matches!(self.mode, Mode::Radius | Mode::AngleLines)
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
@@ -277,30 +418,65 @@ impl CadCommand for MeasureGeomCommand {
|
||||
self.picked = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
fn wants_hover_entity(&self, handle: Handle) -> bool {
|
||||
self.needs_entity_pick()
|
||||
&& !handle.is_null()
|
||||
&& self.hovered.as_ref().map(|(h, _)| *h) != Some(handle)
|
||||
}
|
||||
|
||||
fn inject_hover_entity(&mut self, handle: Handle, entity: EntityType) {
|
||||
self.hovered = Some((handle, entity));
|
||||
}
|
||||
|
||||
fn on_hover_entity(&mut self, handle: Handle, _point: DVec3) -> Vec<WireModel> {
|
||||
if handle.is_null() || self.hovered.as_ref().map(|(h, _)| *h) != Some(handle) {
|
||||
self.hovered = None;
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||
// Never reuse an entity injected for a previous pick.
|
||||
let picked = self.picked.take();
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
match self.picked.as_ref().and_then(Self::radius_of) {
|
||||
Some(radius) => {
|
||||
let diameter = radius * 2.0;
|
||||
let radius_s = crate::entities::common::format_length(radius);
|
||||
let diameter_s = crate::entities::common::format_length(diameter);
|
||||
CmdResult::Measurement(
|
||||
t!(
|
||||
"Radius = %{radius}, Diameter = %{diameter}",
|
||||
radius = radius_s,
|
||||
diameter = diameter_s
|
||||
)
|
||||
.into_owned(),
|
||||
)
|
||||
match self.mode {
|
||||
Mode::Radius => match picked.as_ref().and_then(Self::radius_of) {
|
||||
Some(radius) => CmdResult::Measurement(Self::radius_msg(radius)),
|
||||
None => CmdResult::ReportError(crate::tr!("measurement", "invalid-radius")),
|
||||
},
|
||||
Mode::AngleLines => {
|
||||
let Some(direction) = picked.as_ref().and_then(Self::line_direction) else {
|
||||
return CmdResult::ReportError(crate::tr!("measurement", "invalid-line"));
|
||||
};
|
||||
if let Some((first_handle, first)) = self.first_line {
|
||||
if first_handle == handle {
|
||||
return CmdResult::ReportError(crate::tr!("measurement", "different-line"));
|
||||
}
|
||||
match Self::line_angle(first, direction) {
|
||||
Some(angle) => CmdResult::Measurement(Self::angle_value_msg(angle)),
|
||||
None => CmdResult::ReportError(crate::tr!("measurement", "invalid-line")),
|
||||
}
|
||||
} else {
|
||||
self.first_line = Some((handle, direction));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
// Picked something that is not a circle or arc — keep prompting.
|
||||
None => CmdResult::NeedPoint,
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if matches!(self.mode, Mode::Distance | Mode::Angle | Mode::Area) && !pt.is_finite() {
|
||||
return CmdResult::ReportError(crate::tr!("measurement", "invalid-point"));
|
||||
}
|
||||
if self.mode == Mode::Angle
|
||||
&& !self.points.is_empty()
|
||||
&& Self::direction(pt - self.points[0]).is_none()
|
||||
{
|
||||
return CmdResult::ReportError(crate::tr!("measurement", "degenerate-ray"));
|
||||
}
|
||||
match self.mode {
|
||||
Mode::Distance => {
|
||||
self.points.push(pt);
|
||||
@@ -317,11 +493,13 @@ impl CadCommand for MeasureGeomCommand {
|
||||
Mode::Angle => {
|
||||
self.points.push(pt);
|
||||
if self.points.len() == 3 {
|
||||
CmdResult::Measurement(Self::angle_msg(
|
||||
self.points[0],
|
||||
self.points[1],
|
||||
self.points[2],
|
||||
))
|
||||
match Self::angle_msg(self.points[0], self.points[1], self.points[2]) {
|
||||
Some(text) => CmdResult::Measurement(text),
|
||||
None => {
|
||||
self.points.pop();
|
||||
CmdResult::ReportError(crate::tr!("measurement", "degenerate-ray"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
@@ -360,6 +538,9 @@ impl CadCommand for MeasureGeomCommand {
|
||||
return None;
|
||||
}
|
||||
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| f(*p)).collect();
|
||||
if self.mode == Mode::Angle && self.points.len() >= 2 {
|
||||
pts = vec![f(self.points[1]), f(self.points[0])];
|
||||
}
|
||||
pts.push(f(pt));
|
||||
Some(Self::preview_wire("measuregeom_preview", pts))
|
||||
}
|
||||
@@ -379,5 +560,241 @@ impl CadCommand for MeasureGeomCommand {
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["MEASUREGEOM", "MEA"]
|
||||
names: &["MEASUREGEOM", "MEA", "MEASURERADIUS", "MEASUREANGLE"]
|
||||
}); // MeasureGeomCommand
|
||||
|
||||
#[cfg(test)]
|
||||
mod measurement_tests {
|
||||
use super::*;
|
||||
use crate::entities::common::{set_unit_context, unit_context};
|
||||
use codec::{
|
||||
entities::{Arc, Circle, Line},
|
||||
types::Vector3,
|
||||
};
|
||||
|
||||
fn units() {
|
||||
let mut context = unit_context();
|
||||
context.lunits = 2;
|
||||
context.luprec = 4;
|
||||
context.auprec = 2;
|
||||
set_unit_context(context);
|
||||
}
|
||||
fn result(result: CmdResult) -> String {
|
||||
let CmdResult::Measurement(text) = result else {
|
||||
panic!("expected measurement");
|
||||
};
|
||||
text
|
||||
}
|
||||
fn pick(command: &mut MeasureGeomCommand, entity: EntityType, id: u64) -> CmdResult {
|
||||
command.inject_picked_entity(entity);
|
||||
command.on_entity_pick(Handle::new(id), DVec3::ZERO)
|
||||
}
|
||||
fn line(end: Vector3) -> EntityType {
|
||||
EntityType::Line(Line::from_points(Vector3::ZERO, end))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radius_circle_and_arc_respect_length_precision() {
|
||||
units();
|
||||
for entity in [
|
||||
EntityType::Circle(Circle::from_coords(500000.0, 100000.0, 0.0, 2.34567)),
|
||||
EntityType::Arc(Arc::from_center_radius_angles(
|
||||
Vector3::ZERO,
|
||||
2.34567,
|
||||
0.0,
|
||||
std::f64::consts::FRAC_PI_2,
|
||||
)),
|
||||
] {
|
||||
let text = result(pick(&mut MeasureGeomCommand::radius(), entity, 1));
|
||||
assert!(text.contains("2.3457"), "{text}");
|
||||
assert!(text.contains("4.6913"), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radius_rejects_invalid_entities_and_stale_injection() {
|
||||
let mut command = MeasureGeomCommand::radius();
|
||||
assert!(matches!(
|
||||
pick(&mut command, line(Vector3::UNIT_X), 1),
|
||||
CmdResult::ReportError(_)
|
||||
));
|
||||
for radius in [0.0, -1.0, f64::NAN, f64::INFINITY] {
|
||||
assert!(matches!(
|
||||
pick(
|
||||
&mut command,
|
||||
EntityType::Circle(Circle::from_coords(0.0, 0.0, 0.0, radius)),
|
||||
1
|
||||
),
|
||||
CmdResult::ReportError(_)
|
||||
));
|
||||
}
|
||||
command.inject_picked_entity(EntityType::Circle(Circle::from_coords(0.0, 0.0, 0.0, 2.0)));
|
||||
assert!(matches!(
|
||||
command.on_entity_pick(Handle::NULL, DVec3::ZERO),
|
||||
CmdResult::NeedPoint
|
||||
));
|
||||
assert!(matches!(
|
||||
command.on_entity_pick(Handle::new(2), DVec3::ZERO),
|
||||
CmdResult::ReportError(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_angles_extreme_scales_and_degenerate_rays() {
|
||||
for degrees in [0.0_f64, 30.0, 45.0, 90.0, 135.0, 180.0] {
|
||||
let radians = degrees.to_radians();
|
||||
let ray = DVec3::new(radians.cos(), radians.sin(), 0.0);
|
||||
for scale in [1e-200, 1.0, 1e200] {
|
||||
let actual =
|
||||
MeasureGeomCommand::angle_degrees(DVec3::X * scale, ray * scale).unwrap();
|
||||
assert!((actual - degrees).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
MeasureGeomCommand::angle_degrees(DVec3::X, DVec3::Z),
|
||||
Some(90.0)
|
||||
);
|
||||
assert_eq!(
|
||||
MeasureGeomCommand::angle_degrees(DVec3::ZERO, DVec3::X),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
MeasureGeomCommand::angle_degrees(DVec3::NAN, DVec3::X),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_angle_points_do_not_advance_and_survey_precision_is_retained() {
|
||||
units();
|
||||
let mut command = MeasureGeomCommand::angle();
|
||||
let vertex = DVec3::new(500000.001, 600000.002, 100.0);
|
||||
assert!(matches!(
|
||||
command.on_point(DVec3::NAN),
|
||||
CmdResult::ReportError(_)
|
||||
));
|
||||
assert!(command.points.is_empty());
|
||||
command.on_point(vertex);
|
||||
assert!(matches!(
|
||||
command.on_point(vertex),
|
||||
CmdResult::ReportError(_)
|
||||
));
|
||||
assert_eq!(command.points.len(), 1);
|
||||
command.on_point(vertex + DVec3::X * 0.001);
|
||||
assert!(matches!(
|
||||
command.on_point(vertex),
|
||||
CmdResult::ReportError(_)
|
||||
));
|
||||
assert_eq!(command.points.len(), 2);
|
||||
assert!(result(command.on_point(vertex + DVec3::Y * 0.001)).contains("90.00°"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_preview_two_rays_and_live_value() {
|
||||
units();
|
||||
let mut command = MeasureGeomCommand::angle();
|
||||
command.on_point(DVec3::ZERO);
|
||||
assert!(command.measurement_readout(DVec3::Y).is_none());
|
||||
command.on_point(DVec3::X);
|
||||
assert!(command
|
||||
.measurement_readout(DVec3::Y)
|
||||
.unwrap()
|
||||
.contains("90.00°"));
|
||||
assert!(command
|
||||
.measurement_readout(DVec3::X + DVec3::Y)
|
||||
.unwrap()
|
||||
.contains("45.00°"));
|
||||
assert_eq!(
|
||||
command.on_mouse_move(DVec3::Y).unwrap().points,
|
||||
vec![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_lines_smaller_angle_and_invalid_selections() {
|
||||
units();
|
||||
let mut command = MeasureGeomCommand::angle();
|
||||
command.on_text_input("LINES");
|
||||
assert!(command.needs_entity_pick());
|
||||
assert!(matches!(
|
||||
pick(&mut command, line(Vector3::ZERO), 1),
|
||||
CmdResult::ReportError(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
pick(&mut command, line(Vector3::UNIT_X), 1),
|
||||
CmdResult::NeedPoint
|
||||
));
|
||||
assert!(matches!(
|
||||
pick(&mut command, line(Vector3::UNIT_X), 1),
|
||||
CmdResult::ReportError(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
pick(
|
||||
&mut command,
|
||||
EntityType::Circle(Circle::from_coords(0.0, 0.0, 0.0, 1.0)),
|
||||
2
|
||||
),
|
||||
CmdResult::ReportError(_)
|
||||
));
|
||||
assert!(
|
||||
result(pick(&mut command, line(Vector3::new(-1.0, -1.0, 0.0)), 2)).contains("45.00°")
|
||||
);
|
||||
assert_eq!(
|
||||
MeasureGeomCommand::line_angle(DVec3::X, -DVec3::X),
|
||||
Some(0.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_clears_on_miss_without_completing_measurement() {
|
||||
units();
|
||||
let mut command = MeasureGeomCommand::radius();
|
||||
let handle = Handle::new(5);
|
||||
assert!(command.wants_hover_entity(handle));
|
||||
command.inject_hover_entity(
|
||||
handle,
|
||||
EntityType::Circle(Circle::from_coords(0.0, 0.0, 0.0, 2.0)),
|
||||
);
|
||||
command.on_hover_entity(handle, DVec3::ZERO);
|
||||
assert!(command
|
||||
.measurement_readout(DVec3::ZERO)
|
||||
.unwrap()
|
||||
.contains("2.0000"));
|
||||
assert!(!command.wants_hover_entity(handle));
|
||||
assert!(command.picked.is_none());
|
||||
command.on_hover_entity(Handle::NULL, DVec3::ZERO);
|
||||
assert!(command.measurement_readout(DVec3::ZERO).is_none());
|
||||
command.inject_hover_entity(handle, line(Vector3::UNIT_X));
|
||||
assert!(command.measurement_readout(DVec3::ZERO).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measuregeom_all_four_modes_remain_available() {
|
||||
units();
|
||||
assert_eq!(MeasureGeomCommand::new().options().len(), 4);
|
||||
let mut command = MeasureGeomCommand::new();
|
||||
command.on_text_input("D");
|
||||
command.on_point(DVec3::ZERO);
|
||||
assert!(result(command.on_point(DVec3::new(3.0, 4.0, 0.0))).contains("5.0000"));
|
||||
let mut command = MeasureGeomCommand::new();
|
||||
command.on_text_input("AR");
|
||||
for p in [DVec3::ZERO, DVec3::X * 4.0, DVec3::Y * 3.0] {
|
||||
command.on_point(p);
|
||||
}
|
||||
let text = result(command.on_enter());
|
||||
assert!(text.contains("6.0000") && text.contains("12.0000"));
|
||||
let mut command = MeasureGeomCommand::new();
|
||||
command.on_text_input("A");
|
||||
command.on_point(DVec3::ZERO);
|
||||
command.on_point(DVec3::X);
|
||||
assert!(result(command.on_point(DVec3::Y)).contains("90.00°"));
|
||||
let mut command = MeasureGeomCommand::new();
|
||||
command.on_text_input("R");
|
||||
assert!(result(pick(
|
||||
&mut command,
|
||||
EntityType::Circle(Circle::from_coords(0.0, 0.0, 0.0, 3.0)),
|
||||
1
|
||||
))
|
||||
.contains("3.0000"));
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ impl CadModule for DrawModule {
|
||||
use clipboard::{copy_clip, cut, paste};
|
||||
use draw::{arc, circle, ellipse, hatch, line, polyline, shapes};
|
||||
use groups::{group, ungroup};
|
||||
use inquiry::{area, dist};
|
||||
use inquiry::{area, dist, measuregeom};
|
||||
use layers::{
|
||||
layfrz, layiso, laylck, layoff, layon, laythw, layulk, layuniso, make_current,
|
||||
match_layer, panel,
|
||||
@@ -244,6 +244,8 @@ impl CadModule for DrawModule {
|
||||
icon: dist::ICON,
|
||||
items: vec![
|
||||
("DIST", "Distance", dist::ICON),
|
||||
("MEASURERADIUS", "Radius", measuregeom::RADIUS_ICON),
|
||||
("MEASUREANGLE", "Angle", measuregeom::ANGLE_ICON),
|
||||
("AREA", "Area", area::ICON),
|
||||
],
|
||||
default: "DIST",
|
||||
|
||||
Reference in new issue
Block a user