bundle: update (2026-01-18)
This commit is contained in:
122
extensions/km-hershey/deps/inkex/paths/__init__.py
Normal file
122
extensions/km-hershey/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",
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
670
extensions/km-hershey/deps/inkex/paths/arc.py
Normal file
670
extensions/km-hershey/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/km-hershey/deps/inkex/paths/curves.py
Normal file
499
extensions/km-hershey/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/km-hershey/deps/inkex/paths/interfaces.py
Normal file
731
extensions/km-hershey/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/km-hershey/deps/inkex/paths/lines.py
Normal file
601
extensions/km-hershey/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/km-hershey/deps/inkex/paths/path.py
Normal file
937
extensions/km-hershey/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/km-hershey/deps/inkex/paths/quadratic.py
Normal file
456
extensions/km-hershey/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