#!/usr/bin/python """ Tests for Pyclipper wrapper library. """ from __future__ import print_function try: from unittest2 import TestCase, main except ImportError: from unittest import TestCase, main import sys if sys.version_info < (3,): integer_types = (int, long) else: integer_types = (int,) import pyclipper # Example polygons from http://www.angusj.com/delphi/clipper.php PATH_SUBJ_1 = [[180, 200], [260, 200], [260, 150], [180, 150]] # square, orientation is False PATH_SUBJ_2 = [[215, 160], [230, 190], [200, 190]] # triangle PATH_CLIP_1 = [[190, 210], [240, 210], [240, 130], [190, 130]] # square PATH_SIGMA = [[300, 400], [100, 400], [200, 300], [100, 200], [300, 200]] # greek letter sigma PATTERN = [[4, -6], [6, -6], [-4, 6], [-6, 6]] INVALID_PATH = [[1, 1], ] # less than 2 vertices class TestPyclipperModule(TestCase): def test_has_classes(self): self.assertTrue(hasattr(pyclipper, 'Pyclipper')) self.assertTrue(hasattr(pyclipper, 'PyclipperOffset')) def test_has_namespace_methods(self): for method in ('Orientation', 'Area', 'PointInPolygon', 'SimplifyPolygon', 'SimplifyPolygons', 'CleanPolygon', 'CleanPolygons', 'MinkowskiSum', 'MinkowskiSum2', 'MinkowskiDiff', 'PolyTreeToPaths', 'ClosedPathsFromPolyTree', 'OpenPathsFromPolyTree', 'ReversePath', 'ReversePaths'): self.assertTrue(hasattr(pyclipper, method)) class TestNamespaceMethods(TestCase): def test_orientation(self): self.assertFalse(pyclipper.Orientation(PATH_SUBJ_1)) self.assertTrue(pyclipper.Orientation(PATH_SUBJ_1[::-1])) def test_area(self): # area less than 0 because orientation is False area_neg = pyclipper.Area(PATH_SUBJ_1) area_pos = pyclipper.Area(PATH_SUBJ_1[::-1]) self.assertLess(area_neg, 0) self.assertGreater(area_pos, 0) self.assertEqual(abs(area_neg), area_pos) def test_point_in_polygon(self): # on polygon self.assertEqual(pyclipper.PointInPolygon((180, 200), PATH_SUBJ_1), -1) # in polygon self.assertEqual(pyclipper.PointInPolygon((200, 180), PATH_SUBJ_1), 1) # outside of polygon self.assertEqual(pyclipper.PointInPolygon((500, 500), PATH_SUBJ_1), 0) def test_minkowski_sum(self): solution = pyclipper.MinkowskiSum(PATTERN, PATH_SIGMA, False) self.assertGreater(len(solution), 0) def test_minkowski_sum2(self): solution = pyclipper.MinkowskiSum2(PATTERN, [PATH_SIGMA], False) self.assertGreater(len(solution), 0) def test_minkowski_diff(self): solution = pyclipper.MinkowskiDiff(PATH_SUBJ_1, PATH_SUBJ_2) self.assertGreater(len(solution), 0) def test_reverse_path(self): solution = pyclipper.ReversePath(PATH_SUBJ_1) manualy_reversed = PATH_SUBJ_1[::-1] self.check_reversed_path(solution, manualy_reversed) def test_reverse_paths(self): solution = pyclipper.ReversePaths([PATH_SUBJ_1]) manualy_reversed = [PATH_SUBJ_1[::-1]] self.check_reversed_path(solution[0], manualy_reversed[0]) def check_reversed_path(self, path_1, path_2): if len(path_1) is not len(path_2): return False for i in range(len(path_1)): self.assertEqual(path_1[i][0], path_2[i][0]) self.assertEqual(path_1[i][1], path_2[i][1]) def test_simplify_polygon(self): solution = pyclipper.SimplifyPolygon(PATH_SUBJ_1) self.assertEqual(len(solution), 1) def test_simplify_polygons(self): solution = pyclipper.SimplifyPolygons([PATH_SUBJ_1]) solution_single = pyclipper.SimplifyPolygon(PATH_SUBJ_1) self.assertEqual(len(solution), 1) self.assertEqual(len(solution), len(solution_single)) _do_solutions_match(solution, solution_single) def test_clean_polygon(self): solution = pyclipper.CleanPolygon(PATH_CLIP_1) self.assertEqual(len(solution), len(PATH_CLIP_1)) def test_clean_polygons(self): solution = pyclipper.CleanPolygons([PATH_CLIP_1]) self.assertEqual(len(solution), 1) self.assertEqual(len(solution[0]), len(PATH_CLIP_1)) class TestFilterPyPolyNode(TestCase): def setUp(self): tree = pyclipper.PyPolyNode() tree.Contour.append(PATH_CLIP_1) tree.IsOpen = True child = pyclipper.PyPolyNode() child.IsOpen = False child.Parent = tree child.Contour = PATH_SUBJ_1 tree.Childs.append(child) child = pyclipper.PyPolyNode() child.IsOpen = True child.Parent = tree child.Contour = PATH_SUBJ_2 tree.Childs.append(child) child2 = pyclipper.PyPolyNode() child2.IsOpen = False child2.Parent = child child2.Contour = PATTERN child.Childs.append(child2) # empty contour should not # be included in filtered results child2 = pyclipper.PyPolyNode() child2.IsOpen = False child2.Parent = child child2.Contour = [] child.Childs.append(child2) self.tree = tree def test_polytree_to_paths(self): paths = pyclipper.PolyTreeToPaths(self.tree) self.check_paths(paths, 4) def test_closed_paths_from_polytree(self): paths = pyclipper.ClosedPathsFromPolyTree(self.tree) self.check_paths(paths, 2) def test_open_paths_from_polytree(self): paths = pyclipper.OpenPathsFromPolyTree(self.tree) self.check_paths(paths, 2) def check_paths(self, paths, expected_nr): self.assertEqual(len(paths), expected_nr) self.assertTrue(all((len(path) > 0 for path in paths))) class TestPyclipperAddPaths(TestCase): def setUp(self): self.pc = pyclipper.Pyclipper() def test_add_path(self): # should not raise an exception self.pc.AddPath(PATH_CLIP_1, poly_type=pyclipper.PT_CLIP) def test_add_paths(self): # should not raise an exception self.pc.AddPaths([PATH_SUBJ_1, PATH_SUBJ_2], poly_type=pyclipper.PT_SUBJECT) def test_add_path_invalid_path(self): self.assertRaises(pyclipper.ClipperException, self.pc.AddPath, INVALID_PATH, pyclipper.PT_CLIP, True) def test_add_paths_invalid_path(self): self.assertRaises(pyclipper.ClipperException, self.pc.AddPaths, [INVALID_PATH, INVALID_PATH], pyclipper.PT_CLIP, True) try: self.pc.AddPaths([INVALID_PATH, PATH_CLIP_1], pyclipper.PT_CLIP) self.pc.AddPaths([PATH_CLIP_1, INVALID_PATH], pyclipper.PT_CLIP) except pyclipper.ClipperException: self.fail("add_paths raised ClipperException when not all paths were invalid") class TestClassProperties(TestCase): def check_property_assignment(self, pc, prop_name, values): for val in values: setattr(pc, prop_name, val) self.assertEqual(getattr(pc, prop_name), val) def test_pyclipper_properties(self): pc = pyclipper.Pyclipper() for prop_name in ('ReverseSolution', 'PreserveCollinear', 'StrictlySimple'): self.check_property_assignment(pc, prop_name, [True, False]) def test_pyclipperoffset_properties(self): pc = pyclipper.PyclipperOffset() for prop_name in ('MiterLimit', 'ArcTolerance'): self.check_property_assignment(pc, prop_name, [2.912, 132.12, 12, -123]) class TestPyclipperExecute(TestCase): def setUp(self): self.pc = pyclipper.Pyclipper() self.add_default_paths(self.pc) self.default_args = [pyclipper.CT_INTERSECTION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD] @staticmethod def add_default_paths(pc): pc.AddPath(PATH_CLIP_1, pyclipper.PT_CLIP) pc.AddPaths([PATH_SUBJ_1, PATH_SUBJ_2], pyclipper.PT_SUBJECT) @staticmethod def add_paths(pc, clip_path, subj_paths, addend=None, multiplier=None): pc.AddPath(_modify_vertices(clip_path, addend=addend, multiplier=multiplier), pyclipper.PT_CLIP) for subj_path in subj_paths: pc.AddPath(_modify_vertices(subj_path, addend=addend, multiplier=multiplier), pyclipper.PT_SUBJECT) def test_get_bounds(self): bounds = self.pc.GetBounds() self.assertIsInstance(bounds, pyclipper.PyIntRect) self.assertEqual(bounds.left, 180) self.assertEqual(bounds.right, 260) self.assertEqual(bounds.top, 130) self.assertEqual(bounds.bottom, 210) def test_execute(self): solution = self.pc.Execute(*self.default_args) self.assertEqual(len(solution), 2) def test_execute2(self): solution = self.pc.Execute2(*self.default_args) self.assertIsInstance(solution, pyclipper.PyPolyNode) self.check_pypolynode(solution) def test_execute_empty(self): pc = pyclipper.Pyclipper() with self.assertRaises(pyclipper.ClipperException): pc.Execute(pyclipper.CT_UNION, pyclipper.PFT_NONZERO, pyclipper.PFT_NONZERO) def test_clear(self): self.pc.Clear() with self.assertRaises(pyclipper.ClipperException): self.pc.Execute(*self.default_args) def test_exact_results(self): """ Test whether coordinates passed into the library are returned exactly, if they are not affected by the operation. """ pc = pyclipper.Pyclipper() # Some large triangle. path = [[[0, 1], [0, 0], [15 ** 15, 0]]] pc.AddPaths(path, pyclipper.PT_SUBJECT, True) result = pc.Execute(pyclipper.PT_CLIP, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) assert result == path def check_pypolynode(self, node): self.assertTrue(len(node.Contour) == 0 or len(node.Contour) > 2) # check vertex coordinate, should not be an iterable (in that case # that means that node.Contour is a list of paths, should be path if node.Contour: self.assertFalse(hasattr(node.Contour[0][0], '__iter__')) for child in node.Childs: self.check_pypolynode(child) class TestPyclipperOffset(TestCase): @staticmethod def add_path(pc, path): pc.AddPath(path, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) def test_execute(self): pc = pyclipper.PyclipperOffset() self.add_path(pc, PATH_CLIP_1) solution = pc.Execute(2.0) self.assertIsInstance(solution, list) self.assertEqual(len(solution), 1) def test_execute2(self): pc = pyclipper.PyclipperOffset() self.add_path(pc, PATH_CLIP_1) solution = pc.Execute2(2.0) self.assertIsInstance(solution, pyclipper.PyPolyNode) self.assertEqual(len(pyclipper.OpenPathsFromPolyTree(solution)), 0) self.assertEqual(len(pyclipper.ClosedPathsFromPolyTree(solution)), 1) def test_clear(self): pc = pyclipper.PyclipperOffset() self.add_path(pc, PATH_CLIP_1) pc.Clear() solution = pc.Execute(2.0) self.assertIsInstance(solution, list) self.assertEqual(len(solution), 0) class TestScalingFunctions(TestCase): scale = 2 ** 31 path = [(0, 0), (1, 1)] paths = [path] * 3 def test_value_scale_to(self): value = 0.5 res = pyclipper.scale_to_clipper(value, self.scale) assert isinstance(res, integer_types) assert res == int(value * self.scale) def test_value_scale_from(self): value = 1000000000000 res = pyclipper.scale_from_clipper(value, self.scale) assert isinstance(res, float) # Convert to float to get "normal" division in Python < 3. assert res == float(value) / self.scale def test_path_scale_to(self): res = pyclipper.scale_to_clipper(self.path) assert len(res) == len(self.path) assert all(isinstance(i, list) for i in res) assert all(isinstance(j, integer_types) for i in res for j in i) def test_path_scale_from(self): res = pyclipper.scale_from_clipper(self.path) assert len(res) == len(self.path) assert all(isinstance(i, list) for i in res) assert all(isinstance(j, float) for i in res for j in i) def test_paths_scale_to(self): res = pyclipper.scale_to_clipper(self.paths) assert len(res) == len(self.paths) assert all(isinstance(i, list) for i in res) assert all(isinstance(j, list) for i in res for j in i) assert all(isinstance(k, integer_types) for i in res for j in i for k in j) def test_paths_scale_from(self): res = pyclipper.scale_from_clipper(self.paths) assert len(res) == len(self.paths) assert all(isinstance(i, list) for i in res) assert all(isinstance(j, list) for i in res for j in i) assert all(isinstance(k, float) for i in res for j in i for k in j) class TestNonStandardNumbers(TestCase): def test_sympyzero(self): try: from sympy import Point2D from sympy.core.numbers import Zero except ImportError: self.skipTest("Skipping, sympy not available") path = [(0,0), (0,1)] path = [Point2D(v) for v in [(0,0), (0,1)]] assert type(path[0].x) == Zero path = pyclipper.scale_to_clipper(path) assert path == [[0, 0], [0, 2147483648]] class TestPackageVersion(TestCase): def test__version__(self): assert hasattr(pyclipper, "__version__") assert isinstance(pyclipper.__version__, str) def _do_solutions_match(paths_1, paths_2, factor=None): if len(paths_1) != len(paths_2): return False paths_1 = [_modify_vertices(p, multiplier=factor, converter=round if factor else None) for p in paths_1] paths_2 = [_modify_vertices(p, multiplier=factor, converter=round if factor else None) for p in paths_2] return all(((p_1 in paths_2) for p_1 in paths_1)) def _modify_vertices(path, addend=0.0, multiplier=1.0, converter=None): path = path[:] def convert_coordinate(c): if multiplier is not None: c *= multiplier if addend is not None: c += addend if converter: c = converter(c) return c return [[convert_coordinate(c) for c in v] for v in path] def run_tests(): main() if __name__ == '__main__': run_tests()