bundle: update (2026-01-18)
This commit is contained in:
456
extensions/botbox3000/deps/inkex/paths/quadratic.py
Normal file
456
extensions/botbox3000/deps/inkex/paths/quadratic.py
Normal file
@@ -0,0 +1,456 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user