engeom 0.1.1__cp38-abi3-macosx_11_0_arm64.whl → 0.2.1__cp38-abi3-macosx_11_0_arm64.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.abi3.so +0 -0
- engeom/engeom.pyi +3 -9
- engeom/geom2.pyi +601 -6
- engeom/geom3.pyi +359 -19
- engeom/matplotlib.py +196 -0
- engeom/metrology/__init__.py +5 -0
- engeom/metrology.pyi +32 -0
- engeom/pyvista.py +52 -14
- {engeom-0.1.1.dist-info → engeom-0.2.1.dist-info}/METADATA +1 -1
- engeom-0.2.1.dist-info/RECORD +18 -0
- engeom-0.1.1.dist-info/RECORD +0 -14
- {engeom-0.1.1.dist-info → engeom-0.2.1.dist-info}/WHEEL +0 -0
engeom/geom3.pyi
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from pathlib import Path
|
4
|
-
from typing import Tuple
|
4
|
+
from typing import Tuple, Iterable, List, TypeVar
|
5
5
|
|
6
6
|
import numpy
|
7
|
+
from engeom import DeviationMode, Resample
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
type Transformable3 = Vector3 | Point3 | Plane3 | Iso3
|
9
|
+
Transformable3 = TypeVar("Transformable3", Vector3, Point3, Plane3, Iso3, SurfacePoint3)
|
10
|
+
PointOrVector3 = TypeVar("PointOrVector3", Vector3, Point3)
|
11
11
|
|
12
12
|
|
13
13
|
class Vector3:
|
@@ -32,10 +32,13 @@ class Vector3:
|
|
32
32
|
def z(self) -> float:
|
33
33
|
...
|
34
34
|
|
35
|
+
def __iter__(self) -> Iterable[float]:
|
36
|
+
...
|
37
|
+
|
35
38
|
def __rmul__(self, other: float) -> Vector3:
|
36
39
|
...
|
37
40
|
|
38
|
-
def __add__(self, other:
|
41
|
+
def __add__(self, other: PointOrVector3) -> PointOrVector3:
|
39
42
|
...
|
40
43
|
|
41
44
|
def __sub__(self, other: Vector3) -> Vector3:
|
@@ -53,6 +56,42 @@ class Vector3:
|
|
53
56
|
"""
|
54
57
|
...
|
55
58
|
|
59
|
+
def dot(self, other: Vector3) -> float:
|
60
|
+
"""
|
61
|
+
Calculate the dot product of this vector with another vector.
|
62
|
+
:param other: the other vector to calculate the dot product with.
|
63
|
+
:return: the dot product of the two vectors.
|
64
|
+
"""
|
65
|
+
...
|
66
|
+
|
67
|
+
def cross(self, other: Vector3) -> Vector3:
|
68
|
+
"""
|
69
|
+
Calculate the cross product of this vector with another vector.
|
70
|
+
:param other: the other vector to calculate the cross product with.
|
71
|
+
:return: the cross product of the two vectors.
|
72
|
+
"""
|
73
|
+
...
|
74
|
+
|
75
|
+
def norm(self) -> float:
|
76
|
+
"""
|
77
|
+
Calculate the norm (length) of the vector.
|
78
|
+
:return:
|
79
|
+
"""
|
80
|
+
|
81
|
+
def normalized(self) -> Vector3:
|
82
|
+
"""
|
83
|
+
Return a normalized version of the vector.
|
84
|
+
:return: a new vector that has unit length
|
85
|
+
"""
|
86
|
+
|
87
|
+
def angle_to(self, other: Vector3) -> float:
|
88
|
+
"""
|
89
|
+
Calculate the smallest angle between this vector and another vector.
|
90
|
+
:param other: the other vector to calculate the angle to.
|
91
|
+
:return: the angle between the two vectors in radians.
|
92
|
+
"""
|
93
|
+
...
|
94
|
+
|
56
95
|
|
57
96
|
class Point3:
|
58
97
|
def __init__(self, x: float, y: float, z: float):
|
@@ -76,6 +115,9 @@ class Point3:
|
|
76
115
|
def z(self) -> float:
|
77
116
|
...
|
78
117
|
|
118
|
+
def __iter__(self) -> Iterable[float]:
|
119
|
+
...
|
120
|
+
|
79
121
|
@property
|
80
122
|
def coords(self) -> Vector3:
|
81
123
|
"""
|
@@ -84,7 +126,7 @@ class Point3:
|
|
84
126
|
"""
|
85
127
|
...
|
86
128
|
|
87
|
-
def __sub__(self, other:
|
129
|
+
def __sub__(self, other: PointOrVector3) -> PointOrVector3:
|
88
130
|
...
|
89
131
|
|
90
132
|
def __add__(self, other: Vector3) -> Vector3:
|
@@ -97,6 +139,90 @@ class Point3:
|
|
97
139
|
...
|
98
140
|
|
99
141
|
|
142
|
+
class SurfacePoint3:
|
143
|
+
def __init__(self, x: float, y: float, z: float, nx: float, ny: float, nz: float):
|
144
|
+
"""
|
145
|
+
|
146
|
+
:param x:
|
147
|
+
:param y:
|
148
|
+
:param z:
|
149
|
+
:param nx:
|
150
|
+
:param ny:
|
151
|
+
:param nz:
|
152
|
+
"""
|
153
|
+
...
|
154
|
+
|
155
|
+
@property
|
156
|
+
def point(self) -> Point3:
|
157
|
+
"""
|
158
|
+
Get the coordinates of the point as a Point3 object.
|
159
|
+
:return: a Point3 object
|
160
|
+
"""
|
161
|
+
...
|
162
|
+
|
163
|
+
@property
|
164
|
+
def normal(self) -> Vector3:
|
165
|
+
"""
|
166
|
+
Get the normal of the point as a Vector3 object.
|
167
|
+
:return: a Vector3 object
|
168
|
+
"""
|
169
|
+
...
|
170
|
+
|
171
|
+
def at_distance(self, distance: float) -> Point3:
|
172
|
+
"""
|
173
|
+
Get the point at a distance along the normal from the surface point.
|
174
|
+
:param distance: the distance to move along the normal.
|
175
|
+
:return: the point at the distance along the normal.
|
176
|
+
"""
|
177
|
+
...
|
178
|
+
|
179
|
+
def scalar_projection(self, point: Point3) -> float:
|
180
|
+
"""
|
181
|
+
Calculate the scalar projection of a point onto the axis defined by the surface point position and direction.
|
182
|
+
Positive values indicate that the point is in the normal direction from the surface point, while negative values
|
183
|
+
indicate that the point is in the opposite direction.
|
184
|
+
|
185
|
+
:param point: the point to calculate the projection of.
|
186
|
+
:return: the scalar projection of the point onto the normal.
|
187
|
+
"""
|
188
|
+
...
|
189
|
+
|
190
|
+
def projection(self, point: Point3) -> Point3:
|
191
|
+
"""
|
192
|
+
Calculate the projection of a point onto the axis defined by the surface point position and direction.
|
193
|
+
|
194
|
+
:param point: the point to calculate the projection of.
|
195
|
+
:return: the projection of the point onto the plane.
|
196
|
+
"""
|
197
|
+
...
|
198
|
+
|
199
|
+
def reversed(self) -> SurfacePoint3:
|
200
|
+
"""
|
201
|
+
Return a new surface point with the normal vector inverted, but the position unchanged.
|
202
|
+
:return: a new surface point with the inverted normal vector.
|
203
|
+
"""
|
204
|
+
...
|
205
|
+
|
206
|
+
def planar_distance(self, point: Point3) -> float:
|
207
|
+
"""
|
208
|
+
Calculate the planar (non-normal) distance between the surface point and a point. This is complementary to the
|
209
|
+
scalar projection. A point is projected onto the plane defined by the position and normal of the surface point,
|
210
|
+
and the distance between the surface point position and the projected point is returned. The value will always
|
211
|
+
be positive.
|
212
|
+
|
213
|
+
:param point: the point to calculate the distance to.
|
214
|
+
:return: the planar distance between the surface point and the point.
|
215
|
+
"""
|
216
|
+
...
|
217
|
+
|
218
|
+
def get_plane(self) -> Plane3:
|
219
|
+
"""
|
220
|
+
Get the plane defined by the surface point.
|
221
|
+
:return: the plane defined by the surface point.
|
222
|
+
"""
|
223
|
+
...
|
224
|
+
|
225
|
+
|
100
226
|
class Iso3:
|
101
227
|
""" An isometry (rigid body transformation) in 3D space. """
|
102
228
|
|
@@ -183,7 +309,11 @@ class SvdBasis3:
|
|
183
309
|
fitting basis for the points using a singular value decomposition.
|
184
310
|
"""
|
185
311
|
|
186
|
-
def __init__(
|
312
|
+
def __init__(
|
313
|
+
self,
|
314
|
+
points: numpy.ndarray[float],
|
315
|
+
weights: numpy.ndarray[float] | None = None
|
316
|
+
):
|
187
317
|
"""
|
188
318
|
Create a basis from a set of points. The basis will be calculated using a singular value decomposition of the
|
189
319
|
points.
|
@@ -305,6 +435,8 @@ class Mesh:
|
|
305
435
|
self,
|
306
436
|
vertices: numpy.ndarray[float],
|
307
437
|
triangles: numpy.ndarray[numpy.uint32],
|
438
|
+
merge_duplicates: bool = False,
|
439
|
+
delete_degenerate: bool = False
|
308
440
|
):
|
309
441
|
"""
|
310
442
|
Create an engeom mesh from vertices and triangles. The vertices should be a numpy array of shape (n, 3), while
|
@@ -314,16 +446,26 @@ class Mesh:
|
|
314
446
|
|
315
447
|
:param vertices: a numpy array of shape (n, 3) containing the vertices of the mesh.
|
316
448
|
:param triangles: a numpy array of shape (m, 3) containing the triangles of the mesh, should be uint.
|
449
|
+
:param merge_duplicates: merge duplicate vertices and triangles
|
450
|
+
:param delete_degenerate: delete degenerate triangles
|
317
451
|
"""
|
318
452
|
...
|
319
453
|
|
454
|
+
@property
|
455
|
+
def aabb(self) -> Aabb3:
|
456
|
+
""" Return the axis-aligned bounding box of the mesh. """
|
457
|
+
...
|
458
|
+
|
320
459
|
@staticmethod
|
321
|
-
def load_stl(path: str | Path) -> Mesh:
|
460
|
+
def load_stl(path: str | Path, merge_duplicates: bool = False, delete_degenerate: bool = False) -> Mesh:
|
322
461
|
"""
|
323
462
|
Load a mesh from an STL file. This will return a new mesh object containing the vertices and triangles from the
|
324
|
-
file.
|
463
|
+
file. Optional parameters can be used to control the behavior of the loader when handling duplicate vertices/
|
464
|
+
triangles and degenerate triangles.
|
325
465
|
|
326
466
|
:param path: the path to the STL file to load.
|
467
|
+
:param merge_duplicates: merge duplicate vertices and triangles. If None, the default behavior is to do nothing
|
468
|
+
:param delete_degenerate: delete degenerate triangles. If None, the default behavior is to do nothing
|
327
469
|
:return: the mesh object containing the data from the file.
|
328
470
|
"""
|
329
471
|
...
|
@@ -337,7 +479,7 @@ class Mesh:
|
|
337
479
|
"""
|
338
480
|
...
|
339
481
|
|
340
|
-
def
|
482
|
+
def cloned(self) -> Mesh:
|
341
483
|
"""
|
342
484
|
Will return a copy of the mesh. This is a copy of the data, so modifying the returned mesh will not modify the
|
343
485
|
original mesh.
|
@@ -363,21 +505,18 @@ class Mesh:
|
|
363
505
|
"""
|
364
506
|
...
|
365
507
|
|
366
|
-
|
508
|
+
@property
|
509
|
+
def points(self) -> numpy.ndarray[float]:
|
367
510
|
"""
|
368
|
-
Will return
|
369
|
-
be the same as the original vertices. This is a copy of the data, so modifying the returned array will not
|
370
|
-
modify the mesh.
|
511
|
+
Will return an immutable view of the vertices of the mesh as a numpy array of shape (n, 3).
|
371
512
|
:return: a numpy array of shape (n, 3) containing the vertices of the mesh.
|
372
513
|
"""
|
373
514
|
...
|
374
515
|
|
375
|
-
|
516
|
+
@property
|
517
|
+
def triangles(self) -> numpy.ndarray[numpy.uint32]:
|
376
518
|
"""
|
377
|
-
Will return
|
378
|
-
be the same as the original triangles. This is a copy of the data, so modifying the returned array will not
|
379
|
-
modify the mesh.
|
380
|
-
|
519
|
+
Will return an immutable view of the triangles of the mesh as a numpy array of shape (m, 3).
|
381
520
|
:return: a numpy array of shape (m, 3) containing the triangles of the mesh.
|
382
521
|
"""
|
383
522
|
...
|
@@ -423,3 +562,204 @@ class Mesh:
|
|
423
562
|
:return: a numpy array of shape (n, 6) containing the sampled points.
|
424
563
|
"""
|
425
564
|
...
|
565
|
+
|
566
|
+
def section(self, plane: Plane3, tol: float | None = None) -> List[Curve3]:
|
567
|
+
"""
|
568
|
+
Calculate and return the intersection curves between the mesh and a plane.
|
569
|
+
|
570
|
+
:param plane:
|
571
|
+
:param tol:
|
572
|
+
:return:
|
573
|
+
"""
|
574
|
+
...
|
575
|
+
|
576
|
+
|
577
|
+
class CurveStation3:
|
578
|
+
"""
|
579
|
+
A class representing a station along a curve in 3D space. The station is represented by a point on the curve, a
|
580
|
+
tangent (direction) vector, and a length along the curve.
|
581
|
+
"""
|
582
|
+
|
583
|
+
@property
|
584
|
+
def point(self) -> Point3:
|
585
|
+
""" The 3d position in space on the curve. """
|
586
|
+
...
|
587
|
+
|
588
|
+
@property
|
589
|
+
def direction(self) -> Vector3:
|
590
|
+
""" The tangent (direction) vector of the curve at the station. """
|
591
|
+
...
|
592
|
+
|
593
|
+
@property
|
594
|
+
def direction_point(self) -> SurfacePoint3:
|
595
|
+
"""
|
596
|
+
A `SurfacePoint3` object representing the point on the curve and the curve's tangent/direction vector.
|
597
|
+
"""
|
598
|
+
...
|
599
|
+
|
600
|
+
@property
|
601
|
+
def index(self) -> int:
|
602
|
+
""" The index of the previous vertex on the curve, at or before the station. """
|
603
|
+
...
|
604
|
+
|
605
|
+
@property
|
606
|
+
def length_along(self) -> float:
|
607
|
+
""" The length along the curve from the start of the curve to the station. """
|
608
|
+
...
|
609
|
+
|
610
|
+
|
611
|
+
class Curve3:
|
612
|
+
"""
|
613
|
+
A class representing a polyline in 3D space. The curve is represented by a set of vertices and the segments
|
614
|
+
between them.
|
615
|
+
"""
|
616
|
+
|
617
|
+
def __init__(self, vertices: numpy.ndarray, tol: float = 1.0e-6):
|
618
|
+
"""
|
619
|
+
Create a curve from a set of vertices. The vertices should be a numpy array of shape (n, 3).
|
620
|
+
|
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.
|
623
|
+
"""
|
624
|
+
...
|
625
|
+
|
626
|
+
def clone(self) -> Curve3:
|
627
|
+
"""
|
628
|
+
Will return a copy of the curve. This is a copy of the data, so modifying the returned curve will not modify
|
629
|
+
the original curve.
|
630
|
+
|
631
|
+
:return: a copy of the curve.
|
632
|
+
"""
|
633
|
+
...
|
634
|
+
|
635
|
+
def length(self) -> float:
|
636
|
+
"""
|
637
|
+
Return the total length of the curve in the units of the vertices.
|
638
|
+
|
639
|
+
:return: the length of the curve.
|
640
|
+
"""
|
641
|
+
...
|
642
|
+
|
643
|
+
@property
|
644
|
+
def points(self) -> numpy.ndarray[float]:
|
645
|
+
"""
|
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.
|
648
|
+
"""
|
649
|
+
...
|
650
|
+
|
651
|
+
def at_length(self, length: float) -> CurveStation3:
|
652
|
+
"""
|
653
|
+
Return a station along the curve at the given length. The length is measured from the start of the curve to the
|
654
|
+
station. If the length is greater than the length of the curve or less than 0, an error will be raised.
|
655
|
+
|
656
|
+
:param length: the length along the curve to return the station at.
|
657
|
+
:return: a `CurveStation3` object representing the station along the curve.
|
658
|
+
"""
|
659
|
+
...
|
660
|
+
|
661
|
+
def at_fraction(self, fraction: float) -> CurveStation3:
|
662
|
+
"""
|
663
|
+
Return a station along the curve at the given fraction of the length of the curve. If the fraction is greater
|
664
|
+
than 1 or less than 0, an error will be raised.
|
665
|
+
|
666
|
+
:param fraction: the fraction of the length of the curve to return the station at.
|
667
|
+
:return: a `CurveStation3` object representing the station along the curve.
|
668
|
+
"""
|
669
|
+
...
|
670
|
+
|
671
|
+
def at_closest_to_point(self, point: Point3) -> CurveStation3:
|
672
|
+
"""
|
673
|
+
Return a station along the curve at the closest point to the given point. The station will be the point on the
|
674
|
+
curve that is closest to the given point.
|
675
|
+
|
676
|
+
:param point: the point to find the closest station to.
|
677
|
+
:return: a `CurveStation3` object representing the station along the curve.
|
678
|
+
"""
|
679
|
+
...
|
680
|
+
|
681
|
+
def at_front(self) -> CurveStation3:
|
682
|
+
"""
|
683
|
+
Return a station at the front of the curve. This is equivalent to calling `at_length(0)`.
|
684
|
+
|
685
|
+
:return: a `CurveStation3` object representing the station at the front of the curve.
|
686
|
+
"""
|
687
|
+
...
|
688
|
+
|
689
|
+
def at_back(self) -> CurveStation3:
|
690
|
+
"""
|
691
|
+
Return a station at the back of the curve. This is equivalent to calling `at_length(length)`.
|
692
|
+
|
693
|
+
:return: a `CurveStation3` object representing the station at the back of the curve.
|
694
|
+
"""
|
695
|
+
...
|
696
|
+
|
697
|
+
def resample(self, resample: Resample) -> Curve3:
|
698
|
+
"""
|
699
|
+
Resample the curve using the given resampling method. The resampling method can be one of the following:
|
700
|
+
|
701
|
+
- `Resample.ByCount(count: int)`: resample the curve to have the given number of points.
|
702
|
+
- `Resample.BySpacing(distance: float)`: resample the curve to have points spaced by the given distance.
|
703
|
+
- `Resample.ByMaxSpacing(distance: float)`: resample the curve to have points spaced by a maximum distance.
|
704
|
+
|
705
|
+
:param resample: the resampling method to use.
|
706
|
+
:return: a new curve object with the resampled vertices.
|
707
|
+
"""
|
708
|
+
...
|
709
|
+
|
710
|
+
def simplify(self, tolerance: float) -> Curve3:
|
711
|
+
"""
|
712
|
+
Simplify the curve using the Ramer-Douglas-Peucker algorithm. This will remove vertices from the curve that are
|
713
|
+
within the given tolerance of the line between the previous and next vertices.
|
714
|
+
|
715
|
+
:param tolerance: the tolerance to use when simplifying the curve.
|
716
|
+
:return: a new curve object with the simplified vertices.
|
717
|
+
"""
|
718
|
+
...
|
719
|
+
|
720
|
+
def transformed_by(self, iso: Iso3) -> Curve3:
|
721
|
+
"""
|
722
|
+
Transform the curve by an isometry. This will return a new curve object with the transformed vertices.
|
723
|
+
|
724
|
+
:param iso: the isometry to transform the curve by.
|
725
|
+
:return: a new curve object with the transformed vertices.
|
726
|
+
"""
|
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,11 +1,49 @@
|
|
1
|
+
from typing import List, Iterable, Tuple, Union
|
2
|
+
from enum import Enum
|
3
|
+
import matplotlib.lines
|
1
4
|
import numpy
|
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
|
+
|
2
16
|
|
3
17
|
try:
|
4
18
|
from matplotlib.pyplot import Axes, Circle
|
19
|
+
from matplotlib.colors import ListedColormap
|
5
20
|
except ImportError:
|
6
21
|
pass
|
7
22
|
else:
|
8
23
|
|
24
|
+
class GomColorMap(ListedColormap):
|
25
|
+
def __init__(self):
|
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
|
+
)
|
39
|
+
colors /= 256.0
|
40
|
+
colors = numpy.hstack((colors, numpy.ones((len(colors), 1))))
|
41
|
+
super().__init__(colors)
|
42
|
+
self.set_under("magenta")
|
43
|
+
self.set_over("darkred")
|
44
|
+
|
45
|
+
GOM_CMAP = GomColorMap()
|
46
|
+
|
9
47
|
def set_aspect_fill(ax: Axes):
|
10
48
|
"""
|
11
49
|
Set the aspect ratio of a Matplotlib Axes (subplot) object to be 1:1 in x and y, while also having it expand
|
@@ -39,6 +77,164 @@ else:
|
|
39
77
|
x_mid = (x0 + x1) / 2
|
40
78
|
ax.set_xlim(x_mid - x_range / 2, x_mid + x_range / 2)
|
41
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)
|
42
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)
|
43
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)
|
44
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
|