Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from __future__ import (absolute_import, division, print_function, 

2 unicode_literals) 

3import enum 

4import math 

5import numpy 

6import logging 

7try: # pragma: no cover 

8 from collections import abc 

9except ImportError: # pragma: no cover 

10 import collections as abc 

11 

12from python_utils import logger 

13 

14from .utils import s 

15 

16#: When removing empty areas, remove areas that are smaller than this 

17AREA_SIZE_THRESHOLD = 0 

18#: Vectors in a point 

19VECTORS = 3 

20#: Dimensions used in a vector 

21DIMENSIONS = 3 

22 

23 

24class Dimension(enum.IntEnum): 

25 #: X index (for example, `mesh.v0[0][X]`) 

26 X = 0 

27 #: Y index (for example, `mesh.v0[0][Y]`) 

28 Y = 1 

29 #: Z index (for example, `mesh.v0[0][Z]`) 

30 Z = 2 

31 

32 

33# For backwards compatibility, leave the original references 

34X = Dimension.X 

35Y = Dimension.Y 

36Z = Dimension.Z 

37 

38 

39class RemoveDuplicates(enum.Enum): 

40 ''' 

41 Choose whether to remove no duplicates, leave only a single of the 

42 duplicates or remove all duplicates (leaving holes). 

43 ''' 

44 NONE = 0 

45 SINGLE = 1 

46 ALL = 2 

47 

48 @classmethod 

49 def map(cls, value): 

50 if value is True: 

51 value = cls.SINGLE 

52 elif value and value in cls: 

53 pass 

54 else: 

55 value = cls.NONE 

56 

57 return value 

58 

59 

60def logged(class_): 

61 # For some reason the Logged baseclass is not properly initiated on Linux 

62 # systems while this works on OS X. Please let me know if you can tell me 

63 # what silly mistake I made here 

64 

65 logger_name = logger.Logged._Logged__get_name( 

66 __name__, 

67 class_.__name__, 

68 ) 

69 

70 class_.logger = logging.getLogger(logger_name) 

71 

72 for key in dir(logger.Logged): 

73 if not key.startswith('__'): 

74 setattr(class_, key, getattr(class_, key)) 

75 

76 return class_ 

77 

78 

79@logged 

80class BaseMesh(logger.Logged, abc.Mapping): 

81 ''' 

82 Mesh object with easy access to the vectors through v0, v1 and v2. 

83 The normals, areas, min, max and units are calculated automatically. 

84 

85 :param numpy.array data: The data for this mesh 

86 :param bool calculate_normals: Whether to calculate the normals 

87 :param bool remove_empty_areas: Whether to remove triangles with 0 area 

88 (due to rounding errors for example) 

89 

90 :ivar str name: Name of the solid, only exists in ASCII files 

91 :ivar numpy.array data: Data as :func:`BaseMesh.dtype` 

92 :ivar numpy.array points: All points (Nx9) 

93 :ivar numpy.array normals: Normals for this mesh, calculated automatically 

94 by default (Nx3) 

95 :ivar numpy.array vectors: Vectors in the mesh (Nx3x3) 

96 :ivar numpy.array attr: Attributes per vector (used by binary STL) 

97 :ivar numpy.array x: Points on the X axis by vertex (Nx3) 

98 :ivar numpy.array y: Points on the Y axis by vertex (Nx3) 

99 :ivar numpy.array z: Points on the Z axis by vertex (Nx3) 

100 :ivar numpy.array v0: Points in vector 0 (Nx3) 

101 :ivar numpy.array v1: Points in vector 1 (Nx3) 

102 :ivar numpy.array v2: Points in vector 2 (Nx3) 

103 

104 >>> data = numpy.zeros(10, dtype=BaseMesh.dtype) 

105 >>> mesh = BaseMesh(data, remove_empty_areas=False) 

106 >>> # Increment vector 0 item 0 

107 >>> mesh.v0[0] += 1 

108 >>> mesh.v1[0] += 2 

109 

110 >>> # Check item 0 (contains v0, v1 and v2) 

111 >>> assert numpy.array_equal( 

112 ... mesh[0], 

113 ... numpy.array([1., 1., 1., 2., 2., 2., 0., 0., 0.])) 

114 >>> assert numpy.array_equal( 

115 ... mesh.vectors[0], 

116 ... numpy.array([[1., 1., 1.], 

117 ... [2., 2., 2.], 

118 ... [0., 0., 0.]])) 

119 >>> assert numpy.array_equal( 

120 ... mesh.v0[0], 

121 ... numpy.array([1., 1., 1.])) 

122 >>> assert numpy.array_equal( 

123 ... mesh.points[0], 

124 ... numpy.array([1., 1., 1., 2., 2., 2., 0., 0., 0.])) 

125 >>> assert numpy.array_equal( 

126 ... mesh.data[0], 

127 ... numpy.array(( 

128 ... [0., 0., 0.], 

129 ... [[1., 1., 1.], [2., 2., 2.], [0., 0., 0.]], 

130 ... [0]), 

131 ... dtype=BaseMesh.dtype)) 

132 >>> assert numpy.array_equal(mesh.x[0], numpy.array([1., 2., 0.])) 

133 

134 >>> mesh[0] = 3 

135 >>> assert numpy.array_equal( 

136 ... mesh[0], 

137 ... numpy.array([3., 3., 3., 3., 3., 3., 3., 3., 3.])) 

138 

139 >>> len(mesh) == len(list(mesh)) 

140 True 

141 >>> (mesh.min_ < mesh.max_).all() 

142 True 

143 >>> mesh.update_normals() 

144 >>> mesh.units.sum() 

145 0.0 

146 >>> mesh.v0[:] = mesh.v1[:] = mesh.v2[:] = 0 

147 >>> mesh.points.sum() 

148 0.0 

149 

150 >>> mesh.v0 = mesh.v1 = mesh.v2 = 0 

151 >>> mesh.x = mesh.y = mesh.z = 0 

152 

153 >>> mesh.attr = 1 

154 >>> (mesh.attr == 1).all() 

155 True 

156 

157 >>> mesh.normals = 2 

158 >>> (mesh.normals == 2).all() 

159 True 

160 

161 >>> mesh.vectors = 3 

162 >>> (mesh.vectors == 3).all() 

163 True 

164 

165 >>> mesh.points = 4 

166 >>> (mesh.points == 4).all() 

167 True 

168 ''' 

169 #: - normals: :func:`numpy.float32`, `(3, )` 

170 #: - vectors: :func:`numpy.float32`, `(3, 3)` 

171 #: - attr: :func:`numpy.uint16`, `(1, )` 

172 dtype = numpy.dtype([ 

173 (s('normals'), numpy.float32, (3, )), 

174 (s('vectors'), numpy.float32, (3, 3)), 

175 (s('attr'), numpy.uint16, (1, )), 

176 ]) 

177 dtype = dtype.newbyteorder('<') # Even on big endian arches, use little e. 

178 

179 def __init__(self, data, calculate_normals=True, 

180 remove_empty_areas=False, 

181 remove_duplicate_polygons=RemoveDuplicates.NONE, 

182 name='', speedups=True, **kwargs): 

183 super(BaseMesh, self).__init__(**kwargs) 

184 self.speedups = speedups 

185 if remove_empty_areas: 

186 data = self.remove_empty_areas(data) 

187 

188 if RemoveDuplicates.map(remove_duplicate_polygons).value: 

189 data = self.remove_duplicate_polygons(data, 

190 remove_duplicate_polygons) 

191 

192 self.name = name 

193 self.data = data 

194 

195 if calculate_normals: 

196 self.update_normals() 

197 

198 @property 

199 def attr(self): 

200 return self.data['attr'] 

201 

202 @attr.setter 

203 def attr(self, value): 

204 self.data['attr'] = value 

205 

206 @property 

207 def normals(self): 

208 return self.data['normals'] 

209 

210 @normals.setter 

211 def normals(self, value): 

212 self.data['normals'] = value 

213 

214 @property 

215 def vectors(self): 

216 return self.data['vectors'] 

217 

218 @vectors.setter 

219 def vectors(self, value): 

220 self.data['vectors'] = value 

221 

222 @property 

223 def points(self): 

224 return self.vectors.reshape(self.data.size, 9) 

225 

226 @points.setter 

227 def points(self, value): 

228 self.points[:] = value 

229 

230 @property 

231 def v0(self): 

232 return self.vectors[:, 0] 

233 

234 @v0.setter 

235 def v0(self, value): 

236 self.vectors[:, 0] = value 

237 

238 @property 

239 def v1(self): 

240 return self.vectors[:, 1] 

241 

242 @v1.setter 

243 def v1(self, value): 

244 self.vectors[:, 1] = value 

245 

246 @property 

247 def v2(self): 

248 return self.vectors[:, 2] 

249 

250 @v2.setter 

251 def v2(self, value): 

252 self.vectors[:, 2] = value 

253 

254 @property 

255 def x(self): 

256 return self.points[:, Dimension.X::3] 

257 

258 @x.setter 

259 def x(self, value): 

260 self.points[:, Dimension.X::3] = value 

261 

262 @property 

263 def y(self): 

264 return self.points[:, Dimension.Y::3] 

265 

266 @y.setter 

267 def y(self, value): 

268 self.points[:, Dimension.Y::3] = value 

269 

270 @property 

271 def z(self): 

272 return self.points[:, Dimension.Z::3] 

273 

274 @z.setter 

275 def z(self, value): 

276 self.points[:, Dimension.Z::3] = value 

277 

278 @classmethod 

279 def remove_duplicate_polygons(cls, data, value=RemoveDuplicates.SINGLE): 

280 value = RemoveDuplicates.map(value) 

281 polygons = data['vectors'].sum(axis=1) 

282 # Get a sorted list of indices 

283 idx = numpy.lexsort(polygons.T) 

284 # Get the indices of all different indices 

285 diff = numpy.any(polygons[idx[1:]] != polygons[idx[:-1]], axis=1) 

286 

287 if value is RemoveDuplicates.SINGLE: 

288 # Only return the unique data, the True is so we always get at 

289 # least the originals 

290 return data[numpy.sort(idx[numpy.concatenate(([True], diff))])] 

291 elif value is RemoveDuplicates.ALL: 

292 # We need to return both items of the shifted diff 

293 diff_a = numpy.concatenate(([True], diff)) 

294 diff_b = numpy.concatenate((diff, [True])) 

295 diff = numpy.concatenate((diff, [False])) 

296 

297 # Combine both unique lists 

298 filtered_data = data[numpy.sort(idx[diff_a & diff_b])] 

299 if len(filtered_data) <= len(data) / 2: 

300 return data[numpy.sort(idx[diff_a])] 

301 else: 

302 return data[numpy.sort(idx[diff])] 

303 else: 

304 return data 

305 

306 @classmethod 

307 def remove_empty_areas(cls, data): 

308 vectors = data['vectors'] 

309 v0 = vectors[:, 0] 

310 v1 = vectors[:, 1] 

311 v2 = vectors[:, 2] 

312 normals = numpy.cross(v1 - v0, v2 - v0) 

313 squared_areas = (normals ** 2).sum(axis=1) 

314 return data[squared_areas > AREA_SIZE_THRESHOLD ** 2] 

315 

316 def update_normals(self, update_areas=True): 

317 '''Update the normals and areas for all points''' 

318 normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0) 

319 

320 if update_areas: 

321 self.update_areas(normals) 

322 

323 self.normals[:] = normals 

324 

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 

332 

333 def update_min(self): 

334 self._min = self.vectors.min(axis=(0, 1)) 

335 

336 def update_max(self): 

337 self._max = self.vectors.max(axis=(0, 1)) 

338 

339 def update_areas(self, normals=None): 

340 if normals is None: 

341 normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0) 

342 

343 areas = .5 * numpy.sqrt((normals ** 2).sum(axis=1)) 

344 self.areas = areas.reshape((areas.size, 1)) 

345 

346 def check(self): 

347 '''Check the mesh is valid or not''' 

348 return self.is_closed() 

349 

350 def is_closed(self): # pragma: no cover 

351 """Check the mesh is closed or not""" 

352 if numpy.isclose(self.normals.sum(axis=0), 0, atol=1e-4).all(): 

353 return True 

354 else: 

355 self.warning(''' 

356 Your mesh is not closed, the mass methods will not function 

357 correctly on this mesh. For more info: 

358 https://github.com/WoLpH/numpy-stl/issues/69 

359 '''.strip()) 

360 return False 

361 

362 def get_mass_properties(self): 

363 ''' 

364 Evaluate and return a tuple with the following elements: 

365 - the volume 

366 - the position of the center of gravity (COG) 

367 - the inertia matrix expressed at the COG 

368 

369 Documentation can be found here: 

370 http://www.geometrictools.com/Documentation/PolyhedralMassProperties.pdf 

371 ''' 

372 self.check() 

373 

374 def subexpression(x): 

375 w0, w1, w2 = x[:, 0], x[:, 1], x[:, 2] 

376 temp0 = w0 + w1 

377 f1 = temp0 + w2 

378 temp1 = w0 * w0 

379 temp2 = temp1 + w1 * temp0 

380 f2 = temp2 + w2 * f1 

381 f3 = w0 * temp1 + w1 * temp2 + w2 * f2 

382 g0 = f2 + w0 * (f1 + w0) 

383 g1 = f2 + w1 * (f1 + w1) 

384 g2 = f2 + w2 * (f1 + w2) 

385 return f1, f2, f3, g0, g1, g2 

386 

387 x0, x1, x2 = self.x[:, 0], self.x[:, 1], self.x[:, 2] 

388 y0, y1, y2 = self.y[:, 0], self.y[:, 1], self.y[:, 2] 

389 z0, z1, z2 = self.z[:, 0], self.z[:, 1], self.z[:, 2] 

390 a1, b1, c1 = x1 - x0, y1 - y0, z1 - z0 

391 a2, b2, c2 = x2 - x0, y2 - y0, z2 - z0 

392 d0, d1, d2 = b1 * c2 - b2 * c1, a2 * c1 - a1 * c2, a1 * b2 - a2 * b1 

393 

394 f1x, f2x, f3x, g0x, g1x, g2x = subexpression(self.x) 

395 f1y, f2y, f3y, g0y, g1y, g2y = subexpression(self.y) 

396 f1z, f2z, f3z, g0z, g1z, g2z = subexpression(self.z) 

397 

398 intg = numpy.zeros((10)) 

399 intg[0] = sum(d0 * f1x) 

400 intg[1:4] = sum(d0 * f2x), sum(d1 * f2y), sum(d2 * f2z) 

401 intg[4:7] = sum(d0 * f3x), sum(d1 * f3y), sum(d2 * f3z) 

402 intg[7] = sum(d0 * (y0 * g0x + y1 * g1x + y2 * g2x)) 

403 intg[8] = sum(d1 * (z0 * g0y + z1 * g1y + z2 * g2y)) 

404 intg[9] = sum(d2 * (x0 * g0z + x1 * g1z + x2 * g2z)) 

405 intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120]) 

406 volume = intg[0] 

407 cog = intg[1:4] / volume 

408 cogsq = cog ** 2 

409 inertia = numpy.zeros((3, 3)) 

410 inertia[0, 0] = intg[5] + intg[6] - volume * (cogsq[1] + cogsq[2]) 

411 inertia[1, 1] = intg[4] + intg[6] - volume * (cogsq[2] + cogsq[0]) 

412 inertia[2, 2] = intg[4] + intg[5] - volume * (cogsq[0] + cogsq[1]) 

413 inertia[0, 1] = inertia[1, 0] = -(intg[7] - volume * cog[0] * cog[1]) 

414 inertia[1, 2] = inertia[2, 1] = -(intg[8] - volume * cog[1] * cog[2]) 

415 inertia[0, 2] = inertia[2, 0] = -(intg[9] - volume * cog[2] * cog[0]) 

416 return volume, cog, inertia 

417 

418 def update_units(self): 

419 units = self.normals.copy() 

420 non_zero_areas = self.areas > 0 

421 areas = self.areas 

422 

423 if non_zero_areas.shape[0] != areas.shape[0]: # pragma: no cover 

424 self.warning('Zero sized areas found, ' 

425 'units calculation will be partially incorrect') 

426 

427 if non_zero_areas.any(): 

428 non_zero_areas.shape = non_zero_areas.shape[0] 

429 areas = numpy.hstack((2 * areas[non_zero_areas],) * DIMENSIONS) 

430 units[non_zero_areas] /= areas 

431 

432 self.units = units 

433 

434 @classmethod 

435 def rotation_matrix(cls, axis, theta): 

436 ''' 

437 Generate a rotation matrix to Rotate the matrix over the given axis by 

438 the given theta (angle) 

439 

440 Uses the `Euler-Rodrigues 

441 <https://en.wikipedia.org/wiki/Euler%E2%80%93Rodrigues_formula>`_ 

442 formula for fast rotations. 

443 

444 :param numpy.array axis: Axis to rotate over (x, y, z) 

445 :param float theta: Rotation angle in radians, use `math.radians` to 

446 convert degrees to radians if needed. 

447 ''' 

448 axis = numpy.asarray(axis) 

449 # No need to rotate if there is no actual rotation 

450 if not axis.any(): 

451 return numpy.identity(3) 

452 

453 theta = 0.5 * numpy.asarray(theta) 

454 

455 axis = axis / numpy.linalg.norm(axis) 

456 

457 a = math.cos(theta) 

458 b, c, d = - axis * math.sin(theta) 

459 angles = a, b, c, d 

460 powers = [x * y for x in angles for y in angles] 

461 aa, ab, ac, ad = powers[0:4] 

462 ba, bb, bc, bd = powers[4:8] 

463 ca, cb, cc, cd = powers[8:12] 

464 da, db, dc, dd = powers[12:16] 

465 

466 return numpy.array([[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)], 

467 [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)], 

468 [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]]) 

469 

470 def rotate(self, axis, theta=0, point=None): 

471 ''' 

472 Rotate the matrix over the given axis by the given theta (angle) 

473 

474 Uses the :py:func:`rotation_matrix` in the background. 

475 

476 .. note:: Note that the `point` was accidentaly inverted with the 

477 old version of the code. To get the old and incorrect behaviour 

478 simply pass `-point` instead of `point` or `-numpy.array(point)` if 

479 you're passing along an array. 

480 

481 :param numpy.array axis: Axis to rotate over (x, y, z) 

482 :param float theta: Rotation angle in radians, use `math.radians` to 

483 convert degrees to radians if needed. 

484 :param numpy.array point: Rotation point so manual translation is not 

485 required 

486 ''' 

487 # No need to rotate if there is no actual rotation 

488 if not theta: 

489 return 

490 

491 self.rotate_using_matrix(self.rotation_matrix(axis, theta), point) 

492 

493 def rotate_using_matrix(self, rotation_matrix, point=None): 

494 identity = numpy.identity(rotation_matrix.shape[0]) 

495 # No need to rotate if there is no actual rotation 

496 if not rotation_matrix.any() or (identity == rotation_matrix).all(): 

497 return 

498 

499 if isinstance(point, (numpy.ndarray, list, tuple)) and len(point) == 3: 

500 point = numpy.asarray(point) 

501 elif point is None: 

502 point = numpy.array([0, 0, 0]) 

503 elif isinstance(point, (int, float)): 

504 point = numpy.asarray([point] * 3) 

505 else: 

506 raise TypeError('Incorrect type for point', point) 

507 

508 def _rotate(matrix): 

509 if point.any(): 

510 # Translate while rotating 

511 return (matrix - point).dot(rotation_matrix) + point 

512 else: 

513 # Simply apply the rotation 

514 return matrix.dot(rotation_matrix) 

515 

516 # Rotate the normals 

517 self.normals[:] = _rotate(self.normals[:]) 

518 

519 # Rotate the vectors 

520 for i in range(3): 

521 self.vectors[:, i] = _rotate(self.vectors[:, i]) 

522 

523 def translate(self, translation): 

524 ''' 

525 Translate the mesh in the three directions 

526 

527 :param numpy.array translation: Translation vector (x, y, z) 

528 ''' 

529 assert len(translation) == 3, "Translation vector must be of length 3" 

530 self.x += translation[0] 

531 self.y += translation[1] 

532 self.z += translation[2] 

533 

534 def transform(self, matrix): 

535 ''' 

536 Transform the mesh with a rotation and a translation stored in a 

537 single 4x4 matrix 

538 

539 :param numpy.array matrix: Transform matrix with shape (4, 4), where 

540 matrix[0:3, 0:3] represents the rotation 

541 part of the transformation 

542 matrix[0:3, 3] represents the translation 

543 part of the transformation 

544 ''' 

545 is_a_4x4_matrix = matrix.shape == (4, 4) 

546 assert is_a_4x4_matrix, "Transformation matrix must be of shape (4, 4)" 

547 rotation = matrix[0:3, 0:3] 

548 unit_det_rotation = numpy.allclose(numpy.linalg.det(rotation), 1.0) 

549 assert unit_det_rotation, "Rotation matrix has not a unit determinant" 

550 for i in range(3): 

551 self.vectors[:, i] = numpy.dot(rotation, self.vectors[:, i].T).T 

552 self.x += matrix[0, 3] 

553 self.y += matrix[1, 3] 

554 self.z += matrix[2, 3] 

555 

556 def _get_or_update(key): 

557 def _get(self): 

558 if not hasattr(self, '_%s' % key): 

559 getattr(self, 'update_%s' % key)() 

560 return getattr(self, '_%s' % key) 

561 

562 return _get 

563 

564 def _set(key): 

565 def _set(self, value): 

566 setattr(self, '_%s' % key, value) 

567 

568 return _set 

569 

570 min_ = property(_get_or_update('min'), _set('min'), 

571 doc='Mesh minimum value') 

572 max_ = property(_get_or_update('max'), _set('max'), 

573 doc='Mesh maximum value') 

574 areas = property(_get_or_update('areas'), _set('areas'), 

575 doc='Mesh areas') 

576 units = property(_get_or_update('units'), _set('units'), 

577 doc='Mesh unit vectors') 

578 

579 def __getitem__(self, k): 

580 return self.points[k] 

581 

582 def __setitem__(self, k, v): 

583 self.points[k] = v 

584 

585 def __len__(self): 

586 return self.points.shape[0] 

587 

588 def __iter__(self): 

589 for point in self.points: 

590 yield point 

591 

592 def get_mass_properties_with_density(self, density): 

593 # add density for mesh,density unit kg/m3 when mesh is unit is m 

594 self.check() 

595 

596 def subexpression(x): 

597 w0, w1, w2 = x[:, 0], x[:, 1], x[:, 2] 

598 temp0 = w0 + w1 

599 f1 = temp0 + w2 

600 temp1 = w0 * w0 

601 temp2 = temp1 + w1 * temp0 

602 f2 = temp2 + w2 * f1 

603 f3 = w0 * temp1 + w1 * temp2 + w2 * f2 

604 g0 = f2 + w0 * (f1 + w0) 

605 g1 = f2 + w1 * (f1 + w1) 

606 g2 = f2 + w2 * (f1 + w2) 

607 return f1, f2, f3, g0, g1, g2 

608 

609 x0, x1, x2 = self.x[:, 0], self.x[:, 1], self.x[:, 2] 

610 y0, y1, y2 = self.y[:, 0], self.y[:, 1], self.y[:, 2] 

611 z0, z1, z2 = self.z[:, 0], self.z[:, 1], self.z[:, 2] 

612 a1, b1, c1 = x1 - x0, y1 - y0, z1 - z0 

613 a2, b2, c2 = x2 - x0, y2 - y0, z2 - z0 

614 d0, d1, d2 = b1 * c2 - b2 * c1, a2 * c1 - a1 * c2, a1 * b2 - a2 * b1 

615 

616 f1x, f2x, f3x, g0x, g1x, g2x = subexpression(self.x) 

617 f1y, f2y, f3y, g0y, g1y, g2y = subexpression(self.y) 

618 f1z, f2z, f3z, g0z, g1z, g2z = subexpression(self.z) 

619 

620 intg = numpy.zeros((10)) 

621 intg[0] = sum(d0 * f1x) 

622 intg[1:4] = sum(d0 * f2x), sum(d1 * f2y), sum(d2 * f2z) 

623 intg[4:7] = sum(d0 * f3x), sum(d1 * f3y), sum(d2 * f3z) 

624 intg[7] = sum(d0 * (y0 * g0x + y1 * g1x + y2 * g2x)) 

625 intg[8] = sum(d1 * (z0 * g0y + z1 * g1y + z2 * g2y)) 

626 intg[9] = sum(d2 * (x0 * g0z + x1 * g1z + x2 * g2z)) 

627 intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120]) 

628 volume = intg[0] 

629 cog = intg[1:4] / volume 

630 cogsq = cog ** 2 

631 vmass = volume * density 

632 inertia = numpy.zeros((3, 3)) 

633 

634 inertia[0, 0] = (intg[5] + intg[6]) * density - vmass * ( 

635 cogsq[1] + cogsq[2]) 

636 inertia[1, 1] = (intg[4] + intg[6]) * density - vmass * ( 

637 cogsq[2] + cogsq[0]) 

638 inertia[2, 2] = (intg[4] + intg[5]) * density - vmass * ( 

639 cogsq[0] + cogsq[1]) 

640 inertia[0, 1] = inertia[1, 0] = -( 

641 intg[7] * density - vmass * cog[0] * cog[1]) 

642 inertia[1, 2] = inertia[2, 1] = -( 

643 intg[8] * density - vmass * cog[1] * cog[2]) 

644 inertia[0, 2] = inertia[2, 0] = -( 

645 intg[9] * density - vmass * cog[2] * cog[0]) 

646 

647 return volume, vmass, cog, inertia 

648