From ee80b26e426b1044dd5583899d3a8adf3e32511c Mon Sep 17 00:00:00 2001 From: Alex Bilger Date: Mon, 27 Jul 2026 10:16:06 +0200 Subject: [PATCH] Introduce clamped scalar ensuring value not outside bounds --- .../src/sofa/component/visual/VisualMesh.cpp | 2 +- .../src/sofa/component/visual/VisualMesh.h | 3 +- Sofa/framework/Type/CMakeLists.txt | 2 + .../Type/src/sofa/type/ClampedScalar.cpp | 31 +++ .../Type/src/sofa/type/ClampedScalar.h | 169 ++++++++++++++++ Sofa/framework/Type/test/CMakeLists.txt | 1 + .../Type/test/ClampedScalar_test.cpp | 189 ++++++++++++++++++ 7 files changed, 395 insertions(+), 2 deletions(-) create mode 100644 Sofa/framework/Type/src/sofa/type/ClampedScalar.cpp create mode 100644 Sofa/framework/Type/src/sofa/type/ClampedScalar.h create mode 100644 Sofa/framework/Type/test/ClampedScalar_test.cpp diff --git a/Sofa/Component/Visual/src/sofa/component/visual/VisualMesh.cpp b/Sofa/Component/Visual/src/sofa/component/visual/VisualMesh.cpp index d39edbc9595..f966991f44a 100644 --- a/Sofa/Component/Visual/src/sofa/component/visual/VisualMesh.cpp +++ b/Sofa/Component/Visual/src/sofa/component/visual/VisualMesh.cpp @@ -35,7 +35,7 @@ void registerVisualMesh(sofa::core::ObjectFactory* factory) VisualMesh::VisualMesh() : d_position(initData(&d_position, "position", "The position of the vertices of mesh")) - , d_elementSpace(initData(&d_elementSpace, 0.15_sreal, "elementSpace", + , d_elementSpace(initData(&d_elementSpace, {0.15_sreal, {0_sreal, 1_sreal}}, "elementSpace", "The space between element (scalar between 0 and 1)")) , d_lighting(initData(&d_lighting, true, "lighting", "If true, light is simulated on the mesh. Otherwise, no lighting effect.")) , d_vertexValues(initData(&d_vertexValues, "vertexValues", "Optional list of values associated to the vertices of the mesh. If provided, the values are converted to colors.")) diff --git a/Sofa/Component/Visual/src/sofa/component/visual/VisualMesh.h b/Sofa/Component/Visual/src/sofa/component/visual/VisualMesh.h index 5577e8f5a57..bd3c8cf4f62 100644 --- a/Sofa/Component/Visual/src/sofa/component/visual/VisualMesh.h +++ b/Sofa/Component/Visual/src/sofa/component/visual/VisualMesh.h @@ -26,6 +26,7 @@ #include #include #include +#include namespace sofa::component::visual { @@ -36,7 +37,7 @@ class VisualMesh : public core::visual::VisualModel SOFA_CLASS(VisualMesh, core::visual::VisualModel); Data> d_position; - Data d_elementSpace; + Data> d_elementSpace; Data d_lighting; diff --git a/Sofa/framework/Type/CMakeLists.txt b/Sofa/framework/Type/CMakeLists.txt index 9d8596f2809..1688224668e 100644 --- a/Sofa/framework/Type/CMakeLists.txt +++ b/Sofa/framework/Type/CMakeLists.txt @@ -7,6 +7,7 @@ set(HEADER_FILES ${SOFATYPESRC_ROOT}/config.h.in ${SOFATYPESRC_ROOT}/init.h ${SOFATYPESRC_ROOT}/BoundingBox.h + ${SOFATYPESRC_ROOT}/ClampedScalar.h ${SOFATYPESRC_ROOT}/DualQuat.h ${SOFATYPESRC_ROOT}/DualQuat.inl ${SOFATYPESRC_ROOT}/Frame.h @@ -58,6 +59,7 @@ set(HEADER_FILES set(SOURCE_FILES ${SOFATYPESRC_ROOT}/init.cpp ${SOFATYPESRC_ROOT}/BoundingBox.cpp + ${SOFATYPESRC_ROOT}/ClampedScalar.cpp ${SOFATYPESRC_ROOT}/DualQuat.cpp ${SOFATYPESRC_ROOT}/Frame.cpp ${SOFATYPESRC_ROOT}/Mat.cpp diff --git a/Sofa/framework/Type/src/sofa/type/ClampedScalar.cpp b/Sofa/framework/Type/src/sofa/type/ClampedScalar.cpp new file mode 100644 index 00000000000..c2197921853 --- /dev/null +++ b/Sofa/framework/Type/src/sofa/type/ClampedScalar.cpp @@ -0,0 +1,31 @@ +/****************************************************************************** +* SOFA, Simulation Open-Framework Architecture * +* (c) 2006 INRIA, USTL, UJF, CNRS, MGH * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Authors: The SOFA Team and external contributors (see Authors.txt) * +* * +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#define SOFA_TYPE_CLAMPEDSCALAR_CPP +#include + +namespace sofa::type +{ + +template class SOFA_TYPE_API ClampedScalar; +template class SOFA_TYPE_API ClampedScalar; + +} diff --git a/Sofa/framework/Type/src/sofa/type/ClampedScalar.h b/Sofa/framework/Type/src/sofa/type/ClampedScalar.h new file mode 100644 index 00000000000..c41dfe22911 --- /dev/null +++ b/Sofa/framework/Type/src/sofa/type/ClampedScalar.h @@ -0,0 +1,169 @@ +/****************************************************************************** +* SOFA, Simulation Open-Framework Architecture * +* (c) 2006 INRIA, USTL, UJF, CNRS, MGH * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Authors: The SOFA Team and external contributors (see Authors.txt) * +* * +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#pragma once + +#include + +#include +#include + +namespace sofa::type +{ + +/** + * @class ClampedScalar + * @brief A templated class that ensures a stored scalar value never falls outside + * a defined minimum and maximum range [minBound, maxBound]. + * + * The bounds are set during construction and cannot be changed afterward. + * + * @tparam T The underlying numeric type (e.g., float, double). + */ +template +class ClampedScalar +{ +private: + T m_value; // The current stored value, guaranteed to be within bounds. + T m_minBound; // The absolute minimum allowed value (immutable after construction). + T m_maxBound; // The absolute maximum allowed value (immutable after construction). + +public: + /** + * @brief Constructor initializes the scalar with defined bounds and an initial value. + * + * Note: The constructor automatically clamps the provided initial value against + * the given bounds. + * + * @param initialValue The starting value (will be clamped if out of range). + * @param bounds A pair defining {{min_val, max_val}}. The actual min/max are derived from this pair. + */ + constexpr ClampedScalar(T initialValue = {}, const std::pair bounds = {{}, static_cast(1)}) + : m_minBound(std::min(bounds.first, bounds.second)), m_maxBound(std::max(bounds.first, bounds.second)) + { + // Use the internal setter to ensure the initial value is correctly clamped + setValue(initialValue); + } + + constexpr T getMinBound() const + { + return m_minBound; + } + + constexpr T getMaxBound() const + { + return m_maxBound; + } + + /** + * @brief Sets the internal value after clamping it against the established bounds [minBound, maxBound]. + * + * This method is used internally by constructors and setters. + * + * @param rawValue The raw input value (may be outside bounds). + */ + constexpr void setValue(T rawValue) + { + m_value = std::clamp(rawValue, m_minBound, m_maxBound); + } + + /** + * @brief Returns the current clamped value. + * @return The stored value, guaranteed to be within [min_bound_, max_bound_]. + */ + constexpr T getValue() const + { + return m_value; + } + + /** + * @brief Calculates what the clamped result of a given input would be + * without modifying the object's state. Useful for prediction/calculation. + * @param rawValue The unconstrained input value. + * @return The clamped version of rawValue. + */ + constexpr T calculateClamped(T rawValue) const + { + return std::clamp(rawValue, m_minBound, m_maxBound); + } + + /** + * @brief Conversion operator allows the ClampedScalar to be implicitly treated as its underlying type T. + * @return The stored, clamped value (T). + */ + constexpr operator T() const + { + return m_value; + } + + /** + * @brief Overloads assignment with a raw scalar value for concise state updates. + * + * This operation clamps the new value against the existing bounds and updates the internal state. + * + * @param newValue The raw input value to assign. + * @return Reference to this object. + */ + constexpr ClampedScalar& operator=(T newValue) + { + setValue(newValue); + return *this; + } +}; + +/** + * @brief Overload stream insertion operator (<<). + * Writes only the clamped value to the stream. Bounds are ignored. + * @param o The output stream reference. + * @param s The ClampedScalar object. + * @return Reference to the output stream. + */ +template +std::ostream& operator<<(std::ostream& o, const sofa::type::ClampedScalar& s) +{ + return o << s.getValue(); +} + +/** + * @brief Overload stream extraction operator (>>). + * Reads a raw value from the stream and clamps it against the stored bounds before updating the object's state. + * Bounds are not read or written during this operation. + * @param i The input stream reference. + * @param s The ClampedScalar object to be updated. + * @return Reference to the input stream. + */ +template +std::istream& operator>>(std::istream& i, sofa::type::ClampedScalar& s) +{ + T rawValue{}; + i >> rawValue; + s.setValue(rawValue); + return i; +} + +#if !defined(SOFA_TYPE_CLAMPEDSCALAR_CPP) +extern template class SOFA_TYPE_API ClampedScalar; +extern template class SOFA_TYPE_API ClampedScalar; +#endif + +} + + diff --git a/Sofa/framework/Type/test/CMakeLists.txt b/Sofa/framework/Type/test/CMakeLists.txt index ae528c31343..faa2e8711bb 100644 --- a/Sofa/framework/Type/test/CMakeLists.txt +++ b/Sofa/framework/Type/test/CMakeLists.txt @@ -4,6 +4,7 @@ project(Sofa.Type_test) set(SOURCE_FILES BoundingBox_test.cpp + ClampedScalar_test.cpp MatSym_test.cpp MatTypes_test.cpp Material_test.cpp diff --git a/Sofa/framework/Type/test/ClampedScalar_test.cpp b/Sofa/framework/Type/test/ClampedScalar_test.cpp new file mode 100644 index 00000000000..d4bfb08c558 --- /dev/null +++ b/Sofa/framework/Type/test/ClampedScalar_test.cpp @@ -0,0 +1,189 @@ +/****************************************************************************** + * SOFA, Simulation Open-Framework Architecture * + * (c) 2006 INRIA, USTL, UJF, CNRS, MGH * + * * + * This program is free software; you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as published by * + * the Free Software Foundation; either version 2.1 of the License, or (at * + * your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, but WITHOUT * + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * + * for more details. * + * * + * You should have received a copy of the GNU Lesser General Public License * + * along with this program. If not, see . * + ******************************************************************************* + * Authors: The SOFA Team and external contributors (see Authors.txt) * + * * + * Contact information: contact@sofa-framework.org * + ******************************************************************************/ +#include +#include + +namespace sofa +{ + +using namespace sofa::type; + +TEST(ClampedScalarTestDouble, ConstructionWithinBounds) +{ + constexpr double MIN_VAL = -10.0; + constexpr double MAX_VAL = 20.0; + constexpr double INIT_VAL = 5.0; + + // Initialize using bounds and a valid value + ClampedScalar s(INIT_VAL, {MIN_VAL, MAX_VAL}); + + EXPECT_DOUBLE_EQ(s.getMinBound(), MIN_VAL); + EXPECT_DOUBLE_EQ(s.getMaxBound(), MAX_VAL); + EXPECT_DOUBLE_EQ(s.getValue(), INIT_VAL); +} + +TEST(ClampedScalarTestDouble, ConstructionClampsValueTooHigh) +{ + constexpr double MIN_VAL = 0.0; + constexpr double MAX_VAL = 10.0; + constexpr double OVERFLOW_VAL = 99.0; + + // Initialize with a value higher than max bound + ClampedScalar s(OVERFLOW_VAL, {MIN_VAL, MAX_VAL}); + + EXPECT_DOUBLE_EQ(s.getMinBound(), MIN_VAL); + EXPECT_DOUBLE_EQ(s.getMaxBound(), MAX_VAL); + // The value should be clamped to the maximum bound (10.0) + EXPECT_DOUBLE_EQ(s.getValue(), 10.0); +} + +TEST(ClampedScalarTestDouble, ConstructionClampsValueTooLow) +{ + constexpr double MIN_VAL = -5.0; + constexpr double MAX_VAL = 5.0; + constexpr double UNDERFLOW_VAL = -50.0; + + // Initialize with a value lower than min bound + ClampedScalar s(UNDERFLOW_VAL, {MIN_VAL, MAX_VAL}); + + EXPECT_DOUBLE_EQ(s.getMinBound(), MIN_VAL); + EXPECT_DOUBLE_EQ(s.getMaxBound(), MAX_VAL); + // The value should be clamped to the minimum bound (-5.0) + EXPECT_DOUBLE_EQ(s.getValue(), -5.0); +} + +TEST(ClampedScalarTestDouble, ConstructionHandlesReversedBounds) +{ + constexpr double MIN_VAL = 10.0; + constexpr double MAX_VAL = 0.0; // Input bounds reversed (10 > 0) + constexpr double INIT_VAL = 5.0; + + // Check if the constructor correctly determines min/max regardless of input order + ClampedScalar s(INIT_VAL, {MAX_VAL, MIN_VAL}); + + EXPECT_DOUBLE_EQ(s.getMinBound(), 0.0); // Should use std::min + EXPECT_DOUBLE_EQ(s.getMaxBound(), 10.0); // Should use std::max +} + +TEST(ClampedScalarTestDouble, SetterClampsValueTooHigh) +{ + constexpr double MIN_VAL = -5.0; + constexpr double MAX_VAL = 15.0; + + // Start with a valid initial value + ClampedScalar s(5.0, {MIN_VAL, MAX_VAL}); + + // Set a value higher than max bound (20.0 -> should become 15.0) + s = 20.0; + EXPECT_DOUBLE_EQ(s.getValue(), 15.0); + + // Test setting exactly to the maximum bound + s = 15.0; + EXPECT_DOUBLE_EQ(s.getValue(), 15.0); +} + +TEST(ClampedScalarTestDouble, SetterClampsValueTooLow) +{ + constexpr double MIN_VAL = -15.0; + constexpr double MAX_VAL = 15.0; + + // Start with a valid initial value + ClampedScalar s(5.0, {MIN_VAL, MAX_VAL}); + + // Set a value lower than min bound (-20.0 -> should become -15.0) + s = -20.0; + EXPECT_DOUBLE_EQ(s.getValue(), -15.0); +} + +TEST(ClampedScalarTestDouble, CalculateClampedPredictionHigh) +{ + constexpr double MIN_VAL = 0.0; + constexpr double MAX_VAL = 10.0; + // Start state doesn't matter for prediction + ClampedScalar s(5.0, {MIN_VAL, MAX_VAL}); + + // Predict value far above max bound (99.0 -> should be 10.0) + EXPECT_DOUBLE_EQ(s.calculateClamped(99.0), 10.0); + + // Predict value slightly above max bound (10.1 -> should be 10.0) + EXPECT_DOUBLE_EQ(s.calculateClamped(10.1), 10.0); +} + +TEST(ClampedScalarTestDouble, CalculateClampedPredictionLow) +{ + constexpr double MIN_VAL = -10.0; + constexpr double MAX_VAL = 10.0; + ClampedScalar s(5.0, {MIN_VAL, MAX_VAL}); + + // Predict value far below min bound (-99.0 -> should be -10.0) + EXPECT_DOUBLE_EQ(s.calculateClamped(-99.0), -10.0); + + // Predict value slightly below min bound (-10.1 -> should be -10.0) + EXPECT_DOUBLE_EQ(s.calculateClamped(-10.1), -10.0); +} + +TEST(ClampedScalarTestDouble, StreamOperatorOverloadOutput) +{ + constexpr double MIN_VAL = -10.0; + constexpr double MAX_VAL = 10.0; + // Use a string stream to capture output + std::stringstream ss; + + // Clamping doesn't affect the printed value, only getValue() matters here. + ClampedScalar s(5.0, {MIN_VAL, MAX_VAL}); + + ss << s; // Should print 5.0 + EXPECT_EQ(ss.str(), "5"); + + // Test stream output after setting a clamped value (15.0 -> becomes 10.0) + s = 15.0; // Clamps to 10.0 + std::stringstream ss2; + ss2 << s; + EXPECT_EQ(ss2.str(), "10"); +} + +TEST(ClampedScalarTestDouble, StreamOperatorOverloadInput) +{ + constexpr double MIN_VAL = -5.0; + constexpr double MAX_VAL = 5.0; + // Use a string stream to simulate input data + std::stringstream ss("2"); // Input value is 2.0 + + ClampedScalar s(0.0, {MIN_VAL, MAX_VAL}); + + ss >> s; // Reads 2.0 (within bounds) + EXPECT_DOUBLE_EQ(s.getValue(), 2.0); + + // Test reading a value that is too high (15.0 -> clamps to 5.0) + std::stringstream ss_high("15"); + ClampedScalar s_high(0.0, {MIN_VAL, MAX_VAL}); + ss_high >> s_high; + EXPECT_DOUBLE_EQ(s_high.getValue(), 5.0); + + // Test reading a value that is too low (-20.0 -> clamps to -5.0) + std::stringstream ss_low("-20"); + ClampedScalar s_low(0.0, {MIN_VAL, MAX_VAL}); + ss_low >> s_low; + EXPECT_DOUBLE_EQ(s_low.getValue(), -5.0); +} + +} // namespace sofa