simetri.tikz.tikz

TikZ exporter. Draws shapes using the TikZ package for LaTeX. Sketch objects are converted to TikZ code.

   1"""TikZ exporter. Draws shapes using the TikZ package for LaTeX.
   2Sketch objects are converted to TikZ code."""
   3
   4# This is a proof of concept.
   5# To do: This whole module needs to be restructured.
   6
   7from __future__ import annotations
   8
   9from math import degrees, cos, sin, ceil
  10from typing import List, Union
  11from dataclasses import dataclass, field
  12
  13import numpy as np
  14
  15import simetri.graphics as sg
  16from ..graphics.common import common_properties
  17from ..graphics.all_enums import (
  18    BackStyle,
  19    FontSize,
  20    FontFamily,
  21    MarkerType,
  22    ShadeType,
  23    Types,
  24    TexLoc,
  25    FrameShape,
  26    DocumentClass,
  27    Align,
  28    ArrowLine,
  29    BlendMode,
  30    get_enum_value,
  31    LineWidth,
  32    LineDashArray,
  33)
  34from ..canvas.style_map import shape_style_map, line_style_map, marker_style_map
  35from ..settings.settings import defaults, tikz_defaults
  36from ..geometry.geometry import homogenize, close_points2, round_point
  37from ..geometry.ellipse import ellipse_point
  38from ..graphics.sketch import TagSketch, ShapeSketch
  39from ..graphics.shape import Shape
  40
  41from ..colors.colors import Color
  42
  43
  44np.set_printoptions(legacy="1.21")
  45array = np.array
  46
  47
  48enum_map = {}
  49
  50
  51def scope_code_required(item: Union["Canvas", "Batch"]) -> bool:
  52    """Check if a TikZ namespace is required for the item.
  53
  54    Args:
  55        item (Union["Canvas", "Batch"]): The item to check.
  56
  57    Returns:
  58        bool: True if a TikZ namespace is required, False otherwise.
  59    """
  60    return (
  61        item.blend_mode is not None
  62        or item.transparency_group
  63        or (item.clip and item.mask)
  64    )
  65
  66
  67@dataclass
  68class Tex:
  69    """Tex class for generating tex code.
  70
  71    Attributes:
  72        begin_document (str): The beginning of the document.
  73        end_document (str): The end of the document.
  74        begin_tikz (str): The beginning of the TikZ environment.
  75        end_tikz (str): The end of the TikZ environment.
  76        packages (List[str]): List of required TeX packages.
  77        tikz_libraries (List[str]): List of required TikZ libraries.
  78        tikz_code (str): The generated TikZ code.
  79        sketches (List["Sketch"]): List of Sketch objects.
  80    """
  81
  82    begin_document: str = defaults["begin_doc"]
  83    end_document: str = defaults["end_doc"]
  84    begin_tikz: str = defaults["begin_tikz"]
  85    end_tikz: str = defaults["end_tikz"]
  86    packages: List[str] = None
  87    tikz_libraries: List[str] = None
  88    tikz_code: str = ""  # Generated by the canvas by using sketches
  89    sketches: List["Sketch"] = field(default_factory=list)  # List of TexSketch objects
  90
  91    def __post_init__(self):
  92        """Post-initialization method."""
  93        self.type = Types.TEX
  94        common_properties(self)
  95
  96    def tex_code(self, canvas: "Canvas", aux_code: str) -> str:
  97        """Generate the final TeX code.
  98
  99        Args:
 100            canvas ("Canvas"): The canvas object.
 101            aux_code (str): Auxiliary code to include.
 102
 103        Returns:
 104            str: The final TeX code.
 105        """
 106        doc_code = []
 107        for sketch in self.sketches:
 108            if sketch.location == TexLoc.DOCUMENT:
 109                doc_code.append(sketch.code)
 110        doc_code = "\n".join(doc_code)
 111        back_color = f"\\pagecolor{color2tikz(canvas.back_color)}"
 112        self.begin_document = self.begin_document + back_color + "\n"
 113        if canvas.limits is not None:
 114            begin_tikz = self.begin_tikz + get_limits_code(canvas) + "\n"
 115        else:
 116            begin_tikz = self.begin_tikz + "\n"
 117        if scope_code_required(canvas):
 118            scope = get_canvas_scope(canvas)
 119            code = (
 120                self.get_preamble(canvas)
 121                + self.begin_document
 122                + doc_code
 123                + begin_tikz
 124                + scope
 125                + self.get_tikz_code()
 126                + aux_code
 127                + "\\end{scope}\n"
 128                + self.end_tikz
 129                + self.end_document
 130            )
 131        else:
 132            code = (
 133                self.get_preamble(canvas)
 134                + self.begin_document
 135                + doc_code
 136                + begin_tikz
 137                + self.get_tikz_code()
 138                + aux_code
 139                + self.end_tikz
 140                + self.end_document
 141            )
 142
 143        return code
 144
 145    def get_doc_class(self, border: float, font_size: int) -> str:
 146        """Returns the document class.
 147
 148        Args:
 149            border (float): The border size.
 150            font_size (int): The font size.
 151
 152        Returns:
 153            str: The document class string.
 154        """
 155        return f"\\documentclass[{font_size}pt,tikz,border={border}pt]{{standalone}}\n"
 156
 157    def get_tikz_code(self) -> str:
 158        """Returns the TikZ code.
 159
 160        Returns:
 161            str: The TikZ code.
 162        """
 163        code = ""
 164        for sketch in self.sketches:
 165            if sketch.location == TexLoc.PICTURE:
 166                code += sketch.text + "\n"
 167
 168        return code
 169
 170    def get_tikz_libraries(self) -> str:
 171        """Returns the TikZ libraries.
 172
 173        Returns:
 174            str: The TikZ libraries string.
 175        """
 176        return f"\\usetikzlibrary{{{','.join(self.tikz_libraries)}}}\n"
 177
 178    def get_packages(self, canvas) -> str:
 179        """Returns the required TeX packages.
 180
 181        Args:
 182            canvas: The canvas object.
 183
 184        Returns:
 185            str: The required TeX packages.
 186        """
 187        tikz_libraries = []
 188        tikz_packages = ['tikz', 'pgf']
 189        for page in canvas.pages:
 190            for sketch in page.sketches:
 191                if hasattr(sketch, "draw_frame") and sketch.draw_frame:
 192                    if hasattr(sketch, "frame_shape") and sketch.frame_shape != FrameShape.RECTANGLE:
 193                        if "shapes.geometric" not in tikz_libraries:
 194                            tikz_libraries.append("shapes.geometric")
 195                if sketch.draw_markers:
 196                    if 'patterns' not in tikz_libraries:
 197                        tikz_libraries.append("patterns")
 198                        tikz_libraries.append("patterns.meta")
 199                        tikz_libraries.append("backgrounds")
 200                        tikz_libraries.append("shadings")
 201                if sketch.line_dash_array:
 202                    if 'patterns' not in tikz_libraries:
 203                        tikz_libraries.append("patterns")
 204                if sketch.subtype == Types.TAG_SKETCH:
 205                    if "fontspec" not in tikz_packages:
 206                        tikz_packages.append("fontspec")
 207                else:
 208                    if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
 209                        if "fontspec" not in tikz_packages:
 210                            tikz_packages.append("fontspec")
 211                if hasattr(sketch, 'back_style'):
 212                    if sketch.back_style == BackStyle.COLOR:
 213                        if "xcolor" not in tikz_packages:
 214                            tikz_packages.append("xcolor")
 215                    if sketch.back_style == BackStyle.SHADING:
 216                        if "shadings" not in tikz_libraries:
 217                            tikz_libraries.append("shadings")
 218                    if sketch.back_style == BackStyle.PATTERN:
 219                        if "patterns" not in tikz_libraries:
 220                            tikz_libraries.append("patterns")
 221                            tikz_libraries.append("patterns.meta")
 222
 223
 224        return tikz_libraries, tikz_packages
 225
 226    def get_preamble(self, canvas) -> str:
 227        """Returns the TeX preamble.
 228
 229        Args:
 230            canvas: The canvas object.
 231
 232        Returns:
 233            str: The TeX preamble.
 234        """
 235        libraries, packages = self.get_packages(canvas)
 236
 237        if packages:
 238            packages = f'\\usepackage{{{",".join(packages)}}}\n'
 239            if 'fontspec' in packages:
 240                fonts_section = f"""\\setmainfont{{{defaults['main_font']}}}
 241\\setsansfont{{{defaults['sans_font']}}}
 242\\setmonofont{{{defaults['mono_font']}}}\n"""
 243
 244        if libraries:
 245            libraries = f'\\usetikzlibrary{{{",".join(libraries)}}}\n'
 246
 247        if canvas.border is None:
 248            border = defaults["border"]
 249        elif isinstance(canvas.border, (int, float)):
 250            border = canvas.border
 251        else:
 252            raise ValueError("Canvas.border must be a positive numeric value.")
 253        if border < 0:
 254            raise ValueError("Canvas.border must be a positive numeric value.")
 255        doc_class = self.get_doc_class(border, defaults["font_size"])
 256        # Check if different fonts are used
 257        fonts_section = ""
 258        fonts = canvas.get_fonts_list()
 259        for font in fonts:
 260            if font is None:
 261                continue
 262            font_family = font.replace(" ", "")
 263            fonts_section += f"\\newfontfamily\\{font_family}[Scale=1.0]{{{font}}}\n"
 264        preamble = f"{doc_class}{packages}{libraries}{fonts_section}"
 265
 266        indices = False
 267        for sketch in canvas.active_page.sketches:
 268            if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
 269                indices = True
 270                break
 271        if indices:
 272            font_family = defaults["indices_font_family"]
 273            font_size = defaults["indices_font_size"]
 274            count = 0
 275            for sketch in canvas.active_page.sketches:
 276                if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
 277                    preamble += "\\tikzset{\n"
 278                    node_style = (
 279                        f"nodestyle{count}/.style={{draw, circle, gray, "
 280                        f"text=black, fill=white, line width = .5, inner sep=.5, "
 281                        f"font=\\{font_family}\\{font_size}}}\n}}\n"
 282                    )
 283                    preamble += node_style
 284                    count += 1
 285        for sketch in canvas.active_page.sketches:
 286            if sketch.subtype == Types.TEX_SKETCH:
 287                if sketch.location == TexLoc.PREAMBLE:
 288                    preamble += sketch.code + "\n"
 289        return preamble
 290
 291
 292def get_back_grid_code(grid: Grid, canvas: "Canvas") -> str:
 293    """Page background grid code.
 294
 295    Args:
 296        grid (Grid): The grid object.
 297        canvas ("Canvas"): The canvas object.
 298
 299    Returns:
 300        str: The background grid code.
 301    """
 302    # \usetikzlibrary{backgrounds}
 303    # \begin{scope}[on background layer]
 304    # \fill[gray] (current bounding box.south west) rectangle
 305    # (current bounding box.north east);
 306    # \draw[white,step=.5cm] (current bounding box.south west) grid
 307    # (current bounding box.north east);
 308    # \end{scope}
 309    grid = canvas.active_page.grid
 310    back_color = color2tikz(grid.back_color)
 311    line_color = color2tikz(grid.line_color)
 312    step = grid.spacing
 313    lines = ["\\begin{scope}[on background layer]\n"]
 314    lines.append(f"\\fill[color={back_color}] (current bounding box.south west) ")
 315    lines.append("rectangle (current bounding box.north east);\n")
 316    options = []
 317    if grid.line_dash_array is not None:
 318        options.append(f"dashed, dash pattern={get_dash_pattern(grid.line_dash_array)}")
 319    if grid.line_width is not None:
 320        options.append(f"line width={grid.line_width}")
 321    if options:
 322        options = ",".join(options)
 323        lines.append(f"\\draw[color={line_color}, step={step}, {options}]")
 324    else:
 325        lines.append(f"\\draw[color={line_color},step={step}]")
 326    lines.append("(current bounding box.south west)")
 327    lines.append(" grid (current bounding box.north east);\n")
 328    lines.append("\\end{scope}\n")
 329
 330    return "".join(lines)
 331
 332
 333def get_limits_code(canvas: "Canvas") -> str:
 334    """Get the limits of the canvas for clipping.
 335
 336    Args:
 337        canvas ("Canvas"): The canvas object.
 338
 339    Returns:
 340        str: The limits code for clipping.
 341    """
 342    xmin, ymin, xmax, ymax = canvas.limits
 343    points = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]
 344    vertices = homogenize(points) @ canvas.xform_matrix
 345    coords = " ".join([f"({v[0]}, {v[1]})" for v in vertices])
 346
 347    return f"\\clip plot[] coordinates {{{coords}}};\n"
 348
 349
 350def get_back_code(canvas: "Canvas") -> str:
 351    """Get the background code for the canvas.
 352
 353    Args:
 354        canvas ("Canvas"): The canvas object.
 355
 356    Returns:
 357        str: The background code.
 358    """
 359    back_color = color2tikz(canvas.back_color)
 360    return f"\\pagecolor{back_color}\n"
 361
 362
 363def get_tex_code(canvas: "Canvas") -> str:
 364    """Convert the sketches in the Canvas to TikZ code.
 365
 366    Args:
 367        canvas ("Canvas"): The canvas object.
 368
 369    Returns:
 370        str: The TikZ code.
 371    """
 372
 373    def get_sketch_code(sketch, canvas, ind):
 374        """Get the TikZ code for a sketch.
 375
 376        Args:
 377            sketch: The sketch object.
 378            canvas: The canvas object.
 379            ind: The index.
 380
 381        Returns:
 382            tuple: The TikZ code and the updated index.
 383        """
 384        if sketch.subtype == Types.TAG_SKETCH:
 385            code = draw_tag_sketch(sketch)
 386        elif sketch.subtype == Types.BBOX_SKETCH:
 387            code = draw_bbox_sketch(sketch)
 388        elif sketch.subtype == Types.PATTERN_SKETCH:
 389            code = draw_pattern_sketch(sketch)
 390        elif sketch.subtype == Types.TEX_SKETCH:
 391            if sketch.location == TexLoc.NONE:
 392                code = sketch.code
 393        else:
 394            if sketch.draw_markers and sketch.marker_type == MarkerType.INDICES:
 395                code = draw_shape_sketch(sketch, ind)
 396                ind += 1
 397            else:
 398                code = draw_shape_sketch(sketch)
 399
 400        return code, ind
 401
 402    pages = canvas.pages
 403
 404    if pages:
 405        for i, page in enumerate(pages):
 406            sketches = page.sketches
 407            back_color = f"\\pagecolor{color2tikz(page.back_color)}"
 408            if i == 0:
 409                code = [back_color]
 410            else:
 411                code.append(defaults["end_tikz"])
 412                code.append("\\newpage")
 413                code.append(defaults["begin_tikz"])
 414            ind = 0
 415            for sketch in sketches:
 416                sketch_code, ind = get_sketch_code(sketch, canvas, ind)
 417                code.append(sketch_code)
 418
 419        code = "\n".join(code)
 420    else:
 421        raise ValueError("No pages found in the canvas.")
 422    return canvas.tex.tex_code(canvas, code)
 423
 424
 425class Grid(sg.Shape):
 426    """Grid shape.
 427
 428    Args:
 429        p1: (x_min, y_min)
 430        p2: (x_max, y_max)
 431        dx: x step
 432        dy: y step
 433    """
 434
 435    def __init__(self, p1, p2, dx, dy, **kwargs):
 436        """
 437        Args:
 438            p1: (x_min, y_min)
 439            p2: (x_max, y_max)
 440            dx: x step
 441            dy: y step
 442        """
 443        self.p1 = p1
 444        self.p2 = p2
 445        self.dx = dx
 446        self.dy = dy
 447        self.primary_points = sg.Points([p1, p2])
 448        self.closed = False
 449        self.fill = False
 450        self.stroke = True
 451        self._b_box = None
 452        super().__init__([p1, p2], xform_matrix=None, subtype=sg.Types.GRID, **kwargs)
 453
 454
 455def get_min_size(sketch: ShapeSketch) -> str:
 456    """Returns the minimum size of the tag node.
 457
 458    Args:
 459        sketch (ShapeSketch): The shape sketch object.
 460
 461    Returns:
 462        str: The minimum size of the tag node.
 463    """
 464    options = []
 465    if sketch.frame_shape == "rectangle":
 466        if sketch.frame_min_width is None:
 467            width = defaults["min_width"]
 468        else:
 469            width = sketch.frame_min_width
 470        if sketch.frame_min_height is None:
 471            height = defaults["min_height"]
 472        else:
 473            height = sketch.frame_min_height
 474        options.append(f"minimum width = {width}")
 475        options.append(f"minimum height = {height}")
 476    else:
 477        if sketch.frame_min_size is None:
 478            min_size = defaults["min_size"]
 479        else:
 480            min_size = sketch.frame_min_size
 481        options.append(f"minimum size = {min_size}")
 482
 483    return options
 484
 485
 486def frame_options(sketch: TagSketch) -> List[str]:
 487    """Returns the options for the frame of the tag node.
 488
 489    Args:
 490        sketch (TagSketch): The tag sketch object.
 491
 492    Returns:
 493        List[str]: The options for the frame of the tag node.
 494    """
 495    options = []
 496    if sketch.draw_frame:
 497        options.append(sketch.frame_shape)
 498        line_options = get_line_style_options(sketch, frame=True)
 499        if line_options:
 500            options.extend(line_options)
 501        fill_options = get_fill_style_options(sketch, frame=True)
 502        if fill_options:
 503            options.extend(fill_options)
 504        if sketch.text in [None, ""]:
 505            min_size = get_min_size(sketch)
 506            if min_size:
 507                options.extend(min_size)
 508
 509    return options
 510
 511
 512def color2tikz(color):
 513    """Converts a Color object to a TikZ color string.
 514
 515    Args:
 516        color (Color): The color object.
 517
 518    Returns:
 519        str: The TikZ color string.
 520    """
 521    # \usepackage{xcolor}
 522    # \tikz\node[rounded corners, fill={rgb,255:red,21; green,66; blue,128},
 523    #                                    text=white, draw=black] {hello world};
 524    # \definecolor{mycolor}{rgb}{1,0.2,0.3}
 525    # \definecolor{mycolor}{R_g_b}{255,51,76}
 526    # \definecolor{mypink1}{rgb}{0.858, 0.188, 0.478}
 527    # \definecolor{mypink2}{R_g_b}{219, 48, 122}
 528    # \definecolor{mypink3}{cmyk}{0, 0.7808, 0.4429, 0.1412}
 529    # \definecolor{mygray}{gray}{0.6}
 530    if color is None:
 531        r, g, b, _ = 255, 255, 255, 255
 532        return f"{{rgb,255:red,{r}; green,{g}; blue,{b}}}"
 533    r, g, b = color.rgb255
 534
 535    return f"{{rgb,255:red,{r}; green,{g}; blue,{b}}}"
 536
 537
 538def get_scope_options(item: Union["Canvas", "Sketch"]) -> str:
 539    """Used for creating namespaces in TikZ.
 540
 541    Args:
 542        item (Union["Canvas", "Sketch"]): The item to get scope options for.
 543
 544    Returns:
 545        str: The scope options as a string.
 546    """
 547    options = []
 548
 549    if item.blend_group:
 550        options.append(f"blend group={item.blend_mode}")
 551    elif item.blend_mode:
 552        options.append(f"blend mode={item.blend_mode}")
 553    if item.fill_alpha not in [None, 1]:
 554        options.append(f"fill opacity={item.fill_alpha}")
 555    if item.line_alpha not in [None, 1]:
 556        options.append(f"draw opacity={item.line_alpha}")
 557    if item.text_alpha not in [None, 1]:
 558        options.append(f"text opacity={item.alpha}")
 559    if item.alpha not in [None, 1]:
 560        options.append(f"opacity={item.alpha}")
 561    if item.even_odd_rule:
 562        options.append("even odd rule")
 563    if item.transparency_group:
 564        options.append("transparency group")
 565
 566    return ",".join(options)
 567
 568
 569def get_clip_code(item: Union["Sketch", "Canvas"]) -> str:
 570    """Returns the clip code for a sketch or Canvas.
 571
 572    Args:
 573        item (Union["Sketch", "Canvas"]): The item to get clip code for.
 574
 575    Returns:
 576        str: The clip code as a string.
 577    """
 578    if item.mask.subtype == Types.CIRCLE:
 579        x, y = item.mask.center[:2]
 580        res = f"\\clip({x}, {y}) circle ({item.mask.radius});\n"
 581    elif item.mask.subtype == Types.RECTANGLE:
 582        x, y = item.mask.center[:2]
 583        width, height = item.mask.width, item.mask.height
 584        res = f"\\clip({x}, {y}) rectangle ({width}, {height});\n"
 585    elif item.mask.subtype == Types.SHAPE:
 586        # vertices = item.mask.primary_points.homogen_coords
 587        # coords = " ".join([f"({v[0]}, {v[1]})" for v in vertices])
 588        # res = f"\\clip plot[] coordinates {{{coords}}};\n"
 589        x, y = item.mask.origin[:2]
 590        width, height = item.mask.width, item.mask.height
 591        res = f"\\clip({x}, {y}) rectangle ({width}, {height});\n"
 592    else:
 593        res = ""
 594
 595    return res
 596
 597
 598def get_canvas_scope(canvas):
 599    """Returns the TikZ code for the canvas scope.
 600
 601    Args:
 602        canvas: The canvas object.
 603
 604    Returns:
 605        str: The TikZ code for the canvas scope.
 606    """
 607    options = get_scope_options(canvas)
 608    res = f"\\begin{{scope}}[{options}]\n"
 609    if (canvas.clip and canvas.mask) or canvas.size is not None:
 610        res += get_clip_code(canvas)
 611
 612    return res
 613
 614
 615def draw_batch_sketch(sketch, canvas):
 616    """Converts a BatchSketch to TikZ code.
 617
 618    Args:
 619        sketch: The BatchSketch object.
 620        canvas: The canvas object.
 621
 622    Returns:
 623        str: The TikZ code for the BatchSketch.
 624    """
 625    options = get_scope_options(sketch)
 626    if options:
 627        res = f"\\begin{{scope}}[{options}]\n"
 628    else:
 629        res = ""
 630    if sketch.clip and sketch.mask:
 631        res += get_clip_code(sketch)
 632    for item in sketch.items:
 633        if item.subtype in d_sketch_draw:
 634            res += d_sketch_draw[item.subtype](item, canvas)
 635        else:
 636            raise ValueError(f"Sketch type {item.subtype} not supported.")
 637
 638    if sketch.clip and sketch.mask:
 639        res += get_clip_code(sketch)
 640    if options:
 641        res += "\\end{scope}\n"
 642
 643    return res
 644
 645def draw_bbox_sketch(sketch):
 646    """Converts a BBoxSketch to TikZ code.
 647
 648    Args:
 649        sketch: The BBoxSketch object.
 650        canvas: The canvas object.
 651
 652    Returns:
 653        str: The TikZ code for the BBoxSketch.
 654    """
 655    attrib_map = {
 656        "line_color": "color",
 657        "line_width": "line width",
 658        "line_dash_array": "dash pattern",
 659    }
 660    attrib_list = ["line_color", "line_width", "line_dash_array"]
 661    options = sg_to_tikz(sketch, attrib_list, attrib_map)
 662    options = ", ".join(options)
 663    res = f"\\draw[{options}]"
 664    x1, y1 = sketch.vertices[1]
 665    x2, y2 = sketch.vertices[3]
 666    res += f"({x1}, {y1}) rectangle ({x2}, {y2});\n"
 667
 668    return res
 669
 670def draw_lace_sketch(item):
 671    """Converts a LaceSketch to TikZ code.
 672
 673    Args:
 674        item: The LaceSketch object.
 675
 676    Returns:
 677        str: The TikZ code for the LaceSketch.
 678    """
 679    if item.draw_fragments:
 680        for fragment in item.fragments:
 681            draw_shape_sketch(fragment)
 682    if item.draw_plaits:
 683        for plait in item.plaits:
 684            plait.fill = True
 685            draw_shape_sketch(plait)
 686
 687
 688def get_draw(sketch):
 689    """Returns the draw command for sketches.
 690
 691    Args:
 692        sketch: The sketch object.
 693
 694    Returns:
 695        str: The draw command as a string.
 696    """
 697    # sketch.closed, sketch.fill, sketch.stroke, shading
 698    decision_table = {
 699        (True, True, True, True): "\\shadedraw",
 700        (True, True, True, False): "\\filldraw",
 701        (True, True, False, True): "\\shade",
 702        (True, True, False, False): "\\fill",
 703        (True, False, True, True): "\\draw",
 704        (True, False, True, False): "\\draw",
 705        (True, False, False, True): False,
 706        (True, False, False, False): False,
 707        (False, True, True, True): "\\draw",
 708        (False, True, True, False): "\\draw",
 709        (False, True, False, True): False,
 710        (False, True, False, False): False,
 711        (False, False, True, True): "\\draw",
 712        (False, False, True, False): "\\draw",
 713        (False, False, False, True): False,
 714        (False, False, False, False): False,
 715    }
 716    if sketch.markers_only:
 717        res = "\\draw"
 718    else:
 719        if hasattr(sketch, 'back_style'):
 720            shading = sketch.back_style == BackStyle.SHADING
 721        else:
 722            shading = False
 723        if not hasattr(sketch, 'closed'):
 724            closed = False
 725        else:
 726            closed = sketch.closed
 727        if not hasattr(sketch, 'fill'):
 728            fill = False
 729        else:
 730            fill = sketch.fill
 731        if not hasattr(sketch, 'stroke'):
 732            stroke = False
 733        else:
 734            stroke = sketch.stroke
 735
 736        res = decision_table[(closed, fill, stroke, shading)]
 737
 738    return res
 739
 740
 741def get_frame_options(sketch):
 742    """Returns the options for the frame of a TagSketch.
 743
 744    Args:
 745        sketch: The TagSketch object.
 746
 747    Returns:
 748        list: The options for the frame of the TagSketch.
 749    """
 750    options = get_line_style_options(sketch)
 751    options += get_fill_style_options(sketch)
 752    if sketch.text in [None, ""]:
 753        if sketch.frame.frame_shape == "rectangle":
 754            width = sketch.frame.min_width
 755            height = sketch.frame.min_height
 756            if not width:
 757                width = defaults["min_width"]
 758            if not height:
 759                height = defaults["min_height"]
 760            options += "minimum width = {width}, minimum height = {height}"
 761        else:
 762            size = sketch.frame.min_size
 763            if not size:
 764                size = defaults["min_size"]
 765            options += f"minimum size = {size}"
 766    return options
 767
 768
 769def draw_tag_sketch(sketch):
 770    """Converts a TagSketch to TikZ code.
 771
 772    Args:
 773        sketch: The TagSketch object.
 774        canvas: The canvas object.
 775
 776    Returns:
 777        str: The TikZ code for the TagSketch.
 778    """
 779    # \node at (0,0) {some text};
 780    def get_font_family(sketch):
 781        default_fonts = [defaults['main_font'], defaults['sans_font'], defaults['mono_font']]
 782
 783        if sketch.font_family in default_fonts:
 784            if sketch.font_family == defaults['main_font']:
 785                res = 'tex_family', ''
 786            elif sketch.font_family == defaults['sans_font']:
 787                res = 'tex_family', 'textsf'
 788            else: # defaults['mono_font']
 789                res = 'tex_family', 'texttt'
 790        elif sketch.font_family:
 791            if isinstance(sketch.font_family, FontFamily):
 792                if sketch.font_family == FontFamily.SANSSERIF:
 793                    res = 'tex_family', 'textsf'
 794                elif sketch.font_family == FontFamily.MONOSPACE:
 795                    res = 'tex_family', 'texttt'
 796                else:
 797                    res = 'tex_family', 'textrm'
 798
 799            elif isinstance(sketch.font_family, str):
 800                res = 'new_family', sketch.font_family.replace(" ", "")
 801
 802            else:
 803                raise ValueError(f"Font family {sketch.font_family} not supported.")
 804        else:
 805            res = 'no_family', None
 806
 807        return res
 808
 809    def get_font_size(sketch):
 810
 811        if sketch.font_size:
 812            if isinstance(sketch.font_size, FontSize):
 813                res = 'tex_size', sketch.font_size.value
 814            else:
 815                res = 'num_size', sketch.font_size
 816        else:
 817            res = 'no_size', None
 818
 819        return res
 820
 821    res = []
 822    x, y = sketch.pos[:2]
 823
 824    options = ""
 825    if sketch.draw_frame:
 826        options += "draw"
 827        if sketch.stroke:
 828            if sketch.frame_shape != FrameShape.RECTANGLE:
 829                options += f", {sketch.frame_shape}, "
 830            line_style_options = get_line_style_options(sketch)
 831            if line_style_options:
 832                options += ', ' + ', '.join(line_style_options)
 833            if sketch.frame_inner_sep:
 834                options += f", inner sep={sketch.frame_inner_sep}"
 835            if sketch.minimum_width:
 836                options += f", minimum width={sketch.minimum_width}"
 837            if sketch.smooth and sketch.frame_shape not in [
 838                FrameShape.CIRCLE,
 839                FrameShape.ELLIPSE,
 840            ]:
 841                options += ", smooth"
 842
 843    if sketch.fill and sketch.back_color:
 844        options += f", fill={color2tikz(sketch.frame_back_color)}"
 845    if sketch.anchor:
 846        options += f", anchor={sketch.anchor.value}"
 847    if sketch.back_style == BackStyle.SHADING and sketch.fill:
 848        shading_options = get_shading_options(sketch)[0]
 849        options += ", " + shading_options
 850    if sketch.back_style == BackStyle.PATTERN and sketch.fill:
 851        pattern_options = get_pattern_options(sketch)[0]
 852        options += ", " + pattern_options
 853    if sketch.align != defaults["tag_align"]:
 854        options += f", align={sketch.align.value}"
 855    if sketch.text_width:
 856        options += f", text width={sketch.text_width}"
 857
 858
 859# no_family, tex_family, new_family
 860# no_size, tex_size, num_size
 861
 862# num_size and new_family {\fontsize{20}{24} \selectfont \Verdana ABCDEFG Hello, World! 25}
 863# tex_size and new_family {\large{\selectfont \Verdana ABCDEFG Hello, World! 50}}
 864# no_size and new_family {\selectfont \Verdana ABCDEFG Hello, World! 50}
 865
 866# tex_family {\textsc{\textit{\textbf{\Huge{\texttt{ABCDG Just a test -50}}}}}};
 867
 868# no_family {\textsc{\textit{\textbf{\Huge{ABCDG Just a test -50}}}}};
 869
 870    if sketch.font_color is not None and sketch.font_color != defaults["font_color"]:
 871        options += f", text={color2tikz(sketch.font_color)}"
 872    family, font_family = get_font_family(sketch)
 873    size, font_size = get_font_size(sketch)
 874    tex_text = ''
 875    if sketch.small_caps:
 876        tex_text += '\\textsc{'
 877
 878    if sketch.italic:
 879        tex_text += '\\textit{'
 880
 881    if sketch.bold:
 882        tex_text += '\\textbf{'
 883
 884    if size == 'num_size':
 885        f_size = font_size
 886        f_size2 = ceil(font_size * 1.2)
 887        tex_text += f"\\fontsize{{{f_size}}}{{{f_size2}}}\\selectfont "
 888
 889    elif size == 'tex_size':
 890        tex_text += f"\\{font_size}{{\\selectfont "
 891
 892    else:
 893        tex_text += "\\selectfont "
 894
 895    if family == 'new_family':
 896        tex_text += f"\\{font_family} {sketch.text}}}"
 897
 898    elif family == 'tex_family':
 899        if font_family:
 900            tex_text += f"\\{font_family}{{ {sketch.text}}}}}"
 901        else:
 902            tex_text += f"{{ {sketch.text}}}"
 903    else: # no_family
 904        tex_text += f"{{ {sketch.text}}}"
 905
 906    tex_text = '{' + tex_text
 907
 908    open_braces = tex_text.count('{')
 909    close_braces = tex_text.count('}')
 910    tex_text = tex_text + '}' * (open_braces - close_braces)
 911
 912    res.append(f"\\node[{options}] at ({x}, {y}) {tex_text};\n")
 913
 914    return ''.join(res)
 915
 916
 917def get_dash_pattern(line_dash_array):
 918    """Returns the dash pattern for a line.
 919
 920    Args:
 921        line_dash_array: The dash array for the line.
 922
 923    Returns:
 924        str: The dash pattern as a string.
 925    """
 926    dash_pattern = []
 927    for i, dash in enumerate(line_dash_array):
 928        if i % 2 == 0:
 929            dash_pattern.extend(["on", f"{dash}pt"])
 930        else:
 931            dash_pattern.extend(["off", f"{dash}pt"])
 932
 933    return " ".join(dash_pattern)
 934
 935
 936def sg_to_tikz(sketch, attrib_list, attrib_map, conditions=None, exceptions=None):
 937    """Converts the attributes of a sketch to TikZ options.
 938
 939    Args:
 940        sketch: The sketch object.
 941        attrib_list: The list of attributes to convert.
 942        attrib_map: The map of attributes to TikZ options.
 943        conditions: Optional conditions for the attributes.
 944        exceptions: Optional exceptions for the attributes.
 945
 946    Returns:
 947        list: The TikZ options as a list.
 948    """
 949    skip = ["marker_color", "fill_color"]
 950    tikz_way = {'line_width':LineWidth, 'line_dash_array':LineDashArray}
 951    if exceptions:
 952        skip += exceptions
 953    d_converters = {
 954        "line_color": color2tikz,
 955        "fill_color": color2tikz,
 956        "draw": color2tikz,
 957        "line_dash_array": get_dash_pattern,
 958    }
 959    options = []
 960    for attrib in attrib_list:
 961        if attrib not in attrib_map:
 962            continue
 963        if conditions and attrib in conditions and not conditions[attrib]:
 964            continue
 965        if attrib in tikz_way:
 966            value = getattr(sketch, attrib)
 967            if isinstance(value, tikz_way[attrib]):
 968                option = value.value
 969                options.append(option)
 970                continue
 971            if isinstance(value, str):
 972                if value in tikz_way[attrib]:
 973                    options.append(value)
 974                continue
 975
 976        tikz_attrib = attrib_map[attrib]
 977        if hasattr(sketch, attrib):
 978            value = getattr(sketch, attrib)
 979            if value is not None and tikz_attrib in list(attrib_map.values()):
 980                if attrib in ['smooth']: # smooth is a boolean
 981                    if value:
 982                        options.append("smooth")
 983
 984                elif attrib in skip:
 985                    value = color2tikz(getattr(sketch, attrib))
 986                    options.append(f"{tikz_attrib}={value}")
 987
 988                elif value != tikz_defaults[tikz_attrib]:
 989                    if attrib in d_converters:
 990                        value = d_converters[attrib](value)
 991                    options.append(f"{tikz_attrib}={value}")
 992
 993    return options
 994
 995
 996def get_line_style_options(sketch, exceptions=None):
 997    """Returns the options for the line style.
 998
 999    Args:
1000        sketch: The sketch object.
1001        exceptions: Optional exceptions for the line style options.
1002
1003    Returns:
1004        list: The line style options as a list.
1005    """
1006    attrib_map = {
1007        "line_color": "color",
1008        "line_width": "line width",
1009        "line_dash_array": "dash pattern",
1010        "line_cap": "line cap",
1011        "line_join": "line join",
1012        "line_miter_limit": "miter limit",
1013        "line_dash_phase": "dash phase",
1014        "line_alpha": "draw opacity",
1015        "smooth": "smooth",
1016        "fillet_radius": "rounded corners",
1017    }
1018    attribs = list(line_style_map.keys())
1019    if sketch.stroke:
1020        if exceptions and "draw_fillets" not in exceptions:
1021            conditions = {"fillet_radius": sketch.draw_fillets}
1022        else:
1023            conditions = None
1024        if sketch.line_alpha not in [None, 1]:
1025            sketch.line_alpha = sketch.line_alpha
1026        elif sketch.alpha not in [None, 1]:
1027            sketch.fill_alpha = sketch.alpha
1028        else:
1029            attribs.remove("line_alpha")
1030        if not sketch.smooth:
1031            attribs.remove("smooth")
1032        res = sg_to_tikz(sketch, attribs, attrib_map, conditions, exceptions)
1033    else:
1034        res = []
1035
1036    return res
1037
1038
1039def get_fill_style_options(sketch, exceptions=None, frame=False):
1040    """Returns the options for the fill style.
1041
1042    Args:
1043        sketch: The sketch object.
1044        exceptions: Optional exceptions for the fill style options.
1045        frame: Optional flag for frame fill style.
1046
1047    Returns:
1048        list: The fill style options as a list.
1049    """
1050    attrib_map = {
1051        "fill_color": "fill",
1052        "fill_alpha": "fill opacity",
1053        #'fill_mode': 'even odd rule',
1054        "blend_mode": "blend mode",
1055        "frame_back_color": "fill",
1056    }
1057    attribs = list(shape_style_map.keys())
1058    if sketch.fill_alpha not in [None, 1]:
1059        sketch.fill_alpha = sketch.fill_alpha
1060    elif sketch.alpha not in [None, 1]:
1061        sketch.fill_alpha = sketch.alpha
1062    else:
1063        attribs.remove("fill_alpha")
1064    if sketch.fill and not sketch.back_style == BackStyle.PATTERN:
1065        res = sg_to_tikz(sketch, attribs, attrib_map, exceptions=exceptions)
1066        if frame:
1067            res = [f"fill = {color2tikz(getattr(sketch, 'frame_back_color'))}"] + res
1068    else:
1069        res = []
1070
1071    return res
1072
1073
1074def get_axis_shading_colors(sketch):
1075    """Returns the shading colors for the axis.
1076
1077    Args:
1078        sketch: The sketch object.
1079
1080    Returns:
1081        str: The shading colors for the axis.
1082    """
1083    def get_color(color, color_key):
1084        if isinstance(color, Color):
1085            res = color2tikz(color)
1086        else:
1087            res = defaults[color_key]
1088
1089        return res
1090
1091    left = get_color(sketch.shade_left_color, "shade_left_color")
1092    right = get_color(sketch.shade_right_color, "shade_right_color")
1093    top = get_color(sketch.shade_top_color, "shade_top_color")
1094    bottom = get_color(sketch.shade_bottom_color, "shade_bottom_color")
1095    middle = get_color(sketch.shade_middle_color, "shade_middle_color")
1096
1097    axis_colors = {
1098        ShadeType.AXIS_BOTTOM_MIDDLE: f"bottom color={bottom}, middle color={middle}",
1099        ShadeType.AXIS_LEFT_MIDDLE: f"left color={left}, middle color={middle}",
1100        ShadeType.AXIS_RIGHT_MIDDLE: f"right color={right}, middle color={middle}",
1101        ShadeType.AXIS_TOP_MIDDLE: f"top color={top}, middle color={middle}",
1102        ShadeType.AXIS_LEFT_RIGHT: f"left color={left}, right color={right}",
1103        ShadeType.AXIS_TOP_BOTTOM: f"top color={top}, bottom color={bottom}",
1104    }
1105
1106    res = axis_colors[sketch.shade_type]
1107    return res
1108
1109
1110def get_bilinear_shading_colors(sketch):
1111    """Returns the shading colors for the bilinear shading.
1112
1113    Args:
1114        sketch: The sketch object.
1115
1116    Returns:
1117        str: The shading colors for the bilinear shading.
1118    """
1119    res = []
1120    if sketch.shade_upper_left_color:
1121        res.append(f"upper left = {color2tikz(sketch.shade_upper_left_color)}")
1122    if sketch.shade_upper_right_color:
1123        res.append(f"upper right = {color2tikz(sketch.shade_upper_right_color)}")
1124    if sketch.shade_lower_left_color:
1125        res.append(f"lower left = {color2tikz(sketch.shade_lower_left_color)}")
1126    if sketch.shade_lower_right_color:
1127        res.append(f"lower right = {color2tikz(sketch.shade_lower_right_color)}")
1128
1129    return ", ".join(res)
1130
1131
1132def get_radial_shading_colors(sketch):
1133    """Returns the shading colors for the radial shading.
1134
1135    Args:
1136        sketch: The sketch object.
1137
1138    Returns:
1139        str: The shading colors for the radial shading.
1140    """
1141    res = []
1142    if sketch.shade_type == ShadeType.RADIAL_INNER:
1143        res.append(f"inner color = {color2tikz(sketch.shade_inner_color)}")
1144    elif sketch.shade_type == ShadeType.RADIAL_OUTER:
1145        res.append(f"outer color = {color2tikz(sketch.shade_outer_color)}")
1146    elif sketch.shade_type == ShadeType.RADIAL_INNER_OUTER:
1147        res.append(f"inner color = {color2tikz(sketch.shade_inner_color)}")
1148        res.append(f"outer color = {color2tikz(sketch.shade_outer_color)}")
1149
1150    return ", ".join(res)
1151
1152
1153axis_shading_types = [
1154    ShadeType.AXIS_BOTTOM_MIDDLE,
1155    ShadeType.AXIS_LEFT_MIDDLE,
1156    ShadeType.AXIS_RIGHT_MIDDLE,
1157    ShadeType.AXIS_TOP_MIDDLE,
1158    ShadeType.AXIS_LEFT_RIGHT,
1159    ShadeType.AXIS_TOP_BOTTOM,
1160]
1161
1162radial_shading_types = [
1163    ShadeType.RADIAL_INNER,
1164    ShadeType.RADIAL_OUTER,
1165    ShadeType.RADIAL_INNER_OUTER,
1166]
1167
1168
1169def get_shading_options(sketch):
1170    """Returns the options for the shading.
1171
1172    Args:
1173        sketch: The sketch object.
1174
1175    Returns:
1176        list: The shading options as a list.
1177    """
1178    shade_type = sketch.shade_type
1179    if shade_type in axis_shading_types:
1180        res = get_axis_shading_colors(sketch)
1181        if sketch.shade_axis_angle:
1182            res += f", shading angle={sketch.shade_axis_angle}"
1183    elif shade_type == ShadeType.BILINEAR:
1184        res = get_bilinear_shading_colors(sketch)
1185    elif shade_type in radial_shading_types:
1186        res = get_radial_shading_colors(sketch)
1187    elif shade_type == ShadeType.BALL:
1188        res = f"ball color = {color2tikz(sketch.shade_ball_color)}"
1189    elif shade_type == ShadeType.COLORWHEEL:
1190        res = "shading=color wheel"
1191    elif shade_type == ShadeType.COLORWHEEL_BLACK:
1192        res = "shading=color wheel black center"
1193    elif shade_type == ShadeType.COLORWHEEL_WHITE:
1194        res = "shading=color wheel white center"
1195
1196    return [res]
1197
1198
1199def get_pattern_options(sketch):
1200    """Returns the options for the patterns.
1201
1202    Args:
1203        sketch: The sketch object.
1204
1205    Returns:
1206        list: The pattern options as a list.
1207    """
1208    pattern_type = sketch.pattern_type
1209    if pattern_type:
1210        distance = sketch.pattern_distance
1211        options = f"pattern={{{pattern_type}[distance={distance}, "
1212        angle = degrees(sketch.pattern_angle)
1213        if angle:
1214            options += f"angle={angle}, "
1215        line_width = sketch.pattern_line_width
1216        if line_width:
1217            options += f"line width={line_width}, "
1218        x_shift = sketch.pattern_x_shift
1219        if x_shift:
1220            options += f"xshift={x_shift}, "
1221        y_shift = sketch.pattern_y_shift
1222        if y_shift:
1223            options += f"yshift={y_shift}, "
1224        if pattern_type in ["Stars", "Dots"]:
1225            radius = sketch.pattern_radius
1226            if radius:
1227                options += f"radius={radius}, "
1228            if pattern_type == "Stars":
1229                points = sketch.pattern_points
1230                if points:
1231                    options += f"points={points}, "
1232        options = options.strip()
1233        if options.endswith(","):
1234            options = options[:-1]
1235        options += "]"
1236        color = sketch.pattern_color
1237        if color and color != sg.black:
1238            options += f", pattern color={color2tikz(color)}, "
1239
1240        options += "}"
1241        res = [options]
1242    else:
1243        res = []
1244
1245    return res
1246
1247
1248def get_marker_options(sketch):
1249    """Returns the options for the markers.
1250
1251    Args:
1252        sketch: The sketch object.
1253
1254    Returns:
1255        list: The marker options as a list.
1256    """
1257    attrib_map = {
1258        # 'marker': 'mark',
1259        "marker_size": "mark size",
1260        "marker_angle": "rotate",
1261        # 'fill_color': 'color',
1262        "marker_color": "color",
1263        "marker_fill": "fill",
1264        "marker_opacity": "opacity",
1265        "marker_repeat": "mark repeat",
1266        "marker_phase": "mark phase",
1267        "marker_tension": "tension",
1268        "marker_line_width": "line width",
1269        "marker_line_style": "style",
1270        # 'line_color': 'line color',
1271    }
1272    # if mark_stroke is false make line color same as fill color
1273    if sketch.draw_markers:
1274        res = sg_to_tikz(sketch, marker_style_map.keys(), attrib_map)
1275    else:
1276        res = []
1277
1278    return res
1279
1280
1281def draw_shape_sketch_with_indices(sketch, ind):
1282    """Draws a shape sketch with circle markers with index numbers in them.
1283
1284    Args:
1285        sketch: The shape sketch object.
1286        ind: The index.
1287
1288    Returns:
1289        str: The TikZ code for the shape sketch with indices.
1290    """
1291    begin_scope = get_begin_scope(ind)
1292    body = get_draw(sketch)
1293    if body:
1294        options = get_line_style_options(sketch)
1295        if sketch.fill and sketch.closed:
1296            options += get_fill_style_options(sketch)
1297        if sketch.smooth:
1298            if sketch.closed:
1299                options += ["smooth cycle"]
1300            else:
1301                options += ["smooth"]
1302        options = ", ".join(options)
1303        body += f"[{options}]"
1304    else:
1305        body = ""
1306    vertices = sketch.vertices
1307    vertices = [str(x) for x in vertices]
1308    str_lines = [vertices[0] + "node{0}"]
1309    n = len(vertices)
1310    for i, vertice in enumerate(vertices[1:]):
1311        if (i + 1) % 6 == 0:
1312            if i == n - 1:
1313                str_lines.append(f" -- {vertice} node{{{i+1}}}\n")
1314            else:
1315                str_lines.append(f"\n\t-- {vertice} node{{{i+1}}}")
1316        else:
1317            str_lines.append(f"-- {vertice} node{{{i+1}}}")
1318    if body:
1319        if sketch.closed:
1320            str_lines.append(" -- cycle;\n")
1321        str_lines.append(";\n")
1322    end_scope = get_end_scope()
1323
1324    return begin_scope + body + "".join(str_lines) + end_scope
1325
1326
1327def draw_shape_sketch_with_markers(sketch):
1328    """Draws a shape sketch with markers.
1329
1330    Args:
1331        sketch: The shape sketch object.
1332
1333    Returns:
1334        str: The TikZ code for the shape sketch with markers.
1335    """
1336    # begin_scope = get_begin_scope()
1337    body = get_draw(sketch)
1338    if body:
1339        options = get_line_style_options(sketch)
1340        if sketch.fill and sketch.closed:
1341            options += get_fill_style_options(sketch)
1342        if sketch.smooth and sketch.closed:
1343            options += ["smooth cycle"]
1344        elif sketch.smooth:
1345            options += ["smooth"]
1346        options = ", ".join(options)
1347        if options:
1348            body += f"[{options}]"
1349    else:
1350        body = ""
1351
1352    if sketch.draw_markers:
1353        marker_options = ", ".join(get_marker_options(sketch))
1354    else:
1355        marker_options = ""
1356
1357    vertices = [str(x) for x in sketch.vertices]
1358
1359    str_lines = [vertices[0]]
1360    for i, vertice in enumerate(vertices[1:]):
1361        if (i + 1) % 6 == 0:
1362            str_lines.append(f"\n\t{vertice} ")
1363        else:
1364            str_lines.append(f" {vertice} ")
1365    coordinates = "".join(str_lines)
1366
1367    marker = get_enum_value(MarkerType, sketch.marker_type)
1368    # marker = sketch.marker_type.value
1369    if sketch.markers_only:
1370        markers_only = "only marks ,"
1371    else:
1372        markers_only = ""
1373    if sketch.draw_markers and marker_options:
1374        body += (
1375            f" plot[mark = {marker}, {markers_only}mark options = {{{marker_options}}}] "
1376            f"\ncoordinates {{{coordinates}}};\n"
1377        )
1378    elif sketch.draw_markers:
1379        body += (
1380            f" plot[mark = {marker}, {markers_only}] coordinates {{{coordinates}}};\n"
1381        )
1382    else:
1383        body += f" plot[tension=.5] coordinates {{{coordinates}}};\n"
1384
1385    return body
1386
1387
1388def get_begin_scope(ind=None):
1389    """Returns \begin{scope}[every node/.append style=nodestyle{ind}].
1390
1391    Args:
1392        ind: Optional index for the scope.
1393
1394    Returns:
1395        str: The begin scope string.
1396    """
1397    if ind is None:
1398        res = "\\begin{scope}[]\n"
1399    else:
1400        res = f"\\begin{{scope}}[every node/.append style=nodestyle{ind}]\n"
1401
1402    return res
1403
1404
1405def get_end_scope():
1406    """Returns \\end{scope}.
1407
1408    Returns:
1409        str: The end scope string.
1410    """
1411    return "\\end{scope}\n"
1412
1413def draw_pattern_sketch(sketch):
1414    """Draws a pattern sketch.
1415
1416    Args:
1417        sketch: The pattern sketch object.
1418
1419    Returns:
1420        str: The TikZ code for the pattern sketch.
1421    """
1422    begin_scope = "\\begin{scope}"
1423
1424    options = []
1425
1426    if sketch.back_style == BackStyle.PATTERN and sketch.fill and sketch.closed:
1427        options += get_pattern_options(sketch)
1428    if sketch.stroke:
1429        options += get_line_style_options(sketch)
1430    if sketch.closed and sketch.fill:
1431        options += get_fill_style_options(sketch)
1432    if sketch.smooth:
1433        options += ["smooth"]
1434    if sketch.back_style == BackStyle.SHADING and sketch.fill and sketch.closed:
1435        options += get_shading_options(sketch)
1436    options = ", ".join(options)
1437    if options:
1438        begin_scope += f"[{options}]\n"
1439    end_scope = get_end_scope()
1440
1441    pattern = sketch.pattern
1442    draw = get_draw(sketch)
1443    if not draw:
1444        return ""
1445    all_vertices = sketch.kernel_vertices @ sketch.all_matrices
1446    vertices_list = np.hsplit(all_vertices, sketch.count)
1447    shapes = []
1448    for vertices in vertices_list:
1449        vertices @= sketch.xform_matrix
1450        vertices = [tuple(vert) for vert in vertices[:,:2].tolist()]
1451        n = len(vertices)
1452        str_lines = [f"{vertices[0]}"]
1453        for i, vertice in enumerate(vertices[1:]):
1454            if (i + 1) % 8 == 0:
1455                if i == n - 1:
1456                    str_lines.append(f"-- {vertice} \n")
1457                else:
1458                    str_lines.append(f"\n\t-- {vertice} ")
1459            else:
1460                str_lines.append(f"-- {vertice} ")
1461        if sketch.closed:
1462            str_lines.append("-- cycle;\n")
1463        else:
1464            str_lines.append(";\n")
1465        shapes.append(draw + "".join(str_lines))
1466
1467
1468    return begin_scope + f"[{options}]\n" +  "\n".join(shapes) + end_scope
1469
1470
1471def draw_sketch(sketch):
1472    """Draws a plain shape sketch.
1473
1474    Args:
1475        sketch: The shape sketch object.
1476
1477    Returns:
1478        str: The TikZ code for the plain shape sketch.
1479    """
1480    res = get_draw(sketch)
1481    if not res:
1482        return ""
1483    options = []
1484
1485    if sketch.back_style == BackStyle.PATTERN and sketch.fill and sketch.closed:
1486        options += get_pattern_options(sketch)
1487    if sketch.stroke:
1488        options += get_line_style_options(sketch)
1489    if sketch.closed and sketch.fill:
1490        options += get_fill_style_options(sketch)
1491    if sketch.smooth:
1492        options += ["smooth"]
1493    if sketch.back_style == BackStyle.SHADING and sketch.fill and sketch.closed:
1494        options += get_shading_options(sketch)
1495    options = ", ".join(options)
1496    if options:
1497        res += f"[{options}]"
1498    vertices = sketch.vertices
1499    n = len(vertices)
1500    str_lines = [f"{vertices[0]}"]
1501    for i, vertice in enumerate(vertices[1:]):
1502        if (i + 1) % 8 == 0:
1503            if i == n - 1:
1504                str_lines.append(f"-- {vertice} \n")
1505            else:
1506                str_lines.append(f"\n\t-- {vertice} ")
1507        else:
1508            str_lines.append(f"-- {vertice} ")
1509    if sketch.closed:
1510        str_lines.append("-- cycle;\n")
1511    else:
1512        str_lines.append(";\n")
1513    if res:
1514        res += "".join(str_lines)
1515    else:
1516        res = "".join(str_lines)
1517    return res
1518
1519
1520def draw_tex_sketch(sketch):
1521    """Draws a TeX sketch.
1522
1523    Args:
1524        sketch: The TeX sketch object.
1525
1526    Returns:
1527        str: The TeX code for the TeX sketch.
1528    """
1529
1530    return sketch.code
1531
1532def draw_shape_sketch(sketch, ind=None):
1533    """Draws a shape sketch.
1534
1535    Args:
1536        sketch: The shape sketch object.
1537        ind: Optional index for the shape sketch.
1538
1539    Returns:
1540        str: The TikZ code for the shape sketch.
1541    """
1542    d_subtype_draw = {
1543        sg.Types.ARC_SKETCH: draw_arc_sketch,
1544        sg.Types.BEZIER_SKETCH: draw_bezier_sketch,
1545        sg.Types.CIRCLE_SKETCH: draw_circle_sketch,
1546        sg.Types.ELLIPSE_SKETCH: draw_ellipse_sketch,
1547        sg.Types.LINE_SKETCH: draw_line_sketch,
1548    }
1549    if sketch.subtype in d_subtype_draw:
1550        res = d_subtype_draw[sketch.subtype](sketch)
1551    elif sketch.draw_markers and sketch.marker_type == MarkerType.INDICES:
1552        res = draw_shape_sketch_with_indices(sketch, ind)
1553    elif sketch.draw_markers or sketch.smooth:
1554        res = draw_shape_sketch_with_markers(sketch)
1555    else:
1556        res = draw_sketch(sketch)
1557
1558    return res
1559
1560def draw_line_sketch(sketch):
1561    """Draws a line sketch.
1562
1563    Args:
1564        sketch: The line sketch object.
1565
1566    Returns:
1567        str: The TikZ code for the line sketch.
1568    """
1569    begin_scope = get_begin_scope()
1570    res = "\\draw"
1571    exceptions = ["draw_fillets", "fillet_radius", "line_join", "line_miter_limit"]
1572    options = get_line_style_options(sketch, exceptions=exceptions)
1573
1574    start = sketch.vertices[0]
1575    end = sketch.vertices[1]
1576    options = ", ".join(options)
1577    res += f"[{options}]"
1578    res += f" {start[:2]} -- {end[:2]};\n"
1579    end_scope = get_end_scope()
1580
1581    return begin_scope + res + end_scope
1582
1583
1584def draw_circle_sketch(sketch):
1585    """Draws a circle sketch.
1586
1587    Args:
1588        sketch: The circle sketch object.
1589
1590    Returns:
1591        str: The TikZ code for the circle sketch.
1592    """
1593    begin_scope = get_begin_scope()
1594    res = get_draw(sketch)
1595    if not res:
1596        return ""
1597    options = get_line_style_options(sketch)
1598    fill_options = get_fill_style_options(sketch)
1599    options += fill_options
1600    if sketch.smooth:
1601        options += ["smooth"]
1602    options = ", ".join(options)
1603    if options:
1604        res += f"[{options}]"
1605    x, y = sketch.center[:2]
1606    res += f"({x}, {y}) circle ({sketch.radius});\n"
1607    end_scope = get_end_scope()
1608
1609    return begin_scope + res + end_scope
1610
1611
1612def draw_rect_sketch(sketch):
1613    """Draws a rectangle sketch.
1614
1615    Args:
1616        sketch: The rectangle sketch object.
1617
1618    Returns:
1619        str: The TikZ code for the rectangle sketch.
1620    """
1621    begin_scope = get_begin_scope()
1622    res = get_draw(sketch)
1623    if not res:
1624        return ""
1625    options = get_line_style_options(sketch)
1626    fill_options = get_fill_style_options(sketch)
1627    options += fill_options
1628    if sketch.smooth:
1629        options += ["smooth"]
1630    options = ", ".join(options)
1631    res += f"[{options}]"
1632    x, y = sketch.center[:2]
1633    width, height = sketch.width, sketch.height
1634    res += f"({x}, {y}) rectangle ({width}, {height});\n"
1635    end_scope = get_end_scope()
1636
1637    return begin_scope + res + end_scope
1638
1639def draw_ellipse_sketch(sketch):
1640    """Draws an ellipse sketch.
1641
1642    Args:
1643        sketch: The ellipse sketch object.
1644
1645    Returns:
1646        str: The TikZ code for the ellipse sketch.
1647    """
1648    begin_scope = get_begin_scope()
1649    res = get_draw(sketch)
1650    if not res:
1651        return ""
1652    options = get_line_style_options(sketch)
1653    fill_options = get_fill_style_options(sketch)
1654    options += fill_options
1655    if sketch.smooth:
1656        options += ["smooth"]
1657    angle = degrees(sketch.angle)
1658    x, y = sketch.center[:2]
1659    if angle:
1660        options += [f"rotate around= {{{angle}:({x},{y})}}"]
1661    options = ", ".join(options)
1662    res += f"[{options}]"
1663    a = sketch.x_radius
1664    b = sketch.y_radius
1665
1666    res += f"({x}, {y}) ellipse ({a} and {b});\n"
1667    end_scope = get_end_scope()
1668
1669    return begin_scope + res + end_scope
1670
1671
1672def draw_arc_sketch(sketch):
1673    """Draws an arc sketch.
1674
1675    Args:
1676        sketch: The arc sketch object.
1677
1678    Returns:
1679        str: The TikZ code for the arc sketch.
1680    """
1681    res = get_draw(sketch)
1682    if not res:
1683        return ""
1684    if sketch.closed:
1685        options = ["smooth cycle"]
1686    else:
1687        options = ['smooth']
1688
1689    if sketch.back_style == BackStyle.PATTERN and sketch.fill and sketch.closed:
1690        options += get_pattern_options(sketch)
1691    if sketch.stroke:
1692        options += get_line_style_options(sketch)
1693    if sketch.closed and sketch.fill:
1694        options += get_fill_style_options(sketch)
1695
1696    if sketch.back_style == BackStyle.SHADING and sketch.fill and sketch.closed:
1697        options += get_shading_options(sketch)
1698    options = ", ".join(options)
1699    if options:
1700        res += f"[{options}] plot[tension=.8] coordinates" + "{"
1701    vertices = [round_point(v) for v in sketch.vertices]
1702    n = len(vertices)
1703    str_lines = [f"{vertices[0]}"]
1704    for i, vertice in enumerate(vertices[1:]):
1705        if (i + 1) % 8 == 0:
1706            if i == n - 1:
1707                str_lines.append(f" {vertice} \n")
1708            else:
1709                str_lines.append(f"\n\t {vertice} ")
1710        else:
1711            str_lines.append(f" {vertice} ")
1712    if sketch.closed:
1713        str_lines.append(" cycle;\n")
1714    else:
1715        str_lines.append("};\n")
1716    if res:
1717        res += "".join(str_lines)
1718    else:
1719        res = "".join(str_lines)
1720    return res
1721
1722def draw_bezier_sketch(sketch):
1723    """Draws a Bezier curve sketch.
1724
1725    Args:
1726        sketch: The Bezier curve sketch object.
1727
1728    Returns:
1729        str: The TikZ code for the Bezier curve sketch.
1730    """
1731    begin_scope = get_begin_scope()
1732    res = get_draw(sketch)
1733    if not res:
1734        return ""
1735    options = get_line_style_options(sketch)
1736    options = ", ".join(options)
1737    res += f"[{options}]"
1738    p1, cp1, cp2, p2 = sketch.control_points
1739    x1, y1 = p1[:2]
1740    x2, y2 = cp1[:2]
1741    x3, y3 = cp2[:2]
1742    x4, y4 = p2[:2]
1743    res += f" ({x1}, {y1}) .. controls ({x2}, {y2}) and ({x3}, {y3}) .. ({x4}, {y4});\n"
1744    end_scope = get_end_scope()
1745
1746    return begin_scope + res + end_scope
1747
1748
1749def draw_line(line):
1750    """Tikz code for a line.
1751
1752    Args:
1753        line: The line object.
1754
1755    Returns:
1756        str: The TikZ code for the line.
1757    """
1758    p1 = line.start[:2]
1759    p2 = line.end[:2]
1760    options = []
1761    if line.line_width is not None:
1762        options.append(line.line_width)
1763    if line.color is not None:
1764        color = color2tikz(line.color)
1765        options.append(color)
1766    if line.dash_array is not None:
1767        options.append(line.dash_array)
1768    # options = [line.width, line.color, line.dash_array, line.cap, line.join]
1769    if line.line_width == 0:
1770        res = f"\\path[{', '.join(options)}] {p1} -- {p2};\n"
1771    else:
1772        res = f"\\draw[{', '.join(options)}] {p1} -- {p2};\n"
1773
1774    return res
1775
1776
1777def is_stroked(shape: Shape) -> bool:
1778    """Returns True if the shape is stroked.
1779
1780    Args:
1781        shape (Shape): The shape object.
1782
1783    Returns:
1784        bool: True if the shape is stroked, False otherwise.
1785    """
1786    return shape.stroke and shape.line_color is not None and shape.line_width > 0
1787
1788
1789d_sketch_draw = {
1790    sg.Types.SHAPE: draw_shape_sketch,
1791    sg.Types.TAG_SKETCH: draw_tag_sketch,
1792    sg.Types.LACESKETCH: draw_lace_sketch,
1793    sg.Types.LINE: draw_line_sketch,
1794    sg.Types.CIRCLE: draw_circle_sketch,
1795    sg.Types.ELLIPSE: draw_shape_sketch,
1796    sg.Types.ARC: draw_arc_sketch,
1797    sg.Types.BATCH: draw_batch_sketch,
1798}
def array(unknown):

array(object, dtype=None, *, copy=True, order='K', subok=False, ndmin=0, like=None)

Create an array.

Parameters

object : array_like An array, any object exposing the array interface, an object whose __array__ method returns an array, or any (nested) sequence. If object is a scalar, a 0-dimensional array containing object is returned. dtype : data-type, optional The desired data-type for the array. If not given, NumPy will try to use a default dtype that can represent the values (by applying promotion rules when necessary.) copy : bool, optional If True (default), then the array data is copied. If None, a copy will only be made if __array__ returns a copy, if obj is a nested sequence, or if a copy is needed to satisfy any of the other requirements (dtype, order, etc.). Note that any copy of the data is shallow, i.e., for arrays with object dtype, the new array will point to the same objects. See Examples for ndarray.copy. For False it raises a ValueError if a copy cannot be avoided. Default: True. order : {'K', 'A', 'C', 'F'}, optional Specify the memory layout of the array. If object is not an array, the newly created array will be in C order (row major) unless 'F' is specified, in which case it will be in Fortran order (column major). If object is an array the following holds.

===== ========= ===================================================
order  no copy                     copy=True
===== ========= ===================================================
'K'   unchanged F & C order preserved, otherwise most similar order
'A'   unchanged F order if input is F and not C, otherwise C order
'C'   C order   C order
'F'   F order   F order
===== ========= ===================================================

When ``copy=None`` and a copy is made for other reasons, the result is
the same as if ``copy=True``, with some exceptions for 'A', see the
Notes section. The default order is 'K'.

subok : bool, optional If True, then sub-classes will be passed-through, otherwise the returned array will be forced to be a base-class array (default). ndmin : int, optional Specifies the minimum number of dimensions that the resulting array should have. Ones will be prepended to the shape as needed to meet this requirement. like : array_like, optional Reference object to allow the creation of arrays which are not NumPy arrays. If an array-like passed in as like supports the __array_function__ protocol, the result will be defined by it. In this case, it ensures the creation of an array object compatible with that passed in via this argument.

*New in version 1.20.0.*

Returns

out : ndarray An array object satisfying the specified requirements.

See Also

empty_like : Return an empty array with shape and type of input. ones_like : Return an array of ones with shape and type of input. zeros_like : Return an array of zeros with shape and type of input. full_like : Return a new array with shape of input filled with value. empty : Return a new uninitialized array. ones : Return a new array setting values to one. zeros : Return a new array setting values to zero. full : Return a new array of given shape filled with value. copy: Return an array copy of the given object.

Notes

When order is 'A' and object is an array in neither 'C' nor 'F' order, and a copy is forced by a change in dtype, then the order of the result is not necessarily 'C' as expected. This is likely a bug.

Examples

>>> np.array([1, 2, 3])
array([1, 2, 3])

Upcasting:

>>> np.array([1, 2, 3.0])
array([ 1.,  2.,  3.])

More than one dimension:

>>> np.array([[1, 2], [3, 4]])
array([[1, 2],
       [3, 4]])

Minimum dimensions 2:

>>> np.array([1, 2, 3], ndmin=2)
array([[1, 2, 3]])

Type provided:

>>> np.array([1, 2, 3], dtype=complex)
array([ 1.+0.j,  2.+0.j,  3.+0.j])

Data-type consisting of more than one element:

>>> x = np.array([(1,2),(3,4)],dtype=[('a','<i4'),('b','<i4')])
>>> x['a']
array([1, 3])

Creating an array from sub-classes:

>>> np.array(np.asmatrix('1 2; 3 4'))
array([[1, 2],
       [3, 4]])
>>> np.array(np.asmatrix('1 2; 3 4'), subok=True)
matrix([[1, 2],
        [3, 4]])
enum_map = {}
def scope_code_required(item: "Union['Canvas', 'Batch']") -> bool:
52def scope_code_required(item: Union["Canvas", "Batch"]) -> bool:
53    """Check if a TikZ namespace is required for the item.
54
55    Args:
56        item (Union["Canvas", "Batch"]): The item to check.
57
58    Returns:
59        bool: True if a TikZ namespace is required, False otherwise.
60    """
61    return (
62        item.blend_mode is not None
63        or item.transparency_group
64        or (item.clip and item.mask)
65    )

Check if a TikZ namespace is required for the item.

Arguments:
  • item (Union["Canvas", "Batch"]): The item to check.
Returns:

bool: True if a TikZ namespace is required, False otherwise.

@dataclass
class Tex:
 68@dataclass
 69class Tex:
 70    """Tex class for generating tex code.
 71
 72    Attributes:
 73        begin_document (str): The beginning of the document.
 74        end_document (str): The end of the document.
 75        begin_tikz (str): The beginning of the TikZ environment.
 76        end_tikz (str): The end of the TikZ environment.
 77        packages (List[str]): List of required TeX packages.
 78        tikz_libraries (List[str]): List of required TikZ libraries.
 79        tikz_code (str): The generated TikZ code.
 80        sketches (List["Sketch"]): List of Sketch objects.
 81    """
 82
 83    begin_document: str = defaults["begin_doc"]
 84    end_document: str = defaults["end_doc"]
 85    begin_tikz: str = defaults["begin_tikz"]
 86    end_tikz: str = defaults["end_tikz"]
 87    packages: List[str] = None
 88    tikz_libraries: List[str] = None
 89    tikz_code: str = ""  # Generated by the canvas by using sketches
 90    sketches: List["Sketch"] = field(default_factory=list)  # List of TexSketch objects
 91
 92    def __post_init__(self):
 93        """Post-initialization method."""
 94        self.type = Types.TEX
 95        common_properties(self)
 96
 97    def tex_code(self, canvas: "Canvas", aux_code: str) -> str:
 98        """Generate the final TeX code.
 99
100        Args:
101            canvas ("Canvas"): The canvas object.
102            aux_code (str): Auxiliary code to include.
103
104        Returns:
105            str: The final TeX code.
106        """
107        doc_code = []
108        for sketch in self.sketches:
109            if sketch.location == TexLoc.DOCUMENT:
110                doc_code.append(sketch.code)
111        doc_code = "\n".join(doc_code)
112        back_color = f"\\pagecolor{color2tikz(canvas.back_color)}"
113        self.begin_document = self.begin_document + back_color + "\n"
114        if canvas.limits is not None:
115            begin_tikz = self.begin_tikz + get_limits_code(canvas) + "\n"
116        else:
117            begin_tikz = self.begin_tikz + "\n"
118        if scope_code_required(canvas):
119            scope = get_canvas_scope(canvas)
120            code = (
121                self.get_preamble(canvas)
122                + self.begin_document
123                + doc_code
124                + begin_tikz
125                + scope
126                + self.get_tikz_code()
127                + aux_code
128                + "\\end{scope}\n"
129                + self.end_tikz
130                + self.end_document
131            )
132        else:
133            code = (
134                self.get_preamble(canvas)
135                + self.begin_document
136                + doc_code
137                + begin_tikz
138                + self.get_tikz_code()
139                + aux_code
140                + self.end_tikz
141                + self.end_document
142            )
143
144        return code
145
146    def get_doc_class(self, border: float, font_size: int) -> str:
147        """Returns the document class.
148
149        Args:
150            border (float): The border size.
151            font_size (int): The font size.
152
153        Returns:
154            str: The document class string.
155        """
156        return f"\\documentclass[{font_size}pt,tikz,border={border}pt]{{standalone}}\n"
157
158    def get_tikz_code(self) -> str:
159        """Returns the TikZ code.
160
161        Returns:
162            str: The TikZ code.
163        """
164        code = ""
165        for sketch in self.sketches:
166            if sketch.location == TexLoc.PICTURE:
167                code += sketch.text + "\n"
168
169        return code
170
171    def get_tikz_libraries(self) -> str:
172        """Returns the TikZ libraries.
173
174        Returns:
175            str: The TikZ libraries string.
176        """
177        return f"\\usetikzlibrary{{{','.join(self.tikz_libraries)}}}\n"
178
179    def get_packages(self, canvas) -> str:
180        """Returns the required TeX packages.
181
182        Args:
183            canvas: The canvas object.
184
185        Returns:
186            str: The required TeX packages.
187        """
188        tikz_libraries = []
189        tikz_packages = ['tikz', 'pgf']
190        for page in canvas.pages:
191            for sketch in page.sketches:
192                if hasattr(sketch, "draw_frame") and sketch.draw_frame:
193                    if hasattr(sketch, "frame_shape") and sketch.frame_shape != FrameShape.RECTANGLE:
194                        if "shapes.geometric" not in tikz_libraries:
195                            tikz_libraries.append("shapes.geometric")
196                if sketch.draw_markers:
197                    if 'patterns' not in tikz_libraries:
198                        tikz_libraries.append("patterns")
199                        tikz_libraries.append("patterns.meta")
200                        tikz_libraries.append("backgrounds")
201                        tikz_libraries.append("shadings")
202                if sketch.line_dash_array:
203                    if 'patterns' not in tikz_libraries:
204                        tikz_libraries.append("patterns")
205                if sketch.subtype == Types.TAG_SKETCH:
206                    if "fontspec" not in tikz_packages:
207                        tikz_packages.append("fontspec")
208                else:
209                    if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
210                        if "fontspec" not in tikz_packages:
211                            tikz_packages.append("fontspec")
212                if hasattr(sketch, 'back_style'):
213                    if sketch.back_style == BackStyle.COLOR:
214                        if "xcolor" not in tikz_packages:
215                            tikz_packages.append("xcolor")
216                    if sketch.back_style == BackStyle.SHADING:
217                        if "shadings" not in tikz_libraries:
218                            tikz_libraries.append("shadings")
219                    if sketch.back_style == BackStyle.PATTERN:
220                        if "patterns" not in tikz_libraries:
221                            tikz_libraries.append("patterns")
222                            tikz_libraries.append("patterns.meta")
223
224
225        return tikz_libraries, tikz_packages
226
227    def get_preamble(self, canvas) -> str:
228        """Returns the TeX preamble.
229
230        Args:
231            canvas: The canvas object.
232
233        Returns:
234            str: The TeX preamble.
235        """
236        libraries, packages = self.get_packages(canvas)
237
238        if packages:
239            packages = f'\\usepackage{{{",".join(packages)}}}\n'
240            if 'fontspec' in packages:
241                fonts_section = f"""\\setmainfont{{{defaults['main_font']}}}
242\\setsansfont{{{defaults['sans_font']}}}
243\\setmonofont{{{defaults['mono_font']}}}\n"""
244
245        if libraries:
246            libraries = f'\\usetikzlibrary{{{",".join(libraries)}}}\n'
247
248        if canvas.border is None:
249            border = defaults["border"]
250        elif isinstance(canvas.border, (int, float)):
251            border = canvas.border
252        else:
253            raise ValueError("Canvas.border must be a positive numeric value.")
254        if border < 0:
255            raise ValueError("Canvas.border must be a positive numeric value.")
256        doc_class = self.get_doc_class(border, defaults["font_size"])
257        # Check if different fonts are used
258        fonts_section = ""
259        fonts = canvas.get_fonts_list()
260        for font in fonts:
261            if font is None:
262                continue
263            font_family = font.replace(" ", "")
264            fonts_section += f"\\newfontfamily\\{font_family}[Scale=1.0]{{{font}}}\n"
265        preamble = f"{doc_class}{packages}{libraries}{fonts_section}"
266
267        indices = False
268        for sketch in canvas.active_page.sketches:
269            if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
270                indices = True
271                break
272        if indices:
273            font_family = defaults["indices_font_family"]
274            font_size = defaults["indices_font_size"]
275            count = 0
276            for sketch in canvas.active_page.sketches:
277                if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
278                    preamble += "\\tikzset{\n"
279                    node_style = (
280                        f"nodestyle{count}/.style={{draw, circle, gray, "
281                        f"text=black, fill=white, line width = .5, inner sep=.5, "
282                        f"font=\\{font_family}\\{font_size}}}\n}}\n"
283                    )
284                    preamble += node_style
285                    count += 1
286        for sketch in canvas.active_page.sketches:
287            if sketch.subtype == Types.TEX_SKETCH:
288                if sketch.location == TexLoc.PREAMBLE:
289                    preamble += sketch.code + "\n"
290        return preamble

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 Sketch objects.
Tex( begin_document: str = '\\begin{document}\n', end_document: str = '\\end{document}\n', begin_tikz: str = '\\begin{tikzpicture}[x=1pt, y=1pt, scale=1]\n', end_tikz: str = '\\end{tikzpicture}\n', packages: List[str] = None, tikz_libraries: List[str] = None, tikz_code: str = '', sketches: "List['Sketch']" = <factory>)
begin_document: str = '\\begin{document}\n'
end_document: str = '\\end{document}\n'
begin_tikz: str = '\\begin{tikzpicture}[x=1pt, y=1pt, scale=1]\n'
end_tikz: str = '\\end{tikzpicture}\n'
packages: List[str] = None
tikz_libraries: List[str] = None
tikz_code: str = ''
sketches: "List['Sketch']"
def tex_code(self, canvas: "'Canvas'", aux_code: str) -> str:
 97    def tex_code(self, canvas: "Canvas", aux_code: str) -> str:
 98        """Generate the final TeX code.
 99
100        Args:
101            canvas ("Canvas"): The canvas object.
102            aux_code (str): Auxiliary code to include.
103
104        Returns:
105            str: The final TeX code.
106        """
107        doc_code = []
108        for sketch in self.sketches:
109            if sketch.location == TexLoc.DOCUMENT:
110                doc_code.append(sketch.code)
111        doc_code = "\n".join(doc_code)
112        back_color = f"\\pagecolor{color2tikz(canvas.back_color)}"
113        self.begin_document = self.begin_document + back_color + "\n"
114        if canvas.limits is not None:
115            begin_tikz = self.begin_tikz + get_limits_code(canvas) + "\n"
116        else:
117            begin_tikz = self.begin_tikz + "\n"
118        if scope_code_required(canvas):
119            scope = get_canvas_scope(canvas)
120            code = (
121                self.get_preamble(canvas)
122                + self.begin_document
123                + doc_code
124                + begin_tikz
125                + scope
126                + self.get_tikz_code()
127                + aux_code
128                + "\\end{scope}\n"
129                + self.end_tikz
130                + self.end_document
131            )
132        else:
133            code = (
134                self.get_preamble(canvas)
135                + self.begin_document
136                + doc_code
137                + begin_tikz
138                + self.get_tikz_code()
139                + aux_code
140                + self.end_tikz
141                + self.end_document
142            )
143
144        return code

Generate the final TeX code.

Arguments:
  • canvas ("Canvas"): The canvas object.
  • aux_code (str): Auxiliary code to include.
Returns:

str: The final TeX code.

def get_doc_class(self, border: float, font_size: int) -> str:
146    def get_doc_class(self, border: float, font_size: int) -> str:
147        """Returns the document class.
148
149        Args:
150            border (float): The border size.
151            font_size (int): The font size.
152
153        Returns:
154            str: The document class string.
155        """
156        return f"\\documentclass[{font_size}pt,tikz,border={border}pt]{{standalone}}\n"

Returns the document class.

Arguments:
  • border (float): The border size.
  • font_size (int): The font size.
Returns:

str: The document class string.

def get_tikz_code(self) -> str:
158    def get_tikz_code(self) -> str:
159        """Returns the TikZ code.
160
161        Returns:
162            str: The TikZ code.
163        """
164        code = ""
165        for sketch in self.sketches:
166            if sketch.location == TexLoc.PICTURE:
167                code += sketch.text + "\n"
168
169        return code

Returns the TikZ code.

Returns:

str: The TikZ code.

def get_tikz_libraries(self) -> str:
171    def get_tikz_libraries(self) -> str:
172        """Returns the TikZ libraries.
173
174        Returns:
175            str: The TikZ libraries string.
176        """
177        return f"\\usetikzlibrary{{{','.join(self.tikz_libraries)}}}\n"

Returns the TikZ libraries.

Returns:

str: The TikZ libraries string.

def get_packages(self, canvas) -> str:
179    def get_packages(self, canvas) -> str:
180        """Returns the required TeX packages.
181
182        Args:
183            canvas: The canvas object.
184
185        Returns:
186            str: The required TeX packages.
187        """
188        tikz_libraries = []
189        tikz_packages = ['tikz', 'pgf']
190        for page in canvas.pages:
191            for sketch in page.sketches:
192                if hasattr(sketch, "draw_frame") and sketch.draw_frame:
193                    if hasattr(sketch, "frame_shape") and sketch.frame_shape != FrameShape.RECTANGLE:
194                        if "shapes.geometric" not in tikz_libraries:
195                            tikz_libraries.append("shapes.geometric")
196                if sketch.draw_markers:
197                    if 'patterns' not in tikz_libraries:
198                        tikz_libraries.append("patterns")
199                        tikz_libraries.append("patterns.meta")
200                        tikz_libraries.append("backgrounds")
201                        tikz_libraries.append("shadings")
202                if sketch.line_dash_array:
203                    if 'patterns' not in tikz_libraries:
204                        tikz_libraries.append("patterns")
205                if sketch.subtype == Types.TAG_SKETCH:
206                    if "fontspec" not in tikz_packages:
207                        tikz_packages.append("fontspec")
208                else:
209                    if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
210                        if "fontspec" not in tikz_packages:
211                            tikz_packages.append("fontspec")
212                if hasattr(sketch, 'back_style'):
213                    if sketch.back_style == BackStyle.COLOR:
214                        if "xcolor" not in tikz_packages:
215                            tikz_packages.append("xcolor")
216                    if sketch.back_style == BackStyle.SHADING:
217                        if "shadings" not in tikz_libraries:
218                            tikz_libraries.append("shadings")
219                    if sketch.back_style == BackStyle.PATTERN:
220                        if "patterns" not in tikz_libraries:
221                            tikz_libraries.append("patterns")
222                            tikz_libraries.append("patterns.meta")
223
224
225        return tikz_libraries, tikz_packages

Returns the required TeX packages.

Arguments:
  • canvas: The canvas object.
Returns:

str: The required TeX packages.

def get_preamble(self, canvas) -> str:
227    def get_preamble(self, canvas) -> str:
228        """Returns the TeX preamble.
229
230        Args:
231            canvas: The canvas object.
232
233        Returns:
234            str: The TeX preamble.
235        """
236        libraries, packages = self.get_packages(canvas)
237
238        if packages:
239            packages = f'\\usepackage{{{",".join(packages)}}}\n'
240            if 'fontspec' in packages:
241                fonts_section = f"""\\setmainfont{{{defaults['main_font']}}}
242\\setsansfont{{{defaults['sans_font']}}}
243\\setmonofont{{{defaults['mono_font']}}}\n"""
244
245        if libraries:
246            libraries = f'\\usetikzlibrary{{{",".join(libraries)}}}\n'
247
248        if canvas.border is None:
249            border = defaults["border"]
250        elif isinstance(canvas.border, (int, float)):
251            border = canvas.border
252        else:
253            raise ValueError("Canvas.border must be a positive numeric value.")
254        if border < 0:
255            raise ValueError("Canvas.border must be a positive numeric value.")
256        doc_class = self.get_doc_class(border, defaults["font_size"])
257        # Check if different fonts are used
258        fonts_section = ""
259        fonts = canvas.get_fonts_list()
260        for font in fonts:
261            if font is None:
262                continue
263            font_family = font.replace(" ", "")
264            fonts_section += f"\\newfontfamily\\{font_family}[Scale=1.0]{{{font}}}\n"
265        preamble = f"{doc_class}{packages}{libraries}{fonts_section}"
266
267        indices = False
268        for sketch in canvas.active_page.sketches:
269            if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
270                indices = True
271                break
272        if indices:
273            font_family = defaults["indices_font_family"]
274            font_size = defaults["indices_font_size"]
275            count = 0
276            for sketch in canvas.active_page.sketches:
277                if hasattr(sketch, "marker_type") and sketch.marker_type == "indices":
278                    preamble += "\\tikzset{\n"
279                    node_style = (
280                        f"nodestyle{count}/.style={{draw, circle, gray, "
281                        f"text=black, fill=white, line width = .5, inner sep=.5, "
282                        f"font=\\{font_family}\\{font_size}}}\n}}\n"
283                    )
284                    preamble += node_style
285                    count += 1
286        for sketch in canvas.active_page.sketches:
287            if sketch.subtype == Types.TEX_SKETCH:
288                if sketch.location == TexLoc.PREAMBLE:
289                    preamble += sketch.code + "\n"
290        return preamble

Returns the TeX preamble.

Arguments:
  • canvas: The canvas object.
Returns:

str: The TeX preamble.

def get_back_grid_code(grid: Grid, canvas: "'Canvas'") -> str:
293def get_back_grid_code(grid: Grid, canvas: "Canvas") -> str:
294    """Page background grid code.
295
296    Args:
297        grid (Grid): The grid object.
298        canvas ("Canvas"): The canvas object.
299
300    Returns:
301        str: The background grid code.
302    """
303    # \usetikzlibrary{backgrounds}
304    # \begin{scope}[on background layer]
305    # \fill[gray] (current bounding box.south west) rectangle
306    # (current bounding box.north east);
307    # \draw[white,step=.5cm] (current bounding box.south west) grid
308    # (current bounding box.north east);
309    # \end{scope}
310    grid = canvas.active_page.grid
311    back_color = color2tikz(grid.back_color)
312    line_color = color2tikz(grid.line_color)
313    step = grid.spacing
314    lines = ["\\begin{scope}[on background layer]\n"]
315    lines.append(f"\\fill[color={back_color}] (current bounding box.south west) ")
316    lines.append("rectangle (current bounding box.north east);\n")
317    options = []
318    if grid.line_dash_array is not None:
319        options.append(f"dashed, dash pattern={get_dash_pattern(grid.line_dash_array)}")
320    if grid.line_width is not None:
321        options.append(f"line width={grid.line_width}")
322    if options:
323        options = ",".join(options)
324        lines.append(f"\\draw[color={line_color}, step={step}, {options}]")
325    else:
326        lines.append(f"\\draw[color={line_color},step={step}]")
327    lines.append("(current bounding box.south west)")
328    lines.append(" grid (current bounding box.north east);\n")
329    lines.append("\\end{scope}\n")
330
331    return "".join(lines)

Page background grid code.

Arguments:
  • grid (Grid): The grid object.
  • canvas ("Canvas"): The canvas object.
Returns:

str: The background grid code.

def get_limits_code(canvas: "'Canvas'") -> str:
334def get_limits_code(canvas: "Canvas") -> str:
335    """Get the limits of the canvas for clipping.
336
337    Args:
338        canvas ("Canvas"): The canvas object.
339
340    Returns:
341        str: The limits code for clipping.
342    """
343    xmin, ymin, xmax, ymax = canvas.limits
344    points = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]
345    vertices = homogenize(points) @ canvas.xform_matrix
346    coords = " ".join([f"({v[0]}, {v[1]})" for v in vertices])
347
348    return f"\\clip plot[] coordinates {{{coords}}};\n"

Get the limits of the canvas for clipping.

Arguments:
  • canvas ("Canvas"): The canvas object.
Returns:

str: The limits code for clipping.

def get_back_code(canvas: "'Canvas'") -> str:
351def get_back_code(canvas: "Canvas") -> str:
352    """Get the background code for the canvas.
353
354    Args:
355        canvas ("Canvas"): The canvas object.
356
357    Returns:
358        str: The background code.
359    """
360    back_color = color2tikz(canvas.back_color)
361    return f"\\pagecolor{back_color}\n"

Get the background code for the canvas.

Arguments:
  • canvas ("Canvas"): The canvas object.
Returns:

str: The background code.

def get_tex_code(canvas: "'Canvas'") -> str:
364def get_tex_code(canvas: "Canvas") -> str:
365    """Convert the sketches in the Canvas to TikZ code.
366
367    Args:
368        canvas ("Canvas"): The canvas object.
369
370    Returns:
371        str: The TikZ code.
372    """
373
374    def get_sketch_code(sketch, canvas, ind):
375        """Get the TikZ code for a sketch.
376
377        Args:
378            sketch: The sketch object.
379            canvas: The canvas object.
380            ind: The index.
381
382        Returns:
383            tuple: The TikZ code and the updated index.
384        """
385        if sketch.subtype == Types.TAG_SKETCH:
386            code = draw_tag_sketch(sketch)
387        elif sketch.subtype == Types.BBOX_SKETCH:
388            code = draw_bbox_sketch(sketch)
389        elif sketch.subtype == Types.PATTERN_SKETCH:
390            code = draw_pattern_sketch(sketch)
391        elif sketch.subtype == Types.TEX_SKETCH:
392            if sketch.location == TexLoc.NONE:
393                code = sketch.code
394        else:
395            if sketch.draw_markers and sketch.marker_type == MarkerType.INDICES:
396                code = draw_shape_sketch(sketch, ind)
397                ind += 1
398            else:
399                code = draw_shape_sketch(sketch)
400
401        return code, ind
402
403    pages = canvas.pages
404
405    if pages:
406        for i, page in enumerate(pages):
407            sketches = page.sketches
408            back_color = f"\\pagecolor{color2tikz(page.back_color)}"
409            if i == 0:
410                code = [back_color]
411            else:
412                code.append(defaults["end_tikz"])
413                code.append("\\newpage")
414                code.append(defaults["begin_tikz"])
415            ind = 0
416            for sketch in sketches:
417                sketch_code, ind = get_sketch_code(sketch, canvas, ind)
418                code.append(sketch_code)
419
420        code = "\n".join(code)
421    else:
422        raise ValueError("No pages found in the canvas.")
423    return canvas.tex.tex_code(canvas, code)

Convert the sketches in the Canvas to TikZ code.

Arguments:
  • canvas ("Canvas"): The canvas object.
Returns:

str: The TikZ code.

class Grid(simetri.graphics.shape.Shape):
426class Grid(sg.Shape):
427    """Grid shape.
428
429    Args:
430        p1: (x_min, y_min)
431        p2: (x_max, y_max)
432        dx: x step
433        dy: y step
434    """
435
436    def __init__(self, p1, p2, dx, dy, **kwargs):
437        """
438        Args:
439            p1: (x_min, y_min)
440            p2: (x_max, y_max)
441            dx: x step
442            dy: y step
443        """
444        self.p1 = p1
445        self.p2 = p2
446        self.dx = dx
447        self.dy = dy
448        self.primary_points = sg.Points([p1, p2])
449        self.closed = False
450        self.fill = False
451        self.stroke = True
452        self._b_box = None
453        super().__init__([p1, p2], xform_matrix=None, subtype=sg.Types.GRID, **kwargs)

Grid shape.

Arguments:
  • p1: (x_min, y_min)
  • p2: (x_max, y_max)
  • dx: x step
  • dy: y step
Grid(p1, p2, dx, dy, **kwargs)
436    def __init__(self, p1, p2, dx, dy, **kwargs):
437        """
438        Args:
439            p1: (x_min, y_min)
440            p2: (x_max, y_max)
441            dx: x step
442            dy: y step
443        """
444        self.p1 = p1
445        self.p2 = p2
446        self.dx = dx
447        self.dy = dy
448        self.primary_points = sg.Points([p1, p2])
449        self.closed = False
450        self.fill = False
451        self.stroke = True
452        self._b_box = None
453        super().__init__([p1, p2], xform_matrix=None, subtype=sg.Types.GRID, **kwargs)
Arguments:
  • p1: (x_min, y_min)
  • p2: (x_max, y_max)
  • dx: x step
  • dy: y step
p1
p2
dx
dy
primary_points
closed
fill
stroke
def get_min_size(sketch: simetri.graphics.sketch.ShapeSketch) -> str:
456def get_min_size(sketch: ShapeSketch) -> str:
457    """Returns the minimum size of the tag node.
458
459    Args:
460        sketch (ShapeSketch): The shape sketch object.
461
462    Returns:
463        str: The minimum size of the tag node.
464    """
465    options = []
466    if sketch.frame_shape == "rectangle":
467        if sketch.frame_min_width is None:
468            width = defaults["min_width"]
469        else:
470            width = sketch.frame_min_width
471        if sketch.frame_min_height is None:
472            height = defaults["min_height"]
473        else:
474            height = sketch.frame_min_height
475        options.append(f"minimum width = {width}")
476        options.append(f"minimum height = {height}")
477    else:
478        if sketch.frame_min_size is None:
479            min_size = defaults["min_size"]
480        else:
481            min_size = sketch.frame_min_size
482        options.append(f"minimum size = {min_size}")
483
484    return options

Returns the minimum size of the tag node.

Arguments:
  • sketch (ShapeSketch): The shape sketch object.
Returns:

str: The minimum size of the tag node.

def frame_options(sketch: simetri.graphics.sketch.TagSketch) -> List[str]:
487def frame_options(sketch: TagSketch) -> List[str]:
488    """Returns the options for the frame of the tag node.
489
490    Args:
491        sketch (TagSketch): The tag sketch object.
492
493    Returns:
494        List[str]: The options for the frame of the tag node.
495    """
496    options = []
497    if sketch.draw_frame:
498        options.append(sketch.frame_shape)
499        line_options = get_line_style_options(sketch, frame=True)
500        if line_options:
501            options.extend(line_options)
502        fill_options = get_fill_style_options(sketch, frame=True)
503        if fill_options:
504            options.extend(fill_options)
505        if sketch.text in [None, ""]:
506            min_size = get_min_size(sketch)
507            if min_size:
508                options.extend(min_size)
509
510    return options

Returns the options for the frame of the tag node.

Arguments:
  • sketch (TagSketch): The tag sketch object.
Returns:

List[str]: The options for the frame of the tag node.

def color2tikz(color):
513def color2tikz(color):
514    """Converts a Color object to a TikZ color string.
515
516    Args:
517        color (Color): The color object.
518
519    Returns:
520        str: The TikZ color string.
521    """
522    # \usepackage{xcolor}
523    # \tikz\node[rounded corners, fill={rgb,255:red,21; green,66; blue,128},
524    #                                    text=white, draw=black] {hello world};
525    # \definecolor{mycolor}{rgb}{1,0.2,0.3}
526    # \definecolor{mycolor}{R_g_b}{255,51,76}
527    # \definecolor{mypink1}{rgb}{0.858, 0.188, 0.478}
528    # \definecolor{mypink2}{R_g_b}{219, 48, 122}
529    # \definecolor{mypink3}{cmyk}{0, 0.7808, 0.4429, 0.1412}
530    # \definecolor{mygray}{gray}{0.6}
531    if color is None:
532        r, g, b, _ = 255, 255, 255, 255
533        return f"{{rgb,255:red,{r}; green,{g}; blue,{b}}}"
534    r, g, b = color.rgb255
535
536    return f"{{rgb,255:red,{r}; green,{g}; blue,{b}}}"

Converts a Color object to a TikZ color string.

Arguments:
  • color (Color): The color object.
Returns:

str: The TikZ color string.

def get_scope_options(item: "Union['Canvas', 'Sketch']") -> str:
539def get_scope_options(item: Union["Canvas", "Sketch"]) -> str:
540    """Used for creating namespaces in TikZ.
541
542    Args:
543        item (Union["Canvas", "Sketch"]): The item to get scope options for.
544
545    Returns:
546        str: The scope options as a string.
547    """
548    options = []
549
550    if item.blend_group:
551        options.append(f"blend group={item.blend_mode}")
552    elif item.blend_mode:
553        options.append(f"blend mode={item.blend_mode}")
554    if item.fill_alpha not in [None, 1]:
555        options.append(f"fill opacity={item.fill_alpha}")
556    if item.line_alpha not in [None, 1]:
557        options.append(f"draw opacity={item.line_alpha}")
558    if item.text_alpha not in [None, 1]:
559        options.append(f"text opacity={item.alpha}")
560    if item.alpha not in [None, 1]:
561        options.append(f"opacity={item.alpha}")
562    if item.even_odd_rule:
563        options.append("even odd rule")
564    if item.transparency_group:
565        options.append("transparency group")
566
567    return ",".join(options)

Used for creating namespaces in TikZ.

Arguments:
  • item (Union["Canvas", "Sketch"]): The item to get scope options for.
Returns:

str: The scope options as a string.

def get_clip_code(item: "Union['Sketch', 'Canvas']") -> str:
570def get_clip_code(item: Union["Sketch", "Canvas"]) -> str:
571    """Returns the clip code for a sketch or Canvas.
572
573    Args:
574        item (Union["Sketch", "Canvas"]): The item to get clip code for.
575
576    Returns:
577        str: The clip code as a string.
578    """
579    if item.mask.subtype == Types.CIRCLE:
580        x, y = item.mask.center[:2]
581        res = f"\\clip({x}, {y}) circle ({item.mask.radius});\n"
582    elif item.mask.subtype == Types.RECTANGLE:
583        x, y = item.mask.center[:2]
584        width, height = item.mask.width, item.mask.height
585        res = f"\\clip({x}, {y}) rectangle ({width}, {height});\n"
586    elif item.mask.subtype == Types.SHAPE:
587        # vertices = item.mask.primary_points.homogen_coords
588        # coords = " ".join([f"({v[0]}, {v[1]})" for v in vertices])
589        # res = f"\\clip plot[] coordinates {{{coords}}};\n"
590        x, y = item.mask.origin[:2]
591        width, height = item.mask.width, item.mask.height
592        res = f"\\clip({x}, {y}) rectangle ({width}, {height});\n"
593    else:
594        res = ""
595
596    return res

Returns the clip code for a sketch or Canvas.

Arguments:
  • item (Union["Sketch", "Canvas"]): The item to get clip code for.
Returns:

str: The clip code as a string.

def get_canvas_scope(canvas):
599def get_canvas_scope(canvas):
600    """Returns the TikZ code for the canvas scope.
601
602    Args:
603        canvas: The canvas object.
604
605    Returns:
606        str: The TikZ code for the canvas scope.
607    """
608    options = get_scope_options(canvas)
609    res = f"\\begin{{scope}}[{options}]\n"
610    if (canvas.clip and canvas.mask) or canvas.size is not None:
611        res += get_clip_code(canvas)
612
613    return res

Returns the TikZ code for the canvas scope.

Arguments:
  • canvas: The canvas object.
Returns:

str: The TikZ code for the canvas scope.

def draw_batch_sketch(sketch, canvas):
616def draw_batch_sketch(sketch, canvas):
617    """Converts a BatchSketch to TikZ code.
618
619    Args:
620        sketch: The BatchSketch object.
621        canvas: The canvas object.
622
623    Returns:
624        str: The TikZ code for the BatchSketch.
625    """
626    options = get_scope_options(sketch)
627    if options:
628        res = f"\\begin{{scope}}[{options}]\n"
629    else:
630        res = ""
631    if sketch.clip and sketch.mask:
632        res += get_clip_code(sketch)
633    for item in sketch.items:
634        if item.subtype in d_sketch_draw:
635            res += d_sketch_draw[item.subtype](item, canvas)
636        else:
637            raise ValueError(f"Sketch type {item.subtype} not supported.")
638
639    if sketch.clip and sketch.mask:
640        res += get_clip_code(sketch)
641    if options:
642        res += "\\end{scope}\n"
643
644    return res

Converts a BatchSketch to TikZ code.

Arguments:
  • sketch: The BatchSketch object.
  • canvas: The canvas object.
Returns:

str: The TikZ code for the BatchSketch.

def draw_bbox_sketch(sketch):
646def draw_bbox_sketch(sketch):
647    """Converts a BBoxSketch to TikZ code.
648
649    Args:
650        sketch: The BBoxSketch object.
651        canvas: The canvas object.
652
653    Returns:
654        str: The TikZ code for the BBoxSketch.
655    """
656    attrib_map = {
657        "line_color": "color",
658        "line_width": "line width",
659        "line_dash_array": "dash pattern",
660    }
661    attrib_list = ["line_color", "line_width", "line_dash_array"]
662    options = sg_to_tikz(sketch, attrib_list, attrib_map)
663    options = ", ".join(options)
664    res = f"\\draw[{options}]"
665    x1, y1 = sketch.vertices[1]
666    x2, y2 = sketch.vertices[3]
667    res += f"({x1}, {y1}) rectangle ({x2}, {y2});\n"
668
669    return res

Converts a BBoxSketch to TikZ code.

Arguments:
  • sketch: The BBoxSketch object.
  • canvas: The canvas object.
Returns:

str: The TikZ code for the BBoxSketch.

def draw_lace_sketch(item):
671def draw_lace_sketch(item):
672    """Converts a LaceSketch to TikZ code.
673
674    Args:
675        item: The LaceSketch object.
676
677    Returns:
678        str: The TikZ code for the LaceSketch.
679    """
680    if item.draw_fragments:
681        for fragment in item.fragments:
682            draw_shape_sketch(fragment)
683    if item.draw_plaits:
684        for plait in item.plaits:
685            plait.fill = True
686            draw_shape_sketch(plait)

Converts a LaceSketch to TikZ code.

Arguments:
  • item: The LaceSketch object.
Returns:

str: The TikZ code for the LaceSketch.

def get_draw(sketch):
689def get_draw(sketch):
690    """Returns the draw command for sketches.
691
692    Args:
693        sketch: The sketch object.
694
695    Returns:
696        str: The draw command as a string.
697    """
698    # sketch.closed, sketch.fill, sketch.stroke, shading
699    decision_table = {
700        (True, True, True, True): "\\shadedraw",
701        (True, True, True, False): "\\filldraw",
702        (True, True, False, True): "\\shade",
703        (True, True, False, False): "\\fill",
704        (True, False, True, True): "\\draw",
705        (True, False, True, False): "\\draw",
706        (True, False, False, True): False,
707        (True, False, False, False): False,
708        (False, True, True, True): "\\draw",
709        (False, True, True, False): "\\draw",
710        (False, True, False, True): False,
711        (False, True, False, False): False,
712        (False, False, True, True): "\\draw",
713        (False, False, True, False): "\\draw",
714        (False, False, False, True): False,
715        (False, False, False, False): False,
716    }
717    if sketch.markers_only:
718        res = "\\draw"
719    else:
720        if hasattr(sketch, 'back_style'):
721            shading = sketch.back_style == BackStyle.SHADING
722        else:
723            shading = False
724        if not hasattr(sketch, 'closed'):
725            closed = False
726        else:
727            closed = sketch.closed
728        if not hasattr(sketch, 'fill'):
729            fill = False
730        else:
731            fill = sketch.fill
732        if not hasattr(sketch, 'stroke'):
733            stroke = False
734        else:
735            stroke = sketch.stroke
736
737        res = decision_table[(closed, fill, stroke, shading)]
738
739    return res

Returns the draw command for sketches.

Arguments:
  • sketch: The sketch object.
Returns:

str: The draw command as a string.

def get_frame_options(sketch):
742def get_frame_options(sketch):
743    """Returns the options for the frame of a TagSketch.
744
745    Args:
746        sketch: The TagSketch object.
747
748    Returns:
749        list: The options for the frame of the TagSketch.
750    """
751    options = get_line_style_options(sketch)
752    options += get_fill_style_options(sketch)
753    if sketch.text in [None, ""]:
754        if sketch.frame.frame_shape == "rectangle":
755            width = sketch.frame.min_width
756            height = sketch.frame.min_height
757            if not width:
758                width = defaults["min_width"]
759            if not height:
760                height = defaults["min_height"]
761            options += "minimum width = {width}, minimum height = {height}"
762        else:
763            size = sketch.frame.min_size
764            if not size:
765                size = defaults["min_size"]
766            options += f"minimum size = {size}"
767    return options

Returns the options for the frame of a TagSketch.

Arguments:
  • sketch: The TagSketch object.
Returns:

list: The options for the frame of the TagSketch.

def draw_tag_sketch(sketch):
770def draw_tag_sketch(sketch):
771    """Converts a TagSketch to TikZ code.
772
773    Args:
774        sketch: The TagSketch object.
775        canvas: The canvas object.
776
777    Returns:
778        str: The TikZ code for the TagSketch.
779    """
780    # \node at (0,0) {some text};
781    def get_font_family(sketch):
782        default_fonts = [defaults['main_font'], defaults['sans_font'], defaults['mono_font']]
783
784        if sketch.font_family in default_fonts:
785            if sketch.font_family == defaults['main_font']:
786                res = 'tex_family', ''
787            elif sketch.font_family == defaults['sans_font']:
788                res = 'tex_family', 'textsf'
789            else: # defaults['mono_font']
790                res = 'tex_family', 'texttt'
791        elif sketch.font_family:
792            if isinstance(sketch.font_family, FontFamily):
793                if sketch.font_family == FontFamily.SANSSERIF:
794                    res = 'tex_family', 'textsf'
795                elif sketch.font_family == FontFamily.MONOSPACE:
796                    res = 'tex_family', 'texttt'
797                else:
798                    res = 'tex_family', 'textrm'
799
800            elif isinstance(sketch.font_family, str):
801                res = 'new_family', sketch.font_family.replace(" ", "")
802
803            else:
804                raise ValueError(f"Font family {sketch.font_family} not supported.")
805        else:
806            res = 'no_family', None
807
808        return res
809
810    def get_font_size(sketch):
811
812        if sketch.font_size:
813            if isinstance(sketch.font_size, FontSize):
814                res = 'tex_size', sketch.font_size.value
815            else:
816                res = 'num_size', sketch.font_size
817        else:
818            res = 'no_size', None
819
820        return res
821
822    res = []
823    x, y = sketch.pos[:2]
824
825    options = ""
826    if sketch.draw_frame:
827        options += "draw"
828        if sketch.stroke:
829            if sketch.frame_shape != FrameShape.RECTANGLE:
830                options += f", {sketch.frame_shape}, "
831            line_style_options = get_line_style_options(sketch)
832            if line_style_options:
833                options += ', ' + ', '.join(line_style_options)
834            if sketch.frame_inner_sep:
835                options += f", inner sep={sketch.frame_inner_sep}"
836            if sketch.minimum_width:
837                options += f", minimum width={sketch.minimum_width}"
838            if sketch.smooth and sketch.frame_shape not in [
839                FrameShape.CIRCLE,
840                FrameShape.ELLIPSE,
841            ]:
842                options += ", smooth"
843
844    if sketch.fill and sketch.back_color:
845        options += f", fill={color2tikz(sketch.frame_back_color)}"
846    if sketch.anchor:
847        options += f", anchor={sketch.anchor.value}"
848    if sketch.back_style == BackStyle.SHADING and sketch.fill:
849        shading_options = get_shading_options(sketch)[0]
850        options += ", " + shading_options
851    if sketch.back_style == BackStyle.PATTERN and sketch.fill:
852        pattern_options = get_pattern_options(sketch)[0]
853        options += ", " + pattern_options
854    if sketch.align != defaults["tag_align"]:
855        options += f", align={sketch.align.value}"
856    if sketch.text_width:
857        options += f", text width={sketch.text_width}"
858
859
860# no_family, tex_family, new_family
861# no_size, tex_size, num_size
862
863# num_size and new_family {\fontsize{20}{24} \selectfont \Verdana ABCDEFG Hello, World! 25}
864# tex_size and new_family {\large{\selectfont \Verdana ABCDEFG Hello, World! 50}}
865# no_size and new_family {\selectfont \Verdana ABCDEFG Hello, World! 50}
866
867# tex_family {\textsc{\textit{\textbf{\Huge{\texttt{ABCDG Just a test -50}}}}}};
868
869# no_family {\textsc{\textit{\textbf{\Huge{ABCDG Just a test -50}}}}};
870
871    if sketch.font_color is not None and sketch.font_color != defaults["font_color"]:
872        options += f", text={color2tikz(sketch.font_color)}"
873    family, font_family = get_font_family(sketch)
874    size, font_size = get_font_size(sketch)
875    tex_text = ''
876    if sketch.small_caps:
877        tex_text += '\\textsc{'
878
879    if sketch.italic:
880        tex_text += '\\textit{'
881
882    if sketch.bold:
883        tex_text += '\\textbf{'
884
885    if size == 'num_size':
886        f_size = font_size
887        f_size2 = ceil(font_size * 1.2)
888        tex_text += f"\\fontsize{{{f_size}}}{{{f_size2}}}\\selectfont "
889
890    elif size == 'tex_size':
891        tex_text += f"\\{font_size}{{\\selectfont "
892
893    else:
894        tex_text += "\\selectfont "
895
896    if family == 'new_family':
897        tex_text += f"\\{font_family} {sketch.text}}}"
898
899    elif family == 'tex_family':
900        if font_family:
901            tex_text += f"\\{font_family}{{ {sketch.text}}}}}"
902        else:
903            tex_text += f"{{ {sketch.text}}}"
904    else: # no_family
905        tex_text += f"{{ {sketch.text}}}"
906
907    tex_text = '{' + tex_text
908
909    open_braces = tex_text.count('{')
910    close_braces = tex_text.count('}')
911    tex_text = tex_text + '}' * (open_braces - close_braces)
912
913    res.append(f"\\node[{options}] at ({x}, {y}) {tex_text};\n")
914
915    return ''.join(res)

Converts a TagSketch to TikZ code.

Arguments:
  • sketch: The TagSketch object.
  • canvas: The canvas object.
Returns:

str: The TikZ code for the TagSketch.

def get_dash_pattern(line_dash_array):
918def get_dash_pattern(line_dash_array):
919    """Returns the dash pattern for a line.
920
921    Args:
922        line_dash_array: The dash array for the line.
923
924    Returns:
925        str: The dash pattern as a string.
926    """
927    dash_pattern = []
928    for i, dash in enumerate(line_dash_array):
929        if i % 2 == 0:
930            dash_pattern.extend(["on", f"{dash}pt"])
931        else:
932            dash_pattern.extend(["off", f"{dash}pt"])
933
934    return " ".join(dash_pattern)

Returns the dash pattern for a line.

Arguments:
  • line_dash_array: The dash array for the line.
Returns:

str: The dash pattern as a string.

def sg_to_tikz(sketch, attrib_list, attrib_map, conditions=None, exceptions=None):
937def sg_to_tikz(sketch, attrib_list, attrib_map, conditions=None, exceptions=None):
938    """Converts the attributes of a sketch to TikZ options.
939
940    Args:
941        sketch: The sketch object.
942        attrib_list: The list of attributes to convert.
943        attrib_map: The map of attributes to TikZ options.
944        conditions: Optional conditions for the attributes.
945        exceptions: Optional exceptions for the attributes.
946
947    Returns:
948        list: The TikZ options as a list.
949    """
950    skip = ["marker_color", "fill_color"]
951    tikz_way = {'line_width':LineWidth, 'line_dash_array':LineDashArray}
952    if exceptions:
953        skip += exceptions
954    d_converters = {
955        "line_color": color2tikz,
956        "fill_color": color2tikz,
957        "draw": color2tikz,
958        "line_dash_array": get_dash_pattern,
959    }
960    options = []
961    for attrib in attrib_list:
962        if attrib not in attrib_map:
963            continue
964        if conditions and attrib in conditions and not conditions[attrib]:
965            continue
966        if attrib in tikz_way:
967            value = getattr(sketch, attrib)
968            if isinstance(value, tikz_way[attrib]):
969                option = value.value
970                options.append(option)
971                continue
972            if isinstance(value, str):
973                if value in tikz_way[attrib]:
974                    options.append(value)
975                continue
976
977        tikz_attrib = attrib_map[attrib]
978        if hasattr(sketch, attrib):
979            value = getattr(sketch, attrib)
980            if value is not None and tikz_attrib in list(attrib_map.values()):
981                if attrib in ['smooth']: # smooth is a boolean
982                    if value:
983                        options.append("smooth")
984
985                elif attrib in skip:
986                    value = color2tikz(getattr(sketch, attrib))
987                    options.append(f"{tikz_attrib}={value}")
988
989                elif value != tikz_defaults[tikz_attrib]:
990                    if attrib in d_converters:
991                        value = d_converters[attrib](value)
992                    options.append(f"{tikz_attrib}={value}")
993
994    return options

Converts the attributes of a sketch to TikZ options.

Arguments:
  • 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.

def get_line_style_options(sketch, exceptions=None):
 997def get_line_style_options(sketch, exceptions=None):
 998    """Returns the options for the line style.
 999
1000    Args:
1001        sketch: The sketch object.
1002        exceptions: Optional exceptions for the line style options.
1003
1004    Returns:
1005        list: The line style options as a list.
1006    """
1007    attrib_map = {
1008        "line_color": "color",
1009        "line_width": "line width",
1010        "line_dash_array": "dash pattern",
1011        "line_cap": "line cap",
1012        "line_join": "line join",
1013        "line_miter_limit": "miter limit",
1014        "line_dash_phase": "dash phase",
1015        "line_alpha": "draw opacity",
1016        "smooth": "smooth",
1017        "fillet_radius": "rounded corners",
1018    }
1019    attribs = list(line_style_map.keys())
1020    if sketch.stroke:
1021        if exceptions and "draw_fillets" not in exceptions:
1022            conditions = {"fillet_radius": sketch.draw_fillets}
1023        else:
1024            conditions = None
1025        if sketch.line_alpha not in [None, 1]:
1026            sketch.line_alpha = sketch.line_alpha
1027        elif sketch.alpha not in [None, 1]:
1028            sketch.fill_alpha = sketch.alpha
1029        else:
1030            attribs.remove("line_alpha")
1031        if not sketch.smooth:
1032            attribs.remove("smooth")
1033        res = sg_to_tikz(sketch, attribs, attrib_map, conditions, exceptions)
1034    else:
1035        res = []
1036
1037    return res

Returns the options for the line style.

Arguments:
  • sketch: The sketch object.
  • exceptions: Optional exceptions for the line style options.
Returns:

list: The line style options as a list.

def get_fill_style_options(sketch, exceptions=None, frame=False):
1040def get_fill_style_options(sketch, exceptions=None, frame=False):
1041    """Returns the options for the fill style.
1042
1043    Args:
1044        sketch: The sketch object.
1045        exceptions: Optional exceptions for the fill style options.
1046        frame: Optional flag for frame fill style.
1047
1048    Returns:
1049        list: The fill style options as a list.
1050    """
1051    attrib_map = {
1052        "fill_color": "fill",
1053        "fill_alpha": "fill opacity",
1054        #'fill_mode': 'even odd rule',
1055        "blend_mode": "blend mode",
1056        "frame_back_color": "fill",
1057    }
1058    attribs = list(shape_style_map.keys())
1059    if sketch.fill_alpha not in [None, 1]:
1060        sketch.fill_alpha = sketch.fill_alpha
1061    elif sketch.alpha not in [None, 1]:
1062        sketch.fill_alpha = sketch.alpha
1063    else:
1064        attribs.remove("fill_alpha")
1065    if sketch.fill and not sketch.back_style == BackStyle.PATTERN:
1066        res = sg_to_tikz(sketch, attribs, attrib_map, exceptions=exceptions)
1067        if frame:
1068            res = [f"fill = {color2tikz(getattr(sketch, 'frame_back_color'))}"] + res
1069    else:
1070        res = []
1071
1072    return res

Returns the options for the fill style.

Arguments:
  • 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.

def get_axis_shading_colors(sketch):
1075def get_axis_shading_colors(sketch):
1076    """Returns the shading colors for the axis.
1077
1078    Args:
1079        sketch: The sketch object.
1080
1081    Returns:
1082        str: The shading colors for the axis.
1083    """
1084    def get_color(color, color_key):
1085        if isinstance(color, Color):
1086            res = color2tikz(color)
1087        else:
1088            res = defaults[color_key]
1089
1090        return res
1091
1092    left = get_color(sketch.shade_left_color, "shade_left_color")
1093    right = get_color(sketch.shade_right_color, "shade_right_color")
1094    top = get_color(sketch.shade_top_color, "shade_top_color")
1095    bottom = get_color(sketch.shade_bottom_color, "shade_bottom_color")
1096    middle = get_color(sketch.shade_middle_color, "shade_middle_color")
1097
1098    axis_colors = {
1099        ShadeType.AXIS_BOTTOM_MIDDLE: f"bottom color={bottom}, middle color={middle}",
1100        ShadeType.AXIS_LEFT_MIDDLE: f"left color={left}, middle color={middle}",
1101        ShadeType.AXIS_RIGHT_MIDDLE: f"right color={right}, middle color={middle}",
1102        ShadeType.AXIS_TOP_MIDDLE: f"top color={top}, middle color={middle}",
1103        ShadeType.AXIS_LEFT_RIGHT: f"left color={left}, right color={right}",
1104        ShadeType.AXIS_TOP_BOTTOM: f"top color={top}, bottom color={bottom}",
1105    }
1106
1107    res = axis_colors[sketch.shade_type]
1108    return res

Returns the shading colors for the axis.

Arguments:
  • sketch: The sketch object.
Returns:

str: The shading colors for the axis.

def get_bilinear_shading_colors(sketch):
1111def get_bilinear_shading_colors(sketch):
1112    """Returns the shading colors for the bilinear shading.
1113
1114    Args:
1115        sketch: The sketch object.
1116
1117    Returns:
1118        str: The shading colors for the bilinear shading.
1119    """
1120    res = []
1121    if sketch.shade_upper_left_color:
1122        res.append(f"upper left = {color2tikz(sketch.shade_upper_left_color)}")
1123    if sketch.shade_upper_right_color:
1124        res.append(f"upper right = {color2tikz(sketch.shade_upper_right_color)}")
1125    if sketch.shade_lower_left_color:
1126        res.append(f"lower left = {color2tikz(sketch.shade_lower_left_color)}")
1127    if sketch.shade_lower_right_color:
1128        res.append(f"lower right = {color2tikz(sketch.shade_lower_right_color)}")
1129
1130    return ", ".join(res)

Returns the shading colors for the bilinear shading.

Arguments:
  • sketch: The sketch object.
Returns:

str: The shading colors for the bilinear shading.

def get_radial_shading_colors(sketch):
1133def get_radial_shading_colors(sketch):
1134    """Returns the shading colors for the radial shading.
1135
1136    Args:
1137        sketch: The sketch object.
1138
1139    Returns:
1140        str: The shading colors for the radial shading.
1141    """
1142    res = []
1143    if sketch.shade_type == ShadeType.RADIAL_INNER:
1144        res.append(f"inner color = {color2tikz(sketch.shade_inner_color)}")
1145    elif sketch.shade_type == ShadeType.RADIAL_OUTER:
1146        res.append(f"outer color = {color2tikz(sketch.shade_outer_color)}")
1147    elif sketch.shade_type == ShadeType.RADIAL_INNER_OUTER:
1148        res.append(f"inner color = {color2tikz(sketch.shade_inner_color)}")
1149        res.append(f"outer color = {color2tikz(sketch.shade_outer_color)}")
1150
1151    return ", ".join(res)

Returns the shading colors for the radial shading.

Arguments:
  • sketch: The sketch object.
Returns:

str: The shading colors for the radial shading.

axis_shading_types = [<ShadeType.AXIS_BOTTOM_MIDDLE: 'axis bottom middle'>, <ShadeType.AXIS_LEFT_MIDDLE: 'axis left middle'>, <ShadeType.AXIS_RIGHT_MIDDLE: 'axis right middle'>, <ShadeType.AXIS_TOP_MIDDLE: 'axis top middle'>, <ShadeType.AXIS_LEFT_RIGHT: 'axis left right'>, <ShadeType.AXIS_TOP_BOTTOM: 'axis top bottom'>]
radial_shading_types = [<ShadeType.RADIAL_INNER: 'radial inner'>, <ShadeType.RADIAL_OUTER: 'radial outer'>, <ShadeType.RADIAL_INNER_OUTER: 'radial inner outer'>]
def get_shading_options(sketch):
1170def get_shading_options(sketch):
1171    """Returns the options for the shading.
1172
1173    Args:
1174        sketch: The sketch object.
1175
1176    Returns:
1177        list: The shading options as a list.
1178    """
1179    shade_type = sketch.shade_type
1180    if shade_type in axis_shading_types:
1181        res = get_axis_shading_colors(sketch)
1182        if sketch.shade_axis_angle:
1183            res += f", shading angle={sketch.shade_axis_angle}"
1184    elif shade_type == ShadeType.BILINEAR:
1185        res = get_bilinear_shading_colors(sketch)
1186    elif shade_type in radial_shading_types:
1187        res = get_radial_shading_colors(sketch)
1188    elif shade_type == ShadeType.BALL:
1189        res = f"ball color = {color2tikz(sketch.shade_ball_color)}"
1190    elif shade_type == ShadeType.COLORWHEEL:
1191        res = "shading=color wheel"
1192    elif shade_type == ShadeType.COLORWHEEL_BLACK:
1193        res = "shading=color wheel black center"
1194    elif shade_type == ShadeType.COLORWHEEL_WHITE:
1195        res = "shading=color wheel white center"
1196
1197    return [res]

Returns the options for the shading.

Arguments:
  • sketch: The sketch object.
Returns:

list: The shading options as a list.

def get_pattern_options(sketch):
1200def get_pattern_options(sketch):
1201    """Returns the options for the patterns.
1202
1203    Args:
1204        sketch: The sketch object.
1205
1206    Returns:
1207        list: The pattern options as a list.
1208    """
1209    pattern_type = sketch.pattern_type
1210    if pattern_type:
1211        distance = sketch.pattern_distance
1212        options = f"pattern={{{pattern_type}[distance={distance}, "
1213        angle = degrees(sketch.pattern_angle)
1214        if angle:
1215            options += f"angle={angle}, "
1216        line_width = sketch.pattern_line_width
1217        if line_width:
1218            options += f"line width={line_width}, "
1219        x_shift = sketch.pattern_x_shift
1220        if x_shift:
1221            options += f"xshift={x_shift}, "
1222        y_shift = sketch.pattern_y_shift
1223        if y_shift:
1224            options += f"yshift={y_shift}, "
1225        if pattern_type in ["Stars", "Dots"]:
1226            radius = sketch.pattern_radius
1227            if radius:
1228                options += f"radius={radius}, "
1229            if pattern_type == "Stars":
1230                points = sketch.pattern_points
1231                if points:
1232                    options += f"points={points}, "
1233        options = options.strip()
1234        if options.endswith(","):
1235            options = options[:-1]
1236        options += "]"
1237        color = sketch.pattern_color
1238        if color and color != sg.black:
1239            options += f", pattern color={color2tikz(color)}, "
1240
1241        options += "}"
1242        res = [options]
1243    else:
1244        res = []
1245
1246    return res

Returns the options for the patterns.

Arguments:
  • sketch: The sketch object.
Returns:

list: The pattern options as a list.

def get_marker_options(sketch):
1249def get_marker_options(sketch):
1250    """Returns the options for the markers.
1251
1252    Args:
1253        sketch: The sketch object.
1254
1255    Returns:
1256        list: The marker options as a list.
1257    """
1258    attrib_map = {
1259        # 'marker': 'mark',
1260        "marker_size": "mark size",
1261        "marker_angle": "rotate",
1262        # 'fill_color': 'color',
1263        "marker_color": "color",
1264        "marker_fill": "fill",
1265        "marker_opacity": "opacity",
1266        "marker_repeat": "mark repeat",
1267        "marker_phase": "mark phase",
1268        "marker_tension": "tension",
1269        "marker_line_width": "line width",
1270        "marker_line_style": "style",
1271        # 'line_color': 'line color',
1272    }
1273    # if mark_stroke is false make line color same as fill color
1274    if sketch.draw_markers:
1275        res = sg_to_tikz(sketch, marker_style_map.keys(), attrib_map)
1276    else:
1277        res = []
1278
1279    return res

Returns the options for the markers.

Arguments:
  • sketch: The sketch object.
Returns:

list: The marker options as a list.

def draw_shape_sketch_with_indices(sketch, ind):
1282def draw_shape_sketch_with_indices(sketch, ind):
1283    """Draws a shape sketch with circle markers with index numbers in them.
1284
1285    Args:
1286        sketch: The shape sketch object.
1287        ind: The index.
1288
1289    Returns:
1290        str: The TikZ code for the shape sketch with indices.
1291    """
1292    begin_scope = get_begin_scope(ind)
1293    body = get_draw(sketch)
1294    if body:
1295        options = get_line_style_options(sketch)
1296        if sketch.fill and sketch.closed:
1297            options += get_fill_style_options(sketch)
1298        if sketch.smooth:
1299            if sketch.closed:
1300                options += ["smooth cycle"]
1301            else:
1302                options += ["smooth"]
1303        options = ", ".join(options)
1304        body += f"[{options}]"
1305    else:
1306        body = ""
1307    vertices = sketch.vertices
1308    vertices = [str(x) for x in vertices]
1309    str_lines = [vertices[0] + "node{0}"]
1310    n = len(vertices)
1311    for i, vertice in enumerate(vertices[1:]):
1312        if (i + 1) % 6 == 0:
1313            if i == n - 1:
1314                str_lines.append(f" -- {vertice} node{{{i+1}}}\n")
1315            else:
1316                str_lines.append(f"\n\t-- {vertice} node{{{i+1}}}")
1317        else:
1318            str_lines.append(f"-- {vertice} node{{{i+1}}}")
1319    if body:
1320        if sketch.closed:
1321            str_lines.append(" -- cycle;\n")
1322        str_lines.append(";\n")
1323    end_scope = get_end_scope()
1324
1325    return begin_scope + body + "".join(str_lines) + end_scope

Draws a shape sketch with circle markers with index numbers in them.

Arguments:
  • sketch: The shape sketch object.
  • ind: The index.
Returns:

str: The TikZ code for the shape sketch with indices.

def draw_shape_sketch_with_markers(sketch):
1328def draw_shape_sketch_with_markers(sketch):
1329    """Draws a shape sketch with markers.
1330
1331    Args:
1332        sketch: The shape sketch object.
1333
1334    Returns:
1335        str: The TikZ code for the shape sketch with markers.
1336    """
1337    # begin_scope = get_begin_scope()
1338    body = get_draw(sketch)
1339    if body:
1340        options = get_line_style_options(sketch)
1341        if sketch.fill and sketch.closed:
1342            options += get_fill_style_options(sketch)
1343        if sketch.smooth and sketch.closed:
1344            options += ["smooth cycle"]
1345        elif sketch.smooth:
1346            options += ["smooth"]
1347        options = ", ".join(options)
1348        if options:
1349            body += f"[{options}]"
1350    else:
1351        body = ""
1352
1353    if sketch.draw_markers:
1354        marker_options = ", ".join(get_marker_options(sketch))
1355    else:
1356        marker_options = ""
1357
1358    vertices = [str(x) for x in sketch.vertices]
1359
1360    str_lines = [vertices[0]]
1361    for i, vertice in enumerate(vertices[1:]):
1362        if (i + 1) % 6 == 0:
1363            str_lines.append(f"\n\t{vertice} ")
1364        else:
1365            str_lines.append(f" {vertice} ")
1366    coordinates = "".join(str_lines)
1367
1368    marker = get_enum_value(MarkerType, sketch.marker_type)
1369    # marker = sketch.marker_type.value
1370    if sketch.markers_only:
1371        markers_only = "only marks ,"
1372    else:
1373        markers_only = ""
1374    if sketch.draw_markers and marker_options:
1375        body += (
1376            f" plot[mark = {marker}, {markers_only}mark options = {{{marker_options}}}] "
1377            f"\ncoordinates {{{coordinates}}};\n"
1378        )
1379    elif sketch.draw_markers:
1380        body += (
1381            f" plot[mark = {marker}, {markers_only}] coordinates {{{coordinates}}};\n"
1382        )
1383    else:
1384        body += f" plot[tension=.5] coordinates {{{coordinates}}};\n"
1385
1386    return body

Draws a shape sketch with markers.

Arguments:
  • sketch: The shape sketch object.
Returns:

str: The TikZ code for the shape sketch with markers.

def get_begin_scope(ind=None):
1389def get_begin_scope(ind=None):
1390    """Returns \begin{scope}[every node/.append style=nodestyle{ind}].
1391
1392    Args:
1393        ind: Optional index for the scope.
1394
1395    Returns:
1396        str: The begin scope string.
1397    """
1398    if ind is None:
1399        res = "\\begin{scope}[]\n"
1400    else:
1401        res = f"\\begin{{scope}}[every node/.append style=nodestyle{ind}]\n"
1402
1403    return res

Returns egin{scope}[every node/.append style=nodestyle{ind}].

Arguments:
  • ind: Optional index for the scope.
Returns:

str: The begin scope string.

def get_end_scope():
1406def get_end_scope():
1407    """Returns \\end{scope}.
1408
1409    Returns:
1410        str: The end scope string.
1411    """
1412    return "\\end{scope}\n"

Returns \end{scope}.

Returns:

str: The end scope string.

def draw_pattern_sketch(sketch):
1414def draw_pattern_sketch(sketch):
1415    """Draws a pattern sketch.
1416
1417    Args:
1418        sketch: The pattern sketch object.
1419
1420    Returns:
1421        str: The TikZ code for the pattern sketch.
1422    """
1423    begin_scope = "\\begin{scope}"
1424
1425    options = []
1426
1427    if sketch.back_style == BackStyle.PATTERN and sketch.fill and sketch.closed:
1428        options += get_pattern_options(sketch)
1429    if sketch.stroke:
1430        options += get_line_style_options(sketch)
1431    if sketch.closed and sketch.fill:
1432        options += get_fill_style_options(sketch)
1433    if sketch.smooth:
1434        options += ["smooth"]
1435    if sketch.back_style == BackStyle.SHADING and sketch.fill and sketch.closed:
1436        options += get_shading_options(sketch)
1437    options = ", ".join(options)
1438    if options:
1439        begin_scope += f"[{options}]\n"
1440    end_scope = get_end_scope()
1441
1442    pattern = sketch.pattern
1443    draw = get_draw(sketch)
1444    if not draw:
1445        return ""
1446    all_vertices = sketch.kernel_vertices @ sketch.all_matrices
1447    vertices_list = np.hsplit(all_vertices, sketch.count)
1448    shapes = []
1449    for vertices in vertices_list:
1450        vertices @= sketch.xform_matrix
1451        vertices = [tuple(vert) for vert in vertices[:,:2].tolist()]
1452        n = len(vertices)
1453        str_lines = [f"{vertices[0]}"]
1454        for i, vertice in enumerate(vertices[1:]):
1455            if (i + 1) % 8 == 0:
1456                if i == n - 1:
1457                    str_lines.append(f"-- {vertice} \n")
1458                else:
1459                    str_lines.append(f"\n\t-- {vertice} ")
1460            else:
1461                str_lines.append(f"-- {vertice} ")
1462        if sketch.closed:
1463            str_lines.append("-- cycle;\n")
1464        else:
1465            str_lines.append(";\n")
1466        shapes.append(draw + "".join(str_lines))
1467
1468
1469    return begin_scope + f"[{options}]\n" +  "\n".join(shapes) + end_scope

Draws a pattern sketch.

Arguments:
  • sketch: The pattern sketch object.
Returns:

str: The TikZ code for the pattern sketch.

def draw_sketch(sketch):
1472def draw_sketch(sketch):
1473    """Draws a plain shape sketch.
1474
1475    Args:
1476        sketch: The shape sketch object.
1477
1478    Returns:
1479        str: The TikZ code for the plain shape sketch.
1480    """
1481    res = get_draw(sketch)
1482    if not res:
1483        return ""
1484    options = []
1485
1486    if sketch.back_style == BackStyle.PATTERN and sketch.fill and sketch.closed:
1487        options += get_pattern_options(sketch)
1488    if sketch.stroke:
1489        options += get_line_style_options(sketch)
1490    if sketch.closed and sketch.fill:
1491        options += get_fill_style_options(sketch)
1492    if sketch.smooth:
1493        options += ["smooth"]
1494    if sketch.back_style == BackStyle.SHADING and sketch.fill and sketch.closed:
1495        options += get_shading_options(sketch)
1496    options = ", ".join(options)
1497    if options:
1498        res += f"[{options}]"
1499    vertices = sketch.vertices
1500    n = len(vertices)
1501    str_lines = [f"{vertices[0]}"]
1502    for i, vertice in enumerate(vertices[1:]):
1503        if (i + 1) % 8 == 0:
1504            if i == n - 1:
1505                str_lines.append(f"-- {vertice} \n")
1506            else:
1507                str_lines.append(f"\n\t-- {vertice} ")
1508        else:
1509            str_lines.append(f"-- {vertice} ")
1510    if sketch.closed:
1511        str_lines.append("-- cycle;\n")
1512    else:
1513        str_lines.append(";\n")
1514    if res:
1515        res += "".join(str_lines)
1516    else:
1517        res = "".join(str_lines)
1518    return res

Draws a plain shape sketch.

Arguments:
  • sketch: The shape sketch object.
Returns:

str: The TikZ code for the plain shape sketch.

def draw_tex_sketch(sketch):
1521def draw_tex_sketch(sketch):
1522    """Draws a TeX sketch.
1523
1524    Args:
1525        sketch: The TeX sketch object.
1526
1527    Returns:
1528        str: The TeX code for the TeX sketch.
1529    """
1530
1531    return sketch.code

Draws a TeX sketch.

Arguments:
  • sketch: The TeX sketch object.
Returns:

str: The TeX code for the TeX sketch.

def draw_shape_sketch(sketch, ind=None):
1533def draw_shape_sketch(sketch, ind=None):
1534    """Draws a shape sketch.
1535
1536    Args:
1537        sketch: The shape sketch object.
1538        ind: Optional index for the shape sketch.
1539
1540    Returns:
1541        str: The TikZ code for the shape sketch.
1542    """
1543    d_subtype_draw = {
1544        sg.Types.ARC_SKETCH: draw_arc_sketch,
1545        sg.Types.BEZIER_SKETCH: draw_bezier_sketch,
1546        sg.Types.CIRCLE_SKETCH: draw_circle_sketch,
1547        sg.Types.ELLIPSE_SKETCH: draw_ellipse_sketch,
1548        sg.Types.LINE_SKETCH: draw_line_sketch,
1549    }
1550    if sketch.subtype in d_subtype_draw:
1551        res = d_subtype_draw[sketch.subtype](sketch)
1552    elif sketch.draw_markers and sketch.marker_type == MarkerType.INDICES:
1553        res = draw_shape_sketch_with_indices(sketch, ind)
1554    elif sketch.draw_markers or sketch.smooth:
1555        res = draw_shape_sketch_with_markers(sketch)
1556    else:
1557        res = draw_sketch(sketch)
1558
1559    return res

Draws a shape sketch.

Arguments:
  • sketch: The shape sketch object.
  • ind: Optional index for the shape sketch.
Returns:

str: The TikZ code for the shape sketch.

def draw_line_sketch(sketch):
1561def draw_line_sketch(sketch):
1562    """Draws a line sketch.
1563
1564    Args:
1565        sketch: The line sketch object.
1566
1567    Returns:
1568        str: The TikZ code for the line sketch.
1569    """
1570    begin_scope = get_begin_scope()
1571    res = "\\draw"
1572    exceptions = ["draw_fillets", "fillet_radius", "line_join", "line_miter_limit"]
1573    options = get_line_style_options(sketch, exceptions=exceptions)
1574
1575    start = sketch.vertices[0]
1576    end = sketch.vertices[1]
1577    options = ", ".join(options)
1578    res += f"[{options}]"
1579    res += f" {start[:2]} -- {end[:2]};\n"
1580    end_scope = get_end_scope()
1581
1582    return begin_scope + res + end_scope

Draws a line sketch.

Arguments:
  • sketch: The line sketch object.
Returns:

str: The TikZ code for the line sketch.

def draw_circle_sketch(sketch):
1585def draw_circle_sketch(sketch):
1586    """Draws a circle sketch.
1587
1588    Args:
1589        sketch: The circle sketch object.
1590
1591    Returns:
1592        str: The TikZ code for the circle sketch.
1593    """
1594    begin_scope = get_begin_scope()
1595    res = get_draw(sketch)
1596    if not res:
1597        return ""
1598    options = get_line_style_options(sketch)
1599    fill_options = get_fill_style_options(sketch)
1600    options += fill_options
1601    if sketch.smooth:
1602        options += ["smooth"]
1603    options = ", ".join(options)
1604    if options:
1605        res += f"[{options}]"
1606    x, y = sketch.center[:2]
1607    res += f"({x}, {y}) circle ({sketch.radius});\n"
1608    end_scope = get_end_scope()
1609
1610    return begin_scope + res + end_scope

Draws a circle sketch.

Arguments:
  • sketch: The circle sketch object.
Returns:

str: The TikZ code for the circle sketch.

def draw_rect_sketch(sketch):
1613def draw_rect_sketch(sketch):
1614    """Draws a rectangle sketch.
1615
1616    Args:
1617        sketch: The rectangle sketch object.
1618
1619    Returns:
1620        str: The TikZ code for the rectangle sketch.
1621    """
1622    begin_scope = get_begin_scope()
1623    res = get_draw(sketch)
1624    if not res:
1625        return ""
1626    options = get_line_style_options(sketch)
1627    fill_options = get_fill_style_options(sketch)
1628    options += fill_options
1629    if sketch.smooth:
1630        options += ["smooth"]
1631    options = ", ".join(options)
1632    res += f"[{options}]"
1633    x, y = sketch.center[:2]
1634    width, height = sketch.width, sketch.height
1635    res += f"({x}, {y}) rectangle ({width}, {height});\n"
1636    end_scope = get_end_scope()
1637
1638    return begin_scope + res + end_scope

Draws a rectangle sketch.

Arguments:
  • sketch: The rectangle sketch object.
Returns:

str: The TikZ code for the rectangle sketch.

def draw_ellipse_sketch(sketch):
1640def draw_ellipse_sketch(sketch):
1641    """Draws an ellipse sketch.
1642
1643    Args:
1644        sketch: The ellipse sketch object.
1645
1646    Returns:
1647        str: The TikZ code for the ellipse sketch.
1648    """
1649    begin_scope = get_begin_scope()
1650    res = get_draw(sketch)
1651    if not res:
1652        return ""
1653    options = get_line_style_options(sketch)
1654    fill_options = get_fill_style_options(sketch)
1655    options += fill_options
1656    if sketch.smooth:
1657        options += ["smooth"]
1658    angle = degrees(sketch.angle)
1659    x, y = sketch.center[:2]
1660    if angle:
1661        options += [f"rotate around= {{{angle}:({x},{y})}}"]
1662    options = ", ".join(options)
1663    res += f"[{options}]"
1664    a = sketch.x_radius
1665    b = sketch.y_radius
1666
1667    res += f"({x}, {y}) ellipse ({a} and {b});\n"
1668    end_scope = get_end_scope()
1669
1670    return begin_scope + res + end_scope

Draws an ellipse sketch.

Arguments:
  • sketch: The ellipse sketch object.
Returns:

str: The TikZ code for the ellipse sketch.

def draw_arc_sketch(sketch):
1673def draw_arc_sketch(sketch):
1674    """Draws an arc sketch.
1675
1676    Args:
1677        sketch: The arc sketch object.
1678
1679    Returns:
1680        str: The TikZ code for the arc sketch.
1681    """
1682    res = get_draw(sketch)
1683    if not res:
1684        return ""
1685    if sketch.closed:
1686        options = ["smooth cycle"]
1687    else:
1688        options = ['smooth']
1689
1690    if sketch.back_style == BackStyle.PATTERN and sketch.fill and sketch.closed:
1691        options += get_pattern_options(sketch)
1692    if sketch.stroke:
1693        options += get_line_style_options(sketch)
1694    if sketch.closed and sketch.fill:
1695        options += get_fill_style_options(sketch)
1696
1697    if sketch.back_style == BackStyle.SHADING and sketch.fill and sketch.closed:
1698        options += get_shading_options(sketch)
1699    options = ", ".join(options)
1700    if options:
1701        res += f"[{options}] plot[tension=.8] coordinates" + "{"
1702    vertices = [round_point(v) for v in sketch.vertices]
1703    n = len(vertices)
1704    str_lines = [f"{vertices[0]}"]
1705    for i, vertice in enumerate(vertices[1:]):
1706        if (i + 1) % 8 == 0:
1707            if i == n - 1:
1708                str_lines.append(f" {vertice} \n")
1709            else:
1710                str_lines.append(f"\n\t {vertice} ")
1711        else:
1712            str_lines.append(f" {vertice} ")
1713    if sketch.closed:
1714        str_lines.append(" cycle;\n")
1715    else:
1716        str_lines.append("};\n")
1717    if res:
1718        res += "".join(str_lines)
1719    else:
1720        res = "".join(str_lines)
1721    return res

Draws an arc sketch.

Arguments:
  • sketch: The arc sketch object.
Returns:

str: The TikZ code for the arc sketch.

def draw_bezier_sketch(sketch):
1723def draw_bezier_sketch(sketch):
1724    """Draws a Bezier curve sketch.
1725
1726    Args:
1727        sketch: The Bezier curve sketch object.
1728
1729    Returns:
1730        str: The TikZ code for the Bezier curve sketch.
1731    """
1732    begin_scope = get_begin_scope()
1733    res = get_draw(sketch)
1734    if not res:
1735        return ""
1736    options = get_line_style_options(sketch)
1737    options = ", ".join(options)
1738    res += f"[{options}]"
1739    p1, cp1, cp2, p2 = sketch.control_points
1740    x1, y1 = p1[:2]
1741    x2, y2 = cp1[:2]
1742    x3, y3 = cp2[:2]
1743    x4, y4 = p2[:2]
1744    res += f" ({x1}, {y1}) .. controls ({x2}, {y2}) and ({x3}, {y3}) .. ({x4}, {y4});\n"
1745    end_scope = get_end_scope()
1746
1747    return begin_scope + res + end_scope

Draws a Bezier curve sketch.

Arguments:
  • sketch: The Bezier curve sketch object.
Returns:

str: The TikZ code for the Bezier curve sketch.

def draw_line(line):
1750def draw_line(line):
1751    """Tikz code for a line.
1752
1753    Args:
1754        line: The line object.
1755
1756    Returns:
1757        str: The TikZ code for the line.
1758    """
1759    p1 = line.start[:2]
1760    p2 = line.end[:2]
1761    options = []
1762    if line.line_width is not None:
1763        options.append(line.line_width)
1764    if line.color is not None:
1765        color = color2tikz(line.color)
1766        options.append(color)
1767    if line.dash_array is not None:
1768        options.append(line.dash_array)
1769    # options = [line.width, line.color, line.dash_array, line.cap, line.join]
1770    if line.line_width == 0:
1771        res = f"\\path[{', '.join(options)}] {p1} -- {p2};\n"
1772    else:
1773        res = f"\\draw[{', '.join(options)}] {p1} -- {p2};\n"
1774
1775    return res

Tikz code for a line.

Arguments:
  • line: The line object.
Returns:

str: The TikZ code for the line.

def is_stroked(shape: simetri.graphics.shape.Shape) -> bool:
1778def is_stroked(shape: Shape) -> bool:
1779    """Returns True if the shape is stroked.
1780
1781    Args:
1782        shape (Shape): The shape object.
1783
1784    Returns:
1785        bool: True if the shape is stroked, False otherwise.
1786    """
1787    return shape.stroke and shape.line_color is not None and shape.line_width > 0

Returns True if the shape is stroked.

Arguments:
  • shape (Shape): The shape object.
Returns:

bool: True if the shape is stroked, False otherwise.

d_sketch_draw = {<Types.SHAPE: 'SHAPE'>: <function draw_shape_sketch>, <Types.TAG_SKETCH: 'TAG_SKETCH'>: <function draw_tag_sketch>, <Types.LACESKETCH: 'LACE_SKETCH'>: <function draw_lace_sketch>, <Types.LINE: 'LINE'>: <function draw_line_sketch>, <Types.CIRCLE: 'CIRCLE'>: <function draw_circle_sketch>, <Types.ELLIPSE: 'ELLIPSE'>: <function draw_shape_sketch>, <Types.ARC: 'ARC'>: <function draw_arc_sketch>, <Types.BATCH: 'BATCH'>: <function draw_batch_sketch>}