Skip to content

Commit 5627b28

Browse files
Merge pull request #5 from olly-writes-code/extending-the-bindings
Extending the bindings
2 parents a941c62 + 0212f98 commit 5627b28

File tree

8 files changed

+201
-46
lines changed

8 files changed

+201
-46
lines changed

justfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
build:
2+
meson setup builddir --wipe
3+
4+
compile:
5+
meson compile -C builddir
6+
7+
install:
8+
uv pip install -e .
9+
10+
test:
11+
uv run pytest
12+
13+
refresh-and-test:
14+
just compile
15+
just install
16+
just test

meson.build

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
project(
22
'pyclipper2',
33
'cpp',
4-
version: '0.0.2',
4+
version: '0.0.3',
55
meson_version: '>=1.0.0',
66
default_options: ['cpp_std=c++17', 'b_ndebug=if-release'],
77
)

pyclipper2.code-workspace

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"folders": [
3+
{
4+
"path": "."
5+
}
6+
],
7+
"settings": {
8+
// ensures we have basic type checking for pylance
9+
"python.analysis.typeCheckingMode": "basic",
10+
"files.exclude": {
11+
"**/__pycache__/**": true,
12+
},
13+
"[python]": {
14+
"editor.formatOnSave": true,
15+
"editor.codeActionsOnSave": {
16+
"source.fixAll": "explicit",
17+
"source.organizeImports": "explicit"
18+
},
19+
"editor.defaultFormatter": "charliermarsh.ruff",
20+
},
21+
"notebook.formatOnSave.enabled": true,
22+
"C_Cpp.default.compileCommands": "/Users/olammas/Documents/projects/pyclipper2/builddir/compile_commands.json",
23+
"C_Cpp.default.configurationProvider": "mesonbuild.mesonbuild"
24+
},
25+
"extensions": {
26+
"recommendations": [
27+
// Python tools
28+
"ms-python.python",
29+
"ms-python.vscode-pylance",
30+
"ms-toolsai.jupyter",
31+
"charliermarsh.ruff",
32+
// Developer tools
33+
"eamodio.gitlens",
34+
"nefrob.vscode-just-syntax",
35+
"tamasfe.even-better-toml",
36+
"usernamehw.errorlens",
37+
]
38+
}
39+
}

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,14 @@ build-backend = 'mesonpy'
99

1010
[dependency-groups]
1111
dev = [
12+
"nanobind>=2.9.2", ### we add nanobind here for stub generation
1213
"pytest>=8.4.2",
1314
]
15+
16+
[tool.pytest.ini_options]
17+
testpaths = [
18+
"tests",
19+
]
20+
21+
[tool.pyright]
22+
exclude = ["subprojects", ".venv"]

src/bindings.cpp

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,44 @@ NB_MODULE(pyclipper2, m) {
1313

1414
// Enums
1515
nb::enum_<ClipType>(m, "ClipType")
16-
.value("NoClip", ClipType::NoClip)
17-
.value("Intersection", ClipType::Intersection)
18-
.value("Union", ClipType::Union)
19-
.value("Difference", ClipType::Difference)
20-
.value("Xor", ClipType::Xor);
16+
.value("NO_CLIP", ClipType::NoClip)
17+
.value("INTERSECTION", ClipType::Intersection)
18+
.value("UNION", ClipType::Union)
19+
.value("DIFFERENCE", ClipType::Difference)
20+
.value("XOR", ClipType::Xor);
2121

2222
nb::enum_<FillRule>(m, "FillRule")
23-
.value("EvenOdd", FillRule::EvenOdd)
24-
.value("NonZero", FillRule::NonZero)
25-
.value("Positive", FillRule::Positive)
26-
.value("Negative", FillRule::Negative);
23+
.value("EVEN_ODD", FillRule::EvenOdd)
24+
.value("NON_ZERO", FillRule::NonZero)
25+
.value("POSITIVE", FillRule::Positive)
26+
.value("NEGATIVE", FillRule::Negative);
2727

2828
nb::enum_<JoinType>(m, "JoinType")
29-
.value("Square", JoinType::Square)
30-
.value("Bevel", JoinType::Bevel)
31-
.value("Round", JoinType::Round)
32-
.value("Miter", JoinType::Miter);
29+
.value("SQUARE", JoinType::Square)
30+
.value("BEVEL", JoinType::Bevel)
31+
.value("ROUND", JoinType::Round)
32+
.value("MITER", JoinType::Miter);
3333

3434
nb::enum_<EndType>(m, "EndType")
35-
.value("Polygon", EndType::Polygon)
36-
.value("Joined", EndType::Joined)
37-
.value("Butt", EndType::Butt)
38-
.value("Square", EndType::Square)
39-
.value("Round", EndType::Round);
35+
.value("POLYGON", EndType::Polygon)
36+
.value("JOINED", EndType::Joined)
37+
.value("BUTT", EndType::Butt)
38+
.value("SQUARE", EndType::Square)
39+
.value("ROUND", EndType::Round);
40+
41+
nb::enum_<PathType>(m, "PathType")
42+
.value("SUBJECT", PathType::Subject)
43+
.value("CLIP", PathType::Clip);
44+
45+
nb::enum_<JoinWith>(m, "JoinWith")
46+
.value("NO_JOIN", JoinWith::NoJoin)
47+
.value("LEFT", JoinWith::Left)
48+
.value("RIGHT", JoinWith::Right);
49+
50+
nb::enum_<PointInPolygonResult>(m, "PointInPolygonResult")
51+
.value("IS_ON", PointInPolygonResult::IsOn)
52+
.value("IS_INSIDE", PointInPolygonResult::IsInside)
53+
.value("IS_OUTSIDE", PointInPolygonResult::IsOutside);
4054

4155
// Core types - Point64, PointD
4256
nb::class_<Point64>(m, "Point64")
@@ -83,6 +97,8 @@ NB_MODULE(pyclipper2, m) {
8397
.def("clear", &ClipperOffset::Clear);
8498

8599
// Utility functions
100+
101+
// We must write one for Path64 (int64) and one for PathD (double)
86102
m.def("area",
87103
nb::overload_cast<const Path64&>(&Area<int64_t>),
88104
"Calculate area of a path");
@@ -99,6 +115,16 @@ NB_MODULE(pyclipper2, m) {
99115
nb::overload_cast<const PathD&>(&IsPositive<double>),
100116
"Check if path is positively oriented");
101117

118+
m.def("point_in_polygon",
119+
nb::overload_cast<const Point64&, const Path64&>(&PointInPolygon<int64_t>),
120+
"Check if point is in polygon",
121+
nb::arg("pt"), nb::arg("polygon"));
122+
123+
m.def("point_in_polygon",
124+
nb::overload_cast<const PointD&, const PathD&>(&PointInPolygon<double>),
125+
"Check if point is in polygon",
126+
nb::arg("pt"), nb::arg("polygon"));
127+
102128
// Module constants
103129
m.attr("VERSION") = CLIPPER2_VERSION;
104130
}

src/pyclipper2/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
1-
from .bindings import __doc__, Point64, PointD, Rect64, area, is_positive, ClipperOffset, ClipType, FillRule, JoinType, EndType, VERSION
1+
from .bindings import (
2+
VERSION,
3+
ClipperOffset,
4+
ClipType,
5+
EndType,
6+
FillRule,
7+
JoinType,
8+
JoinWith,
9+
PathType,
10+
Point64,
11+
PointD,
12+
PointInPolygonResult,
13+
Rect64,
14+
__doc__,
15+
area,
16+
is_positive,
17+
point_in_polygon,
18+
)

tests/test_bindings.py

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,129 @@
11
import pyclipper2
22

3+
34
def test_point64():
45
"""Test Point64 creation and attributes"""
56
p = pyclipper2.Point64(100, 200)
67
assert p.x == 100
78
assert p.y == 200
89
print(f"✓ Point64: {p}")
9-
10+
11+
1012
def test_pointd():
1113
"""Test PointD creation and attributes"""
1214
p = pyclipper2.PointD(10.5, 20.7)
1315
assert p.x == 10.5
1416
assert p.y == 20.7
1517
print(f"✓ PointD: {p}")
1618

19+
1720
def test_enums():
1821
"""Test enum values"""
19-
assert pyclipper2.ClipType.Union
20-
assert pyclipper2.FillRule.EvenOdd
21-
assert pyclipper2.JoinType.Round
22-
assert pyclipper2.EndType.Polygon
22+
assert pyclipper2.ClipType.UNION
23+
assert pyclipper2.ClipType.DIFFERENCE
24+
assert pyclipper2.ClipType.INTERSECTION
25+
assert pyclipper2.ClipType.XOR
26+
assert pyclipper2.FillRule.NON_ZERO
27+
assert pyclipper2.FillRule.POSITIVE
28+
assert pyclipper2.FillRule.NEGATIVE
29+
assert pyclipper2.FillRule.EVEN_ODD
30+
assert pyclipper2.EndType.POLYGON
31+
assert pyclipper2.EndType.JOINED
32+
assert pyclipper2.EndType.BUTT
33+
assert pyclipper2.EndType.SQUARE
34+
assert pyclipper2.EndType.ROUND
35+
assert pyclipper2.PathType.SUBJECT
36+
assert pyclipper2.PathType.CLIP
37+
assert pyclipper2.JoinWith.NO_JOIN
38+
assert pyclipper2.JoinWith.LEFT
39+
assert pyclipper2.JoinWith.RIGHT
40+
assert pyclipper2.PointInPolygonResult.IS_INSIDE
41+
assert pyclipper2.PointInPolygonResult.IS_OUTSIDE
42+
assert pyclipper2.PointInPolygonResult.IS_ON
43+
assert pyclipper2.JoinType.ROUND
44+
assert pyclipper2.JoinType.SQUARE
45+
assert pyclipper2.JoinType.BEVEL
46+
assert pyclipper2.JoinType.MITER
2347
print("✓ Enums work")
2448

49+
2550
def test_rect64():
2651
"""Test Rect64"""
2752
r = pyclipper2.Rect64(0, 0, 100, 100)
2853
assert r.left == 0
2954
assert r.right == 100
3055
print("✓ Rect64 works")
3156

57+
3258
def test_area():
3359
"""Test area calculation"""
3460
# Create a simple square path
3561
square = [
3662
pyclipper2.Point64(0, 0),
3763
pyclipper2.Point64(100, 0),
3864
pyclipper2.Point64(100, 100),
39-
pyclipper2.Point64(0, 100)
65+
pyclipper2.Point64(0, 100),
4066
]
4167
area = pyclipper2.area(square)
4268
print(f"✓ Area of square: {area}")
4369
assert area != 0 # Should be 10000
4470

71+
4572
def test_is_positive():
4673
"""Test is_positive orientation"""
4774
# Clockwise square (should be negative in typical coordinate systems)
4875
square = [
4976
pyclipper2.Point64(0, 0),
5077
pyclipper2.Point64(100, 0),
5178
pyclipper2.Point64(100, 100),
52-
pyclipper2.Point64(0, 100)
79+
pyclipper2.Point64(0, 100),
5380
]
5481
result = pyclipper2.is_positive(square)
5582
print(f"✓ is_positive: {result}")
5683

84+
5785
def test_clipper_offset():
5886
"""Test ClipperOffset"""
5987
offset = pyclipper2.ClipperOffset()
6088
print("✓ ClipperOffset created")
61-
89+
6290
# Create a simple path
6391
path = [
6492
pyclipper2.Point64(0, 0),
6593
pyclipper2.Point64(100, 0),
6694
pyclipper2.Point64(100, 100),
67-
pyclipper2.Point64(0, 100)
95+
pyclipper2.Point64(0, 100),
6896
]
69-
70-
offset.add_path(path, pyclipper2.JoinType.Round, pyclipper2.EndType.Polygon)
97+
98+
offset.add_path(path, pyclipper2.JoinType.ROUND, pyclipper2.EndType.POLYGON)
7199
print("✓ Path added to offset")
72-
100+
73101
result = []
74102
offset.execute(10.0, result)
75103
print(f"✓ Offset executed, result paths: {len(result)}")
76104

105+
77106
def test_version():
78107
"""Test version constant"""
79108
version = pyclipper2.VERSION
80109
print(f"✓ Clipper2 version: {version}")
81110

82-
if __name__ == "__main__":
83-
print("Testing Clipper2 Python bindings...\n")
84-
85-
test_point64()
86-
test_pointd()
87-
test_enums()
88-
test_rect64()
89-
test_area()
90-
test_is_positive()
91-
test_clipper_offset()
92-
test_version()
93-
94-
print("\n✅ All tests passed!")
111+
112+
def test_point_in_polygon():
113+
"""Test point_in_polygon function"""
114+
polygon = [
115+
pyclipper2.Point64(0, 0),
116+
pyclipper2.Point64(100, 0),
117+
pyclipper2.Point64(100, 100),
118+
pyclipper2.Point64(0, 100),
119+
]
120+
inside_point = pyclipper2.Point64(50, 50)
121+
outside_point = pyclipper2.Point64(150, 150)
122+
123+
inside_result = pyclipper2.point_in_polygon(inside_point, polygon)
124+
outside_result = pyclipper2.point_in_polygon(outside_point, polygon)
125+
126+
print(f"✓ point_in_polygon (inside): {inside_result}")
127+
print(f"✓ point_in_polygon (outside): {outside_result}")
128+
assert inside_result == pyclipper2.PointInPolygonResult.IS_INSIDE
129+
assert outside_result == pyclipper2.PointInPolygonResult.IS_OUTSIDE

uv.lock

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)