# 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. # """Quadratic and TepidQuadratic path commands""" from __future__ import annotations from typing import overload, Tuple, Callable, Union from math import sqrt import numpy as np from ..transforms import quadratic_extrema, Transform, ComplexLike from .interfaces import ( AbsolutePathCommand, RelativePathCommand, BezierComputationMixin, BezierArcComputationMixin, LengthSettings, ) class QuadraticMixin(BezierComputationMixin, BezierArcComputationMixin): # pylint: disable=unused-argument ccontrol_points: Callable[[complex, complex, complex], Tuple[complex, ...]] def _cderivative( self, first: complex, prev: complex, prev_control: complex, t: float, n: int = 1 ) -> complex: points = self.ccontrol_points(first, prev, prev_control) if n == 1: return 2 * ((points[0] - prev) * (1 - t) + (points[1] - points[0]) * t) if n == 2: return 2 * (prev - 2 * points[0] + points[1]) if n > 2: return 0j raise ValueError("n should be a positive integer.") def _cunit_tangent( self, first: complex, prev: complex, prev_control: complex, t: float ) -> complex: return self.bezier_unit_tangent(prev, prev_control, t) def _cpoint( self, first: complex, prev: complex, prev_control: complex, t: float ) -> complex: control, end = self.ccontrol_points(first, prev, prev_control) return (1 - t) ** 2 * prev + 2 * t * (1 - t) * control + t**2 * end # TODO maybe better treatment of degenerate beziers from # https://github.com/linebender/kurbo/blob/c229a914d303c5989c9e6b1d766def2df27a8185/src/quadbez.rs#L239 # Ported from https://github.com/mathandy/svgpathtools/blob/19df25b99b405ec4fc7616b58384eca7879b6fd4/svgpathtools/path.py#L919 # (MIT licensed) def _length( self, first: complex, prev: complex, prev_control: complex, t0: float = 0, t1: float = 1, settings=LengthSettings(), ) -> float: control, end = self.ccontrol_points(first, prev, prev_control) a = prev - 2 * control + end b = 2 * (control - prev) if abs(a) < 1e-12: s = abs(b) * (t1 - t0) else: c2 = 4 * (a.real**2 + a.imag**2) c1 = 4 * (a.real * b.real + a.imag * b.imag) c0 = b.real**2 + b.imag**2 beta = c1 / (2 * c2) gamma = c0 / c2 - beta**2 dq1_mag = sqrt(c2 * t1**2 + c1 * t1 + c0) dq0_mag = sqrt(c2 * t0**2 + c1 * t0 + c0) # this implicitly handles division by zero try: logarand = (sqrt(c2) * (t1 + beta) + dq1_mag) / ( sqrt(c2) * (t0 + beta) + dq0_mag ) s = ( (t1 + beta) * dq1_mag - (t0 + beta) * dq0_mag + gamma * sqrt(c2) * np.log(logarand) ) / 2 except ZeroDivisionError: s = np.nan if np.isnan(s): tstar = abs(b) / (2 * abs(a)) if t1 < tstar: return abs(a) * (t0**2 - t1**2) - abs(b) * (t0 - t1) elif tstar < t0: return abs(a) * (t1**2 - t0**2) - abs(b) * (t1 - t0) else: return ( abs(a) * (t1**2 + t0**2) - abs(b) * (t1 + t0) + abs(b) ** 2 / (2 * abs(a)) ) return s def _abssplit( self, prev: complex, prev_control: complex, t: float ) -> Tuple[Quadratic, Quadratic]: """Split this Quadratic and return two Quadratics using DeCasteljau's algorithm""" p1, p2 = self.ccontrol_points(0j, prev, prev_control) p1_1 = (1 - t) * prev + t * p1 p1_2 = (1 - t) * p1 + t * p2 p2_1 = (1 - t) * p1_1 + t * p1_2 return Quadratic(p1_1, p2_1), Quadratic(p1_2, p2) 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.arg2) class Quadratic(QuadraticMixin, AbsolutePathCommand): """Absolute Quadratic Curved Line segment""" letter = "Q" nargs = 4 arg1: complex """The (absolute) control point""" arg2: complex """The (absolute) end point""" @property def x2(self) -> float: """x coordinate of the (absolute) control point""" return self.arg1.real @property def y2(self) -> float: """y coordinate of the (absolute) control point""" return self.arg1.imag @property def x3(self) -> float: """x coordinate of the (absolute) end point""" return self.arg2.real @property def y3(self) -> float: """y coordinate of the (absolute) end point""" return self.arg2.imag @property def args(self): return self.x2, self.y2, self.x3, self.y3 @overload def __init__(self, x2: ComplexLike, x3: ComplexLike): ... @overload def __init__(self, x2: float, y2: float, x3: float, y3: float): ... def __init__(self, x2, y2, x3=None, y3=None): if x3 is not None: self.arg1 = x2 + y2 * 1j self.arg2 = x3 + y3 * 1j else: self.arg1, self.arg2 = complex(x2), complex(y2) def update_bounding_box(self, first, last_two_points, bbox): x1, x2, x3 = last_two_points[-1].real, self.x2, self.x3 y1, y2, y3 = last_two_points[-1].imag, self.y2, self.y3 if not (x1 in bbox.x and x2 in bbox.x and x3 in bbox.x): bbox.x += quadratic_extrema(x1, x2, x3) if not (y1 in bbox.y and y2 in bbox.y and y3 in bbox.y): bbox.y += quadratic_extrema(y1, y2, y3) def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: return (self.arg1, self.arg2) def to_relative(self, prev: ComplexLike) -> quadratic: return quadratic(self.arg1 - prev, self.arg2 - prev) def transform(self, transform: Transform) -> Quadratic: return Quadratic( 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 ccurve_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: pt1 = 1.0 / 3 * prev + 2.0 / 3 * self.arg1 pt2 = 2.0 / 3 * self.arg1 + 1.0 / 3 * self.arg2 return pt1, pt2, self.arg2 def reverse(self, first: ComplexLike, prev: ComplexLike) -> Quadratic: prev = complex(prev) return Quadratic(self.x2, self.y2, prev.real, prev.imag) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[Quadratic, Quadratic]: return self._abssplit(prev, prev_control, t) class quadratic(QuadraticMixin, RelativePathCommand): # pylint: disable=invalid-name """Relative quadratic line segment""" letter = "q" nargs = 4 arg1: complex """The (relative) control point""" arg2: complex """The (relative) end point""" @property def dx2(self) -> float: """x coordinate of the (relative) control point""" return self.arg1.real @property def dy2(self) -> float: """y coordinate of the (relative) control point""" return self.arg1.imag @property def dx3(self) -> float: """x coordinate of the (relative) end point""" return self.arg2.real @property def dy3(self) -> float: """y coordinate of the (relative) end point""" return self.arg2.imag @property def args(self): return self.dx2, self.dy2, self.dx3, self.dy3 @overload def __init__(self, dx2: ComplexLike, dx3: ComplexLike): ... @overload def __init__(self, dx2: float, dy2: float, dx3: float, dy3: float): ... def __init__(self, dx2, dy2, dx3=None, dy3=None): if dx3 is not None: self.arg1 = dx2 + dy2 * 1j self.arg2 = dx3 + dy3 * 1j else: self.arg1, self.arg2 = complex(dx2), complex(dy2) def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: return (self.arg1 + prev, self.arg2 + prev) def to_absolute(self, prev: ComplexLike) -> Quadratic: return Quadratic(self.arg1 + prev, self.arg2 + prev) def ccurve_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: pt1 = 1.0 / 3 * prev + 2.0 / 3 * (prev + self.arg1) pt2 = 2.0 / 3 * (prev + self.arg1) + 1.0 / 3 * (prev + self.arg2) return pt1, pt2, prev + self.arg2 def cend_point(self, first: complex, prev: complex) -> complex: return self.arg2 + prev def reverse(self, first: ComplexLike, prev: ComplexLike) -> quadratic: return quadratic(-self.arg2 + self.arg1, -self.arg2) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[quadratic, quadratic]: return self._relsplit(prev, prev_control, t) class TepidQuadratic(QuadraticMixin, AbsolutePathCommand): """Continued Quadratic Line segment""" letter = "T" nargs = 2 arg1: complex """The (absolute) control point""" @property def x3(self) -> float: """x coordinate of the (absolute) end point""" return self.arg1.real @property def y3(self) -> float: """y coordinate of the (absolute) end point""" return self.arg1.imag @property def args(self): return self.x3, self.y3 @overload def __init__(self, x3: ComplexLike): ... @overload def __init__(self, x3: float, y3: float): ... def __init__(self, x3, y3=None): if y3 is not None: self.arg1 = x3 + y3 * 1j else: self.arg1 = complex(x3) def update_bounding_box(self, first, last_two_points, bbox): self.to_quadratic(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, ...]: return (2 * prev - prev_prev, self.arg1) def to_non_shorthand( self, prev: ComplexLike, prev_control: ComplexLike ) -> Quadratic: return self.to_quadratic(prev, prev_control) def to_relative(self, prev: ComplexLike) -> tepidQuadratic: return tepidQuadratic(self.arg1 - prev) def transform(self, transform: Transform) -> TepidQuadratic: return TepidQuadratic(transform.capply_to_point(self.arg1)) def ccurve_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: qp1 = 2 * prev - prev_prev qp2 = self.arg1 pt1 = 1.0 / 3 * prev + 2.0 / 3 * qp1 pt2 = 2.0 / 3 * qp1 + 1.0 / 3 * qp2 return pt1, pt2, qp2 def cend_point(self, first: complex, prev: complex) -> complex: return self.arg1 def to_quadratic(self, prev: ComplexLike, prev_prev: ComplexLike) -> Quadratic: """Convert this continued quadratic into a full quadratic""" return Quadratic( *self.ccontrol_points(complex(prev), complex(prev), complex(prev_prev)) ) def reverse(self, first: ComplexLike, prev: ComplexLike) -> TepidQuadratic: return TepidQuadratic(prev) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[Quadratic, Quadratic]: return self._abssplit(prev, prev_control, t) class tepidQuadratic(QuadraticMixin, RelativePathCommand): # pylint: disable=invalid-name """Relative continued quadratic line segment""" letter = "t" nargs = 2 arg1: complex """The (relative) control point""" @property def dx3(self) -> float: """x coordinate of the (relative) end point""" return self.arg1.real @property def dy3(self) -> float: """y coordinate of the (relative) end point""" return self.arg1.imag @property def args(self): return self.dx3, self.dy3 @overload def __init__(self, dx3: ComplexLike): ... @overload def __init__(self, dx3: float, dy3: float): ... def __init__(self, dx3, dy3=None): if dy3 is not None: self.arg1 = dx3 + dy3 * 1j else: self.arg1 = complex(dx3) def ccontrol_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: return (2 * prev - prev_prev, self.arg1 + prev) def ccurve_points( self, first: complex, prev: complex, prev_prev: complex ) -> Tuple[complex, ...]: qp1 = 2 * prev - prev_prev qp2 = self.arg1 + prev pt1 = 1.0 / 3 * prev + 2.0 / 3 * qp1 pt2 = 2.0 / 3 * qp1 + 1.0 / 3 * qp2 return pt1, pt2, qp2 def to_absolute(self, prev: ComplexLike) -> TepidQuadratic: return TepidQuadratic(self.arg1 + prev) def to_non_shorthand( self, prev: ComplexLike, prev_control: ComplexLike ) -> Quadratic: return self.to_absolute(prev).to_non_shorthand(prev, prev_control) def cend_point(self, first: complex, prev: complex) -> complex: return self.arg1 + prev def reverse(self, first: ComplexLike, prev: ComplexLike) -> tepidQuadratic: return tepidQuadratic(-self.arg1) def _split( self, first: complex, prev: complex, prev_control: complex, t: float ) -> Tuple[quadratic, quadratic]: return self._relsplit(prev, prev_control, t)