"""TikZ exporter. Draws shapes using the TikZ package for LaTeX.
Sketch objects are converted to TikZ code."""
# This is a proof of concept.
# To do: This whole module needs to be restructured.
from __future__ import annotations
from math import degrees, cos, sin, ceil
from typing import List, Union
from dataclasses import dataclass, field
import numpy as np
import simetri.graphics as sg
from ..graphics.common import common_properties
from ..graphics.all_enums import (
BackStyle,
FontSize,
FontFamily,
MarkerType,
ShadeType,
Types,
TexLoc,
FrameShape,
DocumentClass,
Align,
ArrowLine,
BlendMode,
get_enum_value,
LineWidth,
LineDashArray,
)
from ..canvas.style_map import shape_style_map, line_style_map, marker_style_map
from ..settings.settings import defaults, tikz_defaults
from ..geometry.geometry import homogenize, close_points2
from ..geometry.ellipse import ellipse_point
from ..graphics.sketch import TagSketch, ShapeSketch
from ..graphics.shape import Shape
from ..colors import Color
np.set_printoptions(legacy="1.21")
array = np.array
enum_map = {}
[docs]
def scope_code_required(item: Union["Canvas", "Batch"]) -> bool:
"""Check if a TikZ namespace is required for the item.
Args:
item (Union["Canvas", "Batch"]): The item to check.
Returns:
bool: True if a TikZ namespace is required, False otherwise.
"""
return (
item.blend_mode is not None
or item.transparency_group
or (item.clip and item.mask)
)
[docs]
@dataclass
class Tex:
"""Tex class for generating tex code.
Attributes:
begin_document (str): The beginning of the document.
end_document (str): The end of the document.
begin_tikz (str): The beginning of the TikZ environment.
end_tikz (str): The end of the TikZ environment.
packages (List[str]): List of required TeX packages.
tikz_libraries (List[str]): List of required TikZ libraries.
tikz_code (str): The generated TikZ code.
sketches (List["Sketch"]): List of TexSketch objects.
"""
begin_document: str = defaults["begin_doc"]
end_document: str = defaults["end_doc"]
begin_tikz: str = defaults["begin_tikz"]
end_tikz: str = defaults["end_tikz"]
packages: List[str] = None
tikz_libraries: List[str] = None
tikz_code: str = "" # Generated by the canvas by using sketches
sketches: List["Sketch"] = field(default_factory=list) # List of TexSketch objects
def __post_init__(self):
"""Post-initialization method."""
self.type = Types.TEX
common_properties(self)
[docs]
def tex_code(self, canvas: "Canvas", aux_code: str) -> str:
"""Generate the final TeX code.
Args:
canvas ("Canvas"): The canvas object.
aux_code (str): Auxiliary code to include.
Returns:
str: The final TeX code.
"""
doc_code = []
for sketch in self.sketches:
if sketch.location == TexLoc.DOCUMENT:
doc_code.append(sketch.code)
doc_code = "\n".join(doc_code)
back_color = f"\\pagecolor{color2tikz(canvas.back_color)}"
self.begin_document = self.begin_document + back_color + "\n"
if canvas.limits is not None:
begin_tikz = self.begin_tikz + get_limits_code(canvas) + "\n"
else:
begin_tikz = self.begin_tikz + "\n"
if scope_code_required(canvas):
scope = get_canvas_scope(canvas)
code = (
self.get_preamble(canvas)
+ self.begin_document
+ doc_code
+ begin_tikz
+ scope
+ self.get_tikz_code()
+ aux_code
+ "\\end{scope}\n"
+ self.end_tikz
+ self.end_document
)
else:
code = (
self.get_preamble(canvas)
+ self.begin_document
+ doc_code
+ begin_tikz
+ self.get_tikz_code()
+ aux_code
+ self.end_tikz
+ self.end_document
)
return code
[docs]
def get_doc_class(self, border: float, font_size: int) -> str:
"""Returns the document class.
Args:
border (float): The border size.
font_size (int): The font size.
Returns:
str: The document class string.
"""
return f"\\documentclass[{font_size}pt,tikz,border={border}pt]{{standalone}}\n"
[docs]
def get_tikz_code(self) -> str:
"""Returns the TikZ code.
Returns:
str: The TikZ code.
"""
code = ""
for sketch in self.sketches:
if sketch.location == TexLoc.PICTURE:
code += sketch.text + "\n"
return code
[docs]
def get_tikz_libraries(self) -> str:
"""Returns the TikZ libraries.
Returns:
str: The TikZ libraries string.
"""
return f"\\usetikzlibrary{{{','.join(self.tikz_libraries)}}}\n"
[docs]
def get_packages(self, canvas) -> str:
"""Returns the required TeX packages.
Args:
canvas: The canvas object.
Returns:
str: The required TeX packages.
"""
tikz_libraries = []
tikz_packages = ['tikz', 'pgf']
for page in canvas.pages:
for sketch in page.sketches:
if sketch.draw_markers:
if 'patterns' not in tikz_libraries:
tikz_libraries.append("patterns")
tikz_libraries.append("patterns.meta")
tikz_libraries.append("backgrounds")
tikz_libraries.append("shadings")
if sketch.subtype == Types.TAG_SKETCH:
if "fontspec" not in tikz_packages:
tikz_packages.append("fontspec")
else:
if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
if "fontspec" not in tikz_packages:
tikz_packages.append("fontspec")
if hasattr(sketch, 'back_style'):
if sketch.back_style == BackStyle.COLOR:
if "xcolor" not in tikz_packages:
tikz_packages.append("xcolor")
if sketch.back_style == BackStyle.SHADING:
if "shadings" not in tikz_libraries:
tikz_libraries.append("shadings")
if sketch.back_style == BackStyle.PATTERN:
if "patterns" not in tikz_libraries:
tikz_libraries.append("patterns")
tikz_libraries.append("patterns.meta")
return tikz_libraries, tikz_packages
[docs]
def get_preamble(self, canvas) -> str:
"""Returns the TeX preamble.
Args:
canvas: The canvas object.
Returns:
str: The TeX preamble.
"""
libraries, packages = self.get_packages(canvas)
if packages:
packages = f'\\usepackage{{{",".join(packages)}}}\n'
if 'fontspec' in packages:
fonts_section = f"""\\setmainfont{{{defaults['main_font']}}}
\\setsansfont{{{defaults['sans_font']}}}
\\setmonofont{{{defaults['mono_font']}}}\n"""
if libraries:
libraries = f'\\usetikzlibrary{{{",".join(libraries)}}}\n'
if canvas.border is None:
border = defaults["border"]
elif isinstance(canvas.border, (int, float)):
border = canvas.border
else:
raise ValueError("Canvas.border must be a positive numeric value.")
if border < 0:
raise ValueError("Canvas.border must be a positive numeric value.")
doc_class = self.get_doc_class(border, defaults["font_size"])
# Check if different fonts are used
fonts_section = ""
fonts = canvas.get_fonts_list()
for font in fonts:
if font is None:
continue
font_family = font.replace(" ", "")
fonts_section += f"\\newfontfamily\\{font_family}[Scale=1.0]{{{font}}}\n"
preamble = f"{doc_class}{packages}{libraries}{fonts_section}"
indices = False
for sketch in canvas.active_page.sketches:
if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
indices = True
break
if indices:
font_family = defaults["indices_font_family"]
font_size = defaults["indices_font_size"]
count = 0
for sketch in canvas.active_page.sketches:
if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
preamble += "\\tikzset{\n"
node_style = (
f"nodestyle{count}/.style={{draw, circle, gray, "
f"text=black, fill=white, line width = .5, inner sep=.5, "
f"font=\\{font_family}\\{font_size}}}\n}}\n"
)
preamble += node_style
count += 1
return preamble
[docs]
def get_back_grid_code(grid: Grid, canvas: "Canvas") -> str:
"""Page background grid code.
Args:
grid (Grid): The grid object.
canvas ("Canvas"): The canvas object.
Returns:
str: The background grid code.
"""
# \usetikzlibrary{backgrounds}
# \begin{scope}[on background layer]
# \fill[gray] (current bounding box.south west) rectangle
# (current bounding box.north east);
# \draw[white,step=.5cm] (current bounding box.south west) grid
# (current bounding box.north east);
# \end{scope}
grid = canvas.active_page.grid
back_color = color2tikz(grid.back_color)
line_color = color2tikz(grid.line_color)
step = grid.spacing
lines = ["\\begin{scope}[on background layer]\n"]
lines.append(f"\\fill[color={back_color}] (current bounding box.south west) ")
lines.append("rectangle (current bounding box.north east);\n")
options = []
if grid.line_dash_array is not None:
options.append(f"dashed, dash pattern={get_dash_pattern(grid.line_dash_array)}")
if grid.line_width is not None:
options.append(f"line width={grid.line_width}")
if options:
options = ",".join(options)
lines.append(f"\\draw[color={line_color}, step={step}, {options}]")
else:
lines.append(f"\\draw[color={line_color},step={step}]")
lines.append("(current bounding box.south west)")
lines.append(" grid (current bounding box.north east);\n")
lines.append("\\end{scope}\n")
return "".join(lines)
[docs]
def get_limits_code(canvas: "Canvas") -> str:
"""Get the limits of the canvas for clipping.
Args:
canvas ("Canvas"): The canvas object.
Returns:
str: The limits code for clipping.
"""
xmin, ymin, xmax, ymax = canvas.limits
points = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]
vertices = homogenize(points) @ canvas.xform_matrix
coords = " ".join([f"({v[0]}, {v[1]})" for v in vertices])
return f"\\clip plot[] coordinates {{{coords}}};\n"
[docs]
def get_back_code(canvas: "Canvas") -> str:
"""Get the background code for the canvas.
Args:
canvas ("Canvas"): The canvas object.
Returns:
str: The background code.
"""
back_color = color2tikz(canvas.back_color)
return f"\\pagecolor{back_color}\n"
[docs]
def get_tex_code(canvas: "Canvas") -> str:
"""Convert the sketches in the Canvas to TikZ code.
Args:
canvas ("Canvas"): The canvas object.
Returns:
str: The TikZ code.
"""
def get_sketch_code(sketch, canvas, ind):
"""Get the TikZ code for a sketch.
Args:
sketch: The sketch object.
canvas: The canvas object.
ind: The index.
Returns:
tuple: The TikZ code and the updated index.
"""
if sketch.subtype == Types.TAG_SKETCH:
code = draw_tag_sketch(sketch, canvas)
elif sketch.subtype == Types.BBOX_SKETCH:
code = draw_bbox_sketch(sketch, canvas)
else:
if sketch.draw_markers and sketch.marker_type == MarkerType.INDICES:
code = draw_shape_sketch(sketch, ind)
ind += 1
else:
code = draw_shape_sketch(sketch)
return code, ind
pages = canvas.pages
if pages:
for i, page in enumerate(pages):
sketches = page.sketches
back_color = f"\\pagecolor{color2tikz(page.back_color)}"
if i == 0:
code = [back_color]
else:
code.append(defaults["end_tikz"])
code.append("\\newpage")
code.append(defaults["begin_tikz"])
ind = 0
for sketch in sketches:
sketch_code, ind = get_sketch_code(sketch, canvas, ind)
code.append(sketch_code)
code = "\n".join(code)
else:
raise ValueError("No pages found in the canvas.")
return canvas.tex.tex_code(canvas, code)
[docs]
class Grid(sg.Shape):
"""Grid shape.
Args:
p1: (x_min, y_min)
p2: (x_max, y_max)
dx: x step
dy: y step
"""
def __init__(self, p1, p2, dx, dy, **kwargs):
"""
Args:
p1: (x_min, y_min)
p2: (x_max, y_max)
dx: x step
dy: y step
"""
self.p1 = p1
self.p2 = p2
self.dx = dx
self.dy = dy
self.primary_points = sg.Points([p1, p2])
self.closed = False
self.fill = False
self.stroke = True
self._b_box = None
super().__init__([p1, p2], xform_matrix=None, subtype=sg.Types.GRID, **kwargs)
[docs]
def get_min_size(sketch: ShapeSketch) -> str:
"""Returns the minimum size of the tag node.
Args:
sketch (ShapeSketch): The shape sketch object.
Returns:
str: The minimum size of the tag node.
"""
options = []
if sketch.frame_shape == "rectangle":
if sketch.frame_min_width is None:
width = defaults["min_width"]
else:
width = sketch.frame_min_width
if sketch.frame_min_height is None:
height = defaults["min_height"]
else:
height = sketch.frame_min_height
options.append(f"minimum width = {width}")
options.append(f"minimum height = {height}")
else:
if sketch.frame_min_size is None:
min_size = defaults["min_size"]
else:
min_size = sketch.frame_min_size
options.append(f"minimum size = {min_size}")
return options
[docs]
def frame_options(sketch: TagSketch) -> List[str]:
"""Returns the options for the frame of the tag node.
Args:
sketch (TagSketch): The tag sketch object.
Returns:
List[str]: The options for the frame of the tag node.
"""
options = []
if sketch.draw_frame:
options.append(sketch.frame_shape)
line_options = get_line_style_options(sketch, frame=True)
if line_options:
options.extend(line_options)
fill_options = get_fill_style_options(sketch, frame=True)
if fill_options:
options.extend(fill_options)
if sketch.text in [None, ""]:
min_size = get_min_size(sketch)
if min_size:
options.extend(min_size)
return options
[docs]
def color2tikz(color):
"""Converts a Color object to a TikZ color string.
Args:
color (Color): The color object.
Returns:
str: The TikZ color string.
"""
# \usepackage{xcolor}
# \tikz\node[rounded corners, fill={rgb,255:red,21; green,66; blue,128},
# text=white, draw=black] {hello world};
# \definecolor{mycolor}{rgb}{1,0.2,0.3}
# \definecolor{mycolor}{R_g_b}{255,51,76}
# \definecolor{mypink1}{rgb}{0.858, 0.188, 0.478}
# \definecolor{mypink2}{R_g_b}{219, 48, 122}
# \definecolor{mypink3}{cmyk}{0, 0.7808, 0.4429, 0.1412}
# \definecolor{mygray}{gray}{0.6}
if color is None:
r, g, b, _ = 255, 255, 255, 255
return f"{{rgb,255:red,{r}; green,{g}; blue,{b}}}"
r, g, b = color.rgb255
return f"{{rgb,255:red,{r}; green,{g}; blue,{b}}}"
[docs]
def get_scope_options(item: Union["Canvas", "Sketch"]) -> str:
"""Used for creating namespaces in TikZ.
Args:
item (Union["Canvas", "Sketch"]): The item to get scope options for.
Returns:
str: The scope options as a string.
"""
options = []
if item.blend_group:
options.append(f"blend group={item.blend_mode}")
elif item.blend_mode:
options.append(f"blend mode={item.blend_mode}")
if item.fill_alpha not in [None, 1]:
options.append(f"fill opacity={item.fill_alpha}")
if item.line_alpha not in [None, 1]:
options.append(f"draw opacity={item.line_alpha}")
if item.text_alpha not in [None, 1]:
options.append(f"text opacity={item.alpha}")
if item.alpha not in [None, 1]:
options.append(f"opacity={item.alpha}")
if item.even_odd_rule:
options.append("even odd rule")
if item.transparency_group:
options.append("transparency group")
return ",".join(options)
[docs]
def get_clip_code(item: Union["Sketch", "Canvas"]) -> str:
"""Returns the clip code for a sketch or Canvas.
Args:
item (Union["Sketch", "Canvas"]): The item to get clip code for.
Returns:
str: The clip code as a string.
"""
if item.mask.subtype == Types.CIRCLE:
x, y = item.mask.center[:2]
res = f"\\clip({x}, {y}) circle ({item.mask.radius});\n"
elif item.mask.subtype == Types.RECTANGLE:
x, y = item.mask.center[:2]
width, height = item.mask.width, item.mask.height
res = f"\\clip({x}, {y}) rectangle ({width}, {height});\n"
elif item.mask.subtype == Types.SHAPE:
vertices = item.mask.primary_points.homogen_coords
coords = " ".join([f"({v[0]}, {v[1]})" for v in vertices])
res = f"\\clip plot[] coordinates {{{coords}}};\n"
else:
res = ""
return res
[docs]
def get_canvas_scope(canvas):
"""Returns the TikZ code for the canvas scope.
Args:
canvas: The canvas object.
Returns:
str: The TikZ code for the canvas scope.
"""
options = get_scope_options(canvas)
res = f"\\begin{{scope}}[{options}]\n"
if canvas.clip and canvas.mask:
res += get_clip_code(canvas)
return res
[docs]
def draw_batch_sketch(sketch, canvas):
"""Converts a BatchSketch to TikZ code.
Args:
sketch: The BatchSketch object.
canvas: The canvas object.
Returns:
str: The TikZ code for the BatchSketch.
"""
options = get_scope_options(sketch)
if options:
res = f"\\begin{{scope}}[{options}]\n"
else:
res = ""
if sketch.clip and sketch.mask:
res += get_clip_code(sketch)
for item in sketch.items:
if item.subtype in d_sketch_draw:
res += d_sketch_draw[item.subtype](item, canvas)
else:
raise ValueError(f"Sketch type {item.subtype} not supported.")
if sketch.clip and sketch.mask:
res += get_clip_code(sketch)
if options:
res += "\\end{scope}\n"
return res
[docs]
def draw_bbox_sketch(sketch, canvas):
"""Converts a BBoxSketch to TikZ code.
Args:
sketch: The BBoxSketch object.
canvas: The canvas object.
Returns:
str: The TikZ code for the BBoxSketch.
"""
attrib_map = {
"line_color": "color",
"line_width": "line width",
"line_dash_array": "dash pattern",
}
attrib_list = ["line_color", "line_width", "line_dash_array"]
options = sg_to_tikz(sketch, attrib_list, attrib_map)
options = ", ".join(options)
res = f"\\draw[{options}]"
x1, y1 = sketch.vertices[1]
x2, y2 = sketch.vertices[3]
res += f"({x1}, {y1}) rectangle ({x2}, {y2});\n"
return res
[docs]
def draw_lace_sketch(item):
"""Converts a LaceSketch to TikZ code.
Args:
item: The LaceSketch object.
Returns:
str: The TikZ code for the LaceSketch.
"""
if item.draw_fragments:
for fragment in item.fragments:
draw_shape_sketch(fragment)
if item.draw_plaits:
for plait in item.plaits:
plait.fill = True
draw_shape_sketch(plait)
[docs]
def get_draw(sketch):
"""Returns the draw command for sketches.
Args:
sketch: The sketch object.
Returns:
str: The draw command as a string.
"""
# sketch.closed, sketch.fill, sketch.stroke, shading
decision_table = {
(True, True, True, True): "\\shadedraw",
(True, True, True, False): "\\filldraw",
(True, True, False, True): "\\shade",
(True, True, False, False): "\\fill",
(True, False, True, True): "\\draw",
(True, False, True, False): "\\draw",
(True, False, False, True): False,
(True, False, False, False): False,
(False, True, True, True): "\\draw",
(False, True, True, False): "\\draw",
(False, True, False, True): False,
(False, True, False, False): False,
(False, False, True, True): "\\draw",
(False, False, True, False): "\\draw",
(False, False, False, True): False,
(False, False, False, False): False,
}
if sketch.markers_only:
res = "\\draw"
else:
if hasattr(sketch, 'back_style'):
shading = sketch.back_style == BackStyle.SHADING
else:
shading = False
if not hasattr(sketch, 'closed'):
closed = False
else:
closed = sketch.closed
if not hasattr(sketch, 'fill'):
fill = False
else:
fill = sketch.fill
if not hasattr(sketch, 'stroke'):
stroke = False
else:
stroke = sketch.stroke
res = decision_table[(closed, fill, stroke, shading)]
return res
[docs]
def get_frame_options(sketch):
"""Returns the options for the frame of a TagSketch.
Args:
sketch: The TagSketch object.
Returns:
list: The options for the frame of the TagSketch.
"""
options = get_line_style_options(sketch)
options += get_fill_style_options(sketch)
if sketch.text in [None, ""]:
if sketch.frame.frame_shape == "rectangle":
width = sketch.frame.min_width
height = sketch.frame.min_height
if not width:
width = defaults["min_width"]
if not height:
height = defaults["min_height"]
options += "minimum width = {width}, minimum height = {height}"
else:
size = sketch.frame.min_size
if not size:
size = defaults["min_size"]
options += f"minimum size = {size}"
return options
[docs]
def draw_tag_sketch(sketch, canvas):
"""Converts a TagSketch to TikZ code.
Args:
sketch: The TagSketch object.
canvas: The canvas object.
Returns:
str: The TikZ code for the TagSketch.
"""
# \node at (0,0) {some text};
def get_font_family(sketch):
default_fonts = [defaults['main_font'], defaults['sans_font'], defaults['mono_font']]
if sketch.font_family in default_fonts:
if sketch.font_family == defaults['main_font']:
res = 'tex_family', ''
elif sketch.font_family == defaults['sans_font']:
res = 'tex_family', 'textsf'
else: # defaults['mono_font']
res = 'tex_family', 'texttt'
elif sketch.font_family:
if type(sketch.font_family) == FontFamily:
if sketch.font_family == FontFamily.SANSSERIF:
res = 'tex_family', 'textsf'
elif sketch.font_family == FontFamily.MONOSPACE:
res = 'tex_family', 'texttt'
else:
res = 'tex_family', 'textrm'
elif isinstance(sketch.font_family, str):
res = 'new_family', sketch.font_family.replace(" ", "")
else:
raise ValueError(f"Font family {sketch.font_family} not supported.")
else:
res = 'no_family', None
return res
def get_font_size(sketch):
if sketch.font_size:
if isinstance(sketch.font_size, FontSize):
res = 'tex_size', sketch.font_size.value
else:
res = 'num_size', sketch.font_size
else:
res = 'no_size', None
return res
pos = homogenize([sketch.pos]) @ canvas.xform_matrix
x, y = pos[0][:2]
options = ""
if sketch.draw_frame:
options += "draw"
if sketch.stroke:
if sketch.frame_shape != FrameShape.RECTANGLE:
options += f", {sketch.frame_shape}"
line_style_options = get_line_style_options(sketch)
if line_style_options:
options += ', '.join(line_style_options)
if sketch.frame_inner_sep:
options += f", inner sep={sketch.frame_inner_sep}"
if sketch.minimum_width:
options += f", minimum width={sketch.minimum_width}"
if sketch.smooth and sketch.frame_shape not in [
FrameShape.CIRCLE,
FrameShape.ELLIPSE,
]:
options += ", smooth"
if sketch.fill and sketch.back_color:
options += f", fill={color2tikz(sketch.frame_back_color)}"
if sketch.anchor:
options += f", anchor={sketch.anchor.value}"
if sketch.back_style == BackStyle.SHADING and sketch.fill:
shading_options = get_shading_options(sketch)[0]
options += ", " + shading_options
if sketch.back_style == BackStyle.PATTERN and sketch.fill:
pattern_options = get_pattern_options(sketch)[0]
options += ", " + pattern_options
if sketch.align != defaults["tag_align"]:
options += f", align={sketch.align.value}"
if sketch.text_width:
options += f", text width={sketch.text_width}"
# no_family, tex_family, new_family
# no_size, tex_size, num_size
# num_size and new_family {\fontsize{20}{24} \selectfont \Verdana ABCDEFG Hello, World! 25}
# tex_size and new_family {\large{\selectfont \Verdana ABCDEFG Hello, World! 50}}
# no_size and new_family {\selectfont \Verdana ABCDEFG Hello, World! 50}
# tex_family {\textsc{\textit{\textbf{\Huge{\texttt{ABCDG Just a test -50}}}}}};
# no_family {\textsc{\textit{\textbf{\Huge{ABCDG Just a test -50}}}}};
if sketch.font_color is not None and sketch.font_color != defaults["font_color"]:
options += f", text={color2tikz(sketch.font_color)}"
family, font_family = get_font_family(sketch)
size, font_size = get_font_size(sketch)
tex_text = ''
if sketch.small_caps:
tex_text += '\\textsc{'
if sketch.italic:
tex_text += '\\textit{'
if sketch.bold:
tex_text += '\\textbf{'
if size == 'num_size':
f_size = font_size
f_size2 = ceil(font_size * 1.2)
tex_text += f"\\fontsize{{{f_size}}}{{{f_size2}}}\\selectfont "
elif size == 'tex_size':
tex_text += f"\\{font_size}{{\\selectfont "
else:
tex_text += "\\selectfont "
if family == 'new_family':
tex_text += f"\\{font_family} {sketch.text}}}"
elif family == 'tex_family':
if font_family:
tex_text += f"\\{font_family}{{ {sketch.text}}}}}"
else:
tex_text += f"{{ {sketch.text}}}"
else: # no_family
tex_text += f"{{ {sketch.text}}}"
tex_text = '{' + tex_text
open_braces = tex_text.count('{')
close_braces = tex_text.count('}')
tex_text = tex_text + '}' * (open_braces - close_braces)
res = f"\\node[{options}] at ({x}, {y}) {tex_text};\n"
return res
[docs]
def get_dash_pattern(line_dash_array):
"""Returns the dash pattern for a line.
Args:
line_dash_array: The dash array for the line.
Returns:
str: The dash pattern as a string.
"""
dash_pattern = []
for i, dash in enumerate(line_dash_array):
if i % 2 == 0:
dash_pattern.extend(["on", f"{dash}pt"])
else:
dash_pattern.extend(["off", f"{dash}pt"])
return " ".join(dash_pattern)
[docs]
def sg_to_tikz(sketch, attrib_list, attrib_map, conditions=None, exceptions=None):
"""Converts the attributes of a sketch to TikZ options.
Args:
sketch: The sketch object.
attrib_list: The list of attributes to convert.
attrib_map: The map of attributes to TikZ options.
conditions: Optional conditions for the attributes.
exceptions: Optional exceptions for the attributes.
Returns:
list: The TikZ options as a list.
"""
skip = ["marker_color", "fill_color"]
tikz_way = {'line_width':LineWidth, 'line_dash_array':LineDashArray}
if exceptions:
skip += exceptions
d_converters = {
"line_color": color2tikz,
"fill_color": color2tikz,
"draw": color2tikz,
"line_dash_array": get_dash_pattern,
}
options = []
for attrib in attrib_list:
if attrib not in attrib_map:
continue
if conditions and attrib in conditions and not conditions[attrib]:
continue
if attrib in tikz_way:
value = getattr(sketch, attrib)
if isinstance(value, tikz_way[attrib]):
option = value.value
options.append(option)
continue
if isinstance(value, str):
if value in tikz_way[attrib]:
options.append(value)
continue
tikz_attrib = attrib_map[attrib]
if hasattr(sketch, attrib):
value = getattr(sketch, attrib)
if value is not None and tikz_attrib in list(attrib_map.values()):
if attrib in skip:
value = color2tikz(getattr(sketch, attrib))
options.append(f"{tikz_attrib}={value}")
elif value != tikz_defaults[tikz_attrib]:
if attrib in d_converters:
value = d_converters[attrib](value)
options.append(f"{tikz_attrib}={value}")
return options
[docs]
def get_line_style_options(sketch, exceptions=None):
"""Returns the options for the line style.
Args:
sketch: The sketch object.
exceptions: Optional exceptions for the line style options.
Returns:
list: The line style options as a list.
"""
attrib_map = {
"line_color": "color",
"line_width": "line width",
"line_dash_array": "dash pattern",
"line_cap": "line cap",
"line_join": "line join",
"line_miter_limit": "miter limit",
"line_dash_phase": "dash phase",
"line_alpha": "draw opacity",
"smooth": "smooth",
"fillet_radius": "rounded corners",
}
attribs = list(line_style_map.keys())
if sketch.stroke:
if exceptions and "draw_fillets" not in exceptions:
conditions = {"fillet_radius": sketch.draw_fillets}
else:
conditions = None
if sketch.line_alpha not in [None, 1]:
sketch.line_alpha = sketch.line_alpha
elif sketch.alpha not in [None, 1]:
sketch.fill_alpha = sketch.alpha
else:
attribs.remove("line_alpha")
if not sketch.smooth:
attribs.remove("smooth")
res = sg_to_tikz(sketch, attribs, attrib_map, conditions, exceptions)
else:
res = []
return res
[docs]
def get_fill_style_options(sketch, exceptions=None, frame=False):
"""Returns the options for the fill style.
Args:
sketch: The sketch object.
exceptions: Optional exceptions for the fill style options.
frame: Optional flag for frame fill style.
Returns:
list: The fill style options as a list.
"""
attrib_map = {
"fill_color": "fill",
"fill_alpha": "fill opacity",
#'fill_mode': 'even odd rule',
"blend_mode": "blend mode",
"frame_back_color": "fill",
}
attribs = list(shape_style_map.keys())
if sketch.fill_alpha not in [None, 1]:
sketch.fill_alpha = sketch.fill_alpha
elif sketch.alpha not in [None, 1]:
sketch.fill_alpha = sketch.alpha
else:
attribs.remove("fill_alpha")
if sketch.fill and not sketch.back_style == BackStyle.PATTERN:
res = sg_to_tikz(sketch, attribs, attrib_map, exceptions=exceptions)
if frame:
res = [f"fill = {color2tikz(getattr(sketch, 'frame_back_color'))}"] + res
else:
res = []
return res
[docs]
def get_axis_shading_colors(sketch):
"""Returns the shading colors for the axis.
Args:
sketch: The sketch object.
Returns:
str: The shading colors for the axis.
"""
def get_color(color, color_key):
if isinstance(color, Color):
res = color2tikz(color)
else:
res = defaults[color_key]
return res
left = get_color(sketch.shade_left_color, "shade_left_color")
right = get_color(sketch.shade_right_color, "shade_right_color")
top = get_color(sketch.shade_top_color, "shade_top_color")
bottom = get_color(sketch.shade_bottom_color, "shade_bottom_color")
middle = get_color(sketch.shade_middle_color, "shade_middle_color")
axis_colors = {
ShadeType.AXIS_BOTTOM_MIDDLE: f"bottom color={bottom}, middle color={middle}",
ShadeType.AXIS_LEFT_MIDDLE: f"left color={left}, middle color={middle}",
ShadeType.AXIS_RIGHT_MIDDLE: f"right color={right}, middle color={middle}",
ShadeType.AXIS_TOP_MIDDLE: f"top color={top}, middle color={middle}",
ShadeType.AXIS_LEFT_RIGHT: f"left color={left}, right color={right}",
ShadeType.AXIS_TOP_BOTTOM: f"top color={top}, bottom color={bottom}",
}
res = axis_colors[sketch.shade_type]
return res
[docs]
def get_bilinear_shading_colors(sketch):
"""Returns the shading colors for the bilinear shading.
Args:
sketch: The sketch object.
Returns:
str: The shading colors for the bilinear shading.
"""
res = []
if sketch.shade_upper_left_color:
res.append(f"upper left = {color2tikz(sketch.shade_upper_left_color)}")
if sketch.shade_upper_right_color:
res.append(f"upper right = {color2tikz(sketch.shade_upper_right_color)}")
if sketch.shade_lower_left_color:
res.append(f"lower left = {color2tikz(sketch.shade_lower_left_color)}")
if sketch.shade_lower_right_color:
res.append(f"lower right = {color2tikz(sketch.shade_lower_right_color)}")
return ", ".join(res)
[docs]
def get_radial_shading_colors(sketch):
"""Returns the shading colors for the radial shading.
Args:
sketch: The sketch object.
Returns:
str: The shading colors for the radial shading.
"""
res = []
if sketch.shade_type == ShadeType.RADIAL_INNER:
res.append(f"inner color = {color2tikz(sketch.shade_inner_color)}")
elif sketch.shade_type == ShadeType.RADIAL_OUTER:
res.append(f"outer color = {color2tikz(sketch.shade_outer_color)}")
elif sketch.shade_type == ShadeType.RADIAL_INNER_OUTER:
res.append(f"inner color = {color2tikz(sketch.shade_inner_color)}")
res.append(f"outer color = {color2tikz(sketch.shade_outer_color)}")
return ", ".join(res)
axis_shading_types = [
ShadeType.AXIS_BOTTOM_MIDDLE,
ShadeType.AXIS_LEFT_MIDDLE,
ShadeType.AXIS_RIGHT_MIDDLE,
ShadeType.AXIS_TOP_MIDDLE,
ShadeType.AXIS_LEFT_RIGHT,
ShadeType.AXIS_TOP_BOTTOM,
]
radial_shading_types = [
ShadeType.RADIAL_INNER,
ShadeType.RADIAL_OUTER,
ShadeType.RADIAL_INNER_OUTER,
]
[docs]
def get_shading_options(sketch):
"""Returns the options for the shading.
Args:
sketch: The sketch object.
Returns:
list: The shading options as a list.
"""
shade_type = sketch.shade_type
if shade_type in axis_shading_types:
res = get_axis_shading_colors(sketch)
if sketch.shade_axis_angle:
res += f", shading angle={sketch.shade_axis_angle}"
elif shade_type == ShadeType.BILINEAR:
res = get_bilinear_shading_colors(sketch)
elif shade_type in radial_shading_types:
res = get_radial_shading_colors(sketch)
elif shade_type == ShadeType.BALL:
res = f"ball color = {color2tikz(sketch.shade_ball_color)}"
elif shade_type == ShadeType.COLORWHEEL:
res = "shading=color wheel"
elif shade_type == ShadeType.COLORWHEEL_BLACK:
res = "shading=color wheel black center"
elif shade_type == ShadeType.COLORWHEEL_WHITE:
res = "shading=color wheel white center"
return [res]
[docs]
def get_pattern_options(sketch):
"""Returns the options for the patterns.
Args:
sketch: The sketch object.
Returns:
list: The pattern options as a list.
"""
pattern_type = sketch.pattern_type
if pattern_type:
distance = sketch.pattern_distance
options = f"pattern={{{pattern_type}[distance={distance}, "
angle = degrees(sketch.pattern_angle)
if angle:
options += f"angle={angle}, "
line_width = sketch.pattern_line_width
if line_width:
options += f"line width={line_width}, "
x_shift = sketch.pattern_x_shift
if x_shift:
options += f"xshift={x_shift}, "
y_shift = sketch.pattern_y_shift
if y_shift:
options += f"yshift={y_shift}, "
if pattern_type in ["Stars", "Dots"]:
radius = sketch.pattern_radius
if radius:
options += f"radius={radius}, "
if pattern_type == "Stars":
points = sketch.pattern_points
if points:
options += f"points={points}, "
options = options.strip()
if options.endswith(","):
options = options[:-1]
options += "]"
color = sketch.pattern_color
if color and color != sg.black:
options += f", pattern color={color2tikz(color)}, "
options += "}"
res = [options]
else:
res = []
return res
[docs]
def get_marker_options(sketch):
"""Returns the options for the markers.
Args:
sketch: The sketch object.
Returns:
list: The marker options as a list.
"""
attrib_map = {
# 'marker': 'mark',
"marker_size": "mark size",
"marker_angle": "rotate",
# 'fill_color': 'color',
"marker_color": "color",
"marker_fill": "fill",
"marker_opacity": "opacity",
"marker_repeat": "mark repeat",
"marker_phase": "mark phase",
"marker_tension": "tension",
"marker_line_width": "line width",
"marker_line_style": "style",
# 'line_color': 'line color',
}
# if mark_stroke is false make line color same as fill color
if sketch.draw_markers:
res = sg_to_tikz(sketch, marker_style_map.keys(), attrib_map)
else:
res = []
return res
[docs]
def draw_shape_sketch_with_indices(sketch, ind):
"""Draws a shape sketch with circle markers with index numbers in them.
Args:
sketch: The shape sketch object.
ind: The index.
Returns:
str: The TikZ code for the shape sketch with indices.
"""
begin_scope = get_begin_scope(ind)
body = get_draw(sketch)
options = get_line_style_options(sketch)
if sketch.fill and sketch.closed:
options += get_fill_style_options(sketch)
if sketch.smooth:
if sketch.closed:
options += ["smooth cycle"]
else:
options += ["smooth"]
options = ", ".join(options)
body += f"[{options}]"
vertices = sketch.vertices
vertices = [str(x) for x in vertices]
str_lines = [vertices[0] + "node{0}"]
n = len(vertices)
for i, vertice in enumerate(vertices[1:]):
if (i + 1) % 6 == 0:
if i == n - 1:
str_lines.append(f" -- {vertice} node{{{i+1}}}\n")
else:
str_lines.append(f"\n\t-- {vertice} node{{{i+1}}}")
else:
str_lines.append(f"-- {vertice} node{{{i+1}}}")
if sketch.closed:
str_lines.append(" -- cycle;\n")
str_lines.append(";\n")
end_scope = get_end_scope()
return begin_scope + body + "".join(str_lines) + end_scope
[docs]
def draw_shape_sketch_with_markers(sketch):
"""Draws a shape sketch with markers.
Args:
sketch: The shape sketch object.
Returns:
str: The TikZ code for the shape sketch with markers.
"""
# begin_scope = get_begin_scope()
body = get_draw(sketch)
options = get_line_style_options(sketch)
if sketch.fill and sketch.closed:
options += get_fill_style_options(sketch)
if sketch.smooth and sketch.closed:
options += ["smooth cycle"]
elif sketch.smooth:
options += ["smooth"]
options = ", ".join(options)
if options:
body += f"[{options}]"
if sketch.draw_markers:
marker_options = ", ".join(get_marker_options(sketch))
else:
marker_options = ""
vertices = [str(x) for x in sketch.vertices]
str_lines = [vertices[0]]
for i, vertice in enumerate(vertices[1:]):
if (i + 1) % 6 == 0:
str_lines.append(f"\n\t{vertice} ")
else:
str_lines.append(f" {vertice} ")
# if sketch.closed:
# str_lines.append(f" {vertices[0]}")
coordinates = "".join(str_lines)
marker = get_enum_value(MarkerType, sketch.marker_type)
# marker = sketch.marker_type.value
if sketch.markers_only:
markers_only = "only marks ,"
else:
markers_only = ""
if sketch.draw_markers and marker_options:
body += (
f" plot[mark = {marker}, {markers_only}mark options = {{{marker_options}}}] "
f"\ncoordinates {{{coordinates}}};\n"
)
elif sketch.draw_markers:
body += (
f" plot[mark = {marker}, {markers_only}] coordinates {{{coordinates}}};\n"
)
else:
body += f" plot[tension=.5] coordinates {{{coordinates}}};\n"
return body
[docs]
def get_begin_scope(ind=None):
"""Returns \begin{scope}[every node/.append style=nodestyle{ind}].
Args:
ind: Optional index for the scope.
Returns:
str: The begin scope string.
"""
if ind is None:
res = "\\begin{scope}[]\n"
else:
res = f"\\begin{{scope}}[every node/.append style=nodestyle{ind}]\n"
return res
[docs]
def get_end_scope():
"""Returns \\end{scope}.
Returns:
str: The end scope string.
"""
return "\\end{scope}\n"
[docs]
def draw_sketch(sketch):
"""Draws a plain shape sketch.
Args:
sketch: The shape sketch object.
Returns:
str: The TikZ code for the plain shape sketch.
"""
res = get_draw(sketch)
if not res:
return ""
options = []
if sketch.back_style == BackStyle.PATTERN and sketch.fill and sketch.closed:
options += get_pattern_options(sketch)
if sketch.stroke:
options += get_line_style_options(sketch)
if sketch.closed and sketch.fill:
options += get_fill_style_options(sketch)
if sketch.smooth:
options += ["smooth"]
if sketch.back_style == BackStyle.SHADING and sketch.fill and sketch.closed:
options += get_shading_options(sketch)
options = ", ".join(options)
if options:
res += f"[{options}]"
vertices = sketch.vertices
n = len(vertices)
str_lines = [f"{vertices[0]}"]
for i, vertice in enumerate(vertices[1:]):
if (i + 1) % 8 == 0:
if i == n - 1:
str_lines.append(f"-- {vertice} \n")
else:
str_lines.append(f"\n\t-- {vertice} ")
else:
str_lines.append(f"-- {vertice} ")
if sketch.closed:
str_lines.append("-- cycle;\n")
else:
str_lines.append(";\n")
if res:
res += "".join(str_lines)
else:
res = "".join(str_lines)
return res
[docs]
def draw_shape_sketch(sketch, ind=None):
"""Draws a shape sketch.
Args:
sketch: The shape sketch object.
ind: Optional index for the shape sketch.
Returns:
str: The TikZ code for the shape sketch.
"""
d_subtype_draw = {
sg.Types.ARC_SKETCH: draw_arc_sketch,
sg.Types.BEZIER_SKETCH: draw_bezier_sketch,
sg.Types.CIRCLE_SKETCH: draw_circle_sketch,
sg.Types.ELLIPSE_SKETCH: draw_ellipse_sketch,
sg.Types.LINE_SKETCH: draw_line_sketch,
}
if sketch.subtype in d_subtype_draw:
res = d_subtype_draw[sketch.subtype](sketch)
elif sketch.draw_markers and sketch.marker_type == MarkerType.INDICES:
res = draw_shape_sketch_with_indices(sketch, ind)
elif sketch.draw_markers or sketch.smooth:
res = draw_shape_sketch_with_markers(sketch)
else:
res = draw_sketch(sketch)
return res
[docs]
def draw_line_sketch(sketch):
"""Draws a line sketch.
Args:
sketch: The line sketch object.
Returns:
str: The TikZ code for the line sketch.
"""
begin_scope = get_begin_scope()
res = "\\draw"
exceptions = ["draw_fillets", "fillet_radius", "line_join", "line_miter_limit"]
options = get_line_style_options(sketch, exceptions=exceptions)
start = sketch.vertices[0]
end = sketch.vertices[1]
options = ", ".join(options)
res += f"[{options}]"
res += f" {start[:2]} -- {end[:2]};\n"
end_scope = get_end_scope()
return begin_scope + res + end_scope
[docs]
def draw_circle_sketch(sketch):
"""Draws a circle sketch.
Args:
sketch: The circle sketch object.
Returns:
str: The TikZ code for the circle sketch.
"""
begin_scope = get_begin_scope()
res = get_draw(sketch)
options = get_line_style_options(sketch)
fill_options = get_fill_style_options(sketch)
options += fill_options
if sketch.smooth:
options += ["smooth"]
options = ", ".join(options)
res += f"[{options}]"
x, y = sketch.center[:2]
res += f"({x}, {y}) circle ({sketch.radius});\n"
end_scope = get_end_scope()
return begin_scope + res + end_scope
[docs]
def draw_rect_sketch(sketch):
"""Draws a rectangle sketch.
Args:
sketch: The rectangle sketch object.
Returns:
str: The TikZ code for the rectangle sketch.
"""
begin_scope = get_begin_scope()
res = get_draw(sketch)
options = get_line_style_options(sketch)
fill_options = get_fill_style_options(sketch)
options += fill_options
if sketch.smooth:
options += ["smooth"]
options = ", ".join(options)
res += f"[{options}]"
x, y = sketch.center[:2]
width, height = sketch.width, sketch.height
res += f"({x}, {y}) rectangle ({width}, {height});\n"
end_scope = get_end_scope()
return begin_scope + res + end_scope
[docs]
def draw_ellipse_sketch(sketch):
"""Draws an ellipse sketch.
Args:
sketch: The ellipse sketch object.
Returns:
str: The TikZ code for the ellipse sketch.
"""
begin_scope = get_begin_scope()
res = get_draw(sketch)
options = get_line_style_options(sketch)
fill_options = get_fill_style_options(sketch)
options += fill_options
if sketch.smooth:
options += ["smooth"]
angle = degrees(sketch.angle)
x, y = sketch.center[:2]
if angle:
options += [f"rotate around= {{{angle}:({x},{y})}}"]
options = ", ".join(options)
res += f"[{options}]"
a = sketch.x_radius
b = sketch.y_radius
res += f"({x}, {y}) ellipse ({a} and {b});\n"
end_scope = get_end_scope()
return begin_scope + res + end_scope
[docs]
def draw_arc_sketch(sketch):
"""Draws an arc sketch.
Args:
sketch: The arc sketch object.
Returns:
str: The TikZ code for the arc sketch.
"""
begin_scope = get_begin_scope()
res = get_draw(sketch)
options = get_line_style_options(sketch)
options = ", ".join(options)
angle = degrees(sketch.rot_angle)
cx, cy = sketch.center[:2]
x, y = sketch.start_point[:2]
if angle:
options += [f"rotate around= {{{angle}:({cx},{cy})}}"]
a1 = degrees(sketch.start_angle)
a2 = degrees(sketch.end_angle)
r1 = sketch.radius
r2 = sketch.radius2
if sketch.radius != sketch.radius2:
res += f"[{options}]({x}, {y}) arc [start angle = {a1}, end angle = {a2}, x radius = {r1}, y radius = {r2}];\n"
else:
res += f"[{options}]({x}, {y}) arc [start angle = {a1}, end angle = {a2}, radius = {r1}];\n"
end_scope = get_end_scope()
return begin_scope + res + end_scope
[docs]
def draw_bezier_sketch(sketch):
"""Draws a Bezier curve sketch.
Args:
sketch: The Bezier curve sketch object.
Returns:
str: The TikZ code for the Bezier curve sketch.
"""
begin_scope = get_begin_scope()
res = get_draw(sketch)
options = get_line_style_options(sketch)
options = ", ".join(options)
res += f"[{options}]"
p1, cp1, cp2, p2 = sketch.control_points
x1, y1 = p1[:2]
x2, y2 = cp1[:2]
x3, y3 = cp2[:2]
x4, y4 = p2[:2]
res += f" ({x1}, {y1}) .. controls ({x2}, {y2}) and ({x3}, {y3}) .. ({x4}, {y4});\n"
end_scope = get_end_scope()
return begin_scope + res + end_scope
[docs]
def draw_line(line):
"""Tikz code for a line.
Args:
line: The line object.
Returns:
str: The TikZ code for the line.
"""
p1 = line.start[:2]
p2 = line.end[:2]
options = []
if line.line_width is not None:
options.append(line.line_width)
if line.color is not None:
color = color2tikz(line.color)
options.append(color)
if line.dash_array is not None:
options.append(line.dash_array)
# options = [line.width, line.color, line.dash_array, line.cap, line.join]
if line.line_width == 0:
res = f"\\path[{', '.join(options)}] {p1} -- {p2};\n"
else:
res = f"\\draw[{', '.join(options)}] {p1} -- {p2};\n"
return res
[docs]
def is_stroked(shape: Shape) -> bool:
"""Returns True if the shape is stroked.
Args:
shape (Shape): The shape object.
Returns:
bool: True if the shape is stroked, False otherwise.
"""
return shape.stroke and shape.line_color is not None and shape.line_width > 0
d_sketch_draw = {
sg.Types.SHAPE: draw_shape_sketch,
sg.Types.TAG_SKETCH: draw_tag_sketch,
sg.Types.LACESKETCH: draw_lace_sketch,
sg.Types.LINE: draw_line_sketch,
sg.Types.CIRCLE: draw_circle_sketch,
sg.Types.ELLIPSE: draw_shape_sketch,
sg.Types.ARC: draw_arc_sketch,
sg.Types.BATCH: draw_batch_sketch,
}