# -*- coding: utf-8 -*-
"""This module contains classes for generating SVG documents."""
import typing as ty
from abc import ABC, abstractmethod
from dataclasses import dataclass
from cinemol.geometry import Point2D
from cinemol.style import Color, Fill
[docs]
class Shape2D(ABC):
"""Abstract base class for 2D shapes."""
[docs]
@abstractmethod
def to_svg(self) -> str:
"""
Return the SVG representation of the shape.
:return: The SVG representation of the shape.
:rtype: str
"""
[docs]
@dataclass
class Circle2D(Shape2D):
"""A 2D circle.
:param reference: The reference name of the circle.
:type reference: str
:param center: The center of the circle with shape (2,).
:type center: Point2D
:param float radius: The radius of the circle.
"""
reference: str
center: Point2D
radius: float
[docs]
def to_svg(self) -> str:
"""Return the SVG representation of the circle.
:return: The SVG representation of the circle.
:rtype: str
"""
cx, cy, r = self.center.x, self.center.y, self.radius
return f'<circle class="{self.reference}" cx="{cx:.3f}" cy="{cy:.3f}" r="{r:.3f}"/>'
[docs]
@dataclass
class Line2D(Shape2D):
"""A 2D line.
:param reference: The reference name of the line.
:type reference: str
:param start: The start point of the line with shape (2,).
:type start: Point2D
:param end: The end point of the line with shape (2,).
:type end: Point2D
"""
reference: str
start: Point2D
end: Point2D
[docs]
def to_svg(self) -> str:
"""Return the SVG representation of the line.
:return: The SVG representation of the line.
:rtype: str
"""
x1, y1, x2, y2 = self.start.x, self.start.y, self.end.x, self.end.y
return (
"<line "
f'class="{self.reference}" '
f'x1="{x1:.3f}" '
f'y1="{y1:.3f}" '
f'x2="{x2:.3f}" '
f'y2="{y2:.3f}"'
"/>"
)
[docs]
@dataclass
class Polygon2D(Shape2D):
"""
A 2D polygon.
:param reference: The reference name of the polygon.
:type reference: str
:param points: The vertices of the polygon.
:type points: ty.List[Point2D]
"""
reference: str
points: ty.List[Point2D] # Ordered list of points.
[docs]
def to_svg(self) -> str:
"""Return the SVG representation of the polygon.
:return: The SVG representation of the polygon.
:rtype: str
"""
points = " ".join([f"{p.x:.3f},{p.y:.3f}" for p in self.points])
return f'<polygon class="{self.reference}" points="{points}"/>'
[docs]
@dataclass
class ViewBox:
"""A view box.
:param min_x: The minimum x coordinate.
:type min_x: float
:param min_y: The minimum y coordinate.
:type min_y: float
:param width: The width of the view box.
:type width: float
:param height: The height of the view box.
:type height: float
"""
min_x: float
min_y: float
width: float
height: float
def __str__(self) -> str:
"""Return the string representation of the view box.
:return: The string representation of the view box.
:rtype: str
"""
return (
"ViewBox("
f"min_x={self.min_x:.3f}, "
f"min_y={self.min_y:.3f}, "
f"width={self.width:.3f}, "
f"height={self.height:.3f}"
")"
)
[docs]
def to_svg(self) -> str:
"""
Return the SVG representation of the view box.
:return: The SVG representation of the view box.
:rtype: str
"""
return f'viewBox="{self.min_x:.3f} {self.min_y:.3f} {self.width:.3f} {self.height:.3f}"'
[docs]
class Svg:
"""An SVG document."""
def __init__(
self,
view_box: ViewBox,
window: ty.Optional[ty.Tuple[float, float]] = None,
background_color: ty.Optional[Color] = None,
version: float = 1.0,
encoding: str = "UTF-8",
fills: ty.Optional[ty.List[Fill]] = None,
objects: ty.Optional[ty.List[Shape2D]] = None,
) -> None:
"""Initialize the SVG document.
:param view_box: The view box of the SVG document.
:type view_box: ViewBox
:param window: The window of the SVG document.
:type window: ty.Optional[ty.Tuple[float, float]]
:param background_color: The background color of the SVG document.
:type background_color: ty.Optional[Color]
:param version: The version of the SVG document.
:type version: float
:param encoding: The encoding of the SVG document.
:type encoding: str
:param fills: The fills of the SVG document.
:type fills: ty.List[Fill]
:param objects: The objects of the SVG document.
:type objects: ty.List[Shape2D]
"""
self.view_box = view_box
self.window = window
self.background_color = background_color
self.version = version
self.encoding = encoding
if fills is None:
fills = []
self.fills = fills
if objects is None:
objects = []
self.objects = objects
[docs]
def to_svg(self) -> str:
"""
Return the SVG representation of the SVG document.
:return: The SVG representation of the SVG document.
:rtype: str
"""
header = self.header()
footer = self.footer()
styles, definitions = [], []
for fill in self.fills:
style, definition = fill.to_svg()
if style is not None: # Should never be None.
styles.append(style)
if definition is not None:
definitions.append(definition)
styles_str = "\n".join(styles)
definitions_str = "\n".join(definitions)
objects_str = "\n".join([object.to_svg() for object in self.objects])
return (
f"{header}\n"
"<defs>\n"
"<style>\n"
f"{styles_str}\n"
"</style>\n"
f"{definitions_str}\n"
"</defs>\n"
f"{objects_str}\n"
f"{footer}"
)