diff --git a/README.md b/README.md index 63f3437..d6b63d0 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ Ensure the following dependencies are installed: ### OCCT Build -#### For VS2022 +Full guide: **[docs/building-occt.md](docs/building-occt.md)** (Windows prebuilts, wasm/Emscripten, troubleshooting). + +#### For VS2022 (summary) - See: https://dev.opencascade.org/doc/overview/html/build_upgrade__building_occt.html - OCCT 3rd-party binaries: https://github.com/Open-Cascade-SAS/OCCT/releases/download/V7_9_1/3rdparty-vc14-64.zip - Currently building EzyCad has only been tested with the Release build of OCCT. @@ -60,6 +62,7 @@ Ensure the following dependencies are installed: ### Notes for Emscripten Builds - **Known issue:** The Emscripten build with the Ninja generator (`-G Ninja`) is currently not working. Use the default generator (e.g. `emcmake cmake ..` without `-G Ninja`, then `emmake make`) or another generator that works with your Emscripten setup. - Install Emscripten and activate its environment. +- **OCCT 8.0.0 for wasm:** `scripts\build-occt-v8-wasm.cmd` or `.\scripts\build-occt-v8-wasm.ps1` after `emsdk_env` — see [docs/building-occt.md](docs/building-occt.md#webassembly-emscripten). Experimental; manual 7.9.x steps below remain for reference. - Build FreeType (2.10.1) for Emscripten using the instructions: https://stackoverflow.com/questions/61049517/build-latest-freetype-with-emscripten - Add exception support: - `emcmake cmake .. -DCMAKE_CXX_FLAGS="-fexceptions" -DCMAKE_EXE_LINKER_FLAGS="-fexceptions" -DCMAKE_INSTALL_PREFIX=c:/src/freetype-2.10.1_em_install -DCMAKE_POLICY_VERSION_MINIMUM=3.5` diff --git a/agents/README.md b/agents/README.md index cdefeef..637de8b 100644 --- a/agents/README.md +++ b/agents/README.md @@ -8,4 +8,5 @@ To use a note in **Cursor**, copy or symlink the relevant file into your user or | --- | --- | | [ezycad-ascii-source.md](ezycad-ascii-source.md) | ASCII-only comments and strings in `src/`; points at `ezycad_code_style.md` and `scripts/check-nonascii-src.ps1`. | | [discoverability-outreach.md](discoverability-outreach.md) | Draft posts for forums, Reddit, awesome lists (SEO / backlinks). | +| *(repo root)* [docs/building-occt.md](../docs/building-occt.md) | Download/build OCCT for Windows desktop and wasm. | | *(repo root)* [ezycad_doc_style.md](../ezycad_doc_style.md) | User guides, Read the Docs, images, in-app doc URLs. | diff --git a/agents/issues/008-refactor1-dimensions-sketch-nodes-occt-wasm.md b/agents/issues/008-refactor1-dimensions-sketch-nodes-occt-wasm.md new file mode 100644 index 0000000..c86295c --- /dev/null +++ b/agents/issues/008-refactor1-dimensions-sketch-nodes-occt-wasm.md @@ -0,0 +1,63 @@ +# Branch `Trailcode/refactor1`: sketch nodes refactor, dimension settings, OCCT wasm build + +**Opened on GitHub:** https://github.com/trailcode/EzyCad/issues/113 + +**Branch:** `Trailcode/refactor1` (7 commits ahead of `main` at open time) + +**Labels:** `enhancement`, `documentation` + +--- + +## Title (GitHub) + +Sketch nodes PIMPL refactor, global dimension settings, OCCT 8 wasm build tooling + +## Body (GitHub) + +### Summary + +Tracking branch **`Trailcode/refactor1`**: refactor sketch-node internals, add persisted **Settings → Sketch → Dimensions** controls (line width, arrows, label rendering, visibility), fix permanent node annotation scale, and add **OCCT V8.0.0 WebAssembly** build scripts plus **`docs/building-occt.md`**. + +### Scope (implemented on branch) + +**Sketch nodes (`src/sketch_nodes.cpp`, `src/sketch_nodes.h`):** + +- PIMPL-style refactor (`Sketch_nodes` implementation detail moved behind `m_impl`). +- Snap / axis-guide behavior preserved; cleaner separation from `Sketch`. + +**Dimension style (`src/geom.h`, `src/geom.cpp`, `src/gui.*`, `src/sketch.cpp`, `src/occt_view.*`, `src/shp_extrude.*`):** + +- `Length_dimension_style` bundles global dimension display settings. +- `apply_length_dimension_style()` applies `Prs3d_DimensionAspect` + AIS Z-layer for labels. +- **Settings → Sketch → Dimensions** (nested): line width, arrow size/color, text scale, **label rendering** (0–5; default **Z-layer Topmost** to avoid grid ghosting), placement, arrow style/orientation, **show sketch dimensions**. +- `Occt_view::refresh_all_length_dimensions()` rebuilds dims when settings change. +- Removed non-functional flyout / extension-line settings after OCCT limitations. + +**Other:** + +- Permanent node annotation scale fix (`gui.permanent_node_anno_scale`). +- **`scripts/build-occt-v8-wasm.ps1`** + **`scripts/build-occt-v8-wasm.cmd`** — download FreeType, build OCCT `V8_0_0` static + GLES2 for Emscripten. +- **`docs/building-occt.md`** — Windows prebuilts, wasm build, troubleshooting, wrapper-script patterns. +- **`usage-settings.md`** updated for new dimension controls. + +### Out of scope / follow-ups + +- [ ] Verify **EzyCad wasm** links against OCCT 8 install produced by the new script (experimental; README still references 7.9.x manual path). +- [ ] Review commits **`Debug code`** / **`WIP`** — drop or gate debug-only changes before merge. +- [ ] Desktop upgrade to **OCCT 8.0.0** prebuilts (separate from wasm); retest grid + dimension labels after OCCT 8 grid shader changes. +- [ ] Consider typed enum for `edge_dim_text_render_mode` instead of magic `int` 0–5. + +### Test plan + +- [ ] **Settings → Sketch → Dimensions:** change each control; confirm live refresh on sketch length dimensions and extrude preview dims. +- [ ] **Label rendering:** try **Z-layer Top** / **Topmost** (defaults); confirm no grid bleed-through on labels. +- [ ] **Show sketch dimensions** global toggle; per-sketch list overrides still work. +- [ ] Sketch snap: dimension-tool hover, cross-sketch snap, add-node on edge interior (`Sketch_test` if available). +- [ ] **Permanent node annotation size** slider visible effect on sketch nodes. +- [ ] Desktop Release build; open/save settings JSON (`edge_dim_*` keys). +- [ ] (Optional) `.\scripts\build-occt-v8-wasm.ps1` completes; `emcmake` EzyCad with printed `OpenCASCADE_DIR`. + +### Links + +- Branch: `Trailcode/refactor1` +- Doc: `docs/building-occt.md` diff --git a/docs/building-occt.md b/docs/building-occt.md new file mode 100644 index 0000000..3227835 --- /dev/null +++ b/docs/building-occt.md @@ -0,0 +1,236 @@ +# Building Open CASCADE (OCCT) for EzyCad + +EzyCad does **not** vendor OCCT; builds live **outside** the tree. Point CMake at an install via `OpenCASCADE_DIR` (and on Windows desktop, `OCCT_3RD_PARTY_DIR`). + +**Official references** + +- [OCCT releases](https://github.com/Open-Cascade-SAS/OCCT/releases) +- [Build OCCT (overview)](https://dev.opencascade.org/doc/overview/html/build_upgrade__building_occt.html) +- [WebGL / wasm sample (upstream)](https://dev.opencascade.org/doc/occt-7.9.0/overview/html/occt_samples_webgl.html) + +--- + +## Version matrix (EzyCad) + +| Target | Documented / tested | Experimental | CMake variables | +| --- | --- | --- | --- | +| **Windows desktop** | OCCT **7.9.1** prebuilt | OCCT **8.0.0** prebuilt | `OpenCASCADE_DIR`, `OCCT_3RD_PARTY_DIR` | +| **WebAssembly** | OCCT **7.9.x** manual / [wasm-occ-demo](https://github.com/mathysyon/wasm-occ-demo) | OCCT **8.0.0** via `scripts/build-occt-v8-wasm.ps1` | `OpenCASCADE_DIR` only (static install) | + +After upgrading OCCT, retest **sketch dimensions**, **grid**, **fillet/chamfer/boolean**, and **STEP/STL** export. OCCT 8.0 changes grid rendering and many modeling paths. + +--- + +## Windows desktop (easiest): prebuilt binaries + +No OCCT compile. Use **Release** builds (EzyCad is tested with Release OCCT). + +### Download (7.9.1 — current README default) + +```text +# Combined (OCCT + 3rd-party, simplest layout): +https://github.com/Open-Cascade-SAS/OCCT/releases/download/V7_9_1/opencascade-7.9.1-vc14-64-combined.zip + +# Or separate: +https://github.com/Open-Cascade-SAS/OCCT/releases/download/V7_9_1/3rdparty-vc14-64.zip +# + a 7.9.1 opencascade-*-vc14-64 package from the V7_9_1 release page +``` + +### Download (8.0.0 — upgrade path) + +```text +https://github.com/Open-Cascade-SAS/OCCT/releases/download/V8_0_0/occt-combined-release-no-pch.zip +https://github.com/Open-Cascade-SAS/OCCT/releases/download/V8_0_0/3rdparty-vc14-64.zip +``` + +Unpack so `3rdparty-vc14-64` and the OCCT folder share a **parent directory** (required for DRAW; EzyCad only needs CMake paths). + +### Configure EzyCad + +```text +cmake C:\src\EzyCad -DOpenCASCADE_DIR=C:\bin\OCCT-install\cmake -DOCCT_3RD_PARTY_DIR=C:\bin\3rdparty-vc14-64 +``` + +- `OpenCASCADE_DIR` — directory containing `OpenCASCADEConfig.cmake` (often `...\cmake` or `...\lib\cmake\opencascade` depending on package layout; **verify on disk**). +- `OCCT_3RD_PARTY_DIR` — root of `3rdparty-vc14-64` (FreeType, TBB, FFmpeg DLLs copied next to `EzyCad.exe` per `CMakeLists.txt`). + +Use **Visual Studio** generator, **x64**, **Release** for the app. + +--- + +## Windows desktop: build from source + +Only when prebuilts are insufficient (custom flags, Debug OCCT, patches). + +1. Install **VS 2022** (C++), **CMake 3.16+**, **Git**. +2. Download [3rdparty-vc14-64.zip](https://github.com/Open-Cascade-SAS/OCCT/releases) matching the OCCT tag. +3. Clone OCCT, configure with CMake GUI or CLI, `BUILD_LIBRARY_TYPE=Shared` or `Static` as needed, enable `USE_OPENGL`, `USE_FREETYPE`, etc. per [build guide](https://dev.opencascade.org/doc/overview/html/build_upgrade__building_occt.html). +4. Build **INSTALL** target; set `OpenCASCADE_DIR` to the install prefix’s cmake package path. + +Prefer **prebuilt** packages unless you need a custom source build. + +--- + +## WebAssembly (Emscripten) + +EzyCad’s wasm target links **`TKOpenGles`** (not `TKOpenGl`), static OCCT, and **`-fexceptions`** (see `CMakeLists.txt` Emscripten block). + +### Automated: `scripts/build-occt-v8-wasm.ps1` + +Builds **FreeType 2.13.3** + **OCCT `V8_0_0`** static with GLES2 into a single install prefix. + +**Prerequisites:** Git, CMake 3.16+, [Emscripten emsdk](https://emscripten.org/docs/getting_started/downloads.html) 3.0+ with `emsdk_env` active (`emcc`, `emcmake`, `emmake` on `PATH`). + +**Layout created** (default `RootDir` = `%USERPROFILE%\occt-wasm-build`): + +```text +occt-wasm-build/ + src/OCCT/ git clone V8_0_0 + src/freetype-2.13.3/ from .tar.gz (not .tar.xz on Windows) + build/freetype/ + build/occt/ + install/ CMAKE_INSTALL_PREFIX + lib/cmake/opencascade/ → OpenCASCADE_DIR + freetype/ FreeType install (used at OCCT configure time) +``` + +**PowerShell (repo root, after `emsdk_env`):** + +```powershell +.\scripts\build-occt-v8-wasm.ps1 -RootDir C:\src\occt-wasm-build +``` + +Or use `scripts\build-occt-v8-wasm.cmd` if PowerShell script execution is restricted. + +**Script flags:** `-SkipDownload`, `-SkipFreeType`, `-SkipOcct`, `-ReconfigureOnly`, `-Jobs 8`, `-OcctTag V8_0_0`, `-BuildType Release`. + +**Expect:** 1–3+ hours compile, large disk use. Success prints `OpenCASCADE_DIR=...`. + +**Configure EzyCad wasm** (do **not** use `-G Ninja` for EzyCad — known issue in README): + +```text +mkdir build_em +cd build_em +emcmake cmake C:\src\EzyCad -Wno-dev -DOpenCASCADE_DIR=C:\src\occt-wasm-build\install\lib\cmake\opencascade +emmake cmake --build . --config Release +``` + +Serve: `python -m http.server 8000` from the build output directory. + +### OCCT wasm CMake flags (reference) + +Used by `build-occt-v8-wasm.ps1` — keep in sync if editing the script: + +| Variable | Value | Why | +| --- | --- | --- | +| `BUILD_LIBRARY_TYPE` | `Static` | `.a` archives linked into EzyCad.wasm | +| `BUILD_MODULE_Draw` | `OFF` | No Tcl/Tk harness | +| `USE_OPENGL` | `OFF` | Desktop GL driver | +| `USE_GLES2` | `ON` | `TKOpenGles` for WebGL2 | +| `USE_VTK` | `OFF` | Default in 8.0; avoids GLES VTK issues | +| `USE_FREETYPE` | `ON` | Dimension / text rendering | +| `USE_TBB`, `USE_FFMPEG`, `USE_FREEIMAGE`, `USE_OPENVR`, `USE_TK` | `OFF` | Reduce deps and build time | +| `CMAKE_*_FLAGS` | `-fexceptions` | Match EzyCad emscripten link flags | + +FreeType wasm configure also disables optional zlib/png/harfbuzz finds to simplify the emscripten build. + +### Wasm troubleshooting + +| Symptom | Fix | +| --- | --- | +| `running scripts is disabled` | `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` **or** use `build-occt-v8-wasm.cmd` **or** `powershell -ExecutionPolicy Bypass -File ...` | +| `Can't initialize filter; xz` | Script uses `.tar.gz` for FreeType; delete stale `*.tar.xz` under `src/` and re-run | +| `source directory .../build/freetype does not contain CMakeLists.txt` | Fixed: do not name a PowerShell function parameter `$Args` (shadows automatic `$Args`) | +| `emcc` not found | Run `emsdk_env.bat` / `emsdk_env.ps1` in the same shell | +| EzyCad configure hangs on `find_package(OpenCASCADE)` | `emcmake cmake ... --debug-output`; verify `OpenCASCADE_DIR` path | +| OCCT 8 + ghosted dimension labels | Retest `gui.edge_dim_text_render_mode` (Z-layer Topmost); grid compositing changed in 8.0 | + +### Legacy wasm path (7.9.x) + +Manual FreeType 2.10.1 + OCCT 7.9.0 per [wasm-occ-demo](https://github.com/mathysyon/wasm-occ-demo). Prefer the V8 script for new wasm OCCT work; keep 7.9.x until EzyCad wasm is verified on 8.0. + +--- + +## Wrapper scripts and automation + +When adding helper scripts under `scripts/`, follow existing repo conventions. + +### Windows `.cmd` (ExecutionPolicy bypass) + +`scripts/build-occt-v8-wasm.cmd` wraps the PowerShell script (same pattern as `check-nonascii-src.cmd`): + +```bat +@echo off +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build-occt-v8-wasm.ps1" %* +if errorlevel 1 exit /b 1 +``` + +**Batch + emsdk:** + +```bat +call C:\src\emsdk\emsdk_env.bat +cd /d C:\src\EzyCad +scripts\build-occt-v8-wasm.cmd -RootDir C:\src\occt-wasm-build +``` + +### Unix `.sh` + +No first-party wasm shell script yet; a `scripts/build-occt-v8-wasm.sh` may mirror the PowerShell logic: + +- `set -euo pipefail` +- Require `emcc`, `git`, `cmake` +- `ROOT="${ROOT:-$HOME/occt-wasm-build}"` +- Download `freetype-$VER.tar.gz` (gzip portable; avoid `.tar.xz` if `xz` missing) +- `emcmake cmake -S ... -B ...` with the same `-D` flags as the `.ps1` +- `emmake cmake --build ... --target install -j"$(nproc)"` +- Print `OpenCASCADE_DIR=$ROOT/install/lib/cmake/opencascade` + +Use forward slashes in cmake paths on Unix. + +### PowerShell one-liner (no policy change) + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File C:\src\EzyCad\scripts\build-occt-v8-wasm.ps1 -RootDir C:\src\occt-wasm-build +``` + +### curl downloads (CI) + +**Windows desktop 8.0 combined:** + +```bash +curl -L -o occt-combined.zip https://github.com/Open-Cascade-SAS/OCCT/releases/download/V8_0_0/occt-combined-release-no-pch.zip +``` + +**Windows desktop 7.9.1 3rdparty:** + +```bash +curl -L -o 3rdparty.zip https://github.com/Open-Cascade-SAS/OCCT/releases/download/V7_9_1/3rdparty-vc14-64.zip +``` + +### vcpkg (alternative desktop path) + +OCCT 8 supports vcpkg (`USE_VTK=ON` only if needed). EzyCad’s `CMakeLists.txt` is written for `find_package(OpenCASCADE)` + manual `OCCT_3RD_PARTY_DIR` DLL copies — vcpkg integration is **not** wired in-tree. + +--- + +## Maintaining OCCT version pins + +| File | What to update | +| --- | --- | +| `README.md` | Pin version, download URLs, example `OpenCASCADE_DIR` | +| `CMakeLists.txt` | `OCCT_3RD_PARTY_DIR` paths (freetype/tbb/ffmpeg versions in `DLLS_COMMON`) | +| `scripts/build-occt-v8-wasm.ps1` | `$OcctTag`, `$FreeTypeVersion`, cmake flags | +| `docs/building-occt.md` | This document | +| `src/ply_io.cpp` | Comments if triangulation API changes | + +--- + +## Quick reference: EzyCad CMake variables + +| Variable | Platform | Purpose | +| --- | --- | --- | +| `OpenCASCADE_DIR` | All | Path to `OpenCASCADEConfig.cmake` directory | +| `OCCT_3RD_PARTY_DIR` | Windows desktop | Root of 3rdparty bundle for runtime DLLs | +| `CMAKE_BUILD_TYPE` | Native / wasm | `Release` recommended | + +EzyCad wasm also sets `TKOpenGles` in `OpenCASCADE_LIBS` when `CMAKE_CXX_COMPILER_ID` is `Emscripten`. diff --git a/scripts/build-occt-v8-wasm.cmd b/scripts/build-occt-v8-wasm.cmd new file mode 100644 index 0000000..2d9deb9 --- /dev/null +++ b/scripts/build-occt-v8-wasm.cmd @@ -0,0 +1,5 @@ +@echo off +REM Build OCCT V8.0.0 for WebAssembly (see docs/building-occt.md). +REM Activate Emscripten first, e.g. call C:\src\emsdk\emsdk_env.bat +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build-occt-v8-wasm.ps1" %* +if errorlevel 1 exit /b 1 diff --git a/scripts/build-occt-v8-wasm.ps1 b/scripts/build-occt-v8-wasm.ps1 new file mode 100644 index 0000000..f0facc3 --- /dev/null +++ b/scripts/build-occt-v8-wasm.ps1 @@ -0,0 +1,210 @@ +# Download and build Open CASCADE Technology (OCCT) V8.0.0 for WebAssembly (Emscripten). +# +# Builds FreeType static libs, then OCCT static libs with GLES2 (TKOpenGles) for EzyCad's wasm target. +# +# Prerequisites: Git, CMake 3.16+, Emscripten SDK on PATH (run emsdk_env first). +# +# Usage (from repo root, after emsdk_env): +# .\scripts\build-occt-v8-wasm.ps1 -RootDir C:\src\occt-wasm-build +# +# Then configure EzyCad: +# emcmake cmake C:\src\EzyCad -Wno-dev -DOpenCASCADE_DIR=C:\src\occt-wasm-build\install\lib\cmake\opencascade + +#Requires -Version 5.1 +[CmdletBinding()] +param( + [string] $RootDir = (Join-Path $env:USERPROFILE "occt-wasm-build"), + [string] $OcctTag = "V8_0_0", + [string] $FreeTypeVersion = "2.13.3", + [ValidateSet("Release", "Debug", "RelWithDebInfo")] + [string] $BuildType = "Release", + [int] $Jobs = 0, + [switch] $SkipDownload, + [switch] $SkipFreeType, + [switch] $SkipOcct, + [switch] $ReconfigureOnly +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Require-Command([string]$Name) { + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + throw "Required command not found on PATH: $Name (is emsdk_env active?)" + } +} + +function Invoke-EmCMake { + param([string[]]$CmakeArguments) + & emcmake cmake @CmakeArguments + if ($LASTEXITCODE -ne 0) { throw "emcmake cmake failed (exit $LASTEXITCODE)" } +} + +function Invoke-EmInstall { + param([string]$BuildDir, [int]$Parallel) + Push-Location $BuildDir + try { + if ($Parallel -gt 0) { + & emmake cmake --build . --target install --parallel $Parallel + } + else { + & emmake cmake --build . --target install + } + if ($LASTEXITCODE -ne 0) { throw "install failed (exit $LASTEXITCODE)" } + } + finally { + Pop-Location + } +} + +Require-Command git +Require-Command cmake +Require-Command emcc +Require-Command emcmake +Require-Command emmake + +if ($Jobs -le 0) { + $Jobs = [Environment]::ProcessorCount + if ($Jobs -lt 2) { $Jobs = 2 } +} + +$RootDir = [System.IO.Path]::GetFullPath($RootDir) +$SrcDir = Join-Path $RootDir "src" +$BuildDir = Join-Path $RootDir "build" +$InstallDir = Join-Path $RootDir "install" +$OcctSrc = Join-Path $SrcDir "OCCT" +$FtSrc = Join-Path $SrcDir "freetype-$FreeTypeVersion" +$FtBuild = Join-Path $BuildDir "freetype" +$FtInstall = Join-Path $InstallDir "freetype" +$OcctBuild = Join-Path $BuildDir "occt" +$OcctInstall = $InstallDir + +$ExceptionFlags = "-fexceptions" + +Write-Host "=== OCCT $OcctTag WebAssembly build ===" -ForegroundColor Cyan +Write-Host "Root: $RootDir" +Write-Host "Install: $OcctInstall" +Write-Host "Jobs: $Jobs" +Write-Host "Type: $BuildType" +Write-Host "" + +New-Item -ItemType Directory -Force -Path $SrcDir, $BuildDir, $InstallDir | Out-Null + +# --- FreeType --- +if (-not $SkipFreeType) { + if (-not $SkipDownload -and -not (Test-Path $FtSrc)) { + Write-Host ">>> Download FreeType $FreeTypeVersion" -ForegroundColor Yellow + # Use .tar.gz: Windows built-in tar cannot extract .tar.xz without a separate xz tool. + $ftArchive = Join-Path $SrcDir "freetype-$FreeTypeVersion.tar.gz" + $ftUrl = "https://download.savannah.gnu.org/releases/freetype/freetype-$FreeTypeVersion.tar.gz" + Invoke-WebRequest -Uri $ftUrl -OutFile $ftArchive + & tar -xzf $ftArchive -C $SrcDir + if (-not (Test-Path $FtSrc)) { throw "FreeType extract failed: $FtSrc" } + } + + if (-not $ReconfigureOnly -or -not (Test-Path (Join-Path $FtBuild "CMakeCache.txt"))) { + Write-Host ">>> Configure FreeType (wasm)" -ForegroundColor Yellow + New-Item -ItemType Directory -Force -Path $FtBuild | Out-Null + Invoke-EmCMake @( + "-S", $FtSrc, + "-B", $FtBuild, + "-DCMAKE_BUILD_TYPE=$BuildType", + "-DCMAKE_INSTALL_PREFIX=$FtInstall", + "-DCMAKE_CXX_FLAGS=$ExceptionFlags", + "-DCMAKE_C_FLAGS=$ExceptionFlags", + "-DCMAKE_EXE_LINKER_FLAGS=$ExceptionFlags", + "-DBUILD_SHARED_LIBS=OFF", + "-DCMAKE_DISABLE_FIND_PACKAGE_ZLIB=ON", + "-DCMAKE_DISABLE_FIND_PACKAGE_BZip2=ON", + "-DCMAKE_DISABLE_FIND_PACKAGE_PNG=ON", + "-DCMAKE_DISABLE_FIND_PACKAGE_HarfBuzz=ON", + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" + ) + } + + Write-Host ">>> Build & install FreeType" -ForegroundColor Yellow + Invoke-EmInstall -BuildDir $FtBuild -Parallel $Jobs +} + +$FreeTypeDir = Join-Path $FtInstall "lib\cmake\freetype" +if (-not (Test-Path $FreeTypeDir)) { + throw "FreeType CMake package not found: $FreeTypeDir" +} + +# --- OCCT source --- +if (-not $SkipDownload) { + if (-not (Test-Path $OcctSrc)) { + Write-Host ">>> Clone OCCT ($OcctTag)" -ForegroundColor Yellow + & git clone --depth 1 --branch $OcctTag ` + https://github.com/Open-Cascade-SAS/OCCT.git $OcctSrc + if ($LASTEXITCODE -ne 0) { throw "git clone failed" } + } + else { + Write-Host ">>> OCCT source exists; checkout $OcctTag" -ForegroundColor Yellow + Push-Location $OcctSrc + try { + & git fetch --depth 1 origin tag $OcctTag + if ($LASTEXITCODE -ne 0) { throw "git fetch failed" } + & git checkout $OcctTag + if ($LASTEXITCODE -ne 0) { throw "git checkout failed" } + } + finally { + Pop-Location + } + } +} +elseif (-not (Test-Path $OcctSrc)) { + throw "OCCT source missing: $OcctSrc (omit -SkipDownload)" +} + +# --- OCCT build --- +if (-not $SkipOcct) { + if (-not $ReconfigureOnly -or -not (Test-Path (Join-Path $OcctBuild "CMakeCache.txt"))) { + Write-Host ">>> Configure OCCT (wasm static + GLES2)" -ForegroundColor Yellow + New-Item -ItemType Directory -Force -Path $OcctBuild | Out-Null + + # EzyCad links TKOpenGles, not TKOpenGl. + Invoke-EmCMake @( + "-S", $OcctSrc, + "-B", $OcctBuild, + "-DCMAKE_BUILD_TYPE=$BuildType", + "-DCMAKE_INSTALL_PREFIX=$OcctInstall", + "-DBUILD_LIBRARY_TYPE=Static", + "-DBUILD_MODULE_Draw=OFF", + "-DBUILD_SAMPLES=OFF", + "-DBUILD_DOC_Overview=OFF", + "-DUSE_OPENGL=OFF", + "-DUSE_GLES2=ON", + "-DUSE_VTK=OFF", + "-DUSE_FREETYPE=ON", + "-DUSE_TBB=OFF", + "-DUSE_FFMPEG=OFF", + "-DUSE_FREEIMAGE=OFF", + "-DUSE_OPENVR=OFF", + "-DUSE_TK=OFF", + "-Dfreetype_DIR=$FreeTypeDir", + "-DCMAKE_CXX_FLAGS=$ExceptionFlags", + "-DCMAKE_C_FLAGS=$ExceptionFlags", + "-DCMAKE_EXE_LINKER_FLAGS=$ExceptionFlags", + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" + ) + } + + Write-Host ">>> Build & install OCCT (may take 1-3+ hours)" -ForegroundColor Yellow + Invoke-EmInstall -BuildDir $OcctBuild -Parallel $Jobs +} + +$OcctConfig = Join-Path $OcctInstall "lib\cmake\opencascade\OpenCASCADEConfig.cmake" +if (-not (Test-Path $OcctConfig)) { + throw "OCCT install incomplete; missing: $OcctConfig" +} + +$OpenCascadeDir = Join-Path $OcctInstall "lib\cmake\opencascade" +Write-Host "" +Write-Host "=== Done ===" -ForegroundColor Green +Write-Host "OpenCASCADE_DIR=$OpenCascadeDir" +Write-Host "" +Write-Host "EzyCad wasm configure example:" +Write-Host " mkdir build_em" +Write-Host " cd build_em" +Write-Host " emcmake cmake $((Split-Path -Parent $PSScriptRoot)) -Wno-dev -DOpenCASCADE_DIR=$OpenCascadeDir" diff --git a/src/geom.cpp b/src/geom.cpp index 338976c..3592a6e 100644 --- a/src/geom.cpp +++ b/src/geom.cpp @@ -18,9 +18,16 @@ #include #include #include +#include +#include +#include +#include +#include #include +#include #include #include +#include #include #include #include @@ -539,21 +546,144 @@ Prs3d_DimensionTextHorizontalPosition edge_dim_text_h_pos_from_index(int idx) } } -static void apply_length_dimension_text_h_position(const PrsDim_LengthDimension_ptr& dim, - const Prs3d_DimensionTextHorizontalPosition text_h_pos) +namespace { - if (dim.IsNull()) - return; +constexpr double k_dim_text_height_base = 16.0; +Handle(Prs3d_DimensionAspect) clone_dimension_aspect(const PrsDim_LengthDimension_ptr& dim) +{ + if (dim.IsNull()) + return new Prs3d_DimensionAspect(); const Handle(Prs3d_DimensionAspect)& cur = dim->DimensionAspect(); - Handle(Prs3d_DimensionAspect) aspect; if (!cur.IsNull()) - aspect = new Prs3d_DimensionAspect(*cur); + return new Prs3d_DimensionAspect(*cur); + return new Prs3d_DimensionAspect(); +} + +void arrow_style_preset(const int arrow_style, double& angle_deg, bool& arrows_3d) +{ + switch (arrow_style) + { + case 1: + angle_deg = 15.0; + arrows_3d = false; + break; + case 2: + angle_deg = 40.0; + arrows_3d = false; + break; + case 3: + angle_deg = 25.0; + arrows_3d = true; + break; + default: + angle_deg = 25.0; + arrows_3d = false; + break; + } +} + +Prs3d_DimensionArrowOrientation arrow_orientation_from_index(const int idx) +{ + switch (idx) + { + case 1: + return Prs3d_DAO_Internal; + case 2: + return Prs3d_DAO_External; + default: + return Prs3d_DAO_Fit; + } +} + +void apply_dimension_label_text_aspect(const Handle(Prs3d_TextAspect)& text, const Quantity_Color& col, + const Length_dimension_style& style) +{ + text->SetColor(col); + text->SetHeight(k_dim_text_height_base * static_cast(style.text_height_scale)); + + Handle(Graphic3d_AspectText3d) gtext = new Graphic3d_AspectText3d(); + gtext->SetColor(col); + gtext->SetDisplayType(Aspect_TODT_NORMAL); + gtext->SetStyle(Aspect_TOST_NORMAL); + gtext->SetAlphaMode(Graphic3d_AlphaMode_Opaque); + text->SetAspect(gtext); +} +} // namespace + +double length_dimension_auto_flyout(const double edge_len) +{ + constexpr double k_min_flyout = 15.0; + constexpr double k_edge_fraction = 0.12; + return std::max(k_min_flyout, edge_len * k_edge_fraction); +} + +void apply_length_dimension_style(const PrsDim_LengthDimension_ptr& dim, const Length_dimension_style& style) +{ + if (dim.IsNull()) + return; + + Handle(Prs3d_DimensionAspect) aspect = clone_dimension_aspect(dim); + + const Quantity_Color col(style.color_rgb[0], style.color_rgb[1], style.color_rgb[2], Quantity_TOC_RGB); + + Aspect_TypeOfLine typ = Aspect_TOL_SOLID; + if (const Handle(Prs3d_LineAspect)& la = aspect->LineAspect(); !la.IsNull()) + typ = la->Aspect()->Type(); + + aspect->SetLineAspect(new Prs3d_LineAspect(col, typ, static_cast(style.line_width))); + + double angle_deg{}; + bool arrows_3d{}; + arrow_style_preset(style.arrow_style, angle_deg, arrows_3d); + aspect->MakeArrows3d(arrows_3d); + aspect->SetArrowOrientation(arrow_orientation_from_index(style.arrow_orientation)); + + Handle(Prs3d_ArrowAspect) arrow; + if (const Handle(Prs3d_ArrowAspect)& cur_arrow = aspect->ArrowAspect(); !cur_arrow.IsNull()) + arrow = new Prs3d_ArrowAspect(*cur_arrow); else - aspect = new Prs3d_DimensionAspect(); + arrow = new Prs3d_ArrowAspect(); + arrow->SetColor(col); + arrow->SetLength(static_cast(style.arrow_size)); + arrow->SetAngle(angle_deg * (std::numbers::pi / 180.0)); + aspect->SetArrowAspect(arrow); + + if (style.text_render_mode == 1) + { + aspect->SetCommonColor(col); + Handle(Prs3d_TextAspect) text = aspect->TextAspect(); + if (text.IsNull()) + text = new Prs3d_TextAspect(); + text->SetHeight(k_dim_text_height_base * static_cast(style.text_height_scale)); + aspect->SetTextAspect(text); + } + else + { + Handle(Prs3d_TextAspect) text = new Prs3d_TextAspect(); + apply_dimension_label_text_aspect(text, col, style); + aspect->SetTextAspect(text); + } + + aspect->MakeText3d(style.text_render_mode == 3); + aspect->MakeTextShaded(false); + + aspect->SetTextHorizontalPosition(edge_dim_text_h_pos_from_index(style.label_h)); - aspect->SetTextHorizontalPosition(text_h_pos); dim->SetDimensionAspect(aspect); + + switch (style.text_render_mode) + { + case 4: + dim->SetZLayer(Graphic3d_ZLayerId_Top); + break; + case 5: + dim->SetZLayer(Graphic3d_ZLayerId_Topmost); + break; + default: + dim->SetZLayer(Graphic3d_ZLayerId_Default); + break; + } } void apply_length_dimension_line_width(const PrsDim_LengthDimension_ptr& dim, const double line_width) @@ -561,12 +691,7 @@ void apply_length_dimension_line_width(const PrsDim_LengthDimension_ptr& dim, co if (dim.IsNull()) return; - const Handle(Prs3d_DimensionAspect)& cur = dim->DimensionAspect(); - Handle(Prs3d_DimensionAspect) aspect; - if (!cur.IsNull()) - aspect = new Prs3d_DimensionAspect(*cur); - else - aspect = new Prs3d_DimensionAspect(); + Handle(Prs3d_DimensionAspect) aspect = clone_dimension_aspect(dim); Quantity_Color col = Quantity_NOC_YELLOW; Aspect_TypeOfLine typ = Aspect_TOL_SOLID; @@ -577,8 +702,7 @@ void apply_length_dimension_line_width(const PrsDim_LengthDimension_ptr& dim, co typ = g->Type(); } - Handle(Prs3d_LineAspect) new_line = new Prs3d_LineAspect(col, typ, static_cast(line_width)); - aspect->SetLineAspect(new_line); + aspect->SetLineAspect(new Prs3d_LineAspect(col, typ, static_cast(line_width))); dim->SetDimensionAspect(aspect); } @@ -587,12 +711,7 @@ void apply_length_dimension_arrow_size(const PrsDim_LengthDimension_ptr& dim, co if (dim.IsNull()) return; - const Handle(Prs3d_DimensionAspect)& cur = dim->DimensionAspect(); - Handle(Prs3d_DimensionAspect) aspect; - if (!cur.IsNull()) - aspect = new Prs3d_DimensionAspect(*cur); - else - aspect = new Prs3d_DimensionAspect(); + Handle(Prs3d_DimensionAspect) aspect = clone_dimension_aspect(dim); Handle(Prs3d_ArrowAspect) arrow; if (const Handle(Prs3d_ArrowAspect)& cur_arrow = aspect->ArrowAspect(); !cur_arrow.IsNull()) @@ -608,7 +727,8 @@ void apply_length_dimension_arrow_size(const PrsDim_LengthDimension_ptr& dim, co // OCCT draws the dimension on the side given by (plane_normal x edge_vector) for positive flyout. // When that side faces the sketch interior, negate flyout so the annotation sits outside the loop. static void orient_length_dimension_flyout_outward(const PrsDim_LengthDimension_ptr& dim, const gp_Pnt& p1, const gp_Pnt& p2, - const gp_Pnt& interior_ref, const gp_Pln& pln) + const gp_Pnt& interior_ref, const gp_Pln& pln, + const Length_dimension_style& style) { if (dim.IsNull()) return; @@ -627,10 +747,8 @@ static void orient_length_dimension_flyout_outward(const PrsDim_LengthDimension_ if (to_in.SquareMagnitude() < Precision::SquareConfusion()) return; - Standard_Real f = dim->GetFlyout(); const double edge_len = std::sqrt(attach.SquareMagnitude()); - if (std::abs(f) < Precision::Confusion()) - f = std::max(15.0, edge_len * 0.12); + const double f = length_dimension_auto_flyout(edge_len); if (fly_pos.Dot(to_in) > 0.0) dim->SetFlyout(-std::abs(f)); @@ -650,7 +768,8 @@ static bool point_strictly_inside_sketch_faces(const gp_Pnt& p, const std::vecto // Returns true if flyout sign was chosen from face classification. static bool orient_length_dimension_flyout_clear_of_faces(const PrsDim_LengthDimension_ptr& dim, const gp_Pnt& p1, const gp_Pnt& p2, const gp_Pln& pln, - const std::vector& faces) + const std::vector& faces, + const Length_dimension_style& style) { if (dim.IsNull() || faces.empty()) return false; @@ -666,10 +785,8 @@ static bool orient_length_dimension_flyout_clear_of_faces(const PrsDim_LengthDim fly_pos.Normalize(); - Standard_Real f = dim->GetFlyout(); - const double edge_len = std::sqrt(attach.SquareMagnitude()); - if (std::abs(f) < Precision::Confusion()) - f = std::max(15.0, edge_len * 0.12); + const double edge_len = std::sqrt(attach.SquareMagnitude()); + const double f = length_dimension_auto_flyout(edge_len); gp_Pnt mid = p1.Translated(attach.Multiplied(0.5)); @@ -696,40 +813,35 @@ static bool orient_length_dimension_flyout_clear_of_faces(const PrsDim_LengthDim } PrsDim_LengthDimension_ptr create_distance_annotation(const gp_Pnt& p1, const gp_Pnt& p2, const gp_Pln& pln, - const Prs3d_DimensionTextHorizontalPosition text_h_pos, - const std::optional& interior_ref, - const std::vector* sketch_faces_for_flyout, - const double dimension_line_width, const double dimension_arrow_size) + const Length_dimension_style& style, + const std::optional& interior_ref, + const std::vector* sketch_faces_for_flyout) { - // Check if points are too close (invalid for dimension) EZY_ASSERT(unique(p1, p2)); - // Measure between points (not TopoDS_Vertex), so OCCT does not draw vertex-attachment handles at the ends. PrsDim_LengthDimension_ptr dim = new PrsDim_LengthDimension(p1, p2, pln); - apply_length_dimension_text_h_position(dim, text_h_pos); - apply_length_dimension_line_width(dim, static_cast(dimension_line_width)); - apply_length_dimension_arrow_size(dim, static_cast(dimension_arrow_size)); + apply_length_dimension_style(dim, style); bool used_faces = false; if (sketch_faces_for_flyout && !sketch_faces_for_flyout->empty()) - used_faces = orient_length_dimension_flyout_clear_of_faces(dim, p1, p2, pln, *sketch_faces_for_flyout); + used_faces = orient_length_dimension_flyout_clear_of_faces(dim, p1, p2, pln, *sketch_faces_for_flyout, style); if (!used_faces && interior_ref.has_value()) - orient_length_dimension_flyout_outward(dim, p1, p2, *interior_ref, pln); + orient_length_dimension_flyout_outward(dim, p1, p2, *interior_ref, pln, style); + else if (!used_faces) + { + const double f = length_dimension_auto_flyout(p1.Distance(p2)); + dim->SetFlyout(static_cast(f)); + } return dim; } PrsDim_LengthDimension_ptr create_distance_annotation(const gp_Pnt2d& p1, const gp_Pnt2d& p2, const gp_Pln& pln, - const Prs3d_DimensionTextHorizontalPosition text_h_pos, - const std::optional& interior_ref, - const std::vector* sketch_faces_for_flyout, - const double dimension_line_width, const double dimension_arrow_size) + const Length_dimension_style& style, + const std::optional& interior_ref, + const std::vector* sketch_faces_for_flyout) { - gp_Pnt point_1 = to_3d(pln, p1); - gp_Pnt point_2 = to_3d(pln, p2); - - return create_distance_annotation(point_1, point_2, pln, text_h_pos, interior_ref, sketch_faces_for_flyout, - dimension_line_width, dimension_arrow_size); + return create_distance_annotation(to_3d(pln, p1), to_3d(pln, p2), pln, style, interior_ref, sketch_faces_for_flyout); } const gp_Pnt& closest_to_camera(const V3d_View_ptr& view, const std::vector& pnts) diff --git a/src/geom.h b/src/geom.h index ff10b3a..ce1bf90 100644 --- a/src/geom.h +++ b/src/geom.h @@ -117,9 +117,31 @@ gp_Pnt2d get_midpoint(const gp_Pnt2d& p1, const gp_Pnt2d& p2); gp_Pnt2d mirror_point(const gp_Pnt2d& p1, const gp_Pnt2d& p2, const gp_Pnt2d& point_to_mirror); -/// Maps Options -> edge length label index (0-3) to OCCT horizontal text placement. +/// Global length-dimension display settings (GUI / `ezycad_settings.json` -> `gui.*`). +struct Length_dimension_style +{ + float line_width = 1.0f; + float arrow_size = 6.0f; + float color_rgb[3] = {1.f, 1.f, 0.f}; + float text_height_scale = 1.0f; + int label_h = 3; + /// 0 standard, 1 sharp, 2 wide, 3 shaded 3D (see `edge_dim_arrow_style` in settings). + int arrow_style = 0; + /// 0 fit, 1 internal, 2 external (`Prs3d_DAO_*`). + int arrow_orientation = 0; + /// Label rendering mode (`gui.edge_dim_text_render_mode`): 0..5, default 5 (Z-layer Topmost). + int text_render_mode = 5; +}; + +/// Maps edge length label index (0-3) to OCCT horizontal text placement. Prs3d_DimensionTextHorizontalPosition edge_dim_text_h_pos_from_index(int idx); +/// Automatic flyout distance from edge length (before per-dimension override in Sketch List). +double length_dimension_auto_flyout(double edge_len); + +/// Apply full dimension aspect (line, text, arrows, extensions). Call `Redisplay` after. +void apply_length_dimension_style(const PrsDim_LengthDimension_ptr& dim, const Length_dimension_style& style); + /// Rebuild dimension line aspect with \a line_width (call `Redisplay` on the AIS object after). void apply_length_dimension_line_width(const PrsDim_LengthDimension_ptr& dim, double line_width); /// Rebuild dimension arrow aspect with \a arrow_size (call `Redisplay` on the AIS object after). @@ -128,18 +150,15 @@ void apply_length_dimension_arrow_size(const PrsDim_LengthDimension_ptr& dim, do /// When `sketch_faces_for_flyout` is non-null and non-empty, edge dimensions offset to the side that is /// void (not TopAbs_IN) relative to those faces - fixes concave / notch edges where the node centroid lies /// on the wrong side. Otherwise `interior_ref` (e.g. node centroid) is used as a weaker heuristic. -/// OCCT line width scale factor for dimension lines (1.0 = default). PrsDim_LengthDimension_ptr create_distance_annotation(const gp_Pnt& p1, const gp_Pnt& p2, const gp_Pln& pln, - Prs3d_DimensionTextHorizontalPosition text_h_pos = Prs3d_DTHP_Fit, - const std::optional& interior_ref = std::nullopt, - const std::vector* sketch_faces_for_flyout = nullptr, - double dimension_line_width = 1.0, double dimension_arrow_size = 6.0); + const Length_dimension_style& style, + const std::optional& interior_ref = std::nullopt, + const std::vector* sketch_faces_for_flyout = nullptr); PrsDim_LengthDimension_ptr create_distance_annotation(const gp_Pnt2d& p1, const gp_Pnt2d& p2, const gp_Pln& pln, - Prs3d_DimensionTextHorizontalPosition text_h_pos = Prs3d_DTHP_Fit, - const std::optional& interior_ref = std::nullopt, - const std::vector* sketch_faces_for_flyout = nullptr, - double dimension_line_width = 1.0, double dimension_arrow_size = 6.0); + const Length_dimension_style& style, + const std::optional& interior_ref = std::nullopt, + const std::vector* sketch_faces_for_flyout = nullptr); const gp_Pnt& closest_to_camera(const V3d_View_ptr& view, const std::vector& pnts); diff --git a/src/gui.cpp b/src/gui.cpp index c40f3dc..8fad232 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -43,6 +43,36 @@ GUI::GUI() m_view = std::make_unique(*this); gui_instance = this; } + +Length_dimension_style GUI::length_dimension_style() const +{ + Length_dimension_style s{}; + s.line_width = m_edge_dim_line_width; + s.arrow_size = m_edge_dim_arrow_size; + s.color_rgb[0] = m_edge_dim_color[0]; + s.color_rgb[1] = m_edge_dim_color[1]; + s.color_rgb[2] = m_edge_dim_color[2]; + s.text_height_scale = m_edge_dim_text_scale; + s.label_h = m_edge_dim_label_h; + s.arrow_style = m_edge_dim_arrow_style; + s.arrow_orientation = m_edge_dim_arrow_orientation; + s.text_render_mode = m_edge_dim_text_render_mode; + return s; +} + +void GUI::set_show_sketch_dimensions(const bool show) +{ + if (m_show_sketch_dimensions == show) + return; + m_show_sketch_dimensions = show; + apply_sketch_dimensions_visibility(); +} + +void GUI::apply_sketch_dimensions_visibility() +{ + if (m_view) + m_view->apply_sketch_dimensions_visibility(); +} ImFont* GUI::console_font() const { if (!m_console_font || !ImGui::GetCurrentContext()) diff --git a/src/gui.h b/src/gui.h index 383d94c..07dcb24 100644 --- a/src/gui.h +++ b/src/gui.h @@ -15,6 +15,7 @@ #include #include +#include "geom.h" #include "imgui.h" #include "imgui_markdown.h" #include "log.h" @@ -59,6 +60,31 @@ struct Example_file inline constexpr float k_gui_edge_dim_line_width_default = 1.0f; /// Default OCCT arrow length for length dimensions when `edge_dim_arrow_size` is missing from settings JSON. inline constexpr float k_gui_edge_dim_arrow_size_default = 6.0f; +/// Default dimension line/text RGB when `edge_dim_color` is missing (yellow). +inline constexpr float k_gui_edge_dim_color_default[3] = {1.f, 1.f, 0.f}; +/// Text height scale for length dimension labels (`gui.edge_dim_text_scale`). +inline constexpr float k_gui_edge_dim_text_scale_min = 0.5f; +inline constexpr float k_gui_edge_dim_text_scale_max = 3.0f; +inline constexpr float k_gui_edge_dim_text_scale_default = 1.0f; +/// Arrow style preset index (`gui.edge_dim_arrow_style`): 0 standard, 1 sharp, 2 wide, 3 3D. +inline constexpr int k_gui_edge_dim_arrow_style_min = 0; +inline constexpr int k_gui_edge_dim_arrow_style_max = 3; +/// Arrow orientation (`gui.edge_dim_arrow_orientation`): 0 fit, 1 internal, 2 external. +inline constexpr int k_gui_edge_dim_arrow_orientation_min = 0; +inline constexpr int k_gui_edge_dim_arrow_orientation_max = 2; +/// `gui.edge_dim_text_render_mode` indices. +inline constexpr int k_gui_edge_dim_text_render_opaque_2d = 0; +inline constexpr int k_gui_edge_dim_text_render_common_color = 1; +inline constexpr int k_gui_edge_dim_text_render_2d_screen = 2; +inline constexpr int k_gui_edge_dim_text_render_3d_text = 3; +inline constexpr int k_gui_edge_dim_text_render_z_top = 4; +inline constexpr int k_gui_edge_dim_text_render_z_topmost = 5; +inline constexpr int k_gui_edge_dim_text_render_mode_default = k_gui_edge_dim_text_render_z_topmost; +inline constexpr int k_gui_edge_dim_text_render_mode_max = k_gui_edge_dim_text_render_z_topmost; +/// Scale factor for permanent sketch-node '+' annotations (`gui.permanent_node_anno_scale`). +inline constexpr float k_gui_permanent_node_anno_scale_min = 0.25f; +inline constexpr float k_gui_permanent_node_anno_scale_max = 3.0f; +inline constexpr float k_gui_permanent_node_anno_scale_default = 1.0f; /// Allowed range and default for `gui.view_roll_step_deg` (view roll and numpad orbit steps; must match Settings slider). inline constexpr double k_gui_view_roll_step_deg_min = 0.1; inline constexpr double k_gui_view_roll_step_deg_max = 180.0; @@ -98,12 +124,21 @@ class GUI Mode get_mode() const { return m_mode; } Chamfer_mode get_chamfer_mode() const { return m_chamfer_mode; } Fillet_mode get_fillet_mode() const { return m_fillet_mode; } - /// Edge dimension value placement (Options panel, toggle-dimension tool): 0 first point, 1 second, 2 center, 3 auto. + /// Edge dimension value placement: 0 first point, 1 second, 2 center, 3 auto. int edge_dim_label_h() const { return m_edge_dim_label_h; } /// OCCT scale factor for sketch/extrude length dimension lines (1.0 = default thickness). float edge_dim_line_width() const { return m_edge_dim_line_width; } /// OCCT arrow length for sketch/extrude length dimensions. - float edge_dim_arrow_size() const { return m_edge_dim_arrow_size; } + float edge_dim_arrow_size() const { return m_edge_dim_arrow_size; } + /// When false, length dimensions are hidden on all sketches until re-enabled. + bool show_sketch_dimensions() const { return m_show_sketch_dimensions; } + void set_show_sketch_dimensions(bool show); + /// Bundle of global length-dimension display settings for OCCT annotations. + [[nodiscard]] Length_dimension_style length_dimension_style() const; + /// Reapply dimension visibility on all sketches (global show flag + current tool mode). + void apply_sketch_dimensions_visibility(); + /// Scale factor for permanent sketch-node '+' annotations. + float permanent_node_anno_scale() const { return m_permanent_node_anno_scale; } bool get_hide_all_shapes() const { return m_hide_all_shapes; } void set_hide_all_shapes(bool hide) { m_hide_all_shapes = hide; } /// Orthographic camera in Inspection mode (Mode::Normal); persisted as `gui.inspection_orthographic`. @@ -166,8 +201,8 @@ class GUI void save_occt_view_settings(); - /// JSON for scripting: `occt_view` (background, grid) plus `gui.edge_dim_label_h` / `gui.edge_dim_line_width` / - /// `gui.edge_dim_arrow_size` (same keys as `ezycad_settings.json`). Asserts if the OCCT view is missing. + /// JSON for scripting: `occt_view` (background, grid) plus sketch dimension keys under `gui.*` (see `ezycad_settings.json`). + /// Asserts if the OCCT view is missing. [[nodiscard]] std::string occt_view_settings_json() const; /// Default RGBA (0-255) for sketch underlay line tint when importing a new image (see Settings). @@ -304,9 +339,17 @@ class GUI Mode m_mode = Mode::Normal; Chamfer_mode m_chamfer_mode = Chamfer_mode::Shape; Fillet_mode m_fillet_mode = Fillet_mode::Shape; - int m_edge_dim_label_h = 3; // Prs3d_DTHP_Fit - float m_edge_dim_line_width = k_gui_edge_dim_line_width_default; - float m_edge_dim_arrow_size = k_gui_edge_dim_arrow_size_default; + int m_edge_dim_label_h = 3; + float m_edge_dim_line_width = k_gui_edge_dim_line_width_default; + float m_edge_dim_arrow_size = k_gui_edge_dim_arrow_size_default; + float m_edge_dim_color[3] = {k_gui_edge_dim_color_default[0], k_gui_edge_dim_color_default[1], + k_gui_edge_dim_color_default[2]}; + float m_edge_dim_text_scale = k_gui_edge_dim_text_scale_default; + int m_edge_dim_arrow_style = 0; + int m_edge_dim_arrow_orientation = 0; + int m_edge_dim_text_render_mode = k_gui_edge_dim_text_render_mode_default; + bool m_show_sketch_dimensions = true; + float m_permanent_node_anno_scale = k_gui_permanent_node_anno_scale_default; /// Degrees per numpad orbit (8/2/4/6) and Blender-style roll (Shift+NumPad 4/6); persisted in `gui.view_roll_step_deg`. double m_view_roll_step_deg = k_gui_view_roll_step_deg_default; /// Multiplier for `UpdateZoom(Aspect_ScrollDelta(..., int(y * scale)))`; persisted in `gui.view_zoom_scroll_scale`. diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 08f1e0d..cf138d7 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -387,8 +387,6 @@ void GUI::options_() float sketch_label_col_w = ImGui::CalcTextSize("Snap dist").x; sketch_label_col_w = std::max(sketch_label_col_w, ImGui::CalcTextSize("Snap guide mode").x); - if (get_mode() == Mode::Sketch_dim_anno) - sketch_label_col_w = std::max(sketch_label_col_w, ImGui::CalcTextSize("Length value placement").x); if (get_mode() == Mode::Sketch_face_extrude) { sketch_label_col_w = std::max(sketch_label_col_w, ImGui::CalcTextSize("Both sides").x); @@ -433,40 +431,6 @@ void GUI::options_() "Fullscreen: full-view crosshair/axis guides.\n" "Both: show compact marker and fullscreen guides together."); - if (get_mode() == Mode::Sketch_dim_anno) - { - constexpr std::array k_edge_dim_label_placement = { - "Near first point", - "Near second point", - "Center on dimension line", - "Automatic", - }; - int h = m_edge_dim_label_h; - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - right_aligned_label("Length value placement"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(170.0f); - if (ImGui::BeginCombo("##edge_dim_h", k_edge_dim_label_placement[static_cast(h)], ImGuiComboFlags_HeightSmall)) - { - for (int i = 0; i < int(k_edge_dim_label_placement.size()); ++i) - if (ImGui::Selectable(k_edge_dim_label_placement[static_cast(i)], i == h)) - { - m_edge_dim_label_h = i; - save_occt_view_settings(); - } - ImGui::EndCombo(); - } - if (ui_show_help(2) && ImGui::IsItemHovered()) - ImGui::SetTooltip( - "Where to place the numeric value along the dimension line (near the first or second end, center, or auto).\n" - "This does not flip which side of the edge the dimension sits on.\n" - "When the sketch has filled faces, dimensions offset to the void side (point-in-face test).\n" - "Otherwise the average node position is used as a rough inside reference.\n" - "Dimension line width is in Settings -> Sketch."); - } - ImGui::EndTable(); } diff --git a/src/gui_settings.cpp b/src/gui_settings.cpp index c58644f..a2a18d9 100644 --- a/src/gui_settings.cpp +++ b/src/gui_settings.cpp @@ -17,6 +17,7 @@ namespace { const char* const k_settings_version = "1"; +const char* const k_gui_key_permanent_node_anno_scale = "permanent_node_anno_scale"; /// `occt_view` JSON object: view background gradient and grid (shared with `save_occt_view_settings` / /// `occt_view_settings_json`). @@ -54,6 +55,13 @@ std::string GUI::occt_view_settings_json() const {"edge_dim_label_h", m_edge_dim_label_h}, {"edge_dim_line_width", m_edge_dim_line_width}, {"edge_dim_arrow_size", m_edge_dim_arrow_size}, + {"edge_dim_color", {m_edge_dim_color[0], m_edge_dim_color[1], m_edge_dim_color[2]}}, + {"edge_dim_text_scale", m_edge_dim_text_scale}, + {"edge_dim_text_render_mode", m_edge_dim_text_render_mode}, + {"edge_dim_arrow_style", m_edge_dim_arrow_style}, + {"edge_dim_arrow_orientation", m_edge_dim_arrow_orientation}, + {"show_sketch_dimensions", m_show_sketch_dimensions}, + {k_gui_key_permanent_node_anno_scale, m_permanent_node_anno_scale}, {"view_roll_step_deg", m_view_roll_step_deg}, {"view_zoom_scroll_scale", m_view_zoom_scroll_scale}, {"inspection_orthographic", m_inspection_orthographic}, @@ -100,6 +108,13 @@ void GUI::save_occt_view_settings() {"edge_dim_label_h", m_edge_dim_label_h}, {"edge_dim_line_width", m_edge_dim_line_width}, {"edge_dim_arrow_size", m_edge_dim_arrow_size}, + {"edge_dim_color", {m_edge_dim_color[0], m_edge_dim_color[1], m_edge_dim_color[2]}}, + {"edge_dim_text_scale", m_edge_dim_text_scale}, + {"edge_dim_text_render_mode", m_edge_dim_text_render_mode}, + {"edge_dim_arrow_style", m_edge_dim_arrow_style}, + {"edge_dim_arrow_orientation", m_edge_dim_arrow_orientation}, + {"show_sketch_dimensions", m_show_sketch_dimensions}, + {k_gui_key_permanent_node_anno_scale, m_permanent_node_anno_scale}, {"load_last_opened_on_startup", m_load_last_opened_on_startup}, {"last_opened_project_path", m_last_opened_project_path}, {"imgui_rounding_general", m_imgui_rounding_general}, @@ -238,20 +253,58 @@ void GUI::parse_gui_panes_settings_(const std::string& content) if (v >= 0 && v <= 3) m_edge_dim_label_h = v; } - m_edge_dim_line_width = k_gui_edge_dim_line_width_default; - if (g.contains("edge_dim_line_width") && g["edge_dim_line_width"].is_number()) + auto parse_bounded_float = [&g](const char* key, const float min_v, const float max_v, const float default_v) -> float { - const float v = g["edge_dim_line_width"].get(); - if (v >= 0.5f && v <= 8.0f) - m_edge_dim_line_width = v; + float out = default_v; + if (g.contains(key) && g[key].is_number()) + { + const float v = g[key].get(); + if (v >= min_v && v <= max_v) + out = v; + } + return out; + }; + m_edge_dim_line_width = parse_bounded_float("edge_dim_line_width", 0.5f, 8.0f, k_gui_edge_dim_line_width_default); + m_edge_dim_arrow_size = parse_bounded_float("edge_dim_arrow_size", 1.0f, 24.0f, k_gui_edge_dim_arrow_size_default); + m_edge_dim_text_scale = + parse_bounded_float("edge_dim_text_scale", k_gui_edge_dim_text_scale_min, k_gui_edge_dim_text_scale_max, + k_gui_edge_dim_text_scale_default); + auto parse_dim_int = [&g](const char* key, const int min_v, const int max_v, const int default_v) -> int + { + if (g.contains(key) && g[key].is_number_integer()) + { + const int v = g[key].get(); + if (v >= min_v && v <= max_v) + return v; + } + return default_v; + }; + m_edge_dim_text_render_mode = + parse_dim_int("edge_dim_text_render_mode", 0, k_gui_edge_dim_text_render_mode_max, + k_gui_edge_dim_text_render_mode_default); + if (g.contains("edge_dim_color") && g["edge_dim_color"].is_array() && g["edge_dim_color"].size() >= 3) + { + const json& a = g["edge_dim_color"]; + for (int i = 0; i < 3; ++i) + if (a[i].is_number()) + m_edge_dim_color[i] = std::clamp(a[i].get(), 0.f, 1.f); } - m_edge_dim_arrow_size = k_gui_edge_dim_arrow_size_default; - if (g.contains("edge_dim_arrow_size") && g["edge_dim_arrow_size"].is_number()) + if (g.contains("edge_dim_arrow_style") && g["edge_dim_arrow_style"].is_number_integer()) { - const float v = g["edge_dim_arrow_size"].get(); - if (v >= 1.0f && v <= 24.0f) - m_edge_dim_arrow_size = v; + const int v = g["edge_dim_arrow_style"].get(); + if (v >= k_gui_edge_dim_arrow_style_min && v <= k_gui_edge_dim_arrow_style_max) + m_edge_dim_arrow_style = v; } + if (g.contains("edge_dim_arrow_orientation") && g["edge_dim_arrow_orientation"].is_number_integer()) + { + const int v = g["edge_dim_arrow_orientation"].get(); + if (v >= k_gui_edge_dim_arrow_orientation_min && v <= k_gui_edge_dim_arrow_orientation_max) + m_edge_dim_arrow_orientation = v; + } + m_show_sketch_dimensions = b("show_sketch_dimensions", true); + m_permanent_node_anno_scale = + parse_bounded_float(k_gui_key_permanent_node_anno_scale, k_gui_permanent_node_anno_scale_min, + k_gui_permanent_node_anno_scale_max, k_gui_permanent_node_anno_scale_default); m_load_last_opened_on_startup = b("load_last_opened_on_startup", b("load_last_saved_on_startup", false)); if (g.contains("last_opened_project_path") && g["last_opened_project_path"].is_string()) m_last_opened_project_path = g["last_opened_project_path"].get(); @@ -411,6 +464,8 @@ void GUI::load_occt_view_settings_() parse_occt_view_settings_(content); parse_gui_panes_settings_(content); + if (m_view) + apply_sketch_dimensions_visibility(); try { @@ -806,24 +861,28 @@ void GUI::settings_() if (ImGui::CollapsingHeader("Sketch")) { bool ul_changed = false; - bool dim_lw_changed = false; - bool dim_arrow_changed = false; - if (ImGui::BeginTable("settings_sketch", 2, ImGuiTableFlags_SizingStretchProp)) + bool dim_changed = false; + bool node_anno_changed = false; + + ImGui::Indent(ImGui::GetStyle().IndentSpacing); + if (ImGui::CollapsingHeader("Dimensions", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthFixed, k_label_col_w); - ImGui::TableSetupColumn("control", ImGuiTableColumnFlags_WidthStretch); + if (ImGui::BeginTable("settings_sketch_dims", 2, ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthFixed, k_label_col_w); + ImGui::TableSetupColumn("control", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Dimension line width"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Dimension line width"); ImGui::TableSetColumnIndex(1); { float lw = m_edge_dim_line_width; if (ImGui::SliderFloat("##edge_dim_lw", &lw, 0.5f, 8.0f, "%.2f")) { m_edge_dim_line_width = lw; - dim_lw_changed = true; + dim_changed = true; } if (ui_show_help(2)) { @@ -850,7 +909,7 @@ void GUI::settings_() if (ImGui::SliderFloat("##edge_dim_arrow", &arrow, 1.0f, 24.0f, "%.2f")) { m_edge_dim_arrow_size = arrow; - dim_arrow_changed = true; + dim_changed = true; } if (ui_show_help(2)) { @@ -867,6 +926,185 @@ void GUI::settings_() } } + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Dimension color"); + ImGui::TableSetColumnIndex(1); + if (ImGui::ColorEdit3("##edge_dim_color", m_edge_dim_color, ImGuiColorEditFlags_Float)) + dim_changed = true; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Dimension text scale"); + ImGui::TableSetColumnIndex(1); + { + float ts = m_edge_dim_text_scale; + if (ImGui::SliderFloat("##edge_dim_text_scale", &ts, k_gui_edge_dim_text_scale_min, k_gui_edge_dim_text_scale_max, + "%.2f")) + { + m_edge_dim_text_scale = ts; + dim_changed = true; + } + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Label rendering"); + ImGui::TableSetColumnIndex(1); + { + constexpr std::array k_labels = { + "Opaque 2D text", + "SetCommonColor", + "2D screen text", + "3D text", + "Z-layer Top", + "Z-layer Topmost", + }; + int rm = m_edge_dim_text_render_mode; + if (rm < 0 || rm >= static_cast(k_labels.size())) + rm = k_gui_edge_dim_text_render_mode_default; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::BeginCombo("##edge_dim_text_render", k_labels[static_cast(rm)], ImGuiComboFlags_HeightSmall)) + { + for (int i = 0; i < static_cast(k_labels.size()); ++i) + if (ImGui::Selectable(k_labels[static_cast(i)], i == rm)) + { + m_edge_dim_text_render_mode = i; + dim_changed = true; + } + ImGui::EndCombo(); + } + if (ui_show_help(2) && ImGui::IsItemHovered()) + ImGui::SetTooltip("How dimension value labels are composited. Z-layer Top and Topmost avoid ghosting " + "against the grid; Topmost is the default."); + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Length value placement"); + ImGui::TableSetColumnIndex(1); + { + constexpr std::array k_edge_dim_label_placement = { + "Near first point", + "Near second point", + "Center on dimension line", + "Automatic", + }; + int h = m_edge_dim_label_h; + ImGui::SetNextItemWidth(200.0f); + if (ImGui::BeginCombo("##edge_dim_h", k_edge_dim_label_placement[static_cast(h)], ImGuiComboFlags_HeightSmall)) + { + for (int i = 0; i < static_cast(k_edge_dim_label_placement.size()); ++i) + if (ImGui::Selectable(k_edge_dim_label_placement[static_cast(i)], i == h)) + { + m_edge_dim_label_h = i; + dim_changed = true; + } + ImGui::EndCombo(); + } + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Arrow style"); + ImGui::TableSetColumnIndex(1); + { + constexpr std::array k_arrow_styles = {"Standard", "Sharp", "Wide", "3D shaded"}; + int st = m_edge_dim_arrow_style; + ImGui::SetNextItemWidth(160.0f); + if (ImGui::BeginCombo("##edge_dim_arrow_style", k_arrow_styles[static_cast(st)], ImGuiComboFlags_HeightSmall)) + { + for (int i = 0; i < static_cast(k_arrow_styles.size()); ++i) + if (ImGui::Selectable(k_arrow_styles[static_cast(i)], i == st)) + { + m_edge_dim_arrow_style = i; + dim_changed = true; + } + ImGui::EndCombo(); + } + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Arrow orientation"); + ImGui::TableSetColumnIndex(1); + { + constexpr std::array k_arrow_orient = {"Automatic", "Internal", "External"}; + int ao = m_edge_dim_arrow_orientation; + ImGui::SetNextItemWidth(160.0f); + if (ImGui::BeginCombo("##edge_dim_arrow_orient", k_arrow_orient[static_cast(ao)], ImGuiComboFlags_HeightSmall)) + { + for (int i = 0; i < static_cast(k_arrow_orient.size()); ++i) + if (ImGui::Selectable(k_arrow_orient[static_cast(i)], i == ao)) + { + m_edge_dim_arrow_orientation = i; + dim_changed = true; + } + ImGui::EndCombo(); + } + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Show sketch dimensions"); + ImGui::TableSetColumnIndex(1); + { + bool show = m_show_sketch_dimensions; + if (ImGui::Checkbox("##show_sketch_dims", &show)) + { + set_show_sketch_dimensions(show); + save_occt_view_settings(); + } + if (ui_show_help(2) && ImGui::IsItemHovered()) + ImGui::SetTooltip("When off, hides all sketch length dimensions. Tool mode may still limit which sketch shows " + "dimensions when this is on."); + } + + ImGui::EndTable(); + } + } + ImGui::Unindent(ImGui::GetStyle().IndentSpacing); + + if (ImGui::BeginTable("settings_sketch", 2, ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthFixed, k_label_col_w); + ImGui::TableSetupColumn("control", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Permanent node annotation size"); + ImGui::TableSetColumnIndex(1); + { + float size_scale = m_permanent_node_anno_scale; + if (ImGui::SliderFloat("##permanent_node_anno_scale", &size_scale, k_gui_permanent_node_anno_scale_min, + k_gui_permanent_node_anno_scale_max, "%.2f")) + { + m_permanent_node_anno_scale = size_scale; + node_anno_changed = true; + } + if (ui_show_help(2)) + { + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextDisabled("Scale for permanent '+' node markers in sketch mode."); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + } + } + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::AlignTextToFramePadding(); @@ -944,17 +1182,17 @@ void GUI::settings_() ImGui::EndTable(); } - if (dim_lw_changed) + if (dim_changed) { save_occt_view_settings(); if (m_view) - m_view->refresh_all_length_dimension_line_widths(static_cast(m_edge_dim_line_width)); + m_view->refresh_all_length_dimensions(); } - if (dim_arrow_changed) + if (node_anno_changed) { save_occt_view_settings(); if (m_view) - m_view->refresh_all_length_dimension_arrow_sizes(static_cast(m_edge_dim_arrow_size)); + m_view->refresh_all_permanent_node_annotations(); } if (ul_changed) { diff --git a/src/occt_view.cpp b/src/occt_view.cpp index fa03d87..b2f8c7a 100644 --- a/src/occt_view.cpp +++ b/src/occt_view.cpp @@ -1020,6 +1020,63 @@ void Occt_view::refresh_all_length_dimension_arrow_sizes(const double arrow_size sk->refresh_edge_dimension_arrow_sizes(arrow_size); } +void Occt_view::refresh_all_length_dimension_styles(const Length_dimension_style& style) +{ + for (const Sketch_ptr& sk : m_sketches) + if (sk) + sk->refresh_edge_dimension_style(style); + m_shp_extrude.refresh_tmp_dimension_style(style); +} + +void Occt_view::refresh_all_length_dimensions() +{ + for (const Sketch_ptr& sk : m_sketches) + if (sk) + sk->refresh_all_length_dimensions(); + m_shp_extrude.refresh_tmp_dimension_style(m_gui.length_dimension_style()); +} + +void Occt_view::apply_sketch_dimensions_visibility() +{ + if (!m_gui.show_sketch_dimensions()) + { + for (const Sketch_ptr& s : m_sketches) + if (s) + s->set_show_dims(false); + return; + } + + if (is_sketch_mode(get_mode())) + { + if (get_mode() == Mode::Sketch_operation_axis) + { + for (const Sketch_ptr& s : m_sketches) + if (s) + s->set_show_dims(s == m_cur_sketch); + } + else + { + for (const Sketch_ptr& s : m_sketches) + if (s) + s->set_show_dims(true); + } + } + else + { + const bool show = get_mode() == Mode::Shape_polar_duplicate; + for (const Sketch_ptr& s : m_sketches) + if (s) + s->set_show_dims(show); + } +} + +void Occt_view::refresh_all_permanent_node_annotations() +{ + for (const Sketch_ptr& sk : m_sketches) + if (sk) + sk->refresh_permanent_node_annotations(); +} + void Occt_view::dimension_input(const ScreenCoords& screen_coords) { switch (get_mode()) @@ -1515,7 +1572,6 @@ void Occt_view::on_mode() { s->set_show_faces(s == m_cur_sketch); s->set_show_edges(s == m_cur_sketch); - s->set_show_dims(s == m_cur_sketch); } }; @@ -1524,7 +1580,6 @@ void Occt_view::on_mode() for (Sketch_ptr& s : m_sketches) { s->set_show_edges(show); - s->set_show_dims(show); s->set_show_faces(show); } }; @@ -1572,6 +1627,7 @@ void Occt_view::on_mode() shp->set_visible(true); } + apply_sketch_dimensions_visibility(); apply_camera_projection(); } diff --git a/src/occt_view.h b/src/occt_view.h index 284b1df..e1b4605 100644 --- a/src/occt_view.h +++ b/src/occt_view.h @@ -27,6 +27,7 @@ class Sketch; class GUI; +struct Length_dimension_style; class Prs3d_Drawer; class TopoDS_Face; class TopoDS_Wire; @@ -165,6 +166,10 @@ class Occt_view : protected AIS_ViewController void angle_input(const ScreenCoords& screen_coords); void refresh_all_length_dimension_line_widths(double line_width); void refresh_all_length_dimension_arrow_sizes(double arrow_size); + void refresh_all_length_dimension_styles(const Length_dimension_style& style); + void refresh_all_length_dimensions(); + void apply_sketch_dimensions_visibility(); + void refresh_all_permanent_node_annotations(); double get_dimension_scale() const; bool get_show_dim_input() const; void set_show_dim_input(bool show); diff --git a/src/shp_extrude.cpp b/src/shp_extrude.cpp index 19e8cf1..6aad03f 100644 --- a/src/shp_extrude.cpp +++ b/src/shp_extrude.cpp @@ -151,8 +151,8 @@ void Shp_extrude::_update_extrude_preview_(const double extrude_dist, const Plan ctx().Remove(m_tmp_dim, false); m_tmp_dim = create_distance_annotation(gp_Pnt(m_to_extrude_pt->XYZ() + face_offset.XYZ() + extrude_vec.XYZ()), - gp_Pnt(m_to_extrude_pt->XYZ() + face_offset.XYZ()), m_curr_view_pln, Prs3d_DTHP_Fit, - std::nullopt, nullptr, gui().edge_dim_line_width(), gui().edge_dim_arrow_size()); + gp_Pnt(m_to_extrude_pt->XYZ() + face_offset.XYZ()), m_curr_view_pln, + gui().length_dimension_style()); m_tmp_dim->SetCustomValue(extrude_dist / view().get_dimension_scale()); @@ -180,3 +180,11 @@ void Shp_extrude::_update_extrude_preview_(const double extrude_dist, const Plan ctx().Redisplay(m_extruded, true); } } + +void Shp_extrude::refresh_tmp_dimension_style(const Length_dimension_style& style) +{ + if (m_tmp_dim.IsNull()) + return; + apply_length_dimension_style(m_tmp_dim, style); + ctx().Redisplay(m_tmp_dim, true); +} diff --git a/src/shp_extrude.h b/src/shp_extrude.h index c94aa9c..c2b6a02 100644 --- a/src/shp_extrude.h +++ b/src/shp_extrude.h @@ -5,6 +5,7 @@ #include #include +#include "geom.h" #include "shp_operation.h" class AIS_Shape; @@ -22,6 +23,7 @@ class Shp_extrude : private Shp_operation_base bool has_active_extrusion() const; bool get_both_sides() const; void set_both_sides(bool both_sides); + void refresh_tmp_dimension_style(const Length_dimension_style& style); // For testing void set_curr_view_pln(const gp_Pln& pln); diff --git a/src/sketch.cpp b/src/sketch.cpp index 66e41e5..3bab64e 100644 --- a/src/sketch.cpp +++ b/src/sketch.cpp @@ -205,23 +205,10 @@ void Sketch::update_edge_style_(AIS_Shape_ptr& shp) void Sketch::update_node_mark_style_(AIS_Shape_ptr& shp) { - switch (m_edge_style) - { - case Edge_style::Full: - shp->SetWidth(1.25); - shp->SetColor(Quantity_NOC_RED); - shp->SetTransparency(0.0); - break; - - case Edge_style::Background: - shp->SetWidth(1.0); - shp->SetColor(Quantity_Color(0.55, 0.12, 0.12, Quantity_TOC_RGB)); - shp->SetTransparency(0.5); - break; - - default: - EZY_ASSERT(false); - } + // Keep permanent node markers visually stable across sketch switching. + shp->SetWidth(1.25); + shp->SetColor(Quantity_NOC_RED); + shp->SetTransparency(0.0); } void Sketch::sync_permanent_node_annos_() @@ -229,7 +216,8 @@ void Sketch::sync_permanent_node_annos_() if (m_permanent_node_marks.size() < m_nodes.size()) m_permanent_node_marks.resize(m_nodes.size()); - const double half_arm = std::max(plane_pick_snap_radius_world_() * 0.45, Precision::Confusion() * 50.0); + const double size_scale = std::max(static_cast(m_view.gui().permanent_node_anno_scale()), 0.0); + const double half_arm = std::max(plane_pick_snap_radius_world_() * 0.45 * size_scale, Precision::Confusion() * 50.0); const Mode mode = get_mode(); // Show "+" markers for permanent user nodes in sketch modes and polar duplicate (which snaps to sketch nodes). @@ -481,10 +469,9 @@ void Sketch::move_line_string_pt_(const ScreenCoords& screen_coords) // line; if the mouse is (nearly) perpendicular to that line, the projection coincides with pt_a. if (unique(pt_a, final_pt_b)) { - m_tmp_dim_anno = create_distance_annotation( - pt_a, final_pt_b, m_pln, edge_dim_text_h_pos_from_index(m_view.gui().edge_dim_label_h()), - approx_sketch_interior_ref_3d_(), m_dim_classifier_faces.empty() ? nullptr : &m_dim_classifier_faces, - m_view.gui().edge_dim_line_width(), m_view.gui().edge_dim_arrow_size()); + m_tmp_dim_anno = create_distance_annotation(pt_a, final_pt_b, m_pln, m_view.gui().length_dimension_style(), + approx_sketch_interior_ref_3d_(), + m_dim_classifier_faces.empty() ? nullptr : &m_dim_classifier_faces); m_tmp_dim_anno->SetCustomValue(dist); m_ctx.Display(m_tmp_dim_anno, true); @@ -1387,10 +1374,9 @@ void Sketch::rebuild_length_dimension_display_(Length_dimension& d) if (!d.dim.IsNull()) m_ctx.Remove(d.dim, false); - d.dim = create_distance_annotation( - m_nodes[d.node_idx_lo], m_nodes[d.node_idx_hi], m_pln, edge_dim_text_h_pos_from_index(m_view.gui().edge_dim_label_h()), - approx_sketch_interior_ref_3d_(), m_dim_classifier_faces.empty() ? nullptr : &m_dim_classifier_faces, - m_view.gui().edge_dim_line_width(), m_view.gui().edge_dim_arrow_size()); + d.dim = create_distance_annotation(m_nodes[d.node_idx_lo], m_nodes[d.node_idx_hi], m_pln, m_view.gui().length_dimension_style(), + approx_sketch_interior_ref_3d_(), + m_dim_classifier_faces.empty() ? nullptr : &m_dim_classifier_faces); const double dist = m_nodes[d.node_idx_lo].Distance(m_nodes[d.node_idx_hi]); d.dim->SetCustomValue(dist / m_view.get_dimension_scale()); @@ -2502,6 +2488,26 @@ void Sketch::refresh_edge_dimension_arrow_sizes(const double arrow_size) } } +void Sketch::refresh_all_length_dimensions() { refresh_all_length_dimensions_(); } + +void Sketch::refresh_edge_dimension_style(const Length_dimension_style& style) +{ + for (Length_dimension& ld : m_length_dimensions) + if (!ld.dim.IsNull()) + { + apply_length_dimension_style(ld.dim, style); + m_ctx.Redisplay(ld.dim, true); + } + + if (!m_tmp_dim_anno.IsNull()) + { + apply_length_dimension_style(m_tmp_dim_anno, style); + m_ctx.Redisplay(m_tmp_dim_anno, true); + } +} + +void Sketch::refresh_permanent_node_annotations() { sync_permanent_node_annos_(); } + bool Sketch::is_current() const { return this == &m_view.curr_sketch(); } void Sketch::set_current() @@ -2550,7 +2556,8 @@ void Sketch::set_edge_style(Edge_style style) for (Edge& e : m_edges) update_edge_style_(e.shp); - sync_permanent_node_annos_(); + // Permanent node marker style/visibility is managed elsewhere; avoid + // rebuilding marker geometry during sketch switching. update_originating_face_style(); } diff --git a/src/sketch.h b/src/sketch.h index 41700b7..f169138 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -18,6 +18,7 @@ #include "utl.h" class Occt_view; +struct Length_dimension_style; class gp_Pln; class TopoDS_Wire; class Sketch; @@ -103,6 +104,12 @@ class Sketch void refresh_edge_dimension_line_widths(double line_width); /// Apply global dimension arrow size to edge annotations and in-progress rubber-band dim. void refresh_edge_dimension_arrow_sizes(double arrow_size); + /// Apply full global dimension style to edge annotations and in-progress rubber-band dim. + void refresh_edge_dimension_style(const Length_dimension_style& style); + /// Rebuild every length dimension (e.g. after global flyout or label placement defaults change). + void refresh_all_length_dimensions(); + /// Rebuild permanent node '+' markers (e.g. after settings changes). + void refresh_permanent_node_annotations(); // Revolve related Shp_rslt revolve_selected(const double angle); diff --git a/src/sketch_nodes.cpp b/src/sketch_nodes.cpp index 0a50ab0..699a949 100644 --- a/src/sketch_nodes.cpp +++ b/src/sketch_nodes.cpp @@ -5,17 +5,52 @@ #include #include #include +#include #include +#include #include "dbg.h" #include "geom.h" #include "imgui.h" #include "occt_view.h" +namespace +{ +double s_snap_dist_pixels = 35.0; +Sketch_nodes::Snap_guide_mode s_snap_guide_mode = Sketch_nodes::Snap_guide_mode::Traditional; +glm::vec3 s_snap_guide_color{0.0f, 1.0f, 0.0f}; +} // namespace + +struct Sketch_nodes::Impl +{ + Impl(Occt_view& view, const gp_Pln& pln) + : view(view) + , ctx(view.ctx()) + , pln(pln) + { + } + + std::vector nodes; + std::set outside_snap_pts; // Projected snap points from other sketches. + AIS_Shape_ptr snap_anno_axis[2]; + std::optional last_snap_pt; // Used for snap annotation + AIS_Shape_ptr snap_anno; + size_t prev_num_nodes{0}; // Used when an operation is canceled. + + // Owner related + Occt_view& view; + AIS_InteractiveContext& ctx; + const gp_Pln pln; + + /// World-space snap radius at `pt` (same convention as `try_get_node_idx_snap` / `try_pick_existing_node`). + double snap_radius_world_(const gp_Pnt2d& pt) const; + bool view_bounds_2d_(double& min_u, double& min_v, double& max_u, double& max_v) const; + void update_node_snap_anno_(const gp_Pnt2d& pt, const double snap_dist); + void update_axis_snap_anno_(int axis_index, const gp_Pnt2d& axis_pt, double snap_dist); +}; + Sketch_nodes::Sketch_nodes(Occt_view& view, const gp_Pln& pln) - : m_view(view) - , m_ctx(m_view.ctx()) - , m_pln(pln) + : m_impl(std::make_unique(view, pln)) { } @@ -24,9 +59,20 @@ Sketch_nodes::~Sketch_nodes() hide_snap_annos(); // Deletes them from context } +// === Iteration ============================================================= + +std::vector::iterator Sketch_nodes::begin() { return m_impl->nodes.begin(); } +std::vector::iterator Sketch_nodes::end() { return m_impl->nodes.end(); } +std::vector::const_iterator Sketch_nodes::begin() const { return m_impl->nodes.begin(); } +std::vector::const_iterator Sketch_nodes::end() const { return m_impl->nodes.end(); } +std::vector::const_iterator Sketch_nodes::cbegin() const { return m_impl->nodes.cbegin(); } +std::vector::const_iterator Sketch_nodes::cend() const { return m_impl->nodes.cend(); } + +// === Public API ============================================================ + std::optional Sketch_nodes::snap(const ScreenCoords& screen_coords) { - std::optional pt = m_view.pt_on_plane(screen_coords, m_pln); + std::optional pt = m_impl->view.pt_on_plane(screen_coords, m_impl->pln); if (pt) try_get_node_idx_snap(*pt); @@ -36,10 +82,10 @@ std::optional Sketch_nodes::snap(const ScreenCoords& screen_coords) size_t Sketch_nodes::get_node_exact(const gp_Pnt2d& pt, bool permanent_for_new) { std::optional deleted_match; - for (size_t idx = 0, num = m_nodes.size(); idx < num; ++idx) - if (equal(pt, gp_Pnt2d(m_nodes[idx]))) + for (size_t idx = 0, num = m_impl->nodes.size(); idx < num; ++idx) + if (equal(pt, gp_Pnt2d(m_impl->nodes[idx]))) { - Node& n = m_nodes[idx]; + Node& n = m_impl->nodes[idx]; // Never bind to tombstoned nodes while searching for an exact live match. if (n.deleted) { @@ -56,7 +102,7 @@ size_t Sketch_nodes::get_node_exact(const gp_Pnt2d& pt, bool permanent_for_new) // If only a deleted exact match exists, revive it instead of appending a duplicate index. if (deleted_match.has_value()) { - Node& n = m_nodes[*deleted_match]; + Node& n = m_impl->nodes[*deleted_match]; n.deleted = false; if (permanent_for_new) n.permanent = true; @@ -65,14 +111,14 @@ size_t Sketch_nodes::get_node_exact(const gp_Pnt2d& pt, bool permanent_for_new) Node n(pt); n.permanent = permanent_for_new; - const size_t ret = m_nodes.size(); - m_nodes.push_back(n); + const size_t ret = m_impl->nodes.size(); + m_impl->nodes.push_back(n); return ret; } std::optional Sketch_nodes::get_node(const ScreenCoords& screen_coords) { - std::optional pt = m_view.pt_on_plane(screen_coords, m_pln); + std::optional pt = m_impl->view.pt_on_plane(screen_coords, m_impl->pln); if (!pt) // View plane and sketch plane must be perpendicular. return std::nullopt; @@ -84,40 +130,9 @@ std::optional Sketch_nodes::get_node(const ScreenCoords& screen_coords) return add_new_node(*pt); } -double Sketch_nodes::snap_radius_world_(const gp_Pnt2d& pt) const -{ - if (!m_view.is_headless()) - { - gp_Pnt pt3d_on_plane = to_3d(m_pln, pt); - ScreenCoords screen_coords_at_pt = m_view.get_screen_coords(pt3d_on_plane); - - ScreenCoords screen_coords_offset = screen_coords_at_pt; - screen_coords_offset.unsafe_get().x += s_snap_dist_pixels; - - std::optional pt_offset_on_plane_2d; - if (std::optional pt_offset_on_plane_3d = m_view.pt3d_on_plane(screen_coords_offset, m_pln)) - pt_offset_on_plane_2d = to_2d(m_pln, *pt_offset_on_plane_3d); - - if (pt_offset_on_plane_2d) - return pt.Distance(*pt_offset_on_plane_2d); - return 5.0; - } - return s_snap_dist_pixels; -} - -bool Sketch_nodes::view_bounds_2d_(double& min_u, double& min_v, double& max_u, double& max_v) const -{ - if (m_view.is_headless()) - return false; - - const ImGuiIO& io = ImGui::GetIO(); - return m_view.sketch_plane_view_aabb_2d(m_pln, static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y), - min_u, min_v, max_u, max_v); -} - std::optional Sketch_nodes::try_pick_existing_node(const ScreenCoords& screen_coords) { - std::optional pt_opt = m_view.pt_on_plane(screen_coords, m_pln); + std::optional pt_opt = m_impl->view.pt_on_plane(screen_coords, m_impl->pln); if (!pt_opt) { hide_snap_annos(); @@ -125,15 +140,15 @@ std::optional Sketch_nodes::try_pick_existing_node(const ScreenCoords& s } const gp_Pnt2d pt = *pt_opt; - const double snap_dist = snap_radius_world_(pt); + const double snap_dist = m_impl->snap_radius_world_(pt); size_t best_idx = static_cast(-1); double best_sq = std::numeric_limits::max(); - for (size_t idx = 0, num = m_nodes.size(); idx < num; ++idx) + for (size_t idx = 0, num = m_impl->nodes.size(); idx < num; ++idx) { - if (m_nodes[idx].deleted) + if (m_impl->nodes[idx].deleted) continue; - const double sq = m_nodes[idx].SquareDistance(pt); + const double sq = m_impl->nodes[idx].SquareDistance(pt); if (sq < best_sq) { best_sq = sq; @@ -147,7 +162,9 @@ std::optional Sketch_nodes::try_pick_existing_node(const ScreenCoords& s } if (best_sq <= snap_dist * 0.25 * snap_dist) { - try_get_node_idx_snap(m_nodes[best_idx], {}); + // `try_get_node_idx_snap` can modify the input `pt`, we call this function to display snapping annotations. + gp_Pnt2d pt_snapped = m_impl->nodes[best_idx]; + try_get_node_idx_snap(pt_snapped, {}); return best_idx; } hide_snap_annos(); @@ -158,16 +175,16 @@ std::optional Sketch_nodes::try_get_node_idx_snap( gp_Pnt2d& pt, // `pt` could be snapped to a node, an axis of another node, or an outside snap point. const std::vector& to_exclude) { - const double snap_dist = snap_radius_world_(pt); + const double snap_dist = m_impl->snap_radius_world_(pt); hide_snap_annos(); - gp_Pnt2d pt_original = pt; + gp_Pnt2d pt_original = pt; std::optional snap_node_idx[2]; for (int axis_idx = 0; axis_idx < 2; ++axis_idx) { std::optional snap_axis_point; - double best_dist = std::numeric_limits::max(); + double best_dist = std::numeric_limits::max(); auto try_nd_pt = [&](const gp_Pnt2d& nd_pt) -> bool { @@ -198,25 +215,25 @@ std::optional Sketch_nodes::try_get_node_idx_snap( return false; }; - for (size_t nd_idx = 0, num = m_nodes.size(); nd_idx < num; ++nd_idx) + for (size_t nd_idx = 0, num = m_impl->nodes.size(); nd_idx < num; ++nd_idx) { - if (m_nodes[nd_idx].deleted) + if (m_impl->nodes[nd_idx].deleted) continue; if (std::find(to_exclude.begin(), to_exclude.end(), nd_idx) != to_exclude.end()) continue; - if (try_nd_pt(m_nodes[nd_idx])) + if (try_nd_pt(m_impl->nodes[nd_idx])) snap_node_idx[axis_idx] = nd_idx; } - for (const gp_Pnt2d& nd_pt : m_outside_snap_pts) + for (const gp_Pnt2d& nd_pt : m_impl->outside_snap_pts) try_nd_pt(nd_pt); if (snap_axis_point) - update_axis_snap_anno_(axis_idx, *snap_axis_point, sqrt(snap_dist)); - else if (!m_snap_anno_axis[axis_idx].IsNull()) - m_ctx.Erase(m_snap_anno_axis[axis_idx], true); + m_impl->update_axis_snap_anno_(axis_idx, *snap_axis_point, sqrt(snap_dist)); + else if (!m_impl->snap_anno_axis[axis_idx].IsNull()) + m_impl->ctx.Erase(m_impl->snap_anno_axis[axis_idx], true); } if (snap_node_idx[0] == snap_node_idx[1] && snap_node_idx[0].has_value()) @@ -225,106 +242,139 @@ std::optional Sketch_nodes::try_get_node_idx_snap( return {}; } -void Sketch_nodes::update_axis_snap_anno_(int axis_index, const gp_Pnt2d& axis_pt, double snap_dist) -{ - const bool show_traditional = s_snap_guide_mode == Snap_guide_mode::Traditional || s_snap_guide_mode == Snap_guide_mode::Both; - const bool show_fullscreen = s_snap_guide_mode == Snap_guide_mode::Fullscreen || s_snap_guide_mode == Snap_guide_mode::Both; - - TopoDS_Shape fullscreen_shape; - if (show_fullscreen) - { - double min_u{}, min_v{}, max_u{}, max_v{}; - if (view_bounds_2d_(min_u, min_v, max_u, max_v)) - if (axis_index == 0) - { - const gp_Pnt p0 = to_3d(m_pln, gp_Pnt2d(axis_pt.X(), min_v)); - const gp_Pnt p1 = to_3d(m_pln, gp_Pnt2d(axis_pt.X(), max_v)); - fullscreen_shape = BRepBuilderAPI_MakeEdge(p0, p1).Edge(); - } - else - { - const gp_Pnt p0 = to_3d(m_pln, gp_Pnt2d(min_u, axis_pt.Y())); - const gp_Pnt p1 = to_3d(m_pln, gp_Pnt2d(max_u, axis_pt.Y())); - fullscreen_shape = BRepBuilderAPI_MakeEdge(p0, p1).Edge(); - } - } - - TopoDS_Shape anno_shape; - const TopoDS_Shape traditional_shape = create_wire_box(m_pln, to_3d(m_pln, axis_pt), snap_dist, snap_dist); - if (show_traditional && !fullscreen_shape.IsNull()) - { - BRep_Builder builder; - TopoDS_Compound comp; - builder.MakeCompound(comp); - builder.Add(comp, fullscreen_shape); - builder.Add(comp, traditional_shape); - anno_shape = comp; - } - else if (!fullscreen_shape.IsNull()) - anno_shape = fullscreen_shape; - else - anno_shape = traditional_shape; - - if (m_snap_anno_axis[axis_index].IsNull()) - { - m_snap_anno_axis[axis_index] = new AIS_Shape(anno_shape); - m_snap_anno_axis[axis_index]->SetWidth(1.0); - m_snap_anno_axis[axis_index]->SetColor( - Quantity_Color(s_snap_guide_color.x, s_snap_guide_color.y, s_snap_guide_color.z, Quantity_TOC_RGB)); - m_ctx.Display(m_snap_anno_axis[axis_index], true); - } - else - { - m_snap_anno_axis[axis_index]->Set(anno_shape); - m_ctx.Redisplay(m_snap_anno_axis[axis_index], true); - } -} - void Sketch_nodes::hide_snap_annos() { - if (m_snap_anno) - m_ctx.Remove(m_snap_anno, false); + if (m_impl->snap_anno) + m_impl->ctx.Remove(m_impl->snap_anno, false); - m_snap_anno = nullptr; + m_impl->snap_anno = nullptr; - for (AIS_Shape_ptr& anno : m_snap_anno_axis) + for (AIS_Shape_ptr& anno : m_impl->snap_anno_axis) if (anno) { - m_ctx.Remove(anno, false); + m_impl->ctx.Remove(anno, false); anno = nullptr; } - m_ctx.UpdateCurrentViewer(); - m_last_snap_pt = std::nullopt; + m_impl->ctx.UpdateCurrentViewer(); + m_impl->last_snap_pt = std::nullopt; } size_t Sketch_nodes::add_new_node(const gp_Pnt2d& pt, bool is_edge_mid_point, bool is_permanent) { - size_t ret = m_nodes.size(); + size_t ret = m_impl->nodes.size(); Node n(pt); n.midpoint = is_edge_mid_point; n.permanent = is_permanent; - m_nodes.emplace_back(n); + m_impl->nodes.emplace_back(n); // DBG_MSG("Add node: " << pt.Coord().X() << "," << pt.Coord().Y() << " midpoint: " << (int) is_edge_mid_point); return ret; } void Sketch_nodes::get_snap_pts_3d(std::vector& out) { - for (const Node& n : m_nodes) + for (const Node& n : m_impl->nodes) if (!n.deleted) - out.push_back(to_3d(m_pln, n)); + out.push_back(to_3d(m_impl->pln, n)); +} + +Sketch_nodes::Node& Sketch_nodes::operator[](size_t idx) +{ + EZY_ASSERT(idx < size()); + return m_impl->nodes[idx]; } -void Sketch_nodes::update_node_snap_anno_(const gp_Pnt2d& pt, const double snap_dist) +const Sketch_nodes::Node& Sketch_nodes::operator[](size_t idx) const { - if (m_last_snap_pt && equal(*m_last_snap_pt, pt)) + EZY_ASSERT(idx < size()); + return m_impl->nodes[idx]; +} + +Sketch_nodes::Node& Sketch_nodes::operator[](const std::optional idx) +{ + EZY_ASSERT(idx.has_value()); + EZY_ASSERT(*idx < size()); + return m_impl->nodes[idx.value()]; +} + +const Sketch_nodes::Node& Sketch_nodes::operator[](const std::optional idx) const +{ + EZY_ASSERT(idx.has_value()); + EZY_ASSERT(*idx < size()); + return m_impl->nodes[idx.value()]; +} + +bool Sketch_nodes::empty() const { return m_impl->nodes.empty(); } + +size_t Sketch_nodes::size() const { return m_impl->nodes.size(); } + +void Sketch_nodes::json_resize(size_t count) { m_impl->nodes.assign(count, Node{}); } + +void Sketch_nodes::json_set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, + const std::string& name) +{ + EZY_ASSERT(idx < m_impl->nodes.size()); + Node& n = m_impl->nodes[idx]; + n.SetX(pt.X()); + n.SetY(pt.Y()); + n.deleted = deleted; + n.midpoint = midpoint; + n.permanent = permanent; + n.name = name; +} + +void Sketch_nodes::finalize() { m_impl->prev_num_nodes = m_impl->nodes.size(); } + +void Sketch_nodes::cancel() { m_impl->nodes.resize(m_impl->prev_num_nodes); } + +void Sketch_nodes::clear_outside_snap_pnts() { m_impl->outside_snap_pts.clear(); } + +void Sketch_nodes::add_outside_snap_pnt(const gp_Pnt& pt3d) { m_impl->outside_snap_pts.insert(to_2d(m_impl->pln, pt3d)); } + +// === Impl helpers ========================================================== + +double Sketch_nodes::Impl::snap_radius_world_(const gp_Pnt2d& pt) const +{ + if (!view.is_headless()) + { + gp_Pnt pt3d_on_plane = to_3d(pln, pt); + ScreenCoords screen_coords_at_pt = view.get_screen_coords(pt3d_on_plane); + + ScreenCoords screen_coords_offset = screen_coords_at_pt; + screen_coords_offset.unsafe_get().x += s_snap_dist_pixels; + + std::optional pt_offset_on_plane_2d; + if (std::optional pt_offset_on_plane_3d = view.pt3d_on_plane(screen_coords_offset, pln)) + pt_offset_on_plane_2d = to_2d(pln, *pt_offset_on_plane_3d); + + if (pt_offset_on_plane_2d) + return pt.Distance(*pt_offset_on_plane_2d); + return 5.0; + } + return s_snap_dist_pixels; +} + +bool Sketch_nodes::Impl::view_bounds_2d_(double& min_u, double& min_v, double& max_u, double& max_v) const +{ + if (view.is_headless()) + return false; + + const ImGuiIO& io = ImGui::GetIO(); + return view.sketch_plane_view_aabb_2d(pln, static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y), + min_u, min_v, max_u, max_v); +} + +void Sketch_nodes::Impl::update_node_snap_anno_(const gp_Pnt2d& pt, const double snap_dist) +{ + if (last_snap_pt && equal(*last_snap_pt, pt)) return; - m_last_snap_pt = pt; + last_snap_pt = pt; - const bool show_traditional = s_snap_guide_mode == Snap_guide_mode::Traditional || s_snap_guide_mode == Snap_guide_mode::Both; - const bool show_fullscreen = s_snap_guide_mode == Snap_guide_mode::Fullscreen || s_snap_guide_mode == Snap_guide_mode::Both; + const auto mode = s_snap_guide_mode; + const bool show_traditional = + mode == Sketch_nodes::Snap_guide_mode::Traditional || mode == Sketch_nodes::Snap_guide_mode::Both; + const bool show_fullscreen = mode == Sketch_nodes::Snap_guide_mode::Fullscreen || mode == Sketch_nodes::Snap_guide_mode::Both; TopoDS_Shape fullscreen_shape; if (show_fullscreen) @@ -336,10 +386,10 @@ void Sketch_nodes::update_node_snap_anno_(const gp_Pnt2d& pt, const double snap_ TopoDS_Compound comp; builder.MakeCompound(comp); - const gp_Pnt p_h0 = to_3d(m_pln, gp_Pnt2d(min_u, pt.Y())); - const gp_Pnt p_h1 = to_3d(m_pln, gp_Pnt2d(max_u, pt.Y())); - const gp_Pnt p_v0 = to_3d(m_pln, gp_Pnt2d(pt.X(), min_v)); - const gp_Pnt p_v1 = to_3d(m_pln, gp_Pnt2d(pt.X(), max_v)); + const gp_Pnt p_h0 = to_3d(pln, gp_Pnt2d(min_u, pt.Y())); + const gp_Pnt p_h1 = to_3d(pln, gp_Pnt2d(max_u, pt.Y())); + const gp_Pnt p_v0 = to_3d(pln, gp_Pnt2d(pt.X(), min_v)); + const gp_Pnt p_v1 = to_3d(pln, gp_Pnt2d(pt.X(), max_v)); builder.Add(comp, BRepBuilderAPI_MakeEdge(p_h0, p_h1).Edge()); builder.Add(comp, BRepBuilderAPI_MakeEdge(p_v0, p_v1).Edge()); fullscreen_shape = comp; @@ -347,7 +397,7 @@ void Sketch_nodes::update_node_snap_anno_(const gp_Pnt2d& pt, const double snap_ } TopoDS_Shape anno_shape; - const TopoDS_Shape traditional_shape = create_wire_box(m_pln, to_3d(m_pln, pt), snap_dist, snap_dist); + const TopoDS_Shape traditional_shape = create_wire_box(pln, to_3d(pln, pt), snap_dist, snap_dist); if (show_traditional && !fullscreen_shape.IsNull()) { BRep_Builder builder; @@ -362,77 +412,79 @@ void Sketch_nodes::update_node_snap_anno_(const gp_Pnt2d& pt, const double snap_ else anno_shape = traditional_shape; - if (m_snap_anno.IsNull()) + const glm::vec3& c = s_snap_guide_color; + if (snap_anno.IsNull()) { - m_snap_anno = new AIS_Shape(anno_shape); - m_snap_anno->SetWidth(3.0); - m_snap_anno->SetColor(Quantity_Color(s_snap_guide_color.x, s_snap_guide_color.y, s_snap_guide_color.z, Quantity_TOC_RGB)); - m_ctx.Display(m_snap_anno, true); + snap_anno = new AIS_Shape(anno_shape); + snap_anno->SetWidth(3.0); + snap_anno->SetColor(Quantity_Color(c.x, c.y, c.z, Quantity_TOC_RGB)); + ctx.Display(snap_anno, true); } else { - m_snap_anno->Set(anno_shape); - m_ctx.Redisplay(m_snap_anno, true); + snap_anno->Set(anno_shape); + ctx.Redisplay(snap_anno, true); } } -Sketch_nodes::Node& Sketch_nodes::operator[](size_t idx) +void Sketch_nodes::Impl::update_axis_snap_anno_(int axis_index, const gp_Pnt2d& axis_pt, double snap_dist) { - EZY_ASSERT(idx < size()); - return m_nodes[idx]; -} + const auto mode = s_snap_guide_mode; + const bool show_traditional = + mode == Sketch_nodes::Snap_guide_mode::Traditional || mode == Sketch_nodes::Snap_guide_mode::Both; + const bool show_fullscreen = mode == Sketch_nodes::Snap_guide_mode::Fullscreen || mode == Sketch_nodes::Snap_guide_mode::Both; -const Sketch_nodes::Node& Sketch_nodes::operator[](size_t idx) const -{ - EZY_ASSERT(idx < size()); - return m_nodes[idx]; -} - -Sketch_nodes::Node& Sketch_nodes::operator[](const std::optional idx) -{ - EZY_ASSERT(idx.has_value()); - EZY_ASSERT(*idx < size()); - return m_nodes[idx.value()]; -} - -const Sketch_nodes::Node& Sketch_nodes::operator[](const std::optional idx) const -{ - EZY_ASSERT(idx.has_value()); - EZY_ASSERT(*idx < size()); - return m_nodes[idx.value()]; -} - -bool Sketch_nodes::empty() const { return m_nodes.empty(); } - -size_t Sketch_nodes::size() const { return m_nodes.size(); } + TopoDS_Shape fullscreen_shape; + if (show_fullscreen) + { + double min_u{}, min_v{}, max_u{}, max_v{}; + if (view_bounds_2d_(min_u, min_v, max_u, max_v)) + if (axis_index == 0) + { + const gp_Pnt p0 = to_3d(pln, gp_Pnt2d(axis_pt.X(), min_v)); + const gp_Pnt p1 = to_3d(pln, gp_Pnt2d(axis_pt.X(), max_v)); + fullscreen_shape = BRepBuilderAPI_MakeEdge(p0, p1).Edge(); + } + else + { + const gp_Pnt p0 = to_3d(pln, gp_Pnt2d(min_u, axis_pt.Y())); + const gp_Pnt p1 = to_3d(pln, gp_Pnt2d(max_u, axis_pt.Y())); + fullscreen_shape = BRepBuilderAPI_MakeEdge(p0, p1).Edge(); + } + } -void Sketch_nodes::json_resize(size_t count) { m_nodes.assign(count, Node{}); } + TopoDS_Shape anno_shape; + const TopoDS_Shape traditional_shape = create_wire_box(pln, to_3d(pln, axis_pt), snap_dist, snap_dist); + if (show_traditional && !fullscreen_shape.IsNull()) + { + BRep_Builder builder; + TopoDS_Compound comp; + builder.MakeCompound(comp); + builder.Add(comp, fullscreen_shape); + builder.Add(comp, traditional_shape); + anno_shape = comp; + } + else if (!fullscreen_shape.IsNull()) + anno_shape = fullscreen_shape; + else + anno_shape = traditional_shape; -void Sketch_nodes::json_set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, - const std::string& name) -{ - EZY_ASSERT(idx < m_nodes.size()); - Node& n = m_nodes[idx]; - n.SetX(pt.X()); - n.SetY(pt.Y()); - n.deleted = deleted; - n.midpoint = midpoint; - n.permanent = permanent; - n.name = name; + const glm::vec3& c = s_snap_guide_color; + if (snap_anno_axis[axis_index].IsNull()) + { + snap_anno_axis[axis_index] = new AIS_Shape(anno_shape); + snap_anno_axis[axis_index]->SetWidth(1.0); + snap_anno_axis[axis_index]->SetColor(Quantity_Color(c.x, c.y, c.z, Quantity_TOC_RGB)); + ctx.Display(snap_anno_axis[axis_index], true); + } + else + { + snap_anno_axis[axis_index]->Set(anno_shape); + ctx.Redisplay(snap_anno_axis[axis_index], true); + } } -void Sketch_nodes::finalize() { m_prev_num_nodes = m_nodes.size(); } - -void Sketch_nodes::cancel() { m_nodes.resize(m_prev_num_nodes); } - -void Sketch_nodes::clear_outside_snap_pnts() { m_outside_snap_pts.clear(); } - -void Sketch_nodes::add_outside_snap_pnt(const gp_Pnt& pt3d) { m_outside_snap_pts.insert(to_2d(m_pln, pt3d)); } - -// Snap distance related -double Sketch_nodes::s_snap_dist_pixels = 35.0; -Sketch_nodes::Snap_guide_mode Sketch_nodes::s_snap_guide_mode = Snap_guide_mode::Traditional; -glm::vec3 Sketch_nodes::s_snap_guide_color{0.0f, 1.0f, 0.0f}; +// === Snap settings ========================================================= void Sketch_nodes::set_snap_dist(double snap_dist_pixels) { s_snap_dist_pixels = snap_dist_pixels; } diff --git a/src/sketch_nodes.h b/src/sketch_nodes.h index 02d1548..bd2fe6d 100644 --- a/src/sketch_nodes.h +++ b/src/sketch_nodes.h @@ -1,18 +1,16 @@ #pragma once -#include #include #include +#include #include -#include #include +#include #include "types.h" class gp_Pln; -class gp_Pnt2d; class Occt_view; -class AIS_InteractiveContext; class Sketch_json; class Sketch_nodes @@ -37,6 +35,11 @@ class Sketch_nodes Sketch_nodes(Occt_view& view, const gp_Pln& pln); ~Sketch_nodes(); + Sketch_nodes(const Sketch_nodes&) = delete; + Sketch_nodes& operator=(const Sketch_nodes&) = delete; + Sketch_nodes(Sketch_nodes&&) = delete; + Sketch_nodes& operator=(Sketch_nodes&&) = delete; + size_t add_new_node(const gp_Pnt2d& pt, bool is_edge_mid_point = false, bool is_permanent = false); std::optional snap(const ScreenCoords& screen_coords); // If no node exists at `pt`, appends one; `permanent_for_new` sets Node::permanent on that new node only. @@ -73,14 +76,12 @@ class Sketch_nodes static void get_snap_guide_color(float& r, float& g, float& b); // Methods for range-based for loop support - // clang-format off - auto begin() { return m_nodes.begin(); } - auto end() { return m_nodes.end(); } - auto begin() const { return m_nodes.begin(); } - auto end() const { return m_nodes.end(); } - auto cbegin() const { return m_nodes.cbegin(); } - auto cend() const { return m_nodes.cend(); } - // clang-format on + std::vector::iterator begin(); + std::vector::iterator end(); + std::vector::const_iterator begin() const; + std::vector::const_iterator end() const; + std::vector::const_iterator cbegin() const; + std::vector::const_iterator cend() const; private: friend class Sketch_json; @@ -90,24 +91,6 @@ class Sketch_nodes /// Assign slot `idx` (used after `json_resize`). void json_set_node(size_t idx, const gp_Pnt2d& pt, bool deleted, bool midpoint, bool permanent, const std::string& name = {}); - void update_node_snap_anno_(const gp_Pnt2d& pt, const double snap_dist); - void update_axis_snap_anno_(int axis_index, const gp_Pnt2d& axis_pt, double snap_dist); - /// World-space snap radius at `pt` (same convention as `try_get_node_idx_snap` / `try_pick_existing_node`). - double snap_radius_world_(const gp_Pnt2d& pt) const; - bool view_bounds_2d_(double& min_u, double& min_v, double& max_u, double& max_v) const; - - std::vector m_nodes; - static double s_snap_dist_pixels; // Global to all sketches - static Snap_guide_mode s_snap_guide_mode; - static glm::vec3 s_snap_guide_color; - std::set m_outside_snap_pts; // Projected snap points from other sketches. - AIS_Shape_ptr m_snap_anno_axis[2]; - std::optional m_last_snap_pt; // Used for snap annotation - AIS_Shape_ptr m_snap_anno; - size_t m_prev_num_nodes{0}; // Used when a operation is canceled. - - // Owner related - Occt_view& m_view; - AIS_InteractiveContext& m_ctx; - const gp_Pln m_pln; -}; \ No newline at end of file + struct Impl; + std::unique_ptr m_impl; +}; diff --git a/third_party/ImGuiColorTextEdit b/third_party/ImGuiColorTextEdit new file mode 160000 index 0000000..fa6fd43 --- /dev/null +++ b/third_party/ImGuiColorTextEdit @@ -0,0 +1 @@ +Subproject commit fa6fd434d3db973e604653d2147316f41d49ed56 diff --git a/usage-settings.md b/usage-settings.md index 11c9c66..38dc4f0 100644 --- a/usage-settings.md +++ b/usage-settings.md @@ -48,15 +48,23 @@ Between those, the pane has **six** collapsible sections. Expand a section to se 4. **3D view grid** — **Fine grid lines** and **Major grid lines** (passed to Open CASCADE `Aspect_Grid::SetColors`: dense lines vs every-tenth emphasis lines). **Grid step**, **Grid extent X / Y** (full span edge-to-edge), and **Grid display Z offset** in the Settings pane use the **same length scale as sketch length dimensions** (display value = model value / internal `dimension_scale`, default **100**). Saved JSON (`occt_view`) stores **half-extent** in model units for OCCT (`grid_graphic_*`); Settings shows **full** extent (twice the stored half-extent). -5. **Sketch** — **Dimension line width** — slider **0.5** to **8.0** (has `(?)`). **Underlay highlight color** — RGB (has `(?)`). +5. **Sketch** — Expand **Dimensions** (nested, open by default) for length-dimension appearance and behavior (most rows have `(?)` tooltips). Other sketch rows stay in the parent **Sketch** section: + - **Dimension line width** — slider **0.5** to **8.0** + - **Dimension arrow size** — slider **1.0** to **24.0** + - **Dimension color** — RGB for dimension lines, arrow heads, and value text + - **Dimension text scale** — slider **0.5** to **3.0** (multiplier on label height) + - **Label rendering** — *Opaque 2D text*, *SetCommonColor*, *2D screen text*, *3D text*, *Z-layer Top*, *Z-layer Topmost* (default) + - **Length value placement** — combo: *Near first point*, *Near second point*, *Center on dimension line*, *Automatic* (persisted as `edge_dim_label_h` **0**–**3**; updates existing dimensions live) + - **Arrow style** — *Standard*, *Sharp*, *Wide*, or *3D shaded* + - **Arrow orientation** — *Automatic*, *Internal*, or *External* + - **Show sketch dimensions** — global on/off for length dimensions on all sketches (tool mode may still limit which sketch shows dims when on) + - **Permanent node annotation size**, **Underlay highlight color**, **Snap guide color**, **Snap guide mode** (directly under **Sketch**, not inside **Dimensions**) 6. **Startup project** — **Desktop only:** **Load last opened on startup** (checkbox, with `(?)`), then **Last opened path:** … or **(No path saved yet.)** Then **Save current as startup project**, **Clear saved startup** (with `(?)`). **WebAssembly:** no load-last row; only the two buttons and `(?)`. See [Startup project](#startup-project). **Not in this pane** - **View** menu items such as **Options**, **Sketch List**, **Lua Console** — they only show or hide panes; they are not rows inside **Settings**. Their visibility is still saved under `gui.*` in the settings file (see [Settings file reference](#settings-file-reference)). -- **Length value placement** for edge dimensions — **Options** panel when the edge-dimension tool is active; see [Options panel](#options-panel). - **Saving** — On desktop, settings are written when you change options that save, and on exit. On **Emscripten**, use **File -> Save settings** so the browser persists (see [Where settings are stored](#where-settings-are-stored)). ## Options panel @@ -82,13 +90,12 @@ For other non-sketch Options content (for example **Polar duplicate**), see [usa Sketch-related preferences are edited in the **Options** panel while you use a sketch tool, not in the **Settings** pane: -- **Sketch options** (all sketch tools): **Snap dist** and **Snap guide mode** (*Traditional*, *Fullscreen*, *Both*). See [How sketch snap works](usage-sketch.md#sketch-snapping) in the sketch guide (axis guides, vertex lock, cross-sketch targets). -- **Toggle edge dimension** (length dimensions): **Length value placement** - combo: *Near first point*, *Near second point*, *Center on dimension line*, *Automatic*. Maps to the `edge_dim_label_h` key (integers **0** through **3**). Changing it persists like other GUI flags. +- **Sketch options** (all sketch tools): **Snap dist** and **Snap guide mode** (*Traditional*, *Fullscreen*, *Both*). See [How sketch snap works](usage-sketch.md#sketch-snapping) in the sketch guide (axis guides, vertex lock, cross-sketch targets). Snap guide color and mode are also in **Settings -> Sketch**. - **Extrude sketch face**: under **Extrude**, **Both sides** and **Material** for the new solid (same document preset as **Normal** mode Options **Material**). Other modes that still show **Material** in Options use that same preset when relevant (for example **Sketch from planar face**). - **Add edge** / **Add node** (and similar): a **Shortcuts** line documents TAB / Shift+TAB typing behavior. - **Sketch operation** (mirror / revolve axis): mirror, revolve, angle, and clear-axis actions (see [usage-sketch.md](usage-sketch.md#operation-axis-tool)). -**Dimension line width** for length dimensions is in **Settings -> Sketch** (see above). +Global length-dimension style (line width, arrows, color, text) is in **Settings -> Sketch**. Per-dimension visibility, name, and offset remain in **Sketch List -> Dimensions** (saved in the project `.ezy` file). ## Where settings are stored @@ -153,8 +160,15 @@ String: ImGui `.ini` text for window positions and docking saved with **SaveIniS | `show_python_console` | boolean | Python console pane visible (native builds with Python). | | `show_dbg` | boolean | Debug pane visible (debug builds only). | | `inspection_orthographic` | boolean | **Normal** mode Options: orthographic camera when true (default false). | -| `edge_dim_label_h` | integer | Length dimension label placement: **0** to **3** (see [Options panel](#options-panel)). Values outside this range are ignored. | -| `edge_dim_line_width` | number | Sketch length dimension line width (allowed range **0.5** to **8.0** in code). | +| `edge_dim_label_h` | integer | Length dimension label placement: **0** near first point, **1** near second, **2** center, **3** automatic. | +| `edge_dim_line_width` | number | Sketch length dimension line width (**0.5** to **8.0**). | +| `edge_dim_arrow_size` | number | Arrow head length (**1.0** to **24.0**). | +| `edge_dim_color` | array of 3 numbers | Dimension line, arrow, and text RGB (**0** to **1** per channel; default yellow). | +| `edge_dim_text_scale` | number | Label height multiplier (**0.5** to **3.0**; default **1.0**). | +| `edge_dim_text_render_mode` | integer | **0** opaque 2D, **1** SetCommonColor, **2** 2D screen, **3** 3D text, **4** Z Top, **5** Z Topmost (default). | +| `edge_dim_arrow_style` | integer | **0** standard, **1** sharp, **2** wide, **3** 3D shaded. | +| `edge_dim_arrow_orientation` | integer | **0** automatic, **1** internal, **2** external. | +| `show_sketch_dimensions` | boolean | When false, hides length dimensions on all sketches. | | `imgui_rounding_general` | number | Window/child/frame/popup rounding (**0** to **32** clamped in code; sliders stop at 16 in the UI). | | `imgui_rounding_scroll` | number | Scrollbar and grab rounding (same clamp). | | `imgui_rounding_tabs` | number | Tab rounding (same clamp). | @@ -164,7 +178,7 @@ String: ImGui `.ini` text for window positions and docking saved with **SaveIniS | `load_last_opened_on_startup` | boolean | Desktop: open the last `.ezy` on launch. **Legacy:** `load_last_saved_on_startup` is read as a fallback if the newer key is absent. | | `last_opened_project_path` | string | Path of the last opened project for the option above. **Legacy:** `last_saved_project_path` is accepted if the newer key is missing. | -Scripting API **`ezy.occt_view_settings_json()`** returns a JSON string with **`occt_view`** plus selected **`gui`** keys (including **`gui.inspection_orthographic`**, **`gui.edge_dim_label_h`**, **`gui.edge_dim_line_width`**, **`gui.view_roll_step_deg`**, **`gui.view_zoom_scroll_scale`** when saved). See [scripting.md](scripting.md). +Scripting API **`ezy.occt_view_settings_json()`** returns a JSON string with **`occt_view`** plus selected **`gui`** keys (including dimension keys above, **`gui.inspection_orthographic`**, **`gui.view_roll_step_deg`**, **`gui.view_zoom_scroll_scale`** when saved). See [scripting.md](scripting.md). ---