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.
Files changed (100) hide show
  1. clipper2_py-0.1.0/.github/workflows/wheels.yml +115 -0
  2. clipper2_py-0.1.0/.gitignore +10 -0
  3. clipper2_py-0.1.0/.gitmodules +3 -0
  4. clipper2_py-0.1.0/CHANGELOG.md +42 -0
  5. clipper2_py-0.1.0/CMakeLists.txt +36 -0
  6. clipper2_py-0.1.0/DEVIATIONS.md +198 -0
  7. clipper2_py-0.1.0/LICENSE +21 -0
  8. clipper2_py-0.1.0/PKG-INFO +152 -0
  9. clipper2_py-0.1.0/README.md +119 -0
  10. clipper2_py-0.1.0/RELEASE_CHECKLIST.md +116 -0
  11. clipper2_py-0.1.0/docs/api-map.md +225 -0
  12. clipper2_py-0.1.0/docs/guide.md +277 -0
  13. clipper2_py-0.1.0/docs/img/booleans.svg +18 -0
  14. clipper2_py-0.1.0/docs/img/make_images.py +87 -0
  15. clipper2_py-0.1.0/docs/img/offsetting.svg +20 -0
  16. clipper2_py-0.1.0/docs/img/triangulate-minkowski.svg +7 -0
  17. clipper2_py-0.1.0/docs/spec.md +130 -0
  18. clipper2_py-0.1.0/pyproject.toml +80 -0
  19. clipper2_py-0.1.0/src/clipper2/__init__.py +10 -0
  20. clipper2_py-0.1.0/src/clipper2/__init__.pyi +599 -0
  21. clipper2_py-0.1.0/src/clipper2/py.typed +0 -0
  22. clipper2_py-0.1.0/src/clipper2/z.py +3 -0
  23. clipper2_py-0.1.0/src/clipper2/z.pyi +463 -0
  24. clipper2_py-0.1.0/src/cpp/bind_core.cpp +735 -0
  25. clipper2_py-0.1.0/src/cpp/bind_engine.cpp +246 -0
  26. clipper2_py-0.1.0/src/cpp/bind_misc.cpp +165 -0
  27. clipper2_py-0.1.0/src/cpp/bind_offset.cpp +187 -0
  28. clipper2_py-0.1.0/src/cpp/convert.h +568 -0
  29. clipper2_py-0.1.0/src/cpp/module.cpp +57 -0
  30. clipper2_py-0.1.0/tests/reference/CMakeLists.txt +32 -0
  31. clipper2_py-0.1.0/tests/reference/ref_cli.cpp +556 -0
  32. clipper2_py-0.1.0/tests/reference/test_differential.py +977 -0
  33. clipper2_py-0.1.0/tests/reference/test_differential_z.py +622 -0
  34. clipper2_py-0.1.0/tests/test_convert.py +332 -0
  35. clipper2_py-0.1.0/tests/test_core.py +567 -0
  36. clipper2_py-0.1.0/tests/test_engine.py +612 -0
  37. clipper2_py-0.1.0/tests/test_leaks.py +162 -0
  38. clipper2_py-0.1.0/tests/test_misc.py +272 -0
  39. clipper2_py-0.1.0/tests/test_offset.py +502 -0
  40. clipper2_py-0.1.0/tests/test_readme.py +28 -0
  41. clipper2_py-0.1.0/tests/test_stubs.py +286 -0
  42. clipper2_py-0.1.0/tests/typing_sample.py +245 -0
  43. clipper2_py-0.1.0/tests/upstream/clip_file_load.py +161 -0
  44. clipper2_py-0.1.0/tests/upstream/test_export_headers.py +13 -0
  45. clipper2_py-0.1.0/tests/upstream/test_is_collinear.py +58 -0
  46. clipper2_py-0.1.0/tests/upstream/test_lines.py +45 -0
  47. clipper2_py-0.1.0/tests/upstream/test_offset_orientation.py +29 -0
  48. clipper2_py-0.1.0/tests/upstream/test_offsets.py +660 -0
  49. clipper2_py-0.1.0/tests/upstream/test_orientation.py +17 -0
  50. clipper2_py-0.1.0/tests/upstream/test_polygons.py +102 -0
  51. clipper2_py-0.1.0/tests/upstream/test_polytree_holes.py +272 -0
  52. clipper2_py-0.1.0/tests/upstream/test_polytree_intersection.py +22 -0
  53. clipper2_py-0.1.0/tests/upstream/test_polytree_union.py +68 -0
  54. clipper2_py-0.1.0/tests/upstream/test_random_paths.py +81 -0
  55. clipper2_py-0.1.0/tests/upstream/test_rect.py +31 -0
  56. clipper2_py-0.1.0/tests/upstream/test_rect_clip.py +72 -0
  57. clipper2_py-0.1.0/tests/upstream/test_simplify_path.py +10 -0
  58. clipper2_py-0.1.0/tests/upstream/test_trim_collinear.py +31 -0
  59. clipper2_py-0.1.0/tests/upstream/test_windows.py +14 -0
  60. clipper2_py-0.1.0/third_party/Clipper2/CPP/CMakeLists.txt +377 -0
  61. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2.pc.cmakein +12 -0
  62. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Config.cmake.in +6 -0
  63. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.core.h +1159 -0
  64. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.engine.h +635 -0
  65. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.export.h +851 -0
  66. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.h +796 -0
  67. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.minkowski.h +117 -0
  68. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.offset.h +125 -0
  69. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.rectclip.h +80 -0
  70. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.triangulation.h +27 -0
  71. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/include/clipper2/clipper.version.h +6 -0
  72. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/src/clipper.engine.cpp +3163 -0
  73. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/src/clipper.offset.cpp +661 -0
  74. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/src/clipper.rectclip.cpp +1027 -0
  75. clipper2_py-0.1.0/third_party/Clipper2/CPP/Clipper2Lib/src/clipper.triangulation.cpp +1219 -0
  76. clipper2_py-0.1.0/third_party/Clipper2/CPP/GoogleTest in Visual Studio.txt +13 -0
  77. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestExportHeaders.cpp +203 -0
  78. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestIsCollinear.cpp +53 -0
  79. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestLines.cpp +55 -0
  80. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestOffsetOrientation.cpp +29 -0
  81. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestOffsets.cpp +700 -0
  82. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestOrientation.cpp +16 -0
  83. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestPolygons.cpp +125 -0
  84. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestPolytreeHoles.cpp +319 -0
  85. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestPolytreeIntersection.cpp +24 -0
  86. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestPolytreeUnion.cpp +79 -0
  87. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestRandomPaths.cpp +83 -0
  88. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestRect.cpp +62 -0
  89. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestRectClip.cpp +76 -0
  90. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestSimplifyPath.cpp +11 -0
  91. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestTrimCollinear.cpp +27 -0
  92. clipper2_py-0.1.0/third_party/Clipper2/CPP/Tests/TestWindows.cpp +5 -0
  93. clipper2_py-0.1.0/third_party/Clipper2/CPP/clipper.version.in +6 -0
  94. clipper2_py-0.1.0/third_party/Clipper2/LICENSE +23 -0
  95. clipper2_py-0.1.0/third_party/Clipper2/README.md +94 -0
  96. clipper2_py-0.1.0/third_party/Clipper2/Tests/Lines.txt +162 -0
  97. clipper2_py-0.1.0/third_party/Clipper2/Tests/Offsets.txt +18 -0
  98. clipper2_py-0.1.0/third_party/Clipper2/Tests/Polygons.txt +1925 -0
  99. clipper2_py-0.1.0/third_party/Clipper2/Tests/PolytreeHoleOwner.txt +13 -0
  100. 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,10 @@
1
+ build/
2
+ dist/
3
+ wheelhouse/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.so
7
+ *.pyd
8
+ .pytest_cache/
9
+ .venv/
10
+ .DS_Store
@@ -0,0 +1,3 @@
1
+ [submodule "third_party/Clipper2"]
2
+ path = third_party/Clipper2
3
+ url = https://github.com/AngusJohnson/Clipper2
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/clipper2-py.svg)](https://pypi.org/project/clipper2-py/)
37
+ [![Python versions](https://img.shields.io/pypi/pyversions/clipper2-py.svg)](https://pypi.org/project/clipper2-py/)
38
+ [![CI](https://github.com/Maksim-Burtsev/clipper2-py/actions/workflows/wheels.yml/badge.svg)](https://github.com/Maksim-Burtsev/clipper2-py/actions/workflows/wheels.yml)
39
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ ![intersect, union, difference, xor](https://raw.githubusercontent.com/Maksim-Burtsev/clipper2-py/main/docs/img/booleans.svg)
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
+ ![round, miter and bevel joins](https://raw.githubusercontent.com/Maksim-Burtsev/clipper2-py/main/docs/img/offsetting.svg)
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
+ ![triangulate and minkowski_sum](https://raw.githubusercontent.com/Maksim-Burtsev/clipper2-py/main/docs/img/triangulate-minkowski.svg)
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).