bundle: update (2026-01-18)
This commit is contained in:
731
extensions/km-hatch/deps/inkex/paths/interfaces.py
Normal file
731
extensions/km-hatch/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
|
||||
Reference in New Issue
Block a user