engeom 0.1.2__cp38-abi3-win32.whl → 0.2.1__cp38-abi3-win32.whl
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.
- engeom/airfoil/__init__.py +5 -0
- engeom/airfoil.pyi +334 -0
- engeom/align.pyi +2 -2
- engeom/engeom.pyd +0 -0
- engeom/engeom.pyi +3 -3
- engeom/geom2.pyi +234 -16
- engeom/geom3.pyi +66 -26
- engeom/matplotlib.py +189 -29
- engeom/metrology/__init__.py +5 -0
- engeom/metrology.pyi +32 -0
- engeom/pyvista.py +43 -42
- {engeom-0.1.2.dist-info → engeom-0.2.1.dist-info}/METADATA +1 -1
- engeom-0.2.1.dist-info/RECORD +18 -0
- engeom-0.1.2.dist-info/RECORD +0 -14
- {engeom-0.1.2.dist-info → engeom-0.2.1.dist-info}/WHEEL +0 -0
engeom/airfoil.pyi
ADDED
@@ -0,0 +1,334 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
import numpy
|
4
|
+
from enum import Enum
|
5
|
+
|
6
|
+
from .geom2 import Circle2, Curve2, Point2, SurfacePoint2, Arc2
|
7
|
+
from .metrology import Length2
|
8
|
+
|
9
|
+
type MclOrientEnum = MclOrient.TmaxFwd | MclOrient.DirFwd
|
10
|
+
type FaceOrientEnum = FaceOrient.Detect | FaceOrient.UpperDir
|
11
|
+
type EdgeFindEnum = EdgeFind.Open | EdgeFind.OpenIntersect | EdgeFind.Intersect | EdgeFind.RansacRadius
|
12
|
+
type EdgeTypeEnum = EdgeType | Arc2
|
13
|
+
type AfGageEnum = AfGage.OnCamber | AfGage.Radius
|
14
|
+
|
15
|
+
class EdgeType(Enum):
|
16
|
+
Open=0
|
17
|
+
Closed=1
|
18
|
+
|
19
|
+
class AfGage:
|
20
|
+
"""
|
21
|
+
A class representing a measurement for locating a position on an airfoil cross-section.
|
22
|
+
"""
|
23
|
+
class OnCamber:
|
24
|
+
def __init__(self, d: float):
|
25
|
+
"""
|
26
|
+
A gaging method that measures a distance along the mean camber line. A positive distance will be from the
|
27
|
+
leading edge towards the trailing edge, and a negative distance will be from the trailing edge towards the
|
28
|
+
leading edge.
|
29
|
+
:param d: the distance along the mean camber line to find the position
|
30
|
+
"""
|
31
|
+
...
|
32
|
+
|
33
|
+
class Radius:
|
34
|
+
def __init__(self, r: float):
|
35
|
+
"""
|
36
|
+
A gaging method that measures by intersection with a circle of a given radius centered on either the
|
37
|
+
leading or trailing edge point. A positive radius indicates that the circle is located on the leading edge
|
38
|
+
while a negative radius indicates that the circle is located on the trailing edge.
|
39
|
+
:param r: the radius of the circle to find the position
|
40
|
+
"""
|
41
|
+
...
|
42
|
+
|
43
|
+
class FaceOrient:
|
44
|
+
"""
|
45
|
+
An enumeration of the possible ways to orient the upper/lower (suction/pressure, convex/concave) faces of an
|
46
|
+
airfoil cross-section.
|
47
|
+
"""
|
48
|
+
|
49
|
+
class Detect:
|
50
|
+
"""
|
51
|
+
In an airfoil with an MCL that exhibits curvature, this will attempt to detect which direction the camber line
|
52
|
+
curves and thus identify convex/concave. This will fail if the MCL is straight.
|
53
|
+
"""
|
54
|
+
...
|
55
|
+
|
56
|
+
class UpperDir:
|
57
|
+
"""
|
58
|
+
This method will orient the faces based on a vector direction provided by the user.
|
59
|
+
"""
|
60
|
+
|
61
|
+
def __init__(self, x: float, y: float):
|
62
|
+
"""
|
63
|
+
Create a new upper direction parameter. The x and y arguments are components of a direction vector which
|
64
|
+
should distinguish the upper (pressure side, convex) face of the airfoil. At the center of the mean camber
|
65
|
+
line, an intersection in this direction will be taken with each of the two faces. The intersection that
|
66
|
+
is further in the direction of this vector will be considered the upper face of the airfoil, and the other
|
67
|
+
will be considered the lower face.
|
68
|
+
|
69
|
+
:param x: the x component of the upper direction vector
|
70
|
+
:param y: the y component of the upper direction vector
|
71
|
+
"""
|
72
|
+
...
|
73
|
+
|
74
|
+
|
75
|
+
class MclOrient:
|
76
|
+
"""
|
77
|
+
An enumeration of the possible ways to orient (to identify which side is the leading edge and which side is the
|
78
|
+
trailing edge) the mean camber line of an airfoil.
|
79
|
+
"""
|
80
|
+
|
81
|
+
class TmaxFwd:
|
82
|
+
"""
|
83
|
+
This method will take advantage of the fact that for most typical subsonic airfoils the maximum thickness point
|
84
|
+
is closer to the leading edge than the trailing edge.
|
85
|
+
"""
|
86
|
+
...
|
87
|
+
|
88
|
+
class DirFwd:
|
89
|
+
"""
|
90
|
+
This method will orient the airfoil based on a vector direction provided by the user.
|
91
|
+
"""
|
92
|
+
|
93
|
+
def __init__(self, x: float, y: float):
|
94
|
+
"""
|
95
|
+
Create a new forward direction parameter. The x and y arguments are components of a direction vector which
|
96
|
+
should distinguish the forward (leading edge) direction of the airfoil. The position of the first and last
|
97
|
+
inscribed circle will be projected onto this vector, and the larger result (the one that is more in the
|
98
|
+
direction of this vector) will be considered the leading edge of the airfoil.
|
99
|
+
|
100
|
+
For instance, if you know that the airfoil is oriented so that the leading edge will have a smaller x value
|
101
|
+
than the trailing edge, `DirFwd(-1, 0)` will correctly orient the airfoil.
|
102
|
+
:param x: the x component of the forward direction vector
|
103
|
+
:param y: the y component of the forward direction vector
|
104
|
+
"""
|
105
|
+
...
|
106
|
+
|
107
|
+
|
108
|
+
class EdgeFind:
|
109
|
+
"""
|
110
|
+
An enumeration of the possible techniques to find the leading and/or trailing edge geometry of an airfoil.
|
111
|
+
"""
|
112
|
+
|
113
|
+
class Open:
|
114
|
+
"""
|
115
|
+
This algorithm will not attempt to find edge geometry, and will simply leave the inscribed circles for the side
|
116
|
+
as they are. Use this if you know that the airfoil cross-section is open/incomplete on this side, and you don't
|
117
|
+
care to extend the MCL any further.
|
118
|
+
"""
|
119
|
+
...
|
120
|
+
|
121
|
+
class OpenIntersect:
|
122
|
+
def __init__(self, max_iter: int):
|
123
|
+
"""
|
124
|
+
This algorithm will attempt to find the edge geometry by intersecting the end of the inscribed circles
|
125
|
+
camber curve with the open gap in the airfoil cross-section, then refining the end of the MCL with more
|
126
|
+
inscribed circles until the location of the end converges to within 1/100th of the general refinement
|
127
|
+
tolerance.
|
128
|
+
|
129
|
+
If the maximum number of iterations is reached before convergence, the method will throw an error instead.
|
130
|
+
|
131
|
+
:param max_iter: the maximum number of iterations to attempt to find the edge geometry
|
132
|
+
"""
|
133
|
+
...
|
134
|
+
|
135
|
+
class Intersect:
|
136
|
+
"""
|
137
|
+
This algorithm will simply intersect the end of the inscribed circles camber curve with the airfoil
|
138
|
+
cross-section. This is the fastest method with the least amount of assumptions, and makes sense for airfoil
|
139
|
+
edges where you know the mean camber line has very low curvature in the vicinity of the edge.
|
140
|
+
"""
|
141
|
+
...
|
142
|
+
|
143
|
+
class RansacRadius:
|
144
|
+
def __init__(self, in_tol: float, n: int = 500):
|
145
|
+
"""
|
146
|
+
This algorithm uses RANSAC (Random Sample Consensus) to find a constant radius leading edge circle that
|
147
|
+
fits the greatest number of points leftover at the edge within the tolerance `in_tol`.
|
148
|
+
|
149
|
+
The method will try `n` different combinations of three points picked at random from the remaining points
|
150
|
+
at the edge, construct a circle, and then count the number of points within `in_tol` distance of the circle
|
151
|
+
perimeter. The circle with the most points within tolerance will be considered the last inscribed circle.
|
152
|
+
|
153
|
+
The MCL will be extended to this final circle, and then intersected with the airfoil cross-section to find
|
154
|
+
the final edge point.
|
155
|
+
|
156
|
+
:param in_tol: the max distance from the circle perimeter for a point to be considered a RANSAC inlier
|
157
|
+
:param n: The number of RANSAC iterations to perform
|
158
|
+
"""
|
159
|
+
...
|
160
|
+
|
161
|
+
|
162
|
+
class InscribedCircle:
|
163
|
+
@property
|
164
|
+
def circle(self) -> Circle2: ...
|
165
|
+
|
166
|
+
@property
|
167
|
+
def contact_a(self) -> Point2:
|
168
|
+
"""
|
169
|
+
A contact point of the inscribed circle with one side of the airfoil cross-section. Inscribed circles computed
|
170
|
+
together will have a consistent meaning of `a` and `b` sides, but which is the upper or lower surface will
|
171
|
+
depend on the ordering of the circles and the coordinate system of the airfoil.
|
172
|
+
"""
|
173
|
+
...
|
174
|
+
|
175
|
+
@property
|
176
|
+
def contact_b(self) -> Point2:
|
177
|
+
"""
|
178
|
+
The other contact point of the inscribed circle with the airfoil cross-section. Inscribed circles computed
|
179
|
+
together will have a consistent meaning of `a` and `b` sides, but which is the upper or lower surface will
|
180
|
+
depend on the ordering of the circles and the coordinate system of the airfoil.
|
181
|
+
"""
|
182
|
+
...
|
183
|
+
|
184
|
+
|
185
|
+
class EdgeResult:
|
186
|
+
"""
|
187
|
+
Represents the results of an edge detection algorithm
|
188
|
+
"""
|
189
|
+
|
190
|
+
@property
|
191
|
+
def point(self) -> Point2:
|
192
|
+
"""
|
193
|
+
The point on the airfoil cross-section that was detected as the edge.
|
194
|
+
"""
|
195
|
+
...
|
196
|
+
|
197
|
+
@property
|
198
|
+
def geometry(self):
|
199
|
+
...
|
200
|
+
|
201
|
+
|
202
|
+
class AirfoilGeometry:
|
203
|
+
"""
|
204
|
+
The result of an airfoil geometry computation.
|
205
|
+
"""
|
206
|
+
|
207
|
+
@staticmethod
|
208
|
+
def from_analyze(
|
209
|
+
section: Curve2,
|
210
|
+
refine_tol: float,
|
211
|
+
camber_orient: MclOrientEnum,
|
212
|
+
leading: EdgeFindEnum,
|
213
|
+
trailing: EdgeFindEnum,
|
214
|
+
face_orient: FaceOrientEnum,
|
215
|
+
) -> AirfoilGeometry:
|
216
|
+
...
|
217
|
+
|
218
|
+
@property
|
219
|
+
def leading(self) -> EdgeResult | None:
|
220
|
+
"""
|
221
|
+
The result of the leading edge detection algorithm.
|
222
|
+
"""
|
223
|
+
...
|
224
|
+
|
225
|
+
@property
|
226
|
+
def trailing(self) -> EdgeResult | None:
|
227
|
+
"""
|
228
|
+
The result of the trailing edge detection algorithm.
|
229
|
+
"""
|
230
|
+
...
|
231
|
+
|
232
|
+
@property
|
233
|
+
def camber(self) -> Curve2:
|
234
|
+
"""
|
235
|
+
The mean camber line of the airfoil cross-section. The curve will be oriented so that the first point is at
|
236
|
+
the leading edge of the airfoil and the last point is at the trailing edge.
|
237
|
+
:return:
|
238
|
+
"""
|
239
|
+
...
|
240
|
+
|
241
|
+
@property
|
242
|
+
def upper(self) -> Curve2 | None:
|
243
|
+
"""
|
244
|
+
The curve representing the upper (suction, convex) side of the airfoil cross-section. The curve will be oriented
|
245
|
+
in the same winding direction as the original section, so the first point may be at either the leading or
|
246
|
+
trailing edge based on the airfoil geometry and the coordinate system.
|
247
|
+
|
248
|
+
:return: A Curve2, or None if there was an issue detecting the leading or trailing edge.
|
249
|
+
"""
|
250
|
+
...
|
251
|
+
|
252
|
+
@property
|
253
|
+
def lower(self) -> Curve2 | None:
|
254
|
+
"""
|
255
|
+
The curve representing the lower (pressure, concave) side of the airfoil cross-section. The curve will be
|
256
|
+
oriented in the same winding direction as the original section, so the first point may be at either the leading
|
257
|
+
or trailing edge based on the airfoil geometry and the coordinate system.
|
258
|
+
|
259
|
+
:return: A Curve2, or None if there was an issue detecting the leading or trailing edge.
|
260
|
+
"""
|
261
|
+
...
|
262
|
+
|
263
|
+
@property
|
264
|
+
def circle_array(self) -> numpy.ndarray[float]:
|
265
|
+
"""
|
266
|
+
Returns the list of inscribed circles as a numpy array of shape (N, 3) where N is the number of inscribed
|
267
|
+
circles. The first two columns are the x and y coordinates of the circle center, and the third column is the
|
268
|
+
radius of the circle.
|
269
|
+
"""
|
270
|
+
...
|
271
|
+
|
272
|
+
def get_thickness(self, gage: AfGageEnum) -> Length2:
|
273
|
+
"""
|
274
|
+
Get the thickness dimension of the airfoil cross-section.
|
275
|
+
:param gage: the gaging method to use
|
276
|
+
:return:
|
277
|
+
"""
|
278
|
+
...
|
279
|
+
|
280
|
+
def get_tmax(self) -> Length2:
|
281
|
+
"""
|
282
|
+
Get the maximum thickness dimension of the airfoil cross-section.
|
283
|
+
:return:
|
284
|
+
"""
|
285
|
+
...
|
286
|
+
|
287
|
+
def get_tmax_circle(self) -> Circle2:
|
288
|
+
"""
|
289
|
+
Get the circle representing the maximum thickness dimension of the airfoil cross-section.
|
290
|
+
:return:
|
291
|
+
"""
|
292
|
+
...
|
293
|
+
|
294
|
+
|
295
|
+
def compute_inscribed_circles(section: Curve2, refine_tol: float) -> List[InscribedCircle]:
|
296
|
+
"""
|
297
|
+
Compute the unambiguous inscribed circles of an airfoil cross-section.
|
298
|
+
|
299
|
+
The cross-section is represented by a curve in the x-y plane. The curve does not need to be closed, but the points
|
300
|
+
should be oriented in a counter-clockwise direction and should only contain data from the outer surface of the
|
301
|
+
airfoil (internal features/points should not be part of the data).
|
302
|
+
|
303
|
+
The method used to compute these circles is:
|
304
|
+
|
305
|
+
1. We calculate the convex hull of the points in the section and find the longest distance between any two points.
|
306
|
+
2. At the center of the longest distance line, we draw a perpendicular line and look for exactly two intersections
|
307
|
+
with the section. We assume that one of these is on the upper surface of the airfoil and the other is on the
|
308
|
+
lower, though it does not matter which is which.
|
309
|
+
3. We fit the maximum inscribed circle whose center is constrained to the line between these two points. The
|
310
|
+
location and radius of this circle is refined until it converges to within 1/100th of `refine_tol`.
|
311
|
+
4. The inscribed circle has two contact points with the section. The line between these contact points is a good
|
312
|
+
approximation of the direction orthogonal to the mean camber line near the circle. We create a parallel line
|
313
|
+
to this one, advancing from the circle center by 1/4 of the circle radius, and looking for exactly two
|
314
|
+
intersections with the section. If we fail, we try again with a slightly less aggressive advancement until we
|
315
|
+
either succeed or give up.
|
316
|
+
5. We fit the maximum inscribed circle whose center is constrained to the new line, and refine it as in step 3.
|
317
|
+
6. We recursively fit inscribed circles between this new circle and the previous one until the error between the
|
318
|
+
position and radius of any circle is less than `refine_tol` from the linear interpolation between its next and
|
319
|
+
previous neighbors.
|
320
|
+
7. We repeat the process from step 4 until the distance between the center of the most recent circle and the
|
321
|
+
farthest point in the direction of the next advancement is less than 1/4 of the radius of the most recent
|
322
|
+
circle. This terminates the process before we get too close to the leading or trailing edge of the airfoil.
|
323
|
+
8. We repeat the process from step 3, but this time in the opposite direction from the first circle. This will
|
324
|
+
give us the inscribed circles on the other side of the airfoil.
|
325
|
+
|
326
|
+
When finished, we have a list of inscribed circles from the unambiguous regions (not too close to the leading or
|
327
|
+
trailing edges) of the airfoil cross-section. The circles are ordered from one side of the airfoil to the other,
|
328
|
+
but the order may be *either* from the leading to the trailing edge *or* vice versa.
|
329
|
+
|
330
|
+
:param section: the curve representing the airfoil cross-section.
|
331
|
+
:param refine_tol: a tolerance used when refining the inscribed circles, see description for details.
|
332
|
+
:return: a list of inscribed circle objects whose order is contiguous but may be in either direction
|
333
|
+
"""
|
334
|
+
...
|
engeom/align.pyi
CHANGED
engeom/engeom.pyd
CHANGED
Binary file
|
engeom/engeom.pyi
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
from enum import Enum
|
3
3
|
|
4
|
-
type Resample =
|
4
|
+
type Resample = Resample_Count | Resample_Spacing | Resample_MaxSpacing
|
5
5
|
|
6
6
|
class DeviationMode(Enum):
|
7
|
-
|
8
|
-
|
7
|
+
Point = 0
|
8
|
+
Plane = 1
|
engeom/geom2.pyi
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
import numpy
|
3
|
-
from typing import Iterable, Tuple
|
3
|
+
from typing import Iterable, Tuple, Type, TypeVar
|
4
4
|
|
5
5
|
from engeom.engeom import Resample
|
6
6
|
|
7
|
-
|
7
|
+
Transformable2 = TypeVar("Transformable2", Vector2, Point2, Iso2, SurfacePoint2)
|
8
|
+
PointOrVec2 = TypeVar("PointOrVec2", Point2, Vector2)
|
8
9
|
|
9
10
|
|
10
|
-
class Vector2:
|
11
|
+
class Vector2(Iterable[float]):
|
11
12
|
def __init__(self, x: float, y: float):
|
12
13
|
"""
|
13
14
|
|
@@ -24,13 +25,10 @@ class Vector2:
|
|
24
25
|
def y(self) -> float:
|
25
26
|
...
|
26
27
|
|
27
|
-
def __iter__(self) -> Iterable[float]:
|
28
|
-
...
|
29
|
-
|
30
28
|
def __rmul__(self, other: float) -> Vector2:
|
31
29
|
...
|
32
30
|
|
33
|
-
def __add__(self, other:
|
31
|
+
def __add__(self, other: PointOrVec2) -> PointOrVec2:
|
34
32
|
...
|
35
33
|
|
36
34
|
def __sub__(self, other: Vector2) -> Vector2:
|
@@ -79,7 +77,7 @@ class Vector2:
|
|
79
77
|
...
|
80
78
|
|
81
79
|
|
82
|
-
class Point2:
|
80
|
+
class Point2(Iterable[float]):
|
83
81
|
def __init__(self, x: float, y: float):
|
84
82
|
"""
|
85
83
|
|
@@ -96,9 +94,6 @@ class Point2:
|
|
96
94
|
def y(self) -> float:
|
97
95
|
...
|
98
96
|
|
99
|
-
def __iter__(self) -> Iterable[float]:
|
100
|
-
...
|
101
|
-
|
102
97
|
@property
|
103
98
|
def coords(self) -> Vector2:
|
104
99
|
"""
|
@@ -107,7 +102,7 @@ class Point2:
|
|
107
102
|
"""
|
108
103
|
...
|
109
104
|
|
110
|
-
def __sub__(self, other:
|
105
|
+
def __sub__(self, other: PointOrVec2) -> PointOrVec2:
|
111
106
|
...
|
112
107
|
|
113
108
|
def __add__(self, other: Vector2) -> Vector2:
|
@@ -194,6 +189,17 @@ class SurfacePoint2:
|
|
194
189
|
"""
|
195
190
|
...
|
196
191
|
|
192
|
+
def shift_orthogonal(self, distance: float) -> SurfacePoint2:
|
193
|
+
"""
|
194
|
+
Shift the surface point by a distance orthogonal to the normal vector. The direction of travel is the surface
|
195
|
+
point's normal vector rotated 90 degrees clockwise. For instance, if the normal vector is (0, 1), a positive
|
196
|
+
distance will move the point to the right and a negative distance will move the point to the left.
|
197
|
+
|
198
|
+
:param distance: the distance to shift the surface point.
|
199
|
+
:return: a new surface point shifted by the given distance.
|
200
|
+
"""
|
201
|
+
...
|
202
|
+
|
197
203
|
|
198
204
|
class Iso2:
|
199
205
|
def __init__(self, tx: float, ty: float, r: float):
|
@@ -212,7 +218,7 @@ class Iso2:
|
|
212
218
|
"""
|
213
219
|
...
|
214
220
|
|
215
|
-
def __matmul__(self, other:
|
221
|
+
def __matmul__(self, other: Transformable2) -> Transformable2:
|
216
222
|
...
|
217
223
|
|
218
224
|
def inverse(self) -> Iso2:
|
@@ -316,7 +322,7 @@ class Curve2:
|
|
316
322
|
self,
|
317
323
|
vertices: numpy.ndarray,
|
318
324
|
normals: numpy.ndarray | None = None,
|
319
|
-
tol: float
|
325
|
+
tol: float = 1e-6,
|
320
326
|
force_closed: bool = False,
|
321
327
|
hull_ccw: bool = False,
|
322
328
|
):
|
@@ -479,9 +485,10 @@ class Curve2:
|
|
479
485
|
"""
|
480
486
|
...
|
481
487
|
|
482
|
-
|
488
|
+
@property
|
489
|
+
def points(self) -> numpy.ndarray[float]:
|
483
490
|
"""
|
484
|
-
|
491
|
+
Get the points of the curve.
|
485
492
|
:return: a numpy array of shape (N, 2) representing the points of the curve.
|
486
493
|
"""
|
487
494
|
...
|
@@ -514,3 +521,214 @@ class Curve2:
|
|
514
521
|
:return: a new curve object with the transformed vertices.
|
515
522
|
"""
|
516
523
|
...
|
524
|
+
|
525
|
+
|
526
|
+
class Circle2:
|
527
|
+
def __init__(self, x: float, y: float, r: float):
|
528
|
+
"""
|
529
|
+
|
530
|
+
:param x:
|
531
|
+
:param y:
|
532
|
+
:param r:
|
533
|
+
"""
|
534
|
+
...
|
535
|
+
|
536
|
+
|
537
|
+
@property
|
538
|
+
def center(self) -> Point2:
|
539
|
+
"""
|
540
|
+
Get the center of the circle.
|
541
|
+
:return: the center of the circle.
|
542
|
+
"""
|
543
|
+
...
|
544
|
+
|
545
|
+
@property
|
546
|
+
def x(self) -> float:
|
547
|
+
"""
|
548
|
+
Get the x-coordinate of the circle.
|
549
|
+
:return: the x-coordinate of the circle.
|
550
|
+
"""
|
551
|
+
...
|
552
|
+
|
553
|
+
@property
|
554
|
+
def y(self) -> float:
|
555
|
+
"""
|
556
|
+
Get the y-coordinate of the circle.
|
557
|
+
:return: the y-coordinate of the circle.
|
558
|
+
"""
|
559
|
+
...
|
560
|
+
|
561
|
+
@property
|
562
|
+
def r(self) -> float:
|
563
|
+
"""
|
564
|
+
Get the radius of the circle.
|
565
|
+
:return: the radius of the circle.
|
566
|
+
"""
|
567
|
+
...
|
568
|
+
|
569
|
+
@property
|
570
|
+
def aabb(self) -> Aabb2:
|
571
|
+
"""
|
572
|
+
Get the axis-aligned bounding box of the circle.
|
573
|
+
:return: the axis-aligned bounding box of the circle.
|
574
|
+
"""
|
575
|
+
...
|
576
|
+
|
577
|
+
|
578
|
+
class Arc2:
|
579
|
+
def __init__(self, x: float, y: float, r: float, start_radians: float, sweep_radians: float):
|
580
|
+
"""
|
581
|
+
|
582
|
+
:param x:
|
583
|
+
:param y:
|
584
|
+
:param r:
|
585
|
+
:param start_radians:
|
586
|
+
:param sweep_radians:
|
587
|
+
"""
|
588
|
+
|
589
|
+
@property
|
590
|
+
def center(self) -> Point2:
|
591
|
+
"""
|
592
|
+
Get the center of the arc.
|
593
|
+
:return: the center of the arc.
|
594
|
+
"""
|
595
|
+
...
|
596
|
+
|
597
|
+
@property
|
598
|
+
def x(self) -> float:
|
599
|
+
"""
|
600
|
+
Get the x-coordinate of the arc.
|
601
|
+
:return: the x-coordinate of the arc.
|
602
|
+
"""
|
603
|
+
...
|
604
|
+
|
605
|
+
@property
|
606
|
+
def y(self) -> float:
|
607
|
+
"""
|
608
|
+
Get the y-coordinate of the arc.
|
609
|
+
:return: the y-coordinate of the arc.
|
610
|
+
"""
|
611
|
+
...
|
612
|
+
|
613
|
+
@property
|
614
|
+
def r(self) -> float:
|
615
|
+
"""
|
616
|
+
Get the radius of the arc.
|
617
|
+
:return: the radius of the arc.
|
618
|
+
"""
|
619
|
+
...
|
620
|
+
|
621
|
+
@property
|
622
|
+
def start(self) -> float:
|
623
|
+
"""
|
624
|
+
Get the start angle of the arc in radians.
|
625
|
+
:return: the start angle of the arc in radians.
|
626
|
+
"""
|
627
|
+
...
|
628
|
+
|
629
|
+
@property
|
630
|
+
def sweep(self) -> float:
|
631
|
+
"""
|
632
|
+
Get the sweep angle of the arc in radians.
|
633
|
+
:return: the sweep angle of the arc in radians.
|
634
|
+
"""
|
635
|
+
...
|
636
|
+
|
637
|
+
@property
|
638
|
+
def aabb(self) -> Aabb2:
|
639
|
+
"""
|
640
|
+
Get the axis-aligned bounding box of the arc.
|
641
|
+
:return: the axis-aligned bounding box of the arc.
|
642
|
+
"""
|
643
|
+
...
|
644
|
+
|
645
|
+
@property
|
646
|
+
def start_point(self) -> Point2:
|
647
|
+
"""
|
648
|
+
Get the start point of the arc.
|
649
|
+
:return: the start point of the arc.
|
650
|
+
"""
|
651
|
+
...
|
652
|
+
|
653
|
+
@property
|
654
|
+
def end_point(self) -> Point2:
|
655
|
+
"""
|
656
|
+
Get the end point of the arc.
|
657
|
+
:return: the end point of the arc.
|
658
|
+
"""
|
659
|
+
...
|
660
|
+
|
661
|
+
class Aabb2:
|
662
|
+
def __init__(self, x_min: float, x_max: float, y_min: float, y_max: float):
|
663
|
+
"""
|
664
|
+
|
665
|
+
:param x_min:
|
666
|
+
:param x_max:
|
667
|
+
:param y_min:
|
668
|
+
:param y_max:
|
669
|
+
"""
|
670
|
+
...
|
671
|
+
|
672
|
+
@staticmethod
|
673
|
+
def at_point(x: float, y: float, w: float, h: float | None = None) -> Aabb2:
|
674
|
+
"""
|
675
|
+
Create an AABB centered at a point with a given width and height.
|
676
|
+
:param x: the x-coordinate of the center of the AABB.
|
677
|
+
:param y: the y-coordinate of the center of the AABB.
|
678
|
+
:param w: the width of the AABB.
|
679
|
+
:param h: the height of the AABB. If not provided, the AABB will be square.
|
680
|
+
:return: a new AABB object.
|
681
|
+
"""
|
682
|
+
...
|
683
|
+
|
684
|
+
@property
|
685
|
+
def min(self) -> Point2:
|
686
|
+
"""
|
687
|
+
Get the minimum point of the AABB.
|
688
|
+
:return: the minimum point of the AABB.
|
689
|
+
"""
|
690
|
+
...
|
691
|
+
|
692
|
+
@property
|
693
|
+
def max(self) -> Point2:
|
694
|
+
"""
|
695
|
+
Get the maximum point of the AABB.
|
696
|
+
:return: the maximum point of the AABB.
|
697
|
+
"""
|
698
|
+
...
|
699
|
+
|
700
|
+
@property
|
701
|
+
def center(self) -> Point2:
|
702
|
+
"""
|
703
|
+
Get the center point of the AABB.
|
704
|
+
:return: the center point of the AABB.
|
705
|
+
"""
|
706
|
+
...
|
707
|
+
|
708
|
+
@property
|
709
|
+
def extent(self) -> Vector2:
|
710
|
+
"""
|
711
|
+
Get the extent of the AABB.
|
712
|
+
:return: the extent of the AABB.
|
713
|
+
"""
|
714
|
+
...
|
715
|
+
|
716
|
+
def expand(self, d: float) -> Aabb2:
|
717
|
+
"""
|
718
|
+
Expand the AABB by a given distance in all directions. The resulting height and
|
719
|
+
width will be increased by 2 * d.
|
720
|
+
|
721
|
+
:param d: the distance to expand the AABB by.
|
722
|
+
:return: a new AABB object with the expanded bounds.
|
723
|
+
"""
|
724
|
+
...
|
725
|
+
|
726
|
+
def shrink(self, d: float) -> Aabb2:
|
727
|
+
"""
|
728
|
+
Shrink the AABB by a given distance in all directions. The resulting height and
|
729
|
+
width will be decreased by 2 * d.
|
730
|
+
|
731
|
+
:param d: the distance to shrink the AABB by.
|
732
|
+
:return: a new AABB object with the shrunk bounds.
|
733
|
+
"""
|
734
|
+
...
|
engeom/geom3.pyi
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from pathlib import Path
|
4
|
-
from typing import Tuple, Iterable, List
|
4
|
+
from typing import Tuple, Iterable, List, TypeVar
|
5
5
|
|
6
6
|
import numpy
|
7
7
|
from engeom import DeviationMode, Resample
|
8
8
|
|
9
|
-
|
9
|
+
Transformable3 = TypeVar("Transformable3", Vector3, Point3, Plane3, Iso3, SurfacePoint3)
|
10
|
+
PointOrVector3 = TypeVar("PointOrVector3", Vector3, Point3)
|
10
11
|
|
11
12
|
|
12
13
|
class Vector3:
|
@@ -37,7 +38,7 @@ class Vector3:
|
|
37
38
|
def __rmul__(self, other: float) -> Vector3:
|
38
39
|
...
|
39
40
|
|
40
|
-
def __add__(self, other:
|
41
|
+
def __add__(self, other: PointOrVector3) -> PointOrVector3:
|
41
42
|
...
|
42
43
|
|
43
44
|
def __sub__(self, other: Vector3) -> Vector3:
|
@@ -125,7 +126,7 @@ class Point3:
|
|
125
126
|
"""
|
126
127
|
...
|
127
128
|
|
128
|
-
def __sub__(self, other:
|
129
|
+
def __sub__(self, other: PointOrVector3) -> PointOrVector3:
|
129
130
|
...
|
130
131
|
|
131
132
|
def __add__(self, other: Vector3) -> Vector3:
|
@@ -434,8 +435,8 @@ class Mesh:
|
|
434
435
|
self,
|
435
436
|
vertices: numpy.ndarray[float],
|
436
437
|
triangles: numpy.ndarray[numpy.uint32],
|
437
|
-
merge_duplicates: bool
|
438
|
-
delete_degenerate: bool
|
438
|
+
merge_duplicates: bool = False,
|
439
|
+
delete_degenerate: bool = False
|
439
440
|
):
|
440
441
|
"""
|
441
442
|
Create an engeom mesh from vertices and triangles. The vertices should be a numpy array of shape (n, 3), while
|
@@ -445,13 +446,18 @@ class Mesh:
|
|
445
446
|
|
446
447
|
:param vertices: a numpy array of shape (n, 3) containing the vertices of the mesh.
|
447
448
|
:param triangles: a numpy array of shape (m, 3) containing the triangles of the mesh, should be uint.
|
448
|
-
:param merge_duplicates: merge duplicate vertices and triangles
|
449
|
-
:param delete_degenerate: delete degenerate triangles
|
449
|
+
:param merge_duplicates: merge duplicate vertices and triangles
|
450
|
+
:param delete_degenerate: delete degenerate triangles
|
450
451
|
"""
|
451
452
|
...
|
452
453
|
|
454
|
+
@property
|
455
|
+
def aabb(self) -> Aabb3:
|
456
|
+
""" Return the axis-aligned bounding box of the mesh. """
|
457
|
+
...
|
458
|
+
|
453
459
|
@staticmethod
|
454
|
-
def load_stl(path: str | Path, merge_duplicates: bool
|
460
|
+
def load_stl(path: str | Path, merge_duplicates: bool = False, delete_degenerate: bool = False) -> Mesh:
|
455
461
|
"""
|
456
462
|
Load a mesh from an STL file. This will return a new mesh object containing the vertices and triangles from the
|
457
463
|
file. Optional parameters can be used to control the behavior of the loader when handling duplicate vertices/
|
@@ -473,7 +479,7 @@ class Mesh:
|
|
473
479
|
"""
|
474
480
|
...
|
475
481
|
|
476
|
-
def
|
482
|
+
def cloned(self) -> Mesh:
|
477
483
|
"""
|
478
484
|
Will return a copy of the mesh. This is a copy of the data, so modifying the returned mesh will not modify the
|
479
485
|
original mesh.
|
@@ -499,21 +505,18 @@ class Mesh:
|
|
499
505
|
"""
|
500
506
|
...
|
501
507
|
|
502
|
-
|
508
|
+
@property
|
509
|
+
def points(self) -> numpy.ndarray[float]:
|
503
510
|
"""
|
504
|
-
Will return
|
505
|
-
be the same as the original vertices. This is a copy of the data, so modifying the returned array will not
|
506
|
-
modify the mesh.
|
511
|
+
Will return an immutable view of the vertices of the mesh as a numpy array of shape (n, 3).
|
507
512
|
:return: a numpy array of shape (n, 3) containing the vertices of the mesh.
|
508
513
|
"""
|
509
514
|
...
|
510
515
|
|
511
|
-
|
516
|
+
@property
|
517
|
+
def triangles(self) -> numpy.ndarray[numpy.uint32]:
|
512
518
|
"""
|
513
|
-
Will return
|
514
|
-
be the same as the original triangles. This is a copy of the data, so modifying the returned array will not
|
515
|
-
modify the mesh.
|
516
|
-
|
519
|
+
Will return an immutable view of the triangles of the mesh as a numpy array of shape (m, 3).
|
517
520
|
:return: a numpy array of shape (m, 3) containing the triangles of the mesh.
|
518
521
|
"""
|
519
522
|
...
|
@@ -611,11 +614,12 @@ class Curve3:
|
|
611
614
|
between them.
|
612
615
|
"""
|
613
616
|
|
614
|
-
def __init__(self, vertices: numpy.ndarray):
|
617
|
+
def __init__(self, vertices: numpy.ndarray, tol: float = 1.0e-6):
|
615
618
|
"""
|
616
619
|
Create a curve from a set of vertices. The vertices should be a numpy array of shape (n, 3).
|
617
620
|
|
618
621
|
:param vertices: a numpy array of shape (n, 3) containing the vertices of the curve.
|
622
|
+
:param tol: the inherent tolerance of the curve; points closer than this distance will be considered the same.
|
619
623
|
"""
|
620
624
|
...
|
621
625
|
|
@@ -636,13 +640,11 @@ class Curve3:
|
|
636
640
|
"""
|
637
641
|
...
|
638
642
|
|
639
|
-
|
643
|
+
@property
|
644
|
+
def points(self) -> numpy.ndarray[float]:
|
640
645
|
"""
|
641
|
-
Will return
|
642
|
-
|
643
|
-
modify the curve.
|
644
|
-
|
645
|
-
:return: a numpy array of shape (n, 3) containing the vertices of the curve.
|
646
|
+
Will return an immutable view of the vertices of the mesh as a numpy array of shape (n, 3).
|
647
|
+
:return: a numpy array of shape (n, 3) containing the vertices of the mesh.
|
646
648
|
"""
|
647
649
|
...
|
648
650
|
|
@@ -723,3 +725,41 @@ class Curve3:
|
|
723
725
|
:return: a new curve object with the transformed vertices.
|
724
726
|
"""
|
725
727
|
...
|
728
|
+
|
729
|
+
|
730
|
+
class Aabb3:
|
731
|
+
"""
|
732
|
+
A class representing an axis-aligned bounding box in 3D space. The box is defined by its minimum and maximum
|
733
|
+
"""
|
734
|
+
|
735
|
+
def __init__(self, x_min: float, y_min: float, z_min: float, x_max: float, y_max: float, z_max: float):
|
736
|
+
"""
|
737
|
+
Create an axis-aligned bounding box from the minimum and maximum coordinates.
|
738
|
+
:param x_min: the minimum x coordinate of the box.
|
739
|
+
:param y_min: the minimum y coordinate of the box.
|
740
|
+
:param z_min: the minimum z coordinate of the box.
|
741
|
+
:param x_max: the maximum x coordinate of the box.
|
742
|
+
:param y_max: the maximum y coordinate of the box.
|
743
|
+
:param z_max: the maximum z coordinate of the box.
|
744
|
+
"""
|
745
|
+
...
|
746
|
+
|
747
|
+
@property
|
748
|
+
def min(self) -> Point3:
|
749
|
+
""" The minimum point of the box. """
|
750
|
+
...
|
751
|
+
|
752
|
+
@property
|
753
|
+
def max(self) -> Point3:
|
754
|
+
""" The maximum point of the box. """
|
755
|
+
...
|
756
|
+
|
757
|
+
@property
|
758
|
+
def center(self) -> Point3:
|
759
|
+
""" The center point of the box. """
|
760
|
+
...
|
761
|
+
|
762
|
+
@property
|
763
|
+
def extent(self) -> Vector3:
|
764
|
+
""" The extent of the box. """
|
765
|
+
...
|
engeom/matplotlib.py
CHANGED
@@ -1,8 +1,18 @@
|
|
1
|
-
from typing import List
|
2
|
-
|
1
|
+
from typing import List, Iterable, Tuple, Union
|
2
|
+
from enum import Enum
|
3
3
|
import matplotlib.lines
|
4
4
|
import numpy
|
5
|
-
from .geom2 import Curve2
|
5
|
+
from .geom2 import Curve2, Circle2, Aabb2, Point2, Vector2, SurfacePoint2
|
6
|
+
from .metrology import Length2
|
7
|
+
|
8
|
+
PlotCoords = Union[Point2, Vector2, Iterable[float]]
|
9
|
+
|
10
|
+
|
11
|
+
class LabelPlace(Enum):
|
12
|
+
Outside = 1
|
13
|
+
Inside = 2
|
14
|
+
OutsideRev = 3
|
15
|
+
|
6
16
|
|
7
17
|
try:
|
8
18
|
from matplotlib.pyplot import Axes, Circle
|
@@ -10,18 +20,22 @@ try:
|
|
10
20
|
except ImportError:
|
11
21
|
pass
|
12
22
|
else:
|
23
|
+
|
13
24
|
class GomColorMap(ListedColormap):
|
14
25
|
def __init__(self):
|
15
|
-
colors = numpy.array(
|
16
|
-
[
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
26
|
+
colors = numpy.array(
|
27
|
+
[
|
28
|
+
[1, 0, 160],
|
29
|
+
[1, 0, 255],
|
30
|
+
[0, 254, 255],
|
31
|
+
[0, 160, 0],
|
32
|
+
[0, 254, 0],
|
33
|
+
[255, 255, 0],
|
34
|
+
[255, 128, 0],
|
35
|
+
[255, 1, 0],
|
36
|
+
],
|
37
|
+
dtype=numpy.float64,
|
38
|
+
)
|
25
39
|
colors /= 256.0
|
26
40
|
colors = numpy.hstack((colors, numpy.ones((len(colors), 1))))
|
27
41
|
super().__init__(colors)
|
@@ -30,22 +44,6 @@ else:
|
|
30
44
|
|
31
45
|
GOM_CMAP = GomColorMap()
|
32
46
|
|
33
|
-
def add_curve_plots(ax: Axes, *curves: Curve2, **kwargs) -> List[List[matplotlib.lines.Line2D]]:
|
34
|
-
"""
|
35
|
-
Plot a list of curves on a Matplotlib Axes object.
|
36
|
-
:param ax: a Matplotlib Axes object
|
37
|
-
:param curves: a list of Curve2 objects
|
38
|
-
:param kwargs: keyword arguments to pass to the plot function
|
39
|
-
:return: None
|
40
|
-
"""
|
41
|
-
actors = []
|
42
|
-
for curve in curves:
|
43
|
-
points = curve.clone_points()
|
44
|
-
a = ax.plot(points[:, 0], points[:, 1], **kwargs)
|
45
|
-
actors.append(a)
|
46
|
-
return actors
|
47
|
-
|
48
|
-
|
49
47
|
def set_aspect_fill(ax: Axes):
|
50
48
|
"""
|
51
49
|
Set the aspect ratio of a Matplotlib Axes (subplot) object to be 1:1 in x and y, while also having it expand
|
@@ -78,3 +76,165 @@ else:
|
|
78
76
|
x_range = x_scale / y_scale * (x1 - x0)
|
79
77
|
x_mid = (x0 + x1) / 2
|
80
78
|
ax.set_xlim(x_mid - x_range / 2, x_mid + x_range / 2)
|
79
|
+
|
80
|
+
class AxesHelper:
|
81
|
+
def __init__(self, ax: Axes, skip_aspect=False, hide_axes=False):
|
82
|
+
self.ax = ax
|
83
|
+
if not skip_aspect:
|
84
|
+
ax.set_aspect("equal", adjustable="datalim")
|
85
|
+
|
86
|
+
if hide_axes:
|
87
|
+
ax.axis("off")
|
88
|
+
|
89
|
+
def set_bounds(self, box: Aabb2):
|
90
|
+
"""
|
91
|
+
Set the bounds of a Matplotlib Axes object.
|
92
|
+
:param box: an Aabb2 object
|
93
|
+
:return: None
|
94
|
+
"""
|
95
|
+
self.ax.set_xlim(box.min.x, box.max.x)
|
96
|
+
self.ax.set_ylim(box.min.y, box.max.y)
|
97
|
+
|
98
|
+
def plot_circle(self, *circle: Circle2 | Iterable[float], **kwargs):
|
99
|
+
"""
|
100
|
+
Plot a circle on a Matplotlib Axes object.
|
101
|
+
:param circle: a Circle2 object
|
102
|
+
:param kwargs: keyword arguments to pass to the plot function
|
103
|
+
:return: None
|
104
|
+
"""
|
105
|
+
from matplotlib.pyplot import Circle
|
106
|
+
|
107
|
+
for cdata in circle:
|
108
|
+
if isinstance(cdata, Circle2):
|
109
|
+
c = Circle((cdata.center.x, cdata.center.y), cdata.r, **kwargs)
|
110
|
+
else:
|
111
|
+
x, y, r, *_ = cdata
|
112
|
+
c = Circle((x, y), r, **kwargs)
|
113
|
+
self.ax.add_patch(c)
|
114
|
+
|
115
|
+
def plot_curve(self, curve: Curve2, **kwargs):
|
116
|
+
"""
|
117
|
+
Plot a curve on a Matplotlib Axes object.
|
118
|
+
:param curve: a Curve2 object
|
119
|
+
:param kwargs: keyword arguments to pass to the plot function
|
120
|
+
:return: None
|
121
|
+
"""
|
122
|
+
self.ax.plot(curve.points[:, 0], curve.points[:, 1], **kwargs)
|
123
|
+
|
124
|
+
def dimension(
|
125
|
+
self,
|
126
|
+
length: Length2,
|
127
|
+
side_shift: float = 0,
|
128
|
+
format: str = "{value:.3f}",
|
129
|
+
fontsize: int = 10,
|
130
|
+
label_place: LabelPlace = LabelPlace.Outside,
|
131
|
+
label_offset: float | None = None,
|
132
|
+
fontname: str | None = None,
|
133
|
+
):
|
134
|
+
"""
|
135
|
+
Plot a Length2 object on a Matplotlib Axes object.
|
136
|
+
:param side_shift:
|
137
|
+
:param length: a Length2 object
|
138
|
+
:return: None
|
139
|
+
"""
|
140
|
+
from matplotlib.pyplot import Line2D
|
141
|
+
|
142
|
+
pad_scale = self._font_height(12) * 1.5
|
143
|
+
center = length.center.shift_orthogonal(side_shift)
|
144
|
+
leader_a = center.projection(length.a)
|
145
|
+
leader_b = center.projection(length.b)
|
146
|
+
|
147
|
+
if label_place == LabelPlace.Inside:
|
148
|
+
label_offset = label_offset or 0.0
|
149
|
+
label_coords = center.at_distance(label_offset)
|
150
|
+
self.arrow(label_coords, leader_a)
|
151
|
+
self.arrow(label_coords, leader_b)
|
152
|
+
elif label_place == LabelPlace.Outside:
|
153
|
+
label_offset = label_offset or pad_scale * 3
|
154
|
+
label_coords = leader_b + length.direction * label_offset
|
155
|
+
self.arrow(leader_a - length.direction * pad_scale, leader_a)
|
156
|
+
self.arrow(label_coords, leader_b)
|
157
|
+
elif label_place == LabelPlace.OutsideRev:
|
158
|
+
label_offset = label_offset or pad_scale * 3
|
159
|
+
label_coords = leader_a - length.direction * label_offset
|
160
|
+
self.arrow(leader_b + length.direction * pad_scale, leader_b)
|
161
|
+
self.arrow(label_coords, leader_a)
|
162
|
+
|
163
|
+
# Do we need sideways leaders?
|
164
|
+
self._line_if_needed(pad_scale, length.a, leader_a)
|
165
|
+
self._line_if_needed(pad_scale, length.b, leader_b)
|
166
|
+
|
167
|
+
kwargs = {"ha": "center", "va": "center", "fontsize": fontsize}
|
168
|
+
if fontname is not None:
|
169
|
+
kwargs["fontname"] = fontname
|
170
|
+
|
171
|
+
result = self.annotate_text_only(
|
172
|
+
format.format(value=length.value),
|
173
|
+
label_coords,
|
174
|
+
bbox=dict(boxstyle="round,pad=0.3", ec="black", fc="white"),
|
175
|
+
**kwargs,
|
176
|
+
)
|
177
|
+
|
178
|
+
def _line_if_needed(self, pad: float, actual: Point2, leader_end: Point2):
|
179
|
+
half_pad = pad * 0.5
|
180
|
+
v: Vector2 = leader_end - actual
|
181
|
+
if v.norm() < half_pad:
|
182
|
+
return
|
183
|
+
work = SurfacePoint2(*actual, *v)
|
184
|
+
t1 = work.scalar_projection(leader_end) + half_pad
|
185
|
+
self.arrow(actual, work.at_distance(t1), arrow="-")
|
186
|
+
|
187
|
+
def annotate_text_only(self, text: str, pos: PlotCoords, **kwargs):
|
188
|
+
"""
|
189
|
+
Annotate a Matplotlib Axes object with text only.
|
190
|
+
:param text: the text to annotate
|
191
|
+
:param pos: the position of the annotation
|
192
|
+
:param kwargs: keyword arguments to pass to the annotate function
|
193
|
+
:return: None
|
194
|
+
"""
|
195
|
+
return self.ax.annotate(text, xy=_tuplefy(pos), **kwargs)
|
196
|
+
|
197
|
+
def arrow(self, start: PlotCoords, end: PlotCoords, arrow="-|>"):
|
198
|
+
"""
|
199
|
+
Plot an arrow on a Matplotlib Axes object.
|
200
|
+
:param start: the start point of the arrow
|
201
|
+
:param end: the end point of the arrow
|
202
|
+
:param kwargs: keyword arguments to pass to the arrow function
|
203
|
+
:return: None
|
204
|
+
"""
|
205
|
+
self.ax.annotate(
|
206
|
+
"",
|
207
|
+
xy=_tuplefy(end),
|
208
|
+
xytext=_tuplefy(start),
|
209
|
+
arrowprops=dict(arrowstyle=arrow, fc="black"),
|
210
|
+
)
|
211
|
+
|
212
|
+
def _font_height(self, font_size: int) -> float:
|
213
|
+
"""Get the height of a font in data units."""
|
214
|
+
fig_dpi = self.ax.figure.dpi
|
215
|
+
font_height_inches = font_size * 1.0 / 72.0
|
216
|
+
font_height_px = font_height_inches * fig_dpi
|
217
|
+
|
218
|
+
px_per_data = self._get_scale()
|
219
|
+
return font_height_px / px_per_data
|
220
|
+
|
221
|
+
def _get_scale(self) -> float:
|
222
|
+
"""Get the scale of the plot in data units per pixel."""
|
223
|
+
x0, x1 = self.ax.get_xlim()
|
224
|
+
y0, y1 = self.ax.get_ylim()
|
225
|
+
|
226
|
+
bbox = self.ax.get_window_extent()
|
227
|
+
width, height = bbox.width, bbox.height
|
228
|
+
|
229
|
+
# Units are pixels per data unit
|
230
|
+
x_scale = width / (x1 - x0)
|
231
|
+
y_scale = height / (y1 - y0)
|
232
|
+
|
233
|
+
return min(x_scale, y_scale)
|
234
|
+
|
235
|
+
def _tuplefy(item: PlotCoords) -> Tuple[float, float]:
|
236
|
+
if isinstance(item, (Point2, Vector2)):
|
237
|
+
return item.x, item.y
|
238
|
+
else:
|
239
|
+
x, y, *_ = item
|
240
|
+
return x, y
|
engeom/metrology.pyi
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
from .geom2 import Point2, Vector2, SurfacePoint2
|
2
|
+
|
3
|
+
|
4
|
+
class Length2:
|
5
|
+
def __init__(self, a: Point2, b: Point2, direction: Vector2 | None = None):
|
6
|
+
"""
|
7
|
+
|
8
|
+
:param a:
|
9
|
+
:param b:
|
10
|
+
:param direction:
|
11
|
+
"""
|
12
|
+
...
|
13
|
+
|
14
|
+
@property
|
15
|
+
def a(self) -> Point2:
|
16
|
+
...
|
17
|
+
|
18
|
+
@property
|
19
|
+
def b(self) -> Point2:
|
20
|
+
...
|
21
|
+
|
22
|
+
@property
|
23
|
+
def direction(self) -> Vector2:
|
24
|
+
...
|
25
|
+
|
26
|
+
@property
|
27
|
+
def value(self) -> float:
|
28
|
+
...
|
29
|
+
|
30
|
+
@property
|
31
|
+
def center(self) -> SurfacePoint2:
|
32
|
+
...
|
engeom/pyvista.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
This module contains helper functions for working with PyVista.
|
3
3
|
"""
|
4
|
+
|
4
5
|
from __future__ import annotations
|
5
6
|
|
6
7
|
from typing import List
|
@@ -16,48 +17,48 @@ except ImportError:
|
|
16
17
|
pass
|
17
18
|
else:
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
:param mesh: a Mesh object
|
23
|
-
:return: a PyVista PolyData object
|
24
|
-
"""
|
25
|
-
|
26
|
-
if pyvista is None:
|
27
|
-
raise ImportError("PyVista is not installed.")
|
28
|
-
|
29
|
-
vertices = mesh.clone_vertices()
|
30
|
-
faces = mesh.clone_triangles()
|
31
|
-
faces = numpy.hstack((numpy.ones((faces.shape[0], 1), dtype=faces.dtype) * 3, faces))
|
32
|
-
return pyvista.PolyData(vertices, faces)
|
33
|
-
|
20
|
+
class PlotterHelper:
|
21
|
+
def __init__(self, plotter: pyvista.Plotter):
|
22
|
+
self.plotter = plotter
|
34
23
|
|
35
|
-
|
36
|
-
|
37
|
-
curves:
|
38
|
-
color: ColorLike =
|
24
|
+
def add_curves(
|
25
|
+
self,
|
26
|
+
*curves: Curve3,
|
27
|
+
color: ColorLike = "w",
|
39
28
|
width: float = 5.0,
|
40
29
|
label: str | None = None,
|
41
30
|
name: str | None = None,
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
31
|
+
) -> List[pyvista.vtkActor]:
|
32
|
+
"""
|
33
|
+
|
34
|
+
:param curves:
|
35
|
+
:param color:
|
36
|
+
:param width:
|
37
|
+
:param label:
|
38
|
+
:param name:
|
39
|
+
:return:
|
40
|
+
"""
|
41
|
+
result_list = []
|
42
|
+
for curve in curves:
|
43
|
+
added = self.plotter.add_lines(
|
44
|
+
curve.points,
|
45
|
+
connected=True,
|
46
|
+
color=color,
|
47
|
+
width=width,
|
48
|
+
label=label,
|
49
|
+
name=name,
|
50
|
+
)
|
51
|
+
result_list.append(added)
|
52
|
+
|
53
|
+
return result_list
|
54
|
+
|
55
|
+
def add_mesh(self, mesh: Mesh, **kwargs) -> pyvista.vtkActor:
|
56
|
+
"""
|
57
|
+
|
58
|
+
:param mesh:
|
59
|
+
:return:
|
60
|
+
"""
|
61
|
+
prefix = numpy.ones((mesh.triangles.shape[0], 1), dtype=mesh.triangles.dtype)
|
62
|
+
faces = numpy.hstack((prefix * 3, mesh.triangles))
|
63
|
+
data = pyvista.PolyData(mesh.points, faces)
|
64
|
+
return self.plotter.add_mesh(data, **kwargs)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
engeom-0.2.1.dist-info/METADATA,sha256=8MGvMlNyJjmeSSKaFGTM71uhryuOUrx9K_BpDPwVAfQ,339
|
2
|
+
engeom-0.2.1.dist-info/WHEEL,sha256=N94FV15LUMFa6DZ0GIwvnhZKdhS-OmzC5Hp00viT4OQ,90
|
3
|
+
engeom/airfoil/__init__.py,sha256=G6m7JEvHVk3sM2JooJPOg8JNA3VuEp0EIqczSEbC_PY,180
|
4
|
+
engeom/airfoil.pyi,sha256=0TVpXkolFUXbBqJp93FenA_XqvU7FD1DnbncAF0ubow,14654
|
5
|
+
engeom/align/__init__.py,sha256=SEeMqeqLKqJC73Mg8GwPwd9NwWnl-dcCqJ4rPdh8yyc,196
|
6
|
+
engeom/align.pyi,sha256=QCSKrTLkCoaIubcrPU9J-wDZe1lRP0GbPgWZmonXjo0,997
|
7
|
+
engeom/engeom.pyi,sha256=Il7TIk8Z5QgZENLVBgb2Pj11MIHCRbQCdDvgMh9LRgk,194
|
8
|
+
engeom/geom2/__init__.py,sha256=mRu8Zh6DE-EQyhxScoxszPqDjGVzGWVJEQO6RIAtS4A,174
|
9
|
+
engeom/geom2.pyi,sha256=Jh0ES-Gvkl7sFQV7VG6KdwgDbCPhmiETG_YOObYazhU,22741
|
10
|
+
engeom/geom3/__init__.py,sha256=DG5jt2xgS9WRNb58ZkkrcKQQO6bIG-irg-uV_BkHEj4,174
|
11
|
+
engeom/geom3.pyi,sha256=np6QjaeAS1Ms0Y4xNj4IoLRgI-rd9oh2sAH9TJem8Gw,28230
|
12
|
+
engeom/matplotlib.py,sha256=3B2gRowUT5UDy-d46i9yANfoxoCa_BBuRo4HHb7NFYU,9275
|
13
|
+
engeom/metrology/__init__.py,sha256=cpsB0-hJGitzW79Coxwf7r_mpNaeI6yG3myDEVdBJgk,186
|
14
|
+
engeom/metrology.pyi,sha256=dEPRtvc8us6rMLwg3wIWUa92Udew_QN_Y71NV9zGf2s,572
|
15
|
+
engeom/pyvista.py,sha256=g3wwolj-F0kDSVuDl03CN-DbbR2R71dz-hzqard3bQs,1703
|
16
|
+
engeom/__init__.py,sha256=kYgFq3jq1quDfV013wEYQMlUBz4QNSpP6u8lFiuTHvc,115
|
17
|
+
engeom/engeom.pyd,sha256=0ctL7HAYGXQky8eP7PzPPRKcTYtQsnJjyOKsjV6FGcI,1387008
|
18
|
+
engeom-0.2.1.dist-info/RECORD,,
|
engeom-0.1.2.dist-info/RECORD
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
engeom-0.1.2.dist-info/METADATA,sha256=ym_mnfPDlcTOzcYgeEZGMCFsgidl30syGoPDLY_B2eA,339
|
2
|
-
engeom-0.1.2.dist-info/WHEEL,sha256=N94FV15LUMFa6DZ0GIwvnhZKdhS-OmzC5Hp00viT4OQ,90
|
3
|
-
engeom/align/__init__.py,sha256=SEeMqeqLKqJC73Mg8GwPwd9NwWnl-dcCqJ4rPdh8yyc,196
|
4
|
-
engeom/align.pyi,sha256=KBC0nwcyp4YMfY2hRN1gr3DqFah-unqAd_o1KYwJAqc,1022
|
5
|
-
engeom/engeom.pyi,sha256=J44RFBaKfnr1z7WyXCeE-_wEJZvpRJBfHINfDPR93c4,204
|
6
|
-
engeom/geom2/__init__.py,sha256=mRu8Zh6DE-EQyhxScoxszPqDjGVzGWVJEQO6RIAtS4A,174
|
7
|
-
engeom/geom2.pyi,sha256=De3YQSGpS4UQ1zawtIsuVR4-7rcFjqmNvL9lbtzN_d4,17117
|
8
|
-
engeom/geom3/__init__.py,sha256=DG5jt2xgS9WRNb58ZkkrcKQQO6bIG-irg-uV_BkHEj4,174
|
9
|
-
engeom/geom3.pyi,sha256=fEFhdFmEJsDP3X0aE_N0heqMyyPnbYjpeB0hTR2xjOI,27295
|
10
|
-
engeom/matplotlib.py,sha256=Y9JZqpEFHm5EVzbr20P-NdxndNZdDH32OhE_KjxG_S4,3025
|
11
|
-
engeom/pyvista.py,sha256=Lyvm0K_rvhR2wZACtfwCPYwEuABqYSTdjvbBaQS9yI4,1721
|
12
|
-
engeom/__init__.py,sha256=kYgFq3jq1quDfV013wEYQMlUBz4QNSpP6u8lFiuTHvc,115
|
13
|
-
engeom/engeom.pyd,sha256=M-hl-H0q3g7MHVvfUoqdrxmnNPmP24F0IxgbBohQWgc,1140224
|
14
|
-
engeom-0.1.2.dist-info/RECORD,,
|
File without changes
|