# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2017 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # ###########################################################################*/ from __future__ import absolute_import, division, unicode_literals __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "25/07/2016" import unittest from silx.test.utils import ParametricTestCase import numpy from silx.gui.plot3d.scene import utils # angleBetweenVectors ######################################################### class TestAngleBetweenVectors(ParametricTestCase): TESTS = { # name: (refvector, vectors, norm, refangles) 'single vector': ((1., 0., 0.), (1., 0., 0.), (0., 0., 1.), 0.), 'single vector, no norm': ((1., 0., 0.), (1., 0., 0.), None, 0.), 'with orthogonal norm': ((1., 0., 0.), ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)), (0., 0., 1.), (0., 90., 180., 270.)), 'with coplanar norm': # = similar to no norm ((1., 0., 0.), ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)), (1., 0., 0.), (0., 90., 180., 90.)), 'without norm': ((1., 0., 0.), ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)), None, (0., 90., 180., 90.)), 'not unit vectors': ((2., 2., 0.), ((1., 1., 0.), (1., -1., 0.)), None, (0., 90.)), } def testAngleBetweenVectorsFunction(self): for name, params in self.TESTS.items(): refvector, vectors, norm, refangles = params with self.subTest(name): refangles = numpy.radians(refangles) refvector = numpy.array(refvector) vectors = numpy.array(vectors) if norm is not None: norm = numpy.array(norm) testangles = utils.angleBetweenVectors( refvector, vectors, norm) self.assertTrue( numpy.allclose(testangles, refangles, atol=1e-5)) # Plane ####################################################################### class AssertNotificationContext(object): """Context that checks if an event.Notifier is sending events.""" def __init__(self, notifier, count=1): """Initializer. :param event.Notifier notifier: The notifier to test. :param int count: The expected number of calls. """ self._notifier = notifier self._callCount = None self._count = count def __enter__(self): self._callCount = 0 self._notifier.addListener(self._callback) def __exit__(self, exc_type, exc_value, traceback): # Do not return True so exceptions are propagated self._notifier.removeListener(self._callback) assert self._callCount == self._count self._callCount = None def _callback(self, *args, **kwargs): self._callCount += 1 class TestPlaneParameters(ParametricTestCase): """Test Plane.parameters read/write and notifications.""" PARAMETERS = { 'unit normal': (1., 0., 0., 1.), 'not unit normal': (1., 1., 0., 1.), 'd = 0': (1., 0., 0., 0.) } def testParameters(self): """Check parameters read/write and notification.""" plane = utils.Plane() for name, parameters in self.PARAMETERS.items(): with self.subTest(name, parameters=parameters): with AssertNotificationContext(plane): plane.parameters = parameters # Plane parameters are converted to have a unit normal normparams = parameters / numpy.linalg.norm(parameters[:3]) self.assertTrue(numpy.allclose(plane.parameters, normparams)) ZEROS_PARAMETERS = ( (0., 0., 0., 0.), (0., 0., 0., 1.) ) ZEROS = 0., 0., 0., 0. def testParametersNoPlane(self): """Test Plane.parameters with ||normal|| == 0 .""" plane = utils.Plane() plane.parameters = self.ZEROS for parameters in self.ZEROS_PARAMETERS: with self.subTest(parameters=parameters): with AssertNotificationContext(plane, count=0): plane.parameters = parameters self.assertTrue( numpy.allclose(plane.parameters, self.ZEROS, 0., 0.)) # unindexArrays ############################################################### class TestUnindexArrays(ParametricTestCase): """Test unindexArrays function.""" def testBasicModes(self): """Test for modes: points, lines and triangles""" indices = numpy.array((1, 2, 0)) arrays = (numpy.array((0., 1., 2.)), numpy.array(((0, 0), (1, 1), (2, 2)))) refresults = (numpy.array((1., 2., 0.)), numpy.array(((1, 1), (2, 2), (0, 0)))) for mode in ('points', 'lines', 'triangles'): with self.subTest(mode=mode): testresults = utils.unindexArrays(mode, indices, *arrays) for ref, test in zip(refresults, testresults): self.assertTrue(numpy.equal(ref, test).all()) def testPackedLines(self): """Test for modes: line_strip, loop""" indices = numpy.array((1, 2, 0)) arrays = (numpy.array((0., 1., 2.)), numpy.array(((0, 0), (1, 1), (2, 2)))) results = { 'line_strip': ( numpy.array((1., 2., 2., 0.)), numpy.array(((1, 1), (2, 2), (2, 2), (0, 0)))), 'loop': ( numpy.array((1., 2., 2., 0., 0., 1.)), numpy.array(((1, 1), (2, 2), (2, 2), (0, 0), (0, 0), (1, 1)))), } for mode, refresults in results.items(): with self.subTest(mode=mode): testresults = utils.unindexArrays(mode, indices, *arrays) for ref, test in zip(refresults, testresults): self.assertTrue(numpy.equal(ref, test).all()) def testPackedTriangles(self): """Test for modes: triangle_strip, fan""" indices = numpy.array((1, 2, 0, 3)) arrays = (numpy.array((0., 1., 2., 3.)), numpy.array(((0, 0), (1, 1), (2, 2), (3, 3)))) results = { 'triangle_strip': ( numpy.array((1., 2., 0., 2., 0., 3.)), numpy.array(((1, 1), (2, 2), (0, 0), (2, 2), (0, 0), (3, 3)))), 'fan': ( numpy.array((1., 2., 0., 1., 0., 3.)), numpy.array(((1, 1), (2, 2), (0, 0), (1, 1), (0, 0), (3, 3)))), } for mode, refresults in results.items(): with self.subTest(mode=mode): testresults = utils.unindexArrays(mode, indices, *arrays) for ref, test in zip(refresults, testresults): self.assertTrue(numpy.equal(ref, test).all()) def testBadIndices(self): """Test with negative indices and indices higher than array length""" arrays = numpy.array((0, 1)), numpy.array((0, 1, 2)) # negative indices with self.assertRaises(AssertionError): utils.unindexArrays('points', (-1, 0), *arrays) # Too high indices with self.assertRaises(AssertionError): utils.unindexArrays('points', (0, 10), *arrays) # triangleNormals ############################################################# class TestTriangleNormals(ParametricTestCase): """Test triangleNormals function.""" def test(self): """Test for modes: points, lines and triangles""" positions = numpy.array( ((0., 0., 0.), (1., 0., 0.), (0., 1., 0.), # normal = Z (1., 1., 1.), (1., 2., 3.), (4., 5., 6.), # Random triangle # Degenerated triangles: (0., 0., 0.), (1., 0., 0.), (2., 0., 0.), # Colinear points (1., 1., 1.), (1., 1., 1.), (1., 1., 1.), # All same point ), dtype='float32') normals = numpy.array( ((0., 0., 1.), (-0.40824829, 0.81649658, -0.40824829), (0., 0., 0.), (0., 0., 0.)), dtype='float32') testnormals = utils.trianglesNormal(positions) self.assertTrue(numpy.allclose(testnormals, normals)) # suite ####################################################################### def suite(): testsuite = unittest.TestSuite() for test in (TestAngleBetweenVectors, TestPlaneParameters, TestUnindexArrays, TestTriangleNormals): testsuite.addTest( unittest.defaultTestLoader.loadTestsFromTestCase(test)) return testsuite if __name__ == '__main__': unittest.main(defaultTest='suite')