simetri.canvas.canvas

Canvas class for drawing shapes and text on a page. All drawing operations are handled by the Canvas class. Canvas class can draw all graphics objects and text objects. It also provides methods for drawing basic shapes like lines, circles, and polygons.

   1"""Canvas class for drawing shapes and text on a page. All drawing
   2operations are handled by the Canvas class. Canvas class can draw all
   3graphics objects and text objects. It also provides methods for
   4drawing basic shapes like lines, circles, and polygons.
   5"""
   6
   7import os
   8import webbrowser
   9import subprocess
  10import warnings
  11from typing import Optional, Any, Tuple, Sequence
  12from pathlib import Path
  13from dataclasses import dataclass
  14from math import pi
  15
  16from typing_extensions import Self, Union
  17import numpy as np
  18import networkx as nx
  19import fitz
  20
  21from simetri.graphics.affine import (
  22    rotation_matrix,
  23    translation_matrix,
  24    scale_matrix,
  25    scale_in_place_matrix,
  26    identity_matrix,
  27)
  28from simetri.graphics.common import common_properties, _set_Nones, VOID, Point, Vec2
  29from simetri.graphics.all_enums import (
  30    Types,
  31    Drawable,
  32    Result,
  33    Anchor,
  34    TexLoc,
  35    Align,
  36    Axis,
  37)
  38from simetri.settings.settings import defaults
  39from simetri.graphics.bbox import bounding_box
  40from simetri.graphics.batch import Batch
  41from simetri.graphics.shape import Shape
  42from simetri.colors import colors
  43from simetri.canvas import draw
  44from simetri.helpers.utilities import wait_for_file_availability
  45from simetri.helpers.illustration import logo
  46from simetri.tikz.tikz import Tex, get_tex_code
  47from simetri.helpers.validation import validate_args
  48from simetri.canvas.style_map import canvas_args
  49from simetri.notebook import display
  50
  51Color = colors.Color
  52
  53
  54class Canvas:
  55    """Canvas class for drawing shapes and text on a page. All drawing
  56    operations are handled by the Canvas class. Canvas class can draw all
  57    graphics objects and text objects. It also provides methods for
  58    drawing basic shapes like lines, circles, and polygons.
  59    """
  60
  61    def __init__(
  62        self,
  63        back_color: Optional[Color] = None,
  64        border: Optional[float] = None,
  65        size: Optional[Vec2] = None,
  66        **kwargs,
  67    ):
  68        """
  69        Initialize the Canvas.
  70
  71        Args:
  72            back_color (Optional[Color]): The background color of the canvas.
  73            border (Optional[float]): The border width of the canvas.
  74            size (Vec2, optional): The size of the canvas with canvas.origin at (0, 0).
  75            kwargs (dict): Additional keyword arguments. Rarely used.
  76
  77            You should not need to specify "size" since it is calculated.
  78        """
  79        validate_args(kwargs, canvas_args)
  80        _set_Nones(self, ["back_color", "border"], [back_color, border])
  81        self._size = size
  82        self.border = border
  83        self.type = Types.CANVAS
  84        self.subtype = Types.CANVAS
  85        self._code = []
  86        self._font_list = []
  87        self.preamble = defaults["preamble"]
  88        self.back_color = back_color
  89        self.pages = [Page(self.size, self.back_color, self.border)]
  90        self.active_page = self.pages[0]
  91        self._all_vertices = []
  92        self.blend_mode = None
  93        self.blend_group = False
  94        self.transparency_group = False
  95        self.alpha = None
  96        self.line_alpha = None
  97        self.fill_alpha = None
  98        self.text_alpha = None
  99        self.clip = None  # if True then clip the canvas to the mask
 100        self.mask = None  # Mask object
 101        self.even_odd_rule = None  # True or False
 102        self.draw_grid = False
 103        self._origin = [0, 0]
 104        common_properties(self)
 105
 106        for k, v in kwargs.items():
 107            setattr(self, k, v)
 108
 109        self._xform_matrix = identity_matrix()
 110        self._sketch_xform_matrix = identity_matrix()
 111        self.tex: Tex = Tex()
 112        self.render = defaults["render"]
 113        if self._size is not None:
 114            x, y = self.origin[:2]
 115            self._limits = [x, y, x + self.size[0], y + self.size[1]]
 116        else:
 117            self._limits = None
 118
 119    def __setattr__(self, name, value):
 120        if hasattr(self, "active_page") and name in ["back_color", "border"]:
 121            self.active_page.__setattr__(name, value)
 122            self.__dict__[name] = value
 123        elif name in ["size", "origin", "limits"]:
 124            if name == "size":
 125                type(self).size.fset(self, value)
 126            elif name == "origin":
 127                type(self).origin.fset(self, value)
 128            elif name == "limits":
 129                type(self).limits.fset(self, value)
 130        elif name == "scale":
 131            if isinstance(value, (list, tuple)):
 132                type(self).scale.fset(self, value[0], value[1])
 133            else:
 134                type(self).scale.fset(self, value)
 135        elif name == "pos":
 136            if isinstance(value, (list, tuple, np.ndarray)):
 137                type(self).pos.fset(self, value)
 138            else:
 139                raise ValueError("pos must be a list, tuple or ndarray.")
 140        elif name == "angle":
 141            if isinstance(value, (int, float)):
 142                type(self).angle.fset(self, value)
 143            else:
 144                raise ValueError("angle must be a number.")
 145
 146        else:
 147            self.__dict__[name] = value
 148
 149    def display(self) -> Self:
 150        """Show the canvas in a notebook cell."""
 151        display(self)
 152
 153    @property
 154    def size(self) -> Vec2:
 155        """
 156        The size of the canvas.
 157
 158        Returns:
 159            Vec2: The size of the canvas.
 160        """
 161        return self._size
 162
 163    @size.setter
 164    def size(self, value: Vec2) -> None:
 165        """
 166        Set the size of the canvas.
 167
 168        Args:
 169            value (Vec2): The size of the canvas.
 170        """
 171        if len(value) == 2:
 172            self._size = value
 173            x, y = self.origin[:2]
 174            w, h = value
 175            self._limits = (x, y, x + w, y + h)
 176        else:
 177            raise ValueError("Size must be a tuple of 2 values.")
 178
 179    @property
 180    def origin(self) -> Vec2:
 181        """
 182        The origin of the canvas.
 183
 184        Returns:
 185            Vec2: The origin of the canvas.
 186        """
 187        return self._origin[:2]
 188
 189    @origin.setter
 190    def origin(self, value: Vec2) -> None:
 191        """
 192        Set the origin of the canvas.
 193
 194        Args:
 195            value (Vec2): The origin of the canvas.
 196        """
 197        if len(value) == 2:
 198            self._origin = value
 199        else:
 200            raise ValueError("Origin must be a tuple of 2 values.")
 201
 202    @property
 203    def limits(self) -> Vec2:
 204        """
 205        The limits of the canvas.
 206
 207        Returns:
 208            Vec2: The limits of the canvas.
 209        """
 210        if self.size is None:
 211            res = None
 212        else:
 213            x, y = self.origin[:2]
 214            w, h = self.size
 215            res = (x, y, x + w, y + h)
 216
 217        return res
 218
 219    @limits.setter
 220    def limits(self, value: Vec2) -> None:
 221        """
 222        Set the limits of the canvas.
 223
 224        Args:
 225            value (Vec2): The limits of the canvas.
 226        """
 227        if len(value) == 4:
 228            x1, y1, x2, y2 = value
 229            self._size = (x2 - x1, y2 - y1)
 230            self._origin = (x1, y1)
 231        else:
 232            raise ValueError("Limits must be a tuple of 4 values.")
 233
 234    def insert_code(self, code, loc: TexLoc = TexLoc.PICTURE) -> Self:
 235        """
 236        Insert code into the canvas.
 237
 238        Args:
 239            code (str): The code to insert.
 240            loc (TexLoc): The location to insert the code.
 241
 242        Returns:
 243            Self: The canvas object.
 244        """
 245        draw.insert_code(self, code, loc)
 246        return self
 247
 248    def arc(
 249        self,
 250        center: Point,
 251        radius_x: float,
 252        radius_y: float = None,
 253        start_angle: float = 0,
 254        span_angle: float = pi / 2,
 255        rot_angle: float = 0,
 256        **kwargs,
 257    ) -> Self:
 258        """
 259        Draw an arc with the given center, radius, start angle and end angle.
 260
 261        Args:
 262            center (Point): The center of the arc.
 263            radius_x (float): The radius of the arc.
 264            radius_y (float, optional): The second radius of the arc, defaults to None.
 265            start_angle (float): The start angle of the arc.
 266            end_angle (float): The end angle of the arc.
 267            rot_angle (float, optional): The rotation angle of the arc, defaults to 0.
 268            kwargs (dict): Additional keyword arguments.
 269
 270        Returns:
 271            Self: The canvas object.
 272        """
 273        if radius_y is None:
 274            radius_y = radius_x
 275        draw.arc(
 276            self,
 277            center,
 278            radius_x,
 279            radius_y,
 280            start_angle,
 281            span_angle,
 282            rot_angle,
 283            **kwargs,
 284        )
 285        return self
 286
 287    def bezier(self, control_points: Sequence[Point], **kwargs) -> Self:
 288        """
 289        Draw a bezier curve.
 290
 291        Args:
 292            control_points (Sequence[Point]): The control points of the bezier curve.
 293            kwargs (dict): Additional keyword arguments.
 294
 295        Returns:
 296            Self: The canvas object.
 297        """
 298        draw.bezier(self, control_points, **kwargs)
 299        return self
 300
 301    def circle(self, center: Point, radius: float, **kwargs) -> Self:
 302        """
 303        Draw a circle with the given center and radius.
 304
 305        Args:
 306            center (Point): The center of the circle.
 307            radius (float): The radius of the circle.
 308            kwargs (dict): Additional keyword arguments.
 309
 310        Returns:
 311            Self: The canvas object.
 312        """
 313        draw.circle(self, center, radius, **kwargs)
 314        return self
 315
 316    def ellipse(
 317        self, center: Point, width: float, height: float, angle: float = 0, **kwargs
 318    ) -> Self:
 319        """
 320        Draw an ellipse with the given center and radius.
 321
 322        Args:
 323            center (Point): The center of the ellipse.
 324            width (float): The width of the ellipse.
 325            height (float): The height of the ellipse.
 326            angle (float, optional): The angle of the ellipse, defaults to 0.
 327            kwargs (dict): Additional keyword arguments.
 328
 329        Returns:
 330            Self: The canvas object.
 331        """
 332        draw.ellipse(self, center, width, height, angle, **kwargs)
 333        return self
 334
 335    def text(
 336        self,
 337        text: str,
 338        pos: Point,
 339        font_family: str = None,
 340        font_size: int = None,
 341        anchor: Anchor = None,
 342        align: Align = None,
 343        **kwargs,
 344    ) -> Self:
 345        """
 346        Draw text at the given point.
 347
 348        Args:
 349            text (str): The text to draw.
 350            pos (Point): The position to draw the text.
 351            font_family (str, optional): The font family of the text, defaults to None.
 352            font_size (int, optional): The font size of the text, defaults to None.
 353            anchor (Anchor, optional): The anchor of the text, defaults to None.
 354            anchor options: BASE, BASE_EAST, BASE_WEST, BOTTOM, CENTER, EAST, NORTH,
 355            NORTHEAST, NORTHWEST, SOUTH, SOUTHEAST, SOUTHWEST, WEST, MIDEAST, MIDWEST, RIGHT,
 356            LEFT, TOP
 357            align (Align, optional): The alignment of the text, defaults to Align.CENTER.
 358            align options: CENTER, FLUSH_CENTER, FLUSH_LEFT, FLUSH_RIGHT, JUSTIFY, LEFT, RIGHT
 359            kwargs (dict): Additional keyword arguments.
 360            common kwargs: fill_color, line_color, line_width, fill, line, alpha, font_color
 361
 362        Returns:
 363            Self: The canvas object.
 364        """
 365        pos = [pos[0], pos[1], 1]
 366        pos = pos @ self._xform_matrix
 367        draw.text(
 368            self,
 369            txt=text,
 370            pos=pos,
 371            font_family=font_family,
 372            font_size=font_size,
 373            anchor=anchor,
 374            **kwargs,
 375        )
 376        return self
 377
 378    def help_lines(
 379        self,
 380        pos=(-100, -100),
 381        width: float = 400,
 382        height: float = 400,
 383        spacing=25,
 384        cs_size: float = 25,
 385        **kwargs,
 386    ) -> Self:
 387        """
 388        Draw help lines on the canvas.
 389
 390        Args:
 391            pos (tuple): The position to start drawing the help lines.
 392            width (float): The length of the help lines along the x-axis.
 393            height (float): The length of the help lines along the y-axis.
 394            spacing (int): The spacing between the help lines.
 395            cs_size (float): The size of the coordinate system.
 396            kwargs (dict): Additional keyword arguments.
 397
 398        Returns:
 399            Self: The canvas object.
 400        """
 401        draw.help_lines(self, pos, width, height, spacing, cs_size, **kwargs)
 402        return self
 403
 404    def grid(
 405        self, pos: Point, width: float, height: float, spacing: float, **kwargs
 406    ) -> Self:
 407        """
 408        Draw a grid with the given size and spacing.
 409
 410        Args:
 411            pos (Point): The position to start drawing the grid.
 412            width (float): The length of the grid along the x-axis.
 413            height (float): The length of the grid along the y-axis.
 414            spacing (float): The spacing between the grid lines.
 415            kwargs (dict): Additional keyword arguments.
 416
 417        Returns:
 418            Self: The canvas object.
 419        """
 420        draw.grid(self, pos, width, height, spacing, **kwargs)
 421        return self
 422
 423    def line(self, start: Point, end: Point, **kwargs) -> Self:
 424        """
 425        Draw a line from start to end.
 426
 427        Args:
 428            start (Point): The starting point of the line.
 429            end (Point): The ending point of the line.
 430            kwargs (dict): Additional keyword arguments.
 431
 432        Returns:
 433            Self: The canvas object.
 434        """
 435        draw.line(self, start, end, **kwargs)
 436        return self
 437
 438    def rectangle(
 439        self,
 440        center: Point = (0, 0),
 441        width: float = 100,
 442        height: float = 100,
 443        angle: float = 0,
 444        **kwargs,
 445    ) -> Self:
 446        """
 447        Draw a rectangle.
 448
 449        Args:
 450            center (Point): The center of the rectangle.
 451            width (float): The width of the rectangle.
 452            height (float): The height of the rectangle.
 453            angle (float, optional): The angle of the rectangle, defaults to 0.
 454            kwargs (dict): Additional keyword arguments.
 455
 456        Returns:
 457            Self: The canvas object.
 458        """
 459        draw.rectangle(self, center, width, height, angle, **kwargs)
 460        return self
 461
 462    def square(
 463        self, center: Point = (0, 0), size: float = 100, angle: float = 0, **kwargs
 464    ) -> Self:
 465        """
 466        Draw a square with the given center and size.
 467
 468        Args:
 469            center (Point): The center of the square.
 470            size (float): The size of the square.
 471            angle (float, optional): The angle of the square, defaults to 0.
 472            kwargs (dict): Additional keyword arguments.
 473
 474        Returns:
 475            Self: The canvas object.
 476        """
 477        draw.rectangle(self, center, size, size, angle, **kwargs)
 478        return self
 479
 480    def lines(self, points: Sequence[Point], **kwargs) -> Self:
 481        """
 482        Draw a polyline through the given points.
 483
 484        Args:
 485            points (Sequence[Point]): The points to draw the polyline through.
 486            kwargs (dict): Additional keyword arguments.
 487
 488        Returns:
 489            Self: The canvas object.
 490        """
 491        draw.lines(self, points, **kwargs)
 492        return self
 493
 494    def draw_lace(self, lace: Batch, **kwargs) -> Self:
 495        """
 496        Draw the lace.
 497
 498        Args:
 499            lace (Batch): The lace to draw.
 500            kwargs (dict): Additional keyword arguments.
 501
 502        Returns:
 503            Self: The canvas object.
 504        """
 505        draw.draw_lace(self, lace, **kwargs)
 506        return self
 507
 508    def draw_dimension(self, dim: Shape, **kwargs) -> Self:
 509        """
 510        Draw the dimension.
 511
 512        Args:
 513            dim (Shape): The dimension to draw.
 514            kwargs (dict): Additional keyword arguments.
 515
 516        Returns:
 517            Self: The canvas object.
 518        """
 519        draw.draw_dimension(self, dim, **kwargs)
 520        return self
 521
 522    def draw(
 523        self,
 524        item_s: Union[Drawable, list, tuple],
 525        pos: Point = None,
 526        angle: float = 0,
 527        rotocenter: Point = (0, 0),
 528        scale=(1, 1),
 529        about=(0, 0),
 530        **kwargs,
 531    ) -> Self:
 532        """
 533        Draw the item_s. item_s can be a single item or a list of items.
 534
 535        Args:
 536            item_s (Union[Drawable, list, tuple]): The item(s) to draw.
 537            pos (Point, optional): The position to draw the item(s), defaults to None.
 538            angle (float, optional): The angle to rotate the item(s), defaults to 0.
 539            rotocenter (Point, optional): The point about which to rotate, defaults to (0, 0).
 540            scale (tuple, optional): The scale factors for the x and y axes, defaults to (1, 1).
 541            about (tuple, optional): The point about which to scale, defaults to (0, 0).
 542            kwargs (dict): Additional keyword arguments.
 543
 544        Returns:
 545            Self: The canvas object.
 546        """
 547        sketch_xform = self._sketch_xform_matrix
 548        if pos is not None:
 549            sketch_xform = translation_matrix(*pos[:2]) @ sketch_xform
 550        if scale[0] != 1 or scale[1] != 1:
 551            sketch_xform = scale_in_place_matrix(*pos[:2], about) @ sketch_xform
 552        if angle != 0:
 553            sketch_xform = rotation_matrix(angle, rotocenter) @ sketch_xform
 554        self._sketch_xform_matrix = sketch_xform @ self._xform_matrix
 555
 556        if isinstance(item_s, (list, tuple)):
 557            for item in item_s:
 558                draw.draw(self, item, **kwargs)
 559        else:
 560            draw.draw(self, item_s, **kwargs)
 561
 562        self._sketch_xform_matrix = identity_matrix()
 563
 564        return self
 565
 566    def draw_CS(self, size: float = None, **kwargs) -> Self:
 567        """
 568        Draw the Canvas coordinate system.
 569
 570        Args:
 571            size (float, optional): The size of the coordinate system, defaults to None.
 572            kwargs (dict): Additional keyword arguments.
 573
 574        Returns:
 575            Self: The canvas object.
 576        """
 577        draw.draw_CS(self, size, **kwargs)
 578        return self
 579
 580    def draw_frame(
 581        self, margin: Union[float, Sequence] = None, width=None, **kwargs
 582    ) -> Self:
 583        """
 584        Draw a frame around the canvas.
 585
 586        Args:
 587            margins (Union[float, Sequence]): The margins of the frame.
 588            kwargs (dict): Additional keyword arguments.
 589
 590        Returns:
 591            Self: The canvas object.
 592        """
 593        # to do: add shadow and frame color, shadow width. Check canvas.clip.
 594        if margin is None:
 595            margin = defaults["canvas_frame_margin"]
 596        if width is None:
 597            width = defaults["canvas_frame_width"]
 598        b_box = bounding_box(self._all_vertices)
 599        box2 = b_box.get_inflated_b_box(margin)
 600        box3 = box2.get_inflated_b_box(15)
 601        shadow = Shape([box3.northwest, box3.southwest, box3.southeast])
 602        self.draw(shadow, line_color=colors.light_gray, line_width=width)
 603        self.draw(Shape(box2.corners, closed=True), fill=False, line_width=width)
 604
 605        return self
 606
 607    def reset(self) -> Self:
 608        """
 609        Reset the canvas to its initial state.
 610
 611        Returns:
 612            Self: The canvas object.
 613        """
 614        self._code = []
 615        self.preamble = defaults["preamble"]
 616        self.pages = [Page(self.size, self.back_color, self.border)]
 617        self.active_page = self.pages[0]
 618        self._all_vertices = []
 619        self.tex: Tex = Tex()
 620        self.clip: bool = False  # if true then clip the canvas to the mask
 621        self._xform_matrix = identity_matrix()
 622        self._sketch_xform_matrix = identity_matrix()
 623        self.active_page = self.pages[0]
 624        self._all_vertices = []
 625        return self
 626
 627    def __str__(self) -> str:
 628        """
 629        Return a string representation of the canvas.
 630
 631        Returns:
 632            str: The string representation of the canvas.
 633        """
 634        return "Canvas()"
 635
 636    def __repr__(self) -> str:
 637        """
 638        Return a string representation of the canvas.
 639
 640        Returns:
 641            str: The string representation of the canvas.
 642        """
 643        return "Canvas()"
 644
 645    @property
 646    def pos(self) -> Point:
 647        """
 648        The position of the canvas.
 649
 650        Args:
 651            point (Point, optional): The point to set the position to.
 652
 653        Returns:
 654            Point: The position of the canvas.
 655        """
 656
 657        return self._xform_matrix[2, :2].tolist()[:2]
 658
 659    @pos.setter
 660    def pos(self, point: Point) -> None:
 661        """
 662        Set the position of the canvas.
 663
 664        Args:
 665            point (Point): The point to set the position to.
 666        """
 667        self._xform_matrix[2, :2] = point[:2]
 668
 669    @property
 670    def angle(self) -> float:
 671        """
 672        The angle of the canvas.
 673
 674        Returns:
 675            float: The angle of the canvas.
 676        """
 677        xform = self._xform_matrix
 678
 679        return np.arctan2(xform[0, 1], xform[0, 0])
 680
 681    @angle.setter
 682    def angle(self, angle: float) -> None:
 683        """
 684        Set the angle of the canvas.
 685
 686        Args:
 687            angle (float): The angle to set the canvas to.
 688        """
 689        self._xform_matrix = rotation_matrix(angle) @ self._xform_matrix
 690
 691    @property
 692    def scale(self) -> Vec2:
 693        """
 694        The scale of the canvas.
 695
 696        Returns:
 697            Vec2: The scale of the canvas.
 698        """
 699        xform = self._xform_matrix
 700
 701        return np.linalg.norm(xform[:2, 0]), np.linalg.norm(xform[:2, 1])
 702
 703    @scale.setter
 704    def scale(
 705        self, scale_x: float = 1, scale_y: float = None, about: Point = (0, 0)
 706    ) -> None:
 707        """
 708        Set the scale of the canvas.
 709
 710        Args:
 711            scale_x (float): The x-scale to set the canvas to.
 712            scale_y (float): The y-scale to set the canvas to.
 713            about (Point): The point about which to scale the canvas.
 714        """
 715        if scale_y is None:
 716            scale_y = scale_x
 717
 718        self._xform_matrix = (
 719            scale_in_place_matrix(scale_x, scale_y, about=about) @ self._xform_matrix
 720        )
 721
 722    @property
 723    def xform_matrix(self) -> "ndarray":
 724        """
 725        The transformation matrix of the canvas.
 726
 727        Returns:
 728            ndarray: The transformation matrix of the canvas.
 729        """
 730        return self._xform_matrix.copy()
 731
 732    def transform(self, transform_matrix: "ndarray") -> Self:
 733        """
 734        Transforms the canvas by the given transformation matrix.
 735
 736        Args:
 737            transform_matrix (ndarray): The transformation matrix.
 738
 739        Returns:
 740            Self: The Canvas object.
 741        """
 742        self._xform_matrix = transform_matrix @ self._xform_matrix
 743
 744        return self
 745
 746    def reset_transform(self) -> Self:
 747        """
 748        Reset the transformation matrix of the canvas.
 749        The canvas origin is at (0, 0) and the orientation angle is 0.
 750        Transformation matrix is the identity matrix.
 751
 752        Returns:
 753            Self: The canvas object.
 754        """
 755        self._xform_matrix = identity_matrix()
 756
 757        return self
 758
 759    def translate(self, x: float, y: float) -> Self:
 760        """
 761        Translate the canvas by x and y.
 762
 763        Args:
 764            x (float): The translation distance along the x-axis.
 765            y (float): The translation distance along the y-axis.
 766
 767        Returns:
 768            Self: The canvas object.
 769        """
 770
 771        self._xform_matrix = translation_matrix(x, y) @ self._xform_matrix
 772
 773        return self
 774
 775    def rotate(self, angle: float, about=(0, 0)) -> Self:
 776        """
 777        Rotate the canvas by angle in radians about the given point.
 778
 779        Args:
 780            angle (float): The rotation angle in radians.
 781            about (tuple): The point about which to rotate the canvas.
 782
 783        Returns:
 784            Self: The canvas object.
 785        """
 786
 787        self._xform_matrix = rotation_matrix(angle, about) @ self._xform_matrix
 788
 789        return self
 790
 791    def _flip(self, axis: Axis) -> Self:
 792        """
 793        Flip the canvas along the specified axis.
 794
 795        Args:
 796            axis (str): The axis to flip the canvas along ('x' or 'y').
 797
 798        Returns:
 799            Self: The canvas object.
 800        """
 801        if axis == Axis.X:
 802            sx = -self.scale[0]
 803            sy = 1
 804        elif axis == Axis.Y:
 805            sx = 1
 806            sy = -self.scale[1]
 807
 808        self._xform_matrix = scale_matrix(sx, sy) @ self._xform_matrix
 809
 810        return self
 811
 812    def flip_x_axis(self) -> Self:
 813        """
 814        Flip the x-axis direction. Warning: This will reverse the positive rotation direction.
 815
 816        Returns:
 817            Self: The canvas object.
 818        """
 819        warnings.warn(
 820            "Flipping the x-axis will change the positive rotation direction."
 821        )
 822        return self._flip(Axis.X)
 823
 824    def flip_y_axis(self) -> Self:
 825        """
 826        Flip the y-axis direction.
 827
 828        Returns:
 829            Self: The canvas object.
 830        """
 831        warnings.warn(
 832            "Flipping the y-axis will reverse the positive rotation direction."
 833        )
 834
 835        return self._flip(Axis.Y)
 836
 837    @property
 838    def x(self) -> float:
 839        """
 840        The x coordinate of the canvas origin.
 841
 842        Returns:
 843            float: The x coordinate of the canvas origin.
 844        """
 845        return self.pos[0]
 846
 847    @x.setter
 848    def x(self, value: float) -> None:
 849        """
 850        Set the x coordinate of the canvas origin.
 851
 852        Args:
 853            value (float): The x coordinate to set.
 854        """
 855        self.pos = [value, self.pos[1]]
 856
 857    @property
 858    def y(self) -> float:
 859        """
 860        The y coordinate of the canvas origin.
 861
 862        Returns:
 863            float: The y coordinate of the canvas origin.
 864        """
 865        return self.pos[1]
 866
 867    @y.setter
 868    def y(self, value: float) -> None:
 869        """
 870        Set the y coordinate of the canvas origin.
 871
 872        Args:
 873            value (float): The y coordinate to set.
 874        """
 875        self.pos = [self.pos[0], value]
 876
 877    def batch_graph(self, batch: "Batch") -> nx.DiGraph:
 878        """
 879        Return a directed graph of the batch and its elements.
 880        Canvas is the root of the graph.
 881        Graph nodes are the ids of the elements.
 882
 883        Args:
 884            batch (Batch): The batch to create the graph from.
 885
 886        Returns:
 887            nx.DiGraph: The directed graph of the batch and its elements.
 888        """
 889
 890        def add_batch(batch, graph):
 891            graph.add_node(batch.id)
 892            for item in batch.elements:
 893                graph.add_edge(batch.id, item.id)
 894                if item.subtype == Types.BATCH:
 895                    add_batch(item, graph)
 896            return graph
 897
 898        di_graph = nx.DiGraph()
 899        di_graph.add_edge(self.id, batch.id)
 900        for item in batch.elements:
 901            if item.subtype == Types.BATCH:
 902                di_graph.add_edge(batch.id, item.id)
 903                add_batch(item, di_graph)
 904            else:
 905                di_graph.add_edge(batch.id, item.id)
 906
 907        return di_graph
 908
 909    def _resolve_property(self, item: Drawable, property_name: str) -> Any:
 910        """
 911        Handles None values for properties.
 912        try item.property_name first,
 913        then try canvas.property_name,
 914        finally use the default value.
 915
 916        Args:
 917            item (Drawable): The item to resolve the property for.
 918            property_name (str): The name of the property to resolve.
 919
 920        Returns:
 921            Any: The resolved property value.
 922        """
 923        value = getattr(item, property_name)
 924        if value is None:
 925            value = self.__dict__.get(property_name, None)
 926            if value is None:
 927                value = defaults.get(property_name, VOID)
 928            if value == VOID:
 929                print(f"Property {property_name} is not in defaults.")
 930                value = None
 931        return value
 932
 933    def get_fonts_list(self) -> list[str]:
 934        """
 935        Get the list of fonts used in the canvas.
 936
 937        Returns:
 938            list[str]: The list of fonts used in the canvas.
 939        """
 940        user_fonts = set(self._font_list)
 941
 942        latex_fonts = set(
 943            [
 944                defaults["main_font"],
 945                defaults["sans_font"],
 946                defaults["mono_font"],
 947                "serif",
 948                "sansserif",
 949                "monospace",
 950            ]
 951        )
 952        for sketch in self.active_page.sketches:
 953            if sketch.subtype == Types.TAG_SKETCH:
 954                name = sketch.font_family
 955                if name is not None and name not in latex_fonts:
 956                    user_fonts.add(name)
 957        return list(user_fonts.difference(latex_fonts))
 958
 959    def _calculate_size(self, border=None, b_box=None) -> Tuple[float, float]:
 960        """
 961        Calculate the size of the canvas based on the bounding box and border.
 962
 963        Args:
 964            border (float, optional): The border of the canvas, defaults to None.
 965            b_box (Any, optional): The bounding box of the canvas, defaults to None.
 966
 967        Returns:
 968            Tuple[float, float]: The size of the canvas.
 969        """
 970        vertices = self._all_vertices
 971        if vertices:
 972            if b_box is None:
 973                b_box = bounding_box(vertices)
 974
 975            if border is None:
 976                if self.border is None:
 977                    border = defaults["border"]
 978                else:
 979                    border = self.border
 980            w = b_box.width + 2 * border
 981            h = b_box.height + 2 * border
 982            offset_x, offset_y = b_box.southwest
 983            res = w, h, offset_x - border, offset_y - border
 984        else:
 985            res = None
 986        return res
 987
 988    def _show_browser(
 989        self, file_path: Path, show_browser: bool, multi_page_svg: bool
 990    ) -> None:
 991        """
 992        Show the file in the browser.
 993
 994        Args:
 995            file_path (Path): The path to the file.
 996            show_browser (bool): Whether to show the file in the browser.
 997            multi_page_svg (bool): Whether the file is a multi-page SVG.
 998        """
 999        if show_browser is None:
1000            show_browser = defaults["show_browser"]
1001        if show_browser:
1002            file_path = "file:///" + file_path
1003            if multi_page_svg:
1004                for i, _ in enumerate(self.pages):
1005                    f_path = file_path.replace(".svg", f"_page{i + 1}.svg")
1006                    webbrowser.open(f_path)
1007            else:
1008                webbrowser.open(file_path)
1009
1010    def save(
1011        self,
1012        file_path: Path = None,
1013        overwrite: bool = None,
1014        show: bool = None,
1015        print_output=False,
1016    ) -> Self:
1017        """
1018        Save the canvas to a file.
1019
1020        Args:
1021            file_path (Path, optional): The path to save the file.
1022            overwrite (bool, optional): Whether to overwrite the file if it exists.
1023            show (bool, optional): Whether to show the file in the browser.
1024            print_output (bool, optional): Whether to print the output of the compilation.
1025
1026        Returns:
1027            Self: The canvas object.
1028        """
1029
1030        def validate_file_path(file_path: Path, overwrite: bool) -> Result:
1031            """
1032            Validate the file path.
1033
1034            Args:
1035                file_path (Path): The path to the file.
1036                overwrite (bool): Whether to overwrite the file if it exists.
1037
1038            Returns:
1039                Result: The parent directory, file name, and extension.
1040            """
1041            path_exists = os.path.exists(file_path)
1042            if path_exists and not overwrite:
1043                raise FileExistsError(
1044                    f"File {file_path} already exists. \n"
1045                    "Use canvas.save(file_path, overwrite=True) to overwrite the file."
1046                )
1047            parent_dir, file_name = os.path.split(file_path)
1048            file_name, extension = os.path.splitext(file_name)
1049            if extension not in [".pdf", ".eps", ".ps", ".svg", ".png", ".tex"]:
1050                raise RuntimeError("File type is not supported.")
1051            if not os.path.exists(parent_dir):
1052                raise NotADirectoryError(f"Directory {parent_dir} does not exist.")
1053            if not os.access(parent_dir, os.W_OK):
1054                raise PermissionError(f"Directory {parent_dir} is not writable.")
1055
1056            return parent_dir, file_name, extension
1057
1058        def compile_tex(cmd):
1059            """
1060            Compile the TeX file.
1061
1062            Args:
1063                cmd (str): The command to compile the TeX file.
1064
1065            Returns:
1066                str: The output of the compilation.
1067            """
1068            os.chdir(parent_dir)
1069            with subprocess.Popen(
1070                cmd,
1071                stdin=subprocess.PIPE,
1072                stdout=subprocess.PIPE,
1073                shell=True,
1074                text=True,
1075            ) as p:
1076                output = p.communicate("_s\n_l\n")[0]
1077            if print_output:
1078                print(output.split("\n")[-3:])
1079            return output
1080
1081        def remove_aux_files(file_path):
1082            """
1083            Remove auxiliary files generated during compilation.
1084
1085            Args:
1086                file_path (Path): The path to the file.
1087            """
1088            time_out = 1  # seconds
1089            parent_dir, file_name = os.path.split(file_path)
1090            file_name, extension = os.path.splitext(file_name)
1091            aux_file = os.path.join(parent_dir, file_name + ".aux")
1092            if os.path.exists(aux_file):
1093                if not wait_for_file_availability(aux_file, time_out):
1094                    print(
1095                        (
1096                            f"File '{aux_file}' is not available after waiting for "
1097                            f"{time_out} seconds."
1098                        )
1099                    )
1100                else:
1101                    os.remove(aux_file)
1102            log_file = os.path.join(parent_dir, file_name + ".log")
1103            if os.path.exists(log_file):
1104                if not wait_for_file_availability(log_file, time_out):
1105                    print(
1106                        (
1107                            f"File '{log_file}' is not available after waiting for "
1108                            f"{time_out} seconds."
1109                        )
1110                    )
1111                else:
1112                    if not defaults["keep_log_files"]:
1113                        os.remove(log_file)
1114            tex_file = os.path.join(parent_dir, file_name + ".tex")
1115            if os.path.exists(tex_file):
1116                if not wait_for_file_availability(tex_file, time_out):
1117                    print(
1118                        (
1119                            f"File '{tex_file}' is not available after waiting for "
1120                            f"{time_out} seconds."
1121                        )
1122                    )
1123                else:
1124                    os.remove(tex_file)
1125            file_name, extension = os.path.splitext(file_name)
1126            if extension not in [".pdf", ".tex"]:
1127                pdf_file = os.path.join(parent_dir, file_name + ".pdf")
1128                if os.path.exists(pdf_file):
1129                    if not wait_for_file_availability(pdf_file, time_out):
1130                        print(
1131                            (
1132                                f"File '{pdf_file}' is not available after waiting for "
1133                                f"{time_out} seconds."
1134                            )
1135                        )
1136                    else:
1137                        # os.remove(pdf_file)
1138                        pass
1139            log_file = os.path.join(parent_dir, "simetri.log")
1140            if os.path.exists(log_file):
1141                try:
1142                    os.remove(log_file)
1143                except PermissionError:
1144                    # to do: log the error
1145                    pass
1146
1147        def run_job():
1148            """
1149            Run the job to compile and save the file.
1150
1151            Returns:
1152                None
1153            """
1154            output_path = os.path.join(parent_dir, file_name + extension)
1155            cmd = "xelatex " + tex_path + " --output-directory " + parent_dir
1156            res = compile_tex(cmd)
1157            if "No pages of output" in res:
1158                raise RuntimeError("Failed to compile the tex file.")
1159            pdf_path = os.path.join(parent_dir, file_name + ".pdf")
1160            if not os.path.exists(pdf_path):
1161                raise RuntimeError("Failed to compile the tex file.")
1162
1163            if extension in [".eps", ".ps"]:
1164                ps_path = os.path.join(parent_dir, file_name + extension)
1165                os.chdir(parent_dir)
1166                cmd = f"pdf2ps {pdf_path} {ps_path}"
1167                res = subprocess.run(cmd, shell=True, check=False)
1168                if res.returncode != 0:
1169                    raise RuntimeError("Failed to convert pdf to ps.")
1170            elif extension == ".svg":
1171                doc = fitz.open(pdf_path)
1172                page = doc.load_page(0)
1173                svg = page.get_svg_image()
1174                with open(output_path, "w", encoding="utf-8") as f:
1175                    f.write(svg)
1176            elif extension == ".png":
1177                pdf_file = fitz.open(pdf_path)
1178                page = pdf_file[0]
1179                pix = page.get_pixmap()
1180                pix.save(output_path)
1181                pdf_file.close()
1182
1183        parent_dir, file_name, extension = validate_file_path(file_path, overwrite)
1184
1185        tex_code = get_tex_code(self)
1186        tex_path = os.path.join(parent_dir, file_name + ".tex")
1187        with open(tex_path, "w", encoding="utf-8") as f:
1188            f.write(tex_code)
1189        if extension == ".tex":
1190            return self
1191
1192        run_job()
1193        remove_aux_files(file_path)
1194
1195        self._show_browser(file_path=file_path, show_browser=show, multi_page_svg=False)
1196        return self
1197
1198    def new_page(self, **kwargs) -> Self:
1199        """
1200        Create a new page and add it to the canvas.pages.
1201
1202        Args:
1203            kwargs (dict): Additional keyword arguments.
1204
1205        Returns:
1206            Self: The canvas object.
1207        """
1208        page = Page()
1209        self.pages.append(page)
1210        self.active_page = page
1211        for k, v in kwargs.items():
1212            setattr(page, k, v)
1213        return self
1214
1215
1216@dataclass
1217class PageGrid:
1218    """
1219    Grid class for drawing grids on a page.
1220
1221    Args:
1222        spacing (float, optional): The spacing between grid lines.
1223        back_color (Color, optional): The background color of the grid.
1224        line_color (Color, optional): The color of the grid lines.
1225        line_width (float, optional): The width of the grid lines.
1226        line_dash_array (Sequence[float], optional): The dash array for the grid lines.
1227        x_shift (float, optional): The x-axis shift of the grid.
1228        y_shift (float, optional): The y-axis shift of the grid.
1229    """
1230
1231    spacing: float = None
1232    back_color: Color = None
1233    line_color: Color = None
1234    line_width: float = None
1235    line_dash_array: Sequence[float] = None
1236    x_shift: float = None
1237    y_shift: float = None
1238
1239    def __post_init__(self):
1240        self.type = Types.PAGE_GRID
1241        self.subtype = Types.RECTANGULAR
1242        self.spacing = defaults["page_grid_spacing"]
1243        self.back_color = defaults["page_grid_back_color"]
1244        self.line_color = defaults["page_grid_line_color"]
1245        self.line_width = defaults["page_grid_line_width"]
1246        self.line_dash_array = defaults["page_grid_line_dash_array"]
1247        self.x_shift = defaults["page_grid_x_shift"]
1248        self.y_shift = defaults["page_grid_y_shift"]
1249        common_properties(self)
1250
1251
1252@dataclass
1253class Page:
1254    """
1255    Page class for drawing sketches and text on a page. All drawing
1256    operations result as sketches on the canvas.active_page.
1257
1258    Args:
1259        size (Vec2, optional): The size of the page.
1260        back_color (Color, optional): The background color of the page.
1261        mask (Any, optional): The mask of the page.
1262        margins (Any, optional): The margins of the page (left, bottom, right, top).
1263        recto (bool, optional): Whether the page is recto (True) or verso (False).
1264        grid (PageGrid, optional): The grid of the page.
1265        kwargs (dict, optional): Additional keyword arguments.
1266    """
1267
1268    size: Vec2 = None
1269    back_color: Color = None
1270    mask = None
1271    margins = None  # left, bottom, right, top
1272    recto: bool = True  # True if page is recto, False if verso
1273    grid: PageGrid = None
1274    kwargs: dict = None
1275
1276    def __post_init__(self):
1277        self.type = Types.PAGE
1278        self.sketches = []
1279        if self.grid is None:
1280            self.grid = PageGrid()
1281        if self.kwargs:
1282            for k, v in self.kwargs.items():
1283                setattr(self, k, v)
1284        common_properties(self)
1285
1286
1287def hello() -> None:
1288    """
1289    Show a hello message.
1290    Used for testing an installation of simetri.
1291    """
1292    canvas = Canvas()
1293
1294    canvas.text("Hello from simetri.graphics!", (0, -130), bold=True, font_size=20)
1295    canvas.draw(logo())
1296
1297    d_path = os.path.dirname(os.path.abspath(__file__))
1298    f_path = os.path.join(d_path, "hello.pdf")
1299
1300    canvas.save(f_path, overwrite=True)
@dataclass
class Color:
116@dataclass
117class Color:
118    """A class representing an RGB or RGBA color.
119
120    This class represents a color in RGB or RGBA color space. The default values
121    for the components are normalized between 0.0 and 1.0. Values outside this range
122    are automatically converted from the 0-255 range.
123
124    Attributes:
125        red: The red component of the color (0.0 to 1.0).
126        green: The green component of the color (0.0 to 1.0).
127        blue: The blue component of the color (0.0 to 1.0).
128        alpha: The alpha (transparency) component (0.0 to 1.0), default is 1.
129        space: The color space, default is "rgb".
130
131    Examples:
132        >>> red = Color(1.0, 0.0, 0.0)
133        >>> transparent_blue = Color(0.0, 0.0, 1.0, 0.5)
134        >>> rgb255 = Color(255, 0, 128)  # Will be automatically normalized
135    """
136    red: int = 0
137    green: int = 0
138    blue: int = 0
139    alpha: int = 1
140    space: ColorSpace = "rgb"  # for future use
141
142    def __post_init__(self):
143        """Post-initialization to ensure color values are in the correct range."""
144        r, g, b = self.red, self.green, self.blue
145        if r < 0 or r > 1 or g < 0 or g > 1 or b < 0 or b > 1:
146            self.red = r / 255
147            self.green = g / 255
148            self.blue = b / 255
149        if self.alpha < 0 or self.alpha > 1:
150            self.alpha = self.alpha / 255
151        common_properties(self)
152
153    def __str__(self):
154        return f"Color({self.red}, {self.green}, {self.blue})"
155
156    def __repr__(self):
157        return f"Color({self.red}, {self.green}, {self.blue})"
158
159    def copy(self):
160        return Color(self.red, self.green, self.blue, self.alpha)
161
162    @property
163    def __key__(self):
164        return (self.red, self.green, self.blue)
165
166    def __hash__(self):
167        return hash(self.__key__)
168
169    @property
170    def name(self):
171        # search for the color in the named colors
172        pass
173
174    def __eq__(self, other):
175        if isinstance(other, Color):
176            return self.__key__ == other.__key__
177        else:
178            return False
179
180    @property
181    def rgb(self):
182        return (self.red, self.green, self.blue)
183
184    @property
185    def rgba(self):
186        return (self.red, self.green, self.blue, self.alpha)
187
188    @property
189    def rgb255(self):
190        r, g, b = self.rgb
191        if r > 1 or g > 1 or b > 1:
192            return (r, g, b)
193        return tuple(round(i * 255) for i in self.rgb)
194
195    @property
196    def rgba255(self):
197        return tuple(round(i * 255) for i in self.rgba)

A class representing an RGB or RGBA color.

This class represents a color in RGB or RGBA color space. The default values for the components are normalized between 0.0 and 1.0. Values outside this range are automatically converted from the 0-255 range.

Attributes:
  • red: The red component of the color (0.0 to 1.0).
  • green: The green component of the color (0.0 to 1.0).
  • blue: The blue component of the color (0.0 to 1.0).
  • alpha: The alpha (transparency) component (0.0 to 1.0), default is 1.
  • space: The color space, default is "rgb".
Examples:
>>> red = Color(1.0, 0.0, 0.0)
>>> transparent_blue = Color(0.0, 0.0, 1.0, 0.5)
>>> rgb255 = Color(255, 0, 128)  # Will be automatically normalized
Color( red: int = 0, green: int = 0, blue: int = 0, alpha: int = 1, space: simetri.graphics.all_enums.ColorSpace = 'rgb')
red: int = 0
green: int = 0
blue: int = 0
alpha: int = 1
def copy(self):
159    def copy(self):
160        return Color(self.red, self.green, self.blue, self.alpha)
name
169    @property
170    def name(self):
171        # search for the color in the named colors
172        pass
rgb
180    @property
181    def rgb(self):
182        return (self.red, self.green, self.blue)
rgba
184    @property
185    def rgba(self):
186        return (self.red, self.green, self.blue, self.alpha)
rgb255
188    @property
189    def rgb255(self):
190        r, g, b = self.rgb
191        if r > 1 or g > 1 or b > 1:
192            return (r, g, b)
193        return tuple(round(i * 255) for i in self.rgb)
rgba255
195    @property
196    def rgba255(self):
197        return tuple(round(i * 255) for i in self.rgba)
class Canvas:
  55class Canvas:
  56    """Canvas class for drawing shapes and text on a page. All drawing
  57    operations are handled by the Canvas class. Canvas class can draw all
  58    graphics objects and text objects. It also provides methods for
  59    drawing basic shapes like lines, circles, and polygons.
  60    """
  61
  62    def __init__(
  63        self,
  64        back_color: Optional[Color] = None,
  65        border: Optional[float] = None,
  66        size: Optional[Vec2] = None,
  67        **kwargs,
  68    ):
  69        """
  70        Initialize the Canvas.
  71
  72        Args:
  73            back_color (Optional[Color]): The background color of the canvas.
  74            border (Optional[float]): The border width of the canvas.
  75            size (Vec2, optional): The size of the canvas with canvas.origin at (0, 0).
  76            kwargs (dict): Additional keyword arguments. Rarely used.
  77
  78            You should not need to specify "size" since it is calculated.
  79        """
  80        validate_args(kwargs, canvas_args)
  81        _set_Nones(self, ["back_color", "border"], [back_color, border])
  82        self._size = size
  83        self.border = border
  84        self.type = Types.CANVAS
  85        self.subtype = Types.CANVAS
  86        self._code = []
  87        self._font_list = []
  88        self.preamble = defaults["preamble"]
  89        self.back_color = back_color
  90        self.pages = [Page(self.size, self.back_color, self.border)]
  91        self.active_page = self.pages[0]
  92        self._all_vertices = []
  93        self.blend_mode = None
  94        self.blend_group = False
  95        self.transparency_group = False
  96        self.alpha = None
  97        self.line_alpha = None
  98        self.fill_alpha = None
  99        self.text_alpha = None
 100        self.clip = None  # if True then clip the canvas to the mask
 101        self.mask = None  # Mask object
 102        self.even_odd_rule = None  # True or False
 103        self.draw_grid = False
 104        self._origin = [0, 0]
 105        common_properties(self)
 106
 107        for k, v in kwargs.items():
 108            setattr(self, k, v)
 109
 110        self._xform_matrix = identity_matrix()
 111        self._sketch_xform_matrix = identity_matrix()
 112        self.tex: Tex = Tex()
 113        self.render = defaults["render"]
 114        if self._size is not None:
 115            x, y = self.origin[:2]
 116            self._limits = [x, y, x + self.size[0], y + self.size[1]]
 117        else:
 118            self._limits = None
 119
 120    def __setattr__(self, name, value):
 121        if hasattr(self, "active_page") and name in ["back_color", "border"]:
 122            self.active_page.__setattr__(name, value)
 123            self.__dict__[name] = value
 124        elif name in ["size", "origin", "limits"]:
 125            if name == "size":
 126                type(self).size.fset(self, value)
 127            elif name == "origin":
 128                type(self).origin.fset(self, value)
 129            elif name == "limits":
 130                type(self).limits.fset(self, value)
 131        elif name == "scale":
 132            if isinstance(value, (list, tuple)):
 133                type(self).scale.fset(self, value[0], value[1])
 134            else:
 135                type(self).scale.fset(self, value)
 136        elif name == "pos":
 137            if isinstance(value, (list, tuple, np.ndarray)):
 138                type(self).pos.fset(self, value)
 139            else:
 140                raise ValueError("pos must be a list, tuple or ndarray.")
 141        elif name == "angle":
 142            if isinstance(value, (int, float)):
 143                type(self).angle.fset(self, value)
 144            else:
 145                raise ValueError("angle must be a number.")
 146
 147        else:
 148            self.__dict__[name] = value
 149
 150    def display(self) -> Self:
 151        """Show the canvas in a notebook cell."""
 152        display(self)
 153
 154    @property
 155    def size(self) -> Vec2:
 156        """
 157        The size of the canvas.
 158
 159        Returns:
 160            Vec2: The size of the canvas.
 161        """
 162        return self._size
 163
 164    @size.setter
 165    def size(self, value: Vec2) -> None:
 166        """
 167        Set the size of the canvas.
 168
 169        Args:
 170            value (Vec2): The size of the canvas.
 171        """
 172        if len(value) == 2:
 173            self._size = value
 174            x, y = self.origin[:2]
 175            w, h = value
 176            self._limits = (x, y, x + w, y + h)
 177        else:
 178            raise ValueError("Size must be a tuple of 2 values.")
 179
 180    @property
 181    def origin(self) -> Vec2:
 182        """
 183        The origin of the canvas.
 184
 185        Returns:
 186            Vec2: The origin of the canvas.
 187        """
 188        return self._origin[:2]
 189
 190    @origin.setter
 191    def origin(self, value: Vec2) -> None:
 192        """
 193        Set the origin of the canvas.
 194
 195        Args:
 196            value (Vec2): The origin of the canvas.
 197        """
 198        if len(value) == 2:
 199            self._origin = value
 200        else:
 201            raise ValueError("Origin must be a tuple of 2 values.")
 202
 203    @property
 204    def limits(self) -> Vec2:
 205        """
 206        The limits of the canvas.
 207
 208        Returns:
 209            Vec2: The limits of the canvas.
 210        """
 211        if self.size is None:
 212            res = None
 213        else:
 214            x, y = self.origin[:2]
 215            w, h = self.size
 216            res = (x, y, x + w, y + h)
 217
 218        return res
 219
 220    @limits.setter
 221    def limits(self, value: Vec2) -> None:
 222        """
 223        Set the limits of the canvas.
 224
 225        Args:
 226            value (Vec2): The limits of the canvas.
 227        """
 228        if len(value) == 4:
 229            x1, y1, x2, y2 = value
 230            self._size = (x2 - x1, y2 - y1)
 231            self._origin = (x1, y1)
 232        else:
 233            raise ValueError("Limits must be a tuple of 4 values.")
 234
 235    def insert_code(self, code, loc: TexLoc = TexLoc.PICTURE) -> Self:
 236        """
 237        Insert code into the canvas.
 238
 239        Args:
 240            code (str): The code to insert.
 241            loc (TexLoc): The location to insert the code.
 242
 243        Returns:
 244            Self: The canvas object.
 245        """
 246        draw.insert_code(self, code, loc)
 247        return self
 248
 249    def arc(
 250        self,
 251        center: Point,
 252        radius_x: float,
 253        radius_y: float = None,
 254        start_angle: float = 0,
 255        span_angle: float = pi / 2,
 256        rot_angle: float = 0,
 257        **kwargs,
 258    ) -> Self:
 259        """
 260        Draw an arc with the given center, radius, start angle and end angle.
 261
 262        Args:
 263            center (Point): The center of the arc.
 264            radius_x (float): The radius of the arc.
 265            radius_y (float, optional): The second radius of the arc, defaults to None.
 266            start_angle (float): The start angle of the arc.
 267            end_angle (float): The end angle of the arc.
 268            rot_angle (float, optional): The rotation angle of the arc, defaults to 0.
 269            kwargs (dict): Additional keyword arguments.
 270
 271        Returns:
 272            Self: The canvas object.
 273        """
 274        if radius_y is None:
 275            radius_y = radius_x
 276        draw.arc(
 277            self,
 278            center,
 279            radius_x,
 280            radius_y,
 281            start_angle,
 282            span_angle,
 283            rot_angle,
 284            **kwargs,
 285        )
 286        return self
 287
 288    def bezier(self, control_points: Sequence[Point], **kwargs) -> Self:
 289        """
 290        Draw a bezier curve.
 291
 292        Args:
 293            control_points (Sequence[Point]): The control points of the bezier curve.
 294            kwargs (dict): Additional keyword arguments.
 295
 296        Returns:
 297            Self: The canvas object.
 298        """
 299        draw.bezier(self, control_points, **kwargs)
 300        return self
 301
 302    def circle(self, center: Point, radius: float, **kwargs) -> Self:
 303        """
 304        Draw a circle with the given center and radius.
 305
 306        Args:
 307            center (Point): The center of the circle.
 308            radius (float): The radius of the circle.
 309            kwargs (dict): Additional keyword arguments.
 310
 311        Returns:
 312            Self: The canvas object.
 313        """
 314        draw.circle(self, center, radius, **kwargs)
 315        return self
 316
 317    def ellipse(
 318        self, center: Point, width: float, height: float, angle: float = 0, **kwargs
 319    ) -> Self:
 320        """
 321        Draw an ellipse with the given center and radius.
 322
 323        Args:
 324            center (Point): The center of the ellipse.
 325            width (float): The width of the ellipse.
 326            height (float): The height of the ellipse.
 327            angle (float, optional): The angle of the ellipse, defaults to 0.
 328            kwargs (dict): Additional keyword arguments.
 329
 330        Returns:
 331            Self: The canvas object.
 332        """
 333        draw.ellipse(self, center, width, height, angle, **kwargs)
 334        return self
 335
 336    def text(
 337        self,
 338        text: str,
 339        pos: Point,
 340        font_family: str = None,
 341        font_size: int = None,
 342        anchor: Anchor = None,
 343        align: Align = None,
 344        **kwargs,
 345    ) -> Self:
 346        """
 347        Draw text at the given point.
 348
 349        Args:
 350            text (str): The text to draw.
 351            pos (Point): The position to draw the text.
 352            font_family (str, optional): The font family of the text, defaults to None.
 353            font_size (int, optional): The font size of the text, defaults to None.
 354            anchor (Anchor, optional): The anchor of the text, defaults to None.
 355            anchor options: BASE, BASE_EAST, BASE_WEST, BOTTOM, CENTER, EAST, NORTH,
 356            NORTHEAST, NORTHWEST, SOUTH, SOUTHEAST, SOUTHWEST, WEST, MIDEAST, MIDWEST, RIGHT,
 357            LEFT, TOP
 358            align (Align, optional): The alignment of the text, defaults to Align.CENTER.
 359            align options: CENTER, FLUSH_CENTER, FLUSH_LEFT, FLUSH_RIGHT, JUSTIFY, LEFT, RIGHT
 360            kwargs (dict): Additional keyword arguments.
 361            common kwargs: fill_color, line_color, line_width, fill, line, alpha, font_color
 362
 363        Returns:
 364            Self: The canvas object.
 365        """
 366        pos = [pos[0], pos[1], 1]
 367        pos = pos @ self._xform_matrix
 368        draw.text(
 369            self,
 370            txt=text,
 371            pos=pos,
 372            font_family=font_family,
 373            font_size=font_size,
 374            anchor=anchor,
 375            **kwargs,
 376        )
 377        return self
 378
 379    def help_lines(
 380        self,
 381        pos=(-100, -100),
 382        width: float = 400,
 383        height: float = 400,
 384        spacing=25,
 385        cs_size: float = 25,
 386        **kwargs,
 387    ) -> Self:
 388        """
 389        Draw help lines on the canvas.
 390
 391        Args:
 392            pos (tuple): The position to start drawing the help lines.
 393            width (float): The length of the help lines along the x-axis.
 394            height (float): The length of the help lines along the y-axis.
 395            spacing (int): The spacing between the help lines.
 396            cs_size (float): The size of the coordinate system.
 397            kwargs (dict): Additional keyword arguments.
 398
 399        Returns:
 400            Self: The canvas object.
 401        """
 402        draw.help_lines(self, pos, width, height, spacing, cs_size, **kwargs)
 403        return self
 404
 405    def grid(
 406        self, pos: Point, width: float, height: float, spacing: float, **kwargs
 407    ) -> Self:
 408        """
 409        Draw a grid with the given size and spacing.
 410
 411        Args:
 412            pos (Point): The position to start drawing the grid.
 413            width (float): The length of the grid along the x-axis.
 414            height (float): The length of the grid along the y-axis.
 415            spacing (float): The spacing between the grid lines.
 416            kwargs (dict): Additional keyword arguments.
 417
 418        Returns:
 419            Self: The canvas object.
 420        """
 421        draw.grid(self, pos, width, height, spacing, **kwargs)
 422        return self
 423
 424    def line(self, start: Point, end: Point, **kwargs) -> Self:
 425        """
 426        Draw a line from start to end.
 427
 428        Args:
 429            start (Point): The starting point of the line.
 430            end (Point): The ending point of the line.
 431            kwargs (dict): Additional keyword arguments.
 432
 433        Returns:
 434            Self: The canvas object.
 435        """
 436        draw.line(self, start, end, **kwargs)
 437        return self
 438
 439    def rectangle(
 440        self,
 441        center: Point = (0, 0),
 442        width: float = 100,
 443        height: float = 100,
 444        angle: float = 0,
 445        **kwargs,
 446    ) -> Self:
 447        """
 448        Draw a rectangle.
 449
 450        Args:
 451            center (Point): The center of the rectangle.
 452            width (float): The width of the rectangle.
 453            height (float): The height of the rectangle.
 454            angle (float, optional): The angle of the rectangle, defaults to 0.
 455            kwargs (dict): Additional keyword arguments.
 456
 457        Returns:
 458            Self: The canvas object.
 459        """
 460        draw.rectangle(self, center, width, height, angle, **kwargs)
 461        return self
 462
 463    def square(
 464        self, center: Point = (0, 0), size: float = 100, angle: float = 0, **kwargs
 465    ) -> Self:
 466        """
 467        Draw a square with the given center and size.
 468
 469        Args:
 470            center (Point): The center of the square.
 471            size (float): The size of the square.
 472            angle (float, optional): The angle of the square, defaults to 0.
 473            kwargs (dict): Additional keyword arguments.
 474
 475        Returns:
 476            Self: The canvas object.
 477        """
 478        draw.rectangle(self, center, size, size, angle, **kwargs)
 479        return self
 480
 481    def lines(self, points: Sequence[Point], **kwargs) -> Self:
 482        """
 483        Draw a polyline through the given points.
 484
 485        Args:
 486            points (Sequence[Point]): The points to draw the polyline through.
 487            kwargs (dict): Additional keyword arguments.
 488
 489        Returns:
 490            Self: The canvas object.
 491        """
 492        draw.lines(self, points, **kwargs)
 493        return self
 494
 495    def draw_lace(self, lace: Batch, **kwargs) -> Self:
 496        """
 497        Draw the lace.
 498
 499        Args:
 500            lace (Batch): The lace to draw.
 501            kwargs (dict): Additional keyword arguments.
 502
 503        Returns:
 504            Self: The canvas object.
 505        """
 506        draw.draw_lace(self, lace, **kwargs)
 507        return self
 508
 509    def draw_dimension(self, dim: Shape, **kwargs) -> Self:
 510        """
 511        Draw the dimension.
 512
 513        Args:
 514            dim (Shape): The dimension to draw.
 515            kwargs (dict): Additional keyword arguments.
 516
 517        Returns:
 518            Self: The canvas object.
 519        """
 520        draw.draw_dimension(self, dim, **kwargs)
 521        return self
 522
 523    def draw(
 524        self,
 525        item_s: Union[Drawable, list, tuple],
 526        pos: Point = None,
 527        angle: float = 0,
 528        rotocenter: Point = (0, 0),
 529        scale=(1, 1),
 530        about=(0, 0),
 531        **kwargs,
 532    ) -> Self:
 533        """
 534        Draw the item_s. item_s can be a single item or a list of items.
 535
 536        Args:
 537            item_s (Union[Drawable, list, tuple]): The item(s) to draw.
 538            pos (Point, optional): The position to draw the item(s), defaults to None.
 539            angle (float, optional): The angle to rotate the item(s), defaults to 0.
 540            rotocenter (Point, optional): The point about which to rotate, defaults to (0, 0).
 541            scale (tuple, optional): The scale factors for the x and y axes, defaults to (1, 1).
 542            about (tuple, optional): The point about which to scale, defaults to (0, 0).
 543            kwargs (dict): Additional keyword arguments.
 544
 545        Returns:
 546            Self: The canvas object.
 547        """
 548        sketch_xform = self._sketch_xform_matrix
 549        if pos is not None:
 550            sketch_xform = translation_matrix(*pos[:2]) @ sketch_xform
 551        if scale[0] != 1 or scale[1] != 1:
 552            sketch_xform = scale_in_place_matrix(*pos[:2], about) @ sketch_xform
 553        if angle != 0:
 554            sketch_xform = rotation_matrix(angle, rotocenter) @ sketch_xform
 555        self._sketch_xform_matrix = sketch_xform @ self._xform_matrix
 556
 557        if isinstance(item_s, (list, tuple)):
 558            for item in item_s:
 559                draw.draw(self, item, **kwargs)
 560        else:
 561            draw.draw(self, item_s, **kwargs)
 562
 563        self._sketch_xform_matrix = identity_matrix()
 564
 565        return self
 566
 567    def draw_CS(self, size: float = None, **kwargs) -> Self:
 568        """
 569        Draw the Canvas coordinate system.
 570
 571        Args:
 572            size (float, optional): The size of the coordinate system, defaults to None.
 573            kwargs (dict): Additional keyword arguments.
 574
 575        Returns:
 576            Self: The canvas object.
 577        """
 578        draw.draw_CS(self, size, **kwargs)
 579        return self
 580
 581    def draw_frame(
 582        self, margin: Union[float, Sequence] = None, width=None, **kwargs
 583    ) -> Self:
 584        """
 585        Draw a frame around the canvas.
 586
 587        Args:
 588            margins (Union[float, Sequence]): The margins of the frame.
 589            kwargs (dict): Additional keyword arguments.
 590
 591        Returns:
 592            Self: The canvas object.
 593        """
 594        # to do: add shadow and frame color, shadow width. Check canvas.clip.
 595        if margin is None:
 596            margin = defaults["canvas_frame_margin"]
 597        if width is None:
 598            width = defaults["canvas_frame_width"]
 599        b_box = bounding_box(self._all_vertices)
 600        box2 = b_box.get_inflated_b_box(margin)
 601        box3 = box2.get_inflated_b_box(15)
 602        shadow = Shape([box3.northwest, box3.southwest, box3.southeast])
 603        self.draw(shadow, line_color=colors.light_gray, line_width=width)
 604        self.draw(Shape(box2.corners, closed=True), fill=False, line_width=width)
 605
 606        return self
 607
 608    def reset(self) -> Self:
 609        """
 610        Reset the canvas to its initial state.
 611
 612        Returns:
 613            Self: The canvas object.
 614        """
 615        self._code = []
 616        self.preamble = defaults["preamble"]
 617        self.pages = [Page(self.size, self.back_color, self.border)]
 618        self.active_page = self.pages[0]
 619        self._all_vertices = []
 620        self.tex: Tex = Tex()
 621        self.clip: bool = False  # if true then clip the canvas to the mask
 622        self._xform_matrix = identity_matrix()
 623        self._sketch_xform_matrix = identity_matrix()
 624        self.active_page = self.pages[0]
 625        self._all_vertices = []
 626        return self
 627
 628    def __str__(self) -> str:
 629        """
 630        Return a string representation of the canvas.
 631
 632        Returns:
 633            str: The string representation of the canvas.
 634        """
 635        return "Canvas()"
 636
 637    def __repr__(self) -> str:
 638        """
 639        Return a string representation of the canvas.
 640
 641        Returns:
 642            str: The string representation of the canvas.
 643        """
 644        return "Canvas()"
 645
 646    @property
 647    def pos(self) -> Point:
 648        """
 649        The position of the canvas.
 650
 651        Args:
 652            point (Point, optional): The point to set the position to.
 653
 654        Returns:
 655            Point: The position of the canvas.
 656        """
 657
 658        return self._xform_matrix[2, :2].tolist()[:2]
 659
 660    @pos.setter
 661    def pos(self, point: Point) -> None:
 662        """
 663        Set the position of the canvas.
 664
 665        Args:
 666            point (Point): The point to set the position to.
 667        """
 668        self._xform_matrix[2, :2] = point[:2]
 669
 670    @property
 671    def angle(self) -> float:
 672        """
 673        The angle of the canvas.
 674
 675        Returns:
 676            float: The angle of the canvas.
 677        """
 678        xform = self._xform_matrix
 679
 680        return np.arctan2(xform[0, 1], xform[0, 0])
 681
 682    @angle.setter
 683    def angle(self, angle: float) -> None:
 684        """
 685        Set the angle of the canvas.
 686
 687        Args:
 688            angle (float): The angle to set the canvas to.
 689        """
 690        self._xform_matrix = rotation_matrix(angle) @ self._xform_matrix
 691
 692    @property
 693    def scale(self) -> Vec2:
 694        """
 695        The scale of the canvas.
 696
 697        Returns:
 698            Vec2: The scale of the canvas.
 699        """
 700        xform = self._xform_matrix
 701
 702        return np.linalg.norm(xform[:2, 0]), np.linalg.norm(xform[:2, 1])
 703
 704    @scale.setter
 705    def scale(
 706        self, scale_x: float = 1, scale_y: float = None, about: Point = (0, 0)
 707    ) -> None:
 708        """
 709        Set the scale of the canvas.
 710
 711        Args:
 712            scale_x (float): The x-scale to set the canvas to.
 713            scale_y (float): The y-scale to set the canvas to.
 714            about (Point): The point about which to scale the canvas.
 715        """
 716        if scale_y is None:
 717            scale_y = scale_x
 718
 719        self._xform_matrix = (
 720            scale_in_place_matrix(scale_x, scale_y, about=about) @ self._xform_matrix
 721        )
 722
 723    @property
 724    def xform_matrix(self) -> "ndarray":
 725        """
 726        The transformation matrix of the canvas.
 727
 728        Returns:
 729            ndarray: The transformation matrix of the canvas.
 730        """
 731        return self._xform_matrix.copy()
 732
 733    def transform(self, transform_matrix: "ndarray") -> Self:
 734        """
 735        Transforms the canvas by the given transformation matrix.
 736
 737        Args:
 738            transform_matrix (ndarray): The transformation matrix.
 739
 740        Returns:
 741            Self: The Canvas object.
 742        """
 743        self._xform_matrix = transform_matrix @ self._xform_matrix
 744
 745        return self
 746
 747    def reset_transform(self) -> Self:
 748        """
 749        Reset the transformation matrix of the canvas.
 750        The canvas origin is at (0, 0) and the orientation angle is 0.
 751        Transformation matrix is the identity matrix.
 752
 753        Returns:
 754            Self: The canvas object.
 755        """
 756        self._xform_matrix = identity_matrix()
 757
 758        return self
 759
 760    def translate(self, x: float, y: float) -> Self:
 761        """
 762        Translate the canvas by x and y.
 763
 764        Args:
 765            x (float): The translation distance along the x-axis.
 766            y (float): The translation distance along the y-axis.
 767
 768        Returns:
 769            Self: The canvas object.
 770        """
 771
 772        self._xform_matrix = translation_matrix(x, y) @ self._xform_matrix
 773
 774        return self
 775
 776    def rotate(self, angle: float, about=(0, 0)) -> Self:
 777        """
 778        Rotate the canvas by angle in radians about the given point.
 779
 780        Args:
 781            angle (float): The rotation angle in radians.
 782            about (tuple): The point about which to rotate the canvas.
 783
 784        Returns:
 785            Self: The canvas object.
 786        """
 787
 788        self._xform_matrix = rotation_matrix(angle, about) @ self._xform_matrix
 789
 790        return self
 791
 792    def _flip(self, axis: Axis) -> Self:
 793        """
 794        Flip the canvas along the specified axis.
 795
 796        Args:
 797            axis (str): The axis to flip the canvas along ('x' or 'y').
 798
 799        Returns:
 800            Self: The canvas object.
 801        """
 802        if axis == Axis.X:
 803            sx = -self.scale[0]
 804            sy = 1
 805        elif axis == Axis.Y:
 806            sx = 1
 807            sy = -self.scale[1]
 808
 809        self._xform_matrix = scale_matrix(sx, sy) @ self._xform_matrix
 810
 811        return self
 812
 813    def flip_x_axis(self) -> Self:
 814        """
 815        Flip the x-axis direction. Warning: This will reverse the positive rotation direction.
 816
 817        Returns:
 818            Self: The canvas object.
 819        """
 820        warnings.warn(
 821            "Flipping the x-axis will change the positive rotation direction."
 822        )
 823        return self._flip(Axis.X)
 824
 825    def flip_y_axis(self) -> Self:
 826        """
 827        Flip the y-axis direction.
 828
 829        Returns:
 830            Self: The canvas object.
 831        """
 832        warnings.warn(
 833            "Flipping the y-axis will reverse the positive rotation direction."
 834        )
 835
 836        return self._flip(Axis.Y)
 837
 838    @property
 839    def x(self) -> float:
 840        """
 841        The x coordinate of the canvas origin.
 842
 843        Returns:
 844            float: The x coordinate of the canvas origin.
 845        """
 846        return self.pos[0]
 847
 848    @x.setter
 849    def x(self, value: float) -> None:
 850        """
 851        Set the x coordinate of the canvas origin.
 852
 853        Args:
 854            value (float): The x coordinate to set.
 855        """
 856        self.pos = [value, self.pos[1]]
 857
 858    @property
 859    def y(self) -> float:
 860        """
 861        The y coordinate of the canvas origin.
 862
 863        Returns:
 864            float: The y coordinate of the canvas origin.
 865        """
 866        return self.pos[1]
 867
 868    @y.setter
 869    def y(self, value: float) -> None:
 870        """
 871        Set the y coordinate of the canvas origin.
 872
 873        Args:
 874            value (float): The y coordinate to set.
 875        """
 876        self.pos = [self.pos[0], value]
 877
 878    def batch_graph(self, batch: "Batch") -> nx.DiGraph:
 879        """
 880        Return a directed graph of the batch and its elements.
 881        Canvas is the root of the graph.
 882        Graph nodes are the ids of the elements.
 883
 884        Args:
 885            batch (Batch): The batch to create the graph from.
 886
 887        Returns:
 888            nx.DiGraph: The directed graph of the batch and its elements.
 889        """
 890
 891        def add_batch(batch, graph):
 892            graph.add_node(batch.id)
 893            for item in batch.elements:
 894                graph.add_edge(batch.id, item.id)
 895                if item.subtype == Types.BATCH:
 896                    add_batch(item, graph)
 897            return graph
 898
 899        di_graph = nx.DiGraph()
 900        di_graph.add_edge(self.id, batch.id)
 901        for item in batch.elements:
 902            if item.subtype == Types.BATCH:
 903                di_graph.add_edge(batch.id, item.id)
 904                add_batch(item, di_graph)
 905            else:
 906                di_graph.add_edge(batch.id, item.id)
 907
 908        return di_graph
 909
 910    def _resolve_property(self, item: Drawable, property_name: str) -> Any:
 911        """
 912        Handles None values for properties.
 913        try item.property_name first,
 914        then try canvas.property_name,
 915        finally use the default value.
 916
 917        Args:
 918            item (Drawable): The item to resolve the property for.
 919            property_name (str): The name of the property to resolve.
 920
 921        Returns:
 922            Any: The resolved property value.
 923        """
 924        value = getattr(item, property_name)
 925        if value is None:
 926            value = self.__dict__.get(property_name, None)
 927            if value is None:
 928                value = defaults.get(property_name, VOID)
 929            if value == VOID:
 930                print(f"Property {property_name} is not in defaults.")
 931                value = None
 932        return value
 933
 934    def get_fonts_list(self) -> list[str]:
 935        """
 936        Get the list of fonts used in the canvas.
 937
 938        Returns:
 939            list[str]: The list of fonts used in the canvas.
 940        """
 941        user_fonts = set(self._font_list)
 942
 943        latex_fonts = set(
 944            [
 945                defaults["main_font"],
 946                defaults["sans_font"],
 947                defaults["mono_font"],
 948                "serif",
 949                "sansserif",
 950                "monospace",
 951            ]
 952        )
 953        for sketch in self.active_page.sketches:
 954            if sketch.subtype == Types.TAG_SKETCH:
 955                name = sketch.font_family
 956                if name is not None and name not in latex_fonts:
 957                    user_fonts.add(name)
 958        return list(user_fonts.difference(latex_fonts))
 959
 960    def _calculate_size(self, border=None, b_box=None) -> Tuple[float, float]:
 961        """
 962        Calculate the size of the canvas based on the bounding box and border.
 963
 964        Args:
 965            border (float, optional): The border of the canvas, defaults to None.
 966            b_box (Any, optional): The bounding box of the canvas, defaults to None.
 967
 968        Returns:
 969            Tuple[float, float]: The size of the canvas.
 970        """
 971        vertices = self._all_vertices
 972        if vertices:
 973            if b_box is None:
 974                b_box = bounding_box(vertices)
 975
 976            if border is None:
 977                if self.border is None:
 978                    border = defaults["border"]
 979                else:
 980                    border = self.border
 981            w = b_box.width + 2 * border
 982            h = b_box.height + 2 * border
 983            offset_x, offset_y = b_box.southwest
 984            res = w, h, offset_x - border, offset_y - border
 985        else:
 986            res = None
 987        return res
 988
 989    def _show_browser(
 990        self, file_path: Path, show_browser: bool, multi_page_svg: bool
 991    ) -> None:
 992        """
 993        Show the file in the browser.
 994
 995        Args:
 996            file_path (Path): The path to the file.
 997            show_browser (bool): Whether to show the file in the browser.
 998            multi_page_svg (bool): Whether the file is a multi-page SVG.
 999        """
1000        if show_browser is None:
1001            show_browser = defaults["show_browser"]
1002        if show_browser:
1003            file_path = "file:///" + file_path
1004            if multi_page_svg:
1005                for i, _ in enumerate(self.pages):
1006                    f_path = file_path.replace(".svg", f"_page{i + 1}.svg")
1007                    webbrowser.open(f_path)
1008            else:
1009                webbrowser.open(file_path)
1010
1011    def save(
1012        self,
1013        file_path: Path = None,
1014        overwrite: bool = None,
1015        show: bool = None,
1016        print_output=False,
1017    ) -> Self:
1018        """
1019        Save the canvas to a file.
1020
1021        Args:
1022            file_path (Path, optional): The path to save the file.
1023            overwrite (bool, optional): Whether to overwrite the file if it exists.
1024            show (bool, optional): Whether to show the file in the browser.
1025            print_output (bool, optional): Whether to print the output of the compilation.
1026
1027        Returns:
1028            Self: The canvas object.
1029        """
1030
1031        def validate_file_path(file_path: Path, overwrite: bool) -> Result:
1032            """
1033            Validate the file path.
1034
1035            Args:
1036                file_path (Path): The path to the file.
1037                overwrite (bool): Whether to overwrite the file if it exists.
1038
1039            Returns:
1040                Result: The parent directory, file name, and extension.
1041            """
1042            path_exists = os.path.exists(file_path)
1043            if path_exists and not overwrite:
1044                raise FileExistsError(
1045                    f"File {file_path} already exists. \n"
1046                    "Use canvas.save(file_path, overwrite=True) to overwrite the file."
1047                )
1048            parent_dir, file_name = os.path.split(file_path)
1049            file_name, extension = os.path.splitext(file_name)
1050            if extension not in [".pdf", ".eps", ".ps", ".svg", ".png", ".tex"]:
1051                raise RuntimeError("File type is not supported.")
1052            if not os.path.exists(parent_dir):
1053                raise NotADirectoryError(f"Directory {parent_dir} does not exist.")
1054            if not os.access(parent_dir, os.W_OK):
1055                raise PermissionError(f"Directory {parent_dir} is not writable.")
1056
1057            return parent_dir, file_name, extension
1058
1059        def compile_tex(cmd):
1060            """
1061            Compile the TeX file.
1062
1063            Args:
1064                cmd (str): The command to compile the TeX file.
1065
1066            Returns:
1067                str: The output of the compilation.
1068            """
1069            os.chdir(parent_dir)
1070            with subprocess.Popen(
1071                cmd,
1072                stdin=subprocess.PIPE,
1073                stdout=subprocess.PIPE,
1074                shell=True,
1075                text=True,
1076            ) as p:
1077                output = p.communicate("_s\n_l\n")[0]
1078            if print_output:
1079                print(output.split("\n")[-3:])
1080            return output
1081
1082        def remove_aux_files(file_path):
1083            """
1084            Remove auxiliary files generated during compilation.
1085
1086            Args:
1087                file_path (Path): The path to the file.
1088            """
1089            time_out = 1  # seconds
1090            parent_dir, file_name = os.path.split(file_path)
1091            file_name, extension = os.path.splitext(file_name)
1092            aux_file = os.path.join(parent_dir, file_name + ".aux")
1093            if os.path.exists(aux_file):
1094                if not wait_for_file_availability(aux_file, time_out):
1095                    print(
1096                        (
1097                            f"File '{aux_file}' is not available after waiting for "
1098                            f"{time_out} seconds."
1099                        )
1100                    )
1101                else:
1102                    os.remove(aux_file)
1103            log_file = os.path.join(parent_dir, file_name + ".log")
1104            if os.path.exists(log_file):
1105                if not wait_for_file_availability(log_file, time_out):
1106                    print(
1107                        (
1108                            f"File '{log_file}' is not available after waiting for "
1109                            f"{time_out} seconds."
1110                        )
1111                    )
1112                else:
1113                    if not defaults["keep_log_files"]:
1114                        os.remove(log_file)
1115            tex_file = os.path.join(parent_dir, file_name + ".tex")
1116            if os.path.exists(tex_file):
1117                if not wait_for_file_availability(tex_file, time_out):
1118                    print(
1119                        (
1120                            f"File '{tex_file}' is not available after waiting for "
1121                            f"{time_out} seconds."
1122                        )
1123                    )
1124                else:
1125                    os.remove(tex_file)
1126            file_name, extension = os.path.splitext(file_name)
1127            if extension not in [".pdf", ".tex"]:
1128                pdf_file = os.path.join(parent_dir, file_name + ".pdf")
1129                if os.path.exists(pdf_file):
1130                    if not wait_for_file_availability(pdf_file, time_out):
1131                        print(
1132                            (
1133                                f"File '{pdf_file}' is not available after waiting for "
1134                                f"{time_out} seconds."
1135                            )
1136                        )
1137                    else:
1138                        # os.remove(pdf_file)
1139                        pass
1140            log_file = os.path.join(parent_dir, "simetri.log")
1141            if os.path.exists(log_file):
1142                try:
1143                    os.remove(log_file)
1144                except PermissionError:
1145                    # to do: log the error
1146                    pass
1147
1148        def run_job():
1149            """
1150            Run the job to compile and save the file.
1151
1152            Returns:
1153                None
1154            """
1155            output_path = os.path.join(parent_dir, file_name + extension)
1156            cmd = "xelatex " + tex_path + " --output-directory " + parent_dir
1157            res = compile_tex(cmd)
1158            if "No pages of output" in res:
1159                raise RuntimeError("Failed to compile the tex file.")
1160            pdf_path = os.path.join(parent_dir, file_name + ".pdf")
1161            if not os.path.exists(pdf_path):
1162                raise RuntimeError("Failed to compile the tex file.")
1163
1164            if extension in [".eps", ".ps"]:
1165                ps_path = os.path.join(parent_dir, file_name + extension)
1166                os.chdir(parent_dir)
1167                cmd = f"pdf2ps {pdf_path} {ps_path}"
1168                res = subprocess.run(cmd, shell=True, check=False)
1169                if res.returncode != 0:
1170                    raise RuntimeError("Failed to convert pdf to ps.")
1171            elif extension == ".svg":
1172                doc = fitz.open(pdf_path)
1173                page = doc.load_page(0)
1174                svg = page.get_svg_image()
1175                with open(output_path, "w", encoding="utf-8") as f:
1176                    f.write(svg)
1177            elif extension == ".png":
1178                pdf_file = fitz.open(pdf_path)
1179                page = pdf_file[0]
1180                pix = page.get_pixmap()
1181                pix.save(output_path)
1182                pdf_file.close()
1183
1184        parent_dir, file_name, extension = validate_file_path(file_path, overwrite)
1185
1186        tex_code = get_tex_code(self)
1187        tex_path = os.path.join(parent_dir, file_name + ".tex")
1188        with open(tex_path, "w", encoding="utf-8") as f:
1189            f.write(tex_code)
1190        if extension == ".tex":
1191            return self
1192
1193        run_job()
1194        remove_aux_files(file_path)
1195
1196        self._show_browser(file_path=file_path, show_browser=show, multi_page_svg=False)
1197        return self
1198
1199    def new_page(self, **kwargs) -> Self:
1200        """
1201        Create a new page and add it to the canvas.pages.
1202
1203        Args:
1204            kwargs (dict): Additional keyword arguments.
1205
1206        Returns:
1207            Self: The canvas object.
1208        """
1209        page = Page()
1210        self.pages.append(page)
1211        self.active_page = page
1212        for k, v in kwargs.items():
1213            setattr(page, k, v)
1214        return self

Canvas class for drawing shapes and text on a page. All drawing operations are handled by the Canvas class. Canvas class can draw all graphics objects and text objects. It also provides methods for drawing basic shapes like lines, circles, and polygons.

Canvas( back_color: Optional[Color] = None, border: Optional[float] = None, size: Optional[Tuple[float, float]] = None, **kwargs)
 62    def __init__(
 63        self,
 64        back_color: Optional[Color] = None,
 65        border: Optional[float] = None,
 66        size: Optional[Vec2] = None,
 67        **kwargs,
 68    ):
 69        """
 70        Initialize the Canvas.
 71
 72        Args:
 73            back_color (Optional[Color]): The background color of the canvas.
 74            border (Optional[float]): The border width of the canvas.
 75            size (Vec2, optional): The size of the canvas with canvas.origin at (0, 0).
 76            kwargs (dict): Additional keyword arguments. Rarely used.
 77
 78            You should not need to specify "size" since it is calculated.
 79        """
 80        validate_args(kwargs, canvas_args)
 81        _set_Nones(self, ["back_color", "border"], [back_color, border])
 82        self._size = size
 83        self.border = border
 84        self.type = Types.CANVAS
 85        self.subtype = Types.CANVAS
 86        self._code = []
 87        self._font_list = []
 88        self.preamble = defaults["preamble"]
 89        self.back_color = back_color
 90        self.pages = [Page(self.size, self.back_color, self.border)]
 91        self.active_page = self.pages[0]
 92        self._all_vertices = []
 93        self.blend_mode = None
 94        self.blend_group = False
 95        self.transparency_group = False
 96        self.alpha = None
 97        self.line_alpha = None
 98        self.fill_alpha = None
 99        self.text_alpha = None
100        self.clip = None  # if True then clip the canvas to the mask
101        self.mask = None  # Mask object
102        self.even_odd_rule = None  # True or False
103        self.draw_grid = False
104        self._origin = [0, 0]
105        common_properties(self)
106
107        for k, v in kwargs.items():
108            setattr(self, k, v)
109
110        self._xform_matrix = identity_matrix()
111        self._sketch_xform_matrix = identity_matrix()
112        self.tex: Tex = Tex()
113        self.render = defaults["render"]
114        if self._size is not None:
115            x, y = self.origin[:2]
116            self._limits = [x, y, x + self.size[0], y + self.size[1]]
117        else:
118            self._limits = None

Initialize the Canvas.

Arguments:
  • back_color (Optional[Color]): The background color of the canvas.
  • border (Optional[float]): The border width of the canvas.
  • size (Vec2, optional): The size of the canvas with canvas.origin at (0, 0).
  • kwargs (dict): Additional keyword arguments. Rarely used.
  • You should not need to specify "size" since it is calculated.
border
type
subtype
preamble
back_color
pages
active_page
blend_mode
blend_group
transparency_group
alpha
line_alpha
fill_alpha
text_alpha
clip
mask
even_odd_rule
draw_grid
render
def display(self) -> typing_extensions.Self:
150    def display(self) -> Self:
151        """Show the canvas in a notebook cell."""
152        display(self)

Show the canvas in a notebook cell.

size: Tuple[float, float]
154    @property
155    def size(self) -> Vec2:
156        """
157        The size of the canvas.
158
159        Returns:
160            Vec2: The size of the canvas.
161        """
162        return self._size

The size of the canvas.

Returns:

Vec2: The size of the canvas.

origin: Tuple[float, float]
180    @property
181    def origin(self) -> Vec2:
182        """
183        The origin of the canvas.
184
185        Returns:
186            Vec2: The origin of the canvas.
187        """
188        return self._origin[:2]

The origin of the canvas.

Returns:

Vec2: The origin of the canvas.

limits: Tuple[float, float]
203    @property
204    def limits(self) -> Vec2:
205        """
206        The limits of the canvas.
207
208        Returns:
209            Vec2: The limits of the canvas.
210        """
211        if self.size is None:
212            res = None
213        else:
214            x, y = self.origin[:2]
215            w, h = self.size
216            res = (x, y, x + w, y + h)
217
218        return res

The limits of the canvas.

Returns:

Vec2: The limits of the canvas.

def insert_code( self, code, loc: simetri.graphics.all_enums.TexLoc = <TexLoc.PICTURE: 'PICTURE'>) -> typing_extensions.Self:
235    def insert_code(self, code, loc: TexLoc = TexLoc.PICTURE) -> Self:
236        """
237        Insert code into the canvas.
238
239        Args:
240            code (str): The code to insert.
241            loc (TexLoc): The location to insert the code.
242
243        Returns:
244            Self: The canvas object.
245        """
246        draw.insert_code(self, code, loc)
247        return self

Insert code into the canvas.

Arguments:
  • code (str): The code to insert.
  • loc (TexLoc): The location to insert the code.
Returns:

Self: The canvas object.

def arc( self, center: Sequence[float], radius_x: float, radius_y: float = None, start_angle: float = 0, span_angle: float = 1.5707963267948966, rot_angle: float = 0, **kwargs) -> typing_extensions.Self:
249    def arc(
250        self,
251        center: Point,
252        radius_x: float,
253        radius_y: float = None,
254        start_angle: float = 0,
255        span_angle: float = pi / 2,
256        rot_angle: float = 0,
257        **kwargs,
258    ) -> Self:
259        """
260        Draw an arc with the given center, radius, start angle and end angle.
261
262        Args:
263            center (Point): The center of the arc.
264            radius_x (float): The radius of the arc.
265            radius_y (float, optional): The second radius of the arc, defaults to None.
266            start_angle (float): The start angle of the arc.
267            end_angle (float): The end angle of the arc.
268            rot_angle (float, optional): The rotation angle of the arc, defaults to 0.
269            kwargs (dict): Additional keyword arguments.
270
271        Returns:
272            Self: The canvas object.
273        """
274        if radius_y is None:
275            radius_y = radius_x
276        draw.arc(
277            self,
278            center,
279            radius_x,
280            radius_y,
281            start_angle,
282            span_angle,
283            rot_angle,
284            **kwargs,
285        )
286        return self

Draw an arc with the given center, radius, start angle and end angle.

Arguments:
  • center (Point): The center of the arc.
  • radius_x (float): The radius of the arc.
  • radius_y (float, optional): The second radius of the arc, defaults to None.
  • start_angle (float): The start angle of the arc.
  • end_angle (float): The end angle of the arc.
  • rot_angle (float, optional): The rotation angle of the arc, defaults to 0.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def bezier( self, control_points: Sequence[Sequence[float]], **kwargs) -> typing_extensions.Self:
288    def bezier(self, control_points: Sequence[Point], **kwargs) -> Self:
289        """
290        Draw a bezier curve.
291
292        Args:
293            control_points (Sequence[Point]): The control points of the bezier curve.
294            kwargs (dict): Additional keyword arguments.
295
296        Returns:
297            Self: The canvas object.
298        """
299        draw.bezier(self, control_points, **kwargs)
300        return self

Draw a bezier curve.

Arguments:
  • control_points (Sequence[Point]): The control points of the bezier curve.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def circle( self, center: Sequence[float], radius: float, **kwargs) -> typing_extensions.Self:
302    def circle(self, center: Point, radius: float, **kwargs) -> Self:
303        """
304        Draw a circle with the given center and radius.
305
306        Args:
307            center (Point): The center of the circle.
308            radius (float): The radius of the circle.
309            kwargs (dict): Additional keyword arguments.
310
311        Returns:
312            Self: The canvas object.
313        """
314        draw.circle(self, center, radius, **kwargs)
315        return self

Draw a circle with the given center and radius.

Arguments:
  • center (Point): The center of the circle.
  • radius (float): The radius of the circle.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def ellipse( self, center: Sequence[float], width: float, height: float, angle: float = 0, **kwargs) -> typing_extensions.Self:
317    def ellipse(
318        self, center: Point, width: float, height: float, angle: float = 0, **kwargs
319    ) -> Self:
320        """
321        Draw an ellipse with the given center and radius.
322
323        Args:
324            center (Point): The center of the ellipse.
325            width (float): The width of the ellipse.
326            height (float): The height of the ellipse.
327            angle (float, optional): The angle of the ellipse, defaults to 0.
328            kwargs (dict): Additional keyword arguments.
329
330        Returns:
331            Self: The canvas object.
332        """
333        draw.ellipse(self, center, width, height, angle, **kwargs)
334        return self

Draw an ellipse with the given center and radius.

Arguments:
  • center (Point): The center of the ellipse.
  • width (float): The width of the ellipse.
  • height (float): The height of the ellipse.
  • angle (float, optional): The angle of the ellipse, defaults to 0.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def text( self, text: str, pos: Sequence[float], font_family: str = None, font_size: int = None, anchor: simetri.graphics.all_enums.Anchor = None, align: simetri.graphics.all_enums.Align = None, **kwargs) -> typing_extensions.Self:
336    def text(
337        self,
338        text: str,
339        pos: Point,
340        font_family: str = None,
341        font_size: int = None,
342        anchor: Anchor = None,
343        align: Align = None,
344        **kwargs,
345    ) -> Self:
346        """
347        Draw text at the given point.
348
349        Args:
350            text (str): The text to draw.
351            pos (Point): The position to draw the text.
352            font_family (str, optional): The font family of the text, defaults to None.
353            font_size (int, optional): The font size of the text, defaults to None.
354            anchor (Anchor, optional): The anchor of the text, defaults to None.
355            anchor options: BASE, BASE_EAST, BASE_WEST, BOTTOM, CENTER, EAST, NORTH,
356            NORTHEAST, NORTHWEST, SOUTH, SOUTHEAST, SOUTHWEST, WEST, MIDEAST, MIDWEST, RIGHT,
357            LEFT, TOP
358            align (Align, optional): The alignment of the text, defaults to Align.CENTER.
359            align options: CENTER, FLUSH_CENTER, FLUSH_LEFT, FLUSH_RIGHT, JUSTIFY, LEFT, RIGHT
360            kwargs (dict): Additional keyword arguments.
361            common kwargs: fill_color, line_color, line_width, fill, line, alpha, font_color
362
363        Returns:
364            Self: The canvas object.
365        """
366        pos = [pos[0], pos[1], 1]
367        pos = pos @ self._xform_matrix
368        draw.text(
369            self,
370            txt=text,
371            pos=pos,
372            font_family=font_family,
373            font_size=font_size,
374            anchor=anchor,
375            **kwargs,
376        )
377        return self

Draw text at the given point.

Arguments:
  • text (str): The text to draw.
  • pos (Point): The position to draw the text.
  • font_family (str, optional): The font family of the text, defaults to None.
  • font_size (int, optional): The font size of the text, defaults to None.
  • anchor (Anchor, optional): The anchor of the text, defaults to None.
  • anchor options: BASE, BASE_EAST, BASE_WEST, BOTTOM, CENTER, EAST, NORTH,
  • NORTHEAST, NORTHWEST, SOUTH, SOUTHEAST, SOUTHWEST, WEST, MIDEAST, MIDWEST, RIGHT,
  • LEFT, TOP
  • align (Align, optional): The alignment of the text, defaults to Align.CENTER.
  • align options: CENTER, FLUSH_CENTER, FLUSH_LEFT, FLUSH_RIGHT, JUSTIFY, LEFT, RIGHT
  • kwargs (dict): Additional keyword arguments.
  • common kwargs: fill_color, line_color, line_width, fill, line, alpha, font_color
Returns:

Self: The canvas object.

def help_lines( self, pos=(-100, -100), width: float = 400, height: float = 400, spacing=25, cs_size: float = 25, **kwargs) -> typing_extensions.Self:
379    def help_lines(
380        self,
381        pos=(-100, -100),
382        width: float = 400,
383        height: float = 400,
384        spacing=25,
385        cs_size: float = 25,
386        **kwargs,
387    ) -> Self:
388        """
389        Draw help lines on the canvas.
390
391        Args:
392            pos (tuple): The position to start drawing the help lines.
393            width (float): The length of the help lines along the x-axis.
394            height (float): The length of the help lines along the y-axis.
395            spacing (int): The spacing between the help lines.
396            cs_size (float): The size of the coordinate system.
397            kwargs (dict): Additional keyword arguments.
398
399        Returns:
400            Self: The canvas object.
401        """
402        draw.help_lines(self, pos, width, height, spacing, cs_size, **kwargs)
403        return self

Draw help lines on the canvas.

Arguments:
  • pos (tuple): The position to start drawing the help lines.
  • width (float): The length of the help lines along the x-axis.
  • height (float): The length of the help lines along the y-axis.
  • spacing (int): The spacing between the help lines.
  • cs_size (float): The size of the coordinate system.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def grid( self, pos: Sequence[float], width: float, height: float, spacing: float, **kwargs) -> typing_extensions.Self:
405    def grid(
406        self, pos: Point, width: float, height: float, spacing: float, **kwargs
407    ) -> Self:
408        """
409        Draw a grid with the given size and spacing.
410
411        Args:
412            pos (Point): The position to start drawing the grid.
413            width (float): The length of the grid along the x-axis.
414            height (float): The length of the grid along the y-axis.
415            spacing (float): The spacing between the grid lines.
416            kwargs (dict): Additional keyword arguments.
417
418        Returns:
419            Self: The canvas object.
420        """
421        draw.grid(self, pos, width, height, spacing, **kwargs)
422        return self

Draw a grid with the given size and spacing.

Arguments:
  • pos (Point): The position to start drawing the grid.
  • width (float): The length of the grid along the x-axis.
  • height (float): The length of the grid along the y-axis.
  • spacing (float): The spacing between the grid lines.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def line( self, start: Sequence[float], end: Sequence[float], **kwargs) -> typing_extensions.Self:
424    def line(self, start: Point, end: Point, **kwargs) -> Self:
425        """
426        Draw a line from start to end.
427
428        Args:
429            start (Point): The starting point of the line.
430            end (Point): The ending point of the line.
431            kwargs (dict): Additional keyword arguments.
432
433        Returns:
434            Self: The canvas object.
435        """
436        draw.line(self, start, end, **kwargs)
437        return self

Draw a line from start to end.

Arguments:
  • start (Point): The starting point of the line.
  • end (Point): The ending point of the line.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def rectangle( self, center: Sequence[float] = (0, 0), width: float = 100, height: float = 100, angle: float = 0, **kwargs) -> typing_extensions.Self:
439    def rectangle(
440        self,
441        center: Point = (0, 0),
442        width: float = 100,
443        height: float = 100,
444        angle: float = 0,
445        **kwargs,
446    ) -> Self:
447        """
448        Draw a rectangle.
449
450        Args:
451            center (Point): The center of the rectangle.
452            width (float): The width of the rectangle.
453            height (float): The height of the rectangle.
454            angle (float, optional): The angle of the rectangle, defaults to 0.
455            kwargs (dict): Additional keyword arguments.
456
457        Returns:
458            Self: The canvas object.
459        """
460        draw.rectangle(self, center, width, height, angle, **kwargs)
461        return self

Draw a rectangle.

Arguments:
  • center (Point): The center of the rectangle.
  • width (float): The width of the rectangle.
  • height (float): The height of the rectangle.
  • angle (float, optional): The angle of the rectangle, defaults to 0.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def square( self, center: Sequence[float] = (0, 0), size: float = 100, angle: float = 0, **kwargs) -> typing_extensions.Self:
463    def square(
464        self, center: Point = (0, 0), size: float = 100, angle: float = 0, **kwargs
465    ) -> Self:
466        """
467        Draw a square with the given center and size.
468
469        Args:
470            center (Point): The center of the square.
471            size (float): The size of the square.
472            angle (float, optional): The angle of the square, defaults to 0.
473            kwargs (dict): Additional keyword arguments.
474
475        Returns:
476            Self: The canvas object.
477        """
478        draw.rectangle(self, center, size, size, angle, **kwargs)
479        return self

Draw a square with the given center and size.

Arguments:
  • center (Point): The center of the square.
  • size (float): The size of the square.
  • angle (float, optional): The angle of the square, defaults to 0.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def lines( self, points: Sequence[Sequence[float]], **kwargs) -> typing_extensions.Self:
481    def lines(self, points: Sequence[Point], **kwargs) -> Self:
482        """
483        Draw a polyline through the given points.
484
485        Args:
486            points (Sequence[Point]): The points to draw the polyline through.
487            kwargs (dict): Additional keyword arguments.
488
489        Returns:
490            Self: The canvas object.
491        """
492        draw.lines(self, points, **kwargs)
493        return self

Draw a polyline through the given points.

Arguments:
  • points (Sequence[Point]): The points to draw the polyline through.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def draw_lace( self, lace: simetri.graphics.batch.Batch, **kwargs) -> typing_extensions.Self:
495    def draw_lace(self, lace: Batch, **kwargs) -> Self:
496        """
497        Draw the lace.
498
499        Args:
500            lace (Batch): The lace to draw.
501            kwargs (dict): Additional keyword arguments.
502
503        Returns:
504            Self: The canvas object.
505        """
506        draw.draw_lace(self, lace, **kwargs)
507        return self

Draw the lace.

Arguments:
  • lace (Batch): The lace to draw.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def draw_dimension( self, dim: simetri.graphics.shape.Shape, **kwargs) -> typing_extensions.Self:
509    def draw_dimension(self, dim: Shape, **kwargs) -> Self:
510        """
511        Draw the dimension.
512
513        Args:
514            dim (Shape): The dimension to draw.
515            kwargs (dict): Additional keyword arguments.
516
517        Returns:
518            Self: The canvas object.
519        """
520        draw.draw_dimension(self, dim, **kwargs)
521        return self

Draw the dimension.

Arguments:
  • dim (Shape): The dimension to draw.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def draw( self, item_s: Union[ForwardRef(<Types.ARC: 'ARC'>), ForwardRef(<Types.ARC_ARROW: 'ARC_ARROW'>), ForwardRef(<Types.ARROW: 'ARROW'>), ForwardRef(<Types.ARROW_HEAD: 'ARROW_HEAD'>), ForwardRef(<Types.BATCH: 'BATCH'>), ForwardRef(<Types.CIRCLE: 'CIRCLE'>), ForwardRef(<Types.CIRCULAR_GRID: 'CIRCULAR_GRID'>), ForwardRef(<Types.DIMENSION: 'DIMENSION'>), ForwardRef(<Types.DOT: 'DOT'>), ForwardRef(<Types.DOTS: 'DOTS'>), ForwardRef(<Types.EDGE: 'EDGE'>), ForwardRef(<Types.ELLIPSE: 'ELLIPSE'>), ForwardRef(<Types.FRAGMENT: 'FRAGMENT'>), ForwardRef(<Types.HEX_GRID: 'HEX_GRID'>), ForwardRef(<Types.INTERSECTION: 'INTERSECTION'>), ForwardRef(<Types.LACE: 'LACE'>), ForwardRef(<Types.LINPATH: 'LINPATH'>), ForwardRef(<Types.MIXED_GRID: 'MIXED_GRID'>), ForwardRef(<Types.OUTLINE: 'OUTLINE'>), ForwardRef(<Types.OVERLAP: 'OVERLAP'>), ForwardRef(<Types.PARALLEL_POLYLINE: 'PARALLEL_POLYLINE'>), ForwardRef(<Types.PATTERN: 'PATTERN'>), ForwardRef(<Types.PLAIT: 'PLAIT'>), ForwardRef(<Types.POLYLINE: 'POLYLINE'>), ForwardRef(<Types.RECTANGLE: 'RECTANGLE'>), ForwardRef(<Types.SECTION: 'SECTION'>), ForwardRef(<Types.SEGMENT: 'SEGMENT'>), ForwardRef(<Types.SHAPE: 'SHAPE'>), ForwardRef(<Types.SINE_WAVE: 'SINE_WAVE'>), ForwardRef(<Types.SQUARE_GRID: 'SQUARE_GRID'>), ForwardRef(<Types.STAR: 'STAR'>), ForwardRef(<Types.SVG_PATH: 'SVG_PATH'>), ForwardRef(<Types.TAG: 'TAG'>), ForwardRef(<Types.TURTLE: 'TURTLE'>), list, tuple], pos: Sequence[float] = None, angle: float = 0, rotocenter: Sequence[float] = (0, 0), scale=(1, 1), about=(0, 0), **kwargs) -> typing_extensions.Self:
523    def draw(
524        self,
525        item_s: Union[Drawable, list, tuple],
526        pos: Point = None,
527        angle: float = 0,
528        rotocenter: Point = (0, 0),
529        scale=(1, 1),
530        about=(0, 0),
531        **kwargs,
532    ) -> Self:
533        """
534        Draw the item_s. item_s can be a single item or a list of items.
535
536        Args:
537            item_s (Union[Drawable, list, tuple]): The item(s) to draw.
538            pos (Point, optional): The position to draw the item(s), defaults to None.
539            angle (float, optional): The angle to rotate the item(s), defaults to 0.
540            rotocenter (Point, optional): The point about which to rotate, defaults to (0, 0).
541            scale (tuple, optional): The scale factors for the x and y axes, defaults to (1, 1).
542            about (tuple, optional): The point about which to scale, defaults to (0, 0).
543            kwargs (dict): Additional keyword arguments.
544
545        Returns:
546            Self: The canvas object.
547        """
548        sketch_xform = self._sketch_xform_matrix
549        if pos is not None:
550            sketch_xform = translation_matrix(*pos[:2]) @ sketch_xform
551        if scale[0] != 1 or scale[1] != 1:
552            sketch_xform = scale_in_place_matrix(*pos[:2], about) @ sketch_xform
553        if angle != 0:
554            sketch_xform = rotation_matrix(angle, rotocenter) @ sketch_xform
555        self._sketch_xform_matrix = sketch_xform @ self._xform_matrix
556
557        if isinstance(item_s, (list, tuple)):
558            for item in item_s:
559                draw.draw(self, item, **kwargs)
560        else:
561            draw.draw(self, item_s, **kwargs)
562
563        self._sketch_xform_matrix = identity_matrix()
564
565        return self

Draw the item_s. item_s can be a single item or a list of items.

Arguments:
  • item_s (Union[Drawable, list, tuple]): The item(s) to draw.
  • pos (Point, optional): The position to draw the item(s), defaults to None.
  • angle (float, optional): The angle to rotate the item(s), defaults to 0.
  • rotocenter (Point, optional): The point about which to rotate, defaults to (0, 0).
  • scale (tuple, optional): The scale factors for the x and y axes, defaults to (1, 1).
  • about (tuple, optional): The point about which to scale, defaults to (0, 0).
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def draw_CS(self, size: float = None, **kwargs) -> typing_extensions.Self:
567    def draw_CS(self, size: float = None, **kwargs) -> Self:
568        """
569        Draw the Canvas coordinate system.
570
571        Args:
572            size (float, optional): The size of the coordinate system, defaults to None.
573            kwargs (dict): Additional keyword arguments.
574
575        Returns:
576            Self: The canvas object.
577        """
578        draw.draw_CS(self, size, **kwargs)
579        return self

Draw the Canvas coordinate system.

Arguments:
  • size (float, optional): The size of the coordinate system, defaults to None.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def draw_frame( self, margin: Union[float, Sequence] = None, width=None, **kwargs) -> typing_extensions.Self:
581    def draw_frame(
582        self, margin: Union[float, Sequence] = None, width=None, **kwargs
583    ) -> Self:
584        """
585        Draw a frame around the canvas.
586
587        Args:
588            margins (Union[float, Sequence]): The margins of the frame.
589            kwargs (dict): Additional keyword arguments.
590
591        Returns:
592            Self: The canvas object.
593        """
594        # to do: add shadow and frame color, shadow width. Check canvas.clip.
595        if margin is None:
596            margin = defaults["canvas_frame_margin"]
597        if width is None:
598            width = defaults["canvas_frame_width"]
599        b_box = bounding_box(self._all_vertices)
600        box2 = b_box.get_inflated_b_box(margin)
601        box3 = box2.get_inflated_b_box(15)
602        shadow = Shape([box3.northwest, box3.southwest, box3.southeast])
603        self.draw(shadow, line_color=colors.light_gray, line_width=width)
604        self.draw(Shape(box2.corners, closed=True), fill=False, line_width=width)
605
606        return self

Draw a frame around the canvas.

Arguments:
  • margins (Union[float, Sequence]): The margins of the frame.
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

def reset(self) -> typing_extensions.Self:
608    def reset(self) -> Self:
609        """
610        Reset the canvas to its initial state.
611
612        Returns:
613            Self: The canvas object.
614        """
615        self._code = []
616        self.preamble = defaults["preamble"]
617        self.pages = [Page(self.size, self.back_color, self.border)]
618        self.active_page = self.pages[0]
619        self._all_vertices = []
620        self.tex: Tex = Tex()
621        self.clip: bool = False  # if true then clip the canvas to the mask
622        self._xform_matrix = identity_matrix()
623        self._sketch_xform_matrix = identity_matrix()
624        self.active_page = self.pages[0]
625        self._all_vertices = []
626        return self

Reset the canvas to its initial state.

Returns:

Self: The canvas object.

pos: Sequence[float]
646    @property
647    def pos(self) -> Point:
648        """
649        The position of the canvas.
650
651        Args:
652            point (Point, optional): The point to set the position to.
653
654        Returns:
655            Point: The position of the canvas.
656        """
657
658        return self._xform_matrix[2, :2].tolist()[:2]

The position of the canvas.

Arguments:
  • point (Point, optional): The point to set the position to.
Returns:

Point: The position of the canvas.

angle: float
670    @property
671    def angle(self) -> float:
672        """
673        The angle of the canvas.
674
675        Returns:
676            float: The angle of the canvas.
677        """
678        xform = self._xform_matrix
679
680        return np.arctan2(xform[0, 1], xform[0, 0])

The angle of the canvas.

Returns:

float: The angle of the canvas.

scale: Tuple[float, float]
692    @property
693    def scale(self) -> Vec2:
694        """
695        The scale of the canvas.
696
697        Returns:
698            Vec2: The scale of the canvas.
699        """
700        xform = self._xform_matrix
701
702        return np.linalg.norm(xform[:2, 0]), np.linalg.norm(xform[:2, 1])

The scale of the canvas.

Returns:

Vec2: The scale of the canvas.

xform_matrix: 'ndarray'
723    @property
724    def xform_matrix(self) -> "ndarray":
725        """
726        The transformation matrix of the canvas.
727
728        Returns:
729            ndarray: The transformation matrix of the canvas.
730        """
731        return self._xform_matrix.copy()

The transformation matrix of the canvas.

Returns:

ndarray: The transformation matrix of the canvas.

def transform(self, transform_matrix: 'ndarray') -> typing_extensions.Self:
733    def transform(self, transform_matrix: "ndarray") -> Self:
734        """
735        Transforms the canvas by the given transformation matrix.
736
737        Args:
738            transform_matrix (ndarray): The transformation matrix.
739
740        Returns:
741            Self: The Canvas object.
742        """
743        self._xform_matrix = transform_matrix @ self._xform_matrix
744
745        return self

Transforms the canvas by the given transformation matrix.

Arguments:
  • transform_matrix (ndarray): The transformation matrix.
Returns:

Self: The Canvas object.

def reset_transform(self) -> typing_extensions.Self:
747    def reset_transform(self) -> Self:
748        """
749        Reset the transformation matrix of the canvas.
750        The canvas origin is at (0, 0) and the orientation angle is 0.
751        Transformation matrix is the identity matrix.
752
753        Returns:
754            Self: The canvas object.
755        """
756        self._xform_matrix = identity_matrix()
757
758        return self

Reset the transformation matrix of the canvas. The canvas origin is at (0, 0) and the orientation angle is 0. Transformation matrix is the identity matrix.

Returns:

Self: The canvas object.

def translate(self, x: float, y: float) -> typing_extensions.Self:
760    def translate(self, x: float, y: float) -> Self:
761        """
762        Translate the canvas by x and y.
763
764        Args:
765            x (float): The translation distance along the x-axis.
766            y (float): The translation distance along the y-axis.
767
768        Returns:
769            Self: The canvas object.
770        """
771
772        self._xform_matrix = translation_matrix(x, y) @ self._xform_matrix
773
774        return self

Translate the canvas by x and y.

Arguments:
  • x (float): The translation distance along the x-axis.
  • y (float): The translation distance along the y-axis.
Returns:

Self: The canvas object.

def rotate(self, angle: float, about=(0, 0)) -> typing_extensions.Self:
776    def rotate(self, angle: float, about=(0, 0)) -> Self:
777        """
778        Rotate the canvas by angle in radians about the given point.
779
780        Args:
781            angle (float): The rotation angle in radians.
782            about (tuple): The point about which to rotate the canvas.
783
784        Returns:
785            Self: The canvas object.
786        """
787
788        self._xform_matrix = rotation_matrix(angle, about) @ self._xform_matrix
789
790        return self

Rotate the canvas by angle in radians about the given point.

Arguments:
  • angle (float): The rotation angle in radians.
  • about (tuple): The point about which to rotate the canvas.
Returns:

Self: The canvas object.

def flip_x_axis(self) -> typing_extensions.Self:
813    def flip_x_axis(self) -> Self:
814        """
815        Flip the x-axis direction. Warning: This will reverse the positive rotation direction.
816
817        Returns:
818            Self: The canvas object.
819        """
820        warnings.warn(
821            "Flipping the x-axis will change the positive rotation direction."
822        )
823        return self._flip(Axis.X)

Flip the x-axis direction. Warning: This will reverse the positive rotation direction.

Returns:

Self: The canvas object.

def flip_y_axis(self) -> typing_extensions.Self:
825    def flip_y_axis(self) -> Self:
826        """
827        Flip the y-axis direction.
828
829        Returns:
830            Self: The canvas object.
831        """
832        warnings.warn(
833            "Flipping the y-axis will reverse the positive rotation direction."
834        )
835
836        return self._flip(Axis.Y)

Flip the y-axis direction.

Returns:

Self: The canvas object.

x: float
838    @property
839    def x(self) -> float:
840        """
841        The x coordinate of the canvas origin.
842
843        Returns:
844            float: The x coordinate of the canvas origin.
845        """
846        return self.pos[0]

The x coordinate of the canvas origin.

Returns:

float: The x coordinate of the canvas origin.

y: float
858    @property
859    def y(self) -> float:
860        """
861        The y coordinate of the canvas origin.
862
863        Returns:
864            float: The y coordinate of the canvas origin.
865        """
866        return self.pos[1]

The y coordinate of the canvas origin.

Returns:

float: The y coordinate of the canvas origin.

def batch_graph( self, batch: simetri.graphics.batch.Batch) -> networkx.classes.digraph.DiGraph:
878    def batch_graph(self, batch: "Batch") -> nx.DiGraph:
879        """
880        Return a directed graph of the batch and its elements.
881        Canvas is the root of the graph.
882        Graph nodes are the ids of the elements.
883
884        Args:
885            batch (Batch): The batch to create the graph from.
886
887        Returns:
888            nx.DiGraph: The directed graph of the batch and its elements.
889        """
890
891        def add_batch(batch, graph):
892            graph.add_node(batch.id)
893            for item in batch.elements:
894                graph.add_edge(batch.id, item.id)
895                if item.subtype == Types.BATCH:
896                    add_batch(item, graph)
897            return graph
898
899        di_graph = nx.DiGraph()
900        di_graph.add_edge(self.id, batch.id)
901        for item in batch.elements:
902            if item.subtype == Types.BATCH:
903                di_graph.add_edge(batch.id, item.id)
904                add_batch(item, di_graph)
905            else:
906                di_graph.add_edge(batch.id, item.id)
907
908        return di_graph

Return a directed graph of the batch and its elements. Canvas is the root of the graph. Graph nodes are the ids of the elements.

Arguments:
  • batch (Batch): The batch to create the graph from.
Returns:

nx.DiGraph: The directed graph of the batch and its elements.

def get_fonts_list(self) -> list[str]:
934    def get_fonts_list(self) -> list[str]:
935        """
936        Get the list of fonts used in the canvas.
937
938        Returns:
939            list[str]: The list of fonts used in the canvas.
940        """
941        user_fonts = set(self._font_list)
942
943        latex_fonts = set(
944            [
945                defaults["main_font"],
946                defaults["sans_font"],
947                defaults["mono_font"],
948                "serif",
949                "sansserif",
950                "monospace",
951            ]
952        )
953        for sketch in self.active_page.sketches:
954            if sketch.subtype == Types.TAG_SKETCH:
955                name = sketch.font_family
956                if name is not None and name not in latex_fonts:
957                    user_fonts.add(name)
958        return list(user_fonts.difference(latex_fonts))

Get the list of fonts used in the canvas.

Returns:

list[str]: The list of fonts used in the canvas.

def save( self, file_path: pathlib.Path = None, overwrite: bool = None, show: bool = None, print_output=False) -> typing_extensions.Self:
1011    def save(
1012        self,
1013        file_path: Path = None,
1014        overwrite: bool = None,
1015        show: bool = None,
1016        print_output=False,
1017    ) -> Self:
1018        """
1019        Save the canvas to a file.
1020
1021        Args:
1022            file_path (Path, optional): The path to save the file.
1023            overwrite (bool, optional): Whether to overwrite the file if it exists.
1024            show (bool, optional): Whether to show the file in the browser.
1025            print_output (bool, optional): Whether to print the output of the compilation.
1026
1027        Returns:
1028            Self: The canvas object.
1029        """
1030
1031        def validate_file_path(file_path: Path, overwrite: bool) -> Result:
1032            """
1033            Validate the file path.
1034
1035            Args:
1036                file_path (Path): The path to the file.
1037                overwrite (bool): Whether to overwrite the file if it exists.
1038
1039            Returns:
1040                Result: The parent directory, file name, and extension.
1041            """
1042            path_exists = os.path.exists(file_path)
1043            if path_exists and not overwrite:
1044                raise FileExistsError(
1045                    f"File {file_path} already exists. \n"
1046                    "Use canvas.save(file_path, overwrite=True) to overwrite the file."
1047                )
1048            parent_dir, file_name = os.path.split(file_path)
1049            file_name, extension = os.path.splitext(file_name)
1050            if extension not in [".pdf", ".eps", ".ps", ".svg", ".png", ".tex"]:
1051                raise RuntimeError("File type is not supported.")
1052            if not os.path.exists(parent_dir):
1053                raise NotADirectoryError(f"Directory {parent_dir} does not exist.")
1054            if not os.access(parent_dir, os.W_OK):
1055                raise PermissionError(f"Directory {parent_dir} is not writable.")
1056
1057            return parent_dir, file_name, extension
1058
1059        def compile_tex(cmd):
1060            """
1061            Compile the TeX file.
1062
1063            Args:
1064                cmd (str): The command to compile the TeX file.
1065
1066            Returns:
1067                str: The output of the compilation.
1068            """
1069            os.chdir(parent_dir)
1070            with subprocess.Popen(
1071                cmd,
1072                stdin=subprocess.PIPE,
1073                stdout=subprocess.PIPE,
1074                shell=True,
1075                text=True,
1076            ) as p:
1077                output = p.communicate("_s\n_l\n")[0]
1078            if print_output:
1079                print(output.split("\n")[-3:])
1080            return output
1081
1082        def remove_aux_files(file_path):
1083            """
1084            Remove auxiliary files generated during compilation.
1085
1086            Args:
1087                file_path (Path): The path to the file.
1088            """
1089            time_out = 1  # seconds
1090            parent_dir, file_name = os.path.split(file_path)
1091            file_name, extension = os.path.splitext(file_name)
1092            aux_file = os.path.join(parent_dir, file_name + ".aux")
1093            if os.path.exists(aux_file):
1094                if not wait_for_file_availability(aux_file, time_out):
1095                    print(
1096                        (
1097                            f"File '{aux_file}' is not available after waiting for "
1098                            f"{time_out} seconds."
1099                        )
1100                    )
1101                else:
1102                    os.remove(aux_file)
1103            log_file = os.path.join(parent_dir, file_name + ".log")
1104            if os.path.exists(log_file):
1105                if not wait_for_file_availability(log_file, time_out):
1106                    print(
1107                        (
1108                            f"File '{log_file}' is not available after waiting for "
1109                            f"{time_out} seconds."
1110                        )
1111                    )
1112                else:
1113                    if not defaults["keep_log_files"]:
1114                        os.remove(log_file)
1115            tex_file = os.path.join(parent_dir, file_name + ".tex")
1116            if os.path.exists(tex_file):
1117                if not wait_for_file_availability(tex_file, time_out):
1118                    print(
1119                        (
1120                            f"File '{tex_file}' is not available after waiting for "
1121                            f"{time_out} seconds."
1122                        )
1123                    )
1124                else:
1125                    os.remove(tex_file)
1126            file_name, extension = os.path.splitext(file_name)
1127            if extension not in [".pdf", ".tex"]:
1128                pdf_file = os.path.join(parent_dir, file_name + ".pdf")
1129                if os.path.exists(pdf_file):
1130                    if not wait_for_file_availability(pdf_file, time_out):
1131                        print(
1132                            (
1133                                f"File '{pdf_file}' is not available after waiting for "
1134                                f"{time_out} seconds."
1135                            )
1136                        )
1137                    else:
1138                        # os.remove(pdf_file)
1139                        pass
1140            log_file = os.path.join(parent_dir, "simetri.log")
1141            if os.path.exists(log_file):
1142                try:
1143                    os.remove(log_file)
1144                except PermissionError:
1145                    # to do: log the error
1146                    pass
1147
1148        def run_job():
1149            """
1150            Run the job to compile and save the file.
1151
1152            Returns:
1153                None
1154            """
1155            output_path = os.path.join(parent_dir, file_name + extension)
1156            cmd = "xelatex " + tex_path + " --output-directory " + parent_dir
1157            res = compile_tex(cmd)
1158            if "No pages of output" in res:
1159                raise RuntimeError("Failed to compile the tex file.")
1160            pdf_path = os.path.join(parent_dir, file_name + ".pdf")
1161            if not os.path.exists(pdf_path):
1162                raise RuntimeError("Failed to compile the tex file.")
1163
1164            if extension in [".eps", ".ps"]:
1165                ps_path = os.path.join(parent_dir, file_name + extension)
1166                os.chdir(parent_dir)
1167                cmd = f"pdf2ps {pdf_path} {ps_path}"
1168                res = subprocess.run(cmd, shell=True, check=False)
1169                if res.returncode != 0:
1170                    raise RuntimeError("Failed to convert pdf to ps.")
1171            elif extension == ".svg":
1172                doc = fitz.open(pdf_path)
1173                page = doc.load_page(0)
1174                svg = page.get_svg_image()
1175                with open(output_path, "w", encoding="utf-8") as f:
1176                    f.write(svg)
1177            elif extension == ".png":
1178                pdf_file = fitz.open(pdf_path)
1179                page = pdf_file[0]
1180                pix = page.get_pixmap()
1181                pix.save(output_path)
1182                pdf_file.close()
1183
1184        parent_dir, file_name, extension = validate_file_path(file_path, overwrite)
1185
1186        tex_code = get_tex_code(self)
1187        tex_path = os.path.join(parent_dir, file_name + ".tex")
1188        with open(tex_path, "w", encoding="utf-8") as f:
1189            f.write(tex_code)
1190        if extension == ".tex":
1191            return self
1192
1193        run_job()
1194        remove_aux_files(file_path)
1195
1196        self._show_browser(file_path=file_path, show_browser=show, multi_page_svg=False)
1197        return self

Save the canvas to a file.

Arguments:
  • file_path (Path, optional): The path to save the file.
  • overwrite (bool, optional): Whether to overwrite the file if it exists.
  • show (bool, optional): Whether to show the file in the browser.
  • print_output (bool, optional): Whether to print the output of the compilation.
Returns:

Self: The canvas object.

def new_page(self, **kwargs) -> typing_extensions.Self:
1199    def new_page(self, **kwargs) -> Self:
1200        """
1201        Create a new page and add it to the canvas.pages.
1202
1203        Args:
1204            kwargs (dict): Additional keyword arguments.
1205
1206        Returns:
1207            Self: The canvas object.
1208        """
1209        page = Page()
1210        self.pages.append(page)
1211        self.active_page = page
1212        for k, v in kwargs.items():
1213            setattr(page, k, v)
1214        return self

Create a new page and add it to the canvas.pages.

Arguments:
  • kwargs (dict): Additional keyword arguments.
Returns:

Self: The canvas object.

@dataclass
class PageGrid:
1217@dataclass
1218class PageGrid:
1219    """
1220    Grid class for drawing grids on a page.
1221
1222    Args:
1223        spacing (float, optional): The spacing between grid lines.
1224        back_color (Color, optional): The background color of the grid.
1225        line_color (Color, optional): The color of the grid lines.
1226        line_width (float, optional): The width of the grid lines.
1227        line_dash_array (Sequence[float], optional): The dash array for the grid lines.
1228        x_shift (float, optional): The x-axis shift of the grid.
1229        y_shift (float, optional): The y-axis shift of the grid.
1230    """
1231
1232    spacing: float = None
1233    back_color: Color = None
1234    line_color: Color = None
1235    line_width: float = None
1236    line_dash_array: Sequence[float] = None
1237    x_shift: float = None
1238    y_shift: float = None
1239
1240    def __post_init__(self):
1241        self.type = Types.PAGE_GRID
1242        self.subtype = Types.RECTANGULAR
1243        self.spacing = defaults["page_grid_spacing"]
1244        self.back_color = defaults["page_grid_back_color"]
1245        self.line_color = defaults["page_grid_line_color"]
1246        self.line_width = defaults["page_grid_line_width"]
1247        self.line_dash_array = defaults["page_grid_line_dash_array"]
1248        self.x_shift = defaults["page_grid_x_shift"]
1249        self.y_shift = defaults["page_grid_y_shift"]
1250        common_properties(self)

Grid class for drawing grids on a page.

Arguments:
  • spacing (float, optional): The spacing between grid lines.
  • back_color (Color, optional): The background color of the grid.
  • line_color (Color, optional): The color of the grid lines.
  • line_width (float, optional): The width of the grid lines.
  • line_dash_array (Sequence[float], optional): The dash array for the grid lines.
  • x_shift (float, optional): The x-axis shift of the grid.
  • y_shift (float, optional): The y-axis shift of the grid.
PageGrid( spacing: float = None, back_color: Color = None, line_color: Color = None, line_width: float = None, line_dash_array: Sequence[float] = None, x_shift: float = None, y_shift: float = None)
spacing: float = None
back_color: Color = None
line_color: Color = None
line_width: float = None
line_dash_array: Sequence[float] = None
x_shift: float = None
y_shift: float = None
@dataclass
class Page:
1253@dataclass
1254class Page:
1255    """
1256    Page class for drawing sketches and text on a page. All drawing
1257    operations result as sketches on the canvas.active_page.
1258
1259    Args:
1260        size (Vec2, optional): The size of the page.
1261        back_color (Color, optional): The background color of the page.
1262        mask (Any, optional): The mask of the page.
1263        margins (Any, optional): The margins of the page (left, bottom, right, top).
1264        recto (bool, optional): Whether the page is recto (True) or verso (False).
1265        grid (PageGrid, optional): The grid of the page.
1266        kwargs (dict, optional): Additional keyword arguments.
1267    """
1268
1269    size: Vec2 = None
1270    back_color: Color = None
1271    mask = None
1272    margins = None  # left, bottom, right, top
1273    recto: bool = True  # True if page is recto, False if verso
1274    grid: PageGrid = None
1275    kwargs: dict = None
1276
1277    def __post_init__(self):
1278        self.type = Types.PAGE
1279        self.sketches = []
1280        if self.grid is None:
1281            self.grid = PageGrid()
1282        if self.kwargs:
1283            for k, v in self.kwargs.items():
1284                setattr(self, k, v)
1285        common_properties(self)

Page class for drawing sketches and text on a page. All drawing operations result as sketches on the canvas.active_page.

Arguments:
  • size (Vec2, optional): The size of the page.
  • back_color (Color, optional): The background color of the page.
  • mask (Any, optional): The mask of the page.
  • margins (Any, optional): The margins of the page (left, bottom, right, top).
  • recto (bool, optional): Whether the page is recto (True) or verso (False).
  • grid (PageGrid, optional): The grid of the page.
  • kwargs (dict, optional): Additional keyword arguments.
Page( size: Tuple[float, float] = None, back_color: Color = None, recto: bool = True, grid: PageGrid = None, kwargs: dict = None)
size: Tuple[float, float] = None
back_color: Color = None
mask = None
margins = None
recto: bool = True
grid: PageGrid = None
kwargs: dict = None
def hello() -> None:
1288def hello() -> None:
1289    """
1290    Show a hello message.
1291    Used for testing an installation of simetri.
1292    """
1293    canvas = Canvas()
1294
1295    canvas.text("Hello from simetri.graphics!", (0, -130), bold=True, font_size=20)
1296    canvas.draw(logo())
1297
1298    d_path = os.path.dirname(os.path.abspath(__file__))
1299    f_path = os.path.join(d_path, "hello.pdf")
1300
1301    canvas.save(f_path, overwrite=True)

Show a hello message. Used for testing an installation of simetri.