# coding=utf-8 # # Copyright (C) 2018 Martin Owens # Copyright (C) 2023 Jonathan Neuhauser # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # """Line-like path commands (Line, Horz, Vert, ZoneClose and their relative siblings)""" from __future__ import annotations from typing import overload, Tuple, Optional, TYPE_CHECKING, Callable, Union from inkex.paths.interfaces import ILengthSettings, LengthSettings from ..transforms import Transform, BoundingBox, ComplexLike from .interfaces import AbsolutePathCommand, RelativePathCommand, ILengthSettings if TYPE_CHECKING: from .curves import Curve class LineMixin: """Common Line functions""" # pylint: disable=unused-argument arg1: complex cend_point: Callable[[complex, complex], complex] def ccurve_points(self, first: complex, prev: complex, prev_prev: complex): """Common implementation of ccurve_points for Lines""" arg1 = self.cend_point(first, prev) return prev, arg1, arg1 def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex, # pylint: disable=unused-argument ) -> Tuple[complex, ...]: """Common implementation of ccontrol_points for Lines""" return (self.cend_point(first, prev),) def _cderivative( self, first: complex, prev: complex, prev_control: complex, t: float, n: int = 1 ) -> complex: start = self.cend_point(first, prev) if prev == start: raise ValueError("Derivative is not defined for zero-length segments") if n == 1: return start - prev return 0j def _curvature( self, first: complex, prev: complex, prev_control: complex, t: float ) -> float: return 0 def _cpoint( self, first: complex, prev: complex, prev_control: complex, t: float ) -> complex: return self.cend_point(first, prev) * t + (1 - t) * prev def _length( self, first: complex, prev: complex, prev_control: complex, t0: float = 0, t1: float = 1, settings=LengthSettings(), ) -> float: return abs(self.cend_point(first, prev) - prev) * (t1 - t0) def _ilength( self, first: complex, prev: complex, prev_control: complex, length: float, settings: ILengthSettings = ILengthSettings(), ): return length / self._length(first, prev, prev_control) class Line(LineMixin, AbsolutePathCommand): """Line segment""" letter = "L" nargs = 2 arg1: complex """The (absolute) end points of the line.""" @property def x(self): """x coordinate of the line's (absolute) end point.""" return self.arg1.real @property def y(self): """y coordinate of the line's (absolute) end point.""" return self.arg1.imag @property def args(self): return self.x, self.y @overload def __init__(self, x: ComplexLike): ... @overload def __init__(self, x: float, y: float): ... def __init__(self, x, y=None): if y is not None: self.arg1 = x + y * 1j else: self.arg1 = complex(x) def update_bounding_box(self, first, last_two_points, bbox): bbox += BoundingBox( (last_two_points[-1].real, self.x), (last_two_points[-1].imag, self.y) ) def to_relative(self, prev: ComplexLike) -> line: return line(self.arg1 - prev) def transform(self, transform) -> Line: return Line(transform.capply_to_point(self.arg1)) def cend_point(self, first: complex, prev: complex) -> complex: # pylint: disable=unused-argument return self.arg1 def reverse(self, first: ComplexLike, prev: ComplexLike) -> Line: return Line(prev) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[Line, Line]: return Line(self._cpoint(first, prev, prev_control, t)), Line(self.arg1) class line(LineMixin, RelativePathCommand): # pylint: disable=invalid-name """Relative line segment""" letter = "l" nargs = 2 arg1: complex """The (relative) end points of the line.""" @property def dx(self): """x coordinate of the line's (relative) end point.""" return self.arg1.real @property def dy(self): """y coordinate of the line's (relative) end point.""" return self.arg1.imag @property def args(self): return self.dx, self.dy @overload def __init__(self, dx: ComplexLike): ... @overload def __init__(self, dx: float, dy: float): ... def __init__(self, dx, dy=None): if dy is not None: self.arg1 = dx + dy * 1j else: self.arg1 = complex(dx) def to_absolute(self, prev: ComplexLike) -> Line: return Line(prev + self.arg1) def cend_point(self, first: complex, prev: complex) -> complex: # pylint: disable=unused-argument return self.arg1 + prev def reverse(self, first: ComplexLike, prev: ComplexLike) -> line: return line(-self.arg1) def to_curve( self, prev: ComplexLike, prev_prev: Optional[ComplexLike] = 0j ) -> Curve: raise ValueError("Move segments can not be changed into curves.") def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[line, line]: dx1 = self.cpoint(first, prev, prev_control, t) - prev return line(dx1), line(self.arg1 - dx1) class MoveMixin: """Disable derivative / length method for Move command.""" def _cderivative( self, first: complex, prev: complex, prev_control: complex, t: float, n: int = 1 ) -> complex: raise ValueError("Derivative is not supported for move/Move") def _cunit_tangent( self, first: complex, prev: complex, prev_control: complex, t: float ) -> complex: raise ValueError("Unit Tangent is not supported for move/Move") def _curvature( self, first: complex, prev: complex, prev_control: complex, t: float ) -> float: raise ValueError("Curvature is not supported for move/Move") def _cpoint( self, first: complex, prev: complex, prev_control: complex, t: float ) -> complex: raise ValueError("Point is not supported for move/Move") def _length( self, first: complex, prev: complex, prev_control: complex, t0: float = 0, t1: float = 1, settings=LengthSettings(), ) -> float: raise ValueError("Length is not supported for move/Move") def _ilength( self, first: complex, prev: complex, prev_control: complex, length: float, settings: ILengthSettings = ILengthSettings(), ): raise ValueError("ILength is not supported for move/Move") def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[Move, Move]: raise ValueError("Split is not supported for move/Move") class Move(MoveMixin, AbsolutePathCommand): """Move pen segment without a line""" letter = "M" nargs = 2 next_command = Line arg1: complex """The (absolute) end points of the Move command""" @property def x(self): """x coordinate of the Moves's (absolute) end point.""" return self.arg1.real @property def y(self): """y coordinate of the Move's (absolute) end point.""" return self.arg1.imag @property def args(self): return self.x, self.y @overload def __init__(self, x: ComplexLike): ... @overload def __init__(self, x: float, y: float): ... def __init__(self, x, y=None): if y is not None: self.arg1 = x + y * 1j else: self.arg1 = complex(x) def update_bounding_box(self, first, last_two_points, bbox): bbox += BoundingBox(self.x, self.y) def ccurve_points(self, first: complex, prev: complex, prev_prev: complex): return prev, self.arg1, self.arg1 def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: return (self.arg1,) def to_relative(self, prev: ComplexLike) -> move: return move(self.arg1 - prev) def transform(self, transform: Transform) -> Move: return Move(transform.capply_to_point(self.arg1)) def cend_point(self, first: complex, prev: complex) -> complex: return self.arg1 def to_curve( self, prev: ComplexLike, prev_prev: Optional[ComplexLike] = 0j ) -> Curve: raise ValueError("Move segments can not be changed into curves.") def reverse(self, first: ComplexLike, prev: ComplexLike) -> Move: return Move(prev) class move(MoveMixin, RelativePathCommand): # pylint: disable=invalid-name """Relative move segment""" letter = "m" nargs = 2 next_command = line @property def dx(self): """x coordinate of the moves's (relative) end point.""" return self.arg1.real @property def dy(self): """y coordinate of the move's (relative) end point.""" return self.arg1.imag @property def args(self): return self.dx, self.dy @overload def __init__(self, dx: ComplexLike): ... @overload def __init__(self, dx: float, dy: float): ... def __init__(self, dx, dy=None): if dy is not None: self.arg1 = dx + dy * 1j else: self.arg1 = complex(dx) def ccurve_points(self, first: complex, prev: complex, prev_prev: complex): return prev, self.arg1 + prev, self.arg1 + prev def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: return (self.arg1 + prev,) def cend_point(self, first: complex, prev: complex) -> complex: return self.arg1 + prev def to_absolute(self, prev: ComplexLike) -> Move: return Move(prev + self.arg1) def reverse(self, first: ComplexLike, prev: ComplexLike) -> move: return move(prev - first) def to_curve( self, prev: ComplexLike, prev_prev: Optional[ComplexLike] = 0j ) -> Curve: raise ValueError("Move segments can not be changed into curves.") class ZoneClose(LineMixin, AbsolutePathCommand): """Close segment to finish a path""" letter = "Z" nargs = 0 next_command = Move @property def args(self): return () def update_bounding_box(self, first, last_two_points, bbox): pass def transform(self, transform: Transform) -> ZoneClose: return ZoneClose() def to_relative(self, prev: ComplexLike) -> zoneClose: return zoneClose() def cend_point(self, first: complex, prev: complex) -> complex: # pylint: disable=unused-argument return first def to_curve( self, prev: ComplexLike, prev_prev: Optional[ComplexLike] = 0j ) -> Curve: raise ValueError("ZoneClose segments can not be changed into curves.") def reverse(self, first: ComplexLike, prev: ComplexLike) -> Line: return Line(prev) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[Line, ZoneClose]: return Line(self._cpoint(first, prev, prev_control, t)), ZoneClose() class zoneClose(LineMixin, RelativePathCommand): # pylint: disable=invalid-name """Same as above (svg says no difference)""" letter = "z" nargs = 0 next_command = Move @property def args(self): return () def to_absolute(self, prev: ComplexLike): return ZoneClose() def reverse(self, first: ComplexLike, prev: ComplexLike) -> line: return line(prev - first) def cend_point(self, first: complex, prev: complex) -> complex: # pylint: disable=unused-argument return first def to_curve( self, prev: ComplexLike, prev_prev: Optional[ComplexLike] = 0j ) -> Curve: raise ValueError("ZoneClose segments can not be changed into curves.") def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[line, zoneClose]: return line(self.cpoint(first, prev, prev_control, t) - prev), zoneClose() class Horz(LineMixin, AbsolutePathCommand): """Horizontal Line segment""" letter = "H" nargs = 1 @property def args(self): return (self.x,) def __init__(self, x): self.x = x def update_bounding_box(self, first, last_two_points, bbox): bbox += BoundingBox( (last_two_points[-1].real, self.x), last_two_points[-1].imag ) def to_relative(self, prev: ComplexLike) -> horz: return horz(self.x - complex(prev).real) def to_non_shorthand(self, prev: ComplexLike, prev_control: ComplexLike) -> Line: return self.to_line(prev) def transform(self, transform: Transform) -> AbsolutePathCommand: raise ValueError("Horizontal lines can't be transformed directly.") def cend_point(self, first: complex, prev: complex) -> complex: # pylint: disable=unused-argument return self.x + prev.imag * 1j def reverse(self, first: ComplexLike, prev: ComplexLike) -> Horz: return Horz(complex(prev).real) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[Horz, Horz]: return Horz(self.cpoint(first, prev, prev_control, t).real), Horz(self.x) class horz(LineMixin, RelativePathCommand): # pylint: disable=invalid-name """Relative horz line segment""" letter = "h" nargs = 1 @property def args(self): return (self.dx,) def __init__(self, dx): self.dx = dx def to_absolute(self, prev: ComplexLike) -> Horz: return Horz(complex(prev).real + self.dx) def to_non_shorthand(self, prev: ComplexLike, prev_control: ComplexLike) -> Line: return self.to_line(prev) def cend_point(self, first: complex, prev: complex) -> complex: # pylint: disable=unused-argument return (self.dx + prev.real) + prev.imag * 1j def reverse(self, first: ComplexLike, prev: ComplexLike) -> horz: return horz(-self.dx) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[horz, horz]: dx1 = (self.cpoint(first, prev, prev_control, t) - prev).real return horz(dx1), horz(self.dx - dx1) class Vert(LineMixin, AbsolutePathCommand): """Vertical Line segment""" letter = "V" nargs = 1 @property def args(self): return (self.y,) def __init__(self, y): self.y = y def update_bounding_box(self, first, last_two_points, bbox): bbox += BoundingBox( last_two_points[-1].real, (last_two_points[-1].imag, self.y) ) def transform(self, transform: Transform) -> AbsolutePathCommand: raise ValueError("Vertical lines can't be transformed directly.") def to_non_shorthand(self, prev: ComplexLike, prev_control: ComplexLike) -> Line: return self.to_line(prev) def to_relative(self, prev: ComplexLike) -> vert: return vert(self.y - complex(prev).imag) def cend_point(self, first: complex, prev: complex) -> complex: # pylint: disable=unused-argument return prev.real + self.y * 1j def reverse(self, first: ComplexLike, prev: ComplexLike) -> Vert: return Vert(complex(prev).imag) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[Vert, Vert]: return Vert(self.cpoint(first, prev, prev_control, t).imag), Vert(self.y) class vert(LineMixin, RelativePathCommand): # pylint: disable=invalid-name """Relative vertical line segment""" letter = "v" nargs = 1 @property def args(self): return (self.dy,) def __init__(self, dy): self.dy = dy def to_absolute(self, prev: ComplexLike) -> Vert: return Vert(complex(prev).imag + self.dy) def to_non_shorthand(self, prev: ComplexLike, prev_control: ComplexLike) -> Line: return self.to_line(prev) def cend_point(self, first: complex, prev: complex) -> complex: # pylint: disable=unused-argument return prev.real + (prev.imag + self.dy) * 1j def reverse(self, first: ComplexLike, prev: ComplexLike) -> vert: return vert(-self.dy) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[vert, vert]: dy1 = (self.cpoint(first, prev, prev_control, t) - prev).imag return vert(dy1), vert(self.dy - dy1)