bundle: update (2026-01-18)

This commit is contained in:
2026-01-18 02:57:46 +00:00
parent e65941bb8c
commit f822f384e8
90 changed files with 24094 additions and 0 deletions

View File

@@ -0,0 +1,587 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
# Sergei Izmailov <sergei.a.izmailov@gmail.com>
# Thomas Holder <thomas.holder@schrodinger.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.
#
# pylint: disable=arguments-differ
"""
Interface for all shapes/polygons such as lines, paths, rectangles, circles etc.
"""
from __future__ import annotations
from math import cos, pi, sin
import math
from typing import Optional, Tuple, Union
from ..paths.interfaces import PathCommand
from ..paths import Arc, Curve, Move, Path, ZoneClose
from ..paths import Line as PathLine
from ..transforms import Transform, ImmutableVector2d, Vector2d
from ..bezier import pointdistance
from ._utils import addNS
from ._base import ShapeElement
class PathElementBase(ShapeElement):
"""Base element for path based shapes"""
def get_path(self) -> Path:
"""Gets the path of the element, which can also be used in a context manager"""
p = Path(self.get("d"))
p.callback = self.set_path
return p
@classmethod
def new(cls, path, **attrs):
return super().new(d=Path(path), **attrs)
def set_path(self, path):
"""Set the given data as a path as the 'd' attribute"""
self.set("d", str(Path(path)))
def apply_transform(self):
"""Apply the internal transformation to this node and delete"""
if "transform" in self.attrib:
self.path = self.path.transform(self.transform)
self.set("transform", Transform())
@property
def original_path(self):
"""Returns the original path if this is a LPE, or the path if not"""
return Path(self.get("inkscape:original-d", self.path))
@original_path.setter
def original_path(self, path):
if addNS("inkscape:original-d") in self.attrib:
self.set("inkscape:original-d", str(Path(path)))
else:
self.path = path
class PathElement(PathElementBase):
"""Provide a useful extension for path elements"""
tag_name = "path"
MAX_ARC_SUBDIVISIONS = 4
@staticmethod
def _arcpath(
cx: float,
cy: float,
rx: float,
ry: float,
start: float,
end: float,
arctype: str,
) -> Optional[Path]:
"""Compute the path for an arc defined by Inkscape-specific attributes.
For details on arguments, see :func:`arc`.
.. versionadded:: 1.2"""
if abs(rx) < 1e-8 or abs(ry) < 1e-8:
return None
incr = end - start
if incr < 0:
incr += 2 * pi
numsegs = min(1 + int(incr * 2.0 / pi), PathElement.MAX_ARC_SUBDIVISIONS)
incr = incr / numsegs
computed = Path()
computed.append(Move(cos(start), sin(start)))
for seg in range(1, numsegs + 1):
computed.append(
Arc(
1,
1,
0,
incr > pi,
1,
cos(start + seg * incr),
sin(start + seg * incr),
)
)
if abs(incr * numsegs - 2 * pi) > 1e-8 and (
arctype in ("slice", "")
): # slice is default
computed.append(PathLine(0, 0))
if arctype != "arc":
computed.append(ZoneClose())
computed.transform(
Transform().add_translate(cx, cy).add_scale(rx, ry), inplace=True
)
return computed.to_relative()
@classmethod
def arc(cls, center, rx, ry=None, arctype="", pathonly=False, **kw): # pylint: disable=invalid-name
"""Generates a sodipodi elliptical arc (special type). Also computes the path
that Inkscape uses under the hood.
All data may be given as parseable strings or using numeric data types.
Args:
center (tuple-like): Coordinates of the star/polygon center as tuple or
Vector2d
rx (Union[float, str]): Radius in x direction
ry (Union[float, str], optional): Radius in y direction. If not given,
ry=rx. Defaults to None.
arctype (str, optional): "arc", "chord" or "slice". Defaults to "", i.e.
"slice".
.. versionadded:: 1.2
Previously set to "arc" as fixed value
pathonly (bool, optional): Whether to create the path without
Inkscape-specific attributes. Defaults to False.
.. versionadded:: 1.2
Keyword args:
start (Union[float, str]): start angle in radians
end (Union[float, str]): end angle in radians
open (str): whether the path should be open (true/false). Not used in
Inkscape > 1.1
Returns:
PathElement : the created star/polygon
"""
others = [(name, kw.pop(name, None)) for name in ("start", "end", "open")]
elem = cls(**kw)
elem.set("sodipodi:cx", center[0])
elem.set("sodipodi:cy", center[1])
elem.set("sodipodi:rx", rx)
elem.set("sodipodi:ry", ry or rx)
elem.set("sodipodi:type", "arc")
if arctype != "":
elem.set("sodipodi:arc-type", arctype)
for name, value in others:
if value is not None:
elem.set("sodipodi:" + name, str(value).lower())
path = cls._arcpath(
float(center[0]),
float(center[1]),
float(rx),
float(ry or rx),
float(elem.get("sodipodi:start", 0)),
float(elem.get("sodipodi:end", 2 * pi)),
arctype,
)
if pathonly:
elem = cls(**kw)
if path is not None:
elem.path = path
return elem
@classmethod
def arc_from_3_points(
cls,
x: complex,
y: complex,
z: complex,
arctype="slice",
) -> PathElement:
"""
Create an arc through the points x, y, z.
If those points are specified clockwise, the order is not preserved.
This is indicated with the second return value (=clockwise)
Returns a PathElement. May be a line if x,y,z are collinear.
Idea: http://www.math.okstate.edu/~wrightd/INDRA/MobiusonCircles/node4.html
.. versionadded:: 1.4
"""
w = (z - x) / (y - x)
if abs(w.imag) > 1e-12:
c = -((x - y) * (w - abs(w) ** 2) / (2j * w.imag) - x)
r = abs(c - x)
# Now determine the arc flags by checking the angles
deltas = [x - c, y - c, z - c]
ang = [math.atan2(i.imag, i.real) for i in deltas]
# Check if the angles are "in order"
cw = int(any(ang[0 + i] < ang[-2 + i] < ang[-1 + i] for i in range(3)))
if not cw:
# Flip start and end angle
ang = ang[::-1]
return cls.arc(Vector2d(c), r, r, start=ang[0], end=ang[2], arctype=arctype)
else:
# Points lie on a line
# y between x and z -> draw a line, otherwise skip
if x.real <= y.real <= z.real or x.real >= y.real >= z.real:
return cls.new(Path([Move(x), PathLine(z)]))
else:
return cls.new(Path([Move(x), Move(z)]))
@staticmethod
def _starpath(
c: Tuple[float, float],
sides: int,
r: Tuple[float, float], # pylint: disable=invalid-name
arg: Tuple[float, float],
rounded: float,
flatsided: bool,
):
"""Helper method to generate the path for an Inkscape star/ polygon; randomized
is ignored.
For details on arguments, see :func:`star`.
.. versionadded:: 1.2"""
def _star_get_xy(point, index):
cur_arg = arg[point] + 2 * pi / sides * (index % sides)
return Vector2d(*c) + r[point] * Vector2d(cos(cur_arg), sin(cur_arg))
def _rot90_rel(origin, other):
"""Returns a unit length vector at 90 deg from origin to other"""
return (
1
/ pointdistance(other, origin)
* Vector2d(other.y - origin.y, other.x - origin.x)
)
def _star_get_curvepoint(point, index, is_prev: bool):
index = index % sides
orig = _star_get_xy(point, index)
previ = (index - 1 + sides) % sides
nexti = (index + 1) % sides
# neighbors of the current point depend on polygon or star
prev = (
_star_get_xy(point, previ)
if flatsided
else _star_get_xy(1 - point, index if point == 1 else previ)
)
nextp = (
_star_get_xy(point, nexti)
if flatsided
else _star_get_xy(1 - point, index if point == 0 else nexti)
)
mid = 0.5 * (prev + nextp)
# direction of bezier handles
rot = _rot90_rel(orig, mid + 100000 * _rot90_rel(mid, nextp))
ret = (
rounded
* rot
* (
-1 * pointdistance(prev, orig)
if is_prev
else pointdistance(nextp, orig)
)
)
return orig + ret
pointy = abs(rounded) < 1e-4
result = Path()
result.append(Move(*_star_get_xy(0, 0)))
for i in range(0, sides):
# draw to point type 1 for stars
if not flatsided:
if pointy:
result.append(PathLine(*_star_get_xy(1, i)))
else:
result.append(
Curve(
*_star_get_curvepoint(0, i, False),
*_star_get_curvepoint(1, i, True),
*_star_get_xy(1, i),
)
)
# draw to point type 0 for both stars and rectangles
if pointy and i < sides - 1:
result.append(PathLine(*_star_get_xy(0, i + 1)))
if not pointy:
if not flatsided:
result.append(
Curve(
*_star_get_curvepoint(1, i, False),
*_star_get_curvepoint(0, i + 1, True),
*_star_get_xy(0, i + 1),
)
)
else:
result.append(
Curve(
*_star_get_curvepoint(0, i, False),
*_star_get_curvepoint(0, i + 1, True),
*_star_get_xy(0, i + 1),
)
)
result.append(ZoneClose())
return result.to_relative()
@classmethod
def star(
cls,
center,
radii,
sides=5,
rounded=0,
args=(0, 0),
flatsided=False,
pathonly=False,
):
"""Generate a sodipodi star / polygon. Also computes the path that Inkscape uses
under the hood. The arguments for center, radii, sides, rounded and args can be
given as strings or as numeric data.
.. versionadded:: 1.1
Args:
center (Tuple-like): Coordinates of the star/polygon center as tuple or
Vector2d
radii (tuple): Radii of the control points, i.e. their distances from the
center. The control points are specified in polar coordinates. Only the
first control point is used for polygons.
sides (int, optional): Number of sides / tips of the polygon / star.
Defaults to 5.
rounded (int, optional): Controls the rounding radius of the polygon / star.
For `rounded=0`, only straight lines are used. Defaults to 0.
args (tuple, optional): Angle between horizontal axis and control points.
Defaults to (0,0).
.. versionadded:: 1.2
Previously fixed to (0.85, 1.3)
flatsided (bool, optional): True for polygons, False for stars.
Defaults to False.
.. versionadded:: 1.2
pathonly (bool, optional): Whether to create the path without
Inkscape-specific attributes. Defaults to False.
.. versionadded:: 1.2
Returns:
PathElement : the created star/polygon
"""
elem = cls()
elem.set("sodipodi:cx", center[0])
elem.set("sodipodi:cy", center[1])
elem.set("sodipodi:r1", radii[0])
elem.set("sodipodi:r2", radii[1])
elem.set("sodipodi:arg1", args[0])
elem.set("sodipodi:arg2", args[1])
elem.set("sodipodi:sides", max(sides, 3) if flatsided else max(sides, 2))
elem.set("inkscape:rounded", rounded)
elem.set("inkscape:flatsided", str(flatsided).lower())
elem.set("sodipodi:type", "star")
path = cls._starpath(
(float(center[0]), float(center[1])),
int(sides),
(float(radii[0]), float(radii[1])),
(float(args[0]), float(args[1])),
float(rounded),
flatsided,
)
if pathonly:
elem = cls()
# inkex.errormsg(path)
if path is not None:
elem.path = path
return elem
class Polyline(ShapeElement):
"""Like a path, but made up of straight line segments only"""
tag_name = "polyline"
def get_path(self) -> Path:
p = Path("M" + self.get("points"))
p.callback = self.set_path
return p
def set_path(self, path):
if type(path) != Path:
path = Path("M" + str(path))
points = [f"{x:g},{y:g}" for x, y in path.end_points]
self.set("points", " ".join(points))
@classmethod
def new(cls, points=None, **attrs):
p = super().new(**attrs)
p.path = points
return p
class Polygon(Polyline):
"""A closed polyline"""
tag_name = "polygon"
def get_path(self) -> Path:
p = Path("M" + self.get("points") + " Z")
p.callback = self.set_path
return p
class Line(ShapeElement):
"""A line segment connecting two points"""
tag_name = "line"
x1 = property(lambda self: self.to_dimensionless(self.get("x1", 0)))
y1 = property(lambda self: self.to_dimensionless(self.get("y1", 0)))
x2 = property(lambda self: self.to_dimensionless(self.get("x2", 0)))
y2 = property(lambda self: self.to_dimensionless(self.get("y2", 0)))
get_path = lambda self: Path(f"M{self.x1},{self.y1} L{self.x2},{self.y2}")
@classmethod
def new(cls, start, end, **attrs):
start = Vector2d(start)
end = Vector2d(end)
return super().new(x1=start.x, y1=start.y, x2=end.x, y2=end.y, **attrs)
class RectangleBase(ShapeElement):
"""Provide a useful extension for rectangle elements"""
left = property(lambda self: self.to_dimensionless(self.get("x", "0")))
top = property(lambda self: self.to_dimensionless(self.get("y", "0")))
right = property(lambda self: self.left + self.width)
bottom = property(lambda self: self.top + self.height)
width = property(lambda self: self.to_dimensionless(self.get("width", "0")))
height = property(lambda self: self.to_dimensionless(self.get("height", "0")))
rx = property(
lambda self: self.to_dimensionless(self.get("rx", self.get("ry", 0.0)))
)
ry = property(
lambda self: self.to_dimensionless(self.get("ry", self.get("rx", 0.0)))
) # pylint: disable=invalid-name
def get_path(self) -> Path:
"""Calculate the path as the box around the rect"""
if self.rx or self.ry:
# pylint: disable=invalid-name
rx = min(self.rx if self.rx > 0 else self.ry, self.width / 2)
ry = min(self.ry if self.ry > 0 else self.rx, self.height / 2)
cpts = [self.left + rx, self.right - rx, self.top + ry, self.bottom - ry]
return Path(
f"M {cpts[0]},{self.top}"
f"L {cpts[1]},{self.top} "
f"A {rx},{ry} 0 0 1 {self.right},{cpts[2]}"
f"L {self.right},{cpts[3]} "
f"A {rx},{ry} 0 0 1 {cpts[1]},{self.bottom}"
f"L {cpts[0]},{self.bottom} "
f"A {rx},{ry} 0 0 1 {self.left},{cpts[3]}"
f"L {self.left},{cpts[2]} "
f"A {rx},{ry} 0 0 1 {cpts[0]},{self.top} z"
)
return Path(
f"M {self.left},{self.top} h{self.width}v{self.height}h{-self.width} z"
)
class Rectangle(RectangleBase):
"""Provide a useful extension for rectangle elements"""
tag_name = "rect"
@classmethod
def new(cls, left, top, width, height, **attrs):
return super().new(x=left, y=top, width=width, height=height, **attrs)
class EllipseBase(ShapeElement):
"""Absorbs common part of Circle and Ellipse classes"""
def get_path(self) -> Path:
"""Calculate the arc path of this circle"""
rx, ry = self.rxry()
cx, y = self.center.x, self.center.y - ry
return Path(
(
"M {cx},{y} "
"a {rx},{ry} 0 1 0 {rx}, {ry} "
"a {rx},{ry} 0 0 0 -{rx}, -{ry} z"
).format(cx=cx, y=y, rx=rx, ry=ry)
)
@property
def center(self):
"""Return center of circle/ellipse"""
return ImmutableVector2d(
self.to_dimensionless(self.get("cx", "0")),
self.to_dimensionless(self.get("cy", "0")),
)
@center.setter
def center(self, value):
value = Vector2d(value)
self.set("cx", value.x)
self.set("cy", value.y)
def rxry(self):
# type: () -> Vector2d
"""Helper function"""
raise NotImplementedError()
@classmethod
def new(cls, center, radius, **attrs):
circle = super().new(**attrs)
circle.center = center
circle.radius = radius
return circle
class Circle(EllipseBase):
"""Provide a useful extension for circle elements"""
tag_name = "circle"
@property
def radius(self) -> float:
"""Return radius of circle"""
return self.to_dimensionless(self.get("r", "0"))
@radius.setter
def radius(self, value):
self.set("r", self.to_dimensionless(value))
def rxry(self):
r = self.radius
return Vector2d(r, r)
class Ellipse(EllipseBase):
"""Provide a similar extension to the Circle interface for ellipses"""
tag_name = "ellipse"
@property
def radius(self) -> ImmutableVector2d:
"""Return radii of ellipse"""
return ImmutableVector2d(
self.to_dimensionless(self.get("rx", "0")),
self.to_dimensionless(self.get("ry", "0")),
)
@radius.setter
def radius(self, value):
value = Vector2d(value)
self.set("rx", str(value.x))
self.set("ry", str(value.y))
def rxry(self):
return self.radius