# coding=utf-8
#
# Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
# Copyright (C) 2010 Alvin Penner, penner@vaxxine.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.
# barraud@math.univ-lille1.fr
#
# This code defines several functions to make handling of transform
# attribute easier.
#
"""
Provide transformation parsing to extensions
"""
import re
import sys
from decimal import Decimal
from math import cos, radians, sin, sqrt, tan, fabs, atan2, pi
from .utils import strargs
try:
from typing import overload, Tuple, Union, Optional # pylint: disable=unused-import
VectorLike = Union["Vector2d", Tuple[float, float]] # pylint: disable=invalid-name
except ImportError:
overload = lambda x: x
# All the names that get added to the inkex API itself.
__all__ = ('Transform', 'BoundingBox',)
if sys.version_info[0] == 3: # PY3
unicode = str # pylint: disable=redefined-builtin,invalid-name
class Vector2d(object):
"""
Represents an element of 2-dimensional Euclidean space
"""
x = 0.0
y = 0.0
@overload
def __init__(self): # type: () -> None
pass
@overload
def __init__(self, x, y): # type: (float, float) -> None
pass
@overload
def __init__(self, v): # type: (VectorLike) -> None
pass
def __init__(self, *args):
if len(args) == 0:
self.x, self.y = 0.0, 0.0
return
if len(args) == 1:
point = args[0]
if isinstance(point, Vector2d):
self.x, self.y = point.x, point.y
return
elif isinstance(point, (tuple, list)) and len(point) == 2:
self.x, self.y = point
return
elif len(args) == 2:
x, y = args
if isinstance(x, (int, float)) and isinstance(y, (int, float)):
self.x, self.y = x, y
return
raise ValueError("Vector2d can't be constructed from {}".format(repr(args)))
def __add__(self, other): # type: (VectorLike) -> VectorLike
other = Vector2d(other)
return Vector2d(self.x + other.x, self.y + other.y)
def __iadd__(self, other): # type: (VectorLike) -> VectorLike
other = Vector2d(other)
self.x += other.x
self.y += other.y
return self
def __radd__(self, other): # type: (VectorLike) -> VectorLike
other = Vector2d(other)
return Vector2d(self.x + other.x, self.y + other.y)
def __sub__(self, other): # type: (VectorLike) -> VectorLike
other = Vector2d(other)
return Vector2d(self.x - other.x, self.y - other.y)
def __isub__(self, other): # type: (VectorLike) -> VectorLike
other = Vector2d(other)
self.x -= other.x
self.y -= other.y
return self
def __abs__(self):
return self.length
@overload
def assign(self, x, y): # type: (float, float) -> None
pass
@overload
def assign(self, other): # type: (VectorLike) -> None
pass
def assign(self, *args):
self.x, self.y = Vector2d(*args)
def __rsub__(self, other): # type: (VectorLike) -> VectorLike
other = Vector2d(other)
return Vector2d(-self.x + other.x, -self.y + other.y)
def __neg__(self): # type: () -> VectorLike
return Vector2d(-self.x, -self.y)
def __pos__(self): # type: () -> VectorLike
return Vector2d(self.x, self.y)
def __floordiv__(self, factor): # type: (float) -> VectorLike
return Vector2d(self.x / float(factor), self.y / float(factor))
def __truediv__(self, factor): # type: (float) -> VectorLike
return Vector2d(self.x / float(factor), self.y / float(factor))
def __div__(self, factor): # type: (float) -> VectorLike
return Vector2d(self.x / float(factor), self.y / float(factor))
def __mul__(self, factor): # type: (float) -> VectorLike
return Vector2d(self.x * factor, self.y * factor)
def __imul__(self, factor): # type: (float) -> VectorLike
self.x *= factor
self.y *= factor
return self
def __rmul__(self, factor): # type: (float) -> VectorLike
return Vector2d(self.x * factor, self.y * factor)
def __repr__(self):
return "Vector2d({:.6g}, {:.6g})".format(self.x, self.y)
def __str__(self):
return "{:.6g}, {:.6g}".format(self.x, self.y)
def __iter__(self):
yield self.x
yield self.y
def __len__(self):
return 2
def __getitem__(self, item):
return (self.x, self.y)[item]
def to_tuple(self):
return self.x, self.y
def dot(self, other): # type: (VectorLike) -> float
other = Vector2d(other)
return self.x * other.x + self.y * other.y
def is_close(self, other, rtol=1e-5, atol=1e-8
): # type: (Union[VectorLike,Tuple[float,float]], Optional[float], Optional[float]) -> float
other = Vector2d(other)
delta = (self-other).length
return delta < (atol + rtol * other.length)
@property
def length(self): # type: () -> float
return sqrt(fabs(self.dot(self)))
class Scale(object): # pylint: disable=too-few-public-methods
"""A pair of numbers that represent the minimum and maximum values."""
def __init__(self, value=None, *others):
if isinstance(value, Scale):
self.maximum = value.maximum
self.minimum = value.minimum
elif isinstance(value, (tuple, list)) and len(value) == 2:
if value[0] is not None:
self.minimum, self.maximum = min(value), max(value)
else:
self.minimum, self.maximum = value
elif isinstance(value, (int, float, Decimal)):
self.minimum = value
self.maximum = value
elif value is None:
self.minimum = None
self.maximum = None
else:
raise ValueError("Not a number for scaling: {} ({})" \
.format(str(value), type(value).__name__))
for item in others:
self += item
def __bool__(self):
return self.minimum is not None and self.maximum is not None
__nonzero__ = __bool__
def __add__(self, other):
return self.__iadd__(other)
def __iadd__(self, other):
other = Scale(other)
if self.minimum is None:
self.minimum = other.minimum
elif other.minimum is not None:
self.minimum = min((self.minimum, other.minimum))
if self.maximum is None:
self.maximum = other.maximum
elif other.maximum is not None:
self.maximum = max((self.maximum, other.maximum))
return self
def __radd__(self, other):
if other != 0: # ignore sum() initial value
return self + other
return self
def __mul__(self, other):
new = Scale(self)
if other is not None:
new *= other
return new
def __imul__(self, other):
self.minimum *= other
self.maximum *= other
return self
def __iter__(self):
yield self.minimum
yield self.maximum
def __eq__(self, other):
return tuple(self) == tuple(Scale(other))
def __contains__(self, item):
if self.minimum is None or self.maximum is None:
return False
return self.minimum <= item <= self.maximum
def __repr__(self):
return "scale:" + str(tuple(self))
@property
def center(self):
"""Pick the middle of the line"""
if self.minimum is None or self.maximum is None:
return None
return self.minimum + ((self.maximum - self.minimum) / 2)
@property
def size(self):
"""Return the size difference minimum and maximum"""
if self.minimum is None or self.maximum is None:
return None
return self.maximum - self.minimum
[docs]class BoundingBox(object): # pylint: disable=too-few-public-methods
"""
Some functions to compute a rough bbox of a given list of objects.
BoundingBox() - Empty bounding box, bool == False
BoundingBox(x)
BoundingBox(x, y)
BoundingBox((x1, x2, y1, y2))
BoundingBox((x1, x2), (y1, y2))
BoundingBox(((x1, y1), (x2, y2)))
"""
width = property(lambda self: self.x.size)
height = property(lambda self: self.y.size)
top = property(lambda self: self.y.minimum)
left = property(lambda self: self.x.minimum)
bottom = property(lambda self: self.y.maximum)
right = property(lambda self: self.x.maximum)
center_x = property(lambda self: self.x.center)
center_y = property(lambda self: self.y.center)
def __init__(self, x=None, y=None):
if y is None:
if isinstance(x, BoundingBox):
x, y = x.x, x.y
elif isinstance(x, (list, tuple)):
if len(x) == 2:
if isinstance(x[0], (list, tuple)):
y = x[0][1], x[1][1]
x = x[0][0], x[1][0]
else:
x, y = x
elif len(x) == 4:
x, y = x[:2], x[2:]
self.x = Scale(x)
self.y = Scale(y)
def __bool__(self):
return bool(self.x) and bool(self.y)
__nonzero__ = __bool__
def __add__(self, other):
new = BoundingBox(self.x, self.y)
if other is not None:
new += other
return new
def __iadd__(self, other):
other = BoundingBox(other)
self.x += other.x
self.y += other.y
return self
def __radd__(self, other):
if other != 0:
return self + other
return self
def __mul__(self, other):
new = BoundingBox(self.x, self.y)
if other is not None:
new *= other
return new
def __imul__(self, other):
self.x *= other
self.y *= other
return self
def __eq__(self, other):
if isinstance(other, (tuple, BoundingBox)):
return tuple(self) == tuple(other)
return False
def __iter__(self):
yield self.x.minimum
yield self.x.maximum
yield self.y.minimum
yield self.y.maximum
@property
def minimum(self):
"""Return the minimum x,y coords"""
return (self.x.minimum, self.y.minimum)
@property
def maximum(self):
"""Return the maximum x,y coords"""
return (self.x.maximum, self.y.maximum)
def __getitem__(self, index):
return list(self)[index]
def __repr__(self):
return "BoundingBox({})".format(str(tuple(self)))
[docs] def center(self):
"""Returns the middle of the bounding box"""
return Vector2d(self.x.center, self.y.center)
class DirectedLineSegment(object):
"""
A directed line segment
DirectedLineSegment(((x0, y0), (x1, y1)))
"""
start = Vector2d() # start point of segment
end = Vector2d() # end point of segment
x0 = property(lambda self: self.start.x) # pylint: disable=invalid-name
y0 = property(lambda self: self.start.y) # pylint: disable=invalid-name
x1 = property(lambda self: self.end.x)
y1 = property(lambda self: self.end.y)
dx = property(lambda self: self.x1 - self.x0) # pylint: disable=invalid-name
dy = property(lambda self: self.y1 - self.y0) # pylint: disable=invalid-name
@overload
def __init__(self): # type: () -> None
pass
@overload
def __init__(self, other): # type: (DirectedLineSegment) -> None
pass
@overload
def __init__(self, start, end): # type: (VectorLike, VectorLike) -> None
pass
def __init__(self, *args):
if not args: # overload 0
start, end = Vector2d(), Vector2d()
if len(args) == 1: # overload 1
other, = args
start, end = other.start, other.end
elif len(args) == 2: # overload 2
start, end = args
else:
raise ValueError("DirectedLineSegment() can't be constructed from {}".format(args))
self.start = Vector2d(start)
self.end = Vector2d(end)
def __eq__(self, other):
if isinstance(other, (tuple, DirectedLineSegment)):
return tuple(self) == tuple(other)
return False
def __iter__(self):
yield self.x0
yield self.x1
yield self.y0
yield self.y1
@property
def length(self):
"""Get the length from the top left to the bottom right of the line"""
return sqrt((self.dx ** 2) + (self.dy ** 2))
@property
def angle(self):
"""Get the angle of the line created by this segment"""
return pi * (atan2(self.dy, self.dx)) / 180
def distance_to_point(self, x, y):
"""Get the distance to the given point (x, y)"""
segment2 = DirectedLineSegment(self.start, (x, y))
dot2 = segment2.dot(self)
if dot2 <= 0:
return DirectedLineSegment((x, y), self.start).length
if self.dot(self) <= dot2:
return DirectedLineSegment((x, y), self.end).length
return self.perp_distance(x, y)
def perp_distance(self, x, y):
"""Perpendicular distance to the given point"""
if self.length == 0:
return None
return fabs((self.dx * (self.y0 - y)) - ((self.x0 - x) * self.dy)) / self.length
def dot(self, other): # type: (DirectedLineSegment) -> float
"""Get the dot product with the segment with another"""
return self.dx * other.dx + self.dy * other.dy
def point_at_ratio(self, ratio):
"""Get the point at the given ratio along the line"""
return self.x0 + ratio * self.dx, self.y0 + ratio * self.dy
def point_at_length(self, length):
"""Get the point as the length along the line"""
return self.point_at_ratio(length / self.length)
def parallel(self, x, y):
"""Create parallel Segment"""
return DirectedLineSegment((x + self.dx, y + self.dy), (x, y))
def intersect(self, other): # type: (DirectedLineSegment) -> Optional[Vector2d]
"""Get the intersection between two segments"""
other = DirectedLineSegment(other)
denom = (other.dy * self.dx) - (other.dx * self.dy)
num = (other.dx * (self.y0 - other.y0)) - (other.dy * (self.x0 - other.x0))
# num2 = (self.width * (self.top - other.top)) - (self.height * (self.left - other.left))
if denom != 0:
return Vector2d(
self.x0 + ((num / denom) * (other.x1 - self.x0)),
self.y0 + ((num / denom) * (other.y0 - self.y0))
)
return None
def __repr__(self):
return "DirectedLineSegment(({0.start}), ({0.end}))".format(self)
def cubic_extrema(py0, py1, py2, py3):
"""Returns the extreme value, given a set of bezier coordinates"""
atol = 1e-9
cmin, cmax = min(py0, py3), max(py0, py3)
pd1 = py1 - py0
pd2 = py2 - py1
pd3 = py3 - py2
def _is_bigger(point):
if (point > 0) and (point < 1):
pyx = py0 * (1 - point) * (1 - point) * (1 - point) + \
3 * py1 * point * (1 - point) * (1 - point) + \
3 * py2 * point * point * (1 - point) + \
py3 * point * point * point
return min(cmin, pyx), max(cmax, pyx)
return cmin, cmax
if fabs(pd1 - 2 * pd2 + pd3)>atol:
if pd2 * pd2 > pd1 * pd3:
pds = sqrt(pd2 * pd2 - pd1 * pd3)
cmin, cmax = _is_bigger((pd1 - pd2 + pds) / (pd1 - 2 * pd2 + pd3))
cmin, cmax = _is_bigger((pd1 - pd2 - pds) / (pd1 - 2 * pd2 + pd3))
elif fabs(pd2 - pd1)>atol:
cmin, cmax = _is_bigger(-pd1 / (2 * (pd2 - pd1)))
return cmin, cmax
def quadratic_extrema(py0, py1, py2):
atol = 1e-9
cmin, cmax = min(py0, py2), max(py0, py2)
def _is_bigger(point):
if (point > 0) and (point < 1):
pyx = py0 * (1 - point) * (1 - point) + \
2 * py1 * point * (1 - point) + \
py2 * point * point
return min(cmin, pyx), max(cmax, pyx)
return cmin, cmax
if fabs(py0 + py2 - 2 * py1) > atol:
cmin, cmax = _is_bigger((py0 - py1) / (py0 + py2 - 2 * py1))
return cmin, cmax