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 +21 -0
- geomlib-0.1.0/PKG-INFO +33 -0
- geomlib-0.1.0/README.md +18 -0
- geomlib-0.1.0/pyproject.toml +28 -0
- geomlib-0.1.0/setup.cfg +4 -0
- geomlib-0.1.0/src/geomlib/__init__.py +24 -0
- geomlib-0.1.0/src/geomlib/circle.py +199 -0
- geomlib-0.1.0/src/geomlib/constants.py +1 -0
- geomlib-0.1.0/src/geomlib/line.py +113 -0
- geomlib-0.1.0/src/geomlib/point.py +136 -0
- geomlib-0.1.0/src/geomlib/polygon.py +350 -0
- geomlib-0.1.0/src/geomlib/segment.py +91 -0
- geomlib-0.1.0/src/geomlib/triangle.py +815 -0
- geomlib-0.1.0/src/geomlib/utils.py +139 -0
- geomlib-0.1.0/src/geomlib/vector.py +214 -0
- geomlib-0.1.0/src/geomlib.egg-info/PKG-INFO +33 -0
- geomlib-0.1.0/src/geomlib.egg-info/SOURCES.txt +19 -0
- geomlib-0.1.0/src/geomlib.egg-info/dependency_links.txt +1 -0
- geomlib-0.1.0/src/geomlib.egg-info/requires.txt +1 -0
- geomlib-0.1.0/src/geomlib.egg-info/top_level.txt +1 -0
- geomlib-0.1.0/tests/test_geometry.py +0 -0
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
|
geomlib-0.1.0/README.md
ADDED
|
@@ -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"]
|
geomlib-0.1.0/setup.cfg
ADDED
|
@@ -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)
|