From a065d82819209008975c5e687c0b7dad44de89df Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Mon, 21 Feb 2022 22:49:28 +0100 Subject: [PATCH] fix path reversal, computation of path proxy, end points and control points for multiple subpaths --- inkex/elements/_base.py | 4 ++- inkex/paths.py | 43 ++++++++++++++++--------- path_number_nodes.py | 4 +-- tests/test_inkex_paths.py | 68 +++++++++++++++++++++++++++++++++++---- 4 files changed, 93 insertions(+), 26 deletions(-) diff --git a/inkex/elements/_base.py b/inkex/elements/_base.py index 395ac63a..0311eeed 100644 --- a/inkex/elements/_base.py +++ b/inkex/elements/_base.py @@ -192,7 +192,9 @@ class BaseElement(IBaseElement): return self.attrib.pop(addNS(attr), default) # pylint: disable=no-member @overload - def add(self, child1: BaseElement, *children: BaseElement) -> Tuple[BaseElement]: + def add( + self, child1: BaseElement, child2: BaseElement, *children: BaseElement + ) -> Tuple[BaseElement]: ... @overload diff --git a/inkex/paths.py b/inkex/paths.py index 2d2f04bf..4c3e1231 100644 --- a/inkex/paths.py +++ b/inkex/paths.py @@ -1430,10 +1430,11 @@ class Path(list): prev_prev = Vector2d() first = Vector2d() - for i, seg in enumerate(self): # type: PathCommand - if i == 0: - first = seg.end_point(first, prev) - for cpt in seg.control_points(first, prev, prev_prev): + for seg in self: # type: PathCommand + cpts = list(seg.control_points(first, prev, prev_prev)) + if isinstance(seg, (zoneClose, ZoneClose, move, Move)): + first = cpts[-1] + for cpt in cpts: prev_prev = prev prev = cpt yield cpt @@ -1444,10 +1445,10 @@ class Path(list): prev = Vector2d() first = Vector2d() - for i, seg in enumerate(self): # type: PathCommand - if i == 0: - first = seg.end_point(first, prev) + for seg in self: # type: PathCommand end_point = seg.end_point(first, prev) + if isinstance(seg, (zoneClose, ZoneClose, move, Move)): + first = end_point prev = end_point yield end_point @@ -1494,19 +1495,29 @@ class Path(list): """Returns a reversed path""" result = Path() *_, first = self.end_points + closer = None # Go through the path in reverse order - for index, command in reversed(list(enumerate(self.proxy_iterator()))): + for index, prcom in reversed(list(enumerate(self.proxy_iterator()))): + if isinstance(prcom.command, (Move, move, ZoneClose, zoneClose)): + if closer is not None: + if len(result) > 0 and isinstance( + result[-1], (Line, line, Vert, vert, Horz, horz) + ): + result.pop() # We can replace simple lines with Z + result.append(closer) # replace with same type (rel or abs) + if isinstance(prcom.command, (ZoneClose, zoneClose)): + closer = prcom.command + else: + closer = None + if index == 0: - if command.letter == "M": + if prcom.letter == "M": result.insert(0, Move(first.x, first.y)) - elif command.letter == "m": + elif prcom.letter == "m": result.insert(0, move(first.x, first.y)) else: - result.append(command.reverse()) - - if self[-1].letter.lower() == "z": - result.append(self[-1]) + result.append(prcom.reverse()) return result @@ -1526,8 +1537,8 @@ class Path(list): prev_prev = Vector2d() first = Vector2d() - for i, seg in enumerate(self): # type: PathCommand - if i == 0: + for seg in self: # type: PathCommand# + if isinstance(seg, (zoneClose, ZoneClose, move, Move)): first = seg.end_point(first, previous) yield Path.PathCommandProxy(seg, first, previous, prev_prev) if isinstance( diff --git a/path_number_nodes.py b/path_number_nodes.py index 77d61be9..e43fce47 100755 --- a/path_number_nodes.py +++ b/path_number_nodes.py @@ -47,9 +47,9 @@ class NumberNodes(inkex.EffectExtension): for node in filtered: self.add_dot(node) - def add_dot(self, node): + def add_dot(self, node: inkex.PathElement): """Add a dot label for this path element""" - group = node.getparent().add(inkex.Group()) + group: inkex.Group = node.getparent().add(inkex.Group()) dot_group = group.add(inkex.Group()) num_group = group.add(inkex.Group()) path_trans_applied = node.path.transform(node.composed_transform()) diff --git a/tests/test_inkex_paths.py b/tests/test_inkex_paths.py index afd1203f..e8f147bc 100644 --- a/tests/test_inkex_paths.py +++ b/tests/test_inkex_paths.py @@ -713,16 +713,16 @@ class PathTest(TestCase): def test_reverse(self): """Paths can be reversed""" - """Testing reverse() with relative coordinates, closed path""" + # Testing reverse() with relative coordinates, closed path ret = Path( "m 10 50 h 40 v -40 l 50 39.9998 c -22 2 -35 12 -50 25 l -40 -15 l 0 -10 z" ) ret = ret.reverse() self._assertPath( ret, - "m 10 50 l 0 -0.0002 l -0 10 l 40 15 c 15 -13 28 -23 50 -25 l -50 -39.9998 v 40 h -40 z", + "m 10 50 l 0 -0.0002 l -0 10 l 40 15 c 15 -13 28 -23 50 -25 l -50 -39.9998 v 40 z", ) - """Testing reverse() with relative coordinates, open path""" + # Testing reverse() with relative coordinates, open path ret = Path( "m 10 50 h 40 v -40 l 50 39.9998 c -22 2 -35 12 -50 25 l -40 -15 l 0 -10" ) @@ -731,14 +731,14 @@ class PathTest(TestCase): ret, "m 10 49.9998 l -0 10 l 40 15 c 15 -13 28 -23 50 -25 l -50 -39.9998 v 40 h -40", ) - """Testing reverse() with absolute coordinates, closed path""" + # Testing reverse() with absolute coordinates, closed path ret = Path("M 100 35 L 100 25 L 60 10 C 45 23 32 33 10 35 L 60 75 L 60 35 Z") ret = ret.reverse() self._assertPath( ret, - "M 100 35 L 60 35 L 60 75 L 10 35 C 32 33 45 23 60 10 L 100 25 L 100 35 Z", + "M 100 35 L 60 35 L 60 75 L 10 35 C 32 33 45 23 60 10 L 100 25 Z", ) - """Testing reverse() with absolute coordinates, open path""" + # Testing reverse() with absolute coordinates, open path ret = Path( "M 100 35 L 100 25 L 60 10 C 45 23 32 33 10 35 L 60 75 L 60 35 L 100 35" ) @@ -768,7 +768,7 @@ class PathTest(TestCase): self._assertPath( ret, "m 63 47 c -21 -9 -16 -18 -39 -4 M 103 64 c -14 8 -24 0 -34 -11 " - "m -2 21 c -12 -10 -21 -12 -35 -7 M 58 88 l 10 4 c -7 9 -20 -2 -10 -4", + "m -2 21 c -12 -10 -21 -12 -35 -7 M 58 88 l 10 4 c -7 9 -20 -2 -10 -4 z", ) @@ -872,6 +872,60 @@ class SuperPathTest(TestCase): tempsub = CubicSuperPath(tempsub[0]) self.assertEqual(comparison, str(tempsub)) + def test_multiple_relative(self): + """Test for https://gitlab.com/inkscape/extensions/-/issues/450""" + + def compare_complex(current, epts): + for point, comp in zip(current.end_points, epts): + self.assertAlmostTuple(point, comp, msg=f"got {point}, expected {comp}") + for point, comp in zip(current.control_points, epts): + self.assertAlmostTuple(point, comp, msg=f"got {point}, expected {comp}") + # now reverse the path + p_rev = current.reverse() + for point, comp in zip(p_rev.end_points, epts[::-1]): + self.assertAlmostTuple(point, comp, msg=f"got {point}, expected {comp}") + # We expect to have the same amount of closed subpaths after the operation + self.assertEqual( + len(re.findall(r"[Zz]", str(p_rev))), + len(re.findall(r"[Zz]", str(current))), + ) + # now check that transform works correctly + p_trans = current.transform(Transform("translate(10, 20)")) + for point, comp in zip(p_trans.end_points, epts): + comp = comp + Vector2d(10, 20) + self.assertAlmostTuple(point, comp, msg=f"got {point}, expected {comp}") + + path = Path("m 50,20 v -10 h -10 z m 30,-20 v 20 h 20 z m -50,20 v -15 h -15 z") + path2 = Path( + "m 50,20 v -10 h -10 l 10, 10 m 30,-20 v 20 h 20 l -20,-20 m -50,20 v -15 h -15 z" + ) + path3 = Path( + "m 50,20 v -10 h -10 z m 30,-20 v 20 h 20 l -20,-20 m -50,20 v -15 h -15 l 15 15" + ) + pts = [ + (50, 20), + (50, 10), + (40, 10), + (50, 20), + (80, 0), + (80, 20), + (100, 20), + (80, 0), + (30, 20), + (30, 5), + (15, 5), + (30, 20), + ] + compare_complex(path, pts) + compare_complex(path2, pts) + compare_complex(path3, pts) + path4 = Path("m 50,20 v -10 h -10 z z z") + pts4 = [(50, 20), (50, 10), (40, 10), (50, 20), (50, 20), (50, 20)] + compare_complex(path4, pts4) + path5 = Path("m 50,20 z m 10, 10 m 20, 20 v -10 h -10 z") + pts5 = [(50, 20), (50, 20), (60, 30), (80, 50), (80, 40), (70, 40), (80, 50)] + compare_complex(path5, pts5) + class ProxyTest(TestCase): def test_simple_path(self): -- GitLab