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
7 changes: 6 additions & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
build:
name: Build Docker image (cached)
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
Expand Down Expand Up @@ -56,4 +57,8 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# PR caches are scoped to the PR and never reused, so only branches and
# tags write. ignore-error keeps a flaky cache export (the GHA backend
# intermittently answers "error writing layer blob: not_found") from
# failing a build that already succeeded.
cache-to: ${{ github.event_name == 'pull_request' && '' || 'type=gha,mode=max,ignore-error=true' }}
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# [1.4.0] - 2026-08-07

### Added

- XDMF parser (`.xdmf` / `.xmf`) for solver outputs with HDF5 heavy data:
static topology/geometry plus a temporal collection of node/cell attributes.
Frames stream straight into the rendered array (no geometry re-read) and are
cached in memory under a 512 MB budget, so looped playback is zero-I/O.
- Annotation mode (`--annotate`): paint a per-cell `label` array with a surface
brush, undo, and save.
- Playback bar: previous/next frame buttons and a speed drop-up (1x/2x/4x/8x/12x).

### Changed

- Playback is clock-driven: each tick jumps to the frame the elapsed time calls
for, dropping frames instead of queueing them so high speeds stay real-time.
- `TemporalSource` is now an interface with VTKHDF and XDMF backends; parsers
expose their time series through `MeshParser::temporal()`.
- Mesh JSON accepts a `surfaces` key.

# [1.3.0] - 2026-07-07

### Changed
Expand Down
17 changes: 15 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.14)

project(VtkViewer VERSION 1.3.0)
project(VtkViewer VERSION 1.4.0)

set(CMAKE_AUTOMOC ON)

Expand Down Expand Up @@ -80,6 +80,7 @@ find_package(VTK QUIET COMPONENTS
IOImage
IOHDF
FiltersCore
FiltersExtraction
RenderingCore
RenderingOpenGL2
InteractionStyle
Expand Down Expand Up @@ -126,6 +127,7 @@ set(VV_CORE_SOURCES
src/VTKMeshParser.cpp
src/ViewerWindow.cpp
src/XMLMeshParser.cpp
src/XdmfMeshParser.cpp
src/mesh_utils.cpp
)

Expand All @@ -145,6 +147,7 @@ set(VV_HEADERS
src/include/VTKMeshParser.h
src/include/ViewerWindow.h
src/include/XMLMeshParser.h
src/include/XdmfMeshParser.h
src/include/mesh_utils.h
)

Expand Down Expand Up @@ -188,7 +191,16 @@ if(VV_ENABLE_WARNINGS)
endif()
endif()

target_link_libraries(vv PRIVATE fmt::fmt)
# XDMF heavy data is read with the HDF5 C API. Prefer the copy VTK already links
# (VTK::hdf5), so no new dependency is introduced.
if(TARGET VTK::hdf5)
set(VV_HDF5_TARGET VTK::hdf5)
else()
find_package(HDF5 REQUIRED COMPONENTS C)
set(VV_HDF5_TARGET hdf5::hdf5)
endif()

target_link_libraries(vv PRIVATE fmt::fmt ${VV_HDF5_TARGET})
target_link_libraries(vv PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(vv PRIVATE
VTK::CommonCore
Expand All @@ -199,6 +211,7 @@ target_link_libraries(vv PRIVATE
VTK::IOImage
VTK::IOHDF
VTK::FiltersCore
VTK::FiltersExtraction
VTK::RenderingCore
VTK::RenderingOpenGL2
VTK::InteractionStyle
Expand Down
33 changes: 16 additions & 17 deletions src/JsonMeshParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,30 @@ bool isMeshPart(const json& value) {
value["vertices"].is_array() && value["indices"].is_array();
}

bool looksLikeJsonMesh(const json& value) {
if (isMeshPart(value)) {
return true;
}
if (value.is_object() && value.contains("surface")) {
const auto& surface = value["surface"];
return surface.is_array() && !surface.empty() &&
std::all_of(surface.begin(), surface.end(), isMeshPart);
}
if (!value.is_array() || value.empty()) {
return false;
}
return std::all_of(value.begin(), value.end(), isMeshPart);
}

const json* meshPartArray(const json& root) {
if (root.is_array()) {
return &root;
}
if (root.is_object() && root.contains("surface") && root["surface"].is_array()) {
return &root["surface"];
if (!root.is_object()) {
return nullptr;
}
for (const char* key : {"surface", "surfaces"}) {
if (root.contains(key) && root[key].is_array()) {
return &root[key];
}
}
return nullptr;
}

bool looksLikeJsonMesh(const json& value) {
if (isMeshPart(value)) {
return true;
}
const json* parts = meshPartArray(value);
return parts != nullptr && !parts->empty() &&
std::all_of(parts->begin(), parts->end(), isMeshPart);
}

std::string partName(const json& part) {
if (part.contains("name") && part["name"].is_string()) {
return part["name"].get<std::string>();
Expand Down
9 changes: 4 additions & 5 deletions src/MeshLoading.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "VTKHDFMeshParser.h"
#include "VTKMeshParser.h"
#include "XMLMeshParser.h"
#include "XdmfMeshParser.h"
#include "mesh_utils.h"

#include <array>
Expand Down Expand Up @@ -67,6 +68,7 @@ std::vector<std::string> filesToProcessFromArgs(const std::vector<std::string>&
std::vector<std::unique_ptr<MeshParser>> buildParsers() {
std::vector<std::unique_ptr<MeshParser>> parsers;
parsers.emplace_back(std::make_unique<XMLMeshParser>());
parsers.emplace_back(std::make_unique<XdmfMeshParser>());
parsers.emplace_back(std::make_unique<VTKHDFMeshParser>());
parsers.emplace_back(std::make_unique<VTKMeshParser>());
parsers.emplace_back(std::make_unique<JsonMeshParser>());
Expand Down Expand Up @@ -143,11 +145,8 @@ MeshLoadResult loadMeshes(const std::vector<std::string>& meshfiles, bool explod

std::vector<vtkSmartPointer<vtkDataSet>> parsedMeshes = selected->parse(realFilename);

// Capture temporal (playable) info if this file produced it.
if (const auto* hdfParser = dynamic_cast<const VTKHDFMeshParser*>(selected)) {
if (auto temporal = hdfParser->temporal(); temporal && temporal->playable()) {
result.temporal = temporal;
}
if (auto temporal = selected->temporal(); temporal && temporal->playable()) {
result.temporal = temporal;
}

if (parsedMeshes.empty()) {
Expand Down
28 changes: 28 additions & 0 deletions src/MeshRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,34 @@ bool MeshRenderer::setActiveScalar(const std::string& scalarName, FieldAssociati
return true;
}

void MeshRenderer::colorByFixedCategorical(const std::string& scalarName,
FieldAssociation association,
vtkLookupTable* lut,
const double range[2]) {
if (sceneMeshes.empty() || mappers.empty() || !sceneMeshes.front() || !lut) {
return;
}
activeScalarName = scalarName;
activeScalarAssociation = association;
vtkDataSet* mesh = sceneMeshes.front();
vtkDataSetMapper* mapper = mappers.front();
if (association == FieldAssociation::Cell) {
mesh->GetCellData()->SetActiveScalars(scalarName.c_str());
mapper->SetScalarModeToUseCellFieldData();
} else {
mesh->GetPointData()->SetActiveScalars(scalarName.c_str());
mapper->SetScalarModeToUsePointFieldData();
}
mapper->SelectColorArray(scalarName.c_str());
mapper->SetColorModeToMapScalars();
mapper->ScalarVisibilityOn();
mapper->SetLookupTable(lut);
mapper->SetScalarRange(range[0], range[1]);
if (context.window) {
context.window->Render();
}
}

void MeshRenderer::clearActiveScalar() {
activeScalarName.clear();
activeScalarAnalysis = {};
Expand Down
Loading
Loading