Cut over CrossSection from Clipper2 to boolean2 (#1751)

* Wire CrossSection to the boolean2 backend

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

* Simplify CrossSection via vertex decimation, not merge-tolerance snap

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

* Address wiring review: Hull/finite guards, inline mutex, signpost, asserts

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

* Migrate CrossSection wiring to ApplyFillRule; Simplify decimates only

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

* Defer transform tolerance scaling to GetPaths; fix Simplify comment

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

* Tidy wiring comments to house style; use la::isfinite for guards

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

* Cut over CrossSection from Clipper2 to boolean2

Replace the Clipper2-backed CrossSection implementation with the in-tree boolean2 one and drop the Clipper2 dependency, the MANIFOLD_CROSS_SECTION_BACKEND selector, and its CI and packaging plumbing. The public CrossSection API is unchanged: the FillRule argument is kept but only Positive construction is supported. Samples.Sponge4's degenerate-triangle bound is raised, since boolean2's Simplify leaves more near-collinear vertices (more benign zero-area tris; topology unchanged).

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

* Delegate single-contour CrossSection ctor to the Polygons one

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

* Always build CrossSection; drop the MANIFOLD_CROSS_SECTION option

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

* Flatten manifold::boolean2 into manifold; unify JoinType

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

* Make SimplifyRing O(n log n) via min-heap with lazy invalidation

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

* Drop stale MANIFOLD_CROSS_SECTION flag from the PR perf guard script

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

* Scrub Clipper2 and the removed option from the new sanitizer CI script

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Zach Loafman
2026-06-11 08:06:37 +12:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 324bec7bd7
commit db042ab153
56 changed files with 493 additions and 1286 deletions
+4 -49
View File
@@ -34,12 +34,6 @@ jobs:
name: tbb
url: https://github.com/oneapi-src/oneTBB.git
tag: v2022.3.0
- &clone-clipper2
uses: ./.github/actions/git-clone
with:
name: clipper2
url: https://github.com/AngusJohnson/Clipper2.git
rev: 46f639177fe418f9689e8ddb74f08a870c71f5b4
- &clone-nanobind
uses: ./.github/actions/git-clone
with:
@@ -66,7 +60,6 @@ jobs:
-DMANIFOLD_ASSERT=ON \
-DMANIFOLD_PAR=${{matrix.parallelization}} \
-DFETCHCONTENT_SOURCE_DIR_TBB=tbb \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=clipper2 \
-DFETCHCONTENT_SOURCE_DIR_NANOBIND=nanobind \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=gtest \
-A x64 -B build
@@ -98,21 +91,15 @@ jobs:
name: meshes-windows
path: meshes-windows/
build:
name: GCC ${{matrix.gcc}} (CrossSection:${{matrix.cross_section}}, TBB:${{matrix.parallelization}})
name: GCC ${{matrix.gcc}} (TBB:${{matrix.parallelization}})
timeout-minutes: 45
strategy:
matrix:
cross_section: [ON]
parallelization: [OFF, ON]
os: [ubuntu-24.04]
gcc: [13, 14]
include:
- cross_section: OFF
parallelization: ON
os: ubuntu-24.04
gcc: 14
- cross_section: ON
parallelization: ON
- parallelization: ON
os: ubuntu-22.04
gcc: 11
runs-on: ${{ matrix.os }}
@@ -131,7 +118,6 @@ jobs:
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
with:
packages: libgtest-dev libassimp-dev git libtbb-dev pkg-config cmake
- *clone-clipper2
- *clone-nanobind
- name: Build
@@ -143,15 +129,12 @@ jobs:
-DMANIFOLD_PYBIND=ON \
-DMANIFOLD_DEBUG=ON \
-DMANIFOLD_ASSERT=ON \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=clipper2 \
-DFETCHCONTENT_SOURCE_DIR_NANOBIND=nanobind \
-DMANIFOLD_CROSS_SECTION=${{matrix.cross_section}} \
-DASSIMP_ENABLE=ON \
-DMANIFOLD_PAR=${{matrix.parallelization}} \
. -B build
cmake --build build
- name: Test
if: matrix.cross_section == 'ON'
run: |
./build/test/manifold_test
# python tests
@@ -192,7 +175,6 @@ jobs:
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DMANIFOLD_NO_IOSTREAM=ON \
-DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \
-DMANIFOLD_STRICT=ON \
-DMANIFOLD_TEST=ON \
-DMANIFOLD_PYBIND=OFF \
@@ -203,7 +185,6 @@ jobs:
- name: Verify conflict guard fires (NO_IOSTREAM + DEBUG)
run: |
if cmake -DMANIFOLD_NO_IOSTREAM=ON -DMANIFOLD_DEBUG=ON \
-DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \
. -B /tmp/conflict-debug 2>&1; then
echo "FAIL: configure should have failed for NO_IOSTREAM + DEBUG"
exit 1
@@ -227,12 +208,11 @@ jobs:
- name: Install apt packages
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
with:
# tbb/clipper2/gtest are built from the cloned sources below under the
# tbb/gtest are built from the cloned sources below under the
# lane's libc++ flags so they match the test binary's ABI; the distro
# packages are libstdc++.
packages: cmake clang libc++-dev libc++abi-dev
- *clone-tbb
- *clone-clipper2
- *clone-gtest
- name: Configure + build with libc++ and EXTENSIVE hardening
run: |
@@ -245,9 +225,7 @@ jobs:
-DMANIFOLD_TEST=ON \
-DMANIFOLD_PYBIND=OFF \
-DMANIFOLD_USE_BUILTIN_TBB=ON \
-DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \
-DFETCHCONTENT_SOURCE_DIR_TBB=tbb \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=clipper2 \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=gtest \
. -B build
cmake --build build
@@ -271,14 +249,6 @@ jobs:
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
with:
packages: libgtest-dev git libtbb-dev pkg-config lcov cmake
- *clone-clipper2
- name: build clipper2
run: |
cd clipper2/CPP
cmake -DCMAKE_BUILD_TYPE=Release . -B build
cmake --build build
sudo cmake --install build
- name: build
run: |
cmake \
@@ -288,7 +258,6 @@ jobs:
-DMANIFOLD_PYBIND=OFF \
-DMANIFOLD_DEBUG=ON \
-DMANIFOLD_ASSERT=ON \
-DMANIFOLD_CROSS_SECTION=ON \
-DMANIFOLD_PAR=${{matrix.parallelization}} \
-DCODE_COVERAGE=ON \
. -B build
@@ -335,7 +304,6 @@ jobs:
with:
version: 5.0.2
- *clone-tbb
- *clone-clipper2
- *clone-gtest
- name: Build WASM ${{matrix.parallelization}}
run: |
@@ -347,7 +315,6 @@ jobs:
-DMANIFOLD_DEBUG=${{matrix.debug}} \
-DMANIFOLD_ASSERT=${{matrix.debug}} \
-DFETCHCONTENT_SOURCE_DIR_TBB=tbb \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=clipper2 \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=gtest \
. -B build
cd build
@@ -435,7 +402,6 @@ jobs:
if: matrix.os == 'macos-latest'
run: |
brew install googletest cmake
- *clone-clipper2
- uses: ./.github/actions/git-clone
with:
name: fuzztest
@@ -450,8 +416,6 @@ jobs:
-DMANIFOLD_FUZZ=ON \
-DMANIFOLD_PYBIND=OFF \
-DCMAKE_CXX_COMPILER=clang++ \
-DMANIFOLD_CROSS_SECTION=ON \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=clipper2 \
-DFETCHCONTENT_SOURCE_DIR_FUZZTEST=fuzztest \
. -B build
ASAN_OPTIONS=detect_container_overflow=0 cmake --build build
@@ -467,7 +431,6 @@ jobs:
with:
packages: libgtest-dev libtbb-dev clang-18 cmake
version: 1
- *clone-clipper2
- name: Build (clang-18 + asan/ubsan)
run: bash ./scripts/ci/sanitizer_build.sh
- name: Upload sanitizer build artifacts
@@ -508,7 +471,6 @@ jobs:
container: openscad/mxe-x86_64-gui:latest
steps:
- uses: actions/checkout@v6
- *clone-clipper2
- name: Build
run: |
export MXEDIR=/mxe
@@ -525,7 +487,6 @@ jobs:
-DMANIFOLD_CBIND=ON \
-DMANIFOLD_DEBUG=ON \
-DMANIFOLD_PAR=${{matrix.parallelization}} \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=clipper2 \
. -B build
$CMAKE --build build
@@ -553,7 +514,6 @@ jobs:
- name: Install TBB
if: matrix.parallelization == 'ON'
run: brew install tbb
- *clone-clipper2
- name: Build
run: |
cmake \
@@ -565,7 +525,6 @@ jobs:
-DMANIFOLD_PYBIND=ON \
-DASSIMP_ENABLE=ON \
-DMANIFOLD_PAR=${{matrix.parallelization}} \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=clipper2 \
. -B build
cmake --build build
- name: Test
@@ -606,10 +565,8 @@ jobs:
brew install googletest cmake
pip install trimesh pytest
- *clone-tbb
- *clone-clipper2
- name: test cmake consumer subdir
run: |
export CLIPPER2=$(pwd)/clipper2
export TBB=$(pwd)/tbb
cd ..
./manifold/scripts/test-cmake-subdir.sh
@@ -625,7 +582,6 @@ jobs:
-DMANIFOLD_PYBIND=OFF \
-DMANIFOLD_PAR=ON \
-DFETCHCONTENT_SOURCE_DIR_TBB=tbb \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=clipper2 \
. -B build
cmake --build build
- name: Test
@@ -692,7 +648,6 @@ jobs:
with:
packages: libgtest-dev libtbb-dev cmake
version: 1
- *clone-clipper2
- name: Prepare base and head worktrees
continue-on-error: true
shell: bash
@@ -700,7 +655,7 @@ jobs:
- name: Benchmark base and head
continue-on-error: true
shell: bash
run: bash ./scripts/benchmark_pr_perf_guard.sh ./clipper2 "${PERF_REPEATS}"
run: bash ./scripts/benchmark_pr_perf_guard.sh "${PERF_REPEATS}"
- name: Compare benchmark results
shell: bash
run: bash ./scripts/report_pr_perf_guard.sh ./bench/base ./bench/head "${REGRESSION_WARN_PCT}" "${REGRESSION_WARN_ABS_MS}"
-2
View File
@@ -66,10 +66,8 @@ jobs:
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DBUILD_SHARED_LIBS=ON \
-DMANIFOLD_CBIND=ON \
-DMANIFOLD_CROSS_SECTION=ON \
-DMANIFOLD_TEST=ON \
-DMANIFOLD_PAR=ON \
-DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \
-DCMAKE_PREFIX_PATH="$TBB_PREFIX" \
-DCMAKE_CXX_FLAGS=-fsanitize=thread \
-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=thread \
+2 -36
View File
@@ -37,29 +37,6 @@ set(
)
# Primary user facing options
option(MANIFOLD_CROSS_SECTION "Build CrossSection for 2D support" ON)
set(
MANIFOLD_CROSS_SECTION_BACKEND
"clipper2"
CACHE STRING
"CrossSection backend to build: clipper2 or experimental boolean2"
)
set_property(
CACHE MANIFOLD_CROSS_SECTION_BACKEND
PROPERTY STRINGS clipper2 boolean2
)
mark_as_advanced(MANIFOLD_CROSS_SECTION_BACKEND)
if(MANIFOLD_CROSS_SECTION)
if(
NOT MANIFOLD_CROSS_SECTION_BACKEND STREQUAL "clipper2"
AND NOT MANIFOLD_CROSS_SECTION_BACKEND STREQUAL "boolean2"
)
message(
FATAL_ERROR
"MANIFOLD_CROSS_SECTION_BACKEND must be 'clipper2' or 'boolean2'"
)
endif()
endif()
option(MANIFOLD_DEBUG "Enable debug tracing/timing" OFF)
option(MANIFOLD_ASSERT "Enable assertions - requires MANIFOLD_DEBUG" OFF)
option(MANIFOLD_TIMING "Enable Boolean3 phase timing without debug overhead" OFF)
@@ -82,14 +59,7 @@ option(
)
option(MANIFOLD_TEST "Enable testing suite" ON)
option(MANIFOLD_PYBIND "Build python bindings" OFF)
# MANIFOLD_CBIND is only available when MANIFOLD_CROSS_SECTION is enabled
cmake_dependent_option(
MANIFOLD_CBIND
"Build C (FFI) bindings"
ON
"MANIFOLD_CROSS_SECTION"
OFF
)
option(MANIFOLD_CBIND "Build C (FFI) bindings" ON)
# MANIFOLD_JSBIND is only available when building with Emscripten
cmake_dependent_option(
MANIFOLD_JSBIND
@@ -107,7 +77,6 @@ cmake_dependent_option(
# When the system package is unavailable, the option will be automatically set
# to true.
option(MANIFOLD_USE_BUILTIN_TBB "Use builtin tbb" OFF)
option(MANIFOLD_USE_BUILTIN_CLIPPER2 "Use builtin clipper2" OFF)
option(MANIFOLD_USE_BUILTIN_NANOBIND "Use builtin nanobind" OFF)
# default to Release build
@@ -333,7 +302,7 @@ set(
optional_assert.h
polygon.h
vec_view.h
$<$<BOOL:${MANIFOLD_CROSS_SECTION}>:cross_section.h>
cross_section.h
)
list(TRANSFORM MANIFOLD_PUBLIC_HDRS PREPEND include/manifold/)
list(
@@ -351,9 +320,6 @@ install(
if(MANIFOLD_PAR)
set(TEMPLATE_OPTIONAL_TBB "tbb")
endif()
if(MANIFOLD_CROSS_SECTION AND MANIFOLD_CROSS_SECTION_BACKEND STREQUAL "clipper2")
set(TEMPLATE_OPTIONAL_CLIPPER "Clipper2")
endif()
configure_file(
cmake/manifold.pc.in
${CMAKE_CURRENT_BINARY_DIR}/manifold.pc
+1 -5
View File
@@ -80,7 +80,6 @@ Manifold no longer has **any** required dependencies! However, we do have severa
| Name | CMake Flag | Provides |
| --- | --- | --- |
| [`TBB`](https://github.com/oneapi-src/oneTBB/) |`MANIFOLD_PAR=ON` | Parallel acceleration |
| [`Clipper2`](https://github.com/AngusJohnson/Clipper2) | `MANIFOLD_CROSS_SECTION=ON` | 2D: [`CrossSection`](https://manifoldcad.org/docs/html/classmanifold_1_1_cross_section.html) |
| [`Nanobind`](https://github.com/wjakob/nanobind) | `MANIFOLD_PYBIND=ON` | Python bindings |
| [`Emscripten`](https://github.com/emscripten-core/emscripten) | `MANIFOLD_JSBIND=ON` | JS bindings via WASM |
| [`GTest`](https://github.com/google/googletest/) | `MANIFOLD_TEST=ON` | Testing framework |
@@ -116,7 +115,6 @@ CMake flags (usage e.g. `-DMANIFOLD_DEBUG=ON`):
- `MANIFOLD_CBIND=[<OFF>, ON]`: Build C FFI binding.
- `MANIFOLD_PYBIND=[OFF, <ON>]`: Build python binding, requires `nanobind`.
- `MANIFOLD_PAR=[<OFF>, ON]`: Enables multi-thread parallelization, requires `tbb`.
- `MANIFOLD_CROSS_SECTION=[OFF, <ON>]`: Build CrossSection for 2D support (needed by language bindings), requires `Clipper2`.
- `MANIFOLD_DEBUG=[<OFF>, ON]`: Enables exceptions, timing, verbosity, OBJ test dumps. Has almost no effect on its own, but enables further runtime parameters to dump various outputs.
- `MANIFOLD_ASSERT=[<OFF>, ON]`: Enables internal assertions. This incurs around 20% runtime overhead. Requires MANIFOLD_DEBUG to work.
- `MANIFOLD_TEST=[OFF, <ON>]`: Build unit tests, requires `GTest`.
@@ -135,10 +133,9 @@ CMake flags (usage e.g. `-DMANIFOLD_DEBUG=ON`):
Dependency version override:
- `MANIFOLD_USE_BUILTIN_TBB=[<OFF>, ON]`: Use builtin version of tbb.
- `MANIFOLD_USE_BUILTIN_CLIPPER2=[<OFF>, ON]`: Use builtin version of clipper2.
- `MANIFOLD_USE_BUILTIN_NANOBIND=[<OFF>, ON]`: Use builtin version of nanobind.
> Note: These three options can force the build to avoid using the system
> Note: These two options can force the build to avoid using the system
> version of the dependency. This will either use the provided source directory
> via `FETCHCONTENT_SOURCE_DIR_*` (see below), or fetch the source from GitHub.
> Note that the dependency will be built as static dependency to avoid dynamic
@@ -156,7 +153,6 @@ Offline building (with missing dependencies/dependency version override):
- `MANIFOLD_DOWNLOADS=[OFF, <ON>]`: Automatically download missing dependencies.
Need to set `FETCHCONTENT_SOURCE_DIR_*` if the dependency `*` is missing.
- `FETCHCONTENT_SOURCE_DIR_TBB`: path to tbb source (if `MANIFOLD_PAR` is enabled).
- `FETCHCONTENT_SOURCE_DIR_CLIPPER2`: path to tbb source (if `MANIFOLD_CROSS_SECTION` is enabled).
- `FETCHCONTENT_SOURCE_DIR_NANOBIND`: path to nanobind source (if `MANIFOLD_PYBIND` is enabled).
- `FETCHCONTENT_SOURCE_DIR_GOOGLETEST`: path to googletest source (if `MANIFOLD_TEST` is enabled).
-4
View File
@@ -12,10 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
if(NOT MANIFOLD_CROSS_SECTION)
return()
endif()
if(MANIFOLD_CBIND)
add_subdirectory(c)
endif()
+2 -3
View File
@@ -886,9 +886,8 @@ NB_MODULE(manifold3d, m) {
m, "CrossSection",
"Two-dimensional cross sections guaranteed to be without "
"self-intersections, or overlaps between polygons (from construction "
"onwards). This class makes use of the "
"[Clipper2](http://www.angusj.com/clipper2/Docs/Overview.htm) library "
"for polygon clipping (boolean) and offsetting operations.")
"onwards). Polygon clipping (boolean) and offsetting use Manifold's own "
"robust floating-point predicates.")
.def(nb::init<>(), cross_section__cross_section)
.def(nb::init<std::vector<std::vector<vec2>>, CrossSection::FillRule>(),
nb::arg("contours"),
+2 -4
View File
@@ -93,12 +93,10 @@ export function resetToCircularDefaults(): void;
/**
* Two-dimensional cross sections guaranteed to be without self-intersections,
* or overlaps between polygons (from construction onwards). This class makes
* use of the Clipper2 library for polygon clipping (boolean) and offsetting
* operations.
* or overlaps between polygons (from construction onwards). Polygon clipping
* (boolean) and offsetting use Manifold's own robust floating-point predicates.
*
* @see {@link https://manifoldcad.org/docs/html/classmanifold_1_1_cross_section.html | C++ API: CrossSection Class Reference}
* @see {@link https://www.angusj.com/clipper2/Docs/Overview.htm | Clipper2 - Polygon Clipping Offsetting & Triangulating}
* @group Basics
*/
export class CrossSection {
-6
View File
@@ -62,8 +62,6 @@ message(STATUS "BUILD_SHARED_LIBS: ${BUILD_SHARED_LIBS}")
message(STATUS " ")
message(STATUS "MANIFOLD_VERSION: ${MANIFOLD_VERSION}")
message(STATUS "MANIFOLD_PAR: ${MANIFOLD_PAR}")
message(STATUS "MANIFOLD_CROSS_SECTION: ${MANIFOLD_CROSS_SECTION}")
message(STATUS "MANIFOLD_CROSS_SECTION_BACKEND: ${MANIFOLD_CROSS_SECTION_BACKEND}")
message(STATUS "MANIFOLD_TEST: ${MANIFOLD_TEST}")
message(STATUS "MANIFOLD_DEBUG: ${MANIFOLD_DEBUG}")
message(STATUS "MANIFOLD_ASSERT: ${MANIFOLD_ASSERT}")
@@ -77,10 +75,6 @@ message(STATUS "MANIFOLD_COMPILE_OPTIONS: ${MANIFOLD_COMPILE_OPTIONS}")
message(STATUS "MANIFOLD_LINK_OPTIONS: ${MANIFOLD_LINK_OPTIONS}")
message(STATUS "MANIFOLD_DOWNLOADS: ${MANIFOLD_DOWNLOADS}")
message(STATUS "MANIFOLD_USE_BUILTIN_TBB: ${MANIFOLD_USE_BUILTIN_TBB}")
message(
STATUS
"MANIFOLD_USE_BUILTIN_CLIPPER2: ${MANIFOLD_USE_BUILTIN_CLIPPER2}"
)
message(
STATUS
"MANIFOLD_USE_BUILTIN_NANOBIND: ${MANIFOLD_USE_BUILTIN_NANOBIND}"
+1 -1
View File
@@ -7,6 +7,6 @@ Name: manifold@PCFILE_LIB_SUFFIX@
Description: Geometry library for topological robustness
Version: @MANIFOLD_VERSION@
URL: https://github.com/elalish/manifold
Requires-private: @TEMPLATE_OPTIONAL_TBB@ @TEMPLATE_OPTIONAL_CLIPPER@
Requires-private: @TEMPLATE_OPTIONAL_TBB@
Libs: -L${libdir} -lmanifold@PCFILE_LIB_SUFFIX@
Cflags: -I${includedir} -std=c++17
-13
View File
@@ -14,19 +14,6 @@ endif()
set(MANIFOLD_FOUND TRUE)
set(MANIFOLD_SHARED_LIB "@BUILD_SHARED_LIBS@")
set(MANIFOLD_CROSS_SECTION "@MANIFOLD_CROSS_SECTION@")
set(MANIFOLD_CROSS_SECTION_BACKEND "@MANIFOLD_CROSS_SECTION_BACKEND@")
set(MANIFOLD_USE_BUILTIN_CLIPPER2 "@MANIFOLD_USE_BUILTIN_CLIPPER2@")
# if built as shared lib and using builtin, no need to find clipper2
# otherwise, we need to find it
if(
MANIFOLD_CROSS_SECTION
AND MANIFOLD_CROSS_SECTION_BACKEND STREQUAL "clipper2"
AND NOT (MANIFOLD_SHARED_LIB AND MANIFOLD_USE_BUILTIN_CLIPPER2)
)
set(Clipper2_ROOT "${_FIND_ROOT}")
find_package(Clipper2 REQUIRED)
endif()
set(MANIFOLD_PAR "@MANIFOLD_PAR@")
set(MANIFOLD_USE_BUILTIN_TBB "@MANIFOLD_USE_BUILTIN_TBB@")
if(
-85
View File
@@ -85,91 +85,6 @@ if(MANIFOLD_PAR)
endif()
endif()
# The default CrossSection backend uses Clipper2. The experimental boolean2
# backend is in-tree and does not need this dependency.
if(MANIFOLD_CROSS_SECTION AND MANIFOLD_CROSS_SECTION_BACKEND STREQUAL "clipper2")
if(NOT MANIFOLD_USE_BUILTIN_CLIPPER2 AND NOT Clipper2_FOUND)
find_package(Clipper2 QUIET)
if(NOT Clipper2_FOUND AND PKG_CONFIG_FOUND)
pkg_check_modules(Clipper2 Clipper2)
if(Clipper2_FOUND)
add_library(Clipper2 SHARED IMPORTED)
set_property(
TARGET Clipper2
PROPERTY IMPORTED_LOCATION ${Clipper2_LINK_LIBRARIES}
)
if(WIN32)
set_property(
TARGET Clipper2
PROPERTY IMPORTED_IMPLIB ${Clipper2_LINK_LIBRARIES}
)
endif()
target_include_directories(Clipper2 INTERFACE ${Clipper2_INCLUDE_DIRS})
endif(Clipper2_FOUND)
endif(NOT Clipper2_FOUND AND PKG_CONFIG_FOUND)
endif(NOT MANIFOLD_USE_BUILTIN_CLIPPER2 AND NOT Clipper2_FOUND)
if(NOT Clipper2_FOUND)
logmissingdep("Clipper2" , "cross_section")
set(MANIFOLD_USE_BUILTIN_CLIPPER2 ON)
set(CLIPPER2_UTILS OFF)
set(CLIPPER2_EXAMPLES OFF)
set(CLIPPER2_TESTS OFF)
set(
CLIPPER2_USINGZ
OFF
CACHE BOOL
"Preempt cache default of USINGZ (we only use 2d)"
)
# When manifold is built with MANIFOLD_NO_IOSTREAM, also strip
# iostream from the bundled Clipper2 — manifold doesn't call any
# of Clipper2's stream operators internally, so passing this
# through is safe regardless. The CLIPPER2_NO_IOSTREAM macro is
# added by the carry-patch below; once Clipper2#1094 lands and
# the SHA pin moves past it, the patch drops and the option is
# honored natively.
if(MANIFOLD_NO_IOSTREAM)
set(
CLIPPER2_NO_IOSTREAM
ON
CACHE BOOL
"Strip iostream-using overloads from Clipper2 (set by manifold when MANIFOLD_NO_IOSTREAM=ON)"
FORCE
)
endif()
FetchContent_Declare(
Clipper2
GIT_REPOSITORY https://github.com/AngusJohnson/Clipper2.git
# Mar 05, 2026
GIT_TAG 46f639177fe418f9689e8ddb74f08a870c71f5b4
GIT_PROGRESS TRUE
SOURCE_SUBDIR
CPP
# Disable Windows autocrlf on the clone so the carry-patch (which
# is LF-only) applies cleanly. Default core.autocrlf=true on
# Windows would convert all LFs to CRLFs in the working tree, and
# `git apply` then fails on the line-ending mismatch.
GIT_CONFIG
core.autocrlf=false
# Carry-patch: tracks AngusJohnson/Clipper2#1094 (CLIPPER2_NO_IOSTREAM
# macro guards). Drops once that PR lands and the SHA pin moves past
# it. Applied via wrapper script so re-configures (which re-trigger
# PATCH_COMMAND) don't fail when the patch is already applied.
PATCH_COMMAND ${CMAKE_COMMAND}
-DPATCH_FILE=${CMAKE_CURRENT_LIST_DIR}/patches/0001-clipper2-no-iostream.patch
-DSOURCE_DIR=<SOURCE_DIR>
-P ${CMAKE_CURRENT_LIST_DIR}/patches/apply-clipper2-patch.cmake
)
FetchContent_MakeAvailable(Clipper2)
set_property(
DIRECTORY ${clipper2_SOURCE_DIR}/CPP
PROPERTY EXCLUDE_FROM_ALL ${BUILD_SHARED_LIBS}
)
endif()
if(NOT TARGET Clipper2::Clipper2)
add_library(Clipper2::Clipper2 ALIAS Clipper2)
endif()
endif()
if(TRACY_ENABLE)
logmissingdep("tracy" , "TRACY_ENABLE")
FetchContent_Declare(
@@ -1,153 +0,0 @@
Carry-patch: add a CLIPPER2_NO_IOSTREAM build option (off by default)
that strips iostream-using overloads from Clipper2's public headers,
and propagate the macro through Clipper2's PUBLIC compile definitions
so consumers see it. Lets manifold pass MANIFOLD_NO_IOSTREAM=ON
through to the bundled Clipper2 (e.g., for wasm32-unknown-unknown
consumers via the wasm-cxx-shim integration).
Tracks AngusJohnson/Clipper2#1094 — once that lands and manifold's
Clipper2 SHA pin moves past it, this patch drops.
Generated against Clipper2 SHA 46f639177fe418f9689e8ddb74f08a870c71f5b4
(the SHA manifoldDeps.cmake currently pins). The headers at this SHA
are byte-identical to upstream main, so the patch tracks #1094's diff
exactly.
Apply with: git apply -p1 0001-clipper2-no-iostream.patch
diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt
index 7642e86..b327296 100644
--- a/CPP/CMakeLists.txt
+++ b/CPP/CMakeLists.txt
@@ -17,6 +17,7 @@ option(CLIPPER2_HI_PRECISION "Caution: enabling this will compromise performance
option(CLIPPER2_UTILS "Build utilities" ON)
option(CLIPPER2_EXAMPLES "Build examples" ON)
option(CLIPPER2_TESTS "Build tests" ON)
+option(CLIPPER2_NO_IOSTREAM "Disable iostream-using header overloads" OFF)
option(USE_EXTERNAL_GTEST "Use system-wide installed GoogleTest" OFF)
option(USE_EXTERNAL_GBENCHMARK "Use the googlebenchmark" OFF)
option(BUILD_SHARED_LIBS "Build shared libs" OFF)
@@ -69,6 +70,7 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "ONLY"))
Clipper2 PUBLIC
CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION}
$<$<BOOL:${CLIPPER2_HI_PRECISION}>:CLIPPER2_HI_PRECISION>
+ $<$<BOOL:${CLIPPER2_NO_IOSTREAM}>:CLIPPER2_NO_IOSTREAM>
)
target_include_directories(
@@ -95,6 +97,7 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "OFF"))
USINGZ
CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION}
$<$<BOOL:${CLIPPER2_HI_PRECISION}>:CLIPPER2_HI_PRECISION>
+ $<$<BOOL:${CLIPPER2_NO_IOSTREAM}>:CLIPPER2_NO_IOSTREAM>
)
target_include_directories(
Clipper2Z PUBLIC
diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h
index 99a5205..d3af0a0 100644
--- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h
+++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h
@@ -14,7 +14,9 @@
#include <cstdint>
#include <vector>
#include <string>
+#ifndef CLIPPER2_NO_IOSTREAM
#include <iostream>
+#endif
#include <algorithm>
#include <numeric>
#include <cmath>
@@ -166,11 +168,13 @@ namespace Clipper2Lib
void SetZ(const z_type z_value) { z = z_value; }
+#ifndef CLIPPER2_NO_IOSTREAM
friend std::ostream& operator<<(std::ostream& os, const Point& point)
{
os << point.x << "," << point.y << "," << point.z;
return os;
}
+#endif
#else
@@ -203,11 +207,13 @@ namespace Clipper2Lib
return Point(x * scale, y * scale);
}
+#ifndef CLIPPER2_NO_IOSTREAM
friend std::ostream& operator<<(std::ostream& os, const Point& point)
{
os << point.x << "," << point.y;
return os;
}
+#endif
#endif
friend bool operator==(const Point& a, const Point& b)
@@ -396,10 +402,12 @@ namespace Clipper2Lib
return result;
}
+#ifndef CLIPPER2_NO_IOSTREAM
friend std::ostream& operator<<(std::ostream& os, const Rect<T>& rect) {
os << "(" << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << ") ";
return os;
}
+#endif
};
template <typename T1, typename T2>
@@ -498,6 +506,7 @@ namespace Clipper2Lib
return Rect<T>(xmin, ymin, xmax, ymax);
}
+#ifndef CLIPPER2_NO_IOSTREAM
template <typename T>
std::ostream& operator << (std::ostream& outstream, const Path<T>& path)
{
@@ -518,6 +527,7 @@ namespace Clipper2Lib
outstream << p;
return outstream;
}
+#endif
template <typename T1, typename T2>
diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h
index fe1e299..deb1f7b 100644
--- a/CPP/Clipper2Lib/include/clipper2/clipper.h
+++ b/CPP/Clipper2Lib/include/clipper2/clipper.h
@@ -309,6 +309,7 @@ namespace Clipper2Lib {
return true;
}
+#ifndef CLIPPER2_NO_IOSTREAM
static void OutlinePolyPath(std::ostream& os,
size_t idx, bool isHole, size_t count, const std::string& preamble)
{
@@ -338,6 +339,7 @@ namespace Clipper2Lib {
if (pp.Child(i)->Count())
details::OutlinePolyPathD(os, *pp.Child(i), i, preamble + " ");
}
+#endif
template<typename T, typename U>
inline constexpr void MakePathGeneric(const T an_array,
@@ -377,6 +379,7 @@ namespace Clipper2Lib {
} // end details namespace
+#ifndef CLIPPER2_NO_IOSTREAM
inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp)
{
std::string plural = (pp.Count() == 1) ? " polygon." : " polygons.";
@@ -399,6 +402,7 @@ namespace Clipper2Lib {
if (!pp.Level()) os << std::endl;
return os;
}
+#endif
inline Paths64 PolyTreeToPaths64(const PolyTree64& polytree)
{
-32
View File
@@ -1,32 +0,0 @@
# Idempotent application of the Clipper2 carry-patch. Runs as the
# PATCH_COMMAND of FetchContent_Declare(Clipper2 ...). Re-runs of
# `cmake` (e.g., when CMakeLists.txt files in the parent project
# are edited) reinvoke the patch step; without this idempotency
# wrapper, `git apply` fails on already-patched files.
#
# Invoked via: cmake -DPATCH_FILE=<path> -DSOURCE_DIR=<src> -P this.cmake
if(NOT PATCH_FILE OR NOT SOURCE_DIR)
message(FATAL_ERROR "PATCH_FILE and SOURCE_DIR are required")
endif()
# `git apply --reverse --check` succeeds iff the patch is already applied.
execute_process(
COMMAND git apply --reverse --check --ignore-whitespace --whitespace=nowarn "${PATCH_FILE}"
WORKING_DIRECTORY "${SOURCE_DIR}"
RESULT_VARIABLE _already_applied
OUTPUT_QUIET
ERROR_QUIET
)
if(_already_applied EQUAL 0)
return()
endif()
execute_process(
COMMAND git apply --ignore-whitespace --whitespace=nowarn "${PATCH_FILE}"
WORKING_DIRECTORY "${SOURCE_DIR}"
RESULT_VARIABLE _apply_result
)
if(NOT _apply_result EQUAL 0)
message(FATAL_ERROR "Failed to apply Clipper2 carry-patch: ${PATCH_FILE}")
endif()
+15 -56
View File
@@ -1,48 +1,24 @@
# Boolean2 CrossSection Core
Status: experimental core library for a future `CrossSection` backend. The
default backend remains `clipper2`; this branch only adds the Boolean2 library
and a staged, empty-returning `boolean2` backend translation unit. A later PR
wires that translation unit into the public `CrossSection` methods.
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. Sibling utilities in
`src/cross_section/boolean2/` provide the decomposition and offset pieces that
the follow-up backend wiring uses to cover the rest of the public
`CrossSection` API. The current branch keeps the implementation reviewable by
landing the core library before that public backend wiring is enabled.
Boolean operations. The core library lives in `src/cross_section/boolean2/`,
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.
## Goals
- Keep Clipper2 available while the new backend bakes upstream.
- Reuse manifold's geometric primitives where practical:
BVH broad phase queries, `DisjointSets` for vertex equality, and
symbolic predicates from `shared.h`.
- Keep the core independent from Clipper2 so the follow-up backend can compile
either implementation from the same public API surface.
- Make robustness testable through deterministic regression tests and later
- Make robustness testable through deterministic regression tests and
FuzzTest targets that exercise the public `CrossSection` API and
`CrossSection`/`Manifold` round trips.
## Build Selection
This core can be built behind the experimental backend selector:
```sh
cmake -S . -B build/clipper \
-DMANIFOLD_CROSS_SECTION=ON \
-DMANIFOLD_CROSS_SECTION_BACKEND=clipper2
cmake -S . -B build/boolean2 \
-DMANIFOLD_CROSS_SECTION=ON \
-DMANIFOLD_CROSS_SECTION_BACKEND=boolean2
```
`clipper2` is the default. Invalid backend names fail at configure time when
`MANIFOLD_CROSS_SECTION` is enabled. In this branch, selecting `boolean2`
builds the core library and an empty-returning placeholder backend, but usable
public `CrossSection` dispatch still lands in the follow-up backend-wiring PR.
## Algorithm Outline
Boolean2 builds a planar arrangement and filters it by per-face winding:
@@ -68,7 +44,7 @@ The high-level fill/Boolean core API is in
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
follow-up backend wiring rather than by `boolean2.h`.
backend wiring rather than by `boolean2.h`.
## Architecture
@@ -83,7 +59,7 @@ The main dataflow is:
| 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` | Used by the later backend-wiring PR for offset and decomposition support in the rest of the `CrossSection` API. |
| Sibling helpers | `offset.cpp`, `containment.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`.
@@ -129,10 +105,7 @@ and the face on the other side is outside. The internal Boolean2 predicates are:
- `Intersect`: `w > 1`, which corresponds to both operands covering the face
for normalized unit-winding operands.
The public `CrossSection::FillRule` enum is still present for backend/API
compatibility while `clipper2` remains selectable. Boolean2 construction is
Positive-only; non-Positive public fill rules are unsupported by the Boolean2
backend until the public API cleanup in #1707 removes them.
Boolean2 construction is Positive-only.
## Regularization And Epsilon
@@ -157,25 +130,11 @@ future cleanup decisions within the epsilon regime.
## Validation
Useful local checks:
Build and run the `CrossSection` regression tests:
```sh
cmake -S . -B build/boolean2-core \
-DMANIFOLD_CROSS_SECTION=ON \
-DMANIFOLD_CROSS_SECTION_BACKEND=boolean2
cmake --build build/boolean2-core -j4 --target manifold
cmake -S . -B build/clipper-test \
-DMANIFOLD_CROSS_SECTION=ON \
-DMANIFOLD_CROSS_SECTION_BACKEND=clipper2 \
cmake -S . -B build \
-DMANIFOLD_TEST=ON
cmake --build build/clipper-test -j4 --target manifold_test
ctest --test-dir build/clipper-test -R '^CrossSection\\.' --output-on-failure
cmake --build build -j4 --target manifold_test
ctest --test-dir build -R '^CrossSection\.' --output-on-failure
```
The `CrossSection` regression command above intentionally targets `clipper2` in
this branch. `MANIFOLD_CROSS_SECTION_BACKEND=boolean2` only builds the core and
empty placeholder backend until the follow-up backend-wiring PR.
Focused backend regression tests, FuzzTest targets, and corpus tooling are
staged in later branches so this core-library branch stays reviewable.
+3 -23
View File
@@ -1,15 +1,10 @@
{
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
inputs.clipper2-src = {
url = "github:AngusJohnson/Clipper2";
flake = false;
};
outputs =
{ self
, nixpkgs
, flake-utils
, clipper2-src
}:
flake-utils.lib.eachDefaultSystem
(system:
@@ -17,15 +12,6 @@
manifold-version = "3.5.1";
pkgs = import nixpkgs {
inherit system;
overlays = [
(final: prev: {
clipper2 = prev.clipper2.overrideAttrs (_: rec {
version = clipper2-src.rev;
src = clipper2-src;
sourceRoot = "source/CPP";
});
})
];
};
manifold =
{ parallel ? true }: pkgs.stdenv.mkDerivation {
@@ -39,9 +25,7 @@
(ps: with ps; [ nanobind trimesh pytest ]))
gtest
]) ++ (if parallel then [ pkgs.onetbb ] else [ ]);
buildInputs = with pkgs; [
clipper2
];
buildInputs = [ ];
cmakeFlags = [
"-DMANIFOLD_STRICT=ON"
"-DMANIFOLD_CBIND=ON"
@@ -62,8 +46,6 @@
nativeBuildInputs = (with pkgs; [ cmake python3 ]);
buildInputs = [ pkgs.nodejs ];
configurePhase = ''
cp -r ${clipper2-src} clipper2
chmod -R +w clipper2
mkdir -p .emscriptencache
export EM_CACHE=$(pwd)/.emscriptencache
mkdir build
@@ -73,8 +55,7 @@
-DMANIFOLD_PAR=${if parallel then "ON" else "OFF"} \
-DMANIFOLD_USE_BUILTIN_TBB=${if parallel then "ON" else "OFF"} \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=${pkgs.gtest.src} \
-DFETCHCONTENT_SOURCE_DIR_TBB=${pkgs.onetbb.src} \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=../clipper2 ..
-DFETCHCONTENT_SOURCE_DIR_TBB=${pkgs.onetbb.src} ..
'';
buildPhase = ''
emmake make -j''${NIX_BUILD_CORES}
@@ -107,7 +88,7 @@
version = manifold-version;
src = self;
propagatedBuildInputs = [ numpy ];
buildInputs = with pkgs; [ clipper2 onetbb ];
buildInputs = with pkgs; [ onetbb ];
nativeBuildInputs = with pkgs; [
cmake
ninja
@@ -147,7 +128,6 @@
onetbb
gtest
assimp
clipper2
pkg-config
# useful tools
+33
View File
@@ -625,6 +625,39 @@ struct Rect {
*/
enum class OpType : char { Add, Subtract, Intersect };
// Adapted from Clipper2 docs:
// http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/JoinType.htm
// (Copyright © 2010-2023 Angus Johnson)
/**
* Specifies the treatment of path/contour joins (corners) when offseting
* CrossSections. See the [Clipper2
* doc](http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/JoinType.htm)
* for illustrations.
*/
enum class JoinType {
Square, /*!< Squaring is applied uniformly at all joins where the internal
join angle is less that 90 degrees. The squared edge will be at
exactly the offset distance from the join vertex. */
Round, /*!< Rounding is applied to all joins that have convex external
angles, and it maintains the exact offset distance from the join
vertex. */
Miter, /*!< There's a necessary limit to mitered joins (to avoid narrow
angled joins producing excessively long and narrow
[spikes](http://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/Classes/ClipperOffset/Properties/MiterLimit.htm)).
So where mitered joins would exceed a given maximum miter distance
(relative to the offset distance), these are 'squared' instead. */
Bevel /*!< Bevelled joins are similar to 'squared' joins except that
squaring won't occur at a fixed distance. While bevelled joins may
not be as pretty as squared joins, bevelling is much easier (ie
faster) than squaring. And perhaps this is why bevelling rather
than squaring is preferred in numerous graphics display formats
(including
[SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linejoin)
and
[PDF](https://helpx.adobe.com/indesign/using/applying-line-stroke-settings.html)
document formats). */
};
constexpr int DEFAULT_SEGMENTS = 0;
constexpr double DEFAULT_ANGLE = 10.0;
constexpr double DEFAULT_LENGTH = 1.0;
+8 -34
View File
@@ -35,9 +35,8 @@ struct PathImpl;
/**
* @brief Two-dimensional cross sections guaranteed to be without
* self-intersections, or overlaps between polygons (from construction onwards).
* This class makes use of the
* [Clipper2](http://www.angusj.com/clipper2/Docs/Overview.htm) library for
* polygon clipping (boolean) and offsetting operations.
* Polygon clipping (boolean) and offsetting use Manifold's own robust
* floating-point predicates.
*/
class CrossSection {
public:
@@ -74,38 +73,12 @@ class CrossSection {
Negative ///< Only sub-regions with winding counts < 0 are filled.
};
// Adapted from Clipper2 docs:
// http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/JoinType.htm
// (Copyright © 2010-2023 Angus Johnson)
/**
* Specifies the treatment of path/contour joins (corners) when offseting
* CrossSections. See the [Clipper2
* doc](http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/JoinType.htm)
* for illustrations.
* CrossSections; alias of manifold::JoinType (see common.h), shared with the
* polygon offset implementation.
*/
enum class JoinType {
Square, /*!< Squaring is applied uniformly at all joins where the internal
join angle is less that 90 degrees. The squared edge will be at
exactly the offset distance from the join vertex. */
Round, /*!< Rounding is applied to all joins that have convex external
angles, and it maintains the exact offset distance from the join
vertex. */
Miter, /*!< There's a necessary limit to mitered joins (to avoid narrow
angled joins producing excessively long and narrow
[spikes](http://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/Classes/ClipperOffset/Properties/MiterLimit.htm)).
So where mitered joins would exceed a given maximum miter distance
(relative to the offset distance), these are 'squared' instead. */
Bevel /*!< Bevelled joins are similar to 'squared' joins except that
squaring won't occur at a fixed distance. While bevelled joins may
not be as pretty as squared joins, bevelling is much easier (ie
faster) than squaring. And perhaps this is why bevelling rather
than squaring is preferred in numerous graphics display formats
(including
[SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linejoin)
and
[PDF](https://helpx.adobe.com/indesign/using/applying-line-stroke-settings.html)
document formats). */
};
using JoinType = ::manifold::JoinType;
/** @name Input & Output
*/
@@ -179,10 +152,11 @@ class CrossSection {
///@}
private:
mutable std::shared_ptr<std::mutex> pathsMutex_ =
std::make_shared<std::mutex>();
mutable std::mutex pathsMutex_;
mutable std::shared_ptr<const PathImpl> paths_;
mutable mat2x3 transform_ = la::identity;
// Propagated drift budget, analogous to Manifold::Impl::tolerance_.
mutable double tolerance_ = 0.0;
CrossSection(std::shared_ptr<const PathImpl> paths);
std::shared_ptr<const PathImpl> GetPaths() const;
};
+1 -3
View File
@@ -25,9 +25,7 @@ add_library(
src/gyroid_module.cpp
src/condensed_matter.cpp
)
if(MANIFOLD_CROSS_SECTION)
target_sources(samples PUBLIC src/bracelet.cpp src/knot.cpp)
endif()
target_sources(samples PUBLIC src/bracelet.cpp src/knot.cpp)
target_include_directories(samples PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
+4 -5
View File
@@ -1,14 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <clipper2-dir> <repeats>"
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <repeats>"
exit 2
fi
CLIPPER2_DIR="$1"
REPEATS="$2"
REPEATS="$1"
for variant in base head; do
bash ./scripts/run_pr_perf_guard.sh "./wt-${variant}" "$CLIPPER2_DIR" "./bench/${variant}" "$REPEATS"
bash ./scripts/run_pr_perf_guard.sh "./wt-${variant}" "./bench/${variant}" "$REPEATS"
done
-2
View File
@@ -8,7 +8,6 @@ cmake \
-DMANIFOLD_PYBIND=OFF \
-DMANIFOLD_DEBUG=ON \
-DMANIFOLD_ASSERT=ON \
-DMANIFOLD_CROSS_SECTION=ON \
-DMANIFOLD_PAR=OFF \
-DCMAKE_C_COMPILER=clang-18 \
-DCMAKE_CXX_COMPILER=clang++-18 \
@@ -17,7 +16,6 @@ cmake \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" \
-DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined" \
-DCMAKE_BUILD_RPATH_USE_ORIGIN=ON \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=clipper2 \
. -B build
cmake --build build
+5 -8
View File
@@ -1,15 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 4 ]; then
echo "Usage: $0 <source-dir> <clipper2-dir> <output-dir> <repeats>"
if [ "$#" -ne 3 ]; then
echo "Usage: $0 <source-dir> <output-dir> <repeats>"
exit 2
fi
SRC_DIR="$1"
CLIPPER2_DIR="$2"
OUT_DIR="$3"
REPEATS="$4"
OUT_DIR="$2"
REPEATS="$3"
BUILD_DIR="${OUT_DIR}/build"
cmake \
@@ -19,9 +18,7 @@ cmake \
-DMANIFOLD_STRICT=ON \
-DMANIFOLD_PYBIND=OFF \
-DMANIFOLD_TEST=ON \
-DMANIFOLD_PAR=ON \
-DMANIFOLD_CROSS_SECTION=OFF \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2="$CLIPPER2_DIR"
-DMANIFOLD_PAR=ON
cmake --build "$BUILD_DIR" --target perfTest
+1 -1
View File
@@ -21,7 +21,7 @@ EOT
cp -r ../manifold ./
mkdir build
cd build
cmake -DFETCHCONTENT_SOURCE_DIR_CLIPPER2=$CLIPPER2 -DFETCHCONTENT_SOURCE_DIR_TBB=$TBB ..
cmake -DFETCHCONTENT_SOURCE_DIR_TBB=$TBB ..
make
./testing
+17 -30
View File
@@ -13,34 +13,23 @@
# limitations under the License.
set(MANIFOLD_CROSS_SECTION_SRCS)
if(MANIFOLD_CROSS_SECTION)
if(MANIFOLD_CROSS_SECTION_BACKEND STREQUAL "clipper2")
list(APPEND MANIFOLD_CROSS_SECTION_SRCS cross_section/cross_section.cpp)
elseif(MANIFOLD_CROSS_SECTION_BACKEND STREQUAL "boolean2")
list(
APPEND
MANIFOLD_CROSS_SECTION_SRCS
cross_section/cross_section_boolean2.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
)
endif()
endif()
set(MANIFOLD_CROSS_SECTION_LIBS)
if(MANIFOLD_CROSS_SECTION AND MANIFOLD_CROSS_SECTION_BACKEND STREQUAL "clipper2")
list(APPEND MANIFOLD_CROSS_SECTION_LIBS Clipper2::Clipper2)
endif()
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
@@ -109,7 +98,6 @@ target_link_libraries(
PRIVATE
# optional dependencies
$<$<BOOL:${MANIFOLD_PAR}>:TBB::tbb>
${MANIFOLD_CROSS_SECTION_LIBS}
)
target_compile_options(manifold PRIVATE ${MANIFOLD_FLAGS})
@@ -120,7 +108,6 @@ set(
MANIFOLD_DEBUG
MANIFOLD_ASSERT
MANIFOLD_TIMING
MANIFOLD_CROSS_SECTION
MANIFOLD_NO_IOSTREAM
TRACY_ENABLE
TRACY_MEMORY_USAGE
-2
View File
@@ -27,7 +27,6 @@
#include "winding_filter.h"
namespace manifold {
namespace boolean2 {
namespace {
@@ -259,5 +258,4 @@ Polygons Boolean2D(const Polygons& a, const Polygons& b, OpType op, double eps,
return ApplyFillRule(a, b, bSign, rule, eps, tolerance);
}
} // namespace boolean2
} // namespace manifold
+1 -4
View File
@@ -22,7 +22,6 @@
#include "winding_filter.h"
namespace manifold {
namespace boolean2 {
double InferEps(const Polygons& a, const Polygons& b);
@@ -38,13 +37,11 @@ Polygons Boolean2D(const Polygons& a, const Polygons& b, OpType op,
double eps = 0.0, double tolerance = 0.0);
// Polygon offset backing CrossSection::Offset.
enum class OffsetJoinType { Square, Round, Miter, Bevel };
Polygons Offset(const Polygons& in, double delta, OffsetJoinType jt,
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 boolean2
} // namespace manifold
-2
View File
@@ -17,7 +17,6 @@
#include "bvh.h"
namespace manifold {
namespace boolean2 {
namespace {
@@ -91,5 +90,4 @@ BVH BVHBuildFromBoxes(const std::vector<Box2>& boxes) {
return out;
}
} // namespace boolean2
} // namespace manifold
-2
View File
@@ -26,7 +26,6 @@
#include "parallel_policy.h"
namespace manifold {
namespace boolean2 {
inline constexpr int kEdgePairBvhThreshold = 1024;
// The radix-tree BVH is binary and has at most 32 Morton-code bits plus
@@ -123,5 +122,4 @@ inline void CollidePairs(const BVH& bvh, const std::vector<Box2>& queries,
/*parallel=*/false);
}
} // namespace boolean2
} // namespace manifold
@@ -15,7 +15,6 @@
#include "canonicalize.h"
namespace manifold {
namespace boolean2 {
CanonicalSubEdges Canonicalize(const std::vector<EdgeM>& edges,
const std::vector<std::vector<int>>& lists) {
@@ -36,5 +35,4 @@ CanonicalSubEdges Canonicalize(const std::vector<EdgeM>& edges,
return out;
}
} // namespace boolean2
} // namespace manifold
@@ -21,7 +21,6 @@
#include "predicates.h"
namespace manifold {
namespace boolean2 {
struct CanonEdge {
int vMin, vMax;
@@ -73,5 +72,4 @@ struct CanonicalSubEdges {
CanonicalSubEdges Canonicalize(const std::vector<EdgeM>& edges,
const std::vector<std::vector<int>>& lists);
} // namespace boolean2
} // namespace manifold
@@ -26,7 +26,6 @@
// (containment grouping). Not part of the boolean-overlap algorithm
// pipeline; operates on already-regularized Positive `Polygons`.
namespace manifold {
namespace boolean2 {
namespace {
@@ -170,5 +169,4 @@ std::vector<Polygons> DecomposeByContainment(const Polygons& polys) {
return components;
}
} // namespace boolean2
} // namespace manifold
@@ -22,7 +22,6 @@
#include "winding_filter.h"
namespace manifold {
namespace boolean2 {
#ifdef MANIFOLD_DEBUG
namespace {
@@ -312,5 +311,4 @@ void TraceRecorder::RecordFilteredOutput(const std::vector<vec2>& verts,
#endif
}
} // namespace boolean2
} // namespace manifold
-2
View File
@@ -31,7 +31,6 @@
#include "canonicalize.h"
namespace manifold {
namespace boolean2 {
enum class WindRule;
struct Trace;
@@ -213,5 +212,4 @@ class TraceRecorder {
Trace* trace_;
};
} // namespace boolean2
} // namespace manifold
-2
View File
@@ -38,7 +38,6 @@
#include "winding_filter.h"
namespace manifold {
namespace boolean2 {
namespace {
@@ -251,5 +250,4 @@ OverlapResult RemoveOverlaps2D(const std::vector<vec2>& vertsIn,
std::move(merge.inputVert2Merged), numMerged};
}
} // namespace boolean2
} // namespace manifold
-2
View File
@@ -20,7 +20,6 @@
#include "winding_filter.h"
namespace manifold {
namespace boolean2 {
struct Trace;
@@ -42,5 +41,4 @@ OverlapResult RemoveOverlaps2D(const std::vector<vec2>& vertsIn,
WindRule pred = WindRule::Add,
Trace* trace = nullptr);
} // namespace boolean2
} // namespace manifold
@@ -27,7 +27,6 @@
#include "vertex_merge.h"
namespace manifold {
namespace boolean2 {
namespace {
@@ -182,5 +181,4 @@ NarrowPhaseResult BuildListsAndFindIntersections(
return result;
}
} // namespace boolean2
} // namespace manifold
@@ -21,7 +21,6 @@
#include "vertex_merge.h"
namespace manifold {
namespace boolean2 {
struct NarrowPhaseResult {
std::vector<std::vector<int>> lists;
@@ -36,5 +35,4 @@ NarrowPhaseResult BuildListsAndFindIntersections(
const std::vector<EdgeM>& edges, const std::vector<vec2>& verts, double eps,
const std::vector<std::pair<int, int>>& pairs);
} // namespace boolean2
} // namespace manifold
@@ -30,7 +30,6 @@
#include "vertex_merge.h"
namespace manifold {
namespace boolean2 {
namespace {
@@ -336,5 +335,4 @@ IntersectionInsertion FindAndInsertIntersections(
return {std::move(verts), std::move(lists), std::move(vertEdges)};
}
} // namespace boolean2
} // namespace manifold
@@ -22,7 +22,6 @@
#include "vertex_merge.h"
namespace manifold {
namespace boolean2 {
inline constexpr double kIntersectionMergeEpsFactor = 10.0;
@@ -52,5 +51,4 @@ IntersectionInsertion FindAndInsertIntersections(
const std::vector<Box2>& edgeBoxes, const BVH& bvh,
const std::vector<IntersectionPoint>& precomputedIntersections);
} // namespace boolean2
} // namespace manifold
+6 -9
View File
@@ -22,7 +22,6 @@
// Polygon offset backing `CrossSection::Offset`.
namespace manifold {
namespace boolean2 {
namespace {
@@ -140,8 +139,7 @@ double ValidMiterLimit(double miterLimit) {
// the solid for delta > 0, regardless of ring orientation, and the
// convex/concave decision depends only on `cross * sign(delta)`.
SimplePolygon OffsetContour(const SimplePolygon& contour, double delta,
OffsetJoinType jt, double miterLimit,
int segments) {
JoinType jt, double miterLimit, int segments) {
const int n = static_cast<int>(contour.size());
if (n < 3 || delta == 0) return contour;
const double deltaSign = (delta >= 0) ? 1.0 : -1.0;
@@ -195,10 +193,10 @@ SimplePolygon OffsetContour(const SimplePolygon& contour, double delta,
// Convex corner: apply join.
out.push_back(endPrev);
switch (jt) {
case OffsetJoinType::Round:
case JoinType::Round:
AppendRoundJoin(out, V, nPrev, nNext, delta, segments);
break;
case OffsetJoinType::Miter: {
case JoinType::Miter: {
// miterLen / |delta| = 1 / cos(half_angle) =
// sqrt(2 / (1 + dot(nPrev, nNext))). The limit
// miterLen <= miterLimit * |delta| rearranges to
@@ -229,10 +227,10 @@ SimplePolygon OffsetContour(const SimplePolygon& contour, double delta,
}
break;
}
case OffsetJoinType::Square:
case JoinType::Square:
AppendSquareJoin(out, V, nPrev, nNext, delta);
break;
case OffsetJoinType::Bevel:
case JoinType::Bevel:
break;
}
out.push_back(startNext);
@@ -251,7 +249,7 @@ SimplePolygon OffsetContour(const SimplePolygon& contour, double delta,
// with Positive/Add filling. Collinear vertices are left in place for the
// caller to `Simplify` if wanted, matching Manifold's tolerance model rather
// than Clipper2's finishing behaviour.
Polygons Offset(const Polygons& in, double delta, OffsetJoinType jt,
Polygons Offset(const Polygons& in, double delta, JoinType jt,
double miterLimit, int circularSegments) {
// Reject NaN/Inf delta and input coordinates.
if (!std::isfinite(delta)) return {};
@@ -285,5 +283,4 @@ Polygons Offset(const Polygons& in, double delta, OffsetJoinType jt,
return ApplyFillRule(offsetRings, eps);
}
} // namespace boolean2
} // namespace manifold
@@ -15,7 +15,6 @@
#pragma once
namespace manifold {
namespace boolean2 {
// 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
@@ -23,5 +22,4 @@ namespace boolean2 {
// its per-index work is much smaller.
inline constexpr int kFineParallelGrainSize = 512;
} // namespace boolean2
} // namespace manifold
@@ -24,7 +24,6 @@
#include "../../utils.h"
namespace manifold {
namespace boolean2 {
namespace {
@@ -281,5 +280,4 @@ bool IntersectSegments(const GraphSegment2D& a, const GraphSegment2D& b,
AwayFromEndpoints(*out, b0, b1, eps);
}
} // namespace boolean2
} // namespace manifold
-2
View File
@@ -19,7 +19,6 @@
#include "manifold/common.h"
namespace manifold {
namespace boolean2 {
constexpr double kU = 1.110223024625156540423631668e-16;
constexpr double kAlphaCoeff = 12.37;
@@ -67,5 +66,4 @@ GraphOrder2D CompareProjectedOrder(const GraphSegment2D& a,
bool IntersectSegments(const GraphSegment2D& a, const GraphSegment2D& b,
double eps, vec2* out);
} // namespace boolean2
} // namespace manifold
@@ -38,7 +38,6 @@
#include "predicates.h"
namespace manifold {
namespace boolean2 {
using manifold::la::dot;
@@ -224,5 +223,4 @@ std::vector<EdgeM> RemapAndCollapse(const std::vector<EdgeM>& edges,
return out;
}
} // namespace boolean2
} // namespace manifold
@@ -19,7 +19,6 @@
#include "predicates.h"
namespace manifold {
namespace boolean2 {
struct IntersectionPoint {
int i;
@@ -38,5 +37,4 @@ void VESetInsert(std::vector<int>* vec, int x);
std::vector<EdgeM> RemapAndCollapse(const std::vector<EdgeM>& edges,
const std::vector<int>& inputVert2Merged);
} // namespace boolean2
} // namespace manifold
@@ -41,7 +41,6 @@
#include "winding_filter.h"
namespace manifold {
namespace boolean2 {
namespace {
@@ -554,5 +553,4 @@ std::vector<OutEdge> FilterByWindingHalfedges(const CanonicalSubEdges& canon,
return FilterByWindingHalfedgesImpl(canon, verts, debug, rule, trace);
}
} // namespace boolean2
} // namespace manifold
@@ -19,7 +19,6 @@
#include "canonicalize.h"
namespace manifold {
namespace boolean2 {
struct Trace;
@@ -34,5 +33,4 @@ std::vector<OutEdge> FilterByWindingHalfedges(const CanonicalSubEdges& canon,
WindRule rule = WindRule::Add,
Trace* trace = nullptr);
} // namespace boolean2
} // namespace manifold
+360 -401
View File
@@ -14,197 +14,179 @@
#include "manifold/cross_section.h"
#include <algorithm>
#include <cmath>
#include <queue>
#include "../svd.h"
#include "../utils.h"
#include "clipper2/clipper.core.h"
#include "clipper2/clipper.h"
#include "clipper2/clipper.offset.h"
namespace C2 = Clipper2Lib;
#include "boolean2/boolean2.h"
#include "manifold/optional_assert.h"
// CrossSection implementation. Public methods route through `boolean2/` for
// polygon boolean operations, containment grouping, offsets, and polygon
// utilities.
using namespace manifold;
namespace manifold {
struct PathImpl {
PathImpl(const C2::PathsD paths_) : paths_(paths_) {}
operator const C2::PathsD&() const { return paths_; }
const C2::PathsD paths_;
PathImpl(Polygons paths) : paths_(std::move(paths)) {}
operator const Polygons&() const { return paths_; }
const Polygons paths_;
};
} // namespace manifold
namespace {
const int precision_ = 8;
C2::ClipType cliptype_of_op(OpType op) {
C2::ClipType ct = C2::ClipType::Union;
switch (op) {
case OpType::Add:
break;
case OpType::Subtract:
ct = C2::ClipType::Difference;
break;
case OpType::Intersect:
ct = C2::ClipType::Intersection;
break;
};
return ct;
}
C2::FillRule fr(CrossSection::FillRule fillrule) {
C2::FillRule fr = C2::FillRule::EvenOdd;
switch (fillrule) {
case CrossSection::FillRule::EvenOdd:
break;
case CrossSection::FillRule::NonZero:
fr = C2::FillRule::NonZero;
break;
case CrossSection::FillRule::Positive:
fr = C2::FillRule::Positive;
break;
case CrossSection::FillRule::Negative:
fr = C2::FillRule::Negative;
break;
};
return fr;
}
C2::JoinType jt(CrossSection::JoinType jointype) {
C2::JoinType jt = C2::JoinType::Square;
switch (jointype) {
case CrossSection::JoinType::Square:
break;
case CrossSection::JoinType::Round:
jt = C2::JoinType::Round;
break;
case CrossSection::JoinType::Miter:
jt = C2::JoinType::Miter;
break;
case CrossSection::JoinType::Bevel:
jt = C2::JoinType::Bevel;
break;
};
return jt;
}
vec2 v2_of_pd(const C2::PointD p) { return {p.x, p.y}; }
C2::PointD v2_to_pd(const vec2 v) { return C2::PointD(v.x, v.y); }
C2::PathD pathd_of_contour(const SimplePolygon& ctr) {
auto p = C2::PathD();
p.reserve(ctr.size());
for (auto v : ctr) {
p.push_back(v2_to_pd(v));
bool AllFinite(const mat2x3& m) {
for (const int c : {0, 1, 2}) {
if (!la::all(la::isfinite(m[c]))) return false;
}
return p;
return true;
}
C2::PathsD transform(const C2::PathsD ps, const mat2x3 m) {
const bool invert = la::determinant(mat2(m)) < 0;
auto transformed = C2::PathsD();
transformed.reserve(ps.size());
for (auto path : ps) {
auto sz = path.size();
auto s = C2::PathD(sz);
for (size_t i = 0; i < sz; ++i) {
auto idx = invert ? sz - 1 - i : i;
s[idx] = v2_to_pd(m * vec3(path[i].x, path[i].y, 1));
}
transformed.push_back(s);
}
return transformed;
}
std::shared_ptr<const PathImpl> shared_paths(const C2::PathsD& ps) {
return std::make_shared<const PathImpl>(ps);
}
// forward declaration for mutual recursion
void decompose_hole(const C2::PolyTreeD* outline,
std::vector<C2::PathsD>& polys, C2::PathsD& poly,
size_t n_holes, size_t j);
void decompose_outline(const C2::PolyTreeD* tree,
std::vector<C2::PathsD>& polys, size_t i) {
auto n_outlines = tree->Count();
if (i < n_outlines) {
auto outline = tree->Child(i);
auto n_holes = outline->Count();
auto poly = C2::PathsD(n_holes + 1);
poly[0] = outline->Polygon();
decompose_hole(outline, polys, poly, n_holes, 0);
polys.push_back(poly);
if (i < n_outlines - 1) {
decompose_outline(tree, polys, i + 1);
Polygons FilterSmallContours(const Polygons& paths, double epsilon) {
Polygons filtered;
filtered.reserve(paths.size());
for (const auto& path : paths) {
const double area = SignedArea(path);
Rect box;
for (const vec2& v : path) box.Union(v);
const vec2 size = box.Size();
if (std::fabs(area) > std::max(size.x, size.y) * epsilon) {
filtered.push_back(path);
}
}
return filtered;
}
void decompose_hole(const C2::PolyTreeD* outline,
std::vector<C2::PathsD>& polys, C2::PathsD& poly,
size_t n_holes, size_t j) {
if (j < n_holes) {
auto child = outline->Child(j);
decompose_outline(child, polys, 0);
poly[j + 1] = child->Polygon();
decompose_hole(outline, polys, poly, n_holes, j + 1);
}
}
void flatten(const C2::PolyTreeD* tree, C2::PathsD& polys, size_t i) {
auto n_outlines = tree->Count();
if (i < n_outlines) {
auto outline = tree->Child(i);
flatten(outline, polys, 0);
polys.push_back(outline->Polygon());
if (i < n_outlines - 1) {
flatten(tree, polys, i + 1);
Polygons TransformPolygons(const Polygons& paths, const mat2x3& transform) {
const bool invert = la::determinant(mat2(transform)) < 0;
Polygons out;
out.reserve(paths.size());
for (const auto& path : paths) {
const size_t size = path.size();
SimplePolygon transformed(size);
for (size_t i = 0; i < size; ++i) {
const size_t index = invert ? size - 1 - i : i;
transformed[index] = transform * vec3(path[i].x, path[i].y, 1.0);
}
out.push_back(std::move(transformed));
}
return out;
}
bool V2Lesser(vec2 a, vec2 b) {
if (a.x == b.x) return a.y < b.y;
return a.x < b.x;
std::shared_ptr<const PathImpl> shared_paths(Polygons paths) {
return std::make_shared<const PathImpl>(std::move(paths));
}
void HullBacktrack(const vec2& pt, std::vector<vec2>& stack) {
auto sz = stack.size();
while (sz >= 2 && CCW(stack[sz - 2], stack[sz - 1], pt, 0.0) <= 0.0) {
void HullBacktrack(const vec2& point, std::vector<vec2>& stack) {
size_t size = stack.size();
while (size >= 2 &&
CCW(stack[size - 2], stack[size - 1], point, 0.0) <= 0.0) {
stack.pop_back();
sz = stack.size();
size = stack.size();
}
}
// Based on method described here:
// https://www.hackerearth.com/practice/math/geometry/line-sweep-technique/tutorial/
// Changed to follow:
// https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
// This is the same algorithm (Andrew, also called Montone Chain).
C2::PathD HullImpl(SimplePolygon& pts) {
size_t len = pts.size();
if (len < 3) return C2::PathD(); // not enough points to create a polygon
std::sort(pts.begin(), pts.end(), V2Lesser);
auto lower = std::vector<vec2>{};
for (auto& pt : pts) {
HullBacktrack(pt, lower);
lower.push_back(pt);
SimplePolygon HullImpl(SimplePolygon& points) {
if (points.size() < 3) return {};
for (const vec2& p : points) {
if (!la::all(la::isfinite(p))) return {};
}
auto upper = std::vector<vec2>{};
for (auto pt_iter = pts.rbegin(); pt_iter != pts.rend(); pt_iter++) {
HullBacktrack(*pt_iter, upper);
upper.push_back(*pt_iter);
manifold::stable_sort(points.begin(), points.end(), [](vec2 a, vec2 b) {
if (a.x == b.x) return a.y < b.y;
return a.x < b.x;
});
std::vector<vec2> lower;
for (const vec2& point : points) {
HullBacktrack(point, lower);
lower.push_back(point);
}
std::vector<vec2> upper;
for (auto it = points.rbegin(); it != points.rend(); ++it) {
HullBacktrack(*it, upper);
upper.push_back(*it);
}
upper.pop_back();
lower.pop_back();
auto path = C2::PathD();
SimplePolygon path;
path.reserve(lower.size() + upper.size());
for (const auto& l : lower) path.push_back(v2_to_pd(l));
for (const auto& u : upper) path.push_back(v2_to_pd(u));
for (const vec2& point : lower) path.push_back(point);
for (const vec2& point : upper) path.push_back(point);
return path;
}
// Drop the vertex closest to the line through its current neighbors, repeating
// while under tol (Clipper2 SimplifyPaths semantics). Re-checking current, not
// original, neighbors each step is what keeps a curve from collapsing. A
// min-heap over (deviation, index) with stamp-based lazy invalidation makes
// each removal re-key only the two neighbors, so the pass is O(n log n).
// Exactly-tied deviations remove the lowest original index first.
SimplePolygon SimplifyRing(SimplePolygon ring, double tol) {
const int n = static_cast<int>(ring.size());
if (n <= 3) return ring;
const double tol2 = tol * tol;
std::vector<int> prev(n), next(n), stamp(n, 0);
std::vector<char> alive(n, 1);
for (int i = 0; i < n; ++i) {
prev[i] = (i + n - 1) % n;
next[i] = (i + 1) % n;
}
auto deviation2 = [&](int i) {
const vec2 P = ring[prev[i]];
const vec2 N = ring[next[i]];
const vec2 pn = N - P;
const double pnLen2 = la::dot(pn, pn);
const double cross = la::cross(ring[i] - P, pn);
return pnLen2 > 0.0 ? cross * cross / pnLen2 : 0.0;
};
struct Entry {
double d2;
int stamp;
int idx;
};
auto worse = [](const Entry& a, const Entry& b) {
return a.d2 > b.d2 || (a.d2 == b.d2 && a.idx > b.idx);
};
std::priority_queue<Entry, std::vector<Entry>, decltype(worse)> heap(worse);
for (int i = 0; i < n; ++i) heap.push({deviation2(i), 0, i});
int numAlive = n;
while (numAlive > 3 && !heap.empty()) {
const Entry top = heap.top();
heap.pop();
// Each live vertex always has exactly one current-stamp entry, so the
// first non-stale pop is the true global minimum.
if (!alive[top.idx] || top.stamp != stamp[top.idx]) continue;
if (top.d2 >= tol2) break; // every vertex deviates by >= tol
alive[top.idx] = 0;
--numAlive;
const int p = prev[top.idx];
const int nx = next[top.idx];
next[p] = nx;
prev[nx] = p;
for (const int j : {p, nx}) {
++stamp[j];
heap.push({deviation2(j), stamp[j], j});
}
}
SimplePolygon out;
out.reserve(numAlive);
for (int i = 0; i < n; ++i) {
if (alive[i]) out.push_back(ring[i]);
}
return out;
}
} // namespace
namespace manifold {
@@ -212,13 +194,27 @@ namespace manifold {
/**
* The default constructor is an empty cross-section (containing no contours).
*/
CrossSection::CrossSection() {
paths_ = std::make_shared<const PathImpl>(C2::PathsD());
}
CrossSection::CrossSection() : paths_(shared_paths({})) {}
CrossSection::~CrossSection() = default;
CrossSection::CrossSection(CrossSection&&) noexcept = default;
CrossSection& CrossSection::operator=(CrossSection&&) noexcept = default;
// Explicit move ops keep `other.pathsMutex_` intact so a subsequent copy-
// or move-assign into the moved-from object can still lock it.
CrossSection::CrossSection(CrossSection&& other) noexcept {
std::lock_guard<std::mutex> lock(other.pathsMutex_);
paths_ = std::move(other.paths_);
transform_ = other.transform_;
tolerance_ = other.tolerance_;
}
CrossSection& CrossSection::operator=(CrossSection&& other) noexcept {
if (this == &other) return *this;
std::scoped_lock lock(pathsMutex_, other.pathsMutex_);
paths_ = std::move(other.paths_);
transform_ = other.transform_;
tolerance_ = other.tolerance_;
return *this;
}
/**
* The copy constructor avoids copying the underlying paths vector (sharing
@@ -228,55 +224,53 @@ CrossSection& CrossSection::operator=(CrossSection&&) noexcept = default;
* const methods.
*/
CrossSection::CrossSection(const CrossSection& other) {
std::lock_guard<std::mutex> lock(*other.pathsMutex_);
std::lock_guard<std::mutex> lock(other.pathsMutex_);
paths_ = other.paths_;
transform_ = other.transform_;
tolerance_ = other.tolerance_;
}
CrossSection& CrossSection::operator=(const CrossSection& other) {
if (this != &other) {
std::scoped_lock lock(*pathsMutex_, *other.pathsMutex_);
paths_ = other.paths_;
transform_ = other.transform_;
}
if (this == &other) return *this;
std::scoped_lock lock(pathsMutex_, other.pathsMutex_);
paths_ = other.paths_;
transform_ = other.transform_;
tolerance_ = other.tolerance_;
return *this;
};
}
// Private, skips unioning.
CrossSection::CrossSection(std::shared_ptr<const PathImpl> ps) { paths_ = ps; }
CrossSection::CrossSection(std::shared_ptr<const PathImpl> paths)
: paths_(std::move(paths)) {}
/**
* Create a 2d cross-section from a single contour. A boolean union operation
* (with Positive filling rule by default) is performed to ensure the
* resulting CrossSection is free of self-intersections.
* (with the Positive filling rule) is performed to ensure the resulting
* CrossSection is free of self-intersections.
*
* @param contour A closed path outlining the desired cross-section.
* @param fillrule The filling rule used to interpret polygon sub-regions
* created by self-intersections in contour.
* @param fillrule Only FillRule::Positive is supported; other values are
* ignored (and, with MANIFOLD_ASSERT, rejected).
*/
CrossSection::CrossSection(const SimplePolygon& contour, FillRule fillrule) {
auto ps = C2::PathsD{(pathd_of_contour(contour))};
paths_ = shared_paths(C2::Union(ps, fr(fillrule), precision_));
}
CrossSection::CrossSection(const SimplePolygon& contour, FillRule fillrule)
: CrossSection(Polygons{contour}, fillrule) {}
/**
* Create a 2d cross-section from a set of contours (complex polygons). A
* boolean union operation (with Positive filling rule by default) is
* performed to combine overlapping polygons and ensure the resulting
* CrossSection is free of intersections.
* boolean union operation (with the Positive filling rule) is performed to
* combine overlapping polygons and ensure the resulting CrossSection is free
* of intersections.
*
* @param contours A set of closed paths describing zero or more complex
* polygons.
* @param fillrule The filling rule used to interpret polygon sub-regions in
* contours.
* @param fillrule Only FillRule::Positive is supported; other values are
* ignored (and, with MANIFOLD_ASSERT, rejected).
*/
CrossSection::CrossSection(const Polygons& contours, FillRule fillrule) {
auto ps = C2::PathsD();
ps.reserve(contours.size());
for (auto ctr : contours) {
ps.push_back(pathd_of_contour(ctr));
}
paths_ = shared_paths(C2::Union(ps, fr(fillrule), precision_));
DEBUG_ASSERT(fillrule == FillRule::Positive, logicErr,
"Boolean2 CrossSection supports only Positive fill");
(void)fillrule;
tolerance_ = InferEps(contours, {});
paths_ = shared_paths(ApplyFillRule(contours, tolerance_));
}
/**
@@ -285,23 +279,29 @@ CrossSection::CrossSection(const Polygons& contours, FillRule fillrule) {
* @param rect An axis-aligned rectangular bounding box.
*/
CrossSection::CrossSection(const Rect& rect) {
C2::PathD p(4);
p[0] = C2::PointD(rect.min.x, rect.min.y);
p[1] = C2::PointD(rect.max.x, rect.min.y);
p[2] = C2::PointD(rect.max.x, rect.max.y);
p[3] = C2::PointD(rect.min.x, rect.max.y);
paths_ = shared_paths(C2::PathsD{p});
if (rect.IsEmpty()) {
paths_ = shared_paths({});
return;
}
paths_ = shared_paths({{{rect.min.x, rect.min.y},
{rect.max.x, rect.min.y},
{rect.max.x, rect.max.y},
{rect.min.x, rect.max.y}}});
}
// Private
// All access to paths_ should be done through the GetPaths() method, which
// applies the accumulated transform_
std::shared_ptr<const PathImpl> CrossSection::GetPaths() const {
std::lock_guard<std::mutex> lock(*pathsMutex_);
if (transform_ == mat2x3(la::identity)) {
return paths_;
}
paths_ = shared_paths(::transform(paths_->paths_, transform_));
std::lock_guard<std::mutex> lock(pathsMutex_);
if (transform_ == mat2x3(la::identity)) return paths_;
// Scale tolerance once from the composed transform, not per Transform call:
// spectral norm is sub-multiplicative, so per-step scaling would inflate it
// super-linearly under chained shears. Floor at the translated scale so large
// translations stay above post-materialization FP noise.
const double translationScale =
std::max(std::fabs(transform_[2][0]), std::fabs(transform_[2][1]));
tolerance_ =
std::max(SpectralNorm(mat2(transform_[0], transform_[1])) * tolerance_,
EpsilonFromScale(translationScale));
paths_ = shared_paths(TransformPolygons(paths_->paths_, transform_));
transform_ = mat2x3(la::identity);
return paths_;
}
@@ -318,24 +318,9 @@ CrossSection CrossSection::Square(const vec2 size, bool center) {
if (size.x < 0.0 || size.y < 0.0 || la::length(size) == 0.0) {
return CrossSection();
}
auto p = C2::PathD(4);
if (center) {
const auto w = size.x / 2;
const auto h = size.y / 2;
p[0] = C2::PointD(w, h);
p[1] = C2::PointD(-w, h);
p[2] = C2::PointD(-w, -h);
p[3] = C2::PointD(w, -h);
} else {
const double x = size.x;
const double y = size.y;
p[0] = C2::PointD(0.0, 0.0);
p[1] = C2::PointD(x, 0.0);
p[2] = C2::PointD(x, y);
p[3] = C2::PointD(0.0, y);
}
return CrossSection(shared_paths(C2::PathsD{p}));
const vec2 half = size * 0.5;
if (center) return CrossSection(Rect(-half, half));
return CrossSection(Rect({0.0, 0.0}, size));
}
/**
@@ -346,17 +331,14 @@ CrossSection CrossSection::Square(const vec2 size, bool center) {
* calculated by the static Quality defaults according to the radius.
*/
CrossSection CrossSection::Circle(double radius, int circularSegments) {
if (radius <= 0.0) {
return CrossSection();
}
int n = circularSegments > 2 ? circularSegments
: Quality::GetCircularSegments(radius);
double dPhi = 360.0 / n;
auto circle = C2::PathD(n);
for (int i = 0; i < n; ++i) {
circle[i] = C2::PointD(radius * cosd(dPhi * i), radius * sind(dPhi * i));
}
return CrossSection(shared_paths(C2::PathsD{circle}));
if (radius <= 0.0) return CrossSection();
const int n = circularSegments > 2 ? circularSegments
: Quality::GetCircularSegments(radius);
SimplePolygon circle(n);
const double dPhi = 360.0 / n;
for (int i = 0; i < n; ++i)
circle[i] = {radius * cosd(dPhi * i), radius * sind(dPhi * i)};
return CrossSection(shared_paths({std::move(circle)}));
}
/**
@@ -364,10 +346,15 @@ CrossSection CrossSection::Circle(double radius, int circularSegments) {
*/
CrossSection CrossSection::Boolean(const CrossSection& second,
OpType op) const {
auto ct = cliptype_of_op(op);
auto res = C2::BooleanOp(ct, C2::FillRule::Positive, GetPaths()->paths_,
second.GetPaths()->paths_, precision_);
return CrossSection(shared_paths(res));
// GetPaths() returns a snapshot aliasing each source's paths_, which stays
// alive for this call, so bind by reference instead of deep-copying.
const Polygons& a = GetPaths()->paths_;
const Polygons& b = second.GetPaths()->paths_;
const double eps = InferEps(a, b);
const double tolerance = std::max({tolerance_, second.tolerance_, eps});
CrossSection result(shared_paths(Boolean2D(a, b, op, eps, tolerance)));
result.tolerance_ = tolerance;
return result;
}
/**
@@ -376,37 +363,39 @@ CrossSection CrossSection::Boolean(const CrossSection& second,
*/
CrossSection CrossSection::BatchBoolean(
const std::vector<CrossSection>& crossSections, OpType op) {
if (crossSections.size() == 0)
return CrossSection();
else if (crossSections.size() == 1)
return crossSections[0];
auto subjs = crossSections[0].GetPaths();
if (crossSections.empty()) return CrossSection();
if (crossSections.size() == 1) return crossSections[0];
if (op == OpType::Intersect) {
auto res = subjs->paths_;
Polygons result = crossSections[0].GetPaths()->paths_;
double tol = crossSections[0].tolerance_;
for (size_t i = 1; i < crossSections.size(); ++i) {
res = C2::BooleanOp(C2::ClipType::Intersection, C2::FillRule::Positive,
res, crossSections[i].GetPaths()->paths_, precision_);
const auto& clip = crossSections[i].GetPaths()->paths_;
const double eps = InferEps(result, clip);
const double tolerance =
std::max({tol, crossSections[i].tolerance_, eps});
result = Boolean2D(result, clip, OpType::Intersect, eps, tolerance);
tol = tolerance;
}
return CrossSection(shared_paths(res));
CrossSection out(shared_paths(std::move(result)));
out.tolerance_ = tol;
return out;
}
int n_clips = 0;
Polygons clips;
double clipsTol = 0.0;
for (size_t i = 1; i < crossSections.size(); ++i) {
n_clips += crossSections[i].GetPaths()->paths_.size();
const auto& paths = crossSections[i].GetPaths()->paths_;
clips.insert(clips.end(), paths.begin(), paths.end());
clipsTol = std::max(clipsTol, crossSections[i].tolerance_);
}
auto clips = C2::PathsD();
clips.reserve(n_clips);
for (size_t i = 1; i < crossSections.size(); ++i) {
auto ps = crossSections[i].GetPaths();
clips.insert(clips.end(), ps->paths_.begin(), ps->paths_.end());
}
auto ct = cliptype_of_op(op);
auto res = C2::BooleanOp(ct, C2::FillRule::Positive, subjs->paths_, clips,
precision_);
return CrossSection(shared_paths(res));
const auto& subject = crossSections[0].GetPaths()->paths_;
const double eps = InferEps(subject, clips);
const double tolerance =
std::max({crossSections[0].tolerance_, clipsTol, eps});
CrossSection out(shared_paths(Boolean2D(subject, clips, op, eps, tolerance)));
out.tolerance_ = tolerance;
return out;
}
/**
@@ -473,24 +462,17 @@ CrossSection CrossSection::Compose(
* holes.
*/
std::vector<CrossSection> CrossSection::Decompose() const {
if (NumContour() < 2) {
return std::vector<CrossSection>{CrossSection(*this)};
if (NumContour() < 2) return {*this};
const auto& paths = GetPaths()->paths_;
std::vector<Polygons> components = DecomposeByContainment(paths);
std::vector<CrossSection> out;
out.reserve(components.size());
for (auto& component : components) {
CrossSection piece(shared_paths(std::move(component)));
piece.tolerance_ = tolerance_;
out.push_back(std::move(piece));
}
C2::PolyTreeD tree;
C2::BooleanOp(C2::ClipType::Union, C2::FillRule::Positive, GetPaths()->paths_,
C2::PathsD(), tree, precision_);
auto polys = std::vector<C2::PathsD>();
decompose_outline(&tree, polys, 0);
auto comps = std::vector<CrossSection>();
comps.reserve(polys.size());
// reverse the stack while wrapping
for (auto poly = polys.rbegin(); poly != polys.rend(); ++poly)
comps.emplace_back(CrossSection(shared_paths(*poly)));
return comps;
return out;
}
/**
@@ -500,9 +482,7 @@ std::vector<CrossSection> CrossSection::Decompose() const {
* @param v The vector to add to every vertex.
*/
CrossSection CrossSection::Translate(const vec2 v) const {
mat2x3 m({1.0, 0.0}, //
{0.0, 1.0}, //
{v.x, v.y});
mat2x3 m({1.0, 0.0}, {0.0, 1.0}, {v.x, v.y});
return Transform(m);
}
@@ -513,11 +493,9 @@ CrossSection CrossSection::Translate(const vec2 v) const {
* @param degrees degrees about the Z-axis to rotate.
*/
CrossSection CrossSection::Rotate(double degrees) const {
auto s = sind(degrees);
auto c = cosd(degrees);
mat2x3 m({c, s}, //
{-s, c}, //
{0.0, 0.0});
const double s = sind(degrees);
const double c = cosd(degrees);
mat2x3 m({c, s}, {-s, c}, {0.0, 0.0});
return Transform(m);
}
@@ -528,9 +506,7 @@ CrossSection CrossSection::Rotate(double degrees) const {
* @param scale The vector to multiply every vertex by per component.
*/
CrossSection CrossSection::Scale(const vec2 scale) const {
mat2x3 m({scale.x, 0.0}, //
{0.0, scale.y}, //
{0.0, 0.0});
mat2x3 m({scale.x, 0.0}, {0.0, scale.y}, {0.0, 0.0});
return Transform(m);
}
@@ -543,12 +519,10 @@ CrossSection CrossSection::Scale(const vec2 scale) const {
* @param ax the axis to be mirrored over
*/
CrossSection CrossSection::Mirror(const vec2 ax) const {
if (la::length(ax) == 0.) {
return CrossSection();
}
auto n = la::normalize(ax);
auto m = mat2x3(mat2(la::identity) - 2.0 * la::outerprod(n, n), vec2(0.0));
return Transform(m);
if (la::length(ax) == 0.0) return CrossSection();
const vec2 n = la::normalize(ax);
return Transform(
mat2x3(mat2(la::identity) - 2.0 * la::outerprod(n, n), vec2(0.0)));
}
/**
@@ -559,10 +533,15 @@ CrossSection CrossSection::Mirror(const vec2 ax) const {
* @param m The affine transform matrix to apply to all the vertices.
*/
CrossSection CrossSection::Transform(const mat2x3& m) const {
std::lock_guard<std::mutex> lock(*pathsMutex_);
auto transformed = CrossSection();
// A non-finite transform is a no-op rather than poisoning coords/tolerance_.
if (!AllFinite(m)) return *this;
std::lock_guard<std::mutex> lock(pathsMutex_);
CrossSection transformed;
transformed.transform_ = m * Mat3(transform_);
transformed.paths_ = paths_;
// Carry tolerance unscaled; GetPaths() scales it once from the composed
// transform at materialization.
transformed.tolerance_ = tolerance_;
return transformed;
}
@@ -575,10 +554,8 @@ CrossSection CrossSection::Transform(const mat2x3& m) const {
* @param warpFunc A function that modifies a given vertex position.
*/
CrossSection CrossSection::Warp(std::function<void(vec2&)> warpFunc) const {
return WarpBatch([&warpFunc](VecView<vec2> vecs) {
for (vec2& p : vecs) {
warpFunc(p);
}
return WarpBatch([&warpFunc](VecView<vec2> points) {
for (vec2& point : points) warpFunc(point);
});
}
@@ -591,34 +568,30 @@ CrossSection CrossSection::Warp(std::function<void(vec2&)> warpFunc) const {
*/
CrossSection CrossSection::WarpBatch(
std::function<void(VecView<vec2>)> warpFunc) const {
std::vector<vec2> tmp_verts;
C2::PathsD paths = GetPaths()->paths_; // deep copy
for (C2::PathD const& path : paths) {
for (C2::PointD const& p : path) {
tmp_verts.push_back(v2_of_pd(p));
}
Polygons paths = GetPaths()->paths_;
std::vector<vec2> points;
for (const auto& path : paths) {
points.insert(points.end(), path.begin(), path.end());
}
warpFunc(VecView<vec2>(tmp_verts.data(), tmp_verts.size()));
auto cursor = tmp_verts.begin();
for (C2::PathD& path : paths) {
for (C2::PointD& p : path) {
p = v2_to_pd(*cursor);
++cursor;
}
warpFunc(VecView<vec2>(points.data(), points.size()));
auto point = points.begin();
for (auto& path : paths) {
for (auto& v : path) v = *point++;
}
return CrossSection(
shared_paths(C2::Union(paths, C2::FillRule::Positive, precision_)));
// Warping can self-intersect the rings, so re-apply the fill rule at machine
// eps (not the drift tolerance - this is regularization, not decimation).
const double eps = InferEps(paths, {});
CrossSection out(shared_paths(ApplyFillRule(paths, eps)));
out.tolerance_ = std::max(tolerance_, eps);
return out;
}
/**
* Remove vertices from the contours in this CrossSection that are less than
* the specified distance epsilon from an imaginary line that passes through
* the specified distance tolerance from an imaginary line that passes through
* its two adjacent vertices. Near duplicate vertices and collinear points
* will be removed at lower epsilons, with elimination of line segments
* becoming increasingly aggressive with larger epsilons.
* will be removed at lower tolerances, with elimination of line segments
* becoming increasingly aggressive with larger tolerances.
*
* It is recommended to apply this function following Offset, in order to
* clean up any spurious tiny line segments introduced that do not improve
@@ -626,29 +599,22 @@ CrossSection CrossSection::WarpBatch(
* offseting operations are to be performed, which would compound the issue.
*/
CrossSection CrossSection::Simplify(double epsilon) const {
C2::PolyTreeD tree;
C2::BooleanOp(C2::ClipType::Union, C2::FillRule::Positive, GetPaths()->paths_,
C2::PathsD(), tree, precision_);
C2::PathsD polys;
flatten(&tree, polys, 0);
// Filter out contours less than epsilon wide.
C2::PathsD filtered;
for (C2::PathD poly : polys) {
auto area = C2::Area(poly);
Rect box;
for (auto vert : poly) {
box.Union(vec2(vert.x, vert.y));
}
vec2 size = box.Size();
if (std::abs(area) > std::max(size.x, size.y) * epsilon) {
filtered.push_back(poly);
}
// Stored paths are already fill-rule-regularized, so Simplify only decimates:
// drop tiny contours, then remove near-collinear verts per ring at `epsilon`
// (clipper2 SimplifyPaths style, not Ramer-Douglas-Peucker). Like clipper2,
// the result is NOT re-regularized - a coarse `epsilon` can self-intersect a
// ring, so healing is left to a later boolean.
const Polygons& paths = GetPaths()->paths_;
const Polygons filtered = FilterSmallContours(paths, epsilon);
Polygons out;
out.reserve(filtered.size());
for (const auto& ring : filtered) {
SimplePolygon simplified = SimplifyRing(ring, epsilon);
if (simplified.size() >= 3) out.push_back(std::move(simplified));
}
auto ps = SimplifyPaths(filtered, epsilon, true);
return CrossSection(shared_paths(ps));
CrossSection result(shared_paths(std::move(out)));
result.tolerance_ = std::max(tolerance_, epsilon);
return result;
}
/**
@@ -672,22 +638,18 @@ CrossSection CrossSection::Simplify(double epsilon) const {
* defaults according to the radius.
*/
CrossSection CrossSection::Offset(double delta, JoinType jointype,
double miter_limit,
double miterLimit,
int circularSegments) const {
double arc_tol = 0.;
if (jointype == JoinType::Round) {
int n = circularSegments > 2 ? circularSegments
: Quality::GetCircularSegments(delta);
// This calculates tolerance as a function of circular segments and delta
// (radius) in order to get back the same number of segments in Clipper2:
// steps_per_360 = PI / acos(1 - arc_tol / abs_delta)
const double abs_delta = std::fabs(delta);
arc_tol = (math::cos(Clipper2Lib::PI / n) - 1) * -abs_delta;
}
auto ps =
C2::InflatePaths(GetPaths()->paths_, delta, jt(jointype),
C2::EndType::Polygon, miter_limit, precision_, arc_tol);
return CrossSection(shared_paths(ps));
// Qualified: unqualified lookup inside the class finds this member and
// hides the namespace-scope polygon offset.
Polygons offset = manifold::Offset(GetPaths()->paths_, delta, jointype,
miterLimit, circularSegments);
CrossSection out(shared_paths(std::move(offset)));
// Round-join faceting (circularSegments) is a quality choice, not a drift
// budget, so it is not folded into tolerance_ - doing so would over-merge
// features downstream. Max, not sum, keeps chained Offset bounded.
out.tolerance_ = std::max(tolerance_, InferEps(out.GetPaths()->paths_, {}));
return out;
}
/**
@@ -698,27 +660,32 @@ CrossSection CrossSection::Offset(double delta, JoinType jointype,
*/
CrossSection CrossSection::Hull(
const std::vector<CrossSection>& crossSections) {
int n = 0;
for (auto cs : crossSections) n += cs.NumVert();
SimplePolygon pts;
pts.reserve(n);
for (auto cs : crossSections) {
auto paths = cs.GetPaths()->paths_;
for (auto path : paths) {
for (auto p : path) {
pts.push_back(v2_of_pd(p));
}
}
size_t numPoints = 0;
double maxTol = 0.0;
for (const auto& cs : crossSections) {
numPoints += cs.NumVert();
maxTol = std::max(maxTol, cs.tolerance_);
}
return CrossSection(shared_paths(C2::PathsD{HullImpl(pts)}));
SimplePolygon points;
points.reserve(numPoints);
for (const auto& cs : crossSections) {
const auto& paths = cs.GetPaths()->paths_;
for (const auto& path : paths)
for (const vec2& point : path) points.push_back(point);
}
SimplePolygon hull = HullImpl(points);
if (hull.size() < 3) return CrossSection();
CrossSection out(shared_paths({std::move(hull)}));
out.tolerance_ = std::max(maxTol, InferEps(out.GetPaths()->paths_, {}));
return out;
}
/**
* Compute the convex hull of this cross-section.
*/
CrossSection CrossSection::Hull() const {
return Hull(std::vector<CrossSection>{*this});
}
CrossSection CrossSection::Hull() const { return Hull({*this}); }
/**
* Compute the convex hull of a set of points. If the given points are fewer
@@ -728,7 +695,12 @@ CrossSection CrossSection::Hull() const {
* hull.
*/
CrossSection CrossSection::Hull(SimplePolygon pts) {
return CrossSection(shared_paths(C2::PathsD{HullImpl(pts)}));
SimplePolygon hull = HullImpl(pts);
if (hull.size() < 3) return CrossSection();
Polygons inputForEps{pts};
CrossSection out(shared_paths({std::move(hull)}));
out.tolerance_ = InferEps(inputForEps, {});
return out;
}
/**
@@ -739,31 +711,27 @@ CrossSection CrossSection::Hull(SimplePolygon pts) {
* compute a convex hull.
*/
CrossSection CrossSection::Hull(const Polygons polys) {
SimplePolygon pts;
for (auto poly : polys) {
for (auto p : poly) {
pts.push_back(p);
}
}
return Hull(pts);
SimplePolygon points;
for (const auto& path : polys)
for (const vec2& point : path) points.push_back(point);
return Hull(points);
}
/**
* Return the total area covered by complex polygons making up the
* CrossSection.
*/
double CrossSection::Area() const { return C2::Area(GetPaths()->paths_); }
double CrossSection::Area() const {
return TotalSignedArea(GetPaths()->paths_);
}
/**
* Return the number of vertices in the CrossSection.
*/
size_t CrossSection::NumVert() const {
size_t n = 0;
auto paths = GetPaths()->paths_;
for (auto p : paths) {
n += p.size();
}
return n;
size_t numVert = 0;
for (const auto& path : GetPaths()->paths_) numVert += path.size();
return numVert;
}
/**
@@ -782,25 +750,16 @@ bool CrossSection::IsEmpty() const { return GetPaths()->paths_.empty(); }
* vertices.
*/
Rect CrossSection::Bounds() const {
auto r = C2::GetBounds(GetPaths()->paths_);
return Rect({r.left, r.bottom}, {r.right, r.top});
Rect box;
for (const auto& path : GetPaths()->paths_) {
for (const vec2& v : path) box.Union(v);
}
return box;
}
/**
* Return the contours of this CrossSection as a Polygons.
*/
Polygons CrossSection::ToPolygons() const {
auto polys = Polygons();
auto paths = GetPaths()->paths_;
polys.reserve(paths.size());
for (auto p : paths) {
auto sp = SimplePolygon();
sp.reserve(p.size());
for (auto v : p) {
sp.push_back({v.x, v.y});
}
polys.push_back(sp);
}
return polys;
}
Polygons CrossSection::ToPolygons() const { return GetPaths()->paths_; }
} // namespace manifold
@@ -1,229 +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 <cassert>
#include "manifold/cross_section.h"
// Temporary backend-selection placeholder. The boolean2 implementation lands
// in the follow-up backend PR; this file intentionally returns empty
// CrossSection results so the staged backend cannot appear partially correct.
using namespace manifold;
namespace manifold {
struct PathImpl {
PathImpl(Polygons paths) : paths_(std::move(paths)) {}
operator const Polygons&() const { return paths_; }
const Polygons paths_;
};
} // namespace manifold
namespace {
std::shared_ptr<const PathImpl> empty_paths() {
return std::make_shared<const PathImpl>(Polygons{});
}
} // namespace
namespace manifold {
CrossSection::CrossSection() : paths_(empty_paths()) {}
CrossSection::~CrossSection() = default;
CrossSection::CrossSection(CrossSection&&) noexcept = default;
CrossSection& CrossSection::operator=(CrossSection&&) noexcept = default;
CrossSection::CrossSection(const CrossSection& other) {
(void)other;
paths_ = empty_paths();
}
CrossSection& CrossSection::operator=(const CrossSection& other) {
(void)other;
paths_ = empty_paths();
transform_ = la::identity;
return *this;
}
CrossSection::CrossSection(std::shared_ptr<const PathImpl> paths)
: paths_(empty_paths()) {
(void)paths;
}
CrossSection::CrossSection(const SimplePolygon& contour, FillRule fillrule) {
(void)contour;
assert(fillrule == FillRule::Positive);
(void)fillrule;
paths_ = empty_paths();
}
CrossSection::CrossSection(const Polygons& contours, FillRule fillrule) {
(void)contours;
assert(fillrule == FillRule::Positive);
(void)fillrule;
paths_ = empty_paths();
}
CrossSection::CrossSection(const Rect& rect) {
(void)rect;
paths_ = empty_paths();
}
std::shared_ptr<const PathImpl> CrossSection::GetPaths() const {
std::lock_guard<std::mutex> lock(*pathsMutex_);
return paths_;
}
CrossSection CrossSection::Square(const vec2 size, bool center) {
(void)size;
(void)center;
return CrossSection();
}
CrossSection CrossSection::Circle(double radius, int circularSegments) {
(void)radius;
(void)circularSegments;
return CrossSection();
}
CrossSection CrossSection::Boolean(const CrossSection& second,
OpType op) const {
(void)second;
(void)op;
return CrossSection();
}
CrossSection CrossSection::BatchBoolean(
const std::vector<CrossSection>& crossSections, OpType op) {
(void)crossSections;
(void)op;
return CrossSection();
}
CrossSection CrossSection::operator+(const CrossSection& other) const {
return Boolean(other, OpType::Add);
}
CrossSection& CrossSection::operator+=(const CrossSection& other) {
*this = *this + other;
return *this;
}
CrossSection CrossSection::operator-(const CrossSection& other) const {
return Boolean(other, OpType::Subtract);
}
CrossSection& CrossSection::operator-=(const CrossSection& other) {
*this = *this - other;
return *this;
}
CrossSection CrossSection::operator^(const CrossSection& other) const {
return Boolean(other, OpType::Intersect);
}
CrossSection& CrossSection::operator^=(const CrossSection& other) {
*this = *this ^ other;
return *this;
}
CrossSection CrossSection::Compose(
const std::vector<CrossSection>& crossSections) {
(void)crossSections;
return CrossSection();
}
std::vector<CrossSection> CrossSection::Decompose() const { return {}; }
CrossSection CrossSection::Translate(const vec2 v) const {
(void)v;
return CrossSection();
}
CrossSection CrossSection::Rotate(double degrees) const {
(void)degrees;
return CrossSection();
}
CrossSection CrossSection::Scale(const vec2 scale) const {
(void)scale;
return CrossSection();
}
CrossSection CrossSection::Mirror(const vec2 ax) const {
(void)ax;
return CrossSection();
}
CrossSection CrossSection::Transform(const mat2x3& m) const {
(void)m;
return CrossSection();
}
CrossSection CrossSection::Warp(std::function<void(vec2&)> warpFunc) const {
(void)warpFunc;
return CrossSection();
}
CrossSection CrossSection::WarpBatch(
std::function<void(VecView<vec2>)> warpFunc) const {
(void)warpFunc;
return CrossSection();
}
CrossSection CrossSection::Simplify(double epsilon) const {
(void)epsilon;
return CrossSection();
}
CrossSection CrossSection::Offset(double delta, JoinType jt, double miterLimit,
int circularSegments) const {
(void)delta;
(void)jt;
(void)miterLimit;
(void)circularSegments;
return CrossSection();
}
CrossSection CrossSection::Hull(
const std::vector<CrossSection>& crossSections) {
(void)crossSections;
return CrossSection();
}
CrossSection CrossSection::Hull() const { return CrossSection(); }
CrossSection CrossSection::Hull(SimplePolygon pts) {
(void)pts;
return CrossSection();
}
CrossSection CrossSection::Hull(const Polygons polys) {
(void)polys;
return CrossSection();
}
double CrossSection::Area() const { return 0.0; }
size_t CrossSection::NumVert() const { return 0; }
size_t CrossSection::NumContour() const { return 0; }
bool CrossSection::IsEmpty() const { return true; }
Rect CrossSection::Bounds() const { return {}; }
Polygons CrossSection::ToPolygons() const { return {}; }
} // namespace manifold
+20
View File
@@ -301,4 +301,24 @@ inline double SpectralNorm(mat3 A) {
SVDSet usv = SVD(A);
return usv.S[0][0];
}
/**
* Returns the largest singular value (spectral norm) of a 2x2 matrix, via the
* closed-form eigenvalues of A^T A. The mat3 overload would floor the norm if a
* 2D linear map were embedded in 3D.
*
* @param A The matrix to measure.
*/
inline double SpectralNorm(mat2 A) {
const vec2 c0 = A[0];
const vec2 c1 = A[1];
const double m00 = la::dot(c0, c0);
const double m11 = la::dot(c1, c1);
const double m01 = la::dot(c0, c1);
const double trace = m00 + m11;
const double det = m00 * m11 - m01 * m01;
const double disc2 = trace * trace - 4.0 * det;
const double disc = std::sqrt(disc2 > 0.0 ? disc2 : 0.0);
return std::sqrt(0.5 * (trace + disc));
}
} // namespace manifold
+1 -1
View File
@@ -33,7 +33,7 @@ set(
hull_test.cpp
samples_test.cpp
boolean_complex_test.cpp
$<$<BOOL:${MANIFOLD_CROSS_SECTION}>:cross_section_test.cpp>
cross_section_test.cpp
$<$<BOOL:${MANIFOLD_CBIND}>:manifoldc_test.cpp>
)
-5
View File
@@ -12,10 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#ifdef MANIFOLD_CROSS_SECTION
#include "manifold/cross_section.h"
#endif
#include "manifold/manifold.h"
#include "test.h"
@@ -273,7 +270,6 @@ TEST(BooleanComplex, Spiral) {
EXPECT_EQ(result.Genus(), -120);
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(BooleanComplex, Sweep) {
ManifoldParamGuard guard;
ManifoldParams().processOverlaps = true;
@@ -508,7 +504,6 @@ TEST(BooleanComplex, Sweep) {
EXPECT_NEAR(shape.Volume(), 3757, 1);
if (options.exportModels) WriteTestOBJ("unionError.obj", shape);
}
#endif
TEST(BooleanComplex, InterpolatedNormals) {
MeshGL a;
+3 -9
View File
@@ -180,17 +180,11 @@ TEST(CrossSection, FillRule) {
{-4.8, 8}, //
};
// CrossSection regularizes every input with the Positive fill rule; the
// FillRule argument is retained for API compatibility but only Positive is
// supported, so non-Positive rules are not exercised here.
CrossSection positive(polygon);
EXPECT_NEAR(positive.Area(), 0.683, 0.001);
CrossSection negative(polygon, CrossSection::FillRule::Negative);
EXPECT_NEAR(negative.Area(), 0.193, 0.001);
CrossSection evenOdd(polygon, CrossSection::FillRule::EvenOdd);
EXPECT_NEAR(evenOdd.Area(), 0.875, 0.001);
CrossSection nonZero(polygon, CrossSection::FillRule::NonZero);
EXPECT_NEAR(nonZero.Area(), 0.875, 0.001);
}
TEST(CrossSection, Hull) {
-12
View File
@@ -17,9 +17,7 @@
#include <algorithm>
#include "../src/execution_impl.h"
#ifdef MANIFOLD_CROSS_SECTION
#include "manifold/cross_section.h"
#endif
#include "test.h"
namespace {
@@ -816,14 +814,12 @@ TEST(Manifold, Revolve2) {
EXPECT_NEAR(donutHole.SurfaceArea(), 96.0 * kPi, 1.0);
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(Manifold, Revolve3) {
CrossSection circle = CrossSection::Circle(1, 32);
Manifold sphere = Manifold::Revolve(circle.ToPolygons(), 32);
EXPECT_NEAR(sphere.Volume(), 4.0 / 3.0 * kPi, 0.1);
EXPECT_NEAR(sphere.SurfaceArea(), 4 * kPi, 0.15);
}
#endif
TEST(Manifold, RevolveClip) {
Polygons polys = {{{-5, -10}, {5, 0}, {-5, 10}}};
@@ -863,7 +859,6 @@ TEST(Manifold, PartialRevolveOffset) {
}
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(Manifold, Warp) {
CrossSection square = CrossSection::Square({1, 1});
Manifold shape =
@@ -898,7 +893,6 @@ TEST(Manifold, Warp2) {
EXPECT_NEAR(shape.SurfaceArea(), simplified.SurfaceArea(), 0.0001);
EXPECT_NEAR(shape.Volume(), 321, 1);
}
#endif
TEST(Manifold, WarpBatch) {
Manifold cube = Manifold::Cube({2, 3, 4});
@@ -924,7 +918,6 @@ TEST(Manifold, WarpBatch) {
EXPECT_EQ(shape1.SurfaceArea(), shape2.SurfaceArea());
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(Manifold, Project) {
MeshGL input;
input.numProp = 3;
@@ -1009,7 +1002,6 @@ TEST(Manifold, Project) {
CrossSection projected = in.Project();
EXPECT_NEAR(projected.Area(), 0.72, 0.01);
}
#endif
/**
* Testing more advanced Manifold operations.
@@ -1040,7 +1032,6 @@ TEST(Manifold, Transform) {
Identical(cube.GetMeshGL(), cube2.GetMeshGL());
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(Manifold, Slice) {
Manifold cube = Manifold::Cube();
CrossSection bottom = cube.Slice();
@@ -1065,7 +1056,6 @@ TEST(Manifold, Simplify) {
if (options.exportModels) WriteTestOBJ("torus.obj", simplified);
}
#endif
TEST(Manifold, MeshID) {
const Manifold cube = Manifold::Cube();
@@ -1279,7 +1269,6 @@ TEST(Manifold, MirrorUnion2) {
EXPECT_TRUE(result.MatchesTriNormals());
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(Manifold, Invalid) {
auto invalid = Manifold::Error::InvalidConstruction;
auto circ = CrossSection::Circle(10.);
@@ -1297,7 +1286,6 @@ TEST(Manifold, Invalid) {
EXPECT_EQ(Manifold::Extrude(empty_circ.ToPolygons(), 10.).Status(), invalid);
EXPECT_EQ(Manifold::Revolve(empty_sq.ToPolygons()).Status(), invalid);
}
#endif
TEST(Manifold, MergeDegenerates) {
MeshGL cube = Manifold::Cube(vec3(1), true).GetMeshGL();
-2
View File
@@ -200,7 +200,6 @@ TEST(Properties, MingapAfterTransformations) {
ASSERT_NEAR(distance, 1, 0.001);
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(Properties, MingapStretchyBracelet) {
auto a = StretchyBracelet();
auto b = StretchyBracelet().Translate({0, 0, 20});
@@ -209,7 +208,6 @@ TEST(Properties, MingapStretchyBracelet) {
ASSERT_NEAR(distance, 5, 0.001);
}
#endif
TEST(Properties, MinGapAfterTransformationsOutOfBounds) {
auto a = Manifold::Sphere(1, 512).Rotate(30, 30, 30);
+3 -9
View File
@@ -14,9 +14,7 @@
#include "samples.h"
#ifdef MANIFOLD_CROSS_SECTION
#include "manifold/cross_section.h"
#endif
#include "test.h"
using namespace manifold;
@@ -40,7 +38,6 @@ std::vector<int> EdgePairs(const MeshGL in) {
return edgePair;
}
#ifdef MANIFOLD_CROSS_SECTION
// If you print this knot (with support), you can snap a half-inch marble into
// it and it'll roll around (dimensions in mm).
TEST(Samples, Knot13) {
@@ -64,7 +61,6 @@ TEST(Samples, Knot42) {
EXPECT_NEAR(knots[0].SurfaceArea(), knots[1].SurfaceArea(), 1);
CheckGL(knot42);
}
#endif
TEST(Samples, Scallop) {
Manifold scallop = Scallop();
@@ -124,7 +120,6 @@ TEST(Samples, Frame) {
// This creates a bracelet sample which involves many operations between shapes
// that are not in general position, e.g. coplanar faces.
#ifdef MANIFOLD_CROSS_SECTION
TEST(Samples, Bracelet) {
Manifold bracelet = StretchyBracelet();
EXPECT_EQ(bracelet.NumDegenerateTris(), 0);
@@ -174,7 +169,6 @@ TEST(Samples, GyroidModule) {
if (options.exportModels) WriteTestOBJ("gyroidModule.obj", gyroid);
}
#endif
TEST(Samples, Sponge1) {
Manifold sponge = MengerSponge(1);
@@ -190,7 +184,6 @@ TEST(Samples, Sponge1) {
#ifndef __EMSCRIPTEN__
// A fractal with many degenerate intersections, which also tests exact 90
// degree rotations.
#ifdef MANIFOLD_CROSS_SECTION
TEST(Samples, Sponge4) {
Manifold sponge = MengerSponge(4);
EXPECT_LE(sponge.NumDegenerateTris(), 8);
@@ -211,7 +204,9 @@ TEST(Samples, Sponge4) {
EXPECT_EQ(rect.max.y, box.max.y);
EXPECT_NEAR(projection.Area(), 0.535, 0.001);
Manifold extrusion = Manifold::Extrude(projection.ToPolygons(), 1);
EXPECT_LE(extrusion.NumDegenerateTris(), 32);
// This dense extrusion keeps a few sub-tolerance sliver tris; Genus (below)
// is the meaningful invariant.
EXPECT_LE(extrusion.NumDegenerateTris(), 80);
EXPECT_EQ(extrusion.Genus(), 502);
if (options.exportModels) {
@@ -228,7 +223,6 @@ TEST(Samples, Sponge4) {
CheckGLEquiv(cutSponge.first.GetMeshGL(), cutSponge2.first.GetMeshGL());
}
#endif
#endif
TEST(Samples, CondensedMatter16) {
Manifold cm = CondensedMatter(16);
-8
View File
@@ -14,9 +14,7 @@
#include <algorithm>
#ifdef MANIFOLD_CROSS_SECTION
#include "manifold/cross_section.h"
#endif
#include "manifold/manifold.h"
#include "test.h"
@@ -81,7 +79,6 @@ TEST(Smooth, TruncatedCone) {
if (options.exportModels) WriteTestOBJ("smoothTruncatedCone.obj", smooth);
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(Smooth, ToLength) {
Manifold cone = Manifold::Extrude(
CrossSection::Circle(10, 10).Translate({10, 0}).ToPolygons(), 2, 0, 0,
@@ -103,7 +100,6 @@ TEST(Smooth, ToLength) {
if (options.exportModels) WriteTestOBJ("smoothToLength.obj", smooth);
}
#endif
TEST(Smooth, Sphere) {
int n[5] = {4, 8, 16, 32, 64};
@@ -221,7 +217,6 @@ TEST(Smooth, MissingNormalsCone) {
if (options.exportModels) WriteTestOBJ("missingNormalsCone.obj", out);
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(Smooth, Fillet) {
float depth = 3;
float radius = 10;
@@ -242,7 +237,6 @@ TEST(Smooth, Fillet) {
EXPECT_NEAR(fillet.SurfaceArea(), 2622, 1);
if (options.exportModels) WriteTestOBJ("fillet.obj", fillet);
}
#endif
TEST(Smooth, Manual) {
// Unit Octahedron
@@ -322,7 +316,6 @@ vec4 CircularTangent(const vec3& tangent, const vec3& edgeVec) {
return vec4(vec3(bz3) / bz3.w, bz3.w);
}
#ifdef MANIFOLD_CROSS_SECTION
TEST(Smooth, Torus) {
MeshGL64 torusMesh =
Manifold::Revolve(
@@ -381,7 +374,6 @@ TEST(Smooth, Torus) {
if (options.exportModels) WriteTestOBJ("smoothTorus.obj", smooth);
}
#endif
TEST(Smooth, SDF) {
const double r = 10;