Coverage for /Volumes/workspace/numpy-stl/stl/base.py: 98%
336 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-14 23:18 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-14 23:18 +0100
1import enum
2import math
3import numpy
4import logging
5try: # pragma: no cover
6 from collections import abc
7except ImportError: # pragma: no cover
8 import collections as abc
10from python_utils import logger
13#: When removing empty areas, remove areas that are smaller than this
14AREA_SIZE_THRESHOLD = 0
15#: Vectors in a point
16VECTORS = 3
17#: Dimensions used in a vector
18DIMENSIONS = 3
21class Dimension(enum.IntEnum):
22 #: X index (for example, `mesh.v0[0][X]`)
23 X = 0
24 #: Y index (for example, `mesh.v0[0][Y]`)
25 Y = 1
26 #: Z index (for example, `mesh.v0[0][Z]`)
27 Z = 2
30# For backwards compatibility, leave the original references
31X = Dimension.X
32Y = Dimension.Y
33Z = Dimension.Z
36class RemoveDuplicates(enum.Enum):
37 '''
38 Choose whether to remove no duplicates, leave only a single of the
39 duplicates or remove all duplicates (leaving holes).
40 '''
41 NONE = 0
42 SINGLE = 1
43 ALL = 2
45 @classmethod
46 def map(cls, value):
47 if value is True:
48 value = cls.SINGLE
49 elif value and value in cls:
50 pass
51 else:
52 value = cls.NONE
54 return value
57def logged(class_):
58 # For some reason the Logged baseclass is not properly initiated on Linux
59 # systems while this works on OS X. Please let me know if you can tell me
60 # what silly mistake I made here
62 logger_name = logger.Logged._Logged__get_name(
63 __name__,
64 class_.__name__,
65 )
67 class_.logger = logging.getLogger(logger_name)
69 for key in dir(logger.Logged):
70 if not key.startswith('__'):
71 setattr(class_, key, getattr(class_, key))
73 return class_
76@logged
77class BaseMesh(logger.Logged, abc.Mapping):
78 '''
79 Mesh object with easy access to the vectors through v0, v1 and v2.
80 The normals, areas, min, max and units are calculated automatically.
82 :param numpy.array data: The data for this mesh
83 :param bool calculate_normals: Whether to calculate the normals
84 :param bool remove_empty_areas: Whether to remove triangles with 0 area
85 (due to rounding errors for example)
87 :ivar str name: Name of the solid, only exists in ASCII files
88 :ivar numpy.array data: Data as :func:`BaseMesh.dtype`
89 :ivar numpy.array points: All points (Nx9)
90 :ivar numpy.array normals: Normals for this mesh, calculated automatically
91 by default (Nx3)
92 :ivar numpy.array vectors: Vectors in the mesh (Nx3x3)
93 :ivar numpy.array attr: Attributes per vector (used by binary STL)
94 :ivar numpy.array x: Points on the X axis by vertex (Nx3)
95 :ivar numpy.array y: Points on the Y axis by vertex (Nx3)
96 :ivar numpy.array z: Points on the Z axis by vertex (Nx3)
97 :ivar numpy.array v0: Points in vector 0 (Nx3)
98 :ivar numpy.array v1: Points in vector 1 (Nx3)
99 :ivar numpy.array v2: Points in vector 2 (Nx3)
101 >>> data = numpy.zeros(10, dtype=BaseMesh.dtype)
102 >>> mesh = BaseMesh(data, remove_empty_areas=False)
103 >>> # Increment vector 0 item 0
104 >>> mesh.v0[0] += 1
105 >>> mesh.v1[0] += 2
107 >>> # Check item 0 (contains v0, v1 and v2)
108 >>> assert numpy.array_equal(
109 ... mesh[0],
110 ... numpy.array([1., 1., 1., 2., 2., 2., 0., 0., 0.]))
111 >>> assert numpy.array_equal(
112 ... mesh.vectors[0],
113 ... numpy.array([[1., 1., 1.],
114 ... [2., 2., 2.],
115 ... [0., 0., 0.]]))
116 >>> assert numpy.array_equal(
117 ... mesh.v0[0],
118 ... numpy.array([1., 1., 1.]))
119 >>> assert numpy.array_equal(
120 ... mesh.points[0],
121 ... numpy.array([1., 1., 1., 2., 2., 2., 0., 0., 0.]))
122 >>> assert numpy.array_equal(
123 ... mesh.data[0],
124 ... numpy.array((
125 ... [0., 0., 0.],
126 ... [[1., 1., 1.], [2., 2., 2.], [0., 0., 0.]],
127 ... [0]),
128 ... dtype=BaseMesh.dtype))
129 >>> assert numpy.array_equal(mesh.x[0], numpy.array([1., 2., 0.]))
131 >>> mesh[0] = 3
132 >>> assert numpy.array_equal(
133 ... mesh[0],
134 ... numpy.array([3., 3., 3., 3., 3., 3., 3., 3., 3.]))
136 >>> len(mesh) == len(list(mesh))
137 True
138 >>> (mesh.min_ < mesh.max_).all()
139 True
140 >>> mesh.update_normals()
141 >>> mesh.units.sum()
142 0.0
143 >>> mesh.v0[:] = mesh.v1[:] = mesh.v2[:] = 0
144 >>> mesh.points.sum()
145 0.0
147 >>> mesh.v0 = mesh.v1 = mesh.v2 = 0
148 >>> mesh.x = mesh.y = mesh.z = 0
150 >>> mesh.attr = 1
151 >>> (mesh.attr == 1).all()
152 True
154 >>> mesh.normals = 2
155 >>> (mesh.normals == 2).all()
156 True
158 >>> mesh.vectors = 3
159 >>> (mesh.vectors == 3).all()
160 True
162 >>> mesh.points = 4
163 >>> (mesh.points == 4).all()
164 True
165 '''
166 #: - normals: :func:`numpy.float32`, `(3, )`
167 #: - vectors: :func:`numpy.float32`, `(3, 3)`
168 #: - attr: :func:`numpy.uint16`, `(1, )`
169 dtype = numpy.dtype([
170 ('normals', numpy.float32, (3, )),
171 ('vectors', numpy.float32, (3, 3)),
172 ('attr', numpy.uint16, (1, )),
173 ])
174 dtype = dtype.newbyteorder('<') # Even on big endian arches, use little e.
176 def __init__(self, data, calculate_normals=True,
177 remove_empty_areas=False,
178 remove_duplicate_polygons=RemoveDuplicates.NONE,
179 name='', speedups=True, **kwargs):
180 super(BaseMesh, self).__init__(**kwargs)
181 self.speedups = speedups
182 if remove_empty_areas:
183 data = self.remove_empty_areas(data)
185 if RemoveDuplicates.map(remove_duplicate_polygons).value:
186 data = self.remove_duplicate_polygons(data,
187 remove_duplicate_polygons)
189 self.name = name
190 self.data = data
192 if calculate_normals:
193 self.update_normals()
195 @property
196 def attr(self):
197 return self.data['attr']
199 @attr.setter
200 def attr(self, value):
201 self.data['attr'] = value
203 @property
204 def normals(self):
205 return self.data['normals']
207 @normals.setter
208 def normals(self, value):
209 self.data['normals'] = value
211 @property
212 def vectors(self):
213 return self.data['vectors']
215 @vectors.setter
216 def vectors(self, value):
217 self.data['vectors'] = value
219 @property
220 def points(self):
221 return self.vectors.reshape(self.data.size, 9)
223 @points.setter
224 def points(self, value):
225 self.points[:] = value
227 @property
228 def v0(self):
229 return self.vectors[:, 0]
231 @v0.setter
232 def v0(self, value):
233 self.vectors[:, 0] = value
235 @property
236 def v1(self):
237 return self.vectors[:, 1]
239 @v1.setter
240 def v1(self, value):
241 self.vectors[:, 1] = value
243 @property
244 def v2(self):
245 return self.vectors[:, 2]
247 @v2.setter
248 def v2(self, value):
249 self.vectors[:, 2] = value
251 @property
252 def x(self):
253 return self.points[:, Dimension.X::3]
255 @x.setter
256 def x(self, value):
257 self.points[:, Dimension.X::3] = value
259 @property
260 def y(self):
261 return self.points[:, Dimension.Y::3]
263 @y.setter
264 def y(self, value):
265 self.points[:, Dimension.Y::3] = value
267 @property
268 def z(self):
269 return self.points[:, Dimension.Z::3]
271 @z.setter
272 def z(self, value):
273 self.points[:, Dimension.Z::3] = value
275 @classmethod
276 def remove_duplicate_polygons(cls, data, value=RemoveDuplicates.SINGLE):
277 value = RemoveDuplicates.map(value)
278 polygons = data['vectors'].sum(axis=1)
279 # Get a sorted list of indices
280 idx = numpy.lexsort(polygons.T)
281 # Get the indices of all different indices
282 diff = numpy.any(polygons[idx[1:]] != polygons[idx[:-1]], axis=1)
284 if value is RemoveDuplicates.SINGLE:
285 # Only return the unique data, the True is so we always get at
286 # least the originals
287 return data[numpy.sort(idx[numpy.concatenate(([True], diff))])]
288 elif value is RemoveDuplicates.ALL:
289 # We need to return both items of the shifted diff
290 diff_a = numpy.concatenate(([True], diff))
291 diff_b = numpy.concatenate((diff, [True]))
292 diff = numpy.concatenate((diff, [False]))
294 # Combine both unique lists
295 filtered_data = data[numpy.sort(idx[diff_a & diff_b])]
296 if len(filtered_data) <= len(data) / 2:
297 return data[numpy.sort(idx[diff_a])]
298 else:
299 return data[numpy.sort(idx[diff])]
300 else:
301 return data
303 @classmethod
304 def remove_empty_areas(cls, data):
305 vectors = data['vectors']
306 v0 = vectors[:, 0]
307 v1 = vectors[:, 1]
308 v2 = vectors[:, 2]
309 normals = numpy.cross(v1 - v0, v2 - v0)
310 squared_areas = (normals ** 2).sum(axis=1)
311 return data[squared_areas > AREA_SIZE_THRESHOLD ** 2]
313 def update_normals(self, update_areas=True, update_centroids=True):
314 '''Update the normals, areas, and centroids for all points'''
315 normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0)
317 if update_areas:
318 self.update_areas(normals)
320 if update_centroids:
321 self.update_centroids()
323 self.normals[:] = normals
325 def get_unit_normals(self):
326 normals = self.normals.copy()
327 normal = numpy.linalg.norm(normals, axis=1)
328 non_zero = normal > 0
329 if non_zero.any():
330 normals[non_zero] /= normal[non_zero][:, None]
331 return normals
333 def update_min(self):
334 self._min = self.vectors.min(axis=(0, 1))
336 def update_max(self):
337 self._max = self.vectors.max(axis=(0, 1))
339 def update_areas(self, normals=None):
340 if normals is None:
341 normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0)
343 areas = .5 * numpy.sqrt((normals ** 2).sum(axis=1))
344 self.areas = areas.reshape((areas.size, 1))
346 def update_centroids(self):
347 self.centroids = numpy.mean([self.v0, self.v1, self.v2], axis=0)
349 def check(self):
350 '''Check the mesh is valid or not'''
351 return self.is_closed()
353 def is_closed(self): # pragma: no cover
354 """Check the mesh is closed or not"""
355 if numpy.isclose(self.normals.sum(axis=0), 0, atol=1e-4).all():
356 return True
357 else:
358 self.warning('''
359 Your mesh is not closed, the mass methods will not function
360 correctly on this mesh. For more info:
361 https://github.com/WoLpH/numpy-stl/issues/69
362 '''.strip())
363 return False
365 def get_mass_properties(self):
366 '''
367 Evaluate and return a tuple with the following elements:
368 - the volume
369 - the position of the center of gravity (COG)
370 - the inertia matrix expressed at the COG
372 Documentation can be found here:
373 http://www.geometrictools.com/Documentation/PolyhedralMassProperties.pdf
374 '''
375 self.check()
377 def subexpression(x):
378 w0, w1, w2 = x[:, 0], x[:, 1], x[:, 2]
379 temp0 = w0 + w1
380 f1 = temp0 + w2
381 temp1 = w0 * w0
382 temp2 = temp1 + w1 * temp0
383 f2 = temp2 + w2 * f1
384 f3 = w0 * temp1 + w1 * temp2 + w2 * f2
385 g0 = f2 + w0 * (f1 + w0)
386 g1 = f2 + w1 * (f1 + w1)
387 g2 = f2 + w2 * (f1 + w2)
388 return f1, f2, f3, g0, g1, g2
390 x0, x1, x2 = self.x[:, 0], self.x[:, 1], self.x[:, 2]
391 y0, y1, y2 = self.y[:, 0], self.y[:, 1], self.y[:, 2]
392 z0, z1, z2 = self.z[:, 0], self.z[:, 1], self.z[:, 2]
393 a1, b1, c1 = x1 - x0, y1 - y0, z1 - z0
394 a2, b2, c2 = x2 - x0, y2 - y0, z2 - z0
395 d0, d1, d2 = b1 * c2 - b2 * c1, a2 * c1 - a1 * c2, a1 * b2 - a2 * b1
397 f1x, f2x, f3x, g0x, g1x, g2x = subexpression(self.x)
398 f1y, f2y, f3y, g0y, g1y, g2y = subexpression(self.y)
399 f1z, f2z, f3z, g0z, g1z, g2z = subexpression(self.z)
401 intg = numpy.zeros((10))
402 intg[0] = sum(d0 * f1x)
403 intg[1:4] = sum(d0 * f2x), sum(d1 * f2y), sum(d2 * f2z)
404 intg[4:7] = sum(d0 * f3x), sum(d1 * f3y), sum(d2 * f3z)
405 intg[7] = sum(d0 * (y0 * g0x + y1 * g1x + y2 * g2x))
406 intg[8] = sum(d1 * (z0 * g0y + z1 * g1y + z2 * g2y))
407 intg[9] = sum(d2 * (x0 * g0z + x1 * g1z + x2 * g2z))
408 intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120])
409 volume = intg[0]
410 cog = intg[1:4] / volume
411 cogsq = cog ** 2
412 inertia = numpy.zeros((3, 3))
413 inertia[0, 0] = intg[5] + intg[6] - volume * (cogsq[1] + cogsq[2])
414 inertia[1, 1] = intg[4] + intg[6] - volume * (cogsq[2] + cogsq[0])
415 inertia[2, 2] = intg[4] + intg[5] - volume * (cogsq[0] + cogsq[1])
416 inertia[0, 1] = inertia[1, 0] = -(intg[7] - volume * cog[0] * cog[1])
417 inertia[1, 2] = inertia[2, 1] = -(intg[8] - volume * cog[1] * cog[2])
418 inertia[0, 2] = inertia[2, 0] = -(intg[9] - volume * cog[2] * cog[0])
419 return volume, cog, inertia
421 def update_units(self):
422 units = self.normals.copy()
423 non_zero_areas = self.areas > 0
424 areas = self.areas
426 if non_zero_areas.shape[0] != areas.shape[0]: # pragma: no cover
427 self.warning('Zero sized areas found, '
428 'units calculation will be partially incorrect')
430 if non_zero_areas.any():
431 non_zero_areas.shape = non_zero_areas.shape[0]
432 areas = numpy.hstack((2 * areas[non_zero_areas],) * DIMENSIONS)
433 units[non_zero_areas] /= areas
435 self.units = units
437 @classmethod
438 def rotation_matrix(cls, axis, theta):
439 '''
440 Generate a rotation matrix to Rotate the matrix over the given axis by
441 the given theta (angle)
443 Uses the `Euler-Rodrigues
444 <https://en.wikipedia.org/wiki/Euler%E2%80%93Rodrigues_formula>`_
445 formula for fast rotations.
447 :param numpy.array axis: Axis to rotate over (x, y, z)
448 :param float theta: Rotation angle in radians, use `math.radians` to
449 convert degrees to radians if needed.
450 '''
451 axis = numpy.asarray(axis)
452 # No need to rotate if there is no actual rotation
453 if not axis.any():
454 return numpy.identity(3)
456 theta = 0.5 * numpy.asarray(theta)
458 axis = axis / numpy.linalg.norm(axis)
460 a = math.cos(theta)
461 b, c, d = - axis * math.sin(theta)
462 angles = a, b, c, d
463 powers = [x * y for x in angles for y in angles]
464 aa, ab, ac, ad = powers[0:4]
465 ba, bb, bc, bd = powers[4:8]
466 ca, cb, cc, cd = powers[8:12]
467 da, db, dc, dd = powers[12:16]
469 return numpy.array([[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
470 [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
471 [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]])
473 def rotate(self, axis, theta=0, point=None):
474 '''
475 Rotate the matrix over the given axis by the given theta (angle)
477 Uses the :py:func:`rotation_matrix` in the background.
479 .. note:: Note that the `point` was accidentaly inverted with the
480 old version of the code. To get the old and incorrect behaviour
481 simply pass `-point` instead of `point` or `-numpy.array(point)` if
482 you're passing along an array.
484 :param numpy.array axis: Axis to rotate over (x, y, z)
485 :param float theta: Rotation angle in radians, use `math.radians` to
486 convert degrees to radians if needed.
487 :param numpy.array point: Rotation point so manual translation is not
488 required
489 '''
490 # No need to rotate if there is no actual rotation
491 if not theta:
492 return
494 self.rotate_using_matrix(self.rotation_matrix(axis, theta), point)
496 def rotate_using_matrix(self, rotation_matrix, point=None):
497 '''
498 Rotate using a given rotation matrix and optional rotation point
500 Note that this rotation produces clockwise rotations for positive
501 angles which is arguably incorrect but will remain for legacy reasons.
502 For more details, read here:
503 https://github.com/WoLpH/numpy-stl/issues/166
504 '''
506 identity = numpy.identity(rotation_matrix.shape[0])
507 # No need to rotate if there is no actual rotation
508 if not rotation_matrix.any() or (identity == rotation_matrix).all():
509 return
511 if isinstance(point, (numpy.ndarray, list, tuple)) and len(point) == 3:
512 point = numpy.asarray(point)
513 elif point is None:
514 point = numpy.array([0, 0, 0])
515 elif isinstance(point, (int, float)):
516 point = numpy.asarray([point] * 3)
517 else:
518 raise TypeError('Incorrect type for point', point)
520 def _rotate(matrix):
521 if point.any():
522 # Translate while rotating
523 return (matrix - point).dot(rotation_matrix) + point
524 else:
525 # Simply apply the rotation
526 return matrix.dot(rotation_matrix)
528 # Rotate the normals
529 self.normals[:] = _rotate(self.normals[:])
531 # Rotate the vectors
532 for i in range(3):
533 self.vectors[:, i] = _rotate(self.vectors[:, i])
535 def translate(self, translation):
536 '''
537 Translate the mesh in the three directions
539 :param numpy.array translation: Translation vector (x, y, z)
540 '''
541 assert len(translation) == 3, "Translation vector must be of length 3"
542 self.x += translation[0]
543 self.y += translation[1]
544 self.z += translation[2]
546 def transform(self, matrix):
547 '''
548 Transform the mesh with a rotation and a translation stored in a
549 single 4x4 matrix
551 :param numpy.array matrix: Transform matrix with shape (4, 4), where
552 matrix[0:3, 0:3] represents the rotation
553 part of the transformation
554 matrix[0:3, 3] represents the translation
555 part of the transformation
556 '''
557 is_a_4x4_matrix = matrix.shape == (4, 4)
558 assert is_a_4x4_matrix, "Transformation matrix must be of shape (4, 4)"
559 rotation = matrix[0:3, 0:3]
560 unit_det_rotation = numpy.allclose(numpy.linalg.det(rotation), 1.0)
561 assert unit_det_rotation, "Rotation matrix has not a unit determinant"
562 for i in range(3):
563 self.vectors[:, i] = numpy.dot(rotation, self.vectors[:, i].T).T
564 self.x += matrix[0, 3]
565 self.y += matrix[1, 3]
566 self.z += matrix[2, 3]
568 def _get_or_update(key):
569 def _get(self):
570 if not hasattr(self, '_%s' % key):
571 getattr(self, 'update_%s' % key)()
572 return getattr(self, '_%s' % key)
574 return _get
576 def _set(key):
577 def _set(self, value):
578 setattr(self, '_%s' % key, value)
580 return _set
582 min_ = property(_get_or_update('min'), _set('min'),
583 doc='Mesh minimum value')
584 max_ = property(_get_or_update('max'), _set('max'),
585 doc='Mesh maximum value')
586 areas = property(_get_or_update('areas'), _set('areas'),
587 doc='Mesh areas')
588 centroids = property(_get_or_update('centroids'), _set('centroids'),
589 doc='Mesh centroids')
590 units = property(_get_or_update('units'), _set('units'),
591 doc='Mesh unit vectors')
593 def __getitem__(self, k):
594 return self.points[k]
596 def __setitem__(self, k, v):
597 self.points[k] = v
599 def __len__(self):
600 return self.points.shape[0]
602 def __iter__(self):
603 for point in self.points:
604 yield point
606 def __repr__(self):
607 return f'<Mesh: {self.name!r} {self.data.size} vertices>'
609 def get_mass_properties_with_density(self, density):
610 # add density for mesh,density unit kg/m3 when mesh is unit is m
611 self.check()
613 def subexpression(x):
614 w0, w1, w2 = x[:, 0], x[:, 1], x[:, 2]
615 temp0 = w0 + w1
616 f1 = temp0 + w2
617 temp1 = w0 * w0
618 temp2 = temp1 + w1 * temp0
619 f2 = temp2 + w2 * f1
620 f3 = w0 * temp1 + w1 * temp2 + w2 * f2
621 g0 = f2 + w0 * (f1 + w0)
622 g1 = f2 + w1 * (f1 + w1)
623 g2 = f2 + w2 * (f1 + w2)
624 return f1, f2, f3, g0, g1, g2
626 x0, x1, x2 = self.x[:, 0], self.x[:, 1], self.x[:, 2]
627 y0, y1, y2 = self.y[:, 0], self.y[:, 1], self.y[:, 2]
628 z0, z1, z2 = self.z[:, 0], self.z[:, 1], self.z[:, 2]
629 a1, b1, c1 = x1 - x0, y1 - y0, z1 - z0
630 a2, b2, c2 = x2 - x0, y2 - y0, z2 - z0
631 d0, d1, d2 = b1 * c2 - b2 * c1, a2 * c1 - a1 * c2, a1 * b2 - a2 * b1
633 f1x, f2x, f3x, g0x, g1x, g2x = subexpression(self.x)
634 f1y, f2y, f3y, g0y, g1y, g2y = subexpression(self.y)
635 f1z, f2z, f3z, g0z, g1z, g2z = subexpression(self.z)
637 intg = numpy.zeros((10))
638 intg[0] = sum(d0 * f1x)
639 intg[1:4] = sum(d0 * f2x), sum(d1 * f2y), sum(d2 * f2z)
640 intg[4:7] = sum(d0 * f3x), sum(d1 * f3y), sum(d2 * f3z)
641 intg[7] = sum(d0 * (y0 * g0x + y1 * g1x + y2 * g2x))
642 intg[8] = sum(d1 * (z0 * g0y + z1 * g1y + z2 * g2y))
643 intg[9] = sum(d2 * (x0 * g0z + x1 * g1z + x2 * g2z))
644 intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120])
645 volume = intg[0]
646 cog = intg[1:4] / volume
647 cogsq = cog ** 2
648 vmass = volume * density
649 inertia = numpy.zeros((3, 3))
651 inertia[0, 0] = (intg[5] + intg[6]) * density - vmass * (
652 cogsq[1] + cogsq[2])
653 inertia[1, 1] = (intg[4] + intg[6]) * density - vmass * (
654 cogsq[2] + cogsq[0])
655 inertia[2, 2] = (intg[4] + intg[5]) * density - vmass * (
656 cogsq[0] + cogsq[1])
657 inertia[0, 1] = inertia[1, 0] = -(
658 intg[7] * density - vmass * cog[0] * cog[1])
659 inertia[1, 2] = inertia[2, 1] = -(
660 intg[8] * density - vmass * cog[1] * cog[2])
661 inertia[0, 2] = inertia[2, 0] = -(
662 intg[9] * density - vmass * cog[2] * cog[0])
664 return volume, vmass, cog, inertia