Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ jobs:

name: ${{ matrix.preset.os }} / ${{matrix.preset.name}} / ${{matrix.build-type}}
runs-on: ${{ matrix.preset.os }}
env:
VCPKG_BINARY_SOURCES: clear
# Inherit the workflow-level x-gha binary cache. Do not override this
# with `clear`: Windows VTK is otherwise rebuilt from source on every run.

steps:
- name: checkout repository
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ CMakeUserPresets.json
sliced.vtk
contour.vtk

testData/
testData/cases/*/*.vtk
build
4 changes: 3 additions & 1 deletion CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"type": "FILEPATH",
"value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
},
"CMAKE_BUILD_TYPE": "Release",
"TESSELLATOR_ENABLE_CGAL": false
}
},
Expand All @@ -26,13 +27,14 @@
{
"name": "gnu",
"displayName": "GNU g++ compiler",
"binaryDir": "build-rls/",
"inherits": "default"
},
{
"name": "gnu-cgal",
"displayName": "GNU g++ compiler with CGAL",
"inherits": "gnu",
"binaryDir": "build-cgal/",
"binaryDir": "build-rls-cgal/",
"cacheVariables": {
"TESSELLATOR_ENABLE_CGAL": true
}
Expand Down
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,12 @@ This contains the information about the mesh file(s). You can specify a single o
- `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)
- `ghost`: (optional boolean, default: false) Excludes the object from cross-object decisions while still meshing and exporting it normally
- `mesher`: (optional) Override the global mesher settings for this specific object

```json
"objects": [
{"filename": "object1.stl", "group": "group1"},
{"filename": "object1.stl", "group": "group1", "ghost": true},
{"filename": "object2.stl", "group": "group2", "mesher": {"type": "conformal"}}
]
```
Expand All @@ -118,8 +119,14 @@ For **staircase** mesher:
- `splitHexahedra`: (boolean, default: false) Splits filled volumes into one conforming hexahedron per occupied grid cell

For **conformal** mesher:
- `edgePoints`: Controls edge point snapping behavior
- `forbiddenLength`: Minimum length threshold for snapping
- `edgePoints`: (non-negative integer, default: `0`) Number of evenly spaced
candidate snap points added along each grid edge. These points are placed in
the portion of the edge outside the endpoint exclusion regions defined by
`forbiddenLength`. Set to `0` to add no interior edge points.
- `forbiddenLength`: (number, default: `0.0`) Fraction of each grid edge kept
clear next to both endpoints when placing or snapping to edge points. It must
not exceed `0.5`.
- `staircaseSharedCells`: (boolean, default: true) Selectively staircases cells occupied by this conformal object and another object

**Global options:**
- `exportGrid`: (boolean, default: true) Controls whether to export the grid file
Expand All @@ -135,12 +142,26 @@ Example with staircase mesher and compression enabled:
}
```

### `<output>`
This optional entry controls how multi-object results are written:

- `singleFile`: (boolean, default: false) Writes all objects to one
`{basename}.tessellator.vtk` file instead of separate per-object mesh files.
Object groups remain identifiable through the `group` and `groupNames` cell
attributes. Group names must be unique.

```json
"output": {
"singleFile": true
}
```

Example with conformal mesher:
```json
"mesher": {
"type": "conformal",
"options": {
"edgePoints": true,
"edgePoints": 3,
"forbiddenLength": 0.001
}
}
Expand All @@ -150,6 +171,7 @@ Example with conformal mesher:
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.vtk` - Combined multi-object mesh when `output.singleFile` is true
- `{basename}.tessellator.grid.vtk` - Grid file (if `exportGrid` is true)

### Complete Example
Expand Down
172 changes: 160 additions & 12 deletions src/app/launcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
#include "meshers/MesherBase.h"
#include "meshers/StaircaseMesher.h"
#include "meshers/ConformalMesher.h"
#include "core/Staircaser.h"
#include "utils/GridTools.h"
#include "utils/MeshTools.h"
#include "utils/RedundancyCleaner.h"

#include <boost/program_options.hpp>
#include <nlohmann/json.hpp>
Expand All @@ -14,8 +16,10 @@
#include <filesystem>
#include <fstream>
#include <array>
#include <algorithm>
#include <memory>
#include <optional>
#include <set>

namespace meshlib::app {

Expand Down Expand Up @@ -59,6 +63,7 @@ std::vector<ObjectDefinition> readObjectsFromJSON(const nlohmann::json& fileData
if (obj.contains("volume")){
objDef.isVolume = obj["volume"];
}
objDef.ghost = obj.value("ghost", false);
if (obj.contains("mesher")) {
objDef.mesherOverride = obj["mesher"];
}
Expand All @@ -71,6 +76,7 @@ std::vector<ObjectDefinition> readObjectsFromJSON(const nlohmann::json& fileData
if (fileData["object"].contains("volume")){
objDef.isVolume = fileData["object"]["volume"];
}
objDef.ghost = fileData["object"].value("ghost", false);
if (fileData.contains("mesher")) {
objDef.mesherOverride = fileData["mesher"];
}
Expand All @@ -82,6 +88,14 @@ std::vector<ObjectDefinition> readObjectsFromJSON(const nlohmann::json& fileData
return objects;
}

bool readSingleFileOutputOption(const nlohmann::json& fileData)
{
if (!fileData.contains("output")) {
return false;
}
return fileData["output"].value("singleFile", false);
}

Mesh readMesh(const nlohmann::json& fileData, const std::filesystem::path& folderPath, const ObjectDefinition& objDef)
{
std::filesystem::path meshObjectPath = folderPath / objDef.filename;
Expand Down Expand Up @@ -179,8 +193,13 @@ meshlib::meshers::ConformalMesherOptions readConformalMesherOptions(const nlohma
res.volumeGroups.insert(0);
}
if (mesherConfig.contains("options")) {
res.snapperOptions.edgePoints = mesherConfig["options"]["edgePoints"];
res.snapperOptions.forbiddenLength = mesherConfig["options"]["forbiddenLength"];
const auto& options = mesherConfig["options"];
res.snapperOptions.edgePoints = options.value(
"edgePoints", res.snapperOptions.edgePoints);
res.snapperOptions.forbiddenLength = options.value(
"forbiddenLength", res.snapperOptions.forbiddenLength);
res.staircaseSharedCells = options.value(
"staircaseSharedCells", res.staircaseSharedCells);
}
return res;
}
Expand Down Expand Up @@ -218,6 +237,105 @@ std::unique_ptr<meshlib::meshers::MesherBase> buildMesher(const Mesh& in, const
}
}

namespace {

struct MeshedObject {
ObjectDefinition definition;
std::string mesherType;
std::string extension;
bool staircaseSharedCells = false;
Mesh mesh;
};

Mesh mergeObjectMeshes(const std::vector<MeshedObject>& objects)
{
Mesh combined;
if (objects.empty()) {
return combined;
}

combined.grid = objects.front().mesh.grid;
for (const auto& object : objects) {
if (object.mesh.grid != combined.grid) {
throw std::runtime_error("Cannot combine object meshes with different grids.");
}
if (object.mesh.groups.size() != 1) {
throw std::runtime_error(
"Each object must produce exactly one mesh group for combined processing.");
}

Mesh namedMesh = object.mesh;
namedMesh.groups.front().name = object.definition.group;
if (combined.groups.empty()) {
combined = std::move(namedMesh);
} else {
utils::meshTools::mergeMeshAsNewGroup(combined, namedMesh);
}
}
utils::RedundancyCleaner::fuseCoords(combined);
utils::RedundancyCleaner::cleanCoords(combined);
return combined;
}

void validateCombinedGroupNames(const std::vector<ObjectDefinition>& objects)
{
std::set<std::string> groupNames;
for (const auto& object : objects) {
if (!groupNames.insert(object.group).second) {
throw std::runtime_error(
"Combined output requires unique object group names; duplicate group: " +
object.group);
}
}
}

void staircaseSharedConformalCells(std::vector<MeshedObject>& objects)
{
const bool hasEnabledConformalObject = std::any_of(
objects.begin(), objects.end(), [](const MeshedObject& object) {
return !object.definition.ghost &&
object.mesherType == conformal_mesher &&
object.staircaseSharedCells;
});
const auto participatingObjectCount = std::count_if(
objects.begin(), objects.end(), [](const MeshedObject& object) {
return !object.definition.ghost;
});
if (!hasEnabledConformalObject || participatingObjectCount < 2) {
return;
}

Mesh relativeCombined = mergeObjectMeshes(objects);
utils::meshTools::convertToRelativeCoordinates(relativeCombined);
std::set<GroupId> ghostGroups;
for (GroupId groupId = 0; groupId < objects.size(); ++groupId) {
if (objects[groupId].definition.ghost) {
ghostGroups.insert(groupId);
}
}
const auto sharedCells = meshlib::meshers::ConformalMesher::cellsSharedByGroups(
relativeCombined, ghostGroups);
if (sharedCells.empty()) {
return;
}

for (auto& object : objects) {
if (object.definition.ghost || object.mesherType != conformal_mesher ||
!object.staircaseSharedCells) {
continue;
}

Mesh relativeMesh = object.mesh;
utils::meshTools::convertToRelativeCoordinates(relativeMesh);
object.mesh = meshlib::core::Staircaser{relativeMesh}.getSelectiveMesh(
sharedCells, meshlib::core::Staircaser::GapsFillingType::Insert);
object.mesh.groups.front().name = object.definition.group;
utils::meshTools::convertToAbsoluteCoordinates(object.mesh);
}
}

} // namespace

int launcher(int argc, const char* argv[])
{
po::options_description desc("Allowed options");
Expand Down Expand Up @@ -247,9 +365,13 @@ int launcher(int argc, const char* argv[])
std::vector<ObjectDefinition> objects = readObjectsFromJSON(inputFileData);
std::filesystem::path outputFolder = getFolder(inputFileName);
auto basename = getBasename(inputFileName);
const bool singleFileOutput = readSingleFileOutputOption(inputFileData);
if (singleFileOutput) {
validateCombinedGroupNames(objects);
}

Mesh firstMesh;
bool first = true;
std::vector<MeshedObject> meshedObjects;
meshedObjects.reserve(objects.size());

for (const auto& objDef : objects) {
std::cout << "\n-- Processing object: " << objDef.filename << " (group: " << objDef.group << ")" << std::endl;
Expand All @@ -258,20 +380,46 @@ int launcher(int argc, const char* argv[])

auto mesher = buildMesher(mesh, inputFileData, objDef);
Mesh resultMesh = mesher->mesh();
if (resultMesh.groups.size() == 1) {
resultMesh.groups.front().name = objDef.group;
}

if (first) {
firstMesh = resultMesh;
first = false;
const auto mesherType = readMesherType(inputFileData, objDef.mesherOverride);
bool staircaseSharedCells = false;
if (mesherType == conformal_mesher) {
staircaseSharedCells = readConformalMesherOptions(
inputFileData, objDef.isVolume, objDef.mesherOverride)
.staircaseSharedCells;
}
meshedObjects.push_back({
objDef,
mesherType,
readExtension(inputFileData, objDef.mesherOverride),
staircaseSharedCells,
std::move(resultMesh)
});
}

staircaseSharedConformalCells(meshedObjects);

auto extension = readExtension(inputFileData, objDef.mesherOverride);
std::string outputFileName = objDef.group + ".tessellator." + extension + ".vtk";
exportMeshToVTU(outputFolder / outputFileName, resultMesh);
if (singleFileOutput && !meshedObjects.empty()) {
const auto outputFileName = basename + ".tessellator.vtk";
exportMeshToVTU(
outputFolder / outputFileName, mergeObjectMeshes(meshedObjects));
std::cout << "-- Exported: " << outputFileName << std::endl;
} else {
for (const auto& object : meshedObjects) {
const std::string outputFileName = object.definition.group +
".tessellator." + object.extension + ".vtk";
exportMeshToVTU(outputFolder / outputFileName, object.mesh);
std::cout << "-- Exported: " << outputFileName << std::endl;
}
}

if (!first && readExportGridOption(inputFileData, std::nullopt)) {
exportGridToVTU(outputFolder / (basename + ".tessellator.grid.vtk"), firstMesh.grid);
if (!meshedObjects.empty() && readExportGridOption(inputFileData, std::nullopt)) {
exportGridToVTU(
outputFolder / (basename + ".tessellator.grid.vtk"),
meshedObjects.front().mesh.grid);
std::cout << "-- Exported grid: " << basename << ".tessellator.grid.vtk" << std::endl;
}

Expand Down
4 changes: 3 additions & 1 deletion src/app/launcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ struct ObjectDefinition {
std::string filename;
std::string group;
bool isVolume = false;
bool ghost = false;
std::optional<nlohmann::json> mesherOverride;
};

int launcher(int argc, const char* argv[]);
Grid parseGridFromJSON(const nlohmann::json& fileData);
std::vector<ObjectDefinition> readObjectsFromJSON(const nlohmann::json& fileData);
bool readSingleFileOutputOption(const nlohmann::json& fileData);
Mesh readMesh(const std::string& fn, const ObjectDefinition& objDef);
std::unique_ptr<meshlib::meshers::MesherBase> buildMesher(const Mesh& in, const nlohmann::json& fileData, const ObjectDefinition& objDef);

}
}
Loading
Loading