pypgl 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.
- pypgl-0.1.0/.github/workflows/wheels.yml +92 -0
- pypgl-0.1.0/.gitignore +7 -0
- pypgl-0.1.0/CLAUDE.md +169 -0
- pypgl-0.1.0/CMakeLists.txt +88 -0
- pypgl-0.1.0/LICENSE +21 -0
- pypgl-0.1.0/PKG-INFO +217 -0
- pypgl-0.1.0/README.md +169 -0
- pypgl-0.1.0/ROADMAP.md +246 -0
- pypgl-0.1.0/doc/README.md +32 -0
- pypgl-0.1.0/doc/algorithms.md +58 -0
- pypgl-0.1.0/doc/canvas.md +224 -0
- pypgl-0.1.0/doc/data_structures.md +72 -0
- pypgl-0.1.0/doc/figures/canvas_example_intersection.svg +8 -0
- pypgl-0.1.0/doc/figures/canvas_example_primitives.svg +16 -0
- pypgl-0.1.0/doc/figures/canvas_example_viewport.svg +13 -0
- pypgl-0.1.0/doc/figures/example2.svg +5 -0
- pypgl-0.1.0/doc/figures/example_shapetree_triangles.svg +213 -0
- pypgl-0.1.0/doc/figures/example_triangulation.svg +424 -0
- pypgl-0.1.0/doc/figures/example_triangulation2.svg +932 -0
- pypgl-0.1.0/doc/figures/logo.png +0 -0
- pypgl-0.1.0/doc/figures/logotext.svg +56 -0
- pypgl-0.1.0/doc/figures/logotextdark.svg +56 -0
- pypgl-0.1.0/doc/figures/predicate1.svg +36 -0
- pypgl-0.1.0/doc/figures/predicate2.svg +36 -0
- pypgl-0.1.0/doc/figures/predicate3.svg +36 -0
- pypgl-0.1.0/doc/figures/predicate4.svg +36 -0
- pypgl-0.1.0/doc/figures/predicate5.svg +36 -0
- pypgl-0.1.0/doc/figures/predicate6.svg +36 -0
- pypgl-0.1.0/doc/figures/predicate7.svg +36 -0
- pypgl-0.1.0/doc/figures/predicate8.svg +36 -0
- pypgl-0.1.0/doc/shape_methods.md +226 -0
- pypgl-0.1.0/doc/shapes.md +460 -0
- pypgl-0.1.0/doc/todo.md +60 -0
- pypgl-0.1.0/pypgl/__init__.py +138 -0
- pypgl-0.1.0/pypgl/py.typed +0 -0
- pypgl-0.1.0/pypgl.md +271 -0
- pypgl-0.1.0/pyproject.toml +83 -0
- pypgl-0.1.0/src/bind_canvas.cpp +123 -0
- pypgl-0.1.0/src/bind_disk.cpp +120 -0
- pypgl-0.1.0/src/bind_lines.cpp +135 -0
- pypgl-0.1.0/src/bind_point.cpp +75 -0
- pypgl-0.1.0/src/bind_polygons.cpp +184 -0
- pypgl-0.1.0/src/bind_segment.cpp +88 -0
- pypgl-0.1.0/src/casters.h +144 -0
- pypgl-0.1.0/src/common.h +219 -0
- pypgl-0.1.0/src/module.cpp +21 -0
- pypgl-0.1.0/src/stubgen_patterns.txt +31 -0
- pypgl-0.1.0/tests/test_canvas.py +215 -0
- pypgl-0.1.0/tests/test_core_shapes.py +272 -0
- pypgl-0.1.0/tests/test_disk.py +157 -0
- pypgl-0.1.0/tests/test_exact.py +50 -0
- pypgl-0.1.0/tests/test_intersection.py +40 -0
- pypgl-0.1.0/tests/test_line_helpers.py +97 -0
- pypgl-0.1.0/tests/test_predicates.py +40 -0
- pypgl-0.1.0/tests/test_stubs.py +79 -0
- pypgl-0.1.0/tests/test_transforms.py +135 -0
- pypgl-0.1.0/tests/test_vertex_queries.py +134 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: Build wheels
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: wheels-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build_wheels:
|
|
17
|
+
name: Wheels on ${{ matrix.os }}
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
# Native runners only (no emulation/cross): nanobind_add_stub imports the
|
|
23
|
+
# freshly built _pgl to emit _pgl.pyi, so the build host must run the
|
|
24
|
+
# target binary. macos-14 = arm64 (Apple Silicon).
|
|
25
|
+
# macos-13 (x86_64 Intel) is dropped: GitHub is retiring the Intel
|
|
26
|
+
# runners, so those jobs sit queued for hours and time out. Apple Silicon
|
|
27
|
+
# is covered by macos-14; we don't ship Intel-mac wheels.
|
|
28
|
+
# windows-2022 is pinned (not windows-latest) so the "Visual Studio 17
|
|
29
|
+
# 2022" generator name in [tool.cibuildwheel.windows] stays valid.
|
|
30
|
+
os: [ubuntu-latest, macos-14, windows-2022]
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- name: Build wheels
|
|
35
|
+
uses: pypa/cibuildwheel@v2.21
|
|
36
|
+
# cibuildwheel reads [tool.cibuildwheel] from pyproject.toml.
|
|
37
|
+
|
|
38
|
+
- uses: actions/upload-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: wheels-${{ matrix.os }}
|
|
41
|
+
path: ./wheelhouse/*.whl
|
|
42
|
+
|
|
43
|
+
build_sdist:
|
|
44
|
+
name: Build sdist
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
|
|
49
|
+
- name: Build sdist
|
|
50
|
+
run: pipx run build --sdist
|
|
51
|
+
|
|
52
|
+
- uses: actions/upload-artifact@v4
|
|
53
|
+
with:
|
|
54
|
+
name: sdist
|
|
55
|
+
path: ./dist/*.tar.gz
|
|
56
|
+
|
|
57
|
+
publish-testpypi:
|
|
58
|
+
name: Publish to TestPyPI
|
|
59
|
+
needs: [build_wheels, build_sdist]
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
# Dry run: manual "Run workflow" uploads to test.pypi.org for validation.
|
|
62
|
+
# Uses Trusted Publishing (OIDC) — no API token.
|
|
63
|
+
if: github.event_name == 'workflow_dispatch'
|
|
64
|
+
environment: testpypi
|
|
65
|
+
permissions:
|
|
66
|
+
id-token: write
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
path: dist
|
|
71
|
+
merge-multiple: true
|
|
72
|
+
|
|
73
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
74
|
+
with:
|
|
75
|
+
repository-url: https://test.pypi.org/legacy/
|
|
76
|
+
|
|
77
|
+
publish:
|
|
78
|
+
name: Publish to PyPI
|
|
79
|
+
needs: [build_wheels, build_sdist]
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
# Only on a version tag. Uses PyPI Trusted Publishing (OIDC) — no API token.
|
|
82
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
83
|
+
environment: pypi
|
|
84
|
+
permissions:
|
|
85
|
+
id-token: write
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/download-artifact@v4
|
|
88
|
+
with:
|
|
89
|
+
path: dist
|
|
90
|
+
merge-multiple: true
|
|
91
|
+
|
|
92
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
pypgl-0.1.0/.gitignore
ADDED
pypgl-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project status
|
|
6
|
+
|
|
7
|
+
**Core shapes done** (milestones 1–2 of [pypgl.md](pypgl.md)): all
|
|
8
|
+
shapes are bound — `Point`, `Segment`, `OrientedSegment`,
|
|
9
|
+
`Line`, `OrientedLine`, `Ray`, `Halfplane`, `Triangle`, `Rectangle`, `Convex`,
|
|
10
|
+
`Disk` — each with the full 7-predicate × 10-shape matrix
|
|
11
|
+
(`PGL_BIND_ALL_PREDICATES` in [src/common.h](src/common.h)), constructors,
|
|
12
|
+
accessors/measures, and typed `intersection` results for the 0D/1D-result pairs.
|
|
13
|
+
The two casters work and the exact round-trip / `optional`→`None` /
|
|
14
|
+
`variant`→concrete mappings are verified by the `tests/` suite.
|
|
15
|
+
|
|
16
|
+
**Notebook UX done** (milestone 3): `Canvas`
|
|
17
|
+
([src/bind_canvas.cpp](src/bind_canvas.cpp)) is bound — pgl's stream API
|
|
18
|
+
(`canvas << pgl::stroke("red") << shape`) does not map to Python, so each stream
|
|
19
|
+
operation is re-exposed as a method: fluent `scale`/`width`/`height`/`size`/
|
|
20
|
+
`margin`/`pointRadius`/`borders` and the numeric `strokeWidth` configuration;
|
|
21
|
+
fluent `stroke`/`fill`/`fillOpacity`/`strokeOpacity` style commands applied to the
|
|
22
|
+
*current* style (so only shapes drawn afterwards capture it); one `draw(shape)`
|
|
23
|
+
overload per bound shape; and `toSVG()`/`writeSVG(path)`. The fluent self-returns
|
|
24
|
+
use `nb::rv_policy::reference_internal`. `_repr_svg_` is added Python-side in
|
|
25
|
+
[pypgl/__init__.py](pypgl/__init__.py) — on the canvas it returns `toSVG()`, on
|
|
26
|
+
every shape it renders a one-shot `Canvas().draw(self)` — so shapes and canvases
|
|
27
|
+
display inline in Jupyter.
|
|
28
|
+
|
|
29
|
+
**Type stubs done** (milestone 4): `_pgl.pyi` is generated at build time by
|
|
30
|
+
`nanobind_add_stub` in [CMakeLists.txt](CMakeLists.txt) — from the *bare* `_pgl`
|
|
31
|
+
module, with the Python-layer sugar re-added via
|
|
32
|
+
[src/stubgen_patterns.txt](src/stubgen_patterns.txt) — and shipped next to
|
|
33
|
+
`py.typed` (PEP 561).
|
|
34
|
+
|
|
35
|
+
**Wheels CI done** (milestone 4): `cibuildwheel` is configured in
|
|
36
|
+
[pyproject.toml](pyproject.toml) and run by
|
|
37
|
+
[.github/workflows/wheels.yml](.github/workflows/wheels.yml) — CPython 3.9–3.13 on
|
|
38
|
+
`manylinux_2_28` (GCC 12 for C++20), macOS arm64+x86_64, and Windows, plus sdist.
|
|
39
|
+
pgl has no native deps, so the build only `FetchContent`s the **pinned** pgl commit
|
|
40
|
+
(kept in lockstep with `.pgl-ref`, since the sdist omits the gitignored
|
|
41
|
+
`.pgl-ref/`). Native-arch only (no QEMU/cross) because `nanobind_add_stub` imports
|
|
42
|
+
the just-built `_pgl` to emit `_pgl.pyi`. A `publish` job (PyPI Trusted Publishing,
|
|
43
|
+
gated on `v*` tags) is wired but not yet enabled on PyPI.
|
|
44
|
+
|
|
45
|
+
Still to do: broaden `intersection` to 2D∩2D / `Halfplane` (Convex/Polygon
|
|
46
|
+
results), the rest of milestone 4 (confirm the PyPI name + enable Trusted
|
|
47
|
+
Publishing, then tag a release; consider STABLE_ABI to cut the wheel count), and
|
|
48
|
+
the experimental `Polygon`.
|
|
49
|
+
[pypgl.md](pypgl.md) remains the authoritative design contract —
|
|
50
|
+
update it in lockstep if a decision changes; [ROADMAP.md](ROADMAP.md) tracks
|
|
51
|
+
progress.
|
|
52
|
+
|
|
53
|
+
`Disk` ([src/bind_disk.cpp](src/bind_disk.cpp)) is bound as its own complete
|
|
54
|
+
class — its full predicate matrix against every shape (including itself) plus
|
|
55
|
+
exact `center`/`squaredRadius`/`bbox`/`pointInside`. `area` is irrational (π) so
|
|
56
|
+
it always returns Python `float`; `radius` returns an exact `Fraction` when the
|
|
57
|
+
disk was built from a center and radius (delegating the exact/inexact decision to
|
|
58
|
+
pgl's throwing `radius<ERational>()`) and a `float` otherwise (square root);
|
|
59
|
+
`squaredDistance` to a disk is likewise `float`. `diameter()` is reconstructed as
|
|
60
|
+
an exact `Segment` for center+radius disks (pgl ships only a floating-point one)
|
|
61
|
+
and raises for an irrational radius; `fbox()` is not bound (its double-coordinate
|
|
62
|
+
return type is not registered). Disk's
|
|
63
|
+
*column* is not added to the other shapes' matrices yet (e.g. `triangle.contains(disk)`),
|
|
64
|
+
partly because pgl still lacks `Triangle::contains(Disk)` and
|
|
65
|
+
`Convex::squaredDistance(Disk)`; symmetric relations are reachable from the Disk
|
|
66
|
+
side.
|
|
67
|
+
|
|
68
|
+
The package directory is [pypgl/](pypgl/) (so `import pypgl` works); the compiled
|
|
69
|
+
extension is `pypgl._pgl`. Binding sources live in [src/](src/).
|
|
70
|
+
|
|
71
|
+
## What pypgl is
|
|
72
|
+
|
|
73
|
+
Python bindings for **Pangolin** (`pgl`), a header-only C++20 exact-geometry
|
|
74
|
+
library at `github.com/gfonsecabr/pgl`. The public API mirrors the C++ one:
|
|
75
|
+
`import pypgl` and type/method names stay unchanged. pgl is consumed via CMake
|
|
76
|
+
`FetchContent` (pinned `GIT_TAG`), never vendored or submoduled.
|
|
77
|
+
|
|
78
|
+
## Load-bearing design decisions
|
|
79
|
+
|
|
80
|
+
These are the choices that constrain everything else; violating them defeats the
|
|
81
|
+
point of the project:
|
|
82
|
+
|
|
83
|
+
- **One numeric instantiation only:** `pgl::ERational = pgl::Rational<pgl::BigInt>`.
|
|
84
|
+
Do **not** bind the `double` / `Rational<int64_t>` family. This is what keeps the
|
|
85
|
+
binary and API surface bounded. "The number type" / `Num` always means `ERational`.
|
|
86
|
+
- **Exactness is a hard contract.** Coordinates are accepted as `int`, `Fraction`,
|
|
87
|
+
or `"a/b"` strings. **Reject `float` loudly** with a message pointing at the
|
|
88
|
+
accepted forms — never silently approximate.
|
|
89
|
+
- **nanobind, not pybind11.** Chosen for small binaries / fast compile because
|
|
90
|
+
binding a templated header-only library is instantiation-heavy.
|
|
91
|
+
- **Bind concrete shapes, not the `Shape` variant wrapper.** Each shape is its own
|
|
92
|
+
Python class.
|
|
93
|
+
|
|
94
|
+
## Architecture
|
|
95
|
+
|
|
96
|
+
The only hand-written plumbing is two type casters in `src/casters.h`; everything
|
|
97
|
+
else is mechanical `.def(...)`:
|
|
98
|
+
|
|
99
|
+
1. `pgl::BigInt` ↔ Python `int` — via decimal string round-trip (lossless; uses
|
|
100
|
+
pgl's existing `operator<<`/`operator>>`). A machine-int fast path is a later
|
|
101
|
+
optimization, not a correctness requirement.
|
|
102
|
+
2. `pgl::ERational` ↔ Python `fractions.Fraction` — built from `numerator()` /
|
|
103
|
+
`denominator()` (stored in lowest terms), each term flowing through the BigInt
|
|
104
|
+
caster so arbitrarily large coordinates round-trip.
|
|
105
|
+
|
|
106
|
+
What falls out for free from pgl's typed API (built-in nanobind casters):
|
|
107
|
+
`std::optional<T>` → `T`/`None`; `std::variant<Point, Segment, …>` → the concrete
|
|
108
|
+
shape (so `intersection` returns `None` / `Point` / `Segment` with no sentinels);
|
|
109
|
+
`operator<<` → `__repr__`; `operator==`/`<`/`std::hash` → usable in `set`/`dict`.
|
|
110
|
+
|
|
111
|
+
**Layering:** the compiled `_pgl` extension stays minimal (just `.def`s). All
|
|
112
|
+
Pythonic sugar lives in `pgl/__init__.py`: vertex iteration, `point in shape` →
|
|
113
|
+
`shape.contains(point)` (point-in-shape only — keep shape-vs-shape as explicit
|
|
114
|
+
methods), pickling, and `_repr_svg_` for inline Jupyter rendering via `Canvas`.
|
|
115
|
+
|
|
116
|
+
**Translation units:** one `bind_*.cpp` per shape group (point, segment, lines,
|
|
117
|
+
polygons, canvas) so heavy template instantiation compiles in parallel and objects
|
|
118
|
+
stay small. A `PGL_BIND_PREDICATES(cls, OtherTypes...)` macro in `src/common.h`
|
|
119
|
+
keeps the seven uniform predicates (`contains`, `boundaryContains`,
|
|
120
|
+
`interiorContains`, `intersects`, `interiorsIntersect`, `separates`, `crosses`)
|
|
121
|
+
consistent across classes; each predicate is overloaded per accepted shape type.
|
|
122
|
+
|
|
123
|
+
`Disk` and `Polygon` are **experimental** — keep them gated until their C++
|
|
124
|
+
predicates settle, so the public API doesn't churn.
|
|
125
|
+
|
|
126
|
+
## Build & test
|
|
127
|
+
|
|
128
|
+
Build backend is `scikit-build-core` (PEP 517); `nanobind` is a build dependency
|
|
129
|
+
found via `python -m nanobind --cmake_dir` (not FetchContent). Development uses a
|
|
130
|
+
venv:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python3 -m venv .venv
|
|
134
|
+
.venv/bin/pip install scikit-build-core nanobind pytest
|
|
135
|
+
.venv/bin/pip install -e . --no-build-isolation # --no-build-isolation so CMake finds the venv's nanobind
|
|
136
|
+
.venv/bin/python -m pytest tests/ -q
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Re-run the `pip install -e .` line after editing any `src/*.cpp` — the editable
|
|
140
|
+
install rebuilds the extension; importing alone does not.
|
|
141
|
+
|
|
142
|
+
**pgl headers.** pgl is header-only and (currently) ships no CMake target or
|
|
143
|
+
release tags, so we do **not** `FetchContent_MakeAvailable` a `pgl::pgl` target.
|
|
144
|
+
[CMakeLists.txt](CMakeLists.txt) resolves `PGL_INCLUDE_DIR` in this order: an
|
|
145
|
+
explicit `-DPGL_INCLUDE_DIR=…`, then an in-tree `.pgl-ref/` checkout (the offline
|
|
146
|
+
default — a gitignored `git clone` of github.com/gfonsecabr/pgl), then FetchContent
|
|
147
|
+
from GitHub. `.pgl-ref/` is the local copy of the real pgl API; **read it to get
|
|
148
|
+
exact signatures** rather than trusting the design doc's header paths (which differ,
|
|
149
|
+
e.g. real headers are `include/shape/point.hpp`, `include/implementation/io.hpp`).
|
|
150
|
+
|
|
151
|
+
Co-develop against another pgl checkout:
|
|
152
|
+
`pip install -e . --no-build-isolation -C cmake.define.PGL_INCLUDE_DIR=/path/to/pgl/include`
|
|
153
|
+
|
|
154
|
+
Wheels (later milestone): `cibuildwheel` in GitHub Actions; ship generated
|
|
155
|
+
`_pgl.pyi` stubs + `py.typed`.
|
|
156
|
+
|
|
157
|
+
## Gotchas learned while binding
|
|
158
|
+
|
|
159
|
+
- **pgl's `BigInt::operator>>` reads a whole whitespace-delimited token**, so it
|
|
160
|
+
cannot drive `Rational::operator>>`'s `"a/b"` parse (the `/` gets swallowed). The
|
|
161
|
+
`ERational` caster therefore parses string coordinates through Python's
|
|
162
|
+
`fractions.Fraction` instead, then uses the uniform numerator/denominator path.
|
|
163
|
+
- **Predicate/intersection methods are templated** on the result/other-shape type
|
|
164
|
+
with defaults (`ResultNumber = NumberType = ERational`). Bind them via lambdas
|
|
165
|
+
(`[](const Self&, const Other&){ return self.method(other); }`) so the default
|
|
166
|
+
instantiation is chosen; don't try to take their address.
|
|
167
|
+
- The `in`/iteration sugar is added Python-side in [pypgl/__init__.py](pypgl/__init__.py)
|
|
168
|
+
by assigning to the nanobind classes (`Point.__contains__ = …`), which nanobind
|
|
169
|
+
permits.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.15...3.30)
|
|
2
|
+
project(pypgl LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
7
|
+
|
|
8
|
+
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
|
9
|
+
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
|
|
10
|
+
endif()
|
|
11
|
+
|
|
12
|
+
find_package(Python 3.9 COMPONENTS Interpreter Development.Module REQUIRED)
|
|
13
|
+
|
|
14
|
+
# Locate the installed nanobind (a build dependency) and load its CMake config.
|
|
15
|
+
execute_process(
|
|
16
|
+
COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
|
|
17
|
+
OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT)
|
|
18
|
+
find_package(nanobind CONFIG REQUIRED)
|
|
19
|
+
|
|
20
|
+
# Locate pgl's header-only include/ directory. Resolution order:
|
|
21
|
+
# 1. an explicit -DPGL_INCLUDE_DIR=... (co-development against a local checkout)
|
|
22
|
+
# 2. an in-tree .pgl-ref/ checkout (offline default)
|
|
23
|
+
# 3. CMake FetchContent from GitHub (pinned to a tag once pgl publishes them)
|
|
24
|
+
set(PGL_INCLUDE_DIR "" CACHE PATH "Path to pgl's include/ directory")
|
|
25
|
+
if(NOT PGL_INCLUDE_DIR)
|
|
26
|
+
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.pgl-ref/include/pgl.hpp")
|
|
27
|
+
set(PGL_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.pgl-ref/include")
|
|
28
|
+
else()
|
|
29
|
+
include(FetchContent)
|
|
30
|
+
# Pinned to the exact commit the in-tree .pgl-ref/ dev checkout is at, so CI
|
|
31
|
+
# wheel builds (which don't ship the gitignored .pgl-ref/) are reproducible
|
|
32
|
+
# and match local development. Bump in lockstep with .pgl-ref. Switch to a
|
|
33
|
+
# release tag once pgl publishes them.
|
|
34
|
+
FetchContent_Declare(pgl
|
|
35
|
+
GIT_REPOSITORY https://github.com/gfonsecabr/pgl
|
|
36
|
+
GIT_TAG 110300042bf65463aca25a559b2f47d65323b225)
|
|
37
|
+
FetchContent_MakeAvailable(pgl)
|
|
38
|
+
set(PGL_INCLUDE_DIR "${pgl_SOURCE_DIR}/include")
|
|
39
|
+
endif()
|
|
40
|
+
endif()
|
|
41
|
+
message(STATUS "pypgl: using pgl headers from ${PGL_INCLUDE_DIR}")
|
|
42
|
+
|
|
43
|
+
nanobind_add_module(_pgl
|
|
44
|
+
NB_STATIC
|
|
45
|
+
src/module.cpp
|
|
46
|
+
src/bind_point.cpp
|
|
47
|
+
src/bind_segment.cpp
|
|
48
|
+
src/bind_lines.cpp
|
|
49
|
+
src/bind_polygons.cpp
|
|
50
|
+
src/bind_disk.cpp
|
|
51
|
+
src/bind_canvas.cpp)
|
|
52
|
+
|
|
53
|
+
target_include_directories(_pgl PRIVATE "${PGL_INCLUDE_DIR}" src)
|
|
54
|
+
|
|
55
|
+
# clang-cl (the Windows toolset — see pyproject's [tool.cibuildwheel.windows])
|
|
56
|
+
# emits calls to compiler-rt's 128-bit-integer builtins (__udivti3, __modti3,
|
|
57
|
+
# __divti3, __floattidf, __fixdfti) for the __int128 path pgl takes, but lld-link
|
|
58
|
+
# does not auto-link the builtins archive the way GCC/Clang do on Unix. Link it
|
|
59
|
+
# explicitly, located relative to the compiler.
|
|
60
|
+
if(MSVC AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
61
|
+
get_filename_component(_clang_bin_dir "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
|
62
|
+
file(GLOB _clang_rt_builtins
|
|
63
|
+
"${_clang_bin_dir}/../lib/clang/*/lib/windows/clang_rt.builtins-x86_64.lib")
|
|
64
|
+
if(_clang_rt_builtins)
|
|
65
|
+
list(GET _clang_rt_builtins -1 _clang_rt_builtins) # highest clang version
|
|
66
|
+
target_link_libraries(_pgl PRIVATE "${_clang_rt_builtins}")
|
|
67
|
+
message(STATUS "pypgl: linking compiler-rt builtins ${_clang_rt_builtins}")
|
|
68
|
+
else()
|
|
69
|
+
message(WARNING "pypgl: clang_rt.builtins-x86_64.lib not found near ${_clang_bin_dir}")
|
|
70
|
+
endif()
|
|
71
|
+
endif()
|
|
72
|
+
|
|
73
|
+
install(TARGETS _pgl LIBRARY DESTINATION pypgl)
|
|
74
|
+
|
|
75
|
+
# Generate a type stub (_pgl.pyi) for the compiled module and ship it next to
|
|
76
|
+
# py.typed (PEP 561) so type checkers and IDEs see the full exact-geometry API.
|
|
77
|
+
# The stub is built from the bare `_pgl` module (importing it standalone, before
|
|
78
|
+
# pypgl/__init__.py runs); src/stubgen_patterns.txt re-adds the Python-layer
|
|
79
|
+
# sugar that __init__.py applies at import time (len/indexing/iteration over a
|
|
80
|
+
# shape's defining points and `point in shape`).
|
|
81
|
+
nanobind_add_stub(_pgl_stub
|
|
82
|
+
MODULE _pgl
|
|
83
|
+
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_pgl.pyi"
|
|
84
|
+
PYTHON_PATH "$<TARGET_FILE_DIR:_pgl>"
|
|
85
|
+
PATTERN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/src/stubgen_patterns.txt"
|
|
86
|
+
DEPENDS _pgl)
|
|
87
|
+
|
|
88
|
+
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/_pgl.pyi" DESTINATION pypgl)
|
pypgl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Guilherme D. da Fonseca
|
|
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.
|
pypgl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pypgl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for the Pangolin (pgl) exact geometry library
|
|
5
|
+
Author: Guilherme D. da Fonseca
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Guilherme D. da Fonseca
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Classifier: Development Status :: 3 - Alpha
|
|
29
|
+
Classifier: Intended Audience :: Science/Research
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
32
|
+
Classifier: Operating System :: MacOS
|
|
33
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
34
|
+
Classifier: Programming Language :: C++
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
42
|
+
Classifier: Typing :: Typed
|
|
43
|
+
Project-URL: Homepage, https://github.com/gfonsecabr/pypgl
|
|
44
|
+
Project-URL: Repository, https://github.com/gfonsecabr/pypgl
|
|
45
|
+
Project-URL: pgl (C++ library), https://github.com/gfonsecabr/pgl
|
|
46
|
+
Requires-Python: >=3.9
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
<img align="left" src="https://raw.githubusercontent.com/gfonsecabr/pypgl/main/doc/figures/logo.png" width="23%"/>
|
|
50
|
+
|
|
51
|
+
<picture>
|
|
52
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/gfonsecabr/pypgl/main/doc/figures/logotextdark.svg"/>
|
|
53
|
+
<img alt="Pangolin: Plane Geometry Library" src="https://raw.githubusercontent.com/gfonsecabr/pypgl/main/doc/figures/logotext.svg" width="65%"/>
|
|
54
|
+
</picture>
|
|
55
|
+
|
|
56
|
+
<!-- [](https://github.com/gfonsecabr/pgl/actions/workflows/tests.yml)
|
|
57
|
+
[.svg)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) -->
|
|
58
|
+
[.svg)](https://opensource.org/licenses/MIT)
|
|
59
|
+
<!-- [.svg)](https://gfonsecabr.github.io/pgl/benchmarks/index.html) -->
|
|
60
|
+
|
|
61
|
+
> ⚠️ **Work in Progress**: This library is still under construction and contains **bugs and missing features**. Use in production environments is not recommended.
|
|
62
|
+
|
|
63
|
+
Pangolin (or `pgl`) is a C++ library for computational geometry in the plane and `pypgl` is the official python binding for it. It is designed to be pleasant to use and always exact. It accepts integer and rational coordinates, but rejects floating point numbers.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import pypgl as pgl
|
|
67
|
+
|
|
68
|
+
p = pgl.Point(1, 0)
|
|
69
|
+
q = pgl.Point(4, '15/2')
|
|
70
|
+
s = pgl.Segment(p, q)
|
|
71
|
+
t = pgl.Segment(0, 8, '7/3', 1)
|
|
72
|
+
if s.intersects(t):
|
|
73
|
+
print(s, "intersects", t)
|
|
74
|
+
# Output: (1,0)--(4,15/2) intersects (0,8)--(7/3,1)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Shapes and Predicates
|
|
78
|
+
|
|
79
|
+
| Family | Shapes |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| 0-dimensional | [`Point`](doc/shapes.md#point) |
|
|
82
|
+
| 1-dimensional | [`Segment`](doc/shapes.md#segment), [`OrientedSegment`](doc/shapes.md#oriented-segment), [`Line`](doc/shapes.md#line), [`OrientedLine`](doc/shapes.md#oriented-line), [`Ray`](doc/shapes.md#ray), ~~[`Polyline`](doc/shapes.md#polyline), [`PolyFunction`](doc/shapes.md#monotone-polyline)~~ |
|
|
83
|
+
| 2-dimensional | [`Halfplane`](doc/shapes.md#half-plane), [`Triangle`](doc/shapes.md#triangle), [`Rectangle`](doc/shapes.md#rectangle), [`Disk`](doc/shapes.md#disk), [`Convex`](doc/shapes.md#convex), [`Polygon`](doc/shapes.md#polygon) |
|
|
84
|
+
|
|
85
|
+
The following [predicates](doc/shape_methods.md#predicates) are implemented as methods of all shapes.
|
|
86
|
+
|
|
87
|
+
- `contains(Shape)` Does it contain the other shape?
|
|
88
|
+
- `boundaryContains(Shape)` Does its boundary contain the other shape?
|
|
89
|
+
- `interiorContains(Shape)` Does it contain the other shape in the interior?
|
|
90
|
+
- `intersects(Shape)` Do the two shapes intersect?
|
|
91
|
+
- `interiorsIntersect(Shape)` Do the interiors of the two shapes intersect?
|
|
92
|
+
- `separates(Shape)` Does one shape cut the other into two (or more) components?
|
|
93
|
+
- `crosses(Shape)` Do both shapes separate each other?
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import pypgl as pgl
|
|
97
|
+
|
|
98
|
+
o = pgl.Point() # Point (0,0)
|
|
99
|
+
d = pgl.Disk(o, 10) # Disk of radius 10 centered at (0,0)
|
|
100
|
+
if d.contains(o):
|
|
101
|
+
print("Disk contains", o)
|
|
102
|
+
diam = d.diameter()
|
|
103
|
+
if d.contains(diam):
|
|
104
|
+
print("Disk contains the diameter")
|
|
105
|
+
if not d.interiorContains(diam):
|
|
106
|
+
print("Disk's interior does not contain the diameter")
|
|
107
|
+
# Output:
|
|
108
|
+
# Disk contains (0,0)
|
|
109
|
+
# Disk contains the diameter
|
|
110
|
+
# Disk's interior does not contain the diameter
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Other Methods
|
|
114
|
+
|
|
115
|
+
Several [other methods](doc/shape_methods.md) are supported by the shapes.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import pypgl as pgl
|
|
119
|
+
|
|
120
|
+
c = pgl.Convex([pgl.Point(0, 0), pgl.Point(1, 0), pgl.Point(1, 2), pgl.Point(0, 1)])
|
|
121
|
+
s = c.diameter()
|
|
122
|
+
print("The diameter of", c,
|
|
123
|
+
"is defined by", s,
|
|
124
|
+
"and has length", s.length())
|
|
125
|
+
# Output: The diameter of Convex[(0,0),(1,0),(1,2),(0,1)] is defined by (0,0)--(1,2) and has length 2.23607
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Visualization
|
|
129
|
+
|
|
130
|
+
A `Canvas` class is provided for [SVG visualization](doc/canvas.md):
|
|
131
|
+
|
|
132
|
+
<img align="right" src="https://raw.githubusercontent.com/gfonsecabr/pypgl/main/doc/figures/example2.svg" width="200"/>
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import pypgl as pgl
|
|
136
|
+
|
|
137
|
+
canvas = pgl.Canvas()
|
|
138
|
+
canvas.draw(pgl.Point(0, 0))
|
|
139
|
+
|
|
140
|
+
tri = pgl.Triangle(-1, -1, 0, 2, 1, -2)
|
|
141
|
+
canvas.stroke("green")
|
|
142
|
+
canvas.draw(tri)
|
|
143
|
+
canvas.stroke("blue")
|
|
144
|
+
canvas.draw(2*tri)
|
|
145
|
+
canvas.writeSVG("example2.svg")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## Algorithms and Data Structures
|
|
150
|
+
|
|
151
|
+
<img align="right" src="https://raw.githubusercontent.com/gfonsecabr/pypgl/main/doc/figures/example_triangulation.svg" width="200"/>
|
|
152
|
+
|
|
153
|
+
PGL includes [fundamental algorithms](doc/algorithms.md) and [data structures](doc/data_structures.md) such as:
|
|
154
|
+
|
|
155
|
+
- Convex hull: computed with Graham scan.
|
|
156
|
+
- Line segment intersection: Bentley-Ottmann sweep line using rational numbers.
|
|
157
|
+
- Sort points: by angle or Hilbert order.
|
|
158
|
+
- Kd-tree: for points and a generalization for other bounded shapes.
|
|
159
|
+
- Triangulation: including Delaunay and constrained Delaunay triangulations for points and polygons.
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
## Installation
|
|
163
|
+
|
|
164
|
+
`pypgl` requires **Python 3.9 or newer**.
|
|
165
|
+
|
|
166
|
+
### From PyPI
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pip install pypgl
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Pre-built wheels are published for CPython 3.9–3.13 on Linux (`manylinux_2_28`,
|
|
173
|
+
x86_64), macOS (Apple Silicon), and Windows, so most users need no compiler.
|
|
174
|
+
|
|
175
|
+
> ℹ️ The first PyPI release is not out yet. Until then, install from source as
|
|
176
|
+
> shown below.
|
|
177
|
+
|
|
178
|
+
### From source
|
|
179
|
+
|
|
180
|
+
Installing from a source tree or directly from GitHub builds the extension
|
|
181
|
+
locally and therefore needs a **C++20 compiler** (GCC 12+, Clang 15+, or, on
|
|
182
|
+
Windows, the LLVM/ClangCL toolset). The header-only `pgl` library is fetched
|
|
183
|
+
automatically by CMake — nothing else to install.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pip install git+https://github.com/gfonsecabr/pypgl.git
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Development install
|
|
190
|
+
|
|
191
|
+
Work on the bindings from a checkout with an editable, in-place build:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
git clone https://github.com/gfonsecabr/pypgl.git
|
|
195
|
+
cd pypgl
|
|
196
|
+
python3 -m venv .venv
|
|
197
|
+
.venv/bin/pip install scikit-build-core nanobind pytest
|
|
198
|
+
.venv/bin/pip install -e . --no-build-isolation
|
|
199
|
+
.venv/bin/python -m pytest tests/ -q
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`--no-build-isolation` lets CMake find the venv's `nanobind`. Re-run the
|
|
203
|
+
`pip install -e .` step after editing any `src/*.cpp`, since the editable
|
|
204
|
+
install is what rebuilds the extension. To build against a local `pgl` checkout
|
|
205
|
+
instead of the pinned upstream commit:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
.venv/bin/pip install -e . --no-build-isolation \
|
|
209
|
+
-C cmake.define.PGL_INCLUDE_DIR=/path/to/pgl/include
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## More Information
|
|
213
|
+
|
|
214
|
+
- For a brief description, check the documents at the [doc folder](doc/).
|
|
215
|
+
- For some simple examples, check the files at the [examples folder](examples/).
|
|
216
|
+
- Check the [C++ version](https://github.com/gfonsecabr/pgl).
|
|
217
|
+
|