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)
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.