// Copyright (c) 2025 OPEN CASCADE SAS // // This file is part of Open CASCADE Technology software library. // // This library is free software; you can redistribute it and/or modify it under // the terms of the GNU Lesser General Public License version 2.1 as published // by the Free Software Foundation, with special exception defined in the file // OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT // distribution for complete text of the license and disclaimer of any warranty. // // Alternatively, this file may be used under the terms of Open CASCADE // commercial license or contractual agreement. #ifndef _ExtremaPC_HeaderFile #define _ExtremaPC_HeaderFile #include "gp_Pnt.hxx" #include "MathUtils_Domain.hxx" #include "NCollection_DynamicArray.hxx" #include "Precision.hxx" #include #include //! @file ExtremaPC.hxx //! @brief Common types and utilities for Point-Curve extrema computation. //! //! The ExtremaPC package provides modern C++ implementation of point-curve //! extrema computation using std::variant for curve type dispatch and //! BVH-based hierarchical algorithms for numerical curves. namespace ExtremaPC { //================================================================================================== //! @name Precision Constants //! Centralized tolerance and precision values used throughout the ExtremaPC package. //! These replace magic numbers to improve code clarity and maintainability. //================================================================================================== //! Default tolerance for root finding and distance comparison. //! Used when no explicit tolerance is provided. constexpr double THE_DEFAULT_TOLERANCE = Precision::Confusion(); //! Tolerance for parameter domain comparison (cache validation). //! Uses PConfusion which is appropriate for parametric space. constexpr double THE_PARAM_TOLERANCE = Precision::PConfusion(); //! Ratio of parameter range for neighbor point sampling. //! Used to evaluate if an endpoint is a local extremum. constexpr double THE_NEIGHBOR_STEP_RATIO = 0.01; //! Ratio of parameter range for search interval expansion. //! Used in grid-based methods to expand candidate intervals. constexpr double THE_INTERVAL_EXPAND_RATIO = 0.05; //! Ratio of parameter range for refinement step. //! Used in iterative refinement algorithms. constexpr double THE_REFINEMENT_STEP_RATIO = 0.001; //! Multiplier for X (parameter) tolerance in Newton refinement. constexpr double THE_NEWTON_XTOL_FACTOR = 0.01; //! Multiplier for F (function) tolerance in Newton refinement. constexpr double THE_NEWTON_FTOL_FACTOR = 0.001; //! Default search radius for hint-based search (fraction of parameter range). constexpr double THE_HINT_SEARCH_RADIUS = 0.1; //! Factor for narrowing parameter range during refinement iterations. constexpr double THE_RANGE_NARROWING_FACTOR = 0.25; //! Threshold for skipping max candidates that are clearly worse. //! If estimated distance < best * threshold, skip candidate. constexpr double THE_MAX_SKIP_THRESHOLD = 0.9; //! Multiplier for near-zero F detection during grid scan. //! F values smaller than tolerance * this factor are considered near-zero. constexpr double THE_NEAR_ZERO_F_FACTOR = 10.0; //! Multiplier for fallback F tolerance when Newton fails. //! Used to accept grid points as approximate solutions. constexpr double THE_FALLBACK_F_FACTOR = 100.0; //! Maximum number of Newton iterations for refinement. constexpr int THE_MAX_NEWTON_ITERATIONS = 20; //! Number of samples for iterative grid refinement fallback. constexpr int THE_REFINEMENT_NB_SAMPLES = 20; //! Number of refinement passes when Newton fails. constexpr int THE_REFINEMENT_NB_PASSES = 3; //! Minimum number of samples for Bezier curves. constexpr int THE_BEZIER_MIN_SAMPLES = 24; //! Multiplier for degree to compute Bezier samples: samples = max(min, multiplier * (degree + 1)). constexpr int THE_BEZIER_DEGREE_MULTIPLIER = 3; //! Default number of samples for general (other) curves. constexpr int THE_OTHER_CURVE_NB_SAMPLES = 64; //! Fallback number of samples for BSpline curves when curve is null. constexpr int THE_BSPLINE_FALLBACK_SAMPLES = 32; //! Multiplier for BSpline curve samples per knot span: samples = multiplier * (degree + 1). //! For a degree 3 curve: 2*4 = 8 samples per span. //! This is higher than the surface counterpart because curves are 1D and require //! finer sampling to detect extrema reliably. Surfaces use degree+2 per direction, //! resulting in (degree+2)^2 samples per cell, which provides adequate coverage. constexpr int THE_BSPLINE_SPAN_MULTIPLIER = 2; //! 1D parameter domain for curves (alias for MathUtils::Domain1D). using Domain1D = MathUtils::Domain1D; //! Status of extrema computation. enum class Status { OK, //!< Computation succeeded, finite number of extrema found NotDone, //!< Computation not performed InfiniteSolutions, //!< Infinite solutions exist (e.g., point at circle center) NoSolution, //!< No extrema found in the given parameter range NumericalError //!< Numerical issues during computation }; //! Search mode for extrema computation. //! Controls which extrema to find, enabling performance optimizations. enum class SearchMode { MinMax, //!< Find all extrema (both minima and maxima) - default Min, //!< Find only minimum distance (enables early termination in BVH) Max //!< Find only maximum distance }; //! Result of a single extremum computation. struct ExtremumResult { double Parameter = 0.0; //!< Parameter value on curve gp_Pnt Point; //!< Point on curve at parameter double SquareDistance = 0.0; //!< Square of the distance from query point to curve point bool IsMinimum = true; //!< True if this is a local minimum, false if maximum }; //! Result of extrema computation containing all found extrema. //! Non-copyable to enforce use of const reference from Perform(). struct Result { ExtremaPC::Status Status = ExtremaPC::Status::NotDone; //!< Computation status NCollection_DynamicArray Extrema{8}; //!< Collection of found extrema //! For infinite solutions, stores the constant squared distance. //! Only meaningful when Status == Status::InfiniteSolutions. double InfiniteSquareDistance = 0.0; //! Default constructor. Result() = default; //! Copy constructor is deleted. Result(const Result&) = delete; //! Copy assignment is deleted. Result& operator=(const Result&) = delete; //! Move constructor. Result(Result&&) = default; //! Move assignment. Result& operator=(Result&&) = default; //! Returns true if computation succeeded with finite number of extrema. bool IsDone() const { return Status == Status::OK; } //! Returns true if there are infinite solutions. bool IsInfinite() const { return Status == Status::InfiniteSolutions; } //! Returns number of extrema found (0 if infinite or failed). int NbExt() const { return Extrema.Length(); } //! Access extremum by 0-based index. const ExtremumResult& operator[](int theIndex) const { return Extrema.Value(theIndex); } //! Returns the squared distance of the closest extremum. //! Returns infinity if no extrema found. double MinSquareDistance() const { if (Extrema.IsEmpty()) { return std::numeric_limits::infinity(); } double aMinSqDist = Extrema.Value(0).SquareDistance; for (int i = 1; i < Extrema.Length(); ++i) { if (Extrema.Value(i).SquareDistance < aMinSqDist) { aMinSqDist = Extrema.Value(i).SquareDistance; } } return aMinSqDist; } //! Returns the index of the closest extremum (0-based). //! Returns -1 if no extrema found. int MinIndex() const { if (Extrema.IsEmpty()) { return -1; } int aMinIdx = 0; double aMinSqDist = Extrema.Value(0).SquareDistance; for (int i = 1; i < Extrema.Length(); ++i) { if (Extrema.Value(i).SquareDistance < aMinSqDist) { aMinSqDist = Extrema.Value(i).SquareDistance; aMinIdx = i; } } return aMinIdx; } //! Returns the squared distance of the farthest extremum. //! Returns 0 if no extrema found. double MaxSquareDistance() const { if (Extrema.IsEmpty()) { return 0.0; } double aMaxSqDist = Extrema.Value(0).SquareDistance; for (int i = 1; i < Extrema.Length(); ++i) { if (Extrema.Value(i).SquareDistance > aMaxSqDist) { aMaxSqDist = Extrema.Value(i).SquareDistance; } } return aMaxSqDist; } //! Returns the index of the farthest extremum (0-based). //! Returns -1 if no extrema found. int MaxIndex() const { if (Extrema.IsEmpty()) { return -1; } int aMaxIdx = 0; double aMaxSqDist = Extrema.Value(0).SquareDistance; for (int i = 1; i < Extrema.Length(); ++i) { if (Extrema.Value(i).SquareDistance > aMaxSqDist) { aMaxSqDist = Extrema.Value(i).SquareDistance; aMaxIdx = i; } } return aMaxIdx; } //! Clear the result for reuse. //! Preserves allocated memory in Extrema vector. void Clear() { Status = Status::NotDone; Extrema.Clear(); InfiniteSquareDistance = 0.0; } }; //! Configuration for extrema computation. struct Config { double Tolerance = THE_DEFAULT_TOLERANCE; //!< Tolerance for root finding std::optional Domain; //!< Parameter domain (nullopt = use natural/unbounded) int NbSamples = 32; //!< Number of samples for numerical methods SearchMode Mode = SearchMode::MinMax; //!< Search mode (MinMax, Min, or Max) bool IncludeEndpoints = true; //!< Include endpoints as potential extrema }; //! @brief Adds endpoint extrema to result for bounded curves. //! //! This function adds the curve endpoints as extrema when: //! - The parameter bounds are finite //! - The endpoint is a true local extremum (checked via neighbor distance) //! - The endpoint doesn't duplicate an existing extremum //! //! An endpoint is considered a local minimum if the distance to a neighboring //! point on the curve is greater than the distance to the endpoint. //! An endpoint is considered a local maximum if the distance to a neighboring //! point is less than the distance to the endpoint. //! //! @tparam CurveEvaluator Type with Value(double) method returning gp_Pnt //! @param theResult result to add endpoints to //! @param theP query point //! @param theDomain parameter domain //! @param theEval curve evaluator //! @param theTol tolerance for duplicate detection //! @param theMode search mode template inline void AddEndpointExtrema(Result& theResult, const gp_Pnt& theP, const Domain1D& theDomain, const CurveEvaluator& theEval, double theTol, SearchMode theMode) { // Check for infinite bounds if (!theDomain.IsFinite()) { return; } const double theUMin = theDomain.Min; const double theUMax = theDomain.Max; // Helper to check if parameter or point already exists in result auto isDuplicate = [&](double theU, const gp_Pnt& thePt) -> bool { for (int i = 0; i < theResult.Extrema.Length(); ++i) { // Check parameter proximity if (std::abs(theResult.Extrema.Value(i).Parameter - theU) < theTol) { return true; } // Check point proximity (handles periodic curves where different params map to same point) double aSqDist = theResult.Extrema.Value(i).Point.SquareDistance(thePt); if (aSqDist < theTol * theTol) { return true; } } return false; }; // Evaluate endpoints gp_Pnt aPtMin = theEval.Value(theUMin); gp_Pnt aPtMax = theEval.Value(theUMax); double aSqDistMin = theP.SquareDistance(aPtMin); double aSqDistMax = theP.SquareDistance(aPtMax); // Sample step for neighbor point evaluation double aStep = (theUMax - theUMin) * THE_NEIGHBOR_STEP_RATIO; if (aStep < theTol) { aStep = theTol; } // Check if UMin is a local extremum by comparing to neighbor gp_Pnt aNeighborMin = theEval.Value(theUMin + aStep); double aNeighborDistMin = theP.SquareDistance(aNeighborMin); bool aIsMinAtUMin = (aSqDistMin <= aNeighborDistMin); bool aIsMaxAtUMin = (aSqDistMin >= aNeighborDistMin); // Check if UMax is a local extremum by comparing to neighbor gp_Pnt aNeighborMax = theEval.Value(theUMax - aStep); double aNeighborDistMax = theP.SquareDistance(aNeighborMax); bool aIsMinAtUMax = (aSqDistMax <= aNeighborDistMax); bool aIsMaxAtUMax = (aSqDistMax >= aNeighborDistMax); // Also check if endpoints themselves are duplicates (for periodic curves) bool aEndpointsAreSame = aPtMin.SquareDistance(aPtMax) < theTol * theTol; // For periodic/closed curves, there are no true endpoints to consider if (aEndpointsAreSame) { return; } // Add endpoints only if they are true local extrema matching the search mode if (theMode == SearchMode::Min || theMode == SearchMode::MinMax) { // Add UMin if it's a local minimum if (aIsMinAtUMin && !isDuplicate(theUMin, aPtMin)) { ExtremumResult anExt; anExt.Parameter = theUMin; anExt.Point = aPtMin; anExt.SquareDistance = aSqDistMin; anExt.IsMinimum = true; theResult.Extrema.Append(anExt); } // Add UMax if it's a local minimum (and not same point as UMin) if (aIsMinAtUMax && !aEndpointsAreSame && !isDuplicate(theUMax, aPtMax)) { ExtremumResult anExt; anExt.Parameter = theUMax; anExt.Point = aPtMax; anExt.SquareDistance = aSqDistMax; anExt.IsMinimum = true; theResult.Extrema.Append(anExt); } } if (theMode == SearchMode::Max || theMode == SearchMode::MinMax) { // Add UMin if it's a local maximum if (aIsMaxAtUMin && !aIsMinAtUMin && !isDuplicate(theUMin, aPtMin)) { ExtremumResult anExt; anExt.Parameter = theUMin; anExt.Point = aPtMin; anExt.SquareDistance = aSqDistMin; anExt.IsMinimum = false; theResult.Extrema.Append(anExt); } // Add UMax if it's a local maximum (and not same point as UMin) if (aIsMaxAtUMax && !aIsMinAtUMax && !aEndpointsAreSame && !isDuplicate(theUMax, aPtMax)) { ExtremumResult anExt; anExt.Parameter = theUMax; anExt.Point = aPtMax; anExt.SquareDistance = aSqDistMax; anExt.IsMinimum = false; theResult.Extrema.Append(anExt); } } } } // namespace ExtremaPC #endif // _ExtremaPC_HeaderFile