Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0c0a095
Boolean: connect lone contours to their faces by monotone chords inst…
Grantim Oct 20, 2026
531e6fd
getMonotonePlan on exact predicates: not bound, as PreciseCutPoint is…
Grantim Oct 20, 2026
954df3a
Lone contours without exact predicates: the normal of the face from i…
Grantim Oct 20, 2026
4b2d628
cutMesh: preciseOwnVert next to preciseOtherVert
Grantim Oct 20, 2026
c7364ef
Sweep: loops of 2 edges only for the monotone plan
Grantim Oct 20, 2026
2659a05
Lone contours: a face whose plan fails stays in the mesh
Grantim Oct 20, 2026
64c8cea
getMonotonePlan on exact predicates: loops with two vertices defined …
Grantim Oct 20, 2026
6a45c11
Lone contours: normalized float normal, Boolean progress over the who…
Grantim Oct 20, 2026
b2a23d8
Lone contour tests: an otherwise uncut face, selfBoolean, the same ra…
Grantim Oct 20, 2026
69a7160
cutMesh: one helper for the exact coordinates of a vertex of either mesh
Grantim Oct 20, 2026
82d1166
Sweep: allowTwoEdgeLoops lets through the loops of 2 edges only
Grantim Oct 20, 2026
e0cc178
getMonotonePlan on exact predicates: all points added and checked bef…
Grantim Oct 20, 2026
8a553ca
Boolean: closed contours of two points are dropped before the cut
Grantim Oct 20, 2026
c484b3c
Lone contours: the contour points of the vertices only for the exact …
Grantim Oct 20, 2026
eec0a63
Lone contour cut tests: open contours, ForceFill::Good
Grantim Oct 20, 2026
4522489
Terrain embedding: the structure is cut with the boolean's exact sort…
Grantim Oct 21, 2026
06b2581
Sweep: findClosestToFront falls through to a valid index
Grantim Oct 21, 2026
748f222
Boolean: the precut outputs take the meshes after the cuts
Grantim Oct 21, 2026
084b993
Lone contour tests: rotated rigidB2A, nested islands, mapper, exact slit
Grantim Oct 21, 2026
44aaabc
getMonotonePlan docs: the loops of all the holes of one face
Grantim Oct 21, 2026
dc34a45
Lone contour tests: braces around the gtest macros under if and else
Grantim Oct 21, 2026
2dbe7e3
Terrain embedding: the last section of a wall contour ends at its fir…
Grantim Oct 21, 2026
5ca06b8
Lone contours: a plan without chords is a failure
Grantim Oct 21, 2026
465316e
cutMesh: the hole on the right of a path along an old edge maps to th…
Grantim Oct 21, 2026
3e22a82
Lone contour tests: chords in the edge map, a crossing contour, a ver…
Grantim Oct 21, 2026
e6c9a2f
Boolean: the precut outputs carry the data for the boolean's exact cut
Grantim Oct 21, 2026
329a164
Terrain embedding: the structure is cut with the precut output's sort…
Grantim Oct 21, 2026
5779018
Terrain embedding: lone contours of the terrain cut are connected by …
Grantim Oct 21, 2026
5bb123e
Deprecate the lone contour functions the boolean no longer uses
Grantim Oct 21, 2026
52bac53
Lone contours without exact predicates: a face of zero area is reported
Grantim Oct 21, 2026
9f9115c
Sweep: stops after an event in which the exact predicates failed
Grantim Oct 21, 2026
60ee8c2
Boolean: the sorting data and the copies only for the meshes actually…
Grantim Oct 21, 2026
05ba9cc
CutTrianglePredicates: crossings defined alike are told apart by the …
Grantim Oct 21, 2026
11a120b
Lone contours: the crossings of a multiple edge are told apart by the…
Grantim Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 109 additions & 18 deletions source/MRMesh/MR2DContoursTriangulation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "MRBitSetParallelFor.h"
#include "MRPrecisePredicates2.h"
#include "MRPrecisePredicates3.h"
#include "MRCutTrianglePredicates.h"
#include "MRPch/MRTBB.h"
#include <queue>
#include <algorithm>
Expand Down Expand Up @@ -51,6 +52,9 @@ struct SweepLinePredicates
std::function<void( VertId v, VertId a, VertId b, VertId c, VertId d )> addIntersectionPoint;
// position of vertex v; tp is passed at call time because the queue's topology is moved into the output mesh
std::function<Vector3f( const MeshTopology& tp, VertId v )> point;
// whether the predicates met points they cannot tell apart, after which their answers are arbitrary (empty if they cannot fail);
// findIntersections and makeMonotone check it after every event, the former also before the first one, and then stop with a failure
std::function<bool()> failed;
};

// the sweep-line predicates that depend only on the projected integer coordinates `pts2`;
Expand Down Expand Up @@ -256,8 +260,10 @@ int findClosestToFront( const MeshTopology& tp, const SweepLinePredicates& predi
break;
i = nextI;
}
assert( false );
return 0;
// only failed predicates get here (CutTrianglePredicates on invalid points), whose run stops at the end of the event;
// 1 is a valid index for every caller, while 0 is the queried edge itself (a position of -1 for the start vertex)
assert( predicates.failed && predicates.failed() );
return 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says that inconsistent CutTrianglePredicates (a failure found during the sweep) get here and the run is discarded. Yet assert( false ) stays, so Debug, iterator-debug and sanitizer builds abort on input that Release handles. One example is the case named in getMonotonePlan: three crossings of one cut on one line, which the addPoint checks miss.

That comment also says the sweep "goes on with arbitrary answers that can break it", so other sweep asserts and invariants can fire too. Could the sweep stop as soon as preds.failed() is set, or could this return a failure that the caller checks, instead of an index chosen to be in range?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9f9115c: SweepLinePredicates has an optional failed(). findIntersections and makeMonotone stop with a failure after any event that set it, and also before the first event. This assert now requires it to be set. The other asserts reachable within one event depend only on topology.

}

// same as above, reading the reference ray target from edges[0]'s dest (so edges[0] must be a valid
Expand Down Expand Up @@ -304,6 +310,9 @@ struct SweepLineParams
/// one can disable merge for identical vertices, merge is useful on symbol contours
bool allowMerge{ true };

/// if set, the loops of 2 edges (a slit: one edge taken both ways) are swept too, otherwise skipped; shorter loops are always skipped
bool allowTwoEdgeLoops{ false };

/// optional out per-face winding numbers
Vector<int, FaceId>* outFaceWinding{ nullptr };

Expand Down Expand Up @@ -334,9 +343,12 @@ class SweepLineQueue

bool findIntersections();
void injectIntersections( IntersectionsMap* interMap );
void makeMonotone();
bool makeMonotone();
void triangulate();
private:
// whether the predicates failed, see SweepLinePredicates::failed
bool predicatesFailed_() const { return predicates_.failed && predicates_.failed(); }

// clears the cached buffers of the previous run, keeping their capacity
void resetCache_();

Expand Down Expand Up @@ -458,6 +470,8 @@ class SweepLineQueue
bool doSegmentSegmentIntersect_( VertId aOrg, VertId aDest, VertId bOrg, VertId bDest ) const;
void checkIntersection_( int index, bool lower );
void checkIntersection_( int indexLower );
// an intersection was found while params_.abortWhenIntersect, so the run fails
bool intersectionFound_{ false };

public:
// all the buffers the sweep-line triangulation reuses between runs sharing the cache;
Expand Down Expand Up @@ -580,14 +594,15 @@ MeshTopology* SweepLineQueue::runTopology( IntersectionsMap* interMap )
if ( !findIntersections() )
return nullptr;
injectIntersections( interMap );
makeMonotone();
if ( !makeMonotone() )
return nullptr;
triangulate();
return &tp_;
}

bool SweepLineQueue::findIntersections()
{
if ( multipleEdge_ )
if ( multipleEdge_ || predicatesFailed_() )
return false;
stage_ = Stage::Intersections;
events_.clear();
Expand All @@ -599,11 +614,9 @@ bool SweepLineQueue::findIntersections()
else if ( event.type == EventType::Destination )
processDestenationEvent_( event.index );
else if ( event.type == EventType::Intersection )
{
if ( params_.abortWhenIntersect )
return false;
processIntersectionEvent_( event.index );
}
if ( intersectionFound_ || predicatesFailed_() )
return false;
events_.push_back( event );
}
return true;
Expand Down Expand Up @@ -717,7 +730,7 @@ void SweepLineQueue::injectIntersections( IntersectionsMap* interMap )
e = newE;
}

void SweepLineQueue::makeMonotone()
bool SweepLineQueue::makeMonotone()
{
stage_ = Stage::Monotonation;
startVertIndex_ = 0;
Expand All @@ -729,7 +742,10 @@ void SweepLineQueue::makeMonotone()
else
processDestenationEvent_( event.index );
calculateWinding_();
if ( predicatesFailed_() )
return false;
}
return true;
}

void SweepLineQueue::triangulate()
Expand Down Expand Up @@ -1145,6 +1161,11 @@ void SweepLineQueue::checkIntersection_( int i )

if ( !doSegmentSegmentIntersect_( org1, dest1, org2, dest2 ) )
return;
if ( params_.abortWhenIntersect )
{
intersectionFound_ = true; // the run fails, so the point of the intersection is not needed
return;
}

auto minEdgeId = std::min( activeSweepEdges_[i].edgeId, activeSweepEdges_[i + 1].edgeId );
auto maxEdgeId = std::max( activeSweepEdges_[i].edgeId, activeSweepEdges_[i + 1].edgeId );
Expand Down Expand Up @@ -1349,7 +1370,7 @@ void SweepLineQueue::initMeshByLoops_( const MeshTopology& inTp, const EdgeLoops
for ( int loopId = 0; loopId < int( loops.size() ); ++loopId )
{
const auto& loop = loops[loopId];
if ( loop.size() < 3 )
if ( loop.size() < ( params_.allowTwoEdgeLoops ? 2 : 3 ) )
continue;
for ( int lId = 0; lId < loop.size() && !multipleEdge_; ++lId )
addNewEdge( loop[lId], loopId, lId );
Expand Down Expand Up @@ -1809,25 +1830,25 @@ MeshTopology* triangulateDisjointContoursTopology( const Mesh& mesh, const EdgeL
return triangulator.runTopology();
}

std::optional<HoleFillPlan> getMonotonePlan( const Mesh& mesh, const EdgeLoops& loops, const Vector3f& normal )
// the monotone plan of the region bounded by loops, with the predicates that makePredicates builds for the given patch-to-input edge map
template <typename MakePredicates>
static std::optional<HoleFillPlan> getMonotonePlanImpl( const Mesh& mesh, const EdgeLoops& loops, MakePredicates&& makePredicates )
{
HoleFillPlan res;
if ( loops.empty() )
return res;
for ( const auto& loop : loops )
if ( loop.size() < 3 )
return {}; // the sweep skips such loops entirely, so their holes would stay unaddressed

SweepLineQueue::Cache cache;
WholeEdgeMap patchToInEdges; // initMeshByLoops_ reserves it from the loop sizes
std::vector<MonotoneChord> chords;
SweepLineQueue triangulator( cache, mesh.topology, meshSpacePredicates( mesh, loops, normal, patchToInEdges, &cache.pts2Buffer ), loops,
{ .abortWhenIntersect = true, .outPatchMap = &patchToInEdges, .outChords = &chords } );
SweepLineQueue triangulator( cache, mesh.topology, makePredicates( patchToInEdges, cache ), loops,
{ .abortWhenIntersect = true, .allowTwoEdgeLoops = true, .outPatchMap = &patchToInEdges, .outChords = &chords } );
if ( !triangulator.findIntersections() )
return {};
// passing findIntersections here means there is nothing to inject, so monotonation can run at
// once; and it is where to stop - monotone parts are what the caller fills
triangulator.makeMonotone();
if ( !triangulator.makeMonotone() )
return {};

// an anchor is a patch edge: either a loop edge copied from the mesh (patchToInEdges maps it back),
// or, past those, a chord referenced by the item that will create it
Expand Down Expand Up @@ -1861,6 +1882,76 @@ std::optional<HoleFillPlan> getMonotonePlan( const Mesh& mesh, const EdgeLoops&
return res;
}

std::optional<HoleFillPlan> getMonotonePlan( const Mesh& mesh, const EdgeLoops& loops, const Vector3f& normal )
{
return getMonotonePlanImpl( mesh, loops, [&] ( const WholeEdgeMap& patchToInEdges, SweepLineQueue::Cache& cache )
{
return meshSpacePredicates( mesh, loops, normal, patchToInEdges, &cache.pts2Buffer );
} );
}

std::optional<HoleFillPlan> getMonotonePlan( const Mesh& mesh, const EdgeLoops& loops,
const std::array<PreciseVertCoords, 3>& tri, const std::function<PreciseCutPoint( VertId )>& pointOf )
{
// the distinct vertices of the loops (a vertex can be on several loops, or twice on a slit), sorted; vertex verts[i] is point i
// of the predicates, all added before the sweep, since no predicate may run after a failed addPoint
std::vector<VertId> verts;
for ( const auto& loop : loops )
for ( auto e : loop )
verts.push_back( mesh.topology.org( e ) );
std::sort( verts.begin(), verts.end() );
verts.erase( std::unique( verts.begin(), verts.end() ), verts.end() );
CutTrianglePredicates preds;
preds.reset( tri );
// two vertices defined alike make the predicates fail only if the sweep compares them, unless the shifts of their edges tell them apart
// (as the crossings of the edges of a multiple edge of the other mesh with their wedges), so every two of them are compared before
// the sweep, and the loops are rejected if that fails: a vertex point by its id, a crossing by the ids of its segment and triangle
// in any order, with the index of the point
std::vector<std::pair<std::array<int, 5>, int>> keys( verts.size() );
for ( int i = 0; i < verts.size(); ++i )
{
const auto d = pointOf( verts[i] );
preds.addPoint( VertId( i ), d );
auto& [k, pos] = keys[i];
pos = i;
if ( d.isVertex() )
{
k = { d.seg[0].id, -1, -1, -1, -1 };
continue;
}
k = { d.seg[0].id, d.seg[1].id, d.tri[0].id, d.tri[1].id, d.tri[2].id };
std::sort( k.begin(), k.begin() + 2 );
std::sort( k.begin() + 2, k.end() );
}
if ( preds.failed() )
return {};
std::sort( keys.begin(), keys.end() );
for ( size_t i = 0; i < keys.size(); ++i )
for ( size_t j = i + 1; j < keys.size() && keys[j].first == keys[i].first; ++j )
(void)preds.less( VertId( keys[i].second ), VertId( keys[j].second ) );
if ( preds.failed() )
return {};

Vector<VertId, VertId> pointIds; // the point of every vertex of the sweep
return getMonotonePlanImpl( mesh, loops, [&] ( const WholeEdgeMap&, SweepLineQueue::Cache& )
{
// samePos, addIntersectionPoint and point stay empty: the sweep on loops merges no points, stops at the first crossing
// and triangulates nothing here, so it never calls them
SweepLinePredicates p;
p.less = [&] ( VertId l, VertId r ) { return preds.less( pointIds[l], pointIds[r] ); };
p.ccw = [&] ( VertId a, VertId b, VertId c ) { return preds.ccw( pointIds[a], pointIds[b], pointIds[c] ); };
p.ccwFromBehind = [&] ( VertId b, VertId pivot ) { return preds.ccwFromBehind( pointIds[b], pointIds[pivot] ); };
p.addInputPoint = [&] ( VertId v, int contourId, int pointId )
{
const auto it = std::lower_bound( verts.begin(), verts.end(), mesh.topology.org( loops[contourId][pointId] ) );
pointIds.autoResizeSet( v, VertId( int( it - verts.begin() ) ) );
};
// the predicates can still fail on invalid points that the checks above miss, as three crossings of one cut on one line
p.failed = [&] { return preds.failed(); };
return p;
} );
}

}

}
20 changes: 18 additions & 2 deletions source/MRMesh/MR2DContoursTriangulation.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
namespace MR
{

struct PreciseVertCoords;
struct PreciseCutPoint;

namespace PlanarTriangulation
{

Expand Down Expand Up @@ -139,8 +142,11 @@ MR_BIND_IGNORE MRMESH_API MeshTopology* triangulateDisjointContoursTopology( con
/**
* @brief splits the near-planar region bounded by \p loops in monotone parts, without triangulating them
* this is the sweep-line triangulation stopped right after monotonation
* @param loops one closed EdgeLoop of at least 3 edges per boundary of one region, bounding holes of
* \p mesh as trackRightBoundaryLoop returns them; which way round a loop runs does not matter
* @param loops one closed EdgeLoop per boundary of one region, bounding holes of \p mesh as trackRightBoundaryLoop
* returns them (a slit of one edge is a loop of 2 edges, and a loop of one edge is skipped); which way round a loop runs
* does not matter; the holes can be several parts of one region split by edges between them, as all the holes of a face
* cut along contours (the parts between the contours that cross its sides, and both sides of every island), since an edge
* taken both ways adds zero winding
* @param normal the orientation the region is monotone around, see \ref triangulateDisjointContours
* @return std::nullopt if the loops intersect or a chord would land in a face instead of a hole, so that
* the caller can fall back on filling every loop separately; otherwise a plan with numTris == 0
Expand All @@ -149,5 +155,15 @@ MR_BIND_IGNORE MRMESH_API MeshTopology* triangulateDisjointContoursTopology( con
*/
MRMESH_API std::optional<HoleFillPlan> getMonotonePlan( const Mesh& mesh, const EdgeLoops& loops, const Vector3f& normal );

/// same as above for the region of one triangle of a mesh cut by the boolean (the loops of all the holes of the triangle),
/// on the exact predicates of \ref CutTrianglePredicates, which never contradict the boolean's ones, so the loops are disjoint
/// whenever the boolean's contours are
/// \param tri the triangle, counterclockwise around its normal
/// \param pointOf the definition of every vertex on the loops, valid as \ref CutTrianglePredicates::addPoint requires
/// \return std::nullopt also if two vertices on the loops are defined alike and their wedges do not tell them apart, or the predicates fail
/// on the points
MR_BIND_IGNORE MRMESH_API std::optional<HoleFillPlan> getMonotonePlan( const Mesh& mesh, const EdgeLoops& loops,
const std::array<PreciseVertCoords, 3>& tri, const std::function<PreciseCutPoint( VertId )>& pointOf );

}
}
Loading
Loading