197 lines
6.6 KiB
Python
197 lines
6.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
|
|
# Sergei Izmailov <sergei.a.izmailov@gmail.com>
|
|
# Ryan Jarvis <ryan@shopboxretail.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 group based elements such as Groups, Use, Markers etc.
|
|
"""
|
|
|
|
from lxml import etree # pylint: disable=unused-import
|
|
|
|
from ..paths import Path
|
|
from ..transforms import BoundingBox, Transform, Vector2d
|
|
|
|
from ._utils import addNS
|
|
from ._base import ShapeElement, ViewboxMixin
|
|
from ._polygons import PathElement
|
|
|
|
try:
|
|
from typing import Optional, List # pylint: disable=unused-import
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
class GroupBase(ShapeElement):
|
|
"""Base Group element"""
|
|
|
|
def get_path(self):
|
|
ret = Path()
|
|
for child in self:
|
|
if isinstance(child, ShapeElement):
|
|
child_path = child.path.transform(child.transform)
|
|
if child_path and child_path[0].is_relative:
|
|
child_path[0] = child_path[0].to_absolute(Vector2d(0, 0))
|
|
ret += child_path
|
|
return ret
|
|
|
|
def bounding_box(self, transform=None):
|
|
# type: (Optional[Transform]) -> Optional[BoundingBox]
|
|
"""BoundingBox of the shape
|
|
|
|
.. versionchanged:: 1.4
|
|
Exclude invisible child objects from bounding box computation
|
|
|
|
.. versionchanged:: 1.1
|
|
result adjusted for element's clip path if applicable.
|
|
"""
|
|
bbox = None
|
|
effective_transform = Transform(transform) @ self.transform
|
|
for child in self:
|
|
if isinstance(child, ShapeElement) and child.is_visible():
|
|
child_bbox = child.bounding_box(transform=effective_transform)
|
|
if child_bbox is not None:
|
|
bbox += child_bbox
|
|
clip = self.clip
|
|
if clip is None or bbox is None:
|
|
return bbox
|
|
return bbox & clip.bounding_box(Transform(transform) @ self.transform)
|
|
|
|
def shape_box(self, transform=None):
|
|
# type: (Optional[Transform]) -> Optional[BoundingBox]
|
|
"""BoundingBox of the unclipped shape
|
|
|
|
.. versionchanged:: 1.4
|
|
returns the bounding box without possible clip effects of child objects
|
|
|
|
.. versionadded:: 1.1
|
|
Previous :func:`bounding_box` function, returning the bounding box
|
|
without computing the effect of a possible clip."""
|
|
bbox = None
|
|
effective_transform = Transform(transform) @ self.transform
|
|
for child in self:
|
|
if isinstance(child, ShapeElement):
|
|
child_bbox = child.shape_box(transform=effective_transform)
|
|
if child_bbox is not None:
|
|
bbox += child_bbox
|
|
return bbox
|
|
|
|
def bake_transforms_recursively(self, apply_to_paths=True):
|
|
"""Bake transforms, i.e. each leaf node has the effective transform (starting
|
|
from this group) set, and parent transforms are removed.
|
|
|
|
.. versionadded:: 1.4
|
|
|
|
Args:
|
|
apply_to_paths (bool, optional): For path elements, the
|
|
path data is transformed with its effective transform. Nodes and handles
|
|
will have the same position as before, but visual appearance of the
|
|
stroke may change (stroke-width is not touched). Defaults to True.
|
|
"""
|
|
# pylint: disable=attribute-defined-outside-init
|
|
self.transform: Transform
|
|
for element in self:
|
|
if isinstance(element, PathElement) and apply_to_paths:
|
|
element.path = element.path.transform(self.transform)
|
|
else:
|
|
element.transform = self.transform @ element.transform
|
|
if isinstance(element, GroupBase):
|
|
element.bake_transforms_recursively(apply_to_paths)
|
|
self.transform = None
|
|
|
|
|
|
class Group(GroupBase):
|
|
"""Any group element (layer or regular group)"""
|
|
|
|
tag_name = "g"
|
|
|
|
@classmethod
|
|
def new(cls, label, *children, **attrs):
|
|
attrs["inkscape:label"] = label
|
|
return super().new(*children, **attrs)
|
|
|
|
def effective_style(self):
|
|
"""A blend of each child's style mixed together (last child wins)"""
|
|
style = self.style
|
|
for child in self:
|
|
style.update(child.effective_style())
|
|
return style
|
|
|
|
@property
|
|
def groupmode(self):
|
|
"""Return the type of group this is"""
|
|
return self.get("inkscape:groupmode", "group")
|
|
|
|
|
|
class Layer(Group):
|
|
"""Inkscape extension of svg:g"""
|
|
|
|
def _init(self):
|
|
self.set("inkscape:groupmode", "layer")
|
|
|
|
@classmethod
|
|
def is_class_element(cls, elem):
|
|
# type: (etree.Element) -> bool
|
|
return (
|
|
elem.get("{http://www.inkscape.org/namespaces/inkscape}groupmode", None)
|
|
== "layer"
|
|
)
|
|
|
|
|
|
class Anchor(GroupBase):
|
|
"""An anchor or link tag"""
|
|
|
|
tag_name = "a"
|
|
|
|
@classmethod
|
|
def new(cls, href, *children, **attrs):
|
|
attrs["xlink:href"] = href
|
|
return super().new(*children, **attrs)
|
|
|
|
|
|
class ClipPath(GroupBase):
|
|
"""A path used to clip objects"""
|
|
|
|
tag_name = "clipPath"
|
|
|
|
|
|
class Marker(GroupBase, ViewboxMixin):
|
|
"""The <marker> element defines the graphic that is to be used for drawing
|
|
arrowheads or polymarkers on a given <path>, <line>, <polyline> or <polygon>
|
|
element."""
|
|
|
|
tag_name = "marker"
|
|
|
|
def get_viewbox(self) -> List[float]:
|
|
"""Returns the viewbox of the Marker, falling back to
|
|
[0 0 markerWidth markerHeight]
|
|
|
|
.. versionadded:: 1.3"""
|
|
vbox = self.get("viewBox", None)
|
|
result = self.parse_viewbox(vbox)
|
|
if result is None:
|
|
# use viewport, https://www.w3.org/TR/SVG11/painting.html#MarkerElement
|
|
return [
|
|
0,
|
|
0,
|
|
self.to_dimensionless(self.get("markerWidth")),
|
|
self.to_dimensionless(self.get("markerHeight")),
|
|
]
|
|
return result
|