From 19189038fbe21f92441f15cf92e07a115e4a9c71 Mon Sep 17 00:00:00 2001 From: epernod Date: Thu, 20 Nov 2025 01:31:21 +0100 Subject: [PATCH 01/17] add support for loading cell data arrays in BaseVTKLoader --- src/sofa/vtk/BaseVTKLoader.cpp | 87 +++++++++++++++++++++- src/sofa/vtk/BaseVTKLoader.h | 19 +++++ src/sofa/vtk/UnstructuredGridVTKLoader.cpp | 3 - src/sofa/vtk/UnstructuredGridVTKLoader.h | 1 - src/sofa/vtk/VTKtoSOFA.cpp | 54 ++++++++++++++ src/sofa/vtk/VTKtoSOFA.h | 5 ++ 6 files changed, 164 insertions(+), 5 deletions(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index 89dff35..ef2d092 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace { @@ -20,6 +21,73 @@ vtkSmartPointer getDataSet(std::string fileName) namespace sofavtk { +sofa::core::objectmodel::Data* BaseVTKLoader::getCellVectorData(const std::string& name) const +{ + auto it = m_cellVectorData.find(name); + if (it != m_cellVectorData.end()) + { + return it->second.get(); + } + return nullptr; +} + +void BaseVTKLoader::loadCellVectorData(vtkSmartPointer dataset) +{ + // Clear any previously loaded dynamic data + for (auto& [name, dataPtr] : m_cellVectorData) + { + this->removeData(dataPtr.get()); + } + m_cellVectorData.clear(); + + const auto& names = d_cellVectorDataNames.getValue(); + if (names.empty()) + { + return; + } + + vtkCellData* cellData = dataset->GetCellData(); + if (!cellData) + { + msg_warning() << "No cell data available in the dataset"; + return; + } + + for (const auto& arrayName : names) + { + vtkDataArray* array = cellData->GetArray(arrayName.c_str()); + if (!array) + { + msg_warning() << "Cell data array '" << arrayName << "' not found in VTK file"; + continue; + } + + if (array->GetNumberOfComponents() != 3) + { + msg_warning() << "Cell data array '" << arrayName << "' has " + << array->GetNumberOfComponents() << " components, expected 3 (Vec3)"; + continue; + } + + // Create a new Data object for this array + auto dataPtr = std::make_unique>(); + dataPtr->setName(arrayName); + dataPtr->setHelp("Cell vector data loaded from VTK file"); + + // Add it to the component so it becomes visible in SOFA + this->addData(dataPtr.get(), arrayName); + + // Load the data from VTK + auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); + sofavtk::loadVTKCellData_3D(dataset, arrayName.c_str(), accessor.wref()); + + msg_info() << "Loaded cell vector data '" << arrayName << "' with " << accessor->size() << " entries"; + + // Store the pointer + m_cellVectorData[arrayName] = std::move(dataPtr); + } +} + bool BaseVTKLoader::doLoad() { const auto& fileName = d_filename.getFullPath(); @@ -29,6 +97,8 @@ bool BaseVTKLoader::doLoad() if (dataSet != nullptr) { + //sofavtk::listDataArrays(dataSet); + { auto positions = sofa::helper::getWriteOnlyAccessor(this->d_positions); sofavtk::extractPoints(positions.wref(), dataSet); @@ -36,16 +106,31 @@ bool BaseVTKLoader::doLoad() sofavtk::extractCells(*this, dataSet); + // Load user-specified cell vector data + loadCellVectorData(dataSet); + + loadVTKData(dataSet); + return true; } return false; } -void BaseVTKLoader::doClearBuffers() {} +void BaseVTKLoader::doClearBuffers() +{ + // Clear dynamically created Data objects + for (auto& [name, dataPtr] : m_cellVectorData) + { + this->removeData(dataPtr.get()); + } + m_cellVectorData.clear(); +} + } // namespace sofavtk + namespace { diff --git a/src/sofa/vtk/BaseVTKLoader.h b/src/sofa/vtk/BaseVTKLoader.h index 28ad021..29d2f41 100644 --- a/src/sofa/vtk/BaseVTKLoader.h +++ b/src/sofa/vtk/BaseVTKLoader.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace sofavtk { @@ -11,14 +13,31 @@ struct SOFA_VTK_API BaseVTKLoader : sofa::core::loader::MeshLoader { SOFA_ABSTRACT_CLASS(BaseVTKLoader, sofa::core::loader::MeshLoader); + using Vec3Vector = sofa::type::vector; + + /// Names of VTK cell data arrays (3-component vectors) to load + sofa::core::objectmodel::Data> d_cellVectorDataNames{ + initData(&d_cellVectorDataNames, "cellVectorDataNames", + "Names of cell data arrays (Vec3) to load from the VTK file")}; + + /// Get a loaded cell vector data by its VTK array name. Returns nullptr if not found. + sofa::core::objectmodel::Data* getCellVectorData(const std::string& name) const; + private: bool doLoad() final; void doClearBuffers() final; + void loadCellVectorData(vtkSmartPointer dataset); + + /// Storage for dynamically created Data objects + std::map>> m_cellVectorData; + protected: virtual vtkSmartPointer getDataSet(const sofa::core::objectmodel::DataFileName& fileName) = 0; + + virtual void loadVTKData(vtkSmartPointer dataset) {} }; } diff --git a/src/sofa/vtk/UnstructuredGridVTKLoader.cpp b/src/sofa/vtk/UnstructuredGridVTKLoader.cpp index 3c0c5bc..1707f88 100644 --- a/src/sofa/vtk/UnstructuredGridVTKLoader.cpp +++ b/src/sofa/vtk/UnstructuredGridVTKLoader.cpp @@ -1,9 +1,6 @@ #include #include -#include -#include - #include #include #include diff --git a/src/sofa/vtk/UnstructuredGridVTKLoader.h b/src/sofa/vtk/UnstructuredGridVTKLoader.h index a3eff33..75879c8 100644 --- a/src/sofa/vtk/UnstructuredGridVTKLoader.h +++ b/src/sofa/vtk/UnstructuredGridVTKLoader.h @@ -12,7 +12,6 @@ struct SOFA_VTK_API UnstructuredGridVTKLoader : BaseVTKLoader vtkSmartPointer getDataSet( const sofa::core::objectmodel::DataFileName& fileName) override; - }; } diff --git a/src/sofa/vtk/VTKtoSOFA.cpp b/src/sofa/vtk/VTKtoSOFA.cpp index 581e7fe..d276408 100644 --- a/src/sofa/vtk/VTKtoSOFA.cpp +++ b/src/sofa/vtk/VTKtoSOFA.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include namespace sofavtk { @@ -117,4 +119,56 @@ void extractCells(sofa::core::loader::MeshLoader& loader, vtkSmartPointer dataset) +{ + if (!dataset) return; + + // --- Point Data --- + vtkPointData* pointData = dataset->GetPointData(); + std::cout << "Point Data Arrays:" << std::endl; + for (int i = 0; i < pointData->GetNumberOfArrays(); ++i) + { + vtkDataArray* array = pointData->GetArray(i); + std::cout << " [" << i << "] " << array->GetName() + << " components: " << array->GetNumberOfComponents() + << " tuples: " << array->GetNumberOfTuples() << std::endl; + } + + // --- Cell Data --- + vtkCellData* cellData = dataset->GetCellData(); + std::cout << "Cell Data Arrays:" << std::endl; + for (int i = 0; i < cellData->GetNumberOfArrays(); ++i) + { + vtkDataArray* array = cellData->GetArray(i); + std::cout << " [" << i << "] " << array->GetName() + << " components: " << array->GetNumberOfComponents() + << " tuples: " << array->GetNumberOfTuples() << std::endl; + } +} + + +void loadVTKCellData_3D(vtkSmartPointer dataSet, const char* cellDataName, sofa::type::vector& data) +{ + vtkCellData* cellData = dataSet->GetCellData(); + + vtkDataArray* array = cellData->GetArray(cellDataName); + + if (array) + { + const auto nbCells = dataSet->GetNumberOfCells(); + data.resize(nbCells); + + std::cout << "vtkCellData found, loading: " << cellDataName << " -> " << nbCells << " cells" << std::endl; + for (vtkIdType cellId = 0; cellId < nbCells; ++cellId) + { + double value[3]; // assuming max 3 components + array->GetTuple(cellId, value); + //std::cout << "Cell " << cellId << ": "; + + data[cellId] = sofa::type::Vec3(value[0], value[1], value[2]); + } + } +} + } // namespace sofavtk diff --git a/src/sofa/vtk/VTKtoSOFA.h b/src/sofa/vtk/VTKtoSOFA.h index 08fed83..87c512f 100644 --- a/src/sofa/vtk/VTKtoSOFA.h +++ b/src/sofa/vtk/VTKtoSOFA.h @@ -17,4 +17,9 @@ void SOFA_VTK_API extractCells( vtkSmartPointer dataSet ); +void SOFA_VTK_API listDataArrays(vtkSmartPointer dataSet); + +void SOFA_VTK_API loadVTKCellData_3D(vtkSmartPointer dataSet, const char* cellDataName, + sofa::type::vector& data); + } From ab544b9de9bb05b1866bcaf994c9a306aaea0e2d Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 15 Jan 2026 14:44:13 +0100 Subject: [PATCH 02/17] add support for scalar, symmetric tensor, and full tensor cell data --- src/sofa/vtk/BaseVTKLoader.cpp | 119 +++++++++++------------ src/sofa/vtk/BaseVTKLoader.h | 28 +++--- src/sofa/vtk/UnstructuredGridVTKLoader.h | 1 + src/sofa/vtk/VTKtoSOFA.cpp | 57 +---------- src/sofa/vtk/VTKtoSOFA.h | 27 ++++- 5 files changed, 98 insertions(+), 134 deletions(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index ef2d092..4b91c08 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -21,31 +21,9 @@ vtkSmartPointer getDataSet(std::string fileName) namespace sofavtk { -sofa::core::objectmodel::Data* BaseVTKLoader::getCellVectorData(const std::string& name) const +void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, + const std::string& arrayName) { - auto it = m_cellVectorData.find(name); - if (it != m_cellVectorData.end()) - { - return it->second.get(); - } - return nullptr; -} - -void BaseVTKLoader::loadCellVectorData(vtkSmartPointer dataset) -{ - // Clear any previously loaded dynamic data - for (auto& [name, dataPtr] : m_cellVectorData) - { - this->removeData(dataPtr.get()); - } - m_cellVectorData.clear(); - - const auto& names = d_cellVectorDataNames.getValue(); - if (names.empty()) - { - return; - } - vtkCellData* cellData = dataset->GetCellData(); if (!cellData) { @@ -53,39 +31,61 @@ void BaseVTKLoader::loadCellVectorData(vtkSmartPointer dataset) return; } - for (const auto& arrayName : names) + vtkDataArray* array = cellData->GetArray(arrayName.c_str()); + if (!array) { - vtkDataArray* array = cellData->GetArray(arrayName.c_str()); - if (!array) - { - msg_warning() << "Cell data array '" << arrayName << "' not found in VTK file"; - continue; - } + msg_warning() << "Cell data array '" << arrayName << "' not found in VTK file"; + return; + } - if (array->GetNumberOfComponents() != 3) - { - msg_warning() << "Cell data array '" << arrayName << "' has " - << array->GetNumberOfComponents() << " components, expected 3 (Vec3)"; - continue; - } + const int numComponents = array->GetNumberOfComponents(); - // Create a new Data object for this array - auto dataPtr = std::make_unique>(); - dataPtr->setName(arrayName); - dataPtr->setHelp("Cell vector data loaded from VTK file"); + // Dispatch based on number of components + switch (numComponents) + { + case 1: + loadCellDataArray(dataset, arrayName); + break; + case 2: + loadCellDataArray, 2>(dataset, arrayName); + break; + case 3: + loadCellDataArray, 3>(dataset, arrayName); + break; + case 4: + loadCellDataArray, 4>(dataset, arrayName); + break; + case 6: + loadCellDataArray, 6>(dataset, arrayName); + break; + case 9: + loadCellDataArray, 9>(dataset, arrayName); + break; + default: + msg_warning() << "Cell data array '" << arrayName + << "' has unsupported number of components: " << numComponents; + break; + } +} - // Add it to the component so it becomes visible in SOFA - this->addData(dataPtr.get(), arrayName); +template +void BaseVTKLoader::loadCellDataArray(vtkSmartPointer dataset, + const std::string& arrayName) +{ + // Create a new Data object for this array and add it to Base + auto dataPtr = std::make_unique>>(); + dataPtr->setName(arrayName); + dataPtr->setHelp("Cell data loaded from VTK file"); + this->addData(dataPtr.get(), arrayName); - // Load the data from VTK - auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); - sofavtk::loadVTKCellData_3D(dataset, arrayName.c_str(), accessor.wref()); + // Load the data from VTK + auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); + sofavtk::extractCellData(dataset, arrayName.c_str(), accessor.wref()); - msg_info() << "Loaded cell vector data '" << arrayName << "' with " << accessor->size() << " entries"; + msg_info() << "Loaded cell data '" << arrayName << "' with " << accessor->size() << " entries"; - // Store the pointer - m_cellVectorData[arrayName] = std::move(dataPtr); - } + // Store the pointer because Base does not manage it + m_cellData[arrayName] = std::move(dataPtr); } bool BaseVTKLoader::doLoad() @@ -97,8 +97,6 @@ bool BaseVTKLoader::doLoad() if (dataSet != nullptr) { - //sofavtk::listDataArrays(dataSet); - { auto positions = sofa::helper::getWriteOnlyAccessor(this->d_positions); sofavtk::extractPoints(positions.wref(), dataSet); @@ -106,10 +104,8 @@ bool BaseVTKLoader::doLoad() sofavtk::extractCells(*this, dataSet); - // Load user-specified cell vector data - loadCellVectorData(dataSet); - - loadVTKData(dataSet); + for (const auto& arrayName : d_cellDataNames.getValue()) + loadCellDataArrayByName(dataSet, arrayName); return true; } @@ -119,18 +115,15 @@ bool BaseVTKLoader::doLoad() void BaseVTKLoader::doClearBuffers() { - // Clear dynamically created Data objects - for (auto& [name, dataPtr] : m_cellVectorData) - { - this->removeData(dataPtr.get()); - } - m_cellVectorData.clear(); + for (auto& pair : m_cellData) + this->removeData(pair.second.get()); + + m_cellData.clear(); } } // namespace sofavtk - namespace { diff --git a/src/sofa/vtk/BaseVTKLoader.h b/src/sofa/vtk/BaseVTKLoader.h index 29d2f41..cc13c3a 100644 --- a/src/sofa/vtk/BaseVTKLoader.h +++ b/src/sofa/vtk/BaseVTKLoader.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -13,31 +14,30 @@ struct SOFA_VTK_API BaseVTKLoader : sofa::core::loader::MeshLoader { SOFA_ABSTRACT_CLASS(BaseVTKLoader, sofa::core::loader::MeshLoader); - using Vec3Vector = sofa::type::vector; - - /// Names of VTK cell data arrays (3-component vectors) to load - sofa::core::objectmodel::Data> d_cellVectorDataNames{ - initData(&d_cellVectorDataNames, "cellVectorDataNames", - "Names of cell data arrays (Vec3) to load from the VTK file")}; - - /// Get a loaded cell vector data by its VTK array name. Returns nullptr if not found. - sofa::core::objectmodel::Data* getCellVectorData(const std::string& name) const; + /// Names of VTK cell data arrays to load + sofa::core::objectmodel::Data> d_cellDataNames{ + initData(&d_cellDataNames, "cellDataNames", + "Names of cell data arrays to load from the VTK file")}; private: bool doLoad() final; void doClearBuffers() final; - void loadCellVectorData(vtkSmartPointer dataset); + /// Load a cell data array from VTK dataset + template + void loadCellDataArray(vtkSmartPointer dataset, + const std::string& arrayName); - /// Storage for dynamically created Data objects - std::map>> m_cellVectorData; + /// Dispatch cell data loading based on VTK type and number of components + void loadCellDataArrayByName(vtkSmartPointer dataset, const std::string& arrayName); + + /// Unified storage for all dynamically created Data objects + std::map> m_cellData; protected: virtual vtkSmartPointer getDataSet(const sofa::core::objectmodel::DataFileName& fileName) = 0; - - virtual void loadVTKData(vtkSmartPointer dataset) {} }; } diff --git a/src/sofa/vtk/UnstructuredGridVTKLoader.h b/src/sofa/vtk/UnstructuredGridVTKLoader.h index 75879c8..a3eff33 100644 --- a/src/sofa/vtk/UnstructuredGridVTKLoader.h +++ b/src/sofa/vtk/UnstructuredGridVTKLoader.h @@ -12,6 +12,7 @@ struct SOFA_VTK_API UnstructuredGridVTKLoader : BaseVTKLoader vtkSmartPointer getDataSet( const sofa::core::objectmodel::DataFileName& fileName) override; + }; } diff --git a/src/sofa/vtk/VTKtoSOFA.cpp b/src/sofa/vtk/VTKtoSOFA.cpp index d276408..4fb1fa2 100644 --- a/src/sofa/vtk/VTKtoSOFA.cpp +++ b/src/sofa/vtk/VTKtoSOFA.cpp @@ -4,8 +4,6 @@ #include #include #include -#include -#include namespace sofavtk { @@ -101,7 +99,7 @@ void extractCells(sofa::core::loader::MeshLoader& loader, vtkSmartPointer types = vtkSmartPointer ::New(); + vtkSmartPointer types = vtkSmartPointer::New(); dataSet->GetCellTypes(types); const vtkIdType nbElementTypes = types->GetNumberOfTypes(); for (vtkIdType i = 0; i < nbElementTypes; ++i) @@ -119,56 +117,7 @@ void extractCells(sofa::core::loader::MeshLoader& loader, vtkSmartPointer dataset) -{ - if (!dataset) return; - - // --- Point Data --- - vtkPointData* pointData = dataset->GetPointData(); - std::cout << "Point Data Arrays:" << std::endl; - for (int i = 0; i < pointData->GetNumberOfArrays(); ++i) - { - vtkDataArray* array = pointData->GetArray(i); - std::cout << " [" << i << "] " << array->GetName() - << " components: " << array->GetNumberOfComponents() - << " tuples: " << array->GetNumberOfTuples() << std::endl; - } - - // --- Cell Data --- - vtkCellData* cellData = dataset->GetCellData(); - std::cout << "Cell Data Arrays:" << std::endl; - for (int i = 0; i < cellData->GetNumberOfArrays(); ++i) - { - vtkDataArray* array = cellData->GetArray(i); - std::cout << " [" << i << "] " << array->GetName() - << " components: " << array->GetNumberOfComponents() - << " tuples: " << array->GetNumberOfTuples() << std::endl; - } -} - - -void loadVTKCellData_3D(vtkSmartPointer dataSet, const char* cellDataName, sofa::type::vector& data) -{ - vtkCellData* cellData = dataSet->GetCellData(); - - vtkDataArray* array = cellData->GetArray(cellDataName); - - if (array) - { - const auto nbCells = dataSet->GetNumberOfCells(); - data.resize(nbCells); - - std::cout << "vtkCellData found, loading: " << cellDataName << " -> " << nbCells << " cells" << std::endl; - for (vtkIdType cellId = 0; cellId < nbCells; ++cellId) - { - double value[3]; // assuming max 3 components - array->GetTuple(cellId, value); - //std::cout << "Cell " << cellId << ": "; - - data[cellId] = sofa::type::Vec3(value[0], value[1], value[2]); - } - } -} +// Explicit instantiation for scalar case only +template void extractCellData(vtkSmartPointer, const char*, sofa::type::vector&); } // namespace sofavtk diff --git a/src/sofa/vtk/VTKtoSOFA.h b/src/sofa/vtk/VTKtoSOFA.h index 87c512f..d4eb2d0 100644 --- a/src/sofa/vtk/VTKtoSOFA.h +++ b/src/sofa/vtk/VTKtoSOFA.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include namespace sofavtk { @@ -17,9 +19,28 @@ void SOFA_VTK_API extractCells( vtkSmartPointer dataSet ); -void SOFA_VTK_API listDataArrays(vtkSmartPointer dataSet); +template +void extractCellData(vtkSmartPointer dataSet, const char* arrayName, + sofa::type::vector& data) +{ + vtkCellData* cellData = dataSet->GetCellData(); + if (!cellData) return; + + vtkDataArray* array = cellData->GetArray(arrayName); + if (!array) return; + + const auto nbCells = dataSet->GetNumberOfCells(); + data.resize(nbCells); -void SOFA_VTK_API loadVTKCellData_3D(vtkSmartPointer dataSet, const char* cellDataName, - sofa::type::vector& data); + for (vtkIdType cellId = 0; cellId < nbCells; ++cellId) + { + const double* values = array->GetTuple(cellId); + + if constexpr (NumComponents == 1) + data[cellId] = static_cast(values[0]); + else + data[cellId] = DataType(values); + } +} } From 0422b306811ce79109bbd8272808197f79061eec Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 5 Mar 2026 12:05:29 +0100 Subject: [PATCH 03/17] use vtkArrayDispatch for cell data loading, add integer scalar support --- src/sofa/vtk/BaseVTKLoader.cpp | 11 ++++++- src/sofa/vtk/VTKtoSOFA.cpp | 3 -- src/sofa/vtk/VTKtoSOFA.h | 59 ++++++++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index 4b91c08..43115f7 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -39,8 +39,17 @@ void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, } const int numComponents = array->GetNumberOfComponents(); + const int dataType = array->GetDataType(); + const bool isInteger = (dataType != VTK_FLOAT && dataType != VTK_DOUBLE); - // Dispatch based on number of components + // Integer scalar arrays (material IDs, region flags, indices, 0/1 switches) + if (isInteger && numComponents == 1) + { + loadCellDataArray(dataset, arrayName); + return; + } + + // Float/double arrays dispatched by component count switch (numComponents) { case 1: diff --git a/src/sofa/vtk/VTKtoSOFA.cpp b/src/sofa/vtk/VTKtoSOFA.cpp index 4fb1fa2..6926799 100644 --- a/src/sofa/vtk/VTKtoSOFA.cpp +++ b/src/sofa/vtk/VTKtoSOFA.cpp @@ -117,7 +117,4 @@ void extractCells(sofa::core::loader::MeshLoader& loader, vtkSmartPointer(vtkSmartPointer, const char*, sofa::type::vector&); - } // namespace sofavtk diff --git a/src/sofa/vtk/VTKtoSOFA.h b/src/sofa/vtk/VTKtoSOFA.h index d4eb2d0..295b73f 100644 --- a/src/sofa/vtk/VTKtoSOFA.h +++ b/src/sofa/vtk/VTKtoSOFA.h @@ -4,8 +4,10 @@ #include #include #include +#include #include #include +#include namespace sofavtk { @@ -19,28 +21,57 @@ void SOFA_VTK_API extractCells( vtkSmartPointer dataSet ); -template -void extractCellData(vtkSmartPointer dataSet, const char* arrayName, - sofa::type::vector& data) +namespace detail { - vtkCellData* cellData = dataSet->GetCellData(); - if (!cellData) return; - vtkDataArray* array = cellData->GetArray(arrayName); - if (!array) return; - - const auto nbCells = dataSet->GetNumberOfCells(); - data.resize(nbCells); +/// Worker functor invoked by vtkArrayDispatch with the concrete array type. +/// Falls back to the vtkDataArray virtual API when dispatch finds no match. +template +struct CellDataWorker +{ + sofa::type::vector& data; - for (vtkIdType cellId = 0; cellId < nbCells; ++cellId) + template + void operator()(ArrayT* array) const { - const double* values = array->GetTuple(cellId); + const vtkIdType nbCells = array->GetNumberOfTuples(); + data.resize(nbCells); if constexpr (NumComponents == 1) - data[cellId] = static_cast(values[0]); + { + vtkIdType i = 0; + for (const auto v : vtk::DataArrayValueRange<1>(array)) + data[i++] = static_cast(v); + } else - data[cellId] = DataType(values); + { + vtkIdType i = 0; + for (const auto tuple : vtk::DataArrayTupleRange(array)) + { + for (int c = 0; c < NumComponents; ++c) + data[i][c] = static_cast(tuple[c]); + ++i; + } + } } +}; + +} // namespace detail + +template +void extractCellData(vtkSmartPointer dataSet, const char* arrayName, + sofa::type::vector& data) +{ + vtkCellData* cellData = dataSet->GetCellData(); + if (!cellData) return; + + vtkDataArray* array = cellData->GetArray(arrayName); + if (!array) return; + + detail::CellDataWorker worker{data}; + using Dispatcher = vtkArrayDispatch::DispatchByValueType; + if (!Dispatcher::Execute(array, worker)) + worker(array); // fallback via vtkDataArray virtual API } } From 52f2aacce24552b91f2621a4a85915910e6f9056 Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 5 Mar 2026 13:10:56 +0100 Subject: [PATCH 04/17] expand dispatch-deduced type to int and float variants --- src/sofa/vtk/BaseVTKLoader.cpp | 59 ++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index 43115f7..f8f76e8 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -16,12 +16,46 @@ vtkSmartPointer getDataSet(std::string fileName) return reader->GetOutput(); } +/// Dispatch worker for scalar cell data arrays. +/// The concrete array type ArrayT is deduced by vtkArrayDispatch, so T = vtk::GetAPIType +/// is the exact native C++ scalar type. Data is copied losslessly with no manual type checks. +struct ScalarCellDataWorker +{ + sofavtk::BaseVTKLoader& loader; + const std::string& arrayName; + std::unique_ptr result; + vtkIdType numLoaded = 0; + + template + void operator()(ArrayT* array) + { + using T = vtk::GetAPIType; + + auto dataPtr = std::make_unique>>(); + dataPtr->setName(arrayName); + dataPtr->setHelp("Cell data loaded from VTK file"); + + { + auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); + auto& vec = accessor.wref(); + vec.resize(array->GetNumberOfTuples()); + vtkIdType i = 0; + for (const auto v : vtk::DataArrayValueRange<1>(array)) + vec[i++] = v; + numLoaded = i; + } + + loader.addData(dataPtr.get(), arrayName); + result = std::move(dataPtr); + } +}; + } namespace sofavtk { -void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, +void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, const std::string& arrayName) { vtkCellData* cellData = dataset->GetCellData(); @@ -39,22 +73,24 @@ void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, } const int numComponents = array->GetNumberOfComponents(); - const int dataType = array->GetDataType(); - const bool isInteger = (dataType != VTK_FLOAT && dataType != VTK_DOUBLE); - // Integer scalar arrays (material IDs, region flags, indices, 0/1 switches) - if (isInteger && numComponents == 1) + if (numComponents == 1) { - loadCellDataArray(dataset, arrayName); + ScalarCellDataWorker worker{*this, arrayName}; + using Dispatcher = vtkArrayDispatch::DispatchByValueType; + if (!Dispatcher::Execute(array, worker)) + worker(array); // fallback: ArrayT = vtkDataArray, T = double + if (worker.result) + { + msg_info() << "Loaded cell data '" << arrayName << "' with " << worker.numLoaded << " entries"; + m_cellData[arrayName] = std::move(worker.result); + } return; } - // Float/double arrays dispatched by component count + // Multi-component arrays: always mapped to Vec switch (numComponents) { - case 1: - loadCellDataArray(dataset, arrayName); - break; case 2: loadCellDataArray, 2>(dataset, arrayName); break; @@ -81,19 +117,16 @@ template void BaseVTKLoader::loadCellDataArray(vtkSmartPointer dataset, const std::string& arrayName) { - // Create a new Data object for this array and add it to Base auto dataPtr = std::make_unique>>(); dataPtr->setName(arrayName); dataPtr->setHelp("Cell data loaded from VTK file"); this->addData(dataPtr.get(), arrayName); - // Load the data from VTK auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); sofavtk::extractCellData(dataset, arrayName.c_str(), accessor.wref()); msg_info() << "Loaded cell data '" << arrayName << "' with " << accessor->size() << " entries"; - // Store the pointer because Base does not manage it m_cellData[arrayName] = std::move(dataPtr); } From e645f238da9e2bf291d6f6565e6aea2e56ac328f Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 5 Mar 2026 13:43:51 +0100 Subject: [PATCH 05/17] add MultiComponentCellDataWorker with compile-time instantiation of functions; limited to 9 components --- src/sofa/vtk/BaseVTKLoader.cpp | 106 ++++++++++++++++++++------------- src/sofa/vtk/BaseVTKLoader.h | 6 -- src/sofa/vtk/VTKtoSOFA.h | 4 +- 3 files changed, 66 insertions(+), 50 deletions(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index f8f76e8..eda76d2 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -16,9 +16,6 @@ vtkSmartPointer getDataSet(std::string fileName) return reader->GetOutput(); } -/// Dispatch worker for scalar cell data arrays. -/// The concrete array type ArrayT is deduced by vtkArrayDispatch, so T = vtk::GetAPIType -/// is the exact native C++ scalar type. Data is copied losslessly with no manual type checks. struct ScalarCellDataWorker { sofavtk::BaseVTKLoader& loader; @@ -50,6 +47,57 @@ struct ScalarCellDataWorker } }; +struct MultiComponentCellDataWorker +{ + sofavtk::BaseVTKLoader& loader; + const std::string& arrayName; + int numComponents; + std::unique_ptr result; + vtkIdType numLoaded = 0; + + template + void operator()(ArrayT* array) + { + using T = vtk::GetAPIType; + dispatchN(array); + } + +private: + template + void dispatchN(ArrayT* array) + { + if (numComponents == N) + fill(array); + else if constexpr (N < 9) + dispatchN(array); + } + + template + void fill(ArrayT* array) + { + auto dataPtr = std::make_unique>>>(); + dataPtr->setName(arrayName); + dataPtr->setHelp("Cell data loaded from VTK file"); + + { + auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); + auto& vec = accessor.wref(); + vec.resize(array->GetNumberOfTuples()); + vtkIdType i = 0; + for (const auto tuple : vtk::DataArrayTupleRange(array)) + { + for (int c = 0; c < N; ++c) + vec[i][c] = tuple[c]; + ++i; + } + numLoaded = i; + } + + loader.addData(dataPtr.get(), arrayName); + result = std::move(dataPtr); + } +}; + } namespace sofavtk @@ -74,12 +122,13 @@ void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, const int numComponents = array->GetNumberOfComponents(); + using Dispatcher = vtkArrayDispatch::DispatchByValueType; + if (numComponents == 1) { ScalarCellDataWorker worker{*this, arrayName}; - using Dispatcher = vtkArrayDispatch::DispatchByValueType; if (!Dispatcher::Execute(array, worker)) - worker(array); // fallback: ArrayT = vtkDataArray, T = double + worker(array); if (worker.result) { msg_info() << "Loaded cell data '" << arrayName << "' with " << worker.numLoaded << " entries"; @@ -88,46 +137,21 @@ void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, return; } - // Multi-component arrays: always mapped to Vec - switch (numComponents) + if (numComponents > 9) { - case 2: - loadCellDataArray, 2>(dataset, arrayName); - break; - case 3: - loadCellDataArray, 3>(dataset, arrayName); - break; - case 4: - loadCellDataArray, 4>(dataset, arrayName); - break; - case 6: - loadCellDataArray, 6>(dataset, arrayName); - break; - case 9: - loadCellDataArray, 9>(dataset, arrayName); - break; - default: msg_warning() << "Cell data array '" << arrayName - << "' has unsupported number of components: " << numComponents; - break; + << "' has " << numComponents << " components (max supported: 9)"; + return; } -} - -template -void BaseVTKLoader::loadCellDataArray(vtkSmartPointer dataset, - const std::string& arrayName) -{ - auto dataPtr = std::make_unique>>(); - dataPtr->setName(arrayName); - dataPtr->setHelp("Cell data loaded from VTK file"); - this->addData(dataPtr.get(), arrayName); - - auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); - sofavtk::extractCellData(dataset, arrayName.c_str(), accessor.wref()); - - msg_info() << "Loaded cell data '" << arrayName << "' with " << accessor->size() << " entries"; - m_cellData[arrayName] = std::move(dataPtr); + MultiComponentCellDataWorker worker{*this, arrayName, numComponents}; + if (!Dispatcher::Execute(array, worker)) + worker(array); + if (worker.result) + { + msg_info() << "Loaded cell data '" << arrayName << "' with " << worker.numLoaded << " entries"; + m_cellData[arrayName] = std::move(worker.result); + } } bool BaseVTKLoader::doLoad() diff --git a/src/sofa/vtk/BaseVTKLoader.h b/src/sofa/vtk/BaseVTKLoader.h index cc13c3a..2f90260 100644 --- a/src/sofa/vtk/BaseVTKLoader.h +++ b/src/sofa/vtk/BaseVTKLoader.h @@ -24,12 +24,6 @@ struct SOFA_VTK_API BaseVTKLoader : sofa::core::loader::MeshLoader bool doLoad() final; void doClearBuffers() final; - /// Load a cell data array from VTK dataset - template - void loadCellDataArray(vtkSmartPointer dataset, - const std::string& arrayName); - - /// Dispatch cell data loading based on VTK type and number of components void loadCellDataArrayByName(vtkSmartPointer dataset, const std::string& arrayName); /// Unified storage for all dynamically created Data objects diff --git a/src/sofa/vtk/VTKtoSOFA.h b/src/sofa/vtk/VTKtoSOFA.h index 295b73f..4a3a379 100644 --- a/src/sofa/vtk/VTKtoSOFA.h +++ b/src/sofa/vtk/VTKtoSOFA.h @@ -24,8 +24,6 @@ void SOFA_VTK_API extractCells( namespace detail { -/// Worker functor invoked by vtkArrayDispatch with the concrete array type. -/// Falls back to the vtkDataArray virtual API when dispatch finds no match. template struct CellDataWorker { @@ -71,7 +69,7 @@ void extractCellData(vtkSmartPointer dataSet, const char* arrayName, detail::CellDataWorker worker{data}; using Dispatcher = vtkArrayDispatch::DispatchByValueType; if (!Dispatcher::Execute(array, worker)) - worker(array); // fallback via vtkDataArray virtual API + worker(array); } } From 13cfdf5f821f135278cbf044d23e0ef03513e804 Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 5 Mar 2026 14:42:13 +0100 Subject: [PATCH 06/17] add/restrict dispatch to explicitly supported value types --- src/sofa/vtk/BaseVTKLoader.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index eda76d2..8048294 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -16,6 +16,16 @@ vtkSmartPointer getDataSet(std::string fileName) return reader->GetOutput(); } +// long and unsigned long have platform-dependent sizes (32-bit on Windows, 64-bit on Linux/Mac). +// Remap them to the fixed-size type of the same width so the SOFA Data type is consistent +// and predictable regardless of platform. +template struct CanonicalLong { using type = T; }; +template<> struct CanonicalLong { using type = std::conditional_t; }; +template<> struct CanonicalLong { using type = std::conditional_t; }; + +template +using CanonicalLong_t = typename CanonicalLong::type; + struct ScalarCellDataWorker { sofavtk::BaseVTKLoader& loader; @@ -26,7 +36,7 @@ struct ScalarCellDataWorker template void operator()(ArrayT* array) { - using T = vtk::GetAPIType; + using T = CanonicalLong_t>; auto dataPtr = std::make_unique>>(); dataPtr->setName(arrayName); @@ -58,7 +68,7 @@ struct MultiComponentCellDataWorker template void operator()(ArrayT* array) { - using T = vtk::GetAPIType; + using T = CanonicalLong_t>; dispatchN(array); } @@ -122,7 +132,15 @@ void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, const int numComponents = array->GetNumberOfComponents(); - using Dispatcher = vtkArrayDispatch::DispatchByValueType; + // Explicitly supported value types. Each maps to a fixed-size C++ type that SOFA's + // type system reliably handles. long and unsigned long are included and remapped via + // CanonicalLong_t to int/long long based on their size on the current platform. + using SupportedTypes = vtkTypeList::Create< + float, double, + int, unsigned int, + long, unsigned long, + long long, unsigned long long>; + using Dispatcher = vtkArrayDispatch::DispatchByValueType; if (numComponents == 1) { From 7b1243c6d8973991808fdc48af837de8f65443f4 Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 5 Mar 2026 16:46:15 +0100 Subject: [PATCH 07/17] refactor --- src/sofa/vtk/BaseVTKLoader.cpp | 30 +++++++++---------- src/sofa/vtk/BaseVTKLoader.h | 7 +++-- src/sofa/vtk/VTKtoSOFA.h | 55 ---------------------------------- 3 files changed, 19 insertions(+), 73 deletions(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index 8048294..57464db 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -1,8 +1,11 @@ #include #include +#include +#include +#include +#include #include #include -#include namespace { @@ -31,7 +34,6 @@ struct ScalarCellDataWorker sofavtk::BaseVTKLoader& loader; const std::string& arrayName; std::unique_ptr result; - vtkIdType numLoaded = 0; template void operator()(ArrayT* array) @@ -49,7 +51,6 @@ struct ScalarCellDataWorker vtkIdType i = 0; for (const auto v : vtk::DataArrayValueRange<1>(array)) vec[i++] = v; - numLoaded = i; } loader.addData(dataPtr.get(), arrayName); @@ -63,7 +64,6 @@ struct MultiComponentCellDataWorker const std::string& arrayName; int numComponents; std::unique_ptr result; - vtkIdType numLoaded = 0; template void operator()(ArrayT* array) @@ -100,7 +100,6 @@ struct MultiComponentCellDataWorker vec[i][c] = tuple[c]; ++i; } - numLoaded = i; } loader.addData(dataPtr.get(), arrayName); @@ -142,16 +141,21 @@ void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, long long, unsigned long long>; using Dispatcher = vtkArrayDispatch::DispatchByValueType; - if (numComponents == 1) - { - ScalarCellDataWorker worker{*this, arrayName}; + auto dispatch = [&](auto& worker) { if (!Dispatcher::Execute(array, worker)) worker(array); if (worker.result) { - msg_info() << "Loaded cell data '" << arrayName << "' with " << worker.numLoaded << " entries"; + msg_info() << "Loaded cell data '" << arrayName << "' with " + << array->GetNumberOfTuples() << " entries"; m_cellData[arrayName] = std::move(worker.result); } + }; + + if (numComponents == 1) + { + ScalarCellDataWorker worker{*this, arrayName}; + dispatch(worker); return; } @@ -163,13 +167,7 @@ void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, } MultiComponentCellDataWorker worker{*this, arrayName, numComponents}; - if (!Dispatcher::Execute(array, worker)) - worker(array); - if (worker.result) - { - msg_info() << "Loaded cell data '" << arrayName << "' with " << worker.numLoaded << " entries"; - m_cellData[arrayName] = std::move(worker.result); - } + dispatch(worker); } bool BaseVTKLoader::doLoad() diff --git a/src/sofa/vtk/BaseVTKLoader.h b/src/sofa/vtk/BaseVTKLoader.h index 2f90260..fe675c5 100644 --- a/src/sofa/vtk/BaseVTKLoader.h +++ b/src/sofa/vtk/BaseVTKLoader.h @@ -14,11 +14,14 @@ struct SOFA_VTK_API BaseVTKLoader : sofa::core::loader::MeshLoader { SOFA_ABSTRACT_CLASS(BaseVTKLoader, sofa::core::loader::MeshLoader); - /// Names of VTK cell data arrays to load sofa::core::objectmodel::Data> d_cellDataNames{ initData(&d_cellDataNames, "cellDataNames", "Names of cell data arrays to load from the VTK file")}; + sofa::core::objectmodel::Data> d_pointDataNames{ + initData(&d_pointDataNames, "pointDataNames", + "Names of point data arrays to load from the VTK file")}; + private: bool doLoad() final; @@ -26,8 +29,8 @@ struct SOFA_VTK_API BaseVTKLoader : sofa::core::loader::MeshLoader void loadCellDataArrayByName(vtkSmartPointer dataset, const std::string& arrayName); - /// Unified storage for all dynamically created Data objects std::map> m_cellData; + std::map> m_pointData; protected: diff --git a/src/sofa/vtk/VTKtoSOFA.h b/src/sofa/vtk/VTKtoSOFA.h index 4a3a379..08fed83 100644 --- a/src/sofa/vtk/VTKtoSOFA.h +++ b/src/sofa/vtk/VTKtoSOFA.h @@ -4,10 +4,6 @@ #include #include #include -#include -#include -#include -#include namespace sofavtk { @@ -21,55 +17,4 @@ void SOFA_VTK_API extractCells( vtkSmartPointer dataSet ); -namespace detail -{ - -template -struct CellDataWorker -{ - sofa::type::vector& data; - - template - void operator()(ArrayT* array) const - { - const vtkIdType nbCells = array->GetNumberOfTuples(); - data.resize(nbCells); - - if constexpr (NumComponents == 1) - { - vtkIdType i = 0; - for (const auto v : vtk::DataArrayValueRange<1>(array)) - data[i++] = static_cast(v); - } - else - { - vtkIdType i = 0; - for (const auto tuple : vtk::DataArrayTupleRange(array)) - { - for (int c = 0; c < NumComponents; ++c) - data[i][c] = static_cast(tuple[c]); - ++i; - } - } - } -}; - -} // namespace detail - -template -void extractCellData(vtkSmartPointer dataSet, const char* arrayName, - sofa::type::vector& data) -{ - vtkCellData* cellData = dataSet->GetCellData(); - if (!cellData) return; - - vtkDataArray* array = cellData->GetArray(arrayName); - if (!array) return; - - detail::CellDataWorker worker{data}; - using Dispatcher = vtkArrayDispatch::DispatchByValueType; - if (!Dispatcher::Execute(array, worker)) - worker(array); -} - } From 7fa2fc604a2997cce1571cd6180a8b6870f66b6b Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 5 Mar 2026 17:21:40 +0100 Subject: [PATCH 08/17] enable loading point data arrays --- src/sofa/vtk/BaseVTKLoader.cpp | 64 +++++++++++++++++++++++++++++++++- src/sofa/vtk/BaseVTKLoader.h | 1 + 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index 57464db..bbf03c1 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -170,6 +171,61 @@ void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, dispatch(worker); } +void BaseVTKLoader::loadPointDataArrayByName(vtkSmartPointer dataset, + const std::string& arrayName) +{ + vtkPointData* pointData = dataset->GetPointData(); + if (!pointData) + { + msg_warning() << "No point data available in the dataset"; + return; + } + + vtkDataArray* array = pointData->GetArray(arrayName.c_str()); + if (!array) + { + msg_warning() << "Point data array '" << arrayName << "' not found in VTK file"; + return; + } + + const int numComponents = array->GetNumberOfComponents(); + + using SupportedTypes = vtkTypeList::Create< + float, double, + int, unsigned int, + long, unsigned long, + long long, unsigned long long>; + using Dispatcher = vtkArrayDispatch::DispatchByValueType; + + auto dispatch = [&](auto& worker) { + if (!Dispatcher::Execute(array, worker)) + worker(array); + if (worker.result) + { + msg_info() << "Loaded point data '" << arrayName << "' with " + << array->GetNumberOfTuples() << " entries"; + m_pointData[arrayName] = std::move(worker.result); + } + }; + + if (numComponents == 1) + { + ScalarCellDataWorker worker{*this, arrayName}; + dispatch(worker); + return; + } + + if (numComponents > 9) + { + msg_warning() << "Point data array '" << arrayName + << "' has " << numComponents << " components (max supported: 9)"; + return; + } + + MultiComponentCellDataWorker worker{*this, arrayName, numComponents}; + dispatch(worker); +} + bool BaseVTKLoader::doLoad() { const auto& fileName = d_filename.getFullPath(); @@ -189,6 +245,9 @@ bool BaseVTKLoader::doLoad() for (const auto& arrayName : d_cellDataNames.getValue()) loadCellDataArrayByName(dataSet, arrayName); + for (const auto& arrayName : d_pointDataNames.getValue()) + loadPointDataArrayByName(dataSet, arrayName); + return true; } @@ -199,8 +258,11 @@ void BaseVTKLoader::doClearBuffers() { for (auto& pair : m_cellData) this->removeData(pair.second.get()); - m_cellData.clear(); + + for (auto& pair : m_pointData) + this->removeData(pair.second.get()); + m_pointData.clear(); } diff --git a/src/sofa/vtk/BaseVTKLoader.h b/src/sofa/vtk/BaseVTKLoader.h index fe675c5..09e9315 100644 --- a/src/sofa/vtk/BaseVTKLoader.h +++ b/src/sofa/vtk/BaseVTKLoader.h @@ -28,6 +28,7 @@ struct SOFA_VTK_API BaseVTKLoader : sofa::core::loader::MeshLoader void doClearBuffers() final; void loadCellDataArrayByName(vtkSmartPointer dataset, const std::string& arrayName); + void loadPointDataArrayByName(vtkSmartPointer dataset, const std::string& arrayName); std::map> m_cellData; std::map> m_pointData; From 2852525b6f6de8e9407dfc40181718f781d981ba Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 5 Mar 2026 17:26:08 +0100 Subject: [PATCH 09/17] refactor --- src/sofa/vtk/BaseVTKLoader.cpp | 112 ++++++++------------------------- src/sofa/vtk/BaseVTKLoader.h | 5 +- 2 files changed, 30 insertions(+), 87 deletions(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index bbf03c1..be4e60e 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -12,7 +12,7 @@ namespace { template -vtkSmartPointer getDataSet(std::string fileName) +vtkSmartPointer getDataSet(const std::string& fileName) { vtkNew reader; reader->SetFileName(fileName.c_str()); @@ -30,7 +30,7 @@ template<> struct CanonicalLong { using type = std::con template using CanonicalLong_t = typename CanonicalLong::type; -struct ScalarCellDataWorker +struct ScalarDataWorker { sofavtk::BaseVTKLoader& loader; const std::string& arrayName; @@ -43,7 +43,7 @@ struct ScalarCellDataWorker auto dataPtr = std::make_unique>>(); dataPtr->setName(arrayName); - dataPtr->setHelp("Cell data loaded from VTK file"); + dataPtr->setHelp("Data array loaded from VTK file"); { auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); @@ -59,7 +59,7 @@ struct ScalarCellDataWorker } }; -struct MultiComponentCellDataWorker +struct MultiComponentDataWorker { sofavtk::BaseVTKLoader& loader; const std::string& arrayName; @@ -88,7 +88,7 @@ struct MultiComponentCellDataWorker { auto dataPtr = std::make_unique>>>(); dataPtr->setName(arrayName); - dataPtr->setHelp("Cell data loaded from VTK file"); + dataPtr->setHelp("Data array loaded from VTK file"); { auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); @@ -113,20 +113,13 @@ struct MultiComponentCellDataWorker namespace sofavtk { -void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, - const std::string& arrayName) +void BaseVTKLoader::loadDataArrayByName(vtkFieldData* fieldData, const std::string& arrayName, + std::map>& storage) { - vtkCellData* cellData = dataset->GetCellData(); - if (!cellData) - { - msg_warning() << "No cell data available in the dataset"; - return; - } - - vtkDataArray* array = cellData->GetArray(arrayName.c_str()); + vtkDataArray* array = fieldData->GetArray(arrayName.c_str()); if (!array) { - msg_warning() << "Cell data array '" << arrayName << "' not found in VTK file"; + msg_warning() << fieldData->GetClassName() << " array '" << arrayName << "' not found in VTK file"; return; } @@ -147,82 +140,27 @@ void BaseVTKLoader::loadCellDataArrayByName(vtkSmartPointer dataset, worker(array); if (worker.result) { - msg_info() << "Loaded cell data '" << arrayName << "' with " - << array->GetNumberOfTuples() << " entries"; - m_cellData[arrayName] = std::move(worker.result); - } - }; - - if (numComponents == 1) - { - ScalarCellDataWorker worker{*this, arrayName}; - dispatch(worker); - return; - } - - if (numComponents > 9) - { - msg_warning() << "Cell data array '" << arrayName - << "' has " << numComponents << " components (max supported: 9)"; - return; - } - - MultiComponentCellDataWorker worker{*this, arrayName, numComponents}; - dispatch(worker); -} - -void BaseVTKLoader::loadPointDataArrayByName(vtkSmartPointer dataset, - const std::string& arrayName) -{ - vtkPointData* pointData = dataset->GetPointData(); - if (!pointData) - { - msg_warning() << "No point data available in the dataset"; - return; - } - - vtkDataArray* array = pointData->GetArray(arrayName.c_str()); - if (!array) - { - msg_warning() << "Point data array '" << arrayName << "' not found in VTK file"; - return; - } - - const int numComponents = array->GetNumberOfComponents(); - - using SupportedTypes = vtkTypeList::Create< - float, double, - int, unsigned int, - long, unsigned long, - long long, unsigned long long>; - using Dispatcher = vtkArrayDispatch::DispatchByValueType; - - auto dispatch = [&](auto& worker) { - if (!Dispatcher::Execute(array, worker)) - worker(array); - if (worker.result) - { - msg_info() << "Loaded point data '" << arrayName << "' with " - << array->GetNumberOfTuples() << " entries"; - m_pointData[arrayName] = std::move(worker.result); + msg_info() << "Loaded " << fieldData->GetClassName() << " '" << arrayName + << "' with " << array->GetNumberOfTuples() << " entries"; + storage[arrayName] = std::move(worker.result); } }; if (numComponents == 1) { - ScalarCellDataWorker worker{*this, arrayName}; + ScalarDataWorker worker{*this, arrayName}; dispatch(worker); return; } if (numComponents > 9) { - msg_warning() << "Point data array '" << arrayName + msg_warning() << fieldData->GetClassName() << " array '" << arrayName << "' has " << numComponents << " components (max supported: 9)"; return; } - MultiComponentCellDataWorker worker{*this, arrayName, numComponents}; + MultiComponentDataWorker worker{*this, arrayName, numComponents}; dispatch(worker); } @@ -242,11 +180,14 @@ bool BaseVTKLoader::doLoad() sofavtk::extractCells(*this, dataSet); + auto* cellData = dataSet->GetCellData(); + auto* pointData = dataSet->GetPointData(); + for (const auto& arrayName : d_cellDataNames.getValue()) - loadCellDataArrayByName(dataSet, arrayName); + loadDataArrayByName(cellData, arrayName, m_cellData); for (const auto& arrayName : d_pointDataNames.getValue()) - loadPointDataArrayByName(dataSet, arrayName); + loadDataArrayByName(pointData, arrayName, m_pointData); return true; } @@ -256,13 +197,14 @@ bool BaseVTKLoader::doLoad() void BaseVTKLoader::doClearBuffers() { - for (auto& pair : m_cellData) - this->removeData(pair.second.get()); - m_cellData.clear(); + auto clearMap = [&](auto& map) { + for (const auto& [name, data] : map) + this->removeData(data.get()); + map.clear(); + }; - for (auto& pair : m_pointData) - this->removeData(pair.second.get()); - m_pointData.clear(); + clearMap(m_cellData); + clearMap(m_pointData); } diff --git a/src/sofa/vtk/BaseVTKLoader.h b/src/sofa/vtk/BaseVTKLoader.h index 09e9315..17fc029 100644 --- a/src/sofa/vtk/BaseVTKLoader.h +++ b/src/sofa/vtk/BaseVTKLoader.h @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -27,8 +28,8 @@ struct SOFA_VTK_API BaseVTKLoader : sofa::core::loader::MeshLoader bool doLoad() final; void doClearBuffers() final; - void loadCellDataArrayByName(vtkSmartPointer dataset, const std::string& arrayName); - void loadPointDataArrayByName(vtkSmartPointer dataset, const std::string& arrayName); + void loadDataArrayByName(vtkFieldData* fieldData, const std::string& arrayName, + std::map>& storage); std::map> m_cellData; std::map> m_pointData; From 6a8ad459ba4e6fd216fd4d0fccac09ff470d9b51 Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 5 Mar 2026 23:09:33 +0100 Subject: [PATCH 10/17] add signed (Int8) and unsigned (UInt8) char to dispatch SupportedTypes --- src/sofa/vtk/BaseVTKLoader.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index be4e60e..99adc31 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -130,6 +130,7 @@ void BaseVTKLoader::loadDataArrayByName(vtkFieldData* fieldData, const std::stri // CanonicalLong_t to int/long long based on their size on the current platform. using SupportedTypes = vtkTypeList::Create< float, double, + signed char, unsigned char, int, unsigned int, long, unsigned long, long long, unsigned long long>; From 90a62793bcc932e4204499d3e646b76f126c32b7 Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Thu, 5 Mar 2026 23:46:47 +0100 Subject: [PATCH 11/17] add unit tests for cell and point data array loading --- tests/CMakeLists.txt | 12 +- tests/TestCellPointData.cpp | 232 ++++++++++++++++++++++++++++++++++++ tests/TestFixtures.h | 127 ++++++++++++++++++++ 3 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 tests/TestCellPointData.cpp create mode 100644 tests/TestFixtures.h diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7696cb6..b4a7a2b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,13 +1,23 @@ cmake_minimum_required(VERSION 3.12) project(SOFAVTK_test VERSION 1.0) + find_package(Sofa.Testing REQUIRED) +find_package(VTK COMPONENTS CommonCore CommonDataModel IOXML REQUIRED) set(SOURCE_FILES test.cpp + TestCellPointData.cpp ) add_executable(${PROJECT_NAME} ${SOURCE_FILES}) -target_link_libraries(${PROJECT_NAME} Sofa.Testing SOFA.VTK) +target_link_libraries(${PROJECT_NAME} Sofa.Testing SOFA.VTK ${VTK_LIBRARIES}) + +# VTK reader/writer implementations are loaded via factory objects that need +# to be registered before use; autoinit handles this for the test binary. +vtk_module_autoinit( + TARGETS ${PROJECT_NAME} + MODULES ${VTK_LIBRARIES} +) add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) diff --git a/tests/TestCellPointData.cpp b/tests/TestCellPointData.cpp new file mode 100644 index 0000000..1aac723 --- /dev/null +++ b/tests/TestCellPointData.cpp @@ -0,0 +1,232 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "TestFixtures.h" + +namespace +{ + +// Returns a pointer to the inner vector of a typed Data field on obj, or +// nullptr if the field is missing or has a different type. +template +const sofa::type::vector* getVec(sofa::core::objectmodel::Base* obj, + const std::string& name) +{ + auto* base = obj->findData(name); + if (!base) + return nullptr; + auto* typed = + dynamic_cast>*>(base); + return typed ? &typed->getValue() : nullptr; +} + +} // namespace + +class CellPointDataTest : public sofa::testing::BaseSimulationTest +{ +protected: + std::string m_fixturePath; + std::unique_ptr m_scene; + + void doSetUp() override + { + m_fixturePath = + (std::filesystem::temp_directory_path() / "sofa_vtk_test_fixture.vtu") + .string(); + writeTestFixture(m_fixturePath); + } + + sofavtk::UnstructuredGridVTKLoader* makeLoader( + const std::vector& cellNames = {}, + const std::vector& pointNames = {}) + { + m_scene = std::make_unique(); + auto loader = + sofa::core::objectmodel::New(); + m_scene->root->addObject(loader); + + loader->d_filename.setValue(m_fixturePath); + loader->d_cellDataNames.setValue( + sofa::type::vector(cellNames.begin(), cellNames.end())); + loader->d_pointDataNames.setValue( + sofa::type::vector(pointNames.begin(), pointNames.end())); + + m_scene->initScene(); + return loader.get(); + } +}; + +TEST_F(CellPointDataTest, CellData_ScalarFloat) +{ + auto* loader = makeLoader({"pressure"}); + const auto* v = getVec(loader, "pressure"); + ASSERT_NE(v, nullptr); + ASSERT_EQ(v->size(), 4u); + EXPECT_FLOAT_EQ((*v)[0], 1.0f); + EXPECT_FLOAT_EQ((*v)[1], 2.0f); + EXPECT_FLOAT_EQ((*v)[2], 3.0f); + EXPECT_FLOAT_EQ((*v)[3], 4.0f); +} + +TEST_F(CellPointDataTest, CellData_ScalarInt) +{ + auto* loader = makeLoader({"material_id"}); + const auto* v = getVec(loader, "material_id"); + ASSERT_NE(v, nullptr); + ASSERT_EQ(v->size(), 4u); + EXPECT_EQ((*v)[0], 10); + EXPECT_EQ((*v)[1], 20); + EXPECT_EQ((*v)[2], 30); + EXPECT_EQ((*v)[3], 40); +} + +TEST_F(CellPointDataTest, CellData_Vec3Double) +{ + using Vec3d = sofa::type::Vec<3, double>; + auto* loader = makeLoader({"fiber_direction"}); + const auto* v = getVec(loader, "fiber_direction"); + ASSERT_NE(v, nullptr); + ASSERT_EQ(v->size(), 4u); + + EXPECT_DOUBLE_EQ((*v)[0][0], 1.0); + EXPECT_DOUBLE_EQ((*v)[0][1], 0.0); + EXPECT_DOUBLE_EQ((*v)[0][2], 0.0); + + EXPECT_DOUBLE_EQ((*v)[1][0], 0.0); + EXPECT_DOUBLE_EQ((*v)[1][1], 1.0); + EXPECT_DOUBLE_EQ((*v)[1][2], 0.0); + + EXPECT_DOUBLE_EQ((*v)[2][0], 0.0); + EXPECT_DOUBLE_EQ((*v)[2][1], 0.0); + EXPECT_DOUBLE_EQ((*v)[2][2], 1.0); + + EXPECT_NEAR((*v)[3][0], 0.577, 1e-6); + EXPECT_NEAR((*v)[3][1], 0.577, 1e-6); + EXPECT_NEAR((*v)[3][2], 0.577, 1e-6); +} + +TEST_F(CellPointDataTest, CellData_Int8) +{ + auto* loader = makeLoader({"int8_data"}); + const auto* v = getVec(loader, "int8_data"); + ASSERT_NE(v, nullptr); + ASSERT_EQ(v->size(), 4u); + EXPECT_EQ(static_cast((*v)[0]), 1); + EXPECT_EQ(static_cast((*v)[1]), 2); + EXPECT_EQ(static_cast((*v)[2]), 3); + EXPECT_EQ(static_cast((*v)[3]), 4); +} + +TEST_F(CellPointDataTest, CellData_Int32) +{ + auto* loader = makeLoader({"int32_data"}); + const auto* v = getVec(loader, "int32_data"); + ASSERT_NE(v, nullptr); + ASSERT_EQ(v->size(), 4u); + EXPECT_EQ((*v)[0], 100); + EXPECT_EQ((*v)[1], 200); + EXPECT_EQ((*v)[2], 300); + EXPECT_EQ((*v)[3], 400); +} + +TEST_F(CellPointDataTest, CellData_Int64) +{ + auto* loader = makeLoader({"int64_data"}); + const auto* v = getVec(loader, "int64_data"); + ASSERT_NE(v, nullptr); + ASSERT_EQ(v->size(), 4u); + EXPECT_EQ((*v)[0], 10000LL); + EXPECT_EQ((*v)[1], 20000LL); + EXPECT_EQ((*v)[2], 30000LL); + EXPECT_EQ((*v)[3], 40000LL); +} + +TEST_F(CellPointDataTest, PointData_ScalarDouble) +{ + auto* loader = makeLoader({}, {"temperature"}); + const auto* v = getVec(loader, "temperature"); + ASSERT_NE(v, nullptr); + ASSERT_EQ(v->size(), 5u); + EXPECT_DOUBLE_EQ((*v)[0], 0.1); + EXPECT_DOUBLE_EQ((*v)[1], 0.2); + EXPECT_DOUBLE_EQ((*v)[2], 0.3); + EXPECT_DOUBLE_EQ((*v)[3], 0.4); + EXPECT_DOUBLE_EQ((*v)[4], 0.5); +} + +TEST_F(CellPointDataTest, PointData_Vec3Double) +{ + using Vec3d = sofa::type::Vec<3, double>; + auto* loader = makeLoader({}, {"velocity"}); + const auto* v = getVec(loader, "velocity"); + ASSERT_NE(v, nullptr); + ASSERT_EQ(v->size(), 5u); + + EXPECT_DOUBLE_EQ((*v)[0][0], 1.0); + EXPECT_DOUBLE_EQ((*v)[0][1], 0.0); + EXPECT_DOUBLE_EQ((*v)[0][2], 0.0); + + EXPECT_DOUBLE_EQ((*v)[3][0], 2.0); + EXPECT_DOUBLE_EQ((*v)[3][1], 0.0); + EXPECT_DOUBLE_EQ((*v)[3][2], 0.0); + + EXPECT_DOUBLE_EQ((*v)[4][0], 0.0); + EXPECT_DOUBLE_EQ((*v)[4][1], 2.0); + EXPECT_DOUBLE_EQ((*v)[4][2], 0.0); +} + +TEST_F(CellPointDataTest, MissingArrayName) +{ + auto* loader = makeLoader({"nonexistent"}); + EXPECT_EQ(loader->findData("nonexistent"), nullptr); +} + +TEST_F(CellPointDataTest, TooManyComponents) +{ + auto* loader = makeLoader({"too_many"}); + EXPECT_EQ(loader->findData("too_many"), nullptr); +} + +TEST_F(CellPointDataTest, MultipleArraysAtOnce) +{ + auto* loader = makeLoader({"pressure", "material_id"}, {"temperature"}); + + const auto* pressure = getVec(loader, "pressure"); + ASSERT_NE(pressure, nullptr); + EXPECT_EQ(pressure->size(), 4u); + + const auto* matId = getVec(loader, "material_id"); + ASSERT_NE(matId, nullptr); + EXPECT_EQ(matId->size(), 4u); + + const auto* temp = getVec(loader, "temperature"); + ASSERT_NE(temp, nullptr); + EXPECT_EQ(temp->size(), 5u); +} + +TEST_F(CellPointDataTest, Reload) +{ + auto* loader = makeLoader({"pressure"}); + ASSERT_NE(getVec(loader, "pressure"), nullptr); + EXPECT_EQ(loader->findData("material_id"), nullptr); + + loader->d_cellDataNames.setValue({"material_id"}); + loader->load(); + + EXPECT_EQ(loader->findData("pressure"), nullptr); + const auto* matId = getVec(loader, "material_id"); + ASSERT_NE(matId, nullptr); + EXPECT_EQ(matId->size(), 4u); +} + +// temperature is point data; requesting it as cell data must not load it +TEST_F(CellPointDataTest, CellVsPoint_Separation) +{ + auto* loader = makeLoader({"temperature"}, {}); + EXPECT_EQ(loader->findData("temperature"), nullptr); +} diff --git a/tests/TestFixtures.h b/tests/TestFixtures.h new file mode 100644 index 0000000..a633698 --- /dev/null +++ b/tests/TestFixtures.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Writes a 5-point / 4-tet unstructured grid (.vtu) with known cell and point +// data arrays so tests can load it and assert exact values. +// +// Geometry: +// Points : (0,0,0) (1,0,0) (0,1,0) (0,0,1) (1,1,1) +// Tets : {0,1,2,4} {0,1,3,4} {0,2,3,4} {1,2,3,4} +// +// Cell data (4 entries per array): +// "pressure" float scalar {1, 2, 3, 4} +// "material_id" int32 scalar {10, 20, 30, 40} +// "fiber_direction" double vec3 {(1,0,0),(0,1,0),(0,0,1),(0.577,...)} +// "int8_data" int8 scalar {1, 2, 3, 4} +// "int32_data" int32 scalar {100, 200, 300, 400} +// "int64_data" int64 scalar {10000, 20000, 30000, 40000} +// "too_many" double 10-comp (all zeros) +// +// Point data (5 entries per array): +// "temperature" double scalar {0.1, 0.2, 0.3, 0.4, 0.5} +// "velocity" double vec3 {(1,0,0),(0,1,0),(0,0,1),(2,0,0),(0,2,0)} + +inline void writeTestFixture(const std::string& path) +{ + vtkNew pts; + pts->InsertNextPoint(0, 0, 0); + pts->InsertNextPoint(1, 0, 0); + pts->InsertNextPoint(0, 1, 0); + pts->InsertNextPoint(0, 0, 1); + pts->InsertNextPoint(1, 1, 1); + + vtkNew grid; + grid->SetPoints(pts); + + vtkIdType t0[4] = {0, 1, 2, 4}; + vtkIdType t1[4] = {0, 1, 3, 4}; + vtkIdType t2[4] = {0, 2, 3, 4}; + vtkIdType t3[4] = {1, 2, 3, 4}; + grid->InsertNextCell(VTK_TETRA, 4, t0); + grid->InsertNextCell(VTK_TETRA, 4, t1); + grid->InsertNextCell(VTK_TETRA, 4, t2); + grid->InsertNextCell(VTK_TETRA, 4, t3); + + // ---- Cell data ---- + + vtkNew pressure; + pressure->SetName("pressure"); + for (float v : {1.0f, 2.0f, 3.0f, 4.0f}) + pressure->InsertNextValue(v); + grid->GetCellData()->AddArray(pressure); + + vtkNew materialId; + materialId->SetName("material_id"); + for (int v : {10, 20, 30, 40}) + materialId->InsertNextValue(v); + grid->GetCellData()->AddArray(materialId); + + vtkNew fiberDir; + fiberDir->SetName("fiber_direction"); + fiberDir->SetNumberOfComponents(3); + double fd[][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0.577, 0.577, 0.577}}; + for (auto& t : fd) + fiberDir->InsertNextTuple(t); + grid->GetCellData()->AddArray(fiberDir); + + vtkNew int8Data; + int8Data->SetName("int8_data"); + for (int v : {1, 2, 3, 4}) + int8Data->InsertNextValue(static_cast(v)); + grid->GetCellData()->AddArray(int8Data); + + vtkNew int32Data; + int32Data->SetName("int32_data"); + for (int v : {100, 200, 300, 400}) + int32Data->InsertNextValue(v); + grid->GetCellData()->AddArray(int32Data); + + vtkNew int64Data; + int64Data->SetName("int64_data"); + for (long long v : {10000LL, 20000LL, 30000LL, 40000LL}) + int64Data->InsertNextValue(v); + grid->GetCellData()->AddArray(int64Data); + + // 10-component array to exercise the >9-components warning path + vtkNew tooMany; + tooMany->SetName("too_many"); + tooMany->SetNumberOfComponents(10); + double z[10] = {}; + for (int i = 0; i < 4; ++i) + tooMany->InsertNextTuple(z); + grid->GetCellData()->AddArray(tooMany); + + // ---- Point data ---- + + vtkNew temperature; + temperature->SetName("temperature"); + for (double v : {0.1, 0.2, 0.3, 0.4, 0.5}) + temperature->InsertNextValue(v); + grid->GetPointData()->AddArray(temperature); + + vtkNew velocity; + velocity->SetName("velocity"); + velocity->SetNumberOfComponents(3); + double vt[][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {2, 0, 0}, {0, 2, 0}}; + for (auto& t : vt) + velocity->InsertNextTuple(t); + grid->GetPointData()->AddArray(velocity); + + vtkNew writer; + writer->SetFileName(path.c_str()); + writer->SetInputData(grid); + writer->Write(); +} From c41a9416ac79b513171eeac0c1539bd6cce57278 Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Fri, 6 Mar 2026 00:08:47 +0100 Subject: [PATCH 12/17] set PATH for test binary on Windows so VTK and SOFA DLLs are found --- tests/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b4a7a2b..b816ba2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,3 +21,9 @@ vtk_module_autoinit( ) add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) + +# On Windows the test binary needs VTK and SOFA DLLs on PATH at runtime. +if(WIN32) + set_tests_properties(${PROJECT_NAME} PROPERTIES + ENVIRONMENT "PATH=$\;$\;$ENV{PATH}") +endif() From 05ea06d33ba5ab51e7f3306ed78b6aabe762376c Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Fri, 6 Mar 2026 11:58:50 +0100 Subject: [PATCH 13/17] add a unit test with a generated VTK test file --- tests/CMakeLists.txt | 16 +--- tests/TestCellPointData.cpp | 139 +++++++++++++++++----------------- tests/TestFixtures.h | 141 ++++++----------------------------- tests/fixtures/test_mesh.vtu | 31 ++++++++ 4 files changed, 127 insertions(+), 200 deletions(-) create mode 100644 tests/fixtures/test_mesh.vtu diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b816ba2..13cd92a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,7 +2,6 @@ cmake_minimum_required(VERSION 3.12) project(SOFAVTK_test VERSION 1.0) find_package(Sofa.Testing REQUIRED) -find_package(VTK COMPONENTS CommonCore CommonDataModel IOXML REQUIRED) set(SOURCE_FILES test.cpp @@ -11,19 +10,6 @@ set(SOURCE_FILES add_executable(${PROJECT_NAME} ${SOURCE_FILES}) -target_link_libraries(${PROJECT_NAME} Sofa.Testing SOFA.VTK ${VTK_LIBRARIES}) - -# VTK reader/writer implementations are loaded via factory objects that need -# to be registered before use; autoinit handles this for the test binary. -vtk_module_autoinit( - TARGETS ${PROJECT_NAME} - MODULES ${VTK_LIBRARIES} -) +target_link_libraries(${PROJECT_NAME} Sofa.Testing SOFA.VTK) add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) - -# On Windows the test binary needs VTK and SOFA DLLs on PATH at runtime. -if(WIN32) - set_tests_properties(${PROJECT_NAME} PROPERTIES - ENVIRONMENT "PATH=$\;$\;$ENV{PATH}") -endif() diff --git a/tests/TestCellPointData.cpp b/tests/TestCellPointData.cpp index 1aac723..44a582f 100644 --- a/tests/TestCellPointData.cpp +++ b/tests/TestCellPointData.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include "TestFixtures.h" @@ -30,17 +29,8 @@ const sofa::type::vector* getVec(sofa::core::objectmodel::Base* obj, class CellPointDataTest : public sofa::testing::BaseSimulationTest { protected: - std::string m_fixturePath; std::unique_ptr m_scene; - void doSetUp() override - { - m_fixturePath = - (std::filesystem::temp_directory_path() / "sofa_vtk_test_fixture.vtu") - .string(); - writeTestFixture(m_fixturePath); - } - sofavtk::UnstructuredGridVTKLoader* makeLoader( const std::vector& cellNames = {}, const std::vector& pointNames = {}) @@ -50,7 +40,7 @@ class CellPointDataTest : public sofa::testing::BaseSimulationTest sofa::core::objectmodel::New(); m_scene->root->addObject(loader); - loader->d_filename.setValue(m_fixturePath); + loader->d_filename.setValue(fixtureVtuPath()); loader->d_cellDataNames.setValue( sofa::type::vector(cellNames.begin(), cellNames.end())); loader->d_pointDataNames.setValue( @@ -61,53 +51,64 @@ class CellPointDataTest : public sofa::testing::BaseSimulationTest } }; +// Fixture has 10 cells (2 quads + 2 tris + 2 hexas + 4 tets) +// and 22 points. + +// pressure = centroid z: 0.0 for surface cells, 1.5 for hexas, 2.25 for tets TEST_F(CellPointDataTest, CellData_ScalarFloat) { auto* loader = makeLoader({"pressure"}); const auto* v = getVec(loader, "pressure"); ASSERT_NE(v, nullptr); - ASSERT_EQ(v->size(), 4u); - EXPECT_FLOAT_EQ((*v)[0], 1.0f); - EXPECT_FLOAT_EQ((*v)[1], 2.0f); - EXPECT_FLOAT_EQ((*v)[2], 3.0f); - EXPECT_FLOAT_EQ((*v)[3], 4.0f); + ASSERT_EQ(v->size(), 10u); + // Zone A: centroid z = 0.0 + EXPECT_FLOAT_EQ((*v)[0], 0.0f); + EXPECT_FLOAT_EQ((*v)[3], 0.0f); + // Zone B: centroid z = 1.5 + EXPECT_FLOAT_EQ((*v)[4], 1.5f); + EXPECT_FLOAT_EQ((*v)[5], 1.5f); + // Zone C: centroid z = 2.25 + EXPECT_FLOAT_EQ((*v)[6], 2.25f); + EXPECT_FLOAT_EQ((*v)[9], 2.25f); } +// material_id = zone id: 1=surface, 2=hexa, 3=tet TEST_F(CellPointDataTest, CellData_ScalarInt) { auto* loader = makeLoader({"material_id"}); const auto* v = getVec(loader, "material_id"); ASSERT_NE(v, nullptr); - ASSERT_EQ(v->size(), 4u); - EXPECT_EQ((*v)[0], 10); - EXPECT_EQ((*v)[1], 20); - EXPECT_EQ((*v)[2], 30); - EXPECT_EQ((*v)[3], 40); + ASSERT_EQ(v->size(), 10u); + EXPECT_EQ((*v)[0], 1); // quad + EXPECT_EQ((*v)[2], 1); // triangle + EXPECT_EQ((*v)[4], 2); // hexa + EXPECT_EQ((*v)[6], 3); // tet } +// fiber_direction = normalised centroid vector TEST_F(CellPointDataTest, CellData_Vec3Double) { using Vec3d = sofa::type::Vec<3, double>; auto* loader = makeLoader({"fiber_direction"}); const auto* v = getVec(loader, "fiber_direction"); ASSERT_NE(v, nullptr); - ASSERT_EQ(v->size(), 4u); + ASSERT_EQ(v->size(), 10u); - EXPECT_DOUBLE_EQ((*v)[0][0], 1.0); - EXPECT_DOUBLE_EQ((*v)[0][1], 0.0); - EXPECT_DOUBLE_EQ((*v)[0][2], 0.0); - - EXPECT_DOUBLE_EQ((*v)[1][0], 0.0); - EXPECT_DOUBLE_EQ((*v)[1][1], 1.0); - EXPECT_DOUBLE_EQ((*v)[1][2], 0.0); + // Every entry must be a unit vector + for (std::size_t i = 0; i < v->size(); ++i) + { + const double mag = (*v)[i].norm(); + EXPECT_NEAR(mag, 1.0, 1e-9) << "cell " << i << " fiber_direction is not unit"; + } - EXPECT_DOUBLE_EQ((*v)[2][0], 0.0); - EXPECT_DOUBLE_EQ((*v)[2][1], 0.0); - EXPECT_DOUBLE_EQ((*v)[2][2], 1.0); + // Cell 0 centroid = (0.5, 0.5, 0) → normalised (1/√2, 1/√2, 0) + const double inv_sqrt2 = 1.0 / std::sqrt(2.0); + EXPECT_NEAR((*v)[0][0], inv_sqrt2, 1e-9); + EXPECT_NEAR((*v)[0][1], inv_sqrt2, 1e-9); + EXPECT_NEAR((*v)[0][2], 0.0, 1e-9); - EXPECT_NEAR((*v)[3][0], 0.577, 1e-6); - EXPECT_NEAR((*v)[3][1], 0.577, 1e-6); - EXPECT_NEAR((*v)[3][2], 0.577, 1e-6); + // Zone C tets: centroid z >> x,y so z-component dominates + EXPECT_GT((*v)[6][2], 0.9); } TEST_F(CellPointDataTest, CellData_Int8) @@ -115,11 +116,10 @@ TEST_F(CellPointDataTest, CellData_Int8) auto* loader = makeLoader({"int8_data"}); const auto* v = getVec(loader, "int8_data"); ASSERT_NE(v, nullptr); - ASSERT_EQ(v->size(), 4u); - EXPECT_EQ(static_cast((*v)[0]), 1); - EXPECT_EQ(static_cast((*v)[1]), 2); - EXPECT_EQ(static_cast((*v)[2]), 3); - EXPECT_EQ(static_cast((*v)[3]), 4); + ASSERT_EQ(v->size(), 10u); + // int8_data = cell index + 1 + for (std::size_t i = 0; i < v->size(); ++i) + EXPECT_EQ(static_cast((*v)[i]), static_cast(i + 1)); } TEST_F(CellPointDataTest, CellData_Int32) @@ -127,11 +127,10 @@ TEST_F(CellPointDataTest, CellData_Int32) auto* loader = makeLoader({"int32_data"}); const auto* v = getVec(loader, "int32_data"); ASSERT_NE(v, nullptr); - ASSERT_EQ(v->size(), 4u); - EXPECT_EQ((*v)[0], 100); - EXPECT_EQ((*v)[1], 200); - EXPECT_EQ((*v)[2], 300); - EXPECT_EQ((*v)[3], 400); + ASSERT_EQ(v->size(), 10u); + // int32_data = (cell index + 1) * 100 + for (std::size_t i = 0; i < v->size(); ++i) + EXPECT_EQ((*v)[i], static_cast((i + 1) * 100)); } TEST_F(CellPointDataTest, CellData_Int64) @@ -139,45 +138,49 @@ TEST_F(CellPointDataTest, CellData_Int64) auto* loader = makeLoader({"int64_data"}); const auto* v = getVec(loader, "int64_data"); ASSERT_NE(v, nullptr); - ASSERT_EQ(v->size(), 4u); - EXPECT_EQ((*v)[0], 10000LL); - EXPECT_EQ((*v)[1], 20000LL); - EXPECT_EQ((*v)[2], 30000LL); - EXPECT_EQ((*v)[3], 40000LL); + ASSERT_EQ(v->size(), 10u); + // int64_data = (cell index + 1) * 10000 + for (std::size_t i = 0; i < v->size(); ++i) + EXPECT_EQ((*v)[i], static_cast((i + 1) * 10000)); } +// temperature = x + y + z per point, range 0.0 (P0 at origin) to 5.0 (P19) TEST_F(CellPointDataTest, PointData_ScalarDouble) { auto* loader = makeLoader({}, {"temperature"}); const auto* v = getVec(loader, "temperature"); ASSERT_NE(v, nullptr); - ASSERT_EQ(v->size(), 5u); - EXPECT_DOUBLE_EQ((*v)[0], 0.1); - EXPECT_DOUBLE_EQ((*v)[1], 0.2); - EXPECT_DOUBLE_EQ((*v)[2], 0.3); - EXPECT_DOUBLE_EQ((*v)[3], 0.4); - EXPECT_DOUBLE_EQ((*v)[4], 0.5); + ASSERT_EQ(v->size(), 22u); + EXPECT_DOUBLE_EQ((*v)[0], 0.0); // P0 (0,0,0) + EXPECT_DOUBLE_EQ((*v)[1], 1.0); // P1 (1,0,0) + EXPECT_DOUBLE_EQ((*v)[5], 3.0); // P5 (2,1,0) + EXPECT_DOUBLE_EQ((*v)[19], 5.0); // P19 (2,1,2) + EXPECT_DOUBLE_EQ((*v)[21], 4.0); // P21 (0.5,0.5,3) } +// velocity = (x, y, z) per point — direction and magnitude encode position TEST_F(CellPointDataTest, PointData_Vec3Double) { using Vec3d = sofa::type::Vec<3, double>; auto* loader = makeLoader({}, {"velocity"}); const auto* v = getVec(loader, "velocity"); ASSERT_NE(v, nullptr); - ASSERT_EQ(v->size(), 5u); + ASSERT_EQ(v->size(), 22u); - EXPECT_DOUBLE_EQ((*v)[0][0], 1.0); + // P0 (0,0,0) → zero velocity + EXPECT_DOUBLE_EQ((*v)[0][0], 0.0); EXPECT_DOUBLE_EQ((*v)[0][1], 0.0); EXPECT_DOUBLE_EQ((*v)[0][2], 0.0); - EXPECT_DOUBLE_EQ((*v)[3][0], 2.0); - EXPECT_DOUBLE_EQ((*v)[3][1], 0.0); - EXPECT_DOUBLE_EQ((*v)[3][2], 0.0); + // P5 (2,1,0) → velocity = coordinates + EXPECT_DOUBLE_EQ((*v)[5][0], 2.0); + EXPECT_DOUBLE_EQ((*v)[5][1], 1.0); + EXPECT_DOUBLE_EQ((*v)[5][2], 0.0); - EXPECT_DOUBLE_EQ((*v)[4][0], 0.0); - EXPECT_DOUBLE_EQ((*v)[4][1], 2.0); - EXPECT_DOUBLE_EQ((*v)[4][2], 0.0); + // P21 (0.5,0.5,3) → velocity = coordinates + EXPECT_DOUBLE_EQ((*v)[21][0], 0.5); + EXPECT_DOUBLE_EQ((*v)[21][1], 0.5); + EXPECT_DOUBLE_EQ((*v)[21][2], 3.0); } TEST_F(CellPointDataTest, MissingArrayName) @@ -198,15 +201,15 @@ TEST_F(CellPointDataTest, MultipleArraysAtOnce) const auto* pressure = getVec(loader, "pressure"); ASSERT_NE(pressure, nullptr); - EXPECT_EQ(pressure->size(), 4u); + EXPECT_EQ(pressure->size(), 10u); const auto* matId = getVec(loader, "material_id"); ASSERT_NE(matId, nullptr); - EXPECT_EQ(matId->size(), 4u); + EXPECT_EQ(matId->size(), 10u); const auto* temp = getVec(loader, "temperature"); ASSERT_NE(temp, nullptr); - EXPECT_EQ(temp->size(), 5u); + EXPECT_EQ(temp->size(), 22u); } TEST_F(CellPointDataTest, Reload) @@ -221,7 +224,7 @@ TEST_F(CellPointDataTest, Reload) EXPECT_EQ(loader->findData("pressure"), nullptr); const auto* matId = getVec(loader, "material_id"); ASSERT_NE(matId, nullptr); - EXPECT_EQ(matId->size(), 4u); + EXPECT_EQ(matId->size(), 10u); } // temperature is point data; requesting it as cell data must not load it diff --git a/tests/TestFixtures.h b/tests/TestFixtures.h index a633698..b3637bb 100644 --- a/tests/TestFixtures.h +++ b/tests/TestFixtures.h @@ -1,127 +1,34 @@ #pragma once #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include -// Writes a 5-point / 4-tet unstructured grid (.vtu) with known cell and point -// data arrays so tests can load it and assert exact values. +// Returns the absolute path to the pre-generated test fixture. // -// Geometry: -// Points : (0,0,0) (1,0,0) (0,1,0) (0,0,1) (1,1,1) -// Tets : {0,1,2,4} {0,1,3,4} {0,2,3,4} {1,2,3,4} +// Geometry: 22 points, 10 cells — three spatial zones: +// Zone A z=0 2 quads (cells 0-1) + 2 triangles (cells 2-3) +// Zone B z=1→2 2 hexahedra (cells 4-5) +// Zone C z=2→3 4 tetrahedra (cells 6-9) // -// Cell data (4 entries per array): -// "pressure" float scalar {1, 2, 3, 4} -// "material_id" int32 scalar {10, 20, 30, 40} -// "fiber_direction" double vec3 {(1,0,0),(0,1,0),(0,0,1),(0.577,...)} -// "int8_data" int8 scalar {1, 2, 3, 4} -// "int32_data" int32 scalar {100, 200, 300, 400} -// "int64_data" int64 scalar {10000, 20000, 30000, 40000} -// "too_many" double 10-comp (all zeros) +// All data values are functions of coordinates, verifiable by selecting +// an element in ParaView and computing the expected value directly: // -// Point data (5 entries per array): -// "temperature" double scalar {0.1, 0.2, 0.3, 0.4, 0.5} -// "velocity" double vec3 {(1,0,0),(0,1,0),(0,0,1),(2,0,0),(0,2,0)} +// Cell data (10 entries): +// pressure float32 centroid z [0.0, 0.0, 0.0, 0.0, 1.5, 1.5, 2.25, 2.25, 2.25, 2.25] +// material_id int32 zone id [1, 1, 1, 1, 2, 2, 3, 3, 3, 3] +// fiber_direction float64 normalised centroid vector +// int8_data int8 cell index [1 .. 10] +// int32_data int32 index * 100 [100 .. 1000] +// int64_data int64 index * 10000 [10000 .. 100000] +// too_many float64 10 components, all 0 (>9-component guard) +// +// Point data (22 entries): +// temperature float64 x + y + z [0.0 .. 5.0] +// velocity float64 (x, y, z) radial from origin -inline void writeTestFixture(const std::string& path) +inline std::string fixtureVtuPath() { - vtkNew pts; - pts->InsertNextPoint(0, 0, 0); - pts->InsertNextPoint(1, 0, 0); - pts->InsertNextPoint(0, 1, 0); - pts->InsertNextPoint(0, 0, 1); - pts->InsertNextPoint(1, 1, 1); - - vtkNew grid; - grid->SetPoints(pts); - - vtkIdType t0[4] = {0, 1, 2, 4}; - vtkIdType t1[4] = {0, 1, 3, 4}; - vtkIdType t2[4] = {0, 2, 3, 4}; - vtkIdType t3[4] = {1, 2, 3, 4}; - grid->InsertNextCell(VTK_TETRA, 4, t0); - grid->InsertNextCell(VTK_TETRA, 4, t1); - grid->InsertNextCell(VTK_TETRA, 4, t2); - grid->InsertNextCell(VTK_TETRA, 4, t3); - - // ---- Cell data ---- - - vtkNew pressure; - pressure->SetName("pressure"); - for (float v : {1.0f, 2.0f, 3.0f, 4.0f}) - pressure->InsertNextValue(v); - grid->GetCellData()->AddArray(pressure); - - vtkNew materialId; - materialId->SetName("material_id"); - for (int v : {10, 20, 30, 40}) - materialId->InsertNextValue(v); - grid->GetCellData()->AddArray(materialId); - - vtkNew fiberDir; - fiberDir->SetName("fiber_direction"); - fiberDir->SetNumberOfComponents(3); - double fd[][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0.577, 0.577, 0.577}}; - for (auto& t : fd) - fiberDir->InsertNextTuple(t); - grid->GetCellData()->AddArray(fiberDir); - - vtkNew int8Data; - int8Data->SetName("int8_data"); - for (int v : {1, 2, 3, 4}) - int8Data->InsertNextValue(static_cast(v)); - grid->GetCellData()->AddArray(int8Data); - - vtkNew int32Data; - int32Data->SetName("int32_data"); - for (int v : {100, 200, 300, 400}) - int32Data->InsertNextValue(v); - grid->GetCellData()->AddArray(int32Data); - - vtkNew int64Data; - int64Data->SetName("int64_data"); - for (long long v : {10000LL, 20000LL, 30000LL, 40000LL}) - int64Data->InsertNextValue(v); - grid->GetCellData()->AddArray(int64Data); - - // 10-component array to exercise the >9-components warning path - vtkNew tooMany; - tooMany->SetName("too_many"); - tooMany->SetNumberOfComponents(10); - double z[10] = {}; - for (int i = 0; i < 4; ++i) - tooMany->InsertNextTuple(z); - grid->GetCellData()->AddArray(tooMany); - - // ---- Point data ---- - - vtkNew temperature; - temperature->SetName("temperature"); - for (double v : {0.1, 0.2, 0.3, 0.4, 0.5}) - temperature->InsertNextValue(v); - grid->GetPointData()->AddArray(temperature); - - vtkNew velocity; - velocity->SetName("velocity"); - velocity->SetNumberOfComponents(3); - double vt[][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {2, 0, 0}, {0, 2, 0}}; - for (auto& t : vt) - velocity->InsertNextTuple(t); - grid->GetPointData()->AddArray(velocity); - - vtkNew writer; - writer->SetFileName(path.c_str()); - writer->SetInputData(grid); - writer->Write(); + // __FILE__ is this header; fixtures/ sits alongside tests/ + std::filesystem::path here = std::filesystem::path(__FILE__).parent_path(); + return (here / "fixtures" / "test_mesh.vtu").string(); } diff --git a/tests/fixtures/test_mesh.vtu b/tests/fixtures/test_mesh.vtu new file mode 100644 index 0000000..701819a --- /dev/null +++ b/tests/fixtures/test_mesh.vtu @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _AQAAAACAAACwAAAAIQAAAA==eJxjYEAGH+yhDAfsfA4H7Hxi1QkQEEfni2CIAwBGOgiaAQAAAACAAAAQAgAATAAAAA==eJxjYMAHPtjjkHDAqw1DH4yPYZ4DdvEHUP4PNPEfOMRx2Y/LHejuJ+ReXDS6+wmFFy7/Y4QnLvfhss8BuzyMDwvPBwTEORwAa3ElpA==AQAAAACAAAAoAAAAEwAAAA==eJxjYEAHB+whWMABGQMAMIIDPw==AQAAAACAAAAoAAAAEwAAAA==eJxjZGBgYETCTFDMjIYBAYAAFQ==AQAAAACAAADwAAAApgAAAA==eJw7Y12fNm/BM/szUJoBCqbl5gjPjX5nz+/pkSRsewUu7uWl5fXT9KY945q2a4sy38LFnRmvTol3eml/uEj8ZNrSx3BxtQxR22/el+1htKXMguiPH9/Yv+g6/0+O7Zm92U6RlXWxZ+F89+aIXW9nnYbSu+03TF5c6M333n7nQRH3JWcv239fI72lNf2U/TSl+VllW97C+TB5mDhMP8w8mDkAnZpoUw==AQAAAACAAAAKAAAAEgAAAA==eJxjZGJmYWVj5+DkAgAA5gA4AQAAAACAAAAoAAAAJwAAAA==eJxLYWBgOAHEOowMDBOA+AsQRzAxMOwBYgVmBoYWIH4BxAB0vAWNAQAAAACAAABQAAAALgAAAA==eJwTUGcAAwU/CG1QCqEd5kDogMMQOuEVhC4QZATTDRYQekI8hF7QBqEBJmYH1g==AQAAAACAAAAgAwAAEAAAAA==eJxjYBgFo2AU4AIAAyAAAQ==AQAAAACAAAAIAQAAQwAAAA==eJxtjgEKACAIA/ez9rSe5tMqKrkkQZyo5yRGb2is72zV3DP0rHF0QPOejMuvXOblV1/8nz7J88vIG2+fKtoeXtAXpA==AQAAAACAAABwAQAAUQAAAA==eJx1zjcSwCAMRFFwxgEnuP9VKfhqdkY0b5QJob+IE46SH3B2+ixeJLb+FTdMeOKBGW/88JW5HS/ZY3MP/rJH9xeszl2t63+0bvns1BsnxwIuAQAAAACAAABQAAAAIwAAAA==eJxjYYAADijNDaX5oLQYlJaD0kpQWg1Ka0FpPSgNAB5QAPo=AQAAAACAAAAKAAAAEAAAAA==eJzj5GRl5eHhAgIAAegAXQ== + + From b642a6452c8c35cceb6701c95d37b5854f037fdb Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Fri, 6 Mar 2026 13:44:33 +0100 Subject: [PATCH 14/17] add VTK header dependency back to test build --- tests/CMakeLists.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 13cd92a..a323d5a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.12) project(SOFAVTK_test VERSION 1.0) find_package(Sofa.Testing REQUIRED) +# BaseVTKLoader.h exposes vtkDataSet/vtkSmartPointer in its public interface, +# so the test needs VTK headers even though it does not use VTK directly. +find_package(VTK COMPONENTS CommonCore CommonDataModel REQUIRED) set(SOURCE_FILES test.cpp @@ -10,6 +13,11 @@ set(SOURCE_FILES add_executable(${PROJECT_NAME} ${SOURCE_FILES}) -target_link_libraries(${PROJECT_NAME} Sofa.Testing SOFA.VTK) +target_link_libraries(${PROJECT_NAME} Sofa.Testing SOFA.VTK ${VTK_LIBRARIES}) + +vtk_module_autoinit( + TARGETS ${PROJECT_NAME} + MODULES ${VTK_LIBRARIES} +) add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) From 7a26a52f5a7747a7b4bece692edb38bcf7aeb41a Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Fri, 6 Mar 2026 14:22:00 +0100 Subject: [PATCH 15/17] add a scene to demonstrate loading of Data arrays --- examples/xml/LoadDataArrays.scn | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 examples/xml/LoadDataArrays.scn diff --git a/examples/xml/LoadDataArrays.scn b/examples/xml/LoadDataArrays.scn new file mode 100644 index 0000000..79638b3 --- /dev/null +++ b/examples/xml/LoadDataArrays.scn @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + From ee8843350af5bbf453dbb6c1f211443fe4a248a2 Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Wed, 11 Mar 2026 13:48:03 +0100 Subject: [PATCH 16/17] refactor and clean --- examples/xml/LoadDataArrays.scn | 12 +++--------- src/sofa/vtk/BaseVTKLoader.cpp | 33 ++++++++++++++++---------------- src/sofa/vtk/BaseVTKLoader.h | 3 --- src/sofa/vtk/VTKtoSOFA.cpp | 2 +- tests/CMakeLists.txt | 3 --- tests/TestCellPointData.cpp | 29 +++++++++++++++++++++++++++- tests/TestFixtures.h | 34 --------------------------------- 7 files changed, 49 insertions(+), 67 deletions(-) delete mode 100644 tests/TestFixtures.h diff --git a/examples/xml/LoadDataArrays.scn b/examples/xml/LoadDataArrays.scn index 79638b3..6a9572f 100644 --- a/examples/xml/LoadDataArrays.scn +++ b/examples/xml/LoadDataArrays.scn @@ -1,13 +1,13 @@ diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index 99adc31..cf2c256 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -20,12 +20,10 @@ vtkSmartPointer getDataSet(const std::string& fileName) return reader->GetOutput(); } -// long and unsigned long have platform-dependent sizes (32-bit on Windows, 64-bit on Linux/Mac). -// Remap them to the fixed-size type of the same width so the SOFA Data type is consistent -// and predictable regardless of platform. -template struct CanonicalLong { using type = T; }; -template<> struct CanonicalLong { using type = std::conditional_t; }; -template<> struct CanonicalLong { using type = std::conditional_t; }; +// Remap long and unsigned long which are platform dependent to a canonical fixed-width type +template struct CanonicalLong { using type = T;}; +template<> struct CanonicalLong { using type = std::conditional_t;}; +template<> struct CanonicalLong { using type = std::conditional_t; }; template using CanonicalLong_t = typename CanonicalLong::type; @@ -49,9 +47,10 @@ struct ScalarDataWorker auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); auto& vec = accessor.wref(); vec.resize(array->GetNumberOfTuples()); - vtkIdType i = 0; - for (const auto v : vtk::DataArrayValueRange<1>(array)) - vec[i++] = v; + auto values = vtk::DataArrayValueRange<1>(array); + const vtkIdType n = static_cast(array->GetNumberOfTuples()); + for (vtkIdType i = 0; i < n; ++i) + vec[i] = values[i]; } loader.addData(dataPtr.get(), arrayName); @@ -74,6 +73,8 @@ struct MultiComponentDataWorker } private: + // Recursively dispatch fill function specialization based on the number of components until a + // match is found or the max supported is reached. template void dispatchN(ArrayT* array) { @@ -94,12 +95,12 @@ struct MultiComponentDataWorker auto accessor = sofa::helper::getWriteOnlyAccessor(*dataPtr); auto& vec = accessor.wref(); vec.resize(array->GetNumberOfTuples()); - vtkIdType i = 0; - for (const auto tuple : vtk::DataArrayTupleRange(array)) + auto tuples = vtk::DataArrayTupleRange(array); + const vtkIdType n = static_cast(array->GetNumberOfTuples()); + for (vtkIdType i = 0; i < n; ++i) { for (int c = 0; c < N; ++c) - vec[i][c] = tuple[c]; - ++i; + vec[i][c] = tuples[i][c]; } } @@ -125,9 +126,9 @@ void BaseVTKLoader::loadDataArrayByName(vtkFieldData* fieldData, const std::stri const int numComponents = array->GetNumberOfComponents(); - // Explicitly supported value types. Each maps to a fixed-size C++ type that SOFA's - // type system reliably handles. long and unsigned long are included and remapped via - // CanonicalLong_t to int/long long based on their size on the current platform. + // Explicitly supported value types. Each maps to a fixed-size C++ type through vtkArrayDispatch + // long and unsigned long are included and remapped via CanonicalLong_t to int/long long based + // on their size on the current platform. using SupportedTypes = vtkTypeList::Create< float, double, signed char, unsigned char, diff --git a/src/sofa/vtk/BaseVTKLoader.h b/src/sofa/vtk/BaseVTKLoader.h index 17fc029..c5f4fa2 100644 --- a/src/sofa/vtk/BaseVTKLoader.h +++ b/src/sofa/vtk/BaseVTKLoader.h @@ -4,9 +4,6 @@ #include #include #include -#include -#include -#include namespace sofavtk { diff --git a/src/sofa/vtk/VTKtoSOFA.cpp b/src/sofa/vtk/VTKtoSOFA.cpp index 6926799..581e7fe 100644 --- a/src/sofa/vtk/VTKtoSOFA.cpp +++ b/src/sofa/vtk/VTKtoSOFA.cpp @@ -99,7 +99,7 @@ void extractCells(sofa::core::loader::MeshLoader& loader, vtkSmartPointer types = vtkSmartPointer::New(); + vtkSmartPointer types = vtkSmartPointer ::New(); dataSet->GetCellTypes(types); const vtkIdType nbElementTypes = types->GetNumberOfTypes(); for (vtkIdType i = 0; i < nbElementTypes; ++i) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a323d5a..bc98c0c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,9 +1,6 @@ cmake_minimum_required(VERSION 3.12) project(SOFAVTK_test VERSION 1.0) - find_package(Sofa.Testing REQUIRED) -# BaseVTKLoader.h exposes vtkDataSet/vtkSmartPointer in its public interface, -# so the test needs VTK headers even though it does not use VTK directly. find_package(VTK COMPONENTS CommonCore CommonDataModel REQUIRED) set(SOURCE_FILES diff --git a/tests/TestCellPointData.cpp b/tests/TestCellPointData.cpp index 44a582f..a81e353 100644 --- a/tests/TestCellPointData.cpp +++ b/tests/TestCellPointData.cpp @@ -5,7 +5,34 @@ #include #include -#include "TestFixtures.h" +#include +#include + +// Fixture: 22 points, 10 cells — three spatial zones: +// Zone A z=0 2 quads (cells 0-1) + 2 triangles (cells 2-3) +// Zone B z=1→2 2 hexahedra (cells 4-5) +// Zone C z=2→3 4 tetrahedra (cells 6-9) +// +// All data values are functions of coordinates, verifiable by selecting +// an element in ParaView and computing the expected value directly: +// +// Cell data (10 entries): +// pressure float32 centroid z [0.0, 0.0, 0.0, 0.0, 1.5, 1.5, 2.25, 2.25, 2.25, 2.25] +// material_id int32 zone id [1, 1, 1, 1, 2, 2, 3, 3, 3, 3] +// fiber_direction float64 normalised centroid vector +// int8_data int8 cell index [1 .. 10] +// int32_data int32 index * 100 [100 .. 1000] +// int64_data int64 index * 10000 [10000 .. 100000] +// too_many float64 10 components, all 0 (>9-component guard) +// +// Point data (22 entries): +// temperature float64 x + y + z [0.0 .. 5.0] +// velocity float64 (x, y, z) radial from origin +static std::string fixtureVtuPath() +{ + std::filesystem::path here = std::filesystem::path(__FILE__).parent_path(); + return (here / "fixtures" / "test_mesh.vtu").string(); +} namespace { diff --git a/tests/TestFixtures.h b/tests/TestFixtures.h deleted file mode 100644 index b3637bb..0000000 --- a/tests/TestFixtures.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include - -// Returns the absolute path to the pre-generated test fixture. -// -// Geometry: 22 points, 10 cells — three spatial zones: -// Zone A z=0 2 quads (cells 0-1) + 2 triangles (cells 2-3) -// Zone B z=1→2 2 hexahedra (cells 4-5) -// Zone C z=2→3 4 tetrahedra (cells 6-9) -// -// All data values are functions of coordinates, verifiable by selecting -// an element in ParaView and computing the expected value directly: -// -// Cell data (10 entries): -// pressure float32 centroid z [0.0, 0.0, 0.0, 0.0, 1.5, 1.5, 2.25, 2.25, 2.25, 2.25] -// material_id int32 zone id [1, 1, 1, 1, 2, 2, 3, 3, 3, 3] -// fiber_direction float64 normalised centroid vector -// int8_data int8 cell index [1 .. 10] -// int32_data int32 index * 100 [100 .. 1000] -// int64_data int64 index * 10000 [10000 .. 100000] -// too_many float64 10 components, all 0 (>9-component guard) -// -// Point data (22 entries): -// temperature float64 x + y + z [0.0 .. 5.0] -// velocity float64 (x, y, z) radial from origin - -inline std::string fixtureVtuPath() -{ - // __FILE__ is this header; fixtures/ sits alongside tests/ - std::filesystem::path here = std::filesystem::path(__FILE__).parent_path(); - return (here / "fixtures" / "test_mesh.vtu").string(); -} From 26d2a1b1545f0819aa838e6d23f3a155ba73531c Mon Sep 17 00:00:00 2001 From: Themis Skamagkis Date: Tue, 31 Mar 2026 15:07:07 +0200 Subject: [PATCH 17/17] map array types to SOFA types --- src/sofa/vtk/BaseVTKLoader.cpp | 25 +++++++---- tests/TestCellPointData.cpp | 76 +++++++++++++++++----------------- 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/sofa/vtk/BaseVTKLoader.cpp b/src/sofa/vtk/BaseVTKLoader.cpp index cf2c256..009c5f4 100644 --- a/src/sofa/vtk/BaseVTKLoader.cpp +++ b/src/sofa/vtk/BaseVTKLoader.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -20,13 +21,23 @@ vtkSmartPointer getDataSet(const std::string& fileName) return reader->GetOutput(); } -// Remap long and unsigned long which are platform dependent to a canonical fixed-width type -template struct CanonicalLong { using type = T;}; -template<> struct CanonicalLong { using type = std::conditional_t;}; -template<> struct CanonicalLong { using type = std::conditional_t; }; +// Map VTK array value types to SOFA-registered Data types. +// float/double → SReal; small signed integers → int; unsigned integers → sofa::Index; +// long long / unsigned long long are kept as-is (both registered). +// long/unsigned long are platform-dependent and folded into the appropriate fixed-width type. +template struct SofaType { using type = T; }; +template<> struct SofaType { using type = SReal; }; +template<> struct SofaType { using type = SReal; }; +template<> struct SofaType { using type = int; }; +template<> struct SofaType { using type = int; }; +template<> struct SofaType { using type = sofa::Index; }; +template<> struct SofaType { using type = sofa::Index; }; +template<> struct SofaType { using type = sofa::Index; }; +template<> struct SofaType { using type = std::conditional_t; }; +template<> struct SofaType { using type = std::conditional_t; }; template -using CanonicalLong_t = typename CanonicalLong::type; +using SofaType_t = typename SofaType::type; struct ScalarDataWorker { @@ -37,7 +48,7 @@ struct ScalarDataWorker template void operator()(ArrayT* array) { - using T = CanonicalLong_t>; + using T = SofaType_t>; auto dataPtr = std::make_unique>>(); dataPtr->setName(arrayName); @@ -68,7 +79,7 @@ struct MultiComponentDataWorker template void operator()(ArrayT* array) { - using T = CanonicalLong_t>; + using T = SofaType_t>; dispatchN(array); } diff --git a/tests/TestCellPointData.cpp b/tests/TestCellPointData.cpp index a81e353..e85d56b 100644 --- a/tests/TestCellPointData.cpp +++ b/tests/TestCellPointData.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include @@ -85,18 +87,18 @@ class CellPointDataTest : public sofa::testing::BaseSimulationTest TEST_F(CellPointDataTest, CellData_ScalarFloat) { auto* loader = makeLoader({"pressure"}); - const auto* v = getVec(loader, "pressure"); + const auto* v = getVec(loader, "pressure"); ASSERT_NE(v, nullptr); ASSERT_EQ(v->size(), 10u); // Zone A: centroid z = 0.0 - EXPECT_FLOAT_EQ((*v)[0], 0.0f); - EXPECT_FLOAT_EQ((*v)[3], 0.0f); + EXPECT_NEAR((*v)[0], 0.0, 1e-6); + EXPECT_NEAR((*v)[3], 0.0, 1e-6); // Zone B: centroid z = 1.5 - EXPECT_FLOAT_EQ((*v)[4], 1.5f); - EXPECT_FLOAT_EQ((*v)[5], 1.5f); + EXPECT_NEAR((*v)[4], 1.5, 1e-6); + EXPECT_NEAR((*v)[5], 1.5, 1e-6); // Zone C: centroid z = 2.25 - EXPECT_FLOAT_EQ((*v)[6], 2.25f); - EXPECT_FLOAT_EQ((*v)[9], 2.25f); + EXPECT_NEAR((*v)[6], 2.25, 1e-6); + EXPECT_NEAR((*v)[9], 2.25, 1e-6); } // material_id = zone id: 1=surface, 2=hexa, 3=tet @@ -115,33 +117,33 @@ TEST_F(CellPointDataTest, CellData_ScalarInt) // fiber_direction = normalised centroid vector TEST_F(CellPointDataTest, CellData_Vec3Double) { - using Vec3d = sofa::type::Vec<3, double>; + using Vec3r = sofa::type::Vec<3, SReal>; auto* loader = makeLoader({"fiber_direction"}); - const auto* v = getVec(loader, "fiber_direction"); + const auto* v = getVec(loader, "fiber_direction"); ASSERT_NE(v, nullptr); ASSERT_EQ(v->size(), 10u); // Every entry must be a unit vector for (std::size_t i = 0; i < v->size(); ++i) { - const double mag = (*v)[i].norm(); - EXPECT_NEAR(mag, 1.0, 1e-9) << "cell " << i << " fiber_direction is not unit"; + const SReal mag = (*v)[i].norm(); + EXPECT_NEAR(mag, 1.0, 1e-6) << "cell " << i << " fiber_direction is not unit"; } // Cell 0 centroid = (0.5, 0.5, 0) → normalised (1/√2, 1/√2, 0) - const double inv_sqrt2 = 1.0 / std::sqrt(2.0); - EXPECT_NEAR((*v)[0][0], inv_sqrt2, 1e-9); - EXPECT_NEAR((*v)[0][1], inv_sqrt2, 1e-9); - EXPECT_NEAR((*v)[0][2], 0.0, 1e-9); + const SReal inv_sqrt2 = SReal(1) / std::sqrt(SReal(2)); + EXPECT_NEAR((*v)[0][0], inv_sqrt2, 1e-6); + EXPECT_NEAR((*v)[0][1], inv_sqrt2, 1e-6); + EXPECT_NEAR((*v)[0][2], 0.0, 1e-6); // Zone C tets: centroid z >> x,y so z-component dominates - EXPECT_GT((*v)[6][2], 0.9); + EXPECT_GT((*v)[6][2], SReal(0.9)); } TEST_F(CellPointDataTest, CellData_Int8) { auto* loader = makeLoader({"int8_data"}); - const auto* v = getVec(loader, "int8_data"); + const auto* v = getVec(loader, "int8_data"); ASSERT_NE(v, nullptr); ASSERT_EQ(v->size(), 10u); // int8_data = cell index + 1 @@ -175,39 +177,39 @@ TEST_F(CellPointDataTest, CellData_Int64) TEST_F(CellPointDataTest, PointData_ScalarDouble) { auto* loader = makeLoader({}, {"temperature"}); - const auto* v = getVec(loader, "temperature"); + const auto* v = getVec(loader, "temperature"); ASSERT_NE(v, nullptr); ASSERT_EQ(v->size(), 22u); - EXPECT_DOUBLE_EQ((*v)[0], 0.0); // P0 (0,0,0) - EXPECT_DOUBLE_EQ((*v)[1], 1.0); // P1 (1,0,0) - EXPECT_DOUBLE_EQ((*v)[5], 3.0); // P5 (2,1,0) - EXPECT_DOUBLE_EQ((*v)[19], 5.0); // P19 (2,1,2) - EXPECT_DOUBLE_EQ((*v)[21], 4.0); // P21 (0.5,0.5,3) + EXPECT_NEAR((*v)[0], 0.0, 1e-6); // P0 (0,0,0) + EXPECT_NEAR((*v)[1], 1.0, 1e-6); // P1 (1,0,0) + EXPECT_NEAR((*v)[5], 3.0, 1e-6); // P5 (2,1,0) + EXPECT_NEAR((*v)[19], 5.0, 1e-6); // P19 (2,1,2) + EXPECT_NEAR((*v)[21], 4.0, 1e-6); // P21 (0.5,0.5,3) } // velocity = (x, y, z) per point — direction and magnitude encode position TEST_F(CellPointDataTest, PointData_Vec3Double) { - using Vec3d = sofa::type::Vec<3, double>; + using Vec3r = sofa::type::Vec<3, SReal>; auto* loader = makeLoader({}, {"velocity"}); - const auto* v = getVec(loader, "velocity"); + const auto* v = getVec(loader, "velocity"); ASSERT_NE(v, nullptr); ASSERT_EQ(v->size(), 22u); // P0 (0,0,0) → zero velocity - EXPECT_DOUBLE_EQ((*v)[0][0], 0.0); - EXPECT_DOUBLE_EQ((*v)[0][1], 0.0); - EXPECT_DOUBLE_EQ((*v)[0][2], 0.0); + EXPECT_NEAR((*v)[0][0], 0.0, 1e-6); + EXPECT_NEAR((*v)[0][1], 0.0, 1e-6); + EXPECT_NEAR((*v)[0][2], 0.0, 1e-6); // P5 (2,1,0) → velocity = coordinates - EXPECT_DOUBLE_EQ((*v)[5][0], 2.0); - EXPECT_DOUBLE_EQ((*v)[5][1], 1.0); - EXPECT_DOUBLE_EQ((*v)[5][2], 0.0); + EXPECT_NEAR((*v)[5][0], 2.0, 1e-6); + EXPECT_NEAR((*v)[5][1], 1.0, 1e-6); + EXPECT_NEAR((*v)[5][2], 0.0, 1e-6); // P21 (0.5,0.5,3) → velocity = coordinates - EXPECT_DOUBLE_EQ((*v)[21][0], 0.5); - EXPECT_DOUBLE_EQ((*v)[21][1], 0.5); - EXPECT_DOUBLE_EQ((*v)[21][2], 3.0); + EXPECT_NEAR((*v)[21][0], 0.5, 1e-6); + EXPECT_NEAR((*v)[21][1], 0.5, 1e-6); + EXPECT_NEAR((*v)[21][2], 3.0, 1e-6); } TEST_F(CellPointDataTest, MissingArrayName) @@ -226,7 +228,7 @@ TEST_F(CellPointDataTest, MultipleArraysAtOnce) { auto* loader = makeLoader({"pressure", "material_id"}, {"temperature"}); - const auto* pressure = getVec(loader, "pressure"); + const auto* pressure = getVec(loader, "pressure"); ASSERT_NE(pressure, nullptr); EXPECT_EQ(pressure->size(), 10u); @@ -234,7 +236,7 @@ TEST_F(CellPointDataTest, MultipleArraysAtOnce) ASSERT_NE(matId, nullptr); EXPECT_EQ(matId->size(), 10u); - const auto* temp = getVec(loader, "temperature"); + const auto* temp = getVec(loader, "temperature"); ASSERT_NE(temp, nullptr); EXPECT_EQ(temp->size(), 22u); } @@ -242,7 +244,7 @@ TEST_F(CellPointDataTest, MultipleArraysAtOnce) TEST_F(CellPointDataTest, Reload) { auto* loader = makeLoader({"pressure"}); - ASSERT_NE(getVec(loader, "pressure"), nullptr); + ASSERT_NE(getVec(loader, "pressure"), nullptr); EXPECT_EQ(loader->findData("material_id"), nullptr); loader->d_cellDataNames.setValue({"material_id"});