Flatten the boolean2 engine into src/ and consolidate to fewer files (#1754)

* Move boolean2 engine files up to src/; rename to boolean2_* form

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Consolidate the nine boolean2 engine headers into src/boolean2.h

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Merge the boolean2 pipeline and containment sources into their targets

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update gen_docs, Doxyfile, docs, and format.sh for the flat boolean2 layout

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Restore the module contract comments dropped by the source merges

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix Boolean2.md architecture table and two merge leftovers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Zach Loafman
2026-06-11 22:27:35 +12:00
committed by GitHub
co-authored by Claude Opus 4.8
parent db042ab153
commit 9008e157a1
32 changed files with 1776 additions and 2196 deletions
+1 -1
View File
@@ -976,7 +976,7 @@ INPUT = include/manifold/manifold.h \
src/manifold.cpp \
src/constructors.cpp \
src/polygon.cpp \
src/cross_section/cross_section.cpp \
src/cross_section.cpp \
src/sdf.cpp \
README.md \
samples/include/samples.h \
+1 -1
View File
@@ -52,7 +52,7 @@ set(
${PROJECT_SOURCE_DIR}/src/manifold.cpp
${PROJECT_SOURCE_DIR}/src/constructors.cpp
${PROJECT_SOURCE_DIR}/src/sort.cpp
${PROJECT_SOURCE_DIR}/src/cross_section/cross_section.cpp
${PROJECT_SOURCE_DIR}/src/cross_section.cpp
${PROJECT_SOURCE_DIR}/src/polygon.cpp
${PROJECT_SOURCE_DIR}/include/manifold/common.h
${CMAKE_CURRENT_SOURCE_DIR}/gen_docs.py
+1 -1
View File
@@ -92,7 +92,7 @@ collect(f"{base}/src/manifold.cpp", lambda s: method_re.search(s))
collect(f"{base}/src/constructors.cpp", lambda s: method_re.search(s))
collect(f"{base}/src/sort.cpp", lambda s: method_re.search(s))
collect(f"{base}/src/sdf.cpp", lambda s: method_re.search(s))
collect(f"{base}/src/cross_section/cross_section.cpp", lambda s: method_re.search(s))
collect(f"{base}/src/cross_section.cpp", lambda s: method_re.search(s))
collect(f"{base}/src/polygon.cpp", select_functions)
collect(f"{base}/include/manifold/common.h", select_functions)
+27 -24
View File
@@ -1,11 +1,13 @@
# Boolean2 CrossSection Core
#Boolean2 CrossSection Core
Boolean2 is the sole `CrossSection` implementation. There is no
Boolean2 is the sole `CrossSection` implementation.There is no
`MANIFOLD_CROSS_SECTION_BACKEND` selector and no Clipper2 dependency; Boolean2
is always built in-tree.
Boolean2 is a manifold-native 2D arrangement pipeline for polygon fill and
Boolean operations. The core library lives in `src/cross_section/boolean2/`,
Boolean operations. The core library lives in `src/boolean2*.{
h, cpp
}`,
and the public `CrossSection` methods dispatch to it. Sibling utilities provide
the decomposition and offset pieces that cover the rest of the public
`CrossSection` API.
@@ -40,29 +42,29 @@ Boolean2 builds a planar arrangement and filters it by per-face winding:
whose adjacent faces disagree under the requested rule.
The high-level fill/Boolean core API is in
`src/cross_section/boolean2/boolean2.h`. The lower-level driver returns
`src/boolean2.h`. The lower-level driver returns
retained directed sub-edges plus the merged vertex map, and the wrapper turns
those edges back into regularized `manifold::Polygons`. Offset and containment
helpers live in `offset.h` and `containment.h`; they are included by the
backend wiring rather than by `boolean2.h`.
helpers live in `boolean2_offset.cpp`, declared alongside the core API in
`boolean2.h`.
## Architecture
The main dataflow is:
`boolean2.h` -> `boolean2.cpp` -> `driver.cpp` ->
`canonicalize.cpp` -> `winding_filter.cpp` -> regularized `Polygons`.
`boolean2.h` -> `boolean2.cpp` (conversion -> arrangement driver ->
canonicalization) -> `boolean2_winding.cpp` -> regularized `Polygons`.
| Layer | Files | Role |
| --- | --- | --- |
| Public core API | `boolean2.h`, `boolean2.cpp` | Converts `Polygons` to local vertices plus directed edges, invokes one arrangement pass, and turns retained edges back into regularized output. |
| Arrangement coordinator | `driver.cpp` | Runs one pass of merge, edge-pair discovery, edge vertex insertion, crossing insertion, canonicalization, and winding filtering. |
| Geometry leaves | `vertex_merge.cpp`, `bvh.cpp`, `edge_vert_lists.cpp`, `intersections.cpp` | Provide the local geometric operations used by the arrangement pass. |
| Output filter | `canonicalize.cpp`, `winding_filter.cpp` | Splits directed edges into canonical sub-edges, builds halfedges, propagates winding, and keeps result boundary edges. |
| Sibling helpers | `offset.cpp`, `containment.cpp` | Offset and decomposition support for the rest of the `CrossSection` API. |
| Public core API | `boolean2.h`, `boolean2.cpp` (entry points) | Converts `Polygons` to local vertices plus directed edges, invokes one arrangement pass, and turns retained edges back into regularized output. |
| Arrangement coordinator | `boolean2.cpp` (driver section) | Runs one pass of merge, edge-pair discovery, edge vertex insertion, crossing insertion, canonicalization, and winding filtering. |
| Geometry leaves | `boolean2.cpp` (BVH, vertex merge, edge-vertex lists, intersections sections) | Provide the local geometric operations used by the arrangement pass. |
| Output filter | `boolean2.cpp` (canonicalize section), `boolean2_winding.cpp` | Splits directed edges into canonical sub-edges, builds halfedges, propagates winding, and keeps result boundary edges. |
| Sibling helpers | `boolean2_offset.cpp` | Offset and decomposition support for the rest of the `CrossSection` API. |
Shared leaf utilities live in `predicates.*`, and debug/performance tracing
lives in `diagnostics.h`.
Shared leaf utilities live in `boolean2_predicates.cpp`, and debug/performance
tracing lives in `boolean2_diagnostics.h`.
## Relationship To The Sketch
@@ -78,15 +80,16 @@ The main implementation differences are:
- Vertex merging uses deterministic union-find over all pairs within epsilon,
then chooses the source vertex nearest each cluster centroid as the
representative. The sketch called out weighted, up-to-date positions for
chains of nearby vertices; Boolean2 treats the first arrangement pass as the
robustness boundary rather than relying on a production fixed-point cleanup
loop.
- Broad phases use the local boolean2 sweep/BVH helpers. This keeps the core
independent from the 3D `Collider` surface while preserving the intended
sub-quadratic candidate search.
- Proper crossings are discovered from broad-phase edge pairs rather than a
Bentley-Ottmann sweep. Endpoint-on-edge and collinear degeneracies are
handled by the edge vertex lists; isolated crossings are inserted or snapped
chains of nearby vertices;
Boolean2 treats the first arrangement pass as the robustness boundary rather
than relying on a production fixed -
point cleanup loop.-
Broad phases use the local boolean2 sweep /
BVH helpers.This keeps the core independent from
the 3D `Collider` surface while preserving the intended sub -
quadratic candidate search.- Proper crossings are discovered from broad -
phase edge pairs rather than a Bentley - Ottmann sweep.Endpoint - on -
edge and collinear degeneracies are handled by the edge vertex lists; isolated crossings are inserted or snapped
to neighboring list vertices.
- Output filtering uses a halfedge face traversal and winding propagation
instead of independent midpoint ray casts for every sub-edge. This makes one
-1
View File
@@ -17,7 +17,6 @@ $CLANG_FORMAT -i bindings/wasm/test/*.ts &
$CLANG_FORMAT -i bindings/wasm/test/fixtures/*.{ts,mjs} &
$CLANG_FORMAT -i bindings/wasm/types/*.ts &
$CLANG_FORMAT -i src/*.{h,cpp} &
$CLANG_FORMAT -i src/*/*.cpp &
$CLANG_FORMAT -i include/manifold/*.h &
black --quiet bindings/python/examples/*.py &
+8 -31
View File
@@ -12,30 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
set(MANIFOLD_CROSS_SECTION_SRCS)
list(
APPEND
MANIFOLD_CROSS_SECTION_SRCS
cross_section/cross_section.cpp
cross_section/boolean2/boolean2.cpp
cross_section/boolean2/bvh.cpp
cross_section/boolean2/canonicalize.cpp
cross_section/boolean2/containment.cpp
cross_section/boolean2/diagnostics.cpp
cross_section/boolean2/driver.cpp
cross_section/boolean2/edge_vert_lists.cpp
cross_section/boolean2/intersections.cpp
cross_section/boolean2/offset.cpp
cross_section/boolean2/predicates.cpp
cross_section/boolean2/vertex_merge.cpp
cross_section/boolean2/winding_filter.cpp
)
set(
MANIFOLD_SRCS
boolean2.cpp
boolean2_diagnostics.cpp
boolean2_offset.cpp
boolean2_predicates.cpp
boolean2_winding.cpp
boolean3.cpp
boolean_result.cpp
constructors.cpp
cross_section.cpp
csg_tree.cpp
edge_op.cpp
execution_impl.cpp
@@ -51,24 +38,14 @@ set(
sort.cpp
subdivision.cpp
tree2d.cpp
${MANIFOLD_CROSS_SECTION_SRCS}
)
set(
MANIFOLD_PRIVATE_HDRS
boolean2.h
boolean2_diagnostics.h
boolean3.h
collider.h
cross_section/boolean2/boolean2.h
cross_section/boolean2/bvh.h
cross_section/boolean2/canonicalize.h
cross_section/boolean2/diagnostics.h
cross_section/boolean2/driver.h
cross_section/boolean2/edge_vert_lists.h
cross_section/boolean2/intersections.h
cross_section/boolean2/parallel_policy.h
cross_section/boolean2/predicates.h
cross_section/boolean2/vertex_merge.h
cross_section/boolean2/winding_filter.h
csg_tree.h
execution_impl.h
hashtable.h
+1240
View File
File diff suppressed because it is too large Load Diff
+337
View File
@@ -0,0 +1,337 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <algorithm>
#include <cstdint>
#include <limits>
#include <utility>
#include <vector>
#include "collider.h"
#include "manifold/common.h"
#include "manifold/optional_assert.h"
#include "parallel.h"
namespace manifold {
// Shared grain size for boolean2 parallel loops whose per-item work is small
// but non-trivial: BVH traversal, pair narrow tests, and merge broad-phase
// sweeps. Radix-tree construction uses a coarser build-specific grain because
// its per-index work is much smaller.
inline constexpr int kFineParallelGrainSize = 512;
constexpr double kU = 1.110223024625156540423631668e-16;
constexpr double kAlphaCoeff = 12.37;
struct EdgeM {
int v0, v1;
int mult = 1;
};
using OutEdge = EdgeM;
enum class GraphOrderKind {
ALessOrtho,
AGreaterOrtho,
EndpointTouch,
NoProjectionOverlap
};
struct GraphSegment2D {
vec2 p0;
vec2 p1;
// Stable fallback for geometrically identical ties (e.g. two input loops
// sharing an edge). Must come from a deterministic source, not BVH pair
// order.
int stableEdgeId = -1;
};
struct GraphOrder2D {
GraphOrderKind atMinProjection = GraphOrderKind::NoProjectionOverlap;
GraphOrderKind atMaxProjection = GraphOrderKind::NoProjectionOverlap;
bool coincidentOverlap = false;
bool properCrossing = false;
};
double SignedArea(const SimplePolygon& loop);
double TotalSignedArea(const Polygons& polys);
double EpsilonFromScale(double L, int k_budget = 1000);
double Coord(vec2 p, int axis);
// Projection-frame graph order over a positive-width shared projection
// interval. `ALessOrtho`/`AGreaterOrtho` compare the coordinate orthogonal to
// `axis`, so for axis==1 they compare x over a y interval.
GraphOrder2D CompareProjectedOrder(const GraphSegment2D& a,
const GraphSegment2D& b, int axis,
double overlapL, double overlapR,
double eps = 0.0);
bool IntersectSegments(const GraphSegment2D& a, const GraphSegment2D& b,
double eps, vec2* out);
inline constexpr int kEdgePairBvhThreshold = 1024;
// The radix-tree BVH is binary and has at most 32 Morton-code bits plus
// 32 index tie-breaker bits, so a depth-first traversal stack of 64 is enough.
inline constexpr int kBvhTraversalStackCapacity = 64;
struct Box2 {
vec2 min = vec2(std::numeric_limits<double>::infinity());
vec2 max = vec2(-std::numeric_limits<double>::infinity());
Box2() = default;
Box2(vec2 a, vec2 b)
: min(std::min(a.x, b.x), std::min(a.y, b.y)),
max(std::max(a.x, b.x), std::max(a.y, b.y)) {}
Box2 Union(const Box2& b) const {
return {vec2(std::min(min.x, b.min.x), std::min(min.y, b.min.y)),
vec2(std::max(max.x, b.max.x), std::max(max.y, b.max.y))};
}
vec2 Center() const { return (min + max) * 0.5; }
bool DoesOverlap(const Box2& b) const {
return min.x <= b.max.x && max.x >= b.min.x && min.y <= b.max.y &&
max.y >= b.min.y;
}
};
Box2 BoxOf2DPoint(vec2 p, double eps);
Box2 BoxOf2DEdge(vec2 p0, vec2 p1, double eps);
uint32_t MortonCode2(vec2 position, Box2 bBox);
struct BVH {
std::vector<Box2> nodeBBox;
std::vector<std::pair<int, int>> internalChildren;
std::vector<int> leafToOrig;
bool Empty() const { return internalChildren.empty(); }
};
BVH BVHBuildFromBoxes(const std::vector<Box2>& boxes);
template <typename Recorder, typename F>
inline void BVHCollisions(const BVH& bvh, Recorder& recorder, F&& queryBox,
int n, bool parallel) {
using namespace collider_internal;
if (bvh.Empty()) return;
auto collideOne = [&](int queryIdx) {
const Box2 query = queryBox(queryIdx);
int stack[kBvhTraversalStackCapacity];
int top = -1;
int node = kRoot;
auto& local = recorder.local();
while (true) {
const int internal = Node2Internal(node);
const int child1 = bvh.internalChildren[internal].first;
const int child2 = bvh.internalChildren[internal].second;
auto recordOrTraverse = [&](int child) {
const bool overlaps = bvh.nodeBBox[child].DoesOverlap(query);
if (overlaps && IsLeaf(child)) {
const int leafIdx = Node2Leaf(child);
recorder.record(queryIdx, leafIdx, local);
}
return overlaps && IsInternal(child);
};
const bool traverse1 = recordOrTraverse(child1);
const bool traverse2 = recordOrTraverse(child2);
if (!traverse1 && !traverse2) {
if (top < 0) break;
node = stack[top--];
} else {
node = traverse1 ? child1 : child2;
if (traverse1 && traverse2) {
DEBUG_ASSERT(top + 1 < kBvhTraversalStackCapacity, logicErr,
"Boolean2 BVH traversal stack overflow");
stack[++top] = child2;
}
}
}
};
manifold::for_each_n(
parallel ? autoPolicy(n, kFineParallelGrainSize) : ExecutionPolicy::Seq,
countAt(0), n, collideOne);
}
template <typename F>
inline void CollidePairs(const BVH& bvh, const std::vector<Box2>& queries,
F&& f) {
if (bvh.Empty() || queries.empty()) return;
auto adapter = [&](int qi, int leafIdx) { f(qi, bvh.leafToOrig[leafIdx]); };
auto recorder = MakeSimpleRecorder(adapter);
auto qf = [&](int i) { return queries[i]; };
BVHCollisions(bvh, recorder, qf, static_cast<int>(queries.size()),
/*parallel=*/false);
}
struct CanonEdge {
int vMin, vMax;
int mult;
};
struct CanonicalSubEdges {
std::vector<CanonEdge> edges;
inline void Add(int v0, int v1, int mult) {
if (v0 == v1) return;
int vMin = std::min(v0, v1);
int vMax = std::max(v0, v1);
int signedMult = (v0 < v1) ? mult : -mult;
edges.push_back({vMin, vMax, signedMult});
}
inline void Finalize() {
manifold::stable_sort(edges.begin(), edges.end(),
[](const CanonEdge& a, const CanonEdge& b) {
if (a.vMin != b.vMin) return a.vMin < b.vMin;
return a.vMax < b.vMax;
});
size_t w = 0;
for (size_t r = 0; r < edges.size();) {
size_t k = r;
int sumMult = 0;
while (k < edges.size() && edges[k].vMin == edges[r].vMin &&
edges[k].vMax == edges[r].vMax) {
sumMult += edges[k].mult;
++k;
}
if (sumMult != 0) {
edges[w] = {edges[r].vMin, edges[r].vMax, sumMult};
++w;
}
r = k;
}
edges.resize(w);
}
};
// Split each directed input edge at the vertices in `lists[e]`, then merge
// matching undirected sub-edges by summing signed multiplicities.
//
// `edges` are the collapsed input edges. `lists[e]` is the sorted list of
// interior vertices that split edge `e`. The returned `edges` vector is sorted
// by `(vMin, vMax)` and omits sub-edges whose summed multiplicity is zero.
CanonicalSubEdges Canonicalize(const std::vector<EdgeM>& edges,
const std::vector<std::vector<int>>& lists);
struct IntersectionPoint {
int i;
int j;
vec2 p;
};
struct VertexMerge {
std::vector<int> inputVert2Merged;
std::vector<vec2> verts;
};
VertexMerge MergeVerts(const std::vector<vec2>& in, double eps);
bool VESetContains(const std::vector<int>& vec, int x);
void VESetInsert(std::vector<int>* vec, int x);
std::vector<EdgeM> RemapAndCollapse(const std::vector<EdgeM>& edges,
const std::vector<int>& inputVert2Merged);
struct NarrowPhaseResult {
std::vector<std::vector<int>> lists;
std::vector<IntersectionPoint> intersections;
};
// Combined narrow phase over broad-phase edge pairs. Produces sorted
// edge-vertex split lists and independent proper edge-edge intersection
// candidates without mutating `verts` or `edges`; serial vs TBB execution is
// an internal thresholded implementation detail.
NarrowPhaseResult BuildListsAndFindIntersections(
const std::vector<EdgeM>& edges, const std::vector<vec2>& verts, double eps,
const std::vector<std::pair<int, int>>& pairs);
inline constexpr double kIntersectionMergeEpsFactor = 10.0;
void CollectIntersectionPairs(const std::vector<EdgeM>& edges,
const std::vector<vec2>& verts, double eps,
const std::vector<Box2>& edgeBoxes,
const BVH& bvh,
std::vector<std::pair<int, int>>* pairs);
// Serially materialize precomputed proper intersections into caller-owned
// containers. `verts` and `lists` are taken by value so callers can move in the
// post-narrow-phase state; the returned fields are those same containers after
// appending/snapping intersection vertices and updating edge split lists.
// `vertEdges` records which edges meet at each materialized or snapped
// intersection vertex; nearby-intersection merge uses that incidence to
// distinguish unrelated near vertices from vertices that share an intersection
// source.
struct IntersectionInsertion {
std::vector<vec2> verts;
std::vector<std::vector<int>> lists;
std::vector<std::vector<int>> vertEdges;
};
IntersectionInsertion FindAndInsertIntersections(
const std::vector<EdgeM>& edges, std::vector<vec2> verts,
std::vector<std::vector<int>> lists, double eps,
const std::vector<Box2>& edgeBoxes, const BVH& bvh,
const std::vector<IntersectionPoint>& precomputedIntersections);
struct Trace;
enum class WindRule {
Add,
Intersect,
};
std::vector<OutEdge> FilterByWindingHalfedges(const CanonicalSubEdges& canon,
const std::vector<vec2>& verts,
bool debug = false,
WindRule rule = WindRule::Add,
Trace* trace = nullptr);
struct OverlapResult {
std::vector<vec2> verts;
std::vector<OutEdge> edges;
std::vector<int> inputVert2Merged;
int numMergedVerts;
};
// `eps` is the fresh per-op FP-noise bound (3D analogue: Impl::epsilon_).
// `tolerance` is the propagated drift bound (3D analogue: Impl::tolerance_);
// floored to `eps` if smaller. Nearby-intersection new-to-old snaps use
// `tolerance + eps` for prior drift plus current-op error; MergeVerts stays at
// `eps`.
OverlapResult RemoveOverlaps2D(const std::vector<vec2>& vertsIn,
const std::vector<EdgeM>& edgesIn, double eps,
double tolerance = 0.0, bool debug = false,
WindRule pred = WindRule::Add,
Trace* trace = nullptr);
double InferEps(const Polygons& a, const Polygons& b);
std::pair<std::vector<vec2>, std::vector<EdgeM>> PolygonsToInput(
const Polygons& polys);
Polygons OutEdgesToPolygons(const std::vector<vec2>& verts,
const std::vector<OutEdge>& edges);
// Regularize one polygon set under the Positive (Add) winding rule at
// machine-scale eps. Fill-rule application, not tolerance decimation.
Polygons ApplyFillRule(const Polygons& polys, double eps);
Polygons Boolean2D(const Polygons& a, const Polygons& b, OpType op,
double eps = 0.0, double tolerance = 0.0);
// Polygon offset backing CrossSection::Offset.
Polygons Offset(const Polygons& in, double delta, JoinType jt,
double miterLimit = 2.0, int circularSegments = 0);
// Group regularized simple loops into outer-ring components with their
// directly contained holes, backing CrossSection::Decompose.
std::vector<Polygons> DecomposeByContainment(const Polygons& polys);
} // namespace manifold
@@ -12,15 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "diagnostics.h"
#include "boolean2_diagnostics.h"
#ifdef MANIFOLD_DEBUG
#include <sstream>
#include <string>
#endif
#include "winding_filter.h"
namespace manifold {
#ifdef MANIFOLD_DEBUG
@@ -28,11 +28,10 @@
#include "manifold/manifold.h"
#endif
#include "canonicalize.h"
#include "boolean2.h"
namespace manifold {
enum class WindRule;
struct Trace;
struct PhaseAcc {
@@ -14,11 +14,12 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
#include "../../utils.h"
#include "boolean2.h"
#include "predicates.h"
#include "manifold/optional_assert.h"
#include "utils.h"
// Polygon offset backing `CrossSection::Offset`.
namespace manifold {
@@ -283,4 +284,151 @@ Polygons Offset(const Polygons& in, double delta, JoinType jt,
return ApplyFillRule(offsetRings, eps);
}
// ===== Containment decompose =====
// Standalone polygon utilities used by `CrossSection::Decompose`
// (containment grouping). Not part of the boolean-overlap algorithm
// pipeline; operates on already-regularized Positive `Polygons`.
namespace {
bool PointOnSegment(vec2 p, vec2 a, vec2 b, double eps) {
// Cheap bounding-box reject first; CCW (the colinearity test) is costlier.
if (p.x < std::min(a.x, b.x) - eps || p.x > std::max(a.x, b.x) + eps ||
p.y < std::min(a.y, b.y) - eps || p.y > std::max(a.y, b.y) + eps) {
return false;
}
return CCW(a, b, p, eps) == 0;
}
// Standard ray-cast point-in-polygon: cast +x ray from `p`, count
// crossings of `ring`'s edges. Returns true when `p` is inside `ring`
// or on its boundary. Boundary inclusion matters for containment
// grouping: boolean output can contain a hole ring touching its outer
// ring at a vertex, and that hole still belongs to the outer component.
// `eps` is `ring`'s scale-derived tolerance, hoisted in by the caller so it
// is computed once per ring rather than once per query point.
bool PointInRing(vec2 p, const SimplePolygon& ring, double eps) {
bool inside = false;
const int n = static_cast<int>(ring.size());
for (int i = 0, j = n - 1; i < n; j = i++) {
const vec2 a = ring[i], b = ring[j];
if (PointOnSegment(p, a, b, eps)) return true;
if (((a.y > p.y) != (b.y > p.y)) &&
(p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x)) {
inside = !inside;
}
}
return inside;
}
// Half the larger bbox extent: the length scale feeding EpsilonFromScale.
// This is a SIZE scale (extent), deliberately not Rect::Scale() (which is the
// max absolute coordinate); the two diverge for rings far from the origin.
double BoxScale(const Rect& box) {
const vec2 size = box.Size();
return 0.5 * std::max(size.x, size.y);
}
struct RingInfo {
Rect box; // bbox
double area; // signed; CCW > 0, CW (hole) < 0
double eps; // scale-derived tolerance, EpsilonFromScale(BoxScale(box))
};
RingInfo Summarize(const SimplePolygon& ring) {
RingInfo r;
for (const vec2& v : ring) r.box.Union(v);
r.area = SignedArea(ring);
r.eps = EpsilonFromScale(BoxScale(r.box));
return r;
}
// Tolerant bbox containment prefilter: is a's box inside b's box, inflated by
// b's epsilon? Uses the same eps as the later ring test so it is never
// stricter than that test.
bool BoxInside(const RingInfo& a, const RingInfo& b) {
const double eps = b.eps;
return a.box.min.x >= b.box.min.x - eps && a.box.min.y >= b.box.min.y - eps &&
a.box.max.x <= b.box.max.x + eps && a.box.max.y <= b.box.max.y + eps;
}
bool RingInside(const SimplePolygon& a, const SimplePolygon& b, double bEps) {
return std::all_of(a.begin(), a.end(),
[&](const vec2& p) { return PointInRing(p, b, bEps); });
}
} // namespace
// Decompose regularized simple loops into outer-ring components with their
// directly contained holes.
std::vector<Polygons> DecomposeByContainment(const Polygons& polys) {
Polygons rings;
std::vector<RingInfo> info;
rings.reserve(polys.size());
info.reserve(polys.size());
for (const auto& r : polys) {
if (r.size() < 3) continue; // sub-3 rings bound no area
RingInfo ri = Summarize(r);
// Drop near-zero-area (collinear/sliver) rings before they leak as
// degenerate hole contours. Area is length^2, so the threshold is
// length * epsilon (scale-consistent), matching the CrossSection
// area-drop idiom rather than comparing an area against a length eps.
const vec2 size = ri.box.Size();
if (std::fabs(ri.area) <= std::max(size.x, size.y) * ri.eps) continue;
rings.push_back(r);
info.push_back(ri);
}
const int n = static_cast<int>(rings.size());
if (n == 0) return {};
// For each ring, find its parent: the smallest-area ring (by |area|)
// that contains it. O(n^2) bbox/ring-in-poly check; fine for the
// hundreds of rings typical of CrossSection inputs.
std::vector<int> parent(n, -1);
for (int i = 0; i < n; ++i) {
double bestParentArea = std::numeric_limits<double>::infinity();
for (int j = 0; j < n; ++j) {
if (i == j) continue;
if (!BoxInside(info[i], info[j])) continue;
if (!RingInside(rings[i], rings[j], info[j].eps)) continue;
const double aj = std::fabs(info[j].area);
if (aj < bestParentArea) {
bestParentArea = aj;
parent[i] = j;
}
}
}
// Every positive ring seeds a component, regardless of its parent's sign.
// For regularized Positive input a positive ring never nests directly in
// another positive ring, so this is the ordinary even-odd nesting; for raw
// input it makes positive-in-positive a deliberate double-region rather than
// a silent area drop.
std::vector<int> compOf(n, -1);
std::vector<Polygons> components;
for (int i = 0; i < n; ++i) {
if (info[i].area <= 0) continue;
compOf[i] = static_cast<int>(components.size());
components.emplace_back();
components.back().push_back(rings[i]);
}
// Hole rings attach to the component of their nearest positive ancestor.
for (int i = 0; i < n; ++i) {
if (info[i].area > 0) continue; // skip outers
int p = parent[i];
// Walk up until we find a positive ring; that's the containing component.
// Bounded by the ring count in case a malformed parent chain loops.
for (int hops = 0; p >= 0 && info[p].area < 0 && hops <= n; ++hops) {
p = parent[p];
}
if (p < 0 || compOf[p] < 0) continue; // orphan hole; drop
components[compOf[p]].push_back(rings[i]);
}
// Seeding every positive ring above leaves none without a component.
for (int i = 0; i < n; ++i) {
DEBUG_ASSERT(info[i].area <= 0 || compOf[i] >= 0, logicErr,
"positive ring left without a component");
}
return components;
}
} // namespace manifold
@@ -14,14 +14,13 @@
//
// Leaf primitives for the Boolean2 pipeline.
#include "predicates.h"
#include <algorithm>
#include <cmath>
#include <vector>
#include "../../shared.h"
#include "../../utils.h"
#include "boolean2.h"
#include "shared.h"
#include "utils.h"
namespace manifold {
@@ -33,12 +33,10 @@
#include <utility>
#include <vector>
#include "../../parallel.h"
#include "canonicalize.h"
#include "diagnostics.h"
#include "boolean2.h"
#include "boolean2_diagnostics.h"
#include "manifold/optional_assert.h"
#include "predicates.h"
#include "winding_filter.h"
#include "parallel.h"
namespace manifold {
@@ -18,10 +18,10 @@
#include <cmath>
#include <queue>
#include "../svd.h"
#include "../utils.h"
#include "boolean2/boolean2.h"
#include "boolean2.h"
#include "manifold/optional_assert.h"
#include "svd.h"
#include "utils.h"
// CrossSection implementation. Public methods route through `boolean2/` for
// polygon boolean operations, containment grouping, offsets, and polygon
-261
View File
@@ -1,261 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Polygons-typed API over the Boolean2 arrangement pipeline.
#include "boolean2.h"
#include <algorithm>
#include <cmath>
#include <utility>
#include <vector>
#include "driver.h"
#include "manifold/optional_assert.h"
#include "predicates.h"
#include "winding_filter.h"
namespace manifold {
namespace {
bool AllFinite(const Polygons& polys) {
for (const auto& loop : polys) {
for (const vec2& v : loop) {
if (!std::isfinite(v.x) || !std::isfinite(v.y)) return false;
}
}
return true;
}
void AccumulateBounds(const Polygons& polys, Rect& box) {
for (const auto& loop : polys) {
for (const vec2& v : loop) {
box.Union(v);
}
}
}
vec2 MakeLocalOrigin(const Polygons& a, const Polygons& b = {}) {
Rect box;
AccumulateBounds(a, box);
AccumulateBounds(b, box);
return box.IsFinite() ? box.Center() : vec2(0.0);
}
Polygons TranslatePolygons(const Polygons& polys, vec2 delta) {
Polygons out = polys;
for (auto& loop : out) {
for (vec2& v : loop) v = v + delta;
}
return out;
}
// Classify the CCW turn from `ref` to `dir` for deterministic polar ordering.
// Group 0 is (0, pi], group 1 is (pi, 2pi), and group 2 is the zero turn.
// A zero turn is ordered last so loop tracing does not immediately reverse
// over the incoming edge when any positive turn is available.
int CcwTurnGroup(vec2 ref, vec2 dir) {
const double cross = la::cross(ref, dir);
if (cross > 0) return 0;
if (cross < 0) return 1;
return la::dot(ref, dir) < 0 ? 0 : 2;
}
// Order outgoing candidates for loop extraction by the smallest positive CCW
// turn from the reverse incoming direction. Collinear ties prefer the nearer
// endpoint, then stable edge id, so extraction is deterministic.
bool CcwTurnLess(vec2 ref, vec2 a, int edgeA, vec2 b, int edgeB) {
const int groupA = CcwTurnGroup(ref, a);
const int groupB = CcwTurnGroup(ref, b);
if (groupA != groupB) return groupA < groupB;
const double cross = la::cross(a, b);
if (cross != 0) return cross > 0;
const double dist2A = la::dot(a, a);
const double dist2B = la::dot(b, b);
if (dist2A != dist2B) return dist2A < dist2B;
return edgeA < edgeB;
}
void PushLoopIfNondegenerate(const std::vector<vec2>& verts,
const std::vector<int>& loopVerts,
Polygons& polys) {
if (loopVerts.size() >= 3) {
SimplePolygon loop;
loop.reserve(loopVerts.size());
for (int v : loopVerts) loop.push_back(verts[v]);
polys.push_back(std::move(loop));
}
}
void PushSimpleLoops(const std::vector<vec2>& verts, std::vector<int> loopVerts,
Polygons& polys) {
for (;;) {
bool split = false;
for (size_t i = 1; i < loopVerts.size() && !split; ++i) {
for (size_t j = 0; j < i; ++j) {
if (loopVerts[i] != loopVerts[j]) continue;
std::vector<int> simple(loopVerts.begin() + j, loopVerts.begin() + i);
PushLoopIfNondegenerate(verts, simple, polys);
loopVerts.erase(loopVerts.begin() + j + 1, loopVerts.begin() + i + 1);
split = true;
break;
}
}
if (!split) break;
}
PushLoopIfNondegenerate(verts, loopVerts, polys);
}
void AppendInput(const Polygons& polys, int mult, std::vector<vec2>& verts,
std::vector<EdgeM>& edges) {
for (const auto& loop : polys) {
if (loop.size() < 3) continue;
const int base = static_cast<int>(verts.size());
const int n = static_cast<int>(loop.size());
for (const auto& v : loop) verts.push_back(v);
for (int i = 0; i < n; ++i) {
edges.push_back({base + i, base + ((i + 1) % n), mult});
}
}
}
// `bSign` is +1 for Add/Intersect-style accumulation and -1 for Subtract.
Polygons ApplyFillRule(const Polygons& a, const Polygons& b, int bSign,
WindRule rule, double eps, double tolerance) {
DEBUG_ASSERT(bSign == 1 || bSign == -1, logicErr,
"Boolean2 input multiplicity must be +/-1");
if (!AllFinite(a) || !AllFinite(b)) return {};
const vec2 origin = MakeLocalOrigin(a, b);
Polygons localA = TranslatePolygons(a, -origin);
Polygons localB = TranslatePolygons(b, -origin);
if (eps <= 0.0) eps = InferEps(localA, localB);
if (tolerance < eps) tolerance = eps;
std::vector<vec2> verts;
std::vector<EdgeM> edges;
AppendInput(localA, 1, verts, edges);
AppendInput(localB, bSign, verts, edges);
if (verts.empty()) return {};
OverlapResult r =
RemoveOverlaps2D(verts, edges, eps, tolerance, /*debug=*/false, rule);
return TranslatePolygons(OutEdgesToPolygons(r.verts, r.edges), origin);
}
} // namespace
// Flatten manifold::Polygons into the lower-level (verts, edges) input.
// Each loop becomes a sequence of edges with mult=+1.
std::pair<std::vector<vec2>, std::vector<EdgeM>> PolygonsToInput(
const Polygons& polys) {
std::vector<vec2> verts;
std::vector<EdgeM> edges;
AppendInput(polys, 1, verts, edges);
return {std::move(verts), std::move(edges)};
}
// Walk retained directed sub-edges into regularized polygon loops.
Polygons OutEdgesToPolygons(const std::vector<vec2>& verts,
const std::vector<OutEdge>& edges) {
const int nE = static_cast<int>(edges.size());
// Per-vertex outgoing edges; the next-pointer loop scans each list with
// deterministic cross/dot comparisons.
std::vector<std::vector<int>> outgoing(verts.size());
for (int i = 0; i < nE; ++i) outgoing[edges[i].v0].push_back(i);
std::vector<bool> visited(nE, false);
Polygons polys;
for (int start = 0; start < nE; ++start) {
if (visited[start]) continue;
const int startV = edges[start].v0;
std::vector<int> loopVerts;
int cur = start;
bool closed = false;
while (cur >= 0 && !visited[cur]) {
visited[cur] = true;
loopVerts.push_back(edges[cur].v0);
const int destV = edges[cur].v1;
// The next-pointer scan skips visited edges (including `start`), so
// detect closure by the walk reaching startV rather than by re-selecting
// the start edge as `next`.
if (destV == startV) {
closed = true;
break;
}
if (destV < 0 || destV >= static_cast<int>(outgoing.size()) ||
outgoing[destV].empty()) {
cur = -1;
break;
}
// Continue with the smallest positive CCW turn from the reverse incoming
// edge, matching the halfedge winding filter.
const vec2 vp = verts[destV];
const vec2 ref = verts[edges[cur].v0] - vp;
const auto& lst = outgoing[destV];
int next = -1;
vec2 bestDir(0, 0);
for (int e : lst) {
if (visited[e]) continue;
const vec2 d = verts[edges[e].v1] - vp;
if (next < 0 || CcwTurnLess(ref, d, e, bestDir, next)) {
next = e;
bestDir = d;
}
}
cur = next;
}
if (!closed) {
DEBUG_ASSERT(false, logicErr,
"retained directed edges must form closed walks");
continue;
}
if (loopVerts.size() >= 3) {
PushSimpleLoops(verts, std::move(loopVerts), polys);
}
}
return polys;
}
// Apply the Positive (Add) winding rule to one polygon set, regularizing it at
// machine-scale eps with no extra tolerance. This is fill-rule application -
// what construction and Offset use to resolve self-intersections - as opposed
// to CrossSection::Simplify, which decimates at a user-designated tolerance.
Polygons ApplyFillRule(const Polygons& polys, double eps) {
return ApplyFillRule(polys, {}, 1, WindRule::Add, eps, 0.0);
}
// Infer eps from a polygon set's coordinate half-extent via Smith's
// alpha-budget formula. Keep this translation invariant so callers can safely
// use it before any local-origin translation.
double InferEps(const Polygons& a, const Polygons& b) {
Rect box;
AccumulateBounds(a, box);
AccumulateBounds(b, box);
if (!box.IsFinite()) return 0.0;
const vec2 halfSize = 0.5 * box.Size();
return EpsilonFromScale(Rect(-halfSize, halfSize).Scale());
}
// Binary boolean over one combined edge set; Subtract flips B's multiplicity.
Polygons Boolean2D(const Polygons& a, const Polygons& b, OpType op, double eps,
double tolerance) {
const int bSign = op == OpType::Subtract ? -1 : 1;
const WindRule rule =
op == OpType::Intersect ? WindRule::Intersect : WindRule::Add;
return ApplyFillRule(a, b, bSign, rule, eps, tolerance);
}
} // namespace manifold
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <utility>
#include <vector>
#include "manifold/common.h"
#include "predicates.h"
#include "winding_filter.h"
namespace manifold {
double InferEps(const Polygons& a, const Polygons& b);
std::pair<std::vector<vec2>, std::vector<EdgeM>> PolygonsToInput(
const Polygons& polys);
Polygons OutEdgesToPolygons(const std::vector<vec2>& verts,
const std::vector<OutEdge>& edges);
// Regularize one polygon set under the Positive (Add) winding rule at
// machine-scale eps. Fill-rule application, not tolerance decimation.
Polygons ApplyFillRule(const Polygons& polys, double eps);
Polygons Boolean2D(const Polygons& a, const Polygons& b, OpType op,
double eps = 0.0, double tolerance = 0.0);
// Polygon offset backing CrossSection::Offset.
Polygons Offset(const Polygons& in, double delta, JoinType jt,
double miterLimit = 2.0, int circularSegments = 0);
// Group regularized simple loops into outer-ring components with their
// directly contained holes, backing CrossSection::Decompose.
std::vector<Polygons> DecomposeByContainment(const Polygons& polys);
} // namespace manifold
-93
View File
@@ -1,93 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// BVH helpers for eps-padded 2D box queries.
#include "bvh.h"
namespace manifold {
namespace {
constexpr int kRadixTreeBuildGrainSize = 10000;
} // namespace
Box2 BoxOf2DPoint(vec2 p, double eps) {
const vec2 pad(eps, eps);
return Box2(p - pad, p + pad);
}
Box2 BoxOf2DEdge(vec2 p0, vec2 p1, double eps) {
const vec2 pad(eps, eps);
Box2 b(p0, p1);
return Box2(b.min - pad, b.max + pad);
}
uint32_t MortonCode2(vec2 position, Box2 bBox) {
using collider_internal::SpreadBits3;
const vec2 size = bBox.max - bBox.min;
const double xNorm = size.x > 0 ? (position.x - bBox.min.x) / size.x : 0.5;
const double yNorm = size.y > 0 ? (position.y - bBox.min.y) / size.y : 0.5;
const double xClamped = std::min(1023.0, std::max(0.0, 1024.0 * xNorm));
const double yClamped = std::min(1023.0, std::max(0.0, 1024.0 * yNorm));
const uint32_t x = SpreadBits3(static_cast<uint32_t>(xClamped));
const uint32_t y = SpreadBits3(static_cast<uint32_t>(yClamped));
return x * 2 + y;
}
BVH BVHBuildFromBoxes(const std::vector<Box2>& boxes) {
using namespace collider_internal;
const int n = static_cast<int>(boxes.size());
BVH out;
out.leafToOrig.resize(n);
for (int i = 0; i < n; ++i) out.leafToOrig[i] = i;
if (n == 0) return out;
Box2 bbox = boxes[0];
for (const auto& b : boxes) bbox = bbox.Union(b);
std::vector<uint32_t> morton(n);
for (int i = 0; i < n; ++i) morton[i] = MortonCode2(boxes[i].Center(), bbox);
manifold::stable_sort(out.leafToOrig.begin(), out.leafToOrig.end(),
[&](int a, int b) { return morton[a] < morton[b]; });
std::vector<uint32_t> sortedMorton(n);
for (int i = 0; i < n; ++i) {
sortedMorton[i] = morton[out.leafToOrig[i]];
}
const int numNodes = 2 * n - 1;
out.nodeBBox.resize(numNodes);
std::vector<int> nodeParent(numNodes, -1);
out.internalChildren.resize(n - 1, std::make_pair(-1, -1));
// Radix-tree node creation does little work per index, so use a coarser
// grain than BVH query traversal/narrow predicates to avoid scheduling
// overhead dominating construction.
manifold::for_each_n(
autoPolicy(n - 1, kRadixTreeBuildGrainSize), countAt(0), n - 1,
CreateRadixTree(
{VecView<int>(nodeParent.data(), nodeParent.size()),
VecView<std::pair<int, int>>(out.internalChildren.data(),
out.internalChildren.size()),
VecView<const uint32_t>(sortedMorton)}));
for (int i = 0; i < n; ++i)
out.nodeBBox[Leaf2Node(i)] = boxes[out.leafToOrig[i]];
auto buildNode = [&](auto&& self, int node) -> Box2 {
if (IsLeaf(node)) return out.nodeBBox[node];
const auto [left, right] = out.internalChildren[Node2Internal(node)];
out.nodeBBox[node] = self(self, left).Union(self(self, right));
return out.nodeBBox[node];
};
if (n > 1) buildNode(buildNode, kRoot);
return out;
}
} // namespace manifold
-125
View File
@@ -1,125 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <algorithm>
#include <cstdint>
#include <limits>
#include <utility>
#include <vector>
#include "../../collider.h"
#include "../../parallel.h"
#include "manifold/optional_assert.h"
#include "parallel_policy.h"
namespace manifold {
inline constexpr int kEdgePairBvhThreshold = 1024;
// The radix-tree BVH is binary and has at most 32 Morton-code bits plus
// 32 index tie-breaker bits, so a depth-first traversal stack of 64 is enough.
inline constexpr int kBvhTraversalStackCapacity = 64;
struct Box2 {
vec2 min = vec2(std::numeric_limits<double>::infinity());
vec2 max = vec2(-std::numeric_limits<double>::infinity());
Box2() = default;
Box2(vec2 a, vec2 b)
: min(std::min(a.x, b.x), std::min(a.y, b.y)),
max(std::max(a.x, b.x), std::max(a.y, b.y)) {}
Box2 Union(const Box2& b) const {
return {vec2(std::min(min.x, b.min.x), std::min(min.y, b.min.y)),
vec2(std::max(max.x, b.max.x), std::max(max.y, b.max.y))};
}
vec2 Center() const { return (min + max) * 0.5; }
bool DoesOverlap(const Box2& b) const {
return min.x <= b.max.x && max.x >= b.min.x && min.y <= b.max.y &&
max.y >= b.min.y;
}
};
Box2 BoxOf2DPoint(vec2 p, double eps);
Box2 BoxOf2DEdge(vec2 p0, vec2 p1, double eps);
uint32_t MortonCode2(vec2 position, Box2 bBox);
struct BVH {
std::vector<Box2> nodeBBox;
std::vector<std::pair<int, int>> internalChildren;
std::vector<int> leafToOrig;
bool Empty() const { return internalChildren.empty(); }
};
BVH BVHBuildFromBoxes(const std::vector<Box2>& boxes);
template <typename Recorder, typename F>
inline void BVHCollisions(const BVH& bvh, Recorder& recorder, F&& queryBox,
int n, bool parallel) {
using namespace collider_internal;
if (bvh.Empty()) return;
auto collideOne = [&](int queryIdx) {
const Box2 query = queryBox(queryIdx);
int stack[kBvhTraversalStackCapacity];
int top = -1;
int node = kRoot;
auto& local = recorder.local();
while (true) {
const int internal = Node2Internal(node);
const int child1 = bvh.internalChildren[internal].first;
const int child2 = bvh.internalChildren[internal].second;
auto recordOrTraverse = [&](int child) {
const bool overlaps = bvh.nodeBBox[child].DoesOverlap(query);
if (overlaps && IsLeaf(child)) {
const int leafIdx = Node2Leaf(child);
recorder.record(queryIdx, leafIdx, local);
}
return overlaps && IsInternal(child);
};
const bool traverse1 = recordOrTraverse(child1);
const bool traverse2 = recordOrTraverse(child2);
if (!traverse1 && !traverse2) {
if (top < 0) break;
node = stack[top--];
} else {
node = traverse1 ? child1 : child2;
if (traverse1 && traverse2) {
DEBUG_ASSERT(top + 1 < kBvhTraversalStackCapacity, logicErr,
"Boolean2 BVH traversal stack overflow");
stack[++top] = child2;
}
}
}
};
manifold::for_each_n(
parallel ? autoPolicy(n, kFineParallelGrainSize) : ExecutionPolicy::Seq,
countAt(0), n, collideOne);
}
template <typename F>
inline void CollidePairs(const BVH& bvh, const std::vector<Box2>& queries,
F&& f) {
if (bvh.Empty() || queries.empty()) return;
auto adapter = [&](int qi, int leafIdx) { f(qi, bvh.leafToOrig[leafIdx]); };
auto recorder = MakeSimpleRecorder(adapter);
auto qf = [&](int i) { return queries[i]; };
BVHCollisions(bvh, recorder, qf, static_cast<int>(queries.size()),
/*parallel=*/false);
}
} // namespace manifold
@@ -1,38 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "canonicalize.h"
namespace manifold {
CanonicalSubEdges Canonicalize(const std::vector<EdgeM>& edges,
const std::vector<std::vector<int>>& lists) {
CanonicalSubEdges out;
// Pre-reserve. Each input edge contributes (1 + lists[e].size()) sub-edges.
size_t total = edges.size();
for (const auto& l : lists) total += l.size();
out.edges.reserve(total);
for (size_t e = 0; e < edges.size(); ++e) {
int prev = edges[e].v0;
for (int v : lists[e]) {
out.Add(prev, v, edges[e].mult);
prev = v;
}
out.Add(prev, edges[e].v1, edges[e].mult);
}
out.Finalize();
return out;
}
} // namespace manifold
-75
View File
@@ -1,75 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <algorithm>
#include <vector>
#include "../../parallel.h"
#include "predicates.h"
namespace manifold {
struct CanonEdge {
int vMin, vMax;
int mult;
};
struct CanonicalSubEdges {
std::vector<CanonEdge> edges;
inline void Add(int v0, int v1, int mult) {
if (v0 == v1) return;
int vMin = std::min(v0, v1);
int vMax = std::max(v0, v1);
int signedMult = (v0 < v1) ? mult : -mult;
edges.push_back({vMin, vMax, signedMult});
}
inline void Finalize() {
manifold::stable_sort(edges.begin(), edges.end(),
[](const CanonEdge& a, const CanonEdge& b) {
if (a.vMin != b.vMin) return a.vMin < b.vMin;
return a.vMax < b.vMax;
});
size_t w = 0;
for (size_t r = 0; r < edges.size();) {
size_t k = r;
int sumMult = 0;
while (k < edges.size() && edges[k].vMin == edges[r].vMin &&
edges[k].vMax == edges[r].vMax) {
sumMult += edges[k].mult;
++k;
}
if (sumMult != 0) {
edges[w] = {edges[r].vMin, edges[r].vMax, sumMult};
++w;
}
r = k;
}
edges.resize(w);
}
};
// Split each directed input edge at the vertices in `lists[e]`, then merge
// matching undirected sub-edges by summing signed multiplicities.
//
// `edges` are the collapsed input edges. `lists[e]` is the sorted list of
// interior vertices that split edge `e`. The returned `edges` vector is sorted
// by `(vMin, vMax)` and omits sub-edges whose summed multiplicity is zero.
CanonicalSubEdges Canonicalize(const std::vector<EdgeM>& edges,
const std::vector<std::vector<int>>& lists);
} // namespace manifold
-172
View File
@@ -1,172 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
#include "../../utils.h"
#include "boolean2.h"
#include "manifold/optional_assert.h"
#include "predicates.h"
// Standalone polygon utilities used by `CrossSection::Decompose`
// (containment grouping). Not part of the boolean-overlap algorithm
// pipeline; operates on already-regularized Positive `Polygons`.
namespace manifold {
namespace {
bool PointOnSegment(vec2 p, vec2 a, vec2 b, double eps) {
// Cheap bounding-box reject first; CCW (the colinearity test) is costlier.
if (p.x < std::min(a.x, b.x) - eps || p.x > std::max(a.x, b.x) + eps ||
p.y < std::min(a.y, b.y) - eps || p.y > std::max(a.y, b.y) + eps) {
return false;
}
return CCW(a, b, p, eps) == 0;
}
// Standard ray-cast point-in-polygon: cast +x ray from `p`, count
// crossings of `ring`'s edges. Returns true when `p` is inside `ring`
// or on its boundary. Boundary inclusion matters for containment
// grouping: boolean output can contain a hole ring touching its outer
// ring at a vertex, and that hole still belongs to the outer component.
// `eps` is `ring`'s scale-derived tolerance, hoisted in by the caller so it
// is computed once per ring rather than once per query point.
bool PointInRing(vec2 p, const SimplePolygon& ring, double eps) {
bool inside = false;
const int n = static_cast<int>(ring.size());
for (int i = 0, j = n - 1; i < n; j = i++) {
const vec2 a = ring[i], b = ring[j];
if (PointOnSegment(p, a, b, eps)) return true;
if (((a.y > p.y) != (b.y > p.y)) &&
(p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x)) {
inside = !inside;
}
}
return inside;
}
// Half the larger bbox extent: the length scale feeding EpsilonFromScale.
// This is a SIZE scale (extent), deliberately not Rect::Scale() (which is the
// max absolute coordinate); the two diverge for rings far from the origin.
double BoxScale(const Rect& box) {
const vec2 size = box.Size();
return 0.5 * std::max(size.x, size.y);
}
struct RingInfo {
Rect box; // bbox
double area; // signed; CCW > 0, CW (hole) < 0
double eps; // scale-derived tolerance, EpsilonFromScale(BoxScale(box))
};
RingInfo Summarize(const SimplePolygon& ring) {
RingInfo r;
for (const vec2& v : ring) r.box.Union(v);
r.area = SignedArea(ring);
r.eps = EpsilonFromScale(BoxScale(r.box));
return r;
}
// Tolerant bbox containment prefilter: is a's box inside b's box, inflated by
// b's epsilon? Uses the same eps as the later ring test so it is never
// stricter than that test.
bool BoxInside(const RingInfo& a, const RingInfo& b) {
const double eps = b.eps;
return a.box.min.x >= b.box.min.x - eps && a.box.min.y >= b.box.min.y - eps &&
a.box.max.x <= b.box.max.x + eps && a.box.max.y <= b.box.max.y + eps;
}
bool RingInside(const SimplePolygon& a, const SimplePolygon& b, double bEps) {
return std::all_of(a.begin(), a.end(),
[&](const vec2& p) { return PointInRing(p, b, bEps); });
}
} // namespace
// Decompose regularized simple loops into outer-ring components with their
// directly contained holes.
std::vector<Polygons> DecomposeByContainment(const Polygons& polys) {
Polygons rings;
std::vector<RingInfo> info;
rings.reserve(polys.size());
info.reserve(polys.size());
for (const auto& r : polys) {
if (r.size() < 3) continue; // sub-3 rings bound no area
RingInfo ri = Summarize(r);
// Drop near-zero-area (collinear/sliver) rings before they leak as
// degenerate hole contours. Area is length^2, so the threshold is
// length * epsilon (scale-consistent), matching the CrossSection
// area-drop idiom rather than comparing an area against a length eps.
const vec2 size = ri.box.Size();
if (std::fabs(ri.area) <= std::max(size.x, size.y) * ri.eps) continue;
rings.push_back(r);
info.push_back(ri);
}
const int n = static_cast<int>(rings.size());
if (n == 0) return {};
// For each ring, find its parent: the smallest-area ring (by |area|)
// that contains it. O(n^2) bbox/ring-in-poly check; fine for the
// hundreds of rings typical of CrossSection inputs.
std::vector<int> parent(n, -1);
for (int i = 0; i < n; ++i) {
double bestParentArea = std::numeric_limits<double>::infinity();
for (int j = 0; j < n; ++j) {
if (i == j) continue;
if (!BoxInside(info[i], info[j])) continue;
if (!RingInside(rings[i], rings[j], info[j].eps)) continue;
const double aj = std::fabs(info[j].area);
if (aj < bestParentArea) {
bestParentArea = aj;
parent[i] = j;
}
}
}
// Every positive ring seeds a component, regardless of its parent's sign.
// For regularized Positive input a positive ring never nests directly in
// another positive ring, so this is the ordinary even-odd nesting; for raw
// input it makes positive-in-positive a deliberate double-region rather than
// a silent area drop.
std::vector<int> compOf(n, -1);
std::vector<Polygons> components;
for (int i = 0; i < n; ++i) {
if (info[i].area <= 0) continue;
compOf[i] = static_cast<int>(components.size());
components.emplace_back();
components.back().push_back(rings[i]);
}
// Hole rings attach to the component of their nearest positive ancestor.
for (int i = 0; i < n; ++i) {
if (info[i].area > 0) continue; // skip outers
int p = parent[i];
// Walk up until we find a positive ring; that's the containing component.
// Bounded by the ring count in case a malformed parent chain loops.
for (int hops = 0; p >= 0 && info[p].area < 0 && hops <= n; ++hops) {
p = parent[p];
}
if (p < 0 || compOf[p] < 0) continue; // orphan hole; drop
components[compOf[p]].push_back(rings[i]);
}
// Seeding every positive ring above leaves none without a component.
for (int i = 0; i < n; ++i) {
DEBUG_ASSERT(info[i].area <= 0 || compOf[i] >= 0, logicErr,
"positive ring left without a component");
}
return components;
}
} // namespace manifold
-253
View File
@@ -1,253 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// End-to-end Boolean2 driver. Stitches together vertex merging,
// edge collapse, near-vertex indexing, proper edge-edge crossing insertion,
// nearby-intersection merging, sub-edge canonicalization,
// and winding-rule filtering.
// Returns an OverlapResult holding the merged-vert list, the retained
// directed sub-edges, the inputVert2Merged remap, and the merged-vert count.
#include "driver.h"
#include <algorithm>
#include <cmath>
#include <utility>
#include <vector>
#include "../../disjoint_sets.h"
#include "../../parallel.h"
#include "bvh.h"
#include "canonicalize.h"
#include "diagnostics.h"
#include "edge_vert_lists.h"
#include "intersections.h"
#include "predicates.h"
#include "vertex_merge.h"
#include "winding_filter.h"
namespace manifold {
namespace {
// Unlike MergeVerts, this pass only considers vertices with intersection
// incidence: newly inserted intersection points and old vertices that an
// intersection snapped onto. The close-pair threshold is intersection-specific,
// and endpoint tolerance snaps are limited to truly-new vertices, so this does
// not re-run broad old-vertex clustering across the input.
void MergeNearbyIntersectionVerts(
std::vector<vec2>& verts, std::vector<EdgeM>& edges,
std::vector<std::vector<int>>& lists,
const std::vector<std::vector<int>>& vertEdges, int oldVertEnd, double eps,
double tolerance, std::vector<int>& inputVert2Merged) {
DisjointSets uf(static_cast<int>(verts.size()));
const double newToNewThresh = kIntersectionMergeEpsFactor * eps;
const double newToNewThresh2 = newToNewThresh * newToNewThresh;
// Fresh intersection vs old endpoint: prior drift plus current-op error.
const double newToOldThresh = tolerance + eps;
const double newToOldThresh2 = newToOldThresh * newToOldThresh;
std::vector<std::pair<int, int>> pairs;
std::vector<std::pair<double, int>> tlist;
for (size_t e = 0; e < edges.size(); ++e) {
const auto& list = lists[e];
const int v0 = edges[e].v0;
const int v1 = edges[e].v1;
const vec2 a = verts[v0];
const vec2 b = verts[v1];
const vec2 ab = b - a;
const double abLen2 = dot(ab, ab);
if (abLen2 == 0) continue;
const double abLen = std::sqrt(abLen2);
const double tThresh = newToNewThresh / abLen;
tlist.clear();
tlist.reserve(list.size());
for (int v : list) {
// Skip pure-old verts; keep snapped-onto-old for new-to-new pairing.
if (v >= static_cast<int>(vertEdges.size()) || vertEdges[v].empty())
continue;
const vec2 p = verts[v];
const double t = dot(p - a, ab) / abLen2;
tlist.emplace_back(t, v);
// Only truly-new verts: widening old-old would stack error across ops.
if (v >= oldVertEnd && newToOldThresh > 0.0) {
const vec2 dA = p - a;
if (dot(dA, dA) <= newToOldThresh2) {
const int p0 = std::min(v, v0);
const int q0 = std::max(v, v0);
if (p0 != q0) pairs.emplace_back(p0, q0);
}
const vec2 dB = p - b;
if (dot(dB, dB) <= newToOldThresh2) {
const int p0 = std::min(v, v1);
const int q0 = std::max(v, v1);
if (p0 != q0) pairs.emplace_back(p0, q0);
}
}
}
if (tlist.size() < 2) continue;
for (size_t i = 0; i < tlist.size(); ++i) {
for (size_t j = i + 1;
j < tlist.size() && tlist[j].first - tlist[i].first <= tThresh;
++j) {
const int va = tlist[i].second;
const int vb = tlist[j].second;
const vec2 d = verts[vb] - verts[va];
if (dot(d, d) > newToNewThresh2) continue;
const int p = std::min(va, vb);
const int q = std::max(va, vb);
pairs.emplace_back(p, q);
}
}
}
manifold::stable_sort(pairs.begin(), pairs.end());
pairs.erase(std::unique(pairs.begin(), pairs.end()), pairs.end());
for (const auto& pr : pairs) uf.unite(pr.first, pr.second);
const int nV = static_cast<int>(verts.size());
std::vector<vec2> sumPos(nV, vec2{0, 0});
std::vector<int> sumCnt(nV, 0);
int distinctClusters = 0;
for (int i = 0; i < nV; ++i) {
int r = uf.find(i);
if (sumCnt[r] == 0) ++distinctClusters;
sumPos[r] = sumPos[r] + verts[i];
sumCnt[r] += 1;
}
if (distinctClusters == nV) return;
std::vector<int> rootToNew(nV, -1);
std::vector<vec2> newVerts;
newVerts.reserve(distinctClusters);
for (int r = 0; r < nV; ++r) {
if (sumCnt[r] == 0) continue;
rootToNew[r] = static_cast<int>(newVerts.size());
newVerts.push_back(sumPos[r] * (1.0 / sumCnt[r]));
}
std::vector<int> preMergeToPost(nV);
for (int i = 0; i < nV; ++i) preMergeToPost[i] = rootToNew[uf.find(i)];
for (auto& e : edges) {
e.v0 = preMergeToPost[e.v0];
e.v1 = preMergeToPost[e.v1];
}
for (auto& list : lists) {
for (auto& v : list) v = preMergeToPost[v];
list.erase(std::unique(list.begin(), list.end()), list.end());
}
for (auto& r : inputVert2Merged) r = preMergeToPost[r];
verts = std::move(newVerts);
}
} // namespace
OverlapResult RemoveOverlaps2D(const std::vector<vec2>& vertsIn,
const std::vector<EdgeM>& edgesIn, double eps,
double tolerance, bool debug, WindRule pred,
Trace* trace) {
if (tolerance < eps) tolerance = eps;
auto& P = GlobalPhases();
ScopedTiming totalTiming(P.totalNs);
TraceRecorder traceRecorder(trace, eps, pred);
traceRecorder.RecordInput(vertsIn, edgesIn);
// Vertex merge.
VertexMerge merge;
{
ScopedTiming timing(P.mergeNs);
merge = MergeVerts(vertsIn, eps);
}
const int numMerged = static_cast<int>(merge.verts.size());
traceRecorder.RecordMergedVertices(merge.verts, merge.inputVert2Merged);
// Edge collapse.
std::vector<EdgeM> edges;
{
ScopedTiming timing(P.remapNs);
edges = RemapAndCollapse(edgesIn, merge.inputVert2Merged);
}
traceRecorder.RecordCollapsedEdges(merge.verts, edges);
// Build a shared edge-box array for edge-edge broad phase and near-vertex
// derivation. Medium cases use a sweep over these boxes; very large cases
// build a BVH when tree construction amortizes over enough queries.
thread_local static std::vector<Box2> edgeBoxes;
edgeBoxes.resize(edges.size());
for (size_t e = 0; e < edges.size(); ++e) {
edgeBoxes[e] =
BoxOf2DEdge(merge.verts[edges[e].v0], merge.verts[edges[e].v1], eps);
}
BVH bvh;
{
ScopedTiming timing(P.bvhBuildNs);
if (edges.size() >= kEdgePairBvhThreshold)
bvh = BVHBuildFromBoxes(edgeBoxes);
}
thread_local static std::vector<std::pair<int, int>> intersectionPairs;
// Collect edge pairs once, then derive both intersection candidates and
// near-vertex lists from that pair set. In polygon arrangements, a vertex
// that can split a non-incident edge must have an eps-padded vertex box
// overlapping that edge's eps-padded box, so it appears as one endpoint of
// an overlapping edge pair.
{
ScopedTiming timing(P.broadPairWorkNs);
CollectIntersectionPairs(edges, merge.verts, eps, edgeBoxes, bvh,
&intersectionPairs);
}
traceRecorder.RecordBroadPhasePairs(merge.verts, edges, intersectionPairs);
// Combined narrow phase: derive edge-vertex split lists and independent
// edge-edge intersections from the same broad-phase pair set. The helper
// decides internally whether the pair loop is serial or TBB.
NarrowPhaseResult narrow;
{
ScopedTiming timing(P.narrowPhaseNs);
narrow = BuildListsAndFindIntersections(edges, merge.verts, eps,
intersectionPairs);
}
traceRecorder.RecordEdgeVertLists(merge.verts, edges, narrow.lists);
IntersectionInsertion inserted;
{
ScopedTiming timing(P.findIxNs);
inserted = FindAndInsertIntersections(edges, std::move(merge.verts),
std::move(narrow.lists), eps,
edgeBoxes, bvh, narrow.intersections);
}
merge.verts = std::move(inserted.verts);
std::vector<std::vector<int>> lists = std::move(inserted.lists);
std::vector<std::vector<int>> vertEdges = std::move(inserted.vertEdges);
traceRecorder.RecordInsertedIntersections(merge.verts, edges, lists);
{
ScopedTiming timing(P.nearbyIxMergeNs);
MergeNearbyIntersectionVerts(merge.verts, edges, lists, vertEdges,
numMerged, eps, tolerance,
merge.inputVert2Merged);
}
traceRecorder.RecordNearbyIntersectionMerge(merge.verts, edges, lists);
// Sub-edge canonicalization.
CanonicalSubEdges canon;
{
ScopedTiming timing(P.canonNs);
canon = Canonicalize(edges, lists);
}
traceRecorder.RecordCanonicalSubedges(merge.verts, canon);
// Halfedge face-traversal winding filter.
std::vector<OutEdge> out;
{
ScopedTiming timing(P.filterHalfedgeNs);
out = FilterByWindingHalfedges(canon, merge.verts, debug, pred, trace);
}
traceRecorder.RecordFilteredOutput(merge.verts, out);
CountTimingCase();
return {std::move(merge.verts), std::move(out),
std::move(merge.inputVert2Merged), numMerged};
}
} // namespace manifold
-44
View File
@@ -1,44 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <vector>
#include "predicates.h"
#include "winding_filter.h"
namespace manifold {
struct Trace;
struct OverlapResult {
std::vector<vec2> verts;
std::vector<OutEdge> edges;
std::vector<int> inputVert2Merged;
int numMergedVerts;
};
// `eps` is the fresh per-op FP-noise bound (3D analogue: Impl::epsilon_).
// `tolerance` is the propagated drift bound (3D analogue: Impl::tolerance_);
// floored to `eps` if smaller. Nearby-intersection new-to-old snaps use
// `tolerance + eps` for prior drift plus current-op error; MergeVerts stays at
// `eps`.
OverlapResult RemoveOverlaps2D(const std::vector<vec2>& vertsIn,
const std::vector<EdgeM>& edgesIn, double eps,
double tolerance = 0.0, bool debug = false,
WindRule pred = WindRule::Add,
Trace* trace = nullptr);
} // namespace manifold
@@ -1,184 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Edge-vertex list construction: for each input edge, the sorted-by-parameter
// list of vertices that lie within eps of the edge interior. Candidates are
// derived from the same edge-pair broad phase used for intersections.
#include "edge_vert_lists.h"
#include <algorithm>
#include <vector>
#include "../../parallel.h"
#include "parallel_policy.h"
#include "predicates.h"
#include "vertex_merge.h"
namespace manifold {
namespace {
// Pair-count threshold above which the combined helper uses TBB. Smaller
// inputs stay serial to avoid setup overhead.
constexpr size_t kFusedNarrowParallelMin = 1024;
struct EdgeGeom {
vec2 a, ab;
double abLen2;
};
struct EdgeVertHit {
int e;
double t;
int v;
};
void BuildEdgeGeometry(const std::vector<EdgeM>& edges,
const std::vector<vec2>& verts,
std::vector<EdgeGeom>& edgeG) {
const int nE = static_cast<int>(edges.size());
edgeG.resize(nE);
for (int e = 0; e < nE; ++e) {
edgeG[e].a = verts[edges[e].v0];
edgeG[e].ab = verts[edges[e].v1] - edgeG[e].a;
edgeG[e].abLen2 = dot(edgeG[e].ab, edgeG[e].ab);
}
}
void RecordEdgeVertHit(const std::vector<EdgeM>& edges,
const std::vector<vec2>& verts,
const std::vector<EdgeGeom>& edgeG, double eps2, int v,
int e, std::vector<EdgeVertHit>* hits) {
if (v == edges[e].v0 || v == edges[e].v1) return;
const auto& g = edgeG[e];
if (g.abLen2 == 0) return;
const vec2 ap = verts[v] - g.a;
const double dotAB = ap.x * g.ab.x + ap.y * g.ab.y;
if (dotAB <= 0 || dotAB >= g.abLen2) return;
const double cross = ap.x * g.ab.y - ap.y * g.ab.x;
const double cross2 = cross * cross;
const double eps2_abLen2 = eps2 * g.abLen2;
if (cross2 > eps2_abLen2) return;
hits->push_back({e, dotAB / g.abLen2, v});
}
bool SharesEndpoint(const EdgeM& a, const EdgeM& b) {
return a.v0 == b.v0 || a.v0 == b.v1 || a.v1 == b.v0 || a.v1 == b.v1;
}
void ProcessEdgePair(const std::vector<EdgeM>& edges,
const std::vector<vec2>& verts,
const std::vector<EdgeGeom>& edgeG, double eps,
const std::pair<int, int>& pr,
std::vector<EdgeVertHit>* hits,
std::vector<IntersectionPoint>* intersections) {
const int i = pr.first;
const int j = pr.second;
const auto& ei = edges[i];
const auto& ej = edges[j];
const double eps2 = eps * eps;
RecordEdgeVertHit(edges, verts, edgeG, eps2, ei.v0, j, hits);
RecordEdgeVertHit(edges, verts, edgeG, eps2, ei.v1, j, hits);
RecordEdgeVertHit(edges, verts, edgeG, eps2, ej.v0, i, hits);
RecordEdgeVertHit(edges, verts, edgeG, eps2, ej.v1, i, hits);
if (SharesEndpoint(ei, ej)) return;
vec2 p;
if (IntersectSegments({verts[ei.v0], verts[ei.v1], i},
{verts[ej.v0], verts[ej.v1], j}, eps, &p)) {
intersections->push_back({i, j, p});
}
}
void MaterializeEdgeVertLists(int nE, std::vector<EdgeVertHit>& flatHits,
std::vector<std::vector<int>>& lists) {
lists.assign(nE, {});
manifold::stable_sort(flatHits.begin(), flatHits.end(),
[](const EdgeVertHit& a, const EdgeVertHit& b) {
if (a.e != b.e) return a.e < b.e;
if (a.t != b.t) return a.t < b.t;
return a.v < b.v;
});
for (size_t i = 0; i < flatHits.size();) {
const int e = flatHits[i].e;
size_t j = i;
while (j < flatHits.size() && flatHits[j].e == e) ++j;
auto& lst = lists[e];
lst.reserve(j - i);
int lastV = -1;
for (size_t k = i; k < j; ++k) {
if (flatHits[k].v == lastV) continue;
lst.push_back(flatHits[k].v);
lastV = flatHits[k].v;
}
i = j;
}
}
void SortIntersections(std::vector<IntersectionPoint>* intersections) {
manifold::stable_sort(
intersections->begin(), intersections->end(),
[](const IntersectionPoint& a, const IntersectionPoint& b) {
if (a.i != b.i) return a.i < b.i;
return a.j < b.j;
});
}
} // namespace
NarrowPhaseResult BuildListsAndFindIntersections(
const std::vector<EdgeM>& edges, const std::vector<vec2>& verts, double eps,
const std::vector<std::pair<int, int>>& pairs) {
const int nE = static_cast<int>(edges.size());
NarrowPhaseResult result;
// Per-calling-thread scratch; workers read these only through const refs.
thread_local static std::vector<EdgeGeom> edgeG;
BuildEdgeGeometry(edges, verts, edgeG);
const auto& edgeGRef = edgeG;
std::vector<EdgeVertHit> flatHits;
#if (MANIFOLD_PAR == 1)
if (pairs.size() >= kFusedNarrowParallelMin) {
struct Local {
std::vector<EdgeVertHit> hits;
std::vector<IntersectionPoint> ix;
};
tbb::combinable<Local> tls;
manifold::for_each_n(autoPolicy(pairs.size(), kFineParallelGrainSize),
countAt(size_t{0}), pairs.size(), [&](size_t idx) {
auto& l = tls.local();
ProcessEdgePair(edges, verts, edgeGRef, eps,
pairs[idx], &l.hits, &l.ix);
});
tls.combine_each([&](const Local& l) {
flatHits.insert(flatHits.end(), l.hits.begin(), l.hits.end());
result.intersections.insert(result.intersections.end(), l.ix.begin(),
l.ix.end());
});
} else
#endif
{
for (const auto& pr : pairs) {
ProcessEdgePair(edges, verts, edgeGRef, eps, pr, &flatHits,
&result.intersections);
}
}
MaterializeEdgeVertLists(nE, flatHits, result.lists);
SortIntersections(&result.intersections);
return result;
}
} // namespace manifold
@@ -1,38 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <utility>
#include <vector>
#include "predicates.h"
#include "vertex_merge.h"
namespace manifold {
struct NarrowPhaseResult {
std::vector<std::vector<int>> lists;
std::vector<IntersectionPoint> intersections;
};
// Combined narrow phase over broad-phase edge pairs. Produces sorted
// edge-vertex split lists and independent proper edge-edge intersection
// candidates without mutating `verts` or `edges`; serial vs TBB execution is
// an internal thresholded implementation detail.
NarrowPhaseResult BuildListsAndFindIntersections(
const std::vector<EdgeM>& edges, const std::vector<vec2>& verts, double eps,
const std::vector<std::pair<int, int>>& pairs);
} // namespace manifold
@@ -1,338 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Edge-edge intersection discovery: broad phase collects candidate edge
// pairs; the edge-vertex narrow phase precomputes proper intersections;
// FindAndInsertIntersections then snaps/inserts them into both edges'
// parameter lists. Eager-propagation re-sweeps any vert that snapped onto
// its k-th edge to detect k+1 incidences in one pass.
#include "intersections.h"
#include <algorithm>
#include <utility>
#include <vector>
#include "../../parallel.h"
#include "bvh.h"
#include "predicates.h"
#include "vertex_merge.h"
namespace manifold {
namespace {
// Encode (first, second) as uint64 and run manifold::stable_sort, which
// dispatches to the LSB-radix specialization for integral types - much
// faster than the comparator path for pair<int,int>. Casts through
// uint32_t preserve non-negative edge indices and put `first` in the
// high bits so int64 order matches lexicographic pair order.
void RadixSortPairs(std::vector<std::pair<int, int>>* pairs) {
const size_t n = pairs->size();
if (n < 2) return;
thread_local static std::vector<uint64_t> encoded;
encoded.resize(n);
for (size_t i = 0; i < n; ++i) {
const auto& pr = (*pairs)[i];
encoded[i] =
(static_cast<uint64_t>(static_cast<uint32_t>(pr.first)) << 32) |
static_cast<uint32_t>(pr.second);
}
manifold::stable_sort(encoded.begin(), encoded.end());
for (size_t i = 0; i < n; ++i) {
(*pairs)[i] = {static_cast<int>(encoded[i] >> 32),
static_cast<int>(encoded[i] & 0xFFFFFFFFu)};
}
}
void SortSmallInts(std::vector<int>* values) {
if (values->size() < 32) {
for (size_t i = 1; i < values->size(); ++i) {
const int x = (*values)[i];
size_t j = i;
while (j > 0 && x < (*values)[j - 1]) {
(*values)[j] = (*values)[j - 1];
--j;
}
(*values)[j] = x;
}
} else {
manifold::stable_sort(values->begin(), values->end());
}
}
// Drop shared-endpoint pairs when both non-shared endpoints are more than eps
// from the opposite edge line; then no near-line sliver split is possible.
bool SharedEndpointSafelySkippable(const EdgeM& a, const EdgeM& b,
const std::vector<vec2>& verts, double eps) {
int vS, wA, wB;
if (a.v0 == b.v0) {
vS = a.v0;
wA = a.v1;
wB = b.v1;
} else if (a.v0 == b.v1) {
vS = a.v0;
wA = a.v1;
wB = b.v0;
} else if (a.v1 == b.v0) {
vS = a.v1;
wA = a.v0;
wB = b.v1;
} else if (a.v1 == b.v1) {
vS = a.v1;
wA = a.v0;
wB = b.v0;
} else {
return false; // not shared
}
const vec2& pS = verts[vS];
const vec2& pA = verts[wA];
const vec2& pB = verts[wB];
const double dAx = pA.x - pS.x;
const double dAy = pA.y - pS.y;
const double dBx = pB.x - pS.x;
const double dBy = pB.y - pS.y;
const double cross = dAx * dBy - dAy * dBx;
const double cross2 = cross * cross;
const double lenA2 = dAx * dAx + dAy * dAy;
const double lenB2 = dBx * dBx + dBy * dBy;
const double eps2 = eps * eps;
// Drop iff w_A is > eps from line B AND w_B is > eps from line A.
// The check is symmetric in the |cross| numerator, so we only have
// to compare cross^2 against eps^2 * max(lenA2, lenB2).
return cross2 > eps2 * std::max(lenA2, lenB2);
}
// Broad phase: find overlapping edge AABBs, drop safe shared-endpoint pairs,
// and return lex-sorted pairs for deterministic insertion.
#if (MANIFOLD_PAR == 1)
struct PairsRecorder {
using Local = std::vector<std::pair<int, int>>;
const std::vector<int>& leafToOrig;
const std::vector<EdgeM>& edges;
const std::vector<vec2>& verts;
double eps;
tbb::combinable<Local> tls;
inline void record(int queryIdx, int leafIdx, Local& local) const {
const int li = leafToOrig[leafIdx];
if (queryIdx >= li) return;
if (SharedEndpointSafelySkippable(edges[queryIdx], edges[li], verts, eps))
return;
local.emplace_back(queryIdx, li);
}
Local& local() { return tls.local(); }
};
#endif
} // namespace
void CollectIntersectionPairs(const std::vector<EdgeM>& edges,
const std::vector<vec2>& verts, double eps,
const std::vector<Box2>& edgeBoxes,
const BVH& bvh,
std::vector<std::pair<int, int>>* pairs) {
const int nE = static_cast<int>(edges.size());
pairs->clear();
if (bvh.leafToOrig.empty()) {
thread_local static std::vector<int> order;
thread_local static std::vector<std::vector<int>> byFirst;
order.resize(nE);
for (int i = 0; i < nE; ++i) order[i] = i;
manifold::stable_sort(order.begin(), order.end(), [&](int a, int b) {
if (edgeBoxes[a].min.x != edgeBoxes[b].min.x)
return edgeBoxes[a].min.x < edgeBoxes[b].min.x;
return a < b;
});
if (static_cast<int>(byFirst.size()) < nE) byFirst.resize(nE);
for (int i = 0; i < nE; ++i) byFirst[i].clear();
int numPairs = 0;
for (int oi = 0; oi < nE; ++oi) {
const int i = order[oi];
const Box2& bi = edgeBoxes[i];
for (int oj = oi + 1; oj < nE; ++oj) {
const int j = order[oj];
const Box2& bj = edgeBoxes[j];
if (bj.min.x > bi.max.x) break;
if (bi.min.y <= bj.max.y && bi.max.y >= bj.min.y) {
if (SharedEndpointSafelySkippable(edges[i], edges[j], verts, eps))
continue;
const int first = std::min(i, j);
const int second = std::max(i, j);
byFirst[first].push_back(second);
++numPairs;
}
}
}
pairs->reserve(numPairs);
for (int first = 0; first < nE; ++first) {
auto& seconds = byFirst[first];
if (seconds.empty()) continue;
SortSmallInts(&seconds);
for (int second : seconds) pairs->emplace_back(first, second);
}
return;
}
#if (MANIFOLD_PAR == 1)
PairsRecorder rec{bvh.leafToOrig, edges, verts, eps, {}};
auto qf = [&](int i) { return edgeBoxes[i]; };
BVHCollisions(bvh, rec, qf, nE, /*parallel=*/true);
rec.tls.combine_each([&](const auto& localPairs) {
pairs->insert(pairs->end(), localPairs.begin(), localPairs.end());
});
RadixSortPairs(pairs);
#else
CollidePairs(bvh, edgeBoxes, [&](int qi, int li) {
if (qi >= li) return;
if (SharedEndpointSafelySkippable(edges[qi], edges[li], verts, eps)) return;
pairs->emplace_back(qi, li);
});
RadixSortPairs(pairs);
#endif
}
IntersectionInsertion FindAndInsertIntersections(
const std::vector<EdgeM>& edges, std::vector<vec2> verts,
std::vector<std::vector<int>> lists, double eps,
const std::vector<Box2>& edgeBoxes, const BVH& bvh,
const std::vector<IntersectionPoint>& precomputedIntersections) {
const int nE = static_cast<int>(edges.size());
const double eps2 = eps * eps;
std::vector<std::vector<int>> vertEdges;
vertEdges.resize(verts.size());
const size_t origNumVerts = verts.size();
const int firstNewVert = static_cast<int>(origNumVerts);
// Serial snap+insert pass. Each iteration may push to `verts` and
// mutate `lists[*]`, so subsequent iterations see the latest state.
// Avoid structured-binding capture (C++20-only) by naming locals.
for (const IntersectionPoint& ix : precomputedIntersections) {
const int i = ix.i;
const int j = ix.j;
const vec2 p = ix.p;
// Snap: is p within eps of any existing vert? Search the union of
// (i,j)'s endpoints and existing list members of i and j.
auto nearVert = [&](int candidate) -> bool {
vec2 d = p - verts[candidate];
return dot(d, d) <= eps2;
};
int snapTo = -1;
for (int v : {edges[i].v0, edges[i].v1, edges[j].v0, edges[j].v1}) {
if (nearVert(v)) {
snapTo = v;
break;
}
}
if (snapTo < 0) {
for (int v : lists[i]) {
if (nearVert(v)) {
snapTo = v;
break;
}
}
}
if (snapTo < 0) {
for (int v : lists[j]) {
if (nearVert(v)) {
snapTo = v;
break;
}
}
}
int vNew;
if (snapTo >= 0) {
vNew = snapTo;
} else {
vNew = static_cast<int>(verts.size());
verts.push_back(p);
vertEdges.emplace_back();
}
VESetInsert(&vertEdges[vNew], i);
VESetInsert(&vertEdges[vNew], j);
auto insertSorted = [&](int eIdx) {
if (vNew == edges[eIdx].v0 || vNew == edges[eIdx].v1) return;
auto& lst = lists[eIdx];
if (VESetContains(lst, vNew)) return;
vec2 a = verts[edges[eIdx].v0];
vec2 b = verts[edges[eIdx].v1];
vec2 ab = b - a;
double abLen2 = dot(ab, ab);
if (abLen2 == 0) return;
double tNew = dot(p - a, ab) / abLen2;
auto pos =
std::lower_bound(lst.begin(), lst.end(), tNew, [&](int v, double t) {
double tv = dot(verts[v] - a, ab) / abLen2;
return tv < t;
});
if (pos == lst.end() || *pos != vNew) lst.insert(pos, vNew);
};
insertSorted(i);
insertSorted(j);
}
// Eager propagation: after all independent edge-pair intersections are
// inserted, add each new intersection vertex to every other edge it
// geometrically splits. Otherwise a later canonical sub-edge can pass through
// a new vertex without being split there, leaving the halfedge arrangement
// dependent on tiny angular-sort differences.
if (verts.size() == origNumVerts)
return {std::move(verts), std::move(lists), std::move(vertEdges)};
const int numNewVerts = static_cast<int>(verts.size() - origNumVerts);
// Per-(qi, eIdx) propagation step. Same logic for BVH and brute-force
// broad phases.
auto propagateNarrow = [&](int qi, int eIdx) {
const int v = firstNewVert + qi;
if (v == edges[eIdx].v0 || v == edges[eIdx].v1) return;
if (VESetContains(vertEdges[v], eIdx)) return; // already incident
const vec2 a = verts[edges[eIdx].v0];
const vec2 b = verts[edges[eIdx].v1];
const vec2 ab = b - a;
const double abLen2 = dot(ab, ab);
if (abLen2 == 0) return;
const vec2 p = verts[v];
const double t = dot(p - a, ab) / abLen2;
if (t <= 0 || t >= 1) return;
const vec2 closest = a + ab * t;
const vec2 d = p - closest;
if (dot(d, d) > eps2) return;
auto& lst = lists[eIdx];
auto pos =
std::lower_bound(lst.begin(), lst.end(), t, [&](int vv, double tQ) {
double tv = dot(verts[vv] - a, ab) / abLen2;
return tv < tQ;
});
if (pos == lst.end() || *pos != v) lst.insert(pos, v);
VESetInsert(&vertEdges[v], eIdx);
};
if (bvh.leafToOrig.empty()) {
for (int qi = 0; qi < numNewVerts; ++qi) {
const Box2 queryBox = BoxOf2DPoint(verts[firstNewVert + qi], eps);
for (int e = 0; e < nE; ++e) {
if (queryBox.DoesOverlap(edgeBoxes[e])) propagateNarrow(qi, e);
}
}
} else {
auto adapter = [&](int qi, int leafIdx) {
propagateNarrow(qi, bvh.leafToOrig[leafIdx]);
};
auto recorder = MakeSimpleRecorder(adapter);
auto qf = [&](int qi) {
return BoxOf2DPoint(verts[firstNewVert + qi], eps);
};
BVHCollisions(bvh, recorder, qf, numNewVerts, /*parallel=*/false);
}
return {std::move(verts), std::move(lists), std::move(vertEdges)};
}
} // namespace manifold
@@ -1,54 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <utility>
#include <vector>
#include "bvh.h"
#include "predicates.h"
#include "vertex_merge.h"
namespace manifold {
inline constexpr double kIntersectionMergeEpsFactor = 10.0;
void CollectIntersectionPairs(const std::vector<EdgeM>& edges,
const std::vector<vec2>& verts, double eps,
const std::vector<Box2>& edgeBoxes,
const BVH& bvh,
std::vector<std::pair<int, int>>* pairs);
// Serially materialize precomputed proper intersections into caller-owned
// containers. `verts` and `lists` are taken by value so callers can move in the
// post-narrow-phase state; the returned fields are those same containers after
// appending/snapping intersection vertices and updating edge split lists.
// `vertEdges` records which edges meet at each materialized or snapped
// intersection vertex; nearby-intersection merge uses that incidence to
// distinguish unrelated near vertices from vertices that share an intersection
// source.
struct IntersectionInsertion {
std::vector<vec2> verts;
std::vector<std::vector<int>> lists;
std::vector<std::vector<int>> vertEdges;
};
IntersectionInsertion FindAndInsertIntersections(
const std::vector<EdgeM>& edges, std::vector<vec2> verts,
std::vector<std::vector<int>> lists, double eps,
const std::vector<Box2>& edgeBoxes, const BVH& bvh,
const std::vector<IntersectionPoint>& precomputedIntersections);
} // namespace manifold
@@ -1,25 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
namespace manifold {
// Shared grain size for boolean2 parallel loops whose per-item work is small
// but non-trivial: BVH traversal, pair narrow tests, and merge broad-phase
// sweeps. Radix-tree construction uses a coarser build-specific grain because
// its per-index work is much smaller.
inline constexpr int kFineParallelGrainSize = 512;
} // namespace manifold
-69
View File
@@ -1,69 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <vector>
#include "manifold/common.h"
namespace manifold {
constexpr double kU = 1.110223024625156540423631668e-16;
constexpr double kAlphaCoeff = 12.37;
struct EdgeM {
int v0, v1;
int mult = 1;
};
using OutEdge = EdgeM;
enum class GraphOrderKind {
ALessOrtho,
AGreaterOrtho,
EndpointTouch,
NoProjectionOverlap
};
struct GraphSegment2D {
vec2 p0;
vec2 p1;
// Stable fallback for geometrically identical ties (e.g. two input loops
// sharing an edge). Must come from a deterministic source, not BVH pair
// order.
int stableEdgeId = -1;
};
struct GraphOrder2D {
GraphOrderKind atMinProjection = GraphOrderKind::NoProjectionOverlap;
GraphOrderKind atMaxProjection = GraphOrderKind::NoProjectionOverlap;
bool coincidentOverlap = false;
bool properCrossing = false;
};
double SignedArea(const SimplePolygon& loop);
double TotalSignedArea(const Polygons& polys);
double EpsilonFromScale(double L, int k_budget = 1000);
double Coord(vec2 p, int axis);
// Projection-frame graph order over a positive-width shared projection
// interval. `ALessOrtho`/`AGreaterOrtho` compare the coordinate orthogonal to
// `axis`, so for axis==1 they compare x over a y interval.
GraphOrder2D CompareProjectedOrder(const GraphSegment2D& a,
const GraphSegment2D& b, int axis,
double overlapL, double overlapR,
double eps = 0.0);
bool IntersectSegments(const GraphSegment2D& a, const GraphSegment2D& b,
double eps, vec2* out);
} // namespace manifold
-226
View File
@@ -1,226 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Vertex-merge and edge-collapse passes. MergeVerts buckets verts within eps of
// each other onto an existing representative vertex; CollapseDegenerateEdges
// drops any input edge whose endpoints merged to the same vert. Both run before
// the BVH-broad intersection discovery.
//
// Also hosts the tiny "sorted-vector as set" helpers (VESetContains,
// VESetInsert) used by the per-edge / per-vert adjacency tracking in
// edge_vert_lists.h and intersections.h.
#include "vertex_merge.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <numeric>
#include <utility>
#include <vector>
#include "../../disjoint_sets.h"
#include "../../parallel.h"
#include "bvh.h"
#include "parallel_policy.h"
#include "predicates.h"
namespace manifold {
using manifold::la::dot;
VertexMerge MergeVerts(const std::vector<vec2>& in, double eps) {
const int n = static_cast<int>(in.size());
DisjointSets uf(n);
const double eps2 = eps * eps;
// Broad-phase: collect candidate (i, j) pairs whose padded boxes overlap.
// Small inputs use brute force; larger inputs use an x-sorted sweep. The
// pairs are sorted so the unite step below is deterministic.
//
// Shared with worker threads below, so this must not be thread-local.
std::vector<std::pair<int, int>> pairs;
if (n < 32) {
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
// AABB overlap test (the same the BVH would do): both axes must
// overlap on [v[i] - eps, v[i] + eps] intersect [v[j] - eps, v[j] +
// eps].
const vec2 d = in[i] - in[j];
if (std::fabs(d.x) <= 2 * eps && std::fabs(d.y) <= 2 * eps)
pairs.emplace_back(i, j);
}
}
} else {
// Sort by x and scan forward until x-distance exceeds 2*eps. Pairs are
// sorted at the end so the unite step below stays deterministic.
thread_local static std::vector<int> idx;
idx.resize(n);
std::iota(idx.begin(), idx.end(), 0);
manifold::stable_sort(idx.begin(), idx.end(),
[&](int a, int b) { return in[a].x < in[b].x; });
const double thresh = 2 * eps;
// Each i iteration writes only to its local pair buffer, so large inputs
// can collect candidates in parallel.
constexpr int kParallelMergeMin = 1024;
#if (MANIFOLD_PAR == 1)
if (n >= kParallelMergeMin) {
// Bind before worker dispatch; direct `idx` access would resolve to each
// worker's thread-local copy.
auto& idxRef = idx;
tbb::combinable<std::vector<std::pair<int, int>>> tls;
manifold::for_each_n(autoPolicy(n, kFineParallelGrainSize), countAt(0), n,
[&](int i) {
const int ai = idxRef[i];
const double ax = in[ai].x;
const double ay = in[ai].y;
auto& local = tls.local();
for (int j = i + 1; j < n; ++j) {
const int bi = idxRef[j];
const double dx = in[bi].x - ax;
if (dx > thresh) break;
if (std::fabs(in[bi].y - ay) > thresh) continue;
if (ai < bi)
local.emplace_back(ai, bi);
else
local.emplace_back(bi, ai);
}
});
tls.combine_each([&](const std::vector<std::pair<int, int>>& l) {
pairs.insert(pairs.end(), l.begin(), l.end());
});
} else
#endif
{
for (int i = 0; i < n; ++i) {
const int ai = idx[i];
const double ax = in[ai].x;
const double ay = in[ai].y;
for (int j = i + 1; j < n; ++j) {
const int bi = idx[j];
const double dx = in[bi].x - ax;
if (dx > thresh) break;
if (std::fabs(in[bi].y - ay) > thresh) continue;
if (ai < bi)
pairs.emplace_back(ai, bi);
else
pairs.emplace_back(bi, ai);
}
}
}
manifold::stable_sort(pairs.begin(), pairs.end());
}
// Fast path: no candidates means no merges, identity inputVert2Merged.
if (pairs.empty()) {
std::vector<int> inputVert2Merged(n);
std::iota(inputVert2Merged.begin(), inputVert2Merged.end(), 0);
return {std::move(inputVert2Merged), in};
}
// Parallelize the geometric distance gate (read-only on `in`); unite
// serially in sorted pair order so cluster roots are deterministic
// regardless of thread scheduling. Same pattern as the nearby-
// intersection merge in driver.cpp (after intersection insertion).
//
// Written by worker threads, so this must be shared storage.
std::vector<uint8_t> doUnite;
doUnite.assign(pairs.size(), 0);
manifold::for_each_n(manifold::autoPolicy(pairs.size()), manifold::countAt(0),
pairs.size(), [&](size_t k) {
const auto [i, j] = pairs[k];
vec2 d = in[i] - in[j];
if (dot(d, d) <= eps2) doUnite[k] = 1;
});
bool anyMerge = false;
for (size_t k = 0; k < pairs.size(); ++k) {
if (doUnite[k]) {
uf.unite(pairs[k].first, pairs[k].second);
anyMerge = true;
}
}
if (!anyMerge) {
std::vector<int> inputVert2Merged(n);
std::iota(inputVert2Merged.begin(), inputVert2Merged.end(), 0);
return {std::move(inputVert2Merged), in};
}
// Transitive proximity merge: pick an existing component vertex so a second
// MergeVerts pass cannot create a new within-eps pair via centroid drift.
std::vector<vec2> sumPos(n, vec2{0, 0});
std::vector<int> sumCnt(n, 0);
for (int i = 0; i < n; ++i) {
int r = uf.find(i);
sumPos[r] = sumPos[r] + in[i];
sumCnt[r] += 1;
}
// Pick the source vertex closest to the component centroid. This keeps the
// representative near the center while preserving idempotence: any output
// representative was an input vertex, so two representatives can be within
// eps only if their original components should already have been connected.
std::vector<int> representative(n, -1);
std::vector<double> representativeDist2(
n, std::numeric_limits<double>::infinity());
for (int i = 0; i < n; ++i) {
const int r = uf.find(i);
const vec2 centroid = sumPos[r] * (1.0 / sumCnt[r]);
const vec2 d = in[i] - centroid;
const double dist2 = dot(d, d);
if (dist2 < representativeDist2[r] ||
(dist2 == representativeDist2[r] && i < representative[r])) {
representative[r] = i;
representativeDist2[r] = dist2;
}
}
// Assign new indices in ascending root-id order so output ordering is
// deterministic and matches what the old std::map iteration produced.
std::vector<int> rootToNew(n, -1);
std::vector<vec2> verts;
verts.reserve(n);
for (int r = 0; r < n; ++r) {
if (sumCnt[r] == 0) continue;
rootToNew[r] = static_cast<int>(verts.size());
verts.push_back(in[representative[r]]);
}
std::vector<int> inputVert2Merged(n);
for (int i = 0; i < n; ++i) inputVert2Merged[i] = rootToNew[uf.find(i)];
return {std::move(inputVert2Merged), std::move(verts)};
}
// Per-edge split lists and per-vertex edge-incidence lists both hold small sets
// of int ids. Almost always 2-4 elements; occasionally larger at concurrent
// intersection points. A sorted std::vector<int> beats a std::set<int> by 5-10x
// on per-op cost for sets this small (no node allocation, no tree rebalancing,
// contiguous memory). Helpers keep the "set" semantics: idempotent insert, fast
// contains, ordered iteration.
bool VESetContains(const std::vector<int>& vec, int x) {
return std::binary_search(vec.begin(), vec.end(), x);
}
void VESetInsert(std::vector<int>* vec, int x) {
auto it = std::lower_bound(vec->begin(), vec->end(), x);
if (it == vec->end() || *it != x) vec->insert(it, x);
}
// Drop edges whose endpoints map to the same vertex after MergeVerts.
std::vector<EdgeM> RemapAndCollapse(const std::vector<EdgeM>& edges,
const std::vector<int>& inputVert2Merged) {
std::vector<EdgeM> out;
out.reserve(edges.size());
for (const auto& e : edges) {
int a = inputVert2Merged[e.v0];
int b = inputVert2Merged[e.v1];
if (a != b) out.push_back({a, b, e.mult});
}
return out;
}
} // namespace manifold
-40
View File
@@ -1,40 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <vector>
#include "predicates.h"
namespace manifold {
struct IntersectionPoint {
int i;
int j;
vec2 p;
};
struct VertexMerge {
std::vector<int> inputVert2Merged;
std::vector<vec2> verts;
};
VertexMerge MergeVerts(const std::vector<vec2>& in, double eps);
bool VESetContains(const std::vector<int>& vec, int x);
void VESetInsert(std::vector<int>* vec, int x);
std::vector<EdgeM> RemapAndCollapse(const std::vector<EdgeM>& edges,
const std::vector<int>& inputVert2Merged);
} // namespace manifold
@@ -1,36 +0,0 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <vector>
#include "canonicalize.h"
namespace manifold {
struct Trace;
enum class WindRule {
Add,
Intersect,
};
std::vector<OutEdge> FilterByWindingHalfedges(const CanonicalSubEdges& canon,
const std::vector<vec2>& verts,
bool debug = false,
WindRule rule = WindRule::Add,
Trace* trace = nullptr);
} // namespace manifold