geomlib 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.
geomlib-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victor Phung
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.
geomlib-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: geomlib
3
+ Version: 0.1.0
4
+ Summary: A computational geometry library for Python
5
+ Author: Victor Phung
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy>=1.25
14
+ Dynamic: license-file
15
+
16
+ # geomlib
17
+
18
+ A lightweight computational geometry library for Python.
19
+
20
+ ## Features
21
+
22
+ - Points and vectors
23
+ - Lines and segments
24
+ - Circles
25
+ - Triangle centers (incenter, circumcenter, orthocenter, Lemoine point)
26
+ - Convex hull
27
+ - Rotating calipers
28
+ - Minimum bounding rectangle
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install geomlib
@@ -0,0 +1,18 @@
1
+ # geomlib
2
+
3
+ A lightweight computational geometry library for Python.
4
+
5
+ ## Features
6
+
7
+ - Points and vectors
8
+ - Lines and segments
9
+ - Circles
10
+ - Triangle centers (incenter, circumcenter, orthocenter, Lemoine point)
11
+ - Convex hull
12
+ - Rotating calipers
13
+ - Minimum bounding rectangle
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install geomlib
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "geomlib"
7
+ version = "0.1.0"
8
+ description = "A computational geometry library for Python"
9
+ readme = "README.md"
10
+ authors = [{name="Victor Phung"}]
11
+ license = {text = "MIT"}
12
+ requires-python = ">=3.9"
13
+
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ dependencies = [
21
+ "numpy>=1.25",
22
+ ]
23
+
24
+ [tool.setuptools]
25
+ package-dir = {"" = "src"}
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ from .point import Point
2
+ from .line import Line
3
+ from .circle import Circle
4
+ from .triangle import Triangle
5
+ from .vector import Vector
6
+ from .segment import Segment
7
+ from .polygon import Polygon
8
+
9
+ from .utils import cross_product, is_collinear, is_concyclic, closest_pair
10
+
11
+ # Expose the public API
12
+ __all__ = [
13
+ "Point",
14
+ "Line",
15
+ "Circle",
16
+ "Triangle",
17
+ "Vector",
18
+ "Segment",
19
+ "Polygon",
20
+ "cross_product",
21
+ "is_collinear",
22
+ "is_concyclic",
23
+ "closest_pair"
24
+ ]
@@ -0,0 +1,199 @@
1
+ from geomlib.point import Point
2
+ from geomlib.line import Line
3
+ from geomlib.constants import EPS
4
+ import math
5
+
6
+ class Circle:
7
+ __slots__ = ['center', 'radius']
8
+
9
+ def __init__(self, center: Point, radius: float):
10
+ if radius < 0:
11
+ raise ValueError("Radius must be non-negative")
12
+ self.center = center
13
+ self.radius = radius
14
+
15
+ @staticmethod
16
+ def from_points(p1: Point, p2: Point, p3: Point) -> 'Circle':
17
+ """
18
+ Returns a Circle object that passes through three points p1, p2, and p3.
19
+ """
20
+ A = 2 * (p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y))
21
+ if abs(A) < EPS:
22
+ raise ValueError("The three points are collinear.")
23
+
24
+ B = (p1.x ** 2 + p1.y ** 2) * (p2.y - p3.y) + (p2.x ** 2 + p2.y ** 2) * (p3.y - p1.y) + (p3.x ** 2 + p3.y ** 2) * (p1.y - p2.y)
25
+ C = (p1.x ** 2 + p1.y ** 2) * (p3.x - p2.x) + (p2.x ** 2 + p2.y ** 2) * (p1.x - p3.x) + (p3.x ** 2 + p3.y ** 2) * (p2.x - p1.x)
26
+
27
+ center_x = -B / A
28
+ center_y = -C / A
29
+ center = Point(center_x, center_y)
30
+ radius = center.distance_to(p1)
31
+ return Circle(center, radius)
32
+
33
+ def diameter(self) -> float:
34
+ """
35
+ Returns the diameter of the circle.
36
+ """
37
+ return 2 * self.radius
38
+
39
+ def point_at_angle(self, angle: float) -> Point:
40
+ """
41
+ Returns the point on the circle at the given angle (in radians).
42
+
43
+ Parameters
44
+ ----------
45
+ angle : float
46
+ The angle (in radians) at which to find the point.
47
+
48
+ Returns
49
+ -------
50
+ Point
51
+ The point on the circle at the given angle.
52
+ """
53
+ return Point(
54
+ self.center.x + self.radius * math.cos(angle),
55
+ self.center.y + self.radius * math.sin(angle)
56
+ )
57
+
58
+ def arc_length(self, angle: float) -> float:
59
+ """
60
+ Parameters
61
+ ----------
62
+ angle : float
63
+ The angle (in radians) of the arc.
64
+
65
+ Returns
66
+ -------
67
+ float
68
+ The length of the arc.
69
+ """
70
+ return self.radius * angle
71
+
72
+ def get_intersection(self, other: 'Circle') -> list[Point]:
73
+ """
74
+ Find the intersections of two circles.
75
+
76
+ Parameters
77
+ ----------
78
+ other : Circle
79
+ The other circle to find the intersections with.
80
+
81
+ Returns
82
+ -------
83
+ list[Point]
84
+ A list of up to two points that are the intersections of the two circles.
85
+ """
86
+
87
+ d = self.center.distance_to(other.center)
88
+ if d > self.radius + other.radius + EPS:
89
+ return [] # No intersection
90
+ if d < abs(self.radius - other.radius) - EPS:
91
+ return [] # One circle is inside the other
92
+ if d < EPS:
93
+ return []
94
+
95
+ a = (self.radius ** 2 - other.radius ** 2 + d ** 2) / (2 * d)
96
+ h = math.sqrt(max(0, self.radius ** 2 - a ** 2))
97
+ x = self.center.x + a * (other.center.x - self.center.x) / d
98
+ y = self.center.y + a * (other.center.y - self.center.y) / d
99
+ intersection1 = Point(x + h * (other.center.y - self.center.y) / d, y - h * (other.center.x - self.center.x) / d)
100
+ intersection2 = Point(x - h * (other.center.y - self.center.y) / d, y + h * (other.center.x - self.center.x) / d)
101
+
102
+ if h < EPS:
103
+ return [intersection1] # One intersection (tangent)
104
+
105
+ return [intersection1, intersection2]
106
+
107
+ def get_intersection_with_line(self, line: Line) -> list[Point]:
108
+ d = line.distance_to_point(self.center)
109
+
110
+ if d > self.radius + EPS:
111
+ return []
112
+
113
+ # projection of center onto line
114
+ denom = line.a * line.a + line.b * line.b
115
+ t = -(line.a*self.center.x + line.b*self.center.y + line.c) / denom
116
+
117
+ foot = Point(
118
+ self.center.x + line.a*t,
119
+ self.center.y + line.b*t
120
+ )
121
+
122
+ if abs(d - self.radius) < EPS:
123
+ return [foot]
124
+
125
+ offset = math.sqrt(self.radius**2 - d**2)
126
+
127
+ direction = line.get_direction_vector().normalize()
128
+
129
+ p1 = foot + direction * offset
130
+ p2 = foot - direction * offset
131
+
132
+ return [p1, p2]
133
+
134
+ def contains(self, point: Point) -> bool:
135
+ """
136
+ Check if a point is contained within the circle.
137
+
138
+ Parameters
139
+ ----------
140
+ point : Point
141
+ The point to check.
142
+
143
+ Returns
144
+ -------
145
+ bool
146
+ True if the point is contained within the circle, False otherwise.
147
+ """
148
+ return self.center.distance_to(point) <= self.radius + EPS
149
+
150
+ def intersects(self, other: 'Circle') -> bool:
151
+ """
152
+ Check if two circles intersect.
153
+
154
+ Parameters
155
+ ----------
156
+ other : Circle
157
+ The other circle to check intersection with.
158
+
159
+ Returns
160
+ -------
161
+ bool
162
+ True if the two circles intersect, False otherwise.
163
+ """
164
+ return self.center.distance_to(other.center) <= self.radius + other.radius + EPS
165
+
166
+ def area(self) -> float:
167
+ """
168
+ Calculate the area of the circle.
169
+
170
+ Returns
171
+ -------
172
+ float
173
+ The area of the circle.
174
+ """
175
+ return math.pi * self.radius ** 2
176
+
177
+ def perimeter(self) -> float:
178
+ """
179
+ Calculate the perimeter of the circle.
180
+
181
+ Returns
182
+ -------
183
+ float
184
+ The perimeter of the circle.
185
+ """
186
+ return 2 * math.pi * self.radius
187
+
188
+ def __str__(self) -> str:
189
+ return f"Circle(center={self.center}, radius={self.radius})"
190
+
191
+ def __repr__(self):
192
+ return f"Circle({self.center!r}, {self.radius})"
193
+
194
+ def __eq__(self, other) -> bool:
195
+ return self.center == other.center and abs(self.radius - other.radius) < EPS
196
+
197
+ def __ne__(self, other) -> bool:
198
+ return not self == other
199
+
@@ -0,0 +1 @@
1
+ EPS = 1e-12
@@ -0,0 +1,113 @@
1
+ from geomlib.point import Point
2
+ from geomlib.vector import Vector
3
+ from geomlib.constants import EPS
4
+ import math
5
+
6
+ class Line:
7
+ __slots__ = ['a', 'b', 'c']
8
+
9
+ def __init__(self, p: Point, q: Point):
10
+ if p == q:
11
+ raise ValueError("Cannot define line from identical points")
12
+
13
+ self.a = p.y - q.y
14
+ self.b = q.x - p.x
15
+ self.c = p.x * q.y - q.x * p.y
16
+
17
+ @staticmethod
18
+ def from_coeff(a: float, b: float, c: float) -> "Line":
19
+ line = Line.__new__(Line)
20
+ line.a = a
21
+ line.b = b
22
+ line.c = c
23
+ return line
24
+
25
+ @staticmethod
26
+ def from_point_and_normal_vector(p: Point, n: Vector) -> "Line":
27
+ a = n.x
28
+ b = n.y
29
+ c = -(a * p.x + b * p.y)
30
+ return Line.from_coeff(a, b, c)
31
+
32
+ def intersect(self, other: "Line") -> Point:
33
+ """
34
+ Compute the intersection point of two lines.
35
+
36
+ :param other: The other line to intersect with.
37
+ :type other: Line
38
+ :return: The intersection point of the two lines, or None if the lines are parallel.
39
+ :rtype: Point or None
40
+ """
41
+ det = self.a * other.b - other.a * self.b
42
+ if abs(det) < EPS:
43
+ return None # Lines are parallel
44
+
45
+ x = (self.b * other.c - other.b * self.c) / det
46
+ y = (self.c * other.a - other.c * self.a) / det
47
+ return Point(x, y)
48
+
49
+ def distance_to_point(self, point: Point) -> float:
50
+ """
51
+ Compute the distance from a point to a line.
52
+
53
+ :param point: The point to compute the distance to.
54
+ :type point: Point
55
+ :return: The distance from the point to the line.
56
+ :rtype: float
57
+ """
58
+ return abs(self.a * point.x + self.b * point.y + self.c) / math.hypot(self.a, self.b)
59
+
60
+ def get_normal_vector(self) -> Vector:
61
+ """
62
+ Return a vector perpendicular to the line.
63
+
64
+ The returned vector is of the same magnitude as the line, but is perpendicular to it.
65
+
66
+ :return: A vector perpendicular to the line.
67
+ :rtype: Vector
68
+ """
69
+ return Vector(self.a, self.b)
70
+
71
+ def get_direction_vector(self) -> Vector:
72
+ """
73
+ Return a vector parallel to the line.
74
+
75
+ The returned vector is of the same magnitude as the line, but is parallel to it.
76
+
77
+ :return: A vector parallel to the line.
78
+ :rtype: Vector
79
+ """
80
+ return Vector(self.b, -self.a)
81
+
82
+ def contains(self, point: Point) -> bool:
83
+ """
84
+ Check if a point lies on the line.
85
+
86
+ :param point: The point to check.
87
+ :type point: Point
88
+ :return: True if the point lies on the line, False otherwise.
89
+ :rtype: bool
90
+ """
91
+ return abs(self.a * point.x + self.b * point.y + self.c) / math.hypot(self.a, self.b) < EPS
92
+
93
+ def __eq__(self, other):
94
+ if not isinstance(other, Line):
95
+ return NotImplemented
96
+
97
+ return (
98
+ abs(self.a * other.b - other.a * self.b) < EPS and
99
+ abs(self.a * other.c - other.a * self.c) < EPS and
100
+ abs(self.b * other.c - other.b * self.c) < EPS
101
+ )
102
+
103
+ def __ne__(self, other):
104
+ if isinstance(other, Line):
105
+ return not self.__eq__(other)
106
+ return NotImplemented
107
+
108
+ def __str__(self):
109
+ return f"{self.a}x + {self.b}y + {self.c} = 0"
110
+
111
+ def __repr__(self):
112
+ return f"Line({self.a}, {self.b}, {self.c})"
113
+
@@ -0,0 +1,136 @@
1
+ from typing import TYPE_CHECKING
2
+ import math
3
+ from geomlib.constants import EPS
4
+
5
+ # avoid circular import
6
+ if TYPE_CHECKING:
7
+ from geomlib.vector import Vector
8
+
9
+ class Point:
10
+ __slots__ = ["x", "y"]
11
+
12
+ def __init__(self, x: float, y: float):
13
+ self.x = x
14
+ self.y = y
15
+
16
+ def distance_to(self, other: 'Point') -> float:
17
+ """
18
+ Calculate the Euclidean distance between two points self and other.
19
+
20
+ :param other: The other point to calculate the distance to.
21
+ :type other: Point
22
+ :return: The Euclidean distance between self and other.
23
+ :rtype: float
24
+ :raises TypeError: If other is not a Point instance.
25
+ """
26
+ if isinstance(other, Point):
27
+ return math.hypot(self.x - other.x, self.y - other.y)
28
+ raise TypeError("Distance can only be calculated between two Point instances")
29
+
30
+ def cross3(self, A: 'Point', B: 'Point') -> float:
31
+ """
32
+ Calculate the cross product of the vectors self->A and self->B.
33
+
34
+ The cross product is positive if B is to the left of A (counter-clockwise direction),
35
+ negative if B is to the right of A (clockwise direction), and zero if A, B and self are collinear.
36
+
37
+ :return: The cross product of the vectors self->A and self->B.
38
+ :rtype: float
39
+ """
40
+ return (A.x - self.x) * (B.y - self.y) - (A.y - self.y) * (B.x - self.x)
41
+
42
+ def cross(self, other: 'Point') -> float:
43
+ """
44
+ Return the 2D cross product of vectors from the origin to self and other.
45
+ Equivalent to determinant |self other|.
46
+
47
+ :return: The cross product of the vectors self->other.
48
+ :rtype: float
49
+ """
50
+ if isinstance(other, Point):
51
+ return self.x * other.y - self.y * other.x
52
+ return NotImplemented
53
+
54
+ def midpoint(self, other):
55
+ """
56
+ Calculate the midpoint between two points self and other.
57
+
58
+ :param other: The other point to calculate the midpoint with.
59
+ :type other: Point
60
+ :return: The midpoint between self and other.
61
+ :rtype: Point
62
+ """
63
+ return (self + other) / 2
64
+
65
+ def norm(self):
66
+ """
67
+ Calculate the Euclidean norm of the point self.
68
+
69
+ :return: The Euclidean norm of the point self.
70
+ :rtype: float
71
+ """
72
+ return math.hypot(self.x, self.y)
73
+
74
+ def rotate(self, angle: float, in_radian: bool = False) -> 'Point':
75
+ """
76
+ Rotate the point self by an angle in either degrees or radians.
77
+
78
+ :param angle: The angle to rotate the point by.
79
+ :type angle: float
80
+ :param in_radian: If the angle is given in radians (True) or degrees (False).
81
+ :type in_radian: bool
82
+ :return: The rotated point.
83
+ :rtype: Point
84
+ :raises TypeError: If angle is not a float instance.
85
+ """
86
+ if not in_radian:
87
+ angle = math.radians(angle)
88
+ return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.x * math.sin(angle) + self.y * math.cos(angle))
89
+
90
+ def __add__(self, other):
91
+ if isinstance(other, Vector):
92
+ return Point(self.x + other.x, self.y + other.y)
93
+ return NotImplemented
94
+
95
+ def __sub__(self, other):
96
+ if isinstance(other, Point):
97
+ return Vector(self.x - other.x, self.y - other.y)
98
+ if isinstance(other, Vector):
99
+ return Point(self.x - other.x, self.y - other.y)
100
+ return NotImplemented
101
+
102
+ def __mul__(self, other):
103
+ if isinstance(other, (int, float)):
104
+ return Point(self.x * other, self.y * other)
105
+ return NotImplemented
106
+
107
+ def __rmul__(self, other):
108
+ return self.__mul__(other)
109
+
110
+ def __truediv__(self, other):
111
+ if isinstance(other, (int, float)):
112
+ if other == 0:
113
+ raise ZeroDivisionError("Cannot divide by zero")
114
+ return Point(self.x / other, self.y / other)
115
+ return NotImplemented
116
+
117
+ def __eq__(self, other):
118
+ if isinstance(other, Point):
119
+ return abs(self.x - other.x) < EPS and abs(self.y - other.y) < EPS
120
+ return NotImplemented
121
+
122
+ def __ne__(self, other):
123
+ if isinstance(other, Point):
124
+ return not self.__eq__(other)
125
+ return NotImplemented
126
+
127
+ def __str__(self):
128
+ return f"({self.x}, {self.y})"
129
+
130
+ def __repr__(self):
131
+ return f"Point({self.x}, {self.y})"
132
+
133
+ def __lt__(self, other):
134
+ if not isinstance(other, Point):
135
+ return NotImplemented
136
+ return (self.x, self.y) < (other.x, other.y)