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:
co-authored by
Claude Opus 4.8
parent
db042ab153
commit
9008e157a1
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+337
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user