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/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
- type Transformable3 = Vector3 | Point3 | Plane3 | Iso3 | SurfacePoint3
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: Vector3 | Point3) -> Vector3 | Point3:
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: Vector3 | Point3) -> Vector3 | Point3:
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
- triangles: numpy.ndarray[numpy.uint32],
437
- merge_duplicates: bool | None = None,
438
- delete_degenerate: bool | None = None
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 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. If None, the default behavior is to do nothing
449
- :param delete_degenerate: delete degenerate triangles. If None, the default behavior is to do nothing
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 | None = None, delete_degenerate: bool | None = None) -> Mesh:
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 clone(self) -> Mesh:
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
- def clone_vertices(self) -> numpy.ndarray[float]:
509
+ @property
510
+ def vertices(self) -> numpy.ndarray[float]:
503
511
  """
504
- Will return a copy of the vertices of the mesh as a numpy array. If the mesh has not been modified, this will
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
- def clone_triangles(self) -> numpy.ndarray[numpy.uint32]:
517
+ @property
518
+ def faces(self) -> numpy.ndarray[numpy.uint32]:
512
519
  """
513
- Will return a copy of the triangles of the mesh as a numpy array. If the mesh has not been modified, this will
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
- def clone_vertices(self) -> numpy.ndarray[float]:
769
+ @property
770
+ def points(self) -> numpy.ndarray[float]:
640
771
  """
641
- Will return a copy of the vertices of the curve as a numpy array. If the curve has not been modified, this will
642
- be the same as the original vertices. This is a copy of the data, so modifying the returned array will not
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
- [1, 0, 160],
17
- [1, 0, 255],
18
- [0, 254, 255],
19
- [0, 160, 0],
20
- [0, 254, 0],
21
- [255, 255, 0],
22
- [255, 128, 0],
23
- [255, 1, 0]
24
- ], dtype=numpy.float64)
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
@@ -0,0 +1,5 @@
1
+ from ..engeom import _metrology
2
+
3
+ # Global import of all functions
4
+ for name in [n for n in dir(_metrology) if not n.startswith("_")]:
5
+ globals()[name] = getattr(_metrology, name)
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
+ ...