457 lines
14 KiB
Python
457 lines
14 KiB
Python
# coding=utf-8
|
|
#
|
|
# Copyright (C) 2018 Martin Owens <doctormo@gmail.com>
|
|
# Copyright (C) 2023 Jonathan Neuhauser <jonathan.neuhauser@outlook.com>
|
|
#
|
|
# 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)
|