Repository navigation
Conversation
CutMeshParameters::forceFillMode (enum ForceFill) becomes bool forceFill. With the boolean's sorting data, the forced cut finds where the contours cross in bad faces on the exact predicates (PlanarTriangulation:: findCrossings), splits both edges at one new vertex, puts the pieces in resultCut, and connects the islands of lone contours left there. getBadFacesAfterCut checks every edge of a loop, not only the one it starts from. The forced boolean tells the parts of a mesh cut along crossing contours by the winding numbers of the other mesh around them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew edges The second bitset of the current loop, the loop vector and the clearing pass cost +34% of the function (spheres benchmark); the walk number of every new edge costs +10% and finds the same faces where the loops are valid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
makeFaceHoles collects the path edges inside the faces, removes the faces that no other contour removed and finds their hole loops, for both connectLoneContours and splitContourCrossings; the cut triangles of the path edges are found once there instead of keeping their contour segments. splitCrossings adds its points by Mesh::addPoint, insertPieces inserts the pieces at once, shorter comments. Same results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ExactCutPoints::crossings holds the PreciseCutPoint of a split crossing, made from the cut triangles of its two edges, instead of the two triangle ids that point() converted on every call. Same results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iece loop Same results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… point Same results; makeBox comment names hx. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th sides findCutFaces marks the faces in a FaceBitSet instead of sorting every contour face; windingsAroundParts returns at once unless a part is to the left of a path and to the right of another one, as a number above 1 needs. Same results; the forced 3366 spheres went from +12.4% to +1.7% of master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orter helpers - cutMesh: split crossings only under forceFill again (no second scan of the bad faces) - ExactCutPoints takes sortData by pointer and is passed by reference; point() as on master - addMonotoneChords keeps master's names; makeFaceHoles sizes its result at once - splitCrossings: one splice pair moves an edge off its origin; ring from the crossing - findCutFaces: one loop for both modes, both sides of an edge, shorter CutFace - windingsAroundParts -> uniteByWindings (no dead aboveOne/maxW); findMeshPart one if - cutOf via getAt, crossing key by std::minmax, shorter comments and test Same results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Ring, setLoopPredicates walkOf keeps for every new undirected edge the start edge of the last walk that met it, and the face is marked where a loop passes an edge both ways; holeLoops takes the rings from MeshTopology::getLeftRing; addLoopPoints and loopPointsPredicates become one setLoopPredicates. Same results and speed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…crossing split - addMonotoneChords adds the faces where it fails to badFaces and puts them back as master's connectLoneContours did; the island pass passes its own removedHere - splitContourCrossings filters the moved contour faces in place, runs the crossings with a running vertex id - findMeshPart keeps master's comment; shorter doc comments - the new tests sit in master's anonymous namespace and after its square tests; topArea by the triangle centers Same results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The graph cut of the forced separation now runs only when a cut path is open, as on a mesh with holes. With closed paths the paths alone bound the parts, so a part on both sides of them reaches the winding numbers whole: for example the ring between a box and a smaller box inside it, which the graph cut used to split along its shortest edges. A part reached with two numbers, as through a face whose crossings are not reported bad, keeps the first one instead of failing the boolean with "Cannot separate". Without the graph cut, the forced boolean is 66-74% faster on the big cases and 29% faster on 3366 spheres. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a loop passing an edge both ways can have a bad face, and that is a property of the loop, not of the walk's start edge. So all loops are first walked in parallel, each thread walking a loop once; the serial walks, as before, start only from the path edges of the loops that pass an edge twice, in the same order, so they give the same faces. In a valid cut there are none. A loop of up to 32 edges is checked by a 256-bit mask of the edge ids met, a longer one by the number of the walk kept per new edge. The function alone is 2.6-10x faster on all threads and 0.85-1.03x on one; the results of the boolean test cases and the contour test cases are bit-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arts uniteByWindings numbered every set of parts linked by the paths from 0 at its lowest part, which is right only if that part is outside the other mesh. A part below the outside (an inside-out shell, a zero-volume shell or double-sided triangle that the perturbation turns inside out, an inside-out pocket of a self-crossing shell, or conflicting links) moved the outside to 1 and flipped the whole cut mesh; and a shell of the other mesh enclosing the linked parts without crossing them made them all outside. Now every set is numbered from its part with the most path edges among those with a vertex not on the paths (an original vertex), whose number is the exact winding number of the other mesh there: the signed count of the other mesh's triangles that a segment from the vertex straight up crosses, by doTriangleSegmentIntersect with the cut's int coordinates and its vertex id order, so the simulation of simplicity agrees with the paths. The other mesh's AABB boxes are transformed by rigidB2A and compared in unrounded int units with a margin, since a transformed box can exceed the int range. The old numbering stays where the other mesh is open, without converters, or where no part of a set has such a vertex. New test BooleanForceCutExactWindings: a box enclosing the cube, and an inside-out box crossing another box, both with and without rigidB2A; it fails before this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit fd7078c: the parallel getBadFacesAfterCut moves to its own pull request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Under force fill,
cutMeshnow splits the contours where they cross inside bad faces, using the exact predicates of #7222, when it has the boolean's sorting data. The crossing vertices also go intoresultCut. A forced boolean with a self-intersecting mesh now cuts the faces where its contours cross. Before, it kept such a face whole and left the islands in it as separate shells. It now tells the parts of the cut mesh apart by the winding number of the other mesh around them, and runs the graph cut only where the paths are open.CutMeshParameters::forceFill: the enumforceFillMode(ForceFill { None, Good, All }) becomesbool forceFill, wherefalse=Noneandtrue=All.Goodhad no caller. All callers are updated: the boolean'sforceCut,offsetSurfaceLineToMeshContours, the tool path smoothing, the Wasm binding and the tests.PlanarTriangulation::findCrossingsruns the sweep of the exactgetMonotonePlanover all loops of one cut triangle. Each crossing it meets becomes a point ofCutTrianglePredicates, defined by the planes of the two cuts. It returns the crossings in sweep order, which is also their order along every edge. The face fails if an edge without a cut (a side of the face) crosses another edge, or if the predicates fail. The exactgetMonotonePlanandfindCrossingsshare the setup of the points and predicates (setLoopPredicates), whose check for points defined alike now also covers crossings of two cuts.cutMesh, underforceFillwithsortData):resultCut, and keep the edge's face and side info.fbsWithContourIntersections: their contours do cross there.pointOfinconnectLoneContours) moved into a struct used by both steps. The new step adds the crossings to it, and the cut of every path edge. The faces the contours enter are collected before the cut, only underforceFillwithsortData.getBadFacesAfterCutflagged a loop only if it came back along the reverse of the edge it started from. A loop that ran another edge both ways was missed if it was walked from an edge whose two sides are in different loops. For example, two contours whose chords cross in a face, where one chord crosses two others, gave one bad face out of two. Now every new edge records the start edge of the walk that passed it, and a walk that meets one of its own new edges again flags the face. Only new edges (contour pieces, chords) can bound one face's hole on both sides in valid loops. This also affects plain cuts: such faces are reported as bad instead of being filled with overlapping triangles. On the regression set this only changes three selfBoolean errors (more nested-intersection faces, see below).findMeshPart, forced boolean only):Not covered yet:
getBadFacesAfterCutreports nothing, because every chord crosses an even number of others (two boxes whose sides cross twice in one face). Nothing splits them, and their fill overlaps; the windings around them can be wrong, so the forced result can be wrong well beyond that face. The plain boolean misses these faces too and returns the overlapping fill without an error;sortData, which have no exact predicates.Tests
CutMeshForceFillCrossingIslands: two overlapping boxes poke through one triangle of a cube, giving two lone contours that cross twice.CutMeshForceFillCrossingContours: two long boxes across the top of the cube cross four times, twice in each of its triangles.CutMeshForceFillIslandCrossingContour: an island crosses a contour that goes through the face.BooleanForceCutCrossingContours: the boxes of the first test, with the cube as A and as B.forceCutkeeps the top of the cube outside the union of the footprints (inside it for the intersection).forceBooleangives one closed shell. Master kept that triangle whole, with each box tip as its own shell.MRTest passes in Release and Debug (605).
Boolean regression set against master ad45339 (no Boolean change since): no regressions. 3093 of 3097 Release rows and 1259 of 1262 Debug rows are bit-identical. The changed rows:
spec_SelfIntSec, now without the graph cut: 4 components instead of 43 (the smallest of 84 faces instead of 1), volume 1.44 instead of 3.17. Its mesh A crosses itself too deeply forselfBoolean, so there is no reference.Contour regression set: bit-identical to master in Release and Debug.
Random check against an exact reference: a cube and 2-3 random overlapping boxes or low-resolution spheres through its top; about 56,000 pairs over six kinds (rotated, on a grid, axis-aligned). The forced Union keeps the cube's surface outside B. The reference is the plain Union of the cube and B's pieces united first.
Benchmark
3 alternating rounds per comparison, no forced run dropped as disturbed; every MRTest was opted out of Windows power throttling, which otherwise halves the speed while the launching window is in the background.
Plain boolean against master 7291b95 (the last commit does not change it: within noise against the previous head, 3366 spheres -0.2%):
3366 spheres (DifferenceAB, 2000 rotations): AVG 11.024 → 11.031 ms (+0.1%), MEDIAN 10.848 → 10.864.
Forced boolean (
forceCut), the last commit against the previous head of this PR. That head was within noise of master here: big cases -1.3..+2.9%, spheres +1.4%, since both ran the graph cut.3366 spheres (DifferenceAB, 2000 rotations): AVG 19.806 → 14.088 ms (-28.9%), MEDIAN 19.570 → 13.873.
Generated with Claude Code