primlib 0.3.0__py3-none-any.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.
- primlib/__init__.py +6 -0
- primlib/_version.py +24 -0
- primlib/line_set.py +435 -0
- primlib/point_cloud.py +711 -0
- primlib/triangle_mesh.py +1910 -0
- primlib/utils.py +53 -0
- primlib/vertices.py +604 -0
- primlib/visualizer.py +179 -0
- primlib-0.3.0.dist-info/METADATA +124 -0
- primlib-0.3.0.dist-info/RECORD +13 -0
- primlib-0.3.0.dist-info/WHEEL +5 -0
- primlib-0.3.0.dist-info/licenses/LICENSE +22 -0
- primlib-0.3.0.dist-info/top_level.txt +1 -0
primlib/__init__.py
ADDED
primlib/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
primlib/line_set.py
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Self, override
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import ArrayLike
|
|
7
|
+
|
|
8
|
+
from primlib.triangle_mesh import TriangleMesh
|
|
9
|
+
from primlib.utils import scalar_to_bicolor, scalar_to_color
|
|
10
|
+
from primlib.vertices import Vertices
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LineSet(Vertices):
|
|
14
|
+
__slots__ = ("_line_colors", "_lines")
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
vertices: ArrayLike | None = None,
|
|
19
|
+
lines: ArrayLike | None = None,
|
|
20
|
+
vertex_colors: ArrayLike | None = None,
|
|
21
|
+
line_colors: ArrayLike | None = None,
|
|
22
|
+
):
|
|
23
|
+
"""Constructor
|
|
24
|
+
|
|
25
|
+
:param vertices: Vertices of the line set, shape (n, 3)
|
|
26
|
+
:param lines: Lines of the line set as vertex indices,
|
|
27
|
+
shape (m, 2), integer type
|
|
28
|
+
:param vertex_colors: RGB colors for each vertex, shape (n, 3),
|
|
29
|
+
values [0.0..1.0]
|
|
30
|
+
:param line_colors: RGB colors for each line, shape (m, 3),
|
|
31
|
+
values [0.0..1.0]
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(vertices=vertices, colors=vertex_colors)
|
|
34
|
+
# Define types and initialize
|
|
35
|
+
self._lines: np.ndarray = np.zeros((0, 2), dtype=int)
|
|
36
|
+
self._line_colors: np.ndarray = np.zeros((0, 3))
|
|
37
|
+
# Initialize using parameters and setters
|
|
38
|
+
self.lines = lines
|
|
39
|
+
self.line_colors = line_colors
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def num_vertices(self) -> int:
|
|
43
|
+
"""Number of vertices of the line set
|
|
44
|
+
:returns: Number of vertices
|
|
45
|
+
"""
|
|
46
|
+
return self._vertices.shape[0]
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def num_lines(self) -> int:
|
|
50
|
+
"""Number of faces of the line set
|
|
51
|
+
:returns: Number of lines
|
|
52
|
+
"""
|
|
53
|
+
return self._lines.shape[0]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def empty(self) -> bool:
|
|
57
|
+
"""Check if line set is empty
|
|
58
|
+
:returns: True if line set has no vertices and no lines
|
|
59
|
+
"""
|
|
60
|
+
return self.num_vertices == 0 and self.num_lines == 0
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str:
|
|
63
|
+
"""Get string representation of line set
|
|
64
|
+
:returns: String representation
|
|
65
|
+
"""
|
|
66
|
+
if self.empty:
|
|
67
|
+
return "LineSet(empty)"
|
|
68
|
+
pmin = self.min()
|
|
69
|
+
pmax = self.max()
|
|
70
|
+
result = (
|
|
71
|
+
f"LineSet({self.num_vertices:_} vertices: "
|
|
72
|
+
+ f"X[{pmin[0]:.3f}..{pmax[0]:.3f}], "
|
|
73
|
+
+ f"Y[{pmin[1]:.3f}..{pmax[1]:.3f}], "
|
|
74
|
+
+ f"Z[{pmin[2]:.3f}..{pmax[2]:.3f}]"
|
|
75
|
+
)
|
|
76
|
+
if self.has_colors:
|
|
77
|
+
result += ", vertex_colors"
|
|
78
|
+
result += f", {self.num_lines:_} lines"
|
|
79
|
+
if self.has_line_colors:
|
|
80
|
+
result += ", line_colors"
|
|
81
|
+
result += ")"
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
@override
|
|
85
|
+
def __eq__(self, other: object) -> bool:
|
|
86
|
+
"""Check two line sets for equality.
|
|
87
|
+
|
|
88
|
+
:param other: Other object
|
|
89
|
+
:returns: True if line sets are equal
|
|
90
|
+
"""
|
|
91
|
+
if not isinstance(other, LineSet):
|
|
92
|
+
return NotImplemented
|
|
93
|
+
return (
|
|
94
|
+
super().__eq__(other)
|
|
95
|
+
# Lines
|
|
96
|
+
and self._lines.shape == other._lines.shape
|
|
97
|
+
and np.array_equal(self._lines, other._lines)
|
|
98
|
+
# Line colors
|
|
99
|
+
and self._line_colors.shape == other._line_colors.shape
|
|
100
|
+
and np.allclose(
|
|
101
|
+
self._line_colors,
|
|
102
|
+
other._line_colors,
|
|
103
|
+
atol=1.0 / 255.0,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
def __ne__(self, other: object) -> bool:
|
|
109
|
+
"""Check two line sets for inequality
|
|
110
|
+
|
|
111
|
+
:param other: Other object
|
|
112
|
+
:returns: True, if line sets are not equal
|
|
113
|
+
"""
|
|
114
|
+
return not self.__eq__(other)
|
|
115
|
+
|
|
116
|
+
@override
|
|
117
|
+
def __iadd__(self, other: Vertices) -> Self:
|
|
118
|
+
"""Concatenate another line set in-place
|
|
119
|
+
|
|
120
|
+
:param other: Other line set
|
|
121
|
+
:returns: This line set (self)
|
|
122
|
+
"""
|
|
123
|
+
if not isinstance(other, LineSet):
|
|
124
|
+
raise TypeError("Can only append another LineSet")
|
|
125
|
+
if self.has_line_colors != other.has_line_colors:
|
|
126
|
+
raise ValueError("Both line sets must have the same properties")
|
|
127
|
+
# Append vertices and vertex properties
|
|
128
|
+
vertex_offset = self.num_vertices
|
|
129
|
+
super().__iadd__(other)
|
|
130
|
+
# Append lines, adjusting vertex indices
|
|
131
|
+
self._lines = np.vstack((self._lines, other._lines + vertex_offset))
|
|
132
|
+
# Append line colors
|
|
133
|
+
if self.has_line_colors:
|
|
134
|
+
self._line_colors = np.vstack((self._line_colors, other._line_colors))
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
@override
|
|
138
|
+
def __add__(self, other: Vertices) -> "LineSet":
|
|
139
|
+
"""Concatenate two line sets
|
|
140
|
+
|
|
141
|
+
:param other: Other line set
|
|
142
|
+
:returns: New line set
|
|
143
|
+
"""
|
|
144
|
+
result = self.copy()
|
|
145
|
+
result += other
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
def copy(self) -> Self:
|
|
149
|
+
"""Get copy of line set
|
|
150
|
+
|
|
151
|
+
:returns: Copy
|
|
152
|
+
"""
|
|
153
|
+
return copy.deepcopy(self)
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def lines(self) -> np.ndarray:
|
|
157
|
+
"""Get lines
|
|
158
|
+
|
|
159
|
+
Lines are 2-tuples of indices of type integer referencing vertices;
|
|
160
|
+
for example: the line (3, 7) is a line connecting vertices 3 and 7.
|
|
161
|
+
|
|
162
|
+
:returns: Lines, shape (n, 2), type int
|
|
163
|
+
"""
|
|
164
|
+
return self._lines
|
|
165
|
+
|
|
166
|
+
@lines.setter
|
|
167
|
+
def lines(self, value: ArrayLike | None) -> None:
|
|
168
|
+
"""Set lines
|
|
169
|
+
|
|
170
|
+
Lines are 2-tuples of indices of type integer referencing vertices;
|
|
171
|
+
for example: the line (3, 7) is a line connecting vertices 3 and 7.
|
|
172
|
+
|
|
173
|
+
If there are line colors defined in the line set,
|
|
174
|
+
then the number of lines has to match their number.
|
|
175
|
+
|
|
176
|
+
:param value: Lines, shape (n, 2), type int
|
|
177
|
+
"""
|
|
178
|
+
if value is None:
|
|
179
|
+
value = np.zeros((0, 2), dtype=int)
|
|
180
|
+
else:
|
|
181
|
+
value = np.asarray(value, dtype=int)
|
|
182
|
+
if value.ndim != 2 or value.shape[1] != 2:
|
|
183
|
+
raise Exception("Provide lines of shape (n, 2)")
|
|
184
|
+
if self.has_line_colors and value.shape[0] != self._line_colors.shape[0]:
|
|
185
|
+
raise Exception(
|
|
186
|
+
"Line set already has line colors, provide proper amount of lines"
|
|
187
|
+
)
|
|
188
|
+
unique_indices = np.unique(value)
|
|
189
|
+
if unique_indices.size > 0 and (
|
|
190
|
+
unique_indices[0] < 0 or unique_indices[-1] >= self.num_vertices
|
|
191
|
+
):
|
|
192
|
+
logging.warning("Some lines reference invalid vertices.")
|
|
193
|
+
if unique_indices.size != self.num_vertices:
|
|
194
|
+
logging.warning("Some vertices are not referenced by lines.")
|
|
195
|
+
self._lines = value
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def line_colors(self) -> np.ndarray:
|
|
199
|
+
"""Get line colors
|
|
200
|
+
|
|
201
|
+
RGB values in range [0..1].
|
|
202
|
+
|
|
203
|
+
:returns: Line colors, shape (n, 3)
|
|
204
|
+
"""
|
|
205
|
+
return self._line_colors
|
|
206
|
+
|
|
207
|
+
@line_colors.setter
|
|
208
|
+
def line_colors(self, value: ArrayLike | None) -> None:
|
|
209
|
+
"""Set line colors
|
|
210
|
+
|
|
211
|
+
If there are lines defined in the line set, then the number
|
|
212
|
+
of line colors has to match the number of lines.
|
|
213
|
+
|
|
214
|
+
Line colors are RGB values in range [0..1]. If values outside that
|
|
215
|
+
range are provided, values will be clipped and a warnig is given.
|
|
216
|
+
|
|
217
|
+
:param value: Line colors, shape (n, 3)
|
|
218
|
+
"""
|
|
219
|
+
if value is None:
|
|
220
|
+
line_colors = np.zeros((0, 3))
|
|
221
|
+
else:
|
|
222
|
+
line_colors = np.asarray(value, dtype=float)
|
|
223
|
+
if line_colors.ndim != 2 or line_colors.shape[1] != 3:
|
|
224
|
+
raise ValueError("Provide line colors of shape (n, 3)")
|
|
225
|
+
if line_colors.shape[0] > 0 and line_colors.shape[0] != self.num_lines:
|
|
226
|
+
raise ValueError("Provide as many line colors as lines")
|
|
227
|
+
if not np.all(np.logical_and(line_colors >= 0.0, line_colors <= 1.0)):
|
|
228
|
+
logging.warning("Some colors are out of bounds. Clipping to [0,1] ...")
|
|
229
|
+
line_colors = np.clip(line_colors, 0.0, 1.0)
|
|
230
|
+
self._line_colors = line_colors
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def has_line_colors(self) -> bool:
|
|
234
|
+
"""Check if line set has line colors
|
|
235
|
+
|
|
236
|
+
:returns: True, if line set has line colors
|
|
237
|
+
"""
|
|
238
|
+
return self._line_colors.shape[0] > 0
|
|
239
|
+
|
|
240
|
+
def colorize_lines_by_scalar(
|
|
241
|
+
self,
|
|
242
|
+
scalars: ArrayLike,
|
|
243
|
+
cmap: str = "viridis",
|
|
244
|
+
min_max: tuple[float, float] | None = None,
|
|
245
|
+
nan_color: tuple[float, float, float] = (1, 0, 0),
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Colorizes line set lines by scalar
|
|
248
|
+
|
|
249
|
+
:param scalars: Scalar values, length has to match number of lines
|
|
250
|
+
:param cmap: Matplotlib color map string
|
|
251
|
+
:param min_max: Minimum and maximum
|
|
252
|
+
:param nan_color: Color for NaN values, RGB values [0..1]
|
|
253
|
+
"""
|
|
254
|
+
scalars = np.asarray(scalars)
|
|
255
|
+
if scalars.ndim != 1 or scalars.size != self.num_lines:
|
|
256
|
+
raise Exception("Provide correct number of values to colorize lines")
|
|
257
|
+
self._line_colors = scalar_to_color(scalars, cmap, min_max, nan_color)
|
|
258
|
+
|
|
259
|
+
def colorize_lines_by_scalar_bicolor(
|
|
260
|
+
self,
|
|
261
|
+
scalars: ArrayLike,
|
|
262
|
+
cmaps: tuple[str, str] = ("Blues", "Reds"),
|
|
263
|
+
vmax: float | None = None,
|
|
264
|
+
nan_color: tuple[float, float, float] = (0, 1, 0),
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Colorizes line set lines by scalar, pos/neg in separate colors
|
|
267
|
+
|
|
268
|
+
:param scalars: Scalar values, length has to match number of lines
|
|
269
|
+
:param cmaps: Matplotlib color map strings for pos/neg
|
|
270
|
+
:param vmax: Maximum (pos/neg) value
|
|
271
|
+
:param nan_color: Color for NaN values, RGB values [0..1]
|
|
272
|
+
"""
|
|
273
|
+
scalars = np.asarray(scalars)
|
|
274
|
+
if scalars.ndim != 1 or scalars.size != self.num_lines:
|
|
275
|
+
raise Exception("Provide correct number of values to colorize lines")
|
|
276
|
+
self._line_colors = scalar_to_bicolor(scalars, cmaps, vmax, nan_color)
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def create_from_point_sequence(cls, points: np.ndarray) -> "LineSet":
|
|
280
|
+
return cls(
|
|
281
|
+
vertices=points,
|
|
282
|
+
lines=np.vstack(
|
|
283
|
+
(
|
|
284
|
+
np.arange(0, points.shape[0] - 1),
|
|
285
|
+
np.arange(1, points.shape[0]),
|
|
286
|
+
)
|
|
287
|
+
).T,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def create_from_normals(
|
|
292
|
+
cls,
|
|
293
|
+
vertices: Vertices,
|
|
294
|
+
scale: float = 1.0,
|
|
295
|
+
keep_ratio: float = 1.0,
|
|
296
|
+
rng: np.random.Generator | None = None,
|
|
297
|
+
) -> "LineSet":
|
|
298
|
+
"""Create line set from the normals of a point cloud or triangle mesh
|
|
299
|
+
|
|
300
|
+
:param vertices: Vertices object, i.e. point cloud or triangle mesh
|
|
301
|
+
:param scale: Length of the normal vectors
|
|
302
|
+
:param keep_ratio: Ratio of normal vectors to create,
|
|
303
|
+
0.1 randomly samples 10% in normal vectors in vertices
|
|
304
|
+
:param rng: NumPy random number generator
|
|
305
|
+
:returns: Resulting line set
|
|
306
|
+
"""
|
|
307
|
+
if not vertices.has_normals:
|
|
308
|
+
raise Exception("No normals to create lineset from")
|
|
309
|
+
if np.isclose(keep_ratio, 1.0):
|
|
310
|
+
num_normals = vertices.vertices.shape[0]
|
|
311
|
+
verts = np.vstack(
|
|
312
|
+
(
|
|
313
|
+
vertices.vertices,
|
|
314
|
+
vertices.vertices + scale * vertices.normals,
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
if rng is None:
|
|
319
|
+
rng = np.random.default_rng()
|
|
320
|
+
keep_ratio = np.clip(keep_ratio, 0.0, 1.0)
|
|
321
|
+
num_normals = np.round(keep_ratio * vertices.vertices.shape[0]).astype(int)
|
|
322
|
+
normal_indices = rng.choice(
|
|
323
|
+
vertices.vertices.shape[0], num_normals, replace=False
|
|
324
|
+
)
|
|
325
|
+
verts = np.vstack(
|
|
326
|
+
(
|
|
327
|
+
vertices.vertices[normal_indices],
|
|
328
|
+
vertices.vertices[normal_indices]
|
|
329
|
+
+ scale * vertices.normals[normal_indices],
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
return cls(
|
|
333
|
+
vertices=verts,
|
|
334
|
+
lines=np.vstack(
|
|
335
|
+
(
|
|
336
|
+
np.arange(num_normals),
|
|
337
|
+
np.arange(num_normals) + num_normals,
|
|
338
|
+
)
|
|
339
|
+
).T,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def create_from_mesh(cls, mesh: TriangleMesh) -> "LineSet":
|
|
344
|
+
"""Creates line set from triangle edges of a mesh
|
|
345
|
+
|
|
346
|
+
Creates a line set that represents the wire frame of the triangle
|
|
347
|
+
mesh: we extract each unique edge of the triangle mesh and add
|
|
348
|
+
that as a line to our line set.
|
|
349
|
+
|
|
350
|
+
:param mesh: Triangle mesh
|
|
351
|
+
:returns: Resulting line set
|
|
352
|
+
"""
|
|
353
|
+
vertices = mesh.vertices
|
|
354
|
+
lines = mesh.get_unique_edges()
|
|
355
|
+
if mesh.has_colors:
|
|
356
|
+
vertex_colors = mesh.colors
|
|
357
|
+
else:
|
|
358
|
+
# Edges separate faces; does not make sense to use
|
|
359
|
+
# face colors to colorize edges!?
|
|
360
|
+
vertex_colors = None
|
|
361
|
+
return cls(
|
|
362
|
+
vertices=vertices,
|
|
363
|
+
lines=lines,
|
|
364
|
+
vertex_colors=vertex_colors,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
@classmethod
|
|
368
|
+
def create_box(cls, scale: float | ArrayLike = 1.0) -> "LineSet":
|
|
369
|
+
"""Create a box shaped line set
|
|
370
|
+
|
|
371
|
+
Box has 8 vertices and 12 lines.
|
|
372
|
+
Box starts at (0, 0, 0) and extends to (scale, scale, scale).
|
|
373
|
+
|
|
374
|
+
:param scale: Scale of rectangle for X, Y and Z as single float or 3d
|
|
375
|
+
:returns: Resulting line set
|
|
376
|
+
"""
|
|
377
|
+
if isinstance(scale, float):
|
|
378
|
+
scale = np.array((scale, scale, scale))
|
|
379
|
+
else:
|
|
380
|
+
scale = np.asarray(scale)
|
|
381
|
+
if scale.size != 3:
|
|
382
|
+
raise Exception("Provide proper scale.")
|
|
383
|
+
vertices = scale * np.array(
|
|
384
|
+
[
|
|
385
|
+
[0.0, 0.0, 0.0], # 0: bottom-back-left
|
|
386
|
+
[1.0, 0.0, 0.0], # 1: bottom-back-right
|
|
387
|
+
[1.0, 1.0, 0.0], # 2: bottom-front-right
|
|
388
|
+
[0.0, 1.0, 0.0], # 3: bottom-front-left
|
|
389
|
+
[0.0, 0.0, 1.0], # 4: top-back-left
|
|
390
|
+
[1.0, 0.0, 1.0], # 5: top-back-right
|
|
391
|
+
[1.0, 1.0, 1.0], # 6: top-front-right
|
|
392
|
+
[0.0, 1.0, 1.0], # 7: top-front-left
|
|
393
|
+
],
|
|
394
|
+
dtype=float,
|
|
395
|
+
)
|
|
396
|
+
lines = np.array(
|
|
397
|
+
[
|
|
398
|
+
# Bottom lines
|
|
399
|
+
[0, 1],
|
|
400
|
+
[1, 2],
|
|
401
|
+
[2, 3],
|
|
402
|
+
[3, 0],
|
|
403
|
+
# Top lines
|
|
404
|
+
[4, 5],
|
|
405
|
+
[5, 6],
|
|
406
|
+
[6, 7],
|
|
407
|
+
[7, 4],
|
|
408
|
+
# Side lines
|
|
409
|
+
[0, 4],
|
|
410
|
+
[1, 5],
|
|
411
|
+
[2, 6],
|
|
412
|
+
[3, 7],
|
|
413
|
+
]
|
|
414
|
+
)
|
|
415
|
+
return cls(vertices=vertices, lines=lines)
|
|
416
|
+
|
|
417
|
+
@classmethod
|
|
418
|
+
def create_bounding_box(cls, bounds: np.ndarray):
|
|
419
|
+
"""Create a line set representing a line set
|
|
420
|
+
|
|
421
|
+
Box has 8 vertices and 12 lines.
|
|
422
|
+
Box starts at bounds[0] and extends to bounds[1].
|
|
423
|
+
|
|
424
|
+
Can directly used with Vertices::bounds() to generate a
|
|
425
|
+
bounding box for e.g. a point cloud or triangle mesh.
|
|
426
|
+
|
|
427
|
+
:param bounds: Min and max values for all coordinates, shape (2, 3)
|
|
428
|
+
:returns: Resulting line set
|
|
429
|
+
"""
|
|
430
|
+
if bounds.ndim != 2 and bounds.shape != (2, 3):
|
|
431
|
+
raise Exception(f"Invalid bounds {bounds}, expected shape (2, 3)")
|
|
432
|
+
bounds = np.array(bounds)
|
|
433
|
+
box = cls.create_box(scale=bounds[1] - bounds[0])
|
|
434
|
+
box.vertices += bounds[0]
|
|
435
|
+
return box
|