clipper2-py 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clipper2_py-0.1.0/.github/workflows/wheels.yml +115 -0
- clipper2_py-0.1.0/.gitignore +10 -0
- clipper2_py-0.1.0/.gitmodules +3 -0
- clipper2_py-0.1.0/CHANGELOG.md +42 -0
- clipper2_py-0.1.0/CMakeLists.txt +36 -0
- clipper2_py-0.1.0/DEVIATIONS.md +198 -0
- clipper2_py-0.1.0/LICENSE +21 -0
- clipper2_py-0.1.0/PKG-INFO +152 -0
- clipper2_py-0.1.0/README.md +119 -0
- clipper2_py-0.1.0/RELEASE_CHECKLIST.md +116 -0
- clipper2_py-0.1.0/docs/api-map.md +225 -0
- clipper2_py-0.1.0/docs/guide.md +277 -0
- clipper2_py-0.1.0/docs/img/booleans.svg +18 -0
- clipper2_py-0.1.0/docs/img/make_images.py +87 -0
- clipper2_py-0.1.0/docs/img/offsetting.svg +20 -0
- clipper2_py-0.1.0/docs/img/triangulate-minkowski.svg +7 -0
- clipper2_py-0.1.0/docs/spec.md +130 -0
- clipper2_py-0.1.0/pyproject.toml +80 -0
- clipper2_py-0.1.0/src/clipper2/__init__.py +10 -0
- clipper2_py-0.1.0/src/clipper2/__init__.pyi +599 -0
- clipper2_py-0.1.0/src/clipper2/py.typed +0 -0
- clipper2_py-0.1.0/src/clipper2/z.py +3 -0
- clipper2_py-0.1.0/src/clipper2/z.pyi +463 -0
- clipper2_py-0.1.0/src/cpp/bind_core.cpp +735 -0
- clipper2_py-0.1.0/src/cpp/bind_engine.cpp +246 -0
- clipper2_py-0.1.0/src/cpp/bind_misc.cpp +165 -0
- clipper2_py-0.1.0/src/cpp/bind_offset.cpp +187 -0
- clipper2_py-0.1.0/src/cpp/convert.h +568 -0
- clipper2_py-0.1.0/src/cpp/module.cpp +57 -0
- clipper2_py-0.1.0/tests/reference/CMakeLists.txt +32 -0
- clipper2_py-0.1.0/tests/reference/ref_cli.cpp +556 -0
- clipper2_py-0.1.0/tests/reference/test_differential.py +977 -0
- clipper2_py-0.1.0/tests/reference/test_differential_z.py +622 -0
- clipper2_py-0.1.0/tests/test_convert.py +332 -0
- clipper2_py-0.1.0/tests/test_core.py +567 -0
- clipper2_py-0.1.0/tests/test_engine.py +612 -0
- clipper2_py-0.1.0/tests/test_leaks.py +162 -0
- clipper2_py-0.1.0/tests/test_misc.py +272 -0
- clipper2_py-0.1.0/tests/test_offset.py +502 -0
- clipper2_py-0.1.0/tests/test_readme.py +28 -0
- clipper2_py-0.1.0/tests/test_stubs.py +286 -0
- clipper2_py-0.1.0/tests/typing_sample.py +245 -0
- clipper2_py-0.1.0/tests/upstream/clip_file_load.py +161 -0
- clipper2_py-0.1.0/tests/upstream/test_export_headers.py +13 -0
- clipper2_py-0.1.0/tests/upstream/test_is_collinear.py +58 -0
- clipper2_py-0.1.0/tests/upstream/test_lines.py +45 -0
- clipper2_py-0.1.0/tests/upstream/test_offset_orientation.py +29 -0
- clipper2_py-0.1.0/tests/upstream/test_offsets.py +660 -0
- clipper2_py-0.1.0/tests/upstream/test_orientation.py +17 -0
- clipper2_py-0.1.0/tests/upstream/test_polygons.py +102 -0
- clipper2_py-0.1.0/tests/upstream/test_polytree_holes.py +272 -0
- clipper2_py-0.1.0/tests/upstream/test_polytree_intersection.py +22 -0
- clipper2_py-0.1.0/tests/upstream/test_polytree_union.py +68 -0
- clipper2_py-0.1.0/tests/upstream/test_random_paths.py +81 -0
- clipper2_py-0.1.0/tests/upstream/test_rect.py +31 -0
- clipper2_py-0.1.0/tests/upstream/test_rect_clip.py +72 -0
- clipper2_py-0.1.0/tests/upstream/test_simplify_path.py +10 -0
- clipper2_py-0.1.0/tests/upstream/test_trim_collinear.py +31 -0
- clipper2_py-0.1.0/tests/upstream/test_windows.py +14 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/CMakeLists.txt +377 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2.pc.cmakein +12 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Config.cmake.in +6 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.core.h +1159 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.engine.h +635 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.export.h +851 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.h +796 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h +117 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.offset.h +125 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.rectclip.h +80 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.triangulation.h +27 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.version.h +6 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/src/clipper.engine.cpp +3163 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/src/clipper.offset.cpp +661 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/src/clipper.rectclip.cpp +1027 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/src/clipper.triangulation.cpp +1219 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/GoogleTest in Visual Studio.txt +13 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestExportHeaders.cpp +203 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestIsCollinear.cpp +53 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestLines.cpp +55 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestOffsetOrientation.cpp +29 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestOffsets.cpp +700 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestOrientation.cpp +16 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestPolygons.cpp +125 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestPolytreeHoles.cpp +319 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestPolytreeIntersection.cpp +24 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestPolytreeUnion.cpp +79 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestRandomPaths.cpp +83 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestRect.cpp +62 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestRectClip.cpp +76 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestSimplifyPath.cpp +11 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestTrimCollinear.cpp +27 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestWindows.cpp +5 -0
- clipper2_py-0.1.0/third_party/Clipper2/CPP/clipper.version.in +6 -0
- clipper2_py-0.1.0/third_party/Clipper2/LICENSE +23 -0
- clipper2_py-0.1.0/third_party/Clipper2/README.md +94 -0
- clipper2_py-0.1.0/third_party/Clipper2/Tests/Lines.txt +162 -0
- clipper2_py-0.1.0/third_party/Clipper2/Tests/Offsets.txt +18 -0
- clipper2_py-0.1.0/third_party/Clipper2/Tests/Polygons.txt +1925 -0
- clipper2_py-0.1.0/third_party/Clipper2/Tests/PolytreeHoleOwner.txt +13 -0
- clipper2_py-0.1.0/third_party/Clipper2/Tests/PolytreeHoleOwner2.txt +3946 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
name: wheels
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: ${{ matrix.os }} ${{ matrix.arch }}
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
include:
|
|
18
|
+
- { os: ubuntu-24.04, arch: x86_64 }
|
|
19
|
+
- { os: ubuntu-24.04-arm, arch: aarch64 }
|
|
20
|
+
- { os: macos-14, arch: arm64 }
|
|
21
|
+
- { os: macos-14, arch: x86_64 }
|
|
22
|
+
- { os: windows-2022, arch: AMD64 }
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v7
|
|
25
|
+
with:
|
|
26
|
+
submodules: true
|
|
27
|
+
|
|
28
|
+
- name: Check tag matches project version
|
|
29
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
30
|
+
shell: bash
|
|
31
|
+
run: |
|
|
32
|
+
v=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)
|
|
33
|
+
[ "v$v" = "${GITHUB_REF_NAME}" ] || {
|
|
34
|
+
echo "tag ${GITHUB_REF_NAME} does not match pyproject version $v"; exit 1; }
|
|
35
|
+
|
|
36
|
+
- uses: pypa/cibuildwheel@v4.2.0
|
|
37
|
+
env:
|
|
38
|
+
CIBW_ARCHS: ${{ matrix.arch }}
|
|
39
|
+
|
|
40
|
+
- uses: actions/upload-artifact@v7
|
|
41
|
+
with:
|
|
42
|
+
name: wheels-${{ matrix.os }}-${{ matrix.arch }}
|
|
43
|
+
path: wheelhouse/*.whl
|
|
44
|
+
|
|
45
|
+
sdist:
|
|
46
|
+
name: sdist
|
|
47
|
+
runs-on: ubuntu-24.04
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v7
|
|
50
|
+
with:
|
|
51
|
+
submodules: true
|
|
52
|
+
|
|
53
|
+
- name: Build sdist
|
|
54
|
+
run: pipx run build --sdist
|
|
55
|
+
|
|
56
|
+
- name: Install from sdist and run tests
|
|
57
|
+
run: |
|
|
58
|
+
python -m venv /tmp/sdist-venv
|
|
59
|
+
/tmp/sdist-venv/bin/pip install dist/*.tar.gz pytest
|
|
60
|
+
/tmp/sdist-venv/bin/python -m pytest tests
|
|
61
|
+
|
|
62
|
+
- uses: actions/upload-artifact@v7
|
|
63
|
+
with:
|
|
64
|
+
name: wheels-sdist
|
|
65
|
+
path: dist/*.tar.gz
|
|
66
|
+
|
|
67
|
+
numpy-floor:
|
|
68
|
+
name: tests with numpy 1.23 + differential tests
|
|
69
|
+
runs-on: ubuntu-24.04
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v7
|
|
72
|
+
with:
|
|
73
|
+
submodules: true
|
|
74
|
+
|
|
75
|
+
- uses: actions/setup-python@v7
|
|
76
|
+
with:
|
|
77
|
+
python-version: "3.10"
|
|
78
|
+
|
|
79
|
+
- name: Build the C++ reference CLI
|
|
80
|
+
run: |
|
|
81
|
+
cmake -S tests/reference -B build/ref -DCMAKE_BUILD_TYPE=Release
|
|
82
|
+
cmake --build build/ref
|
|
83
|
+
|
|
84
|
+
- name: Install and run tests
|
|
85
|
+
run: |
|
|
86
|
+
python -m pip install "numpy==1.23.*" pytest mypy .
|
|
87
|
+
python -m pytest tests
|
|
88
|
+
|
|
89
|
+
publish:
|
|
90
|
+
needs: [build, sdist, numpy-floor]
|
|
91
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
92
|
+
runs-on: ubuntu-24.04
|
|
93
|
+
environment: pypi
|
|
94
|
+
permissions:
|
|
95
|
+
id-token: write
|
|
96
|
+
contents: write
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/download-artifact@v8
|
|
99
|
+
with:
|
|
100
|
+
pattern: wheels-*
|
|
101
|
+
path: dist
|
|
102
|
+
merge-multiple: true
|
|
103
|
+
|
|
104
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
105
|
+
with:
|
|
106
|
+
skip-existing: true
|
|
107
|
+
|
|
108
|
+
- name: Publish GitHub release
|
|
109
|
+
env:
|
|
110
|
+
GH_TOKEN: ${{ github.token }}
|
|
111
|
+
run: |
|
|
112
|
+
gh release create "${GITHUB_REF_NAME}" dist/* \
|
|
113
|
+
--repo "${GITHUB_REPOSITORY}" --generate-notes ||
|
|
114
|
+
gh release upload "${GITHUB_REF_NAME}" dist/* \
|
|
115
|
+
--repo "${GITHUB_REPOSITORY}" --clobber
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This package
|
|
4
|
+
has its own [semantic version](https://semver.org/spec/v2.0.0.html), independent of the
|
|
5
|
+
Clipper2 version it bundles; `clipper2.CLIPPER2_VERSION` tells the latter.
|
|
6
|
+
|
|
7
|
+
## 0.1.0 — 2026-09-19
|
|
8
|
+
|
|
9
|
+
First release. Bundles Clipper2 2.0.1 (upstream tag `Clipper2_2.0.1`).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The complete public Clipper2 API as `clipper2`: the boolean operations (`union`,
|
|
14
|
+
`intersect`, `difference`, `xor`, `boolean_op`, `boolean_op_tree`) and the `Clipper64` /
|
|
15
|
+
`ClipperD` engine classes with open subjects, `execute`, `execute_tree` and
|
|
16
|
+
`ReuseableDataContainer64`.
|
|
17
|
+
- Offsetting: `inflate_paths` and `ClipperOffset`, including upstream's variable-offset
|
|
18
|
+
delta callback.
|
|
19
|
+
- `rect_clip`, `rect_clip_lines` and the `RectClip64` / `RectClipLines64` classes;
|
|
20
|
+
`minkowski_sum` and `minkowski_diff`; `triangulate`.
|
|
21
|
+
- The read-only `PolyPath64` / `PolyPathD` tree, `Rect64` / `RectD`, the enums, and the
|
|
22
|
+
geometry helpers of `clipper.core.h` and `clipper.h` (`area`, `get_bounds`,
|
|
23
|
+
`point_in_polygon`, `simplify_path`, `ramer_douglas_peucker`, `ellipse`, `distance`,
|
|
24
|
+
`length`, `make_path`, …).
|
|
25
|
+
- `clipper2.z`: the same library built with `USINGZ`, with N×3 paths and a z callback that
|
|
26
|
+
returns the z of each new point.
|
|
27
|
+
- numpy arrays in and out: a path is an N×2 array, paths are a list of them, and the
|
|
28
|
+
input's dtype picks upstream's 64 or D family.
|
|
29
|
+
- `Clipper2Error` for `Clipper2Lib::Clipper2Exception`, with upstream's message.
|
|
30
|
+
- Type stubs and a `py.typed` marker.
|
|
31
|
+
- Wheels for Linux x86_64 and aarch64 (manylinux_2_28), macOS x86_64 and arm64, and
|
|
32
|
+
Windows AMD64, on CPython 3.10–3.14.
|
|
33
|
+
|
|
34
|
+
### Known issues
|
|
35
|
+
|
|
36
|
+
- Upstream's `Triangulate` does not return, or exhausts memory, on some degenerate inputs;
|
|
37
|
+
the binding cannot guard against it
|
|
38
|
+
([#2](https://github.com/Maksim-Burtsev/clipper2-py/issues/2)). Upstream's crash in
|
|
39
|
+
`ClipperOffset` on an empty path with an open end type is guarded: such paths are dropped
|
|
40
|
+
([#1](https://github.com/Maksim-Burtsev/clipper2-py/issues/1)).
|
|
41
|
+
- `clipper.export.h`, upstream's C ABI for DLL users, is not bound. See [DEVIATIONS.md](DEVIATIONS.md)
|
|
42
|
+
and [docs/api-map.md](docs/api-map.md).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.15)
|
|
2
|
+
project(clipper2_py LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
|
|
7
|
+
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
|
|
8
|
+
|
|
9
|
+
set(CLIPPER2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/third_party/Clipper2/CPP/Clipper2Lib")
|
|
10
|
+
if(NOT EXISTS "${CLIPPER2_DIR}/src/clipper.engine.cpp")
|
|
11
|
+
message(FATAL_ERROR "third_party/Clipper2 is empty: run git submodule update --init")
|
|
12
|
+
endif()
|
|
13
|
+
# Upstream sources are compiled straight into each extension: the Z build changes the Point
|
|
14
|
+
# layout, so the two extensions cannot share one library.
|
|
15
|
+
set(CLIPPER2_SOURCES
|
|
16
|
+
"${CLIPPER2_DIR}/src/clipper.engine.cpp"
|
|
17
|
+
"${CLIPPER2_DIR}/src/clipper.offset.cpp"
|
|
18
|
+
"${CLIPPER2_DIR}/src/clipper.rectclip.cpp"
|
|
19
|
+
"${CLIPPER2_DIR}/src/clipper.triangulation.cpp")
|
|
20
|
+
set(BINDING_SOURCES
|
|
21
|
+
src/cpp/module.cpp
|
|
22
|
+
src/cpp/bind_core.cpp
|
|
23
|
+
src/cpp/bind_engine.cpp
|
|
24
|
+
src/cpp/bind_offset.cpp
|
|
25
|
+
src/cpp/bind_misc.cpp)
|
|
26
|
+
|
|
27
|
+
set(PYBIND11_FINDPYTHON ON)
|
|
28
|
+
find_package(pybind11 CONFIG REQUIRED)
|
|
29
|
+
|
|
30
|
+
foreach(mod _clipper2 _clipper2z)
|
|
31
|
+
pybind11_add_module(${mod} ${BINDING_SOURCES} ${CLIPPER2_SOURCES})
|
|
32
|
+
target_include_directories(${mod} PRIVATE "${CLIPPER2_DIR}/include" src/cpp)
|
|
33
|
+
target_compile_definitions(${mod} PRIVATE CLIPPER2_PY_MODULE_NAME=${mod})
|
|
34
|
+
install(TARGETS ${mod} DESTINATION clipper2)
|
|
35
|
+
endforeach()
|
|
36
|
+
target_compile_definitions(_clipper2z PRIVATE USINGZ)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Deviations
|
|
2
|
+
|
|
3
|
+
The algorithms are upstream Clipper2's, untouched: the same input produces the same result
|
|
4
|
+
as the C++ library. What follows is everything a C++ user would notice when moving to
|
|
5
|
+
Python. Nothing here changes what the library computes.
|
|
6
|
+
|
|
7
|
+
`docs/api-map.md` lists every upstream symbol with its Python name or the reason it is not
|
|
8
|
+
bound.
|
|
9
|
+
|
|
10
|
+
## 1. Names
|
|
11
|
+
|
|
12
|
+
`InflatePaths` → `inflate_paths`, `Clipper64.AddSubject` → `add_subject`. Getter/setter
|
|
13
|
+
pairs such as `MiterLimit()` / `MiterLimit(v)` are properties (`miter_limit`); a plain
|
|
14
|
+
getter stays a method (`is_valid()`, `mid_point()`), except on the `PolyPath` tree, where
|
|
15
|
+
`level`, `is_hole`, `parent`, `polygon`, `scale` are attributes and `area()` is a method.
|
|
16
|
+
Class names keep their CamelCase, enum members are UPPER_SNAKE (`FillRule.NON_ZERO`).
|
|
17
|
+
|
|
18
|
+
Keyword-argument names are upstream's parameter names, snake_cased. Upstream is not always
|
|
19
|
+
consistent, and neither are we: `boolean_op(..., precision=...)` but
|
|
20
|
+
`intersect(..., decimal_prec=...)`, `Clipper64.execute(clip_type=..., fill_rule=...)` but
|
|
21
|
+
`boolean_op(cliptype=..., fillrule=...)`, `minkowski_sum(..., decimal_places=...)` but
|
|
22
|
+
`triangulate(..., dec_places=...)`, and `ClipperOffset.add_path(path, jt_, et_)` with the
|
|
23
|
+
trailing underscores upstream's parameters carry.
|
|
24
|
+
|
|
25
|
+
## 2. Points, paths and the two families
|
|
26
|
+
|
|
27
|
+
A point goes in as any pair `(x, y)` and comes back as a tuple. A path goes in as any
|
|
28
|
+
sequence of points or an N×2 array and comes back as an N×2 numpy array (`int64` for the 64
|
|
29
|
+
family, `float64` for D); paths come back as a list of such arrays.
|
|
30
|
+
|
|
31
|
+
Which family runs is decided by the dtype `numpy.asarray` infers for the point and path
|
|
32
|
+
arguments: integer → 64, float → D. Mixing families in one call is a `TypeError` (in C++ it
|
|
33
|
+
would not compile). An empty input has no dtype: it takes the family of the other
|
|
34
|
+
arguments, and the 64 family if there are none. An integer array is accepted by a D
|
|
35
|
+
function (C++ converts `int` to `double` implicitly); a float array is refused by a 64
|
|
36
|
+
function.
|
|
37
|
+
|
|
38
|
+
C++ picks between `Area(Path)` and `Area(Paths)` by static type. Python has only the shape
|
|
39
|
+
of the argument, so `area`, `get_bounds`, `ramer_douglas_peucker`, `strip_near_equal`,
|
|
40
|
+
`strip_duplicates`, `rect_clip` and `rect_clip_lines` choose by nesting depth: a sequence of
|
|
41
|
+
points is a path, a sequence of those is paths. An empty sequence cannot be a point, so it is
|
|
42
|
+
an empty path: `[[]]` is paths holding one empty path, and a bare `[]` is an empty path.
|
|
43
|
+
|
|
44
|
+
A generator, a `map` object or any other one-shot iterator is accepted wherever a point, a
|
|
45
|
+
path or paths are: the binding materialises it once on the way in, at every level (a list
|
|
46
|
+
of generators of points works too). C++ takes a container; here the argument is walked more
|
|
47
|
+
than once — to pick the family and then to convert it — and an iterator would be empty the
|
|
48
|
+
second time. Strings and bytes are still refused.
|
|
49
|
+
|
|
50
|
+
`rect_clip` and `rect_clip_lines` take the family from the rect as well, because that is what
|
|
51
|
+
picks the overload in C++: a `Rect64` with float paths, or a `RectD` with integer ones, is a
|
|
52
|
+
`TypeError`. `ClipperOffset` and the `RectClip64` / `RectClipLines64` classes are 64-only
|
|
53
|
+
upstream, so float input is a `TypeError` there too.
|
|
54
|
+
|
|
55
|
+
## 3. Validation the C++ compiler does for free
|
|
56
|
+
|
|
57
|
+
* A wrong shape is a `ValueError`.
|
|
58
|
+
* An integer that does not fit `int64` is an `OverflowError`; a `uint64` array above
|
|
59
|
+
`INT64_MAX` too. Nothing is ever truncated silently. Which family runs is decided from
|
|
60
|
+
the Python objects, not from the array numpy would make of them: `[(0, 0), (2**63, 0)]`
|
|
61
|
+
is 64-family input whose coordinate overflows, not float64 input. (An `int` and a `float`
|
|
62
|
+
*mixed* in one argument is still what numpy makes of it: the D family.)
|
|
63
|
+
* Anything that is not an integer or float dtype is a `TypeError` — including `bool`
|
|
64
|
+
arrays, string arrays and numpy `object` arrays. A Python `bool` is never a coordinate
|
|
65
|
+
either, wherever it sits: `distance(True, (1, 2))` and `area([(True, 2), (3, 4), (5, 9)])`
|
|
66
|
+
are `TypeError`s, although numpy would quietly make them 1 and 0.
|
|
67
|
+
* A float where C++ takes `int64_t` (`translate_path(path, 0.5, 0)` on the 64 family) is a
|
|
68
|
+
`TypeError`; an int where C++ takes `double` is fine.
|
|
69
|
+
* An index outside a `PolyPath`'s children raises `IndexError`; C++ has undefined behaviour
|
|
70
|
+
there. Negative indices are not accepted: upstream has no such concept.
|
|
71
|
+
|
|
72
|
+
## 4. Out-parameters became return values
|
|
73
|
+
|
|
74
|
+
* `Clipper64.execute(clip_type, fill_rule)` returns `(closed_paths, open_paths)`;
|
|
75
|
+
`execute_tree` returns `(tree, open_paths)`. Same for `ClipperD`.
|
|
76
|
+
* The `BooleanOp` overload that fills a `PolyTree` is `boolean_op_tree`.
|
|
77
|
+
* `ClipperOffset.execute(delta)` returns the paths and `execute_tree(delta)` the tree. The
|
|
78
|
+
`Execute` overload that takes a `DeltaCallback64` is `execute(callable)`: as upstream, it
|
|
79
|
+
offsets with a delta of 1.0 and leaves the callback set on the object afterwards.
|
|
80
|
+
* `triangulate(...)` returns `(TriangulateResult, paths)`.
|
|
81
|
+
* `get_line_intersect_pt(...)` returns the point, or `None` where upstream returns `false`
|
|
82
|
+
and leaves its out-parameter untouched.
|
|
83
|
+
* `strip_duplicates(path, is_closed_path)` returns the stripped path; C++ edits its
|
|
84
|
+
argument in place.
|
|
85
|
+
* Upstream's `Execute` returning `false` (an internal inconsistency it reports through the
|
|
86
|
+
return value) raises `Clipper2Error` with upstream's own "There is an undefined error in
|
|
87
|
+
Clipper2" text.
|
|
88
|
+
|
|
89
|
+
## 5. `precision` / `decimal_prec`
|
|
90
|
+
|
|
91
|
+
Only the D family has these parameters upstream, so they default to `None`: passing one to
|
|
92
|
+
a 64-family call is a `TypeError`. `trim_collinear` (`precision`) and `triangulate`
|
|
93
|
+
(`dec_places`) are the two whose D overload has no default upstream, so they require the
|
|
94
|
+
argument for float input.
|
|
95
|
+
|
|
96
|
+
In three functions upstream's D overload inserts this parameter in the middle of the list:
|
|
97
|
+
`InflatePaths(…, miter_limit, precision, arc_tolerance)`, `TrimCollinear(path, precision,
|
|
98
|
+
is_open_path)`, `Triangulate(pp, decPlaces, solution, useDelaunay)`. One Python function
|
|
99
|
+
serves both families, so a call written by position would change meaning with the input's
|
|
100
|
+
dtype. There the parameter and everything after it are keyword-only:
|
|
101
|
+
`inflate_paths(paths, delta, jt, et, miter_limit=2.0, *, precision=None, arc_tolerance=0.0)`,
|
|
102
|
+
`trim_collinear(path, *, precision=None, is_open_path=False)`,
|
|
103
|
+
`triangulate(pp, *, dec_places=None, use_delaunay=True)`.
|
|
104
|
+
|
|
105
|
+
## 6. Errors
|
|
106
|
+
|
|
107
|
+
`Clipper2Lib::Clipper2Exception` becomes `clipper2.Clipper2Error` (a subclass of
|
|
108
|
+
`Exception`) with upstream's message. Where C++ silently returns an empty result, so do we.
|
|
109
|
+
`clipper2.z.Clipper2Error` is the very same class, as are the enums, `Rect64` and `RectD`.
|
|
110
|
+
|
|
111
|
+
## 7. The z build
|
|
112
|
+
|
|
113
|
+
`clipper2.z` is the same library compiled with `USINGZ`. Points are `(x, y, z)` and paths
|
|
114
|
+
are N×3 arrays. Upstream's `z` is `int64_t` in **both** families (`z_type` in
|
|
115
|
+
clipper.core.h), so in the D family the third column is an integer value carried in a
|
|
116
|
+
`float64` array, and a fractional z on the way in is truncated towards zero — exactly what
|
|
117
|
+
upstream's own `MakePathZD` does. An N×2 path is also accepted and gets z = 0, which is
|
|
118
|
+
what upstream's `Point` constructor does.
|
|
119
|
+
|
|
120
|
+
Because that column is a `float64`, a D-family z on the way **in** is checked where C++
|
|
121
|
+
would only `static_cast`: a `nan` or an infinity is a `ValueError`, a value outside `int64`
|
|
122
|
+
an `OverflowError` (both are undefined behaviour in C++). On the way **out** nothing is
|
|
123
|
+
checked, and a z with |z| > 2⁵³ cannot be represented exactly in the float64 column of a D
|
|
124
|
+
path: the array rounds it. A point returned as a tuple carries the exact integer.
|
|
125
|
+
|
|
126
|
+
`Rect64` and `RectD` are among the types registered once, in the non-USINGZ extension
|
|
127
|
+
(deviation 6), so in `clipper2.z` they stay two-dimensional: `mid_point()` returns
|
|
128
|
+
`(x, y)`, `as_path()` an N×2 array, and `contains(pt)` wants a plain `(x, y)` — a
|
|
129
|
+
three-element point is a `ValueError` there. Everything compiled into both modules
|
|
130
|
+
(`clipper2.z.mid_point`, `clipper2.z.ellipse`, …) does use three-element points.
|
|
131
|
+
|
|
132
|
+
`set_z_callback(fn)` takes `fn(e1bot, e1top, e2bot, e2top, pt)` and uses its **return
|
|
133
|
+
value** as the z of `pt`; upstream's callback writes `pt.z` through a reference, which
|
|
134
|
+
Python cannot do. `set_z_callback(None)` removes the callback.
|
|
135
|
+
|
|
136
|
+
Objects must not be passed between `clipper2` and `clipper2.z`: the two builds lay points
|
|
137
|
+
out differently, so a `clipper2.PolyPath64` is not a `clipper2.z.PolyPath64`. Where such an
|
|
138
|
+
object is a function argument (`poly_tree_to_paths64`, `poly_tree_to_paths_d`,
|
|
139
|
+
`check_polytree_fully_contains_children`, `add_reuseable_data`) the binding checks and
|
|
140
|
+
raises `TypeError`. Reaching an unbound method of one module's class with the other
|
|
141
|
+
module's instance (`clipper2.Clipper64.clear(z_clipper)`) is not guarded.
|
|
142
|
+
|
|
143
|
+
## 8. Callbacks and the GIL
|
|
144
|
+
|
|
145
|
+
The GIL is released while upstream code runs and re-acquired for callbacks, so two threads
|
|
146
|
+
clip in parallel. An exception raised inside a callback — a z callback, or `ClipperOffset`'s
|
|
147
|
+
delta callback — propagates out of `execute()`, with its original type, message and
|
|
148
|
+
traceback. It is **not** allowed to unwind through upstream's C++, which would skip the
|
|
149
|
+
clean-up `Execute` ends with (`ClipperOffset::Execute(double, PolyTree64&)` deletes the
|
|
150
|
+
solution it allocated, and leaked it that way). The binding holds the exception instead:
|
|
151
|
+
the first one wins, the callback is not called again for the rest of that `execute`, the
|
|
152
|
+
callback returns a neutral value to upstream (the z callback leaves `pt.z` as upstream set
|
|
153
|
+
it, the delta callback offsets by 0), and once `Execute` has returned and cleaned up after
|
|
154
|
+
itself the exception is re-raised and no result is returned. The object is left as after a
|
|
155
|
+
normal `execute` — usable, with its paths still added — and every object holds its own
|
|
156
|
+
pending exception, so two threads may fail independently.
|
|
157
|
+
|
|
158
|
+
## 9. A clipper that is executing is off limits
|
|
159
|
+
|
|
160
|
+
While `Clipper64`, `ClipperD` or `ClipperOffset` is inside `execute` / `execute_tree`, any
|
|
161
|
+
other call on that **same** object that would mutate or execute it — `add_*`, `clear`,
|
|
162
|
+
`execute*`, `set_z_callback`, `set_delta_callback`, `add_reuseable_data`, a property setter,
|
|
163
|
+
`default_z` — raises `RuntimeError("<ClassName> is executing")`, whether it comes from a
|
|
164
|
+
callback or from another thread (the GIL is released, so another thread can get that far).
|
|
165
|
+
C++ has undefined behaviour here: upstream's engine is walking the very structures such a
|
|
166
|
+
call would rebuild. Reading a property is still allowed, and a different object is never
|
|
167
|
+
affected.
|
|
168
|
+
|
|
169
|
+
`add_reuseable_data` keeps a reference to the container until the clipper's `clear()` or
|
|
170
|
+
its death, because upstream keeps raw `Vertex` pointers into it; in C++ keeping it alive is
|
|
171
|
+
the caller's job.
|
|
172
|
+
|
|
173
|
+
`ClipperOffset`'s delta callback is upstream's `DeltaCallback64`:
|
|
174
|
+
`fn(path, path_normals, curr_idx, prev_idx)` returns the delta to use, with `path` as an
|
|
175
|
+
int64 array and `path_normals` as a float64 one.
|
|
176
|
+
|
|
177
|
+
## 10. One guard against an upstream crash
|
|
178
|
+
|
|
179
|
+
Clipper2 2.0.1 (and its main branch) reads `path.size() - 1` of an **empty path** when the
|
|
180
|
+
end type is an open one (`JOINED`, `BUTT`, `SQUARE`, `ROUND`) and crashes the process. From
|
|
181
|
+
Python that would take the interpreter down, so `inflate_paths`, `ClipperOffset.add_path`
|
|
182
|
+
and `add_paths` drop empty paths before upstream sees them when the end type is an open
|
|
183
|
+
one. An empty path has nothing to offset, and with `EndType.POLYGON` upstream itself
|
|
184
|
+
produces nothing for it, so no result changes. This is the only place where the binding
|
|
185
|
+
steps in front of upstream's code
|
|
186
|
+
([#1](https://github.com/Maksim-Burtsev/clipper2-py/issues/1)).
|
|
187
|
+
|
|
188
|
+
## 11. What is not bound
|
|
189
|
+
|
|
190
|
+
`clipper.export.h` (a C ABI for DLL users) and, per the coverage table in
|
|
191
|
+
`docs/api-map.md`, upstream's C++-only plumbing: the type-conversion templates
|
|
192
|
+
(`ScalePath`, `TransformPaths`, …) — use the D functions' `precision` or numpy's `astype` —
|
|
193
|
+
the engine-internal structs, and the scalar helpers whose Python equivalent is exact
|
|
194
|
+
(`Sqr` is `x * x`, `ProductsAreEqual` is `a * b == c * d` on Python's arbitrary-precision
|
|
195
|
+
ints).
|
|
196
|
+
|
|
197
|
+
`MakePath` and `MakePathD` are bound because upstream's documentation uses them; templates
|
|
198
|
+
are instantiated for the 64 and D families only.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maksim Burtsev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clipper2-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for Clipper2: polygon clipping, offsetting and triangulation
|
|
5
|
+
Keywords: clipper,clipper2,polygon,clipping,offsetting,boolean,geometry,triangulation,minkowski,cam,gis
|
|
6
|
+
Author-Email: Maksim Burtsev <mburtsev17@gmail.com>
|
|
7
|
+
License-Expression: MIT AND BSL-1.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: third_party/Clipper2/LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: C++
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Project-URL: Homepage, https://github.com/Maksim-Burtsev/clipper2-py
|
|
25
|
+
Project-URL: Issues, https://github.com/Maksim-Burtsev/clipper2-py/issues
|
|
26
|
+
Project-URL: Upstream, https://github.com/AngusJohnson/Clipper2
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: numpy>=1.23
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest; extra == "test"
|
|
31
|
+
Requires-Dist: mypy; extra == "test"
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# clipper2-py
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/clipper2-py/)
|
|
37
|
+
[](https://pypi.org/project/clipper2-py/)
|
|
38
|
+
[](https://github.com/Maksim-Burtsev/clipper2-py/actions/workflows/wheels.yml)
|
|
39
|
+
[](https://github.com/Maksim-Burtsev/clipper2-py/blob/main/LICENSE)
|
|
40
|
+
|
|
41
|
+
Python bindings for [Clipper2](https://github.com/AngusJohnson/Clipper2), Angus Johnson's
|
|
42
|
+
C++ library for polygon clipping, offsetting and triangulation. Unofficial; the whole C++
|
|
43
|
+
API, numpy in and out.
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import clipper2
|
|
49
|
+
|
|
50
|
+
subject = [[(0, 0), (100, 0), (100, 100), (0, 100)]]
|
|
51
|
+
clip = [[(50, 50), (150, 50), (150, 150), (50, 150)]]
|
|
52
|
+
|
|
53
|
+
solution = clipper2.intersect(subject, clip, clipper2.FillRule.NON_ZERO)
|
|
54
|
+
print(solution[0].tolist())
|
|
55
|
+
# -> [[100, 100], [50, 100], [50, 50], [100, 50]]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install clipper2-py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Wheels for Linux (x86_64, aarch64), macOS (x86_64, arm64) and Windows (AMD64),
|
|
63
|
+
CPython 3.10–3.14. Clipper2 is compiled in; numpy is the only dependency.
|
|
64
|
+
|
|
65
|
+
## Offsetting
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from clipper2 import EndType, JoinType
|
|
71
|
+
|
|
72
|
+
square = [[(0, 0), (100, 0), (100, 100), (0, 100)]]
|
|
73
|
+
grown = clipper2.inflate_paths(square, 10, JoinType.MITER, EndType.POLYGON)
|
|
74
|
+
print(grown[0].tolist())
|
|
75
|
+
# -> [[110, 110], [-10, 110], [-10, -10], [110, -10]]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Open paths become strokes with `EndType.BUTT`, `SQUARE` or `ROUND`. `ClipperOffset` is
|
|
79
|
+
the class behind it, with per-vertex deltas through a callback.
|
|
80
|
+
|
|
81
|
+
## Triangulation, Minkowski sums, rectangle clipping
|
|
82
|
+
|
|
83
|
+

|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
result, triangles = clipper2.triangulate(square)
|
|
87
|
+
print(result.name, len(triangles))
|
|
88
|
+
# -> SUCCESS 2
|
|
89
|
+
|
|
90
|
+
rect = clipper2.Rect64(50, 50, 200, 200)
|
|
91
|
+
print(clipper2.rect_clip(rect, square)[0].tolist())
|
|
92
|
+
# -> [[50, 50], [100, 50], [100, 100], [50, 100]]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Integers or floats
|
|
96
|
+
|
|
97
|
+
Clipper2 has two families of every function: `Paths64` on `int64` and `PathsD` on
|
|
98
|
+
`double`. Here there is one function, and the input picks the family:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
print(clipper2.union([[(0, 0), (10, 0), (10, 10)]], clipper2.FillRule.NON_ZERO)[0].dtype)
|
|
102
|
+
# -> int64
|
|
103
|
+
print(clipper2.union([[(0, 0), (10, 0), (10.5, 10)]], clipper2.FillRule.NON_ZERO)[0].dtype)
|
|
104
|
+
# -> float64
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Paths go in as lists of pairs or N×2 numpy arrays and come back as numpy arrays.
|
|
108
|
+
|
|
109
|
+
## The same library, not a lookalike
|
|
110
|
+
|
|
111
|
+
- **Names are upstream's, in snake_case**: `InflatePaths` → `inflate_paths`,
|
|
112
|
+
`Clipper64.AddSubject` → `add_subject`, `FillRule::NonZero` → `FillRule.NON_ZERO`.
|
|
113
|
+
[Every C++ symbol and its Python name](https://github.com/Maksim-Burtsev/clipper2-py/blob/main/docs/api-map.md).
|
|
114
|
+
- **Results are upstream's, bit for bit.** 13 500 random cases are compared with a C++
|
|
115
|
+
reference program on every platform, for every wheel, and must be exactly equal —
|
|
116
|
+
including the `USINGZ` build, which is `clipper2.z`. Upstream's own test suite is ported
|
|
117
|
+
one to one.
|
|
118
|
+
- **Every difference from C++ is written down** in [DEVIATIONS.md](https://github.com/Maksim-Burtsev/clipper2-py/blob/main/DEVIATIONS.md): out
|
|
119
|
+
parameters became return values, errors became exceptions, and where C++ has undefined
|
|
120
|
+
behaviour Python raises.
|
|
121
|
+
|
|
122
|
+
More examples — `Clipper64` with open paths and the polytree, offset callbacks, the z
|
|
123
|
+
module — are in the [guide](https://github.com/Maksim-Burtsev/clipper2-py/blob/main/docs/guide.md).
|
|
124
|
+
|
|
125
|
+
## Benchmarks
|
|
126
|
+
|
|
127
|
+
Apple M4, CPython 3.13, best of 5, conversion from numpy included:
|
|
128
|
+
|
|
129
|
+
| | clipper2-py | pyclipr | pyclipper | shapely |
|
|
130
|
+
| --- | --- | --- | --- | --- |
|
|
131
|
+
| union of 1000 polygons | 34.4 ms | 35.7 ms | 71.2 ms | **11.4 ms** |
|
|
132
|
+
| intersection, 10⁵ vertices | **5.25 ms** | 5.46 ms | 30.5 ms | 9.51 ms |
|
|
133
|
+
| round offset, 1000 vertices | 0.36 ms | 0.39 ms | 1.46 ms | **0.24 ms** |
|
|
134
|
+
| one small call | **1.14 µs** | 2.43 µs | 2.31 µs | 13.9 µs |
|
|
135
|
+
|
|
136
|
+
pyclipr wraps the same Clipper2 and is as fast, but ships no Linux wheels and has its own
|
|
137
|
+
API. pyclipper is Clipper1. shapely (GEOS) wins on big unions and on buffering.
|
|
138
|
+
[Method and full results](https://github.com/Maksim-Burtsev/clipper2-py/blob/main/benchmarks/RESULTS.md).
|
|
139
|
+
|
|
140
|
+
## Known upstream issue
|
|
141
|
+
|
|
142
|
+
`triangulate` may never return, or may exhaust memory, on paths with **duplicate or
|
|
143
|
+
self-intersecting vertices** — a bug of Clipper2 2.0.1 that is also in its main branch, and
|
|
144
|
+
which inputs trigger it depends on the platform
|
|
145
|
+
([#2](https://github.com/Maksim-Burtsev/clipper2-py/issues/2)). Upstream documents that
|
|
146
|
+
`Triangulate` does not accept intersecting paths; passing the input through `union` first
|
|
147
|
+
makes it safe. The binding changes nothing in upstream's algorithms, so it cannot hide this.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT for the binding; Clipper2 is under the Boost Software License 1.0. Both texts ship in
|
|
152
|
+
every wheel. `clipper2.CLIPPER2_VERSION` tells which Clipper2 is inside (2.0.1).
|