Source code for simetri.helpers.illustration

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

from dataclasses import dataclass
from math import pi, atan2
from PIL import ImageFont

import fitz
import numpy as np

# from reportlab.pdfbase import pdfmetrics # to do: remove this

from ..graphics.core import Base
from ..graphics.bbox import bounding_box
from ..graphics.points import Points
from ..graphics.batch import Batch
from ..graphics.shape import Shape

from ..graphics.shapes import reg_poly_points_side_length
from ..graphics.common import get_defaults, common_properties, Point, _set_Nones
from ..graphics.all_enums import (
    Types,
    LineJoin,
    Anchor,
    FrameShape,
    HeadPos,
    ArrowLine,
    Placement,
    FontSize,
)
from ..canvas.style_map import shape_style_map, tag_style_map, TagStyle
from ..graphics.affine import identity_matrix
from ..geometry.ellipse import Arc
from ..geometry.geometry import (
    distance,
    line_angle,
    extended_line,
    line_by_point_angle_length,
    mid_point,
)
from .utilities import get_transform, detokenize
from ..colors.swatches import swatches_255
from ..settings.settings import defaults
from ..colors import colors
from .validation import validate_args

Color = colors.Color
array = np.array






[docs] def convert_latex_font_size(latex_font_size: FontSize): """Converts LaTeX font size to a numerical value. Args: latex_font_size (FontSize): The LaTeX font size. Returns: int: The corresponding numerical font size. """ d_font_size = { FontSize.TINY: 5, FontSize.SMALL: 7, FontSize.NORMAL: 10, FontSize.LARGE: 12, FontSize.LARGE2: 14, FontSize.LARGE3: 17, FontSize.HUGE: 20, FontSize.HUGE2: 25, } return d_font_size[latex_font_size]
[docs] def letter_F_points(): """Returns the points of the capital letter F. Returns: list: A list of points representing the letter F. """ return [ (0.0, 0.0), (20.0, 0.0), (20.0, 40.0), (40.0, 40.0), (40.0, 60.0), (20.0, 60.0), (20.0, 80.0), (50.0, 80.0), (50.0, 100.0), (0.0, 100.0), (0.0, 0.0), ]
[docs] def letter_F(scale=1, **kwargs): """Returns a Shape object representing the capital letter F. Args: 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. """ F = Shape(letter_F_points(), closed=True) if scale != 1: F.scale(scale) for k, v in kwargs.items(): if k in shape_style_map: setattr(F, k, v) else: raise AttributeError(f"{k}. Invalid attribute!") return F
[docs] def cube(size: float = 100): """Returns a Batch object representing a cube. Args: size (float, optional): The size of the cube. Defaults to 100. Returns: Batch: A Batch object representing the cube. """ points = reg_poly_points_side_length((0, 0), 6, size) center = (0, 0) face1 = Shape([points[0], center] + points[4:], closed=True) cube_ = face1.rotate(-2 * pi / 3, (0, 0), reps=2) cube_[0].fill_color = Color(0.3, 0.3, 0.3) cube_[1].fill_color = Color(0.4, 0.4, 0.4) cube_[2].fill_color = Color(0.6, 0.6, 0.6) return cube_
[docs] def pdf_to_svg(pdf_path, svg_path): """Converts a single-page PDF file to SVG. Args: pdf_path (str): The path to the PDF file. svg_path (str): The path to save the SVG file. """ doc = fitz.open(pdf_path) page = doc.load_page(0) svg = page.get_svg_image() with open(svg_path, "w", encoding="utf-8") as f: f.write(svg)
# To do: use a different name for the Annotation class # annotation is a label with an arrow
[docs] class Annotation(Batch): """An Annotation object is a label with an arrow pointing to a specific location. Args: 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. """ def __init__( self, text, pos, frame, root_pos, arrow_line=ArrowLine.STRAIGHT_END, **kwargs ): self.text = text self.pos = pos self.frame = frame self.root_pos = root_pos self.arrow_line = arrow_line self.kwargs = kwargs super().__init__(subtype=Types.ANNOTATION, **kwargs)
[docs] @dataclass class TagFrame: """Frame objects are used with Tag objects to create boxes. Args: 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. """ frame_shape: FrameShape = "rectangle" line_width: float = 1 line_dash_array: list = None line_join: LineJoin = "miter" line_color: Color = colors.black back_color: Color = colors.white 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 def __post_init__(self): self.type = Types.FRAME self.subtype = Types.FRAME common_properties(self, id_only=True)
[docs] class Tag(Base): """A Tag object is very similar to TikZ library's nodes. It is a text with a frame. Args: 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. """ def __init__( self, text: str, pos: Point, font_family: str = None, font_size: int = None, font_color: Color = None, anchor: Anchor = Anchor.CENTER, bold: bool = False, italic: bool = False, text_width: float = None, placement: Placement = None, minimum_size: float = None, minimum_width: float = None, minimum_height: float = None, frame=None, xform_matrix=None, **kwargs, ): self.__dict__["style"] = TagStyle() self.__dict__["_style_map"] = tag_style_map self._set_aliases() tag_attribs = list(tag_style_map.keys()) tag_attribs.append("subtype") _set_Nones( self, ["font_family", "font_size", "font_color"], [font_family, font_size, font_color], ) validate_args(kwargs, tag_attribs) x, y = pos[:2] self._init_pos = array([x, y, 1.0]) self.text = detokenize(text) if frame is None: self.frame = TagFrame() self.type = Types.TAG self.subtype = Types.TAG self.style = TagStyle() self.style.draw_frame = True if font_family: self.font_family = font_family if font_size: self.font_size = font_size else: self.font_size = defaults["font_size"] if xform_matrix is None: self.xform_matrix = identity_matrix() else: self.xform_matrix = get_transform(xform_matrix) self.anchor = anchor self.bold = bold self.italic = italic self.text_width = text_width self.placement = placement self.minimum_size = minimum_size self.minimum_width = minimum_width self.minimum_height = minimum_height for k, v in kwargs.items(): setattr(self, k, v) x1, y1, x2, y2 = self.text_bounds() w = x2 - x1 h = y2 - y1 self.points = Points([(0, 0, 1), (w, 0, 1), (w, h, 1), (0, h, 1)]) common_properties(self) def __setattr__(self, name, value): obj, attrib = self.__dict__["_aliasses"].get(name, (None, None)) if obj: setattr(obj, attrib, value) else: self.__dict__[name] = value def __getattr__(self, name): obj, attrib = self.__dict__["_aliasses"].get(name, (None, None)) if obj: res = getattr(obj, attrib) else: try: res = super().__getattr__(name) except AttributeError: res = self.__dict__[name] return res def _set_aliases(self): _aliasses = {} for alias, path_attrib in self._style_map.items(): style_path, attrib = path_attrib obj = self for attrib_name in style_path.split("."): obj = obj.__dict__[attrib_name] if obj is not self: _aliasses[alias] = (obj, attrib) self.__dict__["_aliasses"] = _aliasses def _update(self, xform_matrix, reps: int = 0): if reps == 0: self.xform_matrix = self.xform_matrix @ xform_matrix res = self else: res = [] for _ in range(reps): tag = self.copy() tag._update(xform_matrix) res.append(tag) res = Batch(res) return res @property def pos(self) -> Point: """Returns the position of the text. Returns: Point: The position of the text. """ return (self._init_pos @ self.xform_matrix)[:2].tolist()
[docs] def copy(self) -> "Tag": """Returns a copy of the Tag object. Returns: Tag: A copy of the Tag object. """ return Tag(0, 0, self.text, self.font_style, self.xform_matrix)
[docs] def text_bounds(self) -> tuple[float, float, float, float]: """Returns the bounds of the text. Returns: tuple: The bounds of the text (xmin, ymin, xmax, ymax). """ if self.font_size is None: font_size = defaults["font_size"] elif type(self.font_size) in [int, float]: font_size = self.font_size elif self.font_size in FontSize: font_size = convert_latex_font_size(self.font_size) else: raise ValueError("Invalid font size.") try: font = ImageFont.truetype(f"{self.font_family}.ttf", font_size) except OSError: font = ImageFont.load_default() xmin, ymin, xmax, ymax = font.getbbox(self.text) return xmin, ymin, xmax, ymax
@property def final_coords(self): """Returns the final coordinates of the text. Returns: array: The final coordinates of the text. """ return self.points.homogen_coords @ self.xform_matrix @property def b_box(self): """Returns the bounding box of the text. Returns: tuple: The bounding box of the text. """ return bounding_box(self.final_coords) def __str__(self) -> str: return f"Tag({self.text})" def __repr__(self) -> str: return f"Tag({self.text})"
[docs] class ArrowHead(Shape): """An ArrowHead object is a shape that represents the head of an arrow. Args: 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. """ def __init__( self, length: float = None, width_: float = None, points: list = None, **kwargs ): length, width_ = get_defaults( ["arrow_head_length", "arrow_head_width"], [length, width_] ) if points is None: w2 = width_ / 2 points = [(0, 0), (0, -w2), (length, 0), (0, w2)] super().__init__(points, closed=True, subtype=Types.ARROW_HEAD, **kwargs) self.head_length = length self.head_width = width_ self.kwargs = kwargs
[docs] def draw_cs_tiny(canvas, pos=(0, 0), x_len=25, y_len=25, neg_x_len=5, neg_y_len=5): """Draws a tiny coordinate system. Args: canvas: The canvas to draw on. pos (tuple, optional): The position of the coordinate system. Defaults to (0, 0). x_len (int, optional): The length of the x-axis. Defaults to 25. y_len (int, optional): The length of the y-axis. Defaults to 25. neg_x_len (int, optional): The negative length of the x-axis. Defaults to 5. neg_y_len (int, optional): The negative length of the y-axis. Defaults to 5. """ x, y = pos[:2] canvas.circle((x, y), 2, fill=False, line_color=colors.gray) canvas.draw(Shape([(x - neg_x_len, y), (x + x_len, y)]), line_color=colors.gray) canvas.draw(Shape([(x, y - neg_y_len), (x, y + y_len)]), line_color=colors.gray)
[docs] def draw_cs_small(canvas, pos=(0, 0), x_len=80, y_len=100, neg_x_len=5, neg_y_len=5): """Draws a small coordinate system. Args: canvas: The canvas to draw on. pos (tuple, optional): The position of the coordinate system. Defaults to (0, 0). x_len (int, optional): The length of the x-axis. Defaults to 80. y_len (int, optional): The length of the y-axis. Defaults to 100. neg_x_len (int, optional): The negative length of the x-axis. Defaults to 5. neg_y_len (int, optional): The negative length of the y-axis. Defaults to 5. """ x, y = pos[:2] x_axis = arrow( (-neg_x_len + x, y), (x_len + 10 + x, y), head_length=8, head_width=2 ) y_axis = arrow( (x, -neg_y_len + y), (x, y_len + 10 + y), head_length=8, head_width=2 ) canvas.draw(x_axis, line_width=1) canvas.draw(y_axis, line_width=1)
[docs] def arrow( p1, p2, head_length=10, head_width=4, line_width=1, line_color=colors.black, fill_color=colors.black, centered=False, ): """Return an arrow from p1 to p2. Args: 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. """ x1, y1 = p1[:2] x2, y2 = p2[:2] dx = x2 - x1 dy = y2 - y1 angle = atan2(dy, dx) body = Shape( [(x1, y1), (x2, y2)], closed=False, line_color=line_color, fill_color=fill_color, line_width=line_width, ) w2 = head_width / 2 head = Shape( [(-head_length, w2), (0, 0), (-head_length, -w2)], closed=True, line_color=line_color, fill_color=fill_color, line_width=line_width, ) head.rotate(angle) if centered: head.translate(*mid_point((x1, y1), (x2, y2))) else: head.translate(x2, y2) return Batch([body, head])
[docs] class ArcArrow(Batch): """An ArcArrow object is an arrow with an arc. Args: 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. """ def __init__( self, center: Point, radius: float, start_angle: float, end_angle: float, xform_matrix: array = None, **kwargs, ): self.center = center self.radius = radius self.start_angle = start_angle self.end_angle = end_angle # create the arc self.arc = Arc(center, radius, start_angle, end_angle) self.arc.fill = False # create arrow_head1 self.arrow_head1 = ArrowHead() # create arrow_head2 self.arrow_head2 = ArrowHead() start = self.arc.start_point end = self.arc.end_point self.points = [center, start, end] self.arrow_head1.translate(-1 * self.arrow_head1.head_length, 0) self.arrow_head1.rotate(start_angle - pi / 2) self.arrow_head1.translate(*start) self.arrow_head2.translate(-1 * self.arrow_head2.head_length, 0) self.arrow_head2.rotate(end_angle + pi / 2) self.arrow_head2.translate(*end) items = [self.arc, self.arrow_head1, self.arrow_head2] super().__init__(items, subtype=Types.ARC_ARROW, **kwargs) for k, v in kwargs.items(): if k in shape_style_map: setattr(self, k, v) # we should check for valid values here else: raise AttributeError(f"{k}. Invalid attribute!") self.xform_matrix = get_transform(xform_matrix)
[docs] class Arrow(Batch): """An Arrow object is a line with an arrow head. Args: 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. """ def __init__( self, p1: Point, p2: Point, head_pos: HeadPos = HeadPos.END, head: Shape = None, **kwargs, ): self.p1 = p1 self.p2 = p2 self.head_pos = head_pos self.head = head self.kwargs = kwargs length = distance(p1, p2) angle = line_angle(p1, p2) self.line = Shape([(0, 0), (length, 0)]) if head is None: self.head = ArrowHead() else: self.head = head if self.head_pos == HeadPos.END: x = length self.head.translate(x - self.head.head_length, 0) self.head.rotate(angle) self.line.rotate(angle) self.line.translate(*p1) self.head.translate(*p1) self.heads = [self.head] elif self.head_pos == HeadPos.START: self.head = [None] elif self.head_pos == HeadPos.BOTH: self.head2 = ArrowHead() self.head2.rotate(pi) self.head2.translate(self.head2.head_length, 0) self.head2.rotate(angle) self.head2.translate(*p1) x = length self.head.translate(x - self.head.head_length, 0) self.head.rotate(angle) self.line.rotate(angle) self.line.translate(*p1) self.head.translate(*p1) self.heads = [self.head, self.head2] elif self.head_pos == HeadPos.NONE: self.heads = [None] items = [self.line] + self.heads super().__init__(items, subtype=Types.ARROW, **kwargs)
[docs] class AngularDimension(Batch): """An AngularDimension object is a dimension that represents an angle. Args: 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. """ def __init__( self, center: Point, radius: float, start_angle: float, end_angle: float, ext_angle: float, gap_angle: float, text_offset: float = None, gap: float = None, **kwargs, ): text_offset, gap = get_defaults(["text_offset", "gap"], [text_offset, gap]) self.center = center self.radius = radius self.start_angle = start_angle self.end_angle = end_angle self.ext_angle = ext_angle self.gap_angle = gap_angle self.text_offset = text_offset self.gap = gap super().__init__(subtype=Types.ANGULAR_DIMENSION, **kwargs)
[docs] class Dimension(Batch): """A Dimension object is a line with arrows and a text. Args: 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. """ # To do: This is too long and convoluted. Refactor it. def __init__( self, text: str, p1: Point, p2: Point, ext_length: float, ext_length2: float = None, orientation: Anchor = None, text_pos: Anchor = Anchor.CENTER, text_offset: float = 0, gap: float = None, reverse_arrows: bool = False, reverse_arrow_length: float = None, parallel: bool = False, ext1pnt: Point = None, ext2pnt: Point = None, scale: float = 1, font_size: int = 12, **kwargs, ): ext_length2, gap, reverse_arrow_length = get_defaults( ["ext_length2", "gap", "rev_arrow_length"], [ext_length2, gap, reverse_arrow_length], ) if text == "": self.text = str(distance(p1, p2) / scale) else: self.text = text self.p1 = p1 self.p2 = p2 self.ext_length = ext_length self.ext_length2 = ext_length2 self.orientation = orientation self.text_pos = text_pos self.text_offset = text_offset self.gap = gap self.reverse_arrows = reverse_arrows self.reverse_arrow_length = reverse_arrow_length self.kwargs = kwargs self.ext1 = None self.ext2 = None self.ext3 = None self.arrow1 = None self.arrow2 = None self.dim_line = None self.mid_line = None self.ext1pnt = ext1pnt self.ext2pnt = ext2pnt x1, y1 = p1[:2] x2, y2 = p2[:2] # px1_1 : extension1 point 1 # px1_2 : extension1 point 2 # px2_1 : extension2 point 1 # px2_2 : extension2 point 2 # px3_1 : extension3 point 1 # px3_2 : extension3 point 2 # pa1 : arrow point 1 # pa2 : arrow point 2 # ptext : text point super().__init__(subtype=Types.DIMENSION, **kwargs) dist_tol = defaults["dist_tol"] if font_size is not None: self.font_size = font_size if parallel: if orientation is None: orientation = Anchor.NORTHEAST if orientation == Anchor.NORTHEAST: angle = line_angle(p1, p2) + pi / 2 elif orientation == Anchor.NORTHWEST: angle = line_angle(p1, p2) + pi / 2 elif orientation == Anchor.SOUTHEAST: angle = line_angle(p1, p2) - pi / 2 elif orientation == Anchor.SOUTHWEST: angle = line_angle(p1, p2) + pi / 2 if self.ext1pnt is None: px1_1 = line_by_point_angle_length(p1, angle, self.gap)[1] else: px1_1 = self.ext1pnt px1_2 = line_by_point_angle_length(p1, angle, self.gap + self.ext_length)[1] if self.ext2pnt is None: px2_1 = line_by_point_angle_length(p2, angle, self.gap)[1] else: px2_1 = self.ext2pnt px2_2 = line_by_point_angle_length(p2, angle, self.gap + self.ext_length)[1] pa1 = line_by_point_angle_length(px1_2, angle, self.gap * -1.5)[1] pa2 = line_by_point_angle_length(px2_2, angle, self.gap * -1.5)[1] self.text_pos = mid_point(pa1, pa2) self.dim_line = Arrow(pa1, pa2, head_pos=HeadPos.BOTH) self.ext1 = Shape([px1_1, px1_2]) self.ext2 = Shape([px2_1, px2_2]) self.append(self.dim_line) self.append(self.ext1) self.append(self.ext2) else: if abs(x1 - x2) < dist_tol: # vertical line if self.orientation is None: orientation = Anchor.EAST if orientation in [Anchor.WEST, Anchor.SOUTHWEST, Anchor.NORTHWEST]: x = x1 - self.gap px1_1 = (x, y1) px1_2 = (x - ext_length, y1) px2_1 = (x, y2) px2_2 = (x - ext_length, y2) x = px1_2[0] + self.gap * 1.5 pa1 = (x, y1) pa2 = (x, y2) elif orientation in [Anchor.EAST, Anchor.SOUTHEAST, Anchor.NORTHEAST]: x = x1 + self.gap px1_1 = (x, y1) px1_2 = (x + ext_length, y1) px2_1 = (x, y2) px2_2 = (x + ext_length, y2) x = px1_2[0] - self.gap * 1.5 pa1 = (x, y1) pa2 = (x, y2) elif orientation == Anchor.CENTER: pa1 = (x1, y1) pa2 = (x1, y2) x = pa1[0] if orientation in [Anchor.SOUTHWEST, Anchor.SOUTHEAST]: px3_1 = pa2 y = y2 - self.ext_length2 px3_2 = (x, y) self.ext3 = Shape([px3_1, px3_2]) self.text_pos = (x, y - self.text_offset) elif orientation in [Anchor.NORTHWEST, Anchor.NORTHEAST]: px3_1 = pa1 y = y1 + self.ext_length2 px3_2 = (x, y) self.ext3 = Shape([px3_1, px3_2]) self.text_pos = (x, y + self.text_offset) elif orientation == Anchor.SOUTH: px3_1 = pa2 y = y2 - self.ext_length2 px3_2 = (x, y) self.ext3 = Shape([px3_1, px3_2]) self.text_pos = (x, y - self.text_offset) elif orientation == Anchor.NORTH: px3_2 = pa1 y = y2 + self.ext_length2 px3_1 = (x, y) self.ext3 = Shape([px3_1, px3_2]) self.text_pos = (x, y + self.text_offset) else: self.text_pos = (x, y1 - (y1 - y2) / 2) if orientation not in [Anchor.CENTER, Anchor.NORTH, Anchor.SOUTH]: if self.ext1pnt is None: self.ext1 = Shape([px1_1, px1_2]) else: self.ext1 = Shape([ext1pnt, px1_2]) if self.ext2pnt is None: self.ext2 = Shape([px2_1, px2_2]) else: self.ext2 = Shape([ext2pnt, px2_2]) elif abs(y1 - y2) < dist_tol: # horizontal line if self.orientation is None: orientation = Anchor.SOUTH if orientation in [Anchor.SOUTH, Anchor.SOUTHWEST, Anchor.SOUTHEAST]: y = y1 - self.gap px1_1 = (x1, y) px1_2 = (x1, y - ext_length) px2_1 = (x2, y) px2_2 = (x2, y - ext_length) y = px1_2[1] + self.gap * 1.5 pa1 = (x1, y) pa2 = (x2, y) elif orientation in [Anchor.NORTH, Anchor.NORTHWEST, Anchor.NORTHEAST]: y = y1 + self.gap px1_1 = (x1, y) px1_2 = (x1, y + ext_length) px2_1 = (x2, y) px2_2 = (x2, y + ext_length) y = px1_2[1] - self.gap * 1.5 pa1 = (x1, y) pa2 = (x2, y) elif orientation in [Anchor.WEST, Anchor.EAST]: pa1 = (x1, y1) pa2 = (x2, y2) if orientation == Anchor.WEST: px3_1 = (pa1[0] - self.ext_length2, pa1[1]) px3_2 = pa1 self.text_pos = (px3_1[0] - self.text_offset, pa1[1]) else: px3_1 = pa2 px3_2 = (pa2[0] + self.ext_length2, pa1[1]) self.text_pos = (px3_1[0] + self.text_offset, pa1[1]) self.ext3 = Shape([px3_1, px3_2]) elif orientation == Anchor.CENTER: pa1 = (x1, y1) pa2 = (x2, y2) y = pa1[1] if orientation in [Anchor.SOUTHWEST, Anchor.NORTHWEST]: px3_1 = pa1 x = x1 - self.ext_length2 px3_2 = (x, y) self.ext3 = Shape([px3_1, px3_2]) self.text_pos = (x - self.text_offset, y) elif orientation in [Anchor.NORTHEAST, Anchor.SOUTHEAST]: px3_1 = pa2 x = x2 + self.ext_length2 px3_2 = (x, y) self.ext3 = Shape([px3_1, px3_2]) self.text_pos = (x + self.text_offset, y) elif orientation in [Anchor.CENTER, Anchor.NORTH, Anchor.SOUTH]: self.text_pos = (x1 + (x2 - x1) / 2, y) if orientation not in [Anchor.CENTER, Anchor.WEST, Anchor.EAST]: if self.ext1pnt is None: self.ext1 = Shape([px1_1, px1_2]) else: self.ext1Shape([ext1pnt, px1_2]) if self.ext2pnt is None: self.ext2 = Shape([px2_1, px2_2]) else: self.ext2 = Shape([ext2pnt, px2_2]) if self.reverse_arrows: dist = self.reverse_arrow_length p2 = extended_line(dist, [pa1, pa2])[1] self.arrow1 = Arrow(p2, pa2) p2 = extended_line(dist, [pa2, pa1])[1] self.arrow2 = Arrow(p2, pa1) self.append(self.arrow1) self.append(self.arrow2) self.mid_line = Shape([pa1, pa2]) self.append(self.mid_line) dist = self.text_offset + self.reverse_arrow_length if orientation in [Anchor.EAST, Anchor.NORTHEAST, Anchor.NORTH]: self.text_pos = extended_line(dist, [pa1, pa2])[1] else: self.text_pos = extended_line(dist, [pa2, pa1])[1] else: self.dim_line = Arrow(pa1, pa2, head_pos=HeadPos.BOTH) self.append(self.dim_line) if self.ext1 is not None: self.append(self.ext1) if self.ext2 is not None: self.append(self.ext2) if self.ext3 is not None: self.append(self.ext3)