GeomPP — Python Bindings
Python bindings for geompp — a C++ 2D/3D geometry library.
Changelog — full release notes for every version.
Install
pip install geompp
Pre-built wheels are available for:
| Platform | Python versions |
|---|---|
| Linux x86_64 | 3.8 · 3.9 · 3.10 · 3.11 · 3.12 · 3.13 · 3.14 |
| Windows x64 | 3.8 · 3.9 · 3.10 · 3.11 · 3.12 · 3.13 · 3.14 |
If your platform or Python version is not in the table above, pip will compile from source — you will need CMake ≥ 3.15 and a C++20-capable compiler.
Classes
| 2D | 3D |
|---|---|
| Point2D | Point3D |
| Vector2D | Vector3D |
| Line2D | Line3D |
| Ray2D | Ray3D |
| LineSegment2D | LineSegment3D |
| Polyline2D | Polyline3D |
| Triangle2D | Triangle3D |
| Polygon2D | Polygon3D |
| BBox2D | BBox3D |
| BBall2D | BBall3D |
| BRect2D | |
| BPrism3D | |
| GeometryCollection2D | GeometryCollection3D |
| Plane | |
| View2D |
Free functions
| Function | Description |
|---|---|
are_collinear(p1, p2, p3) |
Three points on the same line |
are_coplanar(points) |
List of Point3D on the same plane |
closest_world_plane_to(points) |
XY / YZ / ZX plane nearest to the point cloud |
are_ccw(points[, ref_plane]) |
Counter-clockwise winding (2D or 3D) |
are_cw(points[, ref_plane]) |
Clockwise winding (2D or 3D) |
remove_collinear(points) |
Drop collinear intermediate points |
remove_duplicates(points) |
Drop duplicate points |
average(points) |
Arithmetic mean |
linear_combination(points, weights) |
Weighted sum |
has_intersections(segments) |
Shamos–Hoey: True if any two segments in list[LineSegment2D] cross |
find_intersections(segments) |
Bentley–Ottmann: returns list[Point2D] — every crossing point, sorted left-to-right |
convex_hull(points) |
Andrew's monotone chain: convex hull of a list[Point2D], returned in CCW order |
convex_hull(points, normal=None) |
Convex hull of a coplanar list[Point3D]; optional Vector3D normal (auto-detected if omitted) |
principal_axes(points) |
PCA on a list[Point3D]: returns CoordinateFrame (.x primary, .y secondary, .z best-fit normal) |
principal_normal(points) |
Best-fit plane normal of a list[Point3D] (PCA eigenvector with smallest eigenvalue) |
principal_direction(points) |
Dominant direction of a list[Point3D] (PCA eigenvector with largest eigenvalue) |
tangents_to(polygon, point_or_polygon) |
PolygonTangents2D/PolygonTangents3D (.left/.right) — tangent segments to a point, or common outer tangents to another polygon |
How to use it
You can look at the test suite to see detailed usage.
A quick list of code examples per topic is provided here.
Code Examples
1. Creation and I/O operations
1.1 Create geometries from classes
import geompp as g
# This will be the precision used by all functions for this run of the program.
g.set_decimal_precision(g.DP_THREE)
# two line segments intersecting at (0,0,1)
s1 = g.LineSegment3D.make(g.Point3D(1, 0, 0), g.Point3D(-1, 0, 2))
s2 = g.LineSegment3D.make(g.Point3D(0, 1, 0), g.Point3D(0, -1, 2))
print(f"s1 = {s1.to_wkt()}")
print(f"s2 = {s2.to_wkt()}")
if s1.intersects(s2):
result = s1.intersection(s2)
if result is not None:
print(f"intersection found: {result.to_wkt()}")
result.to_file("intersection.wkt")
print("intersection written to intersection.wkt")
else:
print("no intersection found")
will print out
s1 = LINESTRING (1 0 0, -1 0 2)
s2 = LINESTRING (0 1 0, 0 -1 2)
intersection found: POINT (0 0 1)
intersection written to intersection.wkt
1.2 Create geometries from text
import geompp as g
g.set_decimal_precision(g.DP_THREE)
# two line segments intersecting at (0,0,1)
s1 = g.LineSegment3D.from_wkt("LINESTRING (1 0 0, -1 0 2)")
s2 = g.LineSegment3D.from_wkt("LINESTRING (0 1 0, 0 -1 2)")
print(f"s1 = {s1.to_wkt()}")
print(f"s2 = {s2.to_wkt()}")
if s1.intersects(s2):
result = s1.intersection(s2)
if result is not None:
print(f"intersection found: {result.to_wkt()}")
result.to_file("intersection.wkt")
print("intersection written to intersection.wkt")
else:
print("no intersection found")
will print out
s1 = LINESTRING (1 0 0, -1 0 2)
s2 = LINESTRING (0 1 0, 0 -1 2)
intersection found: POINT (0 0 1)
intersection written to intersection.wkt
1.3 Import geometries from a file
import geompp as g
lsv_path = "sample_geometries.lsv"
# POINT (1 2 3)
# POINT (4 5 6)
# LINESTRING (0 0 0, 1 1 1)
# LINESTRING (2 0 0, 2 3 4)
# LINE (0 0 0, 1 0 0)
# RAY (0 0 0, 0 1 0)
g.set_decimal_precision(g.DP_THREE)
parser = g.WktParser.open(lsv_path)
if not parser.has_next():
print(f"no geometries found in file {lsv_path}")
else:
while parser.has_next():
entry = parser.next()
if entry is None:
print("skipped unrecognised line")
continue
print(g.WktParser.to_wkt(entry))
will print out exactly the list of geometries above.
2. Geometry Operations
2.1 Containment
Triangle3D.contains(p) and Polygon2D/3D.contains(p) test whether a point lies inside a shape
using barycentric coordinates and the winding number, respectively.
LineSegment.contains(p) checks whether a point lies on the segment;
location(p) returns the parameter t ∈ [0, 1] for a point already on it, and interpolate(t) reverses the mapping.
import geompp as g
# Triangle3D — barycentric containment
tri = g.Triangle3D.make(
g.Point3D(0, 0, 0), g.Point3D(4, 0, 0), g.Point3D(0, 4, 0))
print(tri.contains(g.Point3D(1, 1, 0))) # True — inside
print(tri.contains(g.Point3D(3, 3, 0))) # False — outside
# Polygon2D — winding-number containment
poly = g.Polygon2D.make([
g.Point2D(0, 0), g.Point2D(6, 0),
g.Point2D(6, 4), g.Point2D(0, 4),
])
print(poly.contains(g.Point2D(3, 2))) # True
print(poly.contains(g.Point2D(7, 2))) # False
# LineSegment2D — containment, location, and interpolation
seg = g.LineSegment2D.make(g.Point2D(0, 0), g.Point2D(4, 0))
print(seg.contains(g.Point2D(2, 0))) # True
print(seg.contains(g.Point2D(2, 1))) # False
t = seg.location(g.Point2D(2, 0)) # 0.5
mid = seg.interpolate(0.5) # Point2D(2, 0)
print(f"t = {t:.1f}")
print(f"mid = {mid.to_wkt()}")
True
False
True
False
True
False
t = 0.5
mid = POINT (2 0)
2.2 Intersection
Every 2D primitive (Line2D, Ray2D, LineSegment2D, Triangle2D, Polygon2D) can intersect any
other 2D primitive, and the same holds in 3D across Line3D, Ray3D, LineSegment3D, Triangle3D,
and Plane. Results are the geometry object or None; polygon intersections return a
list[LineSegment2D] because a line can produce multiple chords through a concave shape.
find_intersections(segments) (Bentley–Ottmann) reports all crossing points across an arbitrary set
of 2D segments, sorted left-to-right.
import geompp as g
# LineSegment2D → Polygon2D: segment is clipped to the polygon interior
square = g.Polygon2D.make([
g.Point2D(0, 0), g.Point2D(4, 0),
g.Point2D(4, 4), g.Point2D(0, 4),
])
seg = g.LineSegment2D.make(g.Point2D(-1, 2), g.Point2D(5, 2))
seg_chords = square.intersection(seg)
for c in (seg_chords or []):
print(c.to_wkt()) # LINESTRING (0 2, 4 2)
# Ray2D → Polygon2D: ray entering from outside, clipped at the exit boundary
ray2d = g.Ray2D.make(g.Point2D(-1, 2), g.Vector2D(1, 0))
ray_chords = square.intersection(ray2d)
for c in (ray_chords or []):
print(c.to_wkt()) # LINESTRING (0 2, 4 2)
# Line2D → concave Polygon2D: vertical line through a C-shape produces two chords
cshape = g.Polygon2D.make([
g.Point2D(0, 0), g.Point2D(4, 0), g.Point2D(4, 1),
g.Point2D(1, 1), g.Point2D(1, 3), g.Point2D(4, 3),
g.Point2D(4, 4), g.Point2D(0, 4),
])
line2d = g.Line2D.make(g.Point2D(2, 0), g.Point2D(2, 1))
lin_chords = cshape.intersection(line2d)
for c in (lin_chords or []):
print(c.to_wkt())
# LINESTRING (2 0, 2 1)
# LINESTRING (2 3, 2 4)
# Ray3D → Triangle3D
tri = g.Triangle3D.make(
g.Point3D(0, 0, 0), g.Point3D(4, 0, 0), g.Point3D(0, 4, 0))
ray = g.Ray3D.make(g.Point3D(1, 1, 3), g.Vector3D(0, 0, -1))
hit = tri.intersection(ray)
print(hit.to_wkt() if hit else None) # POINT (1 1 0)
# LineSegment2D vs LineSegment2D
s1 = g.LineSegment2D.make(g.Point2D(0, 1), g.Point2D(4, 1))
s2 = g.LineSegment2D.make(g.Point2D(2, 0), g.Point2D(2, 4))
cross = s1.intersection(s2)
print(cross.to_wkt() if cross else None) # POINT (2 1)
# find_intersections — all crossing points (Bentley–Ottmann)
segs = [
g.LineSegment2D.make(g.Point2D(0, 0), g.Point2D(4, 4)),
g.LineSegment2D.make(g.Point2D(0, 4), g.Point2D(4, 0)),
g.LineSegment2D.make(g.Point2D(0, 2), g.Point2D(4, 2)),
]
for p in g.find_intersections(segs):
print(p.to_wkt())
LINESTRING (0 2, 4 2)
LINESTRING (0 2, 4 2)
LINESTRING (2 0, 2 1)
LINESTRING (2 3, 2 4)
POINT (1 1 0)
POINT (2 1)
POINT (1 2)
POINT (2 2)
POINT (3 2)
2.2.1 Split a complex polygon
A complex polygon (also called a self-intersecting polygon) is a polygon whose edges cross
each other. simplify() decomposes it into a list of simple (non-self-intersecting) polygons
via planar-graph half-edge face tracing. Each returned polygon is guaranteed to satisfy
is_simple() == True. If the input is already simple, simplify() returns a single-element
list containing the original polygon.
import geompp as g
g.set_decimal_precision(g.DP_THREE)
# 2D: a "bowtie" — edges B→C and D→A cross at (2,2)
bowtie = g.Polygon2D.make([
g.Point2D(0, 0), g.Point2D(4, 0),
g.Point2D(1, 3), g.Point2D(3, 3)])
print("is simple:", bowtie.is_simple()) # False
parts = bowtie.simplify()
print(f"{len(parts)} simple polygon(s)")
for p in parts:
print(f" {p.to_wkt()} area={p.area()}")
# 3D: same bowtie lifted into the XY plane (z = 0)
bowtie3d = g.Polygon3D.make([
g.Point3D(0,0,0), g.Point3D(4,0,0),
g.Point3D(1,3,0), g.Point3D(3,3,0)])
parts3d = bowtie3d.simplify()
print(f"{len(parts3d)} simple 3D polygon(s)")
is simple: False
2 simple polygon(s)
POLYGON ((0 0, 4 0, 2 2, 0 0)) area=4.0
POLYGON ((2 2, 1 3, 3 3, 2 2)) area=1.0
2 simple 3D polygon(s)
2.3 Overlap
overlaps(other) returns True when two primitives share a 1D region (more than a single point).
overlap(other) returns the shared geometry, or None when they do not overlap or only touch at
a single point. The return type mirrors the "smaller" of the two primitives: a Line × Line
overlap yields a Line; Ray × Ray with opposite directions yields a LineSegment; all
Segment-involving pairs yield a LineSegment.
import geompp as g
g.set_decimal_precision(g.DP_THREE)
# ── Line × Line ─────────────────────────────────────────────────────────────
x_axis = g.Line2D.make(g.Point2D(0, 0), g.Vector2D(1, 0))
x_same = g.Line2D.make(g.Point2D(5, 0), g.Point2D(8, 0)) # same infinite line
x_off = g.Line2D.make(g.Point2D(0, 1), g.Vector2D(1, 0)) # parallel, offset
print(x_axis.overlaps(x_same)) # True
print(x_axis.overlaps(x_off)) # False
ov_ll = x_axis.overlap(x_same) # Line2D or None
if ov_ll is not None:
print(ov_ll.to_wkt()) # LINE (0 0, 1 0)
# ── Ray × Ray (same direction) ─────────────────────────────────────────────
r1 = g.Ray2D.make(g.Point2D(0, 0), g.Vector2D(1, 0))
r2 = g.Ray2D.make(g.Point2D(3, 0), g.Vector2D(1, 0)) # inside r1
ov_rr = r1.overlap(r2) # Ray2D or LineSegment2D or None
if ov_rr is not None:
print(ov_rr.to_wkt()) # RAY (3 0, 1 0)
# ── Ray × Ray (anti-parallel) ──────────────────────────────────────────────
r3 = g.Ray2D.make(g.Point2D(7, 0), g.Vector2D(-1, 0)) # heads toward r1
ov_anti = r1.overlap(r3)
if ov_anti is not None:
print(ov_anti.to_wkt())
# LINESTRING (0 0, 7 0)
# ── Segment × Segment ──────────────────────────────────────────────────────
a = g.LineSegment2D.make(g.Point2D(0, 0), g.Point2D(5, 0))
b = g.LineSegment2D.make(g.Point2D(3, 0), g.Point2D(8, 0))
print(a.overlaps(b)) # True
ov_ss = a.overlap(b) # LineSegment2D or None
if ov_ss is not None:
print(ov_ss.to_wkt()) # LINESTRING (3 0, 5 0)
# touch at a single endpoint → no overlap
c = g.LineSegment2D.make(g.Point2D(5, 0), g.Point2D(9, 0))
print(a.overlaps(c)) # False
print(a.overlap(c) is None) # True
True
False
LINE (0 0, 1 0)
RAY (3 0, 1 0)
LINESTRING (0 0, 7 0)
True
LINESTRING (3 0, 5 0)
False
True
2.4 Touch
touches(other) returns True when two primitives share exactly one endpoint-contact point
(not an interior crossing, not a shared segment). touch(other) returns that contact point,
or None when there is no touch.
import geompp as g
g.set_decimal_precision(g.DP_THREE)
# Ray origin sits on a line → touch at origin
line = g.Line2D.make(g.Point2D(0, 0), g.Point2D(1, 0)) # x-axis
ray = g.Ray2D.make(g.Point2D(3, 0), g.Vector2D(0, 1)) # vertical at x=3
print(line.touches(ray)) # True
tp = line.touch(ray) # Point2D or None
print(tp.to_wkt()) # POINT (3 0)
# Collinear ray → overlap, not touch
ray_col = g.Ray2D.make(g.Point2D(1, 0), g.Vector2D(1, 0))
print(line.touches(ray_col)) # False
# Anti-parallel rays sharing only their common origin → touch
r1 = g.Ray2D.make(g.Point2D(0, 0), g.Vector2D( 1, 0))
r2 = g.Ray2D.make(g.Point2D(0, 0), g.Vector2D(-1, 0))
print(r1.touches(r2)) # True
print(r1.touch(r2).to_wkt()) # POINT (0 0)
# Anti-parallel rays overlapping → touch returns None
r3 = g.Ray2D.make(g.Point2D(3, 0), g.Vector2D(-1, 0))
print(r1.touches(r3)) # False
# Segment T-junction: endpoint of b lies on a (non-collinear)
a = g.LineSegment2D.make(g.Point2D(0, 0), g.Point2D(5, 0))
b = g.LineSegment2D.make(g.Point2D(3, 0), g.Point2D(3, 3))
print(a.touches(b)) # True
print(a.touch(b).to_wkt()) # POINT (3 0)
# Collinear segments sharing exactly one endpoint → touch
c = g.LineSegment2D.make(g.Point2D(5, 0), g.Point2D(8, 0))
print(a.touches(c)) # True
print(a.touch(c).to_wkt()) # POINT (5 0)
# Overlapping collinear segments → touch returns None
d = g.LineSegment2D.make(g.Point2D(3, 0), g.Point2D(7, 0))
print(a.touches(d)) # False
print(a.touch(d) is None) # True
True
POINT (3 0)
False
True
POINT (0 0)
False
True
POINT (3 0)
True
POINT (5 0)
False
True
2.5 Polyline Overlaps / Touches
Polyline2D and Polyline3D iterate over their constituent segments to collect all
overlapping sub-segments or all touch points. overlap() returns a list of
LineSegment2D/3D or None; touch() returns a list of Point2D/3D or None.
import geompp as g
# L-shaped polyline
pl = g.Polyline2D.make([g.Point2D(0,0), g.Point2D(4,0), g.Point2D(4,3)])
# x-axis line overlaps the horizontal leg
x_axis = g.Line2D.make(g.Point2D(0,0), g.Point2D(1,0))
print(pl.overlaps(line=x_axis)) # True
segs = pl.overlap(line=x_axis)
for s in segs:
print(s.to_wkt()) # LINESTRING (0 0, 4 0)
# T-junction: vertical arm touches a horizontal segment at (3,0)
stem = g.Polyline2D.make([g.Point2D(3,0), g.Point2D(3,3)])
bar = g.LineSegment2D.make(g.Point2D(0,0), g.Point2D(5,0))
print(stem.touches(segment=bar)) # True
pts = stem.touch(segment=bar)
for p in pts:
print(p.to_wkt()) # POINT (3 0)
# Two polylines sharing an endpoint
pl1 = g.Polyline2D.make([g.Point2D(0,0), g.Point2D(3,0)])
pl2 = g.Polyline2D.make([g.Point2D(3,0), g.Point2D(3,3)])
print(pl1.touches(other=pl2)) # True
True
LINESTRING (0 0, 4 0)
True
POINT (3 0)
True
3. Planes
3.1 Coplanarity, winding order, and polygon with holes
import geompp as g
g.set_decimal_precision(g.DP_THREE)
# Four points on the XY plane vs. a set that spans 3D space
pts_flat = [g.Point3D(0,0,0), g.Point3D(1,0,0), g.Point3D(0,1,0), g.Point3D(1,1,0)]
pts_3d = [g.Point3D(0,0,0), g.Point3D(1,0,0), g.Point3D(0,1,0), g.Point3D(0,0,1)]
print(g.are_coplanar(pts_flat)) # True — all on the XY plane
print(g.are_coplanar(pts_3d)) # False — spans 3D space
# Which world-axis plane is closest to the cloud?
plane = g.closest_world_plane_to(pts_flat)
print(plane.normal) # VECTOR (0 0 1) → XY plane
# Winding check
ring = [g.Point3D(0,0,0), g.Point3D(1,0,0), g.Point3D(1,1,0), g.Point3D(0,1,0)]
print(g.are_ccw(ring)) # True
# Polygon3D with a rectangular hole (outer CCW, hole CW)
outer = [g.Point3D(0,0,0), g.Point3D(4,0,0), g.Point3D(4,4,0), g.Point3D(0,4,0)]
hole = [g.Point3D(1,3,0), g.Point3D(3,3,0), g.Point3D(3,1,0), g.Point3D(1,1,0)]
poly = g.Polygon3D.make(outer, [hole])
print(poly.to_wkt())
will print out
True
False
VECTOR (0 0 1)
True
POLYGON ((0 0 0, 4 0 0, 4 4 0, 0 4 0, 0 0 0), (1 3 0, 3 3 0, 3 1 0, 1 1 0, 1 3 0))
3.2 Projecting points onto a plane
plane.project_onto(p) returns the perpendicular projection in 3D world coordinates.
plane.project_into(p) maps the same projected point into the plane's local 2D frame.
plane.evaluate(p2d) is the inverse — local 2D coordinates back to world 3D.
import geompp as g
# XY plane: normal (0, 0, 1), origin (0, 0, 0)
pl = g.Plane.xy()
p = g.Point3D(3.0, 4.0, 7.0)
on = pl.project_onto(p) # Point3D(3, 4, 0) — projection in world 3D
into = pl.project_into(p) # Point2D(3, 4) — in the plane's local frame
back = pl.evaluate(into) # Point3D(3, 4, 0) — local 2D back to world 3D
print(f"on_plane: {on.to_wkt()}")
print(f"in_plane: {into.to_wkt()}")
print(f"back: {back.to_wkt()}")
on_plane: POINT (3 4 0)
in_plane: POINT (3 4)
back: POINT (3 4 0)
3.3 Planar vs non-planar Polyline3D
Polyline3D.is_planar() checks whether all knots lie in a common plane. Only planar polylines support
is_simple(), is_convex(), convex_hull(), and to_polygon() — call is_planar() first.
convex_hull() returns a Polyline3D (an open path). Call to_polygon() on it to close the boundary into a Polygon3D with area.
import geompp as g
# Planar star-like path in the XY plane
planar = g.Polyline3D.make([
g.Point3D(0, 0, 0), g.Point3D(4, 0, 0), g.Point3D(2, 2, 0),
g.Point3D(4, 4, 0), g.Point3D(0, 4, 0),
])
print(f"planar: {planar.is_planar()}") # True
hull = planar.convex_hull() # Polyline3D — open hull
polygon = hull.to_polygon() # Polygon3D — closed region with area
print(f"hull knots: {hull.size()}")
print(f"polygon area: {polygon.area():.3f}")
# Non-planar path: each point rises out of the XY plane
rising = g.Polyline3D.make([
g.Point3D(0, 0, 0), g.Point3D(1, 0, 0),
g.Point3D(1, 1, 1), g.Point3D(0, 1, 2),
])
print(f"planar: {rising.is_planar()}") # False
pts = [rising[i] for i in range(rising.size())]
direction = g.principal_direction(pts)
print(f"dominant direction: {direction}")
will print out
planar: True
hull knots: 4
polygon area: 16.000
planar: False
dominant direction: VECTOR (...)
3.4 View2D — streaming 3D points to 2D
View2D maps 3D points to 2D scalars via x() / y() getters without allocating an intermediate
Point2D list. Axis-aligned views (xy, yz, zx) are the fastest path — just a direct coordinate
read. on_plane computes dot products against the plane's local axes.
from geompp import View2D, Plane, Point3D, Vector3D
# Axis-aligned views (fast path — single coordinate read)
v_xy = View2D.xy() # x→x, y→y (drops z)
v_yz = View2D.yz() # y→x, z→y (drops x)
v_zx = View2D.zx() # z→x, x→y (drops y)
# Custom view onto any plane
plane = Plane.from_origin_and_normal(Point3D(0, 0, 5), Vector3D(0, 0, 1))
v_custom = View2D.on_plane(plane)
pts3d = [Point3D(1, 2, 5), Point3D(3, 4, 5), Point3D(5, 6, 5)]
# Stream 3D points to 2D without allocating a Point2D list
for p in pts3d:
print(f"({v_xy.x(p)}, {v_xy.y(p)})")
(1, 2)
(3, 4)
(5, 6)
4. PCA on a 3D point cloud
principal_axes(points) runs PCA (Jacobi eigendecomposition on the 3×3 covariance matrix) and returns
a CoordinateFrame — three orthonormal axes sorted by variance: x is the direction of most spread,
y the secondary, and z the best-fit plane normal (least variance).
import geompp as g
# 8 points flat in the XY plane, elongated along X
cloud = [
g.Point3D(0, 0, 0), g.Point3D(1, 0, 0),
g.Point3D(2, 0, 0), g.Point3D(3, 0, 0),
g.Point3D(0, 0.1, 0), g.Point3D(1, 0.1, 0),
g.Point3D(2, 0.1, 0), g.Point3D(3, 0.1, 0),
]
frame = g.principal_axes(cloud)
print(f"x (primary): {frame.x}") # ≈ (1, 0, 0)
print(f"y (secondary): {frame.y}") # ≈ (0, 1, 0)
print(f"z (normal): {frame.z}") # ≈ (0, 0, 1)
# Convenience wrappers
normal = g.principal_normal(cloud) # == frame.z
direction = g.principal_direction(cloud) # == frame.x
will print out
x (primary): VECTOR (1 0 0)
y (secondary): VECTOR (0 1 0)
z (normal): VECTOR (0 0 1)
5. Bounding containers
5.1 Simple containers for quick rejection
BBox3D gives the tight axis-aligned box; BBall3D (Ritter 1990) gives an approximate
minimum enclosing sphere — both accept any cloud of points.
import geompp as g
g.set_decimal_precision(g.DP_THREE)
# Vertices of a rough L-shaped structure
verts = [
g.Point3D(0, 0, 0), g.Point3D(6, 0, 0), g.Point3D(6, 2, 0),
g.Point3D(2, 2, 0), g.Point3D(2, 4, 0), g.Point3D(0, 4, 0),
g.Point3D(0, 0, 3), g.Point3D(6, 0, 3), g.Point3D(6, 2, 3),
g.Point3D(2, 2, 3), g.Point3D(2, 4, 3), g.Point3D(0, 4, 3),
]
# Axis-aligned bounding box (construct from a polyline spanning all verts)
box = g.BBox3D(g.Polyline3D.make(verts))
print(box.min.to_wkt()) # POINT (0 0 0)
print(box.max.to_wkt()) # POINT (6 4 3)
print(box.contains(g.Point3D(3, 1, 1))) # True
print(box.contains(g.Point3D(7, 1, 1))) # False
# Bounding ball — one constructor takes the point cloud directly
ball = g.BBall3D(verts)
print(ball.center.to_wkt())
print(f"radius: {ball.radius:.3f}")
print(ball.contains(g.Point3D(3, 1, 1))) # True
# All original vertices must be inside the ball
assert all(ball.contains(p) for p in verts)
will print out
POINT (0 0 0)
POINT (6 4 3)
True
False
POINT (3 ...)
radius: ...
True
5.2 Convex hull
5.2.1 Convex hull of a point cloud
convex_hull(points) (Andrew's monotone chain) wraps any point cloud into its tightest convex polygon:
import geompp as g
g.set_decimal_precision(g.DP_THREE)
# An asymmetric 5-pointed star: 5 outer tips + 5 inner concave vertices.
# The convex hull should be exactly the 5 outer tips.
star = [
g.Point2D( 0, 5), g.Point2D( 4, 2),
g.Point2D( 3, -3), g.Point2D(-2, -4), g.Point2D(-3, 1),
g.Point2D( 2, 1), g.Point2D( 2, -1),
g.Point2D( 0, -1), g.Point2D(-1, -1), g.Point2D(-1, 2),
]
hull = g.convex_hull(star)
print(f"hull has {len(hull)} vertices:")
for p in hull:
print(f" {p.to_wkt()}")
hull has 5 vertices:
POINT (3 -3)
POINT (4 2)
POINT (0 5)
POINT (-3 1)
POINT (-2 -4)
For 3D point clouds, convex_hull(points) also works — points do not need to be perfectly
coplanar. When no explicit normal is provided, the best-fit plane is estimated via PCA
(Jacobi eigendecomposition), and the hull is computed on the projection onto that plane.
You can also pass an explicit normal if known: convex_hull(points, normal).
import geompp as g
# Nearly-coplanar cloud with small Z jitter
cloud = [
g.Point3D(0, 0, 0.1), g.Point3D(4, 0, -0.1),
g.Point3D(4, 4, 0.05), g.Point3D(0, 4, -0.05),
g.Point3D(2, 2, 0.02), # interior
]
hull = g.convex_hull(cloud) # PCA detects near-XY plane, projects, computes hull
print(f"3D hull has {len(hull)} vertices") # 4 — interior point excluded
(CCW order, starting from the lexicographically smallest point)
5.2.2 Convex hull of a polygon
Polygon2D and Polygon3D expose a convex_hull() method that wraps the free function:
import geompp as g
# 3D star polygon (10 vertices, coplanar, CCW)
star = g.Polygon3D.make([
g.Point3D( 0, 5, 0), g.Point3D( 2, 1, 0),
g.Point3D( 4, 2, 0), g.Point3D( 2, -1, 0),
g.Point3D( 3, -3, 0), g.Point3D( 0, -1, 0),
g.Point3D(-2, -4, 0), g.Point3D(-1, -1, 0),
g.Point3D(-3, 1, 0), g.Point3D(-1, 2, 0),
])
hull = star.convex_hull() # Polygon3D — 5-vertex pentagon
print(f"star is convex: {star.is_convex()}") # False — star has concavities
print(f"hull is convex: {hull.is_convex()}") # True
print(f"hull has {hull.size()} vertices:")
for i in range(hull.size()):
print(f" {hull[i].to_wkt()}")
star is convex: False
hull is convex: True
hull has 5 vertices:
POINT (3 -3 0)
POINT (4 2 0)
POINT (0 5 0)
POINT (-3 1 0)
POINT (-2 -4 0)
5.2.3 Convex hull of a simple polyline
Polyline2D.convex_hull() uses Melkman's O(n) algorithm. The polyline must be simple — call is_simple() first.
import geompp as g
# Simple concave path: outer corners with an inner dip at (2,1)
path = g.Polyline2D.make([
g.Point2D(0, 0), g.Point2D(4, 0), g.Point2D(4, 4),
g.Point2D(2, 1), g.Point2D(0, 4),
])
if path.is_simple():
hull = path.convex_hull() # Polygon2D — 4-vertex rectangle
print(f"hull has {hull.size()} vertices")
hull has 4 vertices
5.3 Oriented Minimum Bounding Rectangle
BRect2D computes the tightest axis-aligned-to-input rectangle that encloses a point cloud.
It is defined by a center point, two orthogonal unit axes (axis_u, axis_v), and two half-lengths
(half_len_u, half_len_v).
Algorithm: Freeman & Shapira (1975) / Toussaint (1983) rotating calipers.
- Compute the convex hull of the input cloud (Andrew's monotone chain, O(n log n)).
- For each hull edge, project all hull vertices onto the edge direction and its perpendicular.
- The rectangle aligned with that edge has width = max − min along the edge and height = max − min along the perpendicular.
- Track the edge orientation that minimises area; the center is the midpoint of the extents.
import geompp as g
g.set_decimal_precision(g.DP_THREE)
# An asymmetric pentagon
pts = [
g.Point2D(0, 0), g.Point2D(4, 0),
g.Point2D(5, 2), g.Point2D(2, 4),
g.Point2D(-1, 2),
]
r = g.BRect2D(pts)
print(r.center.to_wkt()) # center of the OBB
print(r.axis_u.to_wkt()) # primary axis (unit vector, along a hull edge)
print(r.axis_v.to_wkt()) # secondary axis (perpendicular, CCW rotation of axis_u)
print(f"half_u: {r.half_len_u:.3f}")
print(f"half_v: {r.half_len_v:.3f}")
print(f"area: {r.area:.3f}")
print(r.contains(g.Point2D(2, 2))) # True — interior point
print(r.contains(g.Point2D(9, 0))) # False — outside
corners = r.corners() # list of 4 Point2D in CCW order
for c in corners:
print(c.to_wkt())
corners() returns the four corners in CCW order; each is guaranteed to be contains()-true.
The contains() test projects the query point onto the local axes and checks both projections against the half-lengths — O(1) per query.
BPrism3D computes the minimum-volume oriented bounding prism via PCA + rotating calipers (requires ≥ 3 non-collinear points):
import geompp as g
pts = [
g.Point3D(0,0,0), g.Point3D(4,0,0), g.Point3D(4,3,0), g.Point3D(0,3,0),
g.Point3D(0,0,2), g.Point3D(4,0,2), g.Point3D(4,3,2), g.Point3D(0,3,2),
]
prism = g.BPrism3D(pts)
print(prism.center.to_wkt()) # roughly POINT (2 1.5 1)
print(f"U: {prism.axis_u.to_wkt()}")
print(f"V: {prism.axis_v.to_wkt()}")
print(f"W: {prism.axis_w.to_wkt()}")
print(f"{prism.width:.1f} × {prism.height:.1f} × {prism.depth:.1f}") # 4 × 3 × 2
print(f"volume: {prism.volume:.1f}") # 24.0
corners = prism.corners() # list of 8 Point3D corners
print(prism.contains(prism.center)) # True
5.4 Polygon extreme points
find_extreme_points(polygon, line) returns the two vertices of a polygon that are extreme — the
least and the greatest — when projected onto a line's direction (the "supporting vertices" along that
axis). It is handy for collision broad-phase (SAT), rotating calipers, and directional clipping.
The result is an ExtremePoints2D (or ExtremePoints3D) with .min_point / .max_point. When the
polygon is convex it uses Daniel Sunday's O(log n) binary search; otherwise it falls back to an
O(n) linear scan. Holes are ignored — only the outer ring participates.
import geompp as g
# Convex diamond; project onto the X-axis to get the left / right tips
diamond = g.Polygon2D.make([
g.Point2D(2, 0), g.Point2D(4, 2), g.Point2D(2, 4), g.Point2D(0, 2)])
x_axis = g.Line2D.make(g.Point2D(0, 0), g.Point2D(1, 0))
ext = g.find_extreme_points(diamond, x_axis) # convex → O(log n)
print(f"min: {ext.min_point.to_wkt()}") # POINT (0 2)
print(f"max: {ext.max_point.to_wkt()}") # POINT (4 2)
# Works in 3D too — the polygon may lie in any plane
para = g.Polygon3D.make([
g.Point3D(0, 0, 0), g.Point3D(2, 0, 2),
g.Point3D(2, 2, 2), g.Point3D(0, 2, 0)])
d = g.Line3D.make(g.Point3D(0, 0, 0), g.Point3D(1, 1, 0))
ext3 = g.find_extreme_points(para, d)
print(f"{ext3.min_point.to_wkt()} .. {ext3.max_point.to_wkt()}")
min: POINT (0 2)
max: POINT (4 2)
POINT (0 0 0) .. POINT (2 2 2)
6. Distance
6.1 from Point
Every core primitive implements .distance_to(point) — the perpendicular / nearest distance to a
point, clamped to the primitive's own domain where relevant (a Ray only measures ahead of its
origin, a LineSegment/Polyline clamps to its own bounded extent). Point2D/Point3D themselves
just measure Euclidean distance to another point.
Polygon2D/Polygon3D.distance_to(point) and Triangle2D/Triangle3D.distance_to(point) are
declared but not yet implemented — they currently raise RuntimeError, so they're omitted from
the example below.
import geompp as g
g.set_decimal_precision(g.DP_THREE)
p2 = g.Point2D(3, 4)
p3 = g.Point3D(3, 4, 5)
print(f"Point2D: {g.Point2D(0, 0).distance_to(p2):.3f}")
print(f"Point3D: {g.Point3D(0, 0, 0).distance_to(p3):.3f}")
line2 = g.Line2D.make(g.Point2D(0, 0), g.Point2D(1, 0))
line3 = g.Line3D.make(g.Point3D(0, 0, 0), g.Point3D(1, 0, 0))
print(f"Line2D: {line2.distance_to(p2):.3f}")
print(f"Line3D: {line3.distance_to(p3):.3f}")
ray2 = g.Ray2D.make(g.Point2D(0, 0), g.Vector2D(1, 0))
ray3 = g.Ray3D.make(g.Point3D(0, 0, 0), g.Vector3D(1, 0, 0))
print(f"Ray2D: {ray2.distance_to(p2):.3f}")
print(f"Ray3D: {ray3.distance_to(p3):.3f}")
seg2 = g.LineSegment2D.make(g.Point2D(0, 0), g.Point2D(6, 0))
seg3 = g.LineSegment3D.make(g.Point3D(0, 0, 0), g.Point3D(6, 0, 0))
print(f"LineSegment2D: {seg2.distance_to(p2):.3f}")
print(f"LineSegment3D: {seg3.distance_to(p3):.3f}")
pl2 = g.Polyline2D.make([g.Point2D(0, 0), g.Point2D(6, 0), g.Point2D(6, 6)])
pl3 = g.Polyline3D.make([g.Point3D(0, 0, 0), g.Point3D(6, 0, 0), g.Point3D(6, 6, 0)])
print(f"Polyline2D: {pl2.distance_to(p2):.3f}")
print(f"Polyline3D: {pl3.distance_to(p3):.3f}")
plane = g.Plane.xy()
print(f"Plane: {plane.distance_to(p3):.3f}")
Point2D: 5.000
Point3D: 7.071
Line2D: 4.000
Line3D: 6.403
Ray2D: 4.000
Ray3D: 6.403
LineSegment2D: 4.000
LineSegment3D: 6.403
Polyline2D: 3.000
Polyline3D: 5.831
Plane: 5.000
6.2 from Other primitives
Line3D, Ray3D, and LineSegment3D each expose .distance_to(Line3D | Ray3D | LineSegment3D)
— pairwise distance between any two of the three (0 if they intersect, overlap, or one contains
the other). This overload set is 3D-only: two 2D primitives are either parallel (a constant
distance, rarely useful on its own) or they intersect (0), so Line2D/Ray2D/LineSegment2D
don't expose it.
If you need the actual closest-approach segment instead of just the scalar, use .distance(...)
(note: no _to) — it returns a LineSegment3D or None when the two intersect or overlap
(matching the zero case of distance_to).
For polygon-to-line distance, use the free function distance_to(polygon, line) (see section 5.4
"Polygon extreme points" for find_extreme_points, its sibling function) — zero if the line
crosses the polygon; for Polygon3D/Line3D it also handles a line coplanar with, parallel to
(fixed offset from), or skew to the polygon's plane.
lineA = g.Line3D.make(g.Point3D(0, 0, 0), g.Point3D(1, 0, 0))
lineB = g.Line3D.make(g.Point3D(0, 1, 1), g.Point3D(1, 1, 1)) # parallel, offset sqrt(2)
print(f"Line3D x Line3D: {lineA.distance_to(lineB):.3f}")
rayB = g.Ray3D.make(g.Point3D(0, 1, 1), g.Vector3D(1, 0, 0))
print(f"Line3D x Ray3D: {lineA.distance_to(rayB):.3f}")
segB = g.LineSegment3D.make(g.Point3D(0, 1, 1), g.Point3D(1, 1, 1))
print(f"Line3D x LineSegment3D: {lineA.distance_to(segB):.3f}")
# the closest-approach connecting segment, instead of just the scalar
connector = lineA.distance(lineB)
print(f"Line3D.distance(Line3D): {connector.to_wkt()}")
# Polygon2D / Polygon3D — distance to an infinite line (zero if the line crosses)
square = g.Polygon2D.make([
g.Point2D(0, 0), g.Point2D(4, 0), g.Point2D(4, 4), g.Point2D(0, 4)])
far_line = g.Line2D.make(g.Point2D(6, -1), g.Point2D(6, 5))
print(f"distance_to(Polygon2D, Line2D): {g.distance_to(square, far_line):.3f}")
Line3D x Line3D: 1.414
Line3D x Ray3D: 1.414
Line3D x LineSegment3D: 1.414
Line3D.distance(Line3D): LINESTRING (0 0 0, 0 1 1)
distance_to(Polygon2D, Line2D): 2.000
7. Polygon tangents
7.1 Point to Polygon
tangents_to(polygon, point) returns PolygonTangents2D/PolygonTangents3D (.left / .right,
each a LineSegment) — the two tangent segments from an external point to a polygon (the point's
"line of sight" grazing the shape on either side, like a taut string pulled around it). Convex
polygons use Daniel Sunday's O(log n) binary search; non-convex polygons are reduced to their
convex hull first (a tangent point can only ever be a hull vertex — a reflex vertex always has the
polygon on both sides of it, so it can never support a tangent line) and the result is mapped back
to the original vertex.
The point must be strictly outside the polygon and not equal to any of its vertices. For
Polygon3D, a tangent is inherently a planar concept — unlike distance_to, there is no "skew"
fallback — so the point must lie in the polygon's own plane, or the call raises RuntimeError.
import geompp as g
g.set_decimal_precision(g.DP_THREE)
square = g.Polygon2D.make([
g.Point2D(0, 0), g.Point2D(4, 0), g.Point2D(4, 4), g.Point2D(0, 4)])
t2 = g.tangents_to(square, g.Point2D(10, -2))
print(f"Polygon2D left: {t2.left.to_wkt()}")
print(f"Polygon2D right: {t2.right.to_wkt()}")
# Polygon3D requires the point to be coplanar with the polygon (here, the z=0 plane)
square3 = g.Polygon3D.make([
g.Point3D(0, 0, 0), g.Point3D(4, 0, 0), g.Point3D(4, 4, 0), g.Point3D(0, 4, 0)])
t3 = g.tangents_to(square3, g.Point3D(10, -2, 0))
print(f"Polygon3D left: {t3.left.to_wkt()}")
print(f"Polygon3D right: {t3.right.to_wkt()}")
Polygon2D left: LINESTRING (10 -2, 0 0)
Polygon2D right: LINESTRING (10 -2, 4 4)
Polygon3D left: LINESTRING (10 -2 0, 0 0 0)
Polygon3D right: LINESTRING (10 -2 0, 4 4 0)
7.2 Polygon to Polygon
tangents_to(polygon, other) returns the two common outer tangent segments between two polygons —
the "belt around two pulleys" lines that touch both shapes without crossing either. Neither polygon
needs to be convex: each is independently reduced to its convex hull when needed, same as the
point overload above. For Polygon3D, both polygons must share the same plane (two polygons in
general 3D position don't have a single well-defined common tangent line), or the call raises
RuntimeError.
import geompp as g
g.set_decimal_precision(g.DP_THREE)
square_a = g.Polygon2D.make([
g.Point2D(0, 0), g.Point2D(4, 0), g.Point2D(4, 4), g.Point2D(0, 4)])
square_b = g.Polygon2D.make([
g.Point2D(10, 1), g.Point2D(14, 1), g.Point2D(14, 5), g.Point2D(10, 5)])
t2 = g.tangents_to(square_a, square_b)
print(f"Polygon2D left: {t2.left.to_wkt()}")
print(f"Polygon2D right: {t2.right.to_wkt()}")
Polygon2D left: LINESTRING (0 4, 10 5)
Polygon2D right: LINESTRING (4 0, 14 1)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file geompp-0.12.0.tar.gz.
File metadata
- Download URL: geompp-0.12.0.tar.gz
- Upload date:
- Size: 67.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf86da54460f0a86557968a090ec87d884b5175b782b7a1842a330598fb6f92e
|
|
| MD5 |
f7f55824b1cd46961dd4569184baf8e5
|
|
| BLAKE2b-256 |
fbab99f0f0efb7ff42f7cce501c3c596142fa58c31414d32ee0aa9e9f0452690
|
File details
Details for the file geompp-0.12.0-cp313-cp313-win_amd64.whl.
File metadata
- Download URL: geompp-0.12.0-cp313-cp313-win_amd64.whl
- Upload date:
- Size: 4.4 MB
- Tags: CPython 3.13, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
011ef16342990e80727dd2b2e19f1653b0a664e426ac2008ef94236c35fbf875
|
|
| MD5 |
42fbabdfc5b5012901fd457f71b07c16
|
|
| BLAKE2b-256 |
d01087fcce28d62193ccebc912b45b493e0cfc82951a71fcf466a95036a90b74
|
File details
Details for the file geompp-0.12.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: geompp-0.12.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 6.8 MB
- Tags: CPython 3.13, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ccb0055fb4b93ee8d2bf62ae4a5ec110309f083b7dce02525c773705b3d73b1
|
|
| MD5 |
ba2ec88fc342f6189b38989b9abb35f0
|
|
| BLAKE2b-256 |
2ff49fc5c646982c610f55740d089b9cd32dedb0f9d1f357781fc8c405b7924a
|
File details
Details for the file geompp-0.12.0-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: geompp-0.12.0-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 3.7 MB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b2062fe001c3a166f91e519357efb2fc399f36e95130233f46c03364c3038066
|
|
| MD5 |
b64767cce23d3dacb81f5e8cca708d66
|
|
| BLAKE2b-256 |
1da6568631252c33bfab2a97ccda737130e420633b269044bc949a6d20dc3d78
|
File details
Details for the file geompp-0.12.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: geompp-0.12.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 5.7 MB
- Tags: CPython 3.12, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f9d999be74d02644457b05f4315bc927f0cf0a99a13ed43623d92cad4e64d97
|
|
| MD5 |
2783dd659ea3e939fd05939227e0fc77
|
|
| BLAKE2b-256 |
1e0a0eacaa1eba674b73f898129ac83571d1b0ce0c57240f69bc72f09f7f428b
|
File details
Details for the file geompp-0.12.0-cp311-cp311-win_amd64.whl.
File metadata
- Download URL: geompp-0.12.0-cp311-cp311-win_amd64.whl
- Upload date:
- Size: 3.1 MB
- Tags: CPython 3.11, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b5ed716295b950be6f24a931056ba64a5e3d9bc37d7f1caabd793b97ab8a247
|
|
| MD5 |
9d2e80087575a90b4ee2c7a050a6dd5f
|
|
| BLAKE2b-256 |
953075978e5f24211c1543f32a925ba7f759d84d03851d875757d640ebd7adcb
|
File details
Details for the file geompp-0.12.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: geompp-0.12.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 4.6 MB
- Tags: CPython 3.11, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
111d951409ce8da7a53f6fc94677443394b6a150f2de9943aac32f9dcb9aba46
|
|
| MD5 |
5152dc1124cbd5649838e3d7b8120245
|
|
| BLAKE2b-256 |
31ba8fe4960f121555235e06c4f4d9389a102afc70e9c75fb8783e81333ce9c4
|
File details
Details for the file geompp-0.12.0-cp310-cp310-win_amd64.whl.
File metadata
- Download URL: geompp-0.12.0-cp310-cp310-win_amd64.whl
- Upload date:
- Size: 2.4 MB
- Tags: CPython 3.10, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d6bd52875087f4bb6863f7cf510ffa7a47ebb076977bae8c6bba5400c7aa99e
|
|
| MD5 |
13d90f3ed3e040765901c269684dbda7
|
|
| BLAKE2b-256 |
3fb6698a403e1b7e0fff770d99d72ca9d9d40a2fc911bb0cae1b079b535a03fa
|
File details
Details for the file geompp-0.12.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: geompp-0.12.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.5 MB
- Tags: CPython 3.10, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2f85bf6dbcff63feced925807840c7c93aa63e5b65bb4f725b85b7c90965635
|
|
| MD5 |
68c09ad6b0d99d0064ba50ac9f3eee93
|
|
| BLAKE2b-256 |
4ea58215f30fa241706ce58507b3f7b5d82ae90e47a8b04ca743bae9029f56d3
|
File details
Details for the file geompp-0.12.0-cp39-cp39-win_amd64.whl.
File metadata
- Download URL: geompp-0.12.0-cp39-cp39-win_amd64.whl
- Upload date:
- Size: 1.8 MB
- Tags: CPython 3.9, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14bd9422a72dccc0d834b724daf97af13d0891e06de55cdc3126d369f7871fee
|
|
| MD5 |
97d556a08b9d82885a5183538ba09dcb
|
|
| BLAKE2b-256 |
34b0e19b582ba4f4409c225763a10aad0308832ce670aaadcee72c8beb0d86b9
|
File details
Details for the file geompp-0.12.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: geompp-0.12.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 2.3 MB
- Tags: CPython 3.9, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78b5c2bcf4b3c656b1aca1ac2b4a77b34475340175b7a3a311345578ce5f7ebd
|
|
| MD5 |
9db425122a40c0655840397783aa1d8e
|
|
| BLAKE2b-256 |
54eb651e89c8eb682d20a92c74f245d0ff0d6c304c8c96fcd099ee4ce4d98e52
|
File details
Details for the file geompp-0.12.0-cp38-cp38-win_amd64.whl.
File metadata
- Download URL: geompp-0.12.0-cp38-cp38-win_amd64.whl
- Upload date:
- Size: 1.1 MB
- Tags: CPython 3.8, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f5e38ef217e10a94655116ef9963307634beed5f9048fdc55603a580e681a02f
|
|
| MD5 |
3b544c5479fcb3daa97b713f828be261
|
|
| BLAKE2b-256 |
94792f3d05b472e725fb0bb6a22d59db4d66db64c24d398048e9b3165cdeed79
|
File details
Details for the file geompp-0.12.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: geompp-0.12.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 1.2 MB
- Tags: CPython 3.8, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
495280b9de809a06f63bd17c4922b0eb774077692ddbac9a37c21c7116963f21
|
|
| MD5 |
030dd2df3399668c2634dcc9cd60dff1
|
|
| BLAKE2b-256 |
b5135c9fb5bd0f136e4e37892ceeb355b8629803f6887f4074958b232d3ba7d5
|