From 57850e468d687276eff62b3d93f506e7b67a0643 Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Tue, 21 Apr 2026 15:13:45 +0200 Subject: [PATCH 01/61] Tessellator | Docker | Create required docker files --- .devcontainer/DOCKER_README.md | 52 +++++++++ .devcontainer/Dockerfile.base | 24 ++++ .devcontainer/devcontainer.json | 26 +++++ .gitignore | 4 +- .vscode/settings.dev.json | 5 + CLAUDE.md | 201 ++++++++++++++++++++++++++++++++ CMakeLists.txt | 14 +++ CMakePresets.json | 18 +++ 8 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/DOCKER_README.md create mode 100644 .devcontainer/Dockerfile.base create mode 100644 .devcontainer/devcontainer.json create mode 100644 .vscode/settings.dev.json create mode 100644 CLAUDE.md diff --git a/.devcontainer/DOCKER_README.md b/.devcontainer/DOCKER_README.md new file mode 100644 index 0000000..7da37e8 --- /dev/null +++ b/.devcontainer/DOCKER_README.md @@ -0,0 +1,52 @@ +# Dev Container Setup + +This directory contains the configuration for a Docker-based development environment for Tessellator. + +## Quick Start + +### Prerequisites +- Docker installed and running +- VS Code with the "Dev Containers" extension + +### Opening the Project in Dev Container + +1. Open this repository in VS Code +2. Click the green icon in the bottom-left corner and select **"Reopen in Container"** +3. VS Code will build the Docker image and start the container automatically + +The entire workspace will be mounted at `/workspace` inside the container. + +## What's Included + +The dev container provides: +- Pre-configured C++ development environment +- CMake and Ninja build tools +- All project dependencies (VTK, Boost, CGAL via vcpkg) +- VS Code extensions: + - **C/C++ Tools** – Code navigation, IntelliSense, debugging + - **CMake Tools** – CMake project management + - **LLDB** – Debugger for C++ + +## Building and Testing + +Once inside the container: + +```bash +# Configure the project +cmake --preset docker -S . -B build + +# Build +cmake --build build -j + +# Run tests +build/bin/tessellator_tests + +# Run the application +build/bin/tessellator -i +``` + +## Notes + +- The container runs as root user +- CMake is pre-configured to skip auto-configure on file open for better performance +- Debugging with LLDB is enabled with appropriate capabilities diff --git a/.devcontainer/Dockerfile.base b/.devcontainer/Dockerfile.base new file mode 100644 index 0000000..63abed7 --- /dev/null +++ b/.devcontainer/Dockerfile.base @@ -0,0 +1,24 @@ +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + ninja-build \ + git \ + pkg-config \ + curl \ + wget \ + zip \ + unzip + +RUN apt-get install -y \ + libboost-graph-dev \ + libboost-program-options-dev \ + libvtk9-dev \ + nlohmann-json3-dev \ + libgtest-dev \ + libgmock-dev \ + libgmp-dev \ + libmpfr-dev + +WORKDIR /workspace diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..aea4b2d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +{ + "name": "Tessellator", + "dockerFile": "Dockerfile.base", + "context": "..", + "remoteUser": "root", + "workspaceFolder": "/workspace", + "mounts": [ + "source=${localWorkspaceFolder},target=/workspace,type=bind", + ], + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "vadimcn.vscode-lldb" + ], + "settings": { + "cmake.configureOnOpen": false, + "cmake.configureOnEdit": false, + "cmake.autoSelectActiveFolder": false, + "cmake.generator": "Ninja" + } + } + }, + "runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"] +} diff --git a/.gitignore b/.gitignore index 2c7a665..0a08b75 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,9 @@ build/ src/*.json .vs/ -.vscode/ +.vscode/settings.json +.vscode/launch.json +.vscode/tasks.json testData/*_out.stl *_out.stl diff --git a/.vscode/settings.dev.json b/.vscode/settings.dev.json new file mode 100644 index 0000000..7214448 --- /dev/null +++ b/.vscode/settings.dev.json @@ -0,0 +1,5 @@ +{ + "cmake.configureOnOpen": false, + "cmake.configureOnEdit": false, + "cmake.autoSelectActiveFolder": false +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fa6ab49 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,201 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +**Tessellator** is a C++17 mesher designed to generate meshes and data structures optimized for FDTD algorithms. The project supports multiple meshing strategies (staircased, conformal, offgrid) and includes extensive test coverage using Google Test. + +## Key Architecture + +### Core Components + +The codebase is organized into modular namespaces under `src/`: + +- **`meshers/`** – Abstract mesh generation interface with three implementations: + - `StaircaseMesher` – Generates staircased meshes from geometric inputs + - `ConformalMesher` – Creates conformal meshes with fixed-distance grid plane intersections + - `OffgridMesher` – Produces offgrid meshes (graded rectilinear support) + - All inherit from `MesherBase` which handles input mesh processing and grid construction + +- **`core/`** – Fundamental mesh processing algorithms: + - `Slicer` – Slices input geometry along grid planes + - `Staircaser` – Converts sliced geometry into staircase patterns + - `Smoother` – Post-processing to smooth mesh irregularities + - `Collapser` – Merges adjacent elements for optimization + - `Snapper` – Aligns geometry with grid coordinates + +- **`types/`** – Core data structures: + - `Mesh` – Central data structure holding coordinates, elements (nodes/lines/surfaces/volumes), and metadata + - `Mesher` – Abstract interface that all meshers implement + - `Vector` – Template-based 3D vector type + - `Grid` – Rectilinear grid representation (3D array of coordinate positions) + +- **`cgal/`** – Optional CGAL-based advanced geometry operations (controlled by `TESSELLATOR_ENABLE_CGAL`): + - `Filler` – Fills closed polyhedrons using CGAL + - `Delaunator` – Triangulation operations + - `Manifolder` – Manifold repair utilities + - `Repairer` – Polyhedron repair tools + - `HPolygonSet`, `PolyhedronTools` – Polygon/polyhedron manipulation + +- **`utils/`** – Utility functions: + - `MeshTools`, `GridTools` – Mesh/grid manipulation + - `Geometry` – Geometric calculations + - `ConvexHull` – Convex hull computation + - `CoordGraph`, `ElemGraph` – Topological graph representations + - `RedundancyCleaner` – Removes redundant geometric data + +- **`app/`** – Application interface: + - `launcher` – CLI entry point and argument parsing + - `vtkIO` – VTK file import/export (STL, VTK formats) + +### Data Flow + +Input geometry (STL/VTK) → VTK I/O → Mesher (chooses algorithm) → Core processing (slicer/staircaser/smoother) → Grid alignment → Output mesh (VTK) + +## Build System + +Uses **CMake 3.20+** with presets and vcpkg for dependency management. + +### Build Presets + +```bash +# GNU/Linux (Ninja) +cmake --preset gnu -S . -B build +cmake --build build -j + +# Windows (MSBuild) +cmake --preset msbuild -S . -B build +cmake --build build --config Release -j +``` + +### CMake Options + +- `TESSELLATOR_ENABLE_TESTS` (ON by default) – Build test suite +- `TESSELLATOR_ENABLE_CGAL` (ON by default) – Enable CGAL-based geometry operations +- `TESSELLATOR_EXECUTION_POLICIES` (OFF by default) – Parallel execution policies + +### Dependencies + +Managed via vcpkg manifest: +- **Required**: VTK, Boost +- **Optional**: CGAL (if `TESSELLATOR_ENABLE_CGAL=ON`) + +To set up locally, create a `CMakeUserPreset.json` file: + +```json +{ + "version": 4, + "include": ["CMakePresets.json"], + "configurePresets": [ + { + "name": "gnu-local", + "environment": { + "VCPKG_ROOT": "~/workspace/vcpkg/" + }, + "cacheVariables": { + "TESSELLATOR_ENABLE_CGAL": true + }, + "inherits": "gnu" + } + ] +} +``` + +## Common Development Tasks + +### Build + +```bash +cmake --preset gnu -S . -B build +cmake --build build -j +``` + +Output binaries go to `build/bin/` and libraries to `build/lib/`. + +### Run Tests + +```bash +# Run all tests +build/bin/tessellator_tests + +# Run specific test suite (e.g., all StaircaseMesher tests) +build/bin/tessellator_tests --gtest_filter=StaircaseMesher* + +# Run single test method +build/bin/tessellator_tests --gtest_filter=StaircaseMesherTest.TestName +``` + +Tests are organized by module under `test/` mirroring the source structure. + +### Run Application + +```bash +# Build and run with a tessellator JSON input file +build/bin/tessellator -i mesh_definition.tessellator.json +``` + +## Code Style & Patterns + +- **C++17** standard; no C++20+ features +- **Namespaces**: Organized as `meshlib`, `meshlib::meshers`, `meshlib::core`, etc. +- **Mesh representation**: All geometry flows through the `Mesh` object with its `Element` struct (type: Node/Line/Surface/Volume) +- **Grid**: Stored as `Grid = std::array, 3>` (coordinate arrays per axis) +- **Logging**: Use static `MesherBase::log()` methods with indentation level for hierarchical output +- **Vertex references**: Elements reference coordinates by index (`CoordinateId`) + +## Testing + +Google Test (GTest) framework with fixtures. Tests for each module live in `test//`. Key fixtures: + +- `MeshFixtures.h` – Shared mesh construction utilities for tests + +**Important**: When modifying core algorithms (Slicer, Staircaser, Smoother, Snapper, Collapser), update corresponding tests in `test/core/` and `test/meshers/`. + +## JSON Input Format + +The tessellator expects a JSON file with two main entries: + +### Grid Definition + +Define the grid structure using either uniform or rectilinear (graded) cells: + +```json +"grid": { + "numberOfCells": [20, 20, 30], + "boundingBox": [[-1, -1, -1], [1, 1, 2]] +} +``` + +Or for non-uniform grids: + +```json +"grid": { + "planes": [ + [600, 603.25], + [25.0, 30.5, 92, 130, 1000], + [1000, 1111, 1111.1] + ] +} +``` + +### Object Definition + +```json +"object": {"filename": "geometry.stl"} +``` + +The filename is relative to the JSON file location. + +## File Organization + +- **Headers** (`.h`) – Include guards, template implementations +- **Implementations** (`.cpp`) – Separate compilation units +- **Test files** – Follow `Test.cpp` pattern with GTest syntax + +## Known Constraints + +- CGAL-dependent code is conditionally compiled; check `if (TESSELLATOR_ENABLE_CGAL)` guards when making changes +- Grid must be rectilinear; arbitrary unstructured grids are not supported +- Mesh must be manifold for some operations (see `Manifolder`) +- Elements reference coordinates by index, not direct storage diff --git a/CMakeLists.txt b/CMakeLists.txt index c9c568f..710b3b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,23 @@ cmake_minimum_required(VERSION 3.20) +# Toolchain detection: support both vcpkg (local dev) and system libraries (Docker) if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{CMAKE_TOOLCHAIN_FILE}) set(CMAKE_TOOLCHAIN_FILE $ENV{CMAKE_TOOLCHAIN_FILE}) endif() +# If CMAKE_TOOLCHAIN_FILE is set, verify it exists (avoid silent failures) +if(DEFINED CMAKE_TOOLCHAIN_FILE AND NOT EXISTS "${CMAKE_TOOLCHAIN_FILE}") + message(WARNING "CMAKE_TOOLCHAIN_FILE specified but not found: ${CMAKE_TOOLCHAIN_FILE}") + message(STATUS "Falling back to system libraries (Docker mode)") + unset(CMAKE_TOOLCHAIN_FILE) +endif() + +if(DEFINED CMAKE_TOOLCHAIN_FILE) + message(STATUS "Using vcpkg toolchain: ${CMAKE_TOOLCHAIN_FILE}") +else() + message(STATUS "Using system libraries (vcpkg toolchain not detected)") +endif() + set(CMAKE_CXX_STANDARD 17) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) diff --git a/CMakePresets.json b/CMakePresets.json index d80f52a..322d7c9 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -27,12 +27,30 @@ "displayName": "GNU g++ compiler", "generator": "Ninja", "inherits": "default" + }, + { + "name": "docker", + "displayName": "Docker (system libraries)", + "description": "Uses system-installed libraries, skips vcpkg. Optimized for containerized builds.", + "generator": "Ninja", + "binaryDir": "build/", + "cacheVariables": { + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_PREFIX_PATH": "/usr/local", + "CMAKE_FIND_ROOT_PATH": "/usr/local", + "TESSELLATOR_ENABLE_TESTS": "ON", + "TESSELLATOR_ENABLE_CGAL": "OFF" + } } ], "buildPresets": [ { "name": "default", "configurePreset": "default" + }, + { + "name": "docker", + "configurePreset": "docker" } ] } \ No newline at end of file From 1c7c6dc8f2583666a7a39947e342a4faf838d4b9 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Wed, 8 Jul 2026 15:20:49 +0200 Subject: [PATCH 02/61] changes citation to make zenodo compliant --- CITATION.cff | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 5368c9d..8b24535 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -13,6 +13,4 @@ authors: - family-names: Rubio Bretones given-names: Amelia orcid: https://orcid.org/00000-0002-9337-9093 -url: "https://github.com/opensemba/tessellator" -title: "opensemba/tessellator an FDTD meshing tool" -date-released: 2024-07-10 \ No newline at end of file +title: "opensemba/tessellator an FDTD meshing tool" \ No newline at end of file From 19f52dce57605ce274f100202b2859c910893307 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Wed, 8 Jul 2026 15:25:55 +0200 Subject: [PATCH 03/61] fixes citation file --- CITATION.cff | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 8b24535..86152ca 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,5 +12,7 @@ authors: orcid: https://orcid.org/0000-0001-7317-1423 - family-names: Rubio Bretones given-names: Amelia - orcid: https://orcid.org/00000-0002-9337-9093 -title: "opensemba/tessellator an FDTD meshing tool" \ No newline at end of file + orcid: https://orcid.org/0000-0002-9337-9093 +url: "https://github.com/opensemba/tessellator" +title: "opensemba/tessellator an FDTD meshing tool" +date-released: 2024-07-10 \ No newline at end of file From 6baa800b3d35f083c438571057d92c3d7f9f0a48 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Fri, 10 Jul 2026 10:23:51 +0000 Subject: [PATCH 04/61] Tessellator | Environment | #72 | Use testMate to run all tests within vscode test explorer. --- .devcontainer/Dockerfile.base | 5 +- .devcontainer/devcontainer.json | 9 ++- .vscode/settings.dev.json | 4 +- CMakePresets.json | 125 ++++++++++++++++++-------------- 4 files changed, 84 insertions(+), 59 deletions(-) diff --git a/.devcontainer/Dockerfile.base b/.devcontainer/Dockerfile.base index 63abed7..b1f20ad 100644 --- a/.devcontainer/Dockerfile.base +++ b/.devcontainer/Dockerfile.base @@ -21,4 +21,7 @@ RUN apt-get install -y \ libgmp-dev \ libmpfr-dev -WORKDIR /workspace +RUN chown -R ubuntu:ubuntu /home/ubuntu + +USER ubuntu +WORKDIR /home/ubuntu/dev diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index aea4b2d..99b7fce 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,17 +2,18 @@ "name": "Tessellator", "dockerFile": "Dockerfile.base", "context": "..", - "remoteUser": "root", - "workspaceFolder": "/workspace", + "remoteUser": "ubuntu", + "workspaceFolder": "/home/ubuntu/dev", "mounts": [ - "source=${localWorkspaceFolder},target=/workspace,type=bind", + "source=${localWorkspaceFolder},target=/home/ubuntu/dev,type=bind" ], "customizations": { "vscode": { "extensions": [ "ms-vscode.cpptools", "ms-vscode.cmake-tools", - "vadimcn.vscode-lldb" + "vadimcn.vscode-lldb", + "matepek.vscode-catch2-test-adapter" ], "settings": { "cmake.configureOnOpen": false, diff --git a/.vscode/settings.dev.json b/.vscode/settings.dev.json index 7214448..45347a3 100644 --- a/.vscode/settings.dev.json +++ b/.vscode/settings.dev.json @@ -1,5 +1,7 @@ { "cmake.configureOnOpen": false, "cmake.configureOnEdit": false, - "cmake.autoSelectActiveFolder": false + "cmake.autoSelectActiveFolder": false, + "testMate.cpp.test.executables": "{build,build-dbg,Build,BUILD,out,Out,OUT}/**/*{test,Test,TEST}*", + "testMate.cpp.test.workingDirectory": "${workspaceFolder}" } diff --git a/CMakePresets.json b/CMakePresets.json index 322d7c9..8c3893e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,56 +1,75 @@ { - "version": 4, - "configurePresets": [ - { - "name": "default", - "hidden": true, - "binaryDir": "build/", - "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": { - "type": "FILEPATH", - "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + "version": 4, + "configurePresets": [ + { + "name": "default", + "hidden": true, + "binaryDir": "build/", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": { + "type": "FILEPATH", + "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + } + }, + { + "name": "msbuild", + "displayName": "MSBuild Configure Settings", + "generator": "Visual Studio 17 2022", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "inherits": "default" + }, + { + "name": "gnu", + "displayName": "GNU g++ compiler", + "generator": "Ninja", + "inherits": "default" + }, + { + "name": "docker", + "displayName": "Docker (system libraries)", + "description": "Uses system-installed libraries, skips vcpkg. Optimized for containerized builds.", + "generator": "Ninja", + "binaryDir": "build/", + "cacheVariables": { + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_PREFIX_PATH": "/usr/local", + "CMAKE_FIND_ROOT_PATH": "/usr/local", + "TESSELLATOR_ENABLE_TESTS": "ON", + "TESSELLATOR_ENABLE_CGAL": "OFF" + } + }, + { + "name": "docker-dbg", + "inherits": "docker", + "displayName": "Docker-dbg (system libraries)", + "description": "Uses system-installed libraries, skips vcpkg. Optimized for containerized builds. Debug mode", + "binaryDir": "build-dbg/", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_PREFIX_PATH": "/usr/local", + "CMAKE_FIND_ROOT_PATH": "/usr/local", + "TESSELLATOR_ENABLE_TESTS": "ON", + "TESSELLATOR_ENABLE_CGAL": "OFF" + } } - } - }, - { - "name": "msbuild", - "displayName": "MSBuild Configure Settings", - "generator": "Visual Studio 17 2022", - "architecture": { - "strategy": "set", - "value": "x64" - }, - "inherits": "default" - }, - { - "name": "gnu", - "displayName": "GNU g++ compiler", - "generator": "Ninja", - "inherits": "default" - }, - { - "name": "docker", - "displayName": "Docker (system libraries)", - "description": "Uses system-installed libraries, skips vcpkg. Optimized for containerized builds.", - "generator": "Ninja", - "binaryDir": "build/", - "cacheVariables": { - "CMAKE_CXX_COMPILER": "g++", - "CMAKE_PREFIX_PATH": "/usr/local", - "CMAKE_FIND_ROOT_PATH": "/usr/local", - "TESSELLATOR_ENABLE_TESTS": "ON", - "TESSELLATOR_ENABLE_CGAL": "OFF" - } - } - ], - "buildPresets": [ - { - "name": "default", - "configurePreset": "default" - }, - { - "name": "docker", - "configurePreset": "docker" - } - ] + ], + "buildPresets": [ + { + "name": "default", + "configurePreset": "default" + }, + { + "name": "docker", + "configurePreset": "docker" + }, + { + "name": "docker-dbg", + "configurePreset": "docker-dbg" + } + ] } \ No newline at end of file From 6cb14dc20a6d6735b24db665871b4970923d8a71 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Fri, 10 Jul 2026 11:51:52 +0000 Subject: [PATCH 05/61] Tessellator | Workflow | #72 | Update vpkg binary cache for GitHub actions --- .github/workflows/build-and-release.yml | 14 ++++++++++++-- .github/workflows/build-and-test.yml | 19 ++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index e7719cf..fc1e4d2 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -5,7 +5,8 @@ on: branches: - main -env: +env: + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" WINDOWS_FILENAME: opensemba-tessellator-windows-x64.tar.gz LINUX_FILENAME: opensemba-tessellator-linux.tar.gz @@ -27,7 +28,7 @@ jobs: strategy: matrix: preset: [ - {"os": windows-latest, "name": "msbuild", "filename": "windows-x64"}, + {"os": windows-2022, "name": "msbuild", "filename": "windows-x64"}, {"os": ubuntu-latest, "name": "gnu", "filename": "linux"} ] build-type: ["Release"] @@ -59,8 +60,17 @@ jobs: sudo apt-get update sudo apt-get install -y libvtk9-dev + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + - name: Setup vcpkg uses: lukka/run-vcpkg@v11 + with: + doNotCache: false - name: Windows configure and build if: matrix.preset.name=='msbuild' diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 73d442b..66b4fb0 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -5,17 +5,20 @@ on: branches: - main - dev - + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true + +env: + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" jobs: builds-and-tests: strategy: matrix: preset: [ - {"os": windows-latest, "name": "msbuild"}, + {"os": windows-2022, "name": "msbuild"}, {"os": ubuntu-latest, "name": "gnu"} ] build-type: ["Debug", "Release"] @@ -45,8 +48,17 @@ jobs: sudo apt-get update sudo apt-get install -y libvtk9-dev + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + - name: Setup vcpkg uses: lukka/run-vcpkg@v11 + with: + doNotCache: false - name: Windows configure and build if: matrix.preset.name=='msbuild' @@ -67,6 +79,3 @@ jobs: - name: Ubuntu Run tests if: matrix.preset.name=='gnu' run: build/bin/tessellator_tests - - - \ No newline at end of file From 558b4960750579b339a14066ff75453bddd20696 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Mon, 13 Jul 2026 14:37:28 +0000 Subject: [PATCH 06/61] Tessellator | Build | #72 | Update vcpkg version --- .github/workflows/build-and-test.yml | 5 +++++ vcpkg-configuration.json | 2 +- vcpkg.json | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 66b4fb0..0b32117 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -48,6 +48,11 @@ jobs: sudo apt-get update sudo apt-get install -y libvtk9-dev + - name: Install prior dependencies in ubuntu + if: matrix.preset.os=='ubuntu-latest' + run: | + sudo apt install autoconf autoconf-archive automake libtool + - name: Export GitHub Actions cache environment variables uses: actions/github-script@v7 with: diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 563e537..3abe562 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,7 +1,7 @@ { "default-registry": { "kind": "git", - "baseline": "e2e3f654bc45c28f2696ace32d66f28429bb3b28", + "baseline": "cd61e1e26a038e82d6550a3ebbe0fbbfe7da78e3", "repository": "https://github.com/microsoft/vcpkg" }, "registries": [ diff --git a/vcpkg.json b/vcpkg.json index d4ed077..0d944bc 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,5 +1,6 @@ { "dependencies": [ + "libaec", "boost-graph", "boost-program-options", { "name": "vtk", "default-features": false, "platform": "windows"}, @@ -11,5 +12,8 @@ "description": "Enables CGAL features: Offgrid mesher, manifolding, repairer, etc.", "dependencies": ["cgal", "eigen3"] } - } + }, + "overrides": [ + { "name": "libaec", "version": "1.1.3#1" } + ] } From 2b56447f1d06944b23c632989e7436dc41c1be02 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Tue, 14 Jul 2026 09:48:06 +0000 Subject: [PATCH 07/61] Tessellator | Build | Implement vcpkg overlay ports --- .github/workflows/build-and-release.yml | 2 -- .github/workflows/build-and-test.yml | 7 ---- overlayPorts/libaec/README.md | 12 +++++++ overlayPorts/libaec/portfile.cmake | 44 +++++++++++++++++++++++++ overlayPorts/libaec/usage | 7 ++++ overlayPorts/libaec/vcpkg.json | 17 ++++++++++ vcpkg-configuration.json | 5 ++- vcpkg.json | 9 ++--- 8 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 overlayPorts/libaec/README.md create mode 100644 overlayPorts/libaec/portfile.cmake create mode 100644 overlayPorts/libaec/usage create mode 100644 overlayPorts/libaec/vcpkg.json diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index fc1e4d2..7d92d77 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -69,8 +69,6 @@ jobs: - name: Setup vcpkg uses: lukka/run-vcpkg@v11 - with: - doNotCache: false - name: Windows configure and build if: matrix.preset.name=='msbuild' diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0b32117..64b1f67 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -48,11 +48,6 @@ jobs: sudo apt-get update sudo apt-get install -y libvtk9-dev - - name: Install prior dependencies in ubuntu - if: matrix.preset.os=='ubuntu-latest' - run: | - sudo apt install autoconf autoconf-archive automake libtool - - name: Export GitHub Actions cache environment variables uses: actions/github-script@v7 with: @@ -62,8 +57,6 @@ jobs: - name: Setup vcpkg uses: lukka/run-vcpkg@v11 - with: - doNotCache: false - name: Windows configure and build if: matrix.preset.name=='msbuild' diff --git a/overlayPorts/libaec/README.md b/overlayPorts/libaec/README.md new file mode 100644 index 0000000..ca36a07 --- /dev/null +++ b/overlayPorts/libaec/README.md @@ -0,0 +1,12 @@ +Original port: https://github.com/microsoft/vcpkg/tree/fedfb6ae868887a13f8a0ba3b29603bd7eb9f118/ports/libaec + +A different download url is used to avoid connection errors. + +libaec provides CMake targets: +``` Cmake + find_package(libaec CONFIG REQUIRED) + # libaec API + target_link_libraries(main PRIVATE libaec::aec) + # szip compatible API + target_link_libraries(main PRIVATE libaec::sz) +``` \ No newline at end of file diff --git a/overlayPorts/libaec/portfile.cmake b/overlayPorts/libaec/portfile.cmake new file mode 100644 index 0000000..0d22668 --- /dev/null +++ b/overlayPorts/libaec/portfile.cmake @@ -0,0 +1,44 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO Deutsches-Klimarechenzentrum/libaec + REF v${VERSION} + SHA512 57ffa961efe9329928bd6199881ff9f6d1f4072d08f0e11bafe19b09bbc6dbc5e0003e52d83a8ba72a50518bf39935a92838f9a27d002b33ab095d99ffc5a838 + HEAD_REF master +) + +string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "static" BUILD_STATIC) + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS + -DBUILD_STATIC_LIBS=${BUILD_STATIC} + -Dlibaec_INSTALL_CMAKEDIR=share/${PORT} +) +vcpkg_cmake_install() +vcpkg_copy_pdbs() +vcpkg_cmake_config_fixup() +vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/share/libaec/libaec-config.cmake" + "if(libaec_USE_STATIC_LIBS)" + "if(\"${BUILD_STATIC}\") # forced by vcpkg" +) + +# Compatibility with user's CMake < 3.18 (vcpkg claims support for >= 3.16): +# Make imported targets global so that libaec-config.cmake can create ALIAS targets. +set(_target_file "libaec_shared-targets") +if(BUILD_STATIC) + set(_target_file "libaec_static-targets") +endif() +file(READ "${CURRENT_PACKAGES_DIR}/share/libaec/${_target_file}.cmake" libaec_targets) +string(REGEX REPLACE " (SHARED|STATIC) IMPORTED" " \\1 IMPORTED \${libaec_maybe_global}" libaec_targets "${libaec_targets}") +file(WRITE "${CURRENT_PACKAGES_DIR}/share/libaec/${_target_file}.cmake" "set(libaec_maybe_global \"\") +if(CMAKE_VERSION VERSION_LESS 3.18) + set(libaec_maybe_global \"GLOBAL\") +endif() +${libaec_targets} +" +) + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") + +file(INSTALL "${CURRENT_PORT_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") \ No newline at end of file diff --git a/overlayPorts/libaec/usage b/overlayPorts/libaec/usage new file mode 100644 index 0000000..26ebd09 --- /dev/null +++ b/overlayPorts/libaec/usage @@ -0,0 +1,7 @@ +libaec provides CMake targets: + + find_package(libaec CONFIG REQUIRED) + # libaec API + target_link_libraries(main PRIVATE libaec::aec) + # szip compatible API + target_link_libraries(main PRIVATE libaec::sz) \ No newline at end of file diff --git a/overlayPorts/libaec/vcpkg.json b/overlayPorts/libaec/vcpkg.json new file mode 100644 index 0000000..a4f3d55 --- /dev/null +++ b/overlayPorts/libaec/vcpkg.json @@ -0,0 +1,17 @@ +{ + "name": "libaec", + "version": "1.1.6", + "description": "Adaptive Entropy Coding library", + "homepage": "https://gitlab.dkrz.de/k202009/libaec", + "license": "BSD-2-Clause", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +} \ No newline at end of file diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 3abe562..e1d933f 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,7 +1,7 @@ { "default-registry": { "kind": "git", - "baseline": "cd61e1e26a038e82d6550a3ebbe0fbbfe7da78e3", + "baseline": "e2e3f654bc45c28f2696ace32d66f28429bb3b28", "repository": "https://github.com/microsoft/vcpkg" }, "registries": [ @@ -10,5 +10,8 @@ "location": "https://aka.ms/vcpkg-ce-default", "name": "microsoft" } + ], + "overlay-ports" : [ + "./overlayPorts" ] } diff --git a/vcpkg.json b/vcpkg.json index 0d944bc..81aa4bf 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,19 +1,16 @@ { "dependencies": [ - "libaec", "boost-graph", "boost-program-options", { "name": "vtk", "default-features": false, "platform": "windows"}, "nlohmann-json", - "gtest" + "gtest", + { "name": "libiconv", "version>=": "1.19" } ], "features": { "cgal": { "description": "Enables CGAL features: Offgrid mesher, manifolding, repairer, etc.", "dependencies": ["cgal", "eigen3"] } - }, - "overrides": [ - { "name": "libaec", "version": "1.1.3#1" } - ] + } } From 3e27811ea30395223ed51e3d26b4b3f38a5d7acd Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Tue, 14 Jul 2026 11:37:35 +0000 Subject: [PATCH 08/61] Tessellator | Build | Upgrade --- .github/workflows/build-and-release.yml | 2 ++ .github/workflows/build-and-test.yml | 2 ++ overlayPorts/libaec/portfile.cmake | 2 +- vcpkg.json | 3 +-- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 7d92d77..7c29fb8 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -69,6 +69,8 @@ jobs: - name: Setup vcpkg uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: eed289f6e06a5e7a5c9e6a729671b0a56af7dd69 - name: Windows configure and build if: matrix.preset.name=='msbuild' diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 64b1f67..4bf49b8 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -57,6 +57,8 @@ jobs: - name: Setup vcpkg uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: eed289f6e06a5e7a5c9e6a729671b0a56af7dd69 - name: Windows configure and build if: matrix.preset.name=='msbuild' diff --git a/overlayPorts/libaec/portfile.cmake b/overlayPorts/libaec/portfile.cmake index 0d22668..cdd5c9f 100644 --- a/overlayPorts/libaec/portfile.cmake +++ b/overlayPorts/libaec/portfile.cmake @@ -2,7 +2,7 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO Deutsches-Klimarechenzentrum/libaec REF v${VERSION} - SHA512 57ffa961efe9329928bd6199881ff9f6d1f4072d08f0e11bafe19b09bbc6dbc5e0003e52d83a8ba72a50518bf39935a92838f9a27d002b33ab095d99ffc5a838 + SHA512 76df7501d1b7d91a43b525ba828f092f18d83f8ab09a9331e5758f93942a9758ad580baca8f9316b92a98639bde2e23cacbc2f33f52d0dd98ce7efe412cf43cd HEAD_REF master ) diff --git a/vcpkg.json b/vcpkg.json index 81aa4bf..d4ed077 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -4,8 +4,7 @@ "boost-program-options", { "name": "vtk", "default-features": false, "platform": "windows"}, "nlohmann-json", - "gtest", - { "name": "libiconv", "version>=": "1.19" } + "gtest" ], "features": { "cgal": { From a72bccd69e062d11df245ff1c572ed339b2a2b05 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Tue, 9 Jun 2026 12:21:07 +0200 Subject: [PATCH 09/61] Adds MesherBase options to have stair and conf options derived from it. Adds bool isVolume to options --- src/meshers/CMakeLists.txt | 6 +++++- src/meshers/ConformalMesherOptions.h | 5 +++-- src/meshers/MesherBaseOptions.h | 14 ++++++++++++++ src/meshers/StaircaserMesherOptions.h | 13 +++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/meshers/MesherBaseOptions.h create mode 100644 src/meshers/StaircaserMesherOptions.h diff --git a/src/meshers/CMakeLists.txt b/src/meshers/CMakeLists.txt index dfdd88c..32eed77 100644 --- a/src/meshers/CMakeLists.txt +++ b/src/meshers/CMakeLists.txt @@ -6,7 +6,11 @@ add_library(tessellator-meshers "OffgridMesher.cpp" "ConformalMesher.cpp" ) -target_link_libraries(tessellator-meshers tessellator-core tessellator-utils) +target_link_libraries(tessellator-meshers + tessellator-core + tessellator-utils + tessellator-cgal + CGAL::CGAL) if(TESSELLATOR_EXECUTION_POLICIES) add_definitions(-DTESSELLATOR_EXECUTION_POLICIES) diff --git a/src/meshers/ConformalMesherOptions.h b/src/meshers/ConformalMesherOptions.h index 86f31d2..5a38c18 100644 --- a/src/meshers/ConformalMesherOptions.h +++ b/src/meshers/ConformalMesherOptions.h @@ -1,14 +1,15 @@ #pragma once #include "types/Mesh.h" +#include "MesherBaseOptions.h" #include "core/SnapperOptions.h" namespace meshlib::meshers { -class ConformalMesherOptions { +class ConformalMesherOptions : public MesherBaseOptions { public: core::SnapperOptions snapperOptions; - std::set volumeGroups{}; + // std::set volumeGroups{}; }; } diff --git a/src/meshers/MesherBaseOptions.h b/src/meshers/MesherBaseOptions.h new file mode 100644 index 0000000..f647815 --- /dev/null +++ b/src/meshers/MesherBaseOptions.h @@ -0,0 +1,14 @@ +#pragma once + +#include "types/Mesh.h" +#include "core/SnapperOptions.h" + +namespace meshlib::meshers { + +class MesherBaseOptions { +public: + bool isVolume; + std::set volumeGroups{}; +}; + +} diff --git a/src/meshers/StaircaserMesherOptions.h b/src/meshers/StaircaserMesherOptions.h new file mode 100644 index 0000000..6418a7b --- /dev/null +++ b/src/meshers/StaircaserMesherOptions.h @@ -0,0 +1,13 @@ +#pragma once + +#include "types/Mesh.h" +#include "MesherBaseOptions.h" +#include "core/SnapperOptions.h" + +namespace meshlib::meshers { + +class ConformalMesherOptions : public MesherBaseOptions { +public: +}; + +} From e2f2d8531abc79d89d3b91e711646a0c6241f587 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Tue, 9 Jun 2026 16:11:09 +0200 Subject: [PATCH 10/61] StaircaseMesher fills volume is body marked as volume. Some changes pending to pass the option --- src/app/launcher.cpp | 21 ++++++++++++++++++- src/meshers/MesherBase.h | 4 ++++ src/meshers/MesherBaseOptions.h | 2 +- src/meshers/StaircaseMesher.cpp | 18 ++++++++++++++-- src/meshers/StaircaseMesher.h | 4 +++- ...sherOptions.h => StaircaseMesherOptions.h} | 2 +- 6 files changed, 45 insertions(+), 6 deletions(-) rename src/meshers/{StaircaserMesherOptions.h => StaircaseMesherOptions.h} (71%) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index b8dd444..6f4cb2c 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -95,6 +95,20 @@ std::string readExtension(const std::string &fn) } } +meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const std::string &fn) +{ + nlohmann::json j; + { + std::ifstream i(fn); + i >> j; + } + meshlib::meshers::StaircaseMesherOptions res; + if (j["object"].contains("volume")) { + res.isVolume = j["object"]["volume"]; + } + return res; +} + meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string &fn) { nlohmann::json j; @@ -103,6 +117,11 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::s i >> j; } meshlib::meshers::ConformalMesherOptions res; + if (j["object"].contains("volume")) { + res.isVolume = j["object"]["volume"]; + } + + if (j["mesher"].contains("options")) { res.snapperOptions.edgePoints = j["mesher"]["options"]["edgePoints"]; res.snapperOptions.forbiddenLength = j["mesher"]["options"]["forbiddenLength"]; @@ -113,7 +132,7 @@ std::unique_ptr buildMesher(const Mesh &in, const { auto mesherType = readMesherType(fn); if (mesherType == meshlib::app::staircase_mesher) { - return std::make_unique(meshlib::meshers::StaircaseMesher{in}); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn)}); } else if (mesherType == meshlib::app::conformal_mesher) { return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn)}); } else { diff --git a/src/meshers/MesherBase.h b/src/meshers/MesherBase.h index a688c29..938f949 100644 --- a/src/meshers/MesherBase.h +++ b/src/meshers/MesherBase.h @@ -1,6 +1,7 @@ #pragma once #include "types/Mesh.h" +#include "MesherBaseOptions.h" namespace meshlib { namespace meshers { @@ -29,6 +30,9 @@ class MesherBase { Grid originalGrid_; Grid enlargedGrid_; + MesherBaseOptions opts_; + + }; } diff --git a/src/meshers/MesherBaseOptions.h b/src/meshers/MesherBaseOptions.h index f647815..7403619 100644 --- a/src/meshers/MesherBaseOptions.h +++ b/src/meshers/MesherBaseOptions.h @@ -7,7 +7,7 @@ namespace meshlib::meshers { class MesherBaseOptions { public: - bool isVolume; + bool isVolume = false; std::set volumeGroups{}; }; diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 7ad6458..89588e3 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -7,6 +7,8 @@ #include "core/Collapser.h" #include "core/Staircaser.h" +#include "cgal/filler/Filler.h" + #include "utils/RedundancyCleaner.h" #include "utils/MeshTools.h" #include "utils/GridTools.h" @@ -17,9 +19,10 @@ using namespace utils; using namespace core; using namespace meshTools; -StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser) : +StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts) : MesherBase(inputMesh), - decimalPlacesInCollapser_(decimalPlacesInCollapser) + decimalPlacesInCollapser_(decimalPlacesInCollapser), + opts_(opts) { log("Preparing surfaces."); surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -49,6 +52,17 @@ void StaircaseMesher::process(Mesh& mesh) const auto dimensions = getHighestDimensionByGroup(mesh); + if (opts_.isVolume){ + std::cout<<"isvolume"< Date: Tue, 9 Jun 2026 16:23:25 +0200 Subject: [PATCH 11/61] Enables CGAL --- CMakePresets.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index 8c3893e..67fb641 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,7 +9,8 @@ "CMAKE_TOOLCHAIN_FILE": { "type": "FILEPATH", "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" - } + }, + "TESSELLATOR_ENABLE_CGAL": true } }, { From bbcf58db9ad06c0b5effafe04f372f852709a8b6 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Tue, 9 Jun 2026 16:52:52 +0200 Subject: [PATCH 12/61] Adds test to launcher --- test/app/launcherTest.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 3942428..e5dd3f7 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -77,6 +77,15 @@ TEST_F(LauncherTest, launches_sphere_case) EXPECT_EQ(exitCode, EXIT_SUCCESS); } +TEST_F(LauncherTest, launches_closed_sphere_case) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/sphere/closed_sphere.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + TEST_F(LauncherTest, launches_conformal_sphere_case) { int ac = 3; From 242b0deb2f7ee8b8f42ccb41ccc98c69e69cf51c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:30:36 +0000 Subject: [PATCH 13/61] Disable GHA vcpkg binary source in CI --- .github/workflows/build-and-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 4bf49b8..5e2b663 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -27,6 +27,8 @@ jobs: name: ${{ matrix.preset.os }} / ${{matrix.preset.name}} / ${{matrix.build-type}} runs-on: ${{ matrix.preset.os }} + env: + VCPKG_BINARY_SOURCES: clear steps: - name: checkout repository From aac1ac20812e7bc8cd6acc0481663a0829e0cd0b Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Wed, 10 Jun 2026 10:16:26 +0200 Subject: [PATCH 14/61] Adds missing json file --- testData/cases/sphere/closed_sphere.tessellator.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 testData/cases/sphere/closed_sphere.tessellator.json diff --git a/testData/cases/sphere/closed_sphere.tessellator.json b/testData/cases/sphere/closed_sphere.tessellator.json new file mode 100644 index 0000000..d95ebfe --- /dev/null +++ b/testData/cases/sphere/closed_sphere.tessellator.json @@ -0,0 +1,10 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "object": {"filename": "sphere.stl", "volume" : true} +} \ No newline at end of file From 91c7ae7df27ed1380f163d1e22b671ded2e96de4 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Wed, 10 Jun 2026 10:45:19 +0200 Subject: [PATCH 15/61] Adds test to check that filled volume has more quads than surface --- src/meshers/StaircaseMesher.cpp | 1 - test/meshers/StaircaseMesherTest.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 89588e3..cd9f753 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -53,7 +53,6 @@ void StaircaseMesher::process(Mesh& mesh) const auto dimensions = getHighestDimensionByGroup(mesh); if (opts_.isVolume){ - std::cout<<"isvolume"< countMeshElementsIf(staircasedMesh, isQuad)); + +} + TEST_F(StaircaseMesherTest, preserves_topological_closedness_for_alhambra) { auto mesh = vtkIO::readInputMesh("testData/cases/alhambra/alhambra.stl"); From eee9be88235a6767e99c276bdcbe7622dc967b8a Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Wed, 15 Jul 2026 07:56:08 +0000 Subject: [PATCH 16/61] Tessellator | Build | #5 | Add docker compatibility with CGAL --- .devcontainer/Dockerfile.base | 6 +++++- CMakeLists.txt | 5 +++++ CMakePresets.json | 7 +++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.devcontainer/Dockerfile.base b/.devcontainer/Dockerfile.base index b1f20ad..4e5549e 100644 --- a/.devcontainer/Dockerfile.base +++ b/.devcontainer/Dockerfile.base @@ -1,4 +1,4 @@ -FROM ubuntu:24.04 +FROM ubuntu:26.04 RUN apt-get update && apt-get install -y \ build-essential \ @@ -21,6 +21,10 @@ RUN apt-get install -y \ libgmp-dev \ libmpfr-dev +RUN apt-get install -y \ + libeigen3-dev \ + libcgal-dev + RUN chown -R ubuntu:ubuntu /home/ubuntu USER ubuntu diff --git a/CMakeLists.txt b/CMakeLists.txt index 710b3b7..959c7cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,11 @@ option(TESSELLATOR_EXECUTION_POLICIES OFF) if(TESSELLATOR_ENABLE_CGAL) list(APPEND VCPKG_MANIFEST_FEATURES "cgal") + SET( EIGEN3_INCLUDE_DIR "$ENV{EIGEN3_INCLUDE_DIR}" ) + if (EIGEN3_INCLUDE_DIR) + INCLUDE_DIRECTORIES ( "$ENV{EIGEN3_INCLUDE_DIR}" ) + INCLUDE_DIRECTORIES ( "$ENV{EIGEN3_INCLUDE_DIR}/Eigen" ) + endif() endif() project(tessellator CXX) diff --git a/CMakePresets.json b/CMakePresets.json index 67fb641..6b26208 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -40,7 +40,10 @@ "CMAKE_PREFIX_PATH": "/usr/local", "CMAKE_FIND_ROOT_PATH": "/usr/local", "TESSELLATOR_ENABLE_TESTS": "ON", - "TESSELLATOR_ENABLE_CGAL": "OFF" + "TESSELLATOR_ENABLE_CGAL": "ON" + }, + "environment": { + "EIGEN3_INCLUDE_DIR": "/usr/include/eigen3" } }, { @@ -55,7 +58,7 @@ "CMAKE_PREFIX_PATH": "/usr/local", "CMAKE_FIND_ROOT_PATH": "/usr/local", "TESSELLATOR_ENABLE_TESTS": "ON", - "TESSELLATOR_ENABLE_CGAL": "OFF" + "TESSELLATOR_ENABLE_CGAL": "ON" } } ], From d2ec255fb886fcb6bf1d33111e32ac9801cf60ed Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 8 May 2026 11:41:09 +0200 Subject: [PATCH 17/61] Add compressor functionality to the tessellator - Implemented Compressor class for merging coplanar quad surfaces. - Updated StaircaseMesher to include compression option. - Enhanced launcher to read compression settings from JSON input. - Added tests for Compressor functionality. --- src/app/launcher.cpp | 17 +- src/core/CMakeLists.txt | 1 + src/core/Compressor.cpp | 604 ++++++++++++++++++++++++++++++++ src/core/Compressor.h | 75 ++++ src/meshers/StaircaseMesher.cpp | 16 +- src/meshers/StaircaseMesher.h | 3 +- src/utils/Types.h | 9 + test/CMakeLists.txt | 1 + test/core/CompressorTest.cpp | 151 ++++++++ 9 files changed, 873 insertions(+), 4 deletions(-) create mode 100644 src/core/Compressor.cpp create mode 100644 src/core/Compressor.h create mode 100644 test/core/CompressorTest.cpp diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 6f4cb2c..8772a52 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -128,11 +128,26 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::s } return res; } + +bool readStaircaseMesherCompressOption(const std::string &fn) +{ + nlohmann::json j; + { + std::ifstream i(fn); + i >> j; + } + if (j["mesher"].contains("options") && + j["mesher"]["options"].contains("compress")) { + return j["mesher"]["options"]["compress"]; + } + return false; +} std::unique_ptr buildMesher(const Mesh &in, const std::string &fn) { auto mesherType = readMesherType(fn); if (mesherType == meshlib::app::staircase_mesher) { - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn)}); + bool compress = readStaircaseMesherCompressOption(fn); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn), compress}); } else if (mesherType == meshlib::app::conformal_mesher) { return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn)}); } else { diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d481f43..faab34a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -2,6 +2,7 @@ message(STATUS "Creating build system for tessellator-core") add_library(tessellator-core "Collapser.cpp" + "Compressor.cpp" "Slicer.cpp" "Snapper.cpp" "Smoother.cpp" diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp new file mode 100644 index 0000000..2e09033 --- /dev/null +++ b/src/core/Compressor.cpp @@ -0,0 +1,604 @@ +#include "Compressor.h" + +#include +#include + +#include "utils/Geometry.h" +#include "utils/GridTools.h" + +namespace meshlib::core { + +using meshlib::Sign; +using meshlib::PlanePoint; +using meshlib::PlaneLinel; +using meshlib::PlaneSurfel; +using meshlib::PlaneSurface; +using meshlib::Contour; +using meshlib::CrossLine; + +std::size_t Compressor::compressSurfaces(Mesh& mesh) { + std::size_t totalOriginal = 0; + std::size_t totalCompressed = 0; + + for (GroupId g = 0; g < mesh.groups.size(); g++) { + std::vector surfs; + std::vector surfIndices; + + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + const Element& elem = mesh.groups[g].elements[e]; + if (elem.type == Element::Type::Surface) { + surfIndices.push_back(e); + surfs.push_back(elem); + } + } + + if (surfs.empty()) { + continue; + } + + totalOriginal += surfs.size(); + std::vector compressedSurfs = compressSurfs_(mesh.coordinates, surfs); + totalCompressed += compressedSurfs.size(); + + // Replace surface elements + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + if (mesh.groups[g].elements[e].type == Element::Type::Surface) { + auto it = std::find(surfIndices.begin(), surfIndices.end(), e); + if (it != surfIndices.end()) { + std::size_t idx = std::distance(surfIndices.begin(), it); + if (idx < compressedSurfs.size()) { + mesh.groups[g].elements[e] = compressedSurfs[idx]; + } + } + } + } + } + + return totalOriginal - totalCompressed; +} + +std::vector Compressor::compressSurfs_( + std::vector& coords, + const std::vector& surfs) { + std::vector res; + std::map>, + std::vector> signDirSurfs; + for (std::size_t s = 0; s < surfs.size(); s++) { + if (surfs[s].vertices.size() != 4) { + res.push_back(surfs[s]); + continue; + } + std::array auxCells; + auxCells[0] = utils::GridTools::toCell(coords[surfs[s].vertices[0]]); + auxCells[1] = utils::GridTools::toCell(coords[surfs[s].vertices[1]]); + auxCells[2] = utils::GridTools::toCell(coords[surfs[s].vertices[2]]); + CellDir gridSurf; + Sign sign = 1; + Axis dir = 0; + for (Axis d = 0; d < 3; d++) { + if (auxCells[0](d) == auxCells[2](d)) { + Cell normal = (auxCells[1] - auxCells[0]) ^ + (auxCells[2] - auxCells[0]); + if (normal(d) >= 0) { + sign = 1; + } else { + sign = -1; + } + dir = d; + gridSurf = auxCells[0](d); + break; + } + } + signDirSurfs[std::make_pair(gridSurf, + std::make_pair(sign, dir))].push_back(s); + } + for (std::map>, + std::vector>::const_iterator + it = signDirSurfs.begin(); it != signDirSurfs.end(); ++it) { + std::vector auxElems; + for (std::size_t i = 0; i < it->second.size(); i++) { + auxElems.push_back(surfs[it->second[i]]); + } + std::vector auxRes = + compressDirSignSurfs_(coords, + it->first.second, + auxElems); + res.insert(res.end(), auxRes.begin(), auxRes.end()); + } + return res; +} + +std::vector Compressor::compressDirSignSurfs_( + std::vector& coords, + const std::pair& signDir, + const std::vector& surfs) { + std::vector res; + std::map> lineSurfs; + std::map> surfLines; + for (std::size_t s = 0; s < surfs.size(); s++) { + for (std::size_t i = 0; i < 4; i++) { + std::size_t j = (i + 1) % 4; + LinIds line; + line[0] = surfs[s].vertices[i]; + line[1] = surfs[s].vertices[j]; + std::sort(line.begin(), line.end()); + lineSurfs[line].insert(s); + surfLines[s].insert(line); + } + } + std::set vis; + for (std::map>::const_iterator + itExt = surfLines.begin(); itExt != surfLines.end(); ++itExt) { + if (vis.count(itExt->first) == 0) { + std::set surfsConn; + std::queue q; + q.push(itExt->first); + vis.insert(itExt->first); + while (!q.empty()) { + ElementId elem = q.front(); + q.pop(); + surfsConn.insert(elem); + for (std::set::const_iterator + itLine = surfLines[elem].begin(); + itLine != surfLines[elem].end(); ++itLine) { + for (std::set::const_iterator + itSurf = lineSurfs[*itLine].begin(); + itSurf != lineSurfs[*itLine].end(); ++itSurf) { + if (vis.count(*itSurf) == 0) { + q.push(*itSurf); + vis.insert(*itSurf); + } + } + } + } + std::vector resCon; + for (std::set::const_iterator + it = surfsConn.begin(); it != surfsConn.end(); ++it) { + resCon.push_back(surfs[*it]); + } + resCon = compressSurf_(coords, signDir, resCon); + res.insert(res.end(), resCon.begin(), resCon.end()); + } + } + return res; +} + +std::vector Compressor::compressSurf_( + std::vector& coords, + const std::pair& signDir, + const std::vector& surfs) { + std::vector res; + Axis d = signDir.second; + Axis d1 = (d + 1) % 3; + Axis d2 = (d + 2) % 3; + CellDir plane = utils::GridTools::toCell(coords[surfs[0].vertices[0]])(d); + std::set surfels; + for (std::size_t s = 0; s < surfs.size(); s++) { + std::pair ext; + ext.first[0] = + utils::GridTools::toCell(coords[surfs[s].vertices[0]])(d1); + ext.first[1] = + utils::GridTools::toCell(coords[surfs[s].vertices[0]])(d2); + ext.second[0] = + utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d1); + ext.second[1] = + utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d2); + for (CellDir i = ext.first[0]; i < ext.second[0]; i++) { + for (CellDir j = ext.first[1]; j < ext.second[1]; j++) { + PlaneSurfel surfel = {{i, j}}; + surfels.insert(surfel); + } + } + } + std::vector aux = compressSurfels_(surfels); + CoordinateMap coordMap; + for (std::size_t s = 0; s < surfs.size(); s++) { + for (std::size_t i = 0; i < 4; i++) { + CoordinateId coordId = surfs[s].vertices[i]; + Coordinate coord = coords[coordId]; + coordMap[coord] = coordId; + } + } + for (std::size_t e = 0; e < aux.size(); e++) { + std::array ext; + ext[0](d) = ext[2](d) = plane; + ext[0](d1) = aux[e].first[0]; + ext[0](d2) = aux[e].first[1]; + ext[2](d1) = aux[e].second[0]; + ext[2](d2) = aux[e].second[1]; + ext[1] = ext[3] = ext[0]; + ext[1](d1) = ext[2](d1); + ext[3](d2) = ext[2](d2); + if (signDir.first < 0) { + std::swap(ext[1], ext[3]); + } + Element resElem; + resElem.type = Element::Type::Surface; + for (std::size_t i = 0; i < 4; i++) { + Relative rel = utils::GridTools::toRelative(ext[i]); + if (coordMap.count(rel) == 0) { + coordMap[rel] = coords.size(); + coords.push_back(rel); + } + resElem.vertices.push_back(coordMap[rel]); + } + res.push_back(resElem); + } + return res; +} + +std::vector Compressor::compressSurfels_( + const std::set& surfs) { + std::vector res; + const std::vector& conts = getContours_(surfs); + std::array, 2> cross = + getCrossingLines_(surfs, conts); + cross = getMaxCompatLines_(cross); + std::set linels; + for (std::size_t c = 0; c < conts.size(); c++) { + for (std::size_t i = 0; i < conts[c].size(); i++) { + std::size_t j = (i + 1) % conts[c].size(); + std::set aux = getLinels_(conts[c][i], conts[c][j]); + linels.insert(aux.begin(), aux.end()); + } + } + for (Axis d = 0; d < 2; d++) { + Axis d1 = (d + 1) % 2; + for (std::size_t i = 0; i < cross[d].size(); i++) { + PlanePoint ini, end; + ini[d1] = end[d1] = cross[d][i].first; + ini[d] = cross[d][i].second.first; + end[d] = cross[d][i].second.second; + std::set aux = getLinels_(ini, end); + linels.insert(aux.begin(), aux.end()); + } + } + addConcaveLinels_(surfs, conts, linels); + std::map> lineSurfs; + std::map> surfLines; + for (std::set::const_iterator + it = surfs.begin(); it != surfs.end(); ++it) { + surfLines.insert(std::make_pair(*it, std::set())); + for (Axis d = 0; d < 2; d++) { + for (CellDir diff = -1; diff <= 1; diff += 2) { + PlaneLinel linel = getSurfaceEdge_(*it, diff, d); + if (linels.count(linel) == 0) { + surfLines[*it].insert(linel); + lineSurfs[linel].insert(*it); + } + } + } + } + std::set vis; + for (std::map>::const_iterator + itSurfExt = surfLines.begin(); + itSurfExt != surfLines.end(); ++itSurfExt) { + if (vis.count(itSurfExt->first) == 0) { + std::queue q; + q.push(itSurfExt->first); + vis.insert(itSurfExt->first); + PlanePoint minPoint = itSurfExt->first; + PlanePoint maxPoint = itSurfExt->first; + while (!q.empty()) { + PlaneSurfel surfel = q.front(); + q.pop(); + if (surfel < minPoint) { + minPoint = surfel; + } + if (surfel > maxPoint) { + maxPoint = surfel; + } + for (std::set::const_iterator + itLin = surfLines[surfel].begin(); + itLin != surfLines[surfel].end(); ++itLin) { + for (std::set::const_iterator + itSurfInt = lineSurfs[*itLin].begin(); + itSurfInt != lineSurfs[*itLin].end(); ++itSurfInt) { + if (vis.count(*itSurfInt) == 0) { + q.push(*itSurfInt); + vis.insert(*itSurfInt); + } + } + } + } + maxPoint[0]++; + maxPoint[1]++; + res.push_back(std::make_pair(minPoint, maxPoint)); + } + } + return res; +} + +std::vector Compressor::getContours_( + const std::set& surfs) { + std::vector res; + if (surfs.empty()) { + return res; + } + std::set vis; + res.push_back( + getContour_(getSurfaceEdge_(*surfs.begin(), -1, 0), surfs, vis)); + for (std::set::const_iterator + it = surfs.begin(); it != surfs.end(); ++it) { + for (Axis d = 0; d < 2; d++) { + for (CellDir diff = -1; diff <= 1; diff += 2) { + PlaneSurfel adjSurf; + PlaneLinel adjEdge; + adjSurf = *it; + adjSurf[d] += diff; + adjEdge = getSurfaceEdge_(*it, diff, d); + if ((surfs.find(adjSurf) == surfs.end()) && + (vis.find(adjEdge) == vis.end())) { + res.push_back(getContour_(adjEdge, surfs, vis)); + } + } + } + } + return res; +} + +Contour Compressor::getContour_( + const PlaneLinel& from, + const std::set& surfs, + std::set& vis) { + Contour res; + std::queue q; + if (vis.find(from) != vis.end()) { + return res; + } + std::vector lines; + q.push(from); + vis.insert(from); + lines.push_back(from); + while (!q.empty()) { + PlaneLinel edge = q.front(); + q.pop(); + PlanePoint pos = edge.first; + Axis d0 = edge.second; + Axis d1 = (d0 + 1) % 2; + PlaneSurfel surf = pos; + if (surfs.find(surf) == surfs.end()) { + surf[d1]--; + } + for (CellDir diff = -1; diff <= 1; diff += 2) { + PlaneSurfel adjSurf1 = surf; + adjSurf1[d0] += diff; + if (surfs.find(adjSurf1) == surfs.end()) { + PlaneLinel adjEdge = getSurfaceEdge_(surf, diff, d0); + if (vis.find(adjEdge) == vis.end()) { + q.push(adjEdge); + vis.insert(adjEdge); + lines.push_back(adjEdge); + break; + } + continue; + } + if (surf == pos) { + adjSurf1[d1]--; + } else { + adjSurf1[d1]++; + } + if (surfs.find(adjSurf1) == surfs.end()) { + PlaneLinel adjEdge = edge; + adjEdge.first[d0] += diff; + if (vis.find(adjEdge) == vis.end()) { + q.push(adjEdge); + vis.insert(adjEdge); + lines.push_back(adjEdge); + break; + } + continue; + } else { + PlaneLinel adjEdge = getSurfaceEdge_(adjSurf1, -diff, d0); + if (vis.find(adjEdge) == vis.end()) { + q.push(adjEdge); + vis.insert(adjEdge); + lines.push_back(adjEdge); + break; + } + continue; + } + } + } + for (std::vector::const_iterator + it = lines.begin(); it != lines.end(); ++it) { + std::vector::const_iterator itPlus = std::next(it); + if (itPlus == lines.end()) { + itPlus = lines.begin(); + } + if (it->second == itPlus->second) { + continue; + } + std::array extremes = {it->first, it->first}; + extremes[1][it->second]++; + std::array extremesP = {itPlus->first, itPlus->first}; + extremesP[1][itPlus->second]++; + for (std::size_t p = 0; p < 4; p++) { + if (extremes[p / 2] == extremesP[p % 2]) { + res.push_back(extremes[p / 2]); + break; + } + } + } + return res; +} + +PlaneLinel Compressor::getSurfaceEdge_(const PlaneSurfel& surf, + const CellDir& diff, + const Axis& dir) { + PlaneLinel res; + res.first = surf; + res.second = (dir + 1) % 2; + if (diff > 0) { + res.first[dir]++; + } + return res; +} + +std::array, 2> + Compressor::getCrossingLines_( + const std::set& surfs, + const std::vector& conts) { + std::array, 2> res; + for (Axis d = 0; d < 2; d++) { + Axis d1 = (d + 1) % 2; + std::map> cross; + for (std::vector::const_iterator + it1 = conts.begin(); it1 != conts.end(); ++it1) { + for (std::vector::const_iterator + it2 = it1->begin(); it2 != it1->end(); ++it2) { + cross[(*it2)[d1]].insert((*it2)[d]); + } + } + for (std::map>::const_iterator + itMap = cross.begin(); itMap != cross.end(); ++itMap) { + for (std::set::const_iterator + itSet = itMap->second.begin(); + itSet != itMap->second.end(); ++itSet) { + std::set::const_iterator itSetPlus = std::next(itSet); + if (itSetPlus == itMap->second.end()) { + break; + } + bool valid = true; + PlanePoint edge; + edge[d1] = itMap->first; + for (CellDir i = *itSet; i < *itSetPlus; i++) { + edge[d] = i; + PlaneSurfel adjSurf1, adjSurf2; + adjSurf1 = adjSurf2 = edge; + adjSurf1[d1]--; + if ((surfs.find(adjSurf1) == surfs.end()) || + (surfs.find(adjSurf2) == surfs.end())) { + valid = false; + break; + } + } + if (valid) { + res[d].push_back( + std::make_pair(itMap->first, + std::make_pair(*itSet, *itSetPlus))); + } + } + } + } + return res; +} + +std::array, 2> + Compressor::getMaxCompatLines_( + const std::array, 2>& cross) { + std::array, 2> res; + if (cross[0].size() > cross[1].size()) { + res[0] = cross[0]; + } else { + res[1] = cross[1]; + } + return res; +} + +std::set Compressor::getLinels_( + const PlanePoint& ini, + const PlanePoint& end) { + std::set res; + for (Axis d = 0; d < 2; d++) { + Axis d1 = (d + 1) % 2; + if (ini[d] == end[d]) { + PlaneLinel linel; + linel.first[d] = ini[d]; + linel.second = d1; + for (CellDir + k = std::min(ini[d1], end[d1]); + k < std::max(ini[d1], end[d1]); k++) { + linel.first[d1] = k; + res.insert(linel); + } + } + } + return res; +} + +void Compressor::addConcaveLinels_(const std::set& surfs, + const std::vector& conts, + std::set& lines) { + std::set concavePoints; + for (std::size_t c = 0; c < conts.size(); c++) { + for (std::size_t i = 0; i < conts[c].size(); i++) { + PlanePoint point = conts[c][i]; + std::size_t numSurfAdj = 0; + std::size_t numLineAdj = 0; + for (CellDir diffx = -1; diffx < 1; diffx++) { + for (CellDir diffy = -1; diffy < 1; diffy++) { + PlaneSurfel surfel = point; + surfel[0] += diffx; + surfel[1] += diffy; + if (surfs.count(surfel) != 0) { + numSurfAdj++; + } + } + } + for (Axis d = 0; d < 2; d++) { + for (CellDir diff = -1; diff < 1; diff++) { + PlaneLinel linel = std::make_pair(point, d); + linel.first[d] += diff; + if (lines.count(linel) != 0) { + numLineAdj++; + } + } + } + if ((numSurfAdj > 2) && (numLineAdj < 3)) { + concavePoints.insert(point); + } + } + } + for (std::set::const_iterator + it = concavePoints.begin(); it != concavePoints.end(); ++it) { + std::size_t numLineAdj = 0; + for (Axis d = 0; d < 2; d++) { + for (CellDir diff = -1; diff < 1; diff++) { + PlaneLinel linel = std::make_pair(*it, d); + linel.first[d] += diff; + if (lines.count(linel) != 0) { + numLineAdj++; + } + } + } + if (numLineAdj > 2) { + continue; + } + for (Axis d = 0; d < 2; d++) { + Axis d1 = (d + 1) % 2; + bool found = false; + for (CellDir diff = -1; diff < 1; diff++) { + PlaneLinel linel = std::make_pair(*it, d); + linel.first[d] += diff; + if (lines.count(linel) == 0) { + lines.insert(linel); + while (true) { + PlaneLinel aux1, aux2; + if (diff < 0) { + aux1 = aux2 = std::make_pair(linel.first, d1); + aux1.first[d1]--; + linel.first[d]--; + } else { + linel.first[d]++; + aux1 = aux2 = std::make_pair(linel.first, d1); + aux1.first[d1]--; + } + if ((lines.count(aux1) != 0) || + (lines.count(aux2) != 0)) { + break; + } + lines.insert(linel); + } + found = true; + break; + } + } + if (found) { + break; + } + } + } +} + +} diff --git a/src/core/Compressor.h b/src/core/Compressor.h new file mode 100644 index 0000000..cfae369 --- /dev/null +++ b/src/core/Compressor.h @@ -0,0 +1,75 @@ +#pragma once + +#include "types/Mesh.h" +#include "utils/Types.h" + +#include +#include + +namespace meshlib::core { + +class Compressor { +public: + // Compress only quad surfaces (4 vertices) that are coplanar and adjacent + // Returns number of surfaces merged (original_count - compressed_count) + static std::size_t compressSurfaces(Mesh& mesh); + +private: + // Group surfaces by (grid_plane, sign, axis) and compress each group + static std::vector compressSurfs_( + std::vector& coords, + const std::vector& surfs); + + // Compress surfaces with same normal direction and sign + static std::vector compressDirSignSurfs_( + std::vector& coords, + const std::pair& signDir, + const std::vector& surfs); + + // Compress connected coplanar surfaces using contour detection + static std::vector compressSurf_( + std::vector& coords, + const std::pair& signDir, + const std::vector& surfs); + + // Merge adjacent surfels into maximal rectangles + static std::vector compressSurfels_( + const std::set& surfs); + + // Detect boundary contours of surfel set + static std::vector getContours_(const std::set& surfs); + + // Trace a single contour from starting edge + static Contour getContour_( + const PlaneLinel& from, + const std::set& surfs, + std::set& visited); + + // Get edge of a surfel in given direction + static PlaneLinel getSurfaceEdge_( + const PlaneSurfel& surf, + const CellDir& diff, + const Axis& dir); + + // Find lines crossing between contours + static std::array, 2> getCrossingLines_( + const std::set& surfs, + const std::vector& contours); + + // Select the set of crossing lines with maximum count + static std::array, 2> getMaxCompatLines_( + const std::array, 2>& cross); + + // Get linels between two points + static std::set getLinels_( + const PlanePoint& ini, + const PlanePoint& end); + + // Add linels at concave corners + static void addConcaveLinels_( + const std::set& surfs, + const std::vector& contours, + std::set& lines); +}; + +} diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index cd9f753..c6a7239 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -6,6 +6,7 @@ #include "core/Slicer.h" #include "core/Collapser.h" #include "core/Staircaser.h" +#include "core/Compressor.h" #include "cgal/filler/Filler.h" @@ -19,10 +20,11 @@ using namespace utils; using namespace core; using namespace meshTools; -StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts) : +StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts, bool compress) : MesherBase(inputMesh), decimalPlacesInCollapser_(decimalPlacesInCollapser), - opts_(opts) + opts_(opts), + compress_(compress) { log("Preparing surfaces."); surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -79,6 +81,16 @@ void StaircaseMesher::process(Mesh& mesh) const logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); logNumberOfLines(countMeshElementsIf(mesh, isLine)); + if (compress_) { + log("Compressing surfaces.", 1); + std::size_t beforeQuads = countMeshElementsIf(mesh, isQuad); + std::size_t merged = Compressor::compressSurfaces(mesh); + std::size_t afterQuads = countMeshElementsIf(mesh, isQuad); + log("Compressed " + std::to_string(beforeQuads) + + " -> " + std::to_string(afterQuads) + + " quads (merged " + std::to_string(merged) + " surfaces)", 1); + } + log("Removing repeated and overlapping elements.", 1); RedundancyCleaner::removeOverlappedElementsByDimension(mesh, dimensions); diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index df0b9bb..070e46e 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -8,12 +8,13 @@ namespace meshlib::meshers { class StaircaseMesher : public MesherBase { public: - StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions()); + StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions(), bool compress = false); virtual ~StaircaseMesher() = default; Mesh mesh() const; private: int decimalPlacesInCollapser_; + bool compress_; Mesh surfaceMesh_; StaircaseMesherOptions opts_; diff --git a/src/utils/Types.h b/src/utils/Types.h index 471ca81..7ac4589 100644 --- a/src/utils/Types.h +++ b/src/utils/Types.h @@ -61,5 +61,14 @@ using HexIds = std::array; using UpdateMap = std::array, 2>, 2>; +// Compressor types +using Sign = int; +using PlanePoint = std::array; +using PlaneLinel = std::pair; +using PlaneSurfel = PlanePoint; +using PlaneSurface = std::pair; +using Contour = std::vector; +using CrossLine = std::pair>; + } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index cea5caa..d345ee2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(tessellator_tests "app/launcherTest.cpp" "app/vtkIOTest.cpp" "core/CollapserTest.cpp" + "core/CompressorTest.cpp" "core/SlicerTest.cpp" "core/SnapperTest.cpp" "core/SmootherTest.cpp" diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp new file mode 100644 index 0000000..a4aa919 --- /dev/null +++ b/test/core/CompressorTest.cpp @@ -0,0 +1,151 @@ +#include +#include "core/Compressor.h" +#include "MeshFixtures.h" + +namespace meshlib::tests { + +class CompressorTest : public ::testing::Test { +protected: + void SetUp() override { + grid_ = { + std::vector{0, 1, 2, 3, 4}, + std::vector{0, 1, 2, 3, 4}, + std::vector{0, 1, 2, 3, 4} + }; + } + + Grid grid_; +}; + +TEST_F(CompressorTest, Compress2x2QuadsIntoOneSurface) { + // Create 4 quads arranged in a 2x2 pattern on the same plane + // They should be merged into a single surface + + Mesh mesh; + mesh.grid = grid_; + + // Quad 1: bottom-left (cells 0,0 to 1,1) + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad 2: bottom-right (cells 1,0 to 2,1) + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + // Quad 3: top-left (cells 0,1 to 1,2) + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + // Quad 4: top-right (cells 1,1 to 2,2) + addQuad(mesh, {1, 1, 0}, {2, 1, 0}, {2, 2, 0}, {1, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 1u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); +} + +TEST_F(CompressorTest, DoesNotCompressNonCoplanarQuads) { + // Create quads on different planes - should not be merged + + Mesh mesh; + mesh.grid = grid_; + + // Quad on z=0 plane + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad on z=1 plane (different plane) + addQuad(mesh, {0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); +} + +TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) { + // Create quads on same plane but not connected - should not be merged + + Mesh mesh; + mesh.grid = grid_; + + // Quad at bottom-left + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad at top-right (disconnected) + addQuad(mesh, {3, 3, 0}, {4, 3, 0}, {4, 4, 0}, {3, 4, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); +} + +TEST_F(CompressorTest, CompressLShapeIntoOneSurface) { + // Create 3 quads in L-shape - should be merged into one surface + + Mesh mesh; + mesh.grid = grid_; + + // Quad 1: bottom-left + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad 2: bottom-right + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + // Quad 3: top-left (forming L-shape) + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 3u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 1u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); +} + +TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { + // Create 8 quads forming a ring with a hole in the middle + // Should be merged into one surface with inner contour + + Mesh mesh; + mesh.grid = grid_; + + // Outer ring of quads (leaving center 1,1 to 2,2 empty) + // Bottom row + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0}); + // Middle row (sides only) + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + addQuad(mesh, {2, 1, 0}, {3, 1, 0}, {3, 2, 0}, {2, 2, 0}); + // Top row + addQuad(mesh, {0, 2, 0}, {1, 2, 0}, {1, 3, 0}, {0, 3, 0}); + addQuad(mesh, {2, 2, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0}); + // Corners to complete the ring + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + addQuad(mesh, {1, 3, 0}, {2, 3, 0}, {2, 2, 0}, {1, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 1u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); +} + +TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { + // Create quads with different normal directions - should not be merged + + Mesh mesh; + mesh.grid = grid_; + + // Quad on z=0 plane, normal pointing +z + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Quad on y=0 plane, normal pointing +y (different orientation) + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 0, 1}, {0, 0, 1}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); +} + +} From cad1b5c978af1327e462258a19a9fa326db004d4 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Tue, 12 May 2026 11:33:02 +0200 Subject: [PATCH 18/61] Fix compressor test crash and element removal - Add groups initialization check in addQuad() helper to prevent segfault - Fix compressSurfaces() to properly remove merged elements - Update test expectations for merged return value --- src/app/CMakeLists.txt | 37 ++++++++++++++++++++---------------- src/app/vtkIO.cpp | 3 +-- src/core/Compressor.cpp | 16 +++++++++------- test/CMakeLists.txt | 22 ++++++++++++--------- test/MeshFixtures.h | 36 +++++++++++++++++++++++++++++++++++ test/core/CompressorTest.cpp | 10 +++++++--- 6 files changed, 87 insertions(+), 37 deletions(-) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 6012a8c..a6ee7f9 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,10 +1,5 @@ message(STATUS "Creating build system for tessellator-app") -add_library(tessellator-app - "vtkIO.cpp" - "launcher.cpp" -) - find_package(VTK COMPONENTS CommonCore IOGeometry @@ -12,18 +7,28 @@ find_package(VTK COMPONENTS FiltersCore ) -find_package(Boost COMPONENTS program_options) +if(VTK_FOUND) + add_library(tessellator-app + "vtkIO.cpp" + "launcher.cpp" + ) -find_package(nlohmann_json) + find_package(Boost COMPONENTS program_options) -target_link_libraries(tessellator-app - ${VTK_LIBRARIES} - Boost::program_options - nlohmann_json::nlohmann_json -) + find_package(nlohmann_json) -add_executable(tessellator - "tessellator.cpp" -) + target_link_libraries(tessellator-app + ${VTK_LIBRARIES} + Boost::program_options + nlohmann_json::nlohmann_json + ) + + add_executable(tessellator + "tessellator.cpp" + ) -target_link_libraries(tessellator tessellator-app tessellator-meshers) + target_link_libraries(tessellator tessellator-app tessellator-meshers) +else() + message(STATUS "VTK not found - tessellator app will not be built") + add_library(tessellator-app INTERFACE) +endif() diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index 9457e14..4565d89 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -1,6 +1,5 @@ #include "vtkIO.h" -#include #include #include #include @@ -43,7 +42,7 @@ vtkSmartPointer readAsVTU(const std::filesystem::path& file } vtkSmartPointer vtu; - std::string extension = vtksys::SystemTools::GetFilenameLastExtension(fn); + std::string extension = fn.substr(fn.find_last_of(".")).empty() ? "" : "." + fn.substr(fn.find_last_of(".")); std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp index 2e09033..82192bd 100644 --- a/src/core/Compressor.cpp +++ b/src/core/Compressor.cpp @@ -40,18 +40,20 @@ std::size_t Compressor::compressSurfaces(Mesh& mesh) { std::vector compressedSurfs = compressSurfs_(mesh.coordinates, surfs); totalCompressed += compressedSurfs.size(); - // Replace surface elements + // Build new elements vector with compressed surfaces + std::vector newElements; + ElementId surfIdx = 0; for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { if (mesh.groups[g].elements[e].type == Element::Type::Surface) { - auto it = std::find(surfIndices.begin(), surfIndices.end(), e); - if (it != surfIndices.end()) { - std::size_t idx = std::distance(surfIndices.begin(), it); - if (idx < compressedSurfs.size()) { - mesh.groups[g].elements[e] = compressedSurfs[idx]; - } + if (surfIdx < compressedSurfs.size()) { + newElements.push_back(compressedSurfs[surfIdx]); + surfIdx++; } + } else { + newElements.push_back(mesh.groups[g].elements[e]); } } + mesh.groups[g].elements = std::move(newElements); } return totalOriginal - totalCompressed; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d345ee2..dd9f73f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,13 +15,7 @@ include_directories( ) add_executable(tessellator_tests - "app/launcherTest.cpp" - "app/vtkIOTest.cpp" - "core/CollapserTest.cpp" "core/CompressorTest.cpp" - "core/SlicerTest.cpp" - "core/SnapperTest.cpp" - "core/SmootherTest.cpp" "core/SmootherToolsTest.cpp" "core/StaircaserTest.cpp" "types/MeshTest.cpp" @@ -32,18 +26,28 @@ add_executable(tessellator_tests "utils/GridToolsTest.cpp" "utils/MeshToolsTest.cpp" "utils/RedundancyCleanerTest.cpp" - "meshers/StaircaseMesherTest.cpp" "meshers/OffgridMesherTest.cpp" - "meshers/ConformalMesherTest.cpp" ) target_link_libraries(tessellator_tests tessellator-meshers - tessellator-app GTest::gtest GTest::gtest_main ) +if(VTK_FOUND) + target_sources(tessellator_tests PRIVATE + "app/vtkIOTest.cpp" + "core/CollapserTest.cpp" + "core/SlicerTest.cpp" + "core/SnapperTest.cpp" + "core/SmootherTest.cpp" + "meshers/StaircaseMesherTest.cpp" + "meshers/ConformalMesherTest.cpp" + ) + target_link_libraries(tessellator_tests tessellator-app) +endif() + if (TESSELLATOR_ENABLE_CGAL) include_directories( ${PROJECT_SOURCE_DIR}/src/cgal/ diff --git a/test/MeshFixtures.h b/test/MeshFixtures.h index bda4612..b23fe42 100644 --- a/test/MeshFixtures.h +++ b/test/MeshFixtures.h @@ -1269,4 +1269,40 @@ static Mesh buildProblematicTriMesh2() } } + +// Helper to add a quad (as a surface with four vertices) to a mesh +static void addQuad(Mesh& mesh, const std::array& v0, const std::array& v1, + const std::array& v2, const std::array& v3) { + // Ensure at least one group exists + if (mesh.groups.empty()) { + mesh.groups.emplace_back(); + } + + // Find or create coordinates + auto findOrAddCoord = [&](const std::array& gridIdx) { + double pos[3]; + pos[0] = mesh.grid[0][gridIdx[0]]; + pos[1] = mesh.grid[1][gridIdx[1]]; + pos[2] = mesh.grid[2][gridIdx[2]]; + + for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { + if (mesh.coordinates[i](0) == pos[0] && + mesh.coordinates[i](1) == pos[1] && + mesh.coordinates[i](2) == pos[2]) { + return i; + } + } + mesh.coordinates.push_back(Coordinate({pos[0], pos[1], pos[2]})); + return static_cast(mesh.coordinates.size() - 1); + }; + + CoordinateId c0 = findOrAddCoord(v0); + CoordinateId c1 = findOrAddCoord(v1); + CoordinateId c2 = findOrAddCoord(v2); + CoordinateId c3 = findOrAddCoord(v3); + + // Add quad as a surface with four vertices + mesh.groups[0].elements.push_back(Element({c0, c1, c2, c3}, Element::Type::Surface)); +} + } \ No newline at end of file diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index a4aa919..86d2411 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -1,6 +1,10 @@ #include #include "core/Compressor.h" #include "MeshFixtures.h" +#include "utils/MeshTools.h" + +using namespace meshlib; +using namespace meshlib::utils::meshTools; namespace meshlib::tests { @@ -37,7 +41,7 @@ TEST_F(CompressorTest, Compress2x2QuadsIntoOneSurface) { auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 1u); + EXPECT_EQ(merged, 3u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); } @@ -96,7 +100,7 @@ TEST_F(CompressorTest, CompressLShapeIntoOneSurface) { auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 1u); + EXPECT_EQ(merged, 2u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); } @@ -125,7 +129,7 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 1u); + EXPECT_EQ(merged, 7u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); } From 0013e880bf50f15ea9a583043a1e1e3cdc8f5f8e Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 09:09:46 +0200 Subject: [PATCH 19/61] Adds IDE configuration files --- .vscode/launch.json | 35 +++++ CMakePresets.json | 9 ++ resources/Eigen.natvis | 253 ++++++++++++++++++++++++++++++ resources/nlohmann_json.natvis | 278 +++++++++++++++++++++++++++++++++ 4 files changed, 575 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 resources/Eigen.natvis create mode 100644 resources/nlohmann_json.natvis diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..17c6a06 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "tessellator_tests (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build-dbg/bin/tesselator_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "visualizerFile": [ + "${workspaceFolder}/resources/Eigen.natvis", + "${workspaceFolder}/resources/nlohmann_json.natvis" + ], + "additionalSOLibSearchPath": "", + "showDisplayString": true, + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + } + ] +} \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json index 6b26208..4628b74 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -29,6 +29,15 @@ "generator": "Ninja", "inherits": "default" }, + { + "name": "gnu-dbg", + "displayName": "GNU g++ compiler - Debug", + "inherits": "gnu", + "binaryDir": "build-dbg/", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, { "name": "docker", "displayName": "Docker (system libraries)", diff --git a/resources/Eigen.natvis b/resources/Eigen.natvis new file mode 100644 index 0000000..dbed89e --- /dev/null +++ b/resources/Eigen.natvis @@ -0,0 +1,253 @@ + + + + + + + + [{$T2}, {$T3}] + + + 2 + $i==0 ? $T2 : $T3 + m_storage.m_data.array + + + Backward + 2 + $i==0 ? $T2 : $T3 + m_storage.m_data.array + + + + + + + [2, 2] + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[2]} + + + {m_storage.m_data.array[2]} {m_storage.m_data.array[3]} + + + {m_storage.m_data.array[1]} {m_storage.m_data.array[3]} + + + + + + + [3, 3] + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[3]} {m_storage.m_data.array[6]} + + + {m_storage.m_data.array[3]} {m_storage.m_data.array[4]} {m_storage.m_data.array[5]} + + + {m_storage.m_data.array[1]} {m_storage.m_data.array[4]} {m_storage.m_data.array[7]} + + + {m_storage.m_data.array[6]} {m_storage.m_data.array[7]} {m_storage.m_data.array[8]} + + + {m_storage.m_data.array[2]} {m_storage.m_data.array[5]} {m_storage.m_data.array[8]} + + + + + + + [3, 4] + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} {m_storage.m_data.array[3]} + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[3]} {m_storage.m_data.array[6]} {m_storage.m_data.array[9]} + + + {m_storage.m_data.array[4]} {m_storage.m_data.array[5]} {m_storage.m_data.array[6]} {m_storage.m_data.array[7]} + + + {m_storage.m_data.array[1]} {m_storage.m_data.array[4]} {m_storage.m_data.array[7]} {m_storage.m_data.array[10]} + + + {m_storage.m_data.array[8]} {m_storage.m_data.array[9]} {m_storage.m_data.array[10]} {m_storage.m_data.array[11]} + + + {m_storage.m_data.array[2]} {m_storage.m_data.array[5]} {m_storage.m_data.array[8]} {m_storage.m_data.array[11]} + + + + + + + [4, 4] + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} {m_storage.m_data.array[3]} + + + {m_storage.m_data.array[0]} {m_storage.m_data.array[4]} {m_storage.m_data.array[8]} {m_storage.m_data.array[12]} + + + {m_storage.m_data.array[4]} {m_storage.m_data.array[5]} {m_storage.m_data.array[6]} {m_storage.m_data.array[7]} + + + {m_storage.m_data.array[1]} {m_storage.m_data.array[5]} {m_storage.m_data.array[9]} {m_storage.m_data.array[13]} + + + {m_storage.m_data.array[8]} {m_storage.m_data.array[9]} {m_storage.m_data.array[10]} {m_storage.m_data.array[11]} + + + {m_storage.m_data.array[2]} {m_storage.m_data.array[6]} {m_storage.m_data.array[10]} {m_storage.m_data.array[14]} + + + {m_storage.m_data.array[12]} {m_storage.m_data.array[13]} {m_storage.m_data.array[14]} {m_storage.m_data.array[15]} + + + {m_storage.m_data.array[3]} {m_storage.m_data.array[7]} {m_storage.m_data.array[11]} {m_storage.m_data.array[15]} + + + + + + + + empty + [{m_storage.m_rows}, {m_storage.m_cols}] (dynamic matrix) + + + 2 + $i==0 ? m_storage.m_rows : m_storage.m_cols + m_storage.m_data + + + Backward + 2 + $i==0 ? m_storage.m_rows : m_storage.m_cols + m_storage.m_data + + + + + + + empty + [{$T2}, {m_storage.m_cols}] (dynamic column matrix) + + + 2 + $i==0 ? $T2 : m_storage.m_cols + m_storage.m_data + + + Backward + 2 + $i==0 ? $T2 : m_storage.m_cols + m_storage.m_data + + + + + + + + empty + [{m_storage.m_rows}, {$T2}] (dynamic row matrix) + + + 2 + $i==0 ? m_storage.m_rows : $T2 + m_storage.m_data + + + Backward + 2 + $i==0 ? m_storage.m_rows : $T2 + m_storage.m_data + + + + + + + + empty + [{m_storage.m_cols}] (dynamic column vector) + + m_storage.m_cols + + m_storage.m_cols + m_storage.m_data + + + + + + + + empty + [{m_storage.m_rows}] (dynamic row vector) + + m_storage.m_rows + + m_storage.m_rows + m_storage.m_data + + + + + + + + [1] {m_storage.m_data.array[0]} + + m_storage.m_data.array[0] + + + + + + + [2] {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} + + m_storage.m_data.array[0] + m_storage.m_data.array[1] + + + + + + + [3] {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} + + m_storage.m_data.array[0] + m_storage.m_data.array[1] + m_storage.m_data.array[2] + + + + + + + [4] {m_storage.m_data.array[0]} {m_storage.m_data.array[1]} {m_storage.m_data.array[2]} {m_storage.m_data.array[3]} + + m_storage.m_data.array[0] + m_storage.m_data.array[1] + m_storage.m_data.array[2] + m_storage.m_data.array[3] + + + + diff --git a/resources/nlohmann_json.natvis b/resources/nlohmann_json.natvis new file mode 100644 index 0000000..5449cae --- /dev/null +++ b/resources/nlohmann_json.natvis @@ -0,0 +1,278 @@ + + + + + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + + + null + {*(m_value.object)} + {*(m_value.array)} + {*(m_value.string)} + {m_value.boolean} + {m_value.number_integer} + {m_value.number_unsigned} + {m_value.number_float} + discarded + + + *(m_value.object),view(simple) + + + *(m_value.array),view(simple) + + + + + + + {second} + + second + + + + From 43c6e774ec3a2581e69abce2f64c7879018b66f2 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 11:18:19 +0200 Subject: [PATCH 20/61] Compressor not working --- .gitignore | 3 +- .vscode/launch.json | 30 +++++++++++++++++++ src/app/vtkIO.cpp | 2 +- test/core/CompressorTest.cpp | 26 ++-------------- .../alhambra.conformal.tessellator.json | 3 +- .../cases/alhambra/alhambra.tessellator.json | 8 ++++- 6 files changed, 44 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 0a08b75..3f18a66 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ CMakeUserPresets.json sliced.vtk contour.vtk -testData/ \ No newline at end of file +testData/ +build diff --git a/.vscode/launch.json b/.vscode/launch.json index 17c6a06..542abba 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,36 @@ { "version": "0.2.0", "configurations": [ + { + "name": "tessellator (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build-dbg/bin/tessellator", + "args": ["-i", "testData/cases/alhambra/alhambra.tessellator.json"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "visualizerFile": [ + "${workspaceFolder}/resources/Eigen.natvis", + "${workspaceFolder}/resources/nlohmann_json.natvis" + ], + "additionalSOLibSearchPath": "", + "showDisplayString": true, + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + }, { "name": "tessellator_tests (gdb)", "type": "cppdbg", diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index 4565d89..f1e6c2e 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -42,7 +42,7 @@ vtkSmartPointer readAsVTU(const std::filesystem::path& file } vtkSmartPointer vtu; - std::string extension = fn.substr(fn.find_last_of(".")).empty() ? "" : "." + fn.substr(fn.find_last_of(".")); + std::string extension = fn.substr(fn.find_last_of(".")).empty() ? "" : fn.substr(fn.find_last_of(".")); std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index 86d2411..3b5affe 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -83,27 +83,6 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); } -TEST_F(CompressorTest, CompressLShapeIntoOneSurface) { - // Create 3 quads in L-shape - should be merged into one surface - - Mesh mesh; - mesh.grid = grid_; - - // Quad 1: bottom-left - addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); - // Quad 2: bottom-right - addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); - // Quad 3: top-left (forming L-shape) - addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); - - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 3u); - - auto merged = core::Compressor::compressSurfaces(mesh); - - EXPECT_EQ(merged, 2u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); -} - TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { // Create 8 quads forming a ring with a hole in the middle // Should be merged into one surface with inner contour @@ -128,9 +107,8 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); auto merged = core::Compressor::compressSurfaces(mesh); - - EXPECT_EQ(merged, 7u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); } TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { diff --git a/testData/cases/alhambra/alhambra.conformal.tessellator.json b/testData/cases/alhambra/alhambra.conformal.tessellator.json index f2b6c20..9f17e14 100644 --- a/testData/cases/alhambra/alhambra.conformal.tessellator.json +++ b/testData/cases/alhambra/alhambra.conformal.tessellator.json @@ -11,7 +11,8 @@ "type" : "conformal", "options" : { "edgePoints" : 6, - "forbiddenLength" : 0.15 + "forbiddenLength" : 0.15, + "compress": false } } } \ No newline at end of file diff --git a/testData/cases/alhambra/alhambra.tessellator.json b/testData/cases/alhambra/alhambra.tessellator.json index 2519ae2..f1d9032 100644 --- a/testData/cases/alhambra/alhambra.tessellator.json +++ b/testData/cases/alhambra/alhambra.tessellator.json @@ -6,5 +6,11 @@ [ 60.0, 60.0, 10.0] ] }, - "object": {"filename": "alhambra.stl"} + "object": {"filename": "alhambra.stl"}, + "mesher": { + "type" : "staircase", + "options" : { + "compress": true + } + } } \ No newline at end of file From 0573325dc5ec1dab25cd60791c1f28603aa01a2e Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 11:40:08 +0200 Subject: [PATCH 21/61] Iterating --- src/meshers/StaircaseMesher.cpp | 12 ++++++------ test/core/CompressorTest.cpp | 9 ++++++++- test/core/StaircaserTest.cpp | 1 - 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index c6a7239..3925743 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -81,6 +81,12 @@ void StaircaseMesher::process(Mesh& mesh) const logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); logNumberOfLines(countMeshElementsIf(mesh, isLine)); + log("Removing repeated and overlapping elements.", 1); + RedundancyCleaner::removeOverlappedElementsByDimension(mesh, dimensions); + + logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); + logNumberOfLines(countMeshElementsIf(mesh, isLine)); + if (compress_) { log("Compressing surfaces.", 1); std::size_t beforeQuads = countMeshElementsIf(mesh, isQuad); @@ -90,12 +96,6 @@ void StaircaseMesher::process(Mesh& mesh) const " -> " + std::to_string(afterQuads) + " quads (merged " + std::to_string(merged) + " surfaces)", 1); } - - log("Removing repeated and overlapping elements.", 1); - RedundancyCleaner::removeOverlappedElementsByDimension(mesh, dimensions); - - logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); - logNumberOfLines(countMeshElementsIf(mesh, isLine)); log("Recovering original grid size.", 1); reduceGrid(mesh, originalGrid_); diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index 3b5affe..e6bb881 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -42,7 +42,14 @@ TEST_F(CompressorTest, Compress2x2QuadsIntoOneSurface) { auto merged = core::Compressor::compressSurfaces(mesh); EXPECT_EQ(merged, 3u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); + ASSERT_EQ(mesh.groups.size(), 1u); + ASSERT_EQ(mesh.groups[0].elements.size(), 1u); + + EXPECT_EQ( + CoordinateIds({0, 4, 8, 7}), + mesh.groups[0].elements[0].vertices + ); + } TEST_F(CompressorTest, DoesNotCompressNonCoplanarQuads) { diff --git a/test/core/StaircaserTest.cpp b/test/core/StaircaserTest.cpp index 4975e5f..baa63b1 100644 --- a/test/core/StaircaserTest.cpp +++ b/test/core/StaircaserTest.cpp @@ -2204,7 +2204,6 @@ TEST_F(StaircaserTest, selectiveStructurerWithEmptySetOfCells) } - TEST_F(StaircaserTest, modifyCoordinateOfASpecificCell) { From 3bc4439aa49788e4b24a372eccd12944440dba27 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 15:05:59 +0200 Subject: [PATCH 22/61] More tests and splitter --- src/core/CMakeLists.txt | 1 + src/core/Splitter.cpp | 182 +++++++++++++++++++++++++++++++++++ src/core/Splitter.h | 42 ++++++++ test/core/CompressorTest.cpp | 111 ++++++++++++++++++++- 4 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 src/core/Splitter.cpp create mode 100644 src/core/Splitter.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index faab34a..142beee 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(tessellator-core "Snapper.cpp" "Smoother.cpp" "SmootherTools.cpp" + "Splitter.cpp" "Staircaser.cpp" ) diff --git a/src/core/Splitter.cpp b/src/core/Splitter.cpp new file mode 100644 index 0000000..b25ac2e --- /dev/null +++ b/src/core/Splitter.cpp @@ -0,0 +1,182 @@ +#include "Splitter.h" + +#include "utils/GridTools.h" + +namespace meshlib::core { + +std::size_t Splitter::splitSurfaces(Mesh& mesh) { + std::size_t totalNewQuads = 0; + + for (GroupId g = 0; g < mesh.groups.size(); g++) { + std::vector newElements; + + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + const Element& elem = mesh.groups[g].elements[e]; + if (elem.type == Element::Type::Surface) { + // Split this surface into unit quads + std::map coordMap; + + // Build initial coord map from existing coordinates + for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { + coordMap[mesh.coordinates[i]] = i; + } + + std::vector splitQuads = splitSurface_( + elem, mesh.coordinates, mesh.grid, coordMap); + + // Add new coordinates from coordMap + for (const auto& [coord, id] : coordMap) { + if (id >= mesh.coordinates.size()) { + mesh.coordinates.push_back(coord); + } + } + + newElements.insert(newElements.end(), splitQuads.begin(), splitQuads.end()); + totalNewQuads += splitQuads.size(); + } else { + newElements.push_back(elem); + } + } + + mesh.groups[g].elements = std::move(newElements); + } + + return totalNewQuads; +} + +std::vector Splitter::splitSurface_( + const Element& surface, + const std::vector& coords, + const Grid& grid, + std::map& coordMap) { + std::vector quads; + + // Get the plane orientation of the surface + auto [normalAxis, plane] = getSurfacePlane_(surface, coords); + + // Get the grid cell bounds + auto [minCell, maxCell] = getSurfaceBounds_(surface, coords); + + // Determine the 2D axes in the plane + Axis axis1 = (normalAxis + 1) % 3; + Axis axis2 = (normalAxis + 2) % 3; + + // Generate unit quads for each cell in the bounds + for (CellDir i = minCell(axis1); i < maxCell(axis1); i++) { + for (CellDir j = minCell(axis2); j < maxCell(axis2); j++) { + Element quad = createUnitQuad_( + plane, normalAxis, i, j, grid, coordMap); + quads.push_back(quad); + } + } + + return quads; +} + +std::pair Splitter::getSurfaceBounds_( + const Element& surface, + const std::vector& coords) { + Cell minCell = {0, 0, 0}; + Cell maxCell = {0, 0, 0}; + + bool first = true; + for (CoordinateId vid : surface.vertices) { + Cell cell = utils::GridTools::toCell(coords[vid]); + if (first) { + minCell = cell; + maxCell = cell; + first = false; + } else { + for (Axis d = 0; d < 3; d++) { + minCell(d) = std::min(minCell(d), cell(d)); + maxCell(d) = std::max(maxCell(d), cell(d)); + } + } + } + + // maxCell represents the cell containing the max coordinate value + // For a surface spanning cells 0 to N-1, the max coordinate is at grid[N] + // So maxCell should be set to N (the cell index of the max coord), which is already correct + // The loop below will iterate i < maxCell, giving cells 0 to maxCell-1 + // No increment needed + + return {minCell, maxCell}; +} + +std::pair Splitter::getSurfacePlane_( + const Element& surface, + const std::vector& coords) { + // Find which axis has constant coordinate (the normal axis) + for (Axis d = 0; d < 3; d++) { + Cell cell0 = utils::GridTools::toCell(coords[surface.vertices[0]]); + bool allSame = true; + for (CoordinateId vid : surface.vertices) { + Cell cell = utils::GridTools::toCell(coords[vid]); + if (cell(d) != cell0(d)) { + allSame = false; + break; + } + } + if (allSame) { + return {d, cell0(d)}; + } + } + + // Fallback (should not happen for valid surfaces) + return {0, 0}; +} + +Element Splitter::createUnitQuad_( + CellDir plane, + Axis normalAxis, + CellDir xCell, + CellDir yCell, + const Grid& grid, + std::map& coordMap) { + Axis axis1 = (normalAxis + 1) % 3; + Axis axis2 = (normalAxis + 2) % 3; + + // Create 4 corner coordinates for the unit quad + std::array corners; + corners[0] = Coordinate({0, 0, 0}); + corners[1] = Coordinate({0, 0, 0}); + corners[2] = Coordinate({0, 0, 0}); + corners[3] = Coordinate({0, 0, 0}); + + // Set coordinates for each corner + corners[0](normalAxis) = grid[normalAxis][plane]; + corners[0](axis1) = grid[axis1][xCell]; + corners[0](axis2) = grid[axis2][yCell]; + + corners[1](normalAxis) = grid[normalAxis][plane]; + corners[1](axis1) = grid[axis1][xCell + 1]; + corners[1](axis2) = grid[axis2][yCell]; + + corners[2](normalAxis) = grid[normalAxis][plane]; + corners[2](axis1) = grid[axis1][xCell + 1]; + corners[2](axis2) = grid[axis2][yCell + 1]; + + corners[3](normalAxis) = grid[normalAxis][plane]; + corners[3](axis1) = grid[axis1][xCell]; + corners[3](axis2) = grid[axis2][yCell + 1]; + + // Get or create coordinate IDs + std::array vids; + for (int i = 0; i < 4; i++) { + auto it = coordMap.find(corners[i]); + if (it != coordMap.end()) { + vids[i] = it->second; + } else { + coordMap[corners[i]] = static_cast(coordMap.size()); + vids[i] = coordMap[corners[i]]; + } + } + + Element quad; + quad.type = Element::Type::Surface; + quad.vertices = {vids[0], vids[1], vids[2], vids[3]}; + + return quad; +} + +} diff --git a/src/core/Splitter.h b/src/core/Splitter.h new file mode 100644 index 0000000..f063765 --- /dev/null +++ b/src/core/Splitter.h @@ -0,0 +1,42 @@ +#pragma once + +#include "types/Mesh.h" +#include "utils/Types.h" + +namespace meshlib::core { + +class Splitter { +public: + // Split all surfaces in mesh into unit quads (1x1 grid cells) + // Returns number of new quads created + static std::size_t splitSurfaces(Mesh& mesh); + +private: + // Split a single surface into unit quads + static std::vector splitSurface_( + const Element& surface, + const std::vector& coords, + const Grid& grid, + std::map& coordMap); + + // Get grid cell bounds for a surface + static std::pair getSurfaceBounds_( + const Element& surface, + const std::vector& coords); + + // Determine the plane orientation of a surface (which axis is normal) + static std::pair getSurfacePlane_( + const Element& surface, + const std::vector& coords); + + // Create a unit quad at given grid cell position + static Element createUnitQuad_( + CellDir plane, + Axis normalAxis, + CellDir xCell, + CellDir yCell, + const Grid& grid, + std::map& coordMap); +}; + +} diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index e6bb881..b87cfa4 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -1,5 +1,6 @@ #include #include "core/Compressor.h" +#include "core/Splitter.h" #include "MeshFixtures.h" #include "utils/MeshTools.h" @@ -92,12 +93,18 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) { TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { // Create 8 quads forming a ring with a hole in the middle - // Should be merged into one surface with inner contour + // The ring decomposes into 3 rectangles (left col, right col, center cols) Mesh mesh; mesh.grid = grid_; // Outer ring of quads (leaving center 1,1 to 2,2 empty) + // Layout: + // x=0 x=1 x=2 x=3 + // y=3 [5] [8] [6] + // y=2 [3] hole [4] + // y=1 [1] [7] [2] + // y=0 // Bottom row addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0}); @@ -115,7 +122,14 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); + auto finalCount = countMeshElementsIf(mesh, isQuad); + + // Optimal decomposition: 3 rectangles + // - Left column (quads 1,3,5): cells x=0, y=0-3 + // - Right column (quads 2,4,6): cells x=2-3, y=0-3 + // - Center (quads 7,8): cells x=1-2, y=0 and y=2-3 + EXPECT_EQ(finalCount, 3u); + EXPECT_EQ(merged, 5u); // 8 - 3 = 5 surfaces merged } TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { @@ -137,4 +151,97 @@ TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); } +TEST_F(CompressorTest, CompressAndSplit2x2GridRoundTrip) { + // Create 4 quads in 2x2 grid, compress to 1 surface, split back to 4 quads + + Mesh mesh; + mesh.grid = grid_; + + // 2x2 grid of quads + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + addQuad(mesh, {1, 1, 0}, {2, 1, 0}, {2, 2, 0}, {1, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); + + // Compress: 4 quads -> 1 surface + auto merged = core::Compressor::compressSurfaces(mesh); + EXPECT_EQ(merged, 3u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); + + // Debug: print compressed surface + std::cerr << "Compressed surface vertices: "; + for (auto vid : mesh.groups[0].elements[0].vertices) { + std::cerr << "(" << mesh.coordinates[vid](0) << "," + << mesh.coordinates[vid](1) << "," + << mesh.coordinates[vid](2) << ") "; + } + std::cerr << std::endl; + + // Split: 1 surface -> 4 quads + auto splitCount = core::Splitter::splitSurfaces(mesh); + std::cout << "Split count: " << splitCount << std::endl; + EXPECT_EQ(splitCount, 4u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); +} + +TEST_F(CompressorTest, CompressAndSplitRingRoundTrip) { + // Create ring of 8 quads, compress, split back + + Mesh mesh; + mesh.grid = grid_; + + // Ring of 8 quads (same as CompressWithHoleCreatesInnerContour) + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0}); + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); + addQuad(mesh, {2, 1, 0}, {3, 1, 0}, {3, 2, 0}, {2, 2, 0}); + addQuad(mesh, {0, 2, 0}, {1, 2, 0}, {1, 3, 0}, {0, 3, 0}); + addQuad(mesh, {2, 2, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0}); + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); + addQuad(mesh, {1, 3, 0}, {2, 3, 0}, {2, 2, 0}, {1, 2, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); + + // Compress: 8 quads -> 3 surfaces + auto merged = core::Compressor::compressSurfaces(mesh); + EXPECT_EQ(merged, 5u); + auto compressedCount = countMeshElementsIf(mesh, isQuad); + EXPECT_EQ(compressedCount, 3u); + + // Split: 3 surfaces -> 8 quads + auto splitCount = core::Splitter::splitSurfaces(mesh); + EXPECT_EQ(splitCount, 8u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); +} + +TEST_F(CompressorTest, CompressAndSplit3x3GridRoundTrip) { + // Create 9 quads in 3x3 grid, compress to 1 surface, split back to 9 quads + + Mesh mesh; + mesh.grid = grid_; + + // 3x3 grid of quads + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + addQuad(mesh, + {i, j, 0}, {i+1, j, 0}, + {i+1, j+1, 0}, {i, j+1, 0}); + } + } + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 9u); + + // Compress: 9 quads -> 1 surface + auto merged = core::Compressor::compressSurfaces(mesh); + EXPECT_EQ(merged, 8u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); + + // Split: 1 surface -> 9 quads + auto splitCount = core::Splitter::splitSurfaces(mesh); + EXPECT_EQ(splitCount, 9u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 9u); +} + } From 1538acd54435e0ef102add69b0682c790aeb92ea Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 19:04:14 +0200 Subject: [PATCH 23/61] tesrs pass --- src/core/Compressor.cpp | 8 ++++++-- test/core/CompressorTest.cpp | 29 ++++++++++------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp index 82192bd..0255ad8 100644 --- a/src/core/Compressor.cpp +++ b/src/core/Compressor.cpp @@ -185,8 +185,12 @@ std::vector Compressor::compressSurf_( utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d1); ext.second[1] = utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d2); - for (CellDir i = ext.first[0]; i < ext.second[0]; i++) { - for (CellDir j = ext.first[1]; j < ext.second[1]; j++) { + CellDir i0 = std::min(ext.first[0], ext.second[0]); + CellDir i1 = std::max(ext.first[0], ext.second[0]); + CellDir j0 = std::min(ext.first[1], ext.second[1]); + CellDir j1 = std::max(ext.first[1], ext.second[1]); + for (CellDir i = i0; i < i1; i++) { + for (CellDir j = j0; j < j1; j++) { PlaneSurfel surfel = {{i, j}}; surfels.insert(surfel); } diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index b87cfa4..e011b4f 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -93,7 +93,7 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) { TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { // Create 8 quads forming a ring with a hole in the middle - // The ring decomposes into 3 rectangles (left col, right col, center cols) + // The ring decomposes into 4 rectangles (left col, right col, top center, bottom center) Mesh mesh; mesh.grid = grid_; @@ -124,12 +124,13 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { auto finalCount = countMeshElementsIf(mesh, isQuad); - // Optimal decomposition: 3 rectangles + // Optimal decomposition: 4 rectangles // - Left column (quads 1,3,5): cells x=0, y=0-3 // - Right column (quads 2,4,6): cells x=2-3, y=0-3 - // - Center (quads 7,8): cells x=1-2, y=0 and y=2-3 - EXPECT_EQ(finalCount, 3u); - EXPECT_EQ(merged, 5u); // 8 - 3 = 5 surfaces merged + // - Top center (quad 8): cell x=1-2, y=2-3 + // - Bottom center (quad 7): cell x=1-2, y=0-1 + EXPECT_EQ(finalCount, 4u); + EXPECT_EQ(merged, 4u); // 8 - 4 = 4 surfaces merged } TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { @@ -170,18 +171,8 @@ TEST_F(CompressorTest, CompressAndSplit2x2GridRoundTrip) { EXPECT_EQ(merged, 3u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); - // Debug: print compressed surface - std::cerr << "Compressed surface vertices: "; - for (auto vid : mesh.groups[0].elements[0].vertices) { - std::cerr << "(" << mesh.coordinates[vid](0) << "," - << mesh.coordinates[vid](1) << "," - << mesh.coordinates[vid](2) << ") "; - } - std::cerr << std::endl; - // Split: 1 surface -> 4 quads auto splitCount = core::Splitter::splitSurfaces(mesh); - std::cout << "Split count: " << splitCount << std::endl; EXPECT_EQ(splitCount, 4u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); } @@ -204,13 +195,13 @@ TEST_F(CompressorTest, CompressAndSplitRingRoundTrip) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); - // Compress: 8 quads -> 3 surfaces + // Compress: 8 quads -> 4 surfaces (left col, right col, top center, bottom center) auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 5u); + EXPECT_EQ(merged, 4u); // 8 - 4 = 4 surfaces merged auto compressedCount = countMeshElementsIf(mesh, isQuad); - EXPECT_EQ(compressedCount, 3u); + EXPECT_EQ(compressedCount, 4u); - // Split: 3 surfaces -> 8 quads + // Split: 4 surfaces -> 8 quads auto splitCount = core::Splitter::splitSurfaces(mesh); EXPECT_EQ(splitCount, 8u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); From 132d5d461a942ee5d9d74527372e9293fc50ff2a Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Fri, 15 May 2026 21:03:42 +0200 Subject: [PATCH 24/61] Surface compressor works! --- src/core/Compressor.cpp | 147 ++++++++++++++++++++++++++++++++++++++++ src/core/Compressor.h | 15 ++++ src/core/Splitter.h | 30 ++++++++ 3 files changed, 192 insertions(+) diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp index 0255ad8..71c0f33 100644 --- a/src/core/Compressor.cpp +++ b/src/core/Compressor.cpp @@ -59,6 +59,153 @@ std::size_t Compressor::compressSurfaces(Mesh& mesh) { return totalOriginal - totalCompressed; } +std::size_t Compressor::compressLines(Mesh& mesh) { + std::size_t totalOriginal = 0; + std::size_t totalCompressed = 0; + + for (GroupId g = 0; g < mesh.groups.size(); g++) { + std::vector lines; + + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + const Element& elem = mesh.groups[g].elements[e]; + if (elem.type == Element::Type::Line) { + lines.push_back(elem); + } + } + + if (lines.empty()) { + continue; + } + + totalOriginal += lines.size(); + std::vector compressedLines = compressLines_(mesh.coordinates, lines); + totalCompressed += compressedLines.size(); + + // Build new elements vector with compressed lines + std::vector newElements; + ElementId lineIdx = 0; + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + if (mesh.groups[g].elements[e].type == Element::Type::Line) { + if (lineIdx < compressedLines.size()) { + newElements.push_back(compressedLines[lineIdx]); + lineIdx++; + } + } else { + newElements.push_back(mesh.groups[g].elements[e]); + } + } + mesh.groups[g].elements = std::move(newElements); + } + + return totalOriginal - totalCompressed; +} + +std::vector Compressor::compressLines_( + const std::vector& coords, + const std::vector& lines) { + std::vector res; + std::map, + std::pair>, + std::vector> signDirLines; + for (std::size_t l = 0; l < lines.size(); l++) { + std::array auxCells; + auxCells[0] = utils::GridTools::toCell(coords[lines[l].vertices[0]]); + auxCells[1] = utils::GridTools::toCell(coords[lines[l].vertices[1]]); + std::array gridLine; + Sign sign = 1; + Axis dir = 0; + for (Axis d = 0; d < 3; d++) { + if (auxCells[0](d) != auxCells[1](d)) { + if (auxCells[0](d) > auxCells[1](d)) { + sign = -1; + } + dir = d; + Axis d1 = (d + 1) % 3; + Axis d2 = (d + 2) % 3; + gridLine[0] = auxCells[0](d1); + gridLine[1] = auxCells[0](d2); + break; + } + } + signDirLines[std::make_pair(gridLine, + std::make_pair(sign, dir))].push_back(l); + } + for (std::map, + std::pair>, + std::vector>::const_iterator + it = signDirLines.begin(); it != signDirLines.end(); ++it) { + std::vector auxElems; + for (std::size_t i = 0; i < it->second.size(); i++) { + auxElems.push_back(lines[it->second[i]]); + } + std::vector auxRes = + compressDirSignLines_(coords, + it->first.second, + auxElems); + res.insert(res.end(), auxRes.begin(), auxRes.end()); + } + return res; +} + +std::vector Compressor::compressDirSignLines_( + const std::vector& coords, + const std::pair& signDir, + const std::vector& lines) { + std::vector res; + std::map> coordLines; + std::map> lineCoords; + for (std::size_t l = 0; l < lines.size(); l++) { + for (std::size_t v = 0; v < 2; v++) { + coordLines[lines[l].vertices[v]].insert(l); + lineCoords[l].insert(lines[l].vertices[v]); + } + } + std::set vis; + for (std::map>::const_iterator + itExt = lineCoords.begin(); itExt != lineCoords.end(); ++itExt) { + if (vis.count(itExt->first) == 0) { + CoordinateId minCell = lines[itExt->first].vertices[0]; + CoordinateId maxCell = lines[itExt->first].vertices[1]; + std::queue q; + q.push(itExt->first); + vis.insert(itExt->first); + while (!q.empty()) { + ElementId elem = q.front(); + q.pop(); + for (std::size_t i = 0; i < 2; i++) { + if (coords[minCell] > coords[lines[elem].vertices[i]]) { + minCell = lines[elem].vertices[i]; + } + if (coords[maxCell] < coords[lines[elem].vertices[i]]) { + maxCell = lines[elem].vertices[i]; + } + } + for (std::set::const_iterator + itCell = lineCoords[elem].begin(); + itCell != lineCoords[elem].end(); ++itCell) { + for (std::set::const_iterator + itLine = coordLines[*itCell].begin(); + itLine != coordLines[*itCell].end(); ++itLine) { + if (vis.count(*itLine) == 0) { + q.push(*itLine); + vis.insert(*itLine); + } + } + } + } + Element newElem; + newElem.type = Element::Type::Line; + newElem.vertices.push_back(minCell); + newElem.vertices.push_back(maxCell); + if (signDir.first < 0) { + std::swap(newElem.vertices[0], newElem.vertices[1]); + } + res.push_back(newElem); + } + } + return res; +} + std::vector Compressor::compressSurfs_( std::vector& coords, const std::vector& surfs) { diff --git a/src/core/Compressor.h b/src/core/Compressor.h index cfae369..5b66e32 100644 --- a/src/core/Compressor.h +++ b/src/core/Compressor.h @@ -14,6 +14,10 @@ class Compressor { // Returns number of surfaces merged (original_count - compressed_count) static std::size_t compressSurfaces(Mesh& mesh); + // Compress collinear line segments that are adjacent + // Returns number of lines merged (original_count - compressed_count) + static std::size_t compressLines(Mesh& mesh); + private: // Group surfaces by (grid_plane, sign, axis) and compress each group static std::vector compressSurfs_( @@ -32,6 +36,17 @@ class Compressor { const std::pair& signDir, const std::vector& surfs); + // Group lines by (grid_line, sign, axis) and compress each group + static std::vector compressLines_( + const std::vector& coords, + const std::vector& lines); + + // Compress lines with same direction and sign + static std::vector compressDirSignLines_( + const std::vector& coords, + const std::pair& signDir, + const std::vector& lines); + // Merge adjacent surfels into maximal rectangles static std::vector compressSurfels_( const std::set& surfs); diff --git a/src/core/Splitter.h b/src/core/Splitter.h index f063765..91206ef 100644 --- a/src/core/Splitter.h +++ b/src/core/Splitter.h @@ -11,6 +11,10 @@ class Splitter { // Returns number of new quads created static std::size_t splitSurfaces(Mesh& mesh); + // Split all lines in mesh into unit grid lines + // Returns number of new unit lines created + static std::size_t splitLines(Mesh& mesh); + private: // Split a single surface into unit quads static std::vector splitSurface_( @@ -37,6 +41,32 @@ class Splitter { CellDir yCell, const Grid& grid, std::map& coordMap); + + // Split a single polyline into unit grid lines + static std::vector splitLine_( + const Element& line, + const std::vector& coords, + const Grid& grid, + std::map& coordMap); + + // Get grid cell bounds for a line + static std::pair getLineBounds_( + const Element& line, + const std::vector& coords); + + // Determine the axis direction of a line + static Axis getLineAxis_( + const Element& line, + const std::vector& coords); + + // Create a unit line at given grid cell position + static Element createUnitLine_( + CellDir fixedCoord1, + CellDir fixedCoord2, + Axis lineAxis, + CellDir cell, + const Grid& grid, + std::map& coordMap); }; } From 12bc2952fdb2702829f7b56851fe554a5436ae9b Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Sat, 16 May 2026 07:35:27 +0200 Subject: [PATCH 25/61] Adds compress lines --- src/app/launcher.cpp | 17 +++- src/core/Splitter.cpp | 147 ++++++++++++++++++++++++++++++++ src/meshers/StaircaseMesher.cpp | 15 +++- src/meshers/StaircaseMesher.h | 3 +- test/MeshFixtures.h | 32 ++++++- test/core/CompressorTest.cpp | 133 ++++++++++++++++++++++++++++- 6 files changed, 337 insertions(+), 10 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 8772a52..5da5f32 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -142,12 +142,27 @@ bool readStaircaseMesherCompressOption(const std::string &fn) } return false; } + +bool readStaircaseMesherCompressLinesOption(const std::string &fn) +{ + nlohmann::json j; + { + std::ifstream i(fn); + i >> j; + } + if (j["mesher"].contains("options") && + j["mesher"]["options"].contains("compressLines")) { + return j["mesher"]["options"]["compressLines"]; + } + return false; +} std::unique_ptr buildMesher(const Mesh &in, const std::string &fn) { auto mesherType = readMesherType(fn); if (mesherType == meshlib::app::staircase_mesher) { bool compress = readStaircaseMesherCompressOption(fn); - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn), compress}); + bool compressLines = readStaircaseMesherCompressLinesOption(fn); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn), compress, compressLines}); } else if (mesherType == meshlib::app::conformal_mesher) { return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn)}); } else { diff --git a/src/core/Splitter.cpp b/src/core/Splitter.cpp index b25ac2e..059b5a7 100644 --- a/src/core/Splitter.cpp +++ b/src/core/Splitter.cpp @@ -44,6 +44,153 @@ std::size_t Splitter::splitSurfaces(Mesh& mesh) { return totalNewQuads; } +std::size_t Splitter::splitLines(Mesh& mesh) { + std::size_t totalNewLines = 0; + + for (GroupId g = 0; g < mesh.groups.size(); g++) { + std::vector newElements; + + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + const Element& elem = mesh.groups[g].elements[e]; + if (elem.type == Element::Type::Line) { + // Split this line into unit grid lines + std::map coordMap; + + // Build initial coord map from existing coordinates + for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { + coordMap[mesh.coordinates[i]] = i; + } + + std::vector splitLines = splitLine_( + elem, mesh.coordinates, mesh.grid, coordMap); + + // Add new coordinates from coordMap + for (const auto& [coord, id] : coordMap) { + if (id >= mesh.coordinates.size()) { + mesh.coordinates.push_back(coord); + } + } + + newElements.insert(newElements.end(), splitLines.begin(), splitLines.end()); + totalNewLines += splitLines.size(); + } else { + newElements.push_back(elem); + } + } + + mesh.groups[g].elements = std::move(newElements); + } + + return totalNewLines; +} + +std::vector Splitter::splitLine_( + const Element& line, + const std::vector& coords, + const Grid& grid, + std::map& coordMap) { + std::vector unitLines; + + // Get the axis direction of the line + Axis lineAxis = getLineAxis_(line, coords); + + // Get the grid cell bounds + auto [minCell, maxCell] = getLineBounds_(line, coords); + + // Determine the fixed coordinate axes (the two axes perpendicular to lineAxis) + Axis axis1 = (lineAxis + 1) % 3; + Axis axis2 = (lineAxis + 2) % 3; + + // Get the fixed coordinate values from minCell + CellDir fixedCoord1 = minCell(axis1); + CellDir fixedCoord2 = minCell(axis2); + + // Generate unit lines for each cell along the line axis + for (CellDir i = minCell(lineAxis); i < maxCell(lineAxis); i++) { + Element unitLine = createUnitLine_( + fixedCoord1, fixedCoord2, lineAxis, i, grid, coordMap); + unitLines.push_back(unitLine); + } + + return unitLines; +} + +std::pair Splitter::getLineBounds_( + const Element& line, + const std::vector& coords) { + Cell minCell = utils::GridTools::toCell(coords[line.vertices[0]]); + Cell maxCell = utils::GridTools::toCell(coords[line.vertices[1]]); + + // Ensure minCell <= maxCell for all axes + for (Axis d = 0; d < 3; d++) { + if (minCell(d) > maxCell(d)) { + std::swap(minCell(d), maxCell(d)); + } + } + + return {minCell, maxCell}; +} + +Axis Splitter::getLineAxis_( + const Element& line, + const std::vector& coords) { + // Find which axis has different coordinate values (the line direction) + Cell cell0 = utils::GridTools::toCell(coords[line.vertices[0]]); + Cell cell1 = utils::GridTools::toCell(coords[line.vertices[1]]); + + for (Axis d = 0; d < 3; d++) { + if (cell0(d) != cell1(d)) { + return d; + } + } + + // Fallback (should not happen for valid lines) + return 0; +} + +Element Splitter::createUnitLine_( + CellDir fixedCoord1, + CellDir fixedCoord2, + Axis lineAxis, + CellDir cell, + const Grid& grid, + std::map& coordMap) { + Axis axis1 = (lineAxis + 1) % 3; + Axis axis2 = (lineAxis + 2) % 3; + + // Create 2 coordinates for the unit line + std::array endpoints; + endpoints[0] = Coordinate({0, 0, 0}); + endpoints[1] = Coordinate({0, 0, 0}); + + // Set coordinates for each endpoint + endpoints[0](lineAxis) = grid[lineAxis][cell]; + endpoints[0](axis1) = grid[axis1][fixedCoord1]; + endpoints[0](axis2) = grid[axis2][fixedCoord2]; + + endpoints[1](lineAxis) = grid[lineAxis][cell + 1]; + endpoints[1](axis1) = grid[axis1][fixedCoord1]; + endpoints[1](axis2) = grid[axis2][fixedCoord2]; + + // Get or create coordinate IDs + std::array vids; + for (int i = 0; i < 2; i++) { + auto it = coordMap.find(endpoints[i]); + if (it != coordMap.end()) { + vids[i] = it->second; + } else { + coordMap[endpoints[i]] = static_cast(coordMap.size()); + vids[i] = coordMap[endpoints[i]]; + } + } + + Element unitLine; + unitLine.type = Element::Type::Line; + unitLine.vertices = {vids[0], vids[1]}; + + return unitLine; +} + std::vector Splitter::splitSurface_( const Element& surface, const std::vector& coords, diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 3925743..3a431d2 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -20,11 +20,12 @@ using namespace utils; using namespace core; using namespace meshTools; -StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts, bool compress) : +StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts, bool compress, bool compressLines) : MesherBase(inputMesh), decimalPlacesInCollapser_(decimalPlacesInCollapser), opts_(opts), - compress_(compress) + compress_(compress), + compressLines_(compressLines) { log("Preparing surfaces."); surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -97,6 +98,16 @@ void StaircaseMesher::process(Mesh& mesh) const " quads (merged " + std::to_string(merged) + " surfaces)", 1); } + if (compressLines_) { + log("Compressing lines.", 1); + std::size_t beforeLines = countMeshElementsIf(mesh, isLine); + std::size_t merged = Compressor::compressLines(mesh); + std::size_t afterLines = countMeshElementsIf(mesh, isLine); + log("Compressed " + std::to_string(beforeLines) + + " -> " + std::to_string(afterLines) + + " lines (merged " + std::to_string(merged) + " segments)", 1); + } + log("Recovering original grid size.", 1); reduceGrid(mesh, originalGrid_); diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index 070e46e..92b863a 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -8,13 +8,14 @@ namespace meshlib::meshers { class StaircaseMesher : public MesherBase { public: - StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions(), bool compress = false); + StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions(), bool compress = false, bool compressLines = false); virtual ~StaircaseMesher() = default; Mesh mesh() const; private: int decimalPlacesInCollapser_; bool compress_; + bool compressLines_; Mesh surfaceMesh_; StaircaseMesherOptions opts_; diff --git a/test/MeshFixtures.h b/test/MeshFixtures.h index b23fe42..8707e29 100644 --- a/test/MeshFixtures.h +++ b/test/MeshFixtures.h @@ -1273,12 +1273,10 @@ static Mesh buildProblematicTriMesh2() // Helper to add a quad (as a surface with four vertices) to a mesh static void addQuad(Mesh& mesh, const std::array& v0, const std::array& v1, const std::array& v2, const std::array& v3) { - // Ensure at least one group exists if (mesh.groups.empty()) { mesh.groups.emplace_back(); } - // Find or create coordinates auto findOrAddCoord = [&](const std::array& gridIdx) { double pos[3]; pos[0] = mesh.grid[0][gridIdx[0]]; @@ -1301,8 +1299,36 @@ static void addQuad(Mesh& mesh, const std::array& v0, const std::array& v0, const std::array& v1) { + if (mesh.groups.empty()) { + mesh.groups.emplace_back(); + } + + auto findOrAddCoord = [&](const std::array& gridIdx) { + double pos[3]; + pos[0] = mesh.grid[0][gridIdx[0]]; + pos[1] = mesh.grid[1][gridIdx[1]]; + pos[2] = mesh.grid[2][gridIdx[2]]; + + for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { + if (mesh.coordinates[i](0) == pos[0] && + mesh.coordinates[i](1) == pos[1] && + mesh.coordinates[i](2) == pos[2]) { + return i; + } + } + mesh.coordinates.push_back(Coordinate({pos[0], pos[1], pos[2]})); + return static_cast(mesh.coordinates.size() - 1); + }; + + CoordinateId c0 = findOrAddCoord(v0); + CoordinateId c1 = findOrAddCoord(v1); + + mesh.groups[0].elements.push_back(Element({c0, c1}, Element::Type::Line)); +} + } \ No newline at end of file diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index e011b4f..54c836e 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -13,9 +13,9 @@ class CompressorTest : public ::testing::Test { protected: void SetUp() override { grid_ = { - std::vector{0, 1, 2, 3, 4}, - std::vector{0, 1, 2, 3, 4}, - std::vector{0, 1, 2, 3, 4} + std::vector{0, 1, 2, 3, 4, 5, 6}, + std::vector{0, 1, 2, 3, 4, 5, 6}, + std::vector{0, 1, 2, 3, 4, 5, 6} }; } @@ -235,4 +235,131 @@ TEST_F(CompressorTest, CompressAndSplit3x3GridRoundTrip) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 9u); } +// ============== Line Compression Tests ============== + +TEST_F(CompressorTest, Compress2CollinearLinesIntoOne) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 1u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + + ASSERT_EQ(mesh.groups[0].elements.size(), 1u); + const auto& line = mesh.groups[0].elements[0]; + EXPECT_EQ(line.type, Element::Type::Line); + EXPECT_EQ(line.vertices.size(), 2u); +} + +TEST_F(CompressorTest, DoesNotCompressNonCollinearLines) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {0, 0, 0}, {0, 1, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); +} + +TEST_F(CompressorTest, DoesNotCompressDisconnectedLines) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {3, 0, 0}, {4, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); +} + +TEST_F(CompressorTest, Compress3LinesIntoOne) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + addLine(mesh, {2, 0, 0}, {3, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 3u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 2u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); +} + +TEST_F(CompressorTest, CompressAndSplit2LineRoundTrip) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + + auto originalLines = countMeshElementsIf(mesh, isLine); + EXPECT_EQ(originalLines, 2u); + + core::Compressor::compressLines(mesh); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + + auto splitCount = core::Splitter::splitLines(mesh); + + EXPECT_EQ(splitCount, 2u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); +} + +TEST_F(CompressorTest, CompressAndSplit5LineRoundTrip) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + addLine(mesh, {2, 0, 0}, {3, 0, 0}); + addLine(mesh, {3, 0, 0}, {4, 0, 0}); + addLine(mesh, {4, 0, 0}, {5, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 5u); + + core::Compressor::compressLines(mesh); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + + auto splitCount = core::Splitter::splitLines(mesh); + + EXPECT_EQ(splitCount, 5u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 5u); +} + +TEST_F(CompressorTest, CompressMixedDirections) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {1, 0, 0}, {2, 0, 0}); + addLine(mesh, {0, 0, 0}, {0, 1, 0}); + addLine(mesh, {0, 1, 0}, {0, 2, 0}); + addLine(mesh, {0, 0, 0}, {0, 0, 1}); + addLine(mesh, {0, 0, 1}, {0, 0, 2}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 6u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 3u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 3u); +} + } From 1129486ef523ba147e432e015adb8ab98c51d88f Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Sun, 17 May 2026 11:11:14 +0200 Subject: [PATCH 26/61] Adds comoressor abf exportGrid options --- src/app/launcher.cpp | 15 ++++++++------- src/meshers/StaircaseMesher.cpp | 11 ++++------- src/meshers/StaircaseMesher.h | 3 +-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 5da5f32..5c36c5a 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -143,7 +143,7 @@ bool readStaircaseMesherCompressOption(const std::string &fn) return false; } -bool readStaircaseMesherCompressLinesOption(const std::string &fn) +bool readExportGridOption(const std::string &fn) { nlohmann::json j; { @@ -151,18 +151,17 @@ bool readStaircaseMesherCompressLinesOption(const std::string &fn) i >> j; } if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("compressLines")) { - return j["mesher"]["options"]["compressLines"]; + j["mesher"]["options"].contains("exportGrid")) { + return j["mesher"]["options"]["exportGrid"]; } - return false; + return true; } std::unique_ptr buildMesher(const Mesh &in, const std::string &fn) { auto mesherType = readMesherType(fn); if (mesherType == meshlib::app::staircase_mesher) { bool compress = readStaircaseMesherCompressOption(fn); - bool compressLines = readStaircaseMesherCompressLinesOption(fn); - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn), compress, compressLines}); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn), compress}); } else if (mesherType == meshlib::app::conformal_mesher) { return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn)}); } else { @@ -203,7 +202,9 @@ int launcher(int argc, const char* argv[]) auto extension = readExtension(inputFilename); exportMeshToVTU(outputFolder / (basename + ".tessellator." + extension + ".vtk"), resultMesh); - exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), resultMesh.grid); + if (readExportGridOption(inputFilename)) { + exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), resultMesh.grid); + } return EXIT_SUCCESS; } diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 3a431d2..84b9410 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -20,12 +20,11 @@ using namespace utils; using namespace core; using namespace meshTools; -StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts, bool compress, bool compressLines) : +StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts, bool compress) : MesherBase(inputMesh), decimalPlacesInCollapser_(decimalPlacesInCollapser), opts_(opts), - compress_(compress), - compressLines_(compressLines) + compress_(compress) { log("Preparing surfaces."); surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -96,12 +95,10 @@ void StaircaseMesher::process(Mesh& mesh) const log("Compressed " + std::to_string(beforeQuads) + " -> " + std::to_string(afterQuads) + " quads (merged " + std::to_string(merged) + " surfaces)", 1); - } - - if (compressLines_) { + log("Compressing lines.", 1); std::size_t beforeLines = countMeshElementsIf(mesh, isLine); - std::size_t merged = Compressor::compressLines(mesh); + merged = Compressor::compressLines(mesh); std::size_t afterLines = countMeshElementsIf(mesh, isLine); log("Compressed " + std::to_string(beforeLines) + " -> " + std::to_string(afterLines) + diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index 92b863a..070e46e 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -8,14 +8,13 @@ namespace meshlib::meshers { class StaircaseMesher : public MesherBase { public: - StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions(), bool compress = false, bool compressLines = false); + StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions(), bool compress = false); virtual ~StaircaseMesher() = default; Mesh mesh() const; private: int decimalPlacesInCollapser_; bool compress_; - bool compressLines_; Mesh surfaceMesh_; StaircaseMesherOptions opts_; From 65d207a1dafdad9785980230d1ebe9de4e510963 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 11:13:19 +0000 Subject: [PATCH 27/61] ci: retry configure step on transient vcpkg download failures Agent-Logs-Url: https://github.com/OpenSEMBA/tessellator/sessions/e5974172-7eed-4d3b-a308-0d68b508b8c4 Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- .github/workflows/build-and-test.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5e2b663..6f78829 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -65,7 +65,19 @@ jobs: - name: Windows configure and build if: matrix.preset.name=='msbuild' run: | - cmake --preset ${{matrix.preset.name}} -S . -B build + $configured = $false + for ($attempt = 1; $attempt -le 3; $attempt++) { + cmake --preset ${{matrix.preset.name}} -S . -B build + if ($LASTEXITCODE -eq 0) { + $configured = $true + break + } + if ($attempt -lt 3) { + Write-Host "Configure failed (attempt $attempt/3). Retrying in 20 seconds..." + Start-Sleep -Seconds 20 + } + } + if (-not $configured) { exit 1 } cmake --build build --config ${{matrix.build-type}} -j - name: Windows Run tests @@ -75,7 +87,15 @@ jobs: - name: Ubuntu configure and build if: matrix.preset.name=='gnu' run: | - cmake --preset ${{matrix.preset.name}} -S . -B build + configured=0 + for attempt in 1 2 3; do + cmake --preset ${{matrix.preset.name}} -S . -B build && configured=1 && break + if [ "$attempt" -lt 3 ]; then + echo "Configure failed (attempt $attempt/3). Retrying in 20 seconds..." + sleep 20 + fi + done + [ "$configured" -eq 1 ] || exit 1 cmake --build build -j - name: Ubuntu Run tests From 0e459a4420da0e5491552619d4c3e99428d3a94f Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Wed, 15 Jul 2026 14:41:23 +0000 Subject: [PATCH 28/61] Tessellator | Build | Fix VTK import checks to allow tests that don't require it and import all of them when available --- .vscode/launch.json | 2 +- CMakeLists.txt | 17 +++++++++++++++++ src/app/CMakeLists.txt | 19 +++++++++---------- test/CMakeLists.txt | 18 +++++++++--------- test/app/vtkIOTest.cpp | 4 ++-- test/core/CollapserTest.cpp | 9 ++++++++- test/core/SlicerTest.cpp | 24 +++++++++++++++++------- test/core/SmootherTest.cpp | 10 ++++++++-- test/core/SnapperTest.cpp | 10 ++++++++-- test/meshers/ConformalMesherTest.cpp | 17 +++++++++++++++-- test/meshers/StaircaseMesherTest.cpp | 14 ++++++++++++-- 11 files changed, 106 insertions(+), 38 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 542abba..5b53cf4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -35,7 +35,7 @@ "name": "tessellator_tests (gdb)", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/build-dbg/bin/tesselator_tests", + "program": "${workspaceFolder}/build-dbg/bin/tessellator_tests", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", diff --git a/CMakeLists.txt b/CMakeLists.txt index 959c7cf..0378d35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) option(TESSELLATOR_ENABLE_TESTS "Compile tests" ON) option(TESSELLATOR_ENABLE_CGAL "Compile using CGAL library" ON) option(TESSELLATOR_EXECUTION_POLICIES OFF) +option(TESSELLATOR_LOAD_APP "Compile app" OFF) if(TESSELLATOR_ENABLE_CGAL) list(APPEND VCPKG_MANIFEST_FEATURES "cgal") @@ -40,6 +41,22 @@ endif() project(tessellator CXX) +find_package(VTK COMPONENTS + CommonCore + IOGeometry + IOLegacy + FiltersCore +) + +if(VTK_FOUND) + set(TESSELLATOR_LOAD_APP ON) + add_definitions(-DAPP_LOADED=1) + +else() + message(STATUS "VTK not found - tessellator app will not be built") + add_definitions(-DAPP_LOADED=0) +endif() + add_subdirectory(src/) if(TESSELLATOR_ENABLE_TESTS) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index a6ee7f9..ed3d2c7 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,13 +1,13 @@ message(STATUS "Creating build system for tessellator-app") -find_package(VTK COMPONENTS - CommonCore - IOGeometry - IOLegacy - FiltersCore -) +if(TESSELLATOR_LOAD_APP) + find_package(VTK COMPONENTS + CommonCore + IOGeometry + IOLegacy + FiltersCore + ) -if(VTK_FOUND) add_library(tessellator-app "vtkIO.cpp" "launcher.cpp" @@ -22,13 +22,12 @@ if(VTK_FOUND) Boost::program_options nlohmann_json::nlohmann_json ) - add_executable(tessellator "tessellator.cpp" ) target_link_libraries(tessellator tessellator-app tessellator-meshers) -else() - message(STATUS "VTK not found - tessellator app will not be built") + + else() add_library(tessellator-app INTERFACE) endif() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dd9f73f..638a03a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,7 +3,6 @@ message(STATUS "Creating build system for tessellator-tests") find_package(GTest CONFIG REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) - include_directories( ${PROJECT_SOURCE_DIR}/src/ ${PROJECT_SOURCE_DIR}/src/app/ @@ -15,8 +14,12 @@ include_directories( ) add_executable(tessellator_tests + "core/CollapserTest.cpp" "core/CompressorTest.cpp" + "core/SlicerTest.cpp" + "core/SmootherTest.cpp" "core/SmootherToolsTest.cpp" + "core/SnapperTest.cpp" "core/StaircaserTest.cpp" "types/MeshTest.cpp" "utils/ConvexHullTest.cpp" @@ -26,7 +29,9 @@ add_executable(tessellator_tests "utils/GridToolsTest.cpp" "utils/MeshToolsTest.cpp" "utils/RedundancyCleanerTest.cpp" + "meshers/ConformalMesherTest.cpp" "meshers/OffgridMesherTest.cpp" + "meshers/StaircaseMesherTest.cpp" ) target_link_libraries(tessellator_tests @@ -35,15 +40,10 @@ target_link_libraries(tessellator_tests GTest::gtest_main ) -if(VTK_FOUND) - target_sources(tessellator_tests PRIVATE +if(TESSELLATOR_LOAD_APP) + target_sources(tessellator_tests PRIVATE + "app/launcherTest.cpp" "app/vtkIOTest.cpp" - "core/CollapserTest.cpp" - "core/SlicerTest.cpp" - "core/SnapperTest.cpp" - "core/SmootherTest.cpp" - "meshers/StaircaseMesherTest.cpp" - "meshers/ConformalMesherTest.cpp" ) target_link_libraries(tessellator_tests tessellator-app) endif() diff --git a/test/app/vtkIOTest.cpp b/test/app/vtkIOTest.cpp index 712fbfa..5541b85 100644 --- a/test/app/vtkIOTest.cpp +++ b/test/app/vtkIOTest.cpp @@ -23,8 +23,8 @@ TEST_F(VTKIOTest, readMeshFromSTL) TEST_F(VTKIOTest, exportAndReadMeshFromVTU) { auto mSTL{ readInputMesh("testData/cases/alhambra/alhambra.stl") }; - exportMeshToVTU("tmp_exported_alhambra.vtu", mSTL); - auto mVTU{ readInputMesh("tmp_exported_alhambra.vtu") }; + exportMeshToVTU("testData/cases/alhambra/tmp_exported_alhambra.vtu", mSTL); + auto mVTU{ readInputMesh("testData/cases/alhambra/tmp_exported_alhambra.vtu") }; EXPECT_EQ(mSTL.coordinates.size(), mVTU.coordinates.size()); EXPECT_EQ(mSTL.groups.size(), mVTU.groups.size()); diff --git a/test/core/CollapserTest.cpp b/test/core/CollapserTest.cpp index 126f887..e81c0ec 100644 --- a/test/core/CollapserTest.cpp +++ b/test/core/CollapserTest.cpp @@ -7,7 +7,10 @@ #include "utils/CoordGraph.h" #include "utils/GridTools.h" #include "utils/MeshTools.h" -#include "app/vtkIO.h" + +#if APP_LOADED + #include "app/vtkIO.h" +#endif namespace meshlib::core { @@ -146,6 +149,8 @@ TEST_F(CollapserTest, preserves_closedness) } +#if APP_LOADED + TEST_F(CollapserTest, closedness_for_sphere) { auto m = vtkIO::readInputMesh("testData/cases/sphere/sphere.stl"); @@ -164,6 +169,8 @@ TEST_F(CollapserTest, closedness_for_sphere) } +#endif + TEST_F(CollapserTest, areas_are_below_threshold_issue) { Mesh m; diff --git a/test/core/SlicerTest.cpp b/test/core/SlicerTest.cpp index 1ae8c6e..2367b93 100644 --- a/test/core/SlicerTest.cpp +++ b/test/core/SlicerTest.cpp @@ -5,10 +5,13 @@ #include "Collapser.h" #include "Geometry.h" #include "MeshTools.h" -#include "app/vtkIO.h" #include "utils/RedundancyCleaner.h" #include "utils/CoordGraph.h" +#if APP_LOADED + #include "app/vtkIO.h" +#endif + namespace meshlib::core { using namespace meshFixtures; @@ -990,6 +993,7 @@ TEST_F(SlicerTest, canKeepOppositeLinesInLineDimensionPolicy) } } +#if APP_LOADED TEST_F(SlicerTest, preserves_topological_closedness_for_alhambra) { @@ -1024,6 +1028,8 @@ TEST_F(SlicerTest, preserves_topological_closedness_for_sphere) // vtkIO::exportMeshToVTU("testData/cases/sphere/sphere.contour.vtk", contourMesh); } +#endif + TEST_F(SlicerTest, sphere_case_patch_contour_check_1) { Mesh m; @@ -1044,14 +1050,17 @@ TEST_F(SlicerTest, sphere_case_patch_contour_check_1) EXPECT_EQ(countContours(m), countContours(slicedMesh)); + #if APP_LOADED //For debugging. // meshTools::convertToAbsoluteCoordinates(slicedMesh); // vtkIO::exportMeshToVTU("testData/cases/sphere/sphere.sliced.vtk", slicedMesh); // auto contourMesh = meshTools::buildMeshFromContours(slicedMesh); // vtkIO::exportMeshToVTU("testData/cases/sphere/sphere.contour.vtk", contourMesh); + #endif } + TEST_F(SlicerTest, sphere_case_patch_contour_check_2) { Mesh m; @@ -1071,13 +1080,14 @@ TEST_F(SlicerTest, sphere_case_patch_contour_check_2) auto slicedMesh = Slicer{m}.getMesh(); EXPECT_EQ(countContours(m), countContours(slicedMesh)); - + #if APP_LOADED // For debugging. - meshTools::convertToAbsoluteCoordinates(slicedMesh); - vtkIO::exportMeshToVTU("sliced.vtk", slicedMesh); + // meshTools::convertToAbsoluteCoordinates(slicedMesh); + // vtkIO::exportMeshToVTU("sliced.vtk", slicedMesh); - auto contourMesh = meshTools::buildMeshFromContours(slicedMesh); - vtkIO::exportMeshToVTU("contour.vtk", contourMesh); + // auto contourMesh = meshTools::buildMeshFromContours(slicedMesh); + // vtkIO::exportMeshToVTU("contour.vtk", contourMesh); + #endif } -} \ No newline at end of file +} diff --git a/test/core/SmootherTest.cpp b/test/core/SmootherTest.cpp index 228ddb0..fce3dac 100644 --- a/test/core/SmootherTest.cpp +++ b/test/core/SmootherTest.cpp @@ -6,7 +6,10 @@ #include "utils/Geometry.h" #include "utils/MeshTools.h" #include "core/Slicer.h" -#include "app/vtkIO.h" + +#if APP_LOADED + #include "app/vtkIO.h" +#endif namespace meshlib::core { using namespace utils; @@ -87,6 +90,8 @@ TEST_F(SmootherTest, touching_by_single_point) EXPECT_EQ(1, countMeshElementsIf(r, isTriangle)); } +#if APP_LOADED + TEST_F(SmootherTest, preserves_topological_closedness_for_alhambra) { @@ -116,7 +121,6 @@ TEST_F(SmootherTest, preserves_topological_closedness_for_alhambra) // vtkIO::exportMeshToVTU("testData/cases/alhambra/alhambra.contour.vtk", contourMesh); } - TEST_F(SmootherTest, preserves_topological_closedness_for_sphere) { auto m = vtkIO::readInputMesh("testData/cases/sphere/sphere.stl"); @@ -146,4 +150,6 @@ TEST_F(SmootherTest, preserves_topological_closedness_for_sphere) // vtkIO::exportMeshToVTU("testData/cases/sphere/sphere.contour.vtk", contourMesh); } +#endif + } \ No newline at end of file diff --git a/test/core/SnapperTest.cpp b/test/core/SnapperTest.cpp index da42665..42cd629 100644 --- a/test/core/SnapperTest.cpp +++ b/test/core/SnapperTest.cpp @@ -1,13 +1,16 @@ #include "gtest/gtest.h" #include "MeshFixtures.h" - #include "Snapper.h" #include "Slicer.h" #include "Smoother.h" #include "utils/Tools.h" #include "utils/Geometry.h" #include "utils/MeshTools.h" -#include "app/vtkIO.h" + + +#if APP_LOADED + #include "app/vtkIO.h" +#endif using namespace meshlib; using namespace core; @@ -46,6 +49,8 @@ TEST_F(SnapperTest, similar_results_for_each_plane) } } +#if APP_LOADED + TEST_F(SnapperTest, preserves_topological_closedness_for_sphere) { auto m = vtkIO::readInputMesh("testData/cases/sphere/sphere.stl"); @@ -78,6 +83,7 @@ TEST_F(SnapperTest, preserves_topological_closedness_for_sphere) // vtkIO::exportMeshToVTU("testData/cases/sphere/sphere.contour.vtk", contourMesh); } +# endif // TEST_F(SnapperTest, triangles_convert_to_lines) // { diff --git a/test/meshers/ConformalMesherTest.cpp b/test/meshers/ConformalMesherTest.cpp index bbc52a7..b13db5d 100644 --- a/test/meshers/ConformalMesherTest.cpp +++ b/test/meshers/ConformalMesherTest.cpp @@ -4,16 +4,24 @@ #include "meshers/ConformalMesher.h" #include "utils/Geometry.h" -#include "app/vtkIO.h" #include "utils/MeshTools.h" +#if APP_LOADED + #include "app/vtkIO.h" +#endif + namespace meshlib::meshers { using namespace meshFixtures; using namespace utils::meshTools; -using namespace vtkIO; + +#if APP_LOADED + using namespace vtkIO; +#endif class ConformalMesherTest : public ::testing::Test { protected: + +#if APP_LOADED Mesh launchConformalMesher(const std::string& inputFilename, const Mesh& inputMesh) { ConformalMesherOptions opts; @@ -31,6 +39,7 @@ class ConformalMesherTest : public ::testing::Test { return res; } +#endif }; TEST_F(ConformalMesherTest, cellsWithMoreThanAVertexPerEdge_1) @@ -333,6 +342,8 @@ TEST_F(ConformalMesherTest, cellsWithMoreThanAPathPerFace_8) EXPECT_EQ(2, res.size()); } +#if APP_LOADED + TEST_F(ConformalMesherTest, sphere) { // Input @@ -421,6 +432,8 @@ TEST_F(ConformalMesherTest, thinCylinder) EXPECT_NE(0, mesh.countElems()); } +#endif + // TEST_F(ConformalMesherTest, plane45_size05_grid_adapted) // { // ConformalMesher mesher(buildPlane45Mesh(0.5)); diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index 3b7419b..1787343 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -1,6 +1,7 @@ #include "gtest/gtest.h" #include "MeshFixtures.h" + #include "meshers/StaircaseMesher.h" #include "Staircaser.h" @@ -12,7 +13,10 @@ #include "utils/MeshTools.h" #include "utils/RedundancyCleaner.h" -#include "app/vtkIO.h" + +#if APP_LOADED + #include "app/vtkIO.h" +#endif namespace meshlib::meshers { @@ -228,6 +232,7 @@ TEST_F(StaircaseMesherTest, testTriNonUniformGridStaircase) EXPECT_EQ(0, countMeshElementsIf(out, isNode)); } +#if APP_LOADED // FOR DEBUG ONLY / OBTAIN VISUAL REPRESENTATION TEST_F(StaircaseMesherTest, DISABLED_visualSelectiveStaircaserCone) @@ -284,7 +289,7 @@ TEST_F(StaircaseMesherTest, DISABLED_visualSelectiveStaircaserCone) meshlib::vtkIO::exportGridToVTU(outputFolder / (basename + ".tessellator.selective.grid.vtk"), resultMesh.grid); } - +#endif TEST_F(StaircaseMesherTest, DISABLED_testStaircaseTriangleWithUniformGrid) { @@ -317,6 +322,8 @@ TEST_F(StaircaseMesherTest, DISABLED_testStaircaseTriangleWithUniformGrid) EXPECT_EQ(6, countMeshElementsIf(resultMesh, isNode)); } +#if APP_LOADED + TEST_F(StaircaseMesherTest, fills_closed_volume_with_quads) { auto mesh = vtkIO::readInputMesh("testData/cases/sphere/sphere.stl"); @@ -524,5 +531,8 @@ TEST_F(StaircaseMesherTest, staircaser_reads_wires_correctly) // meshlib::vtkIO::exportGridToVTU(outputFolder / (basename + ".tessellator.selective.grid.vtk"), resultMesh.grid); } + +#endif + } From 69a330c7d2faf4c40638a3f24771393875d7260d Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Thu, 16 Jul 2026 11:42:04 +0000 Subject: [PATCH 29/61] Tessellator | Docker | Update linter and build for development containers --- .devcontainer/Dockerfile.base | 3 +++ .devcontainer/devcontainer.json | 3 ++- .gitignore | 1 + .vscode/launch.json | 40 +++++++++++++++++++++++++++++++++ .vscode/settings.dev.json | 4 +++- CMakeLists.txt | 5 +++++ CMakePresets.json | 9 ++++++-- 7 files changed, 61 insertions(+), 4 deletions(-) diff --git a/.devcontainer/Dockerfile.base b/.devcontainer/Dockerfile.base index 4e5549e..7248eb0 100644 --- a/.devcontainer/Dockerfile.base +++ b/.devcontainer/Dockerfile.base @@ -25,6 +25,9 @@ RUN apt-get install -y \ libeigen3-dev \ libcgal-dev +RUN apt-get install -y \ + clangd + RUN chown -R ubuntu:ubuntu /home/ubuntu USER ubuntu diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 99b7fce..7847357 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,7 +13,8 @@ "ms-vscode.cpptools", "ms-vscode.cmake-tools", "vadimcn.vscode-lldb", - "matepek.vscode-catch2-test-adapter" + "matepek.vscode-catch2-test-adapter", + "llvm-vs-code-extensions.vscode-clangd" ], "settings": { "cmake.configureOnOpen": false, diff --git a/.gitignore b/.gitignore index 3f18a66..1fd2b13 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ build/ .settings .project .cproject +.cache src/*.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 5b53cf4..96b5b7a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,38 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Debug Tessellator (Docker)", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/build-dbg/bin/tessellator", + "args": ["-i", "${workspaceFolder}/testData/cases/cone/cone.tessellator.json"], + "stopOnEntry": false, + "cwd": "${workspaceFolder}", + "env": {} + }, + { + "name": "Debug Tests (Docker)", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/build-dbg/bin/tessellator_tests", + "args": ["--gtest_filter=*"], + "stopOnEntry": false, + "cwd": "${workspaceFolder}", + "env": {}, + "sourceMap": {} + }, + { + "name": "Debug Tests (Docker) - Specific Suite", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/build-dbg/bin/tessellator_tests", + "args": ["--gtest_filter=${input:testSuite}"], + "stopOnEntry": false, + "cwd": "${workspaceFolder}", + "env": {}, + "sourceMap": {}, + }, { "name": "tessellator (gdb)", "type": "cppdbg", @@ -61,5 +93,13 @@ } ] } + ], + "inputs": [ + { + "id": "testSuite", + "type": "promptString", + "description": "Enter test filter (e.g., StaircaseMesherTest.* or StaircaseMesherTest.TestName)", + "default": "*" + } ] } \ No newline at end of file diff --git a/.vscode/settings.dev.json b/.vscode/settings.dev.json index 45347a3..b90d07d 100644 --- a/.vscode/settings.dev.json +++ b/.vscode/settings.dev.json @@ -3,5 +3,7 @@ "cmake.configureOnEdit": false, "cmake.autoSelectActiveFolder": false, "testMate.cpp.test.executables": "{build,build-dbg,Build,BUILD,out,Out,OUT}/**/*{test,Test,TEST}*", - "testMate.cpp.test.workingDirectory": "${workspaceFolder}" + "testMate.cpp.test.workingDirectory": "${workspaceFolder}", + "extensions.ignoreRecommendations": true, + "C_Cpp.intelliSenseEngine": "disabled" } diff --git a/CMakeLists.txt b/CMakeLists.txt index 0378d35..577007a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,13 +23,18 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_EXPORT_COMPILE_COMMANDS OFF) option(TESSELLATOR_ENABLE_TESTS "Compile tests" ON) option(TESSELLATOR_ENABLE_CGAL "Compile using CGAL library" ON) option(TESSELLATOR_EXECUTION_POLICIES OFF) +option(DOCKER_EXPORT_COMPILE_COMMANDS "Information for the 'clangd' linter extension" OFF) option(TESSELLATOR_LOAD_APP "Compile app" OFF) +if(DOCKER_EXPORT_COMPILE_COMMANDS) + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +endif() if(TESSELLATOR_ENABLE_CGAL) list(APPEND VCPKG_MANIFEST_FEATURES "cgal") SET( EIGEN3_INCLUDE_DIR "$ENV{EIGEN3_INCLUDE_DIR}" ) diff --git a/CMakePresets.json b/CMakePresets.json index 4628b74..6cfa514 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -49,7 +49,8 @@ "CMAKE_PREFIX_PATH": "/usr/local", "CMAKE_FIND_ROOT_PATH": "/usr/local", "TESSELLATOR_ENABLE_TESTS": "ON", - "TESSELLATOR_ENABLE_CGAL": "ON" + "TESSELLATOR_ENABLE_CGAL": "ON", + "DOCKER_EXPORT_COMPILE_COMMANDS": "ON" }, "environment": { "EIGEN3_INCLUDE_DIR": "/usr/include/eigen3" @@ -67,7 +68,11 @@ "CMAKE_PREFIX_PATH": "/usr/local", "CMAKE_FIND_ROOT_PATH": "/usr/local", "TESSELLATOR_ENABLE_TESTS": "ON", - "TESSELLATOR_ENABLE_CGAL": "ON" + "TESSELLATOR_ENABLE_CGAL": "ON", + "DOCKER_EXPORT_COMPILE_COMMANDS": "ON" + }, + "environment": { + "EIGEN3_INCLUDE_DIR": "/usr/include/eigen3" } } ], From ca8a8f6d81813fde58fdf83681800491b4cb4807 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Wed, 15 Jul 2026 15:11:04 +0000 Subject: [PATCH 30/61] Tessellator | Core | Delete Splitter Splitter has several fundamental errors and it's not used outside of compressor tests. If a necessity for a splitter comes around, it can be recovered from these commits and fix, or make it from scratch. --- src/core/CMakeLists.txt | 1 - src/core/Splitter.cpp | 329 ----------------------------------- src/core/Splitter.h | 72 -------- test/core/CompressorTest.cpp | 42 +---- 4 files changed, 8 insertions(+), 436 deletions(-) delete mode 100644 src/core/Splitter.cpp delete mode 100644 src/core/Splitter.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 142beee..faab34a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -7,7 +7,6 @@ add_library(tessellator-core "Snapper.cpp" "Smoother.cpp" "SmootherTools.cpp" - "Splitter.cpp" "Staircaser.cpp" ) diff --git a/src/core/Splitter.cpp b/src/core/Splitter.cpp deleted file mode 100644 index 059b5a7..0000000 --- a/src/core/Splitter.cpp +++ /dev/null @@ -1,329 +0,0 @@ -#include "Splitter.h" - -#include "utils/GridTools.h" - -namespace meshlib::core { - -std::size_t Splitter::splitSurfaces(Mesh& mesh) { - std::size_t totalNewQuads = 0; - - for (GroupId g = 0; g < mesh.groups.size(); g++) { - std::vector newElements; - - for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { - const Element& elem = mesh.groups[g].elements[e]; - if (elem.type == Element::Type::Surface) { - // Split this surface into unit quads - std::map coordMap; - - // Build initial coord map from existing coordinates - for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { - coordMap[mesh.coordinates[i]] = i; - } - - std::vector splitQuads = splitSurface_( - elem, mesh.coordinates, mesh.grid, coordMap); - - // Add new coordinates from coordMap - for (const auto& [coord, id] : coordMap) { - if (id >= mesh.coordinates.size()) { - mesh.coordinates.push_back(coord); - } - } - - newElements.insert(newElements.end(), splitQuads.begin(), splitQuads.end()); - totalNewQuads += splitQuads.size(); - } else { - newElements.push_back(elem); - } - } - - mesh.groups[g].elements = std::move(newElements); - } - - return totalNewQuads; -} - -std::size_t Splitter::splitLines(Mesh& mesh) { - std::size_t totalNewLines = 0; - - for (GroupId g = 0; g < mesh.groups.size(); g++) { - std::vector newElements; - - for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { - const Element& elem = mesh.groups[g].elements[e]; - if (elem.type == Element::Type::Line) { - // Split this line into unit grid lines - std::map coordMap; - - // Build initial coord map from existing coordinates - for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { - coordMap[mesh.coordinates[i]] = i; - } - - std::vector splitLines = splitLine_( - elem, mesh.coordinates, mesh.grid, coordMap); - - // Add new coordinates from coordMap - for (const auto& [coord, id] : coordMap) { - if (id >= mesh.coordinates.size()) { - mesh.coordinates.push_back(coord); - } - } - - newElements.insert(newElements.end(), splitLines.begin(), splitLines.end()); - totalNewLines += splitLines.size(); - } else { - newElements.push_back(elem); - } - } - - mesh.groups[g].elements = std::move(newElements); - } - - return totalNewLines; -} - -std::vector Splitter::splitLine_( - const Element& line, - const std::vector& coords, - const Grid& grid, - std::map& coordMap) { - std::vector unitLines; - - // Get the axis direction of the line - Axis lineAxis = getLineAxis_(line, coords); - - // Get the grid cell bounds - auto [minCell, maxCell] = getLineBounds_(line, coords); - - // Determine the fixed coordinate axes (the two axes perpendicular to lineAxis) - Axis axis1 = (lineAxis + 1) % 3; - Axis axis2 = (lineAxis + 2) % 3; - - // Get the fixed coordinate values from minCell - CellDir fixedCoord1 = minCell(axis1); - CellDir fixedCoord2 = minCell(axis2); - - // Generate unit lines for each cell along the line axis - for (CellDir i = minCell(lineAxis); i < maxCell(lineAxis); i++) { - Element unitLine = createUnitLine_( - fixedCoord1, fixedCoord2, lineAxis, i, grid, coordMap); - unitLines.push_back(unitLine); - } - - return unitLines; -} - -std::pair Splitter::getLineBounds_( - const Element& line, - const std::vector& coords) { - Cell minCell = utils::GridTools::toCell(coords[line.vertices[0]]); - Cell maxCell = utils::GridTools::toCell(coords[line.vertices[1]]); - - // Ensure minCell <= maxCell for all axes - for (Axis d = 0; d < 3; d++) { - if (minCell(d) > maxCell(d)) { - std::swap(minCell(d), maxCell(d)); - } - } - - return {minCell, maxCell}; -} - -Axis Splitter::getLineAxis_( - const Element& line, - const std::vector& coords) { - // Find which axis has different coordinate values (the line direction) - Cell cell0 = utils::GridTools::toCell(coords[line.vertices[0]]); - Cell cell1 = utils::GridTools::toCell(coords[line.vertices[1]]); - - for (Axis d = 0; d < 3; d++) { - if (cell0(d) != cell1(d)) { - return d; - } - } - - // Fallback (should not happen for valid lines) - return 0; -} - -Element Splitter::createUnitLine_( - CellDir fixedCoord1, - CellDir fixedCoord2, - Axis lineAxis, - CellDir cell, - const Grid& grid, - std::map& coordMap) { - Axis axis1 = (lineAxis + 1) % 3; - Axis axis2 = (lineAxis + 2) % 3; - - // Create 2 coordinates for the unit line - std::array endpoints; - endpoints[0] = Coordinate({0, 0, 0}); - endpoints[1] = Coordinate({0, 0, 0}); - - // Set coordinates for each endpoint - endpoints[0](lineAxis) = grid[lineAxis][cell]; - endpoints[0](axis1) = grid[axis1][fixedCoord1]; - endpoints[0](axis2) = grid[axis2][fixedCoord2]; - - endpoints[1](lineAxis) = grid[lineAxis][cell + 1]; - endpoints[1](axis1) = grid[axis1][fixedCoord1]; - endpoints[1](axis2) = grid[axis2][fixedCoord2]; - - // Get or create coordinate IDs - std::array vids; - for (int i = 0; i < 2; i++) { - auto it = coordMap.find(endpoints[i]); - if (it != coordMap.end()) { - vids[i] = it->second; - } else { - coordMap[endpoints[i]] = static_cast(coordMap.size()); - vids[i] = coordMap[endpoints[i]]; - } - } - - Element unitLine; - unitLine.type = Element::Type::Line; - unitLine.vertices = {vids[0], vids[1]}; - - return unitLine; -} - -std::vector Splitter::splitSurface_( - const Element& surface, - const std::vector& coords, - const Grid& grid, - std::map& coordMap) { - std::vector quads; - - // Get the plane orientation of the surface - auto [normalAxis, plane] = getSurfacePlane_(surface, coords); - - // Get the grid cell bounds - auto [minCell, maxCell] = getSurfaceBounds_(surface, coords); - - // Determine the 2D axes in the plane - Axis axis1 = (normalAxis + 1) % 3; - Axis axis2 = (normalAxis + 2) % 3; - - // Generate unit quads for each cell in the bounds - for (CellDir i = minCell(axis1); i < maxCell(axis1); i++) { - for (CellDir j = minCell(axis2); j < maxCell(axis2); j++) { - Element quad = createUnitQuad_( - plane, normalAxis, i, j, grid, coordMap); - quads.push_back(quad); - } - } - - return quads; -} - -std::pair Splitter::getSurfaceBounds_( - const Element& surface, - const std::vector& coords) { - Cell minCell = {0, 0, 0}; - Cell maxCell = {0, 0, 0}; - - bool first = true; - for (CoordinateId vid : surface.vertices) { - Cell cell = utils::GridTools::toCell(coords[vid]); - if (first) { - minCell = cell; - maxCell = cell; - first = false; - } else { - for (Axis d = 0; d < 3; d++) { - minCell(d) = std::min(minCell(d), cell(d)); - maxCell(d) = std::max(maxCell(d), cell(d)); - } - } - } - - // maxCell represents the cell containing the max coordinate value - // For a surface spanning cells 0 to N-1, the max coordinate is at grid[N] - // So maxCell should be set to N (the cell index of the max coord), which is already correct - // The loop below will iterate i < maxCell, giving cells 0 to maxCell-1 - // No increment needed - - return {minCell, maxCell}; -} - -std::pair Splitter::getSurfacePlane_( - const Element& surface, - const std::vector& coords) { - // Find which axis has constant coordinate (the normal axis) - for (Axis d = 0; d < 3; d++) { - Cell cell0 = utils::GridTools::toCell(coords[surface.vertices[0]]); - bool allSame = true; - for (CoordinateId vid : surface.vertices) { - Cell cell = utils::GridTools::toCell(coords[vid]); - if (cell(d) != cell0(d)) { - allSame = false; - break; - } - } - if (allSame) { - return {d, cell0(d)}; - } - } - - // Fallback (should not happen for valid surfaces) - return {0, 0}; -} - -Element Splitter::createUnitQuad_( - CellDir plane, - Axis normalAxis, - CellDir xCell, - CellDir yCell, - const Grid& grid, - std::map& coordMap) { - Axis axis1 = (normalAxis + 1) % 3; - Axis axis2 = (normalAxis + 2) % 3; - - // Create 4 corner coordinates for the unit quad - std::array corners; - corners[0] = Coordinate({0, 0, 0}); - corners[1] = Coordinate({0, 0, 0}); - corners[2] = Coordinate({0, 0, 0}); - corners[3] = Coordinate({0, 0, 0}); - - // Set coordinates for each corner - corners[0](normalAxis) = grid[normalAxis][plane]; - corners[0](axis1) = grid[axis1][xCell]; - corners[0](axis2) = grid[axis2][yCell]; - - corners[1](normalAxis) = grid[normalAxis][plane]; - corners[1](axis1) = grid[axis1][xCell + 1]; - corners[1](axis2) = grid[axis2][yCell]; - - corners[2](normalAxis) = grid[normalAxis][plane]; - corners[2](axis1) = grid[axis1][xCell + 1]; - corners[2](axis2) = grid[axis2][yCell + 1]; - - corners[3](normalAxis) = grid[normalAxis][plane]; - corners[3](axis1) = grid[axis1][xCell]; - corners[3](axis2) = grid[axis2][yCell + 1]; - - // Get or create coordinate IDs - std::array vids; - for (int i = 0; i < 4; i++) { - auto it = coordMap.find(corners[i]); - if (it != coordMap.end()) { - vids[i] = it->second; - } else { - coordMap[corners[i]] = static_cast(coordMap.size()); - vids[i] = coordMap[corners[i]]; - } - } - - Element quad; - quad.type = Element::Type::Surface; - quad.vertices = {vids[0], vids[1], vids[2], vids[3]}; - - return quad; -} - -} diff --git a/src/core/Splitter.h b/src/core/Splitter.h deleted file mode 100644 index 91206ef..0000000 --- a/src/core/Splitter.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "types/Mesh.h" -#include "utils/Types.h" - -namespace meshlib::core { - -class Splitter { -public: - // Split all surfaces in mesh into unit quads (1x1 grid cells) - // Returns number of new quads created - static std::size_t splitSurfaces(Mesh& mesh); - - // Split all lines in mesh into unit grid lines - // Returns number of new unit lines created - static std::size_t splitLines(Mesh& mesh); - -private: - // Split a single surface into unit quads - static std::vector splitSurface_( - const Element& surface, - const std::vector& coords, - const Grid& grid, - std::map& coordMap); - - // Get grid cell bounds for a surface - static std::pair getSurfaceBounds_( - const Element& surface, - const std::vector& coords); - - // Determine the plane orientation of a surface (which axis is normal) - static std::pair getSurfacePlane_( - const Element& surface, - const std::vector& coords); - - // Create a unit quad at given grid cell position - static Element createUnitQuad_( - CellDir plane, - Axis normalAxis, - CellDir xCell, - CellDir yCell, - const Grid& grid, - std::map& coordMap); - - // Split a single polyline into unit grid lines - static std::vector splitLine_( - const Element& line, - const std::vector& coords, - const Grid& grid, - std::map& coordMap); - - // Get grid cell bounds for a line - static std::pair getLineBounds_( - const Element& line, - const std::vector& coords); - - // Determine the axis direction of a line - static Axis getLineAxis_( - const Element& line, - const std::vector& coords); - - // Create a unit line at given grid cell position - static Element createUnitLine_( - CellDir fixedCoord1, - CellDir fixedCoord2, - Axis lineAxis, - CellDir cell, - const Grid& grid, - std::map& coordMap); -}; - -} diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index 54c836e..c895727 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -1,6 +1,5 @@ #include #include "core/Compressor.h" -#include "core/Splitter.h" #include "MeshFixtures.h" #include "utils/MeshTools.h" @@ -152,8 +151,8 @@ TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); } -TEST_F(CompressorTest, CompressAndSplit2x2GridRoundTrip) { - // Create 4 quads in 2x2 grid, compress to 1 surface, split back to 4 quads +TEST_F(CompressorTest, Compress2x2GridRoundTrip) { + // Create 4 quads in 2x2 grid, compress to 1 surface Mesh mesh; mesh.grid = grid_; @@ -170,15 +169,10 @@ TEST_F(CompressorTest, CompressAndSplit2x2GridRoundTrip) { auto merged = core::Compressor::compressSurfaces(mesh); EXPECT_EQ(merged, 3u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); - - // Split: 1 surface -> 4 quads - auto splitCount = core::Splitter::splitSurfaces(mesh); - EXPECT_EQ(splitCount, 4u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); } -TEST_F(CompressorTest, CompressAndSplitRingRoundTrip) { - // Create ring of 8 quads, compress, split back +TEST_F(CompressorTest, CompressRingRoundTrip) { + // Create ring of 8 quads, compress Mesh mesh; mesh.grid = grid_; @@ -200,15 +194,10 @@ TEST_F(CompressorTest, CompressAndSplitRingRoundTrip) { EXPECT_EQ(merged, 4u); // 8 - 4 = 4 surfaces merged auto compressedCount = countMeshElementsIf(mesh, isQuad); EXPECT_EQ(compressedCount, 4u); - - // Split: 4 surfaces -> 8 quads - auto splitCount = core::Splitter::splitSurfaces(mesh); - EXPECT_EQ(splitCount, 8u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); } -TEST_F(CompressorTest, CompressAndSplit3x3GridRoundTrip) { - // Create 9 quads in 3x3 grid, compress to 1 surface, split back to 9 quads +TEST_F(CompressorTest, Compress3x3GridRoundTrip) { + // Create 9 quads in 3x3 grid, compress to 1 surface Mesh mesh; mesh.grid = grid_; @@ -228,11 +217,6 @@ TEST_F(CompressorTest, CompressAndSplit3x3GridRoundTrip) { auto merged = core::Compressor::compressSurfaces(mesh); EXPECT_EQ(merged, 8u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); - - // Split: 1 surface -> 9 quads - auto splitCount = core::Splitter::splitSurfaces(mesh); - EXPECT_EQ(splitCount, 9u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 9u); } // ============== Line Compression Tests ============== @@ -303,7 +287,7 @@ TEST_F(CompressorTest, Compress3LinesIntoOne) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); } -TEST_F(CompressorTest, CompressAndSplit2LineRoundTrip) { +TEST_F(CompressorTest, Compress2LineRoundTrip) { Mesh mesh; mesh.grid = grid_; @@ -315,14 +299,9 @@ TEST_F(CompressorTest, CompressAndSplit2LineRoundTrip) { core::Compressor::compressLines(mesh); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); - - auto splitCount = core::Splitter::splitLines(mesh); - - EXPECT_EQ(splitCount, 2u); - EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); } -TEST_F(CompressorTest, CompressAndSplit5LineRoundTrip) { +TEST_F(CompressorTest, Compress5LineRoundTrip) { Mesh mesh; mesh.grid = grid_; @@ -336,11 +315,6 @@ TEST_F(CompressorTest, CompressAndSplit5LineRoundTrip) { core::Compressor::compressLines(mesh); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); - - auto splitCount = core::Splitter::splitLines(mesh); - - EXPECT_EQ(splitCount, 5u); - EXPECT_EQ(countMeshElementsIf(mesh, isLine), 5u); } TEST_F(CompressorTest, CompressMixedDirections) { From c45947b7921917f204efb613bcd06c3bbc9830cc Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Wed, 15 Jul 2026 15:50:19 +0000 Subject: [PATCH 31/61] Tessellator | Testing | Update and fix compressor tests --- test/MeshFixtures.h | 79 ++++------ test/core/CompressorTest.cpp | 270 ++++++++++++++++++++++++++--------- 2 files changed, 233 insertions(+), 116 deletions(-) diff --git a/test/MeshFixtures.h b/test/MeshFixtures.h index 8707e29..65c77ad 100644 --- a/test/MeshFixtures.h +++ b/test/MeshFixtures.h @@ -1270,65 +1270,48 @@ static Mesh buildProblematicTriMesh2() } +static CoordinateId findOrAddCoord(Mesh& mesh, const std::array& gridIdx) { + double pos[3]; + pos[0] = mesh.grid[0][gridIdx[0]]; + pos[1] = mesh.grid[1][gridIdx[1]]; + pos[2] = mesh.grid[2][gridIdx[2]]; + + for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { + if (mesh.coordinates[i](0) == pos[0] && + mesh.coordinates[i](1) == pos[1] && + mesh.coordinates[i](2) == pos[2]) { + return i; + } + } + mesh.coordinates.push_back(Coordinate({pos[0], pos[1], pos[2]})); + return static_cast(mesh.coordinates.size() - 1); +} + // Helper to add a quad (as a surface with four vertices) to a mesh static void addQuad(Mesh& mesh, const std::array& v0, const std::array& v1, - const std::array& v2, const std::array& v3) { - if (mesh.groups.empty()) { - mesh.groups.emplace_back(); + const std::array& v2, const std::array& v3, GroupId groupId = 0) { + if (mesh.groups.size() <= groupId) { + mesh.groups.resize(groupId + 1); } - auto findOrAddCoord = [&](const std::array& gridIdx) { - double pos[3]; - pos[0] = mesh.grid[0][gridIdx[0]]; - pos[1] = mesh.grid[1][gridIdx[1]]; - pos[2] = mesh.grid[2][gridIdx[2]]; - - for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { - if (mesh.coordinates[i](0) == pos[0] && - mesh.coordinates[i](1) == pos[1] && - mesh.coordinates[i](2) == pos[2]) { - return i; - } - } - mesh.coordinates.push_back(Coordinate({pos[0], pos[1], pos[2]})); - return static_cast(mesh.coordinates.size() - 1); - }; - - CoordinateId c0 = findOrAddCoord(v0); - CoordinateId c1 = findOrAddCoord(v1); - CoordinateId c2 = findOrAddCoord(v2); - CoordinateId c3 = findOrAddCoord(v3); + CoordinateId c0 = findOrAddCoord(mesh, v0); + CoordinateId c1 = findOrAddCoord(mesh, v1); + CoordinateId c2 = findOrAddCoord(mesh, v2); + CoordinateId c3 = findOrAddCoord(mesh, v3); - mesh.groups[0].elements.push_back(Element({c0, c1, c2, c3}, Element::Type::Surface)); + mesh.groups[groupId].elements.push_back(Element({c0, c1, c2, c3}, Element::Type::Surface)); } // Helper to add a line (as a line with two vertices) to a mesh -static void addLine(Mesh& mesh, const std::array& v0, const std::array& v1) { - if (mesh.groups.empty()) { - mesh.groups.emplace_back(); +static void addLine(Mesh& mesh, const std::array& v0, const std::array& v1, GroupId groupId = 0) { + if (mesh.groups.size() <= groupId) { + mesh.groups.resize(groupId + 1); } - auto findOrAddCoord = [&](const std::array& gridIdx) { - double pos[3]; - pos[0] = mesh.grid[0][gridIdx[0]]; - pos[1] = mesh.grid[1][gridIdx[1]]; - pos[2] = mesh.grid[2][gridIdx[2]]; - - for (CoordinateId i = 0; i < static_cast(mesh.coordinates.size()); ++i) { - if (mesh.coordinates[i](0) == pos[0] && - mesh.coordinates[i](1) == pos[1] && - mesh.coordinates[i](2) == pos[2]) { - return i; - } - } - mesh.coordinates.push_back(Coordinate({pos[0], pos[1], pos[2]})); - return static_cast(mesh.coordinates.size() - 1); - }; - - CoordinateId c0 = findOrAddCoord(v0); - CoordinateId c1 = findOrAddCoord(v1); + CoordinateId c0 = findOrAddCoord(mesh, v0); + CoordinateId c1 = findOrAddCoord(mesh, v1); - mesh.groups[0].elements.push_back(Element({c0, c1}, Element::Type::Line)); + mesh.groups[groupId].elements.push_back(Element({c0, c1}, Element::Type::Line)); } } \ No newline at end of file diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index c895727..1aac5ef 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -52,6 +52,50 @@ TEST_F(CompressorTest, Compress2x2QuadsIntoOneSurface) { } +TEST_F(CompressorTest, CompresRepeatedQuadsIntoOneSurface) { + // Create 4 quads arranged in a 2x2 pattern on the same plane + // They should be merged into a single surface + + Mesh mesh; + mesh.grid = grid_; + + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 1u); + ASSERT_EQ(mesh.groups.size(), 1u); + ASSERT_EQ(mesh.groups[0].elements.size(), 1u); + + EXPECT_EQ( + CoordinateIds({0, 1, 2, 3}), + mesh.groups[0].elements[0].vertices + ); + +} + +TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentOrientation) { + // Create quads on different planes - should not be merged + + Mesh mesh; + mesh.grid = grid_; + + // Counter clock-wise quad + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); + // Adjacent clock-wise quad + addQuad(mesh, {1, 0, 0}, {1, 1, 0}, {2, 1, 0}, {1, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); + + auto merged = core::Compressor::compressSurfaces(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); +} + TEST_F(CompressorTest, DoesNotCompressNonCoplanarQuads) { // Create quads on different planes - should not be merged @@ -99,23 +143,26 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { // Outer ring of quads (leaving center 1,1 to 2,2 empty) // Layout: - // x=0 x=1 x=2 x=3 - // y=3 [5] [8] [6] - // y=2 [3] hole [4] - // y=1 [1] [7] [2] - // y=0 + // x=0 x=1 x=2 x=3 + // y=3 + // [5] [8] [6] + // y=2 + // [3] hole [4] + // y=1 + // [1] [7] [2] + // y=0 // Bottom row - addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); - addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0}); - // Middle row (sides only) - addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); - addQuad(mesh, {2, 1, 0}, {3, 1, 0}, {3, 2, 0}, {2, 2, 0}); - // Top row - addQuad(mesh, {0, 2, 0}, {1, 2, 0}, {1, 3, 0}, {0, 3, 0}); - addQuad(mesh, {2, 2, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0}); + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); // 0, 1, 2, 3 + addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0}); // 4, 5, 6, 7 + // Middle row (sides only) + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); // 3, 2, 8, 9 + addQuad(mesh, {2, 1, 0}, {3, 1, 0}, {3, 2, 0}, {2, 2, 0}); // 7, 6, 10, 11 + // Top row + addQuad(mesh, {0, 2, 0}, {1, 2, 0}, {1, 3, 0}, {0, 3, 0}); // 9, 8, 12, 13 + addQuad(mesh, {2, 2, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0}); // 11, 10, 14, 15 // Corners to complete the ring - addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); - addQuad(mesh, {1, 3, 0}, {2, 3, 0}, {2, 2, 0}, {1, 2, 0}); + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); // 1, 4, 7, 2 + addQuad(mesh, {1, 3, 0}, {1, 2, 0}, {2, 2, 0}, {2, 3, 0}); // 12, 8, 11, 15 EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); @@ -124,12 +171,32 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { auto finalCount = countMeshElementsIf(mesh, isQuad); // Optimal decomposition: 4 rectangles - // - Left column (quads 1,3,5): cells x=0, y=0-3 - // - Right column (quads 2,4,6): cells x=2-3, y=0-3 - // - Top center (quad 8): cell x=1-2, y=2-3 - // - Bottom center (quad 7): cell x=1-2, y=0-1 + // - Bottom row (quads 1,7,2): cells x=0-3, y=0-1 + // - Left center (quad 3): cell x=0-1, y=1-2 + // - Top row (quads 5, 8, 6): cell x=0-3, y=2-3 + // - Right center (quad 4): cell x=2-3, y=1-2 EXPECT_EQ(finalCount, 4u); EXPECT_EQ(merged, 4u); // 8 - 4 = 4 surfaces merged + + EXPECT_EQ( + CoordinateIds({0, 5, 6, 3}), + mesh.groups[0].elements[0].vertices + ); + + EXPECT_EQ( + CoordinateIds({3, 2, 8, 9}), + mesh.groups[0].elements[1].vertices + ); + + EXPECT_EQ( + CoordinateIds({9, 10, 14, 13}), + mesh.groups[0].elements[2].vertices + ); + + EXPECT_EQ( + CoordinateIds({7, 6, 10, 11}), + mesh.groups[0].elements[3].vertices + ); } TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { @@ -151,47 +218,44 @@ TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); } -TEST_F(CompressorTest, Compress2x2GridRoundTrip) { - // Create 4 quads in 2x2 grid, compress to 1 surface - - Mesh mesh; - mesh.grid = grid_; - - // 2x2 grid of quads - addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); - addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); - addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); - addQuad(mesh, {1, 1, 0}, {2, 1, 0}, {2, 2, 0}, {1, 2, 0}); - - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); - - // Compress: 4 quads -> 1 surface - auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 3u); - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); -} - TEST_F(CompressorTest, CompressRingRoundTrip) { // Create ring of 8 quads, compress Mesh mesh; mesh.grid = grid_; - // Ring of 8 quads (same as CompressWithHoleCreatesInnerContour) - addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); - addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0}); - addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); - addQuad(mesh, {2, 1, 0}, {3, 1, 0}, {3, 2, 0}, {2, 2, 0}); - addQuad(mesh, {0, 2, 0}, {1, 2, 0}, {1, 3, 0}, {0, 3, 0}); - addQuad(mesh, {2, 2, 0}, {3, 2, 0}, {3, 3, 0}, {2, 3, 0}); - addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); - addQuad(mesh, {1, 3, 0}, {2, 3, 0}, {2, 2, 0}, {1, 2, 0}); - - EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); + // Outer ring of quads (leaving center 1,1 to 2,2 empty) + // Layout: + // x=0 x=1 x=2 x=3 x=4 + // y=4 + // [10] [09] [08] [07] + // y=3 + // [11] [06] + // y=2 + // [12] [05] + // y=1 + // [01] [02] [03] [04] + // y=0 + // Bottom row - // Compress: 8 quads -> 4 surfaces (left col, right col, top center, bottom center) + addQuad(mesh, {0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}); // 01 + addQuad(mesh, {1, 0, 0}, {2, 0, 0}, {2, 1, 0}, {1, 1, 0}); // 02 + addQuad(mesh, {2, 0, 0}, {3, 0, 0}, {3, 1, 0}, {2, 1, 0}); // 03 + addQuad(mesh, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {3, 1, 0}); // 04 + addQuad(mesh, {3, 1, 0}, {4, 1, 0}, {4, 2, 0}, {3, 2, 0}); // 05 + addQuad(mesh, {3, 2, 0}, {4, 2, 0}, {4, 3, 0}, {3, 3, 0}); // 06 + addQuad(mesh, {3, 3, 0}, {4, 3, 0}, {4, 4, 0}, {3, 4, 0}); // 07 + addQuad(mesh, {2, 3, 0}, {3, 3, 0}, {3, 4, 0}, {2, 4, 0}); // 08 + addQuad(mesh, {1, 4, 0}, {1, 3, 0}, {2, 3, 0}, {2, 4, 0}); // 09 + addQuad(mesh, {0, 3, 0}, {1, 3, 0}, {1, 4, 0}, {0, 4, 0}); // 10 + addQuad(mesh, {0, 2, 0}, {1, 2, 0}, {1, 3, 0}, {0, 3, 0}); // 11 + addQuad(mesh, {0, 1, 0}, {1, 1, 0}, {1, 2, 0}, {0, 2, 0}); // 12 + + EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 12u); + + // Compress: 12 quads -> 4 surfaces () auto merged = core::Compressor::compressSurfaces(mesh); - EXPECT_EQ(merged, 4u); // 8 - 4 = 4 surfaces merged + EXPECT_EQ(merged, 8u); // 12 - 4 = 4 surfaces merged auto compressedCount = countMeshElementsIf(mesh, isQuad); EXPECT_EQ(compressedCount, 4u); } @@ -203,6 +267,16 @@ TEST_F(CompressorTest, Compress3x3GridRoundTrip) { mesh.grid = grid_; // 3x3 grid of quads + // Layout: + // x=0 x=1 x=2 x=3 + // y=3 + // [3] [6] [9] + // y=2 + // [2] [5] [8] + // y=1 + // [1] [4] [7] + // y=0 + // for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { addQuad(mesh, @@ -217,6 +291,11 @@ TEST_F(CompressorTest, Compress3x3GridRoundTrip) { auto merged = core::Compressor::compressSurfaces(mesh); EXPECT_EQ(merged, 8u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); + + EXPECT_EQ( + CoordinateIds({0, 12, 15, 7}), + mesh.groups[0].elements[0].vertices + ); } // ============== Line Compression Tests ============== @@ -238,7 +317,27 @@ TEST_F(CompressorTest, Compress2CollinearLinesIntoOne) { ASSERT_EQ(mesh.groups[0].elements.size(), 1u); const auto& line = mesh.groups[0].elements[0]; EXPECT_EQ(line.type, Element::Type::Line); - EXPECT_EQ(line.vertices.size(), 2u); + EXPECT_EQ(CoordinateIds({0, 2}), line.vertices); +} + +TEST_F(CompressorTest, CompressRepeatedLinesIntoOne) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + addLine(mesh, {0, 0, 0}, {1, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 1u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + + ASSERT_EQ(mesh.groups[0].elements.size(), 1u); + const auto& line = mesh.groups[0].elements[0]; + EXPECT_EQ(line.type, Element::Type::Line); + EXPECT_EQ(CoordinateIds({0, 1}), line.vertices); } TEST_F(CompressorTest, DoesNotCompressNonCollinearLines) { @@ -271,56 +370,87 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedLines) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); } -TEST_F(CompressorTest, Compress3LinesIntoOne) { +TEST_F(CompressorTest, DoesNotCompressOverlappingOppositeDirectionLines) { Mesh mesh; mesh.grid = grid_; addLine(mesh, {0, 0, 0}, {1, 0, 0}); - addLine(mesh, {1, 0, 0}, {2, 0, 0}); - addLine(mesh, {2, 0, 0}, {3, 0, 0}); + addLine(mesh, {1, 0, 0}, {0, 0, 0}); - EXPECT_EQ(countMeshElementsIf(mesh, isLine), 3u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); auto merged = core::Compressor::compressLines(mesh); - EXPECT_EQ(merged, 2u); - EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); } -TEST_F(CompressorTest, Compress2LineRoundTrip) { +TEST_F(CompressorTest, DoesNotCompressConnectedOppositeDirectionLines) { Mesh mesh; mesh.grid = grid_; addLine(mesh, {0, 0, 0}, {1, 0, 0}); - addLine(mesh, {1, 0, 0}, {2, 0, 0}); + addLine(mesh, {2, 0, 0}, {1, 0, 0}); - auto originalLines = countMeshElementsIf(mesh, isLine); - EXPECT_EQ(originalLines, 2u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); - core::Compressor::compressLines(mesh); - EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 0u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); } -TEST_F(CompressorTest, Compress5LineRoundTrip) { +TEST_F(CompressorTest, Compress3LinesIntoOne) { Mesh mesh; mesh.grid = grid_; addLine(mesh, {0, 0, 0}, {1, 0, 0}); addLine(mesh, {1, 0, 0}, {2, 0, 0}); addLine(mesh, {2, 0, 0}, {3, 0, 0}); - addLine(mesh, {3, 0, 0}, {4, 0, 0}); - addLine(mesh, {4, 0, 0}, {5, 0, 0}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 3u); + + auto merged = core::Compressor::compressLines(mesh); + + EXPECT_EQ(merged, 2u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + EXPECT_EQ(CoordinateIds({0, 3}), mesh.groups[0].elements[0].vertices); +} + +TEST_F(CompressorTest, Compress5LineRoundTrip) { + Mesh mesh; + mesh.grid = grid_; + + addLine(mesh, {0, 0, 0}, {0, 1, 0}); + addLine(mesh, {0, 1, 0}, {0, 2, 0}); + addLine(mesh, {0, 2, 0}, {0, 3, 0}); + addLine(mesh, {0, 3, 0}, {0, 4, 0}); + addLine(mesh, {0, 4, 0}, {0, 5, 0}); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 5u); core::Compressor::compressLines(mesh); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); + EXPECT_EQ(CoordinateIds({0, 5}), mesh.groups[0].elements[0].vertices); } TEST_F(CompressorTest, CompressMixedDirections) { Mesh mesh; mesh.grid = grid_; + // Layout: + // z + // v6 + // |l6 + // v5 + // |l5 + // v0———v1———v2 x + // l3 / l1 l2 + // v3 + // l4 / + // v4 + // y + addLine(mesh, {0, 0, 0}, {1, 0, 0}); addLine(mesh, {1, 0, 0}, {2, 0, 0}); addLine(mesh, {0, 0, 0}, {0, 1, 0}); @@ -334,6 +464,10 @@ TEST_F(CompressorTest, CompressMixedDirections) { EXPECT_EQ(merged, 3u); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 3u); + + EXPECT_EQ(CoordinateIds({0, 2}), mesh.groups[0].elements[0].vertices); + EXPECT_EQ(CoordinateIds({0, 4}), mesh.groups[0].elements[1].vertices); + EXPECT_EQ(CoordinateIds({0, 6}), mesh.groups[0].elements[2].vertices); } } From f5103325ba2e70ff896d4fea208a4da7aa5d4a59 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Fri, 17 Jul 2026 08:46:10 +0000 Subject: [PATCH 32/61] Tessellator | Core | Refactor Compressor for better readability --- src/core/Compressor.cpp | 515 ++++++++++++++++---------------- src/core/Compressor.h | 28 +- src/meshers/StaircaseMesher.cpp | 4 +- src/utils/Types.h | 3 + test/core/CompressorTest.cpp | 36 +-- 5 files changed, 291 insertions(+), 295 deletions(-) diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp index 71c0f33..f1dbf8d 100644 --- a/src/core/Compressor.cpp +++ b/src/core/Compressor.cpp @@ -3,12 +3,13 @@ #include #include -#include "utils/Geometry.h" +#include "types/Mesh.h" #include "utils/GridTools.h" namespace meshlib::core { using meshlib::Sign; +using meshlib::SignedAxis; using meshlib::PlanePoint; using meshlib::PlaneLinel; using meshlib::PlaneSurfel; @@ -16,58 +17,54 @@ using meshlib::PlaneSurface; using meshlib::Contour; using meshlib::CrossLine; -std::size_t Compressor::compressSurfaces(Mesh& mesh) { +std::size_t Compressor::compressSurfacesInMesh(Mesh& mesh) { std::size_t totalOriginal = 0; std::size_t totalCompressed = 0; - for (GroupId g = 0; g < mesh.groups.size(); g++) { - std::vector surfs; - std::vector surfIndices; + for (Group& group : mesh.groups) { + std::vector surfaces; - for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { - const Element& elem = mesh.groups[g].elements[e]; + for (const Element& elem : group.elements) { if (elem.type == Element::Type::Surface) { - surfIndices.push_back(e); - surfs.push_back(elem); + surfaces.push_back(elem); } } - if (surfs.empty()) { + if (surfaces.empty()) { continue; } - totalOriginal += surfs.size(); - std::vector compressedSurfs = compressSurfs_(mesh.coordinates, surfs); - totalCompressed += compressedSurfs.size(); + totalOriginal += surfaces.size(); + std::vector compressedSurfaces = compressSurfaces_(mesh.coordinates, surfaces); + totalCompressed += compressedSurfaces.size(); // Build new elements vector with compressed surfaces std::vector newElements; - ElementId surfIdx = 0; - for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { - if (mesh.groups[g].elements[e].type == Element::Type::Surface) { - if (surfIdx < compressedSurfs.size()) { - newElements.push_back(compressedSurfs[surfIdx]); - surfIdx++; + ElementId surfaceIdx = 0; + for (ElementId e = 0; e < group.elements.size(); e++) { + if (group.elements[e].type == Element::Type::Surface) { + if (surfaceIdx < compressedSurfaces.size()) { + newElements.push_back(compressedSurfaces[surfaceIdx]); + surfaceIdx++; } } else { - newElements.push_back(mesh.groups[g].elements[e]); + newElements.push_back(group.elements[e]); } } - mesh.groups[g].elements = std::move(newElements); + group.elements = std::move(newElements); } return totalOriginal - totalCompressed; } -std::size_t Compressor::compressLines(Mesh& mesh) { +std::size_t Compressor::compressLinesInMesh(Mesh& mesh) { std::size_t totalOriginal = 0; std::size_t totalCompressed = 0; - for (GroupId g = 0; g < mesh.groups.size(); g++) { + for (Group& group : mesh.groups) { std::vector lines; - for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { - const Element& elem = mesh.groups[g].elements[e]; + for (const Element& elem : group.elements) { if (elem.type == Element::Type::Line) { lines.push_back(elem); } @@ -84,42 +81,41 @@ std::size_t Compressor::compressLines(Mesh& mesh) { // Build new elements vector with compressed lines std::vector newElements; ElementId lineIdx = 0; - for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { - if (mesh.groups[g].elements[e].type == Element::Type::Line) { + for (ElementId e = 0; e < group.elements.size(); e++) { + if (group.elements[e].type == Element::Type::Line) { if (lineIdx < compressedLines.size()) { newElements.push_back(compressedLines[lineIdx]); lineIdx++; } } else { - newElements.push_back(mesh.groups[g].elements[e]); + newElements.push_back(group.elements[e]); } } - mesh.groups[g].elements = std::move(newElements); + group.elements = std::move(newElements); } return totalOriginal - totalCompressed; } std::vector Compressor::compressLines_( - const std::vector& coords, + const std::vector& coords, const std::vector& lines) { - std::vector res; - std::map, - std::pair>, + std::vector result; + std::map, std::vector> signDirLines; for (std::size_t l = 0; l < lines.size(); l++) { std::array auxCells; auxCells[0] = utils::GridTools::toCell(coords[lines[l].vertices[0]]); auxCells[1] = utils::GridTools::toCell(coords[lines[l].vertices[1]]); - std::array gridLine; + GridLine gridLine; Sign sign = 1; - Axis dir = 0; - for (Axis d = 0; d < 3; d++) { + Axis direction = 0; + for (Axis d = X; X <= Z; d++) { if (auxCells[0](d) != auxCells[1](d)) { if (auxCells[0](d) > auxCells[1](d)) { sign = -1; } - dir = d; + direction = d; Axis d1 = (d + 1) % 3; Axis d2 = (d + 2) % 3; gridLine[0] = auxCells[0](d1); @@ -128,50 +124,49 @@ std::vector Compressor::compressLines_( } } signDirLines[std::make_pair(gridLine, - std::make_pair(sign, dir))].push_back(l); + std::make_pair(sign, direction))].push_back(l); } - for (std::map, - std::pair>, + for (std::map, std::vector>::const_iterator it = signDirLines.begin(); it != signDirLines.end(); ++it) { std::vector auxElems; for (std::size_t i = 0; i < it->second.size(); i++) { auxElems.push_back(lines[it->second[i]]); } - std::vector auxRes = + std::vector compressedLines = compressDirSignLines_(coords, it->first.second, auxElems); - res.insert(res.end(), auxRes.begin(), auxRes.end()); + result.insert(result.end(), compressedLines.begin(), compressedLines.end()); } - return res; + return result; } std::vector Compressor::compressDirSignLines_( - const std::vector& coords, - const std::pair& signDir, + const std::vector& coords, + const SignedAxis& signedDir, const std::vector& lines) { - std::vector res; - std::map> coordLines; - std::map> lineCoords; + std::vector result; + std::map> relativeLines; + std::map> lineRelatives; for (std::size_t l = 0; l < lines.size(); l++) { - for (std::size_t v = 0; v < 2; v++) { - coordLines[lines[l].vertices[v]].insert(l); - lineCoords[l].insert(lines[l].vertices[v]); + for (RelativeId vertex : lines[l].vertices) { + relativeLines[vertex].insert(l); + lineRelatives[l].insert(vertex); } } - std::set vis; - for (std::map>::const_iterator - itExt = lineCoords.begin(); itExt != lineCoords.end(); ++itExt) { - if (vis.count(itExt->first) == 0) { - CoordinateId minCell = lines[itExt->first].vertices[0]; - CoordinateId maxCell = lines[itExt->first].vertices[1]; - std::queue q; - q.push(itExt->first); - vis.insert(itExt->first); - while (!q.empty()) { - ElementId elem = q.front(); - q.pop(); + std::set visitedLineIds; + for (std::map>::const_iterator + itExt = lineRelatives.begin(); itExt != lineRelatives.end(); ++itExt) { + if (visitedLineIds.count(itExt->first) == 0) { + RelativeId minCell = lines[itExt->first].vertices[0]; + RelativeId maxCell = lines[itExt->first].vertices[1]; + std::queue linesToVisit; + linesToVisit.push(itExt->first); + visitedLineIds.insert(itExt->first); + while (!linesToVisit.empty()) { + ElementId elem = linesToVisit.front(); + linesToVisit.pop(); for (std::size_t i = 0; i < 2; i++) { if (coords[minCell] > coords[lines[elem].vertices[i]]) { minCell = lines[elem].vertices[i]; @@ -180,15 +175,15 @@ std::vector Compressor::compressDirSignLines_( maxCell = lines[elem].vertices[i]; } } - for (std::set::const_iterator - itCell = lineCoords[elem].begin(); - itCell != lineCoords[elem].end(); ++itCell) { + for (std::set::const_iterator + itCell = lineRelatives[elem].begin(); + itCell != lineRelatives[elem].end(); ++itCell) { for (std::set::const_iterator - itLine = coordLines[*itCell].begin(); - itLine != coordLines[*itCell].end(); ++itLine) { - if (vis.count(*itLine) == 0) { - q.push(*itLine); - vis.insert(*itLine); + itLine = relativeLines[*itCell].begin(); + itLine != relativeLines[*itCell].end(); ++itLine) { + if (visitedLineIds.count(*itLine) == 0) { + linesToVisit.push(*itLine); + visitedLineIds.insert(*itLine); } } } @@ -197,34 +192,35 @@ std::vector Compressor::compressDirSignLines_( newElem.type = Element::Type::Line; newElem.vertices.push_back(minCell); newElem.vertices.push_back(maxCell); - if (signDir.first < 0) { + if (signedDir.first < 0) { std::swap(newElem.vertices[0], newElem.vertices[1]); } - res.push_back(newElem); + result.push_back(newElem); } } - return res; + return result; } -std::vector Compressor::compressSurfs_( - std::vector& coords, - const std::vector& surfs) { - std::vector res; - std::map>, - std::vector> signDirSurfs; - for (std::size_t s = 0; s < surfs.size(); s++) { - if (surfs[s].vertices.size() != 4) { - res.push_back(surfs[s]); +std::vector Compressor::compressSurfaces_( + std::vector& coords, + const std::vector& surfaces) { + + std::vector result; + std::map, std::vector> signDirSurfs; + + for (std::size_t s = 0; s < surfaces.size(); s++) { + if (surfaces[s].vertices.size() != 4) { + result.push_back(surfaces[s]); continue; } std::array auxCells; - auxCells[0] = utils::GridTools::toCell(coords[surfs[s].vertices[0]]); - auxCells[1] = utils::GridTools::toCell(coords[surfs[s].vertices[1]]); - auxCells[2] = utils::GridTools::toCell(coords[surfs[s].vertices[2]]); - CellDir gridSurf; + auxCells[0] = utils::GridTools::toCell(coords[surfaces[s].vertices[0]]); + auxCells[1] = utils::GridTools::toCell(coords[surfaces[s].vertices[1]]); + auxCells[2] = utils::GridTools::toCell(coords[surfaces[s].vertices[2]]); + CellDir gridSurface; Sign sign = 1; - Axis dir = 0; - for (Axis d = 0; d < 3; d++) { + Axis direction = 0; + for (Axis d = X; d <= Z; d++) { if (auxCells[0](d) == auxCells[2](d)) { Cell normal = (auxCells[1] - auxCells[0]) ^ (auxCells[2] - auxCells[0]); @@ -233,109 +229,107 @@ std::vector Compressor::compressSurfs_( } else { sign = -1; } - dir = d; - gridSurf = auxCells[0](d); + direction = d; + gridSurface = auxCells[0](d); break; } } - signDirSurfs[std::make_pair(gridSurf, - std::make_pair(sign, dir))].push_back(s); + signDirSurfs[std::make_pair(gridSurface, + std::make_pair(sign, direction))].push_back(s); } - for (std::map>, + for (std::map, std::vector>::const_iterator it = signDirSurfs.begin(); it != signDirSurfs.end(); ++it) { std::vector auxElems; for (std::size_t i = 0; i < it->second.size(); i++) { - auxElems.push_back(surfs[it->second[i]]); + auxElems.push_back(surfaces[it->second[i]]); } - std::vector auxRes = - compressDirSignSurfs_(coords, - it->first.second, - auxElems); - res.insert(res.end(), auxRes.begin(), auxRes.end()); + std::vector compressedSurfaces = + compressSurfacesWithSameNormal_(coords, it->first.second, auxElems); + result.insert(result.end(), compressedSurfaces.begin(), compressedSurfaces.end()); } - return res; + return result; } -std::vector Compressor::compressDirSignSurfs_( - std::vector& coords, - const std::pair& signDir, - const std::vector& surfs) { - std::vector res; - std::map> lineSurfs; - std::map> surfLines; - for (std::size_t s = 0; s < surfs.size(); s++) { +std::vector Compressor::compressSurfacesWithSameNormal_( + std::vector& coords, + const SignedAxis& signedDir, + const std::vector& surfaces) { + std::vector result; + std::map> edgeSurfaces; + std::map> surfaceEdges; + for (ElementId s = 0; s < surfaces.size(); s++) { for (std::size_t i = 0; i < 4; i++) { std::size_t j = (i + 1) % 4; - LinIds line; - line[0] = surfs[s].vertices[i]; - line[1] = surfs[s].vertices[j]; - std::sort(line.begin(), line.end()); - lineSurfs[line].insert(s); - surfLines[s].insert(line); + LinIds edge; + edge[0] = surfaces[s].vertices[i]; + edge[1] = surfaces[s].vertices[j]; + std::sort(edge.begin(), edge.end()); + edgeSurfaces[edge].insert(s); + surfaceEdges[s].insert(edge); } } - std::set vis; + std::set visitedSurfaceIds; for (std::map>::const_iterator - itExt = surfLines.begin(); itExt != surfLines.end(); ++itExt) { - if (vis.count(itExt->first) == 0) { - std::set surfsConn; - std::queue q; - q.push(itExt->first); - vis.insert(itExt->first); - while (!q.empty()) { - ElementId elem = q.front(); - q.pop(); - surfsConn.insert(elem); + itExt = surfaceEdges.begin(); itExt != surfaceEdges.end(); ++itExt) { + if (visitedSurfaceIds.count(itExt->first) == 0) { + std::set connectedSurfaceIds; + std::queue surfacesToVisit; + surfacesToVisit.push(itExt->first); + visitedSurfaceIds.insert(itExt->first); + while (!surfacesToVisit.empty()) { + ElementId e = surfacesToVisit.front(); + surfacesToVisit.pop(); + connectedSurfaceIds.insert(e); for (std::set::const_iterator - itLine = surfLines[elem].begin(); - itLine != surfLines[elem].end(); ++itLine) { + itLine = surfaceEdges[e].begin(); + itLine != surfaceEdges[e].end(); ++itLine) { for (std::set::const_iterator - itSurf = lineSurfs[*itLine].begin(); - itSurf != lineSurfs[*itLine].end(); ++itSurf) { - if (vis.count(*itSurf) == 0) { - q.push(*itSurf); - vis.insert(*itSurf); + itSurf = edgeSurfaces[*itLine].begin(); + itSurf != edgeSurfaces[*itLine].end(); ++itSurf) { + if (visitedSurfaceIds.count(*itSurf) == 0) { + surfacesToVisit.push(*itSurf); + visitedSurfaceIds.insert(*itSurf); } } } } - std::vector resCon; + std::vector connectedSurfaces; for (std::set::const_iterator - it = surfsConn.begin(); it != surfsConn.end(); ++it) { - resCon.push_back(surfs[*it]); + it = connectedSurfaceIds.begin(); it != connectedSurfaceIds.end(); ++it) { + connectedSurfaces.push_back(surfaces[*it]); } - resCon = compressSurf_(coords, signDir, resCon); - res.insert(res.end(), resCon.begin(), resCon.end()); + std::vector compressedSurfaces = compressConnectedSurfaces_(coords, signedDir, connectedSurfaces); + result.insert(result.end(), compressedSurfaces.begin(), compressedSurfaces.end()); } } - return res; + return result; } -std::vector Compressor::compressSurf_( - std::vector& coords, - const std::pair& signDir, +std::vector Compressor::compressConnectedSurfaces_( + std::vector& coords, + const SignedAxis& signDir, const std::vector& surfs) { - std::vector res; + std::vector result; Axis d = signDir.second; Axis d1 = (d + 1) % 3; Axis d2 = (d + 2) % 3; CellDir plane = utils::GridTools::toCell(coords[surfs[0].vertices[0]])(d); std::set surfels; for (std::size_t s = 0; s < surfs.size(); s++) { - std::pair ext; - ext.first[0] = + std::pair fullSurfacePoints; + fullSurfacePoints.first[0] = utils::GridTools::toCell(coords[surfs[s].vertices[0]])(d1); - ext.first[1] = + fullSurfacePoints.first[1] = utils::GridTools::toCell(coords[surfs[s].vertices[0]])(d2); - ext.second[0] = + fullSurfacePoints.second[0] = utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d1); - ext.second[1] = + fullSurfacePoints.second[1] = utils::GridTools::toCell(coords[surfs[s].vertices[2]])(d2); - CellDir i0 = std::min(ext.first[0], ext.second[0]); - CellDir i1 = std::max(ext.first[0], ext.second[0]); - CellDir j0 = std::min(ext.first[1], ext.second[1]); - CellDir j1 = std::max(ext.first[1], ext.second[1]); + CellDir i0 = std::min(fullSurfacePoints.first[0], fullSurfacePoints.second[0]); + CellDir i1 = std::max(fullSurfacePoints.first[0], fullSurfacePoints.second[0]); + CellDir j0 = std::min(fullSurfacePoints.first[1], fullSurfacePoints.second[1]); + CellDir j1 = std::max(fullSurfacePoints.first[1], fullSurfacePoints.second[1]); for (CellDir i = i0; i < i1; i++) { for (CellDir j = j0; j < j1; j++) { PlaneSurfel surfel = {{i, j}}; @@ -343,98 +337,97 @@ std::vector Compressor::compressSurf_( } } } - std::vector aux = compressSurfels_(surfels); + std::vector maximalRectangles = compressSurfelsIntoMaximalRectangles_(surfels); CoordinateMap coordMap; for (std::size_t s = 0; s < surfs.size(); s++) { for (std::size_t i = 0; i < 4; i++) { - CoordinateId coordId = surfs[s].vertices[i]; - Coordinate coord = coords[coordId]; + RelativeId coordId = surfs[s].vertices[i]; + Relative coord = coords[coordId]; coordMap[coord] = coordId; } } - for (std::size_t e = 0; e < aux.size(); e++) { - std::array ext; - ext[0](d) = ext[2](d) = plane; - ext[0](d1) = aux[e].first[0]; - ext[0](d2) = aux[e].first[1]; - ext[2](d1) = aux[e].second[0]; - ext[2](d2) = aux[e].second[1]; - ext[1] = ext[3] = ext[0]; - ext[1](d1) = ext[2](d1); - ext[3](d2) = ext[2](d2); + for (std::size_t e = 0; e < maximalRectangles.size(); e++) { + std::array corners; + corners[0](d) = corners[2](d) = plane; + corners[0](d1) = maximalRectangles[e].first[0]; + corners[0](d2) = maximalRectangles[e].first[1]; + corners[2](d1) = maximalRectangles[e].second[0]; + corners[2](d2) = maximalRectangles[e].second[1]; + corners[1] = corners[3] = corners[0]; + corners[1](d1) = corners[2](d1); + corners[3](d2) = corners[2](d2); if (signDir.first < 0) { - std::swap(ext[1], ext[3]); + std::swap(corners[1], corners[3]); } - Element resElem; - resElem.type = Element::Type::Surface; + Element newSurface; + newSurface.type = Element::Type::Surface; for (std::size_t i = 0; i < 4; i++) { - Relative rel = utils::GridTools::toRelative(ext[i]); + Relative rel = utils::GridTools::toRelative(corners[i]); if (coordMap.count(rel) == 0) { coordMap[rel] = coords.size(); coords.push_back(rel); } - resElem.vertices.push_back(coordMap[rel]); + newSurface.vertices.push_back(coordMap[rel]); } - res.push_back(resElem); + result.push_back(newSurface); } - return res; + return result; } -std::vector Compressor::compressSurfels_( - const std::set& surfs) { - std::vector res; - const std::vector& conts = getContours_(surfs); - std::array, 2> cross = - getCrossingLines_(surfs, conts); - cross = getMaxCompatLines_(cross); +std::vector Compressor::compressSurfelsIntoMaximalRectangles_( + const std::set& surfels) { + std::vector result; + const std::vector& contours = getContours_(surfels); + std::array, 2> crossingLines = getCrossingLines_(surfels, contours); + crossingLines = getMaxCompactedLines_(crossingLines); std::set linels; - for (std::size_t c = 0; c < conts.size(); c++) { - for (std::size_t i = 0; i < conts[c].size(); i++) { - std::size_t j = (i + 1) % conts[c].size(); - std::set aux = getLinels_(conts[c][i], conts[c][j]); + for (std::size_t c = 0; c < contours.size(); c++) { + for (std::size_t i = 0; i < contours[c].size(); i++) { + std::size_t j = (i + 1) % contours[c].size(); + std::set aux = getLinelsBetween_(contours[c][i], contours[c][j]); linels.insert(aux.begin(), aux.end()); } } for (Axis d = 0; d < 2; d++) { Axis d1 = (d + 1) % 2; - for (std::size_t i = 0; i < cross[d].size(); i++) { + for (std::size_t i = 0; i < crossingLines[d].size(); i++) { PlanePoint ini, end; - ini[d1] = end[d1] = cross[d][i].first; - ini[d] = cross[d][i].second.first; - end[d] = cross[d][i].second.second; - std::set aux = getLinels_(ini, end); + ini[d1] = end[d1] = crossingLines[d][i].first; + ini[d] = crossingLines[d][i].second.first; + end[d] = crossingLines[d][i].second.second; + std::set aux = getLinelsBetween_(ini, end); linels.insert(aux.begin(), aux.end()); } } - addConcaveLinels_(surfs, conts, linels); - std::map> lineSurfs; - std::map> surfLines; + addConcaveLinels_(surfels, contours, linels); + std::map> edgeSurfels; + std::map> surfelEdges; for (std::set::const_iterator - it = surfs.begin(); it != surfs.end(); ++it) { - surfLines.insert(std::make_pair(*it, std::set())); + it = surfels.begin(); it != surfels.end(); ++it) { + surfelEdges.insert(std::make_pair(*it, std::set())); for (Axis d = 0; d < 2; d++) { for (CellDir diff = -1; diff <= 1; diff += 2) { PlaneLinel linel = getSurfaceEdge_(*it, diff, d); if (linels.count(linel) == 0) { - surfLines[*it].insert(linel); - lineSurfs[linel].insert(*it); + surfelEdges[*it].insert(linel); + edgeSurfels[linel].insert(*it); } } } } - std::set vis; + std::set visitedSurfels; for (std::map>::const_iterator - itSurfExt = surfLines.begin(); - itSurfExt != surfLines.end(); ++itSurfExt) { - if (vis.count(itSurfExt->first) == 0) { - std::queue q; - q.push(itSurfExt->first); - vis.insert(itSurfExt->first); + itSurfExt = surfelEdges.begin(); + itSurfExt != surfelEdges.end(); ++itSurfExt) { + if (visitedSurfels.count(itSurfExt->first) == 0) { + std::queue surfelsToVisit; + surfelsToVisit.push(itSurfExt->first); + visitedSurfels.insert(itSurfExt->first); PlanePoint minPoint = itSurfExt->first; PlanePoint maxPoint = itSurfExt->first; - while (!q.empty()) { - PlaneSurfel surfel = q.front(); - q.pop(); + while (!surfelsToVisit.empty()) { + PlaneSurfel surfel = surfelsToVisit.front(); + surfelsToVisit.pop(); if (surfel < minPoint) { minPoint = surfel; } @@ -442,37 +435,39 @@ std::vector Compressor::compressSurfels_( maxPoint = surfel; } for (std::set::const_iterator - itLin = surfLines[surfel].begin(); - itLin != surfLines[surfel].end(); ++itLin) { + itLin = surfelEdges[surfel].begin(); + itLin != surfelEdges[surfel].end(); ++itLin) { for (std::set::const_iterator - itSurfInt = lineSurfs[*itLin].begin(); - itSurfInt != lineSurfs[*itLin].end(); ++itSurfInt) { - if (vis.count(*itSurfInt) == 0) { - q.push(*itSurfInt); - vis.insert(*itSurfInt); + itSurfInt = edgeSurfels[*itLin].begin(); + itSurfInt != edgeSurfels[*itLin].end(); ++itSurfInt) { + if (visitedSurfels.count(*itSurfInt) == 0) { + surfelsToVisit.push(*itSurfInt); + visitedSurfels.insert(*itSurfInt); } } } } maxPoint[0]++; maxPoint[1]++; - res.push_back(std::make_pair(minPoint, maxPoint)); + result.push_back(std::make_pair(minPoint, maxPoint)); } } - return res; + return result; } -std::vector Compressor::getContours_( - const std::set& surfs) { - std::vector res; - if (surfs.empty()) { - return res; +std::vector Compressor::getContours_(const std::set& surfels) { + std::vector result; + if (surfels.empty()) { + return result; } - std::set vis; - res.push_back( - getContour_(getSurfaceEdge_(*surfs.begin(), -1, 0), surfs, vis)); + std::set visitedEdges; + result.push_back(getContourFromStartingEdge_( + getSurfaceEdge_(*surfels.begin(), -1, 0), + surfels, + visitedEdges + )); for (std::set::const_iterator - it = surfs.begin(); it != surfs.end(); ++it) { + it = surfels.begin(); it != surfels.end(); ++it) { for (Axis d = 0; d < 2; d++) { for (CellDir diff = -1; diff <= 1; diff += 2) { PlaneSurfel adjSurf; @@ -480,28 +475,28 @@ std::vector Compressor::getContours_( adjSurf = *it; adjSurf[d] += diff; adjEdge = getSurfaceEdge_(*it, diff, d); - if ((surfs.find(adjSurf) == surfs.end()) && - (vis.find(adjEdge) == vis.end())) { - res.push_back(getContour_(adjEdge, surfs, vis)); + if ((surfels.find(adjSurf) == surfels.end()) && + (visitedEdges.find(adjEdge) == visitedEdges.end())) { + result.push_back(getContourFromStartingEdge_(adjEdge, surfels, visitedEdges)); } } } } - return res; + return result; } -Contour Compressor::getContour_( +Contour Compressor::getContourFromStartingEdge_( const PlaneLinel& from, const std::set& surfs, - std::set& vis) { - Contour res; + std::set& visitedEdges) { + Contour result; std::queue q; - if (vis.find(from) != vis.end()) { - return res; + if (visitedEdges.find(from) != visitedEdges.end()) { + return result; } std::vector lines; q.push(from); - vis.insert(from); + visitedEdges.insert(from); lines.push_back(from); while (!q.empty()) { PlaneLinel edge = q.front(); @@ -518,9 +513,9 @@ Contour Compressor::getContour_( adjSurf1[d0] += diff; if (surfs.find(adjSurf1) == surfs.end()) { PlaneLinel adjEdge = getSurfaceEdge_(surf, diff, d0); - if (vis.find(adjEdge) == vis.end()) { + if (visitedEdges.find(adjEdge) == visitedEdges.end()) { q.push(adjEdge); - vis.insert(adjEdge); + visitedEdges.insert(adjEdge); lines.push_back(adjEdge); break; } @@ -534,18 +529,18 @@ Contour Compressor::getContour_( if (surfs.find(adjSurf1) == surfs.end()) { PlaneLinel adjEdge = edge; adjEdge.first[d0] += diff; - if (vis.find(adjEdge) == vis.end()) { + if (visitedEdges.find(adjEdge) == visitedEdges.end()) { q.push(adjEdge); - vis.insert(adjEdge); + visitedEdges.insert(adjEdge); lines.push_back(adjEdge); break; } continue; } else { PlaneLinel adjEdge = getSurfaceEdge_(adjSurf1, -diff, d0); - if (vis.find(adjEdge) == vis.end()) { + if (visitedEdges.find(adjEdge) == visitedEdges.end()) { q.push(adjEdge); - vis.insert(adjEdge); + visitedEdges.insert(adjEdge); lines.push_back(adjEdge); break; } @@ -568,31 +563,29 @@ Contour Compressor::getContour_( extremesP[1][itPlus->second]++; for (std::size_t p = 0; p < 4; p++) { if (extremes[p / 2] == extremesP[p % 2]) { - res.push_back(extremes[p / 2]); + result.push_back(extremes[p / 2]); break; } } } - return res; + return result; } -PlaneLinel Compressor::getSurfaceEdge_(const PlaneSurfel& surf, - const CellDir& diff, - const Axis& dir) { - PlaneLinel res; - res.first = surf; - res.second = (dir + 1) % 2; +PlaneLinel Compressor::getSurfaceEdge_(const PlaneSurfel& surfel, const CellDir& diff, const Axis& dir) { + PlaneLinel result; + result.first = surfel; + result.second = (dir + 1) % 2; if (diff > 0) { - res.first[dir]++; + result.first[dir]++; } - return res; + return result; } std::array, 2> Compressor::getCrossingLines_( const std::set& surfs, const std::vector& conts) { - std::array, 2> res; + std::array, 2> result; for (Axis d = 0; d < 2; d++) { Axis d1 = (d + 1) % 2; std::map> cross; @@ -627,32 +620,32 @@ std::array, 2> } } if (valid) { - res[d].push_back( + result[d].push_back( std::make_pair(itMap->first, std::make_pair(*itSet, *itSetPlus))); } } } } - return res; + return result; } std::array, 2> - Compressor::getMaxCompatLines_( + Compressor::getMaxCompactedLines_( const std::array, 2>& cross) { - std::array, 2> res; + std::array, 2> result; if (cross[0].size() > cross[1].size()) { - res[0] = cross[0]; + result[0] = cross[0]; } else { - res[1] = cross[1]; + result[1] = cross[1]; } - return res; + return result; } -std::set Compressor::getLinels_( +std::set Compressor::getLinelsBetween_( const PlanePoint& ini, const PlanePoint& end) { - std::set res; + std::set result; for (Axis d = 0; d < 2; d++) { Axis d1 = (d + 1) % 2; if (ini[d] == end[d]) { @@ -663,11 +656,11 @@ std::set Compressor::getLinels_( k = std::min(ini[d1], end[d1]); k < std::max(ini[d1], end[d1]); k++) { linel.first[d1] = k; - res.insert(linel); + result.insert(linel); } } } - return res; + return result; } void Compressor::addConcaveLinels_(const std::set& surfs, diff --git a/src/core/Compressor.h b/src/core/Compressor.h index 5b66e32..7b0fe32 100644 --- a/src/core/Compressor.h +++ b/src/core/Compressor.h @@ -12,50 +12,50 @@ class Compressor { public: // Compress only quad surfaces (4 vertices) that are coplanar and adjacent // Returns number of surfaces merged (original_count - compressed_count) - static std::size_t compressSurfaces(Mesh& mesh); + static std::size_t compressSurfacesInMesh(Mesh& mesh); // Compress collinear line segments that are adjacent // Returns number of lines merged (original_count - compressed_count) - static std::size_t compressLines(Mesh& mesh); + static std::size_t compressLinesInMesh(Mesh& mesh); private: // Group surfaces by (grid_plane, sign, axis) and compress each group - static std::vector compressSurfs_( - std::vector& coords, + static std::vector compressSurfaces_( + std::vector& coords, const std::vector& surfs); // Compress surfaces with same normal direction and sign - static std::vector compressDirSignSurfs_( - std::vector& coords, + static std::vector compressSurfacesWithSameNormal_( + std::vector& coords, const std::pair& signDir, const std::vector& surfs); // Compress connected coplanar surfaces using contour detection - static std::vector compressSurf_( - std::vector& coords, + static std::vector compressConnectedSurfaces_( + std::vector& coords, const std::pair& signDir, const std::vector& surfs); // Group lines by (grid_line, sign, axis) and compress each group static std::vector compressLines_( - const std::vector& coords, + const std::vector& coords, const std::vector& lines); // Compress lines with same direction and sign static std::vector compressDirSignLines_( - const std::vector& coords, + const std::vector& coords, const std::pair& signDir, const std::vector& lines); // Merge adjacent surfels into maximal rectangles - static std::vector compressSurfels_( + static std::vector compressSurfelsIntoMaximalRectangles_( const std::set& surfs); // Detect boundary contours of surfel set static std::vector getContours_(const std::set& surfs); // Trace a single contour from starting edge - static Contour getContour_( + static Contour getContourFromStartingEdge_( const PlaneLinel& from, const std::set& surfs, std::set& visited); @@ -72,11 +72,11 @@ class Compressor { const std::vector& contours); // Select the set of crossing lines with maximum count - static std::array, 2> getMaxCompatLines_( + static std::array, 2> getMaxCompactedLines_( const std::array, 2>& cross); // Get linels between two points - static std::set getLinels_( + static std::set getLinelsBetween_( const PlanePoint& ini, const PlanePoint& end); diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 84b9410..b6d8968 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -90,7 +90,7 @@ void StaircaseMesher::process(Mesh& mesh) const if (compress_) { log("Compressing surfaces.", 1); std::size_t beforeQuads = countMeshElementsIf(mesh, isQuad); - std::size_t merged = Compressor::compressSurfaces(mesh); + std::size_t merged = Compressor::compressSurfacesInMesh(mesh); std::size_t afterQuads = countMeshElementsIf(mesh, isQuad); log("Compressed " + std::to_string(beforeQuads) + " -> " + std::to_string(afterQuads) + @@ -98,7 +98,7 @@ void StaircaseMesher::process(Mesh& mesh) const log("Compressing lines.", 1); std::size_t beforeLines = countMeshElementsIf(mesh, isLine); - merged = Compressor::compressLines(mesh); + merged = Compressor::compressLinesInMesh(mesh); std::size_t afterLines = countMeshElementsIf(mesh, isLine); log("Compressed " + std::to_string(beforeLines) + " -> " + std::to_string(afterLines) + diff --git a/src/utils/Types.h b/src/utils/Types.h index 7ac4589..e5f1668 100644 --- a/src/utils/Types.h +++ b/src/utils/Types.h @@ -63,6 +63,9 @@ using UpdateMap = std::array, 2>, 2>; // Compressor types using Sign = int; +using SignedAxis = std::pair; +using GridLine = std::array; + using PlanePoint = std::array; using PlaneLinel = std::pair; using PlaneSurfel = PlanePoint; diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index 1aac5ef..325a661 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -39,7 +39,7 @@ TEST_F(CompressorTest, Compress2x2QuadsIntoOneSurface) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 4u); - auto merged = core::Compressor::compressSurfaces(mesh); + auto merged = core::Compressor::compressSurfacesInMesh(mesh); EXPECT_EQ(merged, 3u); ASSERT_EQ(mesh.groups.size(), 1u); @@ -64,7 +64,7 @@ TEST_F(CompressorTest, CompresRepeatedQuadsIntoOneSurface) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); - auto merged = core::Compressor::compressSurfaces(mesh); + auto merged = core::Compressor::compressSurfacesInMesh(mesh); EXPECT_EQ(merged, 1u); ASSERT_EQ(mesh.groups.size(), 1u); @@ -90,7 +90,7 @@ TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentOrientation) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); - auto merged = core::Compressor::compressSurfaces(mesh); + auto merged = core::Compressor::compressSurfacesInMesh(mesh); EXPECT_EQ(merged, 0u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); @@ -109,7 +109,7 @@ TEST_F(CompressorTest, DoesNotCompressNonCoplanarQuads) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); - auto merged = core::Compressor::compressSurfaces(mesh); + auto merged = core::Compressor::compressSurfacesInMesh(mesh); EXPECT_EQ(merged, 0u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); @@ -128,7 +128,7 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedQuads) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); - auto merged = core::Compressor::compressSurfaces(mesh); + auto merged = core::Compressor::compressSurfacesInMesh(mesh); EXPECT_EQ(merged, 0u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); @@ -166,7 +166,7 @@ TEST_F(CompressorTest, CompressWithHoleCreatesInnerContour) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 8u); - auto merged = core::Compressor::compressSurfaces(mesh); + auto merged = core::Compressor::compressSurfacesInMesh(mesh); auto finalCount = countMeshElementsIf(mesh, isQuad); @@ -212,7 +212,7 @@ TEST_F(CompressorTest, DoesNotCompressQuadsWithDifferentNormals) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); - auto merged = core::Compressor::compressSurfaces(mesh); + auto merged = core::Compressor::compressSurfacesInMesh(mesh); EXPECT_EQ(merged, 0u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 2u); @@ -254,7 +254,7 @@ TEST_F(CompressorTest, CompressRingRoundTrip) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 12u); // Compress: 12 quads -> 4 surfaces () - auto merged = core::Compressor::compressSurfaces(mesh); + auto merged = core::Compressor::compressSurfacesInMesh(mesh); EXPECT_EQ(merged, 8u); // 12 - 4 = 4 surfaces merged auto compressedCount = countMeshElementsIf(mesh, isQuad); EXPECT_EQ(compressedCount, 4u); @@ -288,7 +288,7 @@ TEST_F(CompressorTest, Compress3x3GridRoundTrip) { EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 9u); // Compress: 9 quads -> 1 surface - auto merged = core::Compressor::compressSurfaces(mesh); + auto merged = core::Compressor::compressSurfacesInMesh(mesh); EXPECT_EQ(merged, 8u); EXPECT_EQ(countMeshElementsIf(mesh, isQuad), 1u); @@ -309,7 +309,7 @@ TEST_F(CompressorTest, Compress2CollinearLinesIntoOne) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); - auto merged = core::Compressor::compressLines(mesh); + auto merged = core::Compressor::compressLinesInMesh(mesh); EXPECT_EQ(merged, 1u); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); @@ -329,7 +329,7 @@ TEST_F(CompressorTest, CompressRepeatedLinesIntoOne) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); - auto merged = core::Compressor::compressLines(mesh); + auto merged = core::Compressor::compressLinesInMesh(mesh); EXPECT_EQ(merged, 1u); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); @@ -349,7 +349,7 @@ TEST_F(CompressorTest, DoesNotCompressNonCollinearLines) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); - auto merged = core::Compressor::compressLines(mesh); + auto merged = core::Compressor::compressLinesInMesh(mesh); EXPECT_EQ(merged, 0u); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); @@ -364,7 +364,7 @@ TEST_F(CompressorTest, DoesNotCompressDisconnectedLines) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); - auto merged = core::Compressor::compressLines(mesh); + auto merged = core::Compressor::compressLinesInMesh(mesh); EXPECT_EQ(merged, 0u); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); @@ -379,7 +379,7 @@ TEST_F(CompressorTest, DoesNotCompressOverlappingOppositeDirectionLines) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); - auto merged = core::Compressor::compressLines(mesh); + auto merged = core::Compressor::compressLinesInMesh(mesh); EXPECT_EQ(merged, 0u); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); @@ -394,7 +394,7 @@ TEST_F(CompressorTest, DoesNotCompressConnectedOppositeDirectionLines) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); - auto merged = core::Compressor::compressLines(mesh); + auto merged = core::Compressor::compressLinesInMesh(mesh); EXPECT_EQ(merged, 0u); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 2u); @@ -410,7 +410,7 @@ TEST_F(CompressorTest, Compress3LinesIntoOne) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 3u); - auto merged = core::Compressor::compressLines(mesh); + auto merged = core::Compressor::compressLinesInMesh(mesh); EXPECT_EQ(merged, 2u); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); @@ -429,7 +429,7 @@ TEST_F(CompressorTest, Compress5LineRoundTrip) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 5u); - core::Compressor::compressLines(mesh); + core::Compressor::compressLinesInMesh(mesh); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 1u); EXPECT_EQ(CoordinateIds({0, 5}), mesh.groups[0].elements[0].vertices); } @@ -460,7 +460,7 @@ TEST_F(CompressorTest, CompressMixedDirections) { EXPECT_EQ(countMeshElementsIf(mesh, isLine), 6u); - auto merged = core::Compressor::compressLines(mesh); + auto merged = core::Compressor::compressLinesInMesh(mesh); EXPECT_EQ(merged, 3u); EXPECT_EQ(countMeshElementsIf(mesh, isLine), 3u); From 4a8a5992abccec1b82bf1ce8dc47fa44fd307d7a Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Fri, 17 Jul 2026 12:45:16 +0000 Subject: [PATCH 33/61] Tessellator | Testing | Update staircase mesher tests to include compression --- test/meshers/StaircaseMesherTest.cpp | 58 +++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index 1787343..4e9f1eb 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -8,7 +8,6 @@ #include "core/Slicer.h" #include "core/Collapser.h" -#include "utils/Geometry.h" #include "utils/GridTools.h" #include "utils/MeshTools.h" #include "utils/RedundancyCleaner.h" @@ -270,16 +269,16 @@ TEST_F(StaircaseMesherTest, DISABLED_visualSelectiveStaircaserCone) } auto resultMesh = meshlib::core::Staircaser{ collapsedMesh }.getSelectiveMesh(cellSet); - // ASSERT_NO_THROW(meshTools::checkNoCellsAreCrossed(resultMesh)); + ASSERT_NO_THROW(meshTools::checkNoCellsAreCrossed(resultMesh)); RedundancyCleaner::removeOverlappedDimensionOneAndLowerElementsAndEquivalentSurfaces(resultMesh); utils::meshTools::reduceGrid(resultMesh, inputMesh.grid); utils::meshTools::convertToAbsoluteCoordinates(resultMesh); - // EXPECT_TRUE(meshTools::isAClosedTopology(inputMesh.groups[0].elements)); - // EXPECT_TRUE(meshTools::isAClosedTopology(surfaceMesh.groups[0].elements)); - // EXPECT_TRUE(meshTools::isAClosedTopology(slicedMesh.groups[0].elements)); - // EXPECT_TRUE(meshTools::isAClosedTopology(resultMesh.groups[0].elements)); + EXPECT_TRUE(meshTools::isAClosedTopology(inputMesh.groups[0].elements)); + EXPECT_TRUE(meshTools::isAClosedTopology(surfaceMesh.groups[0].elements)); + EXPECT_TRUE(meshTools::isAClosedTopology(slicedMesh.groups[0].elements)); + EXPECT_TRUE(meshTools::isAClosedTopology(resultMesh.groups[0].elements)); @@ -291,7 +290,7 @@ TEST_F(StaircaseMesherTest, DISABLED_visualSelectiveStaircaserCone) #endif -TEST_F(StaircaseMesherTest, DISABLED_testStaircaseTriangleWithUniformGrid) +TEST_F(StaircaseMesherTest, testStaircaseTriangleWithUniformGrid) { float lowerCoordinateValue = -0.5; @@ -316,10 +315,49 @@ TEST_F(StaircaseMesherTest, DISABLED_testStaircaseTriangleWithUniformGrid) ASSERT_NO_THROW(resultMesh = StaircaseMesher(inputMesh, 2).mesh()); EXPECT_EQ(0, countRepeatedElements(resultMesh)); - EXPECT_EQ(48, resultMesh.groups[0].elements.size()); + EXPECT_EQ(12, resultMesh.groups[0].elements.size()); EXPECT_EQ(10, countMeshElementsIf(resultMesh, isQuad)); - EXPECT_EQ(32, countMeshElementsIf(resultMesh, isLine)); - EXPECT_EQ(6, countMeshElementsIf(resultMesh, isNode)); + EXPECT_EQ(2, countMeshElementsIf(resultMesh, isLine)); + EXPECT_EQ(0, countMeshElementsIf(resultMesh, isNode)); +} + +TEST_F(StaircaseMesherTest, testStaircaseWithCompression) +{ + float lowerCoordinateValue = -0.5; + float upperCoordinateValue = 0.5; + int numberOfCells = 4; + float step = 0.25; + assert((upperCoordinateValue - lowerCoordinateValue) / (numberOfCells) == step); + + Mesh inputMesh; + inputMesh.grid = GridTools::buildCartesianGrid(lowerCoordinateValue, upperCoordinateValue, numberOfCells + 1); + inputMesh.coordinates = { + Coordinate({ -0.475, -0.15, 0 }), + Coordinate({ 0.475, -0.15, 0 }), + Coordinate({ 0.0 , 0.15, 0 }), + Coordinate({ 0.0 , 0.15, 0.475 }) + }; + inputMesh.groups.resize(1); + inputMesh.groups[0].elements = { + Element({0, 1, 2}, Element::Type::Surface), + Element({2, 3}, Element::Type::Line) + }; + + Mesh nonCompressedMesh = StaircaseMesher(inputMesh, 2, StaircaseMesherOptions(), false).mesh(); + Mesh compressedMesh; + ASSERT_NO_THROW(compressedMesh = StaircaseMesher(inputMesh, 2, StaircaseMesherOptions(), true).mesh()); + + EXPECT_EQ(0, countRepeatedElements(nonCompressedMesh)); + EXPECT_EQ(7, nonCompressedMesh.groups[0].elements.size()); + EXPECT_EQ(4, countMeshElementsIf(nonCompressedMesh, isQuad)); + EXPECT_EQ(3, countMeshElementsIf(nonCompressedMesh, isLine)); + EXPECT_EQ(0, countMeshElementsIf(nonCompressedMesh, isNode)); + + EXPECT_EQ(0, countRepeatedElements(compressedMesh)); + EXPECT_EQ(3, compressedMesh.groups[0].elements.size()); + EXPECT_EQ(1, countMeshElementsIf(compressedMesh, isQuad)); + EXPECT_EQ(2, countMeshElementsIf(compressedMesh, isLine)); + EXPECT_EQ(0, countMeshElementsIf(compressedMesh, isNode)); } #if APP_LOADED From 8a681998d866e0f2c15e736692cc79817f653e16 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Fri, 17 Jul 2026 13:01:13 +0000 Subject: [PATCH 34/61] Tessellator | Testing | Tests for launcher with compression option --- test/app/launcherTest.cpp | 22 ++++++++++++++++++- .../longPolyline.tessellator.json | 8 ++++++- .../longPolyline_compression.tessellator.json | 16 ++++++++++++++ .../longPolyline_legacy.tessellator.json | 10 +++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 testData/cases/longPolyline/longPolyline_compression.tessellator.json create mode 100644 testData/cases/longPolyline/longPolyline_legacy.tessellator.json diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index e5dd3f7..029b376 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -58,6 +58,26 @@ TEST_F(LauncherTest, launches_alhambra_case) EXPECT_EQ(exitCode, EXIT_SUCCESS); } +TEST_F(LauncherTest, parses_staircased_without_compression) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline.tessellator.json" }; + int exitCode; + + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, parses_staircased_with_compression) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline_compression.tessellator.json" }; + int exitCode; + + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + TEST_F(LauncherTest, launches_conformal_alhambra_case) { int ac = 3; @@ -125,7 +145,7 @@ TEST_F(LauncherTest, launches_cone_case) TEST_F(LauncherTest, launches_long_polyline_case) { int ac = 3; - const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline.tessellator.json" }; + const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline_legacy.tessellator.json" }; int exitCode; EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); diff --git a/testData/cases/longPolyline/longPolyline.tessellator.json b/testData/cases/longPolyline/longPolyline.tessellator.json index 1c672f3..7f60504 100644 --- a/testData/cases/longPolyline/longPolyline.tessellator.json +++ b/testData/cases/longPolyline/longPolyline.tessellator.json @@ -6,5 +6,11 @@ [1100, 1100, 1100] ] }, - "object": {"filename": "longPolyline.vtu"} + "object": {"filename": "longPolyline.vtu"}, + "mesher": { + "type" : "staircase", + "options" : { + "compress" : false + } + } } \ No newline at end of file diff --git a/testData/cases/longPolyline/longPolyline_compression.tessellator.json b/testData/cases/longPolyline/longPolyline_compression.tessellator.json new file mode 100644 index 0000000..7294f08 --- /dev/null +++ b/testData/cases/longPolyline/longPolyline_compression.tessellator.json @@ -0,0 +1,16 @@ +{ + "grid": { + "numberOfCells": [20, 20, 20], + "boundingBox": [ + [ 600, 600, 600], + [1100, 1100, 1100] + ] + }, + "object": {"filename": "longPolyline.vtu"}, + "mesher": { + "type" : "staircase", + "options" : { + "compress" : true + } + } +} \ No newline at end of file diff --git a/testData/cases/longPolyline/longPolyline_legacy.tessellator.json b/testData/cases/longPolyline/longPolyline_legacy.tessellator.json new file mode 100644 index 0000000..1c672f3 --- /dev/null +++ b/testData/cases/longPolyline/longPolyline_legacy.tessellator.json @@ -0,0 +1,10 @@ +{ + "grid": { + "numberOfCells": [20, 20, 20], + "boundingBox": [ + [ 600, 600, 600], + [1100, 1100, 1100] + ] + }, + "object": {"filename": "longPolyline.vtu"} +} \ No newline at end of file From fa3db5a2c9cdb3fdae0dd656c72e873a5fde324b Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Mon, 20 Jul 2026 10:03:45 +0000 Subject: [PATCH 35/61] Tessellator | Core | Prevent polyline groups from being compressed --- src/core/Compressor.cpp | 29 ++++++++++++++++++++------- src/core/Compressor.h | 2 +- src/meshers/StaircaseMesher.cpp | 2 +- test/core/CompressorTest.cpp | 30 ++++++++++++++++++++++++++++ test/meshers/StaircaseMesherTest.cpp | 16 ++++++++++----- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/core/Compressor.cpp b/src/core/Compressor.cpp index f1dbf8d..b8c84ca 100644 --- a/src/core/Compressor.cpp +++ b/src/core/Compressor.cpp @@ -57,14 +57,29 @@ std::size_t Compressor::compressSurfacesInMesh(Mesh& mesh) { return totalOriginal - totalCompressed; } -std::size_t Compressor::compressLinesInMesh(Mesh& mesh) { +std::size_t Compressor::compressLinesInMesh(Mesh& mesh, const std::vector & dimensionPolicy) { std::size_t totalOriginal = 0; std::size_t totalCompressed = 0; - for (Group& group : mesh.groups) { + std::vector compress; + if (dimensionPolicy.size() == 0){ + compress = std::vector(mesh.groups.size(), true); + } + else{ + compress.reserve(mesh.groups.size()); + for (auto policy: dimensionPolicy){ + compress.emplace_back(policy == Element::Type::Volume || policy == Element::Type::Surface); + } + } + + for (GroupId g = 0; g < mesh.groups.size(); ++g){ + if (!compress[g]){ + continue; + } + std::vector lines; - for (const Element& elem : group.elements) { + for (const Element& elem : mesh.groups[g].elements) { if (elem.type == Element::Type::Line) { lines.push_back(elem); } @@ -81,17 +96,17 @@ std::size_t Compressor::compressLinesInMesh(Mesh& mesh) { // Build new elements vector with compressed lines std::vector newElements; ElementId lineIdx = 0; - for (ElementId e = 0; e < group.elements.size(); e++) { - if (group.elements[e].type == Element::Type::Line) { + for (ElementId e = 0; e < mesh.groups[g].elements.size(); e++) { + if (mesh.groups[g].elements[e].type == Element::Type::Line) { if (lineIdx < compressedLines.size()) { newElements.push_back(compressedLines[lineIdx]); lineIdx++; } } else { - newElements.push_back(group.elements[e]); + newElements.push_back(mesh.groups[g].elements[e]); } } - group.elements = std::move(newElements); + mesh.groups[g].elements = std::move(newElements); } return totalOriginal - totalCompressed; diff --git a/src/core/Compressor.h b/src/core/Compressor.h index 7b0fe32..57650be 100644 --- a/src/core/Compressor.h +++ b/src/core/Compressor.h @@ -16,7 +16,7 @@ class Compressor { // Compress collinear line segments that are adjacent // Returns number of lines merged (original_count - compressed_count) - static std::size_t compressLinesInMesh(Mesh& mesh); + static std::size_t compressLinesInMesh(Mesh& mesh, const std::vector & dimensionPolicy = {}); private: // Group surfaces by (grid_plane, sign, axis) and compress each group diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index b6d8968..ec5d29a 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -98,7 +98,7 @@ void StaircaseMesher::process(Mesh& mesh) const log("Compressing lines.", 1); std::size_t beforeLines = countMeshElementsIf(mesh, isLine); - merged = Compressor::compressLinesInMesh(mesh); + merged = Compressor::compressLinesInMesh(mesh, dimensions); std::size_t afterLines = countMeshElementsIf(mesh, isLine); log("Compressed " + std::to_string(beforeLines) + " -> " + std::to_string(afterLines) + diff --git a/test/core/CompressorTest.cpp b/test/core/CompressorTest.cpp index 325a661..4a7a4f8 100644 --- a/test/core/CompressorTest.cpp +++ b/test/core/CompressorTest.cpp @@ -470,4 +470,34 @@ TEST_F(CompressorTest, CompressMixedDirections) { EXPECT_EQ(CoordinateIds({0, 6}), mesh.groups[0].elements[2].vertices); } +TEST_F(CompressorTest, DoesNotCompressLinesWhenGroupDimensionIsLowerThanSurface) { + Mesh mesh; + mesh.grid = grid_; + + mesh.groups.resize(3); + + for (GroupId g = 0; g < 3; ++g){ + addLine(mesh, {0, 0, 0}, {0, 1, 0}, g); + addLine(mesh, {0, 1, 0}, {0, 2, 0}, g); + addLine(mesh, {0, 2, 0}, {0, 3, 0}, g); + addLine(mesh, {0, 3, 0}, {0, 4, 0}, g); + addLine(mesh, {0, 4, 0}, {0, 5, 0}, g); + } + + auto dimensions = std::vector({Element::Type::Volume, Element::Type::Surface, Element::Type::Line}); + + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 15u); + + auto merged = core::Compressor::compressLinesInMesh(mesh, dimensions); + + EXPECT_EQ(merged, 8u); + EXPECT_EQ(countMeshElementsIf(mesh, isLine), 7u); + + EXPECT_EQ(1, mesh.groups[0].elements.size()); + EXPECT_EQ(1, mesh.groups[1].elements.size()); + EXPECT_EQ(5, mesh.groups[2].elements.size()); + EXPECT_EQ(CoordinateIds({0, 5}), mesh.groups[0].elements[0].vertices); + EXPECT_EQ(CoordinateIds({0, 5}), mesh.groups[1].elements[0].vertices); +} + } diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index 4e9f1eb..efded82 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -337,26 +337,32 @@ TEST_F(StaircaseMesherTest, testStaircaseWithCompression) Coordinate({ 0.0 , 0.15, 0 }), Coordinate({ 0.0 , 0.15, 0.475 }) }; - inputMesh.groups.resize(1); + inputMesh.groups.resize(2); inputMesh.groups[0].elements = { Element({0, 1, 2}, Element::Type::Surface), Element({2, 3}, Element::Type::Line) }; + inputMesh.groups[1].elements = { + Element({0, 2}, Element::Type::Line), + Element({2, 3}, Element::Type::Line) + }; Mesh nonCompressedMesh = StaircaseMesher(inputMesh, 2, StaircaseMesherOptions(), false).mesh(); Mesh compressedMesh; ASSERT_NO_THROW(compressedMesh = StaircaseMesher(inputMesh, 2, StaircaseMesherOptions(), true).mesh()); - EXPECT_EQ(0, countRepeatedElements(nonCompressedMesh)); + EXPECT_EQ(3, countRepeatedElements(nonCompressedMesh)); EXPECT_EQ(7, nonCompressedMesh.groups[0].elements.size()); + EXPECT_EQ(6, nonCompressedMesh.groups[1].elements.size()); EXPECT_EQ(4, countMeshElementsIf(nonCompressedMesh, isQuad)); - EXPECT_EQ(3, countMeshElementsIf(nonCompressedMesh, isLine)); + EXPECT_EQ(9, countMeshElementsIf(nonCompressedMesh, isLine)); EXPECT_EQ(0, countMeshElementsIf(nonCompressedMesh, isNode)); - EXPECT_EQ(0, countRepeatedElements(compressedMesh)); + EXPECT_EQ(1, countRepeatedElements(compressedMesh)); EXPECT_EQ(3, compressedMesh.groups[0].elements.size()); + EXPECT_EQ(6, nonCompressedMesh.groups[1].elements.size()); EXPECT_EQ(1, countMeshElementsIf(compressedMesh, isQuad)); - EXPECT_EQ(2, countMeshElementsIf(compressedMesh, isLine)); + EXPECT_EQ(8, countMeshElementsIf(compressedMesh, isLine)); EXPECT_EQ(0, countMeshElementsIf(compressedMesh, isNode)); } From 083e0ffb3aa7caa02d28527c8432c74f7fd3c3b9 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Mon, 20 Jul 2026 14:01:38 +0000 Subject: [PATCH 36/61] Tessellator | Mesher | Add the compress boolean into the staircaser meshing options --- src/app/launcher.cpp | 22 ++++++---------------- src/meshers/StaircaseMesher.cpp | 7 +++---- src/meshers/StaircaseMesher.h | 3 +-- src/meshers/StaircaseMesherOptions.h | 3 +-- test/meshers/StaircaseMesherTest.cpp | 7 +++++-- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 5c36c5a..dbeccd0 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -106,6 +106,11 @@ meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const std::s if (j["object"].contains("volume")) { res.isVolume = j["object"]["volume"]; } + if (j["mesher"].contains("options") && + j["mesher"]["options"].contains("compress")) { + res.compress = j["mesher"]["options"]["compress"]; + } + return res; } @@ -129,20 +134,6 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::s return res; } -bool readStaircaseMesherCompressOption(const std::string &fn) -{ - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("compress")) { - return j["mesher"]["options"]["compress"]; - } - return false; -} - bool readExportGridOption(const std::string &fn) { nlohmann::json j; @@ -160,8 +151,7 @@ std::unique_ptr buildMesher(const Mesh &in, const { auto mesherType = readMesherType(fn); if (mesherType == meshlib::app::staircase_mesher) { - bool compress = readStaircaseMesherCompressOption(fn); - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn), compress}); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn)}); } else if (mesherType == meshlib::app::conformal_mesher) { return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn)}); } else { diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index ec5d29a..6e9034d 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -20,11 +20,10 @@ using namespace utils; using namespace core; using namespace meshTools; -StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts, bool compress) : +StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts) : MesherBase(inputMesh), decimalPlacesInCollapser_(decimalPlacesInCollapser), - opts_(opts), - compress_(compress) + opts_(opts) { log("Preparing surfaces."); surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -87,7 +86,7 @@ void StaircaseMesher::process(Mesh& mesh) const logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); logNumberOfLines(countMeshElementsIf(mesh, isLine)); - if (compress_) { + if (opts_.compress) { log("Compressing surfaces.", 1); std::size_t beforeQuads = countMeshElementsIf(mesh, isQuad); std::size_t merged = Compressor::compressSurfacesInMesh(mesh); diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index 070e46e..df0b9bb 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -8,13 +8,12 @@ namespace meshlib::meshers { class StaircaseMesher : public MesherBase { public: - StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions(), bool compress = false); + StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions()); virtual ~StaircaseMesher() = default; Mesh mesh() const; private: int decimalPlacesInCollapser_; - bool compress_; Mesh surfaceMesh_; StaircaseMesherOptions opts_; diff --git a/src/meshers/StaircaseMesherOptions.h b/src/meshers/StaircaseMesherOptions.h index dc06781..53f2b50 100644 --- a/src/meshers/StaircaseMesherOptions.h +++ b/src/meshers/StaircaseMesherOptions.h @@ -1,13 +1,12 @@ #pragma once -#include "types/Mesh.h" #include "MesherBaseOptions.h" -#include "core/SnapperOptions.h" namespace meshlib::meshers { class StaircaseMesherOptions : public MesherBaseOptions { public: + bool compress = false; }; } diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index efded82..6d1dfdb 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -3,6 +3,7 @@ #include "meshers/StaircaseMesher.h" +#include "StaircaseMesherOptions.h" #include "Staircaser.h" #include "core/Slicer.h" @@ -347,9 +348,11 @@ TEST_F(StaircaseMesherTest, testStaircaseWithCompression) Element({2, 3}, Element::Type::Line) }; - Mesh nonCompressedMesh = StaircaseMesher(inputMesh, 2, StaircaseMesherOptions(), false).mesh(); + Mesh nonCompressedMesh = StaircaseMesher(inputMesh, 2).mesh(); Mesh compressedMesh; - ASSERT_NO_THROW(compressedMesh = StaircaseMesher(inputMesh, 2, StaircaseMesherOptions(), true).mesh()); + StaircaseMesherOptions compressOption; + compressOption.compress = true; + ASSERT_NO_THROW(compressedMesh = StaircaseMesher(inputMesh, 2, compressOption).mesh()); EXPECT_EQ(3, countRepeatedElements(nonCompressedMesh)); EXPECT_EQ(7, nonCompressedMesh.groups[0].elements.size()); From a0c0488d28a9ca84b8f9e612a59431eba919ecab Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Tue, 21 Jul 2026 10:33:19 +0200 Subject: [PATCH 37/61] Changes arguments passed by value to reference --- src/core/Staircaser.cpp | 4 ++-- src/core/Staircaser.h | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/core/Staircaser.cpp b/src/core/Staircaser.cpp index 40c9d00..f51f2af 100644 --- a/src/core/Staircaser.cpp +++ b/src/core/Staircaser.cpp @@ -448,7 +448,7 @@ bool Staircaser::isAllCoordinatesOnTheSameCellBoundary(const Element& newElement return !commonStructuredCells.empty(); } -std::set Staircaser::getElementsConvertedInLines(Cell cell, std::map> cellElemMap) { +std::set Staircaser::getElementsConvertedInLines(const Cell cell, const std::map>& cellElemMap) { std::set elementsConvertedInLines; for (const auto& e : cellElemMap.at(cell)) { @@ -492,7 +492,7 @@ std::set Staircaser::getElementsConvertedInLines(Cell cell, std::map elementsConvertedInLines, Group& meshGroup, const Cell cell, std::map> cellElemMap, std::vector> toRemove) { +void Staircaser::splitLinesWithNeighborTriangle(const size_t groupIndex, std::set elementsConvertedInLines, Group& meshGroup, const Cell cell, const std::map>& cellElemMap, std::vector> toRemove) { for (const auto& e: elementsConvertedInLines) { bool processed = false; for (const auto& otherElementsInCell: cellElemMap.at(cell)) { diff --git a/src/core/Staircaser.h b/src/core/Staircaser.h index a0a94e4..c836822 100644 --- a/src/core/Staircaser.h +++ b/src/core/Staircaser.h @@ -78,10 +78,12 @@ class Staircaser : public utils::GridTools { void fillGaps(const RelativePairSet boundaryCoordinatePairs); std::pair obtainNewIndexForElement(const Element& e, const std::set& cellsToStructure, CoordinateMap& coordinateMap); bool isAllCoordinatesOnTheSameCellBoundary(const Element& newElement, const std::set& cellsToStructure); - std::set getElementsConvertedInLines(const Cell cell, std::map> cellElemMap); - void splitLinesWithNeighborTriangle(const size_t groupIndex, std::set elementsConvertedInLines, Group& meshGroup, const Cell cell, std::map> cellElemMap, std::vector> toRemove); + std::set getElementsConvertedInLines(const Cell cell, const std::map>& cellElemMap); + void splitLinesWithNeighborTriangle(const size_t groupIndex, std::set elementsConvertedInLines, Group& meshGroup, const Cell cell, const std::map>& cellElemMap, std::vector> toRemove); }; + + } } \ No newline at end of file From f60edbaa77559a19499834db76ac5ecf64a509b9 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Sun, 17 May 2026 20:09:40 +0200 Subject: [PATCH 38/61] AI assisted implementation of multiobject meshing --- src/app/launcher.cpp | 179 ++++++++++++++++++++++++++++------- src/app/launcher.h | 12 +++ src/app/vtkIO.cpp | 15 +++ src/types/Mesh.h | 8 +- src/utils/MeshTools.cpp | 54 +++++++++++ src/utils/MeshTools.h | 2 + test/app/launcherTest.cpp | 75 +++++++++++++++ test/utils/MeshToolsTest.cpp | 86 +++++++++++++++++ 8 files changed, 394 insertions(+), 37 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index dbeccd0..4d4174b 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -4,6 +4,7 @@ #include "meshers/StaircaseMesher.h" #include "meshers/ConformalMesher.h" #include "utils/GridTools.h" +#include "utils/MeshTools.h" #include #include @@ -13,6 +14,8 @@ #include #include #include +#include +#include namespace meshlib::app { @@ -44,18 +47,51 @@ Grid parseGridFromJSON(const nlohmann::json &j) } } -Mesh readMesh(const std::string &fn) +std::vector readObjectsFromJSON(const std::string& fn) { nlohmann::json j; + { + std::ifstream i(fn); + i >> j; + } + + std::vector objects; + + if (j.contains("objects")) { + for (const auto& obj : j["objects"]) { + ObjectDefinition objDef; + objDef.filename = obj["filename"]; + objDef.group = obj.value("group", std::filesystem::path(obj["filename"]).stem().string()); + if (obj.contains("mesher")) { + objDef.mesherOverride = obj["mesher"]; + } + objects.push_back(objDef); + } + } else if (j.contains("object")) { + ObjectDefinition objDef; + objDef.filename = j["object"]["filename"]; + objDef.group = std::filesystem::path(j["object"]["filename"]).stem().string(); + if (j.contains("mesher")) { + objDef.mesherOverride = j["mesher"]; + } + objects.push_back(objDef); + } else { + throw std::runtime_error("No objects defined in input file"); + } + return objects; +} + +Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef) +{ + nlohmann::json j; { std::ifstream i(fn); i >> j; } std::filesystem::path caseFolder = std::filesystem::path(fn).parent_path(); - std::filesystem::path objPathFromInput = j["object"]["filename"]; - std::filesystem::path meshObjectPath = caseFolder / objPathFromInput; + std::filesystem::path meshObjectPath = caseFolder / objDef.filename; std::cout << "-- Reading mesh groups from: " << meshObjectPath; Mesh res = vtkIO::readInputMesh(meshObjectPath); @@ -65,27 +101,48 @@ Mesh readMesh(const std::string &fn) res.grid = parseGridFromJSON(j["grid"]); std::cout << "....... [OK]" << std::endl; + if (res.groups.empty()) { + res.groups.push_back(Group{objDef.group, {}}); + } else { + res = utils::meshTools::extractGroupsByName(res, {objDef.group}); + if (res.groups.empty()) { + res.groups.push_back(Group{objDef.group, {}}); + } else { + res.groups[0].name = objDef.group; + } + } + return res; } -std::string readMesherType(const std::string &fn) +std::string readMesherType(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } - if (j["mesher"].contains("type")) { - return j["mesher"]["type"]; + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } else { + return meshlib::app::staircase_mesher; + } + + if (mesherConfig.contains("type")) { + return mesherConfig["type"]; } else { return meshlib::app::staircase_mesher; } } -std::string readExtension(const std::string &fn) +std::string readExtension(const std::string& fn, const std::optional& override) { - auto mesherType = readMesherType(fn); + auto mesherType = readMesherType(fn, override); if (mesherType == meshlib::app::staircase_mesher) { return "str"; } else if (mesherType == meshlib::app::conformal_mesher) { @@ -114,46 +171,82 @@ meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const std::s return res; } -meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string &fn) +meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } + meshlib::meshers::ConformalMesherOptions res; - if (j["object"].contains("volume")) { - res.isVolume = j["object"]["volume"]; + if (mesherConfig.contains("options")) { + res.snapperOptions.edgePoints = mesherConfig["options"]["edgePoints"]; + res.snapperOptions.forbiddenLength = mesherConfig["options"]["forbiddenLength"]; } + return res; +} - - if (j["mesher"].contains("options")) { - res.snapperOptions.edgePoints = j["mesher"]["options"]["edgePoints"]; - res.snapperOptions.forbiddenLength = j["mesher"]["options"]["forbiddenLength"]; +bool readStaircaseMesherCompressOption(const std::string& fn, const std::optional& override) +{ + nlohmann::json j; + { + std::ifstream i(fn); + i >> j; } - return res; + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } + + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("compress")) { + return mesherConfig["options"]["compress"]; + } + return false; } -bool readExportGridOption(const std::string &fn) +bool readExportGridOption(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } - if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("exportGrid")) { - return j["mesher"]["options"]["exportGrid"]; + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } + + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("exportGrid")) { + return mesherConfig["options"]["exportGrid"]; } return true; } -std::unique_ptr buildMesher(const Mesh &in, const std::string &fn) + +std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const std::optional& override) { - auto mesherType = readMesherType(fn); + auto mesherType = readMesherType(fn, override); if (mesherType == meshlib::app::staircase_mesher) { - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn)}); + auto staircasedOptions = readStaircaseMesherOptions(fn); + staircasedOptions.compress = readStaircaseMesherCompressOption(fn, override); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, staircasedOptions}); } else if (mesherType == meshlib::app::conformal_mesher) { - return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn)}); + return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn, override)}); } else { throw std::runtime_error("Unsupported mesher type"); } @@ -176,24 +269,38 @@ int launcher(int argc, const char* argv[]) return EXIT_SUCCESS; } - // Input std::string inputFilename = vm["input"].as(); std::cout << "-- Input file is: " << inputFilename << std::endl; - Mesh mesh = readMesh(inputFilename); + std::vector objects = readObjectsFromJSON(inputFilename); + std::filesystem::path outputFolder = getFolder(inputFilename); + auto basename = getBasename(inputFilename); + Mesh firstMesh; + bool first = true; - // Mesh - auto mesher = buildMesher(mesh, inputFilename); - Mesh resultMesh = mesher->mesh(); + for (const auto& objDef : objects) { + std::cout << "\n-- Processing object: " << objDef.filename << " (group: " << objDef.group << ")" << std::endl; - std::filesystem::path outputFolder = getFolder(inputFilename); - auto basename = getBasename(inputFilename); - auto extension = readExtension(inputFilename); - - exportMeshToVTU(outputFolder / (basename + ".tessellator." + extension + ".vtk"), resultMesh); - if (readExportGridOption(inputFilename)) { - exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), resultMesh.grid); + Mesh mesh = readMesh(inputFilename, objDef); + + auto mesher = buildMesher(mesh, inputFilename, objDef.mesherOverride); + Mesh resultMesh = mesher->mesh(); + + if (first) { + firstMesh = resultMesh; + first = false; + } + + auto extension = readExtension(inputFilename, objDef.mesherOverride); + std::string outputFilename = objDef.group + ".tessellator." + extension + ".vtk"; + exportMeshToVTU(outputFolder / outputFilename, resultMesh); + std::cout << "-- Exported: " << outputFilename << std::endl; + } + + if (!first && readExportGridOption(inputFilename, std::nullopt)) { + exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), firstMesh.grid); + std::cout << "-- Exported grid: " << basename << ".tessellator.grid.vtk" << std::endl; } return EXIT_SUCCESS; diff --git a/src/app/launcher.h b/src/app/launcher.h index d65d218..bf65b34 100644 --- a/src/app/launcher.h +++ b/src/app/launcher.h @@ -1,7 +1,10 @@ #pragma once #include "types/Mesh.h" +#include "meshers/MesherBase.h" #include +#include +#include #include namespace meshlib::app { @@ -9,7 +12,16 @@ namespace meshlib::app { const std::string conformal_mesher ("conformal"); const std::string staircase_mesher ("staircase"); +struct ObjectDefinition { + std::string filename; + std::string group; + std::optional mesherOverride; +}; + int launcher(int argc, const char* argv[]); Grid parseGridFromJSON(const nlohmann::json& j); +std::vector readObjectsFromJSON(const std::string& fn); +Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef); +std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const std::optional& override); } \ No newline at end of file diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index f1e6c2e..d55a561 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -162,12 +163,26 @@ vtkSmartPointer toVTKGroupsArray(const Mesh& mesh) return groupsDataArray; } +vtkSmartPointer toVTKGroupNamesArray(const Mesh& mesh) +{ + vtkNew groupNamesArray; + groupNamesArray->SetName("groupNames"); + groupNamesArray->SetNumberOfComponents(1); + + for (const auto& group : mesh.groups) { + groupNamesArray->InsertNextValue(group.name.c_str()); + } + + return groupNamesArray; +} + vtkSmartPointer elementsToVTU(const Mesh& mesh) { vtkNew vtu; vtu->SetPoints(toVTKPoints(mesh.coordinates)); vtu->GetCellData()->AddArray(toVTKGroupsArray(mesh)); + vtu->GetCellData()->AddArray(toVTKGroupNamesArray(mesh)); std::vector cellTypes; cellTypes.reserve(mesh.countElems()); diff --git a/src/types/Mesh.h b/src/types/Mesh.h index bb0d41a..dcccb37 100644 --- a/src/types/Mesh.h +++ b/src/types/Mesh.h @@ -134,10 +134,15 @@ typedef std::size_t ElementId; typedef std::vector Elements; struct Group { + std::string name; std::vector elements; + Group() = default; + Group(const std::vector& elems) : elements(elems) {} + Group(const std::string& n, const std::vector& elems) : name(n), elements(elems) {} + bool operator==(const Group& rhs) const { - return elements == rhs.elements; + return name == rhs.name && elements == rhs.elements; } std::map> buildCoordToElemMap() const { @@ -155,6 +160,7 @@ struct Group { friend class boost::serialization::access; template void serialize(Archive& ar, const unsigned int version) { + ar& name; ar& elements; } }; diff --git a/src/utils/MeshTools.cpp b/src/utils/MeshTools.cpp index b2a25f4..d271f39 100644 --- a/src/utils/MeshTools.cpp +++ b/src/utils/MeshTools.cpp @@ -418,4 +418,58 @@ bool isAClosedTopology(const Elements& es) return CoordGraph(es).getBoundaryGraph().getVertices().size() == 0; } +Mesh extractGroupsByName(const Mesh& mesh, const std::vector& groupNames) +{ + Mesh result; + result.grid = mesh.grid; + + std::map coordRemap; + std::map> groupCoordIds; + + for (const auto& groupName : groupNames) { + groupCoordIds[groupName].clear(); + } + + for (const auto& groupName : groupNames) { + auto it = std::find_if(mesh.groups.begin(), mesh.groups.end(), + [&groupName](const Group& g) { return g.name == groupName; }); + + if (it != mesh.groups.end()) { + result.groups.push_back(*it); + + for (const auto& elem : it->elements) { + for (const auto& vId : elem.vertices) { + std::string coordKey = groupName + "_" + std::to_string(vId); + if (coordRemap.find(coordKey) == coordRemap.end()) { + CoordinateId newVId = result.coordinates.size(); + result.coordinates.push_back(mesh.coordinates[vId]); + coordRemap[coordKey] = newVId; + groupCoordIds[groupName].push_back(vId); + } + } + } + } else { + result.groups.push_back(Group{groupName, {}}); + } + } + + for (auto& group : result.groups) { + std::string groupName = group.name; + std::map elemRemap; + + for (const auto& oldVId : groupCoordIds[groupName]) { + std::string coordKey = groupName + "_" + std::to_string(oldVId); + elemRemap[oldVId] = coordRemap[coordKey]; + } + + for (auto& elem : group.elements) { + for (auto& vId : elem.vertices) { + vId = elemRemap[vId]; + } + } + } + + return result; +} + } \ No newline at end of file diff --git a/src/utils/MeshTools.h b/src/utils/MeshTools.h index 688c0cb..5fc4546 100644 --- a/src/utils/MeshTools.h +++ b/src/utils/MeshTools.h @@ -50,4 +50,6 @@ void mergeMeshAsNewGroup(Mesh& lMesh, const Mesh& iMesh); bool isAClosedTopology(const Elements& es); +Mesh extractGroupsByName(const Mesh& mesh, const std::vector& groupNames); + } \ No newline at end of file diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 029b376..c459e88 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -160,3 +160,78 @@ TEST_F(LauncherTest, launches_conformal_cone_case) EXPECT_EQ(exitCode, EXIT_SUCCESS); } +TEST_F(LauncherTest, readObjectsFromJSON_basic) +{ + auto objects = readObjectsFromJSON("testData/cases/multiObject/basic.tessellator.json"); + EXPECT_EQ(objects.size(), 2); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "sphere_group"); + EXPECT_FALSE(objects[0].mesherOverride.has_value()); + EXPECT_EQ(objects[1].filename, "cone.stl"); + EXPECT_EQ(objects[1].group, "cone_group"); + EXPECT_FALSE(objects[1].mesherOverride.has_value()); +} + +TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) +{ + auto objects = readObjectsFromJSON("testData/cases/multiObject/mixedMesher.tessellator.json"); + EXPECT_EQ(objects.size(), 2); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_FALSE(objects[0].mesherOverride.has_value()); + EXPECT_EQ(objects[1].filename, "cone.stl"); + EXPECT_TRUE(objects[1].mesherOverride.has_value()); + EXPECT_EQ(objects[1].mesherOverride.value()["type"], "conformal"); +} + +TEST_F(LauncherTest, readObjectsFromJSON_singleObject) +{ + auto objects = readObjectsFromJSON("testData/cases/multiObject/singleObject.tessellator.json"); + EXPECT_EQ(objects.size(), 1); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "default_group"); +} + +TEST_F(LauncherTest, readObjectsFromJSON_legacyFormat) +{ + auto objects = readObjectsFromJSON("testData/cases/sphere/sphere.tessellator.json"); + EXPECT_EQ(objects.size(), 1); + EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_EQ(objects[0].group, "sphere"); +} + +TEST_F(LauncherTest, launches_multiObject_basic) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/basic.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, launches_multiObject_mixedMesher) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/mixedMesher.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, launches_multiObject_singleObject) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/singleObject.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + +TEST_F(LauncherTest, launches_multiObject_sameFileMultipleGroups) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/multiObject/sameFileMultipleGroups.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + diff --git a/test/utils/MeshToolsTest.cpp b/test/utils/MeshToolsTest.cpp index b46c30a..8078d16 100644 --- a/test/utils/MeshToolsTest.cpp +++ b/test/utils/MeshToolsTest.cpp @@ -720,4 +720,90 @@ TEST_F(MeshToolsTest, reduceGrid_epsilon_coord) } } +TEST_F(MeshToolsTest, extractGroupsByName_singleGroup) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}), + Coordinate({0.0, 1.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1, 2}, Element::Type::Surface)}), + Group("group_b", {Element({0, 1}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a"}); + + EXPECT_EQ(result.groups.size(), 1); + EXPECT_EQ(result.groups[0].name, "group_a"); + EXPECT_EQ(result.groups[0].elements.size(), 1); + EXPECT_EQ(result.coordinates.size(), 3); +} + +TEST_F(MeshToolsTest, extractGroupsByName_multipleGroups) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}), + Coordinate({0.0, 1.0, 0.0}), + Coordinate({1.0, 1.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1, 2}, Element::Type::Surface)}), + Group("group_b", {Element({0, 1}, Element::Type::Line)}), + Group("group_c", {Element({2, 3}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a", "group_c"}); + + EXPECT_EQ(result.groups.size(), 2); + EXPECT_EQ(result.groups[0].name, "group_a"); + EXPECT_EQ(result.groups[0].elements.size(), 1); + EXPECT_EQ(result.groups[1].name, "group_c"); + EXPECT_EQ(result.groups[1].elements.size(), 1); +} + +TEST_F(MeshToolsTest, extractGroupsByName_nonExistentGroup) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a", "non_existent"}); + + EXPECT_EQ(result.groups.size(), 2); + EXPECT_EQ(result.groups[0].name, "group_a"); + EXPECT_EQ(result.groups[0].elements.size(), 1); + EXPECT_EQ(result.groups[1].name, "non_existent"); + EXPECT_EQ(result.groups[1].elements.size(), 0); +} + +TEST_F(MeshToolsTest, extractGroupsByName_coordinateRemapping) +{ + Mesh m; + m.coordinates = { + Coordinate({0.0, 0.0, 0.0}), + Coordinate({1.0, 0.0, 0.0}), + Coordinate({0.0, 1.0, 0.0}) + }; + m.groups = { + Group("group_a", {Element({0, 1, 2}, Element::Type::Surface)}), + Group("group_b", {Element({0, 1}, Element::Type::Line)}) + }; + + Mesh result = extractGroupsByName(m, {"group_a", "group_b"}); + + EXPECT_EQ(result.groups.size(), 2); + EXPECT_EQ(result.groups[0].elements[0].vertices, std::vector({0, 1, 2})); + EXPECT_EQ(result.groups[1].elements[0].vertices, std::vector({3, 4})); + EXPECT_EQ(result.coordinates.size(), 5); +} + } \ No newline at end of file From 4c607f11de1df9604ba13edd60fcb10d3414cdd8 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Mon, 18 May 2026 10:25:12 +0200 Subject: [PATCH 39/61] Document multi-object support, mesher selection, and options in README --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b777cec..c920511 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,13 @@ The main binary is `tessellator`, which uses a tessellator json format, which wi ``` ## JSON Format -The two main entries are as follows: +The main entries are as follows: ### `` This object must always be present and contains the structure of the grid, which will be used to slice and adjust the mesh provided. It must contain one of these two sets of entries: - ``: is an array of three positive integers which indicate the number of cells in each Cartesian direction. In case of having this entry, it also must contain a ``: - - `` is represented by an array which contairs two triplets of integers, representing the minimum and maximum values of the gread in each cartesian direction. + - `` is represented by an array which contains two triplets of integers, representing the minimum and maximum values of the grid in each cartesian direction. ```json "grid": { @@ -80,17 +80,100 @@ This object must always be present and contains the structure of the grid, which } ``` -### `` -This contains the information about the mesh file. It must contain the following entry: +### `` or `` +This contains the information about the mesh file(s). You can specify a single object or multiple objects: -- `filename`: with an string containing the name of the mesh file. Its location is relative to that of the json file. - - Example: +**Single object:** +- `filename`: A string containing the name of the mesh file. Its location is relative to that of the json file. ```json "object": {"filename": "thinCylinder.stl"} ``` +**Multiple objects:** +- `objects`: An array of object definitions. Each object can have: + - `filename`: (required) The mesh file name, relative to the JSON file location + - `group`: (optional) Group name for the object (defaults to filename without extension) + - `mesher`: (optional) Override the global mesher settings for this specific object + +```json + "objects": [ + {"filename": "object1.stl", "group": "group1"}, + {"filename": "object2.stl", "group": "group2", "mesher": {"type": "conformal"}} + ] +``` + +### `` +This optional entry configures the meshing algorithm and its options. If not specified, the staircase mesher is used with default options. + +**Mesher types:** +- `staircase` (default): Generates staircased meshes from geometric inputs +- `conformal`: Creates conformal meshes with fixed-distance grid plane intersections + +**Mesher options:** + +For **staircase** mesher: +- `compress`: (boolean, default: false) Enables surface compression to merge adjacent coplanar quads into larger surfaces + +For **conformal** mesher: +- `edgePoints`: Controls edge point snapping behavior +- `forbiddenLength`: Minimum length threshold for snapping + +**Global options:** +- `exportGrid`: (boolean, default: true) Controls whether to export the grid file + +Example with staircase mesher and compression enabled: +```json + "mesher": { + "type": "staircase", + "options": { + "compress": true, + "exportGrid": true + } + } +``` + +Example with conformal mesher: +```json + "mesher": { + "type": "conformal", + "options": { + "edgePoints": true, + "forbiddenLength": 0.001 + } + } +``` + +### Output Files +The tessellator generates output files with the following naming convention: +- `{group_name}.tessellator.str.vtk` - Staircase meshed object +- `{group_name}.tessellator.cmsh.vtk` - Conformal meshed object +- `{basename}.tessellator.grid.vtk` - Grid file (if `exportGrid` is true) + +### Complete Example +```json +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "objects": [ + {"filename": "sphere.stl"}, + {"filename": "cylinder.stl", "mesher": {"type": "conformal"}} + ], + "mesher": { + "type": "staircase", + "options": { + "compress": true, + "exportGrid": true + } + } +} +``` + ## Contributing ## Citing this work From f09b12b5f79beae690e79ac2988263654d0f8ff8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 11:20:12 +0000 Subject: [PATCH 40/61] Fix Windows JSON path conversion in launcher Agent-Logs-Url: https://github.com/OpenSEMBA/tessellator/sessions/9d4cb838-6891-4b15-a986-f5f0e798125b Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- src/app/launcher.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 4d4174b..d956a44 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -60,8 +60,8 @@ std::vector readObjectsFromJSON(const std::string& fn) if (j.contains("objects")) { for (const auto& obj : j["objects"]) { ObjectDefinition objDef; - objDef.filename = obj["filename"]; - objDef.group = obj.value("group", std::filesystem::path(obj["filename"]).stem().string()); + objDef.filename = obj["filename"].get(); + objDef.group = obj.value("group", std::filesystem::path(objDef.filename).stem().string()); if (obj.contains("mesher")) { objDef.mesherOverride = obj["mesher"]; } @@ -69,8 +69,8 @@ std::vector readObjectsFromJSON(const std::string& fn) } } else if (j.contains("object")) { ObjectDefinition objDef; - objDef.filename = j["object"]["filename"]; - objDef.group = std::filesystem::path(j["object"]["filename"]).stem().string(); + objDef.filename = j["object"]["filename"].get(); + objDef.group = std::filesystem::path(objDef.filename).stem().string(); if (j.contains("mesher")) { objDef.mesherOverride = j["mesher"]; } @@ -306,4 +306,4 @@ int launcher(int argc, const char* argv[]) return EXIT_SUCCESS; } -} \ No newline at end of file +} From c0abc2389193d6870d06d6b762bcb3c7e3be7b1f Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Tue, 21 Jul 2026 15:29:24 +0000 Subject: [PATCH 41/61] WIP | DRAFT | Fix tests after rebase --- .vscode/settings.dev.json | 8 +++++++- test/app/launcherTest.cpp | 2 ++ .../cases/multiObject/basic.tessellator.json | 16 ++++++++++++++++ testData/cases/multiObject/cone.stl | Bin 0 -> 205184 bytes .../multiObject/mixedMesher.tessellator.json | 16 ++++++++++++++++ .../sameFileMultipleGroups.tessellator.json | 16 ++++++++++++++++ .../multiObject/singleObject.tessellator.json | 15 +++++++++++++++ testData/cases/multiObject/sphere.stl | Bin 0 -> 25984 bytes 8 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 testData/cases/multiObject/basic.tessellator.json create mode 100644 testData/cases/multiObject/cone.stl create mode 100644 testData/cases/multiObject/mixedMesher.tessellator.json create mode 100644 testData/cases/multiObject/sameFileMultipleGroups.tessellator.json create mode 100644 testData/cases/multiObject/singleObject.tessellator.json create mode 100644 testData/cases/multiObject/sphere.stl diff --git a/.vscode/settings.dev.json b/.vscode/settings.dev.json index b90d07d..9d190ee 100644 --- a/.vscode/settings.dev.json +++ b/.vscode/settings.dev.json @@ -5,5 +5,11 @@ "testMate.cpp.test.executables": "{build,build-dbg,Build,BUILD,out,Out,OUT}/**/*{test,Test,TEST}*", "testMate.cpp.test.workingDirectory": "${workspaceFolder}", "extensions.ignoreRecommendations": true, - "C_Cpp.intelliSenseEngine": "disabled" + "C_Cpp.intelliSenseEngine": "disabled", + "clangd.arguments": ["-log=verbose", + "-pretty", + "--background-index", + "--query-driver=/usr/bin/g++" + ], + "clangd.fallbackFlags": ["-std=c++17"] } diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index c459e88..0d656d9 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -61,6 +61,8 @@ TEST_F(LauncherTest, launches_alhambra_case) TEST_F(LauncherTest, parses_staircased_without_compression) { int ac = 3; + // ObjectDefinition definition{"longPolyline.vtu", "Cable"}; + // auto mesh = meshlib::app::readMesh("testData/cases/longPolyline/longPolyline.tessellator.json", definition); const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline.tessellator.json" }; int exitCode; diff --git a/testData/cases/multiObject/basic.tessellator.json b/testData/cases/multiObject/basic.tessellator.json new file mode 100644 index 0000000..7923268 --- /dev/null +++ b/testData/cases/multiObject/basic.tessellator.json @@ -0,0 +1,16 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "sphere_group"}, + {"filename": "cone.stl", "group": "cone_group"} + ] +} diff --git a/testData/cases/multiObject/cone.stl b/testData/cases/multiObject/cone.stl new file mode 100644 index 0000000000000000000000000000000000000000..b855cac399afee6aaee522a1e8efc93cd5dc32e4 GIT binary patch literal 205184 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

Qj6w?%hYIL1}#FE*+Sdky5ss+vBd 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 UQr+V#@vZW|gA-QRf*%h5AC+uZBLDyZ literal 0 HcmV?d00001 diff --git a/testData/cases/multiObject/mixedMesher.tessellator.json b/testData/cases/multiObject/mixedMesher.tessellator.json new file mode 100644 index 0000000..23c3d55 --- /dev/null +++ b/testData/cases/multiObject/mixedMesher.tessellator.json @@ -0,0 +1,16 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "sphere_group"}, + {"filename": "cone.stl", "group": "cone_group", "mesher": {"type": "conformal"}} + ] +} diff --git a/testData/cases/multiObject/sameFileMultipleGroups.tessellator.json b/testData/cases/multiObject/sameFileMultipleGroups.tessellator.json new file mode 100644 index 0000000..b1bcd39 --- /dev/null +++ b/testData/cases/multiObject/sameFileMultipleGroups.tessellator.json @@ -0,0 +1,16 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "group_a"}, + {"filename": "sphere.stl", "group": "group_b"} + ] +} diff --git a/testData/cases/multiObject/singleObject.tessellator.json b/testData/cases/multiObject/singleObject.tessellator.json new file mode 100644 index 0000000..9216505 --- /dev/null +++ b/testData/cases/multiObject/singleObject.tessellator.json @@ -0,0 +1,15 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "default_group"} + ] +} diff --git a/testData/cases/multiObject/sphere.stl b/testData/cases/multiObject/sphere.stl new file mode 100644 index 0000000000000000000000000000000000000000..29885a13380c6e8b354e081e448be9bd5d23a7ee GIT binary patch literal 25984 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 literal 0 HcmV?d00001 From 386970ab8a1d3d7b77732b438fef61bad859b114 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Wed, 22 Jul 2026 14:51:26 +0200 Subject: [PATCH 42/61] Adds test to check manifolder behavior --- test/MeshFixtures.h | 33 +++++++++++++++++++++++++++++++++ test/cgal/ManifolderTest.cpp | 14 ++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/test/MeshFixtures.h b/test/MeshFixtures.h index 65c77ad..d2f93d3 100644 --- a/test/MeshFixtures.h +++ b/test/MeshFixtures.h @@ -38,6 +38,39 @@ static Mesh buildNonManifoldPatchMesh(double stepSize) return m; } + +static Mesh buildTetCubeMesh1x1(double stepSize) +{ + Mesh m; + const double boxMin = 0.0; + const double boxMax = 1.0; + const std::size_t gridLinesNum = (std::size_t)((boxMax - boxMin) / (double)stepSize) + 1; + m.grid = utils::GridTools::buildCartesianGrid(boxMin, boxMax, gridLinesNum); + + m.coordinates = { + Coordinate({ 0.0, 0.0, 0.0 }), + Coordinate({ 1.0, 0.0, 0.0 }), + Coordinate({ 0.0, 1.0, 0.0 }), + Coordinate({ 0.0, 0.0, 1.0 }), + Coordinate({ 1.0, 1.0, 0.0 }), + Coordinate({ 1.0, 1.0, 1.0 }), + Coordinate({ 1.0, 0.0, 1.0 }), + Coordinate({ 0.0, 1.0, 1.0 }) + }; + + m.groups = { Group() }; + m.groups[0].elements = { + Element({0, 1, 2, 3}, Element::Type::Volume), + Element({1, 2, 4, 5}, Element::Type::Volume), + Element({1, 2, 3, 5}, Element::Type::Volume), + Element({1, 3, 5, 6}, Element::Type::Volume), + Element({2, 3, 5, 7}, Element::Type::Volume) + }; + + return m; +} + + static Mesh buildTetAndTriMesh(double stepSize) { Mesh m; diff --git a/test/cgal/ManifolderTest.cpp b/test/cgal/ManifolderTest.cpp index cedd1c3..7908aea 100644 --- a/test/cgal/ManifolderTest.cpp +++ b/test/cgal/ManifolderTest.cpp @@ -87,6 +87,20 @@ TEST_F(ManifolderTest, volume_and_surface) ASSERT_EQ(1, r.countElems()); } +TEST_F(ManifolderTest, cube_volume) +{ + Mesh m = buildTetCubeMesh1x1(1.0); + + Manifolder mani(m); + + ASSERT_EQ(12, mani.getClosedSurfacesMesh().countElems()); + // EXPECT_EQ(4, usedDifferentCoords(mani.getClosedSurfacesMesh()).size()); + + Mesh r = mani.getOpenSurfacesMesh(); + ASSERT_EQ(0, r.groups.size()); + ASSERT_EQ(0, r.countElems()); +} + TEST_F(ManifolderTest, closed_surface) { Mesh m = buildCubeSurfaceMesh(1.0); From e9a186c4e2d95344d4d650a7a28668b2e74b6e36 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Wed, 22 Jul 2026 16:25:23 +0200 Subject: [PATCH 43/61] Enabling tetrahedron reading and processing --- src/app/vtkIO.cpp | 14 ++++++++++++ src/meshers/StaircaseMesher.cpp | 7 ++++++ test/MeshFixtures.h | 32 ---------------------------- test/cgal/ManifolderTest.cpp | 13 ----------- test/meshers/StaircaseMesherTest.cpp | 13 +++++++++++ 5 files changed, 34 insertions(+), 45 deletions(-) diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index d55a561..094e8cc 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -76,6 +77,7 @@ Element vtkCellToElement(vtkCell* cell) vtkVertex* vertex = nullptr; vtkLine* line = nullptr; vtkTriangle* triangle = nullptr; + vtkTetra* tetra = nullptr; switch (cell->GetCellType()) { case VTK_VERTEX: @@ -102,6 +104,17 @@ Element vtkCellToElement(vtkCell* cell) }; elem.type = meshlib::Element::Type::Surface; break; + + case VTK_TETRA: + tetra = vtkTetra::SafeDownCast(cell); + elem.vertices = { + CoordinateId(tetra->GetPointIds()->GetId(0)), + CoordinateId(tetra->GetPointIds()->GetId(1)), + CoordinateId(tetra->GetPointIds()->GetId(2)), + CoordinateId(tetra->GetPointIds()->GetId(3)) + }; + elem.type = meshlib::Element::Type::Volume; + break; } return elem; @@ -131,6 +144,7 @@ Mesh vtuToMesh(vtkUnstructuredGrid* vtu) } } else { mesh.groups.resize(1); + auto k = vtu->GetNumberOfCells(); mesh.groups[0].elements.reserve(vtu->GetNumberOfCells()); for (vtkIdType i = 0; i < vtu->GetNumberOfCells(); i++) { mesh.groups[0].elements.push_back( diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 6e9034d..aab9f75 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -9,6 +9,7 @@ #include "core/Compressor.h" #include "cgal/filler/Filler.h" +#include "cgal/Manifolder.h" #include "utils/RedundancyCleaner.h" #include "utils/MeshTools.h" @@ -26,6 +27,7 @@ StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInColla opts_(opts) { log("Preparing surfaces."); + //here, convert tetra intro hull tris surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); log("Processing surface mesh."); @@ -53,8 +55,13 @@ void StaircaseMesher::process(Mesh& mesh) const auto dimensions = getHighestDimensionByGroup(mesh); + //mani has open_ and closed_ for every group id if (opts_.isVolume){ if (meshTools::isAClosedTopology(mesh.groups[0].elements)){ + + auto mani = meshlib::cgal::Manifolder(mesh); + mesh.groups[0].elements = mani.getClosedSurfacesMesh().groups[0].elements; + meshlib::cgal::filler::Filler f{ mesh }; auto filling = f.getMeshFilling(); mergeMesh(mesh, filling); diff --git a/test/MeshFixtures.h b/test/MeshFixtures.h index d2f93d3..7eda7c0 100644 --- a/test/MeshFixtures.h +++ b/test/MeshFixtures.h @@ -39,38 +39,6 @@ static Mesh buildNonManifoldPatchMesh(double stepSize) } -static Mesh buildTetCubeMesh1x1(double stepSize) -{ - Mesh m; - const double boxMin = 0.0; - const double boxMax = 1.0; - const std::size_t gridLinesNum = (std::size_t)((boxMax - boxMin) / (double)stepSize) + 1; - m.grid = utils::GridTools::buildCartesianGrid(boxMin, boxMax, gridLinesNum); - - m.coordinates = { - Coordinate({ 0.0, 0.0, 0.0 }), - Coordinate({ 1.0, 0.0, 0.0 }), - Coordinate({ 0.0, 1.0, 0.0 }), - Coordinate({ 0.0, 0.0, 1.0 }), - Coordinate({ 1.0, 1.0, 0.0 }), - Coordinate({ 1.0, 1.0, 1.0 }), - Coordinate({ 1.0, 0.0, 1.0 }), - Coordinate({ 0.0, 1.0, 1.0 }) - }; - - m.groups = { Group() }; - m.groups[0].elements = { - Element({0, 1, 2, 3}, Element::Type::Volume), - Element({1, 2, 4, 5}, Element::Type::Volume), - Element({1, 2, 3, 5}, Element::Type::Volume), - Element({1, 3, 5, 6}, Element::Type::Volume), - Element({2, 3, 5, 7}, Element::Type::Volume) - }; - - return m; -} - - static Mesh buildTetAndTriMesh(double stepSize) { Mesh m; diff --git a/test/cgal/ManifolderTest.cpp b/test/cgal/ManifolderTest.cpp index 7908aea..c05f1c5 100644 --- a/test/cgal/ManifolderTest.cpp +++ b/test/cgal/ManifolderTest.cpp @@ -87,19 +87,6 @@ TEST_F(ManifolderTest, volume_and_surface) ASSERT_EQ(1, r.countElems()); } -TEST_F(ManifolderTest, cube_volume) -{ - Mesh m = buildTetCubeMesh1x1(1.0); - - Manifolder mani(m); - - ASSERT_EQ(12, mani.getClosedSurfacesMesh().countElems()); - // EXPECT_EQ(4, usedDifferentCoords(mani.getClosedSurfacesMesh()).size()); - - Mesh r = mani.getOpenSurfacesMesh(); - ASSERT_EQ(0, r.groups.size()); - ASSERT_EQ(0, r.countElems()); -} TEST_F(ManifolderTest, closed_surface) { diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index 6d1dfdb..eb2af21 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -369,6 +369,19 @@ TEST_F(StaircaseMesherTest, testStaircaseWithCompression) EXPECT_EQ(0, countMeshElementsIf(compressedMesh, isNode)); } +TEST_F(StaircaseMesherTest, mesh_tetrahedron_volume){ + + Mesh m = buildCubeVolumeMesh(1.0); + meshlib::meshers::StaircaseMesherOptions opts; + opts.isVolume = true; + auto staircasedMesh = StaircaseMesher{m, 4, opts }.mesh(); + + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); + EXPECT_EQ(12, countMeshElementsIf(staircasedMesh, isQuad)); + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTetrahedron)); + +} + #if APP_LOADED TEST_F(StaircaseMesherTest, fills_closed_volume_with_quads) From 896cf42ee74df5e28fda8e40ce65792a6d1f29a7 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Thu, 23 Jul 2026 09:57:07 +0200 Subject: [PATCH 44/61] adds test before code implementation --- test/meshers/StaircaseMesherTest.cpp | 36 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index eb2af21..c8a1dab 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -369,15 +369,43 @@ TEST_F(StaircaseMesherTest, testStaircaseWithCompression) EXPECT_EQ(0, countMeshElementsIf(compressedMesh, isNode)); } -TEST_F(StaircaseMesherTest, mesh_tetrahedron_volume){ +TEST_F(StaircaseMesherTest, mesh_tetrahedron_volume_2x2){ - Mesh m = buildCubeVolumeMesh(1.0); + Mesh m = buildCubeVolumeMesh(0.5); meshlib::meshers::StaircaseMesherOptions opts; - opts.isVolume = true; + opts.volumeGroups.insert(0); + // opts.isVolume = true; + auto staircasedMesh = StaircaseMesher{m, 4, opts }.mesh(); + + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); + EXPECT_EQ(36, countMeshElementsIf(staircasedMesh, isQuad)); + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTetrahedron)); + +} + +TEST_F(StaircaseMesherTest, mesh_surface_volume_2x2){ + + Mesh m = buildCubeSurfaceMesh(0.5); + meshlib::meshers::StaircaseMesherOptions opts; + opts.volumeGroups.insert(0); + // opts.isVolume = true; + auto staircasedMesh = StaircaseMesher{m, 4, opts }.mesh(); + + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); + EXPECT_EQ(36, countMeshElementsIf(staircasedMesh, isQuad)); + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTetrahedron)); + +} + +TEST_F(StaircaseMesherTest, mesh_surface_not_volume_2x2){ + + Mesh m = buildCubeSurfaceMesh(0.5); + meshlib::meshers::StaircaseMesherOptions opts; + // opts.isVolume = true; auto staircasedMesh = StaircaseMesher{m, 4, opts }.mesh(); EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); - EXPECT_EQ(12, countMeshElementsIf(staircasedMesh, isQuad)); + EXPECT_EQ(24, countMeshElementsIf(staircasedMesh, isQuad)); EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTetrahedron)); } From 3f436b7fe5db50150f51c5c0ace319b993d2cf44 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Thu, 23 Jul 2026 12:56:12 +0000 Subject: [PATCH 45/61] Tessellator | Container | Debug in docker --- .vscode/settings.dev.json | 12 +++++++++++- CMakePresets.json | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.dev.json b/.vscode/settings.dev.json index b90d07d..5303634 100644 --- a/.vscode/settings.dev.json +++ b/.vscode/settings.dev.json @@ -5,5 +5,15 @@ "testMate.cpp.test.executables": "{build,build-dbg,Build,BUILD,out,Out,OUT}/**/*{test,Test,TEST}*", "testMate.cpp.test.workingDirectory": "${workspaceFolder}", "extensions.ignoreRecommendations": true, - "C_Cpp.intelliSenseEngine": "disabled" + "C_Cpp.intelliSenseEngine": "disabled", + "clangd.arguments": ["-log=verbose", + "-pretty", + "--background-index", + "--query-driver=/usr/bin/g++" + ], + "clangd.fallbackFlags": ["-std=c++17"], + "lldb.showDisassembly": "never", + "lldb.launch.preRunCommands": [ + "settings set target.import-std-module true" + ] } diff --git a/CMakePresets.json b/CMakePresets.json index 6cfa514..769d22e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -69,7 +69,8 @@ "CMAKE_FIND_ROOT_PATH": "/usr/local", "TESSELLATOR_ENABLE_TESTS": "ON", "TESSELLATOR_ENABLE_CGAL": "ON", - "DOCKER_EXPORT_COMPILE_COMMANDS": "ON" + "DOCKER_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_CXX_FLAGS_INIT": "-g3" }, "environment": { "EIGEN3_INCLUDE_DIR": "/usr/include/eigen3" From e9f5546a183e57de3dd9cbbf749bbf1cd14890ea Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Tue, 21 Jul 2026 15:29:24 +0000 Subject: [PATCH 46/61] Tessellator | Testing | Fix tests after rebase --- src/app/launcher.cpp | 34 ++++++++++-------- src/app/launcher.h | 3 +- test/app/launcherTest.cpp | 11 +++++- .../cases/multiObject/basic.tessellator.json | 16 +++++++++ testData/cases/multiObject/cone.stl | Bin 0 -> 205184 bytes .../multiObject/mixedMesher.tessellator.json | 16 +++++++++ .../sameFileMultipleGroups.tessellator.json | 16 +++++++++ .../multiObject/singleObject.tessellator.json | 15 ++++++++ testData/cases/multiObject/sphere.stl | Bin 0 -> 25984 bytes 9 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 testData/cases/multiObject/basic.tessellator.json create mode 100644 testData/cases/multiObject/cone.stl create mode 100644 testData/cases/multiObject/mixedMesher.tessellator.json create mode 100644 testData/cases/multiObject/sameFileMultipleGroups.tessellator.json create mode 100644 testData/cases/multiObject/singleObject.tessellator.json create mode 100644 testData/cases/multiObject/sphere.stl diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index d956a44..68dd532 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -1,6 +1,7 @@ #include "launcher.h" #include "vtkIO.h" +#include "meshers/MesherBase.h" #include "meshers/StaircaseMesher.h" #include "meshers/ConformalMesher.h" #include "utils/GridTools.h" @@ -14,7 +15,6 @@ #include #include #include -#include #include namespace meshlib::app { @@ -62,6 +62,9 @@ std::vector readObjectsFromJSON(const std::string& fn) ObjectDefinition objDef; objDef.filename = obj["filename"].get(); objDef.group = obj.value("group", std::filesystem::path(objDef.filename).stem().string()); + if (obj.contains("volume")){ + objDef.isVolume = obj["volume"]; + } if (obj.contains("mesher")) { objDef.mesherOverride = obj["mesher"]; } @@ -71,6 +74,9 @@ std::vector readObjectsFromJSON(const std::string& fn) ObjectDefinition objDef; objDef.filename = j["object"]["filename"].get(); objDef.group = std::filesystem::path(objDef.filename).stem().string(); + if (j["object"].contains("volume")){ + objDef.isVolume = j["object"]["volume"]; + } if (j.contains("mesher")) { objDef.mesherOverride = j["mesher"]; } @@ -104,9 +110,9 @@ Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef) if (res.groups.empty()) { res.groups.push_back(Group{objDef.group, {}}); } else { - res = utils::meshTools::extractGroupsByName(res, {objDef.group}); - if (res.groups.empty()) { - res.groups.push_back(Group{objDef.group, {}}); + auto auxResult = utils::meshTools::extractGroupsByName(res, {objDef.group}); + if (auxResult.countElems() != 0) { + return auxResult; } else { res.groups[0].name = objDef.group; } @@ -152,17 +158,17 @@ std::string readExtension(const std::string& fn, const std::optional> j; } + meshlib::meshers::StaircaseMesherOptions res; - if (j["object"].contains("volume")) { - res.isVolume = j["object"]["volume"]; - } + + res.isVolume = isVolume; if (j["mesher"].contains("options") && j["mesher"]["options"].contains("compress")) { res.compress = j["mesher"]["options"]["compress"]; @@ -238,15 +244,15 @@ bool readExportGridOption(const std::string& fn, const std::optional buildMesher(const Mesh& in, const std::string& fn, const std::optional& override) +std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const ObjectDefinition& objDef) { - auto mesherType = readMesherType(fn, override); + auto mesherType = readMesherType(fn, objDef.mesherOverride); if (mesherType == meshlib::app::staircase_mesher) { - auto staircasedOptions = readStaircaseMesherOptions(fn); - staircasedOptions.compress = readStaircaseMesherCompressOption(fn, override); + auto staircasedOptions = readStaircaseMesherOptions(fn, objDef.isVolume); + staircasedOptions.compress = readStaircaseMesherCompressOption(fn, objDef.mesherOverride); return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, staircasedOptions}); } else if (mesherType == meshlib::app::conformal_mesher) { - return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn, override)}); + return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn, objDef.mesherOverride)}); } else { throw std::runtime_error("Unsupported mesher type"); } @@ -284,7 +290,7 @@ int launcher(int argc, const char* argv[]) Mesh mesh = readMesh(inputFilename, objDef); - auto mesher = buildMesher(mesh, inputFilename, objDef.mesherOverride); + auto mesher = buildMesher(mesh, inputFilename, objDef); Mesh resultMesh = mesher->mesh(); if (first) { diff --git a/src/app/launcher.h b/src/app/launcher.h index bf65b34..2fd828e 100644 --- a/src/app/launcher.h +++ b/src/app/launcher.h @@ -15,6 +15,7 @@ const std::string staircase_mesher ("staircase"); struct ObjectDefinition { std::string filename; std::string group; + bool isVolume = false; std::optional mesherOverride; }; @@ -22,6 +23,6 @@ int launcher(int argc, const char* argv[]); Grid parseGridFromJSON(const nlohmann::json& j); std::vector readObjectsFromJSON(const std::string& fn); Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef); -std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const std::optional& override); +std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const ObjectDefinition& objDef); } \ No newline at end of file diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index c459e88..5c3ef6f 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -61,6 +61,8 @@ TEST_F(LauncherTest, launches_alhambra_case) TEST_F(LauncherTest, parses_staircased_without_compression) { int ac = 3; + // ObjectDefinition definition{"longPolyline.vtu", "Cable"}; + // auto mesh = meshlib::app::readMesh("testData/cases/longPolyline/longPolyline.tessellator.json", definition); const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline.tessellator.json" }; int exitCode; @@ -166,9 +168,11 @@ TEST_F(LauncherTest, readObjectsFromJSON_basic) EXPECT_EQ(objects.size(), 2); EXPECT_EQ(objects[0].filename, "sphere.stl"); EXPECT_EQ(objects[0].group, "sphere_group"); + EXPECT_TRUE(objects[0].isVolume); EXPECT_FALSE(objects[0].mesherOverride.has_value()); EXPECT_EQ(objects[1].filename, "cone.stl"); EXPECT_EQ(objects[1].group, "cone_group"); + EXPECT_FALSE(objects[1].isVolume); EXPECT_FALSE(objects[1].mesherOverride.has_value()); } @@ -177,8 +181,11 @@ TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) auto objects = readObjectsFromJSON("testData/cases/multiObject/mixedMesher.tessellator.json"); EXPECT_EQ(objects.size(), 2); EXPECT_EQ(objects[0].filename, "sphere.stl"); + EXPECT_FALSE(objects[0].isVolume); EXPECT_FALSE(objects[0].mesherOverride.has_value()); + EXPECT_EQ(objects[1].filename, "cone.stl"); + EXPECT_FALSE(objects[1].isVolume); EXPECT_TRUE(objects[1].mesherOverride.has_value()); EXPECT_EQ(objects[1].mesherOverride.value()["type"], "conformal"); } @@ -189,14 +196,16 @@ TEST_F(LauncherTest, readObjectsFromJSON_singleObject) EXPECT_EQ(objects.size(), 1); EXPECT_EQ(objects[0].filename, "sphere.stl"); EXPECT_EQ(objects[0].group, "default_group"); + EXPECT_FALSE(objects[0].isVolume); } TEST_F(LauncherTest, readObjectsFromJSON_legacyFormat) { - auto objects = readObjectsFromJSON("testData/cases/sphere/sphere.tessellator.json"); + auto objects = readObjectsFromJSON("testData/cases/sphere/closed_sphere.tessellator.json"); EXPECT_EQ(objects.size(), 1); EXPECT_EQ(objects[0].filename, "sphere.stl"); EXPECT_EQ(objects[0].group, "sphere"); + EXPECT_TRUE(objects[0].isVolume); } TEST_F(LauncherTest, launches_multiObject_basic) diff --git a/testData/cases/multiObject/basic.tessellator.json b/testData/cases/multiObject/basic.tessellator.json new file mode 100644 index 0000000..5724c92 --- /dev/null +++ b/testData/cases/multiObject/basic.tessellator.json @@ -0,0 +1,16 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "volume": true, "group": "sphere_group"}, + {"filename": "cone.stl", "group": "cone_group"} + ] +} diff --git a/testData/cases/multiObject/cone.stl b/testData/cases/multiObject/cone.stl new file mode 100644 index 0000000000000000000000000000000000000000..b855cac399afee6aaee522a1e8efc93cd5dc32e4 GIT binary patch literal 205184 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

Qj6w?%hYIL1}#FE*+Sdky5ss+vBd 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 UQr+V#@vZW|gA-QRf*%h5AC+uZBLDyZ literal 0 HcmV?d00001 diff --git a/testData/cases/multiObject/mixedMesher.tessellator.json b/testData/cases/multiObject/mixedMesher.tessellator.json new file mode 100644 index 0000000..23c3d55 --- /dev/null +++ b/testData/cases/multiObject/mixedMesher.tessellator.json @@ -0,0 +1,16 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "sphere_group"}, + {"filename": "cone.stl", "group": "cone_group", "mesher": {"type": "conformal"}} + ] +} diff --git a/testData/cases/multiObject/sameFileMultipleGroups.tessellator.json b/testData/cases/multiObject/sameFileMultipleGroups.tessellator.json new file mode 100644 index 0000000..b1bcd39 --- /dev/null +++ b/testData/cases/multiObject/sameFileMultipleGroups.tessellator.json @@ -0,0 +1,16 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "group_a"}, + {"filename": "sphere.stl", "group": "group_b"} + ] +} diff --git a/testData/cases/multiObject/singleObject.tessellator.json b/testData/cases/multiObject/singleObject.tessellator.json new file mode 100644 index 0000000..9216505 --- /dev/null +++ b/testData/cases/multiObject/singleObject.tessellator.json @@ -0,0 +1,15 @@ +{ + "grid": { + "numberOfCells": [50, 50, 50], + "boundingBox": [ + [-100.0, -100.0, -100.0], + [ 100.0, 100.0, 100.0] + ] + }, + "mesher": { + "type": "staircase" + }, + "objects": [ + {"filename": "sphere.stl", "group": "default_group"} + ] +} diff --git a/testData/cases/multiObject/sphere.stl b/testData/cases/multiObject/sphere.stl new file mode 100644 index 0000000000000000000000000000000000000000..29885a13380c6e8b354e081e448be9bd5d23a7ee GIT binary patch literal 25984 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 literal 0 HcmV?d00001 From 9e2f363480df9c2a5d3705841f40edaad67893ab Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Wed, 22 Jul 2026 16:16:54 +0000 Subject: [PATCH 47/61] Tessellator | App | Upgrade structured mesher options for multi-object override --- src/app/launcher.cpp | 44 +++----- src/meshers/ConformalMesher.h | 2 + src/meshers/MesherBase.h | 1 + src/meshers/StaircaseMesher.h | 1 + test/app/launcherTest.cpp | 100 ++++++++++++++---- .../multiObject/mixedMesher.tessellator.json | 5 +- 6 files changed, 103 insertions(+), 50 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 68dd532..16552ce 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -158,20 +158,28 @@ std::string readExtension(const std::string& fn, const std::optional& override) { nlohmann::json j; { std::ifstream i(fn); i >> j; } + + nlohmann::json mesherConfig; + if (override.has_value()) { + mesherConfig = *override; + } else if (j.contains("mesher")) { + mesherConfig = j["mesher"]; + } - meshlib::meshers::StaircaseMesherOptions res; + meshlib::meshers::StaircaseMesherOptions res; + res.isVolume = isVolume; - if (j["mesher"].contains("options") && - j["mesher"]["options"].contains("compress")) { - res.compress = j["mesher"]["options"]["compress"]; + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("compress")) { + res.compress = mesherConfig["options"]["compress"]; } return res; @@ -200,28 +208,6 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::s return res; } -bool readStaircaseMesherCompressOption(const std::string& fn, const std::optional& override) -{ - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - - nlohmann::json mesherConfig; - if (override.has_value()) { - mesherConfig = *override; - } else if (j.contains("mesher")) { - mesherConfig = j["mesher"]; - } - - if (mesherConfig.contains("options") && - mesherConfig["options"].contains("compress")) { - return mesherConfig["options"]["compress"]; - } - return false; -} - bool readExportGridOption(const std::string& fn, const std::optional& override) { nlohmann::json j; @@ -248,9 +234,7 @@ std::unique_ptr buildMesher(const Mesh& in, const { auto mesherType = readMesherType(fn, objDef.mesherOverride); if (mesherType == meshlib::app::staircase_mesher) { - auto staircasedOptions = readStaircaseMesherOptions(fn, objDef.isVolume); - staircasedOptions.compress = readStaircaseMesherCompressOption(fn, objDef.mesherOverride); - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, staircasedOptions}); + return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn, objDef.isVolume, objDef.mesherOverride)}); } else if (mesherType == meshlib::app::conformal_mesher) { return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn, objDef.mesherOverride)}); } else { diff --git a/src/meshers/ConformalMesher.h b/src/meshers/ConformalMesher.h index 8fe5540..26a9858 100644 --- a/src/meshers/ConformalMesher.h +++ b/src/meshers/ConformalMesher.h @@ -17,6 +17,8 @@ class ConformalMesher : public MesherBase { virtual ~ConformalMesher() = default; Mesh mesh() const; + + const ConformalMesherOptions & getOptions() const { return opts_; } static std::set findNonConformalCells(const Mesh& mesh); static std::set cellsWithMoreThanAVertexInsideEdge(const Mesh& mesh); diff --git a/src/meshers/MesherBase.h b/src/meshers/MesherBase.h index 938f949..c0bac3e 100644 --- a/src/meshers/MesherBase.h +++ b/src/meshers/MesherBase.h @@ -11,6 +11,7 @@ class MesherBase { MesherBase(const Mesh& in); virtual ~MesherBase() = default; virtual Mesh mesh() const = 0; + const MesherBaseOptions & getOptions() const { return opts_; } protected: virtual void process(Mesh&) const = 0; diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index df0b9bb..3cabd4b 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -11,6 +11,7 @@ class StaircaseMesher : public MesherBase { StaircaseMesher(const Mesh& in, int decimalPlacesInCollapser = 4, StaircaseMesherOptions opts = StaircaseMesherOptions()); virtual ~StaircaseMesher() = default; Mesh mesh() const; + const StaircaseMesherOptions & getOptions() const { return opts_; } private: int decimalPlacesInCollapser_; diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 5c3ef6f..536e8f0 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -2,6 +2,8 @@ #include "app/launcher.h" #include "types/Mesh.h" +#include "meshers/StaircaseMesher.h" + #include #include @@ -58,26 +60,58 @@ TEST_F(LauncherTest, launches_alhambra_case) EXPECT_EQ(exitCode, EXIT_SUCCESS); } -TEST_F(LauncherTest, parses_staircased_without_compression) +TEST_F(LauncherTest, builds_staircased_mesher_default) { - int ac = 3; - // ObjectDefinition definition{"longPolyline.vtu", "Cable"}; - // auto mesh = meshlib::app::readMesh("testData/cases/longPolyline/longPolyline.tessellator.json", definition); - const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline.tessellator.json" }; - int exitCode; - - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); - EXPECT_EQ(exitCode, EXIT_SUCCESS); + meshlib::Mesh meshMock; + meshMock.grid = { + std::vector{0, 1}, + std::vector{0, 1}, + std::vector{0, 1} + }; + + ObjectDefinition objDef; + auto mesher = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline_legacy.tessellator.json", objDef); + + EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); + const auto & options = dynamic_cast(*mesher).getOptions(); + EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.compress, false); } -TEST_F(LauncherTest, parses_staircased_with_compression) +TEST_F(LauncherTest, builds_staircased_mesher_without_compression) { - int ac = 3; - const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline_compression.tessellator.json" }; - int exitCode; - - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); - EXPECT_EQ(exitCode, EXIT_SUCCESS); + meshlib::Mesh meshMock; + meshMock.grid = { + std::vector{0, 1}, + std::vector{0, 1}, + std::vector{0, 1} + }; + + ObjectDefinition objDef; + auto mesher = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline.tessellator.json", objDef); + + EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); + const auto & options = dynamic_cast(*mesher).getOptions(); + EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.compress, false); +} + +TEST_F(LauncherTest, builds_staircased_mesher_with_compression) +{ + meshlib::Mesh meshMock; + meshMock.grid = { + std::vector{0, 1}, + std::vector{0, 1}, + std::vector{0, 1} + }; + + ObjectDefinition objDef; + auto mesher = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline_compression.tessellator.json", objDef); + + EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); + const auto & options = dynamic_cast(*mesher).getOptions(); + EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.compress, true); } TEST_F(LauncherTest, launches_conformal_alhambra_case) @@ -179,15 +213,24 @@ TEST_F(LauncherTest, readObjectsFromJSON_basic) TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) { auto objects = readObjectsFromJSON("testData/cases/multiObject/mixedMesher.tessellator.json"); - EXPECT_EQ(objects.size(), 2); + EXPECT_EQ(objects.size(), 3); + EXPECT_EQ(objects[0].filename, "sphere.stl"); EXPECT_FALSE(objects[0].isVolume); - EXPECT_FALSE(objects[0].mesherOverride.has_value()); + EXPECT_TRUE(objects[0].mesherOverride.has_value()); + EXPECT_EQ(objects[0].mesherOverride.value()["type"], "staircase"); + EXPECT_TRUE(objects[0].mesherOverride.value().contains("options")); + EXPECT_TRUE(objects[0].mesherOverride.value()["options"].contains("compress")); + EXPECT_TRUE(objects[0].mesherOverride.value()["options"]["compress"]); EXPECT_EQ(objects[1].filename, "cone.stl"); EXPECT_FALSE(objects[1].isVolume); EXPECT_TRUE(objects[1].mesherOverride.has_value()); EXPECT_EQ(objects[1].mesherOverride.value()["type"], "conformal"); + + EXPECT_EQ(objects[2].filename, "cone.stl"); + EXPECT_FALSE(objects[2].isVolume); + EXPECT_FALSE(objects[2].mesherOverride.has_value()); } TEST_F(LauncherTest, readObjectsFromJSON_singleObject) @@ -208,6 +251,27 @@ TEST_F(LauncherTest, readObjectsFromJSON_legacyFormat) EXPECT_TRUE(objects[0].isVolume); } +TEST_F(LauncherTest, builds_staircased_mesher_with_override) +{ + meshlib::Mesh meshMock; + meshMock.grid = { + std::vector{0, 1}, + std::vector{0, 1}, + std::vector{0, 1} + }; + + std::string filename = "testData/cases/multiObject/mixedMesher.tessellator.json"; + + auto objects = readObjectsFromJSON(filename); + + auto mesher = meshlib::app::buildMesher(meshMock, filename, objects[0]); + + EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); + const auto & options = dynamic_cast(*mesher).getOptions(); + EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.compress, true); +} + TEST_F(LauncherTest, launches_multiObject_basic) { int ac = 3; diff --git a/testData/cases/multiObject/mixedMesher.tessellator.json b/testData/cases/multiObject/mixedMesher.tessellator.json index 23c3d55..ca2afa7 100644 --- a/testData/cases/multiObject/mixedMesher.tessellator.json +++ b/testData/cases/multiObject/mixedMesher.tessellator.json @@ -10,7 +10,8 @@ "type": "staircase" }, "objects": [ - {"filename": "sphere.stl", "group": "sphere_group"}, - {"filename": "cone.stl", "group": "cone_group", "mesher": {"type": "conformal"}} + {"filename": "sphere.stl", "group": "sphere_group", "mesher": {"type": "staircase", "options": { "compress" : true} } }, + {"filename": "cone.stl", "group": "cone_group", "mesher": {"type": "conformal"}}, + {"filename": "cone.stl", "group": "default_group"} ] } From d6f9303f5f4bba9539804723ed784262c3bd8cc6 Mon Sep 17 00:00:00 2001 From: carlosgonzalez-elemwave Date: Thu, 23 Jul 2026 09:24:12 +0000 Subject: [PATCH 48/61] Tessellator | App | Apply PR requested changes --- src/app/launcher.cpp | 156 ++++++++++++++++---------------------- src/app/launcher.h | 6 +- src/utils/MeshTools.cpp | 4 - test/app/launcherTest.cpp | 117 +++++++++++++++++++--------- 4 files changed, 150 insertions(+), 133 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 16552ce..6ce1de5 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -24,20 +24,20 @@ using namespace vtkIO; namespace po = boost::program_options; -Grid parseGridFromJSON(const nlohmann::json &j) +Grid parseGridFromJSON(const nlohmann::json &fileData) { - if (j.find("planes") != j.end()) { - return j["planes"]; + if (fileData.find("planes") != fileData.end()) { + return fileData["planes"]; } else { std::array nCells = { - j["numberOfCells"][0], - j["numberOfCells"][1], - j["numberOfCells"][2] + fileData["numberOfCells"][0], + fileData["numberOfCells"][1], + fileData["numberOfCells"][2] }; std::array min, max; - min = j["boundingBox"][0]; - max = j["boundingBox"][1]; + min = fileData["boundingBox"][0]; + max = fileData["boundingBox"][1]; return { utils::GridTools::linspace(min[0], max[0], nCells[0] + 1), @@ -47,18 +47,12 @@ Grid parseGridFromJSON(const nlohmann::json &j) } } -std::vector readObjectsFromJSON(const std::string& fn) +std::vector readObjectsFromJSON(const nlohmann::json& fileData) { - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - std::vector objects; - if (j.contains("objects")) { - for (const auto& obj : j["objects"]) { + if (fileData.contains("objects")) { + for (const auto& obj : fileData["objects"]) { ObjectDefinition objDef; objDef.filename = obj["filename"].get(); objDef.group = obj.value("group", std::filesystem::path(objDef.filename).stem().string()); @@ -70,15 +64,15 @@ std::vector readObjectsFromJSON(const std::string& fn) } objects.push_back(objDef); } - } else if (j.contains("object")) { + } else if (fileData.contains("object")) { ObjectDefinition objDef; - objDef.filename = j["object"]["filename"].get(); + objDef.filename = fileData["object"]["filename"].get(); objDef.group = std::filesystem::path(objDef.filename).stem().string(); - if (j["object"].contains("volume")){ - objDef.isVolume = j["object"]["volume"]; + if (fileData["object"].contains("volume")){ + objDef.isVolume = fileData["object"]["volume"]; } - if (j.contains("mesher")) { - objDef.mesherOverride = j["mesher"]; + if (fileData.contains("mesher")) { + objDef.mesherOverride = fileData["mesher"]; } objects.push_back(objDef); } else { @@ -88,23 +82,16 @@ std::vector readObjectsFromJSON(const std::string& fn) return objects; } -Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef) +Mesh readMesh(const nlohmann::json& fileData, const std::filesystem::path& folderPath, const ObjectDefinition& objDef) { - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - - std::filesystem::path caseFolder = std::filesystem::path(fn).parent_path(); - std::filesystem::path meshObjectPath = caseFolder / objDef.filename; + std::filesystem::path meshObjectPath = folderPath / objDef.filename; std::cout << "-- Reading mesh groups from: " << meshObjectPath; Mesh res = vtkIO::readInputMesh(meshObjectPath); std::cout << "....... [OK]" << std::endl; std::cout << "-- Reading grid from input file"; - res.grid = parseGridFromJSON(j["grid"]); + res.grid = parseGridFromJSON(fileData["grid"]); std::cout << "....... [OK]" << std::endl; if (res.groups.empty()) { @@ -122,19 +109,14 @@ Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef) } -std::string readMesherType(const std::string& fn, const std::optional& override) -{ - nlohmann::json j; +std::string readMesherType(const nlohmann::json& fileData, const std::optional& override) { - std::ifstream i(fn); - i >> j; - } - nlohmann::json mesherConfig; + if (override.has_value()) { mesherConfig = *override; - } else if (j.contains("mesher")) { - mesherConfig = j["mesher"]; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } else { return meshlib::app::staircase_mesher; } @@ -146,9 +128,9 @@ std::string readMesherType(const std::string& fn, const std::optional& override) +std::string readExtension(const nlohmann::json& fileData, const std::optional& override) { - auto mesherType = readMesherType(fn, override); + auto mesherType = readMesherType(fileData, override); if (mesherType == meshlib::app::staircase_mesher) { return "str"; } else if (mesherType == meshlib::app::conformal_mesher) { @@ -158,22 +140,15 @@ std::string readExtension(const std::string& fn, const std::optional& override) -{ - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - +meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const nlohmann::json &fileData, bool isVolume, const std::optional& override) +{ nlohmann::json mesherConfig; if (override.has_value()) { mesherConfig = *override; - } else if (j.contains("mesher")) { - mesherConfig = j["mesher"]; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } - meshlib::meshers::StaircaseMesherOptions res; res.isVolume = isVolume; @@ -185,19 +160,13 @@ meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const std::s return res; } -meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::string& fn, const std::optional& override) -{ - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - +meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const nlohmann::json& fileData, const std::optional& override) +{ nlohmann::json mesherConfig; if (override.has_value()) { mesherConfig = *override; - } else if (j.contains("mesher")) { - mesherConfig = j["mesher"]; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } meshlib::meshers::ConformalMesherOptions res; @@ -208,19 +177,13 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const std::s return res; } -bool readExportGridOption(const std::string& fn, const std::optional& override) -{ - nlohmann::json j; - { - std::ifstream i(fn); - i >> j; - } - +bool readExportGridOption(const nlohmann::json& fileData, const std::optional& override) +{ nlohmann::json mesherConfig; if (override.has_value()) { mesherConfig = *override; - } else if (j.contains("mesher")) { - mesherConfig = j["mesher"]; + } else if (fileData.contains("mesher")) { + mesherConfig = fileData["mesher"]; } if (mesherConfig.contains("options") && @@ -230,13 +193,18 @@ bool readExportGridOption(const std::string& fn, const std::optional buildMesher(const Mesh& in, const std::string& fn, const ObjectDefinition& objDef) +std::unique_ptr buildMesher(const Mesh& in, const nlohmann::json & fileData, const ObjectDefinition& objDef) { - auto mesherType = readMesherType(fn, objDef.mesherOverride); + auto mesherType = readMesherType(fileData, objDef.mesherOverride); + if (mesherType == meshlib::app::staircase_mesher) { - return std::make_unique(meshlib::meshers::StaircaseMesher{in, 4, readStaircaseMesherOptions(fn, objDef.isVolume, objDef.mesherOverride)}); + return std::make_unique(meshlib::meshers::StaircaseMesher{ + in, + 4, + readStaircaseMesherOptions(fileData, objDef.isVolume, objDef.mesherOverride) + }); } else if (mesherType == meshlib::app::conformal_mesher) { - return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fn, objDef.mesherOverride)}); + return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fileData, objDef.mesherOverride)}); } else { throw std::runtime_error("Unsupported mesher type"); } @@ -259,12 +227,18 @@ int launcher(int argc, const char* argv[]) return EXIT_SUCCESS; } - std::string inputFilename = vm["input"].as(); - std::cout << "-- Input file is: " << inputFilename << std::endl; + std::string inputFileName = vm["input"].as(); + std::cout << "-- Input file is: " << inputFileName << std::endl; + + nlohmann::json inputFileData; + { + std::ifstream i(inputFileName); + i >> inputFileData; + } - std::vector objects = readObjectsFromJSON(inputFilename); - std::filesystem::path outputFolder = getFolder(inputFilename); - auto basename = getBasename(inputFilename); + std::vector objects = readObjectsFromJSON(inputFileData); + std::filesystem::path outputFolder = getFolder(inputFileName); + auto basename = getBasename(inputFileName); Mesh firstMesh; bool first = true; @@ -272,9 +246,9 @@ int launcher(int argc, const char* argv[]) for (const auto& objDef : objects) { std::cout << "\n-- Processing object: " << objDef.filename << " (group: " << objDef.group << ")" << std::endl; - Mesh mesh = readMesh(inputFilename, objDef); + Mesh mesh = readMesh(inputFileData, outputFolder, objDef); - auto mesher = buildMesher(mesh, inputFilename, objDef); + auto mesher = buildMesher(mesh, inputFileData, objDef); Mesh resultMesh = mesher->mesh(); if (first) { @@ -282,13 +256,13 @@ int launcher(int argc, const char* argv[]) first = false; } - auto extension = readExtension(inputFilename, objDef.mesherOverride); - std::string outputFilename = objDef.group + ".tessellator." + extension + ".vtk"; - exportMeshToVTU(outputFolder / outputFilename, resultMesh); - std::cout << "-- Exported: " << outputFilename << std::endl; + auto extension = readExtension(inputFileData, objDef.mesherOverride); + std::string outputFileName = objDef.group + ".tessellator." + extension + ".vtk"; + exportMeshToVTU(outputFolder / outputFileName, resultMesh); + std::cout << "-- Exported: " << outputFileName << std::endl; } - if (!first && readExportGridOption(inputFilename, std::nullopt)) { + if (!first && readExportGridOption(inputFileData, std::nullopt)) { exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), firstMesh.grid); std::cout << "-- Exported grid: " << basename << ".tessellator.grid.vtk" << std::endl; } diff --git a/src/app/launcher.h b/src/app/launcher.h index 2fd828e..48823c3 100644 --- a/src/app/launcher.h +++ b/src/app/launcher.h @@ -20,9 +20,9 @@ struct ObjectDefinition { }; int launcher(int argc, const char* argv[]); -Grid parseGridFromJSON(const nlohmann::json& j); -std::vector readObjectsFromJSON(const std::string& fn); +Grid parseGridFromJSON(const nlohmann::json& fileData); +std::vector readObjectsFromJSON(const nlohmann::json& fileData); Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef); -std::unique_ptr buildMesher(const Mesh& in, const std::string& fn, const ObjectDefinition& objDef); +std::unique_ptr buildMesher(const Mesh& in, const nlohmann::json& fileData, const ObjectDefinition& objDef); } \ No newline at end of file diff --git a/src/utils/MeshTools.cpp b/src/utils/MeshTools.cpp index d271f39..02bbe57 100644 --- a/src/utils/MeshTools.cpp +++ b/src/utils/MeshTools.cpp @@ -426,10 +426,6 @@ Mesh extractGroupsByName(const Mesh& mesh, const std::vector& group std::map coordRemap; std::map> groupCoordIds; - for (const auto& groupName : groupNames) { - groupCoordIds[groupName].clear(); - } - for (const auto& groupName : groupNames) { auto it = std::find_if(mesh.groups.begin(), mesh.groups.end(), [&groupName](const Group& g) { return g.name == groupName; }); diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 536e8f0..5b3f3e4 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -17,7 +17,7 @@ TEST_F(LauncherTest, prints_help) { int ac = 2; const char* av[] = { NULL, "-h" }; - EXPECT_EQ(meshlib::app::launcher(ac, av), EXIT_SUCCESS); + EXPECT_EQ(launcher(ac, av), EXIT_SUCCESS); } TEST_F(LauncherTest, parse_rectilinear_grid) @@ -30,7 +30,7 @@ TEST_F(LauncherTest, parse_rectilinear_grid) i >> j; } - meshlib::Grid grid = meshlib::app::parseGridFromJSON(j["grid"]); + meshlib::Grid grid = parseGridFromJSON(j["grid"]); meshlib::Grid expectedGrid({ std::vector{600, 603.25}, @@ -51,15 +51,6 @@ TEST_F(LauncherTest, parse_rectilinear_grid) } } -TEST_F(LauncherTest, launches_alhambra_case) -{ - int ac = 3; - const char* av[] = { NULL, "-i", "testData/cases/alhambra/alhambra.tessellator.json"}; - int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); - EXPECT_EQ(exitCode, EXIT_SUCCESS); -} - TEST_F(LauncherTest, builds_staircased_mesher_default) { meshlib::Mesh meshMock; @@ -68,9 +59,15 @@ TEST_F(LauncherTest, builds_staircased_mesher_default) std::vector{0, 1}, std::vector{0, 1} }; + std::string fileName = "testData/cases/longPolyline/longPolyline_legacy.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } ObjectDefinition objDef; - auto mesher = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline_legacy.tessellator.json", objDef); + auto mesher = buildMesher(meshMock, j, objDef); EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); @@ -86,9 +83,15 @@ TEST_F(LauncherTest, builds_staircased_mesher_without_compression) std::vector{0, 1}, std::vector{0, 1} }; + std::string fileName = "testData/cases/longPolyline/longPolyline.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } ObjectDefinition objDef; - auto mesher = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline.tessellator.json", objDef); + auto mesher = buildMesher(meshMock, j, objDef); EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); @@ -104,22 +107,37 @@ TEST_F(LauncherTest, builds_staircased_mesher_with_compression) std::vector{0, 1}, std::vector{0, 1} }; + std::string fileName = "testData/cases/longPolyline/longPolyline_compression.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } ObjectDefinition objDef; - auto mesher = meshlib::app::buildMesher(meshMock, "testData/cases/longPolyline/longPolyline_compression.tessellator.json", objDef); - + auto mesher = buildMesher(meshMock, j, objDef); + EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); EXPECT_EQ(options.isVolume, false); EXPECT_EQ(options.compress, true); } +TEST_F(LauncherTest, launches_alhambra_case) +{ + int ac = 3; + const char* av[] = { NULL, "-i", "testData/cases/alhambra/alhambra.tessellator.json"}; + int exitCode; + EXPECT_NO_THROW(exitCode = launcher(ac, av)); + EXPECT_EQ(exitCode, EXIT_SUCCESS); +} + TEST_F(LauncherTest, launches_conformal_alhambra_case) { int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/alhambra/alhambra.conformal.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -129,7 +147,7 @@ TEST_F(LauncherTest, launches_sphere_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/sphere/sphere.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -138,7 +156,7 @@ TEST_F(LauncherTest, launches_closed_sphere_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/sphere/closed_sphere.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -147,7 +165,7 @@ TEST_F(LauncherTest, launches_conformal_sphere_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/sphere/sphere.conformal.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -156,7 +174,7 @@ TEST_F(LauncherTest, launches_conformal_thinCylinder_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/thinCylinder/thinCylinder.conformal.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -165,7 +183,7 @@ TEST_F(LauncherTest, launches_thinCylinder_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/thinCylinder/thinCylinder.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -174,7 +192,7 @@ TEST_F(LauncherTest, launches_cone_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/cone/cone.tessellator.json" }; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -183,7 +201,7 @@ TEST_F(LauncherTest, launches_long_polyline_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/longPolyline/longPolyline_legacy.tessellator.json" }; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -192,13 +210,19 @@ TEST_F(LauncherTest, launches_conformal_cone_case) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/cone/cone.conformal.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } TEST_F(LauncherTest, readObjectsFromJSON_basic) { - auto objects = readObjectsFromJSON("testData/cases/multiObject/basic.tessellator.json"); + std::string fileName = "testData/cases/multiObject/basic.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + auto objects = readObjectsFromJSON(j); EXPECT_EQ(objects.size(), 2); EXPECT_EQ(objects[0].filename, "sphere.stl"); EXPECT_EQ(objects[0].group, "sphere_group"); @@ -212,7 +236,13 @@ TEST_F(LauncherTest, readObjectsFromJSON_basic) TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) { - auto objects = readObjectsFromJSON("testData/cases/multiObject/mixedMesher.tessellator.json"); + std::string fileName = "testData/cases/multiObject/mixedMesher.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + auto objects = readObjectsFromJSON(j); EXPECT_EQ(objects.size(), 3); EXPECT_EQ(objects[0].filename, "sphere.stl"); @@ -235,7 +265,13 @@ TEST_F(LauncherTest, readObjectsFromJSON_mixedMesher) TEST_F(LauncherTest, readObjectsFromJSON_singleObject) { - auto objects = readObjectsFromJSON("testData/cases/multiObject/singleObject.tessellator.json"); + std::string fileName = "testData/cases/multiObject/singleObject.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + auto objects = readObjectsFromJSON(j); EXPECT_EQ(objects.size(), 1); EXPECT_EQ(objects[0].filename, "sphere.stl"); EXPECT_EQ(objects[0].group, "default_group"); @@ -244,7 +280,13 @@ TEST_F(LauncherTest, readObjectsFromJSON_singleObject) TEST_F(LauncherTest, readObjectsFromJSON_legacyFormat) { - auto objects = readObjectsFromJSON("testData/cases/sphere/closed_sphere.tessellator.json"); + std::string fileName = "testData/cases/sphere/closed_sphere.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } + auto objects = readObjectsFromJSON(j); EXPECT_EQ(objects.size(), 1); EXPECT_EQ(objects[0].filename, "sphere.stl"); EXPECT_EQ(objects[0].group, "sphere"); @@ -260,11 +302,16 @@ TEST_F(LauncherTest, builds_staircased_mesher_with_override) std::vector{0, 1} }; - std::string filename = "testData/cases/multiObject/mixedMesher.tessellator.json"; + std::string fileName = "testData/cases/multiObject/mixedMesher.tessellator.json"; + nlohmann::json j; + { + std::ifstream i(fileName); + i >> j; + } - auto objects = readObjectsFromJSON(filename); + auto objects = readObjectsFromJSON(j); - auto mesher = meshlib::app::buildMesher(meshMock, filename, objects[0]); + auto mesher = meshlib::app::buildMesher(meshMock, j, objects[0]); EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); @@ -277,7 +324,7 @@ TEST_F(LauncherTest, launches_multiObject_basic) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/multiObject/basic.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -286,7 +333,7 @@ TEST_F(LauncherTest, launches_multiObject_mixedMesher) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/multiObject/mixedMesher.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -295,7 +342,7 @@ TEST_F(LauncherTest, launches_multiObject_singleObject) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/multiObject/singleObject.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } @@ -304,7 +351,7 @@ TEST_F(LauncherTest, launches_multiObject_sameFileMultipleGroups) int ac = 3; const char* av[] = { NULL, "-i", "testData/cases/multiObject/sameFileMultipleGroups.tessellator.json"}; int exitCode; - EXPECT_NO_THROW(exitCode = meshlib::app::launcher(ac, av)); + EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } From c60c5a6b48e5930be19e4c13c9ef580e9fbba720 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Mon, 27 Jul 2026 09:42:31 +0200 Subject: [PATCH 49/61] Commit before merging with dev --- src/cgal/filler/Filler.h | 2 +- src/core/CMakeLists.txt | 6 ++- src/core/Slicer.cpp | 25 +++++++++ src/core/Slicer.h | 7 ++- src/core/VolumeFiller.cpp | 90 +++++++++++++++++++++++++++++++ src/core/VolumeFiller.h | 18 +++++++ src/meshers/StaircaseMesher.cpp | 58 ++++++++++++-------- src/meshers/StaircaseMesher.h | 2 + src/utils/Geometry.cpp | 23 ++++++++ src/utils/Geometry.h | 2 + src/utils/MeshTools.cpp | 22 +++++--- test/CMakeLists.txt | 1 + test/core/SlicerTest.cpp | 16 ++++++ test/core/VolumeFillerTest.cpp | 96 +++++++++++++++++++++++++++++++++ 14 files changed, 338 insertions(+), 30 deletions(-) create mode 100644 src/core/VolumeFiller.cpp create mode 100644 src/core/VolumeFiller.h create mode 100644 test/core/VolumeFillerTest.cpp diff --git a/src/cgal/filler/Filler.h b/src/cgal/filler/Filler.h index 2b2d5af..8f2d4dc 100644 --- a/src/cgal/filler/Filler.h +++ b/src/cgal/filler/Filler.h @@ -32,7 +32,7 @@ class Filler { FillingState getFillingState(const CellIndex&) const; Mesh getMeshFilling() const; - + GridSlices getSlices() const {return slices_;}; private: GridSlices slices_; GridSegmentsArray segmentsArray_; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index faab34a..fe92b76 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -8,6 +8,10 @@ add_library(tessellator-core "Smoother.cpp" "SmootherTools.cpp" "Staircaser.cpp" + "VolumeFiller.cpp" ) -target_link_libraries(tessellator-core tessellator-utils) \ No newline at end of file +target_link_libraries(tessellator-core + tessellator-utils + tessellator-cgal + CGAL::CGAL) \ No newline at end of file diff --git a/src/core/Slicer.cpp b/src/core/Slicer.cpp index fb5d7a2..a33defa 100644 --- a/src/core/Slicer.cpp +++ b/src/core/Slicer.cpp @@ -38,6 +38,31 @@ void orient(const Coordinates& coords, } } +// Mesh Slicer::fill(const Mesh& input){ +// //input mesh should be structured + +// Coordinates sCoords; +// sCoords.reserve(input.coordinates.size() * 100); + +// for (const auto& g : input.groups){ + +// for (auto const& itCell : +// buildCellCoordIdMap(sCoords, buildGroupIntersectionsWithGridPlanes(sCoords, g.elements))) { +// const IdSet& vIds = itCell.second; +// if (vIds.size() < 3) { +// continue; +// } +// } + +// } +// } + +// IdSet Slicer::buildGroupIntersectionsWithGridPlanes( +// Coordinates& sCoords, +// const std::vector& elements) +// { + +// } Slicer::Slicer(const Mesh& input, const std::vector& dimensionPolicy, const SlicerOptions& opts) : GridTools(input.grid), diff --git a/src/core/Slicer.h b/src/core/Slicer.h index e0bae73..7f418a4 100644 --- a/src/core/Slicer.h +++ b/src/core/Slicer.h @@ -24,7 +24,7 @@ class Slicer : public utils::GridTools { Slicer(const Mesh&, const std::vector& dimensionPolicy = {}, const SlicerOptions& opts = SlicerOptions()); Mesh getMesh() const { return mesh_; }; - + // Mesh fill(const Mesh&); static Elements buildTrianglesFromPath(const std::vector&, const std::vector&); @@ -53,6 +53,11 @@ class Slicer : public utils::GridTools { const Cell&, const Coordinate&, const Cell&, const Coordinate&) const; + // IdSet buildGroupIntersectionsWithGridPlanes( + // Coordinates& sCoords, + // const std::vector& elements); + + }; } diff --git a/src/core/VolumeFiller.cpp b/src/core/VolumeFiller.cpp new file mode 100644 index 0000000..0786e1d --- /dev/null +++ b/src/core/VolumeFiller.cpp @@ -0,0 +1,90 @@ +#include "VolumeFiller.h" +#include "cgal/filler/Filler.h" +#include "cgal/filler/FillerTools.h" +#ifdef TESSELLATOR_EXECUTION_POLICIES +#include +#endif + + +namespace meshlib { +namespace core { +using namespace utils; + + +Elements buildTriangleElements(Coordinates& cs, const TriVs tris) +{ + Elements r; + for (const auto& p : tris) { + Element e; + e.type = Element::Type::Surface; + e.vertices.reserve(3); + for (const auto& v : p) { + e.vertices.push_back(cs.size()); + cs.push_back(v); + } + r.push_back(e); + } + return r; +} +Elements buildLineElements(Coordinates& cs, const LinVs lins) +{ + Elements r; + for (const auto& p : lins) { + Element e; + e.type = Element::Type::Line; + e.vertices.reserve(2); + for (const auto& v : p) { + e.vertices.push_back(cs.size()); + cs.push_back(v); + } + r.push_back(e); + } + return r; +} + +VolumeFiller::VolumeFiller(const Mesh& input) +{ + Mesh m; + mesh_.grid = input.grid; + mesh_.groups.resize(input.groups.size()); + + meshlib::cgal::filler::Filler f{input}; + mesh_ = f.getMeshFilling(); + // auto slices = f.getSlices(); + // for (auto gId{0}; gId < mesh_.groups.size(); ++gId) { + + // for (const auto& x : { X, Y, Z }) { + // for (const auto& [i, slice]: slices[x]) { + // // const Priority pr{ getGroupPriority(gId) }; + // const Priority pr{ 0 }; + // auto trivs = slice.buildAllTriVs(pr, x, (meshlib::cgal::filler::Height)i); + // auto triEls = buildTriangleElements( + // m.coordinates, + // slice.buildAllTriVs(pr, x, (meshlib::cgal::filler::Height)i) + // ); + + // auto linvs = slice.buildAllLinVs(pr, x, (meshlib::cgal::filler::Height)i); + // auto lineEls = buildLineElements( + // mesh_.coordinates, + // slice.buildAllLinVs(pr, x, (meshlib::cgal::filler::Height)i) + // ); + + // } + // } + // } + //take structured mesh + //use filler to generate the slices: intersections of each grid plane with the mesh + //convert the slice intersection into a ElemV with dimension n, where n is the number of points of the slice intersection + //build a coordIdMap similar to the one created in Slicer::sliceTriangle, where instead of the intersection of a triangle with the grid planes, we have the intersection of the n-dimensional ElemV + //Create a triangulation of the intersection of the slice with the grid planes, following also slicer::sliceTriangle +} + +Mesh VolumeFiller::getMesh() +{ + return mesh_; +} + + + +} +} \ No newline at end of file diff --git a/src/core/VolumeFiller.h b/src/core/VolumeFiller.h new file mode 100644 index 0000000..439c9fc --- /dev/null +++ b/src/core/VolumeFiller.h @@ -0,0 +1,18 @@ +#pragma once + +#include "utils/GridTools.h" + +namespace meshlib{ +namespace core{ + +class VolumeFiller : public utils::GridTools{ +public: + + VolumeFiller(const Mesh&); + Mesh getMesh(); +private: + Mesh mesh_; +}; + +} +} \ No newline at end of file diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index aab9f75..c424ab8 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -9,7 +9,7 @@ #include "core/Compressor.h" #include "cgal/filler/Filler.h" -#include "cgal/Manifolder.h" +// #include "cgal/Manifolder.h" #include "utils/RedundancyCleaner.h" #include "utils/MeshTools.h" @@ -27,15 +27,41 @@ StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInColla opts_(opts) { log("Preparing surfaces."); - //here, convert tetra intro hull tris - surfaceMesh_ = buildMeshFilteringElements(inputMesh, isNotTetrahedron); - + surfaceMesh_ = MesherBase::buildSurfaceMesh(inputMesh, opts_.volumeGroups); log("Processing surface mesh."); process(surfaceMesh_); + + log("Preparing volumes"); + volumeMesh_ = MesherBase::buildVolumeMesh(inputMesh, opts_.volumeGroups); + fillMesh(volumeMesh_, opts_.volumeGroups); + log("Processing volume mesh."); + process(volumeMesh_); + + mergeMesh(surfaceMesh_, volumeMesh_); + + log("Mesh built succesfully.", 1); +} + +void StaircaseMesher::fillMesh(Mesh& m, const std::set& volumeGroups){ + if (m.countElems() == 0) return; + // auto mani = meshlib::cgal::Manifolder(m); + // for (const auto& gId : volumeGroups){ + // m.groups[gId].elements = mani.getClosedSurfacesMesh().groups[gId].elements; + // } + m.coordinates = utils::GridTools{m.grid}.absoluteToRelative(m.coordinates); + m = meshlib::cgal::filler::Filler(m).getMeshFilling(); + m.coordinates = utils::GridTools{m.grid}.relativeToAbsolute(m.coordinates); - log("Surface mesh built succesfully.", 1); + // m = f.getMeshFilling(); + + // auto filling = f.getMeshFilling(); + // mergeMesh(m, filling); + // // auto dimensions = getHighestDimensionByGroup(m); + // // RedundancyCleaner::removeOverlappedElementsByDimension(m, dimensions); + } + Mesh StaircaseMesher::buildSurfaceMesh(const Mesh& inputMesh, const Mesh & volumeSurface) { auto resultMesh = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -49,33 +75,23 @@ void StaircaseMesher::process(Mesh& mesh) const const auto slicingGrid{ buildSlicingGrid(originalGrid_, enlargedGrid_) }; if (mesh.countElems() == 0) { - mesh.grid = slicingGrid; + // mesh.grid = slicingGrid; return; } auto dimensions = getHighestDimensionByGroup(mesh); - //mani has open_ and closed_ for every group id - if (opts_.isVolume){ - if (meshTools::isAClosedTopology(mesh.groups[0].elements)){ - - auto mani = meshlib::cgal::Manifolder(mesh); - mesh.groups[0].elements = mani.getClosedSurfacesMesh().groups[0].elements; - - meshlib::cgal::filler::Filler f{ mesh }; - auto filling = f.getMeshFilling(); - mergeMesh(mesh, filling); - } else { - throw std::runtime_error("Input object marked to be meshed as a volume, but surface is not closed"); - } - } - log("Slicing.", 1); mesh.grid = slicingGrid; mesh = Slicer{ mesh, dimensions }.getMesh(); logNumberOfTriangles(countMeshElementsIf(mesh, isTriangle)); + // meshlib::cgal::filler::Filler f{mesh}; + // auto filling = f.getMeshFilling(); + // mergeMesh(mesh, filling); + + log("Collapsing.", 1); mesh = Collapser(mesh, decimalPlacesInCollapser_, dimensions).getMesh(); diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index df0b9bb..92fee31 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -16,9 +16,11 @@ class StaircaseMesher : public MesherBase { int decimalPlacesInCollapser_; Mesh surfaceMesh_; + Mesh volumeMesh_; StaircaseMesherOptions opts_; virtual Mesh buildSurfaceMesh(const Mesh& inputMesh, const Mesh& volumeSurface); + static void fillMesh(Mesh& inputMesh, const std::set& volumeGroups); void process(Mesh&) const; }; diff --git a/src/utils/Geometry.cpp b/src/utils/Geometry.cpp index fe2fc4e..2015763 100644 --- a/src/utils/Geometry.cpp +++ b/src/utils/Geometry.cpp @@ -70,6 +70,17 @@ std::vector Geometry::buildDisjointSmoothSets( } +QuaV Geometry::asQuaV(const Element& el, const std::vector& co) { + if (el.vertices.size() != 4) { + throw std::logic_error("Invalid conversion from element to QuaV"); + } + QuaV res; + for (std::size_t i = 0; i < el.vertices.size(); i++) { + res[i] = co[el.vertices[i]]; + } + return res; +} + TriV Geometry::asTriV(const Element& el, const std::vector& co) { if (el.vertices.size() != 3) { throw std::logic_error("Invalid conversion from element to TriV"); @@ -229,6 +240,18 @@ double Geometry::area(const TriV& tri) { return ((tri[0] - tri[1]) ^ (tri[1] - tri[2])).norm() / 2.0; } +double Geometry::area(const QuaV& qua) { + const Coordinates cs{ qua.begin(), qua.end() }; + + VecD crossSum; + for (std::size_t i = 0; i < qua.size(); ++i) { + const auto& p = qua[i]; + const auto& q = qua[(i + 1) % qua.size()]; + crossSum += (p ^ q); + } + return 0.5*crossSum.norm(); +} + } } diff --git a/src/utils/Geometry.h b/src/utils/Geometry.h index 113593b..e185f0c 100644 --- a/src/utils/Geometry.h +++ b/src/utils/Geometry.h @@ -28,6 +28,7 @@ class Geometry { static bool areAdjacentLines(const Element&, const Element&); + static QuaV asQuaV(const Element&, const Coordinates&); static TriV asTriV(const Element&, const Coordinates&); static LinV asLinV(const Element&, const Coordinates&); @@ -38,6 +39,7 @@ class Geometry { static VecD getCentroid(const Element&, const std::vector&); static VecD getCentroid(const TriV&); static double area(const TriV& tri); + static double area(const QuaV& qua); static bool isDegenerate(const TriV& tri, const double& areaTolerance = NORM_TOLERANCE); static bool areCollinear(const Coordinates&); template diff --git a/src/utils/MeshTools.cpp b/src/utils/MeshTools.cpp index d271f39..48e405d 100644 --- a/src/utils/MeshTools.cpp +++ b/src/utils/MeshTools.cpp @@ -273,12 +273,22 @@ void checkNoNullAreasExist(const Mesh& m) msg << info(e, m) << std::endl; } } - else if (Geometry::area(Geometry::asTriV(e, m.coordinates)) == 0.0) { - nullAreas = true; - msg << std::endl; - msg << "Group: " << &g - &m.groups.front() - << ", Element: " << &e - &g.elements.front() << std::endl; - msg << info(e, m) << std::endl; + else if (e.isTriangle()){ + if (Geometry::area(Geometry::asTriV(e, m.coordinates)) == 0.0) { + nullAreas = true; + msg << std::endl; + msg << "Group: " << &g - &m.groups.front() + << ", Element: " << &e - &g.elements.front() << std::endl; + msg << info(e, m) << std::endl; + } + } else if (e.isQuad()){ + if (Geometry::area(Geometry::asQuaV(e, m.coordinates)) == 0.0) { + nullAreas = true; + msg << std::endl; + msg << "Group: " << &g - &m.groups.front() + << ", Element: " << &e - &g.elements.front() << std::endl; + msg << info(e, m) << std::endl; + } } } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 638a03a..b4fbb61 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable(tessellator_tests "core/CollapserTest.cpp" "core/CompressorTest.cpp" "core/SlicerTest.cpp" + "core/VolumeFillerTest.cpp" "core/SmootherTest.cpp" "core/SmootherToolsTest.cpp" "core/SnapperTest.cpp" diff --git a/test/core/SlicerTest.cpp b/test/core/SlicerTest.cpp index 2367b93..70e18b4 100644 --- a/test/core/SlicerTest.cpp +++ b/test/core/SlicerTest.cpp @@ -1090,4 +1090,20 @@ TEST_F(SlicerTest, sphere_case_patch_contour_check_2) #endif } +// TEST_F(SlicerTest, cube1x1x1_size05_grid_fill) +// { +// Mesh m = buildCubeSurfaceMesh(0.5); + +// Mesh out; +// ASSERT_NO_THROW(out = Slicer{m}.getMesh()); +// EXPECT_EQ(48, countMeshElementsIf(out, isTriangle)); +// EXPECT_FALSE(containsDegenerateTriangles(out)); +// EXPECT_EQ(countContours(m), countContours(out)); + +// Mesh filled; +// ASSERT_NO_THROW(filled = Slicer::fill(out)); +// EXPECT_EQ(72, countMeshElementsIf(out, isTriangle)); +// } + + } diff --git a/test/core/VolumeFillerTest.cpp b/test/core/VolumeFillerTest.cpp new file mode 100644 index 0000000..e2a475e --- /dev/null +++ b/test/core/VolumeFillerTest.cpp @@ -0,0 +1,96 @@ +#include "MeshFixtures.h" +#include "gtest/gtest.h" + +#include "VolumeFiller.h" +#include "Slicer.h" +#include "Geometry.h" +#include "MeshTools.h" +#include "GridTools.h" +// #include "utils/RedundancyCleaner.h" +// #include "utils/CoordGraph.h" + +#if APP_LOADED + #include "app/vtkIO.h" +#endif + +namespace meshlib::core { + +using namespace meshFixtures; +using namespace utils; +using namespace meshTools; + +class VolumeFillerTest : public ::testing::Test { +public: +protected: +}; + +static Mesh toRelative(const Mesh& m) +{ + auto r{ m }; + r.coordinates = + utils::GridTools{ m.grid }.absoluteToRelative(m.coordinates); + return r; +} + +TEST_F(VolumeFillerTest, fill_cube1x1x1_size1_grid) +{ + Mesh m = buildCubeSurfaceMesh(01.0); + + Mesh out; + ASSERT_NO_THROW(out = Slicer{m}.getMesh()); + EXPECT_EQ(12, countMeshElementsIf(out, isTriangle)); + + Mesh filled = VolumeFiller{out}.getMesh(); + EXPECT_EQ(12, countMeshElementsIf(filled, isTriangle)); +} + +TEST_F(VolumeFillerTest, fill_cube1x1x1_size05_grid) +{ + //filling with unstruc. triangles + Mesh m = buildCubeSurfaceMesh(0.5); + Mesh filled = VolumeFiller{Slicer{buildCubeSurfaceMesh(0.5) }.getMesh()}.getMesh(); + Mesh filled_no_slicing = VolumeFiller{toRelative(buildCubeSurfaceMesh(0.5))}.getMesh(); + EXPECT_EQ(18, countMeshElementsIf(filled, isTriangle)); + EXPECT_EQ(18, countMeshElementsIf(filled_no_slicing, isTriangle)); + + //slicing hull + Mesh out_1; + ASSERT_NO_THROW(out_1 = Slicer{buildCubeSurfaceMesh(0.5) }.getMesh()); + EXPECT_EQ(48, countMeshElementsIf(out_1, isTriangle)); + + //filling and slicing + Mesh out_2; + GridTools gT{ filled.grid }; + filled.coordinates = gT.relativeToAbsolute(filled.coordinates); + ASSERT_NO_THROW(out_2 = Slicer{filled }.getMesh()); + EXPECT_EQ(72, countMeshElementsIf(out_2, isTriangle)); + + GridTools gT_no{ filled_no_slicing.grid }; + filled_no_slicing.coordinates = gT_no.relativeToAbsolute(filled_no_slicing.coordinates); + ASSERT_NO_THROW(out_2 = Slicer{filled_no_slicing}.getMesh()); + EXPECT_EQ(72, countMeshElementsIf(out_2, isTriangle)); + +} + +TEST_F(VolumeFillerTest, fill_cube1x1x1_size025_grid) +{ + Mesh m = buildCubeSurfaceMesh(0.25); + Mesh filled = VolumeFiller{Slicer{buildCubeSurfaceMesh(0.25) }.getMesh()}.getMesh(); + EXPECT_EQ(30, countMeshElementsIf(filled, isTriangle)); + + Mesh out_1; + ASSERT_NO_THROW(out_1 = Slicer{buildCubeSurfaceMesh(0.25) }.getMesh()); + EXPECT_EQ(192, countMeshElementsIf(out_1, isTriangle)); + + Mesh out_2; + GridTools gT{ filled.grid }; + filled.coordinates = gT.relativeToAbsolute(filled.coordinates); + + ASSERT_NO_THROW(out_2 = Slicer{filled }.getMesh()); + EXPECT_EQ(480, countMeshElementsIf(out_2, isTriangle)); + + // Mesh filled = VolumeFiller{out}.getMesh(); + // ASSERT_NO_THROW(out = Slicer{filled}.getMesh()); + // EXPECT_EQ(72, countMeshElementsIf(out, isTriangle)); +} +} \ No newline at end of file From eb955fb87e776f8d3d4269a0079c087d41fb8219 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Tue, 28 Jul 2026 09:25:52 +0200 Subject: [PATCH 50/61] Changes to propagate group names to vtk output. Property isVolume not needed, use volumeGroups instead --- src/app/launcher.cpp | 12 ++++++++---- src/app/vtkIO.cpp | 9 +++++++-- src/meshers/MesherBaseOptions.h | 1 - src/meshers/StaircaseMesher.cpp | 19 ++++++++++++++++++- src/utils/MeshTools.cpp | 1 + test/app/launcherTest.cpp | 8 ++++---- test/meshers/StaircaseMesherTest.cpp | 4 ++-- 7 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index 6ce1de5..c9bb54c 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -150,8 +150,9 @@ meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const nlohma } meshlib::meshers::StaircaseMesherOptions res; - - res.isVolume = isVolume; + if (isVolume){ + res.volumeGroups.insert(0); + } if (mesherConfig.contains("options") && mesherConfig["options"].contains("compress")) { res.compress = mesherConfig["options"]["compress"]; @@ -160,7 +161,7 @@ meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const nlohma return res; } -meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const nlohmann::json& fileData, const std::optional& override) +meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const nlohmann::json& fileData, bool isVolume, const std::optional& override) { nlohmann::json mesherConfig; if (override.has_value()) { @@ -170,6 +171,9 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const nlohma } meshlib::meshers::ConformalMesherOptions res; + if (isVolume){ + res.volumeGroups.insert(0); + } if (mesherConfig.contains("options")) { res.snapperOptions.edgePoints = mesherConfig["options"]["edgePoints"]; res.snapperOptions.forbiddenLength = mesherConfig["options"]["forbiddenLength"]; @@ -204,7 +208,7 @@ std::unique_ptr buildMesher(const Mesh& in, const readStaircaseMesherOptions(fileData, objDef.isVolume, objDef.mesherOverride) }); } else if (mesherType == meshlib::app::conformal_mesher) { - return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fileData, objDef.mesherOverride)}); + return std::make_unique(meshlib::meshers::ConformalMesher{in, readConformalMesherOptions(fileData, objDef.isVolume, objDef.mesherOverride)}); } else { throw std::runtime_error("Unsupported mesher type"); } diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index 094e8cc..70a105c 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -183,8 +183,13 @@ vtkSmartPointer toVTKGroupNamesArray(const Mesh& mesh) groupNamesArray->SetName("groupNames"); groupNamesArray->SetNumberOfComponents(1); - for (const auto& group : mesh.groups) { - groupNamesArray->InsertNextValue(group.name.c_str()); + // for (const auto& group : mesh.groups) { + // groupNamesArray->InsertNextValue(group.name.c_str()); + // } + for (auto g = 0; g < mesh.groups.size(); g++) { + for (auto e = 0; e < mesh.groups[g].elements.size(); e++) { + groupNamesArray->InsertNextValue( mesh.groups[g].name.c_str() ); + } } return groupNamesArray; diff --git a/src/meshers/MesherBaseOptions.h b/src/meshers/MesherBaseOptions.h index 7403619..55138c1 100644 --- a/src/meshers/MesherBaseOptions.h +++ b/src/meshers/MesherBaseOptions.h @@ -7,7 +7,6 @@ namespace meshlib::meshers { class MesherBaseOptions { public: - bool isVolume = false; std::set volumeGroups{}; }; diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index c424ab8..35bd6b5 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -69,9 +69,24 @@ Mesh StaircaseMesher::buildSurfaceMesh(const Mesh& inputMesh, const Mesh & volum return resultMesh; } +std::vector getGroupNames(const Groups& groups){ + std::vector names; + names.reserve(groups.size()); + for (auto gId{0}; gId < groups.size(); ++gId) { + names.push_back(groups[gId].name); + } + return names; +} + +void copyGroupNames(Mesh& m, const std::vector& names){ + for (auto gId{0}; gId < m.groups.size(); ++gId) { + m.groups[gId].name = names[gId]; + } +} + void StaircaseMesher::process(Mesh& mesh) const { - + const auto groupNames = getGroupNames(mesh.groups); const auto slicingGrid{ buildSlicingGrid(originalGrid_, enlargedGrid_) }; if (mesh.countElems() == 0) { @@ -136,6 +151,8 @@ void StaircaseMesher::process(Mesh& mesh) const logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); logNumberOfLines(countMeshElementsIf(mesh, isLine)); + copyGroupNames(mesh, groupNames); + } diff --git a/src/utils/MeshTools.cpp b/src/utils/MeshTools.cpp index cfd7253..a86479e 100644 --- a/src/utils/MeshTools.cpp +++ b/src/utils/MeshTools.cpp @@ -329,6 +329,7 @@ Mesh buildMeshFilteringElements( inElems.begin(), inElems.end(), std::back_inserter(r.groups[gId].elements), filter); + r.groups[gId].name = in.groups[gId].name; } return r; } diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 5b3f3e4..056f5ee 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -71,7 +71,7 @@ TEST_F(LauncherTest, builds_staircased_mesher_default) EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); - EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.volumeGroups.size(), 0); EXPECT_EQ(options.compress, false); } @@ -95,7 +95,7 @@ TEST_F(LauncherTest, builds_staircased_mesher_without_compression) EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); - EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.volumeGroups.size(), 0); EXPECT_EQ(options.compress, false); } @@ -119,7 +119,7 @@ TEST_F(LauncherTest, builds_staircased_mesher_with_compression) EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); - EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.volumeGroups.size(), 0); EXPECT_EQ(options.compress, true); } @@ -315,7 +315,7 @@ TEST_F(LauncherTest, builds_staircased_mesher_with_override) EXPECT_NO_THROW(auto staircaseMesher = dynamic_cast(*mesher)); const auto & options = dynamic_cast(*mesher).getOptions(); - EXPECT_EQ(options.isVolume, false); + EXPECT_EQ(options.volumeGroups.size(), 0); EXPECT_EQ(options.compress, true); } diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index c8a1dab..334cacd 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -421,10 +421,10 @@ TEST_F(StaircaseMesherTest, fills_closed_volume_with_quads) mesh.grid[Z] = utils::GridTools::linspace(-100.0, 100.0, 51); meshlib::meshers::StaircaseMesherOptions opts; - opts.isVolume = false; + // opts.isVolume = false; auto staircasedMesh = StaircaseMesher{mesh, 4, opts }.mesh(); - opts.isVolume = true; + opts.volumeGroups.insert(0); auto staircasedMeshVolume = StaircaseMesher{mesh, 4, opts }.mesh(); EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); From 2642125713794e507e2b478367d41ee66060e8bd Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Tue, 28 Jul 2026 13:25:31 +0200 Subject: [PATCH 51/61] Modifications to filler to selectively return grid-aligned exterior --- src/cgal/filler/Filler.cpp | 32 ++++++++++- src/cgal/filler/Filler.h | 16 ++++-- src/cgal/filler/Slice.cpp | 27 ++++++++++ src/cgal/filler/Slice.h | 2 + src/core/CMakeLists.txt | 1 - src/core/VolumeFiller.cpp | 90 ------------------------------- src/core/VolumeFiller.h | 18 ------- src/meshers/StaircaseMesher.cpp | 45 ++++++++-------- src/meshers/StaircaseMesher.h | 2 +- test/CMakeLists.txt | 1 - test/cgal/filler/FillerTest.cpp | 61 +++++++++++++++++++++ test/core/VolumeFillerTest.cpp | 96 --------------------------------- 12 files changed, 157 insertions(+), 234 deletions(-) delete mode 100644 src/core/VolumeFiller.cpp delete mode 100644 src/core/VolumeFiller.h delete mode 100644 test/core/VolumeFillerTest.cpp diff --git a/src/cgal/filler/Filler.cpp b/src/cgal/filler/Filler.cpp index ae72d0a..60c23bf 100644 --- a/src/cgal/filler/Filler.cpp +++ b/src/cgal/filler/Filler.cpp @@ -253,6 +253,29 @@ void sliceAlignedByGrid( ); } +void sliceAlignedByGridAndRemove( + Filler::GridSlices& slices, + const Polyhedron& m, + const Grid& g, + const Priority& priority) +{ + + auto polygons{ buildGridPlanesPolygons(makeFacesCCWOriented(m), g)}; + const std::array axis{ X, Y, Z }; + + std::for_each( +#ifdef TESSELLATOR_EXECUTION_POLICIES + std::execution::par, +#endif + axis.begin(), axis.end(), + [&](const auto& x) { + for (const auto& [i, polygon] : polygons[x]) { + slices[x][i].remove(polygon, priority); + } + } + ); +} + Priority Filler::getGroupPriority(const GroupId& gId) const { if (gId < groupPriorities_.size()) { @@ -508,7 +531,8 @@ FillerPolyhedrons buildFillerPolyhedrons( Filler::Filler( const Mesh& volumeMesh, const Mesh& surfaceMesh, - const std::vector& groupPriorities) + const std::vector& groupPriorities, + const FillerMode& fillerMode) { utils::meshTools::checkNoNullAreasExist(volumeMesh); utils::meshTools::checkNoNullAreasExist(surfaceMesh); @@ -547,7 +571,11 @@ Filler::Filler( log("Slicing surfaces", 2); sliceNonAlignedByGrid(slices_, fP.surfaces, grid_, pr, SlicingMode::Surface); log("Slicing aligned", 2); - sliceAlignedByGrid(slices_, fP.aligned, grid_, pr); + if (fillerMode == FillerMode::insideAndOutside) { + sliceAlignedByGrid(slices_, fP.aligned, grid_, pr); + } else if (fillerMode == FillerMode::onlyInside){ + sliceAlignedByGridAndRemove(slices_, fP.aligned, grid_, pr); + } log("Building segments arrays", 2); buildSegmentsArray(segmentsArray_, fP.aligned, grid_, pr); buildSegmentsArray(segmentsArray_, fP.volumes, grid_, pr); diff --git a/src/cgal/filler/Filler.h b/src/cgal/filler/Filler.h index 8f2d4dc..ed87558 100644 --- a/src/cgal/filler/Filler.h +++ b/src/cgal/filler/Filler.h @@ -9,17 +9,26 @@ namespace meshlib::cgal::filler { +enum class FillerMode{ + insideAndOutside, + onlyInside +}; + + class Filler { -public: + public: using Slices = std::map; using GridSlices = std::array; using SegmentsArray = std::map; using GridSegmentsArray = std::array; - + + FillerMode mode = FillerMode::insideAndOutside; + Filler( const Mesh& volumeMesh, const Mesh& surfaceMesh = Mesh(), - const std::vector& groupPriorities = std::vector()); + const std::vector& groupPriorities = std::vector(), + const FillerMode& mode = FillerMode::insideAndOutside); Filler(const Filler&) = delete; Filler(Filler&&) = default; Filler& operator=(const Filler&) = delete; @@ -42,6 +51,7 @@ class Filler { void mergeGroupsWithSamePriority(Groups& vGroups, Groups& sGroups); + }; diff --git a/src/cgal/filler/Slice.cpp b/src/cgal/filler/Slice.cpp index 9a674d7..5133ba6 100644 --- a/src/cgal/filler/Slice.cpp +++ b/src/cgal/filler/Slice.cpp @@ -183,6 +183,21 @@ void Slice::add(const Polylines2& polylines, const Priority& pr) } } +void Slice::remove(const Polylines2& polylines, const Priority& pr) +{ + SliceData& sd = data_[pr]; + + for (const auto& p : polylines) { + if (p.size() == 1) { + continue; + } + auto r{ removeSegmentsContainedInAnyAxis(p) }; + for (const auto& rr : r){ + sd.lines.erase(std::find(sd.lines.begin(), sd.lines.end(), rr)); + } + } +} + FillingState::FillingState(const FillingType& t) : type{ t }, priority_{ 0 } @@ -261,6 +276,18 @@ void Slice::add(const HPolygonSet& polygons, const Priority& pr) removeInSuperiorPriorities(pr); } +void Slice::remove(const HPolygonSet& polygons, const Priority& pr) +{ + if (polygons.isEmpty()) { + return; + } + + SliceData& sd = data_[pr]; + sd.surfaces.difference(polygons); + + removeInSuperiorPriorities(pr); +} + void Slice::mergeLines(const Slice& lhs) { for (const auto& [pr, sd] : lhs.data_) { diff --git a/src/cgal/filler/Slice.h b/src/cgal/filler/Slice.h index b61883c..b04bf76 100644 --- a/src/cgal/filler/Slice.h +++ b/src/cgal/filler/Slice.h @@ -85,8 +85,10 @@ class Slice { FillingState getFillingState(const ArrayIndex&) const; void add(const Polylines2&, const Priority&); + void remove(const Polylines2&, const Priority&); void addAsPolygon(const Polylines2&, const Priority&); void add(const HPolygonSet&, const Priority&); + void remove(const HPolygonSet&, const Priority&); void mergeLines(const Slice& lhs); void buildSearchMap(); void buildTriangulations(); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index fe92b76..205c3a6 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -8,7 +8,6 @@ add_library(tessellator-core "Smoother.cpp" "SmootherTools.cpp" "Staircaser.cpp" - "VolumeFiller.cpp" ) target_link_libraries(tessellator-core diff --git a/src/core/VolumeFiller.cpp b/src/core/VolumeFiller.cpp deleted file mode 100644 index 0786e1d..0000000 --- a/src/core/VolumeFiller.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "VolumeFiller.h" -#include "cgal/filler/Filler.h" -#include "cgal/filler/FillerTools.h" -#ifdef TESSELLATOR_EXECUTION_POLICIES -#include -#endif - - -namespace meshlib { -namespace core { -using namespace utils; - - -Elements buildTriangleElements(Coordinates& cs, const TriVs tris) -{ - Elements r; - for (const auto& p : tris) { - Element e; - e.type = Element::Type::Surface; - e.vertices.reserve(3); - for (const auto& v : p) { - e.vertices.push_back(cs.size()); - cs.push_back(v); - } - r.push_back(e); - } - return r; -} -Elements buildLineElements(Coordinates& cs, const LinVs lins) -{ - Elements r; - for (const auto& p : lins) { - Element e; - e.type = Element::Type::Line; - e.vertices.reserve(2); - for (const auto& v : p) { - e.vertices.push_back(cs.size()); - cs.push_back(v); - } - r.push_back(e); - } - return r; -} - -VolumeFiller::VolumeFiller(const Mesh& input) -{ - Mesh m; - mesh_.grid = input.grid; - mesh_.groups.resize(input.groups.size()); - - meshlib::cgal::filler::Filler f{input}; - mesh_ = f.getMeshFilling(); - // auto slices = f.getSlices(); - // for (auto gId{0}; gId < mesh_.groups.size(); ++gId) { - - // for (const auto& x : { X, Y, Z }) { - // for (const auto& [i, slice]: slices[x]) { - // // const Priority pr{ getGroupPriority(gId) }; - // const Priority pr{ 0 }; - // auto trivs = slice.buildAllTriVs(pr, x, (meshlib::cgal::filler::Height)i); - // auto triEls = buildTriangleElements( - // m.coordinates, - // slice.buildAllTriVs(pr, x, (meshlib::cgal::filler::Height)i) - // ); - - // auto linvs = slice.buildAllLinVs(pr, x, (meshlib::cgal::filler::Height)i); - // auto lineEls = buildLineElements( - // mesh_.coordinates, - // slice.buildAllLinVs(pr, x, (meshlib::cgal::filler::Height)i) - // ); - - // } - // } - // } - //take structured mesh - //use filler to generate the slices: intersections of each grid plane with the mesh - //convert the slice intersection into a ElemV with dimension n, where n is the number of points of the slice intersection - //build a coordIdMap similar to the one created in Slicer::sliceTriangle, where instead of the intersection of a triangle with the grid planes, we have the intersection of the n-dimensional ElemV - //Create a triangulation of the intersection of the slice with the grid planes, following also slicer::sliceTriangle -} - -Mesh VolumeFiller::getMesh() -{ - return mesh_; -} - - - -} -} \ No newline at end of file diff --git a/src/core/VolumeFiller.h b/src/core/VolumeFiller.h deleted file mode 100644 index 439c9fc..0000000 --- a/src/core/VolumeFiller.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "utils/GridTools.h" - -namespace meshlib{ -namespace core{ - -class VolumeFiller : public utils::GridTools{ -public: - - VolumeFiller(const Mesh&); - Mesh getMesh(); -private: - Mesh mesh_; -}; - -} -} \ No newline at end of file diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 35bd6b5..37b92db 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -33,7 +33,7 @@ StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInColla log("Preparing volumes"); volumeMesh_ = MesherBase::buildVolumeMesh(inputMesh, opts_.volumeGroups); - fillMesh(volumeMesh_, opts_.volumeGroups); + fillMesh(volumeMesh_); log("Processing volume mesh."); process(volumeMesh_); @@ -42,22 +42,21 @@ StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInColla log("Mesh built succesfully.", 1); } -void StaircaseMesher::fillMesh(Mesh& m, const std::set& volumeGroups){ +void StaircaseMesher::fillMesh(Mesh& m){ if (m.countElems() == 0) return; - // auto mani = meshlib::cgal::Manifolder(m); - // for (const auto& gId : volumeGroups){ - // m.groups[gId].elements = mani.getClosedSurfacesMesh().groups[gId].elements; - // } - m.coordinates = utils::GridTools{m.grid}.absoluteToRelative(m.coordinates); - m = meshlib::cgal::filler::Filler(m).getMeshFilling(); - m.coordinates = utils::GridTools{m.grid}.relativeToAbsolute(m.coordinates); - - // m = f.getMeshFilling(); - - // auto filling = f.getMeshFilling(); - // mergeMesh(m, filling); - // // auto dimensions = getHighestDimensionByGroup(m); - // // RedundancyCleaner::removeOverlappedElementsByDimension(m, dimensions); + meshlib::cgal::filler::FillerMode mode = meshlib::cgal::filler::FillerMode::onlyInside; + if (m.groups[0].elements[0].isTetrahedron()) { + mode = meshlib::cgal::filler::FillerMode::insideAndOutside; + } + auto filling = m; + filling.coordinates = utils::GridTools{filling.grid}.absoluteToRelative(filling.coordinates); + filling = meshlib::cgal::filler::Filler(filling, Mesh(), std::vector(), mode).getMeshFilling(); + filling.coordinates = utils::GridTools{filling.grid}.relativeToAbsolute(filling.coordinates); + if (mode == meshlib::cgal::filler::FillerMode::insideAndOutside){ + m = filling; + } else if (mode == meshlib::cgal::filler::FillerMode::onlyInside){ + mergeMesh(m, filling); + } } @@ -84,11 +83,18 @@ void copyGroupNames(Mesh& m, const std::vector& names){ } } +static Mesh toAbsolute(const Mesh& m) +{ + auto r{ m }; + r.coordinates = + utils::GridTools{ m.grid }.relativeToAbsolute(m.coordinates); + return r; +} + void StaircaseMesher::process(Mesh& mesh) const { const auto groupNames = getGroupNames(mesh.groups); const auto slicingGrid{ buildSlicingGrid(originalGrid_, enlargedGrid_) }; - if (mesh.countElems() == 0) { // mesh.grid = slicingGrid; return; @@ -102,11 +108,6 @@ void StaircaseMesher::process(Mesh& mesh) const logNumberOfTriangles(countMeshElementsIf(mesh, isTriangle)); - // meshlib::cgal::filler::Filler f{mesh}; - // auto filling = f.getMeshFilling(); - // mergeMesh(mesh, filling); - - log("Collapsing.", 1); mesh = Collapser(mesh, decimalPlacesInCollapser_, dimensions).getMesh(); diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index 037d4e8..c6be343 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -21,7 +21,7 @@ class StaircaseMesher : public MesherBase { StaircaseMesherOptions opts_; virtual Mesh buildSurfaceMesh(const Mesh& inputMesh, const Mesh& volumeSurface); - static void fillMesh(Mesh& inputMesh, const std::set& volumeGroups); + static void fillMesh(Mesh& inputMesh); void process(Mesh&) const; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b4fbb61..638a03a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -17,7 +17,6 @@ add_executable(tessellator_tests "core/CollapserTest.cpp" "core/CompressorTest.cpp" "core/SlicerTest.cpp" - "core/VolumeFillerTest.cpp" "core/SmootherTest.cpp" "core/SmootherToolsTest.cpp" "core/SnapperTest.cpp" diff --git a/test/cgal/filler/FillerTest.cpp b/test/cgal/filler/FillerTest.cpp index b2230e6..e8fbfa7 100644 --- a/test/cgal/filler/FillerTest.cpp +++ b/test/cgal/filler/FillerTest.cpp @@ -155,6 +155,14 @@ class FillerTest : public ::testing::Test { return r; } + static Mesh toAbsolute(const Mesh& m) + { + auto r{ m }; + r.coordinates = + utils::GridTools{ m.grid }.relativeToAbsolute(m.coordinates); + return r; + } + static bool allAreSimple(const FaceFilling& ff) { for (const auto [pr, ss] : ff.tris) { @@ -337,6 +345,59 @@ TEST_F(FillerTest, parallelogram_as_surface) EXPECT_EQ(0, countPWHs(f.getFaceFilling({ Cell({0, 0, 1}), Z }))); } +TEST_F(FillerTest, fill_cube1x1x1_size1_grid) +{ + Mesh m = buildCubeSurfaceMesh(01.0); + + Mesh out; + ASSERT_NO_THROW(out = Slicer{m}.getMesh()); + EXPECT_EQ(12, countMeshElementsIf(out, isTriangle)); + + Mesh filled = Filler{out}.getMeshFilling(); + EXPECT_EQ(12, countMeshElementsIf(filled, isTriangle)); +} + +TEST_F(FillerTest, fill_cube1x1x1_size05_grid) +{ + //filling with unstruc. triangles + Mesh m = buildCubeSurfaceMesh(0.5); + Mesh filled = Filler{Slicer{buildCubeSurfaceMesh(0.5) }.getMesh()}.getMeshFilling(); + Mesh filled_no_slicing = Filler{toRelative(buildCubeSurfaceMesh(0.5))}.getMeshFilling(); + EXPECT_EQ(18, countMeshElementsIf(filled, isTriangle)); + EXPECT_EQ(18, countMeshElementsIf(filled_no_slicing, isTriangle)); + + //slicing hull + Mesh out_1; + ASSERT_NO_THROW(out_1 = Slicer{buildCubeSurfaceMesh(0.5) }.getMesh()); + EXPECT_EQ(48, countMeshElementsIf(out_1, isTriangle)); + + //filling and slicing + Mesh out_2; + ASSERT_NO_THROW(out_2 = Slicer{toAbsolute(filled) }.getMesh()); + EXPECT_EQ(72, countMeshElementsIf(out_2, isTriangle)); + + ASSERT_NO_THROW(out_2 = Slicer{toAbsolute(filled_no_slicing)}.getMesh()); + EXPECT_EQ(72, countMeshElementsIf(out_2, isTriangle)); + +} + +TEST_F(FillerTest, fill_cube1x1x1_size025_grid) +{ + Mesh m = buildCubeSurfaceMesh(0.25); + Mesh filled = Filler{Slicer{buildCubeSurfaceMesh(0.25) }.getMesh()}.getMeshFilling(); + EXPECT_EQ(30, countMeshElementsIf(filled, isTriangle)); + + Mesh out_1; + ASSERT_NO_THROW(out_1 = Slicer{buildCubeSurfaceMesh(0.25) }.getMesh()); + EXPECT_EQ(192, countMeshElementsIf(out_1, isTriangle)); + + Mesh out_2; + ASSERT_NO_THROW(out_2 = Slicer{toAbsolute(filled) }.getMesh()); + EXPECT_EQ(480, countMeshElementsIf(out_2, isTriangle)); + +} + + TEST_F(FillerTest, planeXY_mesh_filling) { Filler f{ Slicer{ buildPlaneXYMesh(1.0) }.getMesh() }; diff --git a/test/core/VolumeFillerTest.cpp b/test/core/VolumeFillerTest.cpp deleted file mode 100644 index e2a475e..0000000 --- a/test/core/VolumeFillerTest.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "MeshFixtures.h" -#include "gtest/gtest.h" - -#include "VolumeFiller.h" -#include "Slicer.h" -#include "Geometry.h" -#include "MeshTools.h" -#include "GridTools.h" -// #include "utils/RedundancyCleaner.h" -// #include "utils/CoordGraph.h" - -#if APP_LOADED - #include "app/vtkIO.h" -#endif - -namespace meshlib::core { - -using namespace meshFixtures; -using namespace utils; -using namespace meshTools; - -class VolumeFillerTest : public ::testing::Test { -public: -protected: -}; - -static Mesh toRelative(const Mesh& m) -{ - auto r{ m }; - r.coordinates = - utils::GridTools{ m.grid }.absoluteToRelative(m.coordinates); - return r; -} - -TEST_F(VolumeFillerTest, fill_cube1x1x1_size1_grid) -{ - Mesh m = buildCubeSurfaceMesh(01.0); - - Mesh out; - ASSERT_NO_THROW(out = Slicer{m}.getMesh()); - EXPECT_EQ(12, countMeshElementsIf(out, isTriangle)); - - Mesh filled = VolumeFiller{out}.getMesh(); - EXPECT_EQ(12, countMeshElementsIf(filled, isTriangle)); -} - -TEST_F(VolumeFillerTest, fill_cube1x1x1_size05_grid) -{ - //filling with unstruc. triangles - Mesh m = buildCubeSurfaceMesh(0.5); - Mesh filled = VolumeFiller{Slicer{buildCubeSurfaceMesh(0.5) }.getMesh()}.getMesh(); - Mesh filled_no_slicing = VolumeFiller{toRelative(buildCubeSurfaceMesh(0.5))}.getMesh(); - EXPECT_EQ(18, countMeshElementsIf(filled, isTriangle)); - EXPECT_EQ(18, countMeshElementsIf(filled_no_slicing, isTriangle)); - - //slicing hull - Mesh out_1; - ASSERT_NO_THROW(out_1 = Slicer{buildCubeSurfaceMesh(0.5) }.getMesh()); - EXPECT_EQ(48, countMeshElementsIf(out_1, isTriangle)); - - //filling and slicing - Mesh out_2; - GridTools gT{ filled.grid }; - filled.coordinates = gT.relativeToAbsolute(filled.coordinates); - ASSERT_NO_THROW(out_2 = Slicer{filled }.getMesh()); - EXPECT_EQ(72, countMeshElementsIf(out_2, isTriangle)); - - GridTools gT_no{ filled_no_slicing.grid }; - filled_no_slicing.coordinates = gT_no.relativeToAbsolute(filled_no_slicing.coordinates); - ASSERT_NO_THROW(out_2 = Slicer{filled_no_slicing}.getMesh()); - EXPECT_EQ(72, countMeshElementsIf(out_2, isTriangle)); - -} - -TEST_F(VolumeFillerTest, fill_cube1x1x1_size025_grid) -{ - Mesh m = buildCubeSurfaceMesh(0.25); - Mesh filled = VolumeFiller{Slicer{buildCubeSurfaceMesh(0.25) }.getMesh()}.getMesh(); - EXPECT_EQ(30, countMeshElementsIf(filled, isTriangle)); - - Mesh out_1; - ASSERT_NO_THROW(out_1 = Slicer{buildCubeSurfaceMesh(0.25) }.getMesh()); - EXPECT_EQ(192, countMeshElementsIf(out_1, isTriangle)); - - Mesh out_2; - GridTools gT{ filled.grid }; - filled.coordinates = gT.relativeToAbsolute(filled.coordinates); - - ASSERT_NO_THROW(out_2 = Slicer{filled }.getMesh()); - EXPECT_EQ(480, countMeshElementsIf(out_2, isTriangle)); - - // Mesh filled = VolumeFiller{out}.getMesh(); - // ASSERT_NO_THROW(out = Slicer{filled}.getMesh()); - // EXPECT_EQ(72, countMeshElementsIf(out, isTriangle)); -} -} \ No newline at end of file From 1d5e5ef6f2271453f9c0b22f709a21b038b6fd19 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Tue, 28 Jul 2026 13:49:19 +0200 Subject: [PATCH 52/61] Minor to avoid copying coordinates from unused mesh in mergeMesh. Adds function to convert to relativeCoordinates --- src/meshers/StaircaseMesher.cpp | 5 ++--- src/utils/MeshTools.cpp | 13 ++++++++++++- src/utils/MeshTools.h | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 37b92db..d2d9006 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -49,15 +49,14 @@ void StaircaseMesher::fillMesh(Mesh& m){ mode = meshlib::cgal::filler::FillerMode::insideAndOutside; } auto filling = m; - filling.coordinates = utils::GridTools{filling.grid}.absoluteToRelative(filling.coordinates); + utils::meshTools::convertToRelativeCoordinates(filling); filling = meshlib::cgal::filler::Filler(filling, Mesh(), std::vector(), mode).getMeshFilling(); - filling.coordinates = utils::GridTools{filling.grid}.relativeToAbsolute(filling.coordinates); + utils::meshTools::convertToAbsoluteCoordinates(filling); if (mode == meshlib::cgal::filler::FillerMode::insideAndOutside){ m = filling; } else if (mode == meshlib::cgal::filler::FillerMode::onlyInside){ mergeMesh(m, filling); } - } diff --git a/src/utils/MeshTools.cpp b/src/utils/MeshTools.cpp index a86479e..00e0f1b 100644 --- a/src/utils/MeshTools.cpp +++ b/src/utils/MeshTools.cpp @@ -309,6 +309,17 @@ void convertToAbsoluteCoordinates(Mesh& m) ); } +void convertToRelativeCoordinates(Mesh& m) +{ + GridTools gT{ m.grid }; + + std::transform( + m.coordinates.begin(), m.coordinates.end(), + m.coordinates.begin(), + [&](const auto& v) { return gT.getRelative(v); } + ); +} + void checkSlicedMeshInvariants(const Mesh& m) { checkNoCellsAreCrossed(m); @@ -389,7 +400,7 @@ void mergeMesh(Mesh& lMesh, const Mesh& iMesh) assert(lMesh.groups.size() == iMesh.groups.size()); auto coordCount{ lMesh.coordinates.size() }; - + if (iMesh.countElems() == 0) return; lMesh.coordinates.insert(lMesh.coordinates.end(), iMesh.coordinates.begin(), iMesh.coordinates.end()); diff --git a/src/utils/MeshTools.h b/src/utils/MeshTools.h index 5fc4546..9bfed81 100644 --- a/src/utils/MeshTools.h +++ b/src/utils/MeshTools.h @@ -35,6 +35,7 @@ void reduceGrid(Mesh&, const Grid&); Mesh reduceGrid(const Mesh& m, const Grid& g); void convertToAbsoluteCoordinates(Mesh&); +void convertToRelativeCoordinates(Mesh&); void checkSlicedMeshInvariants(Mesh& m); From 0572cb3054a6a16e0da7ba55520130fb94eb55f6 Mon Sep 17 00:00:00 2001 From: Alberto-o Date: Thu, 30 Jul 2026 12:28:39 +0200 Subject: [PATCH 53/61] Minors answering PR comments --- .gitignore | 1 + src/core/Slicer.cpp | 26 -------------------------- src/core/Slicer.h | 2 -- 3 files changed, 1 insertion(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 1fd2b13..130eeec 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ src/*.json .vs/ .vscode/settings.json +.vscode/settings.dev.json .vscode/launch.json .vscode/tasks.json diff --git a/src/core/Slicer.cpp b/src/core/Slicer.cpp index a33defa..8246fff 100644 --- a/src/core/Slicer.cpp +++ b/src/core/Slicer.cpp @@ -38,32 +38,6 @@ void orient(const Coordinates& coords, } } -// Mesh Slicer::fill(const Mesh& input){ -// //input mesh should be structured - -// Coordinates sCoords; -// sCoords.reserve(input.coordinates.size() * 100); - -// for (const auto& g : input.groups){ - -// for (auto const& itCell : -// buildCellCoordIdMap(sCoords, buildGroupIntersectionsWithGridPlanes(sCoords, g.elements))) { -// const IdSet& vIds = itCell.second; -// if (vIds.size() < 3) { -// continue; -// } -// } - -// } -// } - -// IdSet Slicer::buildGroupIntersectionsWithGridPlanes( -// Coordinates& sCoords, -// const std::vector& elements) -// { - -// } - Slicer::Slicer(const Mesh& input, const std::vector& dimensionPolicy, const SlicerOptions& opts) : GridTools(input.grid), opts_(opts) diff --git a/src/core/Slicer.h b/src/core/Slicer.h index 7f418a4..b51ae15 100644 --- a/src/core/Slicer.h +++ b/src/core/Slicer.h @@ -24,8 +24,6 @@ class Slicer : public utils::GridTools { Slicer(const Mesh&, const std::vector& dimensionPolicy = {}, const SlicerOptions& opts = SlicerOptions()); Mesh getMesh() const { return mesh_; }; - // Mesh fill(const Mesh&); - static Elements buildTrianglesFromPath(const std::vector&, const std::vector&); private: From 688f9f1b1b8a8eb24de482254bcefe95c216efba Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Thu, 6 Aug 2026 13:18:50 +0200 Subject: [PATCH 54/61] Fixes VTK export for Paraview compatibility. --- src/app/vtkIO.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index d55a561..0bdc367 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -170,7 +170,9 @@ vtkSmartPointer toVTKGroupNamesArray(const Mesh& mesh) groupNamesArray->SetNumberOfComponents(1); for (const auto& group : mesh.groups) { - groupNamesArray->InsertNextValue(group.name.c_str()); + for (std::size_t e = 0; e < group.elements.size(); e++) { + groupNamesArray->InsertNextValue(group.name.c_str()); + } } return groupNamesArray; From cce5a555648303d5988a6cd13c7c8902f832d180 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Thu, 6 Aug 2026 17:20:23 +0200 Subject: [PATCH 55/61] Fix conformal smoothing failures --- src/core/Smoother.cpp | 5 +- src/core/SmootherTools.cpp | 62 +++++++++++++++-- src/core/SmootherTools.h | 9 ++- src/utils/CoordGraph.cpp | 12 +++- test/core/SmootherToolsTest.cpp | 114 +++++++++++++++++++++++++++++++- test/utils/CoordGraphTest.cpp | 27 +++++++- 6 files changed, 219 insertions(+), 10 deletions(-) diff --git a/src/core/Smoother.cpp b/src/core/Smoother.cpp index 9a15452..ff61a17 100644 --- a/src/core/Smoother.cpp +++ b/src/core/Smoother.cpp @@ -51,6 +51,7 @@ Smoother::Smoother(const Mesh& mesh, const SmootherOptions& opts) : sT_.collapsePointsOnCellEdges(res.coordinates, p, singularIds, opts_.contourAlignmentAngle); }); + const Coordinates coordinatesBeforeFeatureCollapse = res.coordinates; std::for_each( #ifdef TESSELLATOR_EXECUTION_POLICIES std::execution::par, @@ -66,6 +67,8 @@ Smoother::Smoother(const Mesh& mesh, const SmootherOptions& opts) : patchs.begin(), patchs.end(), [&](auto& p) { sT_.collapsePointsOnFeatureEdges(res.coordinates, p, singularIds); }); + sT_.revertMovesThatCrossGrid( + g.elements, res.coordinates, coordinatesBeforeFeatureCollapse); std::for_each( #ifdef TESSELLATOR_EXECUTION_POLICIES @@ -95,4 +98,4 @@ Smoother::Smoother(const Mesh& mesh, const SmootherOptions& opts) : } } -} \ No newline at end of file +} diff --git a/src/core/SmootherTools.cpp b/src/core/SmootherTools.cpp index fd38246..ae14d7a 100644 --- a/src/core/SmootherTools.cpp +++ b/src/core/SmootherTools.cpp @@ -57,6 +57,39 @@ void SmootherTools::updateCoordinates( } } +void SmootherTools::revertMovesThatCrossGrid( + const Elements& elements, + Coordinates& coordinates, + const Coordinates& originalCoordinates) const +{ + IdSet movedIds; + for (CoordinateId id = 0; id < coordinates.size(); ++id) { + if (coordinates[id] != originalCoordinates[id]) { + movedIds.insert(id); + } + } + + std::map incidentElements; + for (const auto& element : elements) { + for (const auto id : element.vertices) { + if (movedIds.count(id) != 0) { + incidentElements[id].push_back(&element); + } + } + } + + for (const auto id : movedIds) { + const bool crossesGrid = std::any_of( + incidentElements[id].begin(), incidentElements[id].end(), + [&](const Element* element) { + return elementCrossesGrid(*element, coordinates); + }); + if (crossesGrid) { + coordinates[id] = originalCoordinates[id]; + } + } +} + void SmootherTools::collapsePointsOnFeatureEdges( Coordinates& coords, const ElementsView& patch, @@ -102,7 +135,24 @@ void SmootherTools::collapsePointsOnFeatureEdges( continue; } - Coordinate closest = closestByDistance(coords, i, Point.getClosestVerticesInSet(i, validExterior)); + auto candidates = Point.getClosestVerticesInSet(i, validExterior); + const auto sourceCells = getTouchingCells(coords[i]); + for (auto candidate = candidates.begin(); candidate != candidates.end();) { + const auto candidateCells = getTouchingCells(coords[*candidate]); + const bool sharesCell = std::any_of( + candidateCells.begin(), candidateCells.end(), + [&](const Cell& cell) { return sourceCells.count(cell) != 0; }); + if (sharesCell) { + ++candidate; + } + else { + candidate = candidates.erase(candidate); + } + } + if (candidates.empty()) { + continue; + } + Coordinate closest = closestByDistance(coords, i, candidates); if (isRelativeInCellFace(coords[i]) && !areCoordOnSameFace(coords[i], closest)) { continue; } @@ -207,7 +257,11 @@ void SmootherTools::collapsePointsOnCellEdges( if (isRelativeInCellCorner(coords[i])) { continue; } - Coordinate closest = closestByDistance(coords, i, cG.getClosestVerticesInSet(i, validIds)); + const auto candidates = cG.getClosestVerticesInSet(i, validIds); + if (candidates.empty()) { + continue; + } + Coordinate closest = closestByDistance(coords, i, candidates); if (isRelativeInCellFace(coords[i]) && !areCoordOnSameFace(coords[i], closest)) { continue; } @@ -426,7 +480,7 @@ CoordGraph::Path SmootherTools::pathFromIdToAnyTarget( } const std::size_t i = it - cycle.begin(); - CoordGraph::Path res(startId); + CoordGraph::Path res{startId}; for (std::size_t d = 1; d < cycle.size(); d++) { CoordinateId idTest; if (forward) { @@ -573,4 +627,4 @@ void SmootherTools::reorientSingleElement( } } -} \ No newline at end of file +} diff --git a/src/core/SmootherTools.h b/src/core/SmootherTools.h index 973de4f..2492f54 100644 --- a/src/core/SmootherTools.h +++ b/src/core/SmootherTools.h @@ -41,6 +41,11 @@ class SmootherTools : public utils::GridTools { const ElementsView& patch, const SingularIds& singularIds); + void revertMovesThatCrossGrid( + const Elements& elements, + Coordinates& coordinates, + const Coordinates& originalCoordinates) const; + Coordinates collapsePointsOnContour( const Elements& elems, const Coordinates& coords, @@ -83,6 +88,8 @@ class SmootherTools : public utils::GridTools { const ElementsView& patch); private: + friend class SmootherToolsTestAccess; + std::mutex writingCoordinates_; std::mutex writingElements_; @@ -136,4 +143,4 @@ class SmootherTools : public utils::GridTools { }; } -} \ No newline at end of file +} diff --git a/src/utils/CoordGraph.cpp b/src/utils/CoordGraph.cpp index e4242f6..0e5e0c3 100644 --- a/src/utils/CoordGraph.cpp +++ b/src/utils/CoordGraph.cpp @@ -81,6 +81,10 @@ CoordGraph::CoordGraph(const Elements& elems) CoordGraph::CoordGraph(const ElementsView& es) { for (auto const& e : es) { + if (e->vertices.size() == 1) { + addVertex(e->vertices.front()); + continue; + } for (std::size_t i = 0; i < e->vertices.size(); i++) { this->addEdge( e->vertices[i], @@ -95,6 +99,10 @@ CoordGraph::CoordGraph(const ElementsView& es) CoordGraph::CoordGraph(const Paths& paths) { for (const auto& p : paths) { + if (p.size() == 1) { + addVertex(p.front()); + continue; + } for (std::size_t i = 0; i < p.size(); i++) { this->addEdge( p[i], @@ -221,7 +229,7 @@ IdSet CoordGraph::getClosestVerticesInSet( } auto path = findShortestPath(vI, vB); if (path.empty()) { - throw std::runtime_error("Can not find path to point in set."); + continue; } paths.insert(path); } @@ -586,4 +594,4 @@ std::vector CoordGraph::findCycles() const } } -} \ No newline at end of file +} diff --git a/test/core/SmootherToolsTest.cpp b/test/core/SmootherToolsTest.cpp index 78e2670..a4923f8 100644 --- a/test/core/SmootherToolsTest.cpp +++ b/test/core/SmootherToolsTest.cpp @@ -419,6 +419,30 @@ class SmootherToolsTest : public ::testing::Test { const double alignmentAngle = 5.0; }; +class SmootherToolsTestAccess { +public: + static CoordGraph::Path pathFromIdToAnyTarget( + const CoordinateId startId, + const CoordGraph::Path& cycle, + const bool forward, + const IdSet& target) + { + return SmootherTools::pathFromIdToAnyTarget(startId, cycle, forward, target); + } +}; + +TEST_F(SmootherToolsTest, pathFromLargeCoordinateIdContainsTheIdOnce) +{ + const CoordinateId startId = 1000; + const CoordinateId targetId = 1001; + const CoordGraph::Path cycle = {999, startId, targetId}; + + const auto path = SmootherToolsTestAccess::pathFromIdToAnyTarget( + startId, cycle, true, {targetId}); + + EXPECT_EQ((CoordGraph::Path{startId, targetId}), path); +} + /// Remesh patch with interior points to remove them. /// \verbatim /// 1 ---- 2 @@ -673,6 +697,94 @@ TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdges_singlePatch) EXPECT_EQ(7, countDifferentCoordinates(cs)); } +TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdges_withDisconnectedTargets) +{ + Mesh m; + m.grid = buildUnitLengthGrid(1.0); + m.coordinates = { + Coordinate({0.1, 0.1, 0.2}), + Coordinate({0.5, 0.1, 0.2}), + Coordinate({0.9, 0.1, 0.2}), + Coordinate({0.5, 0.4, 0.2}), + Coordinate({0.1, 0.6, 0.8}), + Coordinate({0.5, 0.6, 0.8}), + Coordinate({0.9, 0.6, 0.8}), + Coordinate({0.5, 0.9, 0.8}), + }; + m.groups = {Group()}; + m.groups[0].elements = { + Element({0, 1, 3}), + Element({1, 2, 3}), + Element({4, 5, 7}), + Element({5, 6, 7}), + }; + + Elements& elements = m.groups[0].elements; + const ElementsView disconnectedPatch = { + &elements[0], &elements[1], &elements[2], &elements[3] + }; + const SmootherTools::SingularIds singularIds( + {0, 1, 2, 4, 5, 6}, {}, {}); + + EXPECT_NO_THROW(SmootherTools(m.grid).collapsePointsOnFeatureEdges( + m.coordinates, disconnectedPatch, singularIds)); + EXPECT_EQ(m.coordinates[0], m.coordinates[1]); + EXPECT_EQ(m.coordinates[4], m.coordinates[5]); +} + +TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdges_staysInTouchingCell) +{ + Mesh m; + m.grid = buildUnitLengthGrid(0.25); + m.coordinates = { + Coordinate({0.9, 0.1, 0.2}), + Coordinate({1.2, 0.1, 0.2}), + Coordinate({1.9, 0.1, 0.2}), + Coordinate({1.5, 0.4, 0.2}), + }; + m.groups = {Group()}; + m.groups[0].elements = { + Element({0, 1, 3}), + Element({1, 2, 3}), + }; + + Elements& elements = m.groups[0].elements; + const ElementsView patch = {&elements[0], &elements[1]}; + const SmootherTools::SingularIds singularIds({0, 1, 2}, {}, {}); + + SmootherTools(m.grid).collapsePointsOnFeatureEdges( + m.coordinates, patch, singularIds); + + EXPECT_EQ(m.coordinates[2], m.coordinates[1]); +} + +TEST_F(SmootherToolsTest, revertsCombinedFeatureMovesThatCrossGrid) +{ + Mesh m; + m.grid = buildUnitLengthGrid(0.25); + m.coordinates = { + Coordinate({1.0, 0.2, 0.2}), + Coordinate({1.0, 0.5, 0.2}), + Coordinate({1.0, 0.8, 0.2}), + }; + m.groups = {Group()}; + m.groups[0].elements = {Element({0, 1, 2})}; + + const Coordinates originalCoordinates = m.coordinates; + m.coordinates[0][X] = 0.0; + m.coordinates[2][X] = 2.0; + ASSERT_TRUE(GridTools(m.grid).elementCrossesGrid( + m.groups[0].elements[0], m.coordinates)); + + SmootherTools(m.grid).revertMovesThatCrossGrid( + m.groups[0].elements, m.coordinates, originalCoordinates); + + EXPECT_FALSE(GridTools(m.grid).elementCrossesGrid( + m.groups[0].elements[0], m.coordinates)); + EXPECT_EQ(originalCoordinates[0], m.coordinates[0]); + EXPECT_EQ(2.0, m.coordinates[2][X]); +} + TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdges_threePatches) { Mesh m = buildCornerMesh(); @@ -860,4 +972,4 @@ TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdges_feature_in_interior) EXPECT_EQ(collapsed[5], collapsed[4]); } -} \ No newline at end of file +} diff --git a/test/utils/CoordGraphTest.cpp b/test/utils/CoordGraphTest.cpp index 6a98958..54e9259 100644 --- a/test/utils/CoordGraphTest.cpp +++ b/test/utils/CoordGraphTest.cpp @@ -341,6 +341,20 @@ TEST_F(CoordGraphTest, ctors) EXPECT_EQ(cG.verticesSize(), cGViews.verticesSize()); EXPECT_EQ(cG.edgesSize(), cGViews.edgesSize()); } + +TEST_F(CoordGraphTest, constructorsAcceptSingleVertexInputs) +{ + const Elements elements = { + Element({7}, Element::Type::Node), + }; + const CoordGraph fromElements(elements); + const CoordGraph fromPaths(CoordGraph::Paths{{9}}); + + EXPECT_EQ(IdSet({7}), fromElements.getVertices()); + EXPECT_EQ(IdSet({9}), fromPaths.getVertices()); + EXPECT_EQ(0, fromElements.edgesSize()); + EXPECT_EQ(0, fromPaths.edgesSize()); +} TEST_F(CoordGraphTest, adding_edge_with_non_existingvertices) { CoordGraph g; @@ -563,6 +577,17 @@ TEST_F(CoordGraphTest, getClosestVerticesInSet_2) EXPECT_EQ(IdSet({ 3 }), g.getClosestVerticesInSet(2, { 3, 4 })); } + +TEST_F(CoordGraphTest, getClosestVerticesInSet_ignoresDisconnectedTargets) +{ + CoordGraph g; + g.addEdge(1, 2); + g.addEdge(3, 4); + + EXPECT_EQ(IdSet({2}), g.getClosestVerticesInSet(1, {2, 3})); + EXPECT_TRUE(g.getClosestVerticesInSet(1, {3}).empty()); +} + TEST_F(CoordGraphTest, graphIntersection) { @@ -1341,4 +1366,4 @@ TEST_F(CoordGraphTest, difference) } } -} \ No newline at end of file +} From 180ea1e698ab2209a6d0d1af4e299aa24b8a4eec Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Mon, 10 Aug 2026 10:36:26 +0200 Subject: [PATCH 56/61] Smoother now works sequentially on cell edges, faces and interiors. --- CMakePresets.json | 1 - src/core/Smoother.cpp | 126 +++++++++++++++++++++++++------- src/core/SmootherTools.cpp | 99 ++++++++++++++++++------- src/core/SmootherTools.h | 30 ++++++-- test/core/SmootherToolsTest.cpp | 43 +++++++---- 5 files changed, 218 insertions(+), 81 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 769d22e..3166b87 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -26,7 +26,6 @@ { "name": "gnu", "displayName": "GNU g++ compiler", - "generator": "Ninja", "inherits": "default" }, { diff --git a/src/core/Smoother.cpp b/src/core/Smoother.cpp index ff61a17..9dc38ea 100644 --- a/src/core/Smoother.cpp +++ b/src/core/Smoother.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #ifdef TESSELLATOR_EXECUTION_POLICIES #include @@ -19,6 +21,63 @@ namespace core { using namespace utils; using namespace meshTools; +namespace { + +using GridEntity = std::pair; + +struct OwnedCoordinates { + std::map edges; + std::map faces; + std::map interiors; +}; + +Cell canonicalCell(Cell cell, const SmootherTools& tools) +{ + for (Axis axis = X; axis <= Z; ++axis) { + cell[axis] = std::max(0, std::min(cell[axis], tools.numCellsDir(axis) - 1)); + } + return cell; +} + +OwnedCoordinates buildOwnedCoordinates( + const Coordinates& coordinates, + const SmootherTools& tools) +{ + OwnedCoordinates owned; + for (CoordinateId id = 0; id < coordinates.size(); ++id) { + const auto& coordinate = coordinates[id]; + if (tools.isRelativeInCellCorner(coordinate)) { + continue; // Grid corners are fixed throughout smoothing. + } + + Cell cell = canonicalCell(tools.toCell(coordinate), tools); + const auto edge = tools.getCellEdgeAxis(coordinate); + if (edge.first) { + owned.edges[{cell, edge.second}].insert(id); + continue; + } + const auto face = tools.getCellFaceAxis(coordinate); + if (face.first) { + owned.faces[{cell, face.second}].insert(id); + continue; + } + owned.interiors[cell].insert(id); + } + return owned; +} + +SmootherTools::IncidentElements buildIncidentElements(const Elements& elements) +{ + SmootherTools::IncidentElements incident; + for (const auto& element : elements) { + for (const auto id : element.vertices) { + incident[id].push_back(&element); + } + } + return incident; +} + +} Smoother::Smoother(const Mesh& mesh, const SmootherOptions& opts) : sT_(SmootherTools(mesh.grid)), @@ -35,6 +94,9 @@ Smoother::Smoother(const Mesh& mesh, const SmootherOptions& opts) : auto const singularIds = sT_.buildSingularIds(g.elements, mesh_.coordinates, opts_.featureDetectionAngle); + // Boundary remeshing is preprocessing. Keep its smooth sets separate + // from the ownership phases below: an element can touch several grid + // entities, while a coordinate has exactly one canonical owner. std::vector patchs; for (auto const& cell : sT_.buildCellElemMap(g.elements, mesh_.coordinates)) { for (auto const& p : @@ -46,37 +108,45 @@ Smoother::Smoother(const Mesh& mesh, const SmootherOptions& opts) : std::for_each(patchs.begin(), patchs.end(), [&](auto& p) { sT_.remeshBoundary(g.elements, res.coordinates, mesh_.coordinates, p); }); - - std::for_each(patchs.begin(), patchs.end(), [&](auto& p) { - sT_.collapsePointsOnCellEdges(res.coordinates, p, singularIds, opts_.contourAlignmentAngle); - }); - const Coordinates coordinatesBeforeFeatureCollapse = res.coordinates; - std::for_each( -#ifdef TESSELLATOR_EXECUTION_POLICIES - std::execution::par, -#endif - patchs.begin(), patchs.end(), [&](auto& p) { - sT_.collapsePointsOnCellFaces(res.coordinates, p, singularIds); - }); + const auto incidentElements = buildIncidentElements(g.elements); + const auto owned = buildOwnedCoordinates(res.coordinates, sT_); + IdSet edgeIds, faceIds, interiorIds; + for (const auto& entity : owned.edges) { + edgeIds.insert(entity.second.begin(), entity.second.end()); + } + for (const auto& entity : owned.faces) { + faceIds.insert(entity.second.begin(), entity.second.end()); + } + for (const auto& entity : owned.interiors) { + interiorIds.insert(entity.second.begin(), entity.second.end()); + } - std::for_each( -#ifdef TESSELLATOR_EXECUTION_POLICIES - std::execution::par, -#endif - patchs.begin(), patchs.end(), [&](auto& p) { - sT_.collapsePointsOnFeatureEdges(res.coordinates, p, singularIds); - }); - sT_.revertMovesThatCrossGrid( - g.elements, res.coordinates, coordinatesBeforeFeatureCollapse); + // Later phases may read an earlier boundary but never move it. + // This is intentionally sequential: entity ownership makes a future + // conflict graph possible, but entities sharing an element still need + // serialization until that scheduler is introduced. + for (const auto& patch : patchs) { + sT_.collapsePointsOnCellEdges(res.coordinates, patch, singularIds, + opts_.contourAlignmentAngle, edgeIds); + sT_.collapsePointsOnFeatureEdges(res.coordinates, patch, singularIds, + incidentElements, edgeIds); + } + meshTools::checkNoCellsAreCrossed(res); - std::for_each( -#ifdef TESSELLATOR_EXECUTION_POLICIES - std::execution::par, -#endif - patchs.begin(), patchs.end(), [&](auto& p) { - sT_.collapseInteriorPointsToBound(res.coordinates, p); - }); + for (const auto& patch : patchs) { + sT_.collapsePointsOnCellFaces(res.coordinates, patch, singularIds, faceIds); + sT_.collapsePointsOnFeatureEdges(res.coordinates, patch, singularIds, + incidentElements, faceIds); + } + meshTools::checkNoCellsAreCrossed(res); + + for (const auto& patch : patchs) { + sT_.collapsePointsOnFeatureEdges(res.coordinates, patch, singularIds, + incidentElements, interiorIds); + sT_.collapseInteriorPointsToBound(res.coordinates, patch, interiorIds); + } + meshTools::checkNoCellsAreCrossed(res); } diff --git a/src/core/SmootherTools.cpp b/src/core/SmootherTools.cpp index ae14d7a..dc46199 100644 --- a/src/core/SmootherTools.cpp +++ b/src/core/SmootherTools.cpp @@ -57,43 +57,66 @@ void SmootherTools::updateCoordinates( } } -void SmootherTools::revertMovesThatCrossGrid( - const Elements& elements, - Coordinates& coordinates, - const Coordinates& originalCoordinates) const +bool SmootherTools::moveWouldCrossGrid( + const CoordinateId& id, + const Coordinate& destination, + const Coordinates& coordinates, + const IncidentElements& incidentElements) const { - IdSet movedIds; - for (CoordinateId id = 0; id < coordinates.size(); ++id) { - if (coordinates[id] != originalCoordinates[id]) { - movedIds.insert(id); - } + const auto incident = incidentElements.find(id); + if (incident == incidentElements.end()) { + return false; } - std::map incidentElements; - for (const auto& element : elements) { - for (const auto id : element.vertices) { - if (movedIds.count(id) != 0) { - incidentElements[id].push_back(&element); + for (const auto* element : incident->second) { + std::set commonCells; + bool firstVertex = true; + for (const auto vertexId : element->vertices) { + const auto& coordinate = vertexId == id ? destination : coordinates[vertexId]; + const auto touchingCells = getTouchingCells(coordinate); + if (firstVertex) { + commonCells = touchingCells; + firstVertex = false; + } + else { + for (auto cell = commonCells.begin(); cell != commonCells.end();) { + if (touchingCells.count(*cell) == 0) { + cell = commonCells.erase(cell); + } + else { + ++cell; + } + } + } + if (commonCells.empty()) { + return true; } } } + return false; +} - for (const auto id : movedIds) { - const bool crossesGrid = std::any_of( - incidentElements[id].begin(), incidentElements[id].end(), - [&](const Element* element) { - return elementCrossesGrid(*element, coordinates); - }); - if (crossesGrid) { - coordinates[id] = originalCoordinates[id]; +void SmootherTools::collapsePointsOnFeatureEdges( + Coordinates& coords, + const ElementsView& patch, + const SingularIds& singularIds, + const IdSet& movableIds) +{ + IncidentElements incidentElements; + for (const auto* element : patch) { + for (const auto id : element->vertices) { + incidentElements[id].push_back(element); } } + collapsePointsOnFeatureEdges(coords, patch, singularIds, incidentElements, movableIds); } void SmootherTools::collapsePointsOnFeatureEdges( Coordinates& coords, const ElementsView& patch, - const SingularIds& singularIds) + const SingularIds& singularIds, + const IncidentElements& incidentElements, + const IdSet& movableIds) { CoordGraph Point = CoordGraph(patch); CoordGraph edges = Point.getBoundaryGraph().intersect(singularIds.featureIds()); @@ -131,6 +154,9 @@ void SmootherTools::collapsePointsOnFeatureEdges( std::map toMove; for (auto const& i : validInterior) { + if (!movableIds.empty() && movableIds.count(i) == 0) { + continue; + } if (isRelativeInCellCorner(coords[i])) { continue; } @@ -162,7 +188,12 @@ void SmootherTools::collapsePointsOnFeatureEdges( toMove[i] = closest; } - updateCoordinates(coords, toMove); + std::lock_guard lock(writingCoordinates_); + for (const auto& move : toMove) { + if (!moveWouldCrossGrid(move.first, move.second, coords, incidentElements)) { + coords[move.first] = move.second; + } + } } Coordinate SmootherTools::closestByDistance( @@ -219,7 +250,8 @@ void SmootherTools::collapsePointsOnCellEdges( Coordinates& coords, const ElementsView& patch, const SingularIds& singularIds, - double alignmentAngle) + double alignmentAngle, + const IdSet& movableIds) { { IdSet vertices = CoordGraph(patch).getVertices(); @@ -249,7 +281,10 @@ void SmootherTools::collapsePointsOnCellEdges( IdSet interiorValid = intersectWithIdSet(interior, protectedIds); - IdSet movable = classifyIds(interior, [&](auto i) {return !protectedIds.count(i); }).first; + IdSet movable = classifyIds(interior, [&](auto i) { + return !protectedIds.count(i) + && (movableIds.empty() || movableIds.count(i) != 0); + }).first; IdSet validIds = mergeIds(cG.getExterior(), interiorValid); std::map toMove; @@ -295,7 +330,8 @@ IdSet SmootherTools::getClosestValidByDistanceInCycle( void SmootherTools::collapsePointsOnCellFaces( Coordinates& coords, const ElementsView& patch, - const SingularIds& sIds) + const SingularIds& sIds, + const IdSet& movableIds) { std::map toMove; std::map > cyclesToValidOrOnFace; @@ -314,6 +350,9 @@ void SmootherTools::collapsePointsOnCellFaces( const IdSet& valid = kv.second.first; const IdSet& onCellFace = kv.second.second; for (auto const& id : onCellFace) { + if (!movableIds.empty() && movableIds.count(id) == 0) { + continue; + } try { IdSet candidates = getClosestValidByDistanceInCycle(id, cycle, valid); if (!candidates.empty()) { @@ -539,13 +578,17 @@ Coordinates SmootherTools::collapsePointsOnContour( void SmootherTools::collapseInteriorPointsToBound( Coordinates& coords, - const ElementsView& patch) + const ElementsView& patch, + const IdSet& movableIds) { IdSet bound, interior; std::tie(bound, interior) = CoordGraph(patch).getBoundAndInteriorVertices(); std::map toMove; for (auto const& vI : interior) { + if (!movableIds.empty() && movableIds.count(vI) == 0) { + continue; + } toMove[vI] = closestByDistance(coords, vI, bound); } diff --git a/src/core/SmootherTools.h b/src/core/SmootherTools.h index 2492f54..3ab957b 100644 --- a/src/core/SmootherTools.h +++ b/src/core/SmootherTools.h @@ -13,6 +13,8 @@ namespace core { class SmootherTools : public utils::GridTools { public: + using IncidentElements = std::map; + class SingularIds { public: SingularIds(const IdSet& featureIds, const IdSet& contourIds, const IdSet& cornerIds) : @@ -39,12 +41,15 @@ class SmootherTools : public utils::GridTools { void collapsePointsOnFeatureEdges( Coordinates& res, const ElementsView& patch, - const SingularIds& singularIds); + const SingularIds& singularIds, + const IdSet& movableIds = {}); - void revertMovesThatCrossGrid( - const Elements& elements, - Coordinates& coordinates, - const Coordinates& originalCoordinates) const; + void collapsePointsOnFeatureEdges( + Coordinates& res, + const ElementsView& patch, + const SingularIds& singularIds, + const IncidentElements& incidentElements, + const IdSet& movableIds = {}); Coordinates collapsePointsOnContour( const Elements& elems, @@ -55,12 +60,14 @@ class SmootherTools : public utils::GridTools { Coordinates& res, const ElementsView& patch, const SingularIds& singularIds , - double alignmentAngle); + double alignmentAngle, + const IdSet& movableIds = {}); void collapsePointsOnCellFaces( Coordinates& res, const ElementsView& patch, - const SingularIds&); + const SingularIds&, + const IdSet& movableIds = {}); void remeshBoundary( Elements& es, @@ -75,7 +82,8 @@ class SmootherTools : public utils::GridTools { void collapseInteriorPointsToBound( Coordinates& coords, - const ElementsView& patch); + const ElementsView& patch, + const IdSet& movableIds = {}); void remeshElementsToOneInteriorPoint( Elements& es, @@ -95,6 +103,12 @@ class SmootherTools : public utils::GridTools { void updateCoordinates(Coordinates& res, std::map toMove); + bool moveWouldCrossGrid( + const CoordinateId& id, + const Coordinate& destination, + const Coordinates& coordinates, + const IncidentElements& incidentElements) const; + static CoordinateId getClosestEndOfPaths( const std::vector& paths); diff --git a/test/core/SmootherToolsTest.cpp b/test/core/SmootherToolsTest.cpp index a4923f8..e93a9d0 100644 --- a/test/core/SmootherToolsTest.cpp +++ b/test/core/SmootherToolsTest.cpp @@ -732,7 +732,7 @@ TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdges_withDisconnectedTargets) EXPECT_EQ(m.coordinates[4], m.coordinates[5]); } -TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdges_staysInTouchingCell) +TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdgesDoesNotCrossTouchingCell) { Mesh m; m.grid = buildUnitLengthGrid(0.25); @@ -755,34 +755,45 @@ TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdges_staysInTouchingCell) SmootherTools(m.grid).collapsePointsOnFeatureEdges( m.coordinates, patch, singularIds); - EXPECT_EQ(m.coordinates[2], m.coordinates[1]); + EXPECT_EQ(Coordinate({1.2, 0.1, 0.2}), m.coordinates[1]); + EXPECT_FALSE(GridTools(m.grid).elementCrossesGrid(elements[1], m.coordinates)); } -TEST_F(SmootherToolsTest, revertsCombinedFeatureMovesThatCrossGrid) +TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdgesRejectsCombinedMovesThatCrossGrid) { Mesh m; m.grid = buildUnitLengthGrid(0.25); m.coordinates = { Coordinate({1.0, 0.2, 0.2}), - Coordinate({1.0, 0.5, 0.2}), - Coordinate({1.0, 0.8, 0.2}), + Coordinate({1.0, 1.0, 0.2}), + Coordinate({1.0, 1.0, 0.8}), + Coordinate({1.0, 1.8, 0.8}), + Coordinate({0.8, 1.0, 0.5}), }; m.groups = {Group()}; - m.groups[0].elements = {Element({0, 1, 2})}; + m.groups[0].elements = { + Element({0, 1, 4}), + Element({1, 2, 4}), + Element({2, 3, 4}), + }; - const Coordinates originalCoordinates = m.coordinates; - m.coordinates[0][X] = 0.0; - m.coordinates[2][X] = 2.0; - ASSERT_TRUE(GridTools(m.grid).elementCrossesGrid( - m.groups[0].elements[0], m.coordinates)); + Elements& elements = m.groups[0].elements; + const ElementsView patch = {&elements[0], &elements[1], &elements[2]}; + const SmootherTools::SingularIds singularIds({0, 1, 2, 3}, {}, {}); + SmootherTools::IncidentElements incidentElements; + for (const auto& element : elements) { + for (const auto id : element.vertices) { + incidentElements[id].push_back(&element); + } + } - SmootherTools(m.grid).revertMovesThatCrossGrid( - m.groups[0].elements, m.coordinates, originalCoordinates); + SmootherTools(m.grid).collapsePointsOnFeatureEdges( + m.coordinates, patch, singularIds, incidentElements); + EXPECT_EQ(m.coordinates[0], m.coordinates[1]); + EXPECT_NE(m.coordinates[3], m.coordinates[2]); EXPECT_FALSE(GridTools(m.grid).elementCrossesGrid( - m.groups[0].elements[0], m.coordinates)); - EXPECT_EQ(originalCoordinates[0], m.coordinates[0]); - EXPECT_EQ(2.0, m.coordinates[2][X]); + elements[1], m.coordinates)); } TEST_F(SmootherToolsTest, collapsePointsOnFeatureEdges_threePatches) From e16bb46c91f165ffe8200ac3606db02427d8e972 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Mon, 10 Aug 2026 12:03:59 +0200 Subject: [PATCH 57/61] Minor --- CMakePresets.json | 1 - src/app/vtkIO.cpp | 4 ++++ test/meshers/StaircaseMesherTest.cpp | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index 769d22e..3166b87 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -26,7 +26,6 @@ { "name": "gnu", "displayName": "GNU g++ compiler", - "generator": "Ninja", "inherits": "default" }, { diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index 236d640..f84822b 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -219,6 +220,9 @@ vtkSmartPointer elementsToVTU(const Mesh& mesh) } else if (elem.isNode()) { cellTypes.push_back(VTK_VERTEX); cell = vtkSmartPointer::New(); + } else if (elem.isTetrahedron()) { + cellTypes.push_back(VTK_TETRA); + cell = vtkSmartPointer::New(); } else { throw std::runtime_error("Unsupported element type"); } diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index 334cacd..f07f49a 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -375,8 +375,19 @@ TEST_F(StaircaseMesherTest, mesh_tetrahedron_volume_2x2){ meshlib::meshers::StaircaseMesherOptions opts; opts.volumeGroups.insert(0); // opts.isVolume = true; + +// #if APP_LOADED +// vtkIO::exportMeshToVTU("testData/cases/mesh_tetrahedron_volume_2x2_before.vtk", m); +// vtkIO::exportGridToVTU("testData/cases/mesh_tetrahedron_volume_2x2_before_grid.vtk", m.grid); +// #endif + auto staircasedMesh = StaircaseMesher{m, 4, opts }.mesh(); +// #if APP_LOADED +// vtkIO::exportMeshToVTU("testData/cases/mesh_tetrahedron_volume_2x2_after.vtk", staircasedMesh); +// vtkIO::exportGridToVTU("testData/cases/mesh_tetrahedron_volume_2x2_after_grid.vtk", staircasedMesh.grid); +// #endif + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); EXPECT_EQ(36, countMeshElementsIf(staircasedMesh, isQuad)); EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTetrahedron)); From 0828f7b2e0a4c10a87d8476d2a6354201b6eafa2 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Mon, 10 Aug 2026 18:03:10 +0200 Subject: [PATCH 58/61] Add VolumeFiller and VolumeShellExtractor classes for mesh processing - Introduced VolumeFiller class to fill a mesh shell with hexahedra. - Implemented VolumeShellExtractor class to extract outward boundaries from volume meshes. - Updated StaircaseMesher to utilize VolumeFiller and VolumeShellExtractor for improved mesh generation. - Added tests for VolumeFiller and VolumeShellExtractor to ensure correct functionality. - Enhanced Mesh class with isHexahedron method to identify hexahedron elements. - Modified existing tests to accommodate new hexahedron handling and validate mesh properties. --- .github/workflows/build-and-test.yml | 9 +- CLAUDE.md | 8 +- CMakeLists.txt | 2 +- CMakePresets.json | 21 +- README.md | 3 +- src/app/vtkIO.cpp | 14 + src/core/CMakeLists.txt | 6 +- src/core/VolumeFiller.cpp | 206 +++++++++++++ src/core/VolumeFiller.h | 18 ++ src/core/VolumeShellExtractor.cpp | 388 +++++++++++++++++++++++++ src/core/VolumeShellExtractor.h | 17 ++ src/meshers/CMakeLists.txt | 6 +- src/meshers/MesherBase.cpp | 10 +- src/meshers/MesherBase.h | 3 +- src/meshers/StaircaseMesher.cpp | 48 ++- src/meshers/StaircaseMesher.h | 2 +- src/types/Mesh.h | 6 +- src/utils/MeshTools.h | 4 +- test/CMakeLists.txt | 6 +- test/app/vtkIOTest.cpp | 26 +- test/core/VolumeFillerTest.cpp | 125 ++++++++ test/core/VolumeShellExtractorTest.cpp | 175 +++++++++++ test/meshers/StaircaseMesherTest.cpp | 35 ++- test/types/MeshTest.cpp | 13 +- test/types/MeshTest.h | 2 +- 25 files changed, 1092 insertions(+), 61 deletions(-) create mode 100644 src/core/VolumeFiller.cpp create mode 100644 src/core/VolumeFiller.h create mode 100644 src/core/VolumeShellExtractor.cpp create mode 100644 src/core/VolumeShellExtractor.h create mode 100644 test/core/VolumeFillerTest.cpp create mode 100644 test/core/VolumeShellExtractorTest.cpp diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6f78829..c6b8acd 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,9 +19,10 @@ jobs: matrix: preset: [ {"os": windows-2022, "name": "msbuild"}, - {"os": ubuntu-latest, "name": "gnu"} + {"os": ubuntu-latest, "name": "gnu"}, + {"os": ubuntu-latest, "name": "gnu-cgal"} ] - build-type: ["Debug", "Release"] + build-type: ["Release"] fail-fast: false @@ -85,7 +86,7 @@ jobs: run: build/bin/${{matrix.build-type}}/tessellator_tests.exe - name: Ubuntu configure and build - if: matrix.preset.name=='gnu' + if: matrix.preset.os=='ubuntu-latest' run: | configured=0 for attempt in 1 2 3; do @@ -99,5 +100,5 @@ jobs: cmake --build build -j - name: Ubuntu Run tests - if: matrix.preset.name=='gnu' + if: matrix.preset.os=='ubuntu-latest' run: build/bin/tessellator_tests diff --git a/CLAUDE.md b/CLAUDE.md index fa6ab49..f9e2bac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,6 +64,10 @@ Uses **CMake 3.20+** with presets and vcpkg for dependency management. cmake --preset gnu -S . -B build cmake --build build -j +# Optional CGAL algorithms +cmake --preset gnu-cgal +cmake --build --preset gnu-cgal -j + # Windows (MSBuild) cmake --preset msbuild -S . -B build cmake --build build --config Release -j @@ -72,7 +76,7 @@ cmake --build build --config Release -j ### CMake Options - `TESSELLATOR_ENABLE_TESTS` (ON by default) – Build test suite -- `TESSELLATOR_ENABLE_CGAL` (ON by default) – Enable CGAL-based geometry operations +- `TESSELLATOR_ENABLE_CGAL` (OFF by default) – Enable CGAL-based geometry operations - `TESSELLATOR_EXECUTION_POLICIES` (OFF by default) – Parallel execution policies ### Dependencies @@ -94,7 +98,7 @@ To set up locally, create a `CMakeUserPreset.json` file: "VCPKG_ROOT": "~/workspace/vcpkg/" }, "cacheVariables": { - "TESSELLATOR_ENABLE_CGAL": true + "TESSELLATOR_ENABLE_CGAL": false }, "inherits": "gnu" } diff --git a/CMakeLists.txt b/CMakeLists.txt index 577007a..5d3a1b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS OFF) option(TESSELLATOR_ENABLE_TESTS "Compile tests" ON) -option(TESSELLATOR_ENABLE_CGAL "Compile using CGAL library" ON) +option(TESSELLATOR_ENABLE_CGAL "Compile using CGAL library" OFF) option(TESSELLATOR_EXECUTION_POLICIES OFF) option(DOCKER_EXPORT_COMPILE_COMMANDS "Information for the 'clangd' linter extension" OFF) option(TESSELLATOR_LOAD_APP "Compile app" OFF) diff --git a/CMakePresets.json b/CMakePresets.json index 3166b87..93003ba 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -10,7 +10,7 @@ "type": "FILEPATH", "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" }, - "TESSELLATOR_ENABLE_CGAL": true + "TESSELLATOR_ENABLE_CGAL": false } }, { @@ -28,6 +28,15 @@ "displayName": "GNU g++ compiler", "inherits": "default" }, + { + "name": "gnu-cgal", + "displayName": "GNU g++ compiler with CGAL", + "inherits": "gnu", + "binaryDir": "build-cgal/", + "cacheVariables": { + "TESSELLATOR_ENABLE_CGAL": true + } + }, { "name": "gnu-dbg", "displayName": "GNU g++ compiler - Debug", @@ -48,7 +57,7 @@ "CMAKE_PREFIX_PATH": "/usr/local", "CMAKE_FIND_ROOT_PATH": "/usr/local", "TESSELLATOR_ENABLE_TESTS": "ON", - "TESSELLATOR_ENABLE_CGAL": "ON", + "TESSELLATOR_ENABLE_CGAL": "OFF", "DOCKER_EXPORT_COMPILE_COMMANDS": "ON" }, "environment": { @@ -67,7 +76,7 @@ "CMAKE_PREFIX_PATH": "/usr/local", "CMAKE_FIND_ROOT_PATH": "/usr/local", "TESSELLATOR_ENABLE_TESTS": "ON", - "TESSELLATOR_ENABLE_CGAL": "ON", + "TESSELLATOR_ENABLE_CGAL": "OFF", "DOCKER_EXPORT_COMPILE_COMMANDS": "ON", "CMAKE_CXX_FLAGS_INIT": "-g3" }, @@ -88,6 +97,10 @@ { "name": "docker-dbg", "configurePreset": "docker-dbg" + }, + { + "name": "gnu-cgal", + "configurePreset": "gnu-cgal" } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index c920511..14b3ce5 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Tessellator is a mesher focused on generate meshes and data structures which are ## Compilation When using presets, make sure to define the environment variable `VCPKG_ROOT` to your `vcpkg` installation. +The standard presets build without CGAL; use the `gnu-cgal` preset for the optional CGAL algorithms. This can be done using a `CMakeUserPreset.json` file, for example: ```json @@ -33,7 +34,7 @@ This can be done using a `CMakeUserPreset.json` file, for example: "VCPKG_ROOT": "~/workspace/vcpkg/" }, "cacheVariables": { - "TESSELLATOR_ENABLE_CGAL": true + "TESSELLATOR_ENABLE_CGAL": false }, "inherits": "gnu" } diff --git a/src/app/vtkIO.cpp b/src/app/vtkIO.cpp index f84822b..71f00b5 100644 --- a/src/app/vtkIO.cpp +++ b/src/app/vtkIO.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -79,6 +80,7 @@ Element vtkCellToElement(vtkCell* cell) vtkLine* line = nullptr; vtkTriangle* triangle = nullptr; vtkTetra* tetra = nullptr; + vtkHexahedron* hexahedron = nullptr; switch (cell->GetCellType()) { case VTK_VERTEX: @@ -116,6 +118,15 @@ Element vtkCellToElement(vtkCell* cell) }; elem.type = meshlib::Element::Type::Volume; break; + + case VTK_HEXAHEDRON: + hexahedron = vtkHexahedron::SafeDownCast(cell); + elem.vertices.reserve(8); + for (vtkIdType id = 0; id < 8; ++id) { + elem.vertices.push_back(CoordinateId(hexahedron->GetPointIds()->GetId(id))); + } + elem.type = meshlib::Element::Type::Volume; + break; } return elem; @@ -223,6 +234,9 @@ vtkSmartPointer elementsToVTU(const Mesh& mesh) } else if (elem.isTetrahedron()) { cellTypes.push_back(VTK_TETRA); cell = vtkSmartPointer::New(); + } else if (elem.isHexahedron()) { + cellTypes.push_back(VTK_HEXAHEDRON); + cell = vtkSmartPointer::New(); } else { throw std::runtime_error("Unsupported element type"); } diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 205c3a6..b3dc8a3 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -8,9 +8,9 @@ add_library(tessellator-core "Smoother.cpp" "SmootherTools.cpp" "Staircaser.cpp" + "VolumeFiller.cpp" + "VolumeShellExtractor.cpp" ) target_link_libraries(tessellator-core - tessellator-utils - tessellator-cgal - CGAL::CGAL) \ No newline at end of file + tessellator-utils) diff --git a/src/core/VolumeFiller.cpp b/src/core/VolumeFiller.cpp new file mode 100644 index 0000000..bde5f6f --- /dev/null +++ b/src/core/VolumeFiller.cpp @@ -0,0 +1,206 @@ +#include "VolumeFiller.h" + +#include "utils/RedundancyCleaner.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace meshlib::core { + +using namespace utils; + +namespace { + +using Ray = std::array; +using Rays = std::array>, 3>; + +Cell coordinateCell(const Coordinate& coordinate, const GridTools& tools) +{ + const Relative relative = tools.getRelative(coordinate); + Cell cell; + for (Axis axis : {X, Y, Z}) { + const auto rounded = std::round(relative[axis]); + if (!GridTools::approxDir(relative[axis], rounded, 1e-7)) { + throw std::runtime_error( + "Volume shell contains a coordinate that is not on a grid vertex."); + } + cell[axis] = static_cast(rounded); + } + return cell; +} + +std::pair quadSurfel( + const Element& quad, + const Coordinates& coordinates, + const GridTools& tools) +{ + if (!quad.isQuad()) { + throw std::runtime_error( + "Volume filling requires a closed shell made exclusively of quads."); + } + + std::array cells; + for (std::size_t index = 0; index < cells.size(); ++index) { + cells[index] = coordinateCell(coordinates.at(quad.vertices[index]), tools); + } + + std::vector fixedAxes; + for (Axis axis : {X, Y, Z}) { + if (std::all_of(cells.begin(), cells.end(), [&](const Cell& cell) { + return cell[axis] == cells.front()[axis]; + })) { + fixedAxes.push_back(axis); + } + } + if (fixedAxes.size() != 1) { + throw std::runtime_error( + "Volume shell contains a quad that is not on one grid face."); + } + + const Axis axis = fixedAxes.front(); + Cell lower = cells.front(); + Cell upper = cells.front(); + for (const auto& cell : cells) { + for (Axis direction : {X, Y, Z}) { + lower[direction] = std::min(lower[direction], cell[direction]); + upper[direction] = std::max(upper[direction], cell[direction]); + } + } + for (Axis direction : {X, Y, Z}) { + if (direction != axis && upper[direction] - lower[direction] != 1) { + throw std::runtime_error( + "Volume shell contains a quad spanning more than one grid face."); + } + } + return {axis, lower}; +} + +CoordinateId findOrAddCoordinate( + Mesh& mesh, + std::map& coordinateIds, + const Cell& cell, + const GridTools& tools) +{ + const auto found = coordinateIds.find(cell); + if (found != coordinateIds.end()) { + return found->second; + } + const CoordinateId id = mesh.coordinates.size(); + mesh.coordinates.push_back(tools.getPos(GridTools::toRelative(cell))); + coordinateIds.emplace(cell, id); + return id; +} + +Element buildHexahedron( + Mesh& mesh, + std::map& coordinateIds, + const Cell& lower, + const Cell& upper, + const GridTools& tools) +{ + std::array vertices; + vertices[0] = Cell({lower[X], lower[Y], lower[Z]}); + vertices[1] = Cell({upper[X], lower[Y], lower[Z]}); + vertices[2] = Cell({upper[X], upper[Y], lower[Z]}); + vertices[3] = Cell({lower[X], upper[Y], lower[Z]}); + vertices[4] = Cell({lower[X], lower[Y], upper[Z]}); + vertices[5] = Cell({upper[X], lower[Y], upper[Z]}); + vertices[6] = Cell({upper[X], upper[Y], upper[Z]}); + vertices[7] = Cell({lower[X], upper[Y], upper[Z]}); + + Element hexahedron; + hexahedron.type = Element::Type::Volume; + for (const auto& vertex : vertices) { + hexahedron.vertices.push_back( + findOrAddCoordinate(mesh, coordinateIds, vertex, tools)); + } + return hexahedron; +} + +} + +VolumeFiller::VolumeFiller(const Mesh& staircasedSurface) : + GridTools(staircasedSurface.grid) +{ + mesh_.grid = staircasedSurface.grid; + mesh_.coordinates = staircasedSurface.coordinates; + mesh_.groups.resize(staircasedSurface.groups.size()); + std::map coordinateIds; + for (CoordinateId id = 0; id < staircasedSurface.coordinates.size(); ++id) { + coordinateIds.emplace( + coordinateCell(staircasedSurface.coordinates[id], *this), id); + } + + for (GroupId groupId = 0; groupId < staircasedSurface.groups.size(); ++groupId) { + const auto& inputGroup = staircasedSurface.groups[groupId]; + auto& outputGroup = mesh_.groups[groupId]; + outputGroup.name = inputGroup.name; + if (inputGroup.elements.empty()) { + continue; + } + + Rays rays; + for (const auto& element : inputGroup.elements) { + if (element.type != Element::Type::Surface) { + continue; + } + Axis axis; + Cell surfel; + std::tie(axis, surfel) = quadSurfel(element, staircasedSurface.coordinates, *this); + const Axis axis1 = (axis + 1) % 3; + const Axis axis2 = (axis + 2) % 3; + rays[axis][{surfel[axis1], surfel[axis2]}].insert(surfel[axis]); + } + + Axis fillAxis = X; + for (Axis axis : {Y, Z}) { + if (rays[axis].size() < rays[fillAxis].size()) { + fillAxis = axis; + } + } + const Axis axis1 = (fillAxis + 1) % 3; + const Axis axis2 = (fillAxis + 2) % 3; + for (const auto& ray : rays[fillAxis]) { + const auto& crossings = ray.second; + if (crossings.size() % 2 != 0) { + std::stringstream message; + message << "Volume shell has an odd number of crossings on a grid ray in group " + << groupId << "."; + throw std::runtime_error(message.str()); + } + for (auto crossing = crossings.begin(); crossing != crossings.end();) { + const CellDir begin = *crossing++; + const CellDir end = *crossing++; + if (begin == end) { + continue; + } + Cell lower; + Cell upper; + lower[fillAxis] = begin; + upper[fillAxis] = end; + lower[axis1] = ray.first[0]; + upper[axis1] = ray.first[0] + 1; + lower[axis2] = ray.first[1]; + upper[axis2] = ray.first[1] + 1; + outputGroup.elements.push_back( + buildHexahedron(mesh_, coordinateIds, lower, upper, *this)); + } + } + } + + RedundancyCleaner::cleanCoords(mesh_); +} + +Mesh VolumeFiller::getMesh() const +{ + return mesh_; +} + +} diff --git a/src/core/VolumeFiller.h b/src/core/VolumeFiller.h new file mode 100644 index 0000000..180166c --- /dev/null +++ b/src/core/VolumeFiller.h @@ -0,0 +1,18 @@ +#pragma once + +#include "types/Mesh.h" +#include "utils/GridTools.h" + +namespace meshlib::core { + +class VolumeFiller : private utils::GridTools { +public: + explicit VolumeFiller(const Mesh& staircasedSurface); + + Mesh getMesh() const; + +private: + Mesh mesh_; +}; + +} diff --git a/src/core/VolumeShellExtractor.cpp b/src/core/VolumeShellExtractor.cpp new file mode 100644 index 0000000..5cfa698 --- /dev/null +++ b/src/core/VolumeShellExtractor.cpp @@ -0,0 +1,388 @@ +#include "VolumeShellExtractor.h" + +#include "utils/Geometry.h" +#include "utils/RedundancyCleaner.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace meshlib::core { + +namespace { + +using Face = std::array; +using Edge = std::array; + +Face faceKey(const Face& face) +{ + Face key = face; + std::sort(key.begin(), key.end()); + return key; +} + +Face asFace(const Element& element) +{ + return {element.vertices[0], element.vertices[1], element.vertices[2]}; +} + +Edge edgeKey(CoordinateId first, CoordinateId second) +{ + return first < second ? Edge{first, second} : Edge{second, first}; +} + +[[noreturn]] void fail(GroupId groupId, const std::string& reason) +{ + std::stringstream message; + message << "Invalid volume group " << groupId << ": " << reason; + throw std::runtime_error(message.str()); +} + +void validateVertexIds( + const Element& element, + const Coordinates& coordinates, + GroupId groupId) +{ + std::set unique; + for (const CoordinateId id : element.vertices) { + if (id >= coordinates.size()) { + fail(groupId, "an element references a coordinate outside the mesh."); + } + for (std::size_t axis = 0; axis < 3; ++axis) { + if (!std::isfinite(coordinates[id][axis])) { + fail(groupId, "an element references a non-finite coordinate."); + } + } + unique.insert(id); + } + if (unique.size() != element.vertices.size()) { + fail(groupId, "an element contains repeated vertices."); + } +} + +double signedTetrahedronVolume6( + const Coordinate& first, + const Coordinate& second, + const Coordinate& third, + const Coordinate& fourth) +{ + return ((second - first) ^ (third - first)) * (fourth - first); +} + +Face outwardFace( + Face face, + CoordinateId opposite, + const Coordinates& coordinates) +{ + const Coordinate& first = coordinates[face[0]]; + const Coordinate normal = + (coordinates[face[1]] - first) ^ (coordinates[face[2]] - first); + if (normal * (coordinates[opposite] - first) > 0.0) { + std::swap(face[1], face[2]); + } + return face; +} + +Elements extractTetrahedronBoundary( + const Group& group, + const Coordinates& coordinates, + GroupId groupId) +{ + std::set> tetrahedrons; + std::map> faces; + + for (const Element& element : group.elements) { + validateVertexIds(element, coordinates, groupId); + std::array tetrahedron{ + element.vertices[0], element.vertices[1], + element.vertices[2], element.vertices[3]}; + auto tetrahedronKey = tetrahedron; + std::sort(tetrahedronKey.begin(), tetrahedronKey.end()); + if (!tetrahedrons.insert(tetrahedronKey).second) { + fail(groupId, "it contains a duplicate tetrahedron."); + } + + if (std::abs(signedTetrahedronVolume6( + coordinates[tetrahedron[0]], coordinates[tetrahedron[1]], + coordinates[tetrahedron[2]], coordinates[tetrahedron[3]])) + <= utils::Geometry::NORM_TOLERANCE) { + fail(groupId, "it contains a degenerate tetrahedron."); + } + + for (std::size_t opposite = 0; opposite < tetrahedron.size(); ++opposite) { + Face face; + std::size_t faceIndex = 0; + for (std::size_t vertex = 0; vertex < tetrahedron.size(); ++vertex) { + if (vertex != opposite) { + face[faceIndex++] = tetrahedron[vertex]; + } + } + face = outwardFace(face, tetrahedron[opposite], coordinates); + auto& occurrences = faces[faceKey(face)]; + occurrences.push_back(face); + if (occurrences.size() > 2) { + fail(groupId, "a tetrahedron face is shared more than twice."); + } + } + } + + Elements boundary; + for (const auto& entry : faces) { + if (entry.second.size() == 1) { + const Face& face = entry.second.front(); + boundary.emplace_back( + CoordinateIds{face[0], face[1], face[2]}, Element::Type::Surface); + } + } + if (boundary.empty()) { + fail(groupId, "its tetrahedrons have no external boundary."); + } + return boundary; +} + +Elements extractSurfaceBoundary( + const Group& group, + const Coordinates& coordinates, + GroupId groupId) +{ + Elements boundary; + std::set faces; + boundary.reserve(group.elements.size()); + for (const Element& element : group.elements) { + validateVertexIds(element, coordinates, groupId); + const Face face = asFace(element); + if (!faces.insert(faceKey(face)).second) { + fail(groupId, "it contains a duplicate triangle."); + } + if (utils::Geometry::isDegenerate(utils::Geometry::asTriV(element, coordinates))) { + fail(groupId, "it contains a degenerate triangle."); + } + boundary.push_back(element); + } + return boundary; +} + +int edgeDirection(const Element& face, const Edge& edge) +{ + for (std::size_t vertex = 0; vertex < face.vertices.size(); ++vertex) { + const CoordinateId first = face.vertices[vertex]; + const CoordinateId second = face.vertices[(vertex + 1) % face.vertices.size()]; + if (first == edge[0] && second == edge[1]) { + return 1; + } + if (first == edge[1] && second == edge[0]) { + return -1; + } + } + throw std::logic_error("A face does not contain its indexed edge."); +} + +using EdgeFaces = std::map>; + +EdgeFaces buildEdgeFaces(const Elements& faces, GroupId groupId) +{ + EdgeFaces edgeFaces; + for (ElementId faceId = 0; faceId < faces.size(); ++faceId) { + const auto& vertices = faces[faceId].vertices; + for (std::size_t vertex = 0; vertex < vertices.size(); ++vertex) { + edgeFaces[edgeKey(vertices[vertex], vertices[(vertex + 1) % vertices.size()])] + .push_back(faceId); + } + } + for (const auto& entry : edgeFaces) { + if (entry.second.size() == 1) { + fail(groupId, "its surface is open."); + } + if (entry.second.size() != 2) { + fail(groupId, "its surface contains a non-manifold edge."); + } + } + return edgeFaces; +} + +void validateVertexFans( + const Elements& faces, + const EdgeFaces& edgeFaces, + GroupId groupId) +{ + std::map> vertexFaces; + std::map>> adjacency; + for (ElementId faceId = 0; faceId < faces.size(); ++faceId) { + for (CoordinateId vertex : faces[faceId].vertices) { + vertexFaces[vertex].insert(faceId); + } + } + for (const auto& entry : edgeFaces) { + const ElementId first = entry.second[0]; + const ElementId second = entry.second[1]; + for (CoordinateId vertex : entry.first) { + adjacency[vertex][first].insert(second); + adjacency[vertex][second].insert(first); + } + } + + for (const auto& entry : vertexFaces) { + const CoordinateId vertex = entry.first; + const auto& incident = entry.second; + std::set visited; + std::queue pending; + pending.push(*incident.begin()); + visited.insert(*incident.begin()); + while (!pending.empty()) { + const ElementId current = pending.front(); + pending.pop(); + for (ElementId next : adjacency[vertex][current]) { + if (visited.insert(next).second) { + pending.push(next); + } + } + } + if (visited.size() != incident.size()) { + fail(groupId, "its surface contains a non-manifold vertex."); + } + } +} + +std::vector> orientComponents( + Elements& faces, + const EdgeFaces& edgeFaces, + GroupId groupId) +{ + struct Neighbor { + ElementId face; + bool requiresFlip; + }; + std::vector> adjacency(faces.size()); + for (const auto& entry : edgeFaces) { + const ElementId first = entry.second[0]; + const ElementId second = entry.second[1]; + const bool requiresFlip = + edgeDirection(faces[first], entry.first) + == edgeDirection(faces[second], entry.first); + adjacency[first].push_back({second, requiresFlip}); + adjacency[second].push_back({first, requiresFlip}); + } + + std::vector flipped(faces.size(), -1); + std::vector> components; + for (ElementId seed = 0; seed < faces.size(); ++seed) { + if (flipped[seed] != -1) { + continue; + } + components.emplace_back(); + std::queue pending; + pending.push(seed); + flipped[seed] = 0; + while (!pending.empty()) { + const ElementId current = pending.front(); + pending.pop(); + components.back().push_back(current); + for (const Neighbor& neighbor : adjacency[current]) { + const int required = flipped[current] ^ neighbor.requiresFlip; + if (flipped[neighbor.face] == -1) { + flipped[neighbor.face] = required; + pending.push(neighbor.face); + } else if (flipped[neighbor.face] != required) { + fail(groupId, "its surface is not orientable."); + } + } + } + } + for (ElementId faceId = 0; faceId < faces.size(); ++faceId) { + if (flipped[faceId] == 1) { + std::swap(faces[faceId].vertices[1], faces[faceId].vertices[2]); + } + } + return components; +} + +void orientOutward( + Elements& faces, + const Coordinates& coordinates, + const std::vector>& components, + GroupId groupId) +{ + for (const auto& component : components) { + const Coordinate origin = coordinates[faces[component.front()].vertices[0]]; + double volume6 = 0.0; + for (ElementId faceId : component) { + const Face face = asFace(faces[faceId]); + volume6 += (coordinates[face[0]] - origin) + * ((coordinates[face[1]] - origin) ^ (coordinates[face[2]] - origin)); + } + if (std::abs(volume6) <= utils::Geometry::NORM_TOLERANCE) { + fail(groupId, "a closed component encloses zero volume."); + } + if (volume6 < 0.0) { + for (ElementId faceId : component) { + std::swap(faces[faceId].vertices[1], faces[faceId].vertices[2]); + } + } + } +} + +Elements extractGroupShell( + const Group& group, + const Coordinates& coordinates, + GroupId groupId) +{ + if (group.elements.empty()) { + return {}; + } + + const bool hasTriangles = std::any_of( + group.elements.begin(), group.elements.end(), + [](const Element& element) { return element.isTriangle(); }); + const bool hasTetrahedrons = std::any_of( + group.elements.begin(), group.elements.end(), + [](const Element& element) { return element.isTetrahedron(); }); + if (hasTriangles && hasTetrahedrons) { + fail(groupId, "triangles and tetrahedrons cannot be mixed."); + } + if (!hasTriangles && !hasTetrahedrons) { + fail(groupId, "only triangles or tetrahedrons are supported."); + } + if (!std::all_of( + group.elements.begin(), group.elements.end(), + [hasTriangles](const Element& element) { + return hasTriangles ? element.isTriangle() : element.isTetrahedron(); + })) { + fail(groupId, "only triangles or tetrahedrons are supported."); + } + + Elements faces = hasTriangles + ? extractSurfaceBoundary(group, coordinates, groupId) + : extractTetrahedronBoundary(group, coordinates, groupId); + const EdgeFaces edgeFaces = buildEdgeFaces(faces, groupId); + validateVertexFans(faces, edgeFaces, groupId); + const auto components = orientComponents(faces, edgeFaces, groupId); + orientOutward(faces, coordinates, components, groupId); + std::sort(faces.begin(), faces.end(), [](const Element& first, const Element& second) { + return faceKey(asFace(first)) < faceKey(asFace(second)); + }); + return faces; +} + +} + +VolumeShellExtractor::VolumeShellExtractor(const Mesh& volumeMesh) : mesh_(volumeMesh) +{ + for (GroupId groupId = 0; groupId < mesh_.groups.size(); ++groupId) { + mesh_.groups[groupId].elements = extractGroupShell( + volumeMesh.groups[groupId], volumeMesh.coordinates, groupId); + } + utils::RedundancyCleaner::cleanCoords(mesh_); +} + +Mesh VolumeShellExtractor::getMesh() const +{ + return mesh_; +} + +} diff --git a/src/core/VolumeShellExtractor.h b/src/core/VolumeShellExtractor.h new file mode 100644 index 0000000..61c85b6 --- /dev/null +++ b/src/core/VolumeShellExtractor.h @@ -0,0 +1,17 @@ +#pragma once + +#include "types/Mesh.h" + +namespace meshlib::core { + +class VolumeShellExtractor { +public: + explicit VolumeShellExtractor(const Mesh& volumeMesh); + + Mesh getMesh() const; + +private: + Mesh mesh_; +}; + +} diff --git a/src/meshers/CMakeLists.txt b/src/meshers/CMakeLists.txt index 32eed77..b48f2ba 100644 --- a/src/meshers/CMakeLists.txt +++ b/src/meshers/CMakeLists.txt @@ -8,12 +8,10 @@ add_library(tessellator-meshers ) target_link_libraries(tessellator-meshers tessellator-core - tessellator-utils - tessellator-cgal - CGAL::CGAL) + tessellator-utils) if(TESSELLATOR_EXECUTION_POLICIES) add_definitions(-DTESSELLATOR_EXECUTION_POLICIES) find_package(TBB CONFIG REQUIRED) target_link_libraries(tessellator-meshers TBB::tbb) -endif() \ No newline at end of file +endif() diff --git a/src/meshers/MesherBase.cpp b/src/meshers/MesherBase.cpp index 20849bb..24c3df0 100644 --- a/src/meshers/MesherBase.cpp +++ b/src/meshers/MesherBase.cpp @@ -52,6 +52,13 @@ void MesherBase::logNumberOfNodes(std::size_t nNodes) log(msg.str(), 2); } +void MesherBase::logNumberOfHexahedra(std::size_t nHexahedra) +{ + std::stringstream msg; + msg << "Mesh contains " << nHexahedra << " hexahedra."; + log(msg.str(), 2); +} + void MesherBase::logGridSize(const Grid& g) { std::stringstream msg; @@ -108,6 +115,7 @@ Mesh MesherBase::buildVolumeMesh(const Mesh& inputMesh, const std::set& Mesh volumeMesh{ inputMesh.grid, inputMesh.coordinates }; volumeMesh.groups.resize(inputMesh.groups.size()); for (const auto& gId : volumeGroups) { + volumeMesh.groups[gId].name = inputMesh.groups[gId].name; mergeGroup(volumeMesh.groups[gId], inputMesh.groups[gId]); } return volumeMesh; @@ -123,4 +131,4 @@ Mesh MesherBase::buildSurfaceMesh(const Mesh& inputMesh, const std::set } } -} \ No newline at end of file +} diff --git a/src/meshers/MesherBase.h b/src/meshers/MesherBase.h index c0bac3e..d05edd2 100644 --- a/src/meshers/MesherBase.h +++ b/src/meshers/MesherBase.h @@ -21,6 +21,7 @@ class MesherBase { static void logNumberOfTriangles(std::size_t nTris); static void logNumberOfLines(std::size_t nLines); static void logNumberOfNodes(std::size_t nNodes); + static void logNumberOfHexahedra(std::size_t nHexahedra); static void logGridSize(const Grid& g); static Grid buildNonSlicingGrid(const Grid& primal, const Grid& enlarged); @@ -37,4 +38,4 @@ class MesherBase { }; } -} \ No newline at end of file +} diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index d2d9006..9f45386 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -7,9 +7,8 @@ #include "core/Collapser.h" #include "core/Staircaser.h" #include "core/Compressor.h" - -#include "cgal/filler/Filler.h" -// #include "cgal/Manifolder.h" +#include "core/VolumeFiller.h" +#include "core/VolumeShellExtractor.h" #include "utils/RedundancyCleaner.h" #include "utils/MeshTools.h" @@ -21,6 +20,9 @@ using namespace utils; using namespace core; using namespace meshTools; +std::vector getGroupNames(const Groups& groups); +void copyGroupNames(Mesh& mesh, const std::vector& names); + StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInCollapser, StaircaseMesherOptions opts) : MesherBase(inputMesh), decimalPlacesInCollapser_(decimalPlacesInCollapser), @@ -33,33 +35,22 @@ StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInColla log("Preparing volumes"); volumeMesh_ = MesherBase::buildVolumeMesh(inputMesh, opts_.volumeGroups); - fillMesh(volumeMesh_); - log("Processing volume mesh."); - process(volumeMesh_); + if (!volumeMesh_.emptyOfElements()) { + volumeMesh_ = VolumeShellExtractor(volumeMesh_).getMesh(); + + log("Processing volume shell."); + process(volumeMesh_, false); + log("Filling volume shell with hexahedra."); + volumeMesh_ = VolumeFiller(volumeMesh_).getMesh(); + logNumberOfHexahedra(countMeshElementsIf(volumeMesh_, isHexahedron)); + } mergeMesh(surfaceMesh_, volumeMesh_); + RedundancyCleaner::cleanCoords(surfaceMesh_); log("Mesh built succesfully.", 1); } -void StaircaseMesher::fillMesh(Mesh& m){ - if (m.countElems() == 0) return; - meshlib::cgal::filler::FillerMode mode = meshlib::cgal::filler::FillerMode::onlyInside; - if (m.groups[0].elements[0].isTetrahedron()) { - mode = meshlib::cgal::filler::FillerMode::insideAndOutside; - } - auto filling = m; - utils::meshTools::convertToRelativeCoordinates(filling); - filling = meshlib::cgal::filler::Filler(filling, Mesh(), std::vector(), mode).getMeshFilling(); - utils::meshTools::convertToAbsoluteCoordinates(filling); - if (mode == meshlib::cgal::filler::FillerMode::insideAndOutside){ - m = filling; - } else if (mode == meshlib::cgal::filler::FillerMode::onlyInside){ - mergeMesh(m, filling); - } -} - - Mesh StaircaseMesher::buildSurfaceMesh(const Mesh& inputMesh, const Mesh & volumeSurface) { auto resultMesh = buildMeshFilteringElements(inputMesh, isNotTetrahedron); @@ -91,6 +82,11 @@ static Mesh toAbsolute(const Mesh& m) } void StaircaseMesher::process(Mesh& mesh) const +{ + process(mesh, opts_.compress); +} + +void StaircaseMesher::process(Mesh& mesh, bool compress) const { const auto groupNames = getGroupNames(mesh.groups); const auto slicingGrid{ buildSlicingGrid(originalGrid_, enlargedGrid_) }; @@ -124,7 +120,7 @@ void StaircaseMesher::process(Mesh& mesh) const logNumberOfQuads(countMeshElementsIf(mesh, isQuad)); logNumberOfLines(countMeshElementsIf(mesh, isLine)); - if (opts_.compress) { + if (compress) { log("Compressing surfaces.", 1); std::size_t beforeQuads = countMeshElementsIf(mesh, isQuad); std::size_t merged = Compressor::compressSurfacesInMesh(mesh); @@ -161,4 +157,4 @@ Mesh StaircaseMesher::mesh() const return surfaceMesh_; } -} \ No newline at end of file +} diff --git a/src/meshers/StaircaseMesher.h b/src/meshers/StaircaseMesher.h index c6be343..d768399 100644 --- a/src/meshers/StaircaseMesher.h +++ b/src/meshers/StaircaseMesher.h @@ -21,8 +21,8 @@ class StaircaseMesher : public MesherBase { StaircaseMesherOptions opts_; virtual Mesh buildSurfaceMesh(const Mesh& inputMesh, const Mesh& volumeSurface); - static void fillMesh(Mesh& inputMesh); void process(Mesh&) const; + void process(Mesh&, bool compress) const; }; diff --git a/src/types/Mesh.h b/src/types/Mesh.h index dcccb37..c96d3b8 100644 --- a/src/types/Mesh.h +++ b/src/types/Mesh.h @@ -69,6 +69,11 @@ struct Element { return type == Type::Volume && vertices.size() == 4; } + bool isHexahedron() const + { + return type == Type::Volume && vertices.size() == 8; + } + bool sharesVertices(const Element& rhs) { bool res = true; @@ -220,4 +225,3 @@ struct Mesh { }; } - diff --git a/src/utils/MeshTools.h b/src/utils/MeshTools.h index 9bfed81..4826a0d 100644 --- a/src/utils/MeshTools.h +++ b/src/utils/MeshTools.h @@ -19,6 +19,8 @@ static bool isQuad(const Element& e) { return e.isQuad(); } static bool isNotQuad(const Element& e) { return !e.isQuad(); } static bool isTetrahedron(const Element& e) { return e.isTetrahedron(); } static bool isNotTetrahedron(const Element& e) { return !e.isTetrahedron(); } +static bool isHexahedron(const Element& e) { return e.isHexahedron(); } +static bool isNotHexahedron(const Element& e) { return !e.isHexahedron(); } std::size_t countMeshElementsIf(const Mesh& mesh, std::function countFilter); std::vector getHighestDimensionByGroup(const Mesh& mesh); @@ -53,4 +55,4 @@ bool isAClosedTopology(const Elements& es); Mesh extractGroupsByName(const Mesh& mesh, const std::vector& groupNames); -} \ No newline at end of file +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 638a03a..3cc1202 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,7 +20,9 @@ add_executable(tessellator_tests "core/SmootherTest.cpp" "core/SmootherToolsTest.cpp" "core/SnapperTest.cpp" - "core/StaircaserTest.cpp" + "core/StaircaserTest.cpp" + "core/VolumeFillerTest.cpp" + "core/VolumeShellExtractorTest.cpp" "types/MeshTest.cpp" "utils/ConvexHullTest.cpp" "utils/CoordGraphTest.cpp" @@ -67,4 +69,4 @@ if (TESSELLATOR_ENABLE_CGAL) target_link_libraries(tessellator_tests tessellator-cgal) endif() -add_test(tessellator ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/tessellator_tests) \ No newline at end of file +add_test(tessellator ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/tessellator_tests) diff --git a/test/app/vtkIOTest.cpp b/test/app/vtkIOTest.cpp index 5541b85..4e89dc5 100644 --- a/test/app/vtkIOTest.cpp +++ b/test/app/vtkIOTest.cpp @@ -43,6 +43,30 @@ TEST_F(VTKIOTest, readElementTypes) EXPECT_TRUE(m.groups[0].elements[2].isTriangle()); } +TEST_F(VTKIOTest, exportAndReadHexahedron) +{ + meshlib::Mesh mesh; + mesh.grid = meshlib::utils::GridTools::buildCartesianGrid(0.0, 1.0, 2); + mesh.coordinates = { + meshlib::Coordinate({0.0, 0.0, 0.0}), meshlib::Coordinate({1.0, 0.0, 0.0}), + meshlib::Coordinate({1.0, 1.0, 0.0}), meshlib::Coordinate({0.0, 1.0, 0.0}), + meshlib::Coordinate({0.0, 0.0, 1.0}), meshlib::Coordinate({1.0, 0.0, 1.0}), + meshlib::Coordinate({1.0, 1.0, 1.0}), meshlib::Coordinate({0.0, 1.0, 1.0})}; + mesh.groups.resize(1); + mesh.groups[0].elements.push_back(meshlib::Element( + {0, 1, 2, 3, 4, 5, 6, 7}, meshlib::Element::Type::Volume)); + + const auto filename = std::filesystem::temp_directory_path() + / "tessellator_hexahedron_roundtrip.vtu"; + exportMeshToVTU(filename, mesh); + const auto result = readInputMesh(filename); + std::filesystem::remove(filename); + + ASSERT_EQ(1, result.countElems()); + EXPECT_TRUE(result.groups[0].elements[0].isHexahedron()); + EXPECT_EQ(mesh.coordinates, result.coordinates); +} + TEST_F(VTKIOTest, exportGridToVTU) { meshlib::Grid grid; @@ -56,4 +80,4 @@ TEST_F(VTKIOTest, exportGridToVTU) auto exported{ readInputMesh(fn) }; EXPECT_EQ(121+121+21, exported.countElems()); -} \ No newline at end of file +} diff --git a/test/core/VolumeFillerTest.cpp b/test/core/VolumeFillerTest.cpp new file mode 100644 index 0000000..239ee7e --- /dev/null +++ b/test/core/VolumeFillerTest.cpp @@ -0,0 +1,125 @@ +#include "gtest/gtest.h" + +#include "core/VolumeFiller.h" +#include "utils/GridTools.h" +#include "utils/MeshTools.h" + +namespace meshlib::core { + +using namespace utils; +using namespace meshTools; + +namespace { + +CoordinateId coordinateId(Mesh& mesh, CellDir x, CellDir y, CellDir z) +{ + const Coordinate coordinate = GridTools(mesh.grid).getPos(Cell({x, y, z})); + const auto found = std::find(mesh.coordinates.begin(), mesh.coordinates.end(), coordinate); + if (found != mesh.coordinates.end()) { + return found - mesh.coordinates.begin(); + } + mesh.coordinates.push_back(coordinate); + return mesh.coordinates.size() - 1; +} + +void addQuad(Mesh& mesh, const std::array& cells) +{ + Element quad; + quad.type = Element::Type::Surface; + for (const auto& cell : cells) { + quad.vertices.push_back(coordinateId(mesh, cell[X], cell[Y], cell[Z])); + } + mesh.groups[0].elements.push_back(quad); +} + +Mesh buildTwoByTwoByTwoShell() +{ + Mesh mesh; + mesh.grid = GridTools::buildCartesianGrid(0.0, 1.0, 3); + mesh.groups.resize(1); + mesh.groups[0].name = "volume"; + + for (CellDir first = 0; first < 2; ++first) { + for (CellDir second = 0; second < 2; ++second) { + for (CellDir x : {CellDir(0), CellDir(2)}) { + addQuad(mesh, { + Cell({x, first, second}), Cell({x, first + 1, second}), + Cell({x, first + 1, second + 1}), Cell({x, first, second + 1})}); + } + for (CellDir y : {CellDir(0), CellDir(2)}) { + addQuad(mesh, { + Cell({first, y, second}), Cell({first + 1, y, second}), + Cell({first + 1, y, second + 1}), Cell({first, y, second + 1})}); + } + for (CellDir z : {CellDir(0), CellDir(2)}) { + addQuad(mesh, { + Cell({first, second, z}), Cell({first + 1, second, z}), + Cell({first + 1, second + 1, z}), Cell({first, second + 1, z})}); + } + } + } + return mesh; +} + +void addBox(Mesh& mesh, CellDir x0, CellDir x1) +{ + addQuad(mesh, {Cell({x0, 0, 0}), Cell({x0, 1, 0}), + Cell({x0, 1, 1}), Cell({x0, 0, 1})}); + addQuad(mesh, {Cell({x1, 0, 0}), Cell({x1, 1, 0}), + Cell({x1, 1, 1}), Cell({x1, 0, 1})}); + addQuad(mesh, {Cell({x0, 0, 0}), Cell({x1, 0, 0}), + Cell({x1, 0, 1}), Cell({x0, 0, 1})}); + addQuad(mesh, {Cell({x0, 1, 0}), Cell({x1, 1, 0}), + Cell({x1, 1, 1}), Cell({x0, 1, 1})}); + addQuad(mesh, {Cell({x0, 0, 0}), Cell({x1, 0, 0}), + Cell({x1, 1, 0}), Cell({x0, 1, 0})}); + addQuad(mesh, {Cell({x0, 0, 1}), Cell({x1, 0, 1}), + Cell({x1, 1, 1}), Cell({x0, 1, 1})}); +} + +} + +TEST(VolumeFillerTest, fillsContinuousRunsWithHexahedra) +{ + const Mesh result = VolumeFiller(buildTwoByTwoByTwoShell()).getMesh(); + + EXPECT_EQ("volume", result.groups[0].name); + EXPECT_EQ(4, countMeshElementsIf(result, isHexahedron)); + EXPECT_EQ(0, countMeshElementsIf(result, isQuad)); + EXPECT_EQ(18, result.coordinates.size()); + + const auto& first = result.groups[0].elements.front(); + const Coordinates expected = { + Coordinate({0.0, 0.0, 0.0}), Coordinate({1.0, 0.0, 0.0}), + Coordinate({1.0, 0.5, 0.0}), Coordinate({0.0, 0.5, 0.0}), + Coordinate({0.0, 0.0, 0.5}), Coordinate({1.0, 0.0, 0.5}), + Coordinate({1.0, 0.5, 0.5}), Coordinate({0.0, 0.5, 0.5})}; + for (std::size_t vertex = 0; vertex < expected.size(); ++vertex) { + EXPECT_EQ(expected[vertex], result.coordinates[first.vertices[vertex]]); + } +} + +TEST(VolumeFillerTest, rejectsAnOpenQuadShell) +{ + Mesh shell = buildTwoByTwoByTwoShell(); + shell.groups[0].elements.erase(shell.groups[0].elements.begin()); + + EXPECT_THROW(VolumeFiller{shell}, std::runtime_error); +} + +TEST(VolumeFillerTest, fillsDisconnectedIntervalsOnTheSameRay) +{ + Mesh shell; + shell.grid = GridTools::buildCartesianGrid(0.0, 3.0, 4); + shell.groups.resize(1); + addBox(shell, 0, 1); + addBox(shell, 2, 3); + + const Mesh result = VolumeFiller(shell).getMesh(); + + ASSERT_EQ(2, result.countElems()); + EXPECT_TRUE(result.groups[0].elements[0].isHexahedron()); + EXPECT_TRUE(result.groups[0].elements[1].isHexahedron()); +} + +} diff --git a/test/core/VolumeShellExtractorTest.cpp b/test/core/VolumeShellExtractorTest.cpp new file mode 100644 index 0000000..8b9d656 --- /dev/null +++ b/test/core/VolumeShellExtractorTest.cpp @@ -0,0 +1,175 @@ +#include "gtest/gtest.h" + +#include "MeshFixtures.h" +#include "core/VolumeShellExtractor.h" +#include "utils/Geometry.h" +#include "utils/MeshTools.h" + +namespace meshlib::core { + +using namespace meshFixtures; +using namespace utils::meshTools; + +namespace { + +double signedVolume6(const Mesh& mesh, const Group& group) +{ + double result = 0.0; + for (const Element& element : group.elements) { + const auto& first = mesh.coordinates[element.vertices[0]]; + const auto& second = mesh.coordinates[element.vertices[1]]; + const auto& third = mesh.coordinates[element.vertices[2]]; + result += first * (second ^ third); + } + return result; +} + +Mesh buildOpenTetrahedronShell() +{ + Mesh mesh = buildTetSurfaceMesh(1.0); + mesh.groups[0].elements.pop_back(); + return mesh; +} + +} + +TEST(VolumeShellExtractorTest, extractsOutwardBoundaryFromTetrahedron) +{ + const Mesh shell = VolumeShellExtractor(buildTetMesh(1.0)).getMesh(); + + EXPECT_EQ(4, countMeshElementsIf(shell, isTriangle)); + EXPECT_TRUE(isAClosedTopology(shell.groups[0].elements)); + EXPECT_GT(signedVolume6(shell, shell.groups[0]), 0.0); +} + +TEST(VolumeShellExtractorTest, removesFacesInsideConnectedTetrahedrons) +{ + const Mesh shell = VolumeShellExtractor(buildTetMeshWithInnerPoint(1.0)).getMesh(); + + EXPECT_EQ(4, countMeshElementsIf(shell, isTriangle)); + EXPECT_EQ(4, shell.coordinates.size()); + EXPECT_TRUE(isAClosedTopology(shell.groups[0].elements)); +} + +TEST(VolumeShellExtractorTest, acceptsAndOrientsClosedTriangleShell) +{ + Mesh input = buildTetSurfaceMesh(1.0); + std::reverse( + input.groups[0].elements[0].vertices.begin(), + input.groups[0].elements[0].vertices.end()); + + const Mesh shell = VolumeShellExtractor(input).getMesh(); + + EXPECT_EQ(4, shell.countElems()); + EXPECT_TRUE(isAClosedTopology(shell.groups[0].elements)); + EXPECT_GT(signedVolume6(shell, shell.groups[0]), 0.0); +} + +TEST(VolumeShellExtractorTest, preservesEmptyGroupsAndNames) +{ + Mesh input = buildTetMesh(1.0); + input.groups[0].name = "volume"; + input.groups.insert(input.groups.begin(), Group{"surface", {}}); + + const Mesh shell = VolumeShellExtractor(input).getMesh(); + + ASSERT_EQ(2, shell.groups.size()); + EXPECT_EQ("surface", shell.groups[0].name); + EXPECT_TRUE(shell.groups[0].elements.empty()); + EXPECT_EQ("volume", shell.groups[1].name); + EXPECT_EQ(4, shell.groups[1].elements.size()); +} + +TEST(VolumeShellExtractorTest, acceptsDisconnectedClosedComponents) +{ + Mesh input = buildTetMesh(1.0); + input.coordinates.insert(input.coordinates.end(), { + Coordinate({2.0, 0.0, 0.0}), + Coordinate({3.0, 0.0, 0.0}), + Coordinate({2.0, 1.0, 0.0}), + Coordinate({2.0, 0.0, 1.0})}); + input.groups[0].elements.push_back( + Element({4, 5, 6, 7}, Element::Type::Volume)); + + const Mesh shell = VolumeShellExtractor(input).getMesh(); + + EXPECT_EQ(8, shell.countElems()); + EXPECT_GT(signedVolume6(shell, shell.groups[0]), 0.0); +} + +TEST(VolumeShellExtractorTest, rejectsOpenShell) +{ + EXPECT_THROW(VolumeShellExtractor{buildOpenTetrahedronShell()}, std::runtime_error); +} + +TEST(VolumeShellExtractorTest, rejectsMixedTrianglesAndTetrahedrons) +{ + Mesh input = buildTetMesh(1.0); + input.groups[0].elements.push_back(Element({0, 1, 2})); + + EXPECT_THROW(VolumeShellExtractor{input}, std::runtime_error); +} + +TEST(VolumeShellExtractorTest, rejectsNonManifoldEdge) +{ + Mesh input = buildTetSurfaceMesh(1.0); + input.coordinates.push_back(Coordinate({0.0, -1.0, 0.0})); + input.groups[0].elements.push_back(Element({0, 1, 4})); + + EXPECT_THROW(VolumeShellExtractor{input}, std::runtime_error); +} + +TEST(VolumeShellExtractorTest, rejectsTetrahedronsTouchingOnlyAtOneVertex) +{ + Mesh input = buildTetMesh(1.0); + input.coordinates.insert(input.coordinates.end(), { + Coordinate({-1.0, 0.0, 0.0}), + Coordinate({0.0, -1.0, 0.0}), + Coordinate({0.0, 0.0, -1.0})}); + input.groups[0].elements.push_back( + Element({0, 4, 5, 6}, Element::Type::Volume)); + + EXPECT_THROW(VolumeShellExtractor{input}, std::runtime_error); +} + +TEST(VolumeShellExtractorTest, rejectsTetrahedronsTouchingOnlyAtOneEdge) +{ + EXPECT_THROW( + VolumeShellExtractor{buildTetsSharingEdgeMesh()}, + std::runtime_error); +} + +TEST(VolumeShellExtractorTest, rejectsFaceSharedByThreeTetrahedrons) +{ + Mesh input = buildTetMesh(1.0); + input.coordinates.push_back(Coordinate({0.0, 0.0, -1.0})); + input.coordinates.push_back(Coordinate({0.2, 0.2, -1.0})); + input.groups[0].elements.push_back( + Element({0, 2, 1, 4}, Element::Type::Volume)); + input.groups[0].elements.push_back( + Element({0, 1, 2, 5}, Element::Type::Volume)); + + EXPECT_THROW(VolumeShellExtractor{input}, std::runtime_error); +} + +TEST(VolumeShellExtractorTest, rejectsInvalidCoordinateId) +{ + Mesh input = buildTetMesh(1.0); + input.groups[0].elements[0].vertices[3] = input.coordinates.size(); + + EXPECT_THROW(VolumeShellExtractor{input}, std::runtime_error); +} + +TEST(VolumeShellExtractorTest, rejectsDegenerateAndUnsupportedElements) +{ + Mesh degenerate = buildTetMesh(1.0); + degenerate.coordinates[3] = Coordinate({0.5, 0.5, 0.0}); + EXPECT_THROW(VolumeShellExtractor{degenerate}, std::runtime_error); + + Mesh unsupported = buildTetMesh(1.0); + unsupported.groups[0].elements = { + Element({0, 1, 2, 3, 0, 1, 2, 3}, Element::Type::Volume)}; + EXPECT_THROW(VolumeShellExtractor{unsupported}, std::runtime_error); +} + +} diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index f07f49a..4f435d7 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -389,8 +389,10 @@ TEST_F(StaircaseMesherTest, mesh_tetrahedron_volume_2x2){ // #endif EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); - EXPECT_EQ(36, countMeshElementsIf(staircasedMesh, isQuad)); + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isQuad)); EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTetrahedron)); + EXPECT_EQ(4, countMeshElementsIf(staircasedMesh, isHexahedron)); + EXPECT_EQ(18, staircasedMesh.coordinates.size()); } @@ -403,8 +405,10 @@ TEST_F(StaircaseMesherTest, mesh_surface_volume_2x2){ auto staircasedMesh = StaircaseMesher{m, 4, opts }.mesh(); EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); - EXPECT_EQ(36, countMeshElementsIf(staircasedMesh, isQuad)); + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isQuad)); EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTetrahedron)); + EXPECT_EQ(4, countMeshElementsIf(staircasedMesh, isHexahedron)); + EXPECT_EQ(18, staircasedMesh.coordinates.size()); } @@ -418,12 +422,32 @@ TEST_F(StaircaseMesherTest, mesh_surface_not_volume_2x2){ EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); EXPECT_EQ(24, countMeshElementsIf(staircasedMesh, isQuad)); EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTetrahedron)); + EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isHexahedron)); } +TEST_F(StaircaseMesherTest, meshesSelectedNonzeroVolumeGroupWithHexahedra) +{ + Mesh mesh = buildCubeSurfaceMesh(0.5); + mesh.groups[0].name = "surface"; + mesh.groups.push_back(mesh.groups[0]); + mesh.groups[1].name = "volume"; + StaircaseMesherOptions options; + options.volumeGroups.insert(1); + + const Mesh result = StaircaseMesher(mesh, 4, options).mesh(); + + EXPECT_EQ("surface", result.groups[0].name); + EXPECT_EQ("volume", result.groups[1].name); + EXPECT_EQ(24, std::count_if( + result.groups[0].elements.begin(), result.groups[0].elements.end(), isQuad)); + EXPECT_EQ(4, std::count_if( + result.groups[1].elements.begin(), result.groups[1].elements.end(), isHexahedron)); +} + #if APP_LOADED -TEST_F(StaircaseMesherTest, fills_closed_volume_with_quads) +TEST_F(StaircaseMesherTest, fills_closed_volume_with_hexahedra) { auto mesh = vtkIO::readInputMesh("testData/cases/sphere/sphere.stl"); @@ -443,8 +467,8 @@ TEST_F(StaircaseMesherTest, fills_closed_volume_with_quads) EXPECT_EQ(0, countMeshElementsIf(staircasedMeshVolume, isTriangle)); EXPECT_EQ(0, countMeshElementsIf(staircasedMeshVolume, isTetrahedron)); - - EXPECT_TRUE(countMeshElementsIf(staircasedMeshVolume, isQuad) > countMeshElementsIf(staircasedMesh, isQuad)); + EXPECT_EQ(0, countMeshElementsIf(staircasedMeshVolume, isQuad)); + EXPECT_GT(countMeshElementsIf(staircasedMeshVolume, isHexahedron), 0); } @@ -634,4 +658,3 @@ TEST_F(StaircaseMesherTest, staircaser_reads_wires_correctly) #endif } - diff --git a/test/types/MeshTest.cpp b/test/types/MeshTest.cpp index a0e799d..2469288 100644 --- a/test/types/MeshTest.cpp +++ b/test/types/MeshTest.cpp @@ -1,7 +1,7 @@ #include "MeshTest.h" #ifdef TESSELLATOR_BOOST -TEST_F(DMesheRTypesMeshTest, serialization_deserialization) { +TEST_F(MeshTest, serialization_deserialization) { Mesh get = buildMesh(); const char* filename = "serialization_deserialization.txt"; @@ -23,4 +23,15 @@ TEST_F(DMesheRTypesMeshTest, serialization_deserialization) { } #endif +TEST_F(MeshTest, identifiesHexahedron) { + Element hexahedron( + {0, 1, 2, 3, 4, 5, 6, 7}, + Element::Type::Volume); + Element surface( + {0, 1, 2, 3, 4, 5, 6, 7}, + Element::Type::Surface); + + EXPECT_TRUE(hexahedron.isHexahedron()); + EXPECT_FALSE(surface.isHexahedron()); +} diff --git a/test/types/MeshTest.h b/test/types/MeshTest.h index 7887de3..3e6ab68 100644 --- a/test/types/MeshTest.h +++ b/test/types/MeshTest.h @@ -9,7 +9,7 @@ using namespace meshlib; -class DMesheRTypesMeshTest : public ::testing::Test { +class MeshTest : public ::testing::Test { protected: static Mesh buildMesh() { Grid grid; From e9e967927acc981bf1f895e98d7af06fbb924dc3 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Mon, 10 Aug 2026 18:45:55 +0200 Subject: [PATCH 59/61] Adds tests for volume filling of alhambra and sphere cases --- README.md | 1 + src/app/launcher.cpp | 4 + src/core/VolumeFiller.cpp | 27 ++-- src/core/VolumeFiller.h | 4 +- src/meshers/StaircaseMesher.cpp | 2 +- src/meshers/StaircaseMesherOptions.h | 1 + test/app/launcherTest.cpp | 25 +++- test/core/VolumeFillerTest.cpp | 23 ++++ test/meshers/StaircaseMesherTest.cpp | 178 ++++++++++++++++++++++++--- 9 files changed, 237 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 14b3ce5..7b1e9a2 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ This optional entry configures the meshing algorithm and its options. If not spe For **staircase** mesher: - `compress`: (boolean, default: false) Enables surface compression to merge adjacent coplanar quads into larger surfaces +- `splitHexahedra`: (boolean, default: false) Splits filled volumes into one conforming hexahedron per occupied grid cell For **conformal** mesher: - `edgePoints`: Controls edge point snapping behavior diff --git a/src/app/launcher.cpp b/src/app/launcher.cpp index c9bb54c..5221e49 100644 --- a/src/app/launcher.cpp +++ b/src/app/launcher.cpp @@ -157,6 +157,10 @@ meshlib::meshers::StaircaseMesherOptions readStaircaseMesherOptions(const nlohma mesherConfig["options"].contains("compress")) { res.compress = mesherConfig["options"]["compress"]; } + if (mesherConfig.contains("options") && + mesherConfig["options"].contains("splitHexahedra")) { + res.splitHexahedra = mesherConfig["options"]["splitHexahedra"]; + } return res; } diff --git a/src/core/VolumeFiller.cpp b/src/core/VolumeFiller.cpp index bde5f6f..d04bbe6 100644 --- a/src/core/VolumeFiller.cpp +++ b/src/core/VolumeFiller.cpp @@ -126,7 +126,9 @@ Element buildHexahedron( } -VolumeFiller::VolumeFiller(const Mesh& staircasedSurface) : +VolumeFiller::VolumeFiller( + const Mesh& staircasedSurface, + bool splitHexahedra) : GridTools(staircasedSurface.grid) { mesh_.grid = staircasedSurface.grid; @@ -181,16 +183,19 @@ VolumeFiller::VolumeFiller(const Mesh& staircasedSurface) : if (begin == end) { continue; } - Cell lower; - Cell upper; - lower[fillAxis] = begin; - upper[fillAxis] = end; - lower[axis1] = ray.first[0]; - upper[axis1] = ray.first[0] + 1; - lower[axis2] = ray.first[1]; - upper[axis2] = ray.first[1] + 1; - outputGroup.elements.push_back( - buildHexahedron(mesh_, coordinateIds, lower, upper, *this)); + const CellDir lastBegin = splitHexahedra ? end - 1 : begin; + for (CellDir cellBegin = begin; cellBegin <= lastBegin; ++cellBegin) { + Cell lower; + Cell upper; + lower[fillAxis] = cellBegin; + upper[fillAxis] = splitHexahedra ? cellBegin + 1 : end; + lower[axis1] = ray.first[0]; + upper[axis1] = ray.first[0] + 1; + lower[axis2] = ray.first[1]; + upper[axis2] = ray.first[1] + 1; + outputGroup.elements.push_back( + buildHexahedron(mesh_, coordinateIds, lower, upper, *this)); + } } } } diff --git a/src/core/VolumeFiller.h b/src/core/VolumeFiller.h index 180166c..ab44a0f 100644 --- a/src/core/VolumeFiller.h +++ b/src/core/VolumeFiller.h @@ -7,7 +7,9 @@ namespace meshlib::core { class VolumeFiller : private utils::GridTools { public: - explicit VolumeFiller(const Mesh& staircasedSurface); + explicit VolumeFiller( + const Mesh& staircasedSurface, + bool splitHexahedra = false); Mesh getMesh() const; diff --git a/src/meshers/StaircaseMesher.cpp b/src/meshers/StaircaseMesher.cpp index 9f45386..fadfc34 100644 --- a/src/meshers/StaircaseMesher.cpp +++ b/src/meshers/StaircaseMesher.cpp @@ -41,7 +41,7 @@ StaircaseMesher::StaircaseMesher(const Mesh& inputMesh, int decimalPlacesInColla log("Processing volume shell."); process(volumeMesh_, false); log("Filling volume shell with hexahedra."); - volumeMesh_ = VolumeFiller(volumeMesh_).getMesh(); + volumeMesh_ = VolumeFiller(volumeMesh_, opts_.splitHexahedra).getMesh(); logNumberOfHexahedra(countMeshElementsIf(volumeMesh_, isHexahedron)); } diff --git a/src/meshers/StaircaseMesherOptions.h b/src/meshers/StaircaseMesherOptions.h index 53f2b50..4bac022 100644 --- a/src/meshers/StaircaseMesherOptions.h +++ b/src/meshers/StaircaseMesherOptions.h @@ -7,6 +7,7 @@ namespace meshlib::meshers { class StaircaseMesherOptions : public MesherBaseOptions { public: bool compress = false; + bool splitHexahedra = false; }; } diff --git a/test/app/launcherTest.cpp b/test/app/launcherTest.cpp index 056f5ee..2cf01ab 100644 --- a/test/app/launcherTest.cpp +++ b/test/app/launcherTest.cpp @@ -73,6 +73,30 @@ TEST_F(LauncherTest, builds_staircased_mesher_default) const auto & options = dynamic_cast(*mesher).getOptions(); EXPECT_EQ(options.volumeGroups.size(), 0); EXPECT_EQ(options.compress, false); + EXPECT_FALSE(options.splitHexahedra); +} + +TEST_F(LauncherTest, buildsStaircasedMesherWithSplitHexahedra) +{ + meshlib::Mesh meshMock; + meshMock.grid = { + std::vector{0, 1}, + std::vector{0, 1}, + std::vector{0, 1} + }; + nlohmann::json config = { + {"mesher", { + {"type", "staircase"}, + {"options", {{"splitHexahedra", true}}} + }} + }; + + ObjectDefinition object; + auto mesher = buildMesher(meshMock, config, object); + const auto& staircase = + dynamic_cast(*mesher); + + EXPECT_TRUE(staircase.getOptions().splitHexahedra); } TEST_F(LauncherTest, builds_staircased_mesher_without_compression) @@ -354,4 +378,3 @@ TEST_F(LauncherTest, launches_multiObject_sameFileMultipleGroups) EXPECT_NO_THROW(exitCode = launcher(ac, av)); EXPECT_EQ(exitCode, EXIT_SUCCESS); } - diff --git a/test/core/VolumeFillerTest.cpp b/test/core/VolumeFillerTest.cpp index 239ee7e..01b7fa8 100644 --- a/test/core/VolumeFillerTest.cpp +++ b/test/core/VolumeFillerTest.cpp @@ -99,6 +99,29 @@ TEST(VolumeFillerTest, fillsContinuousRunsWithHexahedra) } } +TEST(VolumeFillerTest, splitsContinuousRunsIntoUnitCellHexahedra) +{ + const Mesh result = VolumeFiller(buildTwoByTwoByTwoShell(), true).getMesh(); + + EXPECT_EQ(8, countMeshElementsIf(result, isHexahedron)); + const GridTools tools(result.grid); + for (const auto& element : result.groups[0].elements) { + ASSERT_TRUE(element.isHexahedron()); + Cell lower = tools.getCell(result.coordinates[element.vertices[0]]); + Cell upper = lower; + for (CoordinateId vertex : element.vertices) { + const Cell cell = tools.getCell(result.coordinates[vertex]); + for (Axis axis : {X, Y, Z}) { + lower[axis] = std::min(lower[axis], cell[axis]); + upper[axis] = std::max(upper[axis], cell[axis]); + } + } + for (Axis axis : {X, Y, Z}) { + EXPECT_EQ(1, upper[axis] - lower[axis]); + } + } +} + TEST(VolumeFillerTest, rejectsAnOpenQuadShell) { Mesh shell = buildTwoByTwoByTwoShell(); diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index 4f435d7..1f69bd6 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -1,6 +1,10 @@ #include "gtest/gtest.h" #include "MeshFixtures.h" +#include +#include +#include +#include #include "meshers/StaircaseMesher.h" #include "StaircaseMesherOptions.h" @@ -24,6 +28,132 @@ using namespace meshFixtures; using namespace utils; using namespace meshTools; +namespace { + +using EdgeKey = std::array; +using FaceKey = std::array; + +EdgeKey edgeKey(CoordinateId first, CoordinateId second) +{ + return first < second ? EdgeKey{first, second} : EdgeKey{second, first}; +} + +bool isSingleClosedSurface(const Elements& faces) +{ + if (faces.empty()) { + return false; + } + std::map> edgeFaces; + for (ElementId faceId = 0; faceId < faces.size(); ++faceId) { + const auto& vertices = faces[faceId].vertices; + if (vertices.size() < 3) { + return false; + } + for (std::size_t vertex = 0; vertex < vertices.size(); ++vertex) { + edgeFaces[edgeKey(vertices[vertex], vertices[(vertex + 1) % vertices.size()])] + .push_back(faceId); + } + } + + std::vector> adjacency(faces.size()); + for (const auto& entry : edgeFaces) { + if (entry.second.size() != 2) { + return false; + } + const ElementId first = entry.second[0]; + const ElementId second = entry.second[1]; + adjacency[first].insert(second); + adjacency[second].insert(first); + } + + std::set visited{0}; + std::queue pending; + pending.push(0); + while (!pending.empty()) { + const ElementId current = pending.front(); + pending.pop(); + for (ElementId neighbor : adjacency[current]) { + if (visited.insert(neighbor).second) { + pending.push(neighbor); + } + } + } + return visited.size() == faces.size(); +} + +bool isSingleClosedHexahedralVolume(const Mesh& mesh) +{ + static const std::array, 6> hexahedronFaces{{ + {{0, 1, 2, 3}}, {{4, 5, 6, 7}}, + {{0, 1, 5, 4}}, {{1, 2, 6, 5}}, + {{2, 3, 7, 6}}, {{3, 0, 4, 7}} + }}; + + std::vector hexahedra; + for (const auto& group : mesh.groups) { + for (const auto& element : group.elements) { + if (!element.isHexahedron()) { + return false; + } + hexahedra.push_back(&element); + } + } + if (hexahedra.empty()) { + return false; + } + + struct FaceOccurrence { + ElementId hexahedron; + CoordinateIds vertices; + }; + std::map> faceOccurrences; + for (ElementId elementId = 0; elementId < hexahedra.size(); ++elementId) { + for (const auto& face : hexahedronFaces) { + CoordinateIds vertices; + FaceKey key; + for (std::size_t vertex = 0; vertex < face.size(); ++vertex) { + vertices.push_back(hexahedra[elementId]->vertices[face[vertex]]); + key[vertex] = vertices.back(); + } + std::sort(key.begin(), key.end()); + faceOccurrences[key].push_back({elementId, vertices}); + } + } + + Elements boundary; + std::vector> adjacency(hexahedra.size()); + for (const auto& entry : faceOccurrences) { + if (entry.second.size() == 1) { + boundary.emplace_back( + entry.second.front().vertices, Element::Type::Surface); + } else if (entry.second.size() == 2) { + const ElementId first = entry.second[0].hexahedron; + const ElementId second = entry.second[1].hexahedron; + adjacency[first].insert(second); + adjacency[second].insert(first); + } else { + return false; + } + } + + std::set visited{0}; + std::queue pending; + pending.push(0); + while (!pending.empty()) { + const ElementId current = pending.front(); + pending.pop(); + for (ElementId neighbor : adjacency[current]) { + if (visited.insert(neighbor).second) { + pending.push(neighbor); + } + } + } + return visited.size() == hexahedra.size() + && isSingleClosedSurface(boundary); +} + +} + class StaircaseMesherTest : public ::testing::Test { public: @@ -447,7 +577,7 @@ TEST_F(StaircaseMesherTest, meshesSelectedNonzeroVolumeGroupWithHexahedra) #if APP_LOADED -TEST_F(StaircaseMesherTest, fills_closed_volume_with_hexahedra) +TEST_F(StaircaseMesherTest, fillsSphereAsSingleClosedUnitHexahedralVolume) { auto mesh = vtkIO::readInputMesh("testData/cases/sphere/sphere.stl"); @@ -455,20 +585,40 @@ TEST_F(StaircaseMesherTest, fills_closed_volume_with_hexahedra) mesh.grid[Y] = utils::GridTools::linspace(-100.0, 100.0, 51); mesh.grid[Z] = utils::GridTools::linspace(-100.0, 100.0, 51); - meshlib::meshers::StaircaseMesherOptions opts; - // opts.isVolume = false; - auto staircasedMesh = StaircaseMesher{mesh, 4, opts }.mesh(); - - opts.volumeGroups.insert(0); - auto staircasedMeshVolume = StaircaseMesher{mesh, 4, opts }.mesh(); - - EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTriangle)); - EXPECT_EQ(0, countMeshElementsIf(staircasedMesh, isTetrahedron)); + ASSERT_TRUE(isSingleClosedSurface(mesh.groups[0].elements)); + + StaircaseMesherOptions options; + options.volumeGroups.insert(0); + options.splitHexahedra = true; + + // vtkIO::exportMeshToVTU("testData/cases/sphere/sphere.volume.before.vtk", mesh); + const Mesh result = StaircaseMesher{mesh, 4, options}.mesh(); + // vtkIO::exportMeshToVTU("testData/cases/sphere/sphere.volume.after.vtk", result); + + EXPECT_EQ(7967, countMeshElementsIf(result, isHexahedron)); + EXPECT_EQ(result.countElems(), countMeshElementsIf(result, isHexahedron)); + EXPECT_TRUE(isSingleClosedHexahedralVolume(result)); +} + +TEST_F(StaircaseMesherTest, fillsAlhambraAsSingleClosedUnitHexahedralVolume) +{ + auto mesh = vtkIO::readInputMesh("testData/cases/alhambra/alhambra.stl"); + mesh.grid[X] = utils::GridTools::linspace(-60.0, 60.0, 61); + mesh.grid[Y] = utils::GridTools::linspace(-60.0, 60.0, 61); + mesh.grid[Z] = utils::GridTools::linspace(-1.872734, 11.236404, 8); + ASSERT_TRUE(isSingleClosedSurface(mesh.groups[0].elements)); + + StaircaseMesherOptions options; + options.volumeGroups.insert(0); + options.splitHexahedra = true; + + // vtkIO::exportMeshToVTU("testData/cases/alhambra/alhambra.volume.before.vtk", mesh); + const Mesh result = StaircaseMesher{mesh, 4, options}.mesh(); + // vtkIO::exportMeshToVTU("testData/cases/alhambra/alhambra.volume.after.vtk", result); - EXPECT_EQ(0, countMeshElementsIf(staircasedMeshVolume, isTriangle)); - EXPECT_EQ(0, countMeshElementsIf(staircasedMeshVolume, isTetrahedron)); - EXPECT_EQ(0, countMeshElementsIf(staircasedMeshVolume, isQuad)); - EXPECT_GT(countMeshElementsIf(staircasedMeshVolume, isHexahedron), 0); + EXPECT_EQ(7255, countMeshElementsIf(result, isHexahedron)); + EXPECT_EQ(result.countElems(), countMeshElementsIf(result, isHexahedron)); + EXPECT_TRUE(isSingleClosedHexahedralVolume(result)); } From 3de74f8fcc843be6238354b0d1b92d73a84c472f Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Tue, 11 Aug 2026 10:39:32 +0200 Subject: [PATCH 60/61] Removes commented out test --- test/core/SlicerTest.cpp | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/core/SlicerTest.cpp b/test/core/SlicerTest.cpp index 70e18b4..2367b93 100644 --- a/test/core/SlicerTest.cpp +++ b/test/core/SlicerTest.cpp @@ -1090,20 +1090,4 @@ TEST_F(SlicerTest, sphere_case_patch_contour_check_2) #endif } -// TEST_F(SlicerTest, cube1x1x1_size05_grid_fill) -// { -// Mesh m = buildCubeSurfaceMesh(0.5); - -// Mesh out; -// ASSERT_NO_THROW(out = Slicer{m}.getMesh()); -// EXPECT_EQ(48, countMeshElementsIf(out, isTriangle)); -// EXPECT_FALSE(containsDegenerateTriangles(out)); -// EXPECT_EQ(countContours(m), countContours(out)); - -// Mesh filled; -// ASSERT_NO_THROW(filled = Slicer::fill(out)); -// EXPECT_EQ(72, countMeshElementsIf(out, isTriangle)); -// } - - } From 0bc900c969218967d51d4f2faeb5e7aec8faeae7 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Tue, 11 Aug 2026 10:39:49 +0200 Subject: [PATCH 61/61] Minor --- test/meshers/StaircaseMesherTest.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/test/meshers/StaircaseMesherTest.cpp b/test/meshers/StaircaseMesherTest.cpp index 1f69bd6..2ec23d1 100644 --- a/test/meshers/StaircaseMesherTest.cpp +++ b/test/meshers/StaircaseMesherTest.cpp @@ -504,7 +504,6 @@ TEST_F(StaircaseMesherTest, mesh_tetrahedron_volume_2x2){ Mesh m = buildCubeVolumeMesh(0.5); meshlib::meshers::StaircaseMesherOptions opts; opts.volumeGroups.insert(0); - // opts.isVolume = true; // #if APP_LOADED // vtkIO::exportMeshToVTU("testData/cases/mesh_tetrahedron_volume_2x2_before.vtk", m);