initial commit

This commit is contained in:
2026-01-17 21:51:34 -05:00
commit 06a449b44e
90 changed files with 24094 additions and 0 deletions

862
deps/inkex/tween.py vendored Normal file
View File

@@ -0,0 +1,862 @@
# coding=utf-8
#
# Copyright (C) 2005 Aaron Spike, aaron@ekips.org
# 2020 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.
#
"""Module for interpolating attributes and styles
.. versionchanged:: 1.2
Rewritten in inkex 1.2 in an object-oriented structure to support more attributes.
"""
from bisect import bisect_left
import abc
import copy
from .styles import Style
from .elements._filters import LinearGradient, RadialGradient, Stop
from .transforms import Transform
from .colors import Color
from .units import convert_unit, parse_unit, render_unit
from .bezier import bezlenapprx, cspbezsplit, cspbezsplitatlength, csplength
from .paths import Path, CubicSuperPath
from .elements import SvgDocumentElement
from .utils import FragmentError
try:
from typing import Tuple, TypeVar
Value = TypeVar("Value")
Number = TypeVar("Number", int, float)
except ImportError:
pass
def interpcoord(coord_a: Number, coord_b: Number, time: float):
"""Interpolate single coordinate by the amount of time"""
return ValueInterpolator(coord_a, coord_b).interpolate(time)
def interppoints(point1, point2, time):
# type: (Tuple[float, float], Tuple[float, float], float) -> Tuple[float, float]
"""Interpolate coordinate points by amount of time"""
return ArrayInterpolator(point1, point2).interpolate(time)
class AttributeInterpolator(abc.ABC):
"""Interpolate between attributes"""
def __init__(self, start_value, end_value):
self.start_value = start_value
self.end_value = end_value
@staticmethod
def best_style(node):
"""Gets the best possible approximation to a node's style. For nodes inside the
element tree of an SVG file, stylesheets defined in the defs of that file can be
taken into account. This should be the case for input elements, but is not
required - in that case, only the local inline style is used.
During the interpolation process, some nodes are created temporarily, such as
plain gradients of a single color to allow solid<->gradient interpolation. These
are not attached to the document tree and therefore have no root. Since the only
style relevant for them is the inline style, it is acceptable to fallback to it.
Args:
node (BaseElement): The node to get the best approximated style of
Returns:
Style: If the node is rooted, the CSS specified style. Else, the inline
style."""
try:
return node.specified_style()
except FragmentError:
return node.style
@staticmethod
def create_from_attribute(snode, enode, attribute, method=None):
"""Creates an interpolator for an attribute. Currently, only path, transform and
style attributes are supported
Args:
snode (BaseElement): start element
enode (BaseElement): end element
attribute (str): attribute name (for styles, starting with "style/")
method (AttributeInterpolator, optional): (currently only used for paths).
Specifies a method used to interpolate the attribute. Defaults to None.
Raises:
ValueError: if an attribute is passed that is not a style, path or transform
attribute
Returns:
AttributeInterpolator: an interpolator whose type depends on attribute.
"""
if attribute in Style.color_props:
return StyleInterpolator.create_from_fill_stroke(snode, enode, attribute)
if attribute == "d":
if method is None:
method = FirstNodesInterpolator
return method(snode.path, enode.path)
if attribute == "style":
return StyleInterpolator(snode, enode)
if attribute.startswith("style/"):
return StyleInterpolator.create(snode, enode, attribute[6:])
if attribute == "transform":
return TransformInterpolator(snode.transform, enode.transform)
if method is not None:
return method(snode.get(attribute), enode.get(attribute))
raise ValueError("only path and style attributes are supported")
@abc.abstractmethod
def interpolate(self, time=0):
"""Interpolation method, needs to be implemented by subclasses"""
return
class StyleInterpolator(AttributeInterpolator):
"""Class to interpolate styles"""
def __init__(self, start_value, end_value):
super().__init__(start_value, end_value)
self.interpolators = {}
# some keys are always processed in a certain order, these provide alternative
# interpolation routes if e.g. Color<->none is interpolated
all_keys = list(
dict.fromkeys(
["fill", "stroke", "fill-opacity", "stroke-opacity", "stroke-width"]
+ list(self.best_style(start_value).keys())
+ list(self.best_style(end_value).keys())
)
)
for attr in all_keys:
sstyle = self.best_style(start_value)
estyle = self.best_style(end_value)
if attr not in sstyle and attr not in estyle:
continue
try:
interp = StyleInterpolator.create(
self.start_value, self.end_value, attr
)
self.interpolators[attr] = interp
except ValueError:
# no interpolation method known for this attribute
pass
@staticmethod
def create(snode, enode, attribute):
"""Creates an Interpolator for a given style attribute, depending on its type:
- Color properties (such as fill, stroke) -> :class:`ColorInterpolator`,
:class:`GradientInterpolator` ect.
- Unit properties -> :class:`UnitValueInterpolator`
- other properties -> :class:`ValueInterpolator`
Args:
snode (BaseElement): start element
enode (BaseElement): end element
attribute (str): attribute to interpolate
Raises:
ValueError: if the attribute is not in any of the lists
Returns:
AttributeInterpolator: an interpolator object whose type depends on the
attribute.
"""
if attribute in Style.color_props:
return StyleInterpolator.create_from_fill_stroke(snode, enode, attribute)
if attribute in Style.unit_props:
return UnitValueInterpolator(
AttributeInterpolator.best_style(snode)(attribute),
AttributeInterpolator.best_style(enode)(attribute),
)
if attribute in Style.opacity_props:
return ValueInterpolator(
AttributeInterpolator.best_style(snode)(attribute),
AttributeInterpolator.best_style(enode)(attribute),
)
raise ValueError("Unknown attribute")
@staticmethod
def create_from_fill_stroke(snode, enode, attribute):
"""Creates an Interpolator for a given color-like attribute
Args:
snode (BaseElement): start element
enode (BaseElement): end element
attribute (str): attribute to interpolate
Raises:
ValueError: if the attribute is not color-like
ValueError: if the attribute is unset on both start and end style
Returns:
AttributeInterpolator: an interpolator object whose type depends on the
attribute.
"""
if attribute not in Style.color_props:
raise ValueError("attribute must be a color property")
sstyle = AttributeInterpolator.best_style(snode)
estyle = AttributeInterpolator.best_style(enode)
styles = [[snode, sstyle], [enode, estyle]]
for cur, curstyle in styles:
if curstyle(attribute) is None:
cur.style[attribute + "-opacity"] = 0.0
if attribute == "stroke":
cur.style["stroke-width"] = 0.0
# check if style is none, unset or a color
if isinstance(
sstyle(attribute), (LinearGradient, RadialGradient)
) or isinstance(estyle(attribute), (LinearGradient, RadialGradient)):
# if one of the two styles is a gradient, use gradient interpolation.
try:
return GradientInterpolator.create(snode, enode, attribute)
except ValueError:
# different gradient types, just duplicate the first
return TrivialInterpolator(sstyle(attribute))
if sstyle(attribute) is None and estyle(attribute) is None:
return TrivialInterpolator("none")
return ColorInterpolator.create(sstyle, estyle, attribute)
def interpolate(self, time=0):
"""Interpolates a style using the interpolators set in self.interpolators
Args:
time (int, optional): Interpolation position. If 0, start_value is returned,
if 1, end_value is returned. Defaults to 0.
Returns:
inkex.Style: interpolated style
"""
style = Style()
for prop, interp in self.interpolators.items():
style[prop] = interp.interpolate(time)
return style
class TrivialInterpolator(AttributeInterpolator):
"""Trivial interpolator, returns value for every time"""
def __init__(self, value):
super().__init__(value, value)
def interpolate(self, time=0):
return self.start_value
class ValueInterpolator(AttributeInterpolator):
"""Class for interpolation of a single value"""
def __init__(self, start_value=0, end_value=0):
super().__init__(float(start_value), float(end_value))
def interpolate(self, time=0):
"""(Linearly) interpolates a value
Args:
time (int, optional): Interpolation position. If 0, start_value is returned,
if 1, end_value is returned. Defaults to 0.
Returns:
int: interpolated value
"""
return self.start_value + ((self.end_value - self.start_value) * time)
class UnitValueInterpolator(ValueInterpolator):
"""Class for interpolation of a value with unit"""
def __init__(self, start_value=0, end_value=0):
start_val, start_unit = parse_unit(start_value)
end_val = convert_unit(end_value, start_unit)
super().__init__(start_val, end_val)
self.unit = start_unit
def interpolate(self, time=0):
return render_unit(super().interpolate(time), self.unit)
class ArrayInterpolator(AttributeInterpolator):
"""Interpolates array-like objects element-wise, e.g. color, transform,
coordinate"""
def __init__(self, start_value, end_value):
super().__init__(start_value, end_value)
self.interpolators = [
ValueInterpolator(cur, other)
for (cur, other) in zip(start_value, end_value)
]
def interpolate(self, time=0):
"""Interpolates an array element-wise
Args:
time (int, optional): [description]. Defaults to 0.
Returns:
List: interpolated array
"""
return [interp.interpolate(time) for interp in self.interpolators]
class TransformInterpolator(ArrayInterpolator):
"""Class for interpolation of transforms"""
def __init__(self, start_value=Transform(), end_value=Transform()):
"""Creates a transform interpolator.
Args:
start_value (inkex.Transform, optional): start transform. Defaults to
inkex.Transform().
end_value (inkex.Transform, optional): end transform. Defaults to
inkex.Transform().
"""
super().__init__(start_value.to_hexad(), end_value.to_hexad())
def interpolate(self, time=0):
"""Interpolates a transform by interpolating each item in the transform hexad
separately.
Args:
time (int, optional): Interpolation position. If 0, start_value is returned,
if 1, end_value is returned. Defaults to 0.
Returns:
Transform: interpolated transform
"""
return Transform(super().interpolate(time))
class ColorInterpolator(ArrayInterpolator):
"""Class for color interpolation"""
@staticmethod
def create(sst, est, attribute):
"""Creates a ColorInterpolator for either Fill or stroke, depending on the
attribute.
Args:
sst (Style): Start style
est (Style): End style
attribute (string): either fill or stroke
Raises:
ValueError: if none of the start or end style is a color.
Returns:
ColorInterpolator: A ColorInterpolator object
"""
styles = [sst, est]
for cur, other in zip(styles, reversed(styles)):
if not isinstance(cur(attribute), Color) or cur(attribute) is None:
cur[attribute] = other(attribute)
this = ColorInterpolator(
Color(styles[0](attribute)), Color(styles[1](attribute))
)
if this is None:
raise ValueError("One of the two attribute needs to be a plain color")
return this
def __init__(self, start_value=Color("#000000"), end_value=Color("#000000")):
# Remember what type the color was, handle none types as a special case
# so we can tween from "none" to some color effectively.
self.output_type = type(start_value)
if self.output_type.name == "none":
self.output_type = type(end_value)
# We tween alpha is there is any in either value
tween_alpha = (
start_value.effective_alpha != end_value.effective_alpha
or start_value.alpha is not None
or end_value.alpha is not None
)
super().__init__(
start_value.get_values(tween_alpha), end_value.get_values(tween_alpha)
)
def interpolate(self, time=0):
"""Interpolates a color by interpolating its channels separately.
Args:
time (int, optional): Interpolation position. If 0, start_value is returned,
if 1, end_value is returned. Defaults to 0.
Returns:
Color: interpolated color
"""
return self.output_type(list(map(float, super().interpolate(time))))
class GradientInterpolator(AttributeInterpolator):
"""Base class for Gradient Interpolation"""
def __init__(self, start_value, end_value, svg=None):
super().__init__(start_value, end_value)
self.svg = svg
# If one of the styles is empty, set it to the gradient of the other
if start_value is None:
self.start_value = end_value
if end_value is None:
self.end_value = start_value
self.transform_interpolator = TransformInterpolator(
self.start_value.gradientTransform, self.end_value.gradientTransform
)
self.orientation_interpolator = {
attr: UnitValueInterpolator(
self.start_value.get(attr), self.end_value.get(attr)
)
for attr in self.start_value.orientation_attributes
if self.start_value.get(attr) is not None
and self.end_value.get(attr) is not None
}
if not (
self.start_value.href is not None
and self.start_value.href is self.end_value.href
):
# the gradient link to different stops, interpolate between them
# add both start and end offsets, then take distict
newoffsets = sorted(
list(set(self.start_value.stop_offsets + self.end_value.stop_offsets))
)
def func(start, end, time):
return StopInterpolator(start, end).interpolate(time)
sstops = GradientInterpolator.interpolate_linear_list(
self.start_value.stop_offsets,
list(self.start_value.stops),
newoffsets,
func,
)
ostops = GradientInterpolator.interpolate_linear_list(
self.end_value.stop_offsets,
list(self.end_value.stops),
newoffsets,
func,
)
self.newstop_interpolator = [
StopInterpolator(s1, s2) for s1, s2 in zip(sstops, ostops)
]
else:
self.newstop_interpolator = None
@staticmethod
def create(snode, enode, attribute):
"""Creates a `GradientInterpolator` for either fill or stroke, depending on
attribute.
Cases: (A, B) -> Interpolator
- Linear Gradient, Linear Gradient -> LinearGradientInterpolator
- Color or None, Linear Gradient -> LinearGradientInterpolator
- Radial Gradient, Radial Gradient -> RadialGradientInterpolator
- Color or None, Radial Gradient -> RadialGradientInterpolator
- Radial Gradient, Linear Gradient -> ValueError
- Color or None, Color or None -> ValueError
Args:
snode (BaseElement): start element
enode (BaseElement): end element
attribute (string): either fill or stroke
Raises:
ValueError: if none of the styles are a gradient or if they are gradients
of different types
Returns:
GradientInterpolator: an Interpolator object
"""
interpolator = None
gradienttype = None
# first find out which type of interpolator we need
sstyle = AttributeInterpolator.best_style(snode)
estyle = AttributeInterpolator.best_style(enode)
for cur in [sstyle, estyle]:
curgrad = None
if isinstance(cur(attribute), (LinearGradient, RadialGradient)):
curgrad = cur(attribute)
for gradtype, interp in [
[LinearGradient, LinearGradientInterpolator],
[RadialGradient, RadialGradientInterpolator],
]:
if curgrad is not None and isinstance(curgrad, gradtype):
if interpolator is None:
interpolator = interp
gradienttype = gradtype
if not (interp == interpolator):
raise ValueError("Gradient types don't match")
# If one of the styles is empty, set it to the gradient of the other, but with
# zero opacity (and stroke-width for strokes)
# If one of the styles is a plain color, replace it by a gradient with a single
# stop
iterator = [[snode, gradienttype(), enode], [enode, gradienttype(), snode]]
for index in [0, 1]:
curstyle = AttributeInterpolator.best_style(iterator[index][0])
value = curstyle(attribute)
if value is None:
# if the attribute of one of the two ends is unset, set the opacity to
# zero.
iterator[index][0].style[attribute + "-opacity"] = 0.0
if attribute == "stroke":
iterator[index][0].style["stroke-width"] = 0.0
if isinstance(value, Color):
# if the attribute of one of the two ends is a color, convert it to a
# one-stop gradient. Type depends on the type of the other gradient.
interpolator.initialize_position(
iterator[index][1], iterator[index][0].bounding_box()
)
stop = Stop()
stop.style = Style()
stop.style["stop-color"] = value
stop.offset = 0
iterator[index][1].add(stop)
stop = Stop()
stop.style = Style()
stop.style["stop-color"] = value
stop.offset = 1
iterator[index][1].add(stop)
else:
iterator[index][1] = value # is a gradient
if interpolator is None:
raise ValueError("None of the two styles is a gradient")
if interpolator in [LinearGradientInterpolator, RadialGradientInterpolator]:
return interpolator(iterator[0][1], iterator[1][1], snode)
return interpolator(iterator[0][1], iterator[1][1])
@staticmethod
def interpolate_linear_list(positions, values, newpositions, func):
"""Interpolates a list of values given at n positions to the best approximation
at m newpositions.
>>>
|
| x
| x
_________________
pq q p q
(x denotes function values, p: positions, q: newpositions)
A function may be given to interpolate between given values.
Args:
positions (list[number-like]): position of current function values
values (list[Type]): list of arbitrary type,
``len(values) == len(positions)``
newpositions (list[number-like]): position of interpolated values
func (Callable[[Type, Type, float], Type]): Function to interpolate between
values
Returns:
list[Type]: interpolated function values at positions
"""
newvalues = []
positions = list(map(float, positions))
newpositions = list(map(float, newpositions))
for pos in newpositions:
if len(positions) == 1:
newvalues.append(values[0])
else:
# current run:
# idxl pos idxr
# p p | p
# q q
idxl = max(0, bisect_left(positions, pos) - 1)
idxr = min(len(positions) - 1, idxl + 1)
fraction = (pos - positions[idxl]) / (positions[idxr] - positions[idxl])
vall = values[idxl]
valr = values[idxr]
newval = func(vall, valr, fraction)
newvalues.append(newval)
return newvalues
@staticmethod
def append_to_doc(element, gradient):
"""Splits a gradient into stops and orientation, appends it to the document's
defs and returns the href to the orientation gradient.
Args:
element (BaseElement): an element inside the SVG that the gradient should be
added to
gradient (Gradient): the gradient to append to the document
Returns:
Gradient: the orientation gradient, or the gradient object if
element has no root or is None
"""
stops, orientation = gradient.stops_and_orientation()
if element is None or (
element.getparent() is None and not isinstance(element, SvgDocumentElement)
):
return gradient
element.root.defs.add(orientation)
if len(stops) > 0:
element.root.defs.add(stops, orientation)
orientation.href = stops.get_id()
return orientation
def interpolate(self, time=0):
"""Interpolate with another gradient."""
newgrad = self.start_value.copy()
# interpolate transforms
newgrad.gradientTransform = self.transform_interpolator.interpolate(time)
# interpolate orientation
for attr in self.orientation_interpolator.keys():
newgrad.set(attr, self.orientation_interpolator[attr].interpolate(time))
# interpolate stops
if self.newstop_interpolator is not None:
newgrad.remove_all(Stop)
newgrad.add(
*[interp.interpolate(time) for interp in self.newstop_interpolator]
)
if self.svg is None:
return newgrad
return GradientInterpolator.append_to_doc(self.svg, newgrad)
class LinearGradientInterpolator(GradientInterpolator):
"""Class for interpolation of linear gradients"""
def __init__(
self, start_value=LinearGradient(), end_value=LinearGradient(), svg=None
):
super().__init__(start_value, end_value, svg)
@staticmethod
def initialize_position(grad, bbox):
"""Initializes a linear gradient's position"""
grad.set("x1", bbox.left)
grad.set("x2", bbox.right)
grad.set("y1", bbox.center.y)
grad.set("y2", bbox.center.y)
class RadialGradientInterpolator(GradientInterpolator):
"""Class to interpolate radial gradients"""
def __init__(
self, start_value=RadialGradient(), end_value=RadialGradient(), svg=None
):
super().__init__(start_value, end_value, svg)
@staticmethod
def initialize_position(grad, bbox):
"""Initializes a radial gradient's position"""
x, y = bbox.center
grad.set("cx", x)
grad.set("cy", y)
grad.set("fx", x)
grad.set("fy", y)
grad.set("r", bbox.right - bbox.center.x)
class StopInterpolator(AttributeInterpolator):
"""Class to interpolate gradient stops"""
def __init__(self, start_value, end_value):
super().__init__(start_value, end_value)
self.style_interpolator = StyleInterpolator(start_value, end_value)
self.position_interpolator = ValueInterpolator(
float(start_value.offset), float(end_value.offset)
)
def interpolate(self, time=0):
"""Interpolates a gradient stop by interpolating style and offset separately
Args:
time (int, optional): Interpolation position. If 0, start_value is returned,
if 1, end_value is returned. Defaults to 0.
Returns:
Stop: interpolated gradient stop
"""
newstop = Stop()
newstop.style = self.style_interpolator.interpolate(time)
newstop.offset = self.position_interpolator.interpolate(time)
return newstop
class PathInterpolator(AttributeInterpolator):
"""Base class for Path interpolation"""
def __init__(self, start_value=Path(), end_value=Path()):
super().__init__(start_value.to_superpath(), end_value.to_superpath())
self.processed_end_path = None
self.processed_start_path = None
def truncate_subpaths(self):
"""Truncates the longer path so that all subpaths in both paths have an equal
number of bezier commands"""
s = [[]]
e = [[]]
# loop through all subpaths as long as there are remaining ones
while self.start_value and self.end_value:
# if both subpaths contain a bezier command, append it to s and e
if self.start_value[0] and self.end_value[0]:
s[-1].append(self.start_value[0].pop(0))
e[-1].append(self.end_value[0].pop(0))
# if the subpath of start_value is empty, add the remaining empty list as
# new subpath of s and one more item of end_value as new subpath of e.
# Afterwards, the loop terminates
elif self.end_value[0]:
s.append(self.start_value.pop(0))
e[-1].append(self.end_value[0][0])
e.append([self.end_value[0].pop(0)])
elif self.start_value[0]:
e.append(self.end_value.pop(0))
s[-1].append(self.start_value[0][0])
s.append([self.start_value[0].pop(0)])
# if there are no commands left in both start_value or end_value, add empty
# list to both start_value and end_value
else:
s.append(self.start_value.pop(0))
e.append(self.end_value.pop(0))
self.processed_start_path = s
self.processed_end_path = e
def interpolate(self, time=0):
# create an interpolated path for each interval
interp = []
# process subpaths
for ssubpath, esubpath in zip(
self.processed_start_path, self.processed_end_path
):
if not (ssubpath or esubpath):
break
# add a new subpath to the interpolated path
interp.append([])
# process each bezier command in the subpaths (which now have equal length)
for sbezier, ebezier in zip(ssubpath, esubpath):
if not (sbezier or ebezier):
break
# add a new bezier command to the last subpath
interp[-1].append([])
# process points
for point1, point2 in zip(sbezier, ebezier):
if not (point1 or point2):
break
# add a new point to the last bezier command
interp[-1][-1].append(
ArrayInterpolator(point1, point2).interpolate(time)
)
# remove final subpath if empty.
if not interp[-1]:
del interp[-1]
return CubicSuperPath(interp)
class EqualSubsegmentsInterpolator(PathInterpolator):
"""Interpolates the path by rediscretizing the subpaths first."""
@staticmethod
def get_subpath_lenghts(path):
"""prepare lengths for interpolation"""
sp_lenghts, total = csplength(path)
t = 0
lenghts = []
for sp in sp_lenghts:
for l in sp:
t += l / total
lenghts.append(t)
lenghts.sort()
return sp_lenghts, total, lenghts
@staticmethod
def process_path(path, other):
"""Rediscretize path so that all subpaths have an equal number of segments,
so that there is a node at the path "times" where path or other have a node
Args:
path (Path): the first path
other (Path): the second path
Returns:
Array: the prepared path description for the intermediate path"""
sp_lenghts, total, _ = EqualSubsegmentsInterpolator.get_subpath_lenghts(path)
_, _, lenghts = EqualSubsegmentsInterpolator.get_subpath_lenghts(other)
t = 0
s = [[]]
for sp in sp_lenghts:
if not path[0]:
s.append(path.pop(0))
s[-1].append(path[0].pop(0))
for l in sp:
pt = t
t += l / total
if lenghts and t > lenghts[0]:
while lenghts and lenghts[0] < t:
nt = (lenghts[0] - pt) / (t - pt)
bezes = cspbezsplitatlength(s[-1][-1][:], path[0][0][:], nt)
s[-1][-1:] = bezes[:2]
path[0][0] = bezes[2]
pt = lenghts.pop(0)
s[-1].append(path[0].pop(0))
return s
def __init__(self, start_path=Path(), end_path=Path()):
super().__init__(start_path, end_path)
# rediscretisize both paths
start_copy = copy.deepcopy(self.start_value)
# TODO find out why self.start_value.copy() doesn't work
self.start_value = EqualSubsegmentsInterpolator.process_path(
self.start_value, self.end_value
)
self.end_value = EqualSubsegmentsInterpolator.process_path(
self.end_value, start_copy
)
self.truncate_subpaths()
class FirstNodesInterpolator(PathInterpolator):
"""Interpolates a path by discarding the trailing nodes of the longer subpath"""
def __init__(self, start_path=Path(), end_path=Path()):
super().__init__(start_path, end_path)
# which path has fewer segments?
lengthdiff = len(self.start_value) - len(self.end_value)
# swap shortest first
if lengthdiff > 0:
self.start_value, self.end_value = self.end_value, self.start_value
# subdivide the shorter path
for _ in range(abs(lengthdiff)):
maxlen = 0
subpath = 0
segment = 0
for y, _ in enumerate(self.start_value):
for z in range(1, len(self.start_value[y])):
leng = bezlenapprx(
self.start_value[y][z - 1], self.start_value[y][z]
)
if leng > maxlen:
maxlen = leng
subpath = y
segment = z
sp1, sp2 = self.start_value[subpath][segment - 1 : segment + 1]
self.start_value[subpath][segment - 1 : segment + 1] = cspbezsplit(sp1, sp2)
# if swapped, swap them back
if lengthdiff > 0:
self.start_value, self.end_value = self.end_value, self.start_value
self.truncate_subpaths()