try a python script in runner

This commit is contained in:
2026-01-17 20:15:31 -05:00
parent 44af1518a3
commit 83c22af578
109 changed files with 227 additions and 28581 deletions

View File

@@ -1,937 +0,0 @@
# 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)