diff --git a/CMakeLists.txt b/CMakeLists.txt index 52fac89..22ae2cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ ############################################################################## cmake_minimum_required(VERSION 3.12) -project(InfinyToolkit VERSION 0.1 LANGUAGES CXX) +project(InfinyToolkit VERSION 0.2 LANGUAGES CXX) include(cmake/environment.cmake) @@ -171,6 +171,11 @@ target_include_directories(${PROJECT_NAME} PUBLIC "$") ## Install rules for the library; CMake package configurations files +if(MeshRefinement_FOUND) + sofa_add_subdirectory(plugin extensions/MeshRefinement InfinyToolkit.MeshRefinement + BINARY_DIR "${CMAKE_BINARY_DIR}/InfinyToolkit.MeshRefinement") +endif() + sofa_create_package_with_targets( PACKAGE_NAME ${PROJECT_NAME} PACKAGE_VERSION ${PROJECT_VERSION} diff --git a/extensions/MeshRefinement/CMakeLists.txt b/extensions/MeshRefinement/CMakeLists.txt new file mode 100644 index 0000000..74adcaa --- /dev/null +++ b/extensions/MeshRefinement/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.12) +project(InfinyToolkit.MeshRefinement VERSION 0.1 LANGUAGES CXX) + +# This plugin provides a component controller that will be used in SOFA scene to use the public interface MeshRefinementAPI of the MeshRefinement plugin. +set(INFINYTOOLKIT_MESHREFINEMENT_SRC_DIR src/InfinyToolkit/MeshRefinement) + +set(HEADER_FILES + ${INFINYTOOLKIT_MESHREFINEMENT_SRC_DIR}/config.h.in + ${INFINYTOOLKIT_MESHREFINEMENT_SRC_DIR}/init.h + ${INFINYTOOLKIT_MESHREFINEMENT_SRC_DIR}/TetrahedronSubdivisionController.h + ${INFINYTOOLKIT_MESHREFINEMENT_SRC_DIR}/TetrahedronSubdivisionController.inl +) + +set(SOURCE_FILES + ${INFINYTOOLKIT_MESHREFINEMENT_SRC_DIR}/init.cpp + ${INFINYTOOLKIT_MESHREFINEMENT_SRC_DIR}/TetrahedronSubdivisionController.cpp +) + +sofa_find_package(MeshRefinement REQUIRED) +sofa_find_package(Sofa.Component.Topology.Container.Dynamic REQUIRED) + +# Optional: the Cuda template instantiations of the controller. +# Not yet working +#sofa_find_package(SofaCUDA QUIET) +#sofa_find_package(MeshRefinement.CUDA QUIET) +#if(SofaCUDA_FOUND AND MeshRefinement.CUDA_FOUND) +# message("${PROJECT_NAME}: SofaCUDA found - adding the Cuda subdivision controller") +# list(APPEND SOURCE_FILES ${INFINYTOOLKIT_MESHREFINEMENT_SRC_DIR}/CudaSubdivisionController.cpp) +# add_definitions(-DINFINYTOOLKIT_MESHREFINEMENT_USES_SOFACUDA) +#endif() + +add_library(${PROJECT_NAME} SHARED ${HEADER_FILES} ${SOURCE_FILES}) +target_compile_definitions(${PROJECT_NAME} PRIVATE "-DSOFA_BUILD_INFINYTOOLKIT_MESHREFINEMENT") +target_link_libraries(${PROJECT_NAME} MeshRefinement Sofa.Component.Topology.Container.Dynamic) + +#if(SofaCUDA_FOUND AND MeshRefinement.CUDA_FOUND) +# target_link_libraries(${PROJECT_NAME} SofaCUDA MeshRefinement.CUDA) +#endif() + +sofa_create_package_with_targets( + PACKAGE_NAME ${PROJECT_NAME} + PACKAGE_VERSION ${Sofa_VERSION} + TARGETS ${PROJECT_NAME} AUTO_SET_TARGET_PROPERTIES + INCLUDE_SOURCE_DIR "src" + INCLUDE_INSTALL_DIR "${PROJECT_NAME}" + RELOCATABLE "plugins" +) + +if(SOFA_BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/extensions/MeshRefinement/InfinyToolkit.MeshRefinementConfig.cmake.in b/extensions/MeshRefinement/InfinyToolkit.MeshRefinementConfig.cmake.in new file mode 100644 index 0000000..e64de70 --- /dev/null +++ b/extensions/MeshRefinement/InfinyToolkit.MeshRefinementConfig.cmake.in @@ -0,0 +1,9 @@ +# CMake package configuration file for the InfinyToolkit.MeshRefinement library + +@PACKAGE_GUARD@ +@PACKAGE_INIT@ + +find_package(MeshRefinement QUIET REQUIRED) +find_package(Sofa.Component.Topology.Container.Dynamic QUIET REQUIRED) + +check_required_components(InfinyToolkit.MeshRefinement) diff --git a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/CudaSubdivisionController.cpp b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/CudaSubdivisionController.cpp new file mode 100644 index 0000000..7c368bf --- /dev/null +++ b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/CudaSubdivisionController.cpp @@ -0,0 +1,48 @@ +/******************************************************************************* + * - Copyright (C) 2026-Present InfinyTech3D - * + * All Rights Reserved * + * * + * The contents of this file are confidential and proprietary to InfinyTech3D. * + * Unauthorized copying, modification, distribution or disclosure of this * + * file, via any medium, is strictly prohibited. * + * * + * This file may only be used under the terms and conditions of a valid * + * commercial license agreement with InfinyTech3D. * + * * + * For licensing inquiries: contact@infinytech3d.com * + * Further information: https://infinytech3d.com * + ******************************************************************************/ + +/// Instantiates and registers the controller for the Cuda vector types. +/// The matching MeshRefinementAPI instantiations are provided by MeshRefinement.CUDA, +/// Only compiled when both SofaCUDA and MeshRefinement.CUDA are found. + +#include +#include +#include + +namespace sofa::infinytoolkit::meshrefinement +{ + +void registerCudaTetrahedronSubdivisionController(sofa::core::ObjectFactory* factory) +{ + factory->registerObjects(sofa::core::ObjectRegistrationData("Controller handling subdivision operations on a tetrahedral mesh, on Cuda vector types.") + .add< sofa::infinytoolkit::TetrahedronSubdivisionController >() +#ifdef SOFA_GPU_CUDA_DOUBLE + .add< sofa::infinytoolkit::TetrahedronSubdivisionController >() +#endif + ); +} + +} // namespace sofa::infinytoolkit::meshrefinement + +namespace sofa::infinytoolkit +{ + +template class SOFA_INFINYTOOLKIT_MESHREFINEMENT_API TetrahedronSubdivisionController; + +#ifdef SOFA_GPU_CUDA_DOUBLE +template class SOFA_INFINYTOOLKIT_MESHREFINEMENT_API TetrahedronSubdivisionController; +#endif + +} // namespace sofa::infinytoolkit diff --git a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.cpp b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.cpp new file mode 100644 index 0000000..21ace75 --- /dev/null +++ b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.cpp @@ -0,0 +1,38 @@ +/******************************************************************************* + * - Copyright (C) 2019-Present InfinyTech3D - * + * All Rights Reserved * + * * + * The contents of this file are confidential and proprietary to InfinyTech3D. * + * Unauthorized copying, modification, distribution or disclosure of this * + * file, via any medium, is strictly prohibited. * + * * + * This file may only be used under the terms and conditions of a valid * + * commercial license agreement with InfinyTech3D. * + * * + * For licensing inquiries: contact@infinytech3d.com * + * Further information: https://infinytech3d.com * + ******************************************************************************/ +#define SOFA_INFINYTOOLKIT_TETRAHEDRON_SUBDIVISION_CONTROLLER_CPP + +#include +#include + +namespace sofa::infinytoolkit::meshrefinement +{ + +using namespace sofa::core::topology; +using namespace sofa::component::topology; + +void registerTetrahedronSubdivisionController(sofa::core::ObjectFactory* factory) +{ + factory->registerObjects(sofa::core::ObjectRegistrationData("Controller handling subdivision operations on a tetrahedral mesh: cutting along a plane or between two surface triangles, and refining the whole mesh or a given set of tetrahedra.") + .add< sofa::infinytoolkit::TetrahedronSubdivisionController >()); +} + +} // namespace sofa::infinytoolkit::meshrefinement + +namespace sofa::infinytoolkit +{ +template class SOFA_INFINYTOOLKIT_MESHREFINEMENT_API TetrahedronSubdivisionController; + +} // namespace sofa::infinytoolkit diff --git a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.h b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.h new file mode 100644 index 0000000..90c99f2 --- /dev/null +++ b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.h @@ -0,0 +1,181 @@ +/******************************************************************************* + * - Copyright (C) 2019-Present InfinyTech3D - * + * All Rights Reserved * + * * + * The contents of this file are confidential and proprietary to InfinyTech3D. * + * Unauthorized copying, modification, distribution or disclosure of this * + * file, via any medium, is strictly prohibited. * + * * + * This file may only be used under the terms and conditions of a valid * + * commercial license agreement with InfinyTech3D. * + * * + * For licensing inquiries: contact@infinytech3d.com * + * Further information: https://infinytech3d.com * + ******************************************************************************/ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + + +namespace sofa::infinytoolkit +{ + +/** +* Cuts and refines a tetrahedral mesh, driving the MeshRefinement plugin's engine. +* +* Four operations: cut along a plane, cut between two surface triangles, refine a +* given set of tetrahedra, and refine the whole mesh. +* +* Cutting is two-phase: the path is built first and applied second, so a scene can +* draw the plane and inspect it before the topology changes. prepareCutFrom*() +* followed by applyCut() does that; cutFrom*() does both in one call. +* +* Interactive keys, used by the example scenes: +* '1' build the cut plane from the cutPointA / cutPointB / cutDir / cutDepth Data +* '2' apply the cut built by '1' +* '3' refine the tetrahedra listed in testID +* '4' refine the whole mesh +*/ +template +class TetrahedronSubdivisionController : public core::behavior::BaseController +{ +public: + SOFA_CLASS(SOFA_TEMPLATE(TetrahedronSubdivisionController, DataTypes), sofa::core::behavior::BaseController); + + using Real = typename DataTypes::Real; + using Coord = typename DataTypes::Coord; + using VecCoord = typename DataTypes::VecCoord; + + using Vec3 = sofa::type::Vec3; + using TetrahedronSetTopologyContainer = sofa::component::topology::container::dynamic::TetrahedronSetTopologyContainer; + + using TriangleID = sofa::core::topology::Topology::TriangleID; + + /// Sofa API init method of the component + void init() override; + + /// Handles the key presses listed in the class description. + void handleEvent(sofa::core::objectmodel::Event* event) override; + + /// Draws the subdivided tetrahedra and the cut plane, per @sa d_drawTetra and + /// @sa d_drawDebugCut. + void draw(const core::visual::VisualParams* vparams) override; + + + /// @name Cutting + /// @{ + + /// Cuts along the quad spanned by @p pointA, @p pointB and @p direction over + /// @p depth, building and applying the cut in one call. @p createSurface also + /// creates the new surface mesh and its components on the cut faces. + /// @return false if the controller is unusable or the cut could not be built. + bool cutFromPlane(const Vec3& pointA, const Vec3& pointB, const Vec3& direction, + SReal depth, bool createSurface = false); + + /// Cuts between the barycentres of triangles @p triangleA and @p triangleB, over + /// @p depth along the normal of @p triangleA, in one call. + /// @return false if the controller is unusable or either id is out of range. + bool cutFromTriangles(TriangleID triangleA, TriangleID triangleB, + SReal depth, bool createSurface = false); + + /// Builds a cut path without applying it, so that it can be drawn and inspected. + /// @sa applyCut + bool prepareCutFromPlane(const Vec3& pointA, const Vec3& pointB, const Vec3& direction, + SReal depth); + + /// Builds a cut path between two surface triangles without applying it. + /// @sa applyCut + bool prepareCutFromTriangles(TriangleID triangleA, TriangleID triangleB, SReal depth); + + /// Applies the path built by either prepareCutFrom* method. + /// @return false if the controller is unusable. + bool applyCut(bool createSurface); + /// @} + + + /// @name Refinement + /// @{ + + /// Subdivides every tetrahedron of the mesh. + /// @return false if the controller is unusable. + bool refineFullMesh(); + + /// Subdivides the tetrahedra given by @p ids. @p criteria is the edge length + /// under which an edge is left whole; 0 subdivides regardless of length. + /// @return false if the controller is unusable. + bool refineTetrahedra(const std::set& ids, SReal criteria = 0.0); + /// @} + + +protected: + /// Default constructor + TetrahedronSubdivisionController(); + + /// Default destructor + ~TetrahedronSubdivisionController() override; + + /// Builds the cut path from @sa d_cutPointA, @sa d_cutPointB, @sa d_cutDirection + /// and @sa d_cutDepth. Used by the '1' key and by @sa d_performCut. + bool prepareCutFromData(); + + /// Refines the tetrahedra in @sa d_testID, honouring @sa d_delayMode. + void refineFromData(); + + +public: + /// Performs a cut at the next time step, then resets itself to false. + Data d_performCut; + + /// The cut plane: the quad spanned by the two points below, extruded along + /// @sa d_cutDirection over @sa d_cutDepth. + Data d_cutPointA; ///< First point of the cut plane + Data d_cutPointB; ///< Second point of the cut plane + Data d_cutDirection; ///< Direction the plane is extruded along + Data d_cutDepth; ///< Depth of the cut along @sa d_cutDirection + + /// Creates a new surface mesh, and the components to render it, on the cut faces. + Data d_surfaceCut; + /// Texture applied to the surface created by the cut. Only used when + /// @sa d_surfaceCut is true. + Data d_textureName; + + /// Tetrahedra subdivided by the '3' key. @sa refineTetrahedra + Data > d_testID; + /// Edge length under which an edge is left whole; 0 subdivides regardless. + Data d_refineCriteria; + /// Splits the '3' key in two presses: the first computes the neighbourhood table, + /// the second subdivides from it. + Data d_delayMode; + + Data d_drawTetra; ///< Draw the tetrahedra held by the subdividers + Data d_drawScaleTetrahedra; ///< Scale of the drawn tetrahedra; below 1.0 it leaves gaps between them + Data d_drawDebugCut; ///< Draw the cut plane and the intersections it computed + + +private: + /// The subdivision engine. Owned, because a cut is built by one call and applied + /// by another and both have to reach the same engine. + std::unique_ptr > m_mgr = nullptr; + + /// The topology this controller operates on, used to range-check the ids given to it. + TetrahedronSetTopologyContainer::SPtr m_topoCon = nullptr; + + /// True once init() has found a topology and the engine has initialised. + bool m_controllerReady = false; + + /// Press counter for the two-phase '3' key. @sa d_delayMode + int m_refineStatus = 0; +}; + +#if !defined(SOFA_INFINYTOOLKIT_TETRAHEDRON_SUBDIVISION_CONTROLLER_CPP) +extern template class SOFA_INFINYTOOLKIT_MESHREFINEMENT_API TetrahedronSubdivisionController; +#endif + +} // namespace sofa::infinytoolkit diff --git a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.inl b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.inl new file mode 100644 index 0000000..d9f5900 --- /dev/null +++ b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/TetrahedronSubdivisionController.inl @@ -0,0 +1,291 @@ +/******************************************************************************* + * - Copyright (C) 2019-Present InfinyTech3D - * + * All Rights Reserved * + * * + * The contents of this file are confidential and proprietary to InfinyTech3D. * + * Unauthorized copying, modification, distribution or disclosure of this * + * file, via any medium, is strictly prohibited. * + * * + * This file may only be used under the terms and conditions of a valid * + * commercial license agreement with InfinyTech3D. * + * * + * For licensing inquiries: contact@infinytech3d.com * + * Further information: https://infinytech3d.com * + ******************************************************************************/ +#pragma once + +#include + +#include +#include +#include + +#include +#include +#include + +#include + +namespace sofa::infinytoolkit +{ + +using namespace sofa::core::topology; +using namespace sofa::component::topology; + + +template +TetrahedronSubdivisionController::TetrahedronSubdivisionController() + : d_performCut(initData(&d_performCut, false, "performCut", "to activate cut at the current timestep")) + , d_cutPointA(initData(&d_cutPointA, Vec3(0.0, 0.0, 0.0), "cutPointA", "(default=[0, 0, 0])")) + , d_cutPointB(initData(&d_cutPointB, Vec3(0.0, 0.0, 0.0), "cutPointB", "(default=[0, 0, 0])")) + , d_cutDirection(initData(&d_cutDirection, Vec3(0.0, -1.0, 0.0), "cutDir", "(default=[0, -1, 0])")) + , d_cutDepth(initData(&d_cutDepth, SReal(0.0), "cutDepth", "depth value")) + , d_surfaceCut(initData(&d_surfaceCut, false, "surfaceCut", "to activate new surface cut")) + , d_textureName(initData(&d_textureName, std::string(""), "textureName", "texture to apply on the surface created by the cut")) + , d_testID(initData(&d_testID, std::set(), "testID", "Ids of the tetrahedra subdivided by the '3' key")) + , d_refineCriteria(initData(&d_refineCriteria, SReal(0.0), "refineCriteria", "Edge length under which an edge is not subdivided. 0 subdivides regardless of length")) + , d_delayMode(initData(&d_delayMode, false, "delayMode", "Split the '3' key in two presses: compute the neighbourhood table, then subdivide from it")) + , d_drawTetra(initData(&d_drawTetra, false, "drawTetra", "Draw the tetrahedra held by the subdividers")) + , d_drawScaleTetrahedra(initData(&d_drawScaleTetrahedra, (float) 1.0, "drawScaleTetrahedra", "Scale of the terahedra (between 0 and 1; if <1.0, it produces gaps between the tetrahedra)")) + , d_drawDebugCut(initData(&d_drawDebugCut, false, "drawDebugCut", "draw Debug Cut infos")) +{ + this->f_listening.setValue(true); +} + + +template +TetrahedronSubdivisionController::~TetrahedronSubdivisionController() +{ + m_refineStatus = 0; +} + + +template +void TetrahedronSubdivisionController::init() +{ + m_topoCon = this->getContext()->template get(); + if (m_topoCon == nullptr) + { + msg_error() << "No topology found"; + m_controllerReady = false; + return; + } + + if (m_mgr == nullptr) + { + m_mgr = std::make_unique >(); + m_controllerReady = m_mgr->init(this->getContext()); + m_mgr->activateLogs(this->f_printLog.getValue()); + + if (d_surfaceCut.getValue()) + { + const std::string& textName = d_textureName.getValue(); + if (!textName.empty()) + m_mgr->setCutTextureName(textName); + } + } +} + + +template +bool TetrahedronSubdivisionController::prepareCutFromPlane(const Vec3& pointA, const Vec3& pointB, + const Vec3& direction, SReal depth) +{ + if (!m_controllerReady) + { + msg_error() << "Controller is not initialised, cannot cut."; + return false; + } + + // Create Cut quad + sofa::type::fixed_array planPositions; + planPositions[0] = pointA; + planPositions[1] = pointB; + + Vec3 cutDir = direction; + cutDir.normalize(); + + planPositions[2] = planPositions[1] + cutDir * depth; + planPositions[3] = planPositions[0] + cutDir * depth; + const Vec3 planNormal = (planPositions[1] - planPositions[0]).cross(cutDir); + + // The thickness is the tolerance within which points are snapped onto the plane. + m_mgr->createCutPlanPath(planPositions, planNormal, depth * 0.25); + return true; +} + + +template +bool TetrahedronSubdivisionController::prepareCutFromTriangles(TriangleID triangleA, + TriangleID triangleB, SReal depth) +{ + if (!m_controllerReady) + { + msg_error() << "Controller is not initialised, cannot cut."; + return false; + } + + return m_mgr->createCutPlanPathFromTriangles(triangleA, triangleB, depth); +} + + +template +bool TetrahedronSubdivisionController::applyCut(bool createSurface) +{ + if (!m_controllerReady) + { + msg_error() << "Controller is not initialised, cannot cut."; + return false; + } + + m_mgr->processCut(createSurface); + return true; +} + + +template +bool TetrahedronSubdivisionController::cutFromPlane(const Vec3& pointA, const Vec3& pointB, + const Vec3& direction, SReal depth, bool createSurface) +{ + return prepareCutFromPlane(pointA, pointB, direction, depth) && applyCut(createSurface); +} + + +template +bool TetrahedronSubdivisionController::cutFromTriangles(TriangleID triangleA, + TriangleID triangleB, SReal depth, bool createSurface) +{ + return prepareCutFromTriangles(triangleA, triangleB, depth) && applyCut(createSurface); +} + + +template +bool TetrahedronSubdivisionController::refineFullMesh() +{ + if (!m_controllerReady) + { + msg_error() << "Controller is not initialised, cannot refine."; + return false; + } + + return m_mgr->refineFullMesh(); +} + + +template +bool TetrahedronSubdivisionController::refineTetrahedra(const std::set& ids, SReal criteria) +{ + if (!m_controllerReady) + { + msg_error() << "Controller is not initialised, cannot refine."; + return false; + } + + // The engine does not range-check: an id past the end is read straight out of the + // tetrahedron array, which is an access violation rather than an error. + const auto nbTetrahedra = m_topoCon->getNbTetrahedra(); + for (const unsigned int id : ids) + { + if (id >= nbTetrahedra) + { + msg_error() << "Tetrahedron id out of range: " << id << " while the topology has " + << nbTetrahedra << " tetrahedra. Nothing refined."; + return false; + } + } + + return m_mgr->refineTetrahedra(ids, criteria); +} + + +template +bool TetrahedronSubdivisionController::prepareCutFromData() +{ + return prepareCutFromPlane(d_cutPointA.getValue(), d_cutPointB.getValue(), + d_cutDirection.getValue(), d_cutDepth.getValue()); +} + + +template +void TetrahedronSubdivisionController::refineFromData() +{ + if (!m_controllerReady) + return; + + if (d_delayMode.getValue() && m_refineStatus == 0) + { + m_mgr->computeNeighboorhoodTable(d_testID.getValue(), d_refineCriteria.getValue()); + m_refineStatus++; + } + else + { + refineTetrahedra(d_testID.getValue(), d_refineCriteria.getValue()); + m_refineStatus = 0; + } +} + + +template +void TetrahedronSubdivisionController::handleEvent(sofa::core::objectmodel::Event* event) +{ + if (!m_controllerReady) + return; + + if (sofa::core::objectmodel::KeypressedEvent* ev = dynamic_cast(event)) + { + dmsg_info() << "GET KEY " << ev->getKey(); + + switch (ev->getKey()) + { + case '1': + prepareCutFromData(); + break; + case '2': + applyCut(d_surfaceCut.getValue()); + break; + case '3': + refineFromData(); + break; + case '4': + refineFullMesh(); + break; + default: + break; + } + } + + if (sofa::simulation::AnimateEndEvent::checkEventType(event)) + { + if (d_performCut.getValue()) + { + cutFromPlane(d_cutPointA.getValue(), d_cutPointB.getValue(), + d_cutDirection.getValue(), d_cutDepth.getValue(), + d_surfaceCut.getValue()); + + d_performCut.setValue(false); + } + } +} + + +template +void TetrahedronSubdivisionController::draw(const core::visual::VisualParams* vparams) +{ + if (!m_controllerReady) + return; + + [[maybe_unused]] auto stateLifeCycle = vparams->drawTool()->makeStateLifeCycle(); + + if (d_drawTetra.getValue()) + { + m_mgr->drawSubdividedTetrahedra(vparams); + } + + if (d_drawDebugCut.getValue()) + { + m_mgr->drawCutPlan(vparams); + m_mgr->drawDebugCut(vparams); + } +} + +} // namespace sofa::infinytoolkit diff --git a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/config.h.in b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/config.h.in new file mode 100644 index 0000000..5ec4559 --- /dev/null +++ b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/config.h.in @@ -0,0 +1,30 @@ +/******************************************************************************* + * - Copyright (C) 2026-Present InfinyTech3D - * + * All Rights Reserved * + * * + * The contents of this file are confidential and proprietary to InfinyTech3D. * + * Unauthorized copying, modification, distribution or disclosure of this * + * file, via any medium, is strictly prohibited. * + * * + * This file may only be used under the terms and conditions of a valid * + * commercial license agreement with InfinyTech3D. * + * * + * For licensing inquiries: contact@infinytech3d.com * + * Further information: https://infinytech3d.com * + ******************************************************************************/ +#pragma once + +#include + +#ifdef SOFA_BUILD_INFINYTOOLKIT_MESHREFINEMENT +# define SOFA_TARGET @PROJECT_NAME@ +# define SOFA_INFINYTOOLKIT_MESHREFINEMENT_API SOFA_EXPORT_DYNAMIC_LIBRARY +#else +# define SOFA_INFINYTOOLKIT_MESHREFINEMENT_API SOFA_IMPORT_DYNAMIC_LIBRARY +#endif + +namespace sofa::infinytoolkit::meshrefinement +{ + constexpr const char* MODULE_NAME = "@PROJECT_NAME@"; + constexpr const char* MODULE_VERSION = "@PROJECT_VERSION@"; +} // namespace sofa::infinytoolkit::meshrefinement diff --git a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/init.cpp b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/init.cpp new file mode 100644 index 0000000..c71e73a --- /dev/null +++ b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/init.cpp @@ -0,0 +1,72 @@ +/******************************************************************************* + * - Copyright (C) 2026-Present InfinyTech3D - * + * All Rights Reserved * + * * + * The contents of this file are confidential and proprietary to InfinyTech3D. * + * Unauthorized copying, modification, distribution or disclosure of this * + * file, via any medium, is strictly prohibited. * + * * + * This file may only be used under the terms and conditions of a valid * + * commercial license agreement with InfinyTech3D. * + * * + * For licensing inquiries: contact@infinytech3d.com * + * Further information: https://infinytech3d.com * + ******************************************************************************/ +#include +#include + +#include +#include + +namespace sofa::infinytoolkit::meshrefinement +{ + +extern void registerTetrahedronSubdivisionController(sofa::core::ObjectFactory* factory); +#ifdef INFINYTOOLKIT_MESHREFINEMENT_USES_SOFACUDA +extern void registerCudaTetrahedronSubdivisionController(sofa::core::ObjectFactory* factory); +#endif + +extern "C" { + SOFA_EXPORT_DYNAMIC_LIBRARY void initExternalModule(); + SOFA_EXPORT_DYNAMIC_LIBRARY const char* getModuleName(); + SOFA_EXPORT_DYNAMIC_LIBRARY const char* getModuleVersion(); + SOFA_INFINYTOOLKIT_MESHREFINEMENT_API void registerObjects(sofa::core::ObjectFactory* factory); +} + +void initExternalModule() +{ + init(); +} + +const char* getModuleName() +{ + return sofa_tostring(SOFA_TARGET); +} + +const char* getModuleVersion() +{ + return sofa_tostring(PLUGIN_VERSION); +} + +void init() +{ + static bool first = true; + if (first) + { + sofa::helper::system::PluginManager::getInstance().registerPlugin(sofa_tostring(SOFA_TARGET)); + + // the engine this controller drives lives in MeshRefinement + sofa::meshrefinement::initMeshRefinement(); + first = false; + } +} + +void registerObjects(sofa::core::ObjectFactory* factory) +{ + registerTetrahedronSubdivisionController(factory); +#ifdef INFINYTOOLKIT_MESHREFINEMENT_USES_SOFACUDA + registerCudaTetrahedronSubdivisionController(factory); +#endif +} + +} // namespace sofa::infinytoolkit::meshrefinement diff --git a/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/init.h b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/init.h new file mode 100644 index 0000000..025cca6 --- /dev/null +++ b/extensions/MeshRefinement/src/InfinyToolkit/MeshRefinement/init.h @@ -0,0 +1,22 @@ +/******************************************************************************* + * - Copyright (C) 2026-Present InfinyTech3D - * + * All Rights Reserved * + * * + * The contents of this file are confidential and proprietary to InfinyTech3D. * + * Unauthorized copying, modification, distribution or disclosure of this * + * file, via any medium, is strictly prohibited. * + * * + * This file may only be used under the terms and conditions of a valid * + * commercial license agreement with InfinyTech3D. * + * * + * For licensing inquiries: contact@infinytech3d.com * + * Further information: https://infinytech3d.com * + ******************************************************************************/ +#pragma once + +#include + +namespace sofa::infinytoolkit::meshrefinement +{ +SOFA_INFINYTOOLKIT_MESHREFINEMENT_API void init(); +} // namespace sofa::infinytoolkit::meshrefinement diff --git a/extensions/MeshRefinement/tests/CMakeLists.txt b/extensions/MeshRefinement/tests/CMakeLists.txt new file mode 100644 index 0000000..6cf3357 --- /dev/null +++ b/extensions/MeshRefinement/tests/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.12) +project(InfinyToolkit.MeshRefinement_test) + +set(SOURCE_FILES + TetrahedronSubdivisionController_test.cpp +) + +add_definitions("-DINFINYTOOLKIT_MESHREFINEMENT_TEST_SCENES_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/scenes/\"") + +add_executable(${PROJECT_NAME} ${SOURCE_FILES}) +target_link_libraries(${PROJECT_NAME} InfinyToolkit.MeshRefinement Sofa.Testing Sofa.Component.Topology.Container.Dynamic) + +add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) diff --git a/extensions/MeshRefinement/tests/TetrahedronSubdivisionController_test.cpp b/extensions/MeshRefinement/tests/TetrahedronSubdivisionController_test.cpp new file mode 100644 index 0000000..6e2d901 --- /dev/null +++ b/extensions/MeshRefinement/tests/TetrahedronSubdivisionController_test.cpp @@ -0,0 +1,303 @@ +/** + * Covers the public API of TetrahedronSubdivisionController. + * + * The scenes live in scenes/ beside this file, so the test is self-contained and + * needs nothing from the MeshRefinement repository at runtime. They take their + * geometry and their cut parameters from the Data the scene sets, which the cases + * read back rather than hard-coding, so retuning a scene does not invalidate them. + * + * The controller is driven through its own methods as well as by key event. Each case + * asserts the topology actually changed: a cut or a subdivision that silently does + * nothing would otherwise pass. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using sofa::component::topology::container::dynamic::TetrahedronSetTopologyContainer; +using SubdivisionController3d = sofa::infinytoolkit::TetrahedronSubdivisionController; + +namespace +{ + +/// Everything these operations are expected to change. +struct TopologyCounts +{ + sofa::Size nbPoints{ 0 }; + sofa::Size nbEdges{ 0 }; + sofa::Size nbTriangles{ 0 }; + sofa::Size nbTetrahedra{ 0 }; + + bool operator==(const TopologyCounts& o) const + { + return nbPoints == o.nbPoints && nbEdges == o.nbEdges + && nbTriangles == o.nbTriangles && nbTetrahedra == o.nbTetrahedra; + } +}; + +std::ostream& operator<<(std::ostream& os, const TopologyCounts& c) +{ + return os << c.nbPoints << " points, " << c.nbEdges << " edges, " + << c.nbTriangles << " triangles, " << c.nbTetrahedra << " tetrahedra"; +} + + +class TetrahedronSubdivisionController_test : public sofa::testing::BaseTest +{ +public: + void doSetUp() override + { + // node::load needs a Simulation to exist. Plugins are preloaded rather than + // left to the scenes' RequiredPlugin, so that a single case run under + // --gtest_filter behaves like the whole suite. + m_simulation = sofa::simpleapi::createSimulation("DAG"); + sofa::helper::system::DataRepository.addFirstPath(INFINYTOOLKIT_MESHREFINEMENT_TEST_SCENES_DIR); + + for (const auto* plugin : { + "Sofa.Component.Collision.Detection.Algorithm", + "Sofa.Component.Collision.Detection.Intersection", + "Sofa.Component.Collision.Geometry", + "Sofa.Component.Collision.Response.Contact", + "Sofa.Component.Constraint.Projective", + "Sofa.Component.Engine.Select", + "Sofa.Component.IO.Mesh", + "Sofa.Component.LinearSolver.Iterative", + "Sofa.Component.Mapping.Linear", + "Sofa.Component.Mass", + "Sofa.Component.MechanicalLoad", + "Sofa.Component.ODESolver.Backward", + "Sofa.Component.SceneUtility", + "Sofa.Component.SolidMechanics.FEM.Elastic", + "Sofa.Component.StateContainer", + "Sofa.Component.Topology.Container.Constant", + "Sofa.Component.Topology.Container.Dynamic", + "Sofa.Component.Topology.Container.Grid", + "Sofa.Component.Topology.Mapping", + "Sofa.Component.Topology.Utility", + "Sofa.Component.Visual", + "Sofa.GL.Component.Rendering3D", + "MeshRefinement", + "InfinyToolkit.MeshRefinement", + }) + { + sofa::simpleapi::importPlugin(plugin); + } + } + + void doTearDown() override + { + if (m_root) + { + sofa::simulation::node::unload(m_root); + m_root.reset(); + } + } + +protected: + sofa::simulation::Simulation::SPtr m_simulation; + sofa::simulation::NodeSPtr m_root; + SubdivisionController3d* m_controller{ nullptr }; + TetrahedronSetTopologyContainer* m_topology{ nullptr }; + + /// Loads an example scene and resolves the controller and the topology it drives. + void loadScene(const std::string& sceneFile) + { + const std::string scenePath = sofa::helper::system::DataRepository.getFile(sceneFile); + m_root = sofa::simulation::node::load(scenePath); + ASSERT_NE(m_root.get(), nullptr) << "could not load " << sceneFile; + sofa::simulation::node::init(m_root.get()); + + m_controller = m_root->get(sofa::core::objectmodel::BaseContext::SearchDown); + ASSERT_NE(m_controller, nullptr) << "no TetrahedronSubdivisionController in " << sceneFile; + + // Resolved from the controller's own context, not from the root: a scene with a + // topological mapping holds more than one container and the first one found is + // not necessarily the one the controller operates on. + m_topology = m_controller->getContext()->get(); + ASSERT_NE(m_topology, nullptr) << "no TetrahedronSetTopologyContainer beside the controller"; + } + + TopologyCounts counts() const + { + return { m_topology->getNbPoints(), m_topology->getNbEdges(), + m_topology->getNbTriangles(), m_topology->getNbTetrahedra() }; + } + + void step(int count) + { + for (int i = 0; i < count; ++i) + sofa::simulation::node::animate(m_root.get(), m_root->getDt()); + } + + void pressKey(char key) + { + sofa::core::objectmodel::KeypressedEvent event(key); + m_root->propagateEvent(sofa::core::execparams::defaultInstance(), &event); + } +}; + + +/// The cut the 8 migrated cutting scenes perform, driven through the API. +TEST_F(TetrahedronSubdivisionController_test, cutFromPlane_changes_the_topology) +{ + loadScene("TetrahedronCutting_Advanced_01.scn"); + step(10); + + const TopologyCounts before = counts(); + // the scene's own plane, so the geometry stays owned by the scene file + EXPECT_TRUE(m_controller->cutFromPlane(m_controller->d_cutPointA.getValue(), + m_controller->d_cutPointB.getValue(), + m_controller->d_cutDirection.getValue(), + m_controller->d_cutDepth.getValue(), + false)); + step(1); + + const TopologyCounts after = counts(); + EXPECT_FALSE(after == before) << "the cut did nothing: still " << after; + EXPECT_GT(after.nbTetrahedra, before.nbTetrahedra); + std::cout << "[pv] cutFromPlane: " << before << " -> " << after << std::endl; +} + + +/// The same cut through the two-phase shape, which must reach the same result. +TEST_F(TetrahedronSubdivisionController_test, prepare_then_applyCut_matches_the_one_shot) +{ + loadScene("TetrahedronCutting_Advanced_01.scn"); + step(10); + + const TopologyCounts before = counts(); + EXPECT_TRUE(m_controller->prepareCutFromPlane(m_controller->d_cutPointA.getValue(), + m_controller->d_cutPointB.getValue(), + m_controller->d_cutDirection.getValue(), + m_controller->d_cutDepth.getValue())); + + // building the path must not touch the topology on its own + EXPECT_TRUE(counts() == before) << "prepareCutFromPlane already changed the topology"; + + EXPECT_TRUE(m_controller->applyCut(false)); + step(1); + + const TopologyCounts after = counts(); + EXPECT_FALSE(after == before) << "the cut did nothing: still " << after; + std::cout << "[pv] prepare+apply: " << before << " -> " << after << std::endl; +} + + +/// Keys '1' then '2' are what the migrated scenes rely on. +TEST_F(TetrahedronSubdivisionController_test, keys_1_then_2_cut) +{ + loadScene("TetrahedronCutting_Advanced_01.scn"); + step(10); + + const TopologyCounts before = counts(); + pressKey('1'); + step(1); + pressKey('2'); + step(1); + + const TopologyCounts after = counts(); + EXPECT_FALSE(after == before) << "keys 1,2 did not cut: still " << after; +} + + +/// Out-of-range tetrahedron ids must be refused rather than read out of bounds. This +/// is what SimpleCubeRefinement.scn used to do with testID="60" on a 44 tetrahedron +/// mesh: an access violation, not an error. +TEST_F(TetrahedronSubdivisionController_test, refineTetrahedra_rejects_bad_ids) +{ + loadScene("SimpleCubeRefinement.scn"); + step(10); + + const TopologyCounts before = counts(); + const std::set bad { m_topology->getNbTetrahedra() + 10u }; + + EXPECT_MSG_EMIT(Error); + EXPECT_FALSE(m_controller->refineTetrahedra(bad)); + EXPECT_TRUE(counts() == before) << "a refused refinement still changed the topology"; +} + + +/// Out-of-range triangle ids must be refused rather than read out of bounds. +TEST_F(TetrahedronSubdivisionController_test, cutFromTriangles_rejects_bad_ids) +{ + loadScene("TetrahedronCutting_Advanced_01.scn"); + step(10); + + const TopologyCounts before = counts(); + const auto nbTriangles = m_topology->getNbTriangles(); + + // BaseTest fails a test on any Error message, and the guard logs one, so the + // expectation has to be declared before the call. + EXPECT_MSG_EMIT(Error); + EXPECT_FALSE(m_controller->cutFromTriangles(nbTriangles + 10, nbTriangles + 20, 5.0, false)); + EXPECT_TRUE(counts() == before) << "a refused cut still changed the topology"; +} + + +/// Refinement of the tetrahedra named by the scene. +TEST_F(TetrahedronSubdivisionController_test, refineTetrahedra_changes_the_topology) +{ + loadScene("SimpleCubeRefinement.scn"); + step(10); + + const TopologyCounts before = counts(); + ASSERT_FALSE(m_controller->d_testID.getValue().empty()) << "the scene names no tetrahedron to refine"; + + EXPECT_TRUE(m_controller->refineTetrahedra(m_controller->d_testID.getValue())); + step(1); + + const TopologyCounts after = counts(); + EXPECT_FALSE(after == before) << "the refinement did nothing: still " << after; + EXPECT_GT(after.nbTetrahedra, before.nbTetrahedra); + std::cout << "[pv] refineTetrahedra: " << before << " -> " << after << std::endl; +} + + +/// Whole-mesh refinement, which the merge moved from key '2' to key '4'. +TEST_F(TetrahedronSubdivisionController_test, refineFullMesh_grows_the_mesh) +{ + loadScene("SimpleCubeRefinement.scn"); + step(10); + + const TopologyCounts before = counts(); + EXPECT_TRUE(m_controller->refineFullMesh()); + step(1); + + const TopologyCounts after = counts(); + EXPECT_GT(after.nbTetrahedra, before.nbTetrahedra) << "refineFullMesh did not subdivide: still " << after; + std::cout << "[pv] refineFullMesh: " << before << " -> " << after << std::endl; +} + + +/// Key '4' has to reach refineFullMesh, and '2' must no longer do so. +TEST_F(TetrahedronSubdivisionController_test, key_4_refines_the_full_mesh) +{ + loadScene("SimpleCubeRefinement.scn"); + step(10); + + const TopologyCounts before = counts(); + + // '2' applies a cut now; with no cut prepared it must leave the mesh alone + pressKey('2'); + step(1); + EXPECT_TRUE(counts() == before) << "key 2 changed the mesh with no cut prepared"; + + pressKey('4'); + step(1); + const TopologyCounts after = counts(); + EXPECT_GT(after.nbTetrahedra, before.nbTetrahedra) << "key 4 did not refine: still " << after; +} + +} // namespace diff --git a/extensions/MeshRefinement/tests/scenes/SimpleCubeRefinement.scn b/extensions/MeshRefinement/tests/scenes/SimpleCubeRefinement.scn new file mode 100644 index 0000000..5dcc31e --- /dev/null +++ b/extensions/MeshRefinement/tests/scenes/SimpleCubeRefinement.scn @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/MeshRefinement/tests/scenes/TetrahedronCutting_Advanced_01.scn b/extensions/MeshRefinement/tests/scenes/TetrahedronCutting_Advanced_01.scn new file mode 100644 index 0000000..6b9e13a --- /dev/null +++ b/extensions/MeshRefinement/tests/scenes/TetrahedronCutting_Advanced_01.scn @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/MeshRefinement/tests/scenes/headers.xml b/extensions/MeshRefinement/tests/scenes/headers.xml new file mode 100644 index 0000000..af78ba1 --- /dev/null +++ b/extensions/MeshRefinement/tests/scenes/headers.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extensions/MeshRefinement/tests/scenes/mesh/cube_low_res.msh b/extensions/MeshRefinement/tests/scenes/mesh/cube_low_res.msh new file mode 100644 index 0000000..b970e8b --- /dev/null +++ b/extensions/MeshRefinement/tests/scenes/mesh/cube_low_res.msh @@ -0,0 +1,79 @@ +$MeshFormat +2.2 0 8 +$EndMeshFormat +$Nodes +26 +1 -3.5 3.5 5.702223688786035e-07 +2 -3.5 5.702223688786035e-07 -3.5 +3 -3.5 0 0 +4 -3.5 -3.5 -5.702223688786035e-07 +5 -3.5 -5.702223688786035e-07 3.5 +6 -3.5 -3.499999523162842 -3.500000476837158 +7 -3.5 -3.500000476837158 3.499999523162842 +8 -3.5 3.499999523162842 3.500000476837158 +9 -3.5 3.500000476837158 -3.499999523162842 +10 3.5 -3.5 -5.702223688786035e-07 +11 3.5 5.702223688786035e-07 -3.5 +12 3.5 0 0 +13 3.5 3.5 5.702223688786035e-07 +14 3.5 -5.702223688786035e-07 3.5 +15 3.5 3.500000476837158 -3.499999523162842 +16 3.5 3.499999523162842 3.500000476837158 +17 3.5 -3.500000476837158 3.499999523162842 +18 3.5 -3.499999523162842 -3.500000476837158 +19 0 -3.499999523162842 -3.500000476837158 +20 0 5.702223688786035e-07 -3.5 +21 0 3.500000476837158 -3.499999523162842 +22 0 -3.500000476837158 3.499999523162842 +23 0 -5.702223688786035e-07 3.5 +24 0 3.499999523162842 3.500000476837158 +25 0 3.5 5.702223688786035e-07 +26 0 -3.5 -5.702223688786035e-07 +$EndNodes +$Elements +44 +1 4 2 0 1 23 3 12 26 +2 4 2 0 1 23 3 25 12 +3 4 2 0 1 25 3 20 12 +4 4 2 0 1 3 20 12 26 +5 4 2 0 1 5 4 3 26 +6 4 2 0 1 4 6 3 19 +7 4 2 0 1 11 15 21 12 +8 4 2 0 1 14 13 25 16 +9 4 2 0 1 16 25 14 24 +10 4 2 0 1 1 20 3 2 +11 4 2 0 1 20 1 3 25 +12 4 2 0 1 26 5 23 3 +13 4 2 0 1 10 11 20 12 +14 4 2 0 1 5 4 26 7 +15 4 2 0 1 15 13 21 12 +16 4 2 0 1 19 2 6 3 +17 4 2 0 1 17 23 12 22 +18 4 2 0 1 17 23 14 12 +19 4 2 0 1 22 17 10 12 +20 4 2 0 1 1 8 3 24 +21 4 2 0 1 3 8 5 23 +22 4 2 0 1 3 4 19 26 +23 4 2 0 1 3 1 24 25 +24 4 2 0 1 20 11 21 12 +25 4 2 0 1 14 13 12 25 +26 4 2 0 1 20 10 12 26 +27 4 2 0 1 14 25 12 23 +28 4 2 0 1 20 1 25 9 +29 4 2 0 1 5 26 22 7 +30 4 2 0 1 5 26 23 22 +31 4 2 0 1 20 1 9 2 +32 4 2 0 1 11 10 20 18 +33 4 2 0 1 23 3 24 25 +34 4 2 0 1 20 3 19 26 +35 4 2 0 1 3 8 23 24 +36 4 2 0 1 22 23 12 26 +37 4 2 0 1 25 20 21 12 +38 4 2 0 1 10 22 12 26 +39 4 2 0 1 19 2 3 20 +40 4 2 0 1 13 25 21 12 +41 4 2 0 1 20 10 26 18 +42 4 2 0 1 14 25 23 24 +43 4 2 0 1 18 26 20 19 +44 4 2 0 1 20 9 25 21 +$EndElements