Skip to content

Commit

Permalink
fix: #389 initial PoC of SVG backend
Browse files Browse the repository at this point in the history
  • Loading branch information
rvodden committed Jan 29, 2022
1 parent ef7a148 commit 339b7a7
Show file tree
Hide file tree
Showing 11 changed files with 420 additions and 55 deletions.
148 changes: 97 additions & 51 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ numpy = "^1.19.5"
matplotlib = "^3.2.1"
scipy = "^1.7.3"
celluloid = "^0.2.0"
lxml = "^4.7.1"
sphinx = { version = "^4.1.1", optional=true }
sphinx_rtd_theme = { version = ">=0.5,<1.1", optional=true }
sphinx-autodoc-typehints = { version = "^1.11.1", optional=true }
Expand Down
4 changes: 0 additions & 4 deletions pysketcher/backend/matplotlib/_matplotlib_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@
from pysketcher.composition import Composition


# plt.rc("text", usetex=True)
# plt.rcParams["text.latex.preamble"] = r"\usepackage{amsmath}"


class MatplotlibBackend(Backend):
"""Simple interface for plotting. Makes use of Matplotlib for plotting."""

Expand Down
3 changes: 3 additions & 0 deletions pysketcher/backend/svg/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._svg_backend import SvgBackend

__all__ = ["SvgBackend"]
12 changes: 12 additions & 0 deletions pysketcher/backend/svg/_svg_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from abc import ABC, abstractmethod

from lxml import etree

from pysketcher._drawable import Drawable


class SvgAdapter(ABC):
@staticmethod
@abstractmethod
def plot(shape: Drawable, axes: etree.Element, defs: etree.Element):
pass
87 changes: 87 additions & 0 deletions pysketcher/backend/svg/_svg_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import logging
from typing import Callable, Dict, Tuple, Type, Union

from lxml import etree
import pysketcher as ps

from pysketcher.backend.backend import Backend
from ._svg_adapter import SvgAdapter
from ._svg_circle import SvgCircle
from ._svg_line import SvgLine


class SvgBackend(Backend):
"""Simple interface for plotting. Makes use of Matplotlib for plotting."""

_doc: etree.ElementTree
_root: etree.Element
_axes: etree.SubElement
_x_min: float
_y_min: float
_x_max: float
_y_max: float

def __init__(self, x_min, x_max, y_min, y_max):
self._x_min = x_min
self._x_max = x_max
self._y_min = y_min
self._y_max = y_max
self._camera = None
self._defs = None
self._root = etree.Element(
"svg",
attrib={
"xmlns": "http://www.w3.org/2000/svg",
"version": "1.1",
"width": f"{x_max - x_min}cm",
"height": f"{y_max - y_min}cm",
},
)
self._doc = etree.ElementTree(element=self._root)
self._configure_axes()
self._load_defs()

def _load_defs(self):
self._defs = etree.SubElement(self._root, "defs")

def _configure_axes(self):
# self._axes = eT.SubElement(self._root, "g", attrib={
# "transform": f"translate({self._x_min},{self._y_max}) scale(1,-1)"
# })
self._axes = etree.SubElement(self._root, "g")

def add(self, shape: ps.Drawable) -> None:
for typ, adapter in self._adapters.items():
if issubclass(shape.__class__, typ):
adapter.plot(shape, self._axes, self._defs)

def erase(self):
raise NotImplementedError("Erase is not yet implemented")

def show(self):
raise NotImplementedError("Show is not yet implemented")

def save(self, filename: str) -> None:
logging.info(f"Saving to {filename}.")
etree.indent(self._doc, space=" ", level=0)
self._doc.write(filename, xml_declaration=True, encoding="utf-8")

@property
def _adapters(self) -> Dict[Type, SvgAdapter]:
return {
ps.Circle: SvgCircle(),
ps.Line: SvgLine(),
}

def animate(
self,
func: Callable[[float], ps.Drawable],
interval: Union[Tuple[float, float], Tuple[float, float, float]],
):
raise NotImplementedError("Animation is not yet implemented")

def show_animation(self):
raise NotImplementedError("Animation is not yet implemented")

def save_animation(self, filename: str):
raise NotImplementedError("Animation is not yet implemented")
21 changes: 21 additions & 0 deletions pysketcher/backend/svg/_svg_circle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from lxml import etree

from ._svg_adapter import SvgAdapter
from ._svg_style import SvgStyle

import pysketcher as ps


class SvgCircle(SvgAdapter):
@staticmethod
def plot(shape: ps.Circle, axes: etree.Element, defs: etree.Element):
etree.SubElement(
axes,
"circle",
attrib={
"cx": f"{shape.center.x}cm",
"cy": f"{shape.center.y}cm",
"r": f"{shape.radius}cm",
**SvgStyle(shape.style).attribs(defs),
},
)
22 changes: 22 additions & 0 deletions pysketcher/backend/svg/_svg_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from lxml import etree

from ._svg_adapter import SvgAdapter
from ._svg_style import SvgStyle

import pysketcher as ps


class SvgLine(SvgAdapter):
@staticmethod
def plot(shape: ps.Line, axes: etree.Element, defs: etree.Element):
etree.SubElement(
axes,
"line",
attrib={
"x1": f"{shape.start.x}cm",
"y1": f"{shape.start.y}cm",
"x2": f"{shape.end.x}cm",
"y2": f"{shape.end.y}cm",
**SvgStyle(shape.style).attribs(defs),
},
)
159 changes: 159 additions & 0 deletions pysketcher/backend/svg/_svg_style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import pkgutil
from typing import Dict

from lxml import etree

import pysketcher as ps


class SvgStyle:
_style: ps.Style
LINE_STYLE_MAP = {
ps.Style.LineStyle.SOLID: None,
ps.Style.LineStyle.DOTTED: ".1",
ps.Style.LineStyle.DASHED: ".4 .1",
ps.Style.LineStyle.DASH_DOT: ".4 .1 .1 .1",
}

FILL_PATTERN_MAP = {
ps.Style.FillPattern.CIRCLE: "O",
ps.Style.FillPattern.CROSS: "x",
ps.Style.FillPattern.DOT: ".",
ps.Style.FillPattern.HORIZONTAL: "-",
ps.Style.FillPattern.SQUARE: "+",
ps.Style.FillPattern.STAR: "*",
ps.Style.FillPattern.SMALL_CIRCLE: "o",
ps.Style.FillPattern.VERTICAL: "|",
ps.Style.FillPattern.UP_LEFT_TO_RIGHT: "//",
ps.Style.FillPattern.UP_RIGHT_TO_LEFT: "upRightToLeft",
}

COLOR_MAP = {
ps.Style.Color.GREY: "Grey",
ps.Style.Color.BLACK: "Black",
ps.Style.Color.BLUE: "Blue",
ps.Style.Color.BROWN: "Brown",
ps.Style.Color.CYAN: "Cyan",
ps.Style.Color.GREEN: "Green",
ps.Style.Color.MAGENTA: "Magenta",
ps.Style.Color.ORANGE: "Orange",
ps.Style.Color.PURPLE: "Purple",
ps.Style.Color.RED: "Red",
ps.Style.Color.YELLOW: "Yellow",
ps.Style.Color.WHITE: "White",
}

ARROW_MAP = {
ps.Style.ArrowStyle.START: "startArrowHead",
ps.Style.ArrowStyle.END: "endArrowHead",
}

def __init__(self, style: ps.Style):
self._style = style

@property
def line_width(self) -> float:
return self._style.line_width

@property
def line_style(self) -> str:
return self.LINE_STYLE_MAP.get(self._style.line_style)

@property
def line_color(self):
return self.COLOR_MAP.get(self._style.line_color)

@property
def fill_color(self):
return self.COLOR_MAP.get(self._style.fill_color)

@property
def fill_pattern(self):
return self.FILL_PATTERN_MAP.get(self._style.fill_pattern)

@property
def arrow(self):
return self.ARROW_MAP.get(self._style.arrow)

def __str__(self):
return (
"line_style: %s, line_width: %s, line_color: %s,"
" fill_pattern: %s, fill_color: %s, arrow: %s"
% (
self.line_style,
self.line_width,
self.line_color,
self.fill_pattern,
self.fill_color,
self.arrow,
)
)

def _load_def(self, defs: etree.Element, df: str):
df = etree.fromstring(pkgutil.get_data(__name__, f"templates/{df}.xml"))
defs.append(df)

def attribs(self, defs: etree.Element) -> Dict[str, str]:
ret_dict = {}

self._process_line_settings(ret_dict)
self._process_fill_settings(defs, ret_dict)
self._process_arrows(defs, ret_dict)

return ret_dict

def _process_fill_settings(self, defs, ret_dict):
ret_dict["fill"] = self.fill_color if self.fill_color else "none"
if self.fill_pattern:
self._load_def(defs, self.fill_pattern)
ret_dict["fill"] = f"url(#{self.fill_pattern})"

def _process_line_settings(self, ret_dict):
if self.line_width:
ret_dict["stroke-width"] = f"{self.line_width}px"
if self.line_color:
ret_dict["stroke"] = self.line_color
if self.line_style:
ret_dict["stroke-dasharray"] = self.line_style

def _process_arrows(self, defs, ret_dict):
if self._style.arrow in [ps.Style.ArrowStyle.END, ps.Style.ArrowStyle.DOUBLE]:
end_arrow = self.ARROW_MAP[ps.Style.ArrowStyle.END]
self._load_def(defs, end_arrow)
ret_dict["marker-end"] = f"url(#{end_arrow})"
if self._style.arrow in [ps.Style.ArrowStyle.START, ps.Style.ArrowStyle.DOUBLE]:
start_arrow = self.ARROW_MAP[ps.Style.ArrowStyle.START]
self._load_def(defs, start_arrow)
ret_dict["marker-start"] = f"url(#{start_arrow})"


#
# class MatplotlibTextStyle(MatplotlibStyle):
# FONT_FAMILY_MAP = {
# TextStyle.FontFamily.SERIF: "serif",
# TextStyle.FontFamily.SANS: "sans-serif",
# TextStyle.FontFamily.MONO: "monospace",
# }
#
# ALIGNMENT_MAP = {
# TextStyle.Alignment.LEFT: "left",
# TextStyle.Alignment.RIGHT: "right",
# TextStyle.Alignment.CENTER: "center",
# }
#
# _style: TextStyle
#
# def __init__(self, text_style: TextStyle):
# super().__init__(text_style)
#
# @property
# def font_size(self) -> float:
# return self._style.font_size
#
# @property
# def font_family(self) -> str:
# return self.FONT_FAMILY_MAP.get(self._style.font_family)
#
# @property
# def alignment(self) -> str:
# return self.ALIGNMENT_MAP.get(self._style.alignment)
Empty file added tests/backend/svg/.gitignore
Empty file.
18 changes: 18 additions & 0 deletions tests/backend/svg/_test_svg_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pysketcher.backend.svg import SvgBackend

import numpy as np
import pysketcher as ps


class TestSvgBackend:
def test_svg_backend(self):
circle = ps.Circle(ps.Point(1.5, 1.5), 1)
circle.style.line_color = ps.Style.Color.RED
circle.set_fill_pattern(ps.Style.FillPattern.UP_RIGHT_TO_LEFT)
line = ps.Line(ps.Point(1.5, 1.5), circle(-np.pi / 4))
line.set_arrow(ps.Style.ArrowStyle.DOUBLE)
fig = ps.Figure(0, 3, 0, 3, backend=SvgBackend)
fig.add(circle)
fig.add(line)
fig.save("circle.svg")
assert False

0 comments on commit 339b7a7

Please sign in to comment.