engeom 0.1.2__cp38-abi3-win_amd64.whl → 0.2.3__cp38-abi3-win_amd64.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 +8 -3
- engeom/geom2.pyi +234 -16
- engeom/geom3.pyi +195 -29
- engeom/matplotlib.py +181 -29
- engeom/metrology/__init__.py +5 -0
- engeom/metrology.pyi +64 -0
- engeom/pyvista.py +151 -48
- {engeom-0.1.2.dist-info → engeom-0.2.3.dist-info}/METADATA +1 -1
- engeom-0.2.3.dist-info/RECORD +18 -0
- {engeom-0.1.2.dist-info → engeom-0.2.3.dist-info}/WHEEL +1 -1
- engeom-0.1.2.dist-info/RECORD +0 -14
engeom/geom3.pyi
CHANGED
@@ -1,12 +1,14 @@
|
|
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
|
-
from engeom import DeviationMode, Resample
|
7
|
+
from engeom import DeviationMode, Resample, SelectOp
|
8
|
+
from .metrology import Length3
|
8
9
|
|
9
|
-
|
10
|
+
Transformable3 = TypeVar("Transformable3", Vector3, Point3, Plane3, Iso3, SurfacePoint3)
|
11
|
+
PointOrVector3 = TypeVar("PointOrVector3", Vector3, Point3)
|
10
12
|
|
11
13
|
|
12
14
|
class Vector3:
|
@@ -37,7 +39,7 @@ class Vector3:
|
|
37
39
|
def __rmul__(self, other: float) -> Vector3:
|
38
40
|
...
|
39
41
|
|
40
|
-
def __add__(self, other:
|
42
|
+
def __add__(self, other: PointOrVector3) -> PointOrVector3:
|
41
43
|
...
|
42
44
|
|
43
45
|
def __sub__(self, other: Vector3) -> Vector3:
|
@@ -125,7 +127,7 @@ class Point3:
|
|
125
127
|
"""
|
126
128
|
...
|
127
129
|
|
128
|
-
def __sub__(self, other:
|
130
|
+
def __sub__(self, other: PointOrVector3) -> PointOrVector3:
|
129
131
|
...
|
130
132
|
|
131
133
|
def __add__(self, other: Vector3) -> Vector3:
|
@@ -433,9 +435,9 @@ class Mesh:
|
|
433
435
|
def __init__(
|
434
436
|
self,
|
435
437
|
vertices: numpy.ndarray[float],
|
436
|
-
|
437
|
-
merge_duplicates: bool
|
438
|
-
delete_degenerate: bool
|
438
|
+
faces: numpy.ndarray[numpy.uint32],
|
439
|
+
merge_duplicates: bool = False,
|
440
|
+
delete_degenerate: bool = False
|
439
441
|
):
|
440
442
|
"""
|
441
443
|
Create an engeom mesh from vertices and triangles. The vertices should be a numpy array of shape (n, 3), while
|
@@ -444,14 +446,19 @@ class Mesh:
|
|
444
446
|
front/outside.
|
445
447
|
|
446
448
|
:param vertices: a numpy array of shape (n, 3) containing the vertices of the mesh.
|
447
|
-
:param
|
448
|
-
:param merge_duplicates: merge duplicate vertices and triangles
|
449
|
-
:param delete_degenerate: delete degenerate triangles
|
449
|
+
:param faces: a numpy array of shape (m, 3) containing the triangles of the mesh, should be uint.
|
450
|
+
:param merge_duplicates: merge duplicate vertices and triangles
|
451
|
+
:param delete_degenerate: delete degenerate triangles
|
450
452
|
"""
|
451
453
|
...
|
452
454
|
|
455
|
+
@property
|
456
|
+
def aabb(self) -> Aabb3:
|
457
|
+
""" Return the axis-aligned bounding box of the mesh. """
|
458
|
+
...
|
459
|
+
|
453
460
|
@staticmethod
|
454
|
-
def load_stl(path: str | Path, merge_duplicates: bool
|
461
|
+
def load_stl(path: str | Path, merge_duplicates: bool = False, delete_degenerate: bool = False) -> Mesh:
|
455
462
|
"""
|
456
463
|
Load a mesh from an STL file. This will return a new mesh object containing the vertices and triangles from the
|
457
464
|
file. Optional parameters can be used to control the behavior of the loader when handling duplicate vertices/
|
@@ -473,7 +480,7 @@ class Mesh:
|
|
473
480
|
"""
|
474
481
|
...
|
475
482
|
|
476
|
-
def
|
483
|
+
def cloned(self) -> Mesh:
|
477
484
|
"""
|
478
485
|
Will return a copy of the mesh. This is a copy of the data, so modifying the returned mesh will not modify the
|
479
486
|
original mesh.
|
@@ -499,21 +506,18 @@ class Mesh:
|
|
499
506
|
"""
|
500
507
|
...
|
501
508
|
|
502
|
-
|
509
|
+
@property
|
510
|
+
def vertices(self) -> numpy.ndarray[float]:
|
503
511
|
"""
|
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.
|
512
|
+
Will return an immutable view of the vertices of the mesh as a numpy array of shape (n, 3).
|
507
513
|
:return: a numpy array of shape (n, 3) containing the vertices of the mesh.
|
508
514
|
"""
|
509
515
|
...
|
510
516
|
|
511
|
-
|
517
|
+
@property
|
518
|
+
def faces(self) -> numpy.ndarray[numpy.uint32]:
|
512
519
|
"""
|
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
|
-
|
520
|
+
Will return an immutable view of the triangles of the mesh as a numpy array of shape (m, 3).
|
517
521
|
:return: a numpy array of shape (m, 3) containing the triangles of the mesh.
|
518
522
|
"""
|
519
523
|
...
|
@@ -570,6 +574,131 @@ class Mesh:
|
|
570
574
|
"""
|
571
575
|
...
|
572
576
|
|
577
|
+
def face_select_none(self) -> MeshTriangleFilter:
|
578
|
+
"""
|
579
|
+
Start a filter operation on the faces of the mesh beginning with no faces selected. This will return a filter
|
580
|
+
object that can be used to further add or remove faces from the selection.
|
581
|
+
|
582
|
+
:return: a filter object for the triangles of the mesh.
|
583
|
+
"""
|
584
|
+
...
|
585
|
+
|
586
|
+
def face_select_all(self) -> MeshTriangleFilter:
|
587
|
+
"""
|
588
|
+
Start a filter operation on the faces of the mesh beginning with all faces selected. This will return a filter
|
589
|
+
object that can be used to further add or remove faces from the selection.
|
590
|
+
|
591
|
+
:return: a filter object for the triangles of the mesh.
|
592
|
+
"""
|
593
|
+
...
|
594
|
+
|
595
|
+
def separate_patches(self) -> List[Mesh]:
|
596
|
+
"""
|
597
|
+
Separate the mesh into connected patches. This will return a list of new mesh objects, each containing one
|
598
|
+
connected patch of the original mesh. These objects will be clones of the original mesh, so modifying them will
|
599
|
+
have no effect on the original mesh.
|
600
|
+
:return:
|
601
|
+
"""
|
602
|
+
|
603
|
+
def create_from_indices(self, indices: List[int]) -> Mesh:
|
604
|
+
"""
|
605
|
+
Create a new mesh from a list of triangle indices. This will build a new mesh object containing only the
|
606
|
+
triangles (and their respective vertices) identified by the given list of indices. Do not allow duplicate
|
607
|
+
indices in the list.
|
608
|
+
:param indices: the triangle indices to include in the new mesh
|
609
|
+
:return:
|
610
|
+
"""
|
611
|
+
...
|
612
|
+
|
613
|
+
def measure_point_deviation(self, x: float, y: float, z: float, dist_mode: DeviationMode) -> Length3:
|
614
|
+
"""
|
615
|
+
Compute the deviation of a point from this mesh's surface and return it as a measurement object.
|
616
|
+
|
617
|
+
The deviation is the distance from the point to its closest projection onto the mesh using
|
618
|
+
the specified distance mode. The direction of the measurement is the direction between the
|
619
|
+
point and the projection, flipped into the positive half-space of the mesh surface at the
|
620
|
+
projection point.
|
621
|
+
|
622
|
+
If the distance is less than a very small floating point epsilon, the direction will be
|
623
|
+
taken directly from the mesh surface normal.
|
624
|
+
|
625
|
+
The first point `.a` of the measurement is the reference point, and the second point `.b`
|
626
|
+
is the test point.
|
627
|
+
|
628
|
+
:param x: the x component of the point to measure
|
629
|
+
:param y: the y component of the point to measure
|
630
|
+
:param z: the z component of the point to measure
|
631
|
+
:param dist_mode: the deviation mode to use
|
632
|
+
:return:
|
633
|
+
"""
|
634
|
+
|
635
|
+
def boundary_first_flatten(self) -> numpy.ndarray[float]:
|
636
|
+
"""
|
637
|
+
|
638
|
+
:return:
|
639
|
+
"""
|
640
|
+
|
641
|
+
|
642
|
+
class MeshTriangleFilter:
|
643
|
+
def collect(self) -> List[int]:
|
644
|
+
"""
|
645
|
+
Collect the final indices of the triangles that passed the filter.
|
646
|
+
:return:
|
647
|
+
"""
|
648
|
+
...
|
649
|
+
|
650
|
+
def create_mesh(self) -> Mesh:
|
651
|
+
"""
|
652
|
+
Create a new mesh from the filtered triangles. This will build a new mesh object containing only the triangles
|
653
|
+
(and their respective vertices) that are still retained in the filter.
|
654
|
+
:return:
|
655
|
+
"""
|
656
|
+
...
|
657
|
+
|
658
|
+
def facing(self, x: float, y: float, z: float, angle: float, mode: SelectOp) -> MeshTriangleFilter:
|
659
|
+
"""
|
660
|
+
|
661
|
+
:param x:
|
662
|
+
:param y:
|
663
|
+
:param z:
|
664
|
+
:param angle:
|
665
|
+
:param mode:
|
666
|
+
:return:
|
667
|
+
"""
|
668
|
+
...
|
669
|
+
|
670
|
+
def near_mesh(
|
671
|
+
self,
|
672
|
+
other: Mesh,
|
673
|
+
all_points: bool,
|
674
|
+
distance_tol: float,
|
675
|
+
mode: SelectOp,
|
676
|
+
planar_tol: float | None = None,
|
677
|
+
angle_tol: float | None = None,
|
678
|
+
) -> MeshTriangleFilter:
|
679
|
+
"""
|
680
|
+
Reduce the list of indices to only include triangles that are within a certain distance of
|
681
|
+
their closest projection onto another mesh. The distance can require that all points of the
|
682
|
+
triangle are within the tolerance, or just one.
|
683
|
+
|
684
|
+
There are two additional optional tolerances that can be applied.
|
685
|
+
|
686
|
+
1. A planar tolerance, which checks the distance of the vertex projected onto the plane of
|
687
|
+
the reference mesh triangle and looks at how far it is from the projection point. This
|
688
|
+
is useful to filter out triangles that go past the edge of the reference mesh.
|
689
|
+
2. An angle tolerance, which checks the angle between the normal of the current triangle
|
690
|
+
and the normal of the reference triangle. This is useful to filter out triangles that
|
691
|
+
are not facing the same direction as the reference mesh.
|
692
|
+
|
693
|
+
:param other: the mesh to use as a reference
|
694
|
+
:param all_points: if True, all points of the triangle must be within the tolerance, if False, only one point
|
695
|
+
:param distance_tol: the maximum distance between the triangle and its projection onto the reference mesh
|
696
|
+
:param mode:
|
697
|
+
:param planar_tol: the maximum in-plane distance between the triangle and its projection onto the reference mesh
|
698
|
+
:param angle_tol: the maximum angle between the normals of the triangle and the reference mesh
|
699
|
+
"""
|
700
|
+
...
|
701
|
+
|
573
702
|
|
574
703
|
class CurveStation3:
|
575
704
|
"""
|
@@ -611,11 +740,12 @@ class Curve3:
|
|
611
740
|
between them.
|
612
741
|
"""
|
613
742
|
|
614
|
-
def __init__(self, vertices: numpy.ndarray):
|
743
|
+
def __init__(self, vertices: numpy.ndarray, tol: float = 1.0e-6):
|
615
744
|
"""
|
616
745
|
Create a curve from a set of vertices. The vertices should be a numpy array of shape (n, 3).
|
617
746
|
|
618
747
|
:param vertices: a numpy array of shape (n, 3) containing the vertices of the curve.
|
748
|
+
:param tol: the inherent tolerance of the curve; points closer than this distance will be considered the same.
|
619
749
|
"""
|
620
750
|
...
|
621
751
|
|
@@ -636,13 +766,11 @@ class Curve3:
|
|
636
766
|
"""
|
637
767
|
...
|
638
768
|
|
639
|
-
|
769
|
+
@property
|
770
|
+
def points(self) -> numpy.ndarray[float]:
|
640
771
|
"""
|
641
|
-
Will return
|
642
|
-
|
643
|
-
modify the curve.
|
644
|
-
|
645
|
-
:return: a numpy array of shape (n, 3) containing the vertices of the curve.
|
772
|
+
Will return an immutable view of the vertices of the mesh as a numpy array of shape (n, 3).
|
773
|
+
:return: a numpy array of shape (n, 3) containing the vertices of the mesh.
|
646
774
|
"""
|
647
775
|
...
|
648
776
|
|
@@ -723,3 +851,41 @@ class Curve3:
|
|
723
851
|
:return: a new curve object with the transformed vertices.
|
724
852
|
"""
|
725
853
|
...
|
854
|
+
|
855
|
+
|
856
|
+
class Aabb3:
|
857
|
+
"""
|
858
|
+
A class representing an axis-aligned bounding box in 3D space. The box is defined by its minimum and maximum
|
859
|
+
"""
|
860
|
+
|
861
|
+
def __init__(self, x_min: float, y_min: float, z_min: float, x_max: float, y_max: float, z_max: float):
|
862
|
+
"""
|
863
|
+
Create an axis-aligned bounding box from the minimum and maximum coordinates.
|
864
|
+
:param x_min: the minimum x coordinate of the box.
|
865
|
+
:param y_min: the minimum y coordinate of the box.
|
866
|
+
:param z_min: the minimum z coordinate of the box.
|
867
|
+
:param x_max: the maximum x coordinate of the box.
|
868
|
+
:param y_max: the maximum y coordinate of the box.
|
869
|
+
:param z_max: the maximum z coordinate of the box.
|
870
|
+
"""
|
871
|
+
...
|
872
|
+
|
873
|
+
@property
|
874
|
+
def min(self) -> Point3:
|
875
|
+
""" The minimum point of the box. """
|
876
|
+
...
|
877
|
+
|
878
|
+
@property
|
879
|
+
def max(self) -> Point3:
|
880
|
+
""" The maximum point of the box. """
|
881
|
+
...
|
882
|
+
|
883
|
+
@property
|
884
|
+
def center(self) -> Point3:
|
885
|
+
""" The center point of the box. """
|
886
|
+
...
|
887
|
+
|
888
|
+
@property
|
889
|
+
def extent(self) -> Vector3:
|
890
|
+
""" The extent of the box. """
|
891
|
+
...
|
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,157 @@ 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
|
+
template: 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
|
+
pad_scale = self._font_height(12) * 1.5
|
135
|
+
center = length.center.shift_orthogonal(side_shift)
|
136
|
+
leader_a = center.projection(length.a)
|
137
|
+
leader_b = center.projection(length.b)
|
138
|
+
|
139
|
+
if label_place == LabelPlace.Inside:
|
140
|
+
label_offset = label_offset or 0.0
|
141
|
+
label_coords = center.at_distance(label_offset)
|
142
|
+
self.arrow(label_coords, leader_a)
|
143
|
+
self.arrow(label_coords, leader_b)
|
144
|
+
elif label_place == LabelPlace.Outside:
|
145
|
+
label_offset = label_offset or pad_scale * 3
|
146
|
+
label_coords = leader_b + length.direction * label_offset
|
147
|
+
self.arrow(leader_a - length.direction * pad_scale, leader_a)
|
148
|
+
self.arrow(label_coords, leader_b)
|
149
|
+
elif label_place == LabelPlace.OutsideRev:
|
150
|
+
label_offset = label_offset or pad_scale * 3
|
151
|
+
label_coords = leader_a - length.direction * label_offset
|
152
|
+
self.arrow(leader_b + length.direction * pad_scale, leader_b)
|
153
|
+
self.arrow(label_coords, leader_a)
|
154
|
+
|
155
|
+
# Do we need sideways leaders?
|
156
|
+
self._line_if_needed(pad_scale, length.a, leader_a)
|
157
|
+
self._line_if_needed(pad_scale, length.b, leader_b)
|
158
|
+
|
159
|
+
kwargs = {"ha": "center", "va": "center", "fontsize": fontsize}
|
160
|
+
if fontname is not None:
|
161
|
+
kwargs["fontname"] = fontname
|
162
|
+
|
163
|
+
result = self.annotate_text_only(
|
164
|
+
template.format(value=length.value),
|
165
|
+
label_coords,
|
166
|
+
bbox=dict(boxstyle="round,pad=0.3", ec="black", fc="white"),
|
167
|
+
**kwargs,
|
168
|
+
)
|
169
|
+
|
170
|
+
def _line_if_needed(self, pad: float, actual: Point2, leader_end: Point2):
|
171
|
+
half_pad = pad * 0.5
|
172
|
+
v: Vector2 = leader_end - actual
|
173
|
+
if v.norm() < half_pad:
|
174
|
+
return
|
175
|
+
work = SurfacePoint2(*actual, *v)
|
176
|
+
t1 = work.scalar_projection(leader_end) + half_pad
|
177
|
+
self.arrow(actual, work.at_distance(t1), arrow="-")
|
178
|
+
|
179
|
+
def annotate_text_only(self, text: str, pos: PlotCoords, **kwargs):
|
180
|
+
"""
|
181
|
+
Annotate a Matplotlib Axes object with text only.
|
182
|
+
:param text: the text to annotate
|
183
|
+
:param pos: the position of the annotation
|
184
|
+
:param kwargs: keyword arguments to pass to the annotate function
|
185
|
+
:return: None
|
186
|
+
"""
|
187
|
+
return self.ax.annotate(text, xy=_tuplefy(pos), **kwargs)
|
188
|
+
|
189
|
+
def arrow(self, start: PlotCoords, end: PlotCoords, arrow="-|>"):
|
190
|
+
"""
|
191
|
+
Plot an arrow on a Matplotlib Axes object.
|
192
|
+
:param start: the start point of the arrow
|
193
|
+
:param end: the end point of the arrow
|
194
|
+
:param kwargs: keyword arguments to pass to the arrow function
|
195
|
+
:return: None
|
196
|
+
"""
|
197
|
+
self.ax.annotate(
|
198
|
+
"",
|
199
|
+
xy=_tuplefy(end),
|
200
|
+
xytext=_tuplefy(start),
|
201
|
+
arrowprops=dict(arrowstyle=arrow, fc="black"),
|
202
|
+
)
|
203
|
+
|
204
|
+
def _font_height(self, font_size: int) -> float:
|
205
|
+
"""Get the height of a font in data units."""
|
206
|
+
fig_dpi = self.ax.figure.dpi
|
207
|
+
font_height_inches = font_size * 1.0 / 72.0
|
208
|
+
font_height_px = font_height_inches * fig_dpi
|
209
|
+
|
210
|
+
px_per_data = self._get_scale()
|
211
|
+
return font_height_px / px_per_data
|
212
|
+
|
213
|
+
def _get_scale(self) -> float:
|
214
|
+
"""Get the scale of the plot in data units per pixel."""
|
215
|
+
x0, x1 = self.ax.get_xlim()
|
216
|
+
y0, y1 = self.ax.get_ylim()
|
217
|
+
|
218
|
+
bbox = self.ax.get_window_extent()
|
219
|
+
width, height = bbox.width, bbox.height
|
220
|
+
|
221
|
+
# Units are pixels per data unit
|
222
|
+
x_scale = width / (x1 - x0)
|
223
|
+
y_scale = height / (y1 - y0)
|
224
|
+
|
225
|
+
return min(x_scale, y_scale)
|
226
|
+
|
227
|
+
def _tuplefy(item: PlotCoords) -> Tuple[float, float]:
|
228
|
+
if isinstance(item, (Point2, Vector2)):
|
229
|
+
return item.x, item.y
|
230
|
+
else:
|
231
|
+
x, y, *_ = item
|
232
|
+
return x, y
|
engeom/metrology.pyi
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
from .geom2 import Point2, Vector2, SurfacePoint2
|
2
|
+
from .geom3 import Point3, Vector3, SurfacePoint3
|
3
|
+
|
4
|
+
|
5
|
+
class Length2:
|
6
|
+
def __init__(self, a: Point2, b: Point2, direction: Vector2 | None = None):
|
7
|
+
"""
|
8
|
+
|
9
|
+
:param a:
|
10
|
+
:param b:
|
11
|
+
:param direction:
|
12
|
+
"""
|
13
|
+
...
|
14
|
+
|
15
|
+
@property
|
16
|
+
def a(self) -> Point2:
|
17
|
+
...
|
18
|
+
|
19
|
+
@property
|
20
|
+
def b(self) -> Point2:
|
21
|
+
...
|
22
|
+
|
23
|
+
@property
|
24
|
+
def direction(self) -> Vector2:
|
25
|
+
...
|
26
|
+
|
27
|
+
@property
|
28
|
+
def value(self) -> float:
|
29
|
+
...
|
30
|
+
|
31
|
+
@property
|
32
|
+
def center(self) -> SurfacePoint2:
|
33
|
+
...
|
34
|
+
|
35
|
+
|
36
|
+
class Length3:
|
37
|
+
def __init__(self, a: Point3, b: Point3, direction: Vector3 | None = None):
|
38
|
+
"""
|
39
|
+
|
40
|
+
:param a:
|
41
|
+
:param b:
|
42
|
+
:param direction:
|
43
|
+
"""
|
44
|
+
...
|
45
|
+
|
46
|
+
@property
|
47
|
+
def a(self) -> Point3:
|
48
|
+
...
|
49
|
+
|
50
|
+
@property
|
51
|
+
def b(self) -> Point3:
|
52
|
+
...
|
53
|
+
|
54
|
+
@property
|
55
|
+
def direction(self) -> Vector3:
|
56
|
+
...
|
57
|
+
|
58
|
+
@property
|
59
|
+
def value(self) -> float:
|
60
|
+
...
|
61
|
+
|
62
|
+
@property
|
63
|
+
def center(self) -> SurfacePoint3:
|
64
|
+
...
|