diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /share/extensions/inkex/tween.py | |
parent | Initial commit. (diff) | |
download | inkscape-upstream/1.2.2.tar.xz inkscape-upstream/1.2.2.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'share/extensions/inkex/tween.py')
-rw-r--r-- | share/extensions/inkex/tween.py | 847 |
1 files changed, 847 insertions, 0 deletions
diff --git a/share/extensions/inkex/tween.py b/share/extensions/inkex/tween.py new file mode 100644 index 0000000..75bd90c --- /dev/null +++ b/share/extensions/inkex/tween.py @@ -0,0 +1,847 @@ +# 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")): + super().__init__(start_value, end_value) + + def interpolate(self, time=0): + """Interpolates a color by interpolating its r, g, b, a 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 Color(list(map(int, 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.set("xlink:href", f"#{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() |