engeom 0.2.4__cp38-abi3-win_amd64.whl → 0.2.6__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.
@@ -0,0 +1 @@
1
+ from .common import LabelPlace
engeom/_plot/common.py ADDED
@@ -0,0 +1,17 @@
1
+ from enum import Enum
2
+
3
+ class LabelPlace(Enum):
4
+ """
5
+ Represents the different locations where a label can be placed between its anchor points.
6
+ """
7
+
8
+ Outside = 1
9
+ """ The label is placed outside the anchor points, on the side of the second point in the measurement. """
10
+
11
+ Inside = 2
12
+ """ The label is placed between the two anchor points. """
13
+
14
+ OutsideRev = 3
15
+ """ The label is placed outside the two anchor points, on the side of the first point in the measurement. """
16
+
17
+
@@ -1,19 +1,11 @@
1
1
  from typing import List, Iterable, Tuple, Union
2
- from enum import Enum
3
- import matplotlib.lines
4
2
  import numpy
5
- from .geom2 import Curve2, Circle2, Aabb2, Point2, Vector2, SurfacePoint2
6
- from .metrology import Length2
3
+ from .common import LabelPlace
4
+ from engeom.geom2 import Curve2, Circle2, Aabb2, Point2, Vector2, SurfacePoint2
5
+ from engeom.metrology import Distance2
7
6
 
8
7
  PlotCoords = Union[Point2, Vector2, Iterable[float]]
9
8
 
10
-
11
- class LabelPlace(Enum):
12
- Outside = 1
13
- Inside = 2
14
- OutsideRev = 3
15
-
16
-
17
9
  try:
18
10
  from matplotlib.pyplot import Axes, Circle
19
11
  from matplotlib.colors import ListedColormap
@@ -22,6 +14,12 @@ except ImportError:
22
14
  else:
23
15
 
24
16
  class GomColorMap(ListedColormap):
17
+ """
18
+ A color map similar to the 8 discrete colors in the GOM/Zeiss Inspect software.
19
+
20
+ You can use this to instantiate a color map, or you can use the `GOM_CMAP` object directly.
21
+ """
22
+
25
23
  def __init__(self):
26
24
  colors = numpy.array(
27
25
  [
@@ -42,7 +40,13 @@ else:
42
40
  self.set_under("magenta")
43
41
  self.set_over("darkred")
44
42
 
43
+
45
44
  GOM_CMAP = GomColorMap()
45
+ """
46
+ A color map similar to the 8 discrete colors in the GOM/Zeiss Inspect software, already instantiated and
47
+ available in the module.
48
+ """
49
+
46
50
 
47
51
  def set_aspect_fill(ax: Axes):
48
52
  """
@@ -77,8 +81,30 @@ else:
77
81
  x_mid = (x0 + x1) / 2
78
82
  ax.set_xlim(x_mid - x_range / 2, x_mid + x_range / 2)
79
83
 
80
- class AxesHelper:
84
+
85
+ class MatplotlibAxesHelper:
86
+ """
87
+ A helper class for working with Matplotlib. It wraps around a Matplotlib `Axes` object and provides direct
88
+ methods for plotting some `engeom` entities. It also enforces the aspect ratio to be 1:1 and expands the
89
+ subplot to fill its available space.
90
+
91
+ !!! example
92
+ ```python
93
+ from matplotlib.pyplot import figure
94
+ fig = figure()
95
+ ax = fig.subplots()
96
+ helper = MatplotlibAxesHelper(ax)
97
+ ```
98
+ """
99
+
81
100
  def __init__(self, ax: Axes, skip_aspect=False, hide_axes=False):
101
+ """
102
+ Initialize the helper with a Matplotlib `Axes` object.
103
+ :param ax: The Matplotlib `Axes` object to wrap around.
104
+ :param skip_aspect: Set this to true to skip enforcing the aspect ratio to be 1:1.
105
+ :param hide_axes: Set this to true to hide the axes.
106
+ """
107
+
82
108
  self.ax = ax
83
109
  if not skip_aspect:
84
110
  ax.set_aspect("equal", adjustable="datalim")
@@ -90,7 +116,6 @@ else:
90
116
  """
91
117
  Set the bounds of a Matplotlib Axes object.
92
118
  :param box: an Aabb2 object
93
- :return: None
94
119
  """
95
120
  self.ax.set_xlim(box.min.x, box.max.x)
96
121
  self.ax.set_ylim(box.min.y, box.max.y)
@@ -100,7 +125,6 @@ else:
100
125
  Plot a circle on a Matplotlib Axes object.
101
126
  :param circle: a Circle2 object
102
127
  :param kwargs: keyword arguments to pass to the plot function
103
- :return: None
104
128
  """
105
129
  from matplotlib.pyplot import Circle
106
130
 
@@ -117,24 +141,45 @@ else:
117
141
  Plot a curve on a Matplotlib Axes object.
118
142
  :param curve: a Curve2 object
119
143
  :param kwargs: keyword arguments to pass to the plot function
120
- :return: None
121
144
  """
122
145
  self.ax.plot(curve.points[:, 0], curve.points[:, 1], **kwargs)
123
146
 
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,
147
+ def distance(
148
+ self,
149
+ distance: Distance2,
150
+ side_shift: float = 0,
151
+ template: str = "{value:.3f}",
152
+ fontsize: int = 10,
153
+ label_place: LabelPlace = LabelPlace.Outside,
154
+ label_offset: float | None = None,
155
+ fontname: str | None = None,
156
+ scale_value: float = 1.0,
133
157
  ):
158
+ """
159
+ Plot a `Distance2` object on a Matplotlib Axes, drawing the leader lines and adding a text label with the
160
+ distance value.
161
+ :param distance: The `Distance2` object to plot.
162
+ :param side_shift: Shift the ends of the leader lines by this amount of data units. The direction of the
163
+ shift is orthogonal to the distance direction, with positive values shifting to the right.
164
+ :param template: The format string to use for the distance label. The default is "{value:.3f}".
165
+ :param fontsize: The font size to use for the label.
166
+ :param label_place: The placement of the label.
167
+ :param label_offset: The distance offset to use for the label. Will have different meanings depending on
168
+ the `label_place` parameter.
169
+ :param fontname: The name of the font to use for the label.
170
+ :param scale_value: A scaling factor to apply to the value before displaying it in the label. Use this to
171
+ convert between different units of measurement without having to modify the actual value or the coordinate
172
+ system.
173
+ """
134
174
  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)
175
+
176
+ # The offset_dir is the direction from `a` to `b` projected so that it's parallel to the measurement
177
+ # direction.
178
+ offset_dir = distance.direction if distance.value >= 0 else -distance.direction
179
+ center = SurfacePoint2(*distance.center.point, *offset_dir)
180
+ center = center.shift_orthogonal(side_shift)
181
+ leader_a = center.projection(distance.a)
182
+ leader_b = center.projection(distance.b)
138
183
 
139
184
  if label_place == LabelPlace.Inside:
140
185
  label_offset = label_offset or 0.0
@@ -143,29 +188,26 @@ else:
143
188
  self.arrow(label_coords, leader_b)
144
189
  elif label_place == LabelPlace.Outside:
145
190
  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)
191
+ label_coords = leader_b + offset_dir * label_offset
192
+ self.arrow(leader_a - offset_dir * pad_scale, leader_a)
148
193
  self.arrow(label_coords, leader_b)
149
194
  elif label_place == LabelPlace.OutsideRev:
150
195
  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)
196
+ label_coords = leader_a - offset_dir * label_offset
197
+ self.arrow(leader_b + offset_dir * pad_scale, leader_b)
153
198
  self.arrow(label_coords, leader_a)
154
199
 
155
200
  # 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)
201
+ self._line_if_needed(pad_scale, distance.a, leader_a)
202
+ self._line_if_needed(pad_scale, distance.b, leader_b)
158
203
 
159
204
  kwargs = {"ha": "center", "va": "center", "fontsize": fontsize}
160
205
  if fontname is not None:
161
206
  kwargs["fontname"] = fontname
162
207
 
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
- )
208
+ value = distance.value * scale_value
209
+ box_style = dict(boxstyle="round,pad=0.3", ec="black", fc="white")
210
+ self.annotate_text_only(template.format(value=value), label_coords, bbox=box_style, **kwargs)
169
211
 
170
212
  def _line_if_needed(self, pad: float, actual: Point2, leader_end: Point2):
171
213
  half_pad = pad * 0.5
@@ -182,7 +224,7 @@ else:
182
224
  :param text: the text to annotate
183
225
  :param pos: the position of the annotation
184
226
  :param kwargs: keyword arguments to pass to the annotate function
185
- :return: None
227
+ :return: the annotation object
186
228
  """
187
229
  return self.ax.annotate(text, xy=_tuplefy(pos), **kwargs)
188
230
 
@@ -191,10 +233,10 @@ else:
191
233
  Plot an arrow on a Matplotlib Axes object.
192
234
  :param start: the start point of the arrow
193
235
  :param end: the end point of the arrow
194
- :param kwargs: keyword arguments to pass to the arrow function
195
- :return: None
236
+ :param arrow: the style of arrow to use
237
+ :return: the annotation object
196
238
  """
197
- self.ax.annotate(
239
+ return self.ax.annotate(
198
240
  "",
199
241
  xy=_tuplefy(end),
200
242
  xytext=_tuplefy(start),
@@ -202,7 +244,7 @@ else:
202
244
  )
203
245
 
204
246
  def _font_height(self, font_size: int) -> float:
205
- """Get the height of a font in data units."""
247
+ # Get the height of a font in data units
206
248
  fig_dpi = self.ax.figure.dpi
207
249
  font_height_inches = font_size * 1.0 / 72.0
208
250
  font_height_px = font_height_inches * fig_dpi
@@ -211,7 +253,7 @@ else:
211
253
  return font_height_px / px_per_data
212
254
 
213
255
  def _get_scale(self) -> float:
214
- """Get the scale of the plot in data units per pixel."""
256
+ # Get the scale of the plot in data units per pixel.
215
257
  x0, x1 = self.ax.get_xlim()
216
258
  y0, y1 = self.ax.get_ylim()
217
259
 
@@ -224,6 +266,7 @@ else:
224
266
 
225
267
  return min(x_scale, y_scale)
226
268
 
269
+
227
270
  def _tuplefy(item: PlotCoords) -> Tuple[float, float]:
228
271
  if isinstance(item, (Point2, Vector2)):
229
272
  return item.x, item.y
@@ -0,0 +1,256 @@
1
+ """
2
+ This module contains helper functions for working with PyVista.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import List, Any, Dict, Union, Iterable, Tuple
8
+
9
+ import numpy
10
+
11
+ from engeom.geom3 import Mesh, Curve3, Vector3, Point3, Iso3, SurfacePoint3
12
+ from engeom.metrology import Distance3
13
+ from .common import LabelPlace
14
+
15
+ PlotCoords = Union[Point3, Vector3, Iterable[float]]
16
+
17
+ try:
18
+ import pyvista
19
+ except ImportError:
20
+ pass
21
+ else:
22
+ class PyvistaPlotterHelper:
23
+ """
24
+ A helper class for working with PyVista. It wraps around a PyVista `Plotter` object and provides direct methods
25
+ for plotting some `engeom` entities.
26
+
27
+ !!! example
28
+ ```python
29
+ import pyvista
30
+ plotter = pyvista.Plotter()
31
+ helper = PyvistaPlotterHelper(plotter)
32
+ ```
33
+ """
34
+
35
+ def __init__(self, plotter: pyvista.Plotter):
36
+ """
37
+ Initialize the helper with a PyVista `Plotter` object.
38
+
39
+ :param plotter: The PyVista `Plotter` object to wrap around.
40
+ """
41
+ self.plotter = plotter
42
+
43
+ def add_curves(
44
+ self,
45
+ *curves: Curve3,
46
+ color: pyvista.ColorLike = "w",
47
+ width: float = 3.0,
48
+ label: str | None = None,
49
+ name: str | None = None,
50
+ ) -> pyvista.vtkActor:
51
+ """
52
+ Add one or more curves to be plotted.
53
+ :param curves: The curves to add.
54
+ :param color: The color to use for the curve(s).
55
+ :param width: The line width to use for the curve(s).
56
+ :param label: The label to use for the curve(s) if a legend is present.
57
+ :param name: The name to use for the actor in the scene.
58
+ :return: The PyVista actor that was added to the plotter.
59
+ """
60
+ curve_vertices = []
61
+ for curve in curves:
62
+ b = curve.points[1:-1]
63
+ c = numpy.zeros((len(curve.points) + len(b), 3), dtype=curve.points.dtype)
64
+ c[0::2, :] = curve.points[0:-1]
65
+ c[1:-1:2, :] = b
66
+ c[-1] = curve.points[-1]
67
+ curve_vertices.append(c)
68
+
69
+ vertices = numpy.concatenate(curve_vertices, axis=0)
70
+ return self.plotter.add_lines(
71
+ vertices,
72
+ color=color,
73
+ width=width,
74
+ label=label,
75
+ name=name,
76
+ )
77
+
78
+ def add_mesh(self, mesh: Mesh, **kwargs) -> pyvista.vtkActor:
79
+ """
80
+ Add an `engeom` mesh to be plotted. Additional keyword arguments will be passed directly to the PyVista
81
+ `Plotter.add_mesh` method, allowing for customization of the mesh appearance.
82
+
83
+ :param mesh: The mesh object to add to the plotter scene
84
+ :return: The PyVista actor that was added to the plotter.
85
+ """
86
+ if "cmap" in kwargs:
87
+ cmap_extremes = _cmap_extremes(kwargs["cmap"])
88
+ kwargs.update(cmap_extremes)
89
+
90
+ prefix = numpy.ones((mesh.faces.shape[0], 1), dtype=mesh.faces.dtype)
91
+ faces = numpy.hstack((prefix * 3, mesh.faces))
92
+ data = pyvista.PolyData(mesh.vertices, faces)
93
+ return self.plotter.add_mesh(data, **kwargs)
94
+
95
+ def distance(
96
+ self,
97
+ distance: Distance3,
98
+ template: str = "{value:.3f}",
99
+ label_place: LabelPlace = LabelPlace.Outside,
100
+ label_offset: float | None = None,
101
+ font_size: int = 12,
102
+ scale_value: float = 1.0,
103
+ font_family=None,
104
+ ):
105
+ """
106
+ Add a distance entity to the plotter.
107
+ :param distance: The distance entity to add.
108
+ :param template: A format string to use for the label. The `value` key will be replaced with the actual
109
+ value read from the measurement.
110
+ :param label_place: The placement of the label relative to the distance entity's anchor points.
111
+ :param label_offset: The distance offset to use for the label. Will have different meanings depending on
112
+ the `label_place` parameter.
113
+ :param font_size: The size of the text to use for the label.
114
+ :param scale_value: A scaling factor to apply to the value before displaying it in the label. Use this to
115
+ convert between different units of measurement without having to modify the actual value or the coordinate
116
+ system.
117
+ :param font_family: The font family to use for the label.
118
+ """
119
+ label_offset = label_offset or max(abs(distance.value), 1.0) * 3
120
+
121
+ # The offset_dir is the direction from `a` to `b` projected so that it's parallel to the measurement
122
+ # direction.
123
+ offset_dir = distance.direction if distance.value >= 0 else -distance.direction
124
+
125
+ # Rather than arrows, we'll use spheres to indicate the anchor points at the end of the leader lines
126
+ spheres = [distance.a, distance.b]
127
+ builder = LineBuilder()
128
+
129
+ if label_place == LabelPlace.Inside:
130
+ c = SurfacePoint3(*distance.center.point, *offset_dir)
131
+ label_coords = c.at_distance(label_offset)
132
+
133
+ builder.add(distance.a)
134
+ builder.add(distance.a - offset_dir * label_offset * 0.25)
135
+ builder.skip()
136
+
137
+ builder.add(distance.b)
138
+ builder.add(distance.b + offset_dir * label_offset * 0.25)
139
+
140
+ elif label_place == LabelPlace.Outside:
141
+ label_coords = distance.b + offset_dir * label_offset
142
+
143
+ builder.add(distance.a)
144
+ builder.add(distance.a - offset_dir * label_offset * 0.25)
145
+ builder.skip()
146
+
147
+ builder.add(distance.b)
148
+ builder.add(label_coords)
149
+
150
+ elif label_place == LabelPlace.OutsideRev:
151
+ label_coords = distance.a - offset_dir * label_offset
152
+
153
+ builder.add(distance.b)
154
+ builder.add(distance.b + offset_dir * label_offset * 0.25)
155
+ builder.skip()
156
+
157
+ builder.add(distance.a)
158
+ builder.add(label_coords)
159
+
160
+ points = numpy.array([_tuplefy(p) for p in spheres], dtype=numpy.float64)
161
+ self.plotter.add_points(points, color="black", point_size=4, render_points_as_spheres=True)
162
+
163
+ lines = builder.build()
164
+ self.plotter.add_lines(lines, color="black", width=1.5)
165
+
166
+ value = distance.value * scale_value
167
+ self.plotter.add_point_labels(
168
+ [_tuplefy(label_coords)],
169
+ [template.format(value=value)],
170
+ show_points=False,
171
+ background_color="white",
172
+ font_family=font_family,
173
+ # justification_vertical="center",
174
+ # justification_horizontal="center",
175
+ font_size=font_size,
176
+ bold=False,
177
+ )
178
+
179
+ def coordinate_frame(self, iso: Iso3, size: float = 1.0):
180
+ """
181
+ Add a coordinate frame to the plotter. This will appear as three lines, with X in red, Y in green,
182
+ and Z in blue. The length of each line is determined by the `size` parameter.
183
+ :param iso: The isometry to use as the origin and orientation of the coordinate frame.
184
+ :param size: The length of each line in the coordinate frame.
185
+ """
186
+ points = numpy.array([[0, 0, 0], [size, 0, 0], [0, size, 0], [0, 0, size]], dtype=numpy.float64)
187
+ points = iso.transform_points(points)
188
+
189
+ self.plotter.add_lines(points[[0, 1]], color="red", width=5.0)
190
+ self.plotter.add_lines(points[[0, 2]], color="green", width=5.0)
191
+ self.plotter.add_lines(points[[0, 3]], color="blue", width=5.0)
192
+
193
+ def label(self, point: PlotCoords, text: str, **kwargs):
194
+ """
195
+ Add a text label to the plotter.
196
+ :param point: The position of the label in 3D space.
197
+ :param text: The text to display in the label.
198
+ :param kwargs: Additional keyword arguments to pass to the `pyvista.Label` constructor.
199
+ """
200
+ label = pyvista.Label(text=text, position=_tuplefy(point), **kwargs)
201
+ self.plotter.add_actor(label)
202
+
203
+ def arrow(self, start: PlotCoords, direction: PlotCoords,
204
+ tip_length: float = 0.25,
205
+ tip_radius: float = 0.1,
206
+ shaft_radius: float = 0.05,
207
+ **kwargs):
208
+ pd = pyvista.Arrow(_tuplefy(start), _tuplefy(direction), tip_length=tip_length, tip_radius=tip_radius,
209
+ shaft_radius=shaft_radius)
210
+ self.plotter.add_mesh(pd, **kwargs, color="black")
211
+
212
+
213
+ def _cmap_extremes(item: Any) -> Dict[str, pyvista.ColorLike]:
214
+ working = {}
215
+ try:
216
+ from matplotlib.colors import Colormap
217
+ except ImportError:
218
+ return working
219
+ else:
220
+ if isinstance(item, Colormap):
221
+ over = getattr(item, "_rgba_over", None)
222
+ under = getattr(item, "_rgba_under", None)
223
+ if over is not None:
224
+ working["above_color"] = over
225
+ if under is not None:
226
+ working["below_color"] = under
227
+ return working
228
+
229
+
230
+ class LineBuilder:
231
+ def __init__(self):
232
+ self.vertices = []
233
+ self._skip = 1
234
+
235
+ def add(self, points: PlotCoords):
236
+ if self.vertices:
237
+ if self._skip > 0:
238
+ self._skip -= 1
239
+ else:
240
+ self.vertices.append(self.vertices[-1])
241
+
242
+ self.vertices.append(_tuplefy(points))
243
+
244
+ def skip(self):
245
+ self._skip = 2
246
+
247
+ def build(self) -> numpy.ndarray:
248
+ return numpy.array(self.vertices, dtype=numpy.float64)
249
+
250
+
251
+ def _tuplefy(item: PlotCoords) -> Tuple[float, float, float]:
252
+ if isinstance(item, (Point3, Vector3)):
253
+ return item.x, item.y, item.z
254
+ else:
255
+ x, y, z, *_ = item
256
+ return x, y, z
@@ -1,3 +1,7 @@
1
+ """
2
+ This module contains a number of tools to help analyze airfoil geometries in 2D.
3
+ """
4
+
1
5
  from ..engeom import _airfoil
2
6
 
3
7
  # Global import of all functions