From 3c55e6d7e1d690fd65f5ea84a1ab115d6688a28a Mon Sep 17 00:00:00 2001 From: turbuczn Date: Wed, 14 Oct 2026 14:03:12 +0200 Subject: [PATCH] feat(measure): add radius and angle tools with dynamic measurement overlay (cherry picked from commit aac44906a3b2918e1986a1cafd41e3b7b5bfe51c) --- assets/icons/measure-angle.svg | 4 + assets/icons/measure-radius.svg | 5 + docs/measurement-tools.md | 173 ++++++++ locales/ar-SA/opencadstudio.ftl | 17 + locales/bg-BG/opencadstudio.ftl | 17 + locales/cs-CZ/opencadstudio.ftl | 17 + locales/de-DE/opencadstudio.ftl | 17 + locales/el-GR/opencadstudio.ftl | 17 + locales/en-US/opencadstudio.ftl | 17 + locales/es-ES/opencadstudio.ftl | 17 + locales/fi-FI/opencadstudio.ftl | 17 + locales/fr-FR/opencadstudio.ftl | 17 + locales/hi-IN/opencadstudio.ftl | 17 + locales/hu-HU/opencadstudio.ftl | 17 + locales/it-IT/opencadstudio.ftl | 17 + locales/ja-JP/opencadstudio.ftl | 17 + locales/ko-KR/opencadstudio.ftl | 17 + locales/lo-LA/opencadstudio.ftl | 17 + locales/nl-NL/opencadstudio.ftl | 17 + locales/pl-PL/opencadstudio.ftl | 17 + locales/pt-BR/opencadstudio.ftl | 17 + locales/ru-RU/opencadstudio.ftl | 17 + locales/tr-TR/opencadstudio.ftl | 17 + locales/zh-CN/opencadstudio.ftl | 17 + locales/zh-TW/opencadstudio.ftl | 17 + src/app/command_driver/mod.rs | 4 + src/app/commands/draw.rs | 8 +- src/app/commands/mod.rs | 1 + src/app/control/mod.rs | 12 +- src/app/document.rs | 2 + src/app/measurement.rs | 288 +++++++++++++ src/app/mod.rs | 1 + src/app/update/command.rs | 1 + src/app/update/mod.rs | 1 + src/app/update/viewport.rs | 4 + src/app/view/mod.rs | 9 + src/app/view/overlay.rs | 120 +++++- src/command.rs | 11 + src/modules/draw/inquiry/area.rs | 103 +++++ src/modules/draw/inquiry/dist.rs | 39 ++ src/modules/draw/inquiry/measuregeom.rs | 513 +++++++++++++++++++++--- src/modules/draw/mod.rs | 4 +- 42 files changed, 1624 insertions(+), 53 deletions(-) create mode 100644 assets/icons/measure-angle.svg create mode 100644 assets/icons/measure-radius.svg create mode 100644 docs/measurement-tools.md create mode 100644 src/app/measurement.rs diff --git a/assets/icons/measure-angle.svg b/assets/icons/measure-angle.svg new file mode 100644 index 000000000..3a7bd686f --- /dev/null +++ b/assets/icons/measure-angle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/measure-radius.svg b/assets/icons/measure-radius.svg new file mode 100644 index 000000000..cea8f0301 --- /dev/null +++ b/assets/icons/measure-radius.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/measurement-tools.md b/docs/measurement-tools.md new file mode 100644 index 000000000..5ab06bbb5 --- /dev/null +++ b/docs/measurement-tools.md @@ -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` | diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 1b3ac2476..a6848f12a 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -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 } diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 57db6d081..0649b09aa 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -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 } diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 6c0eb0eae..5e5ebc79c 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -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 } diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index b87caa55a..0031cdfcd 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -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 } diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 47421fc23..dc8b4286c 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -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 } diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index aaa070afe..c8736d86f 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -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 } diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 350069c0f..965dc1d78 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -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 } diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index d7168d455..1d58ef2dd 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -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 } diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 213c37ce7..646c4da9f 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -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 } diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 5713ca073..574d0a2a8 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -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 } diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 6090437ed..b3515fd16 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -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 } diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 865bc395d..b0c8d9e9b 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -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 } diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 2e5a6dab4..c4fd50f57 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -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 } diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 47c2ac449..60066bc54 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -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 } diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index ea947238f..bff99fcdc 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -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 } diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index ba179b790..ea6da2744 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -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 } diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index f6c9c62f7..93bbda0ac 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -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 } diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index cef8d43c1..097102537 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -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 } diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index d5a77aac7..e994a5dcd 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -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 } diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 473a377c1..9fd2212c5 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -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 } diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 08dfd371f..f57c72aae 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -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 } diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 70af3b9ca..971c7df49 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -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 } diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index 652fdc623..c0bd44f59 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -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(); diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index ccdb7ba5c..d00d1fe9f 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -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)); } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index d3ff4c6b6..82415f89e 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -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; diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index caaff5e31..bbe0eba25 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -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" diff --git a/src/app/document.rs b/src/app/document.rs index 97a8eb9e3..7e5e14500 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -157,6 +157,7 @@ pub(super) struct DocumentTab { pub(super) properties: PropertiesPanel, pub(super) layers: LayerPanel, pub(super) active_cmd: Option>, + pub(super) measurement: super::measurement::MeasurementDisplay, /// Remaining command tokens queued behind a PAUSE (or `\`) awaiting user interaction. pub(super) pending_pause_tokens: Option>, /// 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, diff --git a/src/app/measurement.rs b/src/app/measurement.rs new file mode 100644 index 000000000..4b9db9588 --- /dev/null +++ b/src/app/measurement.rs @@ -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, + 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 { + 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::>(), + ["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")); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 651eaddca..a0c4a2888 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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; diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 7ca474523..c0d6666b5 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1249,6 +1249,7 @@ impl OpenCADStudio { } pub(super) fn on_command_escape(&mut self) -> Task { + 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); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 9797982ed..ca6c167ec 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -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). diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 00e68266f..301b8fa2a 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -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 { 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 { 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 { 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(); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 6473c1c65..cc4f07a1a 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -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 { diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index de085c127..03c8e0a0b 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -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 for ClampedPin<'_> { @@ -1008,7 +1053,9 @@ impl Widget 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 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 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 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); + } + } +} diff --git a/src/command.rs b/src/command.rs index 38ae3219e..830f98340 100644 --- a/src/command.rs +++ b/src/command.rs @@ -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 { + 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 { diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index ef60cc40a..5dba9d85a 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -29,6 +29,9 @@ pub struct AreaCommand { points: Vec, objects_gathering: bool, selected_entities: Vec, + // Recompute exact curve measurements only when the selection changes, + // not on every UI frame while showing the live readout. + selection_preview: Option, preview_regions: Vec, 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) { self.selected_entities = entities; + self.selection_preview = self.selection_measurement().map(|(value, _)| value); } fn on_selection_complete(&mut self, _handles: Vec) -> 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 { + 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 { 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()); + } +} diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index dd8386cc3..ab4c3c6d7 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -41,6 +41,17 @@ impl CadCommand for DistCommand { } } + fn has_measurement_readout(&self) -> bool { + true + } + + fn measurement_readout(&self, cursor: DVec3) -> Option { + 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); + } +} diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index 6e792e2a8..890405056 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -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, @@ -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 { + 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 { + 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 { + 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 { + 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 { - 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 { + 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 { + 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 { 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 { + 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 { + 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")); + } +} diff --git a/src/modules/draw/mod.rs b/src/modules/draw/mod.rs index 7fb2cc5a7..7ee8d37a8 100644 --- a/src/modules/draw/mod.rs +++ b/src/modules/draw/mod.rs @@ -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",