# 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. # """Curve and Smooth Path Commands""" from __future__ import annotations from typing import overload, Tuple, Callable, Union, cast import numpy as np from ..transforms import cubic_extrema, Transform, Vector2d, ComplexLike from .interfaces import ( AbsolutePathCommand, RelativePathCommand, BezierArcComputationMixin, BezierComputationMixin, ) class CurveMixin(BezierComputationMixin, BezierArcComputationMixin): """Common functionality for curves""" ccontrol_points: Callable[[complex, complex, complex], Tuple[complex, ...]] def ccurve_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: """Common implementation of ccurve_points for Curves""" return self.ccontrol_points(first, prev, prev_prev) def _cderivative( self, first: complex, prev: complex, prev_control: complex, t: float, n: int = 1 ) -> complex: """Returns the nth derivative of the segment at t. .. hint:: Bezier curves can have points where their derivative vanishes. If you are interested in the tangent direction, use the :func:`unit_tangent` method instead.""" points = self.ccontrol_points(first, prev, prev_control) if n == 1: return ( 3 * (points[0] - prev) * ((1 - t) ** 2) + 6 * (points[1] - points[0]) * (1 - t) * t + 3 * (points[2] - points[1]) * t**2 ) elif n == 2: return 6 * ( (1 - t) * (points[1] - 2 * points[0] + prev) + t * (points[2] - 2 * points[1] + points[0]) ) elif n == 3: return 6 * (points[2] - 3 * (points[1] - points[0]) - prev) elif n > 3: return complex(0, 0) else: raise ValueError("n should be a positive integer.") def poly(self, prev, prev_control, return_coeffs=False): """Returns a the cubic as a complex Polynomial object. .. versionadded:: 1.4""" points = self.ccontrol_points(0j, prev, prev_control) coeffs = ( -prev + 3 * (points[0] - points[1]) + points[2], 3 * (prev - 2 * points[0] + points[1]), 3 * (prev + points[0]), prev, ) if return_coeffs: return coeffs return np.poly1d(coeffs) def _cunit_tangent( self, first: complex, prev: complex, prev_control: complex, t: float ) -> complex: return self.bezier_unit_tangent(prev, prev_control, t) def _curvature( self, first: complex, prev: complex, prev_control: complex, t: float ): return self.segment_curvature(prev, prev_control, t) def _abssplit( self, prev: complex, prev_control: complex, t: float ) -> Tuple[Curve, Curve]: """Split this curve and return two Curves using DeCasteljau's algorithm""" p1, p2, p3 = self.ccontrol_points(0j, prev, prev_control) p1_1 = (1 - t) * prev + t * p1 p1_2 = (1 - t) * p1 + t * p2 p1_3 = (1 - t) * p2 + t * p3 p2_1 = (1 - t) * p1_1 + t * p1_2 p2_2 = (1 - t) * p1_2 + t * p1_3 p3_1 = (1 - t) * p2_1 + t * p2_2 return Curve(p1_1, p2_1, p3_1), Curve(p2_2, p1_3, p3) def _relsplit(self, prev: complex, prev_control: complex, t: float): """Split this curve and return two curves""" c1abs, c2abs = self._abssplit(prev, prev_control, t) return c1abs.to_relative(prev), c2abs.to_relative(c1abs.arg3) def _cpoint( self, first: complex, prev: complex, prev_control: complex, t: float ) -> complex: control1, control2, end = self.ccontrol_points(first, prev, prev_control) return ( (1 - t) ** 3 * prev + 3 * t * (1 - t) ** 2 * control1 + 3 * t**2 * (1 - t) * control2 + t**3 * end ) class Curve(CurveMixin, AbsolutePathCommand): """Absolute Curved Line segment""" letter = "C" nargs = 6 arg1: complex """The (absolute) first control point""" arg2: complex """The (absolute) second control point""" arg3: complex """The (absolute) end point""" @property def x2(self) -> float: """x coordinate of the (absolute) first control point""" return self.arg1.real @property def y2(self) -> float: """y coordinate of the (absolute) first control point""" return self.arg1.imag @property def x3(self) -> float: """x coordinate of the (absolute) second control point""" return self.arg2.real @property def y3(self) -> float: """y coordinate of the (absolute) second control point""" return self.arg2.imag @property def x4(self) -> float: """x coordinate of the (absolute) end point""" return self.arg3.real @property def y4(self) -> float: """y coordinate of the (absolute) end point""" return self.arg3.imag @property def args(self): return ( self.arg1.real, self.arg1.imag, self.arg2.real, self.arg2.imag, self.arg3.real, self.arg3.imag, ) @overload def __init__(self, x2: ComplexLike, x3: ComplexLike, x4: ComplexLike): ... @overload def __init__( self, x2: float, y2: float, x3: float, y3: float, x4: float, y4: float ): ... # pylint: disable=too-many-arguments def __init__(self, x2, y2, x3, y3=None, x4=None, y4=None): # pylint: disable=too-many-arguments if y3 is not None: self.arg1 = x2 + y2 * 1j self.arg2 = x3 + y3 * 1j self.arg3 = x4 + y4 * 1j else: self.arg1, self.arg2, self.arg3 = complex(x2), complex(y2), complex(x3) def update_bounding_box(self, first, last_two_points, bbox): x1, x2, x3, x4 = last_two_points[-1].real, self.x2, self.x3, self.x4 y1, y2, y3, y4 = last_two_points[-1].imag, self.y2, self.y3, self.y4 if not (x1 in bbox.x and x2 in bbox.x and x3 in bbox.x and x4 in bbox.x): bbox.x += cubic_extrema(x1, x2, x3, x4) if not (y1 in bbox.y and y2 in bbox.y and y3 in bbox.y and y4 in bbox.y): bbox.y += cubic_extrema(y1, y2, y3, y4) def transform(self, transform: Transform) -> Curve: return Curve( transform.capply_to_point(self.arg1), transform.capply_to_point(self.arg2), transform.capply_to_point(self.arg3), ) def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: # pylint: disable=unused-argument return (self.arg1, self.arg2, self.arg3) def to_relative(self, prev: ComplexLike) -> curve: return curve(self.arg1 - prev, self.arg2 - prev, self.arg3 - prev) def cend_point(self, first: complex, prev: complex) -> complex: return self.arg3 def reverse(self, first: ComplexLike, prev: ComplexLike) -> Curve: return Curve(self.arg2, self.arg1, prev) def to_bez(self): """Convert to [[c1x, c1y], [c2x, c2y], [end_x, end_y]]""" return [Vector2d.c2t(i) for i in self.ccontrol_points(0j, 0j, 0j)] def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[Curve, Curve]: return self._abssplit(prev, prev_control, t) class curve(CurveMixin, RelativePathCommand): # pylint: disable=invalid-name """Relative curved line segment""" letter = "c" nargs = 6 arg1: complex """The (relative) first control point""" arg2: complex """The (relative) second control point""" arg3: complex """The (relative) end point""" @property def dx2(self) -> float: """x coordinate of the (relative) first control point""" return self.arg1.real @property def dy2(self) -> float: """y coordinate of the (relative) first control point""" return self.arg1.imag @property def dx3(self) -> float: """x coordinate of the (relative) second control point""" return self.arg2.real @property def dy3(self) -> float: """y coordinate of the (relative) second control point""" return self.arg2.imag @property def dx4(self) -> float: """x coordinate of the (relative) end point""" return self.arg3.real @property def dy4(self) -> float: """y coordinate of the (relative) end point""" return self.arg3.imag @overload def __init__(self, dx2: ComplexLike, dx3: ComplexLike, dx4: ComplexLike): ... @overload def __init__( self, dx2: float, dy2: float, dx3: float, dy3: float, dx4: float, dy4: float ): ... # pylint: disable=too-many-arguments def __init__(self, dx2, dy2, dx3, dy3=None, dx4=None, dy4=None): # pylint: disable=too-many-arguments if dy3 is not None: self.arg1 = dx2 + dy2 * 1j self.arg2 = dx3 + dy3 * 1j self.arg3 = dx4 + dy4 * 1j else: self.arg1, self.arg2, self.arg3 = complex(dx2), complex(dy2), complex(dx3) @property def args(self): return self.dx2, self.dy2, self.dx3, self.dy3, self.dx4, self.dy4 def to_absolute(self, prev: ComplexLike) -> Curve: return Curve(*self.ccurve_points(0j, complex(prev), 0j)) def cend_point(self, first: complex, prev: complex) -> complex: return self.arg3 + prev def reverse(self, first: ComplexLike, prev: ComplexLike) -> curve: return curve(-self.arg3 + self.arg2, -self.arg3 + self.arg1, -self.arg3) def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: # pylint: disable=unused-argument return ( self.arg1 + prev, self.arg2 + prev, self.arg3 + prev, ) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[curve, curve]: return self._relsplit(prev, prev_control, t) class Smooth(CurveMixin, AbsolutePathCommand): """Absolute Smoothed Curved Line segment""" letter = "S" nargs = 4 arg1: complex """The (absolute) control point""" arg2: complex """The (absolute) end point""" @property def x3(self) -> float: """x coordinate of the (absolute) control point""" return self.arg1.real @property def y3(self) -> float: """y coordinate of the (absolute) control point""" return self.arg1.imag @property def x4(self) -> float: """x coordinate of the (absolute) end point""" return self.arg2.real @property def y4(self) -> float: """y coordinate of the (absolute) end point""" return self.arg2.imag @property def args(self): return self.x3, self.y3, self.x4, self.y4 @overload def __init__(self, x3: ComplexLike, x4: ComplexLike): ... @overload def __init__(self, x3: float, y3: float, x4: float, y4: float): ... def __init__(self, x3, y3, x4=None, y4=None): if x4 is not None: self.arg1 = x3 + y3 * 1j self.arg2 = x4 + y4 * 1j else: self.arg1, self.arg2 = complex(x3), complex(y3) def update_bounding_box(self, first, last_two_points, bbox): # pylint: disable=no-member self.to_curve(last_two_points[-1], last_two_points[-2]).update_bounding_box( first, last_two_points, bbox ) def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: # pylint: disable=unused-argument return (2 * prev - prev_prev, self.arg1, self.arg2) def to_non_shorthand(self, prev: ComplexLike, prev_control: ComplexLike) -> Curve: return self.to_curve(prev, prev_control) def to_relative(self, prev: ComplexLike) -> smooth: return smooth(self.arg1 - prev, self.arg2 - prev) def transform(self, transform: Transform) -> Smooth: return Smooth( transform.capply_to_point(self.arg1), transform.capply_to_point(self.arg2) ) def cend_point(self, first: complex, prev: complex) -> complex: return self.arg2 def reverse(self, first: ComplexLike, prev: ComplexLike) -> Smooth: return Smooth(self.arg1, prev) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[Curve, Curve]: # We can't preserve the smooth type for a split because de Casteljau's # algorithm changes the handles (obviously). # Only in special cases such as splitting to subsequent smooth segments at # t=1/2 such a preservation would be possible crv = cast(Curve, self.to_non_shorthand(prev, prev_control)) return crv._split( # pylint: disable=protected-access, no-member first, prev, prev_control, t ) class smooth(CurveMixin, RelativePathCommand): # pylint: disable=invalid-name """Relative smoothed curved line segment""" letter = "s" nargs = 4 arg1: complex """The (absolute) control point""" arg2: complex """The (absolute) end point""" @property def dx3(self) -> float: """x coordinate of the (relative) control point""" return self.arg1.real @property def dy3(self) -> float: """y coordinate of the (relative) control point""" return self.arg1.imag @property def dx4(self) -> float: """x coordinate of the (relative) end point""" return self.arg2.real @property def dy4(self) -> float: """y coordinate of the (relative) end point""" return self.arg2.imag @property def args(self): return self.dx3, self.dy3, self.dx4, self.dy4 @overload def __init__(self, dx3: ComplexLike, dx4: ComplexLike): ... @overload def __init__(self, dx3: float, dy3: float, dx4: float, dy4: float): ... def __init__(self, dx3, dy3, dx4=None, dy4=None): if dx4 is not None: self.arg1 = dx3 + dy3 * 1j self.arg2 = dx4 + dy4 * 1j else: self.arg1, self.arg2 = complex(dx3), complex(dy3) def to_absolute(self, prev: ComplexLike) -> Smooth: return Smooth(self.arg1 + prev, self.arg2 + prev) def cend_point(self, first: complex, prev: complex) -> complex: return self.arg2 + prev def to_non_shorthand(self, prev: ComplexLike, prev_control: ComplexLike) -> Curve: return self.to_absolute(prev).to_non_shorthand(prev, prev_control) def reverse(self, first: ComplexLike, prev: ComplexLike): return smooth(-self.arg2 + self.arg1, -self.arg2) def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: # pylint: disable=unused-argument return (2 * prev - prev_prev, self.arg1 + prev, self.arg2 + prev) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[curve, curve]: return self._relsplit(prev, prev_control, t)