simetri.helpers.illustration

This module contains functions and classes for creating annotations, arrows, dimensions, etc.

   1"""This module contains functions and classes for creating annotations,
   2arrows, dimensions, etc."""
   3
   4from dataclasses import dataclass
   5from math import pi, atan2
   6from PIL import ImageFont
   7
   8import fitz
   9import numpy as np
  10
  11# from reportlab.pdfbase import pdfmetrics # to do: remove this
  12
  13from ..graphics.core import Base
  14from ..graphics.bbox import bounding_box
  15from ..graphics.points import Points
  16from ..graphics.batch import Batch
  17from ..graphics.shape import Shape
  18
  19from ..graphics.shapes import reg_poly_points_side_length
  20from ..graphics.common import get_defaults, common_properties, Point, _set_Nones
  21from ..graphics.all_enums import (
  22    Types,
  23    LineJoin,
  24    Anchor,
  25    FrameShape,
  26    HeadPos,
  27    ArrowLine,
  28    Placement,
  29    FontSize,
  30)
  31from ..canvas.style_map import shape_style_map, tag_style_map, TagStyle
  32from ..graphics.affine import identity_matrix
  33from ..geometry.ellipse import Arc
  34from ..geometry.geometry import (
  35    distance,
  36    line_angle,
  37    extended_line,
  38    line_by_point_angle_length,
  39    mid_point,
  40)
  41from .utilities import get_transform, detokenize
  42from ..colors.swatches import swatches_255
  43from ..settings.settings import defaults
  44from ..colors import colors
  45from .validation import validate_args
  46
  47Color = colors.Color
  48array = np.array
  49
  50
  51def logo(scale=1):
  52    """Returns the Simetri logo.
  53
  54    Args:
  55        scale (int, optional): Scale factor for the logo. Defaults to 1.
  56
  57    Returns:
  58        Batch: A Batch object containing the logo shapes.
  59    """
  60    w = 10 * scale
  61    points = [
  62        (0, 0),
  63        (-4, 0),
  64        (-4, 6),
  65        (1, 6),
  66        (1, 2),
  67        (-2, 2),
  68        (-2, 4),
  69        (-1, 4),
  70        (-1, 3),
  71        (0, 3),
  72        (0, 5),
  73        (-3, 5),
  74        (-3, 1),
  75        (5, 1),
  76        (5, -10),
  77        (0, -10),
  78        (0, -6),
  79        (3, -6),
  80        (3, -8),
  81        (2, -8),
  82        (2, -7),
  83        (1, -7),
  84        (1, -9),
  85        (4, -9),
  86        (4, -5),
  87        (-4, -5),
  88        (-4, -1),
  89        (-1, -1),
  90        (-1, -3),
  91        (-2, -3),
  92        (-2, -2),
  93        (-3, -2),
  94        (-3, -4),
  95        (0, -4),
  96    ]
  97
  98    points2 = [
  99        (1, 0),
 100        (1, -4),
 101        (4, -4),
 102        (4, -3),
 103        (2, -3),
 104        (2, -1),
 105        (3, -1),
 106        (3, -2),
 107        (4, -2),
 108        (4, 0),
 109    ]
 110
 111    points = [(x * w, y * w) for x, y in points]
 112    points2 = [(x * w, y * w) for x, y in points2]
 113    kernel1 = Shape(points, closed=True)
 114    kernel2 = Shape(points2, closed=True)
 115    rad = 1
 116    line_width = 2
 117    kernel1.fillet_radius = rad
 118    kernel2.fillet_radius = rad
 119    kernel1.line_width = line_width
 120    kernel2.line_width = line_width
 121    fill_color = Color(*swatches_255[62][8])
 122    kernel1.fill_color = fill_color
 123    kernel2.fill_color = colors.white
 124
 125    return Batch([kernel1, kernel2])
 126
 127
 128def convert_latex_font_size(latex_font_size: FontSize):
 129    """Converts LaTeX font size to a numerical value.
 130
 131    Args:
 132        latex_font_size (FontSize): The LaTeX font size.
 133
 134    Returns:
 135        int: The corresponding numerical font size.
 136    """
 137    d_font_size = {
 138        FontSize.TINY: 5,
 139        FontSize.SMALL: 7,
 140        FontSize.NORMAL: 10,
 141        FontSize.LARGE: 12,
 142        FontSize.LARGE2: 14,
 143        FontSize.LARGE3: 17,
 144        FontSize.HUGE: 20,
 145        FontSize.HUGE2: 25,
 146    }
 147
 148    return d_font_size[latex_font_size]
 149
 150
 151def letter_F_points():
 152    """Returns the points of the capital letter F.
 153
 154    Returns:
 155        list: A list of points representing the letter F.
 156    """
 157    return [
 158        (0.0, 0.0),
 159        (20.0, 0.0),
 160        (20.0, 40.0),
 161        (40.0, 40.0),
 162        (40.0, 60.0),
 163        (20.0, 60.0),
 164        (20.0, 80.0),
 165        (50.0, 80.0),
 166        (50.0, 100.0),
 167        (0.0, 100.0),
 168        (0.0, 0.0),
 169    ]
 170
 171
 172def letter_F(scale=1, **kwargs):
 173    """Returns a Shape object representing the capital letter F.
 174
 175    Args:
 176        scale (int, optional): Scale factor for the letter. Defaults to 1.
 177        **kwargs: Additional keyword arguments for shape styling.
 178
 179    Returns:
 180        Shape: A Shape object representing the letter F.
 181    """
 182    F = Shape(letter_F_points(), closed=True)
 183    if scale != 1:
 184        F.scale(scale)
 185    for k, v in kwargs.items():
 186        if k in shape_style_map:
 187            setattr(F, k, v)
 188        else:
 189            raise AttributeError(f"{k}. Invalid attribute!")
 190    return F
 191
 192
 193def cube(size: float = 100):
 194    """Returns a Batch object representing a cube.
 195
 196    Args:
 197        size (float, optional): The size of the cube. Defaults to 100.
 198
 199    Returns:
 200        Batch: A Batch object representing the cube.
 201    """
 202    points = reg_poly_points_side_length((0, 0), 6, size)
 203    center = (0, 0)
 204    face1 = Shape([points[0], center] + points[4:], closed=True)
 205    cube_ = face1.rotate(-2 * pi / 3, (0, 0), reps=2)
 206    cube_[0].fill_color = Color(0.3, 0.3, 0.3)
 207    cube_[1].fill_color = Color(0.4, 0.4, 0.4)
 208    cube_[2].fill_color = Color(0.6, 0.6, 0.6)
 209
 210    return cube_
 211
 212
 213def pdf_to_svg(pdf_path, svg_path):
 214    """Converts a single-page PDF file to SVG.
 215
 216    Args:
 217        pdf_path (str): The path to the PDF file.
 218        svg_path (str): The path to save the SVG file.
 219    """
 220    doc = fitz.open(pdf_path)
 221    page = doc.load_page(0)
 222    svg = page.get_svg_image()
 223    with open(svg_path, "w", encoding="utf-8") as f:
 224        f.write(svg)
 225
 226
 227# To do: use a different name for the Annotation class
 228# annotation is a label with an arrow
 229class Annotation(Batch):
 230    """An Annotation object is a label with an arrow pointing to a specific location.
 231
 232    Args:
 233        text (str): The annotation text.
 234        pos (tuple): The position of the annotation.
 235        frame (FrameShape): The frame shape of the annotation.
 236        root_pos (tuple): The root position of the arrow.
 237        arrow_line (ArrowLine, optional): The type of arrow line. Defaults to ArrowLine.STRAIGHT_END.
 238        **kwargs: Additional keyword arguments for annotation styling.
 239    """
 240
 241    def __init__(
 242        self, text, pos, frame, root_pos, arrow_line=ArrowLine.STRAIGHT_END, **kwargs
 243    ):
 244        self.text = text
 245        self.pos = pos
 246        self.frame = frame
 247        self.root_pos = root_pos
 248        self.arrow_line = arrow_line
 249        self.kwargs = kwargs
 250
 251        super().__init__(subtype=Types.ANNOTATION, **kwargs)
 252
 253
 254@dataclass
 255class TagFrame:
 256    """Frame objects are used with Tag objects to create boxes.
 257
 258    Args:
 259        frame_shape (FrameShape, optional): The shape of the frame. Defaults to "rectangle".
 260        line_width (float, optional): The width of the frame line. Defaults to 1.
 261        line_dash_array (list, optional): The dash pattern for the frame line. Defaults to None.
 262        line_join (LineJoin, optional): The line join style. Defaults to "miter".
 263        line_color (Color, optional): The color of the frame line. Defaults to colors.black.
 264        back_color (Color, optional): The background color of the frame. Defaults to colors.white.
 265        fill (bool, optional): Whether to fill the frame. Defaults to False.
 266        stroke (bool, optional): Whether to stroke the frame. Defaults to True.
 267        double (bool, optional): Whether to use a double line. Defaults to False.
 268        double_distance (float, optional): The distance between double lines. Defaults to 2.
 269        inner_sep (float, optional): The inner separation. Defaults to 10.
 270        outer_sep (float, optional): The outer separation. Defaults to 10.
 271        smooth (bool, optional): Whether to smooth the frame. Defaults to False.
 272        rounded_corners (bool, optional): Whether to use rounded corners. Defaults to False.
 273        fillet_radius (float, optional): The radius of the fillet. Defaults to 10.
 274        draw_fillets (bool, optional): Whether to draw fillets. Defaults to False.
 275        blend_mode (str, optional): The blend mode. Defaults to None.
 276        gradient (str, optional): The gradient. Defaults to None.
 277        pattern (str, optional): The pattern. Defaults to None.
 278        min_width (float, optional): The minimum width. Defaults to None.
 279        min_height (float, optional): The minimum height. Defaults to None.
 280        min_size (float, optional): The minimum size. Defaults to None.
 281    """
 282
 283    frame_shape: FrameShape = "rectangle"
 284    line_width: float = 1
 285    line_dash_array: list = None
 286    line_join: LineJoin = "miter"
 287    line_color: Color = colors.black
 288    back_color: Color = colors.white
 289    fill: bool = False
 290    stroke: bool = True
 291    double: bool = False
 292    double_distance: float = 2
 293    inner_sep: float = 10
 294    outer_sep: float = 10
 295    smooth: bool = False
 296    rounded_corners: bool = False
 297    fillet_radius: float = 10
 298    draw_fillets: bool = False
 299    blend_mode: str = None
 300    gradient: str = None
 301    pattern: str = None
 302    min_width: float = None
 303    min_height: float = None
 304    min_size: float = None
 305
 306    def __post_init__(self):
 307        self.type = Types.FRAME
 308        self.subtype = Types.FRAME
 309        common_properties(self, id_only=True)
 310
 311
 312class Tag(Base):
 313    """A Tag object is very similar to TikZ library's nodes. It is a text with a frame.
 314
 315    Args:
 316        text (str): The text of the tag.
 317        pos (Point): The position of the tag.
 318        font_family (str, optional): The font family. Defaults to None.
 319        font_size (int, optional): The font size. Defaults to None.
 320        font_color (Color, optional): The font color. Defaults to None.
 321        anchor (Anchor, optional): The anchor point. Defaults to Anchor.CENTER.
 322        bold (bool, optional): Whether the text is bold. Defaults to False.
 323        italic (bool, optional): Whether the text is italic. Defaults to False.
 324        text_width (float, optional): The width of the text. Defaults to None.
 325        placement (Placement, optional): The placement of the tag. Defaults to None.
 326        minimum_size (float, optional): The minimum size of the tag. Defaults to None.
 327        minimum_width (float, optional): The minimum width of the tag. Defaults to None.
 328        minimum_height (float, optional): The minimum height of the tag. Defaults to None.
 329        frame (TagFrame, optional): The frame of the tag. Defaults to None.
 330        xform_matrix (array, optional): The transformation matrix. Defaults to None.
 331        **kwargs: Additional keyword arguments for tag styling.
 332    """
 333
 334    def __init__(
 335        self,
 336        text: str,
 337        pos: Point,
 338        font_family: str = None,
 339        font_size: int = None,
 340        font_color: Color = None,
 341        anchor: Anchor = Anchor.CENTER,
 342        bold: bool = False,
 343        italic: bool = False,
 344        text_width: float = None,
 345        placement: Placement = None,
 346        minimum_size: float = None,
 347        minimum_width: float = None,
 348        minimum_height: float = None,
 349        frame=None,
 350        xform_matrix=None,
 351        **kwargs,
 352    ):
 353        self.__dict__["style"] = TagStyle()
 354        self.__dict__["_style_map"] = tag_style_map
 355        self._set_aliases()
 356        tag_attribs = list(tag_style_map.keys())
 357        tag_attribs.append("subtype")
 358        _set_Nones(
 359            self,
 360            ["font_family", "font_size", "font_color"],
 361            [font_family, font_size, font_color],
 362        )
 363        validate_args(kwargs, tag_attribs)
 364        x, y = pos[:2]
 365        self._init_pos = array([x, y, 1.0])
 366
 367        self.text = detokenize(text)
 368        if frame is None:
 369            self.frame = TagFrame()
 370        self.type = Types.TAG
 371        self.subtype = Types.TAG
 372        self.style = TagStyle()
 373        self.style.draw_frame = True
 374        if font_family:
 375            self.font_family = font_family
 376        if font_size:
 377            self.font_size = font_size
 378        else:
 379            self.font_size = defaults["font_size"]
 380        if xform_matrix is None:
 381            self.xform_matrix = identity_matrix()
 382        else:
 383            self.xform_matrix = get_transform(xform_matrix)
 384
 385        self.anchor = anchor
 386        self.bold = bold
 387        self.italic = italic
 388        self.text_width = text_width
 389        self.placement = placement
 390        self.minimum_size = minimum_size
 391        self.minimum_width = minimum_width
 392        self.minimum_height = minimum_height
 393        for k, v in kwargs.items():
 394            setattr(self, k, v)
 395
 396        x1, y1, x2, y2 = self.text_bounds()
 397        w = x2 - x1
 398        h = y2 - y1
 399        self.points = Points([(0, 0, 1), (w, 0, 1), (w, h, 1), (0, h, 1)])
 400        common_properties(self)
 401
 402    def __setattr__(self, name, value):
 403        obj, attrib = self.__dict__["_aliasses"].get(name, (None, None))
 404        if obj:
 405            setattr(obj, attrib, value)
 406        else:
 407            self.__dict__[name] = value
 408
 409    def __getattr__(self, name):
 410        obj, attrib = self.__dict__["_aliasses"].get(name, (None, None))
 411        if obj:
 412            res = getattr(obj, attrib)
 413        else:
 414            try:
 415                res = super().__getattr__(name)
 416            except AttributeError:
 417                res = self.__dict__[name]
 418
 419        return res
 420
 421    def _set_aliases(self):
 422        _aliasses = {}
 423
 424        for alias, path_attrib in self._style_map.items():
 425            style_path, attrib = path_attrib
 426            obj = self
 427            for attrib_name in style_path.split("."):
 428                obj = obj.__dict__[attrib_name]
 429
 430            if obj is not self:
 431                _aliasses[alias] = (obj, attrib)
 432        self.__dict__["_aliasses"] = _aliasses
 433
 434    def _update(self, xform_matrix, reps: int = 0):
 435        if reps == 0:
 436            self.xform_matrix = self.xform_matrix @ xform_matrix
 437            res = self
 438        else:
 439            tags = [self]
 440            tag = self
 441            for _ in range(reps):
 442                tag = tag.copy()
 443                tag._update(xform_matrix)
 444                tags.append(tag)
 445            res = Batch(tags)
 446
 447        return res
 448
 449    @property
 450    def pos(self) -> Point:
 451        """Returns the position of the text.
 452
 453        Returns:
 454            Point: The position of the text.
 455        """
 456        return (self._init_pos @ self.xform_matrix)[:2].tolist()
 457
 458    def copy(self) -> "Tag":
 459        """Returns a copy of the Tag object.
 460
 461        Returns:
 462            Tag: A copy of the Tag object.
 463        """
 464        tag = Tag(self.text, self.pos, xform_matrix=self.xform_matrix)
 465        tag._init_pos = self._init_pos
 466        tag.font_family = self.font_family
 467        tag.font_size = self.font_size
 468        tag.font_color = self.font_color
 469        tag.anchor = self.anchor
 470        tag.bold = self.bold
 471        tag.italic = self.italic
 472        tag.text_width = self.text_width
 473        tag.placement = self.placement
 474        tag.minimum_size = self.minimum_size
 475        tag.minimum_width = self.minimum_width
 476
 477        return tag
 478
 479    def text_bounds(self) -> tuple[float, float, float, float]:
 480        """Returns the bounds of the text.
 481
 482        Returns:
 483            tuple: The bounds of the text (xmin, ymin, xmax, ymax).
 484        """
 485        mult = 1 # font size multiplier
 486        if self.font_size is None:
 487            font_size = defaults["font_size"]
 488        elif type(self.font_size) in [int, float]:
 489            font_size = self.font_size
 490        elif self.font_size in FontSize:
 491            font_size = convert_latex_font_size(self.font_size)
 492        else:
 493            raise ValueError("Invalid font size.")
 494        try:
 495            font = ImageFont.truetype(f"{self.font_family}.ttf", font_size)
 496        except OSError:
 497            font = ImageFont.load_default()
 498            mult = self.font_size / 10
 499        xmin, ymin, xmax, ymax = font.getbbox(self.text)
 500        width = xmax - xmin
 501        height = ymax - ymin
 502        dx = (width * mult) / 2
 503        dy = (height * mult) / 2
 504        xmin -= dx
 505        xmax += dx
 506        ymin -= dy
 507        ymax += dy
 508
 509        return xmin, ymin, xmax, ymax
 510
 511    @property
 512    def final_coords(self):
 513        """Returns the final coordinates of the text.
 514
 515        Returns:
 516            array: The final coordinates of the text.
 517        """
 518        return self.points.homogen_coords @ self.xform_matrix
 519
 520    @property
 521    def b_box(self):
 522        """Returns the bounding box of the text.
 523
 524        Returns:
 525            tuple: The bounding box of the text.
 526        """
 527        xmin, ymin, xmax, ymax = self.text_bounds()
 528        w2 = (xmax - xmin) / 2
 529        h2 = (ymax - ymin) / 2
 530        x, y = self.pos
 531        inner_sep = self.frame.inner_sep
 532        xmin = x - w2 - inner_sep
 533        xmax = x + w2 + inner_sep
 534        ymin = y - h2 - inner_sep
 535        ymax = y + h2 + inner_sep
 536        points = [
 537            (xmin, ymin),
 538            (xmax, ymin),
 539            (xmax, ymax),
 540            (xmin, ymax),
 541        ]
 542        return bounding_box(points)
 543    def __str__(self) -> str:
 544        return f"Tag({self.text})"
 545
 546    def __repr__(self) -> str:
 547        return f"Tag({self.text})"
 548
 549
 550class ArrowHead(Shape):
 551    """An ArrowHead object is a shape that represents the head of an arrow.
 552
 553    Args:
 554        length (float, optional): The length of the arrow head. Defaults to None.
 555        width_ (float, optional): The width of the arrow head. Defaults to None.
 556        points (list, optional): The points defining the arrow head. Defaults to None.
 557        **kwargs: Additional keyword arguments for arrow head styling.
 558    """
 559
 560    def __init__(
 561        self, length: float = None, width_: float = None, points: list = None, **kwargs
 562    ):
 563        length, width_ = get_defaults(
 564            ["arrow_head_length", "arrow_head_width"], [length, width_]
 565        )
 566        if points is None:
 567            w2 = width_ / 2
 568            points = [(0, 0), (0, -w2), (length, 0), (0, w2)]
 569        super().__init__(points, closed=True, subtype=Types.ARROW_HEAD, **kwargs)
 570        self.head_length = length
 571        self.head_width = width_
 572
 573        self.kwargs = kwargs
 574
 575
 576def draw_cs_tiny(canvas, pos=(0, 0), width=25, height=25, neg_width=5, neg_height=5):
 577    """Draws a tiny coordinate system.
 578
 579    Args:
 580        canvas: The canvas to draw on.
 581        pos (tuple, optional): The position of the coordinate system. Defaults to (0, 0).
 582        width (int, optional): The length of the x-axis. Defaults to 25.
 583        height (int, optional): The length of the y-axis. Defaults to 25.
 584        neg_width (int, optional): The negative length of the x-axis. Defaults to 5.
 585        neg_height (int, optional): The negative length of the y-axis. Defaults to 5.
 586    """
 587    x, y = pos[:2]
 588    canvas.circle((x, y), 2, fill=False, line_color=colors.gray)
 589    canvas.draw(Shape([(x - neg_width, y), (x + width, y)]), line_color=colors.gray)
 590    canvas.draw(Shape([(x, y - neg_height), (x, y + height)]), line_color=colors.gray)
 591
 592
 593def draw_cs_small(canvas, pos=(0, 0), width=80, height=100, neg_width=5, neg_height=5):
 594    """Draws a small coordinate system.
 595
 596    Args:
 597        canvas: The canvas to draw on.
 598        pos (tuple, optional): The position of the coordinate system. Defaults to (0, 0).
 599        width (int, optional): The length of the x-axis. Defaults to 80.
 600        height (int, optional): The length of the y-axis. Defaults to 100.
 601        neg_width (int, optional): The negative length of the x-axis. Defaults to 5.
 602        neg_height (int, optional): The negative length of the y-axis. Defaults to 5.
 603    """
 604    x, y = pos[:2]
 605    x_axis = arrow(
 606        (-neg_width + x, y), (width + 10 + x, y), head_length=8, head_width=2
 607    )
 608    y_axis = arrow(
 609        (x, -neg_height + y), (x, height + 10 + y), head_length=8, head_width=2
 610    )
 611    canvas.draw(x_axis, line_width=1)
 612    canvas.draw(y_axis, line_width=1)
 613
 614
 615def arrow(
 616    p1,
 617    p2,
 618    head_length=10,
 619    head_width=4,
 620    line_width=1,
 621    line_color=colors.black,
 622    fill_color=colors.black,
 623    centered=False,
 624):
 625    """Return an arrow from p1 to p2.
 626
 627    Args:
 628        p1 (tuple): The starting point of the arrow.
 629        p2 (tuple): The ending point of the arrow.
 630        head_length (int, optional): The length of the arrow head. Defaults to 10.
 631        head_width (int, optional): The width of the arrow head. Defaults to 4.
 632        line_width (int, optional): The width of the arrow line. Defaults to 1.
 633        line_color (Color, optional): The color of the arrow line. Defaults to colors.black.
 634        fill_color (Color, optional): The fill color of the arrow head. Defaults to colors.black.
 635        centered (bool, optional): Whether the arrow is centered. Defaults to False.
 636
 637    Returns:
 638        Batch: A Batch object containing the arrow shapes.
 639    """
 640    x1, y1 = p1[:2]
 641    x2, y2 = p2[:2]
 642    dx = x2 - x1
 643    dy = y2 - y1
 644    angle = atan2(dy, dx)
 645    body = Shape(
 646        [(x1, y1), (x2, y2)],
 647        closed=False,
 648        line_color=line_color,
 649        fill_color=fill_color,
 650        line_width=line_width,
 651    )
 652    w2 = head_width / 2
 653    head = Shape(
 654        [(-head_length, w2), (0, 0), (-head_length, -w2)],
 655        closed=True,
 656        line_color=line_color,
 657        fill_color=fill_color,
 658        line_width=line_width,
 659    )
 660    head.rotate(angle)
 661    if centered:
 662        head.translate(*mid_point((x1, y1), (x2, y2)))
 663    else:
 664        head.translate(x2, y2)
 665    return Batch([body, head])
 666
 667
 668class ArcArrow(Batch):
 669    """An ArcArrow object is an arrow with an arc.
 670
 671    Args:
 672        center (Point): The center of the arc.
 673        radius (float): The radius of the arc.
 674        start_angle (float): The starting angle of the arc.
 675        end_angle (float): The ending angle of the arc.
 676        xform_matrix (array, optional): The transformation matrix. Defaults to None.
 677        **kwargs: Additional keyword arguments for arc arrow styling.
 678    """
 679
 680    def __init__(
 681        self,
 682        center: Point,
 683        radius: float,
 684        start_angle: float,
 685        end_angle: float,
 686        xform_matrix: array = None,
 687        **kwargs,
 688    ):
 689        self.center = center
 690        self.radius = radius
 691        self.start_angle = start_angle
 692        self.end_angle = end_angle
 693        # create the arc
 694        self.arc = Arc(center, radius, start_angle, end_angle)
 695        self.arc.fill = False
 696        # create arrow_head1
 697        self.arrow_head1 = ArrowHead()
 698        # create arrow_head2
 699        self.arrow_head2 = ArrowHead()
 700        start = self.arc.start_point
 701        end = self.arc.end_point
 702        self.points = [center, start, end]
 703
 704        self.arrow_head1.translate(-1 * self.arrow_head1.head_length, 0)
 705        self.arrow_head1.rotate(start_angle - pi / 2)
 706        self.arrow_head1.translate(*start)
 707        self.arrow_head2.translate(-1 * self.arrow_head2.head_length, 0)
 708        self.arrow_head2.rotate(end_angle + pi / 2)
 709        self.arrow_head2.translate(*end)
 710        items = [self.arc, self.arrow_head1, self.arrow_head2]
 711        super().__init__(items, subtype=Types.ARC_ARROW, **kwargs)
 712        for k, v in kwargs.items():
 713            if k in shape_style_map:
 714                setattr(self, k, v)  # we should check for valid values here
 715            else:
 716                raise AttributeError(f"{k}. Invalid attribute!")
 717        self.xform_matrix = get_transform(xform_matrix)
 718
 719
 720class Arrow(Batch):
 721    """An Arrow object is a line with an arrow head.
 722
 723    Args:
 724        p1 (Point): The starting point of the arrow.
 725        p2 (Point): The ending point of the arrow.
 726        head_pos (HeadPos, optional): The position of the arrow head. Defaults to HeadPos.END.
 727        head (Shape, optional): The shape of the arrow head. Defaults to None.
 728        **kwargs: Additional keyword arguments for arrow styling.
 729    """
 730
 731    def __init__(
 732        self,
 733        p1: Point,
 734        p2: Point,
 735        head_pos: HeadPos = HeadPos.END,
 736        head: Shape = None,
 737        **kwargs,
 738    ):
 739        self.p1 = p1
 740        self.p2 = p2
 741        self.head_pos = head_pos
 742        self.head = head
 743        self.kwargs = kwargs
 744        length = distance(p1, p2)
 745        angle = line_angle(p1, p2)
 746        self.line = Shape([(0, 0), (length, 0)])
 747        if head is None:
 748            self.head = ArrowHead()
 749        else:
 750            self.head = head
 751        if self.head_pos == HeadPos.END:
 752            x = length
 753            self.head.translate(x - self.head.head_length, 0)
 754            self.head.rotate(angle)
 755            self.line.rotate(angle)
 756            self.line.translate(*p1)
 757            self.head.translate(*p1)
 758            self.heads = [self.head]
 759        elif self.head_pos == HeadPos.START:
 760            self.head = [None]
 761        elif self.head_pos == HeadPos.BOTH:
 762            self.head2 = ArrowHead()
 763            self.head2.rotate(pi)
 764            self.head2.translate(self.head2.head_length, 0)
 765            self.head2.rotate(angle)
 766            self.head2.translate(*p1)
 767            x = length
 768            self.head.translate(x - self.head.head_length, 0)
 769            self.head.rotate(angle)
 770            self.line.rotate(angle)
 771            self.line.translate(*p1)
 772            self.head.translate(*p1)
 773            self.heads = [self.head, self.head2]
 774        elif self.head_pos == HeadPos.NONE:
 775            self.heads = [None]
 776
 777        items = [self.line] + self.heads
 778        super().__init__(items, subtype=Types.ARROW, **kwargs)
 779
 780
 781class AngularDimension(Batch):
 782    """An AngularDimension object is a dimension that represents an angle.
 783
 784    Args:
 785        center (Point): The center of the angle.
 786        radius (float): The radius of the angle.
 787        start_angle (float): The starting angle.
 788        end_angle (float): The ending angle.
 789        ext_angle (float): The extension angle.
 790        gap_angle (float): The gap angle.
 791        text_offset (float, optional): The text offset. Defaults to None.
 792        gap (float, optional): The gap. Defaults to None.
 793        **kwargs: Additional keyword arguments for angular dimension styling.
 794    """
 795
 796    def __init__(
 797        self,
 798        center: Point,
 799        radius: float,
 800        start_angle: float,
 801        end_angle: float,
 802        ext_angle: float,
 803        gap_angle: float,
 804        text_offset: float = None,
 805        gap: float = None,
 806        **kwargs,
 807    ):
 808        text_offset, gap = get_defaults(["text_offset", "gap"], [text_offset, gap])
 809        self.center = center
 810        self.radius = radius
 811        self.start_angle = start_angle
 812        self.end_angle = end_angle
 813        self.ext_angle = ext_angle
 814        self.gap_angle = gap_angle
 815        self.text_offset = text_offset
 816        self.gap = gap
 817        super().__init__(subtype=Types.ANGULAR_DIMENSION, **kwargs)
 818
 819
 820class Dimension(Batch):
 821    """A Dimension object is a line with arrows and a text.
 822
 823    Args:
 824        text (str): The text of the dimension.
 825        p1 (Point): The starting point of the dimension.
 826        p2 (Point): The ending point of the dimension.
 827        ext_length (float): The length of the extension lines.
 828        ext_length2 (float, optional): The length of the second extension line. Defaults to None.
 829        orientation (Anchor, optional): The orientation of the dimension. Defaults to None.
 830        text_pos (Anchor, optional): The position of the text. Defaults to Anchor.CENTER.
 831        text_offset (float, optional): The offset of the text. Defaults to 0.
 832        gap (float, optional): The gap. Defaults to None.
 833        reverse_arrows (bool, optional): Whether to reverse the arrows. Defaults to False.
 834        reverse_arrow_length (float, optional): The length of the reversed arrows. Defaults to None.
 835        parallel (bool, optional): Whether the dimension is parallel. Defaults to False.
 836        ext1pnt (Point, optional): The first extension point. Defaults to None.
 837        ext2pnt (Point, optional): The second extension point. Defaults to None.
 838        scale (float, optional): The scale factor. Defaults to 1.
 839        font_size (int, optional): The font size. Defaults to 12.
 840        **kwargs: Additional keyword arguments for dimension styling.
 841    """
 842
 843    # To do: This is too long and convoluted. Refactor it.
 844    def __init__(
 845        self,
 846        text: str,
 847        p1: Point,
 848        p2: Point,
 849        ext_length: float,
 850        ext_length2: float = None,
 851        orientation: Anchor = None,
 852        text_pos: Anchor = Anchor.CENTER,
 853        text_offset: float = 0,
 854        gap: float = None,
 855        reverse_arrows: bool = False,
 856        reverse_arrow_length: float = None,
 857        parallel: bool = False,
 858        ext1pnt: Point = None,
 859        ext2pnt: Point = None,
 860        scale: float = 1,
 861        font_size: int = 12,
 862        **kwargs,
 863    ):
 864        ext_length2, gap, reverse_arrow_length = get_defaults(
 865            ["ext_length2", "gap", "rev_arrow_length"],
 866            [ext_length2, gap, reverse_arrow_length],
 867        )
 868        if text == "":
 869            self.text = str(distance(p1, p2) / scale)
 870        else:
 871            self.text = text
 872        self.p1 = p1
 873        self.p2 = p2
 874        self.ext_length = ext_length
 875        self.ext_length2 = ext_length2
 876        self.orientation = orientation
 877        self.text_pos = text_pos
 878        self.text_offset = text_offset
 879        self.gap = gap
 880        self.reverse_arrows = reverse_arrows
 881        self.reverse_arrow_length = reverse_arrow_length
 882        self.kwargs = kwargs
 883        self.ext1 = None
 884        self.ext2 = None
 885        self.ext3 = None
 886        self.arrow1 = None
 887        self.arrow2 = None
 888        self.dim_line = None
 889        self.mid_line = None
 890        self.ext1pnt = ext1pnt
 891        self.ext2pnt = ext2pnt
 892        x1, y1 = p1[:2]
 893        x2, y2 = p2[:2]
 894
 895        # px1_1 : extension1 point 1
 896        # px1_2 : extension1 point 2
 897        # px2_1 : extension2 point 1
 898        # px2_2 : extension2 point 2
 899        # px3_1 : extension3 point 1
 900        # px3_2 : extension3 point 2
 901        # pa1 : arrow point 1
 902        # pa2 : arrow point 2
 903        # ptext : text point
 904        super().__init__(subtype=Types.DIMENSION, **kwargs)
 905        dist_tol = defaults["dist_tol"]
 906        if font_size is not None:
 907            self.font_size = font_size
 908        if parallel:
 909            if orientation is None:
 910                orientation = Anchor.NORTHEAST
 911
 912            if orientation == Anchor.NORTHEAST:
 913                angle = line_angle(p1, p2) + pi / 2
 914            elif orientation == Anchor.NORTHWEST:
 915                angle = line_angle(p1, p2) + pi / 2
 916            elif orientation == Anchor.SOUTHEAST:
 917                angle = line_angle(p1, p2) - pi / 2
 918            elif orientation == Anchor.SOUTHWEST:
 919                angle = line_angle(p1, p2) + pi / 2
 920            if self.ext1pnt is None:
 921                px1_1 = line_by_point_angle_length(p1, angle, self.gap)[1]
 922            else:
 923                px1_1 = self.ext1pnt
 924            px1_2 = line_by_point_angle_length(p1, angle, self.gap + self.ext_length)[1]
 925            if self.ext2pnt is None:
 926                px2_1 = line_by_point_angle_length(p2, angle, self.gap)[1]
 927            else:
 928                px2_1 = self.ext2pnt
 929            px2_2 = line_by_point_angle_length(p2, angle, self.gap + self.ext_length)[1]
 930
 931            pa1 = line_by_point_angle_length(px1_2, angle, self.gap * -1.5)[1]
 932            pa2 = line_by_point_angle_length(px2_2, angle, self.gap * -1.5)[1]
 933
 934            self.text_pos = mid_point(pa1, pa2)
 935            self.dim_line = Arrow(pa1, pa2, head_pos=HeadPos.BOTH)
 936            self.ext1 = Shape([px1_1, px1_2])
 937            self.ext2 = Shape([px2_1, px2_2])
 938            self.append(self.dim_line)
 939            self.append(self.ext1)
 940            self.append(self.ext2)
 941
 942        else:
 943            if abs(x1 - x2) < dist_tol:
 944                # vertical line
 945                if self.orientation is None:
 946                    orientation = Anchor.EAST
 947
 948                if orientation in [Anchor.WEST, Anchor.SOUTHWEST, Anchor.NORTHWEST]:
 949                    x = x1 - self.gap
 950                    px1_1 = (x, y1)
 951                    px1_2 = (x - ext_length, y1)
 952                    px2_1 = (x, y2)
 953                    px2_2 = (x - ext_length, y2)
 954                    x = px1_2[0] + self.gap * 1.5
 955                    pa1 = (x, y1)
 956                    pa2 = (x, y2)
 957                elif orientation in [Anchor.EAST, Anchor.SOUTHEAST, Anchor.NORTHEAST]:
 958                    x = x1 + self.gap
 959                    px1_1 = (x, y1)
 960                    px1_2 = (x + ext_length, y1)
 961                    px2_1 = (x, y2)
 962                    px2_2 = (x + ext_length, y2)
 963                    x = px1_2[0] - self.gap * 1.5
 964                    pa1 = (x, y1)
 965                    pa2 = (x, y2)
 966                elif orientation == Anchor.CENTER:
 967                    pa1 = (x1, y1)
 968                    pa2 = (x1, y2)
 969                x = pa1[0]
 970                if orientation in [Anchor.SOUTHWEST, Anchor.SOUTHEAST]:
 971                    px3_1 = pa2
 972                    y = y2 - self.ext_length2
 973                    px3_2 = (x, y)
 974                    self.ext3 = Shape([px3_1, px3_2])
 975                    self.text_pos = (x, y - self.text_offset)
 976                elif orientation in [Anchor.NORTHWEST, Anchor.NORTHEAST]:
 977                    px3_1 = pa1
 978                    y = y1 + self.ext_length2
 979                    px3_2 = (x, y)
 980                    self.ext3 = Shape([px3_1, px3_2])
 981                    self.text_pos = (x, y + self.text_offset)
 982                elif orientation == Anchor.SOUTH:
 983                    px3_1 = pa2
 984                    y = y2 - self.ext_length2
 985                    px3_2 = (x, y)
 986                    self.ext3 = Shape([px3_1, px3_2])
 987                    self.text_pos = (x, y - self.text_offset)
 988                elif orientation == Anchor.NORTH:
 989                    px3_2 = pa1
 990                    y = y2 + self.ext_length2
 991                    px3_1 = (x, y)
 992                    self.ext3 = Shape([px3_1, px3_2])
 993                    self.text_pos = (x, y + self.text_offset)
 994                else:
 995                    self.text_pos = (x, y1 - (y1 - y2) / 2)
 996                if orientation not in [Anchor.CENTER, Anchor.NORTH, Anchor.SOUTH]:
 997                    if self.ext1pnt is None:
 998                        self.ext1 = Shape([px1_1, px1_2])
 999                    else:
1000                        self.ext1 = Shape([ext1pnt, px1_2])
1001                    if self.ext2pnt is None:
1002                        self.ext2 = Shape([px2_1, px2_2])
1003                    else:
1004                        self.ext2 = Shape([ext2pnt, px2_2])
1005            elif abs(y1 - y2) < dist_tol:
1006                # horizontal line
1007                if self.orientation is None:
1008                    orientation = Anchor.SOUTH
1009
1010                if orientation in [Anchor.SOUTH, Anchor.SOUTHWEST, Anchor.SOUTHEAST]:
1011                    y = y1 - self.gap
1012                    px1_1 = (x1, y)
1013                    px1_2 = (x1, y - ext_length)
1014                    px2_1 = (x2, y)
1015                    px2_2 = (x2, y - ext_length)
1016                    y = px1_2[1] + self.gap * 1.5
1017                    pa1 = (x1, y)
1018                    pa2 = (x2, y)
1019                elif orientation in [Anchor.NORTH, Anchor.NORTHWEST, Anchor.NORTHEAST]:
1020                    y = y1 + self.gap
1021                    px1_1 = (x1, y)
1022                    px1_2 = (x1, y + ext_length)
1023                    px2_1 = (x2, y)
1024                    px2_2 = (x2, y + ext_length)
1025                    y = px1_2[1] - self.gap * 1.5
1026                    pa1 = (x1, y)
1027                    pa2 = (x2, y)
1028                elif orientation in [Anchor.WEST, Anchor.EAST]:
1029                    pa1 = (x1, y1)
1030                    pa2 = (x2, y2)
1031                    if orientation == Anchor.WEST:
1032                        px3_1 = (pa1[0] - self.ext_length2, pa1[1])
1033                        px3_2 = pa1
1034                        self.text_pos = (px3_1[0] - self.text_offset, pa1[1])
1035                    else:
1036                        px3_1 = pa2
1037                        px3_2 = (pa2[0] + self.ext_length2, pa1[1])
1038                        self.text_pos = (px3_1[0] + self.text_offset, pa1[1])
1039                    self.ext3 = Shape([px3_1, px3_2])
1040                elif orientation == Anchor.CENTER:
1041                    pa1 = (x1, y1)
1042                    pa2 = (x2, y2)
1043
1044                y = pa1[1]
1045                if orientation in [Anchor.SOUTHWEST, Anchor.NORTHWEST]:
1046                    px3_1 = pa1
1047                    x = x1 - self.ext_length2
1048                    px3_2 = (x, y)
1049                    self.ext3 = Shape([px3_1, px3_2])
1050                    self.text_pos = (x - self.text_offset, y)
1051                elif orientation in [Anchor.NORTHEAST, Anchor.SOUTHEAST]:
1052                    px3_1 = pa2
1053                    x = x2 + self.ext_length2
1054                    px3_2 = (x, y)
1055                    self.ext3 = Shape([px3_1, px3_2])
1056                    self.text_pos = (x + self.text_offset, y)
1057                elif orientation in [Anchor.CENTER, Anchor.NORTH, Anchor.SOUTH]:
1058                    self.text_pos = (x1 + (x2 - x1) / 2, y)
1059
1060                if orientation not in [Anchor.CENTER, Anchor.WEST, Anchor.EAST]:
1061                    if self.ext1pnt is None:
1062                        self.ext1 = Shape([px1_1, px1_2])
1063                    else:
1064                        self.ext1Shape([ext1pnt, px1_2])
1065                    if self.ext2pnt is None:
1066                        self.ext2 = Shape([px2_1, px2_2])
1067                    else:
1068                        self.ext2 = Shape([ext2pnt, px2_2])
1069
1070            if self.reverse_arrows:
1071                dist = self.reverse_arrow_length
1072                p2 = extended_line(dist, [pa1, pa2])[1]
1073                self.arrow1 = Arrow(p2, pa2)
1074                p2 = extended_line(dist, [pa2, pa1])[1]
1075                self.arrow2 = Arrow(p2, pa1)
1076                self.append(self.arrow1)
1077                self.append(self.arrow2)
1078                self.mid_line = Shape([pa1, pa2])
1079                self.append(self.mid_line)
1080                dist = self.text_offset + self.reverse_arrow_length
1081                if orientation in [Anchor.EAST, Anchor.NORTHEAST, Anchor.NORTH]:
1082
1083                    self.text_pos = extended_line(dist, [pa1, pa2])[1]
1084                else:
1085                    self.text_pos = extended_line(dist, [pa2, pa1])[1]
1086            else:
1087                self.dim_line = Arrow(pa1, pa2, head_pos=HeadPos.BOTH)
1088                self.append(self.dim_line)
1089            if self.ext1 is not None:
1090                self.append(self.ext1)
1091
1092            if self.ext2 is not None:
1093                self.append(self.ext2)
1094
1095            if self.ext3 is not None:
1096                self.append(self.ext3)
@dataclass
class Color:
116@dataclass
117class Color:
118    """A class representing an RGB or RGBA color.
119
120    This class represents a color in RGB or RGBA color space. The default values
121    for the components are normalized between 0.0 and 1.0. Values outside this range
122    are automatically converted from the 0-255 range.
123
124    Attributes:
125        red: The red component of the color (0.0 to 1.0).
126        green: The green component of the color (0.0 to 1.0).
127        blue: The blue component of the color (0.0 to 1.0).
128        alpha: The alpha (transparency) component (0.0 to 1.0), default is 1.
129        space: The color space, default is "rgb".
130
131    Examples:
132        >>> red = Color(1.0, 0.0, 0.0)
133        >>> transparent_blue = Color(0.0, 0.0, 1.0, 0.5)
134        >>> rgb255 = Color(255, 0, 128)  # Will be automatically normalized
135    """
136    red: int = 0
137    green: int = 0
138    blue: int = 0
139    alpha: int = 1
140    space: ColorSpace = "rgb"  # for future use
141
142    def __post_init__(self):
143        """Post-initialization to ensure color values are in the correct range."""
144        r, g, b = self.red, self.green, self.blue
145        if r < 0 or r > 1 or g < 0 or g > 1 or b < 0 or b > 1:
146            self.red = r / 255
147            self.green = g / 255
148            self.blue = b / 255
149        if self.alpha < 0 or self.alpha > 1:
150            self.alpha = self.alpha / 255
151        common_properties(self)
152
153    def __str__(self):
154        return f"Color({self.red}, {self.green}, {self.blue})"
155
156    def __repr__(self):
157        return f"Color({self.red}, {self.green}, {self.blue})"
158
159    def copy(self):
160        return Color(self.red, self.green, self.blue, self.alpha)
161
162    @property
163    def __key__(self):
164        return (self.red, self.green, self.blue)
165
166    def __hash__(self):
167        return hash(self.__key__)
168
169    @property
170    def name(self):
171        # search for the color in the named colors
172        pass
173
174    def __eq__(self, other):
175        if isinstance(other, Color):
176            return self.__key__ == other.__key__
177        else:
178            return False
179
180    @property
181    def rgb(self):
182        return (self.red, self.green, self.blue)
183
184    @property
185    def rgba(self):
186        return (self.red, self.green, self.blue, self.alpha)
187
188    @property
189    def rgb255(self):
190        r, g, b = self.rgb
191        if r > 1 or g > 1 or b > 1:
192            return (r, g, b)
193        return tuple(round(i * 255) for i in self.rgb)
194
195    @property
196    def rgba255(self):
197        return tuple(round(i * 255) for i in self.rgba)

A class representing an RGB or RGBA color.

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

Attributes:
  • red: The red component of the color (0.0 to 1.0).
  • green: The green component of the color (0.0 to 1.0).
  • blue: The blue component of the color (0.0 to 1.0).
  • alpha: The alpha (transparency) component (0.0 to 1.0), default is 1.
  • space: The color space, default is "rgb".
Examples:
>>> red = Color(1.0, 0.0, 0.0)
>>> transparent_blue = Color(0.0, 0.0, 1.0, 0.5)
>>> rgb255 = Color(255, 0, 128)  # Will be automatically normalized
Color( red: int = 0, green: int = 0, blue: int = 0, alpha: int = 1, space: simetri.graphics.all_enums.ColorSpace = 'rgb')
red: int = 0
green: int = 0
blue: int = 0
alpha: int = 1
def copy(self):
159    def copy(self):
160        return Color(self.red, self.green, self.blue, self.alpha)
name
169    @property
170    def name(self):
171        # search for the color in the named colors
172        pass
rgb
180    @property
181    def rgb(self):
182        return (self.red, self.green, self.blue)
rgba
184    @property
185    def rgba(self):
186        return (self.red, self.green, self.blue, self.alpha)
rgb255
188    @property
189    def rgb255(self):
190        r, g, b = self.rgb
191        if r > 1 or g > 1 or b > 1:
192            return (r, g, b)
193        return tuple(round(i * 255) for i in self.rgb)
rgba255
195    @property
196    def rgba255(self):
197        return tuple(round(i * 255) for i in self.rgba)
def array(unknown):

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

Create an array.

Parameters

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

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

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

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

*New in version 1.20.0.*

Returns

out : ndarray An array object satisfying the specified requirements.

See Also

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

Notes

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

Examples

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

Upcasting:

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

More than one dimension:

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

Minimum dimensions 2:

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

Type provided:

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

Data-type consisting of more than one element:

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

Creating an array from sub-classes:

>>> np.array(np.asmatrix('1 2; 3 4'))
array([[1, 2],
       [3, 4]])
>>> np.array(np.asmatrix('1 2; 3 4'), subok=True)
matrix([[1, 2],
        [3, 4]])
def convert_latex_font_size(latex_font_size: simetri.graphics.all_enums.FontSize):
129def convert_latex_font_size(latex_font_size: FontSize):
130    """Converts LaTeX font size to a numerical value.
131
132    Args:
133        latex_font_size (FontSize): The LaTeX font size.
134
135    Returns:
136        int: The corresponding numerical font size.
137    """
138    d_font_size = {
139        FontSize.TINY: 5,
140        FontSize.SMALL: 7,
141        FontSize.NORMAL: 10,
142        FontSize.LARGE: 12,
143        FontSize.LARGE2: 14,
144        FontSize.LARGE3: 17,
145        FontSize.HUGE: 20,
146        FontSize.HUGE2: 25,
147    }
148
149    return d_font_size[latex_font_size]

Converts LaTeX font size to a numerical value.

Arguments:
  • latex_font_size (FontSize): The LaTeX font size.
Returns:

int: The corresponding numerical font size.

def letter_F_points():
152def letter_F_points():
153    """Returns the points of the capital letter F.
154
155    Returns:
156        list: A list of points representing the letter F.
157    """
158    return [
159        (0.0, 0.0),
160        (20.0, 0.0),
161        (20.0, 40.0),
162        (40.0, 40.0),
163        (40.0, 60.0),
164        (20.0, 60.0),
165        (20.0, 80.0),
166        (50.0, 80.0),
167        (50.0, 100.0),
168        (0.0, 100.0),
169        (0.0, 0.0),
170    ]

Returns the points of the capital letter F.

Returns:

list: A list of points representing the letter F.

def letter_F(scale=1, **kwargs):
173def letter_F(scale=1, **kwargs):
174    """Returns a Shape object representing the capital letter F.
175
176    Args:
177        scale (int, optional): Scale factor for the letter. Defaults to 1.
178        **kwargs: Additional keyword arguments for shape styling.
179
180    Returns:
181        Shape: A Shape object representing the letter F.
182    """
183    F = Shape(letter_F_points(), closed=True)
184    if scale != 1:
185        F.scale(scale)
186    for k, v in kwargs.items():
187        if k in shape_style_map:
188            setattr(F, k, v)
189        else:
190            raise AttributeError(f"{k}. Invalid attribute!")
191    return F

Returns a Shape object representing the capital letter F.

Arguments:
  • scale (int, optional): Scale factor for the letter. Defaults to 1.
  • **kwargs: Additional keyword arguments for shape styling.
Returns:

Shape: A Shape object representing the letter F.

def cube(size: float = 100):
194def cube(size: float = 100):
195    """Returns a Batch object representing a cube.
196
197    Args:
198        size (float, optional): The size of the cube. Defaults to 100.
199
200    Returns:
201        Batch: A Batch object representing the cube.
202    """
203    points = reg_poly_points_side_length((0, 0), 6, size)
204    center = (0, 0)
205    face1 = Shape([points[0], center] + points[4:], closed=True)
206    cube_ = face1.rotate(-2 * pi / 3, (0, 0), reps=2)
207    cube_[0].fill_color = Color(0.3, 0.3, 0.3)
208    cube_[1].fill_color = Color(0.4, 0.4, 0.4)
209    cube_[2].fill_color = Color(0.6, 0.6, 0.6)
210
211    return cube_

Returns a Batch object representing a cube.

Arguments:
  • size (float, optional): The size of the cube. Defaults to 100.
Returns:

Batch: A Batch object representing the cube.

def pdf_to_svg(pdf_path, svg_path):
214def pdf_to_svg(pdf_path, svg_path):
215    """Converts a single-page PDF file to SVG.
216
217    Args:
218        pdf_path (str): The path to the PDF file.
219        svg_path (str): The path to save the SVG file.
220    """
221    doc = fitz.open(pdf_path)
222    page = doc.load_page(0)
223    svg = page.get_svg_image()
224    with open(svg_path, "w", encoding="utf-8") as f:
225        f.write(svg)

Converts a single-page PDF file to SVG.

Arguments:
  • pdf_path (str): The path to the PDF file.
  • svg_path (str): The path to save the SVG file.
class Annotation(simetri.graphics.batch.Batch):
230class Annotation(Batch):
231    """An Annotation object is a label with an arrow pointing to a specific location.
232
233    Args:
234        text (str): The annotation text.
235        pos (tuple): The position of the annotation.
236        frame (FrameShape): The frame shape of the annotation.
237        root_pos (tuple): The root position of the arrow.
238        arrow_line (ArrowLine, optional): The type of arrow line. Defaults to ArrowLine.STRAIGHT_END.
239        **kwargs: Additional keyword arguments for annotation styling.
240    """
241
242    def __init__(
243        self, text, pos, frame, root_pos, arrow_line=ArrowLine.STRAIGHT_END, **kwargs
244    ):
245        self.text = text
246        self.pos = pos
247        self.frame = frame
248        self.root_pos = root_pos
249        self.arrow_line = arrow_line
250        self.kwargs = kwargs
251
252        super().__init__(subtype=Types.ANNOTATION, **kwargs)

An Annotation object is a label with an arrow pointing to a specific location.

Arguments:
  • text (str): The annotation text.
  • pos (tuple): The position of the annotation.
  • frame (FrameShape): The frame shape of the annotation.
  • root_pos (tuple): The root position of the arrow.
  • arrow_line (ArrowLine, optional): The type of arrow line. Defaults to ArrowLine.STRAIGHT_END.
  • **kwargs: Additional keyword arguments for annotation styling.
Annotation( text, pos, frame, root_pos, arrow_line=<ArrowLine.STRAIGHT_END: 'straight end'>, **kwargs)
242    def __init__(
243        self, text, pos, frame, root_pos, arrow_line=ArrowLine.STRAIGHT_END, **kwargs
244    ):
245        self.text = text
246        self.pos = pos
247        self.frame = frame
248        self.root_pos = root_pos
249        self.arrow_line = arrow_line
250        self.kwargs = kwargs
251
252        super().__init__(subtype=Types.ANNOTATION, **kwargs)

Initialize a Batch object.

Arguments:
  • elements (Sequence[Any], optional): The elements to include in the batch.
  • modifiers (Sequence[Modifier], optional): The modifiers to apply to the batch.
  • subtype (Types, optional): The subtype of the batch.
  • kwargs (dict): Additional keyword arguments.
text
pos
frame
root_pos
arrow_line
kwargs
@dataclass
class TagFrame:
255@dataclass
256class TagFrame:
257    """Frame objects are used with Tag objects to create boxes.
258
259    Args:
260        frame_shape (FrameShape, optional): The shape of the frame. Defaults to "rectangle".
261        line_width (float, optional): The width of the frame line. Defaults to 1.
262        line_dash_array (list, optional): The dash pattern for the frame line. Defaults to None.
263        line_join (LineJoin, optional): The line join style. Defaults to "miter".
264        line_color (Color, optional): The color of the frame line. Defaults to colors.black.
265        back_color (Color, optional): The background color of the frame. Defaults to colors.white.
266        fill (bool, optional): Whether to fill the frame. Defaults to False.
267        stroke (bool, optional): Whether to stroke the frame. Defaults to True.
268        double (bool, optional): Whether to use a double line. Defaults to False.
269        double_distance (float, optional): The distance between double lines. Defaults to 2.
270        inner_sep (float, optional): The inner separation. Defaults to 10.
271        outer_sep (float, optional): The outer separation. Defaults to 10.
272        smooth (bool, optional): Whether to smooth the frame. Defaults to False.
273        rounded_corners (bool, optional): Whether to use rounded corners. Defaults to False.
274        fillet_radius (float, optional): The radius of the fillet. Defaults to 10.
275        draw_fillets (bool, optional): Whether to draw fillets. Defaults to False.
276        blend_mode (str, optional): The blend mode. Defaults to None.
277        gradient (str, optional): The gradient. Defaults to None.
278        pattern (str, optional): The pattern. Defaults to None.
279        min_width (float, optional): The minimum width. Defaults to None.
280        min_height (float, optional): The minimum height. Defaults to None.
281        min_size (float, optional): The minimum size. Defaults to None.
282    """
283
284    frame_shape: FrameShape = "rectangle"
285    line_width: float = 1
286    line_dash_array: list = None
287    line_join: LineJoin = "miter"
288    line_color: Color = colors.black
289    back_color: Color = colors.white
290    fill: bool = False
291    stroke: bool = True
292    double: bool = False
293    double_distance: float = 2
294    inner_sep: float = 10
295    outer_sep: float = 10
296    smooth: bool = False
297    rounded_corners: bool = False
298    fillet_radius: float = 10
299    draw_fillets: bool = False
300    blend_mode: str = None
301    gradient: str = None
302    pattern: str = None
303    min_width: float = None
304    min_height: float = None
305    min_size: float = None
306
307    def __post_init__(self):
308        self.type = Types.FRAME
309        self.subtype = Types.FRAME
310        common_properties(self, id_only=True)

Frame objects are used with Tag objects to create boxes.

Arguments:
  • frame_shape (FrameShape, optional): The shape of the frame. Defaults to "rectangle".
  • line_width (float, optional): The width of the frame line. Defaults to 1.
  • line_dash_array (list, optional): The dash pattern for the frame line. Defaults to None.
  • line_join (LineJoin, optional): The line join style. Defaults to "miter".
  • line_color (Color, optional): The color of the frame line. Defaults to colors.black.
  • back_color (Color, optional): The background color of the frame. Defaults to colors.white.
  • fill (bool, optional): Whether to fill the frame. Defaults to False.
  • stroke (bool, optional): Whether to stroke the frame. Defaults to True.
  • double (bool, optional): Whether to use a double line. Defaults to False.
  • double_distance (float, optional): The distance between double lines. Defaults to 2.
  • inner_sep (float, optional): The inner separation. Defaults to 10.
  • outer_sep (float, optional): The outer separation. Defaults to 10.
  • smooth (bool, optional): Whether to smooth the frame. Defaults to False.
  • rounded_corners (bool, optional): Whether to use rounded corners. Defaults to False.
  • fillet_radius (float, optional): The radius of the fillet. Defaults to 10.
  • draw_fillets (bool, optional): Whether to draw fillets. Defaults to False.
  • blend_mode (str, optional): The blend mode. Defaults to None.
  • gradient (str, optional): The gradient. Defaults to None.
  • pattern (str, optional): The pattern. Defaults to None.
  • min_width (float, optional): The minimum width. Defaults to None.
  • min_height (float, optional): The minimum height. Defaults to None.
  • min_size (float, optional): The minimum size. Defaults to None.
TagFrame( frame_shape: simetri.graphics.all_enums.FrameShape = 'rectangle', line_width: float = 1, line_dash_array: list = None, line_join: simetri.graphics.all_enums.LineJoin = 'miter', line_color: Color = Color(0.0, 0.0, 0.0), back_color: Color = Color(1.0, 1.0, 1.0), fill: bool = False, stroke: bool = True, double: bool = False, double_distance: float = 2, inner_sep: float = 10, outer_sep: float = 10, smooth: bool = False, rounded_corners: bool = False, fillet_radius: float = 10, draw_fillets: bool = False, blend_mode: str = None, gradient: str = None, pattern: str = None, min_width: float = None, min_height: float = None, min_size: float = None)
frame_shape: simetri.graphics.all_enums.FrameShape = 'rectangle'
line_width: float = 1
line_dash_array: list = None
line_color: Color = Color(0.0, 0.0, 0.0)
back_color: Color = Color(1.0, 1.0, 1.0)
fill: bool = False
stroke: bool = True
double: bool = False
double_distance: float = 2
inner_sep: float = 10
outer_sep: float = 10
smooth: bool = False
rounded_corners: bool = False
fillet_radius: float = 10
draw_fillets: bool = False
blend_mode: str = None
gradient: str = None
pattern: str = None
min_width: float = None
min_height: float = None
min_size: float = None
class Tag(simetri.graphics.core.Base):
313class Tag(Base):
314    """A Tag object is very similar to TikZ library's nodes. It is a text with a frame.
315
316    Args:
317        text (str): The text of the tag.
318        pos (Point): The position of the tag.
319        font_family (str, optional): The font family. Defaults to None.
320        font_size (int, optional): The font size. Defaults to None.
321        font_color (Color, optional): The font color. Defaults to None.
322        anchor (Anchor, optional): The anchor point. Defaults to Anchor.CENTER.
323        bold (bool, optional): Whether the text is bold. Defaults to False.
324        italic (bool, optional): Whether the text is italic. Defaults to False.
325        text_width (float, optional): The width of the text. Defaults to None.
326        placement (Placement, optional): The placement of the tag. Defaults to None.
327        minimum_size (float, optional): The minimum size of the tag. Defaults to None.
328        minimum_width (float, optional): The minimum width of the tag. Defaults to None.
329        minimum_height (float, optional): The minimum height of the tag. Defaults to None.
330        frame (TagFrame, optional): The frame of the tag. Defaults to None.
331        xform_matrix (array, optional): The transformation matrix. Defaults to None.
332        **kwargs: Additional keyword arguments for tag styling.
333    """
334
335    def __init__(
336        self,
337        text: str,
338        pos: Point,
339        font_family: str = None,
340        font_size: int = None,
341        font_color: Color = None,
342        anchor: Anchor = Anchor.CENTER,
343        bold: bool = False,
344        italic: bool = False,
345        text_width: float = None,
346        placement: Placement = None,
347        minimum_size: float = None,
348        minimum_width: float = None,
349        minimum_height: float = None,
350        frame=None,
351        xform_matrix=None,
352        **kwargs,
353    ):
354        self.__dict__["style"] = TagStyle()
355        self.__dict__["_style_map"] = tag_style_map
356        self._set_aliases()
357        tag_attribs = list(tag_style_map.keys())
358        tag_attribs.append("subtype")
359        _set_Nones(
360            self,
361            ["font_family", "font_size", "font_color"],
362            [font_family, font_size, font_color],
363        )
364        validate_args(kwargs, tag_attribs)
365        x, y = pos[:2]
366        self._init_pos = array([x, y, 1.0])
367
368        self.text = detokenize(text)
369        if frame is None:
370            self.frame = TagFrame()
371        self.type = Types.TAG
372        self.subtype = Types.TAG
373        self.style = TagStyle()
374        self.style.draw_frame = True
375        if font_family:
376            self.font_family = font_family
377        if font_size:
378            self.font_size = font_size
379        else:
380            self.font_size = defaults["font_size"]
381        if xform_matrix is None:
382            self.xform_matrix = identity_matrix()
383        else:
384            self.xform_matrix = get_transform(xform_matrix)
385
386        self.anchor = anchor
387        self.bold = bold
388        self.italic = italic
389        self.text_width = text_width
390        self.placement = placement
391        self.minimum_size = minimum_size
392        self.minimum_width = minimum_width
393        self.minimum_height = minimum_height
394        for k, v in kwargs.items():
395            setattr(self, k, v)
396
397        x1, y1, x2, y2 = self.text_bounds()
398        w = x2 - x1
399        h = y2 - y1
400        self.points = Points([(0, 0, 1), (w, 0, 1), (w, h, 1), (0, h, 1)])
401        common_properties(self)
402
403    def __setattr__(self, name, value):
404        obj, attrib = self.__dict__["_aliasses"].get(name, (None, None))
405        if obj:
406            setattr(obj, attrib, value)
407        else:
408            self.__dict__[name] = value
409
410    def __getattr__(self, name):
411        obj, attrib = self.__dict__["_aliasses"].get(name, (None, None))
412        if obj:
413            res = getattr(obj, attrib)
414        else:
415            try:
416                res = super().__getattr__(name)
417            except AttributeError:
418                res = self.__dict__[name]
419
420        return res
421
422    def _set_aliases(self):
423        _aliasses = {}
424
425        for alias, path_attrib in self._style_map.items():
426            style_path, attrib = path_attrib
427            obj = self
428            for attrib_name in style_path.split("."):
429                obj = obj.__dict__[attrib_name]
430
431            if obj is not self:
432                _aliasses[alias] = (obj, attrib)
433        self.__dict__["_aliasses"] = _aliasses
434
435    def _update(self, xform_matrix, reps: int = 0):
436        if reps == 0:
437            self.xform_matrix = self.xform_matrix @ xform_matrix
438            res = self
439        else:
440            tags = [self]
441            tag = self
442            for _ in range(reps):
443                tag = tag.copy()
444                tag._update(xform_matrix)
445                tags.append(tag)
446            res = Batch(tags)
447
448        return res
449
450    @property
451    def pos(self) -> Point:
452        """Returns the position of the text.
453
454        Returns:
455            Point: The position of the text.
456        """
457        return (self._init_pos @ self.xform_matrix)[:2].tolist()
458
459    def copy(self) -> "Tag":
460        """Returns a copy of the Tag object.
461
462        Returns:
463            Tag: A copy of the Tag object.
464        """
465        tag = Tag(self.text, self.pos, xform_matrix=self.xform_matrix)
466        tag._init_pos = self._init_pos
467        tag.font_family = self.font_family
468        tag.font_size = self.font_size
469        tag.font_color = self.font_color
470        tag.anchor = self.anchor
471        tag.bold = self.bold
472        tag.italic = self.italic
473        tag.text_width = self.text_width
474        tag.placement = self.placement
475        tag.minimum_size = self.minimum_size
476        tag.minimum_width = self.minimum_width
477
478        return tag
479
480    def text_bounds(self) -> tuple[float, float, float, float]:
481        """Returns the bounds of the text.
482
483        Returns:
484            tuple: The bounds of the text (xmin, ymin, xmax, ymax).
485        """
486        mult = 1 # font size multiplier
487        if self.font_size is None:
488            font_size = defaults["font_size"]
489        elif type(self.font_size) in [int, float]:
490            font_size = self.font_size
491        elif self.font_size in FontSize:
492            font_size = convert_latex_font_size(self.font_size)
493        else:
494            raise ValueError("Invalid font size.")
495        try:
496            font = ImageFont.truetype(f"{self.font_family}.ttf", font_size)
497        except OSError:
498            font = ImageFont.load_default()
499            mult = self.font_size / 10
500        xmin, ymin, xmax, ymax = font.getbbox(self.text)
501        width = xmax - xmin
502        height = ymax - ymin
503        dx = (width * mult) / 2
504        dy = (height * mult) / 2
505        xmin -= dx
506        xmax += dx
507        ymin -= dy
508        ymax += dy
509
510        return xmin, ymin, xmax, ymax
511
512    @property
513    def final_coords(self):
514        """Returns the final coordinates of the text.
515
516        Returns:
517            array: The final coordinates of the text.
518        """
519        return self.points.homogen_coords @ self.xform_matrix
520
521    @property
522    def b_box(self):
523        """Returns the bounding box of the text.
524
525        Returns:
526            tuple: The bounding box of the text.
527        """
528        xmin, ymin, xmax, ymax = self.text_bounds()
529        w2 = (xmax - xmin) / 2
530        h2 = (ymax - ymin) / 2
531        x, y = self.pos
532        inner_sep = self.frame.inner_sep
533        xmin = x - w2 - inner_sep
534        xmax = x + w2 + inner_sep
535        ymin = y - h2 - inner_sep
536        ymax = y + h2 + inner_sep
537        points = [
538            (xmin, ymin),
539            (xmax, ymin),
540            (xmax, ymax),
541            (xmin, ymax),
542        ]
543        return bounding_box(points)
544    def __str__(self) -> str:
545        return f"Tag({self.text})"
546
547    def __repr__(self) -> str:
548        return f"Tag({self.text})"

A Tag object is very similar to TikZ library's nodes. It is a text with a frame.

Arguments:
  • text (str): The text of the tag.
  • pos (Point): The position of the tag.
  • font_family (str, optional): The font family. Defaults to None.
  • font_size (int, optional): The font size. Defaults to None.
  • font_color (Color, optional): The font color. Defaults to None.
  • anchor (Anchor, optional): The anchor point. Defaults to Anchor.CENTER.
  • bold (bool, optional): Whether the text is bold. Defaults to False.
  • italic (bool, optional): Whether the text is italic. Defaults to False.
  • text_width (float, optional): The width of the text. Defaults to None.
  • placement (Placement, optional): The placement of the tag. Defaults to None.
  • minimum_size (float, optional): The minimum size of the tag. Defaults to None.
  • minimum_width (float, optional): The minimum width of the tag. Defaults to None.
  • minimum_height (float, optional): The minimum height of the tag. Defaults to None.
  • frame (TagFrame, optional): The frame of the tag. Defaults to None.
  • xform_matrix (array, optional): The transformation matrix. Defaults to None.
  • **kwargs: Additional keyword arguments for tag styling.
Tag( text: str, pos: Sequence[float], font_family: str = None, font_size: int = None, font_color: Color = None, anchor: simetri.graphics.all_enums.Anchor = <Anchor.CENTER: 'center'>, bold: bool = False, italic: bool = False, text_width: float = None, placement: simetri.graphics.all_enums.Placement = None, minimum_size: float = None, minimum_width: float = None, minimum_height: float = None, frame=None, xform_matrix=None, **kwargs)
335    def __init__(
336        self,
337        text: str,
338        pos: Point,
339        font_family: str = None,
340        font_size: int = None,
341        font_color: Color = None,
342        anchor: Anchor = Anchor.CENTER,
343        bold: bool = False,
344        italic: bool = False,
345        text_width: float = None,
346        placement: Placement = None,
347        minimum_size: float = None,
348        minimum_width: float = None,
349        minimum_height: float = None,
350        frame=None,
351        xform_matrix=None,
352        **kwargs,
353    ):
354        self.__dict__["style"] = TagStyle()
355        self.__dict__["_style_map"] = tag_style_map
356        self._set_aliases()
357        tag_attribs = list(tag_style_map.keys())
358        tag_attribs.append("subtype")
359        _set_Nones(
360            self,
361            ["font_family", "font_size", "font_color"],
362            [font_family, font_size, font_color],
363        )
364        validate_args(kwargs, tag_attribs)
365        x, y = pos[:2]
366        self._init_pos = array([x, y, 1.0])
367
368        self.text = detokenize(text)
369        if frame is None:
370            self.frame = TagFrame()
371        self.type = Types.TAG
372        self.subtype = Types.TAG
373        self.style = TagStyle()
374        self.style.draw_frame = True
375        if font_family:
376            self.font_family = font_family
377        if font_size:
378            self.font_size = font_size
379        else:
380            self.font_size = defaults["font_size"]
381        if xform_matrix is None:
382            self.xform_matrix = identity_matrix()
383        else:
384            self.xform_matrix = get_transform(xform_matrix)
385
386        self.anchor = anchor
387        self.bold = bold
388        self.italic = italic
389        self.text_width = text_width
390        self.placement = placement
391        self.minimum_size = minimum_size
392        self.minimum_width = minimum_width
393        self.minimum_height = minimum_height
394        for k, v in kwargs.items():
395            setattr(self, k, v)
396
397        x1, y1, x2, y2 = self.text_bounds()
398        w = x2 - x1
399        h = y2 - y1
400        self.points = Points([(0, 0, 1), (w, 0, 1), (w, h, 1), (0, h, 1)])
401        common_properties(self)
text
type
subtype
style
anchor
bold
italic
text_width
placement
minimum_size
minimum_width
minimum_height
points
pos: Sequence[float]
450    @property
451    def pos(self) -> Point:
452        """Returns the position of the text.
453
454        Returns:
455            Point: The position of the text.
456        """
457        return (self._init_pos @ self.xform_matrix)[:2].tolist()

Returns the position of the text.

Returns:

Point: The position of the text.

def copy(self) -> Tag:
459    def copy(self) -> "Tag":
460        """Returns a copy of the Tag object.
461
462        Returns:
463            Tag: A copy of the Tag object.
464        """
465        tag = Tag(self.text, self.pos, xform_matrix=self.xform_matrix)
466        tag._init_pos = self._init_pos
467        tag.font_family = self.font_family
468        tag.font_size = self.font_size
469        tag.font_color = self.font_color
470        tag.anchor = self.anchor
471        tag.bold = self.bold
472        tag.italic = self.italic
473        tag.text_width = self.text_width
474        tag.placement = self.placement
475        tag.minimum_size = self.minimum_size
476        tag.minimum_width = self.minimum_width
477
478        return tag

Returns a copy of the Tag object.

Returns:

Tag: A copy of the Tag object.

def text_bounds(self) -> tuple[float, float, float, float]:
480    def text_bounds(self) -> tuple[float, float, float, float]:
481        """Returns the bounds of the text.
482
483        Returns:
484            tuple: The bounds of the text (xmin, ymin, xmax, ymax).
485        """
486        mult = 1 # font size multiplier
487        if self.font_size is None:
488            font_size = defaults["font_size"]
489        elif type(self.font_size) in [int, float]:
490            font_size = self.font_size
491        elif self.font_size in FontSize:
492            font_size = convert_latex_font_size(self.font_size)
493        else:
494            raise ValueError("Invalid font size.")
495        try:
496            font = ImageFont.truetype(f"{self.font_family}.ttf", font_size)
497        except OSError:
498            font = ImageFont.load_default()
499            mult = self.font_size / 10
500        xmin, ymin, xmax, ymax = font.getbbox(self.text)
501        width = xmax - xmin
502        height = ymax - ymin
503        dx = (width * mult) / 2
504        dy = (height * mult) / 2
505        xmin -= dx
506        xmax += dx
507        ymin -= dy
508        ymax += dy
509
510        return xmin, ymin, xmax, ymax

Returns the bounds of the text.

Returns:

tuple: The bounds of the text (xmin, ymin, xmax, ymax).

final_coords
512    @property
513    def final_coords(self):
514        """Returns the final coordinates of the text.
515
516        Returns:
517            array: The final coordinates of the text.
518        """
519        return self.points.homogen_coords @ self.xform_matrix

Returns the final coordinates of the text.

Returns:

array: The final coordinates of the text.

b_box
521    @property
522    def b_box(self):
523        """Returns the bounding box of the text.
524
525        Returns:
526            tuple: The bounding box of the text.
527        """
528        xmin, ymin, xmax, ymax = self.text_bounds()
529        w2 = (xmax - xmin) / 2
530        h2 = (ymax - ymin) / 2
531        x, y = self.pos
532        inner_sep = self.frame.inner_sep
533        xmin = x - w2 - inner_sep
534        xmax = x + w2 + inner_sep
535        ymin = y - h2 - inner_sep
536        ymax = y + h2 + inner_sep
537        points = [
538            (xmin, ymin),
539            (xmax, ymin),
540            (xmax, ymax),
541            (xmin, ymax),
542        ]
543        return bounding_box(points)

Returns the bounding box of the text.

Returns:

tuple: The bounding box of the text.

class ArrowHead(simetri.graphics.shape.Shape):
551class ArrowHead(Shape):
552    """An ArrowHead object is a shape that represents the head of an arrow.
553
554    Args:
555        length (float, optional): The length of the arrow head. Defaults to None.
556        width_ (float, optional): The width of the arrow head. Defaults to None.
557        points (list, optional): The points defining the arrow head. Defaults to None.
558        **kwargs: Additional keyword arguments for arrow head styling.
559    """
560
561    def __init__(
562        self, length: float = None, width_: float = None, points: list = None, **kwargs
563    ):
564        length, width_ = get_defaults(
565            ["arrow_head_length", "arrow_head_width"], [length, width_]
566        )
567        if points is None:
568            w2 = width_ / 2
569            points = [(0, 0), (0, -w2), (length, 0), (0, w2)]
570        super().__init__(points, closed=True, subtype=Types.ARROW_HEAD, **kwargs)
571        self.head_length = length
572        self.head_width = width_
573
574        self.kwargs = kwargs

An ArrowHead object is a shape that represents the head of an arrow.

Arguments:
  • length (float, optional): The length of the arrow head. Defaults to None.
  • width_ (float, optional): The width of the arrow head. Defaults to None.
  • points (list, optional): The points defining the arrow head. Defaults to None.
  • **kwargs: Additional keyword arguments for arrow head styling.
ArrowHead( length: float = None, width_: float = None, points: list = None, **kwargs)
561    def __init__(
562        self, length: float = None, width_: float = None, points: list = None, **kwargs
563    ):
564        length, width_ = get_defaults(
565            ["arrow_head_length", "arrow_head_width"], [length, width_]
566        )
567        if points is None:
568            w2 = width_ / 2
569            points = [(0, 0), (0, -w2), (length, 0), (0, w2)]
570        super().__init__(points, closed=True, subtype=Types.ARROW_HEAD, **kwargs)
571        self.head_length = length
572        self.head_width = width_
573
574        self.kwargs = kwargs

Initialize a Shape object.

Arguments:
  • points (Sequence[Point], optional): The points that make up the shape.
  • closed (bool, optional): Whether the shape is closed. Defaults to False.
  • xform_matrix (np.array, optional): The transformation matrix. Defaults to None.
  • **kwargs (dict): Additional attributes for the shape.
Raises:
  • ValueError: If the provided subtype is not valid.
head_length
head_width
kwargs
def draw_cs_tiny(canvas, pos=(0, 0), width=25, height=25, neg_width=5, neg_height=5):
577def draw_cs_tiny(canvas, pos=(0, 0), width=25, height=25, neg_width=5, neg_height=5):
578    """Draws a tiny coordinate system.
579
580    Args:
581        canvas: The canvas to draw on.
582        pos (tuple, optional): The position of the coordinate system. Defaults to (0, 0).
583        width (int, optional): The length of the x-axis. Defaults to 25.
584        height (int, optional): The length of the y-axis. Defaults to 25.
585        neg_width (int, optional): The negative length of the x-axis. Defaults to 5.
586        neg_height (int, optional): The negative length of the y-axis. Defaults to 5.
587    """
588    x, y = pos[:2]
589    canvas.circle((x, y), 2, fill=False, line_color=colors.gray)
590    canvas.draw(Shape([(x - neg_width, y), (x + width, y)]), line_color=colors.gray)
591    canvas.draw(Shape([(x, y - neg_height), (x, y + height)]), line_color=colors.gray)

Draws a tiny coordinate system.

Arguments:
  • canvas: The canvas to draw on.
  • pos (tuple, optional): The position of the coordinate system. Defaults to (0, 0).
  • width (int, optional): The length of the x-axis. Defaults to 25.
  • height (int, optional): The length of the y-axis. Defaults to 25.
  • neg_width (int, optional): The negative length of the x-axis. Defaults to 5.
  • neg_height (int, optional): The negative length of the y-axis. Defaults to 5.
def draw_cs_small(canvas, pos=(0, 0), width=80, height=100, neg_width=5, neg_height=5):
594def draw_cs_small(canvas, pos=(0, 0), width=80, height=100, neg_width=5, neg_height=5):
595    """Draws a small coordinate system.
596
597    Args:
598        canvas: The canvas to draw on.
599        pos (tuple, optional): The position of the coordinate system. Defaults to (0, 0).
600        width (int, optional): The length of the x-axis. Defaults to 80.
601        height (int, optional): The length of the y-axis. Defaults to 100.
602        neg_width (int, optional): The negative length of the x-axis. Defaults to 5.
603        neg_height (int, optional): The negative length of the y-axis. Defaults to 5.
604    """
605    x, y = pos[:2]
606    x_axis = arrow(
607        (-neg_width + x, y), (width + 10 + x, y), head_length=8, head_width=2
608    )
609    y_axis = arrow(
610        (x, -neg_height + y), (x, height + 10 + y), head_length=8, head_width=2
611    )
612    canvas.draw(x_axis, line_width=1)
613    canvas.draw(y_axis, line_width=1)

Draws a small coordinate system.

Arguments:
  • canvas: The canvas to draw on.
  • pos (tuple, optional): The position of the coordinate system. Defaults to (0, 0).
  • width (int, optional): The length of the x-axis. Defaults to 80.
  • height (int, optional): The length of the y-axis. Defaults to 100.
  • neg_width (int, optional): The negative length of the x-axis. Defaults to 5.
  • neg_height (int, optional): The negative length of the y-axis. Defaults to 5.
def arrow( p1, p2, head_length=10, head_width=4, line_width=1, line_color=Color(0.0, 0.0, 0.0), fill_color=Color(0.0, 0.0, 0.0), centered=False):
616def arrow(
617    p1,
618    p2,
619    head_length=10,
620    head_width=4,
621    line_width=1,
622    line_color=colors.black,
623    fill_color=colors.black,
624    centered=False,
625):
626    """Return an arrow from p1 to p2.
627
628    Args:
629        p1 (tuple): The starting point of the arrow.
630        p2 (tuple): The ending point of the arrow.
631        head_length (int, optional): The length of the arrow head. Defaults to 10.
632        head_width (int, optional): The width of the arrow head. Defaults to 4.
633        line_width (int, optional): The width of the arrow line. Defaults to 1.
634        line_color (Color, optional): The color of the arrow line. Defaults to colors.black.
635        fill_color (Color, optional): The fill color of the arrow head. Defaults to colors.black.
636        centered (bool, optional): Whether the arrow is centered. Defaults to False.
637
638    Returns:
639        Batch: A Batch object containing the arrow shapes.
640    """
641    x1, y1 = p1[:2]
642    x2, y2 = p2[:2]
643    dx = x2 - x1
644    dy = y2 - y1
645    angle = atan2(dy, dx)
646    body = Shape(
647        [(x1, y1), (x2, y2)],
648        closed=False,
649        line_color=line_color,
650        fill_color=fill_color,
651        line_width=line_width,
652    )
653    w2 = head_width / 2
654    head = Shape(
655        [(-head_length, w2), (0, 0), (-head_length, -w2)],
656        closed=True,
657        line_color=line_color,
658        fill_color=fill_color,
659        line_width=line_width,
660    )
661    head.rotate(angle)
662    if centered:
663        head.translate(*mid_point((x1, y1), (x2, y2)))
664    else:
665        head.translate(x2, y2)
666    return Batch([body, head])

Return an arrow from p1 to p2.

Arguments:
  • p1 (tuple): The starting point of the arrow.
  • p2 (tuple): The ending point of the arrow.
  • head_length (int, optional): The length of the arrow head. Defaults to 10.
  • head_width (int, optional): The width of the arrow head. Defaults to 4.
  • line_width (int, optional): The width of the arrow line. Defaults to 1.
  • line_color (Color, optional): The color of the arrow line. Defaults to colors.black.
  • fill_color (Color, optional): The fill color of the arrow head. Defaults to colors.black.
  • centered (bool, optional): Whether the arrow is centered. Defaults to False.
Returns:

Batch: A Batch object containing the arrow shapes.

class ArcArrow(simetri.graphics.batch.Batch):
669class ArcArrow(Batch):
670    """An ArcArrow object is an arrow with an arc.
671
672    Args:
673        center (Point): The center of the arc.
674        radius (float): The radius of the arc.
675        start_angle (float): The starting angle of the arc.
676        end_angle (float): The ending angle of the arc.
677        xform_matrix (array, optional): The transformation matrix. Defaults to None.
678        **kwargs: Additional keyword arguments for arc arrow styling.
679    """
680
681    def __init__(
682        self,
683        center: Point,
684        radius: float,
685        start_angle: float,
686        end_angle: float,
687        xform_matrix: array = None,
688        **kwargs,
689    ):
690        self.center = center
691        self.radius = radius
692        self.start_angle = start_angle
693        self.end_angle = end_angle
694        # create the arc
695        self.arc = Arc(center, radius, start_angle, end_angle)
696        self.arc.fill = False
697        # create arrow_head1
698        self.arrow_head1 = ArrowHead()
699        # create arrow_head2
700        self.arrow_head2 = ArrowHead()
701        start = self.arc.start_point
702        end = self.arc.end_point
703        self.points = [center, start, end]
704
705        self.arrow_head1.translate(-1 * self.arrow_head1.head_length, 0)
706        self.arrow_head1.rotate(start_angle - pi / 2)
707        self.arrow_head1.translate(*start)
708        self.arrow_head2.translate(-1 * self.arrow_head2.head_length, 0)
709        self.arrow_head2.rotate(end_angle + pi / 2)
710        self.arrow_head2.translate(*end)
711        items = [self.arc, self.arrow_head1, self.arrow_head2]
712        super().__init__(items, subtype=Types.ARC_ARROW, **kwargs)
713        for k, v in kwargs.items():
714            if k in shape_style_map:
715                setattr(self, k, v)  # we should check for valid values here
716            else:
717                raise AttributeError(f"{k}. Invalid attribute!")
718        self.xform_matrix = get_transform(xform_matrix)

An ArcArrow object is an arrow with an arc.

Arguments:
  • center (Point): The center of the arc.
  • radius (float): The radius of the arc.
  • start_angle (float): The starting angle of the arc.
  • end_angle (float): The ending angle of the arc.
  • xform_matrix (array, optional): The transformation matrix. Defaults to None.
  • **kwargs: Additional keyword arguments for arc arrow styling.
ArcArrow( center: Sequence[float], radius: float, start_angle: float, end_angle: float, xform_matrix: <built-in function array> = None, **kwargs)
681    def __init__(
682        self,
683        center: Point,
684        radius: float,
685        start_angle: float,
686        end_angle: float,
687        xform_matrix: array = None,
688        **kwargs,
689    ):
690        self.center = center
691        self.radius = radius
692        self.start_angle = start_angle
693        self.end_angle = end_angle
694        # create the arc
695        self.arc = Arc(center, radius, start_angle, end_angle)
696        self.arc.fill = False
697        # create arrow_head1
698        self.arrow_head1 = ArrowHead()
699        # create arrow_head2
700        self.arrow_head2 = ArrowHead()
701        start = self.arc.start_point
702        end = self.arc.end_point
703        self.points = [center, start, end]
704
705        self.arrow_head1.translate(-1 * self.arrow_head1.head_length, 0)
706        self.arrow_head1.rotate(start_angle - pi / 2)
707        self.arrow_head1.translate(*start)
708        self.arrow_head2.translate(-1 * self.arrow_head2.head_length, 0)
709        self.arrow_head2.rotate(end_angle + pi / 2)
710        self.arrow_head2.translate(*end)
711        items = [self.arc, self.arrow_head1, self.arrow_head2]
712        super().__init__(items, subtype=Types.ARC_ARROW, **kwargs)
713        for k, v in kwargs.items():
714            if k in shape_style_map:
715                setattr(self, k, v)  # we should check for valid values here
716            else:
717                raise AttributeError(f"{k}. Invalid attribute!")
718        self.xform_matrix = get_transform(xform_matrix)

Initialize a Batch object.

Arguments:
  • elements (Sequence[Any], optional): The elements to include in the batch.
  • modifiers (Sequence[Modifier], optional): The modifiers to apply to the batch.
  • subtype (Types, optional): The subtype of the batch.
  • kwargs (dict): Additional keyword arguments.
center
radius
start_angle
end_angle
arc
arrow_head1
arrow_head2
points
xform_matrix
class Arrow(simetri.graphics.batch.Batch):
721class Arrow(Batch):
722    """An Arrow object is a line with an arrow head.
723
724    Args:
725        p1 (Point): The starting point of the arrow.
726        p2 (Point): The ending point of the arrow.
727        head_pos (HeadPos, optional): The position of the arrow head. Defaults to HeadPos.END.
728        head (Shape, optional): The shape of the arrow head. Defaults to None.
729        **kwargs: Additional keyword arguments for arrow styling.
730    """
731
732    def __init__(
733        self,
734        p1: Point,
735        p2: Point,
736        head_pos: HeadPos = HeadPos.END,
737        head: Shape = None,
738        **kwargs,
739    ):
740        self.p1 = p1
741        self.p2 = p2
742        self.head_pos = head_pos
743        self.head = head
744        self.kwargs = kwargs
745        length = distance(p1, p2)
746        angle = line_angle(p1, p2)
747        self.line = Shape([(0, 0), (length, 0)])
748        if head is None:
749            self.head = ArrowHead()
750        else:
751            self.head = head
752        if self.head_pos == HeadPos.END:
753            x = length
754            self.head.translate(x - self.head.head_length, 0)
755            self.head.rotate(angle)
756            self.line.rotate(angle)
757            self.line.translate(*p1)
758            self.head.translate(*p1)
759            self.heads = [self.head]
760        elif self.head_pos == HeadPos.START:
761            self.head = [None]
762        elif self.head_pos == HeadPos.BOTH:
763            self.head2 = ArrowHead()
764            self.head2.rotate(pi)
765            self.head2.translate(self.head2.head_length, 0)
766            self.head2.rotate(angle)
767            self.head2.translate(*p1)
768            x = length
769            self.head.translate(x - self.head.head_length, 0)
770            self.head.rotate(angle)
771            self.line.rotate(angle)
772            self.line.translate(*p1)
773            self.head.translate(*p1)
774            self.heads = [self.head, self.head2]
775        elif self.head_pos == HeadPos.NONE:
776            self.heads = [None]
777
778        items = [self.line] + self.heads
779        super().__init__(items, subtype=Types.ARROW, **kwargs)

An Arrow object is a line with an arrow head.

Arguments:
  • p1 (Point): The starting point of the arrow.
  • p2 (Point): The ending point of the arrow.
  • head_pos (HeadPos, optional): The position of the arrow head. Defaults to HeadPos.END.
  • head (Shape, optional): The shape of the arrow head. Defaults to None.
  • **kwargs: Additional keyword arguments for arrow styling.
Arrow( p1: Sequence[float], p2: Sequence[float], head_pos: simetri.graphics.all_enums.HeadPos = <HeadPos.END: 'END'>, head: simetri.graphics.shape.Shape = None, **kwargs)
732    def __init__(
733        self,
734        p1: Point,
735        p2: Point,
736        head_pos: HeadPos = HeadPos.END,
737        head: Shape = None,
738        **kwargs,
739    ):
740        self.p1 = p1
741        self.p2 = p2
742        self.head_pos = head_pos
743        self.head = head
744        self.kwargs = kwargs
745        length = distance(p1, p2)
746        angle = line_angle(p1, p2)
747        self.line = Shape([(0, 0), (length, 0)])
748        if head is None:
749            self.head = ArrowHead()
750        else:
751            self.head = head
752        if self.head_pos == HeadPos.END:
753            x = length
754            self.head.translate(x - self.head.head_length, 0)
755            self.head.rotate(angle)
756            self.line.rotate(angle)
757            self.line.translate(*p1)
758            self.head.translate(*p1)
759            self.heads = [self.head]
760        elif self.head_pos == HeadPos.START:
761            self.head = [None]
762        elif self.head_pos == HeadPos.BOTH:
763            self.head2 = ArrowHead()
764            self.head2.rotate(pi)
765            self.head2.translate(self.head2.head_length, 0)
766            self.head2.rotate(angle)
767            self.head2.translate(*p1)
768            x = length
769            self.head.translate(x - self.head.head_length, 0)
770            self.head.rotate(angle)
771            self.line.rotate(angle)
772            self.line.translate(*p1)
773            self.head.translate(*p1)
774            self.heads = [self.head, self.head2]
775        elif self.head_pos == HeadPos.NONE:
776            self.heads = [None]
777
778        items = [self.line] + self.heads
779        super().__init__(items, subtype=Types.ARROW, **kwargs)

Initialize a Batch object.

Arguments:
  • elements (Sequence[Any], optional): The elements to include in the batch.
  • modifiers (Sequence[Modifier], optional): The modifiers to apply to the batch.
  • subtype (Types, optional): The subtype of the batch.
  • kwargs (dict): Additional keyword arguments.
p1
p2
head_pos
head
kwargs
line
class AngularDimension(simetri.graphics.batch.Batch):
782class AngularDimension(Batch):
783    """An AngularDimension object is a dimension that represents an angle.
784
785    Args:
786        center (Point): The center of the angle.
787        radius (float): The radius of the angle.
788        start_angle (float): The starting angle.
789        end_angle (float): The ending angle.
790        ext_angle (float): The extension angle.
791        gap_angle (float): The gap angle.
792        text_offset (float, optional): The text offset. Defaults to None.
793        gap (float, optional): The gap. Defaults to None.
794        **kwargs: Additional keyword arguments for angular dimension styling.
795    """
796
797    def __init__(
798        self,
799        center: Point,
800        radius: float,
801        start_angle: float,
802        end_angle: float,
803        ext_angle: float,
804        gap_angle: float,
805        text_offset: float = None,
806        gap: float = None,
807        **kwargs,
808    ):
809        text_offset, gap = get_defaults(["text_offset", "gap"], [text_offset, gap])
810        self.center = center
811        self.radius = radius
812        self.start_angle = start_angle
813        self.end_angle = end_angle
814        self.ext_angle = ext_angle
815        self.gap_angle = gap_angle
816        self.text_offset = text_offset
817        self.gap = gap
818        super().__init__(subtype=Types.ANGULAR_DIMENSION, **kwargs)

An AngularDimension object is a dimension that represents an angle.

Arguments:
  • center (Point): The center of the angle.
  • radius (float): The radius of the angle.
  • start_angle (float): The starting angle.
  • end_angle (float): The ending angle.
  • ext_angle (float): The extension angle.
  • gap_angle (float): The gap angle.
  • text_offset (float, optional): The text offset. Defaults to None.
  • gap (float, optional): The gap. Defaults to None.
  • **kwargs: Additional keyword arguments for angular dimension styling.
AngularDimension( center: Sequence[float], radius: float, start_angle: float, end_angle: float, ext_angle: float, gap_angle: float, text_offset: float = None, gap: float = None, **kwargs)
797    def __init__(
798        self,
799        center: Point,
800        radius: float,
801        start_angle: float,
802        end_angle: float,
803        ext_angle: float,
804        gap_angle: float,
805        text_offset: float = None,
806        gap: float = None,
807        **kwargs,
808    ):
809        text_offset, gap = get_defaults(["text_offset", "gap"], [text_offset, gap])
810        self.center = center
811        self.radius = radius
812        self.start_angle = start_angle
813        self.end_angle = end_angle
814        self.ext_angle = ext_angle
815        self.gap_angle = gap_angle
816        self.text_offset = text_offset
817        self.gap = gap
818        super().__init__(subtype=Types.ANGULAR_DIMENSION, **kwargs)

Initialize a Batch object.

Arguments:
  • elements (Sequence[Any], optional): The elements to include in the batch.
  • modifiers (Sequence[Modifier], optional): The modifiers to apply to the batch.
  • subtype (Types, optional): The subtype of the batch.
  • kwargs (dict): Additional keyword arguments.
center
radius
start_angle
end_angle
ext_angle
gap_angle
text_offset
gap
class Dimension(simetri.graphics.batch.Batch):
 821class Dimension(Batch):
 822    """A Dimension object is a line with arrows and a text.
 823
 824    Args:
 825        text (str): The text of the dimension.
 826        p1 (Point): The starting point of the dimension.
 827        p2 (Point): The ending point of the dimension.
 828        ext_length (float): The length of the extension lines.
 829        ext_length2 (float, optional): The length of the second extension line. Defaults to None.
 830        orientation (Anchor, optional): The orientation of the dimension. Defaults to None.
 831        text_pos (Anchor, optional): The position of the text. Defaults to Anchor.CENTER.
 832        text_offset (float, optional): The offset of the text. Defaults to 0.
 833        gap (float, optional): The gap. Defaults to None.
 834        reverse_arrows (bool, optional): Whether to reverse the arrows. Defaults to False.
 835        reverse_arrow_length (float, optional): The length of the reversed arrows. Defaults to None.
 836        parallel (bool, optional): Whether the dimension is parallel. Defaults to False.
 837        ext1pnt (Point, optional): The first extension point. Defaults to None.
 838        ext2pnt (Point, optional): The second extension point. Defaults to None.
 839        scale (float, optional): The scale factor. Defaults to 1.
 840        font_size (int, optional): The font size. Defaults to 12.
 841        **kwargs: Additional keyword arguments for dimension styling.
 842    """
 843
 844    # To do: This is too long and convoluted. Refactor it.
 845    def __init__(
 846        self,
 847        text: str,
 848        p1: Point,
 849        p2: Point,
 850        ext_length: float,
 851        ext_length2: float = None,
 852        orientation: Anchor = None,
 853        text_pos: Anchor = Anchor.CENTER,
 854        text_offset: float = 0,
 855        gap: float = None,
 856        reverse_arrows: bool = False,
 857        reverse_arrow_length: float = None,
 858        parallel: bool = False,
 859        ext1pnt: Point = None,
 860        ext2pnt: Point = None,
 861        scale: float = 1,
 862        font_size: int = 12,
 863        **kwargs,
 864    ):
 865        ext_length2, gap, reverse_arrow_length = get_defaults(
 866            ["ext_length2", "gap", "rev_arrow_length"],
 867            [ext_length2, gap, reverse_arrow_length],
 868        )
 869        if text == "":
 870            self.text = str(distance(p1, p2) / scale)
 871        else:
 872            self.text = text
 873        self.p1 = p1
 874        self.p2 = p2
 875        self.ext_length = ext_length
 876        self.ext_length2 = ext_length2
 877        self.orientation = orientation
 878        self.text_pos = text_pos
 879        self.text_offset = text_offset
 880        self.gap = gap
 881        self.reverse_arrows = reverse_arrows
 882        self.reverse_arrow_length = reverse_arrow_length
 883        self.kwargs = kwargs
 884        self.ext1 = None
 885        self.ext2 = None
 886        self.ext3 = None
 887        self.arrow1 = None
 888        self.arrow2 = None
 889        self.dim_line = None
 890        self.mid_line = None
 891        self.ext1pnt = ext1pnt
 892        self.ext2pnt = ext2pnt
 893        x1, y1 = p1[:2]
 894        x2, y2 = p2[:2]
 895
 896        # px1_1 : extension1 point 1
 897        # px1_2 : extension1 point 2
 898        # px2_1 : extension2 point 1
 899        # px2_2 : extension2 point 2
 900        # px3_1 : extension3 point 1
 901        # px3_2 : extension3 point 2
 902        # pa1 : arrow point 1
 903        # pa2 : arrow point 2
 904        # ptext : text point
 905        super().__init__(subtype=Types.DIMENSION, **kwargs)
 906        dist_tol = defaults["dist_tol"]
 907        if font_size is not None:
 908            self.font_size = font_size
 909        if parallel:
 910            if orientation is None:
 911                orientation = Anchor.NORTHEAST
 912
 913            if orientation == Anchor.NORTHEAST:
 914                angle = line_angle(p1, p2) + pi / 2
 915            elif orientation == Anchor.NORTHWEST:
 916                angle = line_angle(p1, p2) + pi / 2
 917            elif orientation == Anchor.SOUTHEAST:
 918                angle = line_angle(p1, p2) - pi / 2
 919            elif orientation == Anchor.SOUTHWEST:
 920                angle = line_angle(p1, p2) + pi / 2
 921            if self.ext1pnt is None:
 922                px1_1 = line_by_point_angle_length(p1, angle, self.gap)[1]
 923            else:
 924                px1_1 = self.ext1pnt
 925            px1_2 = line_by_point_angle_length(p1, angle, self.gap + self.ext_length)[1]
 926            if self.ext2pnt is None:
 927                px2_1 = line_by_point_angle_length(p2, angle, self.gap)[1]
 928            else:
 929                px2_1 = self.ext2pnt
 930            px2_2 = line_by_point_angle_length(p2, angle, self.gap + self.ext_length)[1]
 931
 932            pa1 = line_by_point_angle_length(px1_2, angle, self.gap * -1.5)[1]
 933            pa2 = line_by_point_angle_length(px2_2, angle, self.gap * -1.5)[1]
 934
 935            self.text_pos = mid_point(pa1, pa2)
 936            self.dim_line = Arrow(pa1, pa2, head_pos=HeadPos.BOTH)
 937            self.ext1 = Shape([px1_1, px1_2])
 938            self.ext2 = Shape([px2_1, px2_2])
 939            self.append(self.dim_line)
 940            self.append(self.ext1)
 941            self.append(self.ext2)
 942
 943        else:
 944            if abs(x1 - x2) < dist_tol:
 945                # vertical line
 946                if self.orientation is None:
 947                    orientation = Anchor.EAST
 948
 949                if orientation in [Anchor.WEST, Anchor.SOUTHWEST, Anchor.NORTHWEST]:
 950                    x = x1 - self.gap
 951                    px1_1 = (x, y1)
 952                    px1_2 = (x - ext_length, y1)
 953                    px2_1 = (x, y2)
 954                    px2_2 = (x - ext_length, y2)
 955                    x = px1_2[0] + self.gap * 1.5
 956                    pa1 = (x, y1)
 957                    pa2 = (x, y2)
 958                elif orientation in [Anchor.EAST, Anchor.SOUTHEAST, Anchor.NORTHEAST]:
 959                    x = x1 + self.gap
 960                    px1_1 = (x, y1)
 961                    px1_2 = (x + ext_length, y1)
 962                    px2_1 = (x, y2)
 963                    px2_2 = (x + ext_length, y2)
 964                    x = px1_2[0] - self.gap * 1.5
 965                    pa1 = (x, y1)
 966                    pa2 = (x, y2)
 967                elif orientation == Anchor.CENTER:
 968                    pa1 = (x1, y1)
 969                    pa2 = (x1, y2)
 970                x = pa1[0]
 971                if orientation in [Anchor.SOUTHWEST, Anchor.SOUTHEAST]:
 972                    px3_1 = pa2
 973                    y = y2 - self.ext_length2
 974                    px3_2 = (x, y)
 975                    self.ext3 = Shape([px3_1, px3_2])
 976                    self.text_pos = (x, y - self.text_offset)
 977                elif orientation in [Anchor.NORTHWEST, Anchor.NORTHEAST]:
 978                    px3_1 = pa1
 979                    y = y1 + self.ext_length2
 980                    px3_2 = (x, y)
 981                    self.ext3 = Shape([px3_1, px3_2])
 982                    self.text_pos = (x, y + self.text_offset)
 983                elif orientation == Anchor.SOUTH:
 984                    px3_1 = pa2
 985                    y = y2 - self.ext_length2
 986                    px3_2 = (x, y)
 987                    self.ext3 = Shape([px3_1, px3_2])
 988                    self.text_pos = (x, y - self.text_offset)
 989                elif orientation == Anchor.NORTH:
 990                    px3_2 = pa1
 991                    y = y2 + self.ext_length2
 992                    px3_1 = (x, y)
 993                    self.ext3 = Shape([px3_1, px3_2])
 994                    self.text_pos = (x, y + self.text_offset)
 995                else:
 996                    self.text_pos = (x, y1 - (y1 - y2) / 2)
 997                if orientation not in [Anchor.CENTER, Anchor.NORTH, Anchor.SOUTH]:
 998                    if self.ext1pnt is None:
 999                        self.ext1 = Shape([px1_1, px1_2])
1000                    else:
1001                        self.ext1 = Shape([ext1pnt, px1_2])
1002                    if self.ext2pnt is None:
1003                        self.ext2 = Shape([px2_1, px2_2])
1004                    else:
1005                        self.ext2 = Shape([ext2pnt, px2_2])
1006            elif abs(y1 - y2) < dist_tol:
1007                # horizontal line
1008                if self.orientation is None:
1009                    orientation = Anchor.SOUTH
1010
1011                if orientation in [Anchor.SOUTH, Anchor.SOUTHWEST, Anchor.SOUTHEAST]:
1012                    y = y1 - self.gap
1013                    px1_1 = (x1, y)
1014                    px1_2 = (x1, y - ext_length)
1015                    px2_1 = (x2, y)
1016                    px2_2 = (x2, y - ext_length)
1017                    y = px1_2[1] + self.gap * 1.5
1018                    pa1 = (x1, y)
1019                    pa2 = (x2, y)
1020                elif orientation in [Anchor.NORTH, Anchor.NORTHWEST, Anchor.NORTHEAST]:
1021                    y = y1 + self.gap
1022                    px1_1 = (x1, y)
1023                    px1_2 = (x1, y + ext_length)
1024                    px2_1 = (x2, y)
1025                    px2_2 = (x2, y + ext_length)
1026                    y = px1_2[1] - self.gap * 1.5
1027                    pa1 = (x1, y)
1028                    pa2 = (x2, y)
1029                elif orientation in [Anchor.WEST, Anchor.EAST]:
1030                    pa1 = (x1, y1)
1031                    pa2 = (x2, y2)
1032                    if orientation == Anchor.WEST:
1033                        px3_1 = (pa1[0] - self.ext_length2, pa1[1])
1034                        px3_2 = pa1
1035                        self.text_pos = (px3_1[0] - self.text_offset, pa1[1])
1036                    else:
1037                        px3_1 = pa2
1038                        px3_2 = (pa2[0] + self.ext_length2, pa1[1])
1039                        self.text_pos = (px3_1[0] + self.text_offset, pa1[1])
1040                    self.ext3 = Shape([px3_1, px3_2])
1041                elif orientation == Anchor.CENTER:
1042                    pa1 = (x1, y1)
1043                    pa2 = (x2, y2)
1044
1045                y = pa1[1]
1046                if orientation in [Anchor.SOUTHWEST, Anchor.NORTHWEST]:
1047                    px3_1 = pa1
1048                    x = x1 - self.ext_length2
1049                    px3_2 = (x, y)
1050                    self.ext3 = Shape([px3_1, px3_2])
1051                    self.text_pos = (x - self.text_offset, y)
1052                elif orientation in [Anchor.NORTHEAST, Anchor.SOUTHEAST]:
1053                    px3_1 = pa2
1054                    x = x2 + self.ext_length2
1055                    px3_2 = (x, y)
1056                    self.ext3 = Shape([px3_1, px3_2])
1057                    self.text_pos = (x + self.text_offset, y)
1058                elif orientation in [Anchor.CENTER, Anchor.NORTH, Anchor.SOUTH]:
1059                    self.text_pos = (x1 + (x2 - x1) / 2, y)
1060
1061                if orientation not in [Anchor.CENTER, Anchor.WEST, Anchor.EAST]:
1062                    if self.ext1pnt is None:
1063                        self.ext1 = Shape([px1_1, px1_2])
1064                    else:
1065                        self.ext1Shape([ext1pnt, px1_2])
1066                    if self.ext2pnt is None:
1067                        self.ext2 = Shape([px2_1, px2_2])
1068                    else:
1069                        self.ext2 = Shape([ext2pnt, px2_2])
1070
1071            if self.reverse_arrows:
1072                dist = self.reverse_arrow_length
1073                p2 = extended_line(dist, [pa1, pa2])[1]
1074                self.arrow1 = Arrow(p2, pa2)
1075                p2 = extended_line(dist, [pa2, pa1])[1]
1076                self.arrow2 = Arrow(p2, pa1)
1077                self.append(self.arrow1)
1078                self.append(self.arrow2)
1079                self.mid_line = Shape([pa1, pa2])
1080                self.append(self.mid_line)
1081                dist = self.text_offset + self.reverse_arrow_length
1082                if orientation in [Anchor.EAST, Anchor.NORTHEAST, Anchor.NORTH]:
1083
1084                    self.text_pos = extended_line(dist, [pa1, pa2])[1]
1085                else:
1086                    self.text_pos = extended_line(dist, [pa2, pa1])[1]
1087            else:
1088                self.dim_line = Arrow(pa1, pa2, head_pos=HeadPos.BOTH)
1089                self.append(self.dim_line)
1090            if self.ext1 is not None:
1091                self.append(self.ext1)
1092
1093            if self.ext2 is not None:
1094                self.append(self.ext2)
1095
1096            if self.ext3 is not None:
1097                self.append(self.ext3)

A Dimension object is a line with arrows and a text.

Arguments:
  • text (str): The text of the dimension.
  • p1 (Point): The starting point of the dimension.
  • p2 (Point): The ending point of the dimension.
  • ext_length (float): The length of the extension lines.
  • ext_length2 (float, optional): The length of the second extension line. Defaults to None.
  • orientation (Anchor, optional): The orientation of the dimension. Defaults to None.
  • text_pos (Anchor, optional): The position of the text. Defaults to Anchor.CENTER.
  • text_offset (float, optional): The offset of the text. Defaults to 0.
  • gap (float, optional): The gap. Defaults to None.
  • reverse_arrows (bool, optional): Whether to reverse the arrows. Defaults to False.
  • reverse_arrow_length (float, optional): The length of the reversed arrows. Defaults to None.
  • parallel (bool, optional): Whether the dimension is parallel. Defaults to False.
  • ext1pnt (Point, optional): The first extension point. Defaults to None.
  • ext2pnt (Point, optional): The second extension point. Defaults to None.
  • scale (float, optional): The scale factor. Defaults to 1.
  • font_size (int, optional): The font size. Defaults to 12.
  • **kwargs: Additional keyword arguments for dimension styling.
Dimension( text: str, p1: Sequence[float], p2: Sequence[float], ext_length: float, ext_length2: float = None, orientation: simetri.graphics.all_enums.Anchor = None, text_pos: simetri.graphics.all_enums.Anchor = <Anchor.CENTER: 'center'>, text_offset: float = 0, gap: float = None, reverse_arrows: bool = False, reverse_arrow_length: float = None, parallel: bool = False, ext1pnt: Sequence[float] = None, ext2pnt: Sequence[float] = None, scale: float = 1, font_size: int = 12, **kwargs)
 845    def __init__(
 846        self,
 847        text: str,
 848        p1: Point,
 849        p2: Point,
 850        ext_length: float,
 851        ext_length2: float = None,
 852        orientation: Anchor = None,
 853        text_pos: Anchor = Anchor.CENTER,
 854        text_offset: float = 0,
 855        gap: float = None,
 856        reverse_arrows: bool = False,
 857        reverse_arrow_length: float = None,
 858        parallel: bool = False,
 859        ext1pnt: Point = None,
 860        ext2pnt: Point = None,
 861        scale: float = 1,
 862        font_size: int = 12,
 863        **kwargs,
 864    ):
 865        ext_length2, gap, reverse_arrow_length = get_defaults(
 866            ["ext_length2", "gap", "rev_arrow_length"],
 867            [ext_length2, gap, reverse_arrow_length],
 868        )
 869        if text == "":
 870            self.text = str(distance(p1, p2) / scale)
 871        else:
 872            self.text = text
 873        self.p1 = p1
 874        self.p2 = p2
 875        self.ext_length = ext_length
 876        self.ext_length2 = ext_length2
 877        self.orientation = orientation
 878        self.text_pos = text_pos
 879        self.text_offset = text_offset
 880        self.gap = gap
 881        self.reverse_arrows = reverse_arrows
 882        self.reverse_arrow_length = reverse_arrow_length
 883        self.kwargs = kwargs
 884        self.ext1 = None
 885        self.ext2 = None
 886        self.ext3 = None
 887        self.arrow1 = None
 888        self.arrow2 = None
 889        self.dim_line = None
 890        self.mid_line = None
 891        self.ext1pnt = ext1pnt
 892        self.ext2pnt = ext2pnt
 893        x1, y1 = p1[:2]
 894        x2, y2 = p2[:2]
 895
 896        # px1_1 : extension1 point 1
 897        # px1_2 : extension1 point 2
 898        # px2_1 : extension2 point 1
 899        # px2_2 : extension2 point 2
 900        # px3_1 : extension3 point 1
 901        # px3_2 : extension3 point 2
 902        # pa1 : arrow point 1
 903        # pa2 : arrow point 2
 904        # ptext : text point
 905        super().__init__(subtype=Types.DIMENSION, **kwargs)
 906        dist_tol = defaults["dist_tol"]
 907        if font_size is not None:
 908            self.font_size = font_size
 909        if parallel:
 910            if orientation is None:
 911                orientation = Anchor.NORTHEAST
 912
 913            if orientation == Anchor.NORTHEAST:
 914                angle = line_angle(p1, p2) + pi / 2
 915            elif orientation == Anchor.NORTHWEST:
 916                angle = line_angle(p1, p2) + pi / 2
 917            elif orientation == Anchor.SOUTHEAST:
 918                angle = line_angle(p1, p2) - pi / 2
 919            elif orientation == Anchor.SOUTHWEST:
 920                angle = line_angle(p1, p2) + pi / 2
 921            if self.ext1pnt is None:
 922                px1_1 = line_by_point_angle_length(p1, angle, self.gap)[1]
 923            else:
 924                px1_1 = self.ext1pnt
 925            px1_2 = line_by_point_angle_length(p1, angle, self.gap + self.ext_length)[1]
 926            if self.ext2pnt is None:
 927                px2_1 = line_by_point_angle_length(p2, angle, self.gap)[1]
 928            else:
 929                px2_1 = self.ext2pnt
 930            px2_2 = line_by_point_angle_length(p2, angle, self.gap + self.ext_length)[1]
 931
 932            pa1 = line_by_point_angle_length(px1_2, angle, self.gap * -1.5)[1]
 933            pa2 = line_by_point_angle_length(px2_2, angle, self.gap * -1.5)[1]
 934
 935            self.text_pos = mid_point(pa1, pa2)
 936            self.dim_line = Arrow(pa1, pa2, head_pos=HeadPos.BOTH)
 937            self.ext1 = Shape([px1_1, px1_2])
 938            self.ext2 = Shape([px2_1, px2_2])
 939            self.append(self.dim_line)
 940            self.append(self.ext1)
 941            self.append(self.ext2)
 942
 943        else:
 944            if abs(x1 - x2) < dist_tol:
 945                # vertical line
 946                if self.orientation is None:
 947                    orientation = Anchor.EAST
 948
 949                if orientation in [Anchor.WEST, Anchor.SOUTHWEST, Anchor.NORTHWEST]:
 950                    x = x1 - self.gap
 951                    px1_1 = (x, y1)
 952                    px1_2 = (x - ext_length, y1)
 953                    px2_1 = (x, y2)
 954                    px2_2 = (x - ext_length, y2)
 955                    x = px1_2[0] + self.gap * 1.5
 956                    pa1 = (x, y1)
 957                    pa2 = (x, y2)
 958                elif orientation in [Anchor.EAST, Anchor.SOUTHEAST, Anchor.NORTHEAST]:
 959                    x = x1 + self.gap
 960                    px1_1 = (x, y1)
 961                    px1_2 = (x + ext_length, y1)
 962                    px2_1 = (x, y2)
 963                    px2_2 = (x + ext_length, y2)
 964                    x = px1_2[0] - self.gap * 1.5
 965                    pa1 = (x, y1)
 966                    pa2 = (x, y2)
 967                elif orientation == Anchor.CENTER:
 968                    pa1 = (x1, y1)
 969                    pa2 = (x1, y2)
 970                x = pa1[0]
 971                if orientation in [Anchor.SOUTHWEST, Anchor.SOUTHEAST]:
 972                    px3_1 = pa2
 973                    y = y2 - self.ext_length2
 974                    px3_2 = (x, y)
 975                    self.ext3 = Shape([px3_1, px3_2])
 976                    self.text_pos = (x, y - self.text_offset)
 977                elif orientation in [Anchor.NORTHWEST, Anchor.NORTHEAST]:
 978                    px3_1 = pa1
 979                    y = y1 + self.ext_length2
 980                    px3_2 = (x, y)
 981                    self.ext3 = Shape([px3_1, px3_2])
 982                    self.text_pos = (x, y + self.text_offset)
 983                elif orientation == Anchor.SOUTH:
 984                    px3_1 = pa2
 985                    y = y2 - self.ext_length2
 986                    px3_2 = (x, y)
 987                    self.ext3 = Shape([px3_1, px3_2])
 988                    self.text_pos = (x, y - self.text_offset)
 989                elif orientation == Anchor.NORTH:
 990                    px3_2 = pa1
 991                    y = y2 + self.ext_length2
 992                    px3_1 = (x, y)
 993                    self.ext3 = Shape([px3_1, px3_2])
 994                    self.text_pos = (x, y + self.text_offset)
 995                else:
 996                    self.text_pos = (x, y1 - (y1 - y2) / 2)
 997                if orientation not in [Anchor.CENTER, Anchor.NORTH, Anchor.SOUTH]:
 998                    if self.ext1pnt is None:
 999                        self.ext1 = Shape([px1_1, px1_2])
1000                    else:
1001                        self.ext1 = Shape([ext1pnt, px1_2])
1002                    if self.ext2pnt is None:
1003                        self.ext2 = Shape([px2_1, px2_2])
1004                    else:
1005                        self.ext2 = Shape([ext2pnt, px2_2])
1006            elif abs(y1 - y2) < dist_tol:
1007                # horizontal line
1008                if self.orientation is None:
1009                    orientation = Anchor.SOUTH
1010
1011                if orientation in [Anchor.SOUTH, Anchor.SOUTHWEST, Anchor.SOUTHEAST]:
1012                    y = y1 - self.gap
1013                    px1_1 = (x1, y)
1014                    px1_2 = (x1, y - ext_length)
1015                    px2_1 = (x2, y)
1016                    px2_2 = (x2, y - ext_length)
1017                    y = px1_2[1] + self.gap * 1.5
1018                    pa1 = (x1, y)
1019                    pa2 = (x2, y)
1020                elif orientation in [Anchor.NORTH, Anchor.NORTHWEST, Anchor.NORTHEAST]:
1021                    y = y1 + self.gap
1022                    px1_1 = (x1, y)
1023                    px1_2 = (x1, y + ext_length)
1024                    px2_1 = (x2, y)
1025                    px2_2 = (x2, y + ext_length)
1026                    y = px1_2[1] - self.gap * 1.5
1027                    pa1 = (x1, y)
1028                    pa2 = (x2, y)
1029                elif orientation in [Anchor.WEST, Anchor.EAST]:
1030                    pa1 = (x1, y1)
1031                    pa2 = (x2, y2)
1032                    if orientation == Anchor.WEST:
1033                        px3_1 = (pa1[0] - self.ext_length2, pa1[1])
1034                        px3_2 = pa1
1035                        self.text_pos = (px3_1[0] - self.text_offset, pa1[1])
1036                    else:
1037                        px3_1 = pa2
1038                        px3_2 = (pa2[0] + self.ext_length2, pa1[1])
1039                        self.text_pos = (px3_1[0] + self.text_offset, pa1[1])
1040                    self.ext3 = Shape([px3_1, px3_2])
1041                elif orientation == Anchor.CENTER:
1042                    pa1 = (x1, y1)
1043                    pa2 = (x2, y2)
1044
1045                y = pa1[1]
1046                if orientation in [Anchor.SOUTHWEST, Anchor.NORTHWEST]:
1047                    px3_1 = pa1
1048                    x = x1 - self.ext_length2
1049                    px3_2 = (x, y)
1050                    self.ext3 = Shape([px3_1, px3_2])
1051                    self.text_pos = (x - self.text_offset, y)
1052                elif orientation in [Anchor.NORTHEAST, Anchor.SOUTHEAST]:
1053                    px3_1 = pa2
1054                    x = x2 + self.ext_length2
1055                    px3_2 = (x, y)
1056                    self.ext3 = Shape([px3_1, px3_2])
1057                    self.text_pos = (x + self.text_offset, y)
1058                elif orientation in [Anchor.CENTER, Anchor.NORTH, Anchor.SOUTH]:
1059                    self.text_pos = (x1 + (x2 - x1) / 2, y)
1060
1061                if orientation not in [Anchor.CENTER, Anchor.WEST, Anchor.EAST]:
1062                    if self.ext1pnt is None:
1063                        self.ext1 = Shape([px1_1, px1_2])
1064                    else:
1065                        self.ext1Shape([ext1pnt, px1_2])
1066                    if self.ext2pnt is None:
1067                        self.ext2 = Shape([px2_1, px2_2])
1068                    else:
1069                        self.ext2 = Shape([ext2pnt, px2_2])
1070
1071            if self.reverse_arrows:
1072                dist = self.reverse_arrow_length
1073                p2 = extended_line(dist, [pa1, pa2])[1]
1074                self.arrow1 = Arrow(p2, pa2)
1075                p2 = extended_line(dist, [pa2, pa1])[1]
1076                self.arrow2 = Arrow(p2, pa1)
1077                self.append(self.arrow1)
1078                self.append(self.arrow2)
1079                self.mid_line = Shape([pa1, pa2])
1080                self.append(self.mid_line)
1081                dist = self.text_offset + self.reverse_arrow_length
1082                if orientation in [Anchor.EAST, Anchor.NORTHEAST, Anchor.NORTH]:
1083
1084                    self.text_pos = extended_line(dist, [pa1, pa2])[1]
1085                else:
1086                    self.text_pos = extended_line(dist, [pa2, pa1])[1]
1087            else:
1088                self.dim_line = Arrow(pa1, pa2, head_pos=HeadPos.BOTH)
1089                self.append(self.dim_line)
1090            if self.ext1 is not None:
1091                self.append(self.ext1)
1092
1093            if self.ext2 is not None:
1094                self.append(self.ext2)
1095
1096            if self.ext3 is not None:
1097                self.append(self.ext3)

Initialize a Batch object.

Arguments:
  • elements (Sequence[Any], optional): The elements to include in the batch.
  • modifiers (Sequence[Modifier], optional): The modifiers to apply to the batch.
  • subtype (Types, optional): The subtype of the batch.
  • kwargs (dict): Additional keyword arguments.
p1
p2
ext_length
ext_length2
orientation
text_pos
text_offset
gap
reverse_arrows
reverse_arrow_length
kwargs
ext1
ext2
ext3
arrow1
arrow2
dim_line
mid_line
ext1pnt
ext2pnt