Repository navigation
Conversation
…ead of subdividing the faces A contour inside one face that crosses none of its edges (a lone contour) used to make booleanImpl and selfBoolean subdivide that face, update the AABB tree, find the collisions again and repeat, up to 100 times, dropping the contours that never resolved. Now cutMesh connects them: before the cut it notes the faces of lone contours, and after cutting the edges it takes all hole loops of each such face (the face boundary and the islands or slits of its lone contours) and adds the chords that split the hole into monotone parts. The chords come from the sweep of PlanarTriangulation::getMonotonePlan on CutTrianglePredicates, which never contradict the boolean's exact predicates, so the plans hold where float predicates failed. The faces are planned in parallel and filled sequentially; a face that cannot be planned goes to fbsWithContourIntersections. - getMonotonePlan: a new overload for a cut triangle with exact point definitions; loops of 2 edges (a one-edge slit taken both ways) are accepted; with abortWhenIntersect the sweep stops at the first crossing it detects, without computing the crossing point. - booleanImpl: one collision pass; no subdivision, mesh copies for sorting or face map remaps. - selfBoolean: one pass, sorting on the input mesh. Test: MRMesh.BooleanLoneContours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts corners, and a test The normal summed over the hole loops took in the islands, whose float points add rounding: in a numeric check, far from the origin with an island filling most of the face, it picked another projection axis in up to 40% of the trials and flipped the orientation in up to 3%. From the corners in doubles, every component of the face normal has the exact sign. Test MRMesh.CutMeshLoneContour: closed contours inside one triangle, near and far from the origin, filling half or most of it; cutMesh without sortData must connect and fill them. It fails on master, which loses a part of the face in every trial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exact coordinates of a vertex of the mesh being cut (its id shifted by meshAVertsNum and rigidB2A applied in float if it is mesh B) were built in compareEdgeTrisIntersections and again in connectLoneContours; now both take them from one helper, the same float transform before the converter, so the sorting stays bit-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sweep on mesh loops skipped the loops of fewer than 3 edges, which the previous commits took in for every caller, so that fillContours2D, fillContours2DPlan and triangulateDisjointContours met them too (a digon of two distinct edges failed the whole run as a multiple edge). Only getMonotonePlan needs them, for the slits of open lone contours: SweepLineParams::allowTwoEdgeLoops, set by getMonotonePlan alone, keeps master's behavior for the other callers, and master's capacity hints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
connectLoneContours removes a lone face that no other contour cut before planning its chords; if the plan failed, the face only went to fbsWithContourIntersections and stayed removed, while cutMesh finds the holes to fill only through the cut paths, and the old sides of the face have none: with ForceFill::All or Good the mesh kept a triangle-shaped hole there. Now such a face is put back (no chord of it was added, so its old sides still make its ring). Test MRMesh.CutMeshSelfCrossingLoneContour: a bowtie contour inside one triangle of a cube; the float sweep stops at its crossing, so the triangle is bad and must stay, without a hole along its sides, with ForceFill::None and All. It fails without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alike are rejected before the sweep Two loop vertices with one definition (e.g. both edges of a multiple edge of the other mesh, or two triangles with the same vertices, crossing a lone face) make CutTrianglePredicates fail inside a predicate; the sweep then went on with arbitrary answers, where findClosestToFront could fall through to its last return and an index of -1 reached rightGoingCache_ (guarded by an assert only), and the perturbation expanded for the coincident points took 0.5 s (Release) to 23 s (Debug) for six points. Now the definitions of the distinct loop vertices are found once, sorted by small id keys (a vertex by its id, a crossing by the ids of its segment and triangle in any order), and a repeated key fails the plan before the sweep, which takes the definitions from that list. Tests: MRMesh.PlanarTriangulationMonotonePlanSameDefinitions (an island vertex defined as another, also with the ends and the triangle in another order), MRMesh.BooleanMultipleEdgeThroughLoneFace (the boolean reports the bad face, without asserting). Both also passed without the check, only slower, since the final failed() check discarded the plans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le range - connectLoneContours without sortData: the normal of the face is normalized in doubles before the conversion to float, which could flush it to zero or overflow at extreme coordinate scales. - booleanImpl: params.cb gets the whole range; the 0.8 offset was left from the removed loop over lone contours, so the progress jumped from 0 to 0.8. - getMonotonePlan on exact predicates: a comment on the predicates it leaves empty, which the sweep on loops never calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndom cases on every platform - MRMesh.BooleanLoneContours: also the small sphere alone, so that its triangle of the cube is cut by the lone contour only (removed by connectLoneContours, its corners as vertex points); before, the crossing sphere always cut the same triangle too. - MRMesh.SelfBooleanLoneContour: selfBoolean of a cube with a small sphere piercing one of its triangles, added as one mesh; the result is valid, closed, and keeps the sum of their volumes. - MRMesh.CutMeshLoneContour: uniform values straight from std::mt19937, whose output the standard specifies, unlike that of std::normal_distribution, taken in a fixed order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
preciseOtherVert and preciseOwnVert differed only in the mesh and in which of the two gets the id shift: now preciseVert takes the mesh and whether it is A (the ids of B shifted by meshAVertsNum, its points moved by rigidB2A in float before the converter), preciseOtherVert calls it for the other mesh, and compareEdgeTrisIntersections and connectLoneContours call it for the mesh being cut, so the sorting stays bit-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flag skipped no loop shorter than 3 edges, so it took in a loop of one edge too, which is no slit; now with it the loops of 2 or more edges are swept, and the shorter ones skipped as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore the sweep The points went to CutTrianglePredicates::addPoint from the sweep's addInputPoint, after the sweep had ordered some of them, and a failed addPoint was stopped by a Debug assert only, while the contract allows no predicate after it (in Release less could be cyclic in the parallel sort, or findClosestToFront fall through to its last return). Now the distinct loop vertices are added right after reset, under their indices in the sorted vertex list, the plan fails at once if an addPoint failed (as for a vertex point off the corners or a segment not crossing the plane of its triangle, which pointOf's fallbacks can give), and the sweep's predicates translate its vertex ids to these indices; the final failed() check stays for what only a predicate finds. - The static template getMonotonePlan is getMonotonePlanImpl, so that a call in the .cpp cannot pick it instead of a public overload. - MR2DContoursTriangulation.h declares PreciseVertCoords and PreciseCutPoint instead of including MRCutTrianglePredicates.h and <functional>. Test: MRMesh.PlanarTriangulationMonotonePlanSameDefinitions also with an island vertex defined as a vertex point off the corners and as a segment not reaching the plane; without the fix both fire the assert in Debug. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where two triangles with the same vertices (as at a tip of makeMeshWithMultipleEdge) have both edges that cross one triangle of the other mesh in common, the contour is closed with two points. Such a contour is always lone, as both points are crossings of edges of one mesh with one triangle of the other (in a contour of both kinds, the two segments would be on both sides of one edge), and its two segments coincide, so it has zero area. The cut made it a digon of two edges between the same two vertices in the hole of that triangle, the sweep failed on it as a multiple edge, and the boolean returned "Bad contour" (selfBoolean: "nested intersections"), while master dropped such contours and succeeded. Now booleanImpl and selfBoolean drop them right after ordering the contours, before anything is cut, so both meshes stay consistent; in selfBoolean the two contours of a pair have the same points, so both go together. Test: MRMesh.BooleanTwoPointLoneContour (the tip of makeMeshWithMultipleEdge through a side of a cube, both orders, three operations, and selfBoolean of both as one mesh: valid closed results); without the fix every boolean reports a bad face and selfBoolean nested intersections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…path, comments - connectLoneContours fills vertInters only with sortData, as only the exact definitions of the points (pointOf) take it. - The comment on the normal of the float path says what holds: the corners in doubles give a normal free of the islands' rounding, of which only the dominant component and its sign matter (the products are rounded too, so not every component has the exact sign). - Mesh::updateCachesAfterSplits no longer refers to the boolean's lone-contour iterations, which are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- MRMesh.CutMeshLoneContour: in every trial also the same contour without its last segment and a slit of one segment (a hole loop of 2 edges, for which the sweep takes loops of 2 edges), with the cut path checked along the contour in resultCut. - MRMesh.CutMeshSelfCrossingLoneContour: also ForceFill::Good, which fills no hole of the bad triangle, so its two sides of the island stay open as with None. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing data createCutStructure_ took the pre-cut structure from boolean( ..., outPreCutB ) and cut it with cutMesh without sortData. The boolean no longer subdivides the faces of lone contours, so such contours reached that cut and were connected on float predicates: when their points round together, the island is connected inside out, cutMesh reports success and the embedding fails later. Now the function runs the boolean's steps before the cut itself and passes cutMesh the same sortData as booleanImpl, dropping closed contours of two points as the boolean does. BooleanPreCutResult says that its contours can include lone ones. Test: MRMesh.EmbedStructureLoneContour (fails on the float cut with "Cannot embed structure beyond terrain"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loop of findClosestToFront always returns with consistent predicates, but CutTrianglePredicates after a failure found inside a predicate answers arbitrarily (its run is discarded afterwards), and then the loop could fall through to `return 0`: updateStartRightGoingCache_ made it position -1, reading rightGoingCache_[-1] and rotating from begin() - 1, and findCCWPrev returned the lone edge itself, which splice then skipped, leaving the edge without an origin. Now it returns 1, the first candidate, valid for every caller; the assert stays, and the float predicates never get there. No test: the failures a predicate finds need points that the checks of getMonotonePlan let through (two points defined alike are rejected before the sweep, three crossings on one cut line need input that no valid cut gives), and no small input made one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where the operation does not cut one mesh (A for InsideB and OutsideB, B for InsideA and OutsideA), the sorting data of the other mesh's cut refers to that mesh itself, not to a copy. booleanImpl moved meshA into outPreCutA before cutting B, so B's cut read a moved-from mesh, and moved meshB into outPreCutB on the main thread while the task cutting A could still read it. Now both outputs are filled right after the cuts end, before the bad-contour checks, so they stay filled as before when a cut fails. Pre-existing on master; the cut of a lone contour reads the other mesh for every lone face. Test: MRMesh.BooleanPreCutOfUncutMesh (a cube with a lone contour of a small sphere; InsideA with outPreCutB and InsideB with outPreCutA cut the cube as the operations without the outputs do, and the outputs hold the uncut sphere); without the fix both crash with an access violation in Release, reading the sphere's points in connectLoneContours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- MRMesh.BooleanLoneContours: rigidB2A rotates about a generic axis besides translating, so that the float transform of B's points in the cut's exact definitions must match the one of findCollidingEdgeTrisPrecise; a third configuration has a torus lying in the plane of the lone triangle, cutting it along two nested circles, with a small sphere in its hole (three islands in one face, two of them nested); every result has a mapper, which must map every face to a valid face of one input, the faces strictly inside the lone triangle to it, and those of the triangle inside it. - MRMesh.CutMeshLoneSlitExact: a small square pierces a big triangle of a cube, which is cut along an open lone contour of three points on one line with the sorting data of the boolean's steps before its cut (as in the terrain embedding), with the square as mesh A and as mesh B: no bad faces, no holes, the same area, one cut path of two edges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The float overload said its loops bound one region, while connectLoneContours passes the loops of all the holes of one old face: the parts split by the contours that cross its sides, and both sides of every island. The doc now says that such holes work as one region, since an edge taken both ways adds zero winding, and that a loop of one edge is skipped; the exact overload says it takes the loops of all the holes of the triangle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The EXPECT_* macros expand to an if/else, so one directly under an if without braces fails the GCC builds with -Werror=dangling-else, as in the mapper check of MRMesh.BooleanLoneContours; the macros under the if and else of MRMesh.BooleanMultipleEdgeThroughLoneFace are braced too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st point The connector that prepareTerrainCut passes surfaceLineToMeshContour maps the indices of the section ends in cont (the points of a contour without bow ties, the first one repeated at the end) through filtBowTiesMap, which has no entry for the repeated point, so the last section of every closed contour read one past the end of the map: an out-of-range std::vector access (the assert of the iterator-debug build), otherwise an arbitrary index for the plane the section is tracked in. Now that end index wraps to the first point. Pre-existing on master; MRMesh.EmbedStructureLoneContour is the first test that runs embedStructureToTerrain, and reaches it in both of its cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every island or slit inside a face needs a chord to the rest of the face, so a valid plan of a lone face always has one. A plan without any came where the face was gone before connectLoneContours: an open contour from a neighbor face ending on a side of the lone face makes cutOneEdge fill the face again with that end as a new corner, so only the two sides of the island were planned, with zero winding and no chords; the hole filling then put two overlapping disks of opposite orientation on them, a separate component, and the cut reported nothing. Now such a face goes to the bad faces. On the boolean's contours (no contour ends on a side of a face with another face beyond it) and wherever the chords are found, nothing changes. Test: MRMesh.CutMeshLoneContourInRefilledFace (an island in one triangle of a square of two, an open contour from the other triangle to the middle of their common side, both orders): the triangle is reported; before, the cut succeeded with two components and the area 104 of 100. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at edge's right face A path edge along an edge of the old mesh (between two contour points in vertices) removes only the face on its left, which removedFaces records, while the face on its right can be removed later: by another contour crossing it, or by connectLoneContours when it holds a lone contour. The hole on the right was then found from that path edge with the face on its left, so new2OldMap and new2oldEdgesMap took the faces and edges filled in the right face to the left one, whenever that contour came before the others in the face. Now doPreCutMesh records the right face of such an edge, and the hole on its right gets it. Pre-existing on master for a contour crossing the right face; the lone contours made it reachable with an island in the right face. Only the maps change, not the cut. Test: MRMesh.CutMeshContourAlongSideMaps (a square of two triangles, a contour along the diagonal in both directions with an island in one triangle or a contour cutting off its corner, both orders: valid cut, no bad faces, one component, the area kept, every new face and edge mapped to its triangle); without the fix the faces and edges right of the diagonal map to the other triangle when the contour from 0_v to 2_v comes first, with the island and with the corner contour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tex on a face - MRMesh.CutMeshLoneContour passes new2oldEdgesMap and checks that every new edge, the chords connecting the contour with the sides among them, maps to the triangle, as surface offset needs; its comment says what holds across platforms: the generator's values are the same, the points made of them with std::cos and std::sin can differ in their last bits. - MRMesh.CutMeshLoneContourWithCrossingContour: on the float path, an island in one triangle of a square of two with a contour crossing that triangle's sides, both orders: valid cut, no bad faces, one component, the area kept, every new face and edge mapped to its triangle. - MRMesh.BooleanVertexOnFace: a corner of a small cube, or the lowest vertex of a small sphere, exactly on a big triangle of a cube, both orders, Union, Intersection and DifferenceAB: valid closed results with the volumes of meshes touching at a point, whether simulation of simplicity makes a lone contour of points that are all the same in float (the big cube as mesh A) or no contact (as mesh B). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BooleanPreCutResult held only the mesh and its contours, so a caller cutting it could not pass cutMesh the boolean's sorting data, without which the lone contours are connected on float predicates. Now it also holds the boolean's ordered intersections (after the drop of closed contours of two points), its float-to-int converter, a copy of rigidB2A, the vertex count of mesh A before any cut, and whether the mesh is A; sortData( otherMesh ) makes the SortIntersectionsData of the boolean's own cut of the mesh. booleanImpl fills them with the meshes after the cuts: the intersections are copied into the output of A and moved into the last output, as nothing reads them afterwards. The vertex count of A is taken once before the cuts and also given to the sorting data of both cuts, the same value as before. Test: MRMesh.BooleanPreCutExactCut (a cube with a lone contour of a small sphere, Intersection, the cube as A and as B, without and with a rotated rigidB2A, the output of A, of B, and of both at once): each output holds its uncut mesh and the boolean's data, and cutMesh of it with sortData( the other mesh ) gives the boolean's own cut2origin without bad faces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing data createCutStructure_ repeated the boolean's steps before its cut to get the exact sorting data. Now it takes the precut structure from boolean( terrain, structure, InsideB ) with outPreCutB again, as on master, and cuts it with sortData( terrain ) of that output, so the boolean's own intersections, converter and the drop of closed contours of two points are used. Test: MRMesh.EmbedStructureLoneContour (fails on the float cut). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fedr
left a comment
There was a problem hiding this comment.
Two notes outside the diff:
MREmbedTerrainStructure.cpp:360(prepareTerrainCut): the terrain still subdivides its lone contours withsubdivideLoneContoursand retries, failing with "Cannot resolve lone cut on terrain", althoughcutMeshincutTerrainnow connects lone contours itself. The boolean dropped this mechanism, so the terrain could too, and then there would be one way of handling lone contours instead of two.MRIntersectionContour.cpp:315: after this PR,detectLoneContours,removeLoneContours,removeLoneDegeneratedContoursandupdateCollidingEdgeTrisPrecisehave no callers in MeshLib, andsubdivideLoneContoursis used only by the terrain loop above. Should they be deprecated or removed, or should their docs at least say thatcutMeshnow handles lone contours?
| // find and order intersections | ||
| contours = orderIntersectionContours( meshA.topology, meshB.topology, | ||
| findCollidingEdgeTrisPrecise( meshA, meshB, converters.toInt, params.rigidB2A ) ); | ||
| removeTwoPointLoops( contours ); |
There was a problem hiding this comment.
Master dropped the lone contours it could not resolve (removeLoneContours after the iteration limit, removeLoneDegeneratedContours) and the boolean succeeded. Here, a lone face whose exact plan fails becomes a bad contour face, so boolean() without forceCut (and selfBoolean()) returns an error on the same input. Examples: a multiple edge through a lone face, two loop vertices defined alike, or three crossings of one cut on one line. With forceCut (ForceFill::All), the face is put back, and the island is also filled on both sides, overlapping it.
BooleanMultipleEdgeThroughLoneFace accepts "a valid result or the bad face reported", so it would not catch this regression. Should a face whose plan fails drop its lone contours, as master did, instead of failing the whole boolean?
| // 1 is a valid index for every caller, while 0 is the queried edge itself (a position of -1 for the start vertex) | ||
| assert( false ); | ||
| return 0; | ||
| return 1; |
There was a problem hiding this comment.
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?
| // normalized before the conversion to float, which could flush it to zero or overflow at extreme scales | ||
| const auto& p = mesh.points; | ||
| const auto normal = dirDblArea( Triangle3d{ Vector3d( p[lf.verts[0]] ), Vector3d( p[lf.verts[1]] ), Vector3d( p[lf.verts[2]] ) } ); | ||
| plans[i] = PlanarTriangulation::getMonotonePlan( mesh, loops, Vector3f( normal.normalized() ) ); |
There was a problem hiding this comment.
On the float path (no sortData), a lone contour in a zero-area face gives dirDblArea == 0. normalized() then returns (0,0,0), and getMonotonePlan sweeps on a meaningless projection: the plan fails (where master left the island) or adds chords picked on a collapsed plane. A needle or degenerate triangle passed to cutMeshByContours can hit this. Fall back to a normal summed over the loops when the corner normal is zero, or report the face up front?
| if ( bSubdivided || !intParams.originalMeshB ) | ||
| if ( !intParams.originalMeshB ) | ||
| { | ||
| Timer t( "meshBCopyBuffer" ); |
There was a problem hiding this comment.
When outPreCutB is set, mesh B is never cut, and it now stays in place until fillPreCut. Even so, this branch still deep-copies it into meshBCopyBuffer for the sort data of A, and the same happens for A with outPreCutA. embedStructureToTerrain (InsideB + outPreCutB) copies the whole structure for nothing, and with both outputs set, both meshes are copied while neither is cut. Copy only when needCutMeshB && !params.outPreCutB (likewise for A)?
A lone contour lies inside one face and crosses none of its edges, as where a tip of the other mesh pokes through a big triangle. Until now,
booleanImplsubdivided the faces of such contours, updated the AABB tree, found the collisions again and repeated, up to 100 times (selfBoolean: 3), and removed the contours that never resolved. This PR replaces that loop:cutMeshconnects every lone contour with the rest of its face by new edges, so there is one collision pass, no subdivision and no tree update.What
cutMesh: before the cut, it notes the faces of lone contours. After the edges are cut, it takes all hole loops of each such face (the parts of the face between the contours that cross its sides, and both sides of every island or slit of its lone contours). It then adds the chords that split them into monotone parts, and the usual hole filling triangulates them. The faces are planned in parallel and changed sequentially. A face that cannot be planned goes tofbsWithContourIntersectionsand is put back as it was, so filling the bad faces leaves no hole along its sides. So does a face whose plan has no chord, as where an open contour ending on its side made the cut of that side fill the face again first: its island cannot be connected.PlanarTriangulation::getMonotonePlan: a new overload for a triangle cut by the boolean (not bound, asPreciseCutPointis not). It takes the triangle and the exact definition of every loop vertex, and runs the sweep onCutTrianglePredicates(Exact predicates for the planar sweep inside cut triangles #7165), which never contradict the boolean's predicates. Every loop vertex is added to the predicates before the sweep, and the plan isstd::nulloptif a definition is invalid or two vertices are defined alike, so the sweep never runs a predicate on invalid points. Float predicates are not enough here: on nda/case_00 they failed on 206 of 218 lone faces, because contour points coincide with face corners in float.sortData(callers other than the boolean), the plan runs on the float predicates, oriented by the normal of the face from its corners in doubles (free of the rounding of the islands' points), normalized before the conversion to float. The header declares the exact overload with forward declarations, so it includes nothing new.getMonotonePlan(SweepLineParams::allowTwoEdgeLoops), so open lone contours are connected too instead of being dropped;fillContours2D,fillContours2DPlanandtriangulateDisjointContourskeep the minimum of 3 edges. WithabortWhenIntersect, it stops at the first crossing it detects, without computing the crossing point. This is stricter than master fortriangulateDisjointContours,fillContours2DandfillContours2DPlan: master could drop a pending crossing whose rounded point sorted after the end of one of its edges while another pending crossing came first, and then succeed with crossing edges; such runs now fail, and their callers fall back. None of our regression cases hits it.findClosestToFrontfalls through to a valid index, which only inconsistent predicates (CutTrianglePredicatesafter a failure, whose run is discarded) can reach.cutMesh: one helper,preciseVert, gives the exact coordinates of a vertex of mesh A or B, forcompareEdgeTrisIntersections(bit-identical sorting) and the point definitions of lone faces.cutMesh: the faces and edges filled on the right of a contour running along an existing edge map (new2OldMap,new2oldEdgesMap) to that edge's right face, not to its left one, when the right face is removed afterwards by another contour or by the connection of a lone contour. This was already wrong on master for a contour crossing the right face; only the maps change, not the cut.booleanImpl: one collision pass; no subdivision, mesh copies for sorting or remaps of subdivided faces; the progress callback gets the whole range. Closed contours of two points are dropped before either mesh is cut, as master dropped them: such a contour (two triangles with the same vertices whose two shared edges cross one triangle of the other mesh) has zero area, since its two segments coincide.selfBoolean: one pass; it sorts on the input mesh instead of a copy, and drops closed contours of two points with both contours of their pair.BooleanParameters::outPreCutA/outPreCutBreturn the meshes and contours without subdividing the faces of lone contours, together with the boolean's data for its exact cut:BooleanPreCutResultalso holds the ordered intersections, the float-to-int converter, a copy ofrigidB2A, the vertex count of mesh A and whether the mesh is A, andsortData( otherMesh )gives theSortIntersectionsDataforcutMesh( mesh, contours ). The outputs now take the meshes after both cuts: before, for InsideB/OutsideB (InsideA/OutsideA) the cut of the other mesh sorted by a mesh already moved into the output, or raced with the task still reading it.embedStructureToTerraincuts the structure fromoutPreCutBwith that sorting data, so its lone contours are connected on the exact predicates. It also no longer reads one past the end offiltBowTiesMapfor the last section of every closed wall contour (on master too; found by the iterator-debug CI job through the new test).Tests
MRMesh.BooleanLoneContours: a cube with a small sphere poking through one of its big triangles, alone, with another sphere crossing the edges of the same triangle, and with a torus cutting the triangle along two nested circles around a small sphere; Union, Intersection and DifferenceAB, both orders, with and without a rotatingrigidB2A. It checks the volumes, that there are no holes, the number of components (an island left in its triangle would make one more) and, through a mapper, that the new faces inside the lone triangle map back to it. It fails without this change.MRMesh.SelfBooleanLoneContour: selfBoolean of a cube with a small sphere piercing one of its triangles; the result is valid, closed and keeps the sum of the volumes.MRMesh.BooleanTwoPointLoneContour: the tip ofmakeMeshWithMultipleEdge(two triangles with the same vertices) through a side of a cube, booleans and selfBoolean, give valid closed meshes.MRMesh.BooleanMultipleEdgeThroughLoneFace: a multiple edge crossing a lone face; a valid result or the bad face reported, without asserts.MRMesh.BooleanVertexOnFace: a corner of a small cube, or a vertex of a small sphere, exactly on a big triangle of a cube, both orders: valid closed results with the volumes of meshes touching at a point.MRMesh.BooleanPreCutOfUncutMesh:outPreCutBwith InsideA andoutPreCutAwith InsideB cut the cube as without the outputs (crashed before).MRMesh.BooleanPreCutExactCut: the output of A, of B and of both at once, with and without a rotatedrigidB2A:cutMeshof it withsortData( the other mesh )gives the boolean's own cut.MRMesh.EmbedStructureLoneContour: a terrain peak through a big triangle of the structure; far from the origin its contour points coincide in float, where the float cut connected the island inside out and the embedding failed.MRMesh.CutMeshLoneContour:cutMeshwithoutsortDataon closed contours inside one triangle, their open versions and one-segment slits, 200 random cases (taken straight fromstd::mt19937, the same on every platform) near the origin and at 1e5 from it. It checks that no face is reported bad, the topology is valid, the hole count and the area are kept, the cut path, and that every new edge, the chords among them, maps to the triangle innew2oldEdgesMap. On master every closed case loses a part of the face.MRMesh.CutMeshLoneSlitExact: an open lone contour cut with the boolean'ssortData, the patch as mesh A and as mesh B.MRMesh.CutMeshLoneContourWithCrossingContour: an island in a triangle that another contour crosses, on the float path: connected, the area and the maps kept.MRMesh.CutMeshLoneContourInRefilledFace: an island in a triangle and an open contour ending on its side: the triangle is reported (before: a silent success with the island as a separate double-sided disk).MRMesh.CutMeshContourAlongSideMaps: a contour along a side of a triangle with an island or another contour in it: every new face and edge maps to its triangle (before: to the neighbor when the contour along the side came first).MRMesh.CutMeshSelfCrossingLoneContour: a lone contour crossing itself; its triangle is reported bad and stays in the mesh, with ForceFill::None, Good and All.MRMesh.PlanarTriangulationMonotonePlanSameDefinitions: the exactgetMonotonePlanrejects loops with two vertices defined alike and invalid definitions.MRTest passes in Release and Debug on Windows.
On our local Boolean regression set (3097 Release and 1262 Debug results, against master 29d5815; unchanged by the later commits):
On our local contour cut set and its border sweep, every result equals master's (c3ac207).
Benchmark
Measured at 329a164. Alternating runs of Release builds of master 29d5815 and this PR, 4 rounds (the first master round dropped as disturbed); best of 10 per operation, in ms:
3366 spheres (MeshInspector's Boolean benchmark defaults, 2000 rotations), ms per boolean (AVG and MEDIAN averaged over the rounds, MIN and MAX over all runs), in the same session and in a second one of spheres only (3 undisturbed rounds each):
The PR's round averages ranged from 11.39 to 11.82 ms over both sessions, master's from 11.87 to 11.97 ms. The runs without lone contours (nef, dental with B moved) stay within ±0.9%: the new code costs nothing where there is nothing to connect.
Known limits
sortData, a lone contour whose points coincide in float can be connected inside out without being reported; only the boolean and the terrain embedding passsortData.sortData, if an open contour ends on a side of a lone face,cutOneEdgere-triangulates that face first, so its island cannot be connected; the face is reported infbsWithContourIntersections(master left it unconnected silently).Open questions
detectLoneContours,removeLoneContours,removeLoneDegeneratedContoursandupdateCollidingEdgeTrisPreciseare now used only by tests. Remove them here or in a follow-up?subdivideLoneContoursis still used byembedStructureToTerrainfor the terrain's own cut.Generated with Claude Code