bundle: update (2026-01-16)
This commit is contained in:
122
extensions/botbox3000/deps/inkex/paths/__init__.py
Normal file
122
extensions/botbox3000/deps/inkex/paths/__init__.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# 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.
|
||||
#
|
||||
"""Paths module.
|
||||
|
||||
Most of the functions derivative, unit_tangent, curvature, point, split, length, ilength
|
||||
for the individual path commands are ported from
|
||||
https://github.com/mathandy/svgpathtools/ (MIT licensed)
|
||||
"""
|
||||
|
||||
from typing import Union
|
||||
|
||||
from ..transforms import ComplexLike
|
||||
from .interfaces import (
|
||||
PathCommand,
|
||||
AbsolutePathCommand,
|
||||
RelativePathCommand,
|
||||
LengthSettings,
|
||||
ILengthSettings,
|
||||
)
|
||||
from .lines import Line, line, Move, move, ZoneClose, zoneClose, Horz, horz, Vert, vert
|
||||
from .curves import curve, Curve, smooth, Smooth
|
||||
from .quadratic import quadratic, Quadratic, tepidQuadratic, TepidQuadratic
|
||||
from .arc import Arc, arc, arc_to_path, matprod, rotmat, applymat, norm
|
||||
from .path import CubicSuperPath, Path, InvalidPath
|
||||
|
||||
import numpy as np
|
||||
|
||||
np.seterr(invalid="raise")
|
||||
|
||||
|
||||
# definitions that can't be inside the class due to circular dependencies
|
||||
def to_curve(self, prev: ComplexLike, prev_prev: ComplexLike = 0) -> Curve:
|
||||
"""Convert command to :py:class:`Curve`
|
||||
|
||||
Curve().to_curve() returns a copy
|
||||
"""
|
||||
return Curve(*self.ccurve_points(0 + 0j, complex(prev), complex(prev_prev)))
|
||||
|
||||
|
||||
def to_line(self, prev: ComplexLike) -> Line:
|
||||
"""Converts this segment to a line (copies if already a line)"""
|
||||
return Line(self.cend_point(0, complex(prev)))
|
||||
|
||||
|
||||
PathCommand.to_curve = to_curve # type: ignore
|
||||
PathCommand.to_line = to_line # type: ignore
|
||||
|
||||
PathCommand._letter_to_class = { # pylint: disable=protected-access
|
||||
"M": Move,
|
||||
"L": Line,
|
||||
"V": Vert,
|
||||
"H": Horz,
|
||||
"A": Arc,
|
||||
"C": Curve,
|
||||
"S": Smooth,
|
||||
"Z": ZoneClose,
|
||||
"Q": Quadratic,
|
||||
"T": TepidQuadratic,
|
||||
"m": move,
|
||||
"l": line,
|
||||
"v": vert,
|
||||
"h": horz,
|
||||
"a": arc,
|
||||
"c": curve,
|
||||
"s": smooth,
|
||||
"z": zoneClose,
|
||||
"q": quadratic,
|
||||
"t": tepidQuadratic,
|
||||
}
|
||||
|
||||
|
||||
# All the names that get added to the inkex API itself.
|
||||
__all__ = (
|
||||
"Path",
|
||||
"CubicSuperPath",
|
||||
"PathCommand",
|
||||
"AbsolutePathCommand",
|
||||
"RelativePathCommand",
|
||||
# Path commands:
|
||||
"Line",
|
||||
"line",
|
||||
"Move",
|
||||
"move",
|
||||
"ZoneClose",
|
||||
"zoneClose",
|
||||
"Horz",
|
||||
"horz",
|
||||
"Vert",
|
||||
"vert",
|
||||
"Curve",
|
||||
"curve",
|
||||
"Smooth",
|
||||
"smooth",
|
||||
"Quadratic",
|
||||
"quadratic",
|
||||
"TepidQuadratic",
|
||||
"tepidQuadratic",
|
||||
"Arc",
|
||||
"arc",
|
||||
# errors
|
||||
"InvalidPath",
|
||||
# structs
|
||||
"LengthSettings",
|
||||
"ILengthSettings",
|
||||
)
|
||||
670
extensions/botbox3000/deps/inkex/paths/arc.py
Normal file
670
extensions/botbox3000/deps/inkex/paths/arc.py
Normal file
@@ -0,0 +1,670 @@
|
||||
# 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.
|
||||
#
|
||||
"""Arc path commands"""
|
||||
|
||||
from __future__ import annotations
|
||||
from math import atan2, pi, sqrt, sin, cos, tan, acos, radians, degrees
|
||||
from cmath import exp
|
||||
from typing import overload, Tuple, List, Union, TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..transforms import Transform, Vector2d, ComplexLike
|
||||
|
||||
from .interfaces import (
|
||||
AbsolutePathCommand,
|
||||
RelativePathCommand,
|
||||
LengthSettings,
|
||||
ILengthSettings,
|
||||
BezierArcComputationMixin,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .curves import Curve
|
||||
|
||||
|
||||
class Arc(BezierArcComputationMixin, AbsolutePathCommand):
|
||||
"""Special Arc segment"""
|
||||
|
||||
letter = "A"
|
||||
|
||||
nargs = 7
|
||||
|
||||
radius: complex
|
||||
"""Radius of the Arc"""
|
||||
x_axis_rotation: float
|
||||
|
||||
large_arc: bool
|
||||
sweep: bool
|
||||
|
||||
endpoint: complex
|
||||
"""Endpoint (absolute) of the Arc"""
|
||||
|
||||
@property
|
||||
def rx(self) -> float:
|
||||
"""x radius of the Arc"""
|
||||
return self.radius.real
|
||||
|
||||
@property
|
||||
def ry(self) -> float:
|
||||
"""y radius of the Arc"""
|
||||
return self.radius.imag
|
||||
|
||||
@property
|
||||
def x(self) -> float:
|
||||
"""x coordinate of the (absolute) endpoint of the Arc"""
|
||||
return self.endpoint.real
|
||||
|
||||
@property
|
||||
def y(self) -> float:
|
||||
"""x coordinate of the (relative) endpoint of the Arc"""
|
||||
return self.endpoint.imag
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
return (
|
||||
self.rx,
|
||||
self.ry,
|
||||
self.x_axis_rotation,
|
||||
self.large_arc,
|
||||
self.sweep,
|
||||
self.x,
|
||||
self.y,
|
||||
)
|
||||
|
||||
@property
|
||||
def cargs(self):
|
||||
"""Set of arguments in complex form"""
|
||||
return (
|
||||
self.radius,
|
||||
self.x_axis_rotation,
|
||||
self.large_arc,
|
||||
self.sweep,
|
||||
self.endpoint,
|
||||
)
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
radius: ComplexLike,
|
||||
x_axis_rotation: float,
|
||||
large_arc: bool | int,
|
||||
sweep: bool | int,
|
||||
endpoint: ComplexLike,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
rx: float,
|
||||
ry: float,
|
||||
x_axis_rotation: float,
|
||||
large_arc: bool | int,
|
||||
sweep: bool | int,
|
||||
x: float,
|
||||
y: float,
|
||||
) -> None: ... # pylint: disable=too-many-arguments
|
||||
|
||||
def __init__(self, *args):
|
||||
if len(args) == 5:
|
||||
(
|
||||
self.radius,
|
||||
self.x_axis_rotation,
|
||||
self.large_arc,
|
||||
self.sweep,
|
||||
self.endpoint,
|
||||
) = args
|
||||
self.radius = complex(self.radius)
|
||||
self.endpoint = complex(self.endpoint)
|
||||
elif len(args) == 7:
|
||||
self.radius = args[0] + args[1] * 1j
|
||||
self.x_axis_rotation, self.large_arc, self.sweep = args[2:5]
|
||||
self.endpoint = args[5] + args[6] * 1j
|
||||
|
||||
def parametrize(self, prev):
|
||||
"""Return the parametrisation of the arc:
|
||||
(radius, phi, rot_matrix, center, theta1, deltatheta)
|
||||
See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
# my notation roughly follows theirs
|
||||
phi = radians(self.x_axis_rotation)
|
||||
rot_matrix = exp(1j * phi)
|
||||
|
||||
radius = self.radius
|
||||
rx = self.radius.real
|
||||
ry = self.radius.imag
|
||||
rx_sqd = rx * rx
|
||||
ry_sqd = ry * ry
|
||||
|
||||
# Transform z-> z' = x' + 1j*y'
|
||||
# = self.rot_matrix**(-1)*(z - (end+start)/2)
|
||||
# coordinates. This translates the ellipse so that the midpoint
|
||||
# between self.end and self.start lies on the origin and rotates
|
||||
# the ellipse so that the its axes align with the xy-coordinate axes.
|
||||
# Note: This sends self.end to -self.start
|
||||
zp1 = (1 / rot_matrix) * (prev - self.cend_point(0j, prev)) / 2
|
||||
x1p, y1p = zp1.real, zp1.imag
|
||||
x1p_sqd = x1p * x1p
|
||||
y1p_sqd = y1p * y1p
|
||||
|
||||
# Correct out of range radii
|
||||
radius_check = (x1p_sqd / rx_sqd) + (y1p_sqd / ry_sqd)
|
||||
if radius_check > 1:
|
||||
rx *= sqrt(radius_check)
|
||||
ry *= sqrt(radius_check)
|
||||
radius = rx + 1j * ry
|
||||
rx_sqd = rx * rx
|
||||
ry_sqd = ry * ry
|
||||
|
||||
# Compute c'=(c_x', c_y'), the center of the ellipse in (x', y') coords
|
||||
# Noting that, in our new coord system, (x_2', y_2') = (-x_1', -x_2')
|
||||
# and our ellipse is cut out by of the plane by the algebraic equation
|
||||
# (x'-c_x')**2 / r_x**2 + (y'-c_y')**2 / r_y**2 = 1,
|
||||
# we can find c' by solving the system of two quadratics given by
|
||||
# plugging our transformed endpoints (x_1', y_1') and (x_2', y_2')
|
||||
tmp = rx_sqd * y1p_sqd + ry_sqd * x1p_sqd
|
||||
radicand = (rx_sqd * ry_sqd - tmp) / tmp
|
||||
radical = 0 if np.isclose(radicand, 0) else sqrt(radicand)
|
||||
|
||||
if self.large_arc == self.sweep:
|
||||
cp = -radical * (rx * y1p / ry - 1j * ry * x1p / rx)
|
||||
else:
|
||||
cp = radical * (rx * y1p / ry - 1j * ry * x1p / rx)
|
||||
|
||||
# The center in (x,y) coordinates is easy to find knowing c'
|
||||
center = exp(1j * phi) * cp + (prev + self.cend_point(0j, prev)) / 2
|
||||
|
||||
# Now we do a second transformation, from (x', y') to (u_x, u_y)
|
||||
# coordinates, which is a translation moving the center of the
|
||||
# ellipse to the origin and a dilation stretching the ellipse to be
|
||||
# the unit circle
|
||||
u1 = (x1p - cp.real) / rx + 1j * (y1p - cp.imag) / ry # transformed start
|
||||
u2 = (-x1p - cp.real) / rx + 1j * (-y1p - cp.imag) / ry # transformed end
|
||||
|
||||
# clip in case of floating point error
|
||||
u1 = np.clip(u1.real, -1, 1) + 1j * np.clip(u1.imag, -1, 1)
|
||||
u2 = np.clip(u2.real, -1, 1) + 1j * np.clip(u2.imag, -1, 1)
|
||||
|
||||
# Now compute theta and delta (we'll define them as we go)
|
||||
# delta is the angular distance of the arc (w.r.t the circle)
|
||||
# theta is the angle between the positive x'-axis and the start point
|
||||
# on the circle
|
||||
if u1.imag > 0:
|
||||
theta1 = degrees(acos(u1.real))
|
||||
elif u1.imag < 0:
|
||||
theta1 = -degrees(acos(u1.real))
|
||||
else:
|
||||
if u1.real > 0: # start is on pos u_x axis
|
||||
theta1 = 0
|
||||
else: # start is on neg u_x axis
|
||||
# Note: This behavior disagrees with behavior documented in
|
||||
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
# where theta is set to 0 in this case.
|
||||
theta1 = 180
|
||||
|
||||
det_uv = u1.real * u2.imag - u1.imag * u2.real
|
||||
|
||||
acosand = u1.real * u2.real + u1.imag * u2.imag
|
||||
acosand = np.clip(acosand.real, -1, 1) + np.clip(acosand.imag, -1, 1)
|
||||
|
||||
if det_uv > 0:
|
||||
deltatheta = degrees(acos(acosand))
|
||||
elif det_uv < 0:
|
||||
deltatheta = -degrees(acos(acosand))
|
||||
else:
|
||||
if u1.real * u2.real + u1.imag * u2.imag > 0:
|
||||
# u1 == u2
|
||||
deltatheta = 0
|
||||
else:
|
||||
# u1 == -u2
|
||||
# Note: This behavior disagrees with behavior documented in
|
||||
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
# where deltatheta is set to 0 in this case.
|
||||
deltatheta = 180
|
||||
|
||||
if not self.sweep and deltatheta >= 0:
|
||||
deltatheta -= 360
|
||||
elif self.large_arc and deltatheta <= 0:
|
||||
deltatheta += 360
|
||||
|
||||
return radius, phi, rot_matrix, center, theta1, deltatheta
|
||||
|
||||
def update_bounding_box(self, first, last_two_points, bbox):
|
||||
prev = last_two_points[-1]
|
||||
for seg in self.to_curves(prev=prev):
|
||||
seg.update_bounding_box(first, [None, prev], bbox)
|
||||
prev = seg.cend_point(first, prev)
|
||||
|
||||
def ccontrol_points(
|
||||
self, first: complex, prev: complex, prev_prev: complex
|
||||
) -> Tuple[complex, ...]:
|
||||
return (self.endpoint,)
|
||||
|
||||
def ccurve_points(
|
||||
self, first: complex, prev: complex, prev_prev: complex
|
||||
) -> Tuple[complex, ...]:
|
||||
return NotImplemented
|
||||
|
||||
def to_curves(self, prev: ComplexLike, prev_prev: ComplexLike = 0j) -> List[Curve]:
|
||||
"""Convert this arc into bezier curves"""
|
||||
# TODO Refactor out CubicSuperPath
|
||||
from .path import CubicSuperPath
|
||||
|
||||
path = CubicSuperPath(
|
||||
[arc_to_path(Vector2d.c2t(complex(prev)), self.args)]
|
||||
).to_path(curves_only=True)
|
||||
# Ignore the first move command from to_path()
|
||||
return list(path)[1:]
|
||||
|
||||
def transform(self, transform: Transform) -> Arc:
|
||||
# pylint: disable=invalid-name, too-many-locals
|
||||
newend = transform.capply_to_point(self.endpoint)
|
||||
|
||||
T: Transform = transform
|
||||
if self.x_axis_rotation != 0:
|
||||
T = T @ Transform(rotate=self.x_axis_rotation)
|
||||
a, c, b, d, _, _ = list(T.to_hexad())
|
||||
# T = | a b |
|
||||
# | c d |
|
||||
|
||||
detT = a * d - b * c
|
||||
detT2 = detT**2
|
||||
|
||||
rx = float(self.rx)
|
||||
ry = float(self.ry)
|
||||
|
||||
def get_degen():
|
||||
return Arc(
|
||||
self.radius,
|
||||
self.x_axis_rotation,
|
||||
self.large_arc,
|
||||
self.sweep,
|
||||
newend,
|
||||
)
|
||||
|
||||
if rx == 0.0 or ry == 0.0 or detT2 == 0.0:
|
||||
# degenerate arc
|
||||
# transform only last point
|
||||
return get_degen()
|
||||
A = (d**2 / rx**2 + c**2 / ry**2) / detT2
|
||||
B = -(d * b / rx**2 + c * a / ry**2) / detT2
|
||||
D = (b**2 / rx**2 + a**2 / ry**2) / detT2
|
||||
|
||||
theta = atan2(-2 * B, D - A) / 2
|
||||
theta_deg = theta * 180.0 / pi
|
||||
DA = D - A
|
||||
l2 = 4 * B**2 + DA**2
|
||||
|
||||
if l2 == 0:
|
||||
delta = 0.0
|
||||
else:
|
||||
delta = 0.5 * (-(DA**2) - 4 * B**2) / sqrt(l2)
|
||||
|
||||
half = (A + D) / 2
|
||||
|
||||
try:
|
||||
rx_ = 1.0 / sqrt(half + delta)
|
||||
ry_ = 1.0 / sqrt(half - delta)
|
||||
|
||||
if detT > 0:
|
||||
sweep = self.sweep
|
||||
else:
|
||||
sweep = not self.sweep > 0
|
||||
return Arc(rx_ + 1j * ry_, theta_deg, self.large_arc, sweep, newend)
|
||||
except ZeroDivisionError:
|
||||
return get_degen()
|
||||
|
||||
def to_relative(self, prev: ComplexLike) -> RelativePathCommand:
|
||||
return arc(
|
||||
self.radius,
|
||||
self.x_axis_rotation,
|
||||
self.large_arc,
|
||||
self.sweep,
|
||||
self.endpoint - prev,
|
||||
)
|
||||
|
||||
def cend_point(self, first: complex, prev: complex) -> complex:
|
||||
return self.endpoint
|
||||
|
||||
def reverse(self, first: ComplexLike, prev: ComplexLike) -> Arc:
|
||||
return Arc(
|
||||
self.radius, self.x_axis_rotation, self.large_arc, not self.sweep, prev
|
||||
)
|
||||
|
||||
def _cpoint(
|
||||
self, first: complex, prev: complex, prev_control: complex, t: float
|
||||
) -> complex:
|
||||
# TODO surely this can be expressed in a shorter way using complex geometry?
|
||||
radius, _, rot_matrix, center, theta1, deltatheta = self.parametrize(prev)
|
||||
angle = (theta1 + t * deltatheta) * pi / 180
|
||||
cosphi = rot_matrix.real
|
||||
sinphi = rot_matrix.imag
|
||||
rx = radius.real
|
||||
ry = radius.imag
|
||||
|
||||
x = rx * cosphi * cos(angle) - ry * sinphi * sin(angle) + center.real
|
||||
y = rx * sinphi * cos(angle) + ry * cosphi * sin(angle) + center.imag
|
||||
return x + y * 1j
|
||||
|
||||
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."""
|
||||
radius, phi, _, _, theta1, deltatheta = self.parametrize(prev)
|
||||
angle = radians(theta1 + t * deltatheta)
|
||||
rx = radius.real
|
||||
ry = radius.imag
|
||||
k = (deltatheta * pi / 180) ** n # ((d/dt)angle)**n
|
||||
|
||||
if n % 4 == 0 and n > 0:
|
||||
return (
|
||||
rx * cos(phi) * cos(angle)
|
||||
- ry * sin(phi) * sin(angle)
|
||||
+ 1j * (rx * sin(phi) * cos(angle) + ry * cos(phi) * sin(angle))
|
||||
)
|
||||
elif n % 4 == 1:
|
||||
return k * (
|
||||
-rx * cos(phi) * sin(angle)
|
||||
- ry * sin(phi) * cos(angle)
|
||||
+ 1j * (-rx * sin(phi) * sin(angle) + ry * cos(phi) * cos(angle))
|
||||
)
|
||||
elif n % 4 == 2:
|
||||
return k * (
|
||||
-rx * cos(phi) * cos(angle)
|
||||
+ ry * sin(phi) * sin(angle)
|
||||
+ 1j * (-rx * sin(phi) * cos(angle) - ry * cos(phi) * sin(angle))
|
||||
)
|
||||
elif n % 4 == 3:
|
||||
return k * (
|
||||
rx * cos(phi) * sin(angle)
|
||||
+ ry * sin(phi) * cos(angle)
|
||||
+ 1j * (rx * sin(phi) * sin(angle) - ry * cos(phi) * cos(angle))
|
||||
)
|
||||
else:
|
||||
raise ValueError("n should be a positive integer.")
|
||||
|
||||
def _split(
|
||||
self, first: complex, prev: complex, prev_control: complex, t: float
|
||||
) -> Tuple[Arc, Arc]:
|
||||
"""returns two segments, whose union is this segment and which join
|
||||
at self.point(t)."""
|
||||
radius, _, _, _, _, deltatheta = self.parametrize(prev)
|
||||
|
||||
def crop(t0, t1):
|
||||
return Arc(
|
||||
radius,
|
||||
self.x_axis_rotation,
|
||||
not abs(deltatheta * (t1 - t0)) <= 180,
|
||||
self.sweep,
|
||||
self.cpoint(0j, prev, 0j, t1),
|
||||
)
|
||||
|
||||
return crop(0, t), crop(t, 1)
|
||||
|
||||
def _ilength(
|
||||
self,
|
||||
first: complex,
|
||||
prev: complex,
|
||||
prev_control: complex,
|
||||
length: float,
|
||||
settings: ILengthSettings = ILengthSettings(),
|
||||
):
|
||||
# ilength calls self.parametrize very often, so we cache the result
|
||||
param = self.parametrize
|
||||
params = param(prev)
|
||||
|
||||
def cached(prev): # pylint: disable=unused-argument
|
||||
return params
|
||||
|
||||
self.parametrize = cached # type: ignore
|
||||
try:
|
||||
return super()._ilength(first, prev, prev_control, length, settings)
|
||||
finally:
|
||||
self.parametrize = param # type: ignore
|
||||
|
||||
|
||||
class arc(RelativePathCommand, Arc): # pylint: disable=invalid-name
|
||||
"""Relative Arc line segment"""
|
||||
|
||||
letter = "a"
|
||||
|
||||
nargs = 7
|
||||
|
||||
endpoint: complex
|
||||
"""Endpoint (relative) of the arc"""
|
||||
|
||||
@property
|
||||
def rx(self) -> float:
|
||||
"""x radius of the arc"""
|
||||
return self.radius.real
|
||||
|
||||
@property
|
||||
def ry(self) -> float:
|
||||
"""y radius of the arc"""
|
||||
return self.radius.imag
|
||||
|
||||
@property
|
||||
def dx(self) -> float:
|
||||
"""x coordinate of the (relative) endpoint of the arc"""
|
||||
return self.endpoint.real
|
||||
|
||||
@property
|
||||
def dy(self) -> float:
|
||||
"""x coordinate of the (relative) endpoint of the arc"""
|
||||
return self.endpoint.imag
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
return (
|
||||
self.rx,
|
||||
self.ry,
|
||||
self.x_axis_rotation,
|
||||
self.large_arc,
|
||||
self.sweep,
|
||||
self.dx,
|
||||
self.dy,
|
||||
)
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
radius: ComplexLike,
|
||||
x_axis_rotation: float,
|
||||
large_arc: bool,
|
||||
sweep: bool,
|
||||
endpoint: ComplexLike,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
rx: float,
|
||||
ry: float,
|
||||
x_axis_rotation: float,
|
||||
large_arc: bool,
|
||||
sweep: bool,
|
||||
dx: float,
|
||||
dy: float,
|
||||
) -> None: ... # pylint: disable=too-many-arguments
|
||||
|
||||
def __init__(self, *args):
|
||||
if len(args) == 5:
|
||||
(
|
||||
self.radius,
|
||||
self.x_axis_rotation,
|
||||
self.large_arc,
|
||||
self.sweep,
|
||||
self.endpoint,
|
||||
) = args
|
||||
self.radius = complex(self.radius)
|
||||
self.endpoint = complex(self.endpoint)
|
||||
elif len(args) == 7:
|
||||
self.radius = args[0] + args[1] * 1j
|
||||
self.x_axis_rotation, self.large_arc, self.sweep = args[2:5]
|
||||
self.endpoint = args[5] + args[6] * 1j
|
||||
|
||||
def to_absolute(self, prev: ComplexLike) -> Arc:
|
||||
return Arc(
|
||||
self.radius,
|
||||
self.x_axis_rotation,
|
||||
self.large_arc,
|
||||
self.sweep,
|
||||
self.endpoint + prev,
|
||||
)
|
||||
|
||||
def cend_point(self, first: complex, prev: complex) -> complex:
|
||||
return self.endpoint + prev
|
||||
|
||||
def ccontrol_points(
|
||||
self, first: complex, prev: complex, prev_prev: complex
|
||||
) -> Tuple[complex, ...]:
|
||||
return (self.endpoint + prev,)
|
||||
|
||||
def ccurve_points(
|
||||
self, first: complex, prev: complex, prev_prev: complex
|
||||
) -> Tuple[complex, ...]:
|
||||
return NotImplemented
|
||||
|
||||
def reverse(self, first: ComplexLike, prev: ComplexLike) -> arc:
|
||||
return arc(
|
||||
self.radius,
|
||||
self.x_axis_rotation,
|
||||
self.large_arc,
|
||||
not self.sweep,
|
||||
-self.endpoint,
|
||||
)
|
||||
|
||||
def to_curves(self, prev: ComplexLike, prev_prev: ComplexLike = 0j) -> List[Curve]:
|
||||
return self.to_absolute(prev).to_curves(prev, prev_prev)
|
||||
|
||||
|
||||
def arc_to_path(point, params):
|
||||
"""Approximates an arc with cubic bezier segments.
|
||||
|
||||
Arguments:
|
||||
point: Starting point (absolute coords)
|
||||
params: Arcs parameters as per
|
||||
https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
|
||||
|
||||
Returns a list of triplets of points :
|
||||
[control_point_before, node, control_point_after]
|
||||
(first and last returned triplets are [p1, p1, *] and [*, p2, p2])
|
||||
"""
|
||||
|
||||
# pylint: disable=invalid-name, too-many-locals
|
||||
A = point[:]
|
||||
rx, ry, teta, longflag, sweepflag, x2, y2 = params[:]
|
||||
teta = teta * pi / 180.0
|
||||
B = [x2, y2]
|
||||
# Degenerate ellipse
|
||||
if rx == 0 or ry == 0 or A == B:
|
||||
return [[A[:], A[:], A[:]], [B[:], B[:], B[:]]]
|
||||
|
||||
# turn coordinates so that the ellipse morph into a *unit circle* (not 0-centered)
|
||||
mat = matprod((rotmat(teta), [[1.0 / rx, 0.0], [0.0, 1.0 / ry]], rotmat(-teta)))
|
||||
applymat(mat, A)
|
||||
applymat(mat, B)
|
||||
|
||||
k = [-(B[1] - A[1]), B[0] - A[0]]
|
||||
d = k[0] * k[0] + k[1] * k[1]
|
||||
k[0] /= sqrt(d)
|
||||
k[1] /= sqrt(d)
|
||||
d = sqrt(max(0, 1 - d / 4.0))
|
||||
# k is the unit normal to AB vector, pointing to center O
|
||||
# d is distance from center to AB segment (distance from O to the midpoint of AB)
|
||||
# for the last line, remember this is a unit circle, and kd vector is ortogonal to
|
||||
# AB (Pythagorean thm)
|
||||
|
||||
if longflag == sweepflag:
|
||||
# top-right ellipse in SVG example
|
||||
# https://www.w3.org/TR/SVG/images/paths/arcs02.svg
|
||||
d *= -1
|
||||
|
||||
O = [(B[0] + A[0]) / 2.0 + d * k[0], (B[1] + A[1]) / 2.0 + d * k[1]]
|
||||
OA = [A[0] - O[0], A[1] - O[1]]
|
||||
OB = [B[0] - O[0], B[1] - O[1]]
|
||||
start = acos(OA[0] / norm(OA))
|
||||
if OA[1] < 0:
|
||||
start *= -1
|
||||
end = acos(OB[0] / norm(OB))
|
||||
if OB[1] < 0:
|
||||
end *= -1
|
||||
# start and end are the angles from center of the circle to A and to B respectively
|
||||
|
||||
if sweepflag and start > end:
|
||||
end += 2 * pi
|
||||
if (not sweepflag) and start < end:
|
||||
end -= 2 * pi
|
||||
|
||||
NbSectors = int(abs(start - end) * 2 / pi) + 1
|
||||
dTeta = (end - start) / NbSectors
|
||||
v = 4 * tan(dTeta / 4.0) / 3.0
|
||||
# I would use v = tan(dTeta/2)*4*(sqrt(2)-1)/3 ?
|
||||
p = []
|
||||
for i in range(0, NbSectors + 1, 1):
|
||||
angle = start + i * dTeta
|
||||
v1 = [
|
||||
O[0] + cos(angle) - (-v) * sin(angle),
|
||||
O[1] + sin(angle) + (-v) * cos(angle),
|
||||
]
|
||||
pt = [O[0] + cos(angle), O[1] + sin(angle)]
|
||||
v2 = [O[0] + cos(angle) - v * sin(angle), O[1] + sin(angle) + v * cos(angle)]
|
||||
p.append([v1, pt, v2])
|
||||
p[0][0] = p[0][1][:]
|
||||
p[-1][2] = p[-1][1][:]
|
||||
|
||||
# go back to the original coordinate system
|
||||
mat = matprod((rotmat(teta), [[rx, 0], [0, ry]], rotmat(-teta)))
|
||||
for pts in p:
|
||||
applymat(mat, pts[0])
|
||||
applymat(mat, pts[1])
|
||||
applymat(mat, pts[2])
|
||||
return p
|
||||
|
||||
|
||||
def matprod(mlist):
|
||||
"""Get the product of the mat"""
|
||||
prod = mlist[0]
|
||||
for mat in mlist[1:]:
|
||||
a00 = prod[0][0] * mat[0][0] + prod[0][1] * mat[1][0]
|
||||
a01 = prod[0][0] * mat[0][1] + prod[0][1] * mat[1][1]
|
||||
a10 = prod[1][0] * mat[0][0] + prod[1][1] * mat[1][0]
|
||||
a11 = prod[1][0] * mat[0][1] + prod[1][1] * mat[1][1]
|
||||
prod = [[a00, a01], [a10, a11]]
|
||||
return prod
|
||||
|
||||
|
||||
def rotmat(teta):
|
||||
"""Rotate the mat"""
|
||||
return [[cos(teta), -sin(teta)], [sin(teta), cos(teta)]]
|
||||
|
||||
|
||||
def applymat(mat, point):
|
||||
"""Apply the given mat"""
|
||||
x = mat[0][0] * point[0] + mat[0][1] * point[1]
|
||||
y = mat[1][0] * point[0] + mat[1][1] * point[1]
|
||||
point[0] = x
|
||||
point[1] = y
|
||||
|
||||
|
||||
def norm(point):
|
||||
"""Normalise"""
|
||||
return sqrt(point[0] * point[0] + point[1] * point[1])
|
||||
499
extensions/botbox3000/deps/inkex/paths/curves.py
Normal file
499
extensions/botbox3000/deps/inkex/paths/curves.py
Normal file
@@ -0,0 +1,499 @@
|
||||
# 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.
|
||||
#
|
||||
"""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)
|
||||
731
extensions/botbox3000/deps/inkex/paths/interfaces.py
Normal file
731
extensions/botbox3000/deps/inkex/paths/interfaces.py
Normal file
@@ -0,0 +1,731 @@
|
||||
# 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.
|
||||
#
|
||||
"""Interfaces for path commands"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import math
|
||||
from typing import (
|
||||
List,
|
||||
Any,
|
||||
Tuple,
|
||||
Dict,
|
||||
Type,
|
||||
Generator,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
Optional,
|
||||
Callable,
|
||||
)
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
from ..utils import classproperty, rational_limit
|
||||
from ..transforms import Vector2d, BoundingBox, Transform, ComplexLike
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .curves import Curve
|
||||
from .lines import Line
|
||||
|
||||
|
||||
@dataclass
|
||||
class LengthSettings:
|
||||
"""Settings for :func:`PathCommand.length`
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
|
||||
min_depth: int = 5
|
||||
error: float = 1e-5
|
||||
|
||||
|
||||
@dataclass
|
||||
class ILengthSettings:
|
||||
"""Settings for :func:`PathCommand.ilength`
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
|
||||
min_depth: int = 5
|
||||
|
||||
error: float = 1e-5
|
||||
"""
|
||||
Error tolerance for the computations of the test segment that is performed
|
||||
for each iteration.
|
||||
|
||||
The defaults from svgpathtools are ILENGTH_ERROR=ILENGTH_LENGTH_TOL=1e-12.
|
||||
This is rather slow, particularly _length (which then subdivides the path into
|
||||
2^12 or more segments and adds up the length). For visual editing, this is rather
|
||||
irrelevant (and a lot more accurate than the previous methods)."""
|
||||
|
||||
length_tol: float = 1e-5
|
||||
"""Total (absolute) tolerance of the resulting s value."""
|
||||
|
||||
maxits: int = 10000
|
||||
|
||||
|
||||
class PathCommand(abc.ABC):
|
||||
"""
|
||||
Base class of all path commands
|
||||
"""
|
||||
|
||||
letter = ""
|
||||
# Number of arguments that follow this path commands letter
|
||||
nargs = -1
|
||||
|
||||
@classproperty # From python 3.9 on, just combine @classmethod and @property
|
||||
def name(cls): # pylint: disable=no-self-argument
|
||||
"""The full name of the segment (i.e. Line, Arc, etc)"""
|
||||
return cls.__name__ # pylint: disable=no-member
|
||||
|
||||
@classproperty
|
||||
def next_command(self):
|
||||
"""The implicit next command. This is for automatic chains where the next
|
||||
command isn't given, just a bunch on numbers which we automatically parse."""
|
||||
return self
|
||||
|
||||
@property
|
||||
def is_relative(self) -> bool:
|
||||
"""Whether the command is defined in relative coordinates, i.e. relative to
|
||||
the previous endpoint (lower case path command letter)"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_absolute(self) -> bool:
|
||||
"""Whether the command is defined in absolute coordinates (upper case path
|
||||
command letter)"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_relative(self, prev: ComplexLike) -> RelativePathCommand:
|
||||
"""Return absolute counterpart for absolute commands or copy for relative"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_absolute(self, prev: ComplexLike) -> AbsolutePathCommand:
|
||||
"""Return relative counterpart for relative commands or copy for absolute"""
|
||||
raise NotImplementedError
|
||||
|
||||
def reverse(self, first: ComplexLike, prev: ComplexLike) -> PathCommand:
|
||||
"""Reverse path command
|
||||
|
||||
.. versionadded:: 1.1"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_non_shorthand(
|
||||
self,
|
||||
prev: ComplexLike,
|
||||
prev_control: ComplexLike, # pylint: disable=unused-argument
|
||||
) -> AbsolutePathCommand:
|
||||
"""Return an absolute non-shorthand command
|
||||
|
||||
.. versionadded:: 1.1"""
|
||||
return self.to_absolute(prev)
|
||||
|
||||
# The precision of the numbers when converting to string
|
||||
number_template = "{:.6g}"
|
||||
|
||||
# Maps single letter path command to corresponding class
|
||||
# (filled at the bottom of file, when all classes already defined)
|
||||
_letter_to_class: Dict[str, Type[Any]] = {}
|
||||
|
||||
@staticmethod
|
||||
def letter_to_class(letter):
|
||||
"""Returns class for given path command letter"""
|
||||
return PathCommand._letter_to_class[letter]
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def args(self) -> List[float]:
|
||||
"""Returns path command arguments as tuple of floats"""
|
||||
|
||||
def control_points(
|
||||
self,
|
||||
first: ComplexLike,
|
||||
prev: ComplexLike,
|
||||
prev_prev: ComplexLike,
|
||||
) -> Generator[Vector2d, None, None]:
|
||||
"""Returns list of path command control points"""
|
||||
first, prev, prev_prev = complex(first), complex(prev), complex(prev_prev)
|
||||
yield from [Vector2d(i) for i in self.ccontrol_points(first, prev, prev_prev)]
|
||||
|
||||
@abc.abstractmethod
|
||||
def ccontrol_points(
|
||||
self, first: complex, prev: complex, prev_prev: complex
|
||||
) -> Tuple[complex, ...]:
|
||||
"""Returns list of path command control points"""
|
||||
|
||||
@classmethod
|
||||
def _argt(cls, sep):
|
||||
return sep.join([cls.number_template] * cls.nargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.letter} {self._argt(' ').format(*self.args)}".strip()
|
||||
|
||||
def __repr__(self):
|
||||
# pylint: disable=consider-using-f-string
|
||||
return "{{}}({})".format(self._argt(", ")).format(self.name, *self.args)
|
||||
|
||||
def __eq__(self, other):
|
||||
previous = 0j
|
||||
if type(self) == type(other): # pylint: disable=unidiomatic-typecheck
|
||||
return self.args == other.args
|
||||
if isinstance(other, tuple):
|
||||
return self.args == other
|
||||
if not isinstance(other, PathCommand):
|
||||
raise ValueError("Can't compare types")
|
||||
try:
|
||||
if self.is_relative == other.is_relative:
|
||||
return self.to_curve(previous) == other.to_curve(previous)
|
||||
except ValueError:
|
||||
pass
|
||||
return False
|
||||
|
||||
@abc.abstractmethod
|
||||
def cend_point(self, first: complex, prev: complex) -> complex:
|
||||
"""Complex version of end_point"""
|
||||
|
||||
def end_point(self, first: ComplexLike, prev: ComplexLike) -> Vector2d:
|
||||
"""Returns last control point of path command"""
|
||||
return Vector2d(self.cend_point(complex(first or 0), complex(prev or 0)))
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_bounding_box(
|
||||
self, first: complex, last_two_points: List[complex], bbox: BoundingBox
|
||||
):
|
||||
"""Enlarges given bbox to contain path element.
|
||||
|
||||
Args:
|
||||
first (complex): first point of path. Required to calculate Z segment
|
||||
last_two_points (List[complex]): list with last two control points in abs
|
||||
coords.
|
||||
bbox (BoundingBox): bounding box to update
|
||||
"""
|
||||
|
||||
def to_curve(self, prev: ComplexLike, prev_prev: ComplexLike = 0) -> Curve:
|
||||
# pylint: disable=unused-argument
|
||||
"""Convert command to :py:class:`Curve`
|
||||
|
||||
Curve().to_curve() returns a copy
|
||||
"""
|
||||
return NotImplemented
|
||||
|
||||
def to_curves(self, prev: ComplexLike, prev_prev: ComplexLike = 0) -> List[Curve]:
|
||||
"""Convert command to list of :py:class:`Curve` commands"""
|
||||
return [self.to_curve(prev, prev_prev)]
|
||||
|
||||
def to_line(self, prev: ComplexLike) -> Line:
|
||||
# pylint: disable=unused-argument
|
||||
"""Converts this segment to a line (copies if already a line)"""
|
||||
return NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def ccurve_points(
|
||||
self, first: complex, prev: complex, prev_prev: complex
|
||||
) -> Tuple[complex, ...]:
|
||||
# pylint: disable=unused-argument
|
||||
"""Converts the path element into a single cubic bezier"""
|
||||
|
||||
# Derivation functionality
|
||||
|
||||
def __check_t(self, t: Optional[float], allow_none=True):
|
||||
if not allow_none and (t is None and self.letter not in "zZmMlLhHvV"):
|
||||
raise ValueError("t=None only supported for Line-like commands")
|
||||
if t is not None and not 0 <= t <= 1:
|
||||
raise ValueError("t should be between 0 and 1")
|
||||
return t if t is not None else 0.0
|
||||
|
||||
def cderivative(
|
||||
self,
|
||||
first: complex,
|
||||
prev: complex,
|
||||
prev_control: complex,
|
||||
t: Optional[float] = None,
|
||||
n: int = 1,
|
||||
) -> complex:
|
||||
"""Returns the nth derivative of the segment at t as a complex number.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
"""
|
||||
if n < 1:
|
||||
raise ValueError("n should be a positive integer")
|
||||
# pylint: disable=protected-access
|
||||
return self._cderivative(first, prev, prev_control, self.__check_t(t), n)
|
||||
|
||||
def derivative(
|
||||
self,
|
||||
first: ComplexLike,
|
||||
prev: ComplexLike,
|
||||
prev_control: ComplexLike,
|
||||
t: Optional[float] = None,
|
||||
n: int = 1,
|
||||
) -> Vector2d:
|
||||
"""Returns the nth derivative of the segment at t as a :class:`Vector2D`.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
"""
|
||||
return Vector2d(
|
||||
self.cderivative(
|
||||
complex(first or 0),
|
||||
complex(prev or 0),
|
||||
complex(prev_control or 0),
|
||||
t,
|
||||
n,
|
||||
)
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _cderivative(
|
||||
self, first: complex, prev: complex, prev_control: complex, t: float, n: int = 1
|
||||
) -> complex: ...
|
||||
|
||||
def cunit_tangent(
|
||||
self,
|
||||
first: complex,
|
||||
prev: complex,
|
||||
prev_control: complex,
|
||||
t: Optional[float] = None,
|
||||
) -> complex:
|
||||
"""Returns the unit tangent of the segment at t as a complex number.
|
||||
|
||||
..versionadded:: 1.4
|
||||
"""
|
||||
return self._cunit_tangent(first, prev, prev_control, self.__check_t(t))
|
||||
|
||||
def unit_tangent(
|
||||
self,
|
||||
first: ComplexLike,
|
||||
prev: ComplexLike,
|
||||
prev_control: ComplexLike,
|
||||
t: Optional[float] = None,
|
||||
) -> Vector2d:
|
||||
"""Returns the unit tangent of the segment at t as a :class:`Vector2D`.
|
||||
|
||||
..versionadded:: 1.4
|
||||
"""
|
||||
return Vector2d(
|
||||
self.cunit_tangent(
|
||||
complex(first or 0), complex(prev or 0), complex(prev_control or 0), t
|
||||
)
|
||||
)
|
||||
|
||||
def _cunit_tangent(
|
||||
self, first: complex, prev: complex, prev_control: complex, t: float
|
||||
) -> complex:
|
||||
dseg = self._cderivative(first, prev, t, 1)
|
||||
return dseg / abs(dseg)
|
||||
|
||||
def cnormal(
|
||||
self,
|
||||
first: complex,
|
||||
prev: complex,
|
||||
prev_control: complex,
|
||||
t: Optional[float] = None,
|
||||
) -> complex:
|
||||
"""Returns the (right-hand-rule) normal vector of the segment at t as a complex
|
||||
number.
|
||||
|
||||
..versionadded:: 1.4
|
||||
"""
|
||||
return self.cunit_tangent(first, prev, prev_control, t) * 1j
|
||||
|
||||
def normal(
|
||||
self,
|
||||
first: ComplexLike,
|
||||
prev: ComplexLike,
|
||||
prev_control: ComplexLike,
|
||||
t: Optional[float] = None,
|
||||
) -> Vector2d:
|
||||
"""Returns the (right-hand-rule) normal vector of the segment at t as
|
||||
:class:`Vector2D`.
|
||||
|
||||
..versionadded:: 1.4
|
||||
"""
|
||||
return Vector2d(
|
||||
self.cnormal(
|
||||
complex(first or 0), complex(prev or 0), complex(prev_control or 0), t
|
||||
)
|
||||
)
|
||||
|
||||
def curvature(
|
||||
self,
|
||||
first: ComplexLike,
|
||||
prev: ComplexLike,
|
||||
prev_control: ComplexLike,
|
||||
t: Optional[float] = None,
|
||||
) -> float:
|
||||
"""Returns the curvature of the segment at t.
|
||||
|
||||
..versionadded:: 1.4
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
return self._curvature(
|
||||
complex(first or 0),
|
||||
complex(prev or 0),
|
||||
complex(prev_control or 0),
|
||||
self.__check_t(t),
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _curvature(
|
||||
self, first: complex, prev: complex, prev_control: complex, t: float
|
||||
) -> float: ...
|
||||
|
||||
# Point evaluation, splitting
|
||||
def cpoint(
|
||||
self, first: complex, prev: complex, prev_control: complex, t: float
|
||||
) -> complex:
|
||||
"""Returns the coordinates of the Bezier curve evaluated at t as complex number.
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
return self._cpoint(first, prev, prev_control, self.__check_t(t, False))
|
||||
|
||||
def point(
|
||||
self, first: ComplexLike, prev: ComplexLike, prev_control: ComplexLike, t: float
|
||||
) -> Vector2d:
|
||||
"""Returns the coordinates of the Bezier curve evaluated at t as :class:`Vector2d`.
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
return Vector2d(
|
||||
self.cpoint(
|
||||
complex(first or 0), complex(prev or 0), complex(prev_control or 0), t
|
||||
)
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _cpoint(
|
||||
self, first: complex, prev: complex, prev_control: complex, t: float
|
||||
) -> complex: ...
|
||||
|
||||
def split(
|
||||
self, first: ComplexLike, prev: ComplexLike, prev_control: ComplexLike, t: float
|
||||
) -> Tuple[PathCommand, PathCommand]:
|
||||
"""Returns two segments, whose union is this segment and which join at
|
||||
self.point(t).
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
# no simplification here, we want to preserve the original type
|
||||
return self._split(
|
||||
complex(first),
|
||||
complex(prev),
|
||||
complex(prev_control),
|
||||
self.__check_t(t, False),
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _split(
|
||||
self, first: complex, prev: complex, prev_control: complex, t: float
|
||||
) -> Tuple[PathCommand, PathCommand]: ...
|
||||
|
||||
# Line integration
|
||||
|
||||
def length(
|
||||
self,
|
||||
first: ComplexLike,
|
||||
prev: ComplexLike,
|
||||
prev_control: ComplexLike,
|
||||
t0: float = 0,
|
||||
t1: float = 1,
|
||||
settings=LengthSettings(),
|
||||
) -> float:
|
||||
"""Returns the length of the segment between t0 and t1.
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
# pylint: disable=protected-access
|
||||
return self._length(
|
||||
complex(first),
|
||||
complex(prev),
|
||||
complex(prev_control),
|
||||
self.__check_t(t0, False),
|
||||
self.__check_t(t1, False),
|
||||
settings,
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _length(
|
||||
self,
|
||||
first: complex,
|
||||
prev: complex,
|
||||
prev_control: complex,
|
||||
t0: float = 0,
|
||||
t1: float = 1,
|
||||
settings=LengthSettings(),
|
||||
) -> float: ...
|
||||
|
||||
def ilength(
|
||||
self,
|
||||
first: ComplexLike,
|
||||
prev: ComplexLike,
|
||||
prev_control: ComplexLike,
|
||||
length: float,
|
||||
settings: ILengthSettings = ILengthSettings(),
|
||||
):
|
||||
"""Returns a float ``t``, such that ``self.length(0, t)`` is approximately
|
||||
``length``.
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
# pylint: disable=protected-access
|
||||
return self._ilength(
|
||||
complex(first), complex(prev), complex(prev_control), length, settings
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _ilength(
|
||||
self,
|
||||
first: complex,
|
||||
prev: complex,
|
||||
prev_control: complex,
|
||||
length: float,
|
||||
settings: ILengthSettings = ILengthSettings(),
|
||||
): ...
|
||||
|
||||
|
||||
class RelativePathCommand(PathCommand):
|
||||
"""
|
||||
Abstract base class for relative path commands.
|
||||
|
||||
Implements most of methods of :py:class:`PathCommand` through
|
||||
conversion to :py:class:`AbsolutePathCommand`
|
||||
"""
|
||||
|
||||
@property
|
||||
def is_relative(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_absolute(self):
|
||||
return False
|
||||
|
||||
def to_relative(self, prev: ComplexLike) -> RelativePathCommand:
|
||||
return self.__class__(*self.args)
|
||||
|
||||
def update_bounding_box(self, first, last_two_points, bbox):
|
||||
self.to_absolute(last_two_points[-1]).update_bounding_box(
|
||||
first, last_two_points, bbox
|
||||
)
|
||||
|
||||
|
||||
class AbsolutePathCommand(PathCommand):
|
||||
"""Absolute path command. Unlike :py:class:`RelativePathCommand` can be transformed
|
||||
directly."""
|
||||
|
||||
@property
|
||||
def is_relative(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_absolute(self):
|
||||
return True
|
||||
|
||||
def to_absolute(self, prev: ComplexLike) -> AbsolutePathCommand:
|
||||
return self.__class__(*self.args)
|
||||
|
||||
@abc.abstractmethod
|
||||
def transform(self, transform: Transform) -> AbsolutePathCommand:
|
||||
"""Returns new transformed segment
|
||||
|
||||
:param transform: a transformation to apply
|
||||
"""
|
||||
|
||||
def rotate(self, degrees: float, center: Vector2d) -> AbsolutePathCommand:
|
||||
"""
|
||||
Returns new transformed segment
|
||||
|
||||
:param degrees: rotation angle in degrees
|
||||
:param center: invariant point of rotation
|
||||
"""
|
||||
return self.transform(Transform(rotate=(degrees, center[0], center[1])))
|
||||
|
||||
def translate(self, dr: Vector2d) -> AbsolutePathCommand:
|
||||
"""Translate or scale this path command by dr"""
|
||||
return self.transform(Transform(translate=dr))
|
||||
|
||||
def scale(self, factor: Union[float, Tuple[float, float]]) -> AbsolutePathCommand:
|
||||
"""Returns new transformed segment
|
||||
|
||||
:param factor: scale or (scale_x, scale_y)
|
||||
"""
|
||||
return self.transform(Transform(scale=factor))
|
||||
|
||||
|
||||
class BezierArcComputationMixin:
|
||||
"""Functionality that works the same way for Arcs, Cubic and Quadratic
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
|
||||
_cpoint: Callable[[complex, complex, complex, float], complex]
|
||||
_cderivative: Callable[..., complex]
|
||||
poly: Callable[[complex, complex, complex, Optional[bool]], np.poly1d]
|
||||
|
||||
def inv_arclength(self, prev, prev_control, s, settings=ILengthSettings()):
|
||||
"""Ported from https://github.com/mathandy/svgpathtools/blob/19df25b99b405ec4fc7616b58384eca7879b6fd4/svgpathtools/path.py#L541
|
||||
(MIT Licensed)"""
|
||||
t_upper = 1
|
||||
t_lower = 0
|
||||
iteration = 0
|
||||
while iteration < settings.maxits:
|
||||
iteration += 1
|
||||
t = (t_lower + t_upper) / 2
|
||||
s_t = self._length(0j, prev, prev_control, t1=t, settings=settings)
|
||||
if abs(s_t - s) < settings.length_tol:
|
||||
return t
|
||||
elif s_t < s: # t too small
|
||||
t_lower = t
|
||||
else: # s < s_t, t too big
|
||||
t_upper = t
|
||||
if t_upper == t_lower:
|
||||
# warn("t is as close as a float can be to the correct value, "
|
||||
# "but |s(t) - s| = {} > s_tol".format(abs(s_t-s)))
|
||||
return t
|
||||
raise Exception(f"Maximum iterations reached with s(t) - s = {s_t - s}.")
|
||||
|
||||
def segment_length(
|
||||
self,
|
||||
start,
|
||||
end,
|
||||
start_point,
|
||||
end_point,
|
||||
pos_eval,
|
||||
settings=LengthSettings(),
|
||||
depth=0,
|
||||
):
|
||||
"""
|
||||
Ported from https://github.com/mathandy/svgpathtools/blob/19df25b99b405ec4fc7616b58384eca7879b6fd4/svgpathtools/path.py#L479
|
||||
(MIT licensed)
|
||||
# TODO better treatment of degenerate beziers from
|
||||
# https://github.com/linebender/kurbo/blob/c229a914d303c5989c9e6b1d766def2df27a8185/src/cubicbez.rs#L431
|
||||
"""
|
||||
mid = (start + end) / 2
|
||||
mid_point = pos_eval(mid)
|
||||
length = abs(end_point - start_point)
|
||||
first_half = abs(mid_point - start_point)
|
||||
second_half = abs(end_point - mid_point)
|
||||
|
||||
length2 = first_half + second_half
|
||||
if (length2 - length > settings.error) or (depth < settings.min_depth):
|
||||
# Calculate the length of each segment:
|
||||
depth += 1
|
||||
return self.segment_length(
|
||||
start, mid, start_point, mid_point, pos_eval, settings, depth
|
||||
) + self.segment_length(
|
||||
mid, end, mid_point, end_point, pos_eval, settings, depth
|
||||
)
|
||||
# This is accurate enough.
|
||||
return length2
|
||||
|
||||
def segment_curvature(self, prev: complex, prev_prev: complex, t: float):
|
||||
"""Returns the curvature of the segment at t.
|
||||
|
||||
Ported from https://github.com/mathandy/svgpathtools/blob/19df25b99b405ec4fc7616b58384eca7879b6fd4/svgpathtools/path.py#L386
|
||||
(MIT licensed)
|
||||
"""
|
||||
|
||||
dz = self._cderivative(0j, prev, prev_prev, t)
|
||||
ddz = self._cderivative(0j, prev, prev_prev, t, n=2)
|
||||
dx, dy = dz.real, dz.imag
|
||||
ddx, ddy = ddz.real, ddz.imag
|
||||
try:
|
||||
kappa = abs(dx * ddy - dy * ddx) / math.sqrt(dx * dx + dy * dy) ** 3
|
||||
except (ZeroDivisionError, FloatingPointError):
|
||||
# tangent vector is zero at t, use polytools to find limit
|
||||
p = self.poly(0j, prev, prev_prev, False)
|
||||
dp = p.deriv()
|
||||
ddp = dp.deriv()
|
||||
dx2, dy2 = np.real(dp), np.imag(dp)
|
||||
ddx2, ddy2 = np.real(ddp), np.imag(ddp)
|
||||
f2 = (dx2 * ddy2 - dy2 * ddx2) ** 2
|
||||
g2 = (dx2 * dx2 + dy2 * dy2) ** 3
|
||||
lim2 = rational_limit(f2, g2, t)
|
||||
if lim2 < 0: # impossible, must be numerical error
|
||||
return 0
|
||||
kappa = math.sqrt(lim2)
|
||||
return kappa
|
||||
|
||||
def _length(
|
||||
self,
|
||||
first: complex,
|
||||
prev: complex,
|
||||
prev_control: complex,
|
||||
t0=0,
|
||||
t1=1,
|
||||
settings=LengthSettings(),
|
||||
) -> float:
|
||||
return self.segment_length(
|
||||
t0,
|
||||
t1,
|
||||
self._cpoint(first, prev, prev_control, t0),
|
||||
self._cpoint(first, prev, prev_control, t1),
|
||||
lambda t: self._cpoint(first, prev, prev_control, t),
|
||||
settings,
|
||||
0,
|
||||
)
|
||||
|
||||
def _ilength(
|
||||
self,
|
||||
first: complex,
|
||||
prev: complex,
|
||||
prev_control: complex,
|
||||
length,
|
||||
settings=ILengthSettings(),
|
||||
):
|
||||
return self.inv_arclength(prev, prev_control, length, settings)
|
||||
|
||||
def _curvature(
|
||||
self, first: complex, prev: complex, prev_control: complex, t: float
|
||||
) -> float:
|
||||
return self.segment_curvature(prev, prev_control, t)
|
||||
|
||||
|
||||
class BezierComputationMixin:
|
||||
"""Functionality that works the same for all Beziers (Quadratic, Cubic)
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
|
||||
def _bpoints(self, prev, prev_prev):
|
||||
return (prev,) + self.ccontrol_points(0j, prev, prev_prev)
|
||||
|
||||
def bezier_unit_tangent(self, prev, prev_prev, t):
|
||||
"""Returns the unit tangent of the segment at t.
|
||||
|
||||
Ported from https://github.com/mathandy/svgpathtools/blob/19df25b99b405ec4fc7616b58384eca7879b6fd4/svgpathtools/path.py#L348
|
||||
(MIT licensed)"""
|
||||
|
||||
dseg = self._cderivative(0j, prev, prev_prev, t)
|
||||
|
||||
try:
|
||||
unit_tangent = dseg / abs(dseg)
|
||||
except (ZeroDivisionError, FloatingPointError):
|
||||
# This may be a removable singularity, if so we just need to compute
|
||||
# the limit.
|
||||
# Note: limit{{dseg / abs(dseg)} = sqrt(limit{dseg**2 / abs(dseg)**2})
|
||||
dseg_poly = self.poly(prev, prev_prev).deriv()
|
||||
dseg_abs_squared_poly = np.real(dseg_poly) ** 2 + np.imag(dseg_poly) ** 2
|
||||
try:
|
||||
unit_tangent = np.sqrt(
|
||||
rational_limit(dseg_poly**2, dseg_abs_squared_poly, t)
|
||||
)
|
||||
except ValueError:
|
||||
bef = self.poly(prev, prev_prev).deriv()(t - 1e-4)
|
||||
aft = self.poly(prev, prev_prev).deriv()(t + 1e-4)
|
||||
mes = (
|
||||
"Unit tangent appears to not be well-defined at "
|
||||
f"t = {t}, \n"
|
||||
f"seg.poly().deriv()(t - 1e-4) = {bef}\n"
|
||||
f"seg.poly().deriv()(t + 1e-4) = {aft}"
|
||||
)
|
||||
raise ValueError(mes)
|
||||
return unit_tangent
|
||||
601
extensions/botbox3000/deps/inkex/paths/lines.py
Normal file
601
extensions/botbox3000/deps/inkex/paths/lines.py
Normal file
@@ -0,0 +1,601 @@
|
||||
# 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.
|
||||
#
|
||||
"""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)
|
||||
937
extensions/botbox3000/deps/inkex/paths/path.py
Normal file
937
extensions/botbox3000/deps/inkex/paths/path.py
Normal file
@@ -0,0 +1,937 @@
|
||||
# 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.
|
||||
#
|
||||
"""
|
||||
Path and CubicSuperPath classes
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import copy
|
||||
import warnings
|
||||
from cmath import isclose
|
||||
|
||||
from typing import Optional, Tuple, List, TypeVar, Iterator, Callable, Union
|
||||
from ..transforms import (
|
||||
Transform,
|
||||
BoundingBox,
|
||||
Vector2d,
|
||||
ComplexLike,
|
||||
)
|
||||
from ..utils import strargs
|
||||
|
||||
from .lines import Line, Move, move, ZoneClose, zoneClose
|
||||
from .curves import Curve
|
||||
from .interfaces import (
|
||||
ILengthSettings,
|
||||
LengthSettings,
|
||||
PathCommand,
|
||||
AbsolutePathCommand,
|
||||
)
|
||||
|
||||
Pathlike = TypeVar("Pathlike", bound="PathCommand")
|
||||
AbsolutePathlike = TypeVar("AbsolutePathlike", bound="AbsolutePathCommand")
|
||||
|
||||
LEX_REX = re.compile(r"([MLHVCSQTAZmlhvcsqtaz])([^MLHVCSQTAZmlhvcsqtaz]*)")
|
||||
|
||||
|
||||
class InvalidPath(ValueError):
|
||||
"""Raised when given an invalid path string"""
|
||||
|
||||
|
||||
class Path(list):
|
||||
"""A list of segment commands which combine to draw a shape"""
|
||||
|
||||
callback: Optional[Callable] = None
|
||||
|
||||
class PathCommandProxy:
|
||||
"""
|
||||
A handy class for Path traverse and coordinate access
|
||||
|
||||
Reduces number of arguments in user code compared to bare
|
||||
:class:`PathCommand` methods
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command: PathCommand,
|
||||
first_point: ComplexLike,
|
||||
previous_end_point: ComplexLike,
|
||||
prev2_control_point: ComplexLike,
|
||||
):
|
||||
self.command = command
|
||||
self.cfirst_point = complex(first_point)
|
||||
self.cprevious_end_point = complex(previous_end_point)
|
||||
self.cprev2_control_point = complex(prev2_control_point)
|
||||
|
||||
@property
|
||||
def first_point(self) -> Vector2d:
|
||||
"""First point of the current subpath"""
|
||||
return Vector2d(self.cfirst_point)
|
||||
|
||||
@property
|
||||
def previous_end_point(self) -> Vector2d:
|
||||
"""End point of the previous command"""
|
||||
return Vector2d(self.cprevious_end_point)
|
||||
|
||||
@property
|
||||
def prev2_control_point(self) -> Vector2d:
|
||||
"""Last control point of the previous command"""
|
||||
return Vector2d(self.cprev2_control_point)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The full name of the segment (i.e. Line, Arc, etc)"""
|
||||
return self.command.name
|
||||
|
||||
@property
|
||||
def letter(self) -> str:
|
||||
"""The single letter representation of this command (i.e. L, A, etc)"""
|
||||
return self.command.letter
|
||||
|
||||
@property
|
||||
def next_command(self):
|
||||
"""The implicit next command."""
|
||||
return self.command.next_command
|
||||
|
||||
@property
|
||||
def is_relative(self) -> bool:
|
||||
"""Whether the command is defined in relative coordinates, i.e. relative to
|
||||
the previous endpoint (lower case path command letter)"""
|
||||
return self.command.is_relative
|
||||
|
||||
@property
|
||||
def is_absolute(self) -> bool:
|
||||
"""Whether the command is defined in absolute coordinates (upper case path
|
||||
command letter)"""
|
||||
return self.command.is_absolute
|
||||
|
||||
@property
|
||||
def args(self) -> List[float]:
|
||||
"""Returns path command arguments as tuple of floats"""
|
||||
return self.command.args
|
||||
|
||||
@property
|
||||
def control_points(self) -> List[Vector2d]:
|
||||
"""Returns list of path command control points"""
|
||||
return list(
|
||||
self.command.control_points(
|
||||
self.cfirst_point,
|
||||
self.cprevious_end_point,
|
||||
self.cprev2_control_point,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def end_point(self) -> Vector2d:
|
||||
"""Returns last control point of path command"""
|
||||
return Vector2d(self.cend_point)
|
||||
|
||||
@property
|
||||
def cend_point(self) -> complex:
|
||||
"""Returns last control point of path command (in complex form)"""
|
||||
return self.command.cend_point(self.cfirst_point, self.cprevious_end_point)
|
||||
|
||||
def reverse(self) -> PathCommand:
|
||||
"""Reverse path command"""
|
||||
return self.command.reverse(self.cend_point, self.cprevious_end_point)
|
||||
|
||||
def to_curve(self) -> Curve:
|
||||
"""Convert command to :py:class:`Curve`
|
||||
Curve().to_curve() returns a copy
|
||||
"""
|
||||
return self.command.to_curve(
|
||||
self.cprevious_end_point, self.cprev2_control_point
|
||||
)
|
||||
|
||||
def to_curves(self) -> List[Curve]:
|
||||
"""Convert command to list of :py:class:`Curve` commands"""
|
||||
return self.command.to_curves(
|
||||
self.cprevious_end_point, self.cprev2_control_point
|
||||
)
|
||||
|
||||
def to_absolute(self) -> AbsolutePathCommand:
|
||||
"""Return relative counterpart for relative commands or copy for absolute"""
|
||||
return self.command.to_absolute(self.cprevious_end_point)
|
||||
|
||||
def to_non_shorthand(self) -> AbsolutePathCommand:
|
||||
"""Returns an absolute non-shorthand command
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
return self.command.to_non_shorthand(
|
||||
self.cprevious_end_point, self.cprev2_control_point
|
||||
)
|
||||
|
||||
def split(self, time) -> Tuple[Path.PathCommandProxy, Path.PathCommandProxy]:
|
||||
"""Split this path command into two PathCommandProxy segments.
|
||||
Raises ValueError for Move commands.
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
result = self.command.split(
|
||||
self.cfirst_point,
|
||||
self.cprevious_end_point,
|
||||
self.cprev2_control_point,
|
||||
time,
|
||||
)
|
||||
p1 = Path.PathCommandProxy(
|
||||
result[0],
|
||||
self.cfirst_point,
|
||||
self.previous_end_point,
|
||||
self.prev2_control_point,
|
||||
)
|
||||
prev2: ComplexLike = (
|
||||
0j if len(p1.control_points) < 2 else p1.control_points[-2]
|
||||
)
|
||||
p2 = Path.PathCommandProxy(
|
||||
result[1], self.cfirst_point, p1.end_point, prev2
|
||||
)
|
||||
return (p1, p2)
|
||||
|
||||
def cpoint(self, time) -> complex:
|
||||
"""Returns the coordinates of the Bezier curve evaluated at t as complex number.
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
return self.command.cpoint(
|
||||
self.cfirst_point,
|
||||
self.cprevious_end_point,
|
||||
self.cprev2_control_point,
|
||||
time,
|
||||
)
|
||||
|
||||
def point(self, time) -> Vector2d:
|
||||
"""Returns the coordinates of the Bezier curve evaluated at t as :class:`Vector2d`.
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
return self.command.point(
|
||||
self.cfirst_point,
|
||||
self.cprevious_end_point,
|
||||
self.cprev2_control_point,
|
||||
time,
|
||||
)
|
||||
|
||||
def length(self, t0=0, t1=1, settings=LengthSettings()) -> float:
|
||||
"""Get the length of the command between t0 and t1 in user units
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
return self.command.length(
|
||||
self.cfirst_point,
|
||||
self.cprevious_end_point,
|
||||
self.cprev2_control_point,
|
||||
t0,
|
||||
t1,
|
||||
settings,
|
||||
)
|
||||
|
||||
def ilength(self, length, settings=ILengthSettings()) -> float:
|
||||
"""Tries to compute the time t at which the path segment has the given
|
||||
length along its trajectory
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
return self.command.ilength(
|
||||
self.cfirst_point,
|
||||
self.cprevious_end_point,
|
||||
self.cprev2_control_point,
|
||||
length,
|
||||
settings,
|
||||
)
|
||||
|
||||
def cunit_tangent(self, t) -> complex:
|
||||
"""Returns the unit tangent at t as complex number
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
return self.command.cunit_tangent(
|
||||
self.cfirst_point,
|
||||
self.cprevious_end_point,
|
||||
self.cprev2_control_point,
|
||||
t,
|
||||
)
|
||||
|
||||
def unit_tangent(self, t) -> Vector2d:
|
||||
"""Returns the unit tangent at t as :class:`inkex.Vector2D`
|
||||
|
||||
.. versionadded:: 1.4"""
|
||||
return self.command.unit_tangent(
|
||||
self.cfirst_point,
|
||||
self.cprevious_end_point,
|
||||
self.cprev2_control_point,
|
||||
t,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.command)
|
||||
|
||||
def __repr__(self):
|
||||
return "<" + self.__class__.__name__ + ">" + repr(self.command)
|
||||
|
||||
def __init__(self, path_d=None) -> None:
|
||||
super().__init__()
|
||||
if isinstance(path_d, str):
|
||||
# Returns a generator returning PathCommand objects
|
||||
path_d = self.parse_string(path_d)
|
||||
elif isinstance(path_d, CubicSuperPath):
|
||||
path_d = path_d.to_path()
|
||||
|
||||
for item in path_d or ():
|
||||
if isinstance(item, PathCommand):
|
||||
self.append(item)
|
||||
elif isinstance(item, (list, tuple)) and len(item) == 2:
|
||||
if isinstance(item[1], (list, tuple)):
|
||||
self.append(PathCommand.letter_to_class(item[0])(*item[1]))
|
||||
else:
|
||||
if len(self) == 0:
|
||||
self.append(Move(*item))
|
||||
else:
|
||||
self.append(Line(*item))
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Bad path type: {type(path_d).__name__}"
|
||||
f"({type(item).__name__}, ...): {item}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parse_string(cls, path_d):
|
||||
"""Parse a path string and generate segment objects"""
|
||||
for cmd, numbers in LEX_REX.findall(path_d):
|
||||
args = list(strargs(numbers))
|
||||
cmd = PathCommand.letter_to_class(cmd)
|
||||
i = 0
|
||||
while i < len(args) or cmd.nargs == 0:
|
||||
if len(args[i : i + cmd.nargs]) != cmd.nargs:
|
||||
return
|
||||
seg = cmd(*args[i : i + cmd.nargs])
|
||||
i += cmd.nargs
|
||||
cmd = seg.next_command
|
||||
yield seg
|
||||
|
||||
def bounding_box(self) -> Optional[BoundingBox]:
|
||||
"""Return bounding box of the Path"""
|
||||
if not self:
|
||||
return None
|
||||
iterator = self.proxy_iterator()
|
||||
proxy = next(iterator)
|
||||
bbox = BoundingBox(proxy.first_point.x, proxy.first_point.y)
|
||||
try:
|
||||
while True:
|
||||
proxy = next(iterator)
|
||||
proxy.command.update_bounding_box(
|
||||
complex(proxy.first_point),
|
||||
[
|
||||
proxy.cprev2_control_point,
|
||||
proxy.cprevious_end_point,
|
||||
],
|
||||
bbox,
|
||||
)
|
||||
except StopIteration:
|
||||
return bbox
|
||||
|
||||
def append(self, cmd):
|
||||
"""Append a command to this path."""
|
||||
try:
|
||||
cmd.letter # pylint: disable=pointless-statement
|
||||
super().append(cmd)
|
||||
except AttributeError:
|
||||
self.extend(cmd)
|
||||
warnings.warn(
|
||||
"Passing a list to Path.add is deprecated, please use Path.extend",
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
|
||||
def translate(self, x, y, inplace=False): # pylint: disable=invalid-name
|
||||
"""Move all coords in this path by the given amount"""
|
||||
return self.transform(Transform(translate=(x, y)), inplace=inplace)
|
||||
|
||||
def scale(self, x, y, inplace=False): # pylint: disable=invalid-name
|
||||
"""Scale all coords in this path by the given amounts"""
|
||||
return self.transform(Transform(scale=(x, y)), inplace=inplace)
|
||||
|
||||
def rotate(self, deg, center=None, inplace=False):
|
||||
"""Rotate the path around the given point"""
|
||||
if center is None:
|
||||
# Default center is center of bbox
|
||||
bbox = self.bounding_box()
|
||||
if bbox:
|
||||
center = bbox.center
|
||||
else:
|
||||
center = Vector2d()
|
||||
center = Vector2d(center)
|
||||
return self.transform(
|
||||
Transform(rotate=(deg, center.x, center.y)), inplace=inplace
|
||||
)
|
||||
|
||||
@property
|
||||
def control_points(self) -> Iterator[Vector2d]:
|
||||
"""Returns all control points of the Path"""
|
||||
prev: complex = 0
|
||||
prev_prev: complex = 0
|
||||
first: complex = 0
|
||||
|
||||
seg: PathCommand
|
||||
for seg in self:
|
||||
cpts = seg.ccontrol_points(first, prev, prev_prev)
|
||||
if seg.letter in "zZmM":
|
||||
first = cpts[-1]
|
||||
for cpt in cpts:
|
||||
prev_prev = prev
|
||||
prev = cpt
|
||||
yield Vector2d(cpt)
|
||||
|
||||
@property
|
||||
def cend_points(self) -> Iterator[complex]:
|
||||
"""Complex version of end_points"""
|
||||
prev = 0j
|
||||
first = 0j
|
||||
|
||||
seg: PathCommand
|
||||
for seg in self:
|
||||
end_point = seg.cend_point(first, prev)
|
||||
if seg.letter in "zZmM":
|
||||
first = end_point
|
||||
prev = end_point
|
||||
yield end_point
|
||||
|
||||
@property
|
||||
def end_points(self) -> Iterator[Vector2d]:
|
||||
"""Returns all endpoints of all path commands (i.e. the nodes)"""
|
||||
for i in self.cend_points:
|
||||
yield Vector2d(i)
|
||||
|
||||
def transform(self, transform, inplace=False):
|
||||
"""Convert to new path"""
|
||||
result = Path()
|
||||
previous = 0j
|
||||
previous_new = 0j
|
||||
start_zone = True
|
||||
first = 0j
|
||||
first_new = 0j
|
||||
|
||||
seg: PathCommand
|
||||
for i, seg in enumerate(self):
|
||||
if start_zone:
|
||||
first = seg.cend_point(first, previous)
|
||||
|
||||
if seg.letter in "hHVv":
|
||||
seg = seg.to_line(previous)
|
||||
|
||||
if seg.is_relative:
|
||||
new_seg = (
|
||||
seg.to_absolute(previous)
|
||||
.transform(transform)
|
||||
.to_relative(previous_new)
|
||||
)
|
||||
else:
|
||||
new_seg = seg.transform(transform)
|
||||
|
||||
if start_zone:
|
||||
first_new = new_seg.cend_point(first_new, previous_new)
|
||||
|
||||
if inplace:
|
||||
self[i] = new_seg
|
||||
else:
|
||||
result.append(new_seg)
|
||||
previous = seg.cend_point(first, previous)
|
||||
previous_new = new_seg.cend_point(first_new, previous_new)
|
||||
start_zone = seg.letter in "zZ"
|
||||
if inplace:
|
||||
return self
|
||||
return result
|
||||
|
||||
def reverse(self):
|
||||
"""Returns a reversed path"""
|
||||
result = Path()
|
||||
try:
|
||||
*_, first = self.cend_points
|
||||
except ValueError:
|
||||
# Empty path, return empty path
|
||||
return result
|
||||
closer = None
|
||||
|
||||
# Go through the path in reverse order
|
||||
for index, prcom in reversed(list(enumerate(self.proxy_iterator()))):
|
||||
if prcom.letter in "MmZz":
|
||||
if closer is not None:
|
||||
if len(result) > 0 and result[-1].letter in "LlVvHh":
|
||||
result.pop() # We can replace simple lines with Z
|
||||
result.append(closer) # replace with same type (rel or abs)
|
||||
if prcom.letter in "Zz":
|
||||
closer = prcom.command
|
||||
else:
|
||||
closer = None
|
||||
|
||||
if index == 0:
|
||||
if prcom.letter == "M":
|
||||
result.insert(0, Move(first))
|
||||
elif prcom.letter == "m":
|
||||
result.insert(0, move(first))
|
||||
else:
|
||||
result.append(prcom.reverse())
|
||||
|
||||
return result
|
||||
|
||||
def break_apart(self) -> List[Path]:
|
||||
"""Breaks apart a path into its subpaths
|
||||
|
||||
.. versionadded:: 1.3"""
|
||||
result = [Path()]
|
||||
current = result[0]
|
||||
|
||||
for cmnd in self.proxy_iterator():
|
||||
if cmnd.letter.lower() == "m":
|
||||
current = Path()
|
||||
result.append(current)
|
||||
current.append(Move(cmnd.cend_point))
|
||||
else:
|
||||
current.append(cmnd.command)
|
||||
# Remove all subpaths that are empty or only contain move commands
|
||||
return [
|
||||
i
|
||||
for i in result
|
||||
if len(i) != 0 and not all(j.letter.lower() == "m" for j in i)
|
||||
]
|
||||
|
||||
def close(self):
|
||||
"""Attempt to close the last path segment"""
|
||||
if self and not self[-1].letter in "zZ":
|
||||
self.append(ZoneClose())
|
||||
|
||||
def proxy_iterator(self) -> Iterator[PathCommandProxy]:
|
||||
"""
|
||||
Yields :py:class:`AugmentedPathIterator`
|
||||
|
||||
:rtype: Iterator[ Path.PathCommandProxy ]
|
||||
"""
|
||||
|
||||
previous = 0j
|
||||
prev_prev = 0j
|
||||
first = 0j
|
||||
seg: PathCommand
|
||||
for seg in self:
|
||||
if seg.letter in "zZmM":
|
||||
first = seg.cend_point(first, previous)
|
||||
yield Path.PathCommandProxy(seg, first, previous, prev_prev)
|
||||
if seg.letter in "ctqsCTQS":
|
||||
prev_prev = seg.ccontrol_points(first, previous, prev_prev)[-2]
|
||||
previous = seg.cend_point(first, previous)
|
||||
|
||||
def subpath_iterator(self):
|
||||
"""Yield Path for each subpath."""
|
||||
start_id = 0
|
||||
|
||||
for i, seg in enumerate(self):
|
||||
if isinstance(seg, (move, Move)):
|
||||
if start_id > -1 and i > 0: # add previous path (open path)
|
||||
yield Path(self[start_id:i])
|
||||
start_id = i
|
||||
elif isinstance(seg, (zoneClose, ZoneClose)): # add current path (closed)
|
||||
yield Path(self[start_id : i + 1])
|
||||
start_id = -1
|
||||
elif i == len(self) - 1 and start_id > -1: # add last path (open)
|
||||
yield Path(self[start_id:])
|
||||
|
||||
def to_absolute(self):
|
||||
"""Convert this path to use only absolute coordinates"""
|
||||
return self._to_absolute(True)
|
||||
|
||||
def to_non_shorthand(self) -> Path:
|
||||
"""Convert this path to use only absolute non-shorthand coordinates
|
||||
|
||||
.. versionadded:: 1.1"""
|
||||
return self._to_absolute(False)
|
||||
|
||||
def _to_absolute(self, shorthand: bool) -> Path:
|
||||
"""Make entire Path absolute.
|
||||
|
||||
Args:
|
||||
shorthand (bool): If false, then convert all shorthand commands to
|
||||
non-shorthand.
|
||||
|
||||
Returns:
|
||||
Path: the input path, converted to absolute coordinates.
|
||||
"""
|
||||
|
||||
abspath = Path()
|
||||
|
||||
previous = 0j
|
||||
first = 0j
|
||||
seg: PathCommand
|
||||
for seg in self:
|
||||
if seg.letter in "mM":
|
||||
first = seg.cend_point(first, previous)
|
||||
|
||||
if shorthand:
|
||||
abspath.append(seg.to_absolute(previous))
|
||||
else:
|
||||
if abspath and abspath[-1].letter in "QC":
|
||||
prev_control = list(abspath[-1].control_points(0, 0, 0))[-2]
|
||||
else:
|
||||
prev_control = previous
|
||||
|
||||
abspath.append(seg.to_non_shorthand(previous, prev_control))
|
||||
|
||||
previous = seg.cend_point(first, previous)
|
||||
|
||||
return abspath
|
||||
|
||||
def to_relative(self):
|
||||
"""Convert this path to use only relative coordinates"""
|
||||
abspath = Path()
|
||||
|
||||
previous = 0j
|
||||
first = 0j
|
||||
seg: PathCommand
|
||||
for seg in self:
|
||||
if seg.letter in "mM":
|
||||
first = seg.cend_point(first, previous)
|
||||
|
||||
abspath.append(seg.to_relative(previous))
|
||||
previous = seg.cend_point(first, previous)
|
||||
|
||||
return abspath
|
||||
|
||||
def __str__(self):
|
||||
return " ".join([str(seg) for seg in self])
|
||||
|
||||
@staticmethod
|
||||
def __add_helper__(other):
|
||||
"""Prepare a path for adding (either add or iadd)"""
|
||||
if isinstance(other, str):
|
||||
other = Path(other)
|
||||
return other
|
||||
|
||||
def __iadd__(self, value):
|
||||
return super().__iadd__(self.__add_helper__(value))
|
||||
|
||||
def __add__(self, other):
|
||||
acopy = copy.deepcopy(self)
|
||||
other = self.__add_helper__(other)
|
||||
if isinstance(other, list):
|
||||
acopy.extend(other)
|
||||
return acopy
|
||||
|
||||
def to_arrays(self):
|
||||
"""Returns path in format of parsePath output, returning arrays of absolute
|
||||
command data
|
||||
|
||||
.. deprecated:: 1.0
|
||||
This is compatibility function for older API. Should not be used in new code
|
||||
|
||||
"""
|
||||
return [[seg.letter, list(seg.args)] for seg in self.to_non_shorthand()]
|
||||
|
||||
def to_superpath(self):
|
||||
"""Convert this path into a cubic super path"""
|
||||
return CubicSuperPath(self)
|
||||
|
||||
def copy(self):
|
||||
"""Make a copy"""
|
||||
return copy.deepcopy(self)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self.callback is not None:
|
||||
self.callback(self) # pylint: disable=not-callable
|
||||
|
||||
|
||||
class CubicSuperPath(list):
|
||||
"""
|
||||
A conversion of a path into a predictable list of cubic curves which
|
||||
can be operated on as a list of simplified instructions.
|
||||
|
||||
When converting back into a path, all lines, arcs etc will be converted
|
||||
to curve instructions.
|
||||
|
||||
Structure is held as [SubPath[(point_a, bezier, point_b), ...], ...]
|
||||
"""
|
||||
|
||||
def __init__(self, items):
|
||||
super().__init__()
|
||||
self._closed = True
|
||||
self._prev = 0j
|
||||
self._prev_prev = 0j
|
||||
|
||||
if isinstance(items, str):
|
||||
items = Path(items)
|
||||
|
||||
if isinstance(items, Path):
|
||||
for item in items:
|
||||
self.append_path_command(item)
|
||||
return
|
||||
|
||||
for item in items:
|
||||
self.append(item)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.to_path())
|
||||
|
||||
def append_node_with_handles(self, command: List[Tuple[float, float]]):
|
||||
"""First item: left handle, second item: node coords,
|
||||
third item: right handle"""
|
||||
if self._closed:
|
||||
# Closed means that the previous segment is closed so we need a new one
|
||||
# We always append to the last open segment. CSP starts out closed.
|
||||
self._closed = False
|
||||
super().append([])
|
||||
|
||||
self[-1].append(command)
|
||||
self._prev_prev = command[0][0] + command[0][1] * 1j
|
||||
self._prev = command[1][0] + command[1][1] * 1j
|
||||
|
||||
def append_path_command(self, command: PathCommand):
|
||||
"""Append a path command.
|
||||
|
||||
For ordinary commands:
|
||||
|
||||
..code ::
|
||||
|
||||
old last entry -> [[.., ..], [.., ..], [x1, y1]]
|
||||
new last entry -> [[x2, y2], [x3, y3], [x3, y3]]
|
||||
|
||||
The last tuple is duplicated (retracted handle): either it's the last command
|
||||
of the subpath, then the handle will stay retracted, or it will be replaced
|
||||
with the next path command.
|
||||
"""
|
||||
if command.letter in "mM":
|
||||
carg = command.cend_point(self._first, self._prev)
|
||||
arg = Vector2d.c2t(carg)
|
||||
super().append([[arg[:], arg[:], arg[:]]])
|
||||
self._prev = self._prev_prev = carg
|
||||
self._closed = False
|
||||
return
|
||||
if command.letter in "zZ" and self:
|
||||
# This duplicates the first segment to 'close' the path
|
||||
self[-1].append([self[-1][0][0][:], self[-1][0][1][:], self[-1][0][2][:]])
|
||||
# Then adds a new subpath for the next shape (if any)
|
||||
# self._closed = True
|
||||
self._prev = self._first
|
||||
return
|
||||
if command.letter in "aA":
|
||||
# Arcs are made up of (possibly) more than one curve, depending on their
|
||||
# angle (approximated)
|
||||
for arc_curve in command.to_curves(self._prev, self._prev_prev):
|
||||
self.append_path_command(arc_curve)
|
||||
return
|
||||
# Handle regular curves.
|
||||
|
||||
if self._closed:
|
||||
# Previous segment is closed. Append a new segment first.
|
||||
self._closed = False
|
||||
super().append([])
|
||||
|
||||
cp1, cp2, cp3 = command.ccurve_points(0j, self._prev, self._prev_prev)
|
||||
|
||||
item = [Vector2d.c2t(cp1), Vector2d.c2t(cp2), Vector2d.c2t(cp3)]
|
||||
self._prev = cp3
|
||||
if not command.letter in "QT":
|
||||
self._prev_prev = cp2
|
||||
else:
|
||||
self._prev_prev = command.ccontrol_points(0j, self._prev, self._prev_prev)[
|
||||
0
|
||||
]
|
||||
|
||||
if self[-1]: # There exists a previous segment, replace its outgoing handle.
|
||||
self[-1][-1][-1] = item[0]
|
||||
# Append the segment with the last coordinate (node pos) repeated.
|
||||
self[-1].append(item[1:] + [item[-1][:]])
|
||||
|
||||
def append(self, item):
|
||||
"""Append a segment/node to the superpath and update the internal state.
|
||||
|
||||
item may be specified in any of the following formats:
|
||||
|
||||
- PathCommand
|
||||
- [str, List[float]] - A path command letter and its arguments
|
||||
- [[float, float], [float, float], [float, float]] - Incoming handle, node,
|
||||
outgoing handle.
|
||||
- List[[float, float], [float, float], [float, float]] - An entire subpath.
|
||||
|
||||
|
||||
"""
|
||||
if isinstance(item, list) and len(item) == 2 and isinstance(item[0], str):
|
||||
item = PathCommand.letter_to_class(item[0])(*item[1])
|
||||
if isinstance(item, PathCommand):
|
||||
self.append_path_command(item)
|
||||
return
|
||||
|
||||
if isinstance(item, list):
|
||||
# Item is a subpath: List[Handle, node, Handle]. Just append the
|
||||
# subpath, and update the prev/ prev_prev positions.
|
||||
if (
|
||||
(len(item) != 3 or not all(len(bit) == 2 for bit in item))
|
||||
and len(item[0]) == 3
|
||||
and all(len(bit) == 2 for bit in item[0])
|
||||
):
|
||||
super().append(self._clean(item))
|
||||
|
||||
elif len(item) == 3 and all(len(bit) == 2 for bit in item):
|
||||
# Item is already a csp segment [Handle, node, Handle].
|
||||
if self._closed:
|
||||
# Closed means that the previous segment is closed so we need a new
|
||||
# one.
|
||||
# We always append to the last open segment. CSP starts out closed.
|
||||
self._closed = False
|
||||
super().append([])
|
||||
|
||||
# Item is already a csp segment and has already been shifted.
|
||||
self[-1].append([i.copy() for i in item])
|
||||
else:
|
||||
raise ValueError(f"Unknown super curve list format: {item}")
|
||||
|
||||
self._prev_prev = Vector2d.t2c(self[-1][-1][0])
|
||||
self._prev = Vector2d.t2c(self[-1][-1][1])
|
||||
else:
|
||||
raise ValueError(f"Unknown super curve list format: {item}")
|
||||
|
||||
def _clean(self, lst):
|
||||
"""Recursively clean lists so they have the same type"""
|
||||
if isinstance(lst, (tuple, list)):
|
||||
return [self._clean(child) for child in lst]
|
||||
return lst
|
||||
|
||||
@property
|
||||
def _first(self):
|
||||
try:
|
||||
return self[-1][0][0][0] + self[-1][0][0][1] * 1j
|
||||
except IndexError:
|
||||
return 0 + 0j
|
||||
|
||||
def to_path(self, curves_only=False, rtol=1e-5, atol=1e-8):
|
||||
"""Convert the super path back to an svg path
|
||||
|
||||
Arguments: see :func:`to_segments` for parameters"""
|
||||
return Path(list(self.to_segments(curves_only, rtol, atol)))
|
||||
|
||||
def to_segments(self, curves_only=False, rtol=1e-5, atol=1e-8):
|
||||
"""Generate a set of segments for this cubic super path
|
||||
|
||||
Arguments:
|
||||
curves_only (bool, optional): If False, curves that can be represented
|
||||
by Lineto / ZoneClose commands, will be. Defaults to False.
|
||||
rtol (float, optional): relative tolerance, passed to :func:`is_line` and
|
||||
:func:`inkex.transforms.ImmutableVector2d.is_close` for checking if a
|
||||
line can be replaced by a ZoneClose command. Defaults to 1e-5.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
atol: absolute tolerance, passed to :func:`is_line` and
|
||||
:func:`inkex.transforms.ImmutableVector2d.is_close`. Defaults to 1e-8.
|
||||
|
||||
.. versionadded:: 1.2"""
|
||||
for subpath in self:
|
||||
previous = []
|
||||
for segment in subpath:
|
||||
if not previous:
|
||||
yield Move(Vector2d(segment[1]))
|
||||
elif self.is_line(previous, segment, rtol, atol) and not curves_only:
|
||||
if segment is subpath[-1] and Vector2d(segment[1]).is_close(
|
||||
Vector2d(subpath[0][1]), rtol, atol
|
||||
):
|
||||
yield ZoneClose()
|
||||
else:
|
||||
yield Line(Vector2d(segment[1]))
|
||||
else:
|
||||
yield Curve(
|
||||
Vector2d(previous[2]),
|
||||
Vector2d(segment[0]),
|
||||
Vector2d(segment[1]),
|
||||
)
|
||||
previous = segment
|
||||
|
||||
def transform(self, transform):
|
||||
"""Apply a transformation matrix to this super path"""
|
||||
return self.to_path().transform(transform).to_superpath()
|
||||
|
||||
@staticmethod
|
||||
def is_on(pt_a, pt_b, pt_c, tol=1e-8):
|
||||
"""Checks if point pt_a is on the line between points pt_b and pt_c
|
||||
|
||||
.. versionadded:: 1.2"""
|
||||
return CubicSuperPath.collinear(pt_a, pt_b, pt_c, tol) and (
|
||||
CubicSuperPath.within(pt_a[0], pt_b[0], pt_c[0])
|
||||
if abs(pt_a[0] - pt_b[0]) > 1e-13
|
||||
else CubicSuperPath.within(pt_a[1], pt_b[1], pt_c[1])
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def collinear(pt_a, pt_b, pt_c, tol=1e-8):
|
||||
"""Checks if points pt_a, pt_b, pt_c lie on the same line,
|
||||
i.e. that the cross product (b-a) x (c-a) < tol
|
||||
|
||||
.. versionadded:: 1.2"""
|
||||
return (
|
||||
abs(
|
||||
(pt_b[0] - pt_a[0]) * (pt_c[1] - pt_a[1])
|
||||
- (pt_c[0] - pt_a[0]) * (pt_b[1] - pt_a[1])
|
||||
)
|
||||
< tol
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def within(val_b, val_a, val_c):
|
||||
"""Checks if float val_b is between val_a and val_c
|
||||
|
||||
.. versionadded:: 1.2"""
|
||||
return val_a <= val_b <= val_c or val_c <= val_b <= val_a
|
||||
|
||||
@staticmethod
|
||||
def is_line(previous, segment, rtol=1e-5, atol=1e-8):
|
||||
"""Check whether csp segment (two points) can be expressed as a line has
|
||||
retracted handles or the handles can be retracted without loss of information
|
||||
(i.e. both handles lie on the line)
|
||||
|
||||
.. versionchanged:: 1.2
|
||||
Previously, it was only checked if both control points have retracted
|
||||
handles. Now it is also checked if the handles can be retracted without
|
||||
(visible) loss of information (i.e. both handles lie on the line connecting
|
||||
the nodes).
|
||||
|
||||
Arguments:
|
||||
previous: first node in superpath notation
|
||||
segment: second node in superpath notation
|
||||
rtol (float, optional): relative tolerance, passed to
|
||||
:func:`inkex.transforms.ImmutableVector2d.is_close` for checking handle
|
||||
retraction. Defaults to 1e-5.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
atol (float, optional): absolute tolerance, passed to
|
||||
:func:`inkex.transforms.ImmutableVector2d.is_close` for checking handle
|
||||
retraction and
|
||||
:func:`inkex.paths.CubicSuperPath.is_on` for checking if all points
|
||||
(nodes + handles) lie on a line. Defaults to 1e-8.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
|
||||
retracted = isclose(
|
||||
Vector2d(previous[1]), Vector2d(previous[2]), rel_tol=rtol, abs_tol=atol
|
||||
) and isclose(
|
||||
Vector2d(segment[0]), Vector2d(segment[1]), rel_tol=rtol, abs_tol=atol
|
||||
)
|
||||
|
||||
if retracted:
|
||||
return True
|
||||
|
||||
# Can both handles be retracted without loss of information?
|
||||
# Definitely the case if the handles lie on the same line as the two nodes and
|
||||
# in the correct order
|
||||
# E.g. cspbezsplitatlength outputs non-retracted handles when splitting a
|
||||
# straight line
|
||||
return CubicSuperPath.is_on(
|
||||
segment[0], segment[1], previous[2], atol
|
||||
) and CubicSuperPath.is_on(previous[2], previous[1], segment[0], atol)
|
||||
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