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:
co-authored by
Claude Opus 4.8
parent
324bec7bd7
commit
db042ab153
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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)
|
||||
{
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user