diff --git a/.pyup.yml b/.pyup.yml new file mode 100644 index 0000000..604f69f --- /dev/null +++ b/.pyup.yml @@ -0,0 +1,4 @@ +# autogenerated pyup.io config file +# see https://pyup.io/docs/configuration/ for all available options + +update: insecure diff --git a/.travis.yml b/.travis.yml index ae09065..22a95b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,8 @@ script: - tox -e $TOX_ENV after_success: - coveralls +- pip install codecov +- codecov before_script: flake8 --ignore=W391 stl tests notifications: email: diff --git a/README.rst b/README.rst index b807673..ff263d8 100644 --- a/README.rst +++ b/README.rst @@ -341,16 +341,17 @@ Combining multiple STL files def translate(_solid, step, padding, multiplier, axis): - if axis == 'x': - items = [0, 3, 6] - elif axis == 'y': - items = [1, 4, 7] - elif axis == 'z': - items = [2, 5, 8] - for p in _solid.points: - # point items are ((x, y, z), (x, y, z), (x, y, z)) - for i in range(3): - p[items[i]] += (step * multiplier) + (padding * multiplier) + if 'x' == axis: + items = 0, 3, 6 + elif 'y' == axis: + items = 1, 4, 7 + elif 'z' == axis: + items = 2, 5, 8 + else: + raise RuntimeError('Unknown axis %r, expected x, y or z' % axis) + + # _solid.points.shape == [:, ((x, y, z), (x, y, z), (x, y, z))] + _solid.points[:, items] += (step * multiplier) + (padding * multiplier) def copy_obj(obj, dims, num_rows, num_cols, num_layers): diff --git a/pytest.ini b/pytest.ini index a8eb3e4..4abc3f8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,8 @@ [pytest] basetemp = tmp +doctest_optionflags = NORMALIZE_WHITESPACE + python_files = stl/*.py tests/*.py @@ -21,3 +23,8 @@ pep8ignore = flakes-ignore = docs/*.py ALL + +looponfailroots = + stl + tests + build diff --git a/setup.py b/setup.py index 411c838..0ffe600 100644 --- a/setup.py +++ b/setup.py @@ -8,22 +8,40 @@ setup_kwargs = {} + +def error(*lines): + for line in lines: + print(line, file=sys.stderr) + + try: - import numpy - from Cython import Build - - setup_kwargs['ext_modules'] = Build.cythonize([ - extension.Extension( - 'stl._speedups', - ['stl/_speedups.pyx'], - include_dirs=[numpy.get_include()], - ), - ]) + from stl import stl + if not hasattr(stl, 'BaseStl'): + error('ERROR', + 'You have an incompatible stl package installed' + 'Please run "pip uninstall -y stl" first') + sys.exit(1) except ImportError: - print('WARNING', file=sys.stderr) - print('Cython and Numpy is required for building extension.', - file=sys.stderr) - print('Falling back to pure Python implementation.', file=sys.stderr) + pass + + +if sys.version_info.major == 2 or sys.platform.lower() != 'win32': + try: + import numpy + from Cython import Build + + setup_kwargs['ext_modules'] = Build.cythonize([ + extension.Extension( + 'stl._speedups', + ['stl/_speedups.pyx'], + include_dirs=[numpy.get_include()], + ), + ]) + except ImportError: + error('WARNING', + 'Cython and Numpy is required for building extension.', + 'Falling back to pure Python implementation.') + # To prevent importing about and thereby breaking the coverage info we use this # exec hack @@ -40,7 +58,6 @@ install_requires = [ 'numpy', - 'nine', 'python-utils>=1.6.2', ] diff --git a/stl/__about__.py b/stl/__about__.py index 728ddf9..68dc6ee 100644 --- a/stl/__about__.py +++ b/stl/__about__.py @@ -1,6 +1,6 @@ __package_name__ = 'numpy-stl' __import_name__ = 'stl' -__version__ = '2.3.2' +__version__ = '2.4.0' __author__ = 'Rick van Hattem' __author_email__ = 'Wolph@Wol.ph' __description__ = ' '.join(''' diff --git a/stl/base.py b/stl/base.py index 39e6123..bad215e 100644 --- a/stl/base.py +++ b/stl/base.py @@ -106,23 +106,23 @@ class BaseMesh(logger.Logged, collections.Mapping): >>> # Check item 0 (contains v0, v1 and v2) >>> mesh[0] - array([ 1., 1., 1., 2., 2., 2., 0., 0., 0.], dtype=float32) - >>> mesh.vectors[0] # doctest: +NORMALIZE_WHITESPACE - array([[ 1., 1., 1.], - [ 2., 2., 2.], - [ 0., 0., 0.]], dtype=float32) + array([1., 1., 1., 2., 2., 2., 0., 0., 0.], dtype=float32) + >>> mesh.vectors[0] + array([[1., 1., 1.], + [2., 2., 2.], + [0., 0., 0.]], dtype=float32) >>> mesh.v0[0] - array([ 1., 1., 1.], dtype=float32) + array([1., 1., 1.], dtype=float32) >>> mesh.points[0] - array([ 1., 1., 1., 2., 2., 2., 0., 0., 0.], dtype=float32) - >>> mesh.data[0] # doctest: +NORMALIZE_WHITESPACE - ([ 0., 0., 0.], [[ 1., 1., 1.], [ 2., 2., 2.], [ 0., 0., 0.]], [0]) + array([1., 1., 1., 2., 2., 2., 0., 0., 0.], dtype=float32) + >>> mesh.data[0] + ([0., 0., 0.], [[1., 1., 1.], [2., 2., 2.], [0., 0., 0.]], [0]) >>> mesh.x[0] - array([ 1., 2., 0.], dtype=float32) + array([1., 2., 0.], dtype=float32) >>> mesh[0] = 3 >>> mesh[0] - array([ 3., 3., 3., 3., 3., 3., 3., 3., 3.], dtype=float32) + array([3., 3., 3., 3., 3., 3., 3., 3., 3.], dtype=float32) >>> len(mesh) == len(list(mesh)) True @@ -315,6 +315,17 @@ def update_areas(self): areas = .5 * numpy.sqrt((self.normals ** 2).sum(axis=1)) self.areas = areas.reshape((areas.size, 1)) + def check(self): + if (self.normals.sum(axis=0) >= 1e-4).any(): + self.warning(''' + Your mesh is not closed, the mass methods will not function + correctly on this mesh. For more info: + https://github.com/WoLpH/numpy-stl/issues/69 + '''.strip()) + return False + else: + return True + def get_mass_properties(self): ''' Evaluate and return a tuple with the following elements: @@ -325,6 +336,8 @@ def get_mass_properties(self): Documentation can be found here: http://www.geometrictools.com/Documentation/PolyhedralMassProperties.pdf ''' + self.check() + def subexpression(x): w0, w1, w2 = x[:, 0], x[:, 1], x[:, 2] temp0 = w0 + w1 @@ -421,7 +434,7 @@ def rotation_matrix(cls, axis, theta): [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)], [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]]) - def rotate(self, axis, theta, point=None): + def rotate(self, axis, theta=0, point=None): ''' Rotate the matrix over the given axis by the given theta (angle) @@ -442,6 +455,13 @@ def rotate(self, axis, theta, point=None): if not theta: return + self.rotate_using_matrix(self.rotation_matrix(axis, theta), point) + + def rotate_using_matrix(self, rotation_matrix, point=None): + # No need to rotate if there is no actual rotation + if not rotation_matrix.any(): + return + if isinstance(point, (numpy.ndarray, list, tuple)) and len(point) == 3: point = numpy.asarray(point) elif point is None: @@ -451,12 +471,6 @@ def rotate(self, axis, theta, point=None): else: raise TypeError('Incorrect type for point', point) - rotation_matrix = self.rotation_matrix(axis, theta) - - # No need to rotate if there is no actual rotation - if not rotation_matrix.any(): - return - def _rotate(matrix): if point.any(): # Translate while rotating diff --git a/stl/stl.py b/stl/stl.py index c96525d..b446ef5 100644 --- a/stl/stl.py +++ b/stl/stl.py @@ -214,7 +214,9 @@ def get(prefix=''): @classmethod def _load_ascii(cls, fh, header, speedups=True): - if _speedups and speedups: + # The speedups module is covered by travis but it can't be tested in + # all environments, this makes coverage checks easier + if _speedups and speedups: # pragma: no cover return _speedups.ascii_read(fh, header) else: iterator = cls._ascii_reader(fh, header) @@ -259,7 +261,7 @@ def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True): pass def _write_ascii(self, fh, name): - if _speedups and self.speedups: + if _speedups and self.speedups: # pragma: no cover _speedups.ascii_write(fh, b(name), self.data) else: def p(s, file): diff --git a/tests/stl_corruption.py b/tests/stl_corruption.py index aa6f5e8..f9c2b97 100644 --- a/tests/stl_corruption.py +++ b/tests/stl_corruption.py @@ -1,4 +1,5 @@ from __future__ import print_function +import numpy import pytest import struct @@ -127,3 +128,17 @@ def test_corrupt_binary_file(tmpdir, speedups): fh.seek(0) mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups) + +def test_duplicate_polygons(): + data = numpy.zeros(3, dtype=mesh.Mesh.dtype) + data['vectors'][0] = numpy.array([[0, 0, 0], + [1, 0, 0], + [0, 1, 1.]]) + data['vectors'][0] = numpy.array([[0, 0, 0], + [2, 0, 0], + [0, 2, 1.]]) + data['vectors'][0] = numpy.array([[0, 0, 0], + [3, 0, 0], + [0, 3, 1.]]) + + assert not mesh.Mesh(data, remove_empty_areas=False).check() diff --git a/tests/test_rotate.py b/tests/test_rotate.py index 374b656..65900ea 100644 --- a/tests/test_rotate.py +++ b/tests/test_rotate.py @@ -94,6 +94,27 @@ def test_rotation_over_point(): mesh.rotate([1, 0, 0], math.radians(180), point='x') +def test_double_rotation(): + # Create a single face + data = numpy.zeros(1, dtype=Mesh.dtype) + + data['vectors'][0] = numpy.array([[1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + + mesh = Mesh(data, remove_empty_areas=False) + + rotation_matrix = mesh.rotation_matrix([1, 0, 0], math.radians(180)) + combined_rotation_matrix = numpy.dot(rotation_matrix, rotation_matrix) + + mesh.rotate_using_matrix(combined_rotation_matrix) + utils.array_equals( + mesh.vectors, + numpy.array([[[1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.]]])) + + def test_no_rotation(): # Create a single face data = numpy.zeros(1, dtype=Mesh.dtype)